@restatedev/restate-sdk-clients 1.15.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"ingress.js","names":["status: number","responseText: string","message: string","parameter: unknown","opts: Opts<unknown, unknown> | SendOpts<unknown> | undefined","url: string","signal: AbortSignal | undefined","body","opts: ConnectionOpts","res: Send","opts","serde"],"sources":["../src/ingress.ts"],"sourcesContent":["/*\n * Copyright (c) 2023-2024 - Restate Software, Inc., Restate GmbH\n *\n * This file is part of the Restate SDK for Node.js/TypeScript,\n * which is released under the MIT license.\n *\n * You can find a copy of the license in file LICENSE in the root\n * directory of this repository or package, or at\n * https://github.com/restatedev/sdk-typescript/blob/main/LICENSE\n */\n\nimport {\n type Service,\n type ServiceDefinitionFrom,\n type VirtualObject,\n type WorkflowDefinitionFrom,\n type Workflow,\n type VirtualObjectDefinitionFrom,\n type Serde,\n serde,\n type JournalValueCodec,\n} from \"@restatedev/restate-sdk-core\";\nimport {\n ConnectionOpts,\n Ingress,\n IngressClient,\n IngressSendClient,\n IngressWorkflowClient,\n Output,\n Send,\n ScopedIngress,\n WorkflowSubmission,\n} from \"./api.js\";\n\nimport { Opts, SendOpts } from \"./api.js\";\n\n/**\n * Connect to the restate Ingress\n *\n * @param opts connection options\n * @returns a connection the the restate ingress\n */\nexport function connect(opts: ConnectionOpts): Ingress {\n return new HttpIngress(opts);\n}\n\nexport class HttpCallError extends Error {\n constructor(\n public readonly status: number,\n public readonly responseText: string,\n public override readonly message: string\n ) {\n super(message);\n }\n}\n\ntype InvocationParameters<I> = {\n component: string;\n handler: string;\n key?: string;\n send?: boolean;\n opts?: Opts<I, unknown> | SendOpts<I>;\n parameter?: I;\n method?: string;\n scope?: string;\n};\n\nfunction optsFromArgs(args: unknown[]): {\n parameter?: unknown;\n opts?: Opts<unknown, unknown> | SendOpts<unknown>;\n} {\n let parameter: unknown;\n let opts: Opts<unknown, unknown> | SendOpts<unknown> | undefined;\n switch (args.length) {\n case 0: {\n break;\n }\n case 1: {\n if (args[0] instanceof Opts) {\n opts = args[0];\n } else if (args[0] instanceof SendOpts) {\n opts = args[0];\n } else {\n parameter = args[0];\n }\n break;\n }\n case 2: {\n parameter = args[0];\n if (args[1] instanceof Opts) {\n opts = args[1];\n } else if (args[1] instanceof SendOpts) {\n opts = args[1];\n } else {\n throw new TypeError(\n \"The second argument must be either Opts or SendOpts\"\n );\n }\n break;\n }\n default: {\n throw new TypeError(\"unexpected number of arguments\");\n }\n }\n return {\n parameter,\n opts,\n };\n}\n\nconst IDEMPOTENCY_KEY_HEADER = \"idempotency-key\";\nconst LIMIT_KEY_HEADER = \"x-restate-limit-key\";\n\nconst doComponentInvocation = async <I, O>(\n opts: ConnectionOpts,\n params: InvocationParameters<I>\n): Promise<O> => {\n let attachable = false;\n //\n // ingress URL\n //\n let url: string;\n if (params.scope) {\n // Scoped path: /restate/scope/{scope}/{call|send}/{service}/{key?}/{handler}\n const pathType = params.send ? \"send\" : \"call\";\n const parts = [\n opts.url,\n \"restate/scope\",\n encodeURIComponent(params.scope),\n pathType,\n params.component,\n ];\n if (params.key) {\n parts.push(encodeURIComponent(params.key));\n }\n parts.push(params.handler);\n url = parts.join(\"/\");\n if (params.send && params.opts instanceof SendOpts) {\n const delay = params.opts.delay();\n if (delay) url += `?delay=${delay}ms`;\n }\n } else {\n const fragments = [opts.url, params.component];\n if (params.key) {\n fragments.push(encodeURIComponent(params.key));\n }\n fragments.push(params.handler);\n if (params.send ?? false) {\n if (params.opts instanceof SendOpts) {\n fragments.push(computeDelayAsIso(params.opts));\n } else {\n fragments.push(\"send\");\n }\n }\n url = fragments.join(\"/\");\n }\n //\n // request body\n //\n const inputSerde = params.opts?.opts.input ?? opts.serde ?? serde.json;\n\n const { body, contentType } = serializeBodyWithContentType(\n params.parameter,\n inputSerde,\n opts.journalValueCodec\n );\n //\n // headers\n //\n const headers = {\n ...(opts.headers ?? {}),\n ...(params.opts?.opts?.headers ?? {}),\n };\n if (contentType) {\n headers[\"Content-Type\"] = contentType;\n }\n //\n // idempotency\n //\n const idempotencyKey = params.opts?.opts.idempotencyKey;\n if (idempotencyKey) {\n headers[IDEMPOTENCY_KEY_HEADER] = idempotencyKey;\n attachable = true;\n }\n //\n // limit key\n //\n const limitKey = params.opts?.opts.limitKey;\n if (limitKey) {\n headers[LIMIT_KEY_HEADER] = limitKey;\n }\n\n // Abort signal, if any\n let signal: AbortSignal | undefined;\n if (\n params.opts?.opts.signal !== undefined &&\n params.opts?.opts.timeout !== undefined\n ) {\n throw new Error(\n \"You can't specify both signal and timeout options at the same time\"\n );\n } else if (params.opts?.opts.signal !== undefined) {\n signal = params.opts?.opts.signal;\n } else if (params.opts?.opts.timeout !== undefined) {\n signal = AbortSignal.timeout(params.opts?.opts.timeout);\n }\n\n //\n // make the call\n //\n const httpResponse = await fetch(url, {\n method: params.method ?? \"POST\",\n headers,\n body,\n signal,\n });\n if (!httpResponse.ok) {\n const body = await httpResponse.text();\n throw new HttpCallError(\n httpResponse.status,\n body,\n `Request failed: ${httpResponse.status}\\n${body}`\n );\n }\n const responseBuf = new Uint8Array(await httpResponse.arrayBuffer());\n if (!params.send) {\n const decodedBuf = opts.journalValueCodec\n ? await opts.journalValueCodec.decode(responseBuf)\n : responseBuf;\n const outputSerde = params.opts?.opts.output ?? opts.serde ?? serde.json;\n return outputSerde.deserialize(decodedBuf) as O;\n }\n const json = serde.json.deserialize(responseBuf) as O;\n return { ...json, attachable };\n};\n\nconst doWorkflowHandleCall = async <O>(\n opts: ConnectionOpts,\n wfName: string,\n wfKey: string,\n op: \"output\" | \"attach\",\n callOpts?: Opts<unknown, O> | SendOpts<unknown>\n): Promise<O> => {\n const outputSerde = callOpts?.opts.output ?? opts.serde ?? serde.json;\n //\n // headers\n //\n const headers = {\n ...(opts.headers ?? {}),\n };\n //\n // make the call\n //\n const url = `${opts.url}/restate/workflow/${wfName}/${encodeURIComponent(\n wfKey\n )}/${op}`;\n\n // Abort signal, if any\n let signal: AbortSignal | undefined;\n if (\n callOpts?.opts?.signal !== undefined &&\n callOpts?.opts?.timeout !== undefined\n ) {\n throw new Error(\n \"You can't specify both signal and timeout options at the same time\"\n );\n } else if (callOpts?.opts?.signal !== undefined) {\n signal = callOpts?.opts?.signal;\n } else if (callOpts?.opts?.timeout !== undefined) {\n signal = AbortSignal.timeout(callOpts?.opts?.timeout);\n }\n\n const httpResponse = await fetch(url, {\n method: \"GET\",\n headers,\n signal,\n });\n if (httpResponse.ok) {\n const responseBuf = new Uint8Array(await httpResponse.arrayBuffer());\n const decodedBuf = opts.journalValueCodec\n ? await opts.journalValueCodec.decode(responseBuf)\n : responseBuf;\n return outputSerde.deserialize(decodedBuf) as O;\n }\n const body = await httpResponse.text();\n throw new HttpCallError(\n httpResponse.status,\n body,\n `Request failed: ${httpResponse.status}\\n${body}`\n );\n};\n\nclass HttpIngress implements Ingress {\n constructor(private readonly opts: ConnectionOpts) {}\n\n private proxy(component: string, key?: string, send?: boolean) {\n return new Proxy(\n {},\n {\n get: (_target, prop) => {\n const handler = prop as string;\n return (...args: unknown[]) => {\n const { parameter, opts } = optsFromArgs(args);\n return doComponentInvocation<unknown, unknown>(this.opts, {\n component,\n handler,\n key,\n parameter,\n opts,\n send,\n });\n };\n },\n }\n );\n }\n\n serviceClient<D>(opts: ServiceDefinitionFrom<D>): IngressClient<Service<D>> {\n return this.proxy(opts.name) as IngressClient<Service<D>>;\n }\n\n objectClient<D>(\n opts: VirtualObjectDefinitionFrom<D>,\n key: string\n ): IngressClient<VirtualObject<D>> {\n return this.proxy(opts.name, key) as IngressClient<VirtualObject<D>>;\n }\n\n workflowClient<D>(\n opts: WorkflowDefinitionFrom<D>,\n key: string\n ): IngressWorkflowClient<Workflow<D>> {\n const component = opts.name;\n const conn = this.opts;\n\n const workflowSubmit = async (\n ...args: unknown[]\n ): Promise<WorkflowSubmission<unknown>> => {\n const { parameter, opts } = optsFromArgs(args);\n const res: Send = await doComponentInvocation(conn, {\n component,\n handler: \"run\",\n key,\n send: true,\n parameter,\n opts,\n });\n\n return {\n invocationId: res.invocationId,\n status: res.status,\n attachable: true,\n };\n };\n\n const workflowAttach = (opts?: Opts<void, unknown>) =>\n doWorkflowHandleCall(conn, component, key, \"attach\", opts);\n\n const workflowOutput = async (\n opts?: Opts<void, unknown>\n ): Promise<Output<unknown>> => {\n try {\n const result = await doWorkflowHandleCall(\n conn,\n component,\n key,\n \"output\",\n opts\n );\n\n return {\n ready: true,\n result,\n };\n } catch (e) {\n if (!(e instanceof HttpCallError) || e.status !== 470) {\n throw e;\n }\n return {\n ready: false,\n get result() {\n throw new Error(\"Calling result() on a non ready workflow\");\n },\n };\n }\n };\n\n return new Proxy(\n {},\n {\n get: (_target, prop) => {\n const handler = prop as string;\n if (handler === \"workflowSubmit\") {\n return workflowSubmit;\n } else if (handler === \"workflowAttach\") {\n return workflowAttach;\n } else if (handler === \"workflowOutput\") {\n return workflowOutput;\n }\n // shared handlers pass trough via the ingress's normal invocation form\n // i.e. POST /<svc>/<key>/<handler>\n return (...args: unknown[]) => {\n const { parameter, opts } = optsFromArgs(args);\n return doComponentInvocation(conn, {\n component,\n handler,\n key,\n parameter,\n opts,\n });\n };\n },\n }\n ) as IngressWorkflowClient<Workflow<D>>;\n }\n\n objectSendClient<D>(\n opts: VirtualObjectDefinitionFrom<D>,\n key: string\n ): IngressSendClient<VirtualObject<D>> {\n return this.proxy(opts.name, key, true) as IngressSendClient<\n VirtualObject<D>\n >;\n }\n\n serviceSendClient<D>(\n opts: ServiceDefinitionFrom<D>\n ): IngressSendClient<Service<D>> {\n return this.proxy(opts.name, undefined, true) as IngressSendClient<\n Service<D>\n >;\n }\n\n scope(scopeKey: string): ScopedIngress {\n const conn = this.opts;\n const scopedProxy = (component: string, key?: string, send?: boolean) =>\n new Proxy(\n {},\n {\n get: (_target, prop) => {\n const handler = prop as string;\n return (...args: unknown[]) => {\n const { parameter, opts } = optsFromArgs(args);\n return doComponentInvocation<unknown, unknown>(conn, {\n component,\n handler,\n key,\n parameter,\n opts,\n send,\n scope: scopeKey,\n });\n };\n },\n }\n );\n\n return {\n serviceClient: <D>(opts: ServiceDefinitionFrom<D>) =>\n scopedProxy(opts.name) as IngressClient<Service<D>>,\n serviceSendClient: <D>(opts: ServiceDefinitionFrom<D>) =>\n scopedProxy(opts.name, undefined, true) as IngressSendClient<\n Service<D>\n >,\n workflowClient: <D>(\n opts: WorkflowDefinitionFrom<D>,\n key: string\n ): IngressWorkflowClient<Workflow<D>> => {\n const component = opts.name;\n\n const workflowSubmit = async (\n ...args: unknown[]\n ): Promise<WorkflowSubmission<unknown>> => {\n const { parameter, opts } = optsFromArgs(args);\n const res: Send = await doComponentInvocation(conn, {\n component,\n handler: \"run\",\n key,\n send: true,\n parameter,\n opts,\n scope: scopeKey,\n });\n return {\n invocationId: res.invocationId,\n status: res.status,\n attachable: true,\n };\n };\n\n const workflowAttach = (opts?: Opts<void, unknown>) =>\n doWorkflowHandleCall(conn, component, key, \"attach\", opts);\n\n const workflowOutput = async (\n opts?: Opts<void, unknown>\n ): Promise<Output<unknown>> => {\n try {\n const result = await doWorkflowHandleCall(\n conn,\n component,\n key,\n \"output\",\n opts\n );\n return { ready: true, result };\n } catch (e) {\n if (!(e instanceof HttpCallError) || e.status !== 470) {\n throw e;\n }\n return {\n ready: false,\n get result() {\n throw new Error(\"Calling result() on a non ready workflow\");\n },\n };\n }\n };\n\n return new Proxy(\n {},\n {\n get: (_target, prop) => {\n const handler = prop as string;\n if (handler === \"workflowSubmit\") {\n return workflowSubmit;\n } else if (handler === \"workflowAttach\") {\n return workflowAttach;\n } else if (handler === \"workflowOutput\") {\n return workflowOutput;\n }\n return (...args: unknown[]) => {\n const { parameter, opts } = optsFromArgs(args);\n return doComponentInvocation(conn, {\n component,\n handler,\n key,\n parameter,\n opts,\n scope: scopeKey,\n });\n };\n },\n }\n ) as IngressWorkflowClient<Workflow<D>>;\n },\n };\n }\n\n async call<I, O>(opts: {\n service: string;\n handler: string;\n parameter: I;\n key?: string;\n scope?: string;\n opts?: Opts<I, O>;\n }): Promise<O> {\n return doComponentInvocation<I, O>(this.opts, {\n component: opts.service,\n handler: opts.handler,\n key: opts.key,\n scope: opts.scope,\n parameter: opts.parameter,\n send: false,\n opts: opts.opts,\n });\n }\n\n async send<I>(opts: {\n service: string;\n handler: string;\n parameter: I;\n key?: string;\n scope?: string;\n opts?: SendOpts<I>;\n }): Promise<Send> {\n return doComponentInvocation<I, Send>(this.opts, {\n component: opts.service,\n handler: opts.handler,\n key: opts.key,\n scope: opts.scope,\n parameter: opts.parameter,\n send: true,\n opts: opts.opts,\n });\n }\n\n async resolveAwakeable<T>(\n id: string,\n payload?: T,\n payloadSerde?: Serde<T>\n ): Promise<void> {\n const url = `${this.opts.url}/restate/a/${id}/resolve`;\n const { body, contentType } = serializeBodyWithContentType(\n payload,\n payloadSerde ?? this.opts.serde ?? serde.json,\n this.opts.journalValueCodec\n );\n const headers = {\n ...(this.opts.headers ?? {}),\n };\n if (contentType) {\n headers[\"Content-Type\"] = contentType;\n }\n const httpResponse = await fetch(url, {\n method: \"POST\",\n headers,\n body,\n });\n if (!httpResponse.ok) {\n const body = await httpResponse.text();\n throw new HttpCallError(\n httpResponse.status,\n body,\n `Request failed: ${httpResponse.status}\\n${body}`\n );\n }\n }\n\n async rejectAwakeable(id: string, reason: string): Promise<void> {\n const url = `${this.opts.url}/restate/a/${id}/reject`;\n const headers = {\n \"Content-Type\": \"text/plain\",\n ...(this.opts.headers ?? {}),\n };\n const httpResponse = await fetch(url, {\n method: \"POST\",\n headers,\n body: reason,\n });\n if (!httpResponse.ok) {\n const body = await httpResponse.text();\n throw new HttpCallError(\n httpResponse.status,\n body,\n `Request failed: ${httpResponse.status}\\n${body}`\n );\n }\n }\n\n async result<T>(\n send: Send<T> | WorkflowSubmission<T>,\n resultSerde?: Serde<T>\n ): Promise<T> {\n if (!send.attachable) {\n throw new Error(\n `Unable to fetch the result for ${send.invocationId}.\n A service's result is stored only with an idempotencyKey is supplied when invocating the service.`\n );\n }\n //\n // headers\n //\n const headers = {\n ...(this.opts.headers ?? {}),\n };\n //\n // make the call\n const url = `${this.opts.url}/restate/invocation/${send.invocationId}/attach`;\n\n const httpResponse = await fetch(url, {\n method: \"GET\",\n headers,\n });\n if (httpResponse.ok) {\n const responseBuf = new Uint8Array(await httpResponse.arrayBuffer());\n const decodedBuf = this.opts.journalValueCodec\n ? await this.opts.journalValueCodec.decode(responseBuf)\n : responseBuf;\n return (resultSerde ?? this.opts.serde ?? serde.json).deserialize(\n decodedBuf\n ) as T;\n }\n const body = await httpResponse.text();\n throw new HttpCallError(\n httpResponse.status,\n body,\n `Request failed: ${httpResponse.status}\\n${body}`\n );\n }\n}\n\nfunction computeDelayAsIso(opts: SendOpts): string {\n const delay = opts.delay();\n if (!delay) {\n return \"send\";\n }\n return `send?delay=${delay}ms`;\n}\n\nfunction serializeBodyWithContentType(\n body: unknown,\n serde: Serde<unknown>,\n journalValueCodec?: JournalValueCodec\n): {\n body?: Uint8Array;\n contentType?: string;\n} {\n let buffer = serde.serialize(body);\n if (journalValueCodec) {\n buffer = journalValueCodec.encode(buffer);\n }\n return {\n body: buffer,\n contentType: serde.contentType,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ingress.js","names":["status: number","responseText: string","message: string","parameter: unknown","opts: Opts<unknown, unknown> | SendOpts<unknown> | undefined","url: string","httpResponse: Response","signal: AbortSignal | undefined","opts: ConnectionOpts","res: Send","opts","body","serde"],"sources":["../src/ingress.ts"],"sourcesContent":["/*\n * Copyright (c) 2023-2024 - Restate Software, Inc., Restate GmbH\n *\n * This file is part of the Restate SDK for Node.js/TypeScript,\n * which is released under the MIT license.\n *\n * You can find a copy of the license in file LICENSE in the root\n * directory of this repository or package, or at\n * https://github.com/restatedev/sdk-typescript/blob/main/LICENSE\n */\n\nimport {\n type Service,\n type ServiceDefinitionFrom,\n type VirtualObject,\n type WorkflowDefinitionFrom,\n type Workflow,\n type VirtualObjectDefinitionFrom,\n type Serde,\n serde,\n type JournalValueCodec,\n} from \"@restatedev/restate-sdk-core\";\nimport {\n ConnectionOpts,\n Ingress,\n IngressClient,\n IngressSendClient,\n IngressWorkflowClient,\n Output,\n Send,\n ScopedIngress,\n WorkflowSubmission,\n} from \"./api.js\";\n\nimport { Opts, SendOpts } from \"./api.js\";\nimport {\n abortableSleep,\n backoffDelay,\n defaultShouldRetry,\n parseRetryAfter,\n resolveRetryPolicy,\n} from \"./retry.js\";\n\n/**\n * Connect to the restate Ingress\n *\n * @param opts connection options\n * @returns a connection the the restate ingress\n */\nexport function connect(opts: ConnectionOpts): Ingress {\n return new HttpIngress(opts);\n}\n\nexport class HttpCallError extends Error {\n constructor(\n public readonly status: number,\n public readonly responseText: string,\n public override readonly message: string\n ) {\n super(message);\n }\n}\n\ntype InvocationParameters<I> = {\n component: string;\n handler: string;\n key?: string;\n send?: boolean;\n opts?: Opts<I, unknown> | SendOpts<I>;\n parameter?: I;\n method?: string;\n scope?: string;\n};\n\nfunction optsFromArgs(args: unknown[]): {\n parameter?: unknown;\n opts?: Opts<unknown, unknown> | SendOpts<unknown>;\n} {\n let parameter: unknown;\n let opts: Opts<unknown, unknown> | SendOpts<unknown> | undefined;\n switch (args.length) {\n case 0: {\n break;\n }\n case 1: {\n if (args[0] instanceof Opts) {\n opts = args[0];\n } else if (args[0] instanceof SendOpts) {\n opts = args[0];\n } else {\n parameter = args[0];\n }\n break;\n }\n case 2: {\n parameter = args[0];\n if (args[1] instanceof Opts) {\n opts = args[1];\n } else if (args[1] instanceof SendOpts) {\n opts = args[1];\n } else {\n throw new TypeError(\n \"The second argument must be either Opts or SendOpts\"\n );\n }\n break;\n }\n default: {\n throw new TypeError(\"unexpected number of arguments\");\n }\n }\n return {\n parameter,\n opts,\n };\n}\n\nconst IDEMPOTENCY_KEY_HEADER = \"idempotency-key\";\nconst LIMIT_KEY_HEADER = \"x-restate-limit-key\";\n\nconst doComponentInvocation = async <I, O>(\n opts: ConnectionOpts,\n params: InvocationParameters<I>\n): Promise<O> => {\n let attachable = false;\n //\n // ingress URL\n //\n let url: string;\n if (params.scope) {\n // Scoped path: /restate/scope/{scope}/{call|send}/{service}/{key?}/{handler}\n const pathType = params.send ? \"send\" : \"call\";\n const parts = [\n opts.url,\n \"restate/scope\",\n encodeURIComponent(params.scope),\n pathType,\n params.component,\n ];\n if (params.key) {\n parts.push(encodeURIComponent(params.key));\n }\n parts.push(params.handler);\n url = parts.join(\"/\");\n if (params.send && params.opts instanceof SendOpts) {\n const delay = params.opts.delay();\n if (delay) url += `?delay=${delay}ms`;\n }\n } else {\n const fragments = [opts.url, params.component];\n if (params.key) {\n fragments.push(encodeURIComponent(params.key));\n }\n fragments.push(params.handler);\n if (params.send ?? false) {\n if (params.opts instanceof SendOpts) {\n fragments.push(computeDelayAsIso(params.opts));\n } else {\n fragments.push(\"send\");\n }\n }\n url = fragments.join(\"/\");\n }\n //\n // request body\n //\n const inputSerde = params.opts?.opts.input ?? opts.serde ?? serde.json;\n\n const { body, contentType } = serializeBodyWithContentType(\n params.parameter,\n inputSerde,\n opts.journalValueCodec\n );\n //\n // headers\n //\n const headers = {\n ...(opts.headers ?? {}),\n ...(params.opts?.opts?.headers ?? {}),\n };\n if (contentType) {\n headers[\"Content-Type\"] = contentType;\n }\n //\n // idempotency\n //\n const idempotencyKey = params.opts?.opts.idempotencyKey;\n if (idempotencyKey) {\n headers[IDEMPOTENCY_KEY_HEADER] = idempotencyKey;\n attachable = true;\n }\n //\n // limit key\n //\n const limitKey = params.opts?.opts.limitKey;\n if (limitKey) {\n headers[LIMIT_KEY_HEADER] = limitKey;\n }\n\n // Abort signal, if any\n const userSignal = params.opts?.opts.signal;\n const timeout = params.opts?.opts.timeout;\n if (userSignal !== undefined && timeout !== undefined) {\n throw new Error(\n \"You can't specify both signal and timeout options at the same time\"\n );\n }\n // A fresh timeout signal is minted per attempt below — a single\n // AbortSignal.timeout() would already be aborted on the second attempt.\n const attemptSignal = (): AbortSignal | undefined =>\n userSignal ??\n (timeout !== undefined ? AbortSignal.timeout(timeout) : undefined);\n\n //\n // retries\n //\n // Retries are opt-in (opts.retry) AND only ever attempted when the call\n // carries an idempotency key: Restate then dedupes the request, so re-issuing\n // it after an ambiguous failure (network error, 429, 5xx) safely attaches to\n // the in-flight/completed invocation instead of starting a duplicate. Without\n // a key, retrying is unsafe.\n //\n const retryPolicy = idempotencyKey\n ? resolveRetryPolicy(opts.retry)\n : undefined;\n const shouldRetry = retryPolicy?.shouldRetry ?? defaultShouldRetry;\n\n //\n // make the call\n //\n let httpResponse: Response;\n for (let attempt = 0; ; attempt++) {\n try {\n httpResponse = await fetch(url, {\n method: params.method ?? \"POST\",\n headers,\n body,\n signal: attemptSignal(),\n });\n } catch (e) {\n // network error (fetch rejected) — ambiguous\n if (\n retryPolicy &&\n attempt < retryPolicy.maxAttempts - 1 &&\n !userSignal?.aborted &&\n shouldRetry({ kind: \"network\", error: e }, attempt)\n ) {\n await abortableSleep(backoffDelay(retryPolicy, attempt), userSignal);\n continue;\n }\n throw e;\n }\n if (httpResponse.ok) {\n break;\n }\n // Read the error body once; it is reused for the retry decision and, if we\n // give up, for the thrown HttpCallError.\n const errorBody = await httpResponse.text();\n if (\n retryPolicy &&\n attempt < retryPolicy.maxAttempts - 1 &&\n !userSignal?.aborted &&\n shouldRetry(\n {\n kind: \"response\",\n status: httpResponse.status,\n headers: httpResponse.headers,\n body: errorBody || undefined,\n },\n attempt\n )\n ) {\n const retryAfter = parseRetryAfter(httpResponse.headers);\n await abortableSleep(\n backoffDelay(retryPolicy, attempt, retryAfter),\n userSignal\n );\n continue;\n }\n throw new HttpCallError(\n httpResponse.status,\n errorBody,\n `Request failed: ${httpResponse.status}\\n${errorBody}`\n );\n }\n const responseBuf = new Uint8Array(await httpResponse.arrayBuffer());\n if (!params.send) {\n const decodedBuf = opts.journalValueCodec\n ? await opts.journalValueCodec.decode(responseBuf)\n : responseBuf;\n const outputSerde = params.opts?.opts.output ?? opts.serde ?? serde.json;\n return outputSerde.deserialize(decodedBuf) as O;\n }\n const json = serde.json.deserialize(responseBuf) as O;\n return { ...json, attachable };\n};\n\nconst doWorkflowHandleCall = async <O>(\n opts: ConnectionOpts,\n wfName: string,\n wfKey: string,\n op: \"output\" | \"attach\",\n callOpts?: Opts<unknown, O> | SendOpts<unknown>\n): Promise<O> => {\n const outputSerde = callOpts?.opts.output ?? opts.serde ?? serde.json;\n //\n // headers\n //\n const headers = {\n ...(opts.headers ?? {}),\n };\n //\n // make the call\n //\n const url = `${opts.url}/restate/workflow/${wfName}/${encodeURIComponent(\n wfKey\n )}/${op}`;\n\n // Abort signal, if any\n let signal: AbortSignal | undefined;\n if (\n callOpts?.opts?.signal !== undefined &&\n callOpts?.opts?.timeout !== undefined\n ) {\n throw new Error(\n \"You can't specify both signal and timeout options at the same time\"\n );\n } else if (callOpts?.opts?.signal !== undefined) {\n signal = callOpts?.opts?.signal;\n } else if (callOpts?.opts?.timeout !== undefined) {\n signal = AbortSignal.timeout(callOpts?.opts?.timeout);\n }\n\n const httpResponse = await fetch(url, {\n method: \"GET\",\n headers,\n signal,\n });\n if (httpResponse.ok) {\n const responseBuf = new Uint8Array(await httpResponse.arrayBuffer());\n const decodedBuf = opts.journalValueCodec\n ? await opts.journalValueCodec.decode(responseBuf)\n : responseBuf;\n return outputSerde.deserialize(decodedBuf) as O;\n }\n const body = await httpResponse.text();\n throw new HttpCallError(\n httpResponse.status,\n body,\n `Request failed: ${httpResponse.status}\\n${body}`\n );\n};\n\nclass HttpIngress implements Ingress {\n constructor(private readonly opts: ConnectionOpts) {}\n\n private proxy(component: string, key?: string, send?: boolean) {\n return new Proxy(\n {},\n {\n get: (_target, prop) => {\n const handler = prop as string;\n return (...args: unknown[]) => {\n const { parameter, opts } = optsFromArgs(args);\n return doComponentInvocation<unknown, unknown>(this.opts, {\n component,\n handler,\n key,\n parameter,\n opts,\n send,\n });\n };\n },\n }\n );\n }\n\n serviceClient<D>(opts: ServiceDefinitionFrom<D>): IngressClient<Service<D>> {\n return this.proxy(opts.name) as IngressClient<Service<D>>;\n }\n\n objectClient<D>(\n opts: VirtualObjectDefinitionFrom<D>,\n key: string\n ): IngressClient<VirtualObject<D>> {\n return this.proxy(opts.name, key) as IngressClient<VirtualObject<D>>;\n }\n\n workflowClient<D>(\n opts: WorkflowDefinitionFrom<D>,\n key: string\n ): IngressWorkflowClient<Workflow<D>> {\n const component = opts.name;\n const conn = this.opts;\n\n const workflowSubmit = async (\n ...args: unknown[]\n ): Promise<WorkflowSubmission<unknown>> => {\n const { parameter, opts } = optsFromArgs(args);\n const res: Send = await doComponentInvocation(conn, {\n component,\n handler: \"run\",\n key,\n send: true,\n parameter,\n opts,\n });\n\n return {\n invocationId: res.invocationId,\n status: res.status,\n attachable: true,\n };\n };\n\n const workflowAttach = (opts?: Opts<void, unknown>) =>\n doWorkflowHandleCall(conn, component, key, \"attach\", opts);\n\n const workflowOutput = async (\n opts?: Opts<void, unknown>\n ): Promise<Output<unknown>> => {\n try {\n const result = await doWorkflowHandleCall(\n conn,\n component,\n key,\n \"output\",\n opts\n );\n\n return {\n ready: true,\n result,\n };\n } catch (e) {\n if (!(e instanceof HttpCallError) || e.status !== 470) {\n throw e;\n }\n return {\n ready: false,\n get result() {\n throw new Error(\"Calling result() on a non ready workflow\");\n },\n };\n }\n };\n\n return new Proxy(\n {},\n {\n get: (_target, prop) => {\n const handler = prop as string;\n if (handler === \"workflowSubmit\") {\n return workflowSubmit;\n } else if (handler === \"workflowAttach\") {\n return workflowAttach;\n } else if (handler === \"workflowOutput\") {\n return workflowOutput;\n }\n // shared handlers pass trough via the ingress's normal invocation form\n // i.e. POST /<svc>/<key>/<handler>\n return (...args: unknown[]) => {\n const { parameter, opts } = optsFromArgs(args);\n return doComponentInvocation(conn, {\n component,\n handler,\n key,\n parameter,\n opts,\n });\n };\n },\n }\n ) as IngressWorkflowClient<Workflow<D>>;\n }\n\n objectSendClient<D>(\n opts: VirtualObjectDefinitionFrom<D>,\n key: string\n ): IngressSendClient<VirtualObject<D>> {\n return this.proxy(opts.name, key, true) as IngressSendClient<\n VirtualObject<D>\n >;\n }\n\n serviceSendClient<D>(\n opts: ServiceDefinitionFrom<D>\n ): IngressSendClient<Service<D>> {\n return this.proxy(opts.name, undefined, true) as IngressSendClient<\n Service<D>\n >;\n }\n\n scope(scopeKey: string): ScopedIngress {\n const conn = this.opts;\n const scopedProxy = (component: string, key?: string, send?: boolean) =>\n new Proxy(\n {},\n {\n get: (_target, prop) => {\n const handler = prop as string;\n return (...args: unknown[]) => {\n const { parameter, opts } = optsFromArgs(args);\n return doComponentInvocation<unknown, unknown>(conn, {\n component,\n handler,\n key,\n parameter,\n opts,\n send,\n scope: scopeKey,\n });\n };\n },\n }\n );\n\n return {\n serviceClient: <D>(opts: ServiceDefinitionFrom<D>) =>\n scopedProxy(opts.name) as IngressClient<Service<D>>,\n serviceSendClient: <D>(opts: ServiceDefinitionFrom<D>) =>\n scopedProxy(opts.name, undefined, true) as IngressSendClient<\n Service<D>\n >,\n objectClient: <D>(opts: VirtualObjectDefinitionFrom<D>, key: string) =>\n scopedProxy(opts.name, key) as IngressClient<VirtualObject<D>>,\n objectSendClient: <D>(\n opts: VirtualObjectDefinitionFrom<D>,\n key: string\n ) =>\n scopedProxy(opts.name, key, true) as IngressSendClient<\n VirtualObject<D>\n >,\n workflowClient: <D>(\n opts: WorkflowDefinitionFrom<D>,\n key: string\n ): IngressWorkflowClient<Workflow<D>> => {\n const component = opts.name;\n\n const workflowSubmit = async (\n ...args: unknown[]\n ): Promise<WorkflowSubmission<unknown>> => {\n const { parameter, opts } = optsFromArgs(args);\n const res: Send = await doComponentInvocation(conn, {\n component,\n handler: \"run\",\n key,\n send: true,\n parameter,\n opts,\n scope: scopeKey,\n });\n return {\n invocationId: res.invocationId,\n status: res.status,\n attachable: true,\n };\n };\n\n const workflowAttach = (opts?: Opts<void, unknown>) =>\n doWorkflowHandleCall(conn, component, key, \"attach\", opts);\n\n const workflowOutput = async (\n opts?: Opts<void, unknown>\n ): Promise<Output<unknown>> => {\n try {\n const result = await doWorkflowHandleCall(\n conn,\n component,\n key,\n \"output\",\n opts\n );\n return { ready: true, result };\n } catch (e) {\n if (!(e instanceof HttpCallError) || e.status !== 470) {\n throw e;\n }\n return {\n ready: false,\n get result() {\n throw new Error(\"Calling result() on a non ready workflow\");\n },\n };\n }\n };\n\n return new Proxy(\n {},\n {\n get: (_target, prop) => {\n const handler = prop as string;\n if (handler === \"workflowSubmit\") {\n return workflowSubmit;\n } else if (handler === \"workflowAttach\") {\n return workflowAttach;\n } else if (handler === \"workflowOutput\") {\n return workflowOutput;\n }\n return (...args: unknown[]) => {\n const { parameter, opts } = optsFromArgs(args);\n return doComponentInvocation(conn, {\n component,\n handler,\n key,\n parameter,\n opts,\n scope: scopeKey,\n });\n };\n },\n }\n ) as IngressWorkflowClient<Workflow<D>>;\n },\n };\n }\n\n async call<I, O>(opts: {\n service: string;\n handler: string;\n parameter: I;\n key?: string;\n scope?: string;\n opts?: Opts<I, O>;\n }): Promise<O> {\n return doComponentInvocation<I, O>(this.opts, {\n component: opts.service,\n handler: opts.handler,\n key: opts.key,\n scope: opts.scope,\n parameter: opts.parameter,\n send: false,\n opts: opts.opts,\n });\n }\n\n async send<I>(opts: {\n service: string;\n handler: string;\n parameter: I;\n key?: string;\n scope?: string;\n opts?: SendOpts<I>;\n }): Promise<Send> {\n return doComponentInvocation<I, Send>(this.opts, {\n component: opts.service,\n handler: opts.handler,\n key: opts.key,\n scope: opts.scope,\n parameter: opts.parameter,\n send: true,\n opts: opts.opts,\n });\n }\n\n async resolveAwakeable<T>(\n id: string,\n payload?: T,\n payloadSerde?: Serde<T>\n ): Promise<void> {\n const url = `${this.opts.url}/restate/a/${id}/resolve`;\n const { body, contentType } = serializeBodyWithContentType(\n payload,\n payloadSerde ?? this.opts.serde ?? serde.json,\n this.opts.journalValueCodec\n );\n const headers = {\n ...(this.opts.headers ?? {}),\n };\n if (contentType) {\n headers[\"Content-Type\"] = contentType;\n }\n const httpResponse = await fetch(url, {\n method: \"POST\",\n headers,\n body,\n });\n if (!httpResponse.ok) {\n const body = await httpResponse.text();\n throw new HttpCallError(\n httpResponse.status,\n body,\n `Request failed: ${httpResponse.status}\\n${body}`\n );\n }\n }\n\n async rejectAwakeable(id: string, reason: string): Promise<void> {\n const url = `${this.opts.url}/restate/a/${id}/reject`;\n const headers = {\n \"Content-Type\": \"text/plain\",\n ...(this.opts.headers ?? {}),\n };\n const httpResponse = await fetch(url, {\n method: \"POST\",\n headers,\n body: reason,\n });\n if (!httpResponse.ok) {\n const body = await httpResponse.text();\n throw new HttpCallError(\n httpResponse.status,\n body,\n `Request failed: ${httpResponse.status}\\n${body}`\n );\n }\n }\n\n async result<T>(\n send: Send<T> | WorkflowSubmission<T>,\n resultSerde?: Serde<T>\n ): Promise<T> {\n if (!send.attachable) {\n throw new Error(\n `Unable to fetch the result for ${send.invocationId}.\n A service's result is stored only with an idempotencyKey is supplied when invocating the service.`\n );\n }\n //\n // headers\n //\n const headers = {\n ...(this.opts.headers ?? {}),\n };\n //\n // make the call\n const url = `${this.opts.url}/restate/invocation/${send.invocationId}/attach`;\n\n const httpResponse = await fetch(url, {\n method: \"GET\",\n headers,\n });\n if (httpResponse.ok) {\n const responseBuf = new Uint8Array(await httpResponse.arrayBuffer());\n const decodedBuf = this.opts.journalValueCodec\n ? await this.opts.journalValueCodec.decode(responseBuf)\n : responseBuf;\n return (resultSerde ?? this.opts.serde ?? serde.json).deserialize(\n decodedBuf\n ) as T;\n }\n const body = await httpResponse.text();\n throw new HttpCallError(\n httpResponse.status,\n body,\n `Request failed: ${httpResponse.status}\\n${body}`\n );\n }\n}\n\nfunction computeDelayAsIso(opts: SendOpts): string {\n const delay = opts.delay();\n if (!delay) {\n return \"send\";\n }\n return `send?delay=${delay}ms`;\n}\n\nfunction serializeBodyWithContentType(\n body: unknown,\n serde: Serde<unknown>,\n journalValueCodec?: JournalValueCodec\n): {\n body?: Uint8Array;\n contentType?: string;\n} {\n let buffer = serde.serialize(body);\n if (journalValueCodec) {\n buffer = journalValueCodec.encode(buffer);\n }\n return {\n body: buffer,\n contentType: serde.contentType,\n };\n}\n"],"mappings":"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"}
package/dist/retry.cjs ADDED
@@ -0,0 +1,101 @@
1
+ const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
2
+ let __restatedev_restate_sdk_core = require("@restatedev/restate-sdk-core");
3
+ __restatedev_restate_sdk_core = require_rolldown_runtime.__toESM(__restatedev_restate_sdk_core);
4
+
5
+ //#region src/retry.ts
6
+ const DEFAULT_RETRY_POLICY = {
7
+ maxAttempts: 6,
8
+ initialInterval: 100,
9
+ maxInterval: 2e3,
10
+ exponentiationFactor: 2
11
+ };
12
+ /**
13
+ * Resolve a user supplied retry policy into a fully populated one.
14
+ *
15
+ * Retries are opt-in: returns `undefined` (disabled) when `retry` is omitted or
16
+ * `false`. `true` enables the built-in policy; an object enables it with the
17
+ * provided overrides.
18
+ */
19
+ function resolveRetryPolicy(retry) {
20
+ if (retry === void 0 || retry === false) return;
21
+ if (retry === true) return DEFAULT_RETRY_POLICY;
22
+ return {
23
+ maxAttempts: retry.maxAttempts ?? DEFAULT_RETRY_POLICY.maxAttempts,
24
+ initialInterval: retry.initialInterval !== void 0 ? (0, __restatedev_restate_sdk_core.millisOrDurationToMillis)(retry.initialInterval) : DEFAULT_RETRY_POLICY.initialInterval,
25
+ maxInterval: retry.maxInterval !== void 0 ? (0, __restatedev_restate_sdk_core.millisOrDurationToMillis)(retry.maxInterval) : DEFAULT_RETRY_POLICY.maxInterval,
26
+ exponentiationFactor: retry.exponentiationFactor ?? DEFAULT_RETRY_POLICY.exponentiationFactor,
27
+ shouldRetry: retry.shouldRetry
28
+ };
29
+ }
30
+ /** Whether an HTTP response status warrants a retry. */
31
+ function isRetryableStatus(status) {
32
+ return status === 429 || status >= 500 && status <= 599;
33
+ }
34
+ /**
35
+ * The built-in retry decision: retry network errors, HTTP `429`, and HTTP
36
+ * `5xx`. Exported so a custom {@link RetryPolicy.shouldRetry} can compose with
37
+ * it rather than reimplement it.
38
+ */
39
+ function defaultShouldRetry(failure) {
40
+ return failure.kind === "network" || isRetryableStatus(failure.status);
41
+ }
42
+ /**
43
+ * Compute the backoff for the given (zero based) attempt index using
44
+ * exponential backoff with full jitter, capped at `maxInterval`.
45
+ *
46
+ * When the server provided an explicit `Retry-After` we honor it instead,
47
+ * capped at `maxInterval` to avoid pathologically long waits.
48
+ */
49
+ function backoffDelay(policy, attempt, retryAfterMs) {
50
+ if (retryAfterMs !== void 0) return Math.min(retryAfterMs, policy.maxInterval);
51
+ const exp = policy.initialInterval * Math.pow(policy.exponentiationFactor, attempt);
52
+ const ceiling = Math.min(exp, policy.maxInterval);
53
+ return Math.random() * ceiling;
54
+ }
55
+ /**
56
+ * Parse a `Retry-After` header value into milliseconds.
57
+ *
58
+ * Supports both the delay-seconds form (`"120"`) and the HTTP-date form
59
+ * (`"Wed, 21 Oct 2015 07:28:00 GMT"`). Returns `undefined` when absent or
60
+ * unparseable.
61
+ */
62
+ function parseRetryAfter(headers, now = Date.now()) {
63
+ const value = headers.get("retry-after");
64
+ if (!value) return;
65
+ const seconds = Number(value);
66
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
67
+ const dateMs = Date.parse(value);
68
+ if (Number.isNaN(dateMs)) return;
69
+ return Math.max(0, dateMs - now);
70
+ }
71
+ /**
72
+ * Sleep for `ms`, rejecting early if `signal` aborts in the meantime.
73
+ */
74
+ function abortableSleep(ms, signal) {
75
+ const abortError = () => {
76
+ const reason = signal?.reason;
77
+ return reason instanceof Error ? reason : /* @__PURE__ */ new Error("The operation was aborted");
78
+ };
79
+ return new Promise((resolve, reject) => {
80
+ if (signal?.aborted) {
81
+ reject(abortError());
82
+ return;
83
+ }
84
+ const onAbort = () => {
85
+ clearTimeout(timer);
86
+ reject(abortError());
87
+ };
88
+ const timer = setTimeout(() => {
89
+ signal?.removeEventListener("abort", onAbort);
90
+ resolve();
91
+ }, ms);
92
+ signal?.addEventListener("abort", onAbort, { once: true });
93
+ });
94
+ }
95
+
96
+ //#endregion
97
+ exports.abortableSleep = abortableSleep;
98
+ exports.backoffDelay = backoffDelay;
99
+ exports.defaultShouldRetry = defaultShouldRetry;
100
+ exports.parseRetryAfter = parseRetryAfter;
101
+ exports.resolveRetryPolicy = resolveRetryPolicy;
@@ -0,0 +1,13 @@
1
+ import { RetryFailure } from "./api.cjs";
2
+
3
+ //#region src/retry.d.ts
4
+
5
+ /**
6
+ * The built-in retry decision: retry network errors, HTTP `429`, and HTTP
7
+ * `5xx`. Exported so a custom {@link RetryPolicy.shouldRetry} can compose with
8
+ * it rather than reimplement it.
9
+ */
10
+ declare function defaultShouldRetry(failure: RetryFailure): boolean;
11
+ //#endregion
12
+ export { defaultShouldRetry };
13
+ //# sourceMappingURL=retry.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.d.cts","names":[],"sources":["../src/retry.ts"],"sourcesContent":[],"mappings":";;;;;;;;;iBAwEgB,kBAAA,UAA4B"}
@@ -0,0 +1,13 @@
1
+ import { RetryFailure } from "./api.js";
2
+
3
+ //#region src/retry.d.ts
4
+
5
+ /**
6
+ * The built-in retry decision: retry network errors, HTTP `429`, and HTTP
7
+ * `5xx`. Exported so a custom {@link RetryPolicy.shouldRetry} can compose with
8
+ * it rather than reimplement it.
9
+ */
10
+ declare function defaultShouldRetry(failure: RetryFailure): boolean;
11
+ //#endregion
12
+ export { defaultShouldRetry };
13
+ //# sourceMappingURL=retry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.d.ts","names":[],"sources":["../src/retry.ts"],"sourcesContent":[],"mappings":";;;;;;;;;iBAwEgB,kBAAA,UAA4B"}
package/dist/retry.js ADDED
@@ -0,0 +1,96 @@
1
+ import { millisOrDurationToMillis } from "@restatedev/restate-sdk-core";
2
+
3
+ //#region src/retry.ts
4
+ const DEFAULT_RETRY_POLICY = {
5
+ maxAttempts: 6,
6
+ initialInterval: 100,
7
+ maxInterval: 2e3,
8
+ exponentiationFactor: 2
9
+ };
10
+ /**
11
+ * Resolve a user supplied retry policy into a fully populated one.
12
+ *
13
+ * Retries are opt-in: returns `undefined` (disabled) when `retry` is omitted or
14
+ * `false`. `true` enables the built-in policy; an object enables it with the
15
+ * provided overrides.
16
+ */
17
+ function resolveRetryPolicy(retry) {
18
+ if (retry === void 0 || retry === false) return;
19
+ if (retry === true) return DEFAULT_RETRY_POLICY;
20
+ return {
21
+ maxAttempts: retry.maxAttempts ?? DEFAULT_RETRY_POLICY.maxAttempts,
22
+ initialInterval: retry.initialInterval !== void 0 ? millisOrDurationToMillis(retry.initialInterval) : DEFAULT_RETRY_POLICY.initialInterval,
23
+ maxInterval: retry.maxInterval !== void 0 ? millisOrDurationToMillis(retry.maxInterval) : DEFAULT_RETRY_POLICY.maxInterval,
24
+ exponentiationFactor: retry.exponentiationFactor ?? DEFAULT_RETRY_POLICY.exponentiationFactor,
25
+ shouldRetry: retry.shouldRetry
26
+ };
27
+ }
28
+ /** Whether an HTTP response status warrants a retry. */
29
+ function isRetryableStatus(status) {
30
+ return status === 429 || status >= 500 && status <= 599;
31
+ }
32
+ /**
33
+ * The built-in retry decision: retry network errors, HTTP `429`, and HTTP
34
+ * `5xx`. Exported so a custom {@link RetryPolicy.shouldRetry} can compose with
35
+ * it rather than reimplement it.
36
+ */
37
+ function defaultShouldRetry(failure) {
38
+ return failure.kind === "network" || isRetryableStatus(failure.status);
39
+ }
40
+ /**
41
+ * Compute the backoff for the given (zero based) attempt index using
42
+ * exponential backoff with full jitter, capped at `maxInterval`.
43
+ *
44
+ * When the server provided an explicit `Retry-After` we honor it instead,
45
+ * capped at `maxInterval` to avoid pathologically long waits.
46
+ */
47
+ function backoffDelay(policy, attempt, retryAfterMs) {
48
+ if (retryAfterMs !== void 0) return Math.min(retryAfterMs, policy.maxInterval);
49
+ const exp = policy.initialInterval * Math.pow(policy.exponentiationFactor, attempt);
50
+ const ceiling = Math.min(exp, policy.maxInterval);
51
+ return Math.random() * ceiling;
52
+ }
53
+ /**
54
+ * Parse a `Retry-After` header value into milliseconds.
55
+ *
56
+ * Supports both the delay-seconds form (`"120"`) and the HTTP-date form
57
+ * (`"Wed, 21 Oct 2015 07:28:00 GMT"`). Returns `undefined` when absent or
58
+ * unparseable.
59
+ */
60
+ function parseRetryAfter(headers, now = Date.now()) {
61
+ const value = headers.get("retry-after");
62
+ if (!value) return;
63
+ const seconds = Number(value);
64
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
65
+ const dateMs = Date.parse(value);
66
+ if (Number.isNaN(dateMs)) return;
67
+ return Math.max(0, dateMs - now);
68
+ }
69
+ /**
70
+ * Sleep for `ms`, rejecting early if `signal` aborts in the meantime.
71
+ */
72
+ function abortableSleep(ms, signal) {
73
+ const abortError = () => {
74
+ const reason = signal?.reason;
75
+ return reason instanceof Error ? reason : /* @__PURE__ */ new Error("The operation was aborted");
76
+ };
77
+ return new Promise((resolve, reject) => {
78
+ if (signal?.aborted) {
79
+ reject(abortError());
80
+ return;
81
+ }
82
+ const onAbort = () => {
83
+ clearTimeout(timer);
84
+ reject(abortError());
85
+ };
86
+ const timer = setTimeout(() => {
87
+ signal?.removeEventListener("abort", onAbort);
88
+ resolve();
89
+ }, ms);
90
+ signal?.addEventListener("abort", onAbort, { once: true });
91
+ });
92
+ }
93
+
94
+ //#endregion
95
+ export { abortableSleep, backoffDelay, defaultShouldRetry, parseRetryAfter, resolveRetryPolicy };
96
+ //# sourceMappingURL=retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.js","names":["DEFAULT_RETRY_POLICY: ResolvedRetryPolicy","reason: unknown"],"sources":["../src/retry.ts"],"sourcesContent":["/*\n * Copyright (c) 2023-2026 - Restate Software, Inc., Restate GmbH\n *\n * This file is part of the Restate SDK for Node.js/TypeScript,\n * which is released under the MIT license.\n *\n * You can find a copy of the license in file LICENSE in the root\n * directory of this repository or package, or at\n * https://github.com/restatedev/sdk-typescript/blob/main/LICENSE\n */\n\nimport { millisOrDurationToMillis } from \"@restatedev/restate-sdk-core\";\nimport type { RetryFailure, RetryPolicy } from \"./api.js\";\n\n/** Fully resolved retry policy, with all defaults applied. */\nexport interface ResolvedRetryPolicy {\n maxAttempts: number;\n initialInterval: number;\n maxInterval: number;\n exponentiationFactor: number;\n shouldRetry?: (failure: RetryFailure, attempt: number) => boolean;\n}\n\nconst DEFAULT_RETRY_POLICY: ResolvedRetryPolicy = {\n maxAttempts: 6,\n initialInterval: 100,\n maxInterval: 2000,\n exponentiationFactor: 2,\n};\n\n/**\n * Resolve a user supplied retry policy into a fully populated one.\n *\n * Retries are opt-in: returns `undefined` (disabled) when `retry` is omitted or\n * `false`. `true` enables the built-in policy; an object enables it with the\n * provided overrides.\n */\nexport function resolveRetryPolicy(\n retry: RetryPolicy | boolean | undefined\n): ResolvedRetryPolicy | undefined {\n if (retry === undefined || retry === false) {\n return undefined;\n }\n if (retry === true) {\n return DEFAULT_RETRY_POLICY;\n }\n return {\n maxAttempts: retry.maxAttempts ?? DEFAULT_RETRY_POLICY.maxAttempts,\n initialInterval:\n retry.initialInterval !== undefined\n ? millisOrDurationToMillis(retry.initialInterval)\n : DEFAULT_RETRY_POLICY.initialInterval,\n maxInterval:\n retry.maxInterval !== undefined\n ? millisOrDurationToMillis(retry.maxInterval)\n : DEFAULT_RETRY_POLICY.maxInterval,\n exponentiationFactor:\n retry.exponentiationFactor ?? DEFAULT_RETRY_POLICY.exponentiationFactor,\n shouldRetry: retry.shouldRetry,\n };\n}\n\n/** Whether an HTTP response status warrants a retry. */\nexport function isRetryableStatus(status: number): boolean {\n return status === 429 || (status >= 500 && status <= 599);\n}\n\n/**\n * The built-in retry decision: retry network errors, HTTP `429`, and HTTP\n * `5xx`. Exported so a custom {@link RetryPolicy.shouldRetry} can compose with\n * it rather than reimplement it.\n */\nexport function defaultShouldRetry(failure: RetryFailure): boolean {\n return failure.kind === \"network\" || isRetryableStatus(failure.status);\n}\n\n/**\n * Compute the backoff for the given (zero based) attempt index using\n * exponential backoff with full jitter, capped at `maxInterval`.\n *\n * When the server provided an explicit `Retry-After` we honor it instead,\n * capped at `maxInterval` to avoid pathologically long waits.\n */\nexport function backoffDelay(\n policy: ResolvedRetryPolicy,\n attempt: number,\n retryAfterMs?: number\n): number {\n if (retryAfterMs !== undefined) {\n return Math.min(retryAfterMs, policy.maxInterval);\n }\n const exp =\n policy.initialInterval * Math.pow(policy.exponentiationFactor, attempt);\n const ceiling = Math.min(exp, policy.maxInterval);\n // full jitter: random in [0, ceiling]\n return Math.random() * ceiling;\n}\n\n/**\n * Parse a `Retry-After` header value into milliseconds.\n *\n * Supports both the delay-seconds form (`\"120\"`) and the HTTP-date form\n * (`\"Wed, 21 Oct 2015 07:28:00 GMT\"`). Returns `undefined` when absent or\n * unparseable.\n */\nexport function parseRetryAfter(\n headers: Headers,\n now: number = Date.now()\n): number | undefined {\n const value = headers.get(\"retry-after\");\n if (!value) {\n return undefined;\n }\n const seconds = Number(value);\n if (Number.isFinite(seconds)) {\n return Math.max(0, seconds * 1000);\n }\n const dateMs = Date.parse(value);\n if (Number.isNaN(dateMs)) {\n return undefined;\n }\n return Math.max(0, dateMs - now);\n}\n\n/**\n * Sleep for `ms`, rejecting early if `signal` aborts in the meantime.\n */\nexport function abortableSleep(\n ms: number,\n signal?: AbortSignal\n): Promise<void> {\n const abortError = (): Error => {\n const reason: unknown = signal?.reason;\n return reason instanceof Error\n ? reason\n : new Error(\"The operation was aborted\");\n };\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortError());\n return;\n }\n const onAbort = () => {\n clearTimeout(timer);\n reject(abortError());\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n}\n"],"mappings":";;;AAuBA,MAAMA,uBAA4C;CAChD,aAAa;CACb,iBAAiB;CACjB,aAAa;CACb,sBAAsB;CACvB;;;;;;;;AASD,SAAgB,mBACd,OACiC;AACjC,KAAI,UAAU,UAAa,UAAU,MACnC;AAEF,KAAI,UAAU,KACZ,QAAO;AAET,QAAO;EACL,aAAa,MAAM,eAAe,qBAAqB;EACvD,iBACE,MAAM,oBAAoB,SACtB,yBAAyB,MAAM,gBAAgB,GAC/C,qBAAqB;EAC3B,aACE,MAAM,gBAAgB,SAClB,yBAAyB,MAAM,YAAY,GAC3C,qBAAqB;EAC3B,sBACE,MAAM,wBAAwB,qBAAqB;EACrD,aAAa,MAAM;EACpB;;;AAIH,SAAgB,kBAAkB,QAAyB;AACzD,QAAO,WAAW,OAAQ,UAAU,OAAO,UAAU;;;;;;;AAQvD,SAAgB,mBAAmB,SAAgC;AACjE,QAAO,QAAQ,SAAS,aAAa,kBAAkB,QAAQ,OAAO;;;;;;;;;AAUxE,SAAgB,aACd,QACA,SACA,cACQ;AACR,KAAI,iBAAiB,OACnB,QAAO,KAAK,IAAI,cAAc,OAAO,YAAY;CAEnD,MAAM,MACJ,OAAO,kBAAkB,KAAK,IAAI,OAAO,sBAAsB,QAAQ;CACzE,MAAM,UAAU,KAAK,IAAI,KAAK,OAAO,YAAY;AAEjD,QAAO,KAAK,QAAQ,GAAG;;;;;;;;;AAUzB,SAAgB,gBACd,SACA,MAAc,KAAK,KAAK,EACJ;CACpB,MAAM,QAAQ,QAAQ,IAAI,cAAc;AACxC,KAAI,CAAC,MACH;CAEF,MAAM,UAAU,OAAO,MAAM;AAC7B,KAAI,OAAO,SAAS,QAAQ,CAC1B,QAAO,KAAK,IAAI,GAAG,UAAU,IAAK;CAEpC,MAAM,SAAS,KAAK,MAAM,MAAM;AAChC,KAAI,OAAO,MAAM,OAAO,CACtB;AAEF,QAAO,KAAK,IAAI,GAAG,SAAS,IAAI;;;;;AAMlC,SAAgB,eACd,IACA,QACe;CACf,MAAM,mBAA0B;EAC9B,MAAMC,SAAkB,QAAQ;AAChC,SAAO,kBAAkB,QACrB,yBACA,IAAI,MAAM,4BAA4B;;AAE5C,QAAO,IAAI,SAAS,SAAS,WAAW;AACtC,MAAI,QAAQ,SAAS;AACnB,UAAO,YAAY,CAAC;AACpB;;EAEF,MAAM,gBAAgB;AACpB,gBAAa,MAAM;AACnB,UAAO,YAAY,CAAC;;EAEtB,MAAM,QAAQ,iBAAiB;AAC7B,WAAQ,oBAAoB,SAAS,QAAQ;AAC7C,YAAS;KACR,GAAG;AACN,UAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,MAAM,CAAC;GAC1D"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=retry.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.test.d.ts","sourceRoot":"","sources":["../src/retry.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,251 @@
1
+ /*
2
+ * Copyright (c) 2023-2026 - Restate Software, Inc., Restate GmbH
3
+ *
4
+ * This file is part of the Restate SDK for Node.js/TypeScript,
5
+ * which is released under the MIT license.
6
+ *
7
+ * You can find a copy of the license in file LICENSE in the root
8
+ * directory of this repository or package, or at
9
+ * https://github.com/restatedev/sdk-typescript/blob/main/LICENSE
10
+ */
11
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
12
+ import { abortableSleep, backoffDelay, defaultShouldRetry, isRetryableStatus, parseRetryAfter, resolveRetryPolicy, } from "./retry.js";
13
+ import { connect, HttpCallError } from "./ingress.js";
14
+ import { Opts } from "./api.js";
15
+ describe("resolveRetryPolicy", () => {
16
+ it("is disabled (undefined) when unset — retries are opt-in", () => {
17
+ expect(resolveRetryPolicy(undefined)).toBeUndefined();
18
+ });
19
+ it("returns undefined when disabled with false", () => {
20
+ expect(resolveRetryPolicy(false)).toBeUndefined();
21
+ });
22
+ it("returns the default policy when enabled with true", () => {
23
+ expect(resolveRetryPolicy(true)).toEqual({
24
+ maxAttempts: 6,
25
+ initialInterval: 100,
26
+ maxInterval: 2000,
27
+ exponentiationFactor: 2,
28
+ });
29
+ });
30
+ it("fills in missing fields with defaults", () => {
31
+ expect(resolveRetryPolicy({ maxAttempts: 3 })).toEqual({
32
+ maxAttempts: 3,
33
+ initialInterval: 100,
34
+ maxInterval: 2000,
35
+ exponentiationFactor: 2,
36
+ shouldRetry: undefined,
37
+ });
38
+ });
39
+ it("carries a custom shouldRetry through", () => {
40
+ const shouldRetry = () => false;
41
+ expect(resolveRetryPolicy({ shouldRetry })).toMatchObject({ shouldRetry });
42
+ });
43
+ it("accepts Duration intervals, resolving them to milliseconds", () => {
44
+ expect(resolveRetryPolicy({
45
+ initialInterval: { seconds: 1 },
46
+ maxInterval: { seconds: 30 },
47
+ })).toMatchObject({
48
+ initialInterval: 1000,
49
+ maxInterval: 30_000,
50
+ });
51
+ });
52
+ });
53
+ describe("defaultShouldRetry", () => {
54
+ it("retries network errors and 429/5xx responses", () => {
55
+ expect(defaultShouldRetry({ kind: "network", error: new Error("x") })).toBe(true);
56
+ expect(defaultShouldRetry({
57
+ kind: "response",
58
+ status: 503,
59
+ headers: new Headers(),
60
+ })).toBe(true);
61
+ });
62
+ it("does not retry non-retryable responses", () => {
63
+ expect(defaultShouldRetry({
64
+ kind: "response",
65
+ status: 409,
66
+ headers: new Headers(),
67
+ })).toBe(false);
68
+ });
69
+ });
70
+ describe("isRetryableStatus", () => {
71
+ it("retries on 429 and 5xx", () => {
72
+ expect(isRetryableStatus(429)).toBe(true);
73
+ expect(isRetryableStatus(500)).toBe(true);
74
+ expect(isRetryableStatus(503)).toBe(true);
75
+ expect(isRetryableStatus(599)).toBe(true);
76
+ });
77
+ it("does not retry on 4xx (except 429) or 2xx/3xx", () => {
78
+ expect(isRetryableStatus(200)).toBe(false);
79
+ expect(isRetryableStatus(400)).toBe(false);
80
+ expect(isRetryableStatus(404)).toBe(false);
81
+ expect(isRetryableStatus(409)).toBe(false);
82
+ expect(isRetryableStatus(470)).toBe(false);
83
+ });
84
+ });
85
+ describe("backoffDelay", () => {
86
+ const policy = {
87
+ maxAttempts: 6,
88
+ initialInterval: 100,
89
+ maxInterval: 2000,
90
+ exponentiationFactor: 2,
91
+ };
92
+ it("never exceeds the per-attempt ceiling (full jitter)", () => {
93
+ for (let attempt = 0; attempt < 6; attempt++) {
94
+ const ceiling = Math.min(100 * 2 ** attempt, 2000);
95
+ for (let i = 0; i < 50; i++) {
96
+ const d = backoffDelay(policy, attempt);
97
+ expect(d).toBeGreaterThanOrEqual(0);
98
+ expect(d).toBeLessThanOrEqual(ceiling);
99
+ }
100
+ }
101
+ });
102
+ it("honors Retry-After, capped at maxInterval", () => {
103
+ expect(backoffDelay(policy, 0, 500)).toBe(500);
104
+ expect(backoffDelay(policy, 0, 10_000)).toBe(2000);
105
+ });
106
+ });
107
+ describe("parseRetryAfter", () => {
108
+ const h = (v) => new Headers(v ? { "retry-after": v } : {});
109
+ it("returns undefined when absent", () => {
110
+ expect(parseRetryAfter(h())).toBeUndefined();
111
+ });
112
+ it("parses delay-seconds", () => {
113
+ expect(parseRetryAfter(h("2"))).toBe(2000);
114
+ });
115
+ it("parses an HTTP-date relative to now", () => {
116
+ const now = 1_000_000;
117
+ const date = new Date(now + 3000).toUTCString();
118
+ expect(parseRetryAfter(h(date), now)).toBe(3000);
119
+ });
120
+ it("returns undefined for garbage", () => {
121
+ expect(parseRetryAfter(h("not-a-date"))).toBeUndefined();
122
+ });
123
+ });
124
+ describe("abortableSleep", () => {
125
+ it("resolves after the delay", async () => {
126
+ await expect(abortableSleep(1)).resolves.toBeUndefined();
127
+ });
128
+ it("rejects immediately when the signal is already aborted", async () => {
129
+ const ac = new AbortController();
130
+ ac.abort(new Error("boom"));
131
+ await expect(abortableSleep(1000, ac.signal)).rejects.toThrow("boom");
132
+ });
133
+ it("rejects when aborted mid-sleep", async () => {
134
+ const ac = new AbortController();
135
+ const p = abortableSleep(1000, ac.signal);
136
+ ac.abort(new Error("late"));
137
+ await expect(p).rejects.toThrow("late");
138
+ });
139
+ });
140
+ // ---------------------------------------------------------------------------
141
+ // Integration: retry behavior through connect()/call()
142
+ // ---------------------------------------------------------------------------
143
+ describe("ingress auto-retry", () => {
144
+ const URL = "http://localhost:8080";
145
+ let fetchMock;
146
+ const ok = (body = { ok: true }) => () => Promise.resolve(new Response(JSON.stringify(body), { status: 200 }));
147
+ const fail = (status, headers) => () => Promise.resolve(new Response("nope", { status, headers }));
148
+ const neterr = (msg) => () => Promise.reject(new TypeError(msg));
149
+ // A fetch mock that plays back a queued sequence, repeating the last item.
150
+ const queue = (...attempts) => {
151
+ let i = 0;
152
+ fetchMock.mockImplementation(() => attempts[Math.min(i++, attempts.length - 1)]());
153
+ };
154
+ const fastRetry = {
155
+ initialInterval: 1,
156
+ maxInterval: 2,
157
+ exponentiationFactor: 2,
158
+ };
159
+ const call = (idempotencyKey, retry) => connect({ url: URL, retry }).call({
160
+ service: "svc",
161
+ handler: "greet",
162
+ parameter: {},
163
+ opts: idempotencyKey ? Opts.from({ idempotencyKey }) : undefined,
164
+ });
165
+ beforeEach(() => {
166
+ fetchMock = vi.fn();
167
+ vi.stubGlobal("fetch", fetchMock);
168
+ });
169
+ afterEach(() => {
170
+ vi.unstubAllGlobals();
171
+ });
172
+ it("does NOT retry without an idempotency key", async () => {
173
+ queue(fail(503), ok());
174
+ await expect(call(undefined, fastRetry)).rejects.toBeInstanceOf(HttpCallError);
175
+ expect(fetchMock).toHaveBeenCalledTimes(1);
176
+ });
177
+ it("does NOT retry by default — retries are opt-in", async () => {
178
+ queue(fail(503), ok());
179
+ await expect(call("k1", undefined)).rejects.toBeInstanceOf(HttpCallError);
180
+ expect(fetchMock).toHaveBeenCalledTimes(1);
181
+ });
182
+ it("retry:true enables the built-in policy", async () => {
183
+ queue(fail(503), ok());
184
+ await expect(call("k1", true)).resolves.toEqual({ ok: true });
185
+ expect(fetchMock.mock.calls.length).toBeGreaterThanOrEqual(2);
186
+ });
187
+ it("retries 5xx then succeeds when an idempotency key is set", async () => {
188
+ queue(fail(500), fail(503), ok({ greeting: "hi" }));
189
+ await expect(call("k1", fastRetry)).resolves.toEqual({ greeting: "hi" });
190
+ expect(fetchMock).toHaveBeenCalledTimes(3);
191
+ });
192
+ it("retries on 429", async () => {
193
+ queue(fail(429), ok());
194
+ await expect(call("k1", fastRetry)).resolves.toEqual({ ok: true });
195
+ expect(fetchMock).toHaveBeenCalledTimes(2);
196
+ });
197
+ it("retries on a network error", async () => {
198
+ queue(neterr("connection refused"), ok());
199
+ await expect(call("k1", fastRetry)).resolves.toEqual({ ok: true });
200
+ expect(fetchMock).toHaveBeenCalledTimes(2);
201
+ });
202
+ it("does NOT retry on a non-retryable 4xx", async () => {
203
+ queue(fail(409), ok());
204
+ await expect(call("k1", fastRetry)).rejects.toBeInstanceOf(HttpCallError);
205
+ expect(fetchMock).toHaveBeenCalledTimes(1);
206
+ });
207
+ it("retry:false disables retries even with an idempotency key", async () => {
208
+ queue(fail(503), ok());
209
+ await expect(call("k1", false)).rejects.toBeInstanceOf(HttpCallError);
210
+ expect(fetchMock).toHaveBeenCalledTimes(1);
211
+ });
212
+ it("a custom shouldRetry can narrow the decision (no retry on 500)", async () => {
213
+ queue(fail(500), ok());
214
+ await expect(call("k1", {
215
+ ...fastRetry,
216
+ shouldRetry: (f) => defaultShouldRetry(f) && !(f.kind === "response" && f.status === 500),
217
+ })).rejects.toMatchObject({ status: 500 });
218
+ expect(fetchMock).toHaveBeenCalledTimes(1);
219
+ });
220
+ it("a custom shouldRetry can inspect the response body", async () => {
221
+ queue(fail(503, undefined), ok());
222
+ const seen = [];
223
+ await call("k1", {
224
+ ...fastRetry,
225
+ shouldRetry: (f) => {
226
+ if (f.kind === "response")
227
+ seen.push(f.body);
228
+ return defaultShouldRetry(f);
229
+ },
230
+ });
231
+ expect(seen).toEqual(["nope"]); // body text was available to the predicate
232
+ });
233
+ it("gives up after maxAttempts and throws the last error", async () => {
234
+ queue(fail(500));
235
+ await expect(call("k1", { ...fastRetry, maxAttempts: 3 })).rejects.toMatchObject({ status: 500 });
236
+ expect(fetchMock).toHaveBeenCalledTimes(3); // initial + 2 retries
237
+ });
238
+ it("mints a fresh timeout signal per attempt", async () => {
239
+ queue(fail(500), ok());
240
+ await connect({ url: URL, retry: fastRetry }).call({
241
+ service: "svc",
242
+ handler: "greet",
243
+ parameter: {},
244
+ opts: Opts.from({ idempotencyKey: "k1", timeout: 10_000 }),
245
+ });
246
+ const signals = fetchMock.mock.calls.map((c) => c[1].signal);
247
+ expect(signals[0]).not.toBe(signals[1]);
248
+ expect(signals[0]?.aborted).toBe(false);
249
+ });
250
+ });
251
+ //# sourceMappingURL=retry.test.js.map