@procedures/client 0.2.3 → 0.4.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 +94 -0
- package/README.md +77 -44
- 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 +54 -22
- 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 +67 -6
- 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/node.d.ts +30 -0
- package/dist/adapters/websocket/client/node.d.ts.map +1 -0
- package/dist/adapters/websocket/client/node.js +32 -0
- package/dist/adapters/websocket/client/node.js.map +1 -0
- 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 +229 -75
- package/dist/adapters/websocket/client/transport.js.map +1 -1
- package/dist/adapters/websocket/client/types.d.ts +71 -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/errors/http-errors.d.ts.map +1 -1
- package/dist/client/errors/http-errors.js +6 -0
- package/dist/client/errors/http-errors.js.map +1 -1
- package/dist/client/errors/index.d.ts +1 -1
- package/dist/client/errors/index.js +1 -1
- package/dist/client/errors/registry.d.ts +2 -1
- package/dist/client/errors/registry.d.ts.map +1 -1
- package/dist/client/errors/registry.js +72 -22
- package/dist/client/errors/registry.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 -7
- 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 +48 -46
- 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/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- 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/node.d.ts +11 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +10 -0
- package/dist/node.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 +7 -8
- 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 +1 -1
- package/dist/procedures/core/array.d.ts.map +1 -1
- package/dist/procedures/core/array.js +81 -29
- 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 +17 -2
- package/dist/procedures/core/index.js.map +1 -1
- package/dist/procedures/core/logic.js +1 -1
- package/dist/procedures/core/logic.js.map +1 -1
- package/dist/procedures/core/math.d.ts.map +1 -1
- package/dist/procedures/core/math.js +5 -1
- package/dist/procedures/core/math.js.map +1 -1
- package/dist/procedures/core/meta.d.ts.map +1 -1
- package/dist/procedures/core/meta.js +43 -8
- 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/core/type.d.ts.map +1 -1
- package/dist/procedures/core/type.js +4 -2
- package/dist/procedures/core/type.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 +57 -17
- package/dist/procedures/define-procedure.js.map +1 -1
- package/dist/procedures/discovery.d.ts +4 -6
- package/dist/procedures/discovery.d.ts.map +1 -1
- package/dist/procedures/discovery.js +4 -6
- package/dist/procedures/discovery.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/ref.d.ts +4 -2
- package/dist/procedures/ref.d.ts.map +1 -1
- package/dist/procedures/ref.js +2 -1
- package/dist/procedures/ref.js.map +1 -1
- 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 +16 -0
- package/dist/procedures/types.d.ts.map +1 -1
- package/dist/procedures/types.js.map +1 -1
- package/dist/protocol.d.ts +47 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +68 -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 +112 -5
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +273 -19
- 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 +11 -4
- 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 +61 -26
- package/src/adapters/http/shared/headers.ts +7 -6
- package/src/adapters/local/client/transport.ts +74 -7
- 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/node.ts +34 -0
- package/src/adapters/websocket/client/transport.ts +251 -81
- package/src/adapters/websocket/client/types.ts +81 -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/README.md +2 -0
- package/src/client/errors/factory.ts +12 -8
- package/src/client/errors/http-errors.ts +5 -0
- package/src/client/errors/index.ts +1 -1
- package/src/client/errors/registry.ts +79 -22
- 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 -9
- package/src/client/middleware/resilience.ts +81 -0
- package/src/client/middleware/retry.ts +146 -34
- package/src/client/middleware/timeout.ts +57 -55
- 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/index.ts +4 -0
- package/src/machines.ts +186 -0
- package/src/node.ts +11 -0
- package/src/procedures/capability.ts +7 -8
- 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 +79 -31
- package/src/procedures/core/index.ts +17 -2
- package/src/procedures/core/logic.ts +1 -1
- package/src/procedures/core/math.ts +5 -1
- package/src/procedures/core/meta.ts +45 -8
- 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/core/type.ts +4 -2
- package/src/procedures/define-procedure.ts +73 -20
- package/src/procedures/discovery.ts +4 -6
- 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/ref.ts +4 -2
- package/src/procedures/runtime-programs.ts +27 -0
- package/src/procedures/types.ts +16 -0
- package/src/protocol.ts +108 -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 +328 -12
- 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/dist/procedures/cli/announce.d.ts +0 -67
- package/dist/procedures/cli/announce.d.ts.map +0 -1
- package/dist/procedures/cli/announce.js +0 -392
- package/dist/procedures/cli/announce.js.map +0 -1
- package/dist/procedures/cli/discover.d.ts +0 -66
- package/dist/procedures/cli/discover.d.ts.map +0 -1
- package/dist/procedures/cli/discover.js +0 -310
- package/dist/procedures/cli/discover.js.map +0 -1
- package/dist/procedures/cli/index.d.ts +0 -13
- package/dist/procedures/cli/index.d.ts.map +0 -1
- package/dist/procedures/cli/index.js +0 -203
- package/dist/procedures/cli/index.js.map +0 -1
- package/dist/procedures/cli/init.d.ts +0 -17
- package/dist/procedures/cli/init.d.ts.map +0 -1
- package/dist/procedures/cli/init.js +0 -336
- package/dist/procedures/cli/init.js.map +0 -1
- package/src/procedures/cli/announce.ts +0 -470
- package/src/procedures/cli/discover.ts +0 -390
- package/src/procedures/cli/index.ts +0 -231
- package/src/procedures/cli/init.ts +0 -370
|
@@ -63,4 +63,16 @@ export class LruMap<K, V> {
|
|
|
63
63
|
clear(): void {
|
|
64
64
|
this.entries.clear();
|
|
65
65
|
}
|
|
66
|
+
|
|
67
|
+
/** Delete each key whose value matches. This does not count as a use. The number deleted. */
|
|
68
|
+
deleteWhere(matches: (value: V, key: K) => boolean): number {
|
|
69
|
+
let deleted = 0;
|
|
70
|
+
for (const [key, value] of this.entries) {
|
|
71
|
+
if (matches(value, key)) {
|
|
72
|
+
this.entries.delete(key);
|
|
73
|
+
deleted++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return deleted;
|
|
77
|
+
}
|
|
66
78
|
}
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
import type { ClientMiddleware, ClientRunner, ClientContext, TypedClientMiddleware } from "../types.js";
|
|
11
11
|
import type { RateLimitContext } from "./contexts.js";
|
|
12
12
|
import { abortedItem } from "./items.js";
|
|
13
|
+
import { optionalMs, type Duration } from "../../duration.js";
|
|
14
|
+
import { TIMER_DEFAULTS } from "../../timers.js";
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* Rate limiting options.
|
|
@@ -25,7 +27,7 @@ export interface RateLimitOptions {
|
|
|
25
27
|
* Time window in milliseconds.
|
|
26
28
|
* @default 60000 (1 minute)
|
|
27
29
|
*/
|
|
28
|
-
window?:
|
|
30
|
+
window?: Duration;
|
|
29
31
|
|
|
30
32
|
/**
|
|
31
33
|
* Behavior when rate limit is exceeded.
|
|
@@ -81,7 +83,6 @@ export class RateLimitError extends Error {
|
|
|
81
83
|
*/
|
|
82
84
|
interface QueuedRequest {
|
|
83
85
|
resolve: () => void;
|
|
84
|
-
reject: (error: Error) => void;
|
|
85
86
|
timestamp: number;
|
|
86
87
|
}
|
|
87
88
|
|
|
@@ -96,14 +97,14 @@ interface QueuedRequest {
|
|
|
96
97
|
* // Reject strategy (default)
|
|
97
98
|
* client.use(createRateLimitMiddleware({
|
|
98
99
|
* maxRequests: 100, // 100 requests
|
|
99
|
-
* window:
|
|
100
|
+
* window: minutes(1),
|
|
100
101
|
* strategy: "reject" // Throw error when exceeded
|
|
101
102
|
* }));
|
|
102
103
|
*
|
|
103
104
|
* // Queue strategy
|
|
104
105
|
* client.use(createRateLimitMiddleware({
|
|
105
106
|
* maxRequests: 10,
|
|
106
|
-
* window:
|
|
107
|
+
* window: seconds(1),
|
|
107
108
|
* strategy: "queue", // Queue requests when exceeded
|
|
108
109
|
* maxQueueSize: 50
|
|
109
110
|
* }));
|
|
@@ -114,12 +115,12 @@ export function createRateLimitMiddleware(
|
|
|
114
115
|
): TypedClientMiddleware<RateLimitContext, {}> {
|
|
115
116
|
const {
|
|
116
117
|
maxRequests = 100,
|
|
117
|
-
window = 60000,
|
|
118
118
|
strategy = "reject",
|
|
119
119
|
maxQueueSize = 100,
|
|
120
120
|
message = "Rate limit exceeded",
|
|
121
121
|
onRateLimitExceeded,
|
|
122
122
|
} = options;
|
|
123
|
+
const window = optionalMs(options.window, "window") ?? TIMER_DEFAULTS.rateLimit.windowMs;
|
|
123
124
|
|
|
124
125
|
// Token bucket state
|
|
125
126
|
let tokens = maxRequests;
|
|
@@ -206,7 +207,6 @@ export function createRateLimitMiddleware(
|
|
|
206
207
|
signal?.removeEventListener("abort", onAbort);
|
|
207
208
|
resolve(true);
|
|
208
209
|
},
|
|
209
|
-
reject: () => resolve(false),
|
|
210
210
|
timestamp: Date.now(),
|
|
211
211
|
};
|
|
212
212
|
// A queued request whose caller aborts leaves the queue (before, it kept its place)
|
|
@@ -273,9 +273,9 @@ export function createRateLimitMiddleware(
|
|
|
273
273
|
* @example
|
|
274
274
|
* ```typescript
|
|
275
275
|
* client.use(createPerServiceRateLimiter({
|
|
276
|
-
* users: { maxRequests: 100, window:
|
|
277
|
-
* orders: { maxRequests: 50, window:
|
|
278
|
-
* default: { maxRequests: 200, window:
|
|
276
|
+
* users: { maxRequests: 100, window: minutes(1) },
|
|
277
|
+
* orders: { maxRequests: 50, window: minutes(1) },
|
|
278
|
+
* default: { maxRequests: 200, window: minutes(1) }
|
|
279
279
|
* }));
|
|
280
280
|
* ```
|
|
281
281
|
*/
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The resilience preset (roadmap R10): the timeouts, the cache, the retry, the circuit breaker and
|
|
3
|
+
* the rate limit of a client, in the one order that works.
|
|
4
|
+
*
|
|
5
|
+
* The first layer is the outermost one:
|
|
6
|
+
*
|
|
7
|
+
* 1. The overall timeout: it covers the whole call, retries included.
|
|
8
|
+
* 2. The cache: a hit makes no request, so no try and no slot of the rate limit.
|
|
9
|
+
* 3. The retry: it starts each try.
|
|
10
|
+
* 4. The circuit breaker: it counts each try, and an open breaker stops a retry loop.
|
|
11
|
+
* 5. The rate limit: each try takes a slot.
|
|
12
|
+
* 6. The timeout of one try: inside the retry, so the retry can start the next try.
|
|
13
|
+
*
|
|
14
|
+
* Each order pitfall of a hand-made stack is impossible here: an overall timeout inside the
|
|
15
|
+
* retry, a per-try timeout outside it, or a rate limit outside the retry. The deadline of the
|
|
16
|
+
* call goes to the server as `procedure-timeout` with each try (`client/deadline.ts`).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { ClientMiddleware, ClientRunner, TypedClientMiddleware } from "../types.js";
|
|
20
|
+
import type { CacheContext, CircuitBreakerContext, RateLimitContext, RetryContext, TimeoutContext } from "./contexts.js";
|
|
21
|
+
import { createCacheMiddleware, type CacheOptions } from "./cache.js";
|
|
22
|
+
import { createCircuitBreakerMiddleware, type CircuitBreakerOptions } from "./circuit-breaker.js";
|
|
23
|
+
import { createRateLimitMiddleware, type RateLimitOptions } from "./rate-limit.js";
|
|
24
|
+
import { createRetryMiddleware, type RetryOptions } from "./retry.js";
|
|
25
|
+
import { createOverallTimeoutMiddleware, createTimeoutMiddleware } from "./timeout.js";
|
|
26
|
+
import type { Duration } from "../../duration.js";
|
|
27
|
+
|
|
28
|
+
/** The parts of the preset. An absent part is not in the stack. */
|
|
29
|
+
export interface ResilienceOptions {
|
|
30
|
+
/** The time of the whole call, retries included. */
|
|
31
|
+
overall?: Duration | undefined;
|
|
32
|
+
/** The time of one try. It is at most the remaining time of the call. */
|
|
33
|
+
perAttempt?: Duration | undefined;
|
|
34
|
+
/** The retry. Its `idempotent` option decides which calls go again after an indeterminate try. */
|
|
35
|
+
retry?: RetryOptions | undefined;
|
|
36
|
+
/** The circuit breaker. */
|
|
37
|
+
breaker?: CircuitBreakerOptions | undefined;
|
|
38
|
+
/** The cache of the reads that opt in. */
|
|
39
|
+
cache?: CacheOptions | undefined;
|
|
40
|
+
/** The rate limit of the tries. */
|
|
41
|
+
rateLimit?: RateLimitOptions | undefined;
|
|
42
|
+
/** The message of a timeout error. */
|
|
43
|
+
message?: string | undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The context that the preset gives: the context of each part that it can have. */
|
|
47
|
+
export type ResilienceContext = TimeoutContext & Partial<RetryContext & CacheContext & CircuitBreakerContext & RateLimitContext>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* This function gives one middleware with the parts of `options` in the correct order (see the
|
|
51
|
+
* file comment). Add it with one `use()`.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const client = new Client(transport).use(
|
|
56
|
+
* resilience({
|
|
57
|
+
* overall: seconds(10),
|
|
58
|
+
* perAttempt: seconds(3),
|
|
59
|
+
* retry: { maxRetries: 2, idempotent: idempotentIn(registry) },
|
|
60
|
+
* breaker: { failureThreshold: 5 },
|
|
61
|
+
* }),
|
|
62
|
+
* );
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export function resilience(options: ResilienceOptions): TypedClientMiddleware<ResilienceContext, {}> {
|
|
66
|
+
const layers: ClientMiddleware[] = [];
|
|
67
|
+
const message = options.message;
|
|
68
|
+
if (options.overall) {
|
|
69
|
+
layers.push(createOverallTimeoutMiddleware({ overall: options.overall, ...(message ? { message: `${message} (overall)` } : {}) }) as ClientMiddleware);
|
|
70
|
+
}
|
|
71
|
+
if (options.cache) layers.push(createCacheMiddleware(options.cache) as ClientMiddleware);
|
|
72
|
+
if (options.retry) layers.push(createRetryMiddleware(options.retry) as ClientMiddleware);
|
|
73
|
+
if (options.breaker) layers.push(createCircuitBreakerMiddleware(options.breaker) as ClientMiddleware);
|
|
74
|
+
if (options.rateLimit) layers.push(createRateLimitMiddleware(options.rateLimit) as ClientMiddleware);
|
|
75
|
+
if (options.perAttempt) {
|
|
76
|
+
layers.push(createTimeoutMiddleware({ perAttempt: options.perAttempt, ...(message ? { message: `${message} (per-attempt)` } : {}) }) as ClientMiddleware);
|
|
77
|
+
}
|
|
78
|
+
const composed = <TReq, TRes>(next: ClientRunner<TReq, TRes>): ClientRunner<TReq, TRes> =>
|
|
79
|
+
layers.reduceRight<ClientRunner<TReq, TRes>>((inner, layer) => (layer as ClientMiddleware<TReq, TRes>)(inner), next);
|
|
80
|
+
return composed as TypedClientMiddleware<ResilienceContext, {}>;
|
|
81
|
+
}
|
|
@@ -5,9 +5,14 @@
|
|
|
5
5
|
* Works with any transport!
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { ClientRunner, ClientContext, ResponseItem, TypedClientMiddleware } from "../types.js";
|
|
8
|
+
import type { ClientRunner, ClientContext, Method, ResponseItem, TypedClientMiddleware } from "../types.js";
|
|
9
9
|
import type { RetryContext } from "./contexts.js";
|
|
10
|
-
import { abortedItem, sleep, thrownItem } from "./items.js";
|
|
10
|
+
import { abortedItem, deliveryOf, retryAfterOf, sleep, thrownItem } from "./items.js";
|
|
11
|
+
import { realClock, type Clock } from "../../clock.js";
|
|
12
|
+
import { backoffDelay } from "@mark1russell7/service/backoff";
|
|
13
|
+
import { IDEMPOTENCY_KEY_METADATA, idempotencyKeyOf, newIdempotencyKey } from "../../procedures/idempotency.js";
|
|
14
|
+
import { isDuration, optionalMs, type Duration } from "../../duration.js";
|
|
15
|
+
import { TIMER_DEFAULTS } from "../../timers.js";
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
18
|
* Retry middleware options.
|
|
@@ -20,11 +25,17 @@ export interface RetryOptions {
|
|
|
20
25
|
maxRetries?: number;
|
|
21
26
|
|
|
22
27
|
/**
|
|
23
|
-
*
|
|
28
|
+
* The clock of the backoff and of the budget (roadmap R35). The default is the clock of the
|
|
29
|
+
* platform. A test gives a fake clock.
|
|
30
|
+
*/
|
|
31
|
+
clock?: Clock;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Base delay for exponential backoff, for example `ms(500)`.
|
|
24
35
|
* Actual delay will be: baseDelay * 2^attempt + jitter
|
|
25
|
-
* @default
|
|
36
|
+
* @default 1 second
|
|
26
37
|
*/
|
|
27
|
-
retryDelay?:
|
|
38
|
+
retryDelay?: Duration;
|
|
28
39
|
|
|
29
40
|
/**
|
|
30
41
|
* The factor of the delay from one attempt to the next.
|
|
@@ -33,21 +44,47 @@ export interface RetryOptions {
|
|
|
33
44
|
backoffMultiplier?: number;
|
|
34
45
|
|
|
35
46
|
/**
|
|
36
|
-
* The longest delay between two attempts
|
|
47
|
+
* The longest delay between two attempts.
|
|
37
48
|
* @default no limit
|
|
38
49
|
*/
|
|
39
|
-
maxDelay?:
|
|
50
|
+
maxDelay?: Duration;
|
|
40
51
|
|
|
41
52
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
53
|
+
* The random part of each delay, from 0 to 1. With 1 (full jitter), the delay is a random time
|
|
54
|
+
* from 0 to the backoff, so the clients that failed together do not retry together. 0 gives the
|
|
55
|
+
* backoff exactly.
|
|
56
|
+
* @default 1 (full jitter; roadmap R12: before, 0.1 gave a delay within 10 percent of the backoff)
|
|
45
57
|
*/
|
|
46
58
|
jitter?: number;
|
|
47
59
|
|
|
60
|
+
/**
|
|
61
|
+
* The retry budget of this middleware: each call adds `ratio` of a retry to the budget, and each
|
|
62
|
+
* retry takes one. When the budget is empty, an error goes to the caller without a retry, so a
|
|
63
|
+
* failing service gets at most `ratio` more requests, not `maxRetries` times more. Besides the
|
|
64
|
+
* budget, `minPerSecond` retries a second are always allowed. `false` turns the budget off.
|
|
65
|
+
* @default { ratio: 0.2, minPerSecond: 10 }
|
|
66
|
+
*/
|
|
67
|
+
budget?: { ratio?: number; minPerSecond?: number } | false;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The calls that the middleware can send again after a try with an indeterminate outcome: the
|
|
71
|
+
* request left, and no answer came (a reset, a closed connection, the timeout of one try). The
|
|
72
|
+
* server can have run such a call, so only an idempotent call goes again (roadmap R1).
|
|
73
|
+
*
|
|
74
|
+
* - a function: true for an idempotent method. `idempotentIn(registry)` reads the effects of
|
|
75
|
+
* the procedures that the client knows.
|
|
76
|
+
* - `true`: every call. Use it only with a server that removes duplicate calls by their
|
|
77
|
+
* idempotency key (`ProcedureServer` does).
|
|
78
|
+
* - absent: only a call with `retry: { idempotent: true }` in its context.
|
|
79
|
+
*
|
|
80
|
+
* A try that did not leave the client (a refused connection) and an error that the server
|
|
81
|
+
* marks retryable go again in each case.
|
|
82
|
+
*/
|
|
83
|
+
idempotent?: boolean | ((method: Method) => boolean | undefined);
|
|
84
|
+
|
|
48
85
|
/**
|
|
49
86
|
* Custom function to determine if a request should be retried.
|
|
50
|
-
* Takes precedence over Status.retryable flag.
|
|
87
|
+
* Takes precedence over Status.retryable flag. The rule of `idempotent` still applies.
|
|
51
88
|
*
|
|
52
89
|
* @param item - The response item
|
|
53
90
|
* @param attempt - Current attempt number (0-indexed)
|
|
@@ -96,7 +133,7 @@ export interface RetryOptions {
|
|
|
96
133
|
* @example
|
|
97
134
|
* ```typescript
|
|
98
135
|
* // Create middleware with defaults
|
|
99
|
-
* client.use(createRetryMiddleware({ maxRetries: 3 }));
|
|
136
|
+
* client.use(createRetryMiddleware({ maxRetries: 3, retryDelay: ms(500) }));
|
|
100
137
|
*
|
|
101
138
|
* // Override per-call via context
|
|
102
139
|
* await client.call(method, payload, {
|
|
@@ -105,54 +142,121 @@ export interface RetryOptions {
|
|
|
105
142
|
* ```
|
|
106
143
|
*/
|
|
107
144
|
export function createRetryMiddleware(options: RetryOptions = {}): TypedClientMiddleware<RetryContext, {}> {
|
|
145
|
+
const defaultRetryDelay = optionalMs(options.retryDelay, "retryDelay") ?? TIMER_DEFAULTS.retry.retryDelayMs;
|
|
108
146
|
const {
|
|
109
|
-
|
|
110
|
-
jitter: defaultJitter = 0.1,
|
|
147
|
+
jitter: defaultJitter = 1,
|
|
111
148
|
shouldRetry: customShouldRetry,
|
|
112
149
|
onBeforeRetry,
|
|
113
150
|
onAfterRetry,
|
|
151
|
+
idempotent: idempotentOption,
|
|
114
152
|
} = options;
|
|
115
|
-
const defaultMaxRetries = options.maxRetries ??
|
|
116
|
-
const defaultMultiplier = options.backoffMultiplier ??
|
|
117
|
-
const defaultMaxDelay = options.maxDelay ??
|
|
153
|
+
const defaultMaxRetries = options.maxRetries ?? TIMER_DEFAULTS.retry.maxRetries;
|
|
154
|
+
const defaultMultiplier = options.backoffMultiplier ?? TIMER_DEFAULTS.retry.backoffMultiplier;
|
|
155
|
+
const defaultMaxDelay = optionalMs(options.maxDelay, "maxDelay") ?? TIMER_DEFAULTS.retry.maxDelayMs;
|
|
118
156
|
|
|
119
|
-
/**
|
|
120
|
-
|
|
157
|
+
/**
|
|
158
|
+
* The overrides of one call (`retry: { ... }` in its context), with the delays in milliseconds.
|
|
159
|
+
* Before, only maxAttempts was read.
|
|
160
|
+
*/
|
|
161
|
+
const overridesOf = (value: unknown): { maxAttempts?: number; delay?: number; backoffMultiplier?: number; maxDelay?: number } => {
|
|
121
162
|
if (typeof value !== "object" || value === null) return {};
|
|
122
|
-
const out:
|
|
123
|
-
for (const key of ["maxAttempts", "
|
|
163
|
+
const out: { maxAttempts?: number; delay?: number; backoffMultiplier?: number; maxDelay?: number } = {};
|
|
164
|
+
for (const key of ["maxAttempts", "backoffMultiplier"] as const) {
|
|
124
165
|
const field = (value as Record<string, unknown>)[key];
|
|
125
166
|
if (typeof field === "number" && Number.isFinite(field) && field >= 0) out[key] = field;
|
|
126
167
|
}
|
|
168
|
+
for (const key of ["delay", "maxDelay"] as const) {
|
|
169
|
+
const field = (value as Record<string, unknown>)[key];
|
|
170
|
+
if (field !== undefined) {
|
|
171
|
+
if (!isDuration(field)) throw new TypeError(`retry.${key} must be a duration such as ms(500) (roadmap R11)`);
|
|
172
|
+
out[key] = optionalMs(field, `retry.${key}`)!;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
127
175
|
return out;
|
|
128
176
|
};
|
|
129
177
|
|
|
130
|
-
/**
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
178
|
+
/**
|
|
179
|
+
* The backoff delay of an attempt (from 0): exponential, at most the maximum delay, with the
|
|
180
|
+
* random part of `jitter` (full jitter by default). The backoff is `backoffDelay` of
|
|
181
|
+
* `@mark1russell7/service/backoff` (roadmap R37), whose tries start at 1.
|
|
182
|
+
*/
|
|
183
|
+
const retryDelayOf = (attempt: number, overrides: ReturnType<typeof overridesOf>): number =>
|
|
184
|
+
backoffDelay(
|
|
185
|
+
{
|
|
186
|
+
base: overrides.delay ?? defaultRetryDelay,
|
|
187
|
+
factor: overrides.backoffMultiplier ?? defaultMultiplier,
|
|
188
|
+
cap: overrides.maxDelay ?? defaultMaxDelay,
|
|
189
|
+
jitter: { kind: "subtract", fraction: Math.min(1, Math.max(0, defaultJitter)) },
|
|
190
|
+
},
|
|
191
|
+
attempt + 1,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// The retry budget (R12): a balance of retries that each call fills by `ratio`
|
|
195
|
+
const budget = options.budget === false ? undefined : { ratio: options.budget?.ratio ?? 0.2, minPerSecond: options.budget?.minPerSecond ?? 10 };
|
|
196
|
+
const budgetCap = budget ? Math.max(1, budget.ratio * 100) : 0;
|
|
197
|
+
let balance = budgetCap;
|
|
198
|
+
const clock = options.clock ?? realClock;
|
|
199
|
+
let secondStart = clock.now();
|
|
200
|
+
let retriesThisSecond = 0;
|
|
201
|
+
/** This function takes one retry from the budget, or gives false when the budget is empty. */
|
|
202
|
+
const takeRetry = (): boolean => {
|
|
203
|
+
if (!budget) return true;
|
|
204
|
+
if (balance >= 1) {
|
|
205
|
+
balance -= 1;
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
const now = clock.now();
|
|
209
|
+
if (now - secondStart >= 1000) {
|
|
210
|
+
secondStart = now;
|
|
211
|
+
retriesThisSecond = 0;
|
|
212
|
+
}
|
|
213
|
+
if (retriesThisSecond < budget.minPerSecond) {
|
|
214
|
+
retriesThisSecond++;
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/** True when the call can go again after an indeterminate try: the call says so, or the option. */
|
|
221
|
+
const isIdempotent = (method: Method, value: unknown): boolean => {
|
|
222
|
+
const own = typeof value === "object" && value !== null ? (value as { idempotent?: unknown }).idempotent : undefined;
|
|
223
|
+
if (typeof own === "boolean") return own;
|
|
224
|
+
if (typeof idempotentOption === "function") return idempotentOption(method) === true;
|
|
225
|
+
return idempotentOption === true;
|
|
135
226
|
};
|
|
136
227
|
|
|
137
228
|
return <TReq, TRes>(next: ClientRunner<TReq, TRes>): ClientRunner<TReq, TRes> => {
|
|
138
229
|
return async function* (context: ClientContext<TReq>) {
|
|
139
230
|
// Read from metadata first (user-provided context), then fall back to options
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
231
|
+
const given = context.message.metadata.retry;
|
|
232
|
+
const overrides = overridesOf(given);
|
|
233
|
+
// maxAttempts counts the attempts: the first try and the retries (R12: before, the override
|
|
234
|
+
// was used as the number of retries, against its name and its documentation)
|
|
235
|
+
const maxRetries = overrides.maxAttempts !== undefined ? Math.max(0, overrides.maxAttempts - 1) : defaultMaxRetries;
|
|
236
|
+
if (budget) balance = Math.min(budgetCap, balance + budget.ratio);
|
|
237
|
+
const idempotent = isIdempotent(context.message.method, context.message.metadata.retry);
|
|
238
|
+
const shouldRetryItem = (item: ResponseItem<TRes>, attempt: number): boolean => {
|
|
239
|
+
const wanted = customShouldRetry
|
|
144
240
|
? customShouldRetry(item, attempt)
|
|
145
241
|
: item.status.type === "error" && item.status.retryable;
|
|
242
|
+
// A request that can have run goes again only when the call is idempotent (R1). Before,
|
|
243
|
+
// a reset or a timeout of one try sent a write again.
|
|
244
|
+
return wanted && (deliveryOf(item) !== "indeterminate" || idempotent);
|
|
245
|
+
};
|
|
146
246
|
const signal = context.message.signal;
|
|
147
247
|
const baseId = context.message.id;
|
|
248
|
+
// One key for all the tries of the call: a server that removes duplicates runs it one time
|
|
249
|
+
const idempotencyKey = idempotencyKeyOf(context.message.metadata) ?? newIdempotencyKey();
|
|
250
|
+
context.message.metadata[IDEMPOTENCY_KEY_METADATA] = idempotencyKey;
|
|
148
251
|
|
|
149
252
|
let attempt = 0;
|
|
150
253
|
for (;;) {
|
|
151
254
|
// Update retry metadata with current state
|
|
152
255
|
context.message.metadata.retry = {
|
|
153
|
-
...
|
|
256
|
+
...(typeof given === "object" && given !== null ? (given as object) : {}),
|
|
154
257
|
attempt,
|
|
155
|
-
maxAttempts: maxRetries,
|
|
258
|
+
maxAttempts: maxRetries + 1,
|
|
259
|
+
idempotent,
|
|
156
260
|
};
|
|
157
261
|
// Each attempt gets its own request id. The id correlates the frames of one attempt
|
|
158
262
|
// on the wire: a cancelled attempt can still answer under its id (deep dive TRN-8/9).
|
|
@@ -204,7 +308,15 @@ export function createRetryMiddleware(options: RetryOptions = {}): TypedClientMi
|
|
|
204
308
|
return;
|
|
205
309
|
}
|
|
206
310
|
|
|
207
|
-
|
|
311
|
+
// The budget is empty: the error goes to the caller (R12)
|
|
312
|
+
if (!takeRetry()) {
|
|
313
|
+
yield retryItem;
|
|
314
|
+
if (onAfterRetry) await onAfterRetry(false, attempt);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// A server that asks for a wait (Retry-After) gets at least that wait (R12)
|
|
319
|
+
let delay = Math.max(retryDelayOf(attempt, overrides), retryAfterOf(retryItem) ?? 0);
|
|
208
320
|
|
|
209
321
|
// Before retry hook
|
|
210
322
|
if (onBeforeRetry) {
|
|
@@ -225,7 +337,7 @@ export function createRetryMiddleware(options: RetryOptions = {}): TypedClientMi
|
|
|
225
337
|
}
|
|
226
338
|
|
|
227
339
|
// The backoff stops at once when the caller aborts (deep dive TRN-8)
|
|
228
|
-
if (!(await sleep(delay, signal))) {
|
|
340
|
+
if (!(await sleep(delay, signal, clock))) {
|
|
229
341
|
yield abortedItem<TRes>(context.message.id);
|
|
230
342
|
return;
|
|
231
343
|
}
|