@procedures/client 0.52.2 → 0.53.1
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 +60 -0
- package/README.md +11 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/client/batch.d.ts.map +1 -1
- package/dist/adapters/http/client/batch.js +9 -5
- package/dist/adapters/http/client/batch.js.map +1 -1
- package/dist/adapters/http/server/cors.d.ts.map +1 -1
- package/dist/adapters/http/server/cors.js +2 -0
- package/dist/adapters/http/server/cors.js.map +1 -1
- package/dist/adapters/port/transport.d.ts +27 -0
- package/dist/adapters/port/transport.d.ts.map +1 -1
- package/dist/adapters/port/transport.js +30 -0
- package/dist/adapters/port/transport.js.map +1 -1
- package/dist/browser.d.ts +2 -2
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +1 -1
- package/dist/browser.js.map +1 -1
- package/dist/client/batch-executor.d.ts +3 -0
- package/dist/client/batch-executor.d.ts.map +1 -1
- package/dist/client/batch-executor.js +3 -0
- package/dist/client/batch-executor.js.map +1 -1
- package/dist/client/client.d.ts +2 -0
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +33 -5
- package/dist/client/client.js.map +1 -1
- package/dist/client/middleware/cache.d.ts.map +1 -1
- package/dist/client/middleware/cache.js +2 -0
- package/dist/client/middleware/cache.js.map +1 -1
- package/dist/client/middleware/rate-limit.d.ts.map +1 -1
- package/dist/client/middleware/rate-limit.js +3 -2
- package/dist/client/middleware/rate-limit.js.map +1 -1
- package/dist/events/bus.d.ts +7 -0
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/events/bus.js +22 -4
- package/dist/events/bus.js.map +1 -1
- package/dist/events/publisher.d.ts.map +1 -1
- package/dist/events/publisher.js +4 -3
- package/dist/events/publisher.js.map +1 -1
- package/dist/frame-encoding.d.ts +8 -5
- package/dist/frame-encoding.d.ts.map +1 -1
- package/dist/frame-encoding.js +13 -8
- package/dist/frame-encoding.js.map +1 -1
- package/dist/procedures/interceptors.d.ts +10 -1
- package/dist/procedures/interceptors.d.ts.map +1 -1
- package/dist/procedures/interceptors.js +31 -8
- package/dist/procedures/interceptors.js.map +1 -1
- package/dist/procedures/module.d.ts.map +1 -1
- package/dist/procedures/module.js +7 -2
- package/dist/procedures/module.js.map +1 -1
- package/dist/server/host-policy.d.ts +8 -0
- package/dist/server/host-policy.d.ts.map +1 -1
- package/dist/server/host-policy.js +15 -2
- package/dist/server/host-policy.js.map +1 -1
- package/dist/server/limits.d.ts +1 -0
- package/dist/server/limits.d.ts.map +1 -1
- package/dist/server/limits.js +13 -0
- package/dist/server/limits.js.map +1 -1
- package/dist/server/procedure-server.d.ts +30 -1
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +68 -6
- package/dist/server/procedure-server.js.map +1 -1
- package/dist/tracing/otel.d.ts +55 -0
- package/dist/tracing/otel.d.ts.map +1 -0
- package/dist/tracing/otel.js +114 -0
- package/dist/tracing/otel.js.map +1 -0
- package/dist/tracing/traceparent.d.ts +40 -12
- package/dist/tracing/traceparent.d.ts.map +1 -1
- package/dist/tracing/traceparent.js +49 -14
- package/dist/tracing/traceparent.js.map +1 -1
- package/package.json +15 -5
- package/src/adapters/http/client/batch.ts +8 -5
- package/src/adapters/http/server/cors.ts +2 -0
- package/src/adapters/port/transport.ts +44 -0
- package/src/browser.ts +2 -2
- package/src/client/batch-executor.ts +3 -0
- package/src/client/client.ts +31 -5
- package/src/client/middleware/cache.ts +2 -0
- package/src/client/middleware/rate-limit.ts +4 -2
- package/src/events/bus.ts +25 -4
- package/src/events/publisher.ts +6 -3
- package/src/frame-encoding.ts +15 -8
- package/src/procedures/interceptors.ts +40 -9
- package/src/procedures/module.ts +10 -2
- package/src/server/host-policy.ts +24 -2
- package/src/server/limits.ts +13 -0
- package/src/server/procedure-server.ts +79 -8
- package/src/tracing/otel.ts +122 -0
- package/src/tracing/traceparent.ts +56 -15
|
@@ -4,12 +4,22 @@
|
|
|
4
4
|
* the metadata of a call (and HTTP as the `traceparent` header). The tracing interceptor of a
|
|
5
5
|
* server continues that trace, and gives it to the nested calls, so one trace goes from the
|
|
6
6
|
* client to the last handler. Before, the client and the server made two traces.
|
|
7
|
+
*
|
|
8
|
+
* The checks of the ids are `isValidTraceId` and `isValidSpanId` of `@opentelemetry/api`
|
|
9
|
+
* (decision D7 of otel-ts). They refuse an id of only zeros.
|
|
10
|
+
*/
|
|
11
|
+
import { isValidSpanId, isValidTraceId } from "@opentelemetry/api";
|
|
12
|
+
/**
|
|
13
|
+
* The metadata key of the W3C trace context of a call.
|
|
14
|
+
*
|
|
15
|
+
* @deprecated With an SDK of OpenTelemetry, the registered propagator writes and reads the trace
|
|
16
|
+
* context (roadmap C8 of otel-ts). A later MINOR removes this export, when each app registers an SDK (D6).
|
|
7
17
|
*/
|
|
8
|
-
/** The metadata key of the W3C trace context of a call. */
|
|
9
18
|
export const TRACEPARENT_METADATA_KEY = "traceparent";
|
|
19
|
+
/** The form of version 00. The ids in it have only lowercase digits, as W3C Trace Context requires. */
|
|
10
20
|
const TRACEPARENT = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
|
|
11
|
-
|
|
12
|
-
const
|
|
21
|
+
/** An uppercase hex digit. The checks of the API accept it, but W3C Trace Context does not. */
|
|
22
|
+
const UPPERCASE_HEX = /[A-F]/;
|
|
13
23
|
/**
|
|
14
24
|
* The size of the pool of random bytes. One fill gives 256 trace ids or 512 span ids. Before
|
|
15
25
|
* (roadmap O94), each id called `getRandomValues` for 8 or 16 bytes: in a chain of 200 steps, the
|
|
@@ -41,34 +51,59 @@ function randomHex(bytes) {
|
|
|
41
51
|
next += bytes;
|
|
42
52
|
return hex;
|
|
43
53
|
}
|
|
44
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* This function makes a new trace id: 32 random hex digits.
|
|
56
|
+
*
|
|
57
|
+
* @deprecated With an SDK of OpenTelemetry, the registered propagator writes and reads the trace
|
|
58
|
+
* context (roadmap C8 of otel-ts). A later MINOR removes this export, when each app registers an SDK (D6).
|
|
59
|
+
*/
|
|
45
60
|
export function newTraceId() {
|
|
46
61
|
const id = randomHex(16);
|
|
47
|
-
return id
|
|
62
|
+
return isValidTraceId(id) ? id : newTraceId();
|
|
48
63
|
}
|
|
49
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* This function makes a new span id: 16 random hex digits.
|
|
66
|
+
*
|
|
67
|
+
* @deprecated With an SDK of OpenTelemetry, the registered propagator writes and reads the trace
|
|
68
|
+
* context (roadmap C8 of otel-ts). A later MINOR removes this export, when each app registers an SDK (D6).
|
|
69
|
+
*/
|
|
50
70
|
export function newSpanId() {
|
|
51
71
|
const id = randomHex(8);
|
|
52
|
-
return id
|
|
72
|
+
return isValidSpanId(id) ? id : newSpanId();
|
|
53
73
|
}
|
|
54
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* This function gives true when the ids have the W3C form.
|
|
76
|
+
*
|
|
77
|
+
* @deprecated With an SDK of OpenTelemetry, the registered propagator writes and reads the trace
|
|
78
|
+
* context (roadmap C8 of otel-ts). A later MINOR removes this export, when each app registers an SDK (D6).
|
|
79
|
+
*/
|
|
55
80
|
export function isW3cIds(traceId, spanId) {
|
|
56
|
-
return
|
|
81
|
+
return isValidTraceId(traceId) && isValidSpanId(spanId) && !UPPERCASE_HEX.test(traceId) && !UPPERCASE_HEX.test(spanId);
|
|
57
82
|
}
|
|
58
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* This function gives the `traceparent` of a trace and a span.
|
|
85
|
+
*
|
|
86
|
+
* @deprecated With an SDK of OpenTelemetry, the registered propagator writes and reads the trace
|
|
87
|
+
* context (roadmap C8 of otel-ts). A later MINOR removes this export, when each app registers an SDK (D6).
|
|
88
|
+
*/
|
|
59
89
|
export function formatTraceparent(traceId, spanId, sampled = true) {
|
|
60
90
|
return `00-${traceId}-${spanId}-${sampled ? "01" : "00"}`;
|
|
61
91
|
}
|
|
62
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* This function gives the trace context of a `traceparent` value, or undefined when it is not valid.
|
|
94
|
+
*
|
|
95
|
+
* @deprecated With an SDK of OpenTelemetry, the registered propagator writes and reads the trace
|
|
96
|
+
* context (roadmap C8 of otel-ts). A later MINOR removes this export, when each app registers an SDK (D6).
|
|
97
|
+
*/
|
|
63
98
|
export function parseTraceparent(value) {
|
|
64
99
|
if (typeof value !== "string")
|
|
65
100
|
return undefined;
|
|
66
101
|
const match = TRACEPARENT.exec(value.trim().toLowerCase());
|
|
67
102
|
if (!match)
|
|
68
103
|
return undefined;
|
|
69
|
-
const [, traceId, spanId, flags] = match;
|
|
70
|
-
if (traceId
|
|
104
|
+
const [, traceId = "", spanId = "", flags = ""] = match;
|
|
105
|
+
if (!isValidTraceId(traceId) || !isValidSpanId(spanId))
|
|
71
106
|
return undefined;
|
|
72
|
-
return { traceId
|
|
107
|
+
return { traceId, spanId, sampled: (parseInt(flags, 16) & 1) === 1 };
|
|
73
108
|
}
|
|
74
109
|
//# sourceMappingURL=traceparent.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"traceparent.js","sourceRoot":"","sources":["../../src/tracing/traceparent.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"traceparent.js","sourceRoot":"","sources":["../../src/tracing/traceparent.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@procedures/client",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.53.1",
|
|
5
5
|
"description": "Procedures as data for TypeScript: define typed procedures once and call them in process, over HTTP, WebSocket, the CLI or MCP. Streaming, middleware, retries, caching and a JSON program format.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"rpc",
|
|
@@ -112,19 +112,29 @@
|
|
|
112
112
|
},
|
|
113
113
|
"dependencies": {
|
|
114
114
|
"@mark1russell7/algebra": "^0.8.0",
|
|
115
|
-
"@mark1russell7/coeval": "^0.
|
|
115
|
+
"@mark1russell7/coeval": "^0.26.1",
|
|
116
116
|
"@mark1russell7/collections": "^0.5.0",
|
|
117
117
|
"@mark1russell7/quantity": "^0.3.0",
|
|
118
|
-
"@mark1russell7/service": "^0.
|
|
118
|
+
"@mark1russell7/service": "^0.8.3",
|
|
119
|
+
"@opentelemetry/api": "^1.9.0",
|
|
119
120
|
"@types/ws": "^8.18.1",
|
|
120
121
|
"ws": "^8.18.3",
|
|
121
122
|
"zod": "^4.6.5"
|
|
122
123
|
},
|
|
123
124
|
"devDependencies": {
|
|
124
|
-
"@mark1russell7/api-report": "^0.
|
|
125
|
+
"@mark1russell7/api-report": "^0.5.0",
|
|
125
126
|
"@mark1russell7/cue": "^2.1.1",
|
|
126
|
-
"@mark1russell7/
|
|
127
|
+
"@mark1russell7/otel-ts": "^0.6.0",
|
|
128
|
+
"@mark1russell7/rigor-bench": "^0.2.33",
|
|
127
129
|
"@mark1russell7/rigor-statistics": "^0.1.9",
|
|
130
|
+
"@mark1russell7/rigor-testkit": "^0.1.2",
|
|
131
|
+
"@opentelemetry/api-logs": "^0.223.0",
|
|
132
|
+
"@opentelemetry/context-async-hooks": "^2.12.0",
|
|
133
|
+
"@opentelemetry/core": "^2.12.0",
|
|
134
|
+
"@opentelemetry/sdk-logs": "^0.223.0",
|
|
135
|
+
"@opentelemetry/sdk-metrics": "^2.12.0",
|
|
136
|
+
"@opentelemetry/sdk-node": "~0.223.0",
|
|
137
|
+
"@opentelemetry/sdk-trace-base": "^2.12.0",
|
|
128
138
|
"@stryker-mutator/core": "10.0.0",
|
|
129
139
|
"@stryker-mutator/vitest-runner": "10.0.0",
|
|
130
140
|
"@types/express": "^5.0.0",
|
|
@@ -86,7 +86,12 @@ class Channel<T> {
|
|
|
86
86
|
private readonly items: T[] = [];
|
|
87
87
|
private ended = false;
|
|
88
88
|
private wake: (() => void) | undefined;
|
|
89
|
-
|
|
89
|
+
/**
|
|
90
|
+
* The pushes that wait for room. The reader of the batch waits here, and the last item of an
|
|
91
|
+
* abort can wait here too. Each one gets the room. Before, a second push took the place of the
|
|
92
|
+
* first, and the reader of the batch waited with no end (audit 2026-11-08).
|
|
93
|
+
*/
|
|
94
|
+
private readonly rooms: Array<() => void> = [];
|
|
90
95
|
|
|
91
96
|
/** It resolves when the reader has room for more items. */
|
|
92
97
|
push(item: T): Promise<void> {
|
|
@@ -94,7 +99,7 @@ class Channel<T> {
|
|
|
94
99
|
this.items.push(item);
|
|
95
100
|
this.wake?.();
|
|
96
101
|
if (this.items.length < HIGH_WATER) return Promise.resolve();
|
|
97
|
-
return new Promise<void>((resolve) =>
|
|
102
|
+
return new Promise<void>((resolve) => this.rooms.push(resolve));
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
end(): void {
|
|
@@ -104,9 +109,7 @@ class Channel<T> {
|
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
private free(): void {
|
|
107
|
-
const room
|
|
108
|
-
this.room = undefined;
|
|
109
|
-
room?.();
|
|
112
|
+
for (const room of this.rooms.splice(0)) room();
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
async *read(): AsyncGenerator<T, void, undefined> {
|
|
@@ -50,6 +50,8 @@ export const DEFAULT_CORS_HEADERS: readonly string[] = Object.freeze([
|
|
|
50
50
|
HTTPHeaders.SPAN_ID,
|
|
51
51
|
HTTPHeaders.PARENT_SPAN_ID,
|
|
52
52
|
"traceparent",
|
|
53
|
+
"tracestate",
|
|
54
|
+
"baggage",
|
|
53
55
|
PROTOCOL_HEADER,
|
|
54
56
|
HTTPHeaders.METADATA,
|
|
55
57
|
// The narrowing key of a call (CLI-30)
|
|
@@ -45,6 +45,50 @@ export interface PortLike {
|
|
|
45
45
|
start?(): void;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** The listener of a port of the main process of Electron: it gets an event, and the message is its `data`. */
|
|
49
|
+
type MainListener = (event: { readonly data: unknown }) => void;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A port of the main process of Electron (`MessagePortMain`). The listener of its `on` gets an event with the message
|
|
53
|
+
* in `data`. The Node form of `PortLike` gets the message itself, so `electronPortOf` changes the form.
|
|
54
|
+
*/
|
|
55
|
+
export interface MainPortLike {
|
|
56
|
+
postMessage(message: unknown): void;
|
|
57
|
+
on(type: "message", listener: MainListener): unknown;
|
|
58
|
+
off?(type: "message", listener: MainListener): unknown;
|
|
59
|
+
start(): void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* This function gives the `PortLike` of a port of the main process of Electron. Each listener gets the `data` of the
|
|
64
|
+
* event. `off` removes the listener that `on` added, so the close of a `PortTransport` stops to listen.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* const { port1, port2 } = new MessageChannelMain();
|
|
69
|
+
* new PortTransport(electronPortOf(port1), { serve: server });
|
|
70
|
+
* window.webContents.postMessage("ports", null, [port2]);
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export function electronPortOf(port: MainPortLike): PortLike {
|
|
74
|
+
const listeners = new Map<(data: unknown) => void, MainListener>();
|
|
75
|
+
return {
|
|
76
|
+
postMessage: (message) => port.postMessage(message),
|
|
77
|
+
on(type, listener) {
|
|
78
|
+
const each: MainListener = (event) => listener(event.data);
|
|
79
|
+
listeners.set(listener, each);
|
|
80
|
+
return port.on(type, each);
|
|
81
|
+
},
|
|
82
|
+
off(type, listener) {
|
|
83
|
+
const each = listeners.get(listener);
|
|
84
|
+
if (each === undefined) return undefined;
|
|
85
|
+
listeners.delete(listener);
|
|
86
|
+
return port.off?.(type, each);
|
|
87
|
+
},
|
|
88
|
+
start: () => port.start(),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
48
92
|
/** The options of a `PortTransport`. */
|
|
49
93
|
export interface PortTransportOptions {
|
|
50
94
|
/** The handler of the calls of the other side, for example a `ProcedureServer`. */
|
package/src/browser.ts
CHANGED
|
@@ -469,8 +469,8 @@ export type { MachineDocument, MachineTransition, MachineTransitionDef, MachineT
|
|
|
469
469
|
export { TIMER_DEFAULTS } from "./timers.js";
|
|
470
470
|
export type { TimerDefaults } from "./timers.js";
|
|
471
471
|
// The transport over a message port: a Web Worker, a MessageChannel (roadmap R27)
|
|
472
|
-
export { PortTransport } from "./adapters/port/transport.js";
|
|
473
|
-
export type { PortLike, PortTransportOptions } from "./adapters/port/transport.js";
|
|
472
|
+
export { PortTransport, electronPortOf } from "./adapters/port/transport.js";
|
|
473
|
+
export type { MainPortLike, PortLike, PortTransportOptions } from "./adapters/port/transport.js";
|
|
474
474
|
// The W3C trace context: one trace from the client to the handler (roadmap R16)
|
|
475
475
|
export { TRACEPARENT_METADATA_KEY, formatTraceparent, parseTraceparent, newTraceId, newSpanId, isW3cIds } from "./tracing/traceparent.js";
|
|
476
476
|
export type { TraceContext } from "./tracing/traceparent.js";
|
|
@@ -460,6 +460,9 @@ export function determineStrategy(
|
|
|
460
460
|
|
|
461
461
|
/**
|
|
462
462
|
* Semaphore for limiting concurrent executions.
|
|
463
|
+
*
|
|
464
|
+
* @deprecated Use `semaphore()` of `@mark1russell7/collections`, the home of the semaphore. Its `run(task)` replaces
|
|
465
|
+
* `withPermit(fn)`, and its `acquire()` gives the release function. The next MINOR break of `@procedures/client` removes this class.
|
|
463
466
|
*/
|
|
464
467
|
export class Semaphore {
|
|
465
468
|
private permits: number;
|
package/src/client/client.ts
CHANGED
|
@@ -70,6 +70,8 @@ import {
|
|
|
70
70
|
import { narrowCapability } from "../procedures/capability.js";
|
|
71
71
|
import { DEADLINE_METADATA_KEY, TIMEOUT_METADATA_KEY, absorbTimeout, writeRemainingTime } from "./deadline.js";
|
|
72
72
|
import { ms } from "../duration.js";
|
|
73
|
+
import { context as otelContext, SpanKind, trace } from "@opentelemetry/api";
|
|
74
|
+
import { asInternalCall, callParent, callerTraceOf, endApiSpan, injectContext, startApiSpan, type CallerTrace } from "../tracing/otel.js";
|
|
73
75
|
import type { Capability, ClearanceOrder, NestedCallOptions } from "../procedures/types.js";
|
|
74
76
|
import { PROCEDURE_REGISTRY } from "../procedures/registry.js";
|
|
75
77
|
import { validateSchema } from "../procedures/schema.js";
|
|
@@ -825,8 +827,9 @@ export class Client<TContext = {}> {
|
|
|
825
827
|
// remaining time again for each try (roadmap R10)
|
|
826
828
|
absorbTimeout(message.metadata);
|
|
827
829
|
|
|
828
|
-
// Compose middleware chain
|
|
829
|
-
|
|
830
|
+
// Compose middleware chain. The trace context that the caller gave in the metadata is read
|
|
831
|
+
// before a middleware or a try writes its own (roadmap C19 of otel-ts).
|
|
832
|
+
const runner = this.composeMiddleware<TReq, TRes>(callerTraceOf(message.metadata as Record<string, unknown>));
|
|
830
833
|
|
|
831
834
|
// Execute through middleware chain
|
|
832
835
|
const context: ClientContext<TReq> = { message };
|
|
@@ -897,7 +900,7 @@ export class Client<TContext = {}> {
|
|
|
897
900
|
metadata["timeout"] = NO_SUBSCRIPTION_TIMEOUT as unknown as NonNullable<Metadata["timeout"]>;
|
|
898
901
|
Object.assign(metadata, message.metadata);
|
|
899
902
|
absorbTimeout(metadata);
|
|
900
|
-
return this.composeMiddleware<TReq, TRes>()({ message: { ...message, metadata } });
|
|
903
|
+
return this.composeMiddleware<TReq, TRes>(callerTraceOf(metadata as Record<string, unknown>))({ message: { ...message, metadata } });
|
|
901
904
|
};
|
|
902
905
|
this.subscriptionTransport = { send };
|
|
903
906
|
}
|
|
@@ -907,17 +910,40 @@ export class Client<TContext = {}> {
|
|
|
907
910
|
/**
|
|
908
911
|
* Compose middleware chain using universal middleware composition.
|
|
909
912
|
*
|
|
913
|
+
* @param caller - The trace context that the caller gave in the metadata of the call. It is the
|
|
914
|
+
* parent of the span of each try when no span is active (roadmap C19 of otel-ts).
|
|
910
915
|
* @returns Composed runner function
|
|
911
916
|
* @private
|
|
912
917
|
*/
|
|
913
|
-
private composeMiddleware<TReq, TRes>(): ClientRunner<TReq, TRes> {
|
|
918
|
+
private composeMiddleware<TReq, TRes>(caller: CallerTrace): ClientRunner<TReq, TRes> {
|
|
914
919
|
// Innermost function: call transport
|
|
915
920
|
const self = this;
|
|
916
921
|
const transportRunner: ClientRunner<TReq, TRes> = async function* (context: ClientContext<TReq>) {
|
|
917
922
|
// The remaining time of the deadline goes with each try, so the server stops the handler
|
|
918
923
|
// when this client stops waiting (roadmap R10)
|
|
919
924
|
writeRemainingTime(context.message.metadata);
|
|
920
|
-
|
|
925
|
+
// With an SDK of OpenTelemetry, each try is a span of the API, and the propagator writes its
|
|
926
|
+
// context into the metadata, with the mark of an internal call (roadmap C8 of otel-ts)
|
|
927
|
+
const { metadata, method } = context.message;
|
|
928
|
+
const active = callParent(caller, otelContext.active());
|
|
929
|
+
const span = startApiSpan(`${method.service}.${method.operation}`, SpanKind.CLIENT, active);
|
|
930
|
+
if (span === undefined) {
|
|
931
|
+
yield* self.transport.send<TReq, TRes>(context.message);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
const own = span.spanContext();
|
|
935
|
+
const parent = trace.getSpanContext(active);
|
|
936
|
+
metadata.tracing = { traceId: own.traceId, spanId: own.spanId, ...(parent ? { parentSpanId: parent.spanId } : {}) };
|
|
937
|
+
injectContext(asInternalCall(trace.setSpan(active, span)), metadata as Record<string, unknown>);
|
|
938
|
+
let failure: unknown;
|
|
939
|
+
try {
|
|
940
|
+
yield* self.transport.send<TReq, TRes>(context.message);
|
|
941
|
+
} catch (error) {
|
|
942
|
+
failure = error;
|
|
943
|
+
throw error;
|
|
944
|
+
} finally {
|
|
945
|
+
endApiSpan(span, failure);
|
|
946
|
+
}
|
|
921
947
|
};
|
|
922
948
|
|
|
923
949
|
// The first middleware is the outermost one (onion model)
|
|
@@ -308,8 +308,10 @@ export function createRateLimitMiddleware(
|
|
|
308
308
|
while (queue.length > 0) {
|
|
309
309
|
const decision = take();
|
|
310
310
|
if (!decision.allowed) {
|
|
311
|
-
// The time until the next whole token
|
|
312
|
-
|
|
311
|
+
// The time until the next whole token. A take of one unit is never over a burst of 1 or more,
|
|
312
|
+
// so a capacity refusal does not occur here. If it occurs, the queue waits one window.
|
|
313
|
+
const wait =
|
|
314
|
+
decision.cause === "capacity" ? Math.max(1, window) : Math.max(1, Math.ceil(valueIn(decision.retryAfter, "ms")));
|
|
313
315
|
drainTimer = clock.after(wait, () => {
|
|
314
316
|
drainTimer = undefined;
|
|
315
317
|
scheduleDrain();
|
package/src/events/bus.ts
CHANGED
|
@@ -44,6 +44,19 @@ function nextTask(slice: () => void): void {
|
|
|
44
44
|
else setTimeout(slice, 0);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/** The emit whose handler runs, or undefined. A handler that emits gets the mark of its own emit. */
|
|
48
|
+
let emitting: object | undefined;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* This function gives the mark of the emit whose handler runs: one object for each `emit()` of a
|
|
52
|
+
* `DefaultEventBus`. Each handler of that emit gets the same object. It gives undefined outside a handler, in
|
|
53
|
+
* the replay of the buffer, and for another bus. The publisher ties the shared text of a value to
|
|
54
|
+
* one emit with it (audit 2026-11-08).
|
|
55
|
+
*/
|
|
56
|
+
export function currentEmission(): object | undefined {
|
|
57
|
+
return emitting;
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
/** The handlers of a channel, each with the number of its subscription, in the order of these numbers. */
|
|
48
61
|
type ChannelHandlers = Map<EventHandler<unknown>, number>;
|
|
49
62
|
|
|
@@ -57,6 +70,8 @@ interface PendingEvent {
|
|
|
57
70
|
readonly cursor: Iterator<[EventHandler<unknown>, number]>;
|
|
58
71
|
/** The last subscription at `emit()`. A later handler does not get this event. */
|
|
59
72
|
readonly lastSubscription: number;
|
|
73
|
+
/** The mark of the emit (`currentEmission()`): a small object, so a share keeps no handlers. */
|
|
74
|
+
readonly emission: object;
|
|
60
75
|
done: boolean;
|
|
61
76
|
}
|
|
62
77
|
|
|
@@ -142,11 +157,14 @@ export class DefaultEventBus implements EventBus {
|
|
|
142
157
|
return;
|
|
143
158
|
}
|
|
144
159
|
|
|
145
|
-
|
|
160
|
+
const emission = {};
|
|
161
|
+
for (const handler of channelSubs.keys()) this.call(handler, data, channel, emission);
|
|
146
162
|
}
|
|
147
163
|
|
|
148
164
|
/** This function calls one handler. An error goes to `onError`, and it stops no other handler. */
|
|
149
|
-
private call(handler: EventHandler<unknown>, data: unknown, channel: string): void {
|
|
165
|
+
private call(handler: EventHandler<unknown>, data: unknown, channel: string, emission?: object): void {
|
|
166
|
+
const outer = emitting;
|
|
167
|
+
emitting = emission;
|
|
150
168
|
try {
|
|
151
169
|
const result = handler(data);
|
|
152
170
|
// Handle async handlers
|
|
@@ -155,12 +173,14 @@ export class DefaultEventBus implements EventBus {
|
|
|
155
173
|
}
|
|
156
174
|
} catch (error) {
|
|
157
175
|
this.options.onError(error instanceof Error ? error : new Error(String(error)), channel);
|
|
176
|
+
} finally {
|
|
177
|
+
emitting = outer;
|
|
158
178
|
}
|
|
159
179
|
}
|
|
160
180
|
|
|
161
181
|
/** This function puts an event in the queue of the slices. A full queue finishes its oldest events. */
|
|
162
182
|
private enqueue(channel: string, data: unknown, handlers: ChannelHandlers, fanOut: Required<EventFanOutOptions>): void {
|
|
163
|
-
this.pending.push({ channel, data, handlers, cursor: handlers.entries(), lastSubscription: this.lastSubscription, done: false });
|
|
183
|
+
this.pending.push({ channel, data, handlers, cursor: handlers.entries(), lastSubscription: this.lastSubscription, emission: {}, done: false });
|
|
164
184
|
this.pendingOn.set(channel, (this.pendingOn.get(channel) ?? 0) + 1);
|
|
165
185
|
while (this.pending.length - this.pendingHead > fanOut.maxPending) {
|
|
166
186
|
const oldest = this.pending[this.pendingHead]!;
|
|
@@ -201,7 +221,8 @@ export class DefaultEventBus implements EventBus {
|
|
|
201
221
|
event.done = true;
|
|
202
222
|
break;
|
|
203
223
|
}
|
|
204
|
-
|
|
224
|
+
// Each slice of one emit gives the same mark
|
|
225
|
+
this.call(step.value[0], event.data, event.channel, event.emission);
|
|
205
226
|
calls++;
|
|
206
227
|
}
|
|
207
228
|
return calls;
|
package/src/events/publisher.ts
CHANGED
|
@@ -41,7 +41,7 @@ import { collectionTopicTarget, partitionOfCall } from "../procedures/collection
|
|
|
41
41
|
import type { AnyProcedure, Capability, ProcedureContext } from "../procedures/types.js";
|
|
42
42
|
import type { Principal } from "../server/types.js";
|
|
43
43
|
import type { EventBus, Unsubscribe } from "./types.js";
|
|
44
|
-
import { getGlobalEventBus } from "./bus.js";
|
|
44
|
+
import { currentEmission, getGlobalEventBus } from "./bus.js";
|
|
45
45
|
import { LiveState, type LiveStateOptions } from "./live-state.js";
|
|
46
46
|
import { EVENTS_SUBSCRIBE_PATH, type EventItem, type SubscribeInput } from "./remote.js";
|
|
47
47
|
import { admittedValue, rowsOf } from "./rows.js";
|
|
@@ -221,6 +221,8 @@ function matchesName(field: RightsScope["collection"], name: string | undefined)
|
|
|
221
221
|
interface QueuedEvent {
|
|
222
222
|
readonly topic: string;
|
|
223
223
|
readonly data: unknown;
|
|
224
|
+
/** The mark of the emit of the bus: the subscribers of one emit share the text of its data. */
|
|
225
|
+
readonly emission?: object | undefined;
|
|
224
226
|
}
|
|
225
227
|
|
|
226
228
|
/** The `reset` hint that a re-check gives to a subscriber with a row filter (roadmap CLI-78). */
|
|
@@ -296,7 +298,8 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
|
|
|
296
298
|
/** This function takes one event of the bus. It never waits. */
|
|
297
299
|
private push(topic: string, data: unknown): void {
|
|
298
300
|
if (this.closed) return;
|
|
299
|
-
const
|
|
301
|
+
const emission = currentEmission();
|
|
302
|
+
const event: QueuedEvent = emission === undefined ? { topic, data } : { topic, data, emission };
|
|
300
303
|
const waiter = this.waiter;
|
|
301
304
|
if (waiter && this.queue.length === 0 && !this.recheck) {
|
|
302
305
|
this.waiter = undefined;
|
|
@@ -329,7 +332,7 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
|
|
|
329
332
|
private deliver(event: QueuedEvent): Promise<IteratorResult<EventItem>> {
|
|
330
333
|
const filtered = this.filters?.get(event.topic)?.apply(event.data);
|
|
331
334
|
// Each subscriber of the topic gets this same data, so a transport encodes it once (roadmap CLI-112)
|
|
332
|
-
if (!filtered) return this.item(event.topic, shareEncoding(event.data, this));
|
|
335
|
+
if (!filtered) return this.item(event.topic, shareEncoding(event.data, this, event.emission));
|
|
333
336
|
return filtered.then(
|
|
334
337
|
(data) => (data === DROPPED ? this.next() : this.item(event.topic, data)),
|
|
335
338
|
// A filter that fails: the subscriber reads the changes itself
|
package/src/frame-encoding.ts
CHANGED
|
@@ -7,13 +7,15 @@
|
|
|
7
7
|
* marked value once, at its first frame, and puts that text in each later frame of the same event. The text of a frame
|
|
8
8
|
* is byte-equal to `JSON.stringify` of the frame at that first encode.
|
|
9
9
|
*
|
|
10
|
-
* A source can publish the same object again after a change.
|
|
11
|
-
* new event, so the next frame encodes the object again.
|
|
10
|
+
* A source can publish the same object again after a change. The publisher gives the mark of each emit of the bus.
|
|
11
|
+
* Another mark starts a new event, so the next frame encodes the object again. Without a mark, a receiver that gets
|
|
12
|
+
* the object a second time starts a new event. The text of one event is the value at its first encode.
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
|
-
/** One event of a shared value: the receivers that got it, and its text after the first frame. */
|
|
15
|
+
/** One event of a shared value: the receivers that got it, the mark of its emit, and its text after the first frame. */
|
|
15
16
|
interface Share {
|
|
16
17
|
readonly receivers: WeakSet<object>;
|
|
18
|
+
readonly emission: object | undefined;
|
|
17
19
|
text?: string;
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -29,14 +31,19 @@ let encodes = 0;
|
|
|
29
31
|
const DEPTH = 3;
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
|
-
* This function marks a value as shared by the frames of the receivers of one event, and gives the value back.
|
|
33
|
-
*
|
|
34
|
+
* This function marks a value as shared by the frames of the receivers of one event, and gives the value back. The
|
|
35
|
+
* value starts a new event when the mark of its emit is another mark, or when the receiver got the value before. The
|
|
36
|
+
* first frame of a new event encodes the value again. Before the mark, a receiver that joined after an emit got the
|
|
37
|
+
* text of that emit for a later emit of the changed object (audit 2026-11-08).
|
|
34
38
|
*/
|
|
35
|
-
export function shareEncoding<T>(value: T, receiver: object): T {
|
|
39
|
+
export function shareEncoding<T>(value: T, receiver: object, emission?: object): T {
|
|
36
40
|
if (typeof value !== "object" || value === null) return value;
|
|
37
41
|
const share = shares.get(value);
|
|
38
|
-
if (share === undefined || share.receivers.has(receiver)
|
|
39
|
-
|
|
42
|
+
if (share === undefined || share.receivers.has(receiver) || share.emission !== emission) {
|
|
43
|
+
shares.set(value, { receivers: new WeakSet([receiver]), emission });
|
|
44
|
+
} else {
|
|
45
|
+
share.receivers.add(receiver);
|
|
46
|
+
}
|
|
40
47
|
return value;
|
|
41
48
|
}
|
|
42
49
|
|
|
@@ -10,6 +10,8 @@ import { InvocationError, closableUnread, type Interceptor, type Invocation, typ
|
|
|
10
10
|
import { pathToKey, type ProcedurePath } from "./types.js";
|
|
11
11
|
import { toMs, type Duration } from "../duration.js";
|
|
12
12
|
import { TRACEPARENT_METADATA_KEY, formatTraceparent, newSpanId, newTraceId, parseTraceparent } from "../tracing/traceparent.js";
|
|
13
|
+
import { context as otelContext, isSpanContextValid, SpanKind, trace, type Span } from "@opentelemetry/api";
|
|
14
|
+
import { endApiSpan, remoteParent, startApiSpan } from "../tracing/otel.js";
|
|
13
15
|
|
|
14
16
|
// =============================================================================
|
|
15
17
|
// Timeout
|
|
@@ -134,7 +136,10 @@ export interface InvocationSpan {
|
|
|
134
136
|
* remote caller, when the caller sent a `traceparent` (roadmap R16), or none.
|
|
135
137
|
*/
|
|
136
138
|
readonly parentId: string | undefined;
|
|
137
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* True when the parent span came from the `traceparent` of the call: the caller of a host. With an
|
|
141
|
+
* SDK of OpenTelemetry, a local call of a client also has a remote parent: the span of its client.
|
|
142
|
+
*/
|
|
138
143
|
readonly remoteParent?: boolean | undefined;
|
|
139
144
|
/** The same for every span of one call of a host, and of its caller when it sent a trace. */
|
|
140
145
|
readonly traceId: string;
|
|
@@ -162,6 +167,12 @@ export interface TracingInterceptorOptions {
|
|
|
162
167
|
onStart?: ((span: InvocationSpan) => void) | undefined;
|
|
163
168
|
/** The clock. The default is `performance.now()`. */
|
|
164
169
|
now?: (() => number) | undefined;
|
|
170
|
+
/**
|
|
171
|
+
* With an SDK of OpenTelemetry, each invocation is also a span of the API, and the span ids come
|
|
172
|
+
* from it: a host span (`SERVER`) for a call of a host, an `INTERNAL` span for a nested call.
|
|
173
|
+
* The handler runs in the context of its span. Default: true. With no SDK, this option does nothing.
|
|
174
|
+
*/
|
|
175
|
+
apiSpans?: boolean | undefined;
|
|
165
176
|
}
|
|
166
177
|
|
|
167
178
|
|
|
@@ -173,6 +184,8 @@ export function tracingInterceptor(options: TracingInterceptorOptions): Intercep
|
|
|
173
184
|
const now = options.now ?? (() => performance.now());
|
|
174
185
|
/** The open spans, by the id of their invocation */
|
|
175
186
|
const open = new Map<number, InvocationSpan>();
|
|
187
|
+
/** The open spans of the API, by the id of their invocation */
|
|
188
|
+
const apiOpen = new Map<number, Span>();
|
|
176
189
|
|
|
177
190
|
const finish = (span: InvocationSpan, invocationId: number, error?: unknown): void => {
|
|
178
191
|
span.end = now();
|
|
@@ -186,22 +199,35 @@ export function tracingInterceptor(options: TracingInterceptorOptions): Intercep
|
|
|
186
199
|
span.status = "ok";
|
|
187
200
|
}
|
|
188
201
|
open.delete(invocationId);
|
|
202
|
+
const apiSpan = apiOpen.get(invocationId);
|
|
203
|
+
if (apiSpan !== undefined) {
|
|
204
|
+
apiOpen.delete(invocationId);
|
|
205
|
+
endApiSpan(apiSpan, error);
|
|
206
|
+
}
|
|
189
207
|
options.onSpan(span);
|
|
190
208
|
};
|
|
191
209
|
|
|
192
210
|
return async (invocation, next) => {
|
|
193
211
|
const parent = invocation.parent ? open.get(invocation.parent.id) : undefined;
|
|
194
|
-
|
|
195
|
-
|
|
212
|
+
const key = pathToKey(invocation.path);
|
|
213
|
+
// With an SDK, the span of the API gives the ids (roadmap C8 of otel-ts, D5). The parent is the
|
|
214
|
+
// span of the parent invocation, else the span of the traceparent of the call.
|
|
215
|
+
const parentApi = invocation.parent ? apiOpen.get(invocation.parent.id) : undefined;
|
|
216
|
+
const apiParent = parentApi !== undefined ? trace.setSpan(otelContext.active(), parentApi) : remoteParent(invocation.metadata);
|
|
217
|
+
const apiSpan = options.apiSpans === false ? undefined : startApiSpan(key, invocation.parent ? SpanKind.INTERNAL : SpanKind.SERVER, apiParent);
|
|
218
|
+
const apiParentSpan = apiSpan === undefined ? undefined : trace.getSpanContext(apiParent);
|
|
219
|
+
// With no SDK: a call of a host continues the trace of its caller (W3C traceparent, roadmap R16)
|
|
220
|
+
const remote = parent || apiSpan ? undefined : parseTraceparent(invocation.metadata[TRACEPARENT_METADATA_KEY]);
|
|
196
221
|
// W3C span ids, so a trace backend can join the spans of the client and of the server
|
|
197
|
-
const id = newSpanId();
|
|
222
|
+
const id = apiSpan?.spanContext().spanId ?? newSpanId();
|
|
223
|
+
const apiParentId = apiParentSpan !== undefined && isSpanContextValid(apiParentSpan) ? apiParentSpan.spanId : undefined;
|
|
198
224
|
const span: InvocationSpan = {
|
|
199
225
|
id,
|
|
200
|
-
parentId: parent?.id ?? remote?.spanId,
|
|
201
|
-
...(remote ? { remoteParent: true } : {}),
|
|
202
|
-
traceId: parent?.traceId ?? remote?.traceId ?? newTraceId(),
|
|
226
|
+
parentId: apiSpan ? apiParentId : (parent?.id ?? remote?.spanId),
|
|
227
|
+
...(remote || (apiParentId !== undefined && apiParentSpan?.isRemote === true) ? { remoteParent: true } : {}),
|
|
228
|
+
traceId: apiSpan?.spanContext().traceId ?? parent?.traceId ?? remote?.traceId ?? newTraceId(),
|
|
203
229
|
path: invocation.path,
|
|
204
|
-
key
|
|
230
|
+
key,
|
|
205
231
|
depth: invocation.depth,
|
|
206
232
|
start: now(),
|
|
207
233
|
end: undefined,
|
|
@@ -209,12 +235,16 @@ export function tracingInterceptor(options: TracingInterceptorOptions): Intercep
|
|
|
209
235
|
status: "running",
|
|
210
236
|
};
|
|
211
237
|
open.set(invocation.id, span);
|
|
238
|
+
if (apiSpan !== undefined) apiOpen.set(invocation.id, apiSpan);
|
|
212
239
|
options.onStart?.(span);
|
|
213
240
|
|
|
214
241
|
let output: ProcedureOutput<unknown>;
|
|
215
242
|
try {
|
|
216
243
|
// The nested calls (and their remote calls) carry the span of this invocation (R16)
|
|
217
|
-
|
|
244
|
+
const sampled = apiSpan === undefined || (apiSpan.spanContext().traceFlags & 1) === 1;
|
|
245
|
+
const run = () => next({ metadata: { ...invocation.metadata, [TRACEPARENT_METADATA_KEY]: formatTraceparent(span.traceId, id, sampled) } });
|
|
246
|
+
// The handler runs in the context of its span, so its logs, metrics and calls get the ids
|
|
247
|
+
output = await (apiSpan === undefined ? run() : otelContext.with(trace.setSpan(apiParent, apiSpan), run));
|
|
218
248
|
} catch (error) {
|
|
219
249
|
finish(span, invocation.id, error);
|
|
220
250
|
throw error;
|
|
@@ -286,6 +316,7 @@ export function metricsInterceptor(options: { now?: () => number } = {}): Metric
|
|
|
286
316
|
};
|
|
287
317
|
return {
|
|
288
318
|
interceptor: tracingInterceptor({
|
|
319
|
+
apiSpans: false,
|
|
289
320
|
onSpan: (span) => record(span.key, span.durationMs ?? 0, span.status === "error"),
|
|
290
321
|
...(options.now ? { now: options.now } : {}),
|
|
291
322
|
}),
|