@procedures/client 0.2.2 → 0.3.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 +71 -0
- package/README.md +61 -38
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/client/transport.d.ts.map +1 -1
- package/dist/adapters/http/client/transport.js +35 -3
- package/dist/adapters/http/client/transport.js.map +1 -1
- package/dist/adapters/http/client/types.d.ts +3 -2
- package/dist/adapters/http/client/types.d.ts.map +1 -1
- package/dist/adapters/http/server/transport.d.ts +2 -2
- package/dist/adapters/http/server/transport.d.ts.map +1 -1
- package/dist/adapters/http/server/transport.js +53 -20
- package/dist/adapters/http/server/transport.js.map +1 -1
- package/dist/adapters/http/shared/headers.d.ts +1 -1
- package/dist/adapters/http/shared/headers.d.ts.map +1 -1
- package/dist/adapters/http/shared/headers.js +7 -6
- package/dist/adapters/http/shared/headers.js.map +1 -1
- package/dist/adapters/local/client/transport.d.ts +3 -0
- package/dist/adapters/local/client/transport.d.ts.map +1 -1
- package/dist/adapters/local/client/transport.js +63 -4
- package/dist/adapters/local/client/transport.js.map +1 -1
- package/dist/adapters/local/client/types.d.ts +15 -0
- package/dist/adapters/local/client/types.d.ts.map +1 -1
- package/dist/adapters/local/client/types.js.map +1 -1
- package/dist/adapters/mock/client/transport.d.ts +3 -2
- package/dist/adapters/mock/client/transport.d.ts.map +1 -1
- package/dist/adapters/mock/client/transport.js +8 -5
- package/dist/adapters/mock/client/transport.js.map +1 -1
- package/dist/adapters/mock/client/types.d.ts +7 -6
- package/dist/adapters/mock/client/types.d.ts.map +1 -1
- package/dist/adapters/port/transport.d.ts +62 -0
- package/dist/adapters/port/transport.d.ts.map +1 -0
- package/dist/adapters/port/transport.js +94 -0
- package/dist/adapters/port/transport.js.map +1 -0
- package/dist/adapters/websocket/client/index.d.ts +1 -1
- package/dist/adapters/websocket/client/index.d.ts.map +1 -1
- package/dist/adapters/websocket/client/index.js.map +1 -1
- package/dist/adapters/websocket/client/transport.d.ts +47 -7
- package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/client/transport.js +213 -67
- package/dist/adapters/websocket/client/transport.js.map +1 -1
- package/dist/adapters/websocket/client/types.d.ts +70 -21
- package/dist/adapters/websocket/client/types.d.ts.map +1 -1
- package/dist/adapters/websocket/client/types.js.map +1 -1
- package/dist/adapters/websocket/server/transport.d.ts +65 -24
- package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/server/transport.js +192 -263
- package/dist/adapters/websocket/server/transport.js.map +1 -1
- package/dist/adapters/websocket/server/types.d.ts +20 -1
- package/dist/adapters/websocket/server/types.d.ts.map +1 -1
- package/dist/adapters/websocket/shared/rpc.d.ts +129 -0
- package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -0
- package/dist/adapters/websocket/shared/rpc.js +425 -0
- package/dist/adapters/websocket/shared/rpc.js.map +1 -0
- package/dist/browser.d.ts +29 -7
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +26 -3
- package/dist/browser.js.map +1 -1
- package/dist/client/call-types.d.ts +10 -10
- package/dist/client/call-types.d.ts.map +1 -1
- package/dist/client/call-types.js +1 -6
- package/dist/client/call-types.js.map +1 -1
- package/dist/client/client.d.ts +6 -1
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +23 -9
- package/dist/client/client.js.map +1 -1
- package/dist/client/deadline.d.ts +38 -0
- package/dist/client/deadline.d.ts.map +1 -0
- package/dist/client/deadline.js +60 -0
- package/dist/client/deadline.js.map +1 -0
- package/dist/client/errors/factory.d.ts.map +1 -1
- package/dist/client/errors/factory.js +11 -7
- package/dist/client/errors/factory.js.map +1 -1
- package/dist/client/index.d.ts +9 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +8 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/middleware/cache.d.ts +19 -5
- package/dist/client/middleware/cache.d.ts.map +1 -1
- package/dist/client/middleware/cache.js +106 -18
- package/dist/client/middleware/cache.js.map +1 -1
- package/dist/client/middleware/circuit-breaker.d.ts +28 -10
- package/dist/client/middleware/circuit-breaker.d.ts.map +1 -1
- package/dist/client/middleware/circuit-breaker.js +35 -24
- package/dist/client/middleware/circuit-breaker.js.map +1 -1
- package/dist/client/middleware/contexts.d.ts +16 -10
- package/dist/client/middleware/contexts.d.ts.map +1 -1
- package/dist/client/middleware/contexts.js.map +1 -1
- package/dist/client/middleware/items.d.ts +31 -2
- package/dist/client/middleware/items.d.ts.map +1 -1
- package/dist/client/middleware/items.js +86 -6
- package/dist/client/middleware/items.js.map +1 -1
- package/dist/client/middleware/lru.d.ts +2 -0
- package/dist/client/middleware/lru.d.ts.map +1 -1
- package/dist/client/middleware/lru.js +11 -0
- package/dist/client/middleware/lru.js.map +1 -1
- package/dist/client/middleware/rate-limit.d.ts +7 -6
- package/dist/client/middleware/rate-limit.d.ts.map +1 -1
- package/dist/client/middleware/rate-limit.js +9 -6
- package/dist/client/middleware/rate-limit.js.map +1 -1
- package/dist/client/middleware/resilience.d.ts +61 -0
- package/dist/client/middleware/resilience.d.ts.map +1 -0
- package/dist/client/middleware/resilience.js +59 -0
- package/dist/client/middleware/resilience.js.map +1 -0
- package/dist/client/middleware/retry.d.ts +45 -11
- package/dist/client/middleware/retry.d.ts.map +1 -1
- package/dist/client/middleware/retry.js +102 -22
- package/dist/client/middleware/retry.js.map +1 -1
- package/dist/client/middleware/timeout.d.ts +15 -12
- package/dist/client/middleware/timeout.d.ts.map +1 -1
- package/dist/client/middleware/timeout.js +46 -37
- package/dist/client/middleware/timeout.js.map +1 -1
- package/dist/client/middleware/tracing.d.ts.map +1 -1
- package/dist/client/middleware/tracing.js +11 -3
- package/dist/client/middleware/tracing.js.map +1 -1
- package/dist/client/types.d.ts +9 -1
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/types.js.map +1 -1
- package/dist/clock.d.ts +19 -0
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +18 -0
- package/dist/clock.js.map +1 -0
- package/dist/duration.d.ts +37 -0
- package/dist/duration.d.ts.map +1 -0
- package/dist/duration.js +44 -0
- package/dist/duration.js.map +1 -0
- package/dist/machines.d.ts +90 -0
- package/dist/machines.d.ts.map +1 -0
- package/dist/machines.js +127 -0
- package/dist/machines.js.map +1 -0
- package/dist/procedures/capability.d.ts +3 -6
- package/dist/procedures/capability.d.ts.map +1 -1
- package/dist/procedures/capability.js +11 -9
- package/dist/procedures/capability.js.map +1 -1
- package/dist/procedures/collection/index.d.ts +3 -3
- 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 +49 -1
- package/dist/procedures/collection/procedures.js.map +1 -1
- package/dist/procedures/collection/schemas.d.ts +33 -0
- package/dist/procedures/collection/schemas.d.ts.map +1 -1
- package/dist/procedures/collection/schemas.js +68 -0
- package/dist/procedures/collection/schemas.js.map +1 -1
- package/dist/procedures/collection/storage.d.ts +37 -0
- package/dist/procedures/collection/storage.d.ts.map +1 -1
- package/dist/procedures/core/array.d.ts.map +1 -1
- package/dist/procedures/core/array.js +26 -15
- package/dist/procedures/core/array.js.map +1 -1
- package/dist/procedures/core/index.d.ts.map +1 -1
- package/dist/procedures/core/index.js +8 -0
- package/dist/procedures/core/index.js.map +1 -1
- package/dist/procedures/core/meta.d.ts.map +1 -1
- package/dist/procedures/core/meta.js +2 -1
- package/dist/procedures/core/meta.js.map +1 -1
- package/dist/procedures/core/object.d.ts.map +1 -1
- package/dist/procedures/core/object.js +42 -13
- package/dist/procedures/core/object.js.map +1 -1
- package/dist/procedures/core/operands.d.ts.map +1 -1
- package/dist/procedures/core/operands.js +6 -1
- package/dist/procedures/core/operands.js.map +1 -1
- package/dist/procedures/core/string.d.ts.map +1 -1
- package/dist/procedures/core/string.js +4 -2
- package/dist/procedures/core/string.js.map +1 -1
- package/dist/procedures/define-procedure.d.ts +19 -7
- package/dist/procedures/define-procedure.d.ts.map +1 -1
- package/dist/procedures/define-procedure.js +66 -21
- package/dist/procedures/define-procedure.js.map +1 -1
- package/dist/procedures/effects.d.ts +16 -0
- package/dist/procedures/effects.d.ts.map +1 -1
- package/dist/procedures/effects.js +33 -0
- package/dist/procedures/effects.js.map +1 -1
- package/dist/procedures/idempotency.d.ts +73 -0
- package/dist/procedures/idempotency.d.ts.map +1 -0
- package/dist/procedures/idempotency.js +131 -0
- package/dist/procedures/idempotency.js.map +1 -0
- package/dist/procedures/index.d.ts +7 -3
- package/dist/procedures/index.d.ts.map +1 -1
- package/dist/procedures/index.js +6 -2
- package/dist/procedures/index.js.map +1 -1
- package/dist/procedures/interceptors.d.ts +17 -4
- package/dist/procedures/interceptors.d.ts.map +1 -1
- package/dist/procedures/interceptors.js +18 -7
- package/dist/procedures/interceptors.js.map +1 -1
- package/dist/procedures/invoke.d.ts +15 -3
- package/dist/procedures/invoke.d.ts.map +1 -1
- package/dist/procedures/invoke.js +8 -3
- package/dist/procedures/invoke.js.map +1 -1
- package/dist/procedures/manifest.d.ts +83 -0
- package/dist/procedures/manifest.d.ts.map +1 -0
- package/dist/procedures/manifest.js +90 -0
- package/dist/procedures/manifest.js.map +1 -0
- package/dist/procedures/runtime-programs.d.ts +19 -0
- package/dist/procedures/runtime-programs.d.ts.map +1 -0
- package/dist/procedures/runtime-programs.js +24 -0
- package/dist/procedures/runtime-programs.js.map +1 -0
- package/dist/procedures/types.d.ts +20 -1
- package/dist/procedures/types.d.ts.map +1 -1
- package/dist/procedures/types.js.map +1 -1
- package/dist/protocol.d.ts +40 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +58 -0
- package/dist/protocol.js.map +1 -0
- package/dist/server/host-policy.d.ts +4 -3
- package/dist/server/host-policy.d.ts.map +1 -1
- package/dist/server/host-policy.js +6 -3
- package/dist/server/host-policy.js.map +1 -1
- package/dist/server/host-procedures.d.ts +31 -0
- package/dist/server/host-procedures.d.ts.map +1 -0
- package/dist/server/host-procedures.js +75 -0
- package/dist/server/host-procedures.js.map +1 -0
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/procedure-server.d.ts +110 -4
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +269 -17
- package/dist/server/procedure-server.js.map +1 -1
- package/dist/server/request-auth.d.ts +19 -0
- package/dist/server/request-auth.d.ts.map +1 -0
- package/dist/server/request-auth.js +32 -0
- package/dist/server/request-auth.js.map +1 -0
- package/dist/server/types.d.ts +32 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js.map +1 -1
- package/dist/timers.d.ts +60 -0
- package/dist/timers.d.ts.map +1 -0
- package/dist/timers.js +33 -0
- package/dist/timers.js.map +1 -0
- package/dist/tracing/traceparent.d.ts +26 -0
- package/dist/tracing/traceparent.d.ts.map +1 -0
- package/dist/tracing/traceparent.js +53 -0
- package/dist/tracing/traceparent.js.map +1 -0
- package/package.json +7 -1
- package/src/adapters/http/client/transport.ts +31 -3
- package/src/adapters/http/client/types.ts +3 -2
- package/src/adapters/http/server/transport.ts +60 -24
- package/src/adapters/http/shared/headers.ts +7 -6
- package/src/adapters/local/client/transport.ts +70 -5
- package/src/adapters/local/client/types.ts +18 -0
- package/src/adapters/mock/client/transport.ts +9 -6
- package/src/adapters/mock/client/types.ts +7 -6
- package/src/adapters/port/transport.ts +124 -0
- package/src/adapters/websocket/client/index.ts +1 -0
- package/src/adapters/websocket/client/transport.ts +232 -73
- package/src/adapters/websocket/client/types.ts +80 -21
- package/src/adapters/websocket/server/transport.ts +199 -301
- package/src/adapters/websocket/server/types.ts +20 -1
- package/src/adapters/websocket/shared/rpc.ts +453 -0
- package/src/browser.ts +51 -3
- package/src/client/call-types.ts +10 -14
- package/src/client/client.ts +28 -9
- package/src/client/deadline.ts +64 -0
- package/src/client/errors/factory.ts +12 -8
- package/src/client/index.ts +21 -3
- package/src/client/middleware/cache.ts +120 -23
- package/src/client/middleware/circuit-breaker.ts +65 -35
- package/src/client/middleware/contexts.ts +17 -10
- package/src/client/middleware/items.ts +101 -6
- package/src/client/middleware/lru.ts +12 -0
- package/src/client/middleware/rate-limit.ts +9 -7
- package/src/client/middleware/resilience.ts +81 -0
- package/src/client/middleware/retry.ts +146 -34
- package/src/client/middleware/timeout.ts +55 -46
- package/src/client/middleware/tracing.ts +13 -4
- package/src/client/types.ts +9 -1
- package/src/clock.ts +21 -0
- package/src/duration.ts +56 -0
- package/src/machines.ts +186 -0
- package/src/procedures/capability.ts +11 -9
- package/src/procedures/collection/index.ts +9 -1
- package/src/procedures/collection/procedures.ts +57 -0
- package/src/procedures/collection/schemas.ts +110 -0
- package/src/procedures/collection/storage.ts +38 -0
- package/src/procedures/core/array.ts +27 -16
- package/src/procedures/core/index.ts +8 -0
- package/src/procedures/core/meta.ts +2 -1
- package/src/procedures/core/object.ts +48 -14
- package/src/procedures/core/operands.ts +7 -0
- package/src/procedures/core/string.ts +4 -2
- package/src/procedures/define-procedure.ts +82 -24
- package/src/procedures/effects.ts +35 -0
- package/src/procedures/idempotency.ts +152 -0
- package/src/procedures/index.ts +18 -2
- package/src/procedures/interceptors.ts +28 -11
- package/src/procedures/invoke.ts +19 -5
- package/src/procedures/manifest.ts +158 -0
- package/src/procedures/runtime-programs.ts +27 -0
- package/src/procedures/types.ts +20 -1
- package/src/protocol.ts +97 -0
- package/src/server/host-policy.ts +8 -5
- package/src/server/host-procedures.ts +102 -0
- package/src/server/index.ts +5 -2
- package/src/server/procedure-server.ts +324 -10
- package/src/server/request-auth.ts +35 -0
- package/src/server/types.ts +37 -0
- package/src/timers.ts +82 -0
- package/src/tracing/traceparent.ts +61 -0
package/src/client/client.ts
CHANGED
|
@@ -56,7 +56,8 @@ import { RouteResolver, type ResolvedRoute, type RouteResolutionResult } from ".
|
|
|
56
56
|
import { BatchExecutor, type ExecutionContext } from "./batch-executor.js";
|
|
57
57
|
import type { ProcedureRegistry } from "../procedures/registry.js";
|
|
58
58
|
import type { EventBus } from "../events/types.js";
|
|
59
|
-
import {
|
|
59
|
+
import { narrowCapability } from "../procedures/capability.js";
|
|
60
|
+
import { TIMEOUT_METADATA_KEY, absorbTimeout, writeRemainingTime } from "./deadline.js";
|
|
60
61
|
import type { Capability, ClearanceOrder, NestedCallOptions } from "../procedures/types.js";
|
|
61
62
|
import { PROCEDURE_REGISTRY } from "../procedures/registry.js";
|
|
62
63
|
import { validateSchema } from "../procedures/schema.js";
|
|
@@ -147,6 +148,11 @@ interface CallerContext {
|
|
|
147
148
|
|
|
148
149
|
/** The options of `exec()` and `execStream()`. */
|
|
149
150
|
export interface ExecOptions {
|
|
151
|
+
/**
|
|
152
|
+
* The signal of the program: an abort stops the call that runs, and the calls that did not
|
|
153
|
+
* start do not start (roadmap R14). Each nested call gets it.
|
|
154
|
+
*/
|
|
155
|
+
signal?: AbortSignal | undefined;
|
|
150
156
|
/**
|
|
151
157
|
* This function gets the hydration events of this program only: the refs that hydration runs,
|
|
152
158
|
* also inside control flow and runners, with the call that gets each result. A trace uses it.
|
|
@@ -155,6 +161,14 @@ export interface ExecOptions {
|
|
|
155
161
|
onRef?: ((event: HydrationEvent) => void) | undefined;
|
|
156
162
|
}
|
|
157
163
|
|
|
164
|
+
/** The caller context of `exec()` and `execStream()`: the observer and the signal. */
|
|
165
|
+
function callerOf(options: ExecOptions): CallerContext {
|
|
166
|
+
const caller: CallerContext = {};
|
|
167
|
+
if (options.onRef) caller.onRef = options.onRef;
|
|
168
|
+
if (options.signal) caller.signal = options.signal;
|
|
169
|
+
return caller;
|
|
170
|
+
}
|
|
171
|
+
|
|
158
172
|
/**
|
|
159
173
|
* Generate a unique message ID.
|
|
160
174
|
*/
|
|
@@ -357,7 +371,7 @@ export class Client<TContext = {}> {
|
|
|
357
371
|
* ```typescript
|
|
358
372
|
* // Type accumulation example
|
|
359
373
|
* const client = new Client(transport)
|
|
360
|
-
* .use(
|
|
374
|
+
* .use(createOverallTimeoutMiddleware({ overall: 5000 })) // Client<TimeoutContext>
|
|
361
375
|
* .use(createRetryMiddleware()) // Client<TimeoutContext & RetryContext>
|
|
362
376
|
* .use(createCacheMiddleware({ methods: ["users.get"] })); // Client<... & CacheContext>
|
|
363
377
|
*
|
|
@@ -478,7 +492,7 @@ export class Client<TContext = {}> {
|
|
|
478
492
|
input?: unknown,
|
|
479
493
|
options: ExecOptions = {}
|
|
480
494
|
): Promise<TOutput> {
|
|
481
|
-
const caller: CallerContext = options
|
|
495
|
+
const caller: CallerContext = callerOf(options);
|
|
482
496
|
const { path, input: hydratedInput } = await this.prepareRef(refOrPath, input, caller);
|
|
483
497
|
return this.execInternal<TOutput>(path, hydratedInput, caller);
|
|
484
498
|
}
|
|
@@ -501,7 +515,7 @@ export class Client<TContext = {}> {
|
|
|
501
515
|
options: ExecOptions = {}
|
|
502
516
|
): AsyncIterable<TOutput> {
|
|
503
517
|
const self = this;
|
|
504
|
-
const caller: CallerContext = options
|
|
518
|
+
const caller: CallerContext = callerOf(options);
|
|
505
519
|
return (async function* () {
|
|
506
520
|
const { path, input: hydratedInput } = await self.prepareRef(refOrPath, input, caller);
|
|
507
521
|
yield* self.execStreamInternal<TOutput>(path, hydratedInput, caller);
|
|
@@ -744,6 +758,10 @@ export class Client<TContext = {}> {
|
|
|
744
758
|
(message.metadata as any).__schema = opts.schema;
|
|
745
759
|
}
|
|
746
760
|
|
|
761
|
+
// The remaining time of the caller becomes the deadline of the call: the client sends the
|
|
762
|
+
// remaining time again for each try (roadmap R10)
|
|
763
|
+
absorbTimeout(message.metadata);
|
|
764
|
+
|
|
747
765
|
// Compose middleware chain
|
|
748
766
|
const runner = this.composeMiddleware<TReq, TRes>();
|
|
749
767
|
|
|
@@ -771,6 +789,9 @@ export class Client<TContext = {}> {
|
|
|
771
789
|
// Innermost function: call transport
|
|
772
790
|
const self = this;
|
|
773
791
|
const transportRunner: ClientRunner<TReq, TRes> = async function* (context: ClientContext<TReq>) {
|
|
792
|
+
// The remaining time of the deadline goes with each try, so the server stops the handler
|
|
793
|
+
// when this client stops waiting (roadmap R10)
|
|
794
|
+
writeRemainingTime(context.message.metadata);
|
|
774
795
|
yield* self.transport.send<TReq, TRes>(context.message);
|
|
775
796
|
};
|
|
776
797
|
|
|
@@ -1163,11 +1184,9 @@ export class Client<TContext = {}> {
|
|
|
1163
1184
|
yield* outputItems(await invokeProcedure(procedure, input, options));
|
|
1164
1185
|
return;
|
|
1165
1186
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
}
|
|
1170
|
-
yield* self.stream(self.pathToMethod(path), input, callOptions);
|
|
1187
|
+
// The deadline of the capability goes with the call, as for exec() (roadmap R14: before, a
|
|
1188
|
+
// remote leaf of route() sent the signal, not the deadline)
|
|
1189
|
+
yield* self.stream(self.pathToMethod(path), input, self.remoteCallOptions({ metadata: context.metadata, signal: context.signal }));
|
|
1171
1190
|
})();
|
|
1172
1191
|
}
|
|
1173
1192
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One deadline for a call (roadmap R10).
|
|
3
|
+
*
|
|
4
|
+
* A call has one absolute deadline in its metadata (`__deadline`, a time of `Date.now()`). The
|
|
5
|
+
* key starts with `__`, so it stays in this process. Each part of the client can only make it
|
|
6
|
+
* earlier: the remaining time that the caller sends, the overall timeout and the timeout of each
|
|
7
|
+
* try. Just before the transport sends a try, the client writes the remaining time into
|
|
8
|
+
* `procedure-timeout`. The server uses it to make its own deadline earlier, so a handler stops
|
|
9
|
+
* when its caller stops waiting.
|
|
10
|
+
*
|
|
11
|
+
* Before, the overall timeout and the timeout of each try did not reach the server, and a
|
|
12
|
+
* client could also send `X-Timeout`, which the server ignored.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Metadata } from "./types.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The metadata key of the remaining time of a remote call, in milliseconds. A server uses it
|
|
19
|
+
* only to make its deadline earlier.
|
|
20
|
+
*/
|
|
21
|
+
export const TIMEOUT_METADATA_KEY = "procedure-timeout";
|
|
22
|
+
|
|
23
|
+
/** The metadata key of the absolute deadline of a call. It stays in this process. */
|
|
24
|
+
export const DEADLINE_METADATA_KEY = "__deadline";
|
|
25
|
+
|
|
26
|
+
/** The deadline of a call, or undefined. */
|
|
27
|
+
export function deadlineOf(metadata: Metadata): number | undefined {
|
|
28
|
+
const deadline = metadata[DEADLINE_METADATA_KEY];
|
|
29
|
+
return typeof deadline === "number" && Number.isFinite(deadline) ? deadline : undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* This function makes the deadline of a call earlier: the earlier of its deadline and `at`. It
|
|
34
|
+
* gives the deadline from before, to put back with `restoreDeadline`.
|
|
35
|
+
*/
|
|
36
|
+
export function narrowDeadline(metadata: Metadata, at: number): number | undefined {
|
|
37
|
+
const before = deadlineOf(metadata);
|
|
38
|
+
metadata[DEADLINE_METADATA_KEY] = before === undefined ? at : Math.min(before, at);
|
|
39
|
+
return before;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** This function puts back the deadline from before a `narrowDeadline`. */
|
|
43
|
+
export function restoreDeadline(metadata: Metadata, before: number | undefined): void {
|
|
44
|
+
if (before === undefined) delete metadata[DEADLINE_METADATA_KEY];
|
|
45
|
+
else metadata[DEADLINE_METADATA_KEY] = before;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The remaining time that the caller gives in `procedure-timeout` becomes the deadline of the
|
|
50
|
+
* call, when the call starts. The client then writes the remaining time again for each try.
|
|
51
|
+
*/
|
|
52
|
+
export function absorbTimeout(metadata: Metadata, now: number = Date.now()): void {
|
|
53
|
+
const timeoutMs = metadata[TIMEOUT_METADATA_KEY];
|
|
54
|
+
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs < 0) return;
|
|
55
|
+
delete metadata[TIMEOUT_METADATA_KEY];
|
|
56
|
+
narrowDeadline(metadata, now + timeoutMs);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** This function writes the remaining time of the deadline into `procedure-timeout`, for one try. */
|
|
60
|
+
export function writeRemainingTime(metadata: Metadata, now: number = Date.now()): void {
|
|
61
|
+
const deadline = deadlineOf(metadata);
|
|
62
|
+
if (deadline === undefined) return;
|
|
63
|
+
metadata[TIMEOUT_METADATA_KEY] = Math.max(0, Math.floor(deadline - now));
|
|
64
|
+
}
|
|
@@ -94,8 +94,10 @@ export type ErrorPattern = {
|
|
|
94
94
|
const ERROR_PATTERNS : ErrorPattern[] = [
|
|
95
95
|
{
|
|
96
96
|
code: ErrorCode.ABORTED,
|
|
97
|
+
// The reason of AbortSignal.timeout() is a TimeoutError with the message "The operation was
|
|
98
|
+
// aborted due to timeout": it is a timeout (roadmap R30: before, it was ABORTED)
|
|
97
99
|
detect: (name: string, message: string) =>
|
|
98
|
-
name === "aborterror" || message.includes("abort"),
|
|
100
|
+
name === "aborterror" || (name !== "timeouterror" && message.includes("abort")),
|
|
99
101
|
},
|
|
100
102
|
{
|
|
101
103
|
code: ErrorCode.TIMEOUT,
|
|
@@ -124,13 +126,8 @@ const ERROR_PATTERNS : ErrorPattern[] = [
|
|
|
124
126
|
message.includes("network") ||
|
|
125
127
|
message.includes("connection"),
|
|
126
128
|
},
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
detect: (_name: string, message: string) =>
|
|
130
|
-
message.includes("parse") ||
|
|
131
|
-
message.includes("json") ||
|
|
132
|
-
message.includes("unexpected token"),
|
|
133
|
-
},
|
|
129
|
+
// Before the parse errors: "Converting circular structure to JSON" names JSON too (roadmap R30:
|
|
130
|
+
// before, it was PARSE_ERROR)
|
|
134
131
|
{
|
|
135
132
|
code: ErrorCode.SERIALIZE_ERROR,
|
|
136
133
|
detect: (_name: string, message: string) =>
|
|
@@ -138,6 +135,13 @@ const ERROR_PATTERNS : ErrorPattern[] = [
|
|
|
138
135
|
message.includes("stringify") ||
|
|
139
136
|
message.includes("circular"),
|
|
140
137
|
},
|
|
138
|
+
{
|
|
139
|
+
code: ErrorCode.PARSE_ERROR,
|
|
140
|
+
detect: (_name: string, message: string) =>
|
|
141
|
+
message.includes("parse") ||
|
|
142
|
+
message.includes("json") ||
|
|
143
|
+
message.includes("unexpected token"),
|
|
144
|
+
},
|
|
141
145
|
] as const satisfies ErrorPattern[];
|
|
142
146
|
|
|
143
147
|
/**
|
package/src/client/index.ts
CHANGED
|
@@ -29,7 +29,7 @@ export { LocalTransport } from "../adapters/local/client/index.js";
|
|
|
29
29
|
export type { LocalTransportOptions, Handler } from "../adapters/local/client/index.js";
|
|
30
30
|
|
|
31
31
|
export { WebSocketTransport } from "../adapters/websocket/client/index.js";
|
|
32
|
-
export type { WebSocketTransportOptions } from "../adapters/websocket/client/index.js";
|
|
32
|
+
export type { WebSocketTransportOptions, WebSocketClass } from "../adapters/websocket/client/index.js";
|
|
33
33
|
// WebSocketState is a runtime enum — export as a value (not type-only) so consumers can compare
|
|
34
34
|
// transport.getState() === WebSocketState.CONNECTED. See documentation/BUGS-2026-07.md (L1).
|
|
35
35
|
export { WebSocketState } from "../adapters/websocket/client/index.js";
|
|
@@ -43,10 +43,13 @@ export type {
|
|
|
43
43
|
} from "../adapters/mock/client//index.js";
|
|
44
44
|
|
|
45
45
|
// Middleware
|
|
46
|
-
export { createRetryMiddleware } from "./middleware/retry.js"
|
|
46
|
+
export { createRetryMiddleware } from "./middleware/retry.js";
|
|
47
|
+
// The delivery of a failed try: it did not leave, or its outcome is indeterminate (roadmap R1)
|
|
48
|
+
export { DELIVERY_METADATA_KEY, deliveryOf, isNotSentError } from "./middleware/items.js";
|
|
49
|
+
export type { Delivery } from "./middleware/items.js";
|
|
47
50
|
export type { RetryOptions } from "./middleware/retry.js";
|
|
48
51
|
|
|
49
|
-
export { createCacheMiddleware } from "./middleware/cache.js";
|
|
52
|
+
export { createCacheMiddleware, invalidateCache } from "./middleware/cache.js";
|
|
50
53
|
export type { CacheOptions, CacheStats } from "./middleware/cache.js";
|
|
51
54
|
|
|
52
55
|
export {
|
|
@@ -55,6 +58,21 @@ export {
|
|
|
55
58
|
createCombinedTimeoutMiddleware,
|
|
56
59
|
} from "./middleware/timeout.js";
|
|
57
60
|
export type { TimeoutOptions } from "./middleware/timeout.js";
|
|
61
|
+
// Typed durations (roadmap R11)
|
|
62
|
+
export { ms, seconds, minutes, hours, toMs, isDuration } from "../duration.js";
|
|
63
|
+
export type { Duration } from "../duration.js";
|
|
64
|
+
// The resilience preset and the deadline of a call (roadmap R10)
|
|
65
|
+
export { resilience } from "./middleware/resilience.js";
|
|
66
|
+
export type { ResilienceOptions, ResilienceContext } from "./middleware/resilience.js";
|
|
67
|
+
export {
|
|
68
|
+
TIMEOUT_METADATA_KEY,
|
|
69
|
+
DEADLINE_METADATA_KEY,
|
|
70
|
+
deadlineOf,
|
|
71
|
+
narrowDeadline,
|
|
72
|
+
restoreDeadline,
|
|
73
|
+
absorbTimeout,
|
|
74
|
+
writeRemainingTime,
|
|
75
|
+
} from "./deadline.js";
|
|
58
76
|
|
|
59
77
|
export { createPaginationMiddleware, paginateAll } from "./middleware/pagination.js";
|
|
60
78
|
export type { PaginationOptions } from "./middleware/pagination.js";
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
import type { ClientRunner, ClientContext, ResponseItem, Metadata, Method, TypedClientMiddleware } from "../types.js";
|
|
9
9
|
import type { CacheContext } from "./contexts.js";
|
|
10
10
|
import { LruMap } from "./lru.js";
|
|
11
|
+
import { isDuration, optionalMs, type Duration } from "../../duration.js";
|
|
12
|
+
import type { Effects } from "../../procedures/types.js";
|
|
13
|
+
import { TIMER_DEFAULTS } from "../../timers.js";
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* Cache middleware options.
|
|
@@ -21,10 +24,10 @@ export interface CacheOptions {
|
|
|
21
24
|
methods: readonly string[] | ((method: Method) => boolean);
|
|
22
25
|
|
|
23
26
|
/**
|
|
24
|
-
* Time-to-live for cache entries
|
|
27
|
+
* Time-to-live for cache entries, for example `minutes(1)`. A call can give its own: `cache: { ttl }`.
|
|
25
28
|
* @default 60000 (1 minute)
|
|
26
29
|
*/
|
|
27
|
-
ttl?:
|
|
30
|
+
ttl?: Duration;
|
|
28
31
|
|
|
29
32
|
/**
|
|
30
33
|
* Maximum number of cache entries (LRU eviction when exceeded).
|
|
@@ -39,11 +42,11 @@ export interface CacheOptions {
|
|
|
39
42
|
onStats?: (stats: CacheStats) => void;
|
|
40
43
|
|
|
41
44
|
/**
|
|
42
|
-
* Interval
|
|
45
|
+
* Interval for emitting cache statistics.
|
|
43
46
|
* Only used if onStats is provided.
|
|
44
47
|
* @default 60000 (1 minute)
|
|
45
48
|
*/
|
|
46
|
-
statsInterval?:
|
|
49
|
+
statsInterval?: Duration;
|
|
47
50
|
|
|
48
51
|
/**
|
|
49
52
|
* Custom cache key generator.
|
|
@@ -64,6 +67,14 @@ export interface CacheOptions {
|
|
|
64
67
|
* @returns true if response should be cached
|
|
65
68
|
*/
|
|
66
69
|
shouldCache?: (item: ResponseItem<unknown>) => boolean;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The effects of each method (roadmap R13), for example `effectsIn(registry)`. A call that
|
|
73
|
+
* writes resources deletes the cached entries of the methods that read one of them, when it
|
|
74
|
+
* succeeds. An entry of a method with unknown reads goes too. Without this option, a write
|
|
75
|
+
* deletes nothing, and the entries live until their time to live ends.
|
|
76
|
+
*/
|
|
77
|
+
effects?: (method: Method) => Effects | undefined;
|
|
67
78
|
}
|
|
68
79
|
|
|
69
80
|
/**
|
|
@@ -110,10 +121,21 @@ export function stableStringify(value: unknown): string {
|
|
|
110
121
|
}
|
|
111
122
|
|
|
112
123
|
/**
|
|
113
|
-
* The metadata keys that do not change a response: middleware state and
|
|
114
|
-
*
|
|
124
|
+
* The metadata keys that do not change a response: middleware state, tracing, and the keys of
|
|
125
|
+
* one call (its idempotency key and the remaining time of its deadline). The other keys (auth,
|
|
126
|
+
* pagination, custom fields) are part of the cache key.
|
|
115
127
|
*/
|
|
116
|
-
const VOLATILE_METADATA = new Set([
|
|
128
|
+
const VOLATILE_METADATA = new Set([
|
|
129
|
+
"retry",
|
|
130
|
+
"timeout",
|
|
131
|
+
"tracing",
|
|
132
|
+
"cache",
|
|
133
|
+
"circuitBreaker",
|
|
134
|
+
"rateLimit",
|
|
135
|
+
"idempotency-key",
|
|
136
|
+
"procedure-timeout",
|
|
137
|
+
"traceparent",
|
|
138
|
+
]);
|
|
117
139
|
|
|
118
140
|
/** The part of the metadata that can change a response. */
|
|
119
141
|
function keyMetadata(metadata: Metadata): Record<string, unknown> {
|
|
@@ -153,6 +175,21 @@ function copyItem<T>(item: ResponseItem<T>): ResponseItem<T> | undefined {
|
|
|
153
175
|
interface CacheEntry {
|
|
154
176
|
items: ResponseItem<unknown>[];
|
|
155
177
|
expiresAt: number;
|
|
178
|
+
/** The resources that the method of the entry reads, when the `effects` option knows them */
|
|
179
|
+
reads: readonly string[] | undefined;
|
|
180
|
+
/** The method of the entry, for the invalidation by method */
|
|
181
|
+
method: Method;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The deleters of each cache middleware, for `invalidateCache` */
|
|
185
|
+
const invalidators = new WeakMap<object, (matches?: (method: Method) => boolean) => number>();
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* This function deletes the entries of a cache middleware: all of them, or the entries of the
|
|
189
|
+
* methods that `matches` selects. It gives the number of deleted entries.
|
|
190
|
+
*/
|
|
191
|
+
export function invalidateCache(middleware: object, matches?: (method: Method) => boolean): number {
|
|
192
|
+
return invalidators.get(middleware)?.(matches) ?? 0;
|
|
156
193
|
}
|
|
157
194
|
|
|
158
195
|
/** The overrides of one call: `cache: { ttl, bypass, refresh }` in its context. */
|
|
@@ -160,7 +197,7 @@ function cacheOverrides(value: unknown): { ttl?: number; bypass?: boolean; refre
|
|
|
160
197
|
if (typeof value !== "object" || value === null) return {};
|
|
161
198
|
const { ttl, bypass, refresh } = value as Record<string, unknown>;
|
|
162
199
|
return {
|
|
163
|
-
...(
|
|
200
|
+
...(isDuration(ttl) && ttl.value >= 0 ? { ttl: optionalMs(ttl, "cache.ttl")! } : {}),
|
|
164
201
|
...(bypass === true ? { bypass: true } : {}),
|
|
165
202
|
...(refresh === true ? { refresh: true } : {}),
|
|
166
203
|
};
|
|
@@ -199,18 +236,18 @@ function defaultShouldCache(item: ResponseItem<unknown>): boolean {
|
|
|
199
236
|
* client.use(createCacheMiddleware({
|
|
200
237
|
* methods: ["users.get", "users.list"], // only these calls are cached
|
|
201
238
|
* capacity: 100,
|
|
202
|
-
* ttl:
|
|
239
|
+
* ttl: minutes(1),
|
|
203
240
|
* onStats: (stats) => console.log(`Hit rate: ${stats.hitRate}%`)
|
|
204
241
|
* }));
|
|
205
242
|
* ```
|
|
206
243
|
*/
|
|
207
244
|
export function createCacheMiddleware(options: CacheOptions): TypedClientMiddleware<CacheContext, {}> {
|
|
245
|
+
const ttl = optionalMs(options.ttl, "ttl") ?? TIMER_DEFAULTS.cache.ttlMs;
|
|
246
|
+
const statsInterval = optionalMs(options.statsInterval, "statsInterval") ?? TIMER_DEFAULTS.cache.statsIntervalMs;
|
|
208
247
|
const {
|
|
209
248
|
methods,
|
|
210
|
-
ttl = 60000,
|
|
211
249
|
capacity = 100,
|
|
212
250
|
onStats,
|
|
213
|
-
statsInterval = 60000,
|
|
214
251
|
keyGenerator = defaultKeyGenerator,
|
|
215
252
|
shouldCache = defaultShouldCache,
|
|
216
253
|
} = options;
|
|
@@ -253,11 +290,30 @@ export function createCacheMiddleware(options: CacheOptions): TypedClientMiddlew
|
|
|
253
290
|
: ((names: ReadonlySet<string>) => (method: Method): boolean => names.has(`${method.service}.${method.operation}`))(
|
|
254
291
|
new Set(methods),
|
|
255
292
|
);
|
|
293
|
+
const effectsOf = options.effects;
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The calls in flight, by key: a second call with the same key waits for the first one and
|
|
297
|
+
* gets a copy of its response (roadmap R13). Before, each one made its own request.
|
|
298
|
+
*/
|
|
299
|
+
const inFlight = new Map<string, Promise<CacheEntry | undefined>>();
|
|
256
300
|
|
|
257
|
-
|
|
301
|
+
/** A write that succeeded deletes the entries that read one of its resources (R13). */
|
|
302
|
+
const invalidateAfterWrite = (method: Method): void => {
|
|
303
|
+
const writes = effectsOf?.(method)?.writes;
|
|
304
|
+
if (!writes || writes.length === 0) return;
|
|
305
|
+
cache.deleteWhere((entry) => entry.reads === undefined || entry.reads.some((resource) => writes.includes(resource)));
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const middleware = <TReq, TRes>(next: ClientRunner<TReq, TRes>): ClientRunner<TReq, TRes> => {
|
|
258
309
|
return async function* (context: ClientContext<TReq>) {
|
|
259
310
|
if (!cacheable(context.message.method)) {
|
|
260
|
-
|
|
311
|
+
let failed = false;
|
|
312
|
+
for await (const item of next(context)) {
|
|
313
|
+
if (item.status.type === "error") failed = true;
|
|
314
|
+
yield item;
|
|
315
|
+
}
|
|
316
|
+
if (!failed && effectsOf) invalidateAfterWrite(context.message.method);
|
|
261
317
|
return;
|
|
262
318
|
}
|
|
263
319
|
|
|
@@ -294,23 +350,64 @@ export function createCacheMiddleware(options: CacheOptions): TypedClientMiddlew
|
|
|
294
350
|
cache.delete(cacheKey);
|
|
295
351
|
}
|
|
296
352
|
|
|
353
|
+
// The same call is in flight: wait for it and give a copy of its response (R13)
|
|
354
|
+
const pending = overrides.refresh ? undefined : inFlight.get(cacheKey);
|
|
355
|
+
if (pending) {
|
|
356
|
+
const shared = await pending;
|
|
357
|
+
if (shared) {
|
|
358
|
+
stats.hits++;
|
|
359
|
+
setState(true);
|
|
360
|
+
for (const item of shared.items) yield (copyItem(item) ?? item) as ResponseItem<TRes>;
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
// The first call gave a stream or an error: this call makes its own request
|
|
364
|
+
}
|
|
365
|
+
|
|
297
366
|
// Cache miss - fetch and store. Only a response of one item is cached: a stream is never
|
|
298
367
|
// collected or cached (before, the middleware kept every item of a stream in memory).
|
|
299
368
|
stats.misses++;
|
|
300
369
|
let first: ResponseItem<TRes> | undefined;
|
|
301
370
|
let count = 0;
|
|
371
|
+
let settle: (entry: CacheEntry | undefined) => void = () => undefined;
|
|
372
|
+
const mine = new Promise<CacheEntry | undefined>((resolve) => (settle = resolve));
|
|
373
|
+
if (!overrides.refresh) inFlight.set(cacheKey, mine);
|
|
374
|
+
let entry: CacheEntry | undefined;
|
|
375
|
+
try {
|
|
376
|
+
for await (const item of next(context)) {
|
|
377
|
+
count++;
|
|
378
|
+
if (count === 1) first = item;
|
|
379
|
+
yield item;
|
|
380
|
+
}
|
|
381
|
+
setState(false);
|
|
302
382
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
383
|
+
// An inner layer that changed the key metadata during the call (an auth middleware
|
|
384
|
+
// inside the cache) makes the key wrong: such a response is not stored (R13)
|
|
385
|
+
const keyAfter = keyGenerator(context.message.method, context.message.payload, metadata);
|
|
386
|
+
if (count === 1 && first && shouldCache(first) && keyAfter === cacheKey) {
|
|
387
|
+
const copy = copyItem(first);
|
|
388
|
+
if (copy) {
|
|
389
|
+
entry = {
|
|
390
|
+
items: [copy as ResponseItem<unknown>],
|
|
391
|
+
expiresAt: Date.now() + (overrides.ttl ?? ttl),
|
|
392
|
+
reads: effectsOf?.(context.message.method)?.reads,
|
|
393
|
+
method: context.message.method,
|
|
394
|
+
};
|
|
395
|
+
cache.set(cacheKey, entry);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
} finally {
|
|
399
|
+
if (inFlight.get(cacheKey) === mine) inFlight.delete(cacheKey);
|
|
400
|
+
settle(entry);
|
|
313
401
|
}
|
|
314
402
|
};
|
|
315
403
|
};
|
|
404
|
+
invalidators.set(middleware, (matches) => {
|
|
405
|
+
if (!matches) {
|
|
406
|
+
const size = cache.size;
|
|
407
|
+
cache.clear();
|
|
408
|
+
return size;
|
|
409
|
+
}
|
|
410
|
+
return cache.deleteWhere((entry) => matches(entry.method));
|
|
411
|
+
});
|
|
412
|
+
return middleware;
|
|
316
413
|
}
|