@rdlabo/workers-hono-kit 0.6.18 → 0.7.1
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.
- package/README.md +5 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/realtime/hibernation.d.ts +48 -0
- package/dist/realtime/hibernation.js +57 -0
- package/dist/realtime/legacy-sse.d.ts +25 -0
- package/dist/realtime/legacy-sse.js +110 -0
- package/dist/realtime/retry.d.ts +22 -0
- package/dist/realtime/retry.js +42 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ It provides the building blocks a NestJS-style API needs but that don't run on `
|
|
|
13
13
|
- **Stripe** Workers-native client + async webhook verification.
|
|
14
14
|
- **Payment failure & subscription reconcile**: provider-agnostic `payment_failed` helpers — Stripe decline reasons → Japanese messages, Apple / Google subscription-renewal classification, `iapFailureKey` / receipt (de)serialization, and Stripe reconcile branch decisions.
|
|
15
15
|
- **Testing helpers** (`@rdlabo/workers-hono-kit/testing`): a Drizzle-migration-backed test database, in-memory Firebase fake, configurable test doubles, and Stripe fixtures.
|
|
16
|
+
- **Realtime helpers**: Hibernation WebSocket upgrade/broadcast/close, legacy SSE bridging, and Durable Object retry policy.
|
|
16
17
|
|
|
17
18
|
## Install
|
|
18
19
|
|
|
@@ -82,6 +83,10 @@ npm install ai ai-gateway-provider # createAiGatewayProvider
|
|
|
82
83
|
| `ErrorReporter` / `ErrorReportContext` | Types for a `reportError`-style unhandled-error reporter (e.g. wired to Sentry), paired with `createHttpErrorHandler`'s `onUnhandledError`. |
|
|
83
84
|
| `createSentryErrorReporter(sentry)` / `SentryExceptionReporterLike` | Build an `ErrorReporter` that forwards to Sentry with an optional `request_id` tag (no hard `@sentry/cloudflare` dependency). |
|
|
84
85
|
| `DeferExecutor` / `defaultDefer` / `createWaitUntilDefer(ctx)` | Fire-and-forget executor for Workers: `defaultDefer` swallows rejections (tests); `createWaitUntilDefer` registers work via `ctx.waitUntil`. |
|
|
86
|
+
| `configureHibernationAutoResponse` / `upgradeHibernationWebSocket` / `broadcastHibernationWebSockets` | Hibernation WebSocket room primitives: runtime ping/pong without waking JavaScript, attachment-before-accept upgrade, and broadcast through sockets restored by `getWebSockets()`. |
|
|
87
|
+
| `acknowledgeHibernationWebSocketClose` / `closeHibernationWebSocket` | Safe close helpers, including normalization of reserved received-only close codes. |
|
|
88
|
+
| `createLegacySseBridge(options)` | Migration adapter from legacy SSE clients to one or more Hibernation WebSocket rooms. Downstream cancellation aborts pending upgrades and closes late-arriving sockets. |
|
|
89
|
+
| `retryDurableObjectOperation(operation, options?)` / `isRetryableDurableObjectError(error)` | Retry idempotent DO work only for `retryable && !overloaded`, with jittered exponential backoff. `operation` runs per attempt so callers create a fresh stub after an exception. |
|
|
85
90
|
| `createAiGatewayProvider(config)` / `AiGatewayConfig` / `AiGatewayProvider` | Route `@ai-sdk` models through the Cloudflare AI Gateway, via either a Workers `AI` binding or REST credentials (`accountId` / `gateway` / `token`). |
|
|
86
91
|
| `KVCache` / `KVNamespace` / `KVCacheOptions` | Workers-KV cache-aside helper (key `appName+version+table_type_column`, sha256 for string ids, TTL clamped ≥60s). Set `appName` / `version` per application. |
|
|
87
92
|
| `createStripeClient(secret, opts?)` / `verifyStripeWebhook(...)` / `CreateStripeClientOptions` | Workers-native Stripe client (fetch transport) + async webhook verification (SubtleCrypto). `apiVersion` optional (pin to a fixed Stripe API version). |
|
package/dist/index.d.ts
CHANGED
|
@@ -47,6 +47,12 @@ export { defaultDefer, createWaitUntilDefer } from './http/defer.js';
|
|
|
47
47
|
export type { DeferExecutor } from './http/defer.js';
|
|
48
48
|
export { createSentryErrorReporter } from './http/http-error.js';
|
|
49
49
|
export type { SentryExceptionReporterLike } from './http/http-error.js';
|
|
50
|
+
export { acknowledgeHibernationWebSocketClose, broadcastHibernationWebSockets, closeHibernationWebSocket, configureHibernationAutoResponse, upgradeHibernationWebSocket, } from './realtime/hibernation.js';
|
|
51
|
+
export type { HibernationAutoResponseOptions, HibernationUpgradeOptions, HibernationWebSocketLike, HibernationWebSocketStateLike, WebSocketAutoResponsePairFactory, WebSocketPairFactory, } from './realtime/hibernation.js';
|
|
52
|
+
export { createLegacySseBridge } from './realtime/legacy-sse.js';
|
|
53
|
+
export type { LegacySseBridgeOptions, RealtimeDurableObjectNamespaceLike } from './realtime/legacy-sse.js';
|
|
54
|
+
export { isRetryableDurableObjectError, retryDurableObjectOperation } from './realtime/retry.js';
|
|
55
|
+
export type { DurableObjectErrorLike, DurableObjectRetryOptions } from './realtime/retry.js';
|
|
50
56
|
export { KVCache } from './cache/kv-cache.js';
|
|
51
57
|
export type { KVNamespace, KVCacheOptions } from './cache/kv-cache.js';
|
|
52
58
|
export { createStripeClient, verifyStripeWebhook } from './stripe/client.js';
|
package/dist/index.js
CHANGED
|
@@ -35,6 +35,10 @@ export { createAppErrorHandler } from './http/app-error-handler.js';
|
|
|
35
35
|
export { normalizeTrailingSlash } from './http/trailing-slash.js';
|
|
36
36
|
export { defaultDefer, createWaitUntilDefer } from './http/defer.js';
|
|
37
37
|
export { createSentryErrorReporter } from './http/http-error.js';
|
|
38
|
+
// realtime
|
|
39
|
+
export { acknowledgeHibernationWebSocketClose, broadcastHibernationWebSockets, closeHibernationWebSocket, configureHibernationAutoResponse, upgradeHibernationWebSocket, } from './realtime/hibernation.js';
|
|
40
|
+
export { createLegacySseBridge } from './realtime/legacy-sse.js';
|
|
41
|
+
export { isRetryableDurableObjectError, retryDurableObjectOperation } from './realtime/retry.js';
|
|
38
42
|
// cache
|
|
39
43
|
export { KVCache } from './cache/kv-cache.js';
|
|
40
44
|
// stripe
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Minimal Hibernation WebSocket state used by the shared helpers. */
|
|
2
|
+
export interface HibernationWebSocketStateLike {
|
|
3
|
+
acceptWebSocket(socket: WebSocket): void;
|
|
4
|
+
getWebSockets(): WebSocket[];
|
|
5
|
+
setWebSocketAutoResponse(pair: unknown): void;
|
|
6
|
+
}
|
|
7
|
+
/** A server-side WebSocket that can persist metadata across Durable Object hibernation. */
|
|
8
|
+
export interface HibernationWebSocketLike extends WebSocket {
|
|
9
|
+
serializeAttachment(value: unknown): void;
|
|
10
|
+
}
|
|
11
|
+
/** Constructor shape of the Workers `WebSocketPair` runtime global. */
|
|
12
|
+
export type WebSocketPairFactory = new () => {
|
|
13
|
+
0: HibernationWebSocketLike;
|
|
14
|
+
1: HibernationWebSocketLike;
|
|
15
|
+
};
|
|
16
|
+
/** Constructor shape of the Workers `WebSocketRequestResponsePair` runtime global. */
|
|
17
|
+
export type WebSocketAutoResponsePairFactory = new (request: string, response: string) => unknown;
|
|
18
|
+
/** Options for configuring runtime-handled ping/pong responses. */
|
|
19
|
+
export interface HibernationAutoResponseOptions {
|
|
20
|
+
state: HibernationWebSocketStateLike;
|
|
21
|
+
ping: string;
|
|
22
|
+
pong: string;
|
|
23
|
+
pairFactory?: WebSocketAutoResponsePairFactory;
|
|
24
|
+
}
|
|
25
|
+
/** Options for upgrading a request to a Hibernation WebSocket. */
|
|
26
|
+
export interface HibernationUpgradeOptions {
|
|
27
|
+
state: HibernationWebSocketStateLike;
|
|
28
|
+
request: Request;
|
|
29
|
+
protocol: string;
|
|
30
|
+
attachment?: unknown;
|
|
31
|
+
pairFactory?: WebSocketPairFactory;
|
|
32
|
+
responseFactory?: (client: WebSocket, protocol: string) => Response;
|
|
33
|
+
}
|
|
34
|
+
/** Configure ping/pong at the runtime layer so application heartbeats do not wake the object. */
|
|
35
|
+
export declare function configureHibernationAutoResponse(options: HibernationAutoResponseOptions): void;
|
|
36
|
+
/**
|
|
37
|
+
* Upgrade a request and register its server socket with the Hibernation WebSocket API.
|
|
38
|
+
*
|
|
39
|
+
* Attachments are serialized before acceptance so connection metadata remains available after
|
|
40
|
+
* the Durable Object instance is evicted and reconstructed.
|
|
41
|
+
*/
|
|
42
|
+
export declare function upgradeHibernationWebSocket(options: HibernationUpgradeOptions): Response;
|
|
43
|
+
/** Broadcast one JSON message to all sockets restored by `getWebSockets()`. */
|
|
44
|
+
export declare function broadcastHibernationWebSockets(state: Pick<HibernationWebSocketStateLike, 'getWebSockets'>, payload: unknown): void;
|
|
45
|
+
/** Close a Hibernation WebSocket while tolerating already-closed sockets. */
|
|
46
|
+
export declare function closeHibernationWebSocket(socket: WebSocket, code: number, reason: string): void;
|
|
47
|
+
/** Echo a peer close using a legal close-frame code. */
|
|
48
|
+
export declare function acknowledgeHibernationWebSocketClose(socket: WebSocket, code: number, reason: string): void;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Configure ping/pong at the runtime layer so application heartbeats do not wake the object. */
|
|
2
|
+
export function configureHibernationAutoResponse(options) {
|
|
3
|
+
const Pair = options.pairFactory ?? WebSocketRequestResponsePair;
|
|
4
|
+
options.state.setWebSocketAutoResponse(new Pair(options.ping, options.pong));
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Upgrade a request and register its server socket with the Hibernation WebSocket API.
|
|
8
|
+
*
|
|
9
|
+
* Attachments are serialized before acceptance so connection metadata remains available after
|
|
10
|
+
* the Durable Object instance is evicted and reconstructed.
|
|
11
|
+
*/
|
|
12
|
+
export function upgradeHibernationWebSocket(options) {
|
|
13
|
+
if (options.request.headers.get('Upgrade')?.toLowerCase() !== 'websocket') {
|
|
14
|
+
return new Response('Expected Upgrade: websocket', { status: 426 });
|
|
15
|
+
}
|
|
16
|
+
const Pair = options.pairFactory ?? WebSocketPair;
|
|
17
|
+
const pair = new Pair();
|
|
18
|
+
const client = pair[0];
|
|
19
|
+
const server = pair[1];
|
|
20
|
+
if (options.attachment !== undefined) {
|
|
21
|
+
server.serializeAttachment(options.attachment);
|
|
22
|
+
}
|
|
23
|
+
options.state.acceptWebSocket(server);
|
|
24
|
+
return options.responseFactory
|
|
25
|
+
? options.responseFactory(client, options.protocol)
|
|
26
|
+
: new Response(null, {
|
|
27
|
+
status: 101,
|
|
28
|
+
webSocket: client,
|
|
29
|
+
headers: { 'Sec-WebSocket-Protocol': options.protocol },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** Broadcast one JSON message to all sockets restored by `getWebSockets()`. */
|
|
33
|
+
export function broadcastHibernationWebSockets(state, payload) {
|
|
34
|
+
const message = JSON.stringify(payload);
|
|
35
|
+
for (const socket of state.getWebSockets()) {
|
|
36
|
+
try {
|
|
37
|
+
socket.send(message);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
closeHibernationWebSocket(socket, 1011, 'publish failed');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Close a Hibernation WebSocket while tolerating already-closed sockets. */
|
|
45
|
+
export function closeHibernationWebSocket(socket, code, reason) {
|
|
46
|
+
try {
|
|
47
|
+
socket.close(code, reason);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// The socket is already closed or cannot send a close frame.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Echo a peer close using a legal close-frame code. */
|
|
54
|
+
export function acknowledgeHibernationWebSocketClose(socket, code, reason) {
|
|
55
|
+
const replyCode = code === 1005 || code === 1006 || code === 1015 ? 1000 : code;
|
|
56
|
+
closeHibernationWebSocket(socket, replyCode, reason);
|
|
57
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Minimal Durable Object namespace needed by the legacy SSE bridge. */
|
|
2
|
+
export interface RealtimeDurableObjectNamespaceLike<TId = unknown> {
|
|
3
|
+
idFromName(name: string): TId;
|
|
4
|
+
get(id: TId): {
|
|
5
|
+
fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
/** Options for bridging legacy SSE clients to Hibernation WebSocket rooms. */
|
|
9
|
+
export interface LegacySseBridgeOptions<TId = unknown> {
|
|
10
|
+
namespace: RealtimeDurableObjectNamespaceLike<TId>;
|
|
11
|
+
roomNames: readonly string[];
|
|
12
|
+
signal: AbortSignal;
|
|
13
|
+
protocol: string;
|
|
14
|
+
clientId?: string;
|
|
15
|
+
heartbeatMs?: number;
|
|
16
|
+
pong?: string;
|
|
17
|
+
connectUrl?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Bridge legacy SSE clients to one or more Hibernation WebSocket rooms.
|
|
21
|
+
*
|
|
22
|
+
* The heartbeat lives in the outer Worker only; Durable Objects remain hibernatable. Aborting the
|
|
23
|
+
* downstream request also aborts pending upgrades, including the late-upgrade race during teardown.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createLegacySseBridge<TId>(options: LegacySseBridgeOptions<TId>): Response;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge legacy SSE clients to one or more Hibernation WebSocket rooms.
|
|
3
|
+
*
|
|
4
|
+
* The heartbeat lives in the outer Worker only; Durable Objects remain hibernatable. Aborting the
|
|
5
|
+
* downstream request also aborts pending upgrades, including the late-upgrade race during teardown.
|
|
6
|
+
*/
|
|
7
|
+
export function createLegacySseBridge(options) {
|
|
8
|
+
const encoder = new TextEncoder();
|
|
9
|
+
const sockets = new Set();
|
|
10
|
+
const heartbeatMs = options.heartbeatMs ?? 25_000;
|
|
11
|
+
const pong = options.pong ?? 'pong';
|
|
12
|
+
const connectUrl = options.connectUrl ?? 'https://do/connect';
|
|
13
|
+
let heartbeat;
|
|
14
|
+
let closeStream;
|
|
15
|
+
const stream = new ReadableStream({
|
|
16
|
+
start(controller) {
|
|
17
|
+
let closed = false;
|
|
18
|
+
const upstreamAbort = new AbortController();
|
|
19
|
+
const close = () => {
|
|
20
|
+
if (closed) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
closed = true;
|
|
24
|
+
upstreamAbort.abort();
|
|
25
|
+
if (heartbeat) {
|
|
26
|
+
clearInterval(heartbeat);
|
|
27
|
+
}
|
|
28
|
+
for (const socket of sockets) {
|
|
29
|
+
try {
|
|
30
|
+
socket.close(1000, 'legacy SSE closed');
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// already closed
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
sockets.clear();
|
|
37
|
+
try {
|
|
38
|
+
controller.close();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// already closed
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
closeStream = close;
|
|
45
|
+
if (options.signal.aborted) {
|
|
46
|
+
close();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
options.signal.addEventListener('abort', close, { once: true });
|
|
50
|
+
heartbeat = setInterval(() => {
|
|
51
|
+
try {
|
|
52
|
+
controller.enqueue(encoder.encode('event: ping\ndata: ping\n\n'));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
close();
|
|
56
|
+
}
|
|
57
|
+
}, heartbeatMs);
|
|
58
|
+
const connect = async (roomName) => {
|
|
59
|
+
const id = options.namespace.idFromName(roomName);
|
|
60
|
+
const response = (await options.namespace.get(id).fetch(connectUrl, {
|
|
61
|
+
signal: upstreamAbort.signal,
|
|
62
|
+
headers: {
|
|
63
|
+
Upgrade: 'websocket',
|
|
64
|
+
'Sec-WebSocket-Protocol': options.protocol,
|
|
65
|
+
...(options.clientId ? { 'x-client-id': options.clientId } : {}),
|
|
66
|
+
},
|
|
67
|
+
}));
|
|
68
|
+
const socket = response.webSocket;
|
|
69
|
+
if (response.status !== 101 || !socket) {
|
|
70
|
+
socket?.close(1011, 'legacy SSE upgrade failed');
|
|
71
|
+
throw new Error('Legacy realtime WebSocket upgrade failed');
|
|
72
|
+
}
|
|
73
|
+
if (closed) {
|
|
74
|
+
socket.close(1000, 'legacy SSE already closed');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
sockets.add(socket);
|
|
78
|
+
socket.accept();
|
|
79
|
+
socket.addEventListener('message', ({ data }) => {
|
|
80
|
+
if (typeof data !== 'string' || data === pong) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(data);
|
|
85
|
+
for (const event of Array.isArray(parsed) ? parsed : [parsed]) {
|
|
86
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Ignore malformed upstream messages.
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
socket.addEventListener('close', close, { once: true });
|
|
94
|
+
socket.addEventListener('error', close, { once: true });
|
|
95
|
+
};
|
|
96
|
+
void Promise.all(options.roomNames.map((roomName) => connect(roomName))).catch(close);
|
|
97
|
+
},
|
|
98
|
+
cancel() {
|
|
99
|
+
closeStream?.();
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
return new Response(stream, {
|
|
103
|
+
headers: {
|
|
104
|
+
'Content-Type': 'text/event-stream',
|
|
105
|
+
'Cache-Control': 'no-cache',
|
|
106
|
+
Connection: 'keep-alive',
|
|
107
|
+
'X-Accel-Buffering': 'no',
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Durable Object error flags documented by the Workers runtime. */
|
|
2
|
+
export interface DurableObjectErrorLike {
|
|
3
|
+
retryable?: boolean;
|
|
4
|
+
overloaded?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/** Retry policy for an idempotent Durable Object operation. */
|
|
7
|
+
export interface DurableObjectRetryOptions {
|
|
8
|
+
maxAttempts?: number;
|
|
9
|
+
baseDelayMs?: number;
|
|
10
|
+
maxDelayMs?: number;
|
|
11
|
+
random?: () => number;
|
|
12
|
+
wait?: (delayMs: number) => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
/** Return whether a Durable Object failure may be retried safely by policy. */
|
|
15
|
+
export declare function isRetryableDurableObjectError(error: unknown): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Retry an idempotent Durable Object operation with jittered exponential backoff.
|
|
18
|
+
*
|
|
19
|
+
* `operation` is invoked again for every attempt. Callers must create a fresh stub inside it,
|
|
20
|
+
* because a stub that threw may remain in a broken state. Overload errors are never retried.
|
|
21
|
+
*/
|
|
22
|
+
export declare function retryDurableObjectOperation<T>(operation: (attempt: number) => Promise<T>, options?: DurableObjectRetryOptions): Promise<T>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
function hasFlag(error, flag) {
|
|
2
|
+
if (typeof error !== 'object' || error === null) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
return error[flag] === true;
|
|
6
|
+
}
|
|
7
|
+
function defaultWait(delayMs) {
|
|
8
|
+
const runtimeScheduler = globalThis.scheduler;
|
|
9
|
+
return runtimeScheduler?.wait
|
|
10
|
+
? runtimeScheduler.wait(delayMs)
|
|
11
|
+
: new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
12
|
+
}
|
|
13
|
+
/** Return whether a Durable Object failure may be retried safely by policy. */
|
|
14
|
+
export function isRetryableDurableObjectError(error) {
|
|
15
|
+
return hasFlag(error, 'retryable') && !hasFlag(error, 'overloaded');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Retry an idempotent Durable Object operation with jittered exponential backoff.
|
|
19
|
+
*
|
|
20
|
+
* `operation` is invoked again for every attempt. Callers must create a fresh stub inside it,
|
|
21
|
+
* because a stub that threw may remain in a broken state. Overload errors are never retried.
|
|
22
|
+
*/
|
|
23
|
+
export async function retryDurableObjectOperation(operation, options = {}) {
|
|
24
|
+
const maxAttempts = Math.max(1, options.maxAttempts ?? 3);
|
|
25
|
+
const baseDelayMs = Math.max(0, options.baseDelayMs ?? 100);
|
|
26
|
+
const maxDelayMs = Math.max(baseDelayMs, options.maxDelayMs ?? 1000);
|
|
27
|
+
const random = options.random ?? Math.random;
|
|
28
|
+
const wait = options.wait ?? defaultWait;
|
|
29
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
30
|
+
try {
|
|
31
|
+
return await operation(attempt);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (!isRetryableDurableObjectError(error) || attempt + 1 >= maxAttempts) {
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
const delayMs = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt * random());
|
|
38
|
+
await wait(delayMs);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
throw new Error('Durable Object retry exhausted');
|
|
42
|
+
}
|