@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
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { Server, type ServerOptions } from "./server.js";
|
|
9
|
-
import type { Method, ServerHandler, ServerRequest, ServerResponse } from "./types.js";
|
|
9
|
+
import type { Method, Principal, ServerHandler, ServerRequest, ServerResponse } from "./types.js";
|
|
10
10
|
import { methodToPath, pathToMethod } from "./method.js";
|
|
11
11
|
import type {
|
|
12
12
|
AnyProcedure,
|
|
@@ -19,8 +19,11 @@ import { ProcedureRegistry, PROCEDURE_REGISTRY } from "../procedures/registry.js
|
|
|
19
19
|
import { createCollectionProcedures } from "../procedures/collection/procedures.js";
|
|
20
20
|
import { InvocationError, invokeProcedure, type AdmitItem, type Interceptor, type Invocation } from "../procedures/invoke.js";
|
|
21
21
|
import type { Capability, ClearanceOrder, ResourcesOf } from "../procedures/types.js";
|
|
22
|
-
import { TIMEOUT_METADATA_KEY, narrowCapability } from "../procedures/capability.js";
|
|
22
|
+
import { TIMEOUT_METADATA_KEY, effectsOf, narrowCapability } from "../procedures/capability.js";
|
|
23
|
+
import { IdempotencyCache, idempotencyKeyOf, isRepeatable, type IdempotencyOptions } from "../procedures/idempotency.js";
|
|
24
|
+
import { optionalMs, type Duration } from "../duration.js";
|
|
23
25
|
import type { CollectionStorage } from "../procedures/collection/storage.js";
|
|
26
|
+
import { TIMER_DEFAULTS } from "../timers.js";
|
|
24
27
|
|
|
25
28
|
// =============================================================================
|
|
26
29
|
// Types
|
|
@@ -36,6 +39,17 @@ export interface StorageConfig<T = unknown> {
|
|
|
36
39
|
name: string;
|
|
37
40
|
}
|
|
38
41
|
|
|
42
|
+
/** How `ProcedureServer.useStorage` puts a new storage in the place of an old one (roadmap R28). */
|
|
43
|
+
export interface UseStorageOptions {
|
|
44
|
+
/**
|
|
45
|
+
* True: the server calls `flush()` of the old storage before it changes the storage. When the
|
|
46
|
+
* flush fails, the old storage stays and `useStorage` throws.
|
|
47
|
+
*/
|
|
48
|
+
drain?: boolean | undefined;
|
|
49
|
+
/** True: the server closes the old storage after the change. */
|
|
50
|
+
close?: boolean | undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
/**
|
|
40
54
|
* Options for ProcedureServer.
|
|
41
55
|
*/
|
|
@@ -82,6 +96,93 @@ export interface ProcedureServerOptions extends ServerOptions {
|
|
|
82
96
|
resources?: ((invocation: Invocation) => ResourcesOf) | undefined;
|
|
83
97
|
/** The filter of the output that leaves the server. */
|
|
84
98
|
admitItem?: AdmitItem | undefined;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The results of keyed calls (roadmap R1). A call to a procedure that is not repeatable, with
|
|
102
|
+
* an idempotency key in its metadata, runs one time: a later try with the same key gets the
|
|
103
|
+
* same result. `false` turns this off. The default keeps 1000 results for 10 minutes.
|
|
104
|
+
*/
|
|
105
|
+
idempotency?: IdempotencyOptions | false | undefined;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The token of the server (roadmap R18). Each request with credentials (a network request)
|
|
109
|
+
* must present it as its bearer token, or the server answers `UNAUTHORIZED`. Before, only the
|
|
110
|
+
* Express middleware of the peer examined the token, so a `ProcedureServer` with the HTTP or
|
|
111
|
+
* WebSocket transport of the core and the token of a host policy accepted each request.
|
|
112
|
+
*/
|
|
113
|
+
token?: string | undefined;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* This function gives the principal of a request, or undefined to refuse it with
|
|
117
|
+
* `UNAUTHORIZED`. It runs after the token check. The capability function can read
|
|
118
|
+
* `request.principal`.
|
|
119
|
+
*/
|
|
120
|
+
authenticate?: ((request: ServerRequest<unknown>) => Principal | undefined | Promise<Principal | undefined>) | undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The metadata key of a stream response whose procedure declares `stream: { unbounded: true }`.
|
|
125
|
+
* Its value is "unbounded".
|
|
126
|
+
*/
|
|
127
|
+
export const STREAM_METADATA_KEY = "stream";
|
|
128
|
+
|
|
129
|
+
/** True when a stream response can run without an end (roadmap R15). */
|
|
130
|
+
export function isUnboundedStream(response: { metadata?: Record<string, unknown> | undefined }): boolean {
|
|
131
|
+
return response.metadata?.[STREAM_METADATA_KEY] === "unbounded";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* True when the presented token is the expected one. The comparison takes the same time for each
|
|
136
|
+
* presented token of the same length, so the time does not tell the token.
|
|
137
|
+
*/
|
|
138
|
+
export function tokenMatches(expected: string, presented: string | undefined): boolean {
|
|
139
|
+
if (presented === undefined || presented.length !== expected.length) return false;
|
|
140
|
+
let difference = 0;
|
|
141
|
+
for (let index = 0; index < expected.length; index++) difference |= expected.charCodeAt(index) ^ presented.charCodeAt(index);
|
|
142
|
+
return difference === 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The options of `ProcedureServer.stop()`. */
|
|
146
|
+
export interface StopOptions {
|
|
147
|
+
/**
|
|
148
|
+
* The time for the calls in flight to end (roadmap R14). The server refuses new calls at once
|
|
149
|
+
* with a retryable `UNAVAILABLE`. When the time ends, the server aborts the calls that still
|
|
150
|
+
* run. Absent or 0: the server aborts them at once.
|
|
151
|
+
*/
|
|
152
|
+
grace?: Duration | undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** A signal that aborts when one of the signals aborts. */
|
|
156
|
+
function joinSignals(first: AbortSignal | undefined, second: AbortSignal): AbortSignal {
|
|
157
|
+
if (!first) return second;
|
|
158
|
+
const any = (AbortSignal as { any?: (signals: AbortSignal[]) => AbortSignal }).any;
|
|
159
|
+
if (any) return any([first, second]);
|
|
160
|
+
const controller = new AbortController();
|
|
161
|
+
const abort = (): void => controller.abort();
|
|
162
|
+
if (first.aborted || second.aborted) controller.abort();
|
|
163
|
+
first.addEventListener("abort", abort, { once: true });
|
|
164
|
+
second.addEventListener("abort", abort, { once: true });
|
|
165
|
+
return controller.signal;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** This function waits for a promise, or gives undefined when the signal aborts first. */
|
|
169
|
+
function untilAborted<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T | undefined> {
|
|
170
|
+
if (!signal) return promise;
|
|
171
|
+
if (signal.aborted) return Promise.resolve(undefined);
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
const onAbort = (): void => resolve(undefined);
|
|
174
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
175
|
+
promise.then(
|
|
176
|
+
(value) => {
|
|
177
|
+
signal.removeEventListener("abort", onAbort);
|
|
178
|
+
resolve(value);
|
|
179
|
+
},
|
|
180
|
+
(error: unknown) => {
|
|
181
|
+
signal.removeEventListener("abort", onAbort);
|
|
182
|
+
reject(error);
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
});
|
|
85
186
|
}
|
|
86
187
|
|
|
87
188
|
/** The capability of a request with the remaining time of its caller, when it sends one. */
|
|
@@ -105,6 +206,10 @@ export function errorResponse(id: string, error: unknown): ServerResponse<unknow
|
|
|
105
206
|
const retryable = (error as { retryable?: unknown } | undefined)?.retryable;
|
|
106
207
|
// A refusal of the capability says why (CAPABILITIES-2026-10)
|
|
107
208
|
const reason = error instanceof InvocationError ? error.reason : undefined;
|
|
209
|
+
// An error can ask the client to wait before a retry: retryAfterMs, in milliseconds (R12)
|
|
210
|
+
const retryAfterMs = (error as { retryAfterMs?: unknown } | undefined)?.retryAfterMs;
|
|
211
|
+
const metadata: Record<string, unknown> = reason ? { reason } : {};
|
|
212
|
+
if (typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0) metadata["retry-after"] = retryAfterMs;
|
|
108
213
|
return {
|
|
109
214
|
id,
|
|
110
215
|
status: {
|
|
@@ -113,7 +218,7 @@ export function errorResponse(id: string, error: unknown): ServerResponse<unknow
|
|
|
113
218
|
message: error instanceof Error ? error.message : String(error),
|
|
114
219
|
retryable: typeof retryable === "boolean" ? retryable : false,
|
|
115
220
|
},
|
|
116
|
-
metadata
|
|
221
|
+
metadata,
|
|
117
222
|
};
|
|
118
223
|
}
|
|
119
224
|
|
|
@@ -165,6 +270,22 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
165
270
|
private readonly interceptors: readonly Interceptor[];
|
|
166
271
|
/** The capability options of the requests (CAPABILITIES-2026-10). */
|
|
167
272
|
private readonly capabilityOptions: Pick<ProcedureServerOptions, "capability" | "clearanceOrder" | "resources" | "admitItem">;
|
|
273
|
+
/** The results of keyed calls, or undefined when the option turns them off (R1). */
|
|
274
|
+
private readonly idempotency: IdempotencyCache<ServerResponse<unknown>> | undefined;
|
|
275
|
+
/**
|
|
276
|
+
* The signal of the server: `stop()` aborts it. Each call gets it joined with its request
|
|
277
|
+
* signal (the request and connection scopes) and its deadline (roadmap R14).
|
|
278
|
+
*/
|
|
279
|
+
private lifetime = new AbortController();
|
|
280
|
+
/** True while `stop()` waits for the calls in flight: a new call gets UNAVAILABLE */
|
|
281
|
+
private draining = false;
|
|
282
|
+
/** The calls in flight, a stream until its end */
|
|
283
|
+
private inFlight = 0;
|
|
284
|
+
/** The token of the server, and its authenticate function (R18) */
|
|
285
|
+
private readonly token: string | undefined;
|
|
286
|
+
private readonly authenticate: ProcedureServerOptions["authenticate"];
|
|
287
|
+
/** The functions that `stop()` waits on: each one runs when no call is in flight */
|
|
288
|
+
private idleWaiters: Array<() => void> = [];
|
|
168
289
|
|
|
169
290
|
constructor(options: ProcedureServerOptions = {}) {
|
|
170
291
|
super(options);
|
|
@@ -179,6 +300,9 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
179
300
|
resources: options.resources,
|
|
180
301
|
admitItem: options.admitItem,
|
|
181
302
|
};
|
|
303
|
+
this.idempotency = options.idempotency === false ? undefined : new IdempotencyCache(options.idempotency ?? {});
|
|
304
|
+
this.token = options.token;
|
|
305
|
+
this.authenticate = options.authenticate;
|
|
182
306
|
|
|
183
307
|
// Configure collections
|
|
184
308
|
if (options.collections) {
|
|
@@ -228,12 +352,41 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
228
352
|
/**
|
|
229
353
|
* Configure storage for a collection.
|
|
230
354
|
*
|
|
355
|
+
* Without options, the server uses the new storage at once, and the old storage stays as it
|
|
356
|
+
* is. With options, the change is asynchronous (roadmap R28):
|
|
357
|
+
*
|
|
358
|
+
* - `drain`: the server calls `flush()` of the old storage, then it uses the new storage. A
|
|
359
|
+
* failed flush keeps the old storage and throws. A call that arrives during the flush goes
|
|
360
|
+
* to the old storage.
|
|
361
|
+
* - `close`: the server closes the old storage after the change. The close of a
|
|
362
|
+
* `HybridStorage` sends the writes that arrived during the flush.
|
|
363
|
+
*
|
|
231
364
|
* @param name - Collection name
|
|
232
365
|
* @param storage - Storage backend
|
|
233
|
-
* @
|
|
366
|
+
* @param options - Drain and close the old storage
|
|
367
|
+
* @returns this (for chaining), or a promise of it when options are given
|
|
234
368
|
*/
|
|
235
|
-
useStorage<T>(name: string, storage: CollectionStorage<T>): this
|
|
236
|
-
|
|
369
|
+
useStorage<T>(name: string, storage: CollectionStorage<T>): this;
|
|
370
|
+
useStorage<T>(name: string, storage: CollectionStorage<T>, options: UseStorageOptions): Promise<this>;
|
|
371
|
+
useStorage<T>(name: string, storage: CollectionStorage<T>, options?: UseStorageOptions): this | Promise<this> {
|
|
372
|
+
if (options === undefined) {
|
|
373
|
+
this.storages.set(name, storage as CollectionStorage<unknown>);
|
|
374
|
+
return this;
|
|
375
|
+
}
|
|
376
|
+
return this.replaceStorage(name, storage as CollectionStorage<unknown>, options);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** The change of `useStorage` with options: flush the old storage, change, close the old one. */
|
|
380
|
+
private async replaceStorage(name: string, storage: CollectionStorage<unknown>, options: UseStorageOptions): Promise<this> {
|
|
381
|
+
const old = this.storages.get(name);
|
|
382
|
+
const other = old !== undefined && old !== storage ? old : undefined;
|
|
383
|
+
if (other && options.drain) {
|
|
384
|
+
await other.flush?.();
|
|
385
|
+
}
|
|
386
|
+
this.storages.set(name, storage);
|
|
387
|
+
if (other && options.close) {
|
|
388
|
+
await other.close();
|
|
389
|
+
}
|
|
237
390
|
return this;
|
|
238
391
|
}
|
|
239
392
|
|
|
@@ -333,8 +486,145 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
333
486
|
return procedure ? (request) => this.invoke(procedure, request) : null;
|
|
334
487
|
}
|
|
335
488
|
|
|
336
|
-
/**
|
|
489
|
+
/**
|
|
490
|
+
* Run a procedure for a request. A keyed call to a procedure that is not repeatable runs one
|
|
491
|
+
* time for its key (R1).
|
|
492
|
+
*/
|
|
337
493
|
private async invoke(procedure: AnyProcedure, request: ServerRequest<unknown>): Promise<ServerResponse<unknown>> {
|
|
494
|
+
if (this.draining) {
|
|
495
|
+
// The server stops: a client can send the call to another server, or again later (R14)
|
|
496
|
+
return {
|
|
497
|
+
id: request.id,
|
|
498
|
+
status: { type: "error", code: "UNAVAILABLE", message: "The server is stopping", retryable: true },
|
|
499
|
+
metadata: {},
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
const refusal = await this.admit(request);
|
|
503
|
+
if (refusal) return refusal;
|
|
504
|
+
this.inFlight++;
|
|
505
|
+
let response: ServerResponse<unknown> | undefined;
|
|
506
|
+
try {
|
|
507
|
+
const key = this.idempotency ? idempotencyKeyOf(request.metadata) : undefined;
|
|
508
|
+
response =
|
|
509
|
+
key === undefined || isRepeatable(effectsOf(procedure))
|
|
510
|
+
? await this.run(procedure, request, joinSignals(request.signal, this.lifetime.signal))
|
|
511
|
+
: await this.runKeyed(procedure, request, `${pathToKey(procedure.path)}\u0000${key}`);
|
|
512
|
+
} finally {
|
|
513
|
+
// A stream is in flight until its end; another response is done now
|
|
514
|
+
if (!response?.stream) this.callEnded();
|
|
515
|
+
}
|
|
516
|
+
const stream = response.stream;
|
|
517
|
+
if (stream) {
|
|
518
|
+
const ended = (): void => this.callEnded();
|
|
519
|
+
response = {
|
|
520
|
+
...response,
|
|
521
|
+
stream: (async function* () {
|
|
522
|
+
try {
|
|
523
|
+
yield* stream;
|
|
524
|
+
} finally {
|
|
525
|
+
ended();
|
|
526
|
+
}
|
|
527
|
+
})(),
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
return response;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* The token check and the principal of a request (roadmap R18). A network request (with
|
|
535
|
+
* credentials) must present the token. The authenticate function gives the principal. The
|
|
536
|
+
* function gives an UNAUTHORIZED response, or undefined when the request passes.
|
|
537
|
+
*/
|
|
538
|
+
private async admit(request: ServerRequest<unknown>): Promise<ServerResponse<unknown> | undefined> {
|
|
539
|
+
const unauthorized = (message: string): ServerResponse<unknown> => ({
|
|
540
|
+
id: request.id,
|
|
541
|
+
status: { type: "error", code: "UNAUTHORIZED", message, retryable: false },
|
|
542
|
+
metadata: {},
|
|
543
|
+
});
|
|
544
|
+
if (this.token !== undefined && request.credentials !== undefined) {
|
|
545
|
+
if (!tokenMatches(this.token, request.credentials.bearer)) return unauthorized("Unauthorized: this server requires its token");
|
|
546
|
+
request.principal = { id: "token", via: "token" };
|
|
547
|
+
}
|
|
548
|
+
if (this.authenticate) {
|
|
549
|
+
let principal: Principal | undefined;
|
|
550
|
+
try {
|
|
551
|
+
principal = await this.authenticate(request);
|
|
552
|
+
} catch {
|
|
553
|
+
principal = undefined;
|
|
554
|
+
}
|
|
555
|
+
if (!principal) return unauthorized("Unauthorized: the server did not accept the credentials");
|
|
556
|
+
request.principal = principal;
|
|
557
|
+
}
|
|
558
|
+
return undefined;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** One call in flight ended: `stop()` can go on when it was the last one. */
|
|
562
|
+
private callEnded(): void {
|
|
563
|
+
this.inFlight--;
|
|
564
|
+
if (this.inFlight === 0) {
|
|
565
|
+
const waiters = this.idleWaiters;
|
|
566
|
+
this.idleWaiters = [];
|
|
567
|
+
for (const waiter of waiters) waiter();
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** The number of calls in flight, streams included. */
|
|
572
|
+
get callsInFlight(): number {
|
|
573
|
+
return this.inFlight;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** True while `stop()` waits for the calls in flight: the server refuses new calls. */
|
|
577
|
+
get stopping(): boolean {
|
|
578
|
+
return this.draining;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Run a keyed call one time. The run continues when its caller goes away, so that the next try
|
|
583
|
+
* gets its result: only the deadline of the call and the stop of the server end it. A stream
|
|
584
|
+
* still stops with its caller, because nobody can read it again.
|
|
585
|
+
*/
|
|
586
|
+
private async runKeyed(procedure: AnyProcedure, request: ServerRequest<unknown>, cacheKey: string): Promise<ServerResponse<unknown>> {
|
|
587
|
+
const { result, first } = this.idempotency!.run(
|
|
588
|
+
cacheKey,
|
|
589
|
+
async () => {
|
|
590
|
+
const detached = new AbortController();
|
|
591
|
+
const stopRun = (): void => detached.abort();
|
|
592
|
+
this.lifetime.signal.addEventListener("abort", stopRun, { once: true });
|
|
593
|
+
try {
|
|
594
|
+
const response = await this.run(procedure, request, detached.signal);
|
|
595
|
+
if (response.stream) {
|
|
596
|
+
// A stream follows its caller again
|
|
597
|
+
if (request.signal?.aborted) detached.abort();
|
|
598
|
+
else request.signal?.addEventListener("abort", stopRun, { once: true });
|
|
599
|
+
}
|
|
600
|
+
return response;
|
|
601
|
+
} finally {
|
|
602
|
+
this.lifetime.signal.removeEventListener("abort", stopRun);
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
// A retryable error lets the next try run again: the handler said that a retry is safe
|
|
606
|
+
(response) => !(response.status.type === "error" && response.status.retryable),
|
|
607
|
+
);
|
|
608
|
+
if (first) return result;
|
|
609
|
+
const response = await untilAborted(result, request.signal);
|
|
610
|
+
if (response === undefined) return errorResponse(request.id, new InvocationError("ABORTED", "The request was aborted", procedure.path));
|
|
611
|
+
if (response.stream) {
|
|
612
|
+
return {
|
|
613
|
+
id: request.id,
|
|
614
|
+
status: {
|
|
615
|
+
type: "error",
|
|
616
|
+
code: "DUPLICATE",
|
|
617
|
+
message: "A call with this idempotency key gave a stream, and a stream cannot be given again",
|
|
618
|
+
retryable: false,
|
|
619
|
+
},
|
|
620
|
+
metadata: {},
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
return { ...response, id: request.id };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** Run a procedure for a request with a signal. */
|
|
627
|
+
private async run(procedure: AnyProcedure, request: ServerRequest<unknown>, signal: AbortSignal | undefined): Promise<ServerResponse<unknown>> {
|
|
338
628
|
try {
|
|
339
629
|
// One invocation path for every host (ARCHITECTURE-PROPOSALS P1): input validation, the
|
|
340
630
|
// context, the expose rule for nested calls (BUGS-2026-07 H18) and output validation
|
|
@@ -342,7 +632,7 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
342
632
|
const output = await invokeProcedure(procedure, request.payload, {
|
|
343
633
|
registry: this.procedureRegistry,
|
|
344
634
|
metadata: request.metadata,
|
|
345
|
-
signal
|
|
635
|
+
signal,
|
|
346
636
|
repository: this,
|
|
347
637
|
expose: this.expose,
|
|
348
638
|
strictCalls: this.strictCalls,
|
|
@@ -355,7 +645,14 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
355
645
|
admitItem,
|
|
356
646
|
});
|
|
357
647
|
if (output.kind === "stream") {
|
|
358
|
-
|
|
648
|
+
// The faces that collect a stream read this mark (roadmap R15)
|
|
649
|
+
const unbounded = (procedure.metadata as { stream?: { unbounded?: boolean } } | undefined)?.stream?.unbounded === true;
|
|
650
|
+
return {
|
|
651
|
+
id: request.id,
|
|
652
|
+
status: { type: "success", code: 200 },
|
|
653
|
+
stream: output.items,
|
|
654
|
+
metadata: unbounded ? { [STREAM_METADATA_KEY]: "unbounded" } : {},
|
|
655
|
+
};
|
|
359
656
|
}
|
|
360
657
|
return { id: request.id, status: { type: "success", code: 200 }, payload: output.value, metadata: {} };
|
|
361
658
|
} catch (error) {
|
|
@@ -436,7 +733,24 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
436
733
|
/**
|
|
437
734
|
* Stop server and close storages.
|
|
438
735
|
*/
|
|
439
|
-
override async stop(): Promise<void> {
|
|
736
|
+
override async stop(options: StopOptions = {}): Promise<void> {
|
|
737
|
+
const graceMs = optionalMs(options.grace, "grace") ?? TIMER_DEFAULTS.server.stopGraceMs;
|
|
738
|
+
// A graceful stop: new calls get UNAVAILABLE, and the calls in flight have the grace time
|
|
739
|
+
// to end (roadmap R14). Before, the transports aborted every call at once.
|
|
740
|
+
this.draining = true;
|
|
741
|
+
if (graceMs > 0 && this.inFlight > 0) {
|
|
742
|
+
await new Promise<void>((resolve) => {
|
|
743
|
+
const timer = setTimeout(resolve, graceMs);
|
|
744
|
+
this.idleWaiters.push(() => {
|
|
745
|
+
clearTimeout(timer);
|
|
746
|
+
resolve();
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
this.lifetime.abort();
|
|
751
|
+
// A server can start again: its next calls get a new signal
|
|
752
|
+
this.lifetime = new AbortController();
|
|
753
|
+
this.draining = false;
|
|
440
754
|
await super.stop();
|
|
441
755
|
await this.closeStorages();
|
|
442
756
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The credentials of a network request (roadmap R18). A server transport reads them into
|
|
3
|
+
* `ServerRequest.credentials`, and leaves the secret headers out of the metadata, so a handler
|
|
4
|
+
* and a nested call do not see them. `ProcedureServer` examines them.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Credentials } from "./types.js";
|
|
8
|
+
|
|
9
|
+
/** The headers that carry a secret. They do not go into the metadata of a request. */
|
|
10
|
+
const SECRET_HEADERS = new Set(["authorization", "proxy-authorization", "cookie", "x-api-key"]);
|
|
11
|
+
|
|
12
|
+
/** The headers without the secret ones, and without the headers that are not text. */
|
|
13
|
+
export function withoutSecretHeaders(headers: Record<string, unknown>): Record<string, string> {
|
|
14
|
+
const out: Record<string, string> = {};
|
|
15
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
16
|
+
if (!SECRET_HEADERS.has(key.toLowerCase()) && typeof value === "string") out[key] = value;
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The token of an `Authorization: Bearer <token>` header, or undefined. */
|
|
22
|
+
export function bearerOf(header: unknown): string | undefined {
|
|
23
|
+
return typeof header === "string" && header.startsWith("Bearer ") ? header.slice("Bearer ".length) : undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The credentials of a request or of a WebSocket upgrade: the bearer token of its
|
|
28
|
+
* `Authorization` header, or else of its `token` query (a browser WebSocket cannot set headers).
|
|
29
|
+
*/
|
|
30
|
+
export function credentialsOf(request: { headers: Record<string, unknown>; url?: string | undefined }): Credentials {
|
|
31
|
+
const bearer = bearerOf(request.headers["authorization"]);
|
|
32
|
+
if (bearer !== undefined) return { bearer };
|
|
33
|
+
const query = new URL(request.url ?? "/", "http://localhost").searchParams.get("token");
|
|
34
|
+
return query === null ? {} : { bearer: query };
|
|
35
|
+
}
|
package/src/server/types.ts
CHANGED
|
@@ -29,6 +29,43 @@ export interface ServerRequest<TReq = unknown> {
|
|
|
29
29
|
|
|
30
30
|
/** Abort signal for cancellation */
|
|
31
31
|
signal?: AbortSignal;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The credentials that a network transport read from the request: the bearer token of an
|
|
35
|
+
* `Authorization` header, or of the `token` query of a WebSocket connection (roadmap R18). They
|
|
36
|
+
* are not in the metadata, so a handler and a nested call do not see them. A request without
|
|
37
|
+
* credentials comes from this process (a direct `handle()`, MCP over stdio).
|
|
38
|
+
*/
|
|
39
|
+
credentials?: Credentials | undefined;
|
|
40
|
+
|
|
41
|
+
/** The principal of the request: who sends it. `ProcedureServer` sets it when it authenticates the request. */
|
|
42
|
+
principal?: Principal | undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The credentials of a request. */
|
|
46
|
+
export interface Credentials {
|
|
47
|
+
/** The bearer token that the request presents. */
|
|
48
|
+
readonly bearer?: string | undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Who sends a request (roadmap R18). */
|
|
52
|
+
export interface Principal {
|
|
53
|
+
/** The id of the principal. With the token of a host: "token". */
|
|
54
|
+
readonly id: string;
|
|
55
|
+
/** How the server authenticated it: "token", or the name of an authenticate function. */
|
|
56
|
+
readonly via: string;
|
|
57
|
+
/** More facts about the principal, from an authenticate function. */
|
|
58
|
+
readonly claims?: Readonly<Record<string, unknown>> | undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The contract of a server transport (roadmap R18): a transport turns each protocol request
|
|
63
|
+
* into a `ServerRequest` (with its signal and its credentials), gives it to `handle()`, and turns
|
|
64
|
+
* the `ServerResponse` back (a value, a stream or an error). `Server` and `ProcedureServer`
|
|
65
|
+
* satisfy it, and a test can give a function.
|
|
66
|
+
*/
|
|
67
|
+
export interface RequestHandler {
|
|
68
|
+
handle(request: ServerRequest): Promise<ServerResponse>;
|
|
32
69
|
}
|
|
33
70
|
|
|
34
71
|
/**
|
package/src/timers.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The timer settings of the core, as data (roadmap R34).
|
|
3
|
+
*
|
|
4
|
+
* Each default time of the middleware, the transports and the server comes from this object, so
|
|
5
|
+
* a tool can read the settings without a call. `@procedures/systems` binds them to the symbols of
|
|
6
|
+
* the timer law of `systems`, and examines rules such as "the time of one try is less than the
|
|
7
|
+
* request time of the WebSocket". The values are milliseconds. The options of an application
|
|
8
|
+
* are Durations (roadmap R11), and they replace these values.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** The default times of the core, in milliseconds. */
|
|
12
|
+
export interface TimerDefaults {
|
|
13
|
+
readonly retry: {
|
|
14
|
+
/** The tries after the first one. */
|
|
15
|
+
readonly maxRetries: number;
|
|
16
|
+
readonly retryDelayMs: number;
|
|
17
|
+
readonly backoffMultiplier: number;
|
|
18
|
+
/** The longest backoff. Infinity: no limit. */
|
|
19
|
+
readonly maxDelayMs: number;
|
|
20
|
+
};
|
|
21
|
+
readonly circuitBreaker: {
|
|
22
|
+
readonly failureWindowMs: number;
|
|
23
|
+
readonly resetTimeoutMs: number;
|
|
24
|
+
};
|
|
25
|
+
readonly cache: {
|
|
26
|
+
readonly ttlMs: number;
|
|
27
|
+
readonly statsIntervalMs: number;
|
|
28
|
+
};
|
|
29
|
+
readonly rateLimit: {
|
|
30
|
+
readonly windowMs: number;
|
|
31
|
+
};
|
|
32
|
+
readonly webSocketClient: {
|
|
33
|
+
/** The time of the handshake. */
|
|
34
|
+
readonly connectionTimeoutMs: number;
|
|
35
|
+
/** The time until the first frame of a call. */
|
|
36
|
+
readonly requestTimeoutMs: number;
|
|
37
|
+
readonly heartbeatIntervalMs: number;
|
|
38
|
+
readonly heartbeatTimeoutMs: number;
|
|
39
|
+
/** The pongs that can go missing, one after the other, before the connection closes. */
|
|
40
|
+
readonly heartbeatMisses: number;
|
|
41
|
+
readonly reconnectInitialDelayMs: number;
|
|
42
|
+
readonly reconnectMaxDelayMs: number;
|
|
43
|
+
};
|
|
44
|
+
readonly webSocketServer: {
|
|
45
|
+
readonly heartbeatIntervalMs: number;
|
|
46
|
+
readonly heartbeatMisses: number;
|
|
47
|
+
/** The time until the first frame of a call of the server to a client. */
|
|
48
|
+
readonly reverseRequestTimeoutMs: number;
|
|
49
|
+
};
|
|
50
|
+
readonly idempotency: {
|
|
51
|
+
/** The time that the server keeps the result of a keyed write. */
|
|
52
|
+
readonly ttlMs: number;
|
|
53
|
+
};
|
|
54
|
+
readonly server: {
|
|
55
|
+
/** The time that `ProcedureServer.stop()` waits for the calls that run. */
|
|
56
|
+
readonly stopGraceMs: number;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The default times of the core. The middleware, the transports and the server read them. */
|
|
61
|
+
export const TIMER_DEFAULTS: TimerDefaults = Object.freeze({
|
|
62
|
+
retry: Object.freeze({ maxRetries: 3, retryDelayMs: 1000, backoffMultiplier: 2, maxDelayMs: Number.POSITIVE_INFINITY }),
|
|
63
|
+
circuitBreaker: Object.freeze({ failureWindowMs: 10_000, resetTimeoutMs: 30_000 }),
|
|
64
|
+
cache: Object.freeze({ ttlMs: 60_000, statsIntervalMs: 60_000 }),
|
|
65
|
+
rateLimit: Object.freeze({ windowMs: 60_000 }),
|
|
66
|
+
webSocketClient: Object.freeze({
|
|
67
|
+
connectionTimeoutMs: 10_000,
|
|
68
|
+
requestTimeoutMs: 30_000,
|
|
69
|
+
// 20 s: with the pong timeout of 5 s, the client finds a dead connection after 25 s, before a
|
|
70
|
+
// call on it reaches the request time of 30 s. Before, 30 s gave 35 s (the timer law, R34).
|
|
71
|
+
heartbeatIntervalMs: 20_000,
|
|
72
|
+
heartbeatTimeoutMs: 5_000,
|
|
73
|
+
heartbeatMisses: 1,
|
|
74
|
+
reconnectInitialDelayMs: 1_000,
|
|
75
|
+
reconnectMaxDelayMs: 30_000,
|
|
76
|
+
}),
|
|
77
|
+
// 15 s: with 2 misses, the server finds a dead client after 30 s, not later than a call of the
|
|
78
|
+
// server to it reaches its request time of 30 s. Before, 30 s gave 60 s (the timer law, R34).
|
|
79
|
+
webSocketServer: Object.freeze({ heartbeatIntervalMs: 15_000, heartbeatMisses: 2, reverseRequestTimeoutMs: 30_000 }),
|
|
80
|
+
idempotency: Object.freeze({ ttlMs: 10 * 60_000 }),
|
|
81
|
+
server: Object.freeze({ stopGraceMs: 0 }),
|
|
82
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The W3C trace context (roadmap R16): `traceparent` is `00-<trace id>-<span id>-<flags>`, with a
|
|
3
|
+
* trace id of 32 hex digits and a span id of 16. The tracing middleware of a client sends it in
|
|
4
|
+
* the metadata of a call (and HTTP as the `traceparent` header). The tracing interceptor of a
|
|
5
|
+
* server continues that trace, and gives it to the nested calls, so one trace goes from the
|
|
6
|
+
* client to the last handler. Before, the client and the server made two traces.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** The metadata key of the W3C trace context of a call. */
|
|
10
|
+
export const TRACEPARENT_METADATA_KEY = "traceparent";
|
|
11
|
+
|
|
12
|
+
const TRACEPARENT = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
|
|
13
|
+
const ZERO_TRACE = "0".repeat(32);
|
|
14
|
+
const ZERO_SPAN = "0".repeat(16);
|
|
15
|
+
|
|
16
|
+
/** A trace context: the trace, the span of the caller, and the sampled flag. */
|
|
17
|
+
export interface TraceContext {
|
|
18
|
+
readonly traceId: string;
|
|
19
|
+
readonly spanId: string;
|
|
20
|
+
readonly sampled: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function randomHex(bytes: number): string {
|
|
24
|
+
const values = new Uint8Array(bytes);
|
|
25
|
+
const crypto = (globalThis as { crypto?: { getRandomValues?: (array: Uint8Array) => Uint8Array } }).crypto;
|
|
26
|
+
if (crypto?.getRandomValues) crypto.getRandomValues(values);
|
|
27
|
+
else for (let index = 0; index < bytes; index++) values[index] = Math.floor(Math.random() * 256);
|
|
28
|
+
return Array.from(values, (value) => value.toString(16).padStart(2, "0")).join("");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** This function makes a new trace id: 32 random hex digits. */
|
|
32
|
+
export function newTraceId(): string {
|
|
33
|
+
const id = randomHex(16);
|
|
34
|
+
return id === ZERO_TRACE ? newTraceId() : id;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** This function makes a new span id: 16 random hex digits. */
|
|
38
|
+
export function newSpanId(): string {
|
|
39
|
+
const id = randomHex(8);
|
|
40
|
+
return id === ZERO_SPAN ? newSpanId() : id;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** True when the ids have the W3C form. */
|
|
44
|
+
export function isW3cIds(traceId: string, spanId: string): boolean {
|
|
45
|
+
return /^[0-9a-f]{32}$/.test(traceId) && /^[0-9a-f]{16}$/.test(spanId) && traceId !== ZERO_TRACE && spanId !== ZERO_SPAN;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The `traceparent` of a trace and a span. */
|
|
49
|
+
export function formatTraceparent(traceId: string, spanId: string, sampled = true): string {
|
|
50
|
+
return `00-${traceId}-${spanId}-${sampled ? "01" : "00"}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The trace context of a `traceparent` value, or undefined when it is not valid. */
|
|
54
|
+
export function parseTraceparent(value: unknown): TraceContext | undefined {
|
|
55
|
+
if (typeof value !== "string") return undefined;
|
|
56
|
+
const match = TRACEPARENT.exec(value.trim().toLowerCase());
|
|
57
|
+
if (!match) return undefined;
|
|
58
|
+
const [, traceId, spanId, flags] = match;
|
|
59
|
+
if (traceId === ZERO_TRACE || spanId === ZERO_SPAN) return undefined;
|
|
60
|
+
return { traceId: traceId!, spanId: spanId!, sampled: (parseInt(flags!, 16) & 1) === 1 };
|
|
61
|
+
}
|