@procedures/client 0.6.0 → 0.8.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.
- package/CHANGELOG.md +66 -0
- package/README.md +63 -7
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/client/transport.d.ts +5 -0
- package/dist/adapters/http/client/transport.d.ts.map +1 -1
- package/dist/adapters/http/client/transport.js +10 -4
- package/dist/adapters/http/client/transport.js.map +1 -1
- package/dist/adapters/http/server/ndjson.d.ts +73 -0
- package/dist/adapters/http/server/ndjson.d.ts.map +1 -0
- package/dist/adapters/http/server/ndjson.js +228 -0
- package/dist/adapters/http/server/ndjson.js.map +1 -0
- package/dist/adapters/http/server/transport.d.ts +15 -4
- package/dist/adapters/http/server/transport.d.ts.map +1 -1
- package/dist/adapters/http/server/transport.js +29 -69
- package/dist/adapters/http/server/transport.js.map +1 -1
- package/dist/adapters/http/server/types.d.ts +15 -0
- package/dist/adapters/http/server/types.d.ts.map +1 -1
- package/dist/adapters/sequence.d.ts +14 -0
- package/dist/adapters/sequence.d.ts.map +1 -1
- package/dist/adapters/sequence.js +16 -0
- package/dist/adapters/sequence.js.map +1 -1
- package/dist/adapters/websocket/client/transport.d.ts +7 -3
- package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/client/transport.js +9 -3
- package/dist/adapters/websocket/client/transport.js.map +1 -1
- package/dist/adapters/websocket/server/transport.d.ts +10 -0
- package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/server/transport.js +22 -2
- package/dist/adapters/websocket/server/transport.js.map +1 -1
- package/dist/browser.d.ts +5 -5
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +4 -3
- package/dist/browser.js.map +1 -1
- package/dist/client/errors/registry.d.ts.map +1 -1
- package/dist/client/errors/registry.js +1 -0
- package/dist/client/errors/registry.js.map +1 -1
- package/dist/client/middleware/retry.d.ts +4 -0
- package/dist/client/middleware/retry.d.ts.map +1 -1
- package/dist/client/middleware/retry.js +86 -78
- package/dist/client/middleware/retry.js.map +1 -1
- package/dist/events/bus.d.ts +32 -14
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/events/bus.js +119 -65
- package/dist/events/bus.js.map +1 -1
- package/dist/events/index.d.ts +3 -2
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js.map +1 -1
- package/dist/events/publisher.d.ts.map +1 -1
- package/dist/events/publisher.js +20 -6
- package/dist/events/publisher.js.map +1 -1
- package/dist/events/remote.d.ts +80 -18
- package/dist/events/remote.d.ts.map +1 -1
- package/dist/events/remote.js +448 -58
- package/dist/events/remote.js.map +1 -1
- package/dist/events/types.d.ts +13 -2
- package/dist/events/types.d.ts.map +1 -1
- package/dist/node.d.ts +6 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +5 -0
- package/dist/node.js.map +1 -1
- package/dist/procedures/caller.d.ts +33 -0
- package/dist/procedures/caller.d.ts.map +1 -0
- package/dist/procedures/caller.js +27 -0
- package/dist/procedures/caller.js.map +1 -0
- package/dist/procedures/collection/index.d.ts +2 -2
- package/dist/procedures/collection/index.d.ts.map +1 -1
- package/dist/procedures/collection/index.js +1 -1
- package/dist/procedures/collection/index.js.map +1 -1
- package/dist/procedures/collection/procedures.d.ts.map +1 -1
- package/dist/procedures/collection/procedures.js +21 -1
- package/dist/procedures/collection/procedures.js.map +1 -1
- package/dist/procedures/collection/schemas.d.ts +9 -0
- package/dist/procedures/collection/schemas.d.ts.map +1 -1
- package/dist/procedures/collection/schemas.js +28 -0
- package/dist/procedures/collection/schemas.js.map +1 -1
- package/dist/procedures/collection/storage.d.ts +6 -0
- package/dist/procedures/collection/storage.d.ts.map +1 -1
- package/dist/procedures/index.d.ts +3 -1
- package/dist/procedures/index.d.ts.map +1 -1
- package/dist/procedures/index.js +2 -0
- package/dist/procedures/index.js.map +1 -1
- package/dist/procedures/invoke.d.ts.map +1 -1
- package/dist/procedures/invoke.js +3 -0
- package/dist/procedures/invoke.js.map +1 -1
- package/dist/procedures/types.d.ts +50 -0
- package/dist/procedures/types.d.ts.map +1 -1
- package/dist/procedures/types.js.map +1 -1
- package/dist/protocol.d.ts +1 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +1 -0
- package/dist/protocol.js.map +1 -1
- package/dist/server/procedure-server.d.ts +41 -5
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +94 -9
- package/dist/server/procedure-server.js.map +1 -1
- package/dist/timers.d.ts +9 -0
- package/dist/timers.d.ts.map +1 -1
- package/dist/timers.js +6 -1
- package/dist/timers.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/http/client/transport.ts +10 -4
- package/src/adapters/http/server/ndjson.ts +259 -0
- package/src/adapters/http/server/transport.ts +31 -69
- package/src/adapters/http/server/types.ts +17 -0
- package/src/adapters/sequence.ts +18 -0
- package/src/adapters/websocket/client/transport.ts +9 -3
- package/src/adapters/websocket/server/transport.ts +23 -2
- package/src/browser.ts +9 -2
- package/src/client/errors/registry.ts +1 -0
- package/src/client/middleware/retry.ts +89 -78
- package/src/events/bus.ts +141 -73
- package/src/events/index.ts +3 -0
- package/src/events/publisher.ts +23 -6
- package/src/events/remote.ts +575 -73
- package/src/events/types.ts +15 -2
- package/src/node.ts +6 -0
- package/src/procedures/caller.ts +45 -0
- package/src/procedures/collection/index.ts +3 -0
- package/src/procedures/collection/procedures.ts +22 -0
- package/src/procedures/collection/schemas.ts +37 -0
- package/src/procedures/collection/storage.ts +6 -0
- package/src/procedures/index.ts +4 -0
- package/src/procedures/invoke.ts +2 -0
- package/src/procedures/types.ts +53 -0
- package/src/protocol.ts +3 -1
- package/src/server/procedure-server.ts +111 -11
- package/src/timers.ts +15 -1
|
@@ -8,6 +8,8 @@ import type { Request, Express } from "express";
|
|
|
8
8
|
import type { Server as HttpServer } from "http";
|
|
9
9
|
import type { Method } from "../../../client/types.js";
|
|
10
10
|
import type { BrowserGuardOptions } from "../../../server/browser-guard.js";
|
|
11
|
+
import type { Clock } from "../../../clock.js";
|
|
12
|
+
import type { Duration } from "../../../duration.js";
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* URL strategy for converting HTTP requests to RPC methods.
|
|
@@ -68,6 +70,21 @@ export interface HttpServerTransportOptions extends BrowserGuardOptions {
|
|
|
68
70
|
*/
|
|
69
71
|
bodyLimit?: number;
|
|
70
72
|
|
|
73
|
+
/**
|
|
74
|
+
* The quiet time of an NDJSON stream. After it, the server writes a keep-alive line: a blank
|
|
75
|
+
* line. Each line of the stream starts the time again. Thus a proxy or a client with an idle
|
|
76
|
+
* timeout does not close a quiet stream. `false`, or a duration of zero or less, sends no
|
|
77
|
+
* keep-alive line.
|
|
78
|
+
* @default seconds(15) (`TIMER_DEFAULTS.httpServer.keepAliveMs`)
|
|
79
|
+
*/
|
|
80
|
+
keepAlive?: Duration | false;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The clock of the keep-alive timers (roadmap R35). The default is the clock of the platform. A
|
|
84
|
+
* test gives a fake clock.
|
|
85
|
+
*/
|
|
86
|
+
clock?: Clock;
|
|
87
|
+
|
|
71
88
|
/**
|
|
72
89
|
* Custom CORS options. `origin` is "*", one origin or a list: the response names the
|
|
73
90
|
* request's origin when it is in the list.
|
package/src/adapters/sequence.ts
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* number of lost frames in its metadata key `stream-gap`, and the caller writes a warning. The
|
|
11
11
|
* caller also gives back the credit of the lost frames.
|
|
12
12
|
* - A frame without `seq` comes from an older peer. The caller takes it as before.
|
|
13
|
+
* - A blank line of an NDJSON stream is a keep-alive line. It is not a frame and it has no `seq`,
|
|
14
|
+
* so the reader skips it before the check.
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -18,6 +20,14 @@
|
|
|
18
20
|
*/
|
|
19
21
|
export const STREAM_GAP_METADATA_KEY = "stream-gap";
|
|
20
22
|
|
|
23
|
+
/**
|
|
24
|
+
* The metadata key of an item that the HTTP client transport read from an NDJSON stream. Its value
|
|
25
|
+
* is `true`. The receiver sets it. It does not cross the wire, and the receiver removes a key of
|
|
26
|
+
* this name that the peer sent. The constant `STREAM_METADATA_KEY` of the server has the same
|
|
27
|
+
* name, with the value `unbounded` in the metadata of a response.
|
|
28
|
+
*/
|
|
29
|
+
export const STREAM_ITEM_METADATA_KEY = "stream";
|
|
30
|
+
|
|
21
31
|
/** The order of the stream frames of one call, at the side that receives them. */
|
|
22
32
|
export class StreamSequence {
|
|
23
33
|
private last = 0;
|
|
@@ -48,6 +58,14 @@ export function itemMetadata(metadata: Record<string, unknown> | undefined, gap:
|
|
|
48
58
|
return out;
|
|
49
59
|
}
|
|
50
60
|
|
|
61
|
+
/**
|
|
62
|
+
* This function gives the metadata of an item of an NDJSON stream: the metadata of `itemMetadata`,
|
|
63
|
+
* and the key `stream` with the value `true`. The value of the peer for this key goes away.
|
|
64
|
+
*/
|
|
65
|
+
export function streamItemMetadata(metadata: Record<string, unknown> | undefined, gap: number): Record<string, unknown> {
|
|
66
|
+
return { ...itemMetadata(metadata, gap), [STREAM_ITEM_METADATA_KEY]: true };
|
|
67
|
+
}
|
|
68
|
+
|
|
51
69
|
/** The number of frames that did not arrive immediately before an item: 0 without a gap. */
|
|
52
70
|
export function gapOf(metadata: Record<string, unknown> | undefined): number {
|
|
53
71
|
const gap = metadata?.[STREAM_GAP_METADATA_KEY];
|
|
@@ -874,11 +874,12 @@ export class WebSocketTransport implements Transport {
|
|
|
874
874
|
/**
|
|
875
875
|
* This function subscribes to topics of the bus of the server (`documentation/EVENTS.md`). The
|
|
876
876
|
* handler gets each event in order. After a drop, the transport sends the subscription again on
|
|
877
|
-
* the new connection. The events between the drop and the new confirmation are lost.
|
|
877
|
+
* the new connection. The events between the drop and the new confirmation are lost. The
|
|
878
|
+
* subscribers of the same topics with the same options share one call.
|
|
878
879
|
*
|
|
879
880
|
* @param topics - One topic, or a list of 1 to 64 topics
|
|
880
881
|
* @param handler - The handler of each event. A promise that it gives holds the next event.
|
|
881
|
-
* @param options - `onSubscribed`, `onError`, a signal, the metadata of the call
|
|
882
|
+
* @param options - `onSubscribed`, `onError`, a signal, the metadata of the call, the resubscribe rules
|
|
882
883
|
* @returns The unsubscribe function, with the `ready` promise of the first confirmation
|
|
883
884
|
*
|
|
884
885
|
* @example
|
|
@@ -904,12 +905,17 @@ export class WebSocketTransport implements Transport {
|
|
|
904
905
|
return remoteEvents<T>(this, topics, options, this.subscriptionHooks());
|
|
905
906
|
}
|
|
906
907
|
|
|
907
|
-
/**
|
|
908
|
+
/**
|
|
909
|
+
* The parts of the subscriptions of this transport: its connection, its backoff, its
|
|
910
|
+
* `maxAttempts`, its clock, its close and `onEvent`.
|
|
911
|
+
*/
|
|
908
912
|
private subscriptionHooks(): SubscriptionHooks {
|
|
909
913
|
return {
|
|
910
914
|
lifetime: this.subscriptions.signal,
|
|
911
915
|
ready: (signal) => this.whenConnected(signal),
|
|
912
916
|
backoff: (attempt) => reconnectDelay(attempt, this.options.reconnect),
|
|
917
|
+
maxAttempts: this.options.reconnect.maxAttempts,
|
|
918
|
+
clock: this.clock,
|
|
913
919
|
onEvent: (event) => {
|
|
914
920
|
if (!this.options.onEvent) return;
|
|
915
921
|
try {
|
|
@@ -90,6 +90,12 @@ export class WebSocketServerTransport implements ServerTransport {
|
|
|
90
90
|
*/
|
|
91
91
|
private readonly channels = new Map<string, { incoming: IncomingCalls; outgoing: OutgoingCalls }>();
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* The transport of the remote events of each connection: one object for each connection, so the
|
|
95
|
+
* subscriptions of one connection to the same topics share one call (EVENTS.md).
|
|
96
|
+
*/
|
|
97
|
+
private readonly eventTransports = new Map<string, Transport>();
|
|
98
|
+
|
|
93
99
|
/** The browser checks of each connection (see `server/browser-guard.ts`) */
|
|
94
100
|
private readonly guard: BrowserGuardOptions;
|
|
95
101
|
|
|
@@ -303,6 +309,7 @@ export class WebSocketServerTransport implements ServerTransport {
|
|
|
303
309
|
channel?.incoming.abortAll();
|
|
304
310
|
channel?.outgoing.failAll("WebSocket connection closed");
|
|
305
311
|
this.channels.delete(connectionId);
|
|
312
|
+
this.eventTransports.delete(connectionId);
|
|
306
313
|
const conn = this.trackedConnections.get(connectionId);
|
|
307
314
|
if (conn) {
|
|
308
315
|
this.trackedConnections.delete(connectionId);
|
|
@@ -552,7 +559,7 @@ export class WebSocketServerTransport implements ServerTransport {
|
|
|
552
559
|
handler: RemoteEventHandler<T>,
|
|
553
560
|
options: SubscribeOptions = {},
|
|
554
561
|
): Subscription {
|
|
555
|
-
return subscribeRemote<T>(this.
|
|
562
|
+
return subscribeRemote<T>(this.eventTransportFor(connectionId), topics, handler, options, { resubscribe: false });
|
|
556
563
|
}
|
|
557
564
|
|
|
558
565
|
/**
|
|
@@ -564,7 +571,21 @@ export class WebSocketServerTransport implements ServerTransport {
|
|
|
564
571
|
* @param options - A signal, `onSubscribed`, the metadata of the call
|
|
565
572
|
*/
|
|
566
573
|
events<T = unknown>(connectionId: string, topics: string | readonly string[], options: RemoteEventsOptions = {}): AsyncIterable<RemoteEvent<T>> {
|
|
567
|
-
return remoteEvents<T>(this.
|
|
574
|
+
return remoteEvents<T>(this.eventTransportFor(connectionId), topics, options, { resubscribe: false });
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* The transport of the remote events of a connection: the same object for each subscription of
|
|
579
|
+
* a tracked connection. The close of the connection removes it.
|
|
580
|
+
*/
|
|
581
|
+
private eventTransportFor(connectionId: string): Transport {
|
|
582
|
+
if (!this.trackedConnections.has(connectionId)) return this.transportFor(connectionId);
|
|
583
|
+
let transport = this.eventTransports.get(connectionId);
|
|
584
|
+
if (!transport) {
|
|
585
|
+
transport = this.transportFor(connectionId);
|
|
586
|
+
this.eventTransports.set(connectionId, transport);
|
|
587
|
+
}
|
|
588
|
+
return transport;
|
|
568
589
|
}
|
|
569
590
|
|
|
570
591
|
/**
|
package/src/browser.ts
CHANGED
|
@@ -128,6 +128,7 @@ export {
|
|
|
128
128
|
registerModules,
|
|
129
129
|
createRegistry,
|
|
130
130
|
registerClientProcedures,
|
|
131
|
+
callerOf,
|
|
131
132
|
// Procedure reference system (procedure-as-data)
|
|
132
133
|
PROCEDURE_SYMBOL,
|
|
133
134
|
PROCEDURE_JSON_KEY,
|
|
@@ -213,6 +214,7 @@ export {
|
|
|
213
214
|
|
|
214
215
|
export type {
|
|
215
216
|
Effects,
|
|
217
|
+
FsRootRules,
|
|
216
218
|
PermittedEffects,
|
|
217
219
|
Register,
|
|
218
220
|
ClearanceOf,
|
|
@@ -235,6 +237,8 @@ export type {
|
|
|
235
237
|
StorageMetadata,
|
|
236
238
|
ProcedureModule,
|
|
237
239
|
ModuleInput,
|
|
240
|
+
Caller,
|
|
241
|
+
CallerOptions,
|
|
238
242
|
RegistrationOptions,
|
|
239
243
|
ProcedureRegistryLike,
|
|
240
244
|
ProcedureDefinition,
|
|
@@ -323,8 +327,9 @@ export type { TraceContext } from "./tracing/traceparent.js";
|
|
|
323
327
|
// Wire protocol v1: the version and the one list of error codes (roadmap R25)
|
|
324
328
|
export { PROTOCOL_VERSION, PROTOCOL_HEADER, ERROR_CODES, isProtocolErrorCode, httpStatusOf, protocolVersionOf } from "./protocol.js";
|
|
325
329
|
export type { ProtocolErrorCode, ErrorCodeInfo } from "./protocol.js";
|
|
326
|
-
// The seq of the stream frames: the metadata key of an item after a gap
|
|
327
|
-
|
|
330
|
+
// The seq of the stream frames: the metadata key of an item after a gap, and the key of an item
|
|
331
|
+
// that the HTTP client read from an NDJSON stream (WIRE-PROTOCOL.md)
|
|
332
|
+
export { STREAM_GAP_METADATA_KEY, STREAM_ITEM_METADATA_KEY } from "./adapters/sequence.js";
|
|
328
333
|
// The deadline of a call: the remaining time goes as procedure-timeout (roadmap R10)
|
|
329
334
|
export { TIMEOUT_METADATA_KEY, DEADLINE_METADATA_KEY } from "./client/deadline.js";
|
|
330
335
|
// Typed durations: each public option that is a time takes a Duration (roadmap R11)
|
|
@@ -410,6 +415,7 @@ export type {
|
|
|
410
415
|
TypedEventBus,
|
|
411
416
|
EventHandler,
|
|
412
417
|
EventBusOptions,
|
|
418
|
+
EventListenOptions,
|
|
413
419
|
EventWaitOptions,
|
|
414
420
|
ChannelMap,
|
|
415
421
|
} from "./events/index.js";
|
|
@@ -423,6 +429,7 @@ export type {
|
|
|
423
429
|
RemoteEvent,
|
|
424
430
|
RemoteEventHandler,
|
|
425
431
|
RemoteEventsOptions,
|
|
432
|
+
ResubscribeBackoff,
|
|
426
433
|
SubscribeOptions,
|
|
427
434
|
Subscription,
|
|
428
435
|
} from "./events/index.js";
|
|
@@ -84,6 +84,7 @@ const PROTOCOL_CATEGORIES: Readonly<Record<ProtocolErrorCode, ErrorCategory>> =
|
|
|
84
84
|
CONNECTION_CLOSED: ErrorCategory.NETWORK,
|
|
85
85
|
CONNECTION_NOT_FOUND: ErrorCategory.CLIENT,
|
|
86
86
|
TOPIC_NOT_FOUND: ErrorCategory.CLIENT,
|
|
87
|
+
TOPIC_CLEARED: ErrorCategory.CANCELLED,
|
|
87
88
|
};
|
|
88
89
|
|
|
89
90
|
/**
|
|
@@ -108,6 +108,10 @@ export interface RetryOptions {
|
|
|
108
108
|
/**
|
|
109
109
|
* Hook called after each retry completes (success or failure).
|
|
110
110
|
*
|
|
111
|
+
* The middleware calls it when the call ends: after the last item, or when the reader stops
|
|
112
|
+
* reading. A `Client` with `throwOnError` stops at the error item. Before, the hook then did
|
|
113
|
+
* not run, so it did not hear of a failure. A thrown error and an abort do not call it.
|
|
114
|
+
*
|
|
111
115
|
* @param success - Whether the retry succeeded
|
|
112
116
|
* @param attempt - Attempt number that just completed
|
|
113
117
|
*/
|
|
@@ -249,99 +253,106 @@ export function createRetryMiddleware(options: RetryOptions = {}): TypedClientMi
|
|
|
249
253
|
const idempotencyKey = idempotencyKeyOf(context.message.metadata) ?? newIdempotencyKey();
|
|
250
254
|
context.message.metadata[IDEMPOTENCY_KEY_METADATA] = idempotencyKey;
|
|
251
255
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
256
|
+
// The outcome of the call for onAfterRetry. The hook runs in `finally`: a reader that stops
|
|
257
|
+
// at an error item (a Client with throwOnError) closes this generator at its yield. Before,
|
|
258
|
+
// the hook came after the yield, so it did not hear of a failure.
|
|
259
|
+
let outcome: { success: boolean; attempt: number } | undefined;
|
|
260
|
+
try {
|
|
261
|
+
let attempt = 0;
|
|
262
|
+
for (;;) {
|
|
263
|
+
// Update retry metadata with current state
|
|
264
|
+
context.message.metadata.retry = {
|
|
265
|
+
...(typeof given === "object" && given !== null ? (given as object) : {}),
|
|
266
|
+
attempt,
|
|
267
|
+
maxAttempts: maxRetries + 1,
|
|
268
|
+
idempotent,
|
|
269
|
+
};
|
|
270
|
+
// Each attempt gets its own request id. The id correlates the frames of one attempt
|
|
271
|
+
// on the wire: a cancelled attempt can still answer under its id (deep dive TRN-8/9).
|
|
272
|
+
context.message.id = attempt === 0 ? baseId : `${baseId}.retry${attempt}`;
|
|
264
273
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
274
|
+
// Check if cancelled before attempt
|
|
275
|
+
if (signal?.aborted) {
|
|
276
|
+
yield abortedItem<TRes>(context.message.id);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
270
279
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
280
|
+
// The items pass through as they arrive (BUGS-2026-07 M2: before, the middleware
|
|
281
|
+
// collected the whole stream first). A retry is possible only while no item has
|
|
282
|
+
// reached the reader: the first item is a retryable error, or the attempt throws
|
|
283
|
+
// before its first item. After that, a retry would repeat items.
|
|
284
|
+
let started = false;
|
|
285
|
+
let lastItem: ResponseItem<TRes> | undefined;
|
|
286
|
+
let retryItem: ResponseItem<TRes> | undefined;
|
|
287
|
+
try {
|
|
288
|
+
for await (const item of next(context)) {
|
|
289
|
+
if (!started && attempt < maxRetries && shouldRetryItem(item, attempt)) {
|
|
290
|
+
retryItem = item;
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
started = true;
|
|
294
|
+
lastItem = item;
|
|
295
|
+
outcome = { success: item.status.type === "success", attempt };
|
|
296
|
+
yield item;
|
|
283
297
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
298
|
+
} catch (error) {
|
|
299
|
+
// A thrown error does not call onAfterRetry
|
|
300
|
+
outcome = undefined;
|
|
301
|
+
const isAborted = signal?.aborted || (error instanceof Error && error.name === "AbortError");
|
|
302
|
+
if (started || isAborted || attempt >= maxRetries) {
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
// A thrown error is retried only when it is a network failure, or when shouldRetry
|
|
306
|
+
// says so. Before, every thrown error was retried: a validation error, an open
|
|
307
|
+
// circuit breaker, a rate limit (deep dive TRN-8).
|
|
308
|
+
const item = thrownItem<TRes>(context.message.id, error);
|
|
309
|
+
if (!shouldRetryItem(item, attempt)) {
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
retryItem = item;
|
|
292
313
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
throw error;
|
|
314
|
+
|
|
315
|
+
if (retryItem === undefined) {
|
|
316
|
+
// Success, a non-retryable error, or the last attempt
|
|
317
|
+
outcome = { success: lastItem?.status.type === "success", attempt };
|
|
318
|
+
return;
|
|
299
319
|
}
|
|
300
|
-
retryItem = item;
|
|
301
|
-
}
|
|
302
320
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
321
|
+
// The budget is empty: the error goes to the caller (R12)
|
|
322
|
+
if (!takeRetry()) {
|
|
323
|
+
outcome = { success: false, attempt };
|
|
324
|
+
yield retryItem;
|
|
325
|
+
return;
|
|
307
326
|
}
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
327
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
yield retryItem;
|
|
314
|
-
if (onAfterRetry) await onAfterRetry(false, attempt);
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
328
|
+
// A server that asks for a wait (Retry-After) gets at least that wait (R12)
|
|
329
|
+
let delay = Math.max(retryDelayOf(attempt, overrides), retryAfterOf(retryItem) ?? 0);
|
|
317
330
|
|
|
318
|
-
|
|
319
|
-
|
|
331
|
+
// Before retry hook
|
|
332
|
+
if (onBeforeRetry) {
|
|
333
|
+
const hookResult = await onBeforeRetry(retryItem, attempt);
|
|
334
|
+
if (!hookResult.shouldRetry) {
|
|
335
|
+
// Hook decided not to retry - yield the error and return
|
|
336
|
+
outcome = { success: false, attempt };
|
|
337
|
+
yield retryItem;
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
320
340
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (!hookResult.shouldRetry) {
|
|
325
|
-
// Hook decided not to retry - yield the error and return
|
|
326
|
-
yield retryItem;
|
|
327
|
-
if (onAfterRetry) {
|
|
328
|
-
await onAfterRetry(false, attempt);
|
|
341
|
+
// Use custom delay if provided by hook
|
|
342
|
+
if (hookResult.delayMs !== undefined) {
|
|
343
|
+
delay = hookResult.delayMs;
|
|
329
344
|
}
|
|
330
|
-
return;
|
|
331
345
|
}
|
|
332
346
|
|
|
333
|
-
//
|
|
334
|
-
if (
|
|
335
|
-
|
|
347
|
+
// The backoff stops at once when the caller aborts (deep dive TRN-8)
|
|
348
|
+
if (!(await sleep(delay, signal, clock))) {
|
|
349
|
+
yield abortedItem<TRes>(context.message.id);
|
|
350
|
+
return;
|
|
336
351
|
}
|
|
352
|
+
attempt++;
|
|
337
353
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (!(await sleep(delay, signal, clock))) {
|
|
341
|
-
yield abortedItem<TRes>(context.message.id);
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
attempt++;
|
|
354
|
+
} finally {
|
|
355
|
+
if (outcome && onAfterRetry) await onAfterRetry(outcome.success, outcome.attempt);
|
|
345
356
|
}
|
|
346
357
|
};
|
|
347
358
|
};
|