@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
|
@@ -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
|
}
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
|
|
13
13
|
import type { ClientMiddleware, ClientRunner, ClientContext, TypedClientMiddleware } from "../types.js";
|
|
14
14
|
import type { CircuitBreakerContext } from "./contexts.js";
|
|
15
|
+
import { optionalMs, type Duration } from "../../duration.js";
|
|
16
|
+
import { isAvailabilityFailure } from "./items.js";
|
|
17
|
+
import { realClock, type Cancel, type Clock } from "../../clock.js";
|
|
18
|
+
import { CIRCUIT_BREAKER_MACHINE, type MachineTransition } from "../../machines.js";
|
|
19
|
+
import { TIMER_DEFAULTS } from "../../timers.js";
|
|
15
20
|
|
|
16
21
|
/**
|
|
17
22
|
* Circuit breaker state.
|
|
@@ -29,16 +34,16 @@ export interface CircuitBreakerOptions {
|
|
|
29
34
|
failureThreshold?: number;
|
|
30
35
|
|
|
31
36
|
/**
|
|
32
|
-
* Time window for counting failures
|
|
33
|
-
* @default
|
|
37
|
+
* Time window for counting failures.
|
|
38
|
+
* @default 10 seconds
|
|
34
39
|
*/
|
|
35
|
-
failureWindow?:
|
|
40
|
+
failureWindow?: Duration;
|
|
36
41
|
|
|
37
42
|
/**
|
|
38
|
-
* Time to wait before attempting to close circuit
|
|
39
|
-
* @default
|
|
43
|
+
* Time to wait before attempting to close circuit.
|
|
44
|
+
* @default 30 seconds
|
|
40
45
|
*/
|
|
41
|
-
resetTimeout?:
|
|
46
|
+
resetTimeout?: Duration;
|
|
42
47
|
|
|
43
48
|
/**
|
|
44
49
|
* Number of successful requests required to close circuit from HALF_OPEN.
|
|
@@ -55,8 +60,11 @@ export interface CircuitBreakerOptions {
|
|
|
55
60
|
|
|
56
61
|
/**
|
|
57
62
|
* Custom error predicate.
|
|
58
|
-
* Return true if error should count as failure.
|
|
59
|
-
*
|
|
63
|
+
* Return true if error should count as failure. An error that is not a failure counts as
|
|
64
|
+
* nothing: not a failure and not a success.
|
|
65
|
+
* @default `isAvailabilityFailure`: only UNAVAILABLE, TIMEOUT, a stopped server, a gateway
|
|
66
|
+
* error and network errors count. An abort or a validation error does not (roadmap R12: before,
|
|
67
|
+
* every error counted, so a caller that aborted could open the circuit).
|
|
60
68
|
*/
|
|
61
69
|
isFailure?: (error: Error) => boolean;
|
|
62
70
|
|
|
@@ -64,12 +72,30 @@ export interface CircuitBreakerOptions {
|
|
|
64
72
|
* Callback when circuit state changes.
|
|
65
73
|
*/
|
|
66
74
|
onStateChange?: (oldState: CircuitState, newState: CircuitState) => void;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The clock of the window and of the reset (roadmap R35). The default is the clock of the
|
|
78
|
+
* platform. A test gives a fake clock.
|
|
79
|
+
*/
|
|
80
|
+
clock?: Clock;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* This function gets each change of the state, with its event, as a transition of
|
|
84
|
+
* `CIRCUIT_BREAKER_MACHINE` (roadmap R35). A test replays them against the document.
|
|
85
|
+
*/
|
|
86
|
+
onTransition?: (transition: MachineTransition) => void;
|
|
87
|
+
|
|
88
|
+
/** The name of the breaker in its transitions. The default is "circuit-breaker". */
|
|
89
|
+
name?: string;
|
|
67
90
|
}
|
|
68
91
|
|
|
69
92
|
/**
|
|
70
93
|
* Failure record for tracking errors.
|
|
71
94
|
*/
|
|
72
95
|
interface FailureRecord {
|
|
96
|
+
/** The time of the monotonic clock: the window uses it */
|
|
97
|
+
at: number;
|
|
98
|
+
/** The wall-clock time, for the statistics */
|
|
73
99
|
timestamp: number;
|
|
74
100
|
error: Error;
|
|
75
101
|
}
|
|
@@ -114,8 +140,8 @@ export class CircuitBreakerError extends Error {
|
|
|
114
140
|
* ```typescript
|
|
115
141
|
* client.use(createCircuitBreakerMiddleware({
|
|
116
142
|
* failureThreshold: 5, // Open after 5 failures
|
|
117
|
-
* failureWindow:
|
|
118
|
-
* resetTimeout:
|
|
143
|
+
* failureWindow: seconds(10), // Within 10 seconds
|
|
144
|
+
* resetTimeout: seconds(30), // Try again after 30 seconds
|
|
119
145
|
* successThreshold: 2 // Close after 2 successes
|
|
120
146
|
* }));
|
|
121
147
|
* ```
|
|
@@ -125,13 +151,13 @@ export function createCircuitBreakerMiddleware(
|
|
|
125
151
|
): TypedClientMiddleware<CircuitBreakerContext, {}> {
|
|
126
152
|
const {
|
|
127
153
|
failureThreshold = 5,
|
|
128
|
-
failureWindow = 10000,
|
|
129
|
-
resetTimeout = 30000,
|
|
130
154
|
successThreshold = 2,
|
|
131
|
-
isFailure =
|
|
155
|
+
isFailure = isAvailabilityFailure,
|
|
132
156
|
onStateChange,
|
|
133
157
|
} = options;
|
|
134
158
|
const halfOpenMaxRequests = options.halfOpenMaxRequests ?? successThreshold;
|
|
159
|
+
const failureWindow = optionalMs(options.failureWindow, "failureWindow") ?? TIMER_DEFAULTS.circuitBreaker.failureWindowMs;
|
|
160
|
+
const resetTimeout = optionalMs(options.resetTimeout, "resetTimeout") ?? TIMER_DEFAULTS.circuitBreaker.resetTimeoutMs;
|
|
135
161
|
|
|
136
162
|
let state: CircuitState = "CLOSED";
|
|
137
163
|
let failures: FailureRecord[] = [];
|
|
@@ -139,23 +165,26 @@ export function createCircuitBreakerMiddleware(
|
|
|
139
165
|
let totalRequests = 0;
|
|
140
166
|
let probesInFlight = 0;
|
|
141
167
|
let lastStateChange = Date.now();
|
|
142
|
-
let resetTimer:
|
|
168
|
+
let resetTimer: Cancel | null = null;
|
|
169
|
+
const clock = options.clock ?? realClock;
|
|
170
|
+
const instance = options.name ?? "circuit-breaker";
|
|
143
171
|
|
|
144
172
|
/**
|
|
145
|
-
* Change circuit state.
|
|
173
|
+
* Change circuit state. `on` is the event of `CIRCUIT_BREAKER_MACHINE`.
|
|
146
174
|
*/
|
|
147
|
-
function setState(newState: CircuitState): void {
|
|
175
|
+
function setState(newState: CircuitState, on: string): void {
|
|
148
176
|
const oldState = state;
|
|
149
177
|
state = newState;
|
|
150
178
|
if (oldState !== newState) lastStateChange = Date.now();
|
|
151
179
|
|
|
152
|
-
if (oldState !== newState
|
|
153
|
-
|
|
180
|
+
if (oldState !== newState) {
|
|
181
|
+
options.onTransition?.({ machine: CIRCUIT_BREAKER_MACHINE.id, instance, from: oldState, on, to: newState, at: clock.now() });
|
|
182
|
+
onStateChange?.(oldState, newState);
|
|
154
183
|
}
|
|
155
184
|
|
|
156
185
|
// Clear reset timer when moving to CLOSED
|
|
157
186
|
if (newState === "CLOSED" && resetTimer) {
|
|
158
|
-
|
|
187
|
+
resetTimer();
|
|
159
188
|
resetTimer = null;
|
|
160
189
|
}
|
|
161
190
|
}
|
|
@@ -164,22 +193,23 @@ export function createCircuitBreakerMiddleware(
|
|
|
164
193
|
* Record a failure.
|
|
165
194
|
*/
|
|
166
195
|
function recordFailure(error: Error): void {
|
|
167
|
-
|
|
196
|
+
// The window uses a monotonic clock: a change of the wall clock does not move it (R12)
|
|
197
|
+
const now = clock.now();
|
|
168
198
|
|
|
169
199
|
// Add failure
|
|
170
|
-
failures.push({ timestamp: now, error });
|
|
200
|
+
failures.push({ at: now, timestamp: Date.now(), error });
|
|
171
201
|
|
|
172
202
|
// Remove old failures outside window
|
|
173
|
-
failures = failures.filter((f) => now - f.
|
|
203
|
+
failures = failures.filter((f) => now - f.at < failureWindow);
|
|
174
204
|
|
|
175
205
|
if (state === "HALF_OPEN") {
|
|
176
206
|
// Any failure while testing reopens the circuit immediately — don't wait for the full
|
|
177
207
|
// failureThreshold. See documentation/BUGS-2026-07.md (H11).
|
|
178
|
-
setState("OPEN");
|
|
208
|
+
setState("OPEN", "probe-failure");
|
|
179
209
|
successes = 0;
|
|
180
210
|
scheduleReset();
|
|
181
211
|
} else if (state === "CLOSED" && failures.length >= failureThreshold) {
|
|
182
|
-
setState("OPEN");
|
|
212
|
+
setState("OPEN", "failure-threshold");
|
|
183
213
|
scheduleReset();
|
|
184
214
|
}
|
|
185
215
|
}
|
|
@@ -192,14 +222,14 @@ export function createCircuitBreakerMiddleware(
|
|
|
192
222
|
successes++;
|
|
193
223
|
if (successes >= successThreshold) {
|
|
194
224
|
// Enough successes - close circuit
|
|
195
|
-
setState("CLOSED");
|
|
225
|
+
setState("CLOSED", "probe-successes");
|
|
196
226
|
failures = [];
|
|
197
227
|
successes = 0;
|
|
198
228
|
}
|
|
199
229
|
} else if (state === "CLOSED") {
|
|
200
230
|
// Clear old failures on success
|
|
201
|
-
const now =
|
|
202
|
-
failures = failures.filter((f) => now - f.
|
|
231
|
+
const now = clock.now();
|
|
232
|
+
failures = failures.filter((f) => now - f.at < failureWindow);
|
|
203
233
|
}
|
|
204
234
|
}
|
|
205
235
|
|
|
@@ -208,15 +238,15 @@ export function createCircuitBreakerMiddleware(
|
|
|
208
238
|
*/
|
|
209
239
|
function scheduleReset(): void {
|
|
210
240
|
if (resetTimer) {
|
|
211
|
-
|
|
241
|
+
resetTimer();
|
|
212
242
|
}
|
|
213
243
|
|
|
214
|
-
resetTimer =
|
|
244
|
+
resetTimer = clock.after(resetTimeout, () => {
|
|
215
245
|
if (state === "OPEN") {
|
|
216
|
-
setState("HALF_OPEN");
|
|
246
|
+
setState("HALF_OPEN", "reset-timeout");
|
|
217
247
|
successes = 0;
|
|
218
248
|
}
|
|
219
|
-
}
|
|
249
|
+
});
|
|
220
250
|
// Don't keep the event loop alive just for the reset timer. See BUGS-2026-07.md (M4).
|
|
221
251
|
resetTimer.unref?.();
|
|
222
252
|
}
|
|
@@ -239,10 +269,10 @@ export function createCircuitBreakerMiddleware(
|
|
|
239
269
|
}
|
|
240
270
|
|
|
241
271
|
function getStats(): CircuitBreakerStats {
|
|
242
|
-
const now =
|
|
272
|
+
const now = clock.now();
|
|
243
273
|
return {
|
|
244
274
|
state,
|
|
245
|
-
failures: failures.filter((f) => now - f.
|
|
275
|
+
failures: failures.filter((f) => now - f.at < failureWindow).length,
|
|
246
276
|
successes,
|
|
247
277
|
totalRequests,
|
|
248
278
|
lastFailureTime: failures.length > 0 ? failures[failures.length - 1]!.timestamp : null,
|
|
@@ -257,10 +287,10 @@ export function createCircuitBreakerMiddleware(
|
|
|
257
287
|
totalRequests++;
|
|
258
288
|
// The state for the other middlewares (CircuitBreakerContext)
|
|
259
289
|
const writeState = (): void => {
|
|
260
|
-
const now =
|
|
290
|
+
const now = clock.now();
|
|
261
291
|
context.message.metadata["circuitBreaker"] = {
|
|
262
292
|
state,
|
|
263
|
-
failures: failures.filter((f) => now - f.
|
|
293
|
+
failures: failures.filter((f) => now - f.at < failureWindow).length,
|
|
264
294
|
successes,
|
|
265
295
|
};
|
|
266
296
|
};
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* ```
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import type { Duration } from "../../duration.js";
|
|
19
|
+
|
|
18
20
|
// =============================================================================
|
|
19
21
|
// Retry Middleware Context
|
|
20
22
|
// =============================================================================
|
|
@@ -29,12 +31,17 @@ export interface RetryContext {
|
|
|
29
31
|
attempt?: number;
|
|
30
32
|
/** Maximum number of attempts: an override of a call, then the value that the middleware uses */
|
|
31
33
|
maxAttempts?: number;
|
|
32
|
-
/** An override of the base delay
|
|
33
|
-
delay?:
|
|
34
|
+
/** An override of the base delay */
|
|
35
|
+
delay?: Duration;
|
|
34
36
|
/** An override of the factor of the delay from one attempt to the next */
|
|
35
37
|
backoffMultiplier?: number;
|
|
36
|
-
/** An override of the longest delay
|
|
37
|
-
maxDelay?:
|
|
38
|
+
/** An override of the longest delay */
|
|
39
|
+
maxDelay?: Duration;
|
|
40
|
+
/**
|
|
41
|
+
* True: the call can go again after a try with an indeterminate outcome, because a second
|
|
42
|
+
* run changes nothing more (roadmap R1). It overrides the `idempotent` option of the middleware.
|
|
43
|
+
*/
|
|
44
|
+
idempotent?: boolean;
|
|
38
45
|
};
|
|
39
46
|
}
|
|
40
47
|
|
|
@@ -51,8 +58,8 @@ export interface RetryContext {
|
|
|
51
58
|
*/
|
|
52
59
|
export interface CacheContext {
|
|
53
60
|
cache: {
|
|
54
|
-
/** The time-to-live of the entry that this call stores
|
|
55
|
-
ttl?:
|
|
61
|
+
/** The time-to-live of the entry that this call stores */
|
|
62
|
+
ttl?: Duration;
|
|
56
63
|
/** True: the call skips the cache (no read, no write) */
|
|
57
64
|
bypass?: boolean;
|
|
58
65
|
/** True: the call fetches and replaces the stored response */
|
|
@@ -72,10 +79,10 @@ export interface CacheContext {
|
|
|
72
79
|
*/
|
|
73
80
|
export interface TimeoutContext {
|
|
74
81
|
timeout: {
|
|
75
|
-
/**
|
|
76
|
-
overall?:
|
|
77
|
-
/**
|
|
78
|
-
perAttempt?:
|
|
82
|
+
/** The overall timeout (if configured) */
|
|
83
|
+
overall?: Duration;
|
|
84
|
+
/** The timeout of each attempt (if configured) */
|
|
85
|
+
perAttempt?: Duration;
|
|
79
86
|
};
|
|
80
87
|
}
|
|
81
88
|
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { ResponseItem } from "../types.js";
|
|
11
|
+
import { realClock, type Clock } from "../../clock.js";
|
|
11
12
|
|
|
12
13
|
/** This function makes an error item. */
|
|
13
14
|
export function errorItem<TRes>(id: string, code: string, message: string, retryable: boolean): ResponseItem<TRes> {
|
|
@@ -19,6 +20,72 @@ export function abortedItem<TRes>(id: string): ResponseItem<TRes> {
|
|
|
19
20
|
return errorItem<TRes>(id, "ABORTED", "Request was aborted", false);
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* What a failed try tells about its request, in the metadata of the error item (`delivery`).
|
|
25
|
+
* - `not-sent`: the request did not leave the client. A retry is safe.
|
|
26
|
+
* - `indeterminate`: the request left, and no answer came. The server can have run it, so only
|
|
27
|
+
* an idempotent call can go again (the three outcomes of `@mark1russell7/service`).
|
|
28
|
+
* An error item without the field is an answer of the server: its `retryable` flag decides.
|
|
29
|
+
*/
|
|
30
|
+
export type Delivery = "not-sent" | "indeterminate";
|
|
31
|
+
|
|
32
|
+
/** The metadata key of the delivery of a failed try. */
|
|
33
|
+
export const DELIVERY_METADATA_KEY = "delivery";
|
|
34
|
+
|
|
35
|
+
/** This function marks an error item with the delivery of its request. */
|
|
36
|
+
export function withDelivery<TRes>(item: ResponseItem<TRes>, delivery: Delivery): ResponseItem<TRes> {
|
|
37
|
+
return { ...item, metadata: { ...item.metadata, [DELIVERY_METADATA_KEY]: delivery } };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The metadata key of the time that the server asks a client to wait before a retry, in milliseconds. */
|
|
41
|
+
export const RETRY_AFTER_METADATA_KEY = "retry-after";
|
|
42
|
+
|
|
43
|
+
/** The time that an error item asks a client to wait before a retry, in milliseconds, or undefined. */
|
|
44
|
+
export function retryAfterOf(item: ResponseItem<unknown>): number | undefined {
|
|
45
|
+
const value = item.metadata?.[RETRY_AFTER_METADATA_KEY];
|
|
46
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The error codes that say that the service is not available: the server is down, busy, slow or
|
|
51
|
+
* stopping, or the network failed. A circuit breaker counts only these (roadmap R12). An abort,
|
|
52
|
+
* a validation error, a refusal or an error of the handler says nothing about the health of the
|
|
53
|
+
* service.
|
|
54
|
+
*/
|
|
55
|
+
const AVAILABILITY_CODES = new Set([
|
|
56
|
+
"UNAVAILABLE",
|
|
57
|
+
"TIMEOUT",
|
|
58
|
+
"SERVER_STOPPED",
|
|
59
|
+
"SERVICE_UNAVAILABLE",
|
|
60
|
+
"BAD_GATEWAY",
|
|
61
|
+
"GATEWAY_TIMEOUT",
|
|
62
|
+
"NETWORK_ERROR",
|
|
63
|
+
"CONNECTION_REFUSED",
|
|
64
|
+
"STREAM_ENDED",
|
|
65
|
+
// The connection to the other side closed before the call left (roadmap R30: before, the list
|
|
66
|
+
// did not have this code of R24)
|
|
67
|
+
"CONNECTION_CLOSED",
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
/** True when an error says that the service is not available (see AVAILABILITY_CODES). */
|
|
71
|
+
export function isAvailabilityFailure(error: unknown): boolean {
|
|
72
|
+
const code = (error as { code?: unknown } | undefined)?.code;
|
|
73
|
+
if (typeof code === "string" && AVAILABILITY_CODES.has(code)) return true;
|
|
74
|
+
return isNetworkError(error);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The time of a monotonic clock in milliseconds: a change of the wall clock does not move it. */
|
|
78
|
+
export function monotonicNow(): number {
|
|
79
|
+
const performance = (globalThis as { performance?: { now(): number } }).performance;
|
|
80
|
+
return performance ? performance.now() : Date.now();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The delivery of a failed try, or undefined for an answer of the server. */
|
|
84
|
+
export function deliveryOf(item: ResponseItem<unknown>): Delivery | undefined {
|
|
85
|
+
const delivery = item.metadata?.[DELIVERY_METADATA_KEY];
|
|
86
|
+
return delivery === "not-sent" || delivery === "indeterminate" ? delivery : undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
22
89
|
const NETWORK_CODES = new Set([
|
|
23
90
|
"ECONNREFUSED",
|
|
24
91
|
"ECONNRESET",
|
|
@@ -52,35 +119,63 @@ export function isNetworkError(error: unknown): boolean {
|
|
|
52
119
|
return /WebSocket (connection closed|disconnected)|Connection timeout/.test(error.message);
|
|
53
120
|
}
|
|
54
121
|
|
|
122
|
+
/** The network codes that tell that the connection never opened, so no request left. */
|
|
123
|
+
const NOT_SENT_CODES = new Set([
|
|
124
|
+
"ECONNREFUSED",
|
|
125
|
+
"ENETUNREACH",
|
|
126
|
+
"EHOSTUNREACH",
|
|
127
|
+
"ENOTFOUND",
|
|
128
|
+
"EAI_AGAIN",
|
|
129
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
130
|
+
"CONNECTION_REFUSED",
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* This function tells if a thrown error proves that the request did not leave the client: the
|
|
135
|
+
* connection was refused, the name did not resolve, or the WebSocket did not open in time.
|
|
136
|
+
* A reset or a close during the request does not prove it (the request can have arrived).
|
|
137
|
+
*/
|
|
138
|
+
export function isNotSentError(error: unknown): boolean {
|
|
139
|
+
if (!(error instanceof Error)) return false;
|
|
140
|
+
const code = (error as { code?: unknown }).code;
|
|
141
|
+
if (typeof code === "string" && NOT_SENT_CODES.has(code)) return true;
|
|
142
|
+
const cause = (error as { cause?: unknown }).cause;
|
|
143
|
+
if (cause !== undefined && cause !== error && isNotSentError(cause)) return true;
|
|
144
|
+
// The WebSocket transport fails a request that waited for a connection that did not come
|
|
145
|
+
return /^(WebSocket disconnected|Connection timeout)$/.test(error.message);
|
|
146
|
+
}
|
|
147
|
+
|
|
55
148
|
/**
|
|
56
149
|
* The item that stands for a thrown error, so that `shouldRetry` can decide about it. It is
|
|
57
|
-
* retryable only when the error is a network failure.
|
|
150
|
+
* retryable only when the error is a network failure. Its delivery tells if the request left.
|
|
58
151
|
*/
|
|
59
152
|
export function thrownItem<TRes>(id: string, error: unknown): ResponseItem<TRes> {
|
|
60
153
|
const code = (error as { code?: unknown } | undefined)?.code;
|
|
61
|
-
|
|
154
|
+
const item = errorItem<TRes>(
|
|
62
155
|
id,
|
|
63
156
|
typeof code === "string" ? code : "THROWN",
|
|
64
157
|
error instanceof Error ? error.message : String(error),
|
|
65
158
|
isNetworkError(error),
|
|
66
159
|
);
|
|
160
|
+
return withDelivery(item, isNotSentError(error) ? "not-sent" : "indeterminate");
|
|
67
161
|
}
|
|
68
162
|
|
|
69
163
|
/**
|
|
70
164
|
* This function waits `ms` milliseconds. It returns false at once when the signal aborts (deep
|
|
71
165
|
* dive TRN-8: before, the backoff of the retry middleware ignored the signal).
|
|
72
166
|
*/
|
|
73
|
-
export function sleep(ms: number, signal?: AbortSignal): Promise<boolean> {
|
|
167
|
+
export function sleep(ms: number, signal?: AbortSignal, clock: Clock = realClock): Promise<boolean> {
|
|
74
168
|
if (signal?.aborted) return Promise.resolve(false);
|
|
75
169
|
return new Promise((resolve) => {
|
|
76
170
|
const onAbort = (): void => {
|
|
77
|
-
|
|
171
|
+
cancel();
|
|
78
172
|
resolve(false);
|
|
79
173
|
};
|
|
80
|
-
|
|
174
|
+
// The clock of the caller (roadmap R35): a test gives a fake clock
|
|
175
|
+
const cancel = clock.after(Math.max(0, ms), () => {
|
|
81
176
|
signal?.removeEventListener("abort", onAbort);
|
|
82
177
|
resolve(true);
|
|
83
|
-
}
|
|
178
|
+
});
|
|
84
179
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
85
180
|
});
|
|
86
181
|
}
|