@procedures/client 0.2.3 → 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 +65 -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 +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.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 +57 -17
- 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 +16 -0
- 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 +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 +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 +73 -20
- 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 +16 -0
- 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
|
@@ -8,14 +8,16 @@
|
|
|
8
8
|
|
|
9
9
|
import { InvocationError, type Interceptor, type Invocation, type ProcedureOutput } from "./invoke.js";
|
|
10
10
|
import { pathToKey, type ProcedurePath } from "./types.js";
|
|
11
|
+
import { toMs, type Duration } from "../duration.js";
|
|
12
|
+
import { TRACEPARENT_METADATA_KEY, formatTraceparent, newSpanId, newTraceId, parseTraceparent } from "../tracing/traceparent.js";
|
|
11
13
|
|
|
12
14
|
// =============================================================================
|
|
13
15
|
// Timeout
|
|
14
16
|
// =============================================================================
|
|
15
17
|
|
|
16
18
|
export interface TimeoutInterceptorOptions {
|
|
17
|
-
/** The time limit
|
|
18
|
-
|
|
19
|
+
/** The time limit, for example `minutes(10)`. 0 or less: no limit. */
|
|
20
|
+
limit: Duration;
|
|
19
21
|
/**
|
|
20
22
|
* The invocations to limit. The default is the calls of the host (depth 0): a nested call gets
|
|
21
23
|
* the signal of its parent, so the limit of the parent ends it too.
|
|
@@ -76,6 +78,14 @@ async function* itemsUntil<T>(
|
|
|
76
78
|
* result is not used. For a stream, the limit covers the whole stream.
|
|
77
79
|
*/
|
|
78
80
|
export function timeoutInterceptor(options: TimeoutInterceptorOptions): Interceptor {
|
|
81
|
+
return timeoutAfterMs({ ...options, ms: toMs(options.limit, "limit") });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The timeout interceptor with a limit in milliseconds. The invocation uses it for the deadline
|
|
86
|
+
* of a capability, which is a time in milliseconds.
|
|
87
|
+
*/
|
|
88
|
+
export function timeoutAfterMs(options: Omit<TimeoutInterceptorOptions, "limit"> & { ms: number }): Interceptor {
|
|
79
89
|
const { ms } = options;
|
|
80
90
|
const appliesTo = options.appliesTo ?? ((invocation: Invocation) => invocation.depth === 0);
|
|
81
91
|
return async (invocation, next) => {
|
|
@@ -119,9 +129,14 @@ export function timeoutInterceptor(options: TimeoutInterceptorOptions): Intercep
|
|
|
119
129
|
/** One invocation as a span of a trace. */
|
|
120
130
|
export interface InvocationSpan {
|
|
121
131
|
readonly id: string;
|
|
122
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* The span of the invocation whose handler made this call. A call of a host has the span of its
|
|
134
|
+
* remote caller, when the caller sent a `traceparent` (roadmap R16), or none.
|
|
135
|
+
*/
|
|
123
136
|
readonly parentId: string | undefined;
|
|
124
|
-
/**
|
|
137
|
+
/** True when the parent span is in another process: the caller of a host. */
|
|
138
|
+
readonly remoteParent?: boolean | undefined;
|
|
139
|
+
/** The same for every span of one call of a host, and of its caller when it sent a trace. */
|
|
125
140
|
readonly traceId: string;
|
|
126
141
|
readonly path: ProcedurePath;
|
|
127
142
|
readonly key: string;
|
|
@@ -149,7 +164,6 @@ export interface TracingInterceptorOptions {
|
|
|
149
164
|
now?: (() => number) | undefined;
|
|
150
165
|
}
|
|
151
166
|
|
|
152
|
-
let traceCount = 0;
|
|
153
167
|
|
|
154
168
|
/**
|
|
155
169
|
* This interceptor makes a span for each invocation, with the span of its parent. A host can
|
|
@@ -157,8 +171,6 @@ let traceCount = 0;
|
|
|
157
171
|
*/
|
|
158
172
|
export function tracingInterceptor(options: TracingInterceptorOptions): Interceptor {
|
|
159
173
|
const now = options.now ?? (() => performance.now());
|
|
160
|
-
const prefix = `t${++traceCount}-${Math.random().toString(36).slice(2, 8)}`;
|
|
161
|
-
let spanCount = 0;
|
|
162
174
|
/** The open spans, by the id of their invocation */
|
|
163
175
|
const open = new Map<number, InvocationSpan>();
|
|
164
176
|
|
|
@@ -179,11 +191,15 @@ export function tracingInterceptor(options: TracingInterceptorOptions): Intercep
|
|
|
179
191
|
|
|
180
192
|
return async (invocation, next) => {
|
|
181
193
|
const parent = invocation.parent ? open.get(invocation.parent.id) : undefined;
|
|
182
|
-
|
|
194
|
+
// A call of a host continues the trace of its caller (W3C traceparent, roadmap R16)
|
|
195
|
+
const remote = parent ? undefined : parseTraceparent(invocation.metadata[TRACEPARENT_METADATA_KEY]);
|
|
196
|
+
// W3C span ids, so a trace backend can join the spans of the client and of the server
|
|
197
|
+
const id = newSpanId();
|
|
183
198
|
const span: InvocationSpan = {
|
|
184
199
|
id,
|
|
185
|
-
parentId: parent?.id,
|
|
186
|
-
|
|
200
|
+
parentId: parent?.id ?? remote?.spanId,
|
|
201
|
+
...(remote ? { remoteParent: true } : {}),
|
|
202
|
+
traceId: parent?.traceId ?? remote?.traceId ?? newTraceId(),
|
|
187
203
|
path: invocation.path,
|
|
188
204
|
key: pathToKey(invocation.path),
|
|
189
205
|
depth: invocation.depth,
|
|
@@ -197,7 +213,8 @@ export function tracingInterceptor(options: TracingInterceptorOptions): Intercep
|
|
|
197
213
|
|
|
198
214
|
let output: ProcedureOutput<unknown>;
|
|
199
215
|
try {
|
|
200
|
-
|
|
216
|
+
// The nested calls (and their remote calls) carry the span of this invocation (R16)
|
|
217
|
+
output = await next({ metadata: { ...invocation.metadata, [TRACEPARENT_METADATA_KEY]: formatTraceparent(span.traceId, id) } });
|
|
201
218
|
} catch (error) {
|
|
202
219
|
finish(span, invocation.id, error);
|
|
203
220
|
throw error;
|
package/src/procedures/invoke.ts
CHANGED
|
@@ -39,7 +39,7 @@ import { PROCEDURE_REGISTRY } from "./registry.js";
|
|
|
39
39
|
import { nestedCallRefusal } from "./policy.js";
|
|
40
40
|
import { scopeOf, withScope, type HydrationEvent } from "./ref.js";
|
|
41
41
|
import { UNLIMITED_CAPABILITY, capabilityRefusal, examinesNestedCall, narrowCapability, type CapabilityRefusalReason } from "./capability.js";
|
|
42
|
-
import {
|
|
42
|
+
import { timeoutAfterMs } from "./interceptors.js";
|
|
43
43
|
import type { EventBus } from "../events/types.js";
|
|
44
44
|
import { getGlobalEventBus, withEventBusSignal } from "../events/bus.js";
|
|
45
45
|
|
|
@@ -61,16 +61,29 @@ export type InvocationErrorCode =
|
|
|
61
61
|
/** The capability of the call does not permit it (CAPABILITIES-2026-10). `reason` tells why. */
|
|
62
62
|
| "FORBIDDEN";
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* The reason of a `FORBIDDEN` error. It is a refusal of the capability, or a refusal of a module
|
|
66
|
+
* that the host configured:
|
|
67
|
+
* - `outside-root`: the input has a path outside the root of the host (`createFsModule({ root })`
|
|
68
|
+
* of `@procedures/fs`).
|
|
69
|
+
* - `host-not-allowed`: the input has a URL whose host the host does not allow, or a redirect goes
|
|
70
|
+
* to such a host (`createHttpModule({ allow })` of `@procedures/http`).
|
|
71
|
+
*/
|
|
72
|
+
export type ForbiddenReason = CapabilityRefusalReason | "outside-root" | "host-not-allowed";
|
|
73
|
+
|
|
64
74
|
/** An error of the invocation itself. An error that the handler throws passes through unchanged. */
|
|
65
75
|
export class InvocationError extends Error {
|
|
66
76
|
override readonly name = "InvocationError";
|
|
67
77
|
readonly code: InvocationErrorCode;
|
|
68
78
|
readonly path: ProcedurePath;
|
|
69
79
|
readonly retryable: boolean = false;
|
|
70
|
-
/**
|
|
71
|
-
|
|
80
|
+
/**
|
|
81
|
+
* The reason of a `FORBIDDEN` error: `effects-undeclared`, `effects-exceeded`, `clearance`,
|
|
82
|
+
* `deadline`, `outside-root` or `host-not-allowed`.
|
|
83
|
+
*/
|
|
84
|
+
readonly reason: ForbiddenReason | undefined;
|
|
72
85
|
|
|
73
|
-
constructor(code: InvocationErrorCode, message: string, path: ProcedurePath, reason?:
|
|
86
|
+
constructor(code: InvocationErrorCode, message: string, path: ProcedurePath, reason?: ForbiddenReason) {
|
|
74
87
|
super(message);
|
|
75
88
|
this.code = code;
|
|
76
89
|
this.path = path;
|
|
@@ -304,7 +317,7 @@ export async function invokeProcedure<TOutput = unknown>(
|
|
|
304
317
|
const deadline = invocation.capability.deadline;
|
|
305
318
|
const ownDeadline = deadline !== undefined && options.parent?.capability.deadline !== deadline;
|
|
306
319
|
const interceptors = ownDeadline
|
|
307
|
-
? [
|
|
320
|
+
? [timeoutAfterMs({ ms: Math.max(0, deadline - Date.now()), appliesTo: () => true }), ...(options.interceptors ?? [])]
|
|
308
321
|
: (options.interceptors ?? []);
|
|
309
322
|
if (interceptors.length === 0) return runInvocation<TOutput>(invocation, options);
|
|
310
323
|
|
|
@@ -468,6 +481,7 @@ export function createProcedureContext(
|
|
|
468
481
|
resources: invocation && options.resources ? options.resources(invocation) : (EMPTY_RESOURCES as ResourcesOf),
|
|
469
482
|
};
|
|
470
483
|
if (options.signal) context.signal = options.signal;
|
|
484
|
+
if (options.expose) context.expose = options.expose;
|
|
471
485
|
if (options.repository) context.repository = options.repository;
|
|
472
486
|
if (options.onRef) context.onRef = options.onRef;
|
|
473
487
|
// Every context has a bus. With a signal, the waits of the handler on the bus end when the
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One manifest type (roadmap R17).
|
|
3
|
+
*
|
|
4
|
+
* Before, ten systems described procedures, with three manifest formats, three version schemes
|
|
5
|
+
* and no shared type: `procedure.get` gave booleans for the schemas, `manifest.generate` gave
|
|
6
|
+
* JSON Schemas but no effects, `_discovery.announce` gave no schemas, the MCP tool list gave the
|
|
7
|
+
* effects as hints, and none of them obeyed the expose rule of the host. This module is the one
|
|
8
|
+
* description: a function of a procedure, and a function of a registry with the expose rule.
|
|
9
|
+
* The introspection procedures, the compiler (R20) and the SDK generators read it.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { pathToKey, type AnyProcedure, type Effects, type ProcedurePath } from "./types.js";
|
|
13
|
+
import { jsonSchemaOf, type JsonSchema } from "./schema.js";
|
|
14
|
+
import { isDataDriven } from "./ref.js";
|
|
15
|
+
import { isRuntimeDefined } from "./runtime-programs.js";
|
|
16
|
+
|
|
17
|
+
/** The format of a manifest. A change that is not compatible makes a new format. */
|
|
18
|
+
export const MANIFEST_FORMAT = "procedures.manifest/1";
|
|
19
|
+
|
|
20
|
+
/** The command-line metadata of a procedure: how `mark` reads its arguments and prints its result. */
|
|
21
|
+
export interface CliDescription {
|
|
22
|
+
readonly args?: readonly string[] | undefined;
|
|
23
|
+
readonly shorts?: Readonly<Record<string, string>> | undefined;
|
|
24
|
+
readonly output?: string | undefined;
|
|
25
|
+
readonly interactive?: boolean | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The description of one procedure. */
|
|
29
|
+
export interface ProcedureEntry {
|
|
30
|
+
readonly path: ProcedurePath;
|
|
31
|
+
readonly key: string;
|
|
32
|
+
readonly description?: string | undefined;
|
|
33
|
+
readonly tags?: readonly string[] | undefined;
|
|
34
|
+
/** The declared effects. Absent: not declared. */
|
|
35
|
+
readonly effects?: Effects | undefined;
|
|
36
|
+
/** The JSON Schema of the input (draft-07). */
|
|
37
|
+
readonly input?: JsonSchema | undefined;
|
|
38
|
+
/** The JSON Schema of the output (draft-07). */
|
|
39
|
+
readonly output?: JsonSchema | undefined;
|
|
40
|
+
/** The procedures that a procedure of code calls (`metadata.calls`). */
|
|
41
|
+
readonly calls?: readonly string[] | undefined;
|
|
42
|
+
/** True: the stream of the procedure can run without an end (R15). */
|
|
43
|
+
readonly unboundedStream?: boolean | undefined;
|
|
44
|
+
/** True: the procedure runs its input as a program (control flow, a defined procedure). */
|
|
45
|
+
readonly dataDriven: boolean;
|
|
46
|
+
/** True: `procedure.define` made it at run time. */
|
|
47
|
+
readonly runtime: boolean;
|
|
48
|
+
/** True: the procedure has a handler, so it can run here. */
|
|
49
|
+
readonly hasHandler: boolean;
|
|
50
|
+
readonly deprecated?: boolean | undefined;
|
|
51
|
+
readonly cli?: CliDescription | undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The description of the procedures of a registry. */
|
|
55
|
+
export interface ProcedureManifest {
|
|
56
|
+
readonly format: typeof MANIFEST_FORMAT;
|
|
57
|
+
/** A hash of the entries: two manifests with the same hash describe the same procedures. */
|
|
58
|
+
readonly hash: string;
|
|
59
|
+
readonly procedures: readonly ProcedureEntry[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The options of `describeProcedure` and `describeRegistry`. */
|
|
63
|
+
export interface DescribeOptions {
|
|
64
|
+
/** False leaves out the JSON Schemas. The default is true. */
|
|
65
|
+
readonly schemas?: boolean | undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The options of `describeRegistry`. */
|
|
69
|
+
export interface DescribeRegistryOptions extends DescribeOptions {
|
|
70
|
+
/** The expose rule of the host: a procedure outside it is not in the manifest. */
|
|
71
|
+
readonly expose?: ((path: ProcedurePath) => boolean) | undefined;
|
|
72
|
+
/** More filtering: a procedure for which it gives false is not in the manifest. */
|
|
73
|
+
readonly include?: ((procedure: AnyProcedure) => boolean) | undefined;
|
|
74
|
+
/** True keeps the procedures without a handler (stubs of a remote host). The default is false. */
|
|
75
|
+
readonly withoutHandler?: boolean | undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** This function describes one procedure. */
|
|
79
|
+
export function describeProcedure(procedure: AnyProcedure, options: DescribeOptions = {}): ProcedureEntry {
|
|
80
|
+
const meta = (procedure.metadata ?? {}) as Record<string, unknown>;
|
|
81
|
+
const schemas = options.schemas !== false;
|
|
82
|
+
const effects = meta["effects"];
|
|
83
|
+
const calls = meta["calls"];
|
|
84
|
+
const cli: CliDescription = {
|
|
85
|
+
...(Array.isArray(meta["args"]) ? { args: meta["args"] as string[] } : {}),
|
|
86
|
+
...(meta["shorts"] && typeof meta["shorts"] === "object" ? { shorts: meta["shorts"] as Record<string, string> } : {}),
|
|
87
|
+
...(typeof meta["output"] === "string" ? { output: meta["output"] } : {}),
|
|
88
|
+
...(typeof meta["interactive"] === "boolean" ? { interactive: meta["interactive"] } : {}),
|
|
89
|
+
};
|
|
90
|
+
const input = schemas ? jsonSchemaOf(procedure.input, { io: "input" }) : undefined;
|
|
91
|
+
const output = schemas ? jsonSchemaOf(procedure.output, { io: "output" }) : undefined;
|
|
92
|
+
return {
|
|
93
|
+
path: procedure.path,
|
|
94
|
+
key: pathToKey(procedure.path),
|
|
95
|
+
...(typeof meta["description"] === "string" ? { description: meta["description"] } : {}),
|
|
96
|
+
...(Array.isArray(meta["tags"]) ? { tags: meta["tags"] as string[] } : {}),
|
|
97
|
+
...(effects && typeof effects === "object" ? { effects: effects as Effects } : {}),
|
|
98
|
+
...(input ? { input } : {}),
|
|
99
|
+
...(output ? { output } : {}),
|
|
100
|
+
...(Array.isArray(calls) ? { calls: calls as string[] } : {}),
|
|
101
|
+
...((meta["stream"] as { unbounded?: unknown } | undefined)?.unbounded === true ? { unboundedStream: true } : {}),
|
|
102
|
+
dataDriven: isDataDriven(procedure),
|
|
103
|
+
runtime: isRuntimeDefined(procedure),
|
|
104
|
+
hasHandler: typeof procedure.handler === "function",
|
|
105
|
+
...(meta["deprecated"] === true ? { deprecated: true } : {}),
|
|
106
|
+
...(Object.keys(cli).length > 0 ? { cli } : {}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The procedures of a registry, as the expose rule of the host shows them. */
|
|
111
|
+
export function exposedProcedures(
|
|
112
|
+
registry: { getAll(): AnyProcedure[] },
|
|
113
|
+
options: Pick<DescribeRegistryOptions, "expose" | "include" | "withoutHandler"> = {},
|
|
114
|
+
): AnyProcedure[] {
|
|
115
|
+
return registry
|
|
116
|
+
.getAll()
|
|
117
|
+
.filter(
|
|
118
|
+
(procedure) =>
|
|
119
|
+
(options.withoutHandler === true || typeof procedure.handler === "function") &&
|
|
120
|
+
(!options.expose || options.expose(procedure.path)) &&
|
|
121
|
+
(!options.include || options.include(procedure)),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* This function describes the procedures of a registry that the expose rule shows, in the order
|
|
127
|
+
* of their keys, with a hash of the description.
|
|
128
|
+
*/
|
|
129
|
+
export function describeRegistry(registry: { getAll(): AnyProcedure[] }, options: DescribeRegistryOptions = {}): ProcedureManifest {
|
|
130
|
+
const procedures = exposedProcedures(registry, options)
|
|
131
|
+
.map((procedure) => describeProcedure(procedure, options))
|
|
132
|
+
.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
133
|
+
return { format: MANIFEST_FORMAT, hash: hashOf(procedures), procedures };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A stable JSON text: the keys of each object in order. */
|
|
137
|
+
function stableJson(value: unknown): string {
|
|
138
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
139
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
140
|
+
const object = value as Record<string, unknown>;
|
|
141
|
+
return `{${Object.keys(object)
|
|
142
|
+
.sort()
|
|
143
|
+
.filter((key) => object[key] !== undefined)
|
|
144
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(object[key])}`)
|
|
145
|
+
.join(",")}}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** A 64-bit FNV-1a hash of the stable JSON of a value, as 16 hex digits. */
|
|
149
|
+
export function hashOf(value: unknown): string {
|
|
150
|
+
const text = stableJson(value);
|
|
151
|
+
let hash = 0xcbf29ce484222325n;
|
|
152
|
+
const prime = 0x100000001b3n;
|
|
153
|
+
for (let index = 0; index < text.length; index++) {
|
|
154
|
+
hash ^= BigInt(text.charCodeAt(index));
|
|
155
|
+
hash = (hash * prime) & 0xffffffffffffffffn;
|
|
156
|
+
}
|
|
157
|
+
return hash.toString(16).padStart(16, "0");
|
|
158
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The handlers that `procedure.define` made, with the program (the aggregation) of each. The
|
|
3
|
+
* registry is the only store of the procedures: a procedure is runtime-defined when its handler
|
|
4
|
+
* is a key here. (Before, a separate map kept them, and `procedure.delete` removed only the map
|
|
5
|
+
* entry: deep dive CORE-3.) The program is kept here, not only in the metadata: a caller that
|
|
6
|
+
* gives its own metadata to `procedure.define` replaces the metadata.
|
|
7
|
+
*
|
|
8
|
+
* This module imports nothing, so the manifest (R17) and `procedure.define` can both use it
|
|
9
|
+
* without an import cycle.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const RUNTIME_PROGRAMS = new WeakMap<object, unknown>();
|
|
13
|
+
|
|
14
|
+
/** This function records the program of a handler that `procedure.define` made. */
|
|
15
|
+
export function markRuntimeProgram(handler: object, program: unknown): void {
|
|
16
|
+
RUNTIME_PROGRAMS.set(handler, program);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** True for a procedure that `procedure.define` made. */
|
|
20
|
+
export function isRuntimeDefined(procedure: { handler?: unknown }): boolean {
|
|
21
|
+
return typeof procedure.handler === "function" && RUNTIME_PROGRAMS.has(procedure.handler);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The program of a handler that `procedure.define` made, or undefined. */
|
|
25
|
+
export function runtimeProgramOfHandler(handler: unknown): unknown {
|
|
26
|
+
return typeof handler === "function" ? RUNTIME_PROGRAMS.get(handler) : undefined;
|
|
27
|
+
}
|
package/src/procedures/types.ts
CHANGED
|
@@ -75,6 +75,14 @@ export interface ProcedureMetadata {
|
|
|
75
75
|
*/
|
|
76
76
|
effects?: Effects;
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* The stream of the procedure (roadmap R15). `unbounded: true`: the stream can run without an
|
|
80
|
+
* end (a `follow` of logs, the output of a server process). A face that collects a stream
|
|
81
|
+
* refuses such a call or stops it at a limit: HTTP without NDJSON refuses it, and MCP stops it
|
|
82
|
+
* at its stream limits.
|
|
83
|
+
*/
|
|
84
|
+
stream?: { readonly unbounded?: boolean } | undefined;
|
|
85
|
+
|
|
78
86
|
/** Custom metadata fields */
|
|
79
87
|
[key: string]: unknown;
|
|
80
88
|
}
|
|
@@ -260,6 +268,14 @@ export interface ProcedureContext {
|
|
|
260
268
|
*/
|
|
261
269
|
registry?: ProcedureRegistryLike;
|
|
262
270
|
|
|
271
|
+
/**
|
|
272
|
+
* The expose rule of the host: the procedures that its callers reach. The introspection
|
|
273
|
+
* procedures (`procedure.list`, `procedure.get`, `procedure.manifest` and the others) show only
|
|
274
|
+
* these (roadmap R17: before, a handler could not know the rule, so an MCP tool listed the
|
|
275
|
+
* procedures that the host does not expose). Absent: every procedure.
|
|
276
|
+
*/
|
|
277
|
+
expose?: ((path: ProcedurePath) => boolean) | undefined;
|
|
278
|
+
|
|
263
279
|
/**
|
|
264
280
|
* The hydration observer of the program that runs this procedure (`client.exec(ref, input,
|
|
265
281
|
* { onRef })`). A procedure that hydrates refs itself (control flow, a runner) gives it to
|
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire protocol v1 (roadmap R25). `documentation/WIRE-PROTOCOL.md` specifies the formats.
|
|
3
|
+
*
|
|
4
|
+
* - The version: a WebSocket connection starts with a `hello` frame of each side, and an HTTP
|
|
5
|
+
* request and its response carry the `Procedures-Protocol` header. A side refuses a newer
|
|
6
|
+
* version with `PROTOCOL_VERSION`.
|
|
7
|
+
* - One list of error codes for all faces, with the HTTP status of each.
|
|
8
|
+
* - A change after v1 adds only optional fields, or it makes v2.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** The version of the wire protocol that this package speaks. */
|
|
12
|
+
export const PROTOCOL_VERSION = 1;
|
|
13
|
+
|
|
14
|
+
/** The HTTP header of the protocol version, on a request and on its response. */
|
|
15
|
+
export const PROTOCOL_HEADER = "Procedures-Protocol";
|
|
16
|
+
|
|
17
|
+
/** The facts of one error code. */
|
|
18
|
+
export interface ErrorCodeInfo {
|
|
19
|
+
/** The HTTP status of an error with this code. */
|
|
20
|
+
readonly http: number;
|
|
21
|
+
/** True when a retry of the same call can succeed. The error item can say otherwise. */
|
|
22
|
+
readonly retryable: boolean;
|
|
23
|
+
/** What the code means. */
|
|
24
|
+
readonly meaning: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** An error code of the protocol. */
|
|
28
|
+
export type ProtocolErrorCode =
|
|
29
|
+
| "NOT_FOUND"
|
|
30
|
+
| "NO_HANDLER"
|
|
31
|
+
| "NOT_EXPOSED"
|
|
32
|
+
| "NOT_DECLARED"
|
|
33
|
+
| "FORBIDDEN"
|
|
34
|
+
| "UNAUTHORIZED"
|
|
35
|
+
| "VALIDATION_ERROR"
|
|
36
|
+
| "INVALID_REQUEST"
|
|
37
|
+
| "PROTOCOL_VERSION"
|
|
38
|
+
| "OUTPUT_VALIDATION_ERROR"
|
|
39
|
+
| "NO_OUTPUT"
|
|
40
|
+
| "HANDLER_ERROR"
|
|
41
|
+
| "INTERNAL_ERROR"
|
|
42
|
+
| "NOT_ACCEPTABLE"
|
|
43
|
+
| "DUPLICATE"
|
|
44
|
+
| "DUPLICATE_REQUEST"
|
|
45
|
+
| "ABORTED"
|
|
46
|
+
| "TIMEOUT"
|
|
47
|
+
| "UNAVAILABLE"
|
|
48
|
+
| "CONNECTION_CLOSED"
|
|
49
|
+
| "CONNECTION_NOT_FOUND";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The error codes of the protocol, on each face (HTTP, WebSocket, MCP, local). An error that a
|
|
53
|
+
* handler throws with its own `code` keeps it. Such a code is not in this list, and HTTP gives it
|
|
54
|
+
* the status 500.
|
|
55
|
+
*/
|
|
56
|
+
export const ERROR_CODES: Readonly<Record<ProtocolErrorCode, ErrorCodeInfo>> = {
|
|
57
|
+
NOT_FOUND: { http: 404, retryable: false, meaning: "No procedure has the path, or the host does not expose it" },
|
|
58
|
+
NO_HANDLER: { http: 404, retryable: false, meaning: "The procedure has no handler here (a stub of a remote host)" },
|
|
59
|
+
NOT_EXPOSED: { http: 403, retryable: false, meaning: "A nested call reached a procedure that the host does not expose" },
|
|
60
|
+
NOT_DECLARED: { http: 403, retryable: false, meaning: "A nested call is not in the calls that the procedure declares" },
|
|
61
|
+
FORBIDDEN: { http: 403, retryable: false, meaning: "The capability of the call refuses it. The metadata key reason tells why" },
|
|
62
|
+
UNAUTHORIZED: { http: 401, retryable: false, meaning: "The request does not present the token of the server" },
|
|
63
|
+
VALIDATION_ERROR: { http: 400, retryable: false, meaning: "The input does not agree with the input schema" },
|
|
64
|
+
INVALID_REQUEST: { http: 400, retryable: false, meaning: "The request is not a valid request of the protocol" },
|
|
65
|
+
PROTOCOL_VERSION: { http: 400, retryable: false, meaning: "The other side speaks a newer version of the protocol" },
|
|
66
|
+
OUTPUT_VALIDATION_ERROR: { http: 500, retryable: false, meaning: "The output does not agree with the output schema" },
|
|
67
|
+
NO_OUTPUT: { http: 500, retryable: false, meaning: "The procedure gave no output" },
|
|
68
|
+
HANDLER_ERROR: { http: 500, retryable: false, meaning: "The handler threw an error without a code" },
|
|
69
|
+
INTERNAL_ERROR: { http: 500, retryable: false, meaning: "The server failed outside the handler" },
|
|
70
|
+
NOT_ACCEPTABLE: { http: 406, retryable: false, meaning: "The face cannot give the output: a stream without an end, without NDJSON" },
|
|
71
|
+
DUPLICATE: { http: 409, retryable: false, meaning: "An idempotency key gave a stream, and a stream cannot be given again" },
|
|
72
|
+
DUPLICATE_REQUEST: { http: 409, retryable: false, meaning: "A request with this id is still running on the connection" },
|
|
73
|
+
ABORTED: { http: 499, retryable: false, meaning: "The caller aborted the call" },
|
|
74
|
+
TIMEOUT: { http: 504, retryable: false, meaning: "The deadline of the call passed" },
|
|
75
|
+
UNAVAILABLE: { http: 503, retryable: true, meaning: "The server cannot take the call now: it stops, or it is busy" },
|
|
76
|
+
CONNECTION_CLOSED: { http: 503, retryable: true, meaning: "The connection closed before the call left" },
|
|
77
|
+
CONNECTION_NOT_FOUND: { http: 404, retryable: false, meaning: "The server has no connection with this id" },
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** True when a code is an error code of the protocol. */
|
|
81
|
+
export function isProtocolErrorCode(code: unknown): code is ProtocolErrorCode {
|
|
82
|
+
return typeof code === "string" && Object.prototype.hasOwnProperty.call(ERROR_CODES, code);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The HTTP status of an error code, or undefined for a code that is not in the list. */
|
|
86
|
+
export function httpStatusOf(code: unknown): number | undefined {
|
|
87
|
+
return isProtocolErrorCode(code) ? ERROR_CODES[code].http : undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The protocol version that a peer sends, or undefined when it sends none (an older peer, which
|
|
92
|
+
* speaks the formats of v1 without the version).
|
|
93
|
+
*/
|
|
94
|
+
export function protocolVersionOf(value: unknown): number | undefined {
|
|
95
|
+
const version = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
96
|
+
return Number.isInteger(version) && version > 0 ? version : undefined;
|
|
97
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* procedure.
|
|
11
11
|
* - `strictCalls`: a procedure of code that declares no `metadata.calls` calls only exposed
|
|
12
12
|
* procedures (a host for an outside caller sets it).
|
|
13
|
-
* - `
|
|
13
|
+
* - `timeout`: the time limit of each call from a caller (not of its nested calls).
|
|
14
14
|
* - `token`, `browser`: the checks of the network transports (`client-server` applies them).
|
|
15
15
|
* - `capability`, `readOnly`, `clearanceOrder`, `resources`, `admitItem`: the capability of each
|
|
16
16
|
* call and what the host gives for it (CAPABILITIES-2026-10).
|
|
@@ -24,6 +24,7 @@ import { timeoutInterceptor } from "../procedures/interceptors.js";
|
|
|
24
24
|
import type { BrowserGuardOptions } from "./browser-guard.js";
|
|
25
25
|
import type { ProcedureRegistry } from "../procedures/registry.js";
|
|
26
26
|
import type { ProcedureServerOptions } from "./procedure-server.js";
|
|
27
|
+
import { toMs, type Duration } from "../duration.js";
|
|
27
28
|
|
|
28
29
|
/** The procedures of a host that are tools: namespaces (first path segments), less some keys. */
|
|
29
30
|
export interface ToolSurface {
|
|
@@ -63,8 +64,8 @@ export interface HostPolicyOptions {
|
|
|
63
64
|
exclude?: readonly string[] | undefined;
|
|
64
65
|
/** A procedure of code with no `metadata.calls` calls only exposed procedures. */
|
|
65
66
|
strictCalls?: boolean | undefined;
|
|
66
|
-
/** The time limit of each call from a caller,
|
|
67
|
-
|
|
67
|
+
/** The time limit of each call from a caller, for example `minutes(10)`. 0 or no value: no limit. */
|
|
68
|
+
timeout?: Duration | undefined;
|
|
68
69
|
/** More interceptors, after the policy's own (tracing, metrics). */
|
|
69
70
|
interceptors?: readonly Interceptor[] | undefined;
|
|
70
71
|
/** The token that each network request must send (HTTP and WebSocket transports). */
|
|
@@ -130,8 +131,8 @@ function exposeOf(options: HostPolicyOptions): ((path: ProcedurePath) => boolean
|
|
|
130
131
|
/** This function builds the policy of a host from its options. */
|
|
131
132
|
export function hostPolicy(options: HostPolicyOptions = {}): HostPolicy {
|
|
132
133
|
const interceptors: Interceptor[] = [];
|
|
133
|
-
if (options.
|
|
134
|
-
interceptors.push(timeoutInterceptor({
|
|
134
|
+
if (options.timeout !== undefined && toMs(options.timeout, "timeout") > 0) {
|
|
135
|
+
interceptors.push(timeoutInterceptor({ limit: options.timeout }));
|
|
135
136
|
}
|
|
136
137
|
interceptors.push(...(options.interceptors ?? []));
|
|
137
138
|
return {
|
|
@@ -154,6 +155,8 @@ export function procedureServerOptions(policy: HostPolicy, registry?: ProcedureR
|
|
|
154
155
|
...(registry ? { registry } : {}),
|
|
155
156
|
...(policy.expose ? { expose: policy.expose } : {}),
|
|
156
157
|
strictCalls: policy.strictCalls,
|
|
158
|
+
// The token of the policy: ProcedureServer examines it for each network request (R18)
|
|
159
|
+
...(policy.token !== undefined ? { token: policy.token } : {}),
|
|
157
160
|
...(policy.interceptors.length > 0 ? { interceptors: policy.interceptors } : {}),
|
|
158
161
|
...(policy.capabilityOf ? { capability: policy.capabilityOf } : {}),
|
|
159
162
|
...(policy.clearanceOrder ? { clearanceOrder: policy.clearanceOrder } : {}),
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The procedures of a host (roadmap R19): `host.health`, `host.info` and `host.metrics`.
|
|
3
|
+
*
|
|
4
|
+
* Before, a peer gave its health only as an Express route (`/health`), and the counts of
|
|
5
|
+
* `metricsInterceptor` had no way out. A host pins these procedures on its `ProcedureServer`
|
|
6
|
+
* (`registerProcedure`), so they do not change the registry that the host serves. A call needs
|
|
7
|
+
* the token of the host, as each other call does. The `/health` route stays for the checks
|
|
8
|
+
* that have no token.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { createProcedure } from "../procedures/define.js";
|
|
13
|
+
import { PURE_EFFECTS } from "../procedures/effects.js";
|
|
14
|
+
import type { AnyProcedure } from "../procedures/types.js";
|
|
15
|
+
import type { MetricsInterceptor } from "../procedures/interceptors.js";
|
|
16
|
+
import type { ProcedureServer } from "./procedure-server.js";
|
|
17
|
+
|
|
18
|
+
/** What a host tells about itself. */
|
|
19
|
+
export interface HostSource {
|
|
20
|
+
/** The id of the host (the peer id). */
|
|
21
|
+
readonly id: string;
|
|
22
|
+
/** The time at which the host started, from `Date.now()`. */
|
|
23
|
+
readonly startedAt: number;
|
|
24
|
+
/** The server of the host. */
|
|
25
|
+
readonly server: ProcedureServer;
|
|
26
|
+
/** The endpoints of the host, when it has network transports. */
|
|
27
|
+
readonly endpoints?: (() => ReadonlyArray<{ type: string; address: string }>) | undefined;
|
|
28
|
+
/** The metrics interceptor of the host, when it counts its calls. */
|
|
29
|
+
readonly metrics?: MetricsInterceptor | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const healthOutput = z.object({
|
|
33
|
+
status: z.enum(["ok", "stopping"]).describe("ok, or stopping while the host waits for its calls in flight"),
|
|
34
|
+
uptimeMs: z.number().describe("The time since the start of the host, in milliseconds"),
|
|
35
|
+
procedures: z.number().describe("The number of procedures that the host serves"),
|
|
36
|
+
callsInFlight: z.number().describe("The calls that run now, streams included"),
|
|
37
|
+
peerId: z.string().describe("The id of the host"),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const infoOutput = z.object({
|
|
41
|
+
peerId: z.string().describe("The id of the host"),
|
|
42
|
+
startedAt: z.string().describe("The start time of the host (ISO 8601)"),
|
|
43
|
+
endpoints: z.array(z.object({ type: z.string(), address: z.string() })).describe("The network endpoints of the host"),
|
|
44
|
+
runtime: z.string().describe("The runtime of the host, for example node 24.1.0"),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const metricsOutput = z.object({
|
|
48
|
+
procedures: z
|
|
49
|
+
.record(
|
|
50
|
+
z.string(),
|
|
51
|
+
z.object({ calls: z.number(), errors: z.number(), totalMs: z.number(), maxMs: z.number() }),
|
|
52
|
+
)
|
|
53
|
+
.describe("The calls, the errors and the time of each procedure since the start, by key"),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/** The runtime of this process: its name and version. */
|
|
57
|
+
function runtimeOf(): string {
|
|
58
|
+
const process = (globalThis as { process?: { versions?: { node?: string } } }).process;
|
|
59
|
+
if (process?.versions?.node) return `node ${process.versions.node}`;
|
|
60
|
+
const navigator = (globalThis as { navigator?: { userAgent?: string } }).navigator;
|
|
61
|
+
return navigator?.userAgent ?? "unknown";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** This function gives the procedures of a host: `host.health`, `host.info` and `host.metrics`. */
|
|
65
|
+
export function hostProcedures(source: HostSource): AnyProcedure[] {
|
|
66
|
+
const health = createProcedure()
|
|
67
|
+
.path(["host", "health"])
|
|
68
|
+
.input(z.object({}))
|
|
69
|
+
.output(healthOutput)
|
|
70
|
+
.meta({ description: "The health of the host: its state, its uptime and its calls in flight", effects: PURE_EFFECTS })
|
|
71
|
+
.handler(async () => ({
|
|
72
|
+
status: source.server.stopping ? ("stopping" as const) : ("ok" as const),
|
|
73
|
+
uptimeMs: Date.now() - source.startedAt,
|
|
74
|
+
procedures: source.server.procedureCount,
|
|
75
|
+
callsInFlight: source.server.callsInFlight,
|
|
76
|
+
peerId: source.id,
|
|
77
|
+
}))
|
|
78
|
+
.build();
|
|
79
|
+
|
|
80
|
+
const info = createProcedure()
|
|
81
|
+
.path(["host", "info"])
|
|
82
|
+
.input(z.object({}))
|
|
83
|
+
.output(infoOutput)
|
|
84
|
+
.meta({ description: "The identity of the host: its id, its start time, its endpoints and its runtime", effects: PURE_EFFECTS })
|
|
85
|
+
.handler(async () => ({
|
|
86
|
+
peerId: source.id,
|
|
87
|
+
startedAt: new Date(source.startedAt).toISOString(),
|
|
88
|
+
endpoints: [...(source.endpoints?.() ?? [])],
|
|
89
|
+
runtime: runtimeOf(),
|
|
90
|
+
}))
|
|
91
|
+
.build();
|
|
92
|
+
|
|
93
|
+
const metrics = createProcedure()
|
|
94
|
+
.path(["host", "metrics"])
|
|
95
|
+
.input(z.object({}))
|
|
96
|
+
.output(metricsOutput)
|
|
97
|
+
.meta({ description: "The calls, the errors and the time of each procedure since the start of the host", effects: PURE_EFFECTS })
|
|
98
|
+
.handler(async () => ({ procedures: source.metrics?.snapshot() ?? {} }))
|
|
99
|
+
.build();
|
|
100
|
+
|
|
101
|
+
return [health, info, metrics] as AnyProcedure[];
|
|
102
|
+
}
|
package/src/server/index.ts
CHANGED
|
@@ -37,14 +37,17 @@
|
|
|
37
37
|
export { Server } from "./server.js";
|
|
38
38
|
export type { ServerOptions } from "./server.js";
|
|
39
39
|
|
|
40
|
-
export { ProcedureServer, createProcedureServer } from "./procedure-server.js";
|
|
41
|
-
export type { ProcedureServerOptions, StorageConfig } from "./procedure-server.js";
|
|
40
|
+
export { ProcedureServer, createProcedureServer, STREAM_METADATA_KEY, isUnboundedStream } from "./procedure-server.js";
|
|
41
|
+
export type { ProcedureServerOptions, StorageConfig, StopOptions, UseStorageOptions } from "./procedure-server.js";
|
|
42
42
|
export { pathToMethod, methodToPath } from "./method.js";
|
|
43
43
|
export {
|
|
44
44
|
HandlerNotFoundError,
|
|
45
45
|
ServerError,
|
|
46
46
|
type ServerRequest,
|
|
47
47
|
type ServerResponse,
|
|
48
|
+
type RequestHandler,
|
|
49
|
+
type Credentials,
|
|
50
|
+
type Principal,
|
|
48
51
|
type ServerHandler,
|
|
49
52
|
type ServerMiddleware,
|
|
50
53
|
type ServerContext,
|