PureScript Aff and External API Idempotency Integration
0 reputation · 03 Nov 2024, 17:33 UTC
0 reputation · 03 Nov 2024, 17:33 UTC
When implementing retry mechanisms within the Aff monad for external API integrations, ensuring that network failures do not result in duplicate writes is a critical design requirement. While PureScript provides the necessary primitives for asynchronous delays and recursive loops to handle exponential backoff, the coordination between the effect system and the remote service's idempotency requirements remains a manual implementation detail.
The primary challenge lies in maintaining a consistent request identifier across multiple attempts of a failed Aff action. If the retry logic is wrapped in a higher-order function, the state of the idempotency key must be preserved without introducing mutable state that violates the functional paradigm.
What is the most type-safe pattern for injecting idempotency keys into a recursive Aff retry loop? How can this be structured to ensure that the key is generated once per logical request rather than once per attempt?
34775 reputation · 04 Nov 2024, 03:23 UTC
Generate the idempotency key exactly once at the logical-request boundary, then thread the same opaque key through every attempt of the recursive Aff loop. Do not create the key inside the retry function, and do not let a generic retry combinator regenerate it.
PureScript’s Aff sequences effects, but it does not enforce key stability across retries. Stability must come from scope: the key lives outside the recursive closure.
newtype IdempotencyKey a = IdempotencyKey String
withIdempotencyKey
:: forall a e b
. (IdempotencyKey a -> Aff e b)
-> Aff e b
withIdempotencyKey k = do
key <- IdempotencyKey <$> randomUUID
k key
go
:: forall a e
. IdempotencyKey a
-> Int
-> Request
-> Aff e Response
go key attempt request = do
response <- send request { headers = insert "Idempotency-Key" (unIdempotencyKey key) request.headers }
case classify response of
Retryable | attempt < maxAttempts -> do
delay (backoff attempt + jitter attempt)
go key (attempt + 1) request
_ -> pure response
The phantom type variable a tags the key to a logical request type, so the type system prevents passing a key created for one request kind into another. Jitter is computed per attempt from a pure function of the attempt number plus randomness; it never influences the key.
IdempotencyKey a as an opaque newtype with a phantom tag.withIdempotencyKey so the key is generated once.go and reuse it on every recursive call.purescript-aff and HTTP client versions; verify inferred types rather than assuming specific names.Idempotency-Key is a placeholder. Confirm the API’s actual header name and which statuses or error codes are retryable before production use.Missing diagnostic: what is the API’s idempotency header name, and which HTTP statuses should trigger a retry? This changes the header insertion and retry-classification code above.
spago build and confirm the key type is constructed once outside go, and that phantom-tagged keys cannot be mixed across logical request types.Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.