@procedures/client 0.3.0 → 0.5.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 +68 -0
- package/README.md +62 -7
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/client/transport.d.ts +2 -1
- package/dist/adapters/http/client/transport.d.ts.map +1 -1
- package/dist/adapters/http/client/transport.js +9 -2
- package/dist/adapters/http/client/transport.js.map +1 -1
- package/dist/adapters/http/server/transport.d.ts +3 -1
- package/dist/adapters/http/server/transport.d.ts.map +1 -1
- package/dist/adapters/http/server/transport.js +8 -6
- package/dist/adapters/http/server/transport.js.map +1 -1
- package/dist/adapters/local/client/transport.d.ts +2 -1
- package/dist/adapters/local/client/transport.d.ts.map +1 -1
- package/dist/adapters/local/client/transport.js +12 -4
- package/dist/adapters/local/client/transport.js.map +1 -1
- package/dist/adapters/local/client/types.d.ts +6 -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/sequence.d.ts +36 -0
- package/dist/adapters/sequence.d.ts.map +1 -0
- package/dist/adapters/sequence.js +55 -0
- package/dist/adapters/sequence.js.map +1 -0
- package/dist/adapters/websocket/client/node.d.ts +30 -0
- package/dist/adapters/websocket/client/node.d.ts.map +1 -0
- package/dist/adapters/websocket/client/node.js +32 -0
- package/dist/adapters/websocket/client/node.js.map +1 -0
- package/dist/adapters/websocket/client/transport.d.ts +39 -1
- package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/client/transport.js +119 -16
- package/dist/adapters/websocket/client/transport.js.map +1 -1
- package/dist/adapters/websocket/client/types.d.ts +15 -5
- 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 +22 -1
- package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/server/transport.js +29 -1
- package/dist/adapters/websocket/server/transport.js.map +1 -1
- package/dist/adapters/websocket/server/types.d.ts +6 -0
- package/dist/adapters/websocket/server/types.d.ts.map +1 -1
- package/dist/adapters/websocket/shared/rpc.d.ts +4 -3
- package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -1
- package/dist/adapters/websocket/shared/rpc.js +18 -9
- package/dist/adapters/websocket/shared/rpc.js.map +1 -1
- package/dist/browser.d.ts +3 -0
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +4 -0
- package/dist/browser.js.map +1 -1
- package/dist/client/errors/http-errors.d.ts.map +1 -1
- package/dist/client/errors/http-errors.js +6 -0
- package/dist/client/errors/http-errors.js.map +1 -1
- package/dist/client/errors/index.d.ts +1 -1
- package/dist/client/errors/index.js +1 -1
- package/dist/client/errors/registry.d.ts +2 -1
- package/dist/client/errors/registry.d.ts.map +1 -1
- package/dist/client/errors/registry.js +73 -22
- package/dist/client/errors/registry.js.map +1 -1
- package/dist/client/middleware/rate-limit.d.ts.map +1 -1
- package/dist/client/middleware/rate-limit.js +0 -1
- package/dist/client/middleware/rate-limit.js.map +1 -1
- package/dist/client/middleware/timeout.d.ts.map +1 -1
- package/dist/client/middleware/timeout.js +2 -9
- package/dist/client/middleware/timeout.js.map +1 -1
- package/dist/events/index.d.ts +4 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +3 -0
- package/dist/events/index.js.map +1 -1
- package/dist/events/publisher.d.ts +95 -0
- package/dist/events/publisher.d.ts.map +1 -0
- package/dist/events/publisher.js +268 -0
- package/dist/events/publisher.js.map +1 -0
- package/dist/events/remote.d.ts +164 -0
- package/dist/events/remote.d.ts.map +1 -0
- package/dist/events/remote.js +250 -0
- package/dist/events/remote.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +11 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +10 -0
- package/dist/node.js.map +1 -0
- package/dist/procedures/collection/index.d.ts +1 -1
- package/dist/procedures/collection/index.d.ts.map +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 +6 -1
- package/dist/procedures/collection/procedures.js.map +1 -1
- package/dist/procedures/collection/schemas.d.ts +16 -1
- package/dist/procedures/collection/schemas.d.ts.map +1 -1
- package/dist/procedures/collection/schemas.js +13 -2
- package/dist/procedures/collection/schemas.js.map +1 -1
- package/dist/procedures/collection/storage.d.ts +14 -2
- package/dist/procedures/collection/storage.d.ts.map +1 -1
- package/dist/procedures/core/array.d.ts +1 -1
- package/dist/procedures/core/array.d.ts.map +1 -1
- package/dist/procedures/core/array.js +56 -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 +9 -2
- package/dist/procedures/core/index.js.map +1 -1
- package/dist/procedures/core/logic.js +1 -1
- package/dist/procedures/core/logic.js.map +1 -1
- package/dist/procedures/core/math.d.ts.map +1 -1
- package/dist/procedures/core/math.js +5 -1
- package/dist/procedures/core/math.js.map +1 -1
- package/dist/procedures/core/meta.d.ts.map +1 -1
- package/dist/procedures/core/meta.js +41 -7
- package/dist/procedures/core/meta.js.map +1 -1
- package/dist/procedures/core/type.d.ts.map +1 -1
- package/dist/procedures/core/type.js +4 -2
- package/dist/procedures/core/type.js.map +1 -1
- package/dist/procedures/discovery.d.ts +4 -6
- package/dist/procedures/discovery.d.ts.map +1 -1
- package/dist/procedures/discovery.js +4 -6
- package/dist/procedures/discovery.js.map +1 -1
- package/dist/procedures/invoke.d.ts +4 -2
- package/dist/procedures/invoke.d.ts.map +1 -1
- package/dist/procedures/invoke.js +1 -1
- package/dist/procedures/invoke.js.map +1 -1
- package/dist/procedures/ref.d.ts +4 -2
- package/dist/procedures/ref.d.ts.map +1 -1
- package/dist/procedures/ref.js +2 -1
- package/dist/procedures/ref.js.map +1 -1
- package/dist/protocol.d.ts +10 -3
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +13 -2
- package/dist/protocol.js.map +1 -1
- package/dist/server/host-policy.d.ts +11 -0
- package/dist/server/host-policy.d.ts.map +1 -1
- package/dist/server/host-policy.js +5 -0
- package/dist/server/host-policy.js.map +1 -1
- package/dist/server/procedure-server.d.ts +26 -3
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +38 -6
- package/dist/server/procedure-server.js.map +1 -1
- package/package.json +7 -6
- package/src/adapters/http/client/transport.ts +9 -2
- package/src/adapters/http/server/transport.ts +8 -6
- package/src/adapters/local/client/transport.ts +12 -4
- package/src/adapters/local/client/types.ts +7 -0
- package/src/adapters/sequence.ts +55 -0
- package/src/adapters/websocket/client/node.ts +34 -0
- package/src/adapters/websocket/client/transport.ts +128 -16
- package/src/adapters/websocket/client/types.ts +16 -5
- package/src/adapters/websocket/server/transport.ts +43 -1
- package/src/adapters/websocket/server/types.ts +7 -0
- package/src/adapters/websocket/shared/rpc.ts +19 -9
- package/src/browser.ts +15 -0
- package/src/client/errors/README.md +2 -0
- package/src/client/errors/http-errors.ts +5 -0
- package/src/client/errors/index.ts +1 -1
- package/src/client/errors/registry.ts +80 -22
- package/src/client/middleware/rate-limit.ts +0 -2
- package/src/client/middleware/timeout.ts +2 -9
- package/src/events/index.ts +16 -0
- package/src/events/publisher.ts +314 -0
- package/src/events/remote.ts +369 -0
- package/src/index.ts +4 -0
- package/src/node.ts +11 -0
- package/src/procedures/collection/index.ts +1 -0
- package/src/procedures/collection/procedures.ts +6 -1
- package/src/procedures/collection/schemas.ts +28 -2
- package/src/procedures/collection/storage.ts +14 -2
- package/src/procedures/core/array.ts +53 -16
- package/src/procedures/core/index.ts +9 -2
- package/src/procedures/core/logic.ts +1 -1
- package/src/procedures/core/math.ts +5 -1
- package/src/procedures/core/meta.ts +43 -7
- package/src/procedures/core/type.ts +4 -2
- package/src/procedures/discovery.ts +4 -6
- package/src/procedures/invoke.ts +4 -2
- package/src/procedures/ref.ts +4 -2
- package/src/protocol.ts +16 -3
- package/src/server/host-policy.ts +14 -0
- package/src/server/procedure-server.ts +45 -8
- package/dist/procedures/cli/announce.d.ts +0 -67
- package/dist/procedures/cli/announce.d.ts.map +0 -1
- package/dist/procedures/cli/announce.js +0 -392
- package/dist/procedures/cli/announce.js.map +0 -1
- package/dist/procedures/cli/discover.d.ts +0 -66
- package/dist/procedures/cli/discover.d.ts.map +0 -1
- package/dist/procedures/cli/discover.js +0 -310
- package/dist/procedures/cli/discover.js.map +0 -1
- package/dist/procedures/cli/index.d.ts +0 -13
- package/dist/procedures/cli/index.d.ts.map +0 -1
- package/dist/procedures/cli/index.js +0 -203
- package/dist/procedures/cli/index.js.map +0 -1
- package/dist/procedures/cli/init.d.ts +0 -17
- package/dist/procedures/cli/init.d.ts.map +0 -1
- package/dist/procedures/cli/init.js +0 -336
- package/dist/procedures/cli/init.js.map +0 -1
- package/src/procedures/cli/announce.ts +0 -470
- package/src/procedures/cli/discover.ts +0 -390
- package/src/procedures/cli/index.ts +0 -231
- package/src/procedures/cli/init.ts +0 -370
|
@@ -8,6 +8,7 @@ import type { Message, Method } from "../../../client/types.js";
|
|
|
8
8
|
import type { ProcedureRegistry } from "../../../procedures/registry.js";
|
|
9
9
|
import type { Interceptor } from "../../../procedures/invoke.js";
|
|
10
10
|
import type { Capability, ClearanceOrder } from "../../../procedures/types.js";
|
|
11
|
+
import type { EventPublisher } from "../../../events/publisher.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Handler function for a method.
|
|
@@ -65,4 +66,10 @@ export interface LocalTransportOptions {
|
|
|
65
66
|
|
|
66
67
|
/** The order of the clearance values of the capability. */
|
|
67
68
|
clearanceOrder?: ClearanceOrder | undefined;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The publisher of the remote events (`documentation/EVENTS.md`). The transport serves its
|
|
72
|
+
* procedure `events.subscribe`, and its procedures get its bus as `ctx.bus`.
|
|
73
|
+
*/
|
|
74
|
+
events?: EventPublisher | undefined;
|
|
68
75
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sequence numbers of the stream frames (wire protocol v1, `documentation/WIRE-PROTOCOL.md`).
|
|
3
|
+
*
|
|
4
|
+
* - The callee writes `seq` in each `stream` frame of a call: 1 in the first frame, and one more
|
|
5
|
+
* in each next frame. The end frame has a number too. An NDJSON line `item` or `done` has the
|
|
6
|
+
* same field.
|
|
7
|
+
* - The caller drops a frame whose `seq` is not above the last one. Thus a duplicate frame or a
|
|
8
|
+
* late frame gives no item.
|
|
9
|
+
* - A gap is a loss. The delivery is at most once, so the stream continues. The next item has the
|
|
10
|
+
* number of lost frames in its metadata key `stream-gap`, and the caller writes a warning. The
|
|
11
|
+
* caller also gives back the credit of the lost frames.
|
|
12
|
+
* - A frame without `seq` comes from an older peer. The caller takes it as before.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The metadata key of a stream item after a gap: the number of frames that did not arrive
|
|
17
|
+
* immediately before this item. The receiver sets it. It does not cross the wire.
|
|
18
|
+
*/
|
|
19
|
+
export const STREAM_GAP_METADATA_KEY = "stream-gap";
|
|
20
|
+
|
|
21
|
+
/** The order of the stream frames of one call, at the side that receives them. */
|
|
22
|
+
export class StreamSequence {
|
|
23
|
+
private last = 0;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* This function examines the `seq` of a frame. It gives -1 for a frame to drop: its `seq` is
|
|
27
|
+
* not above the last one. Else it gives the number of frames that did not arrive before this
|
|
28
|
+
* frame. A frame without a valid `seq` gives 0.
|
|
29
|
+
*/
|
|
30
|
+
next(seq: unknown, id: string): number {
|
|
31
|
+
if (typeof seq !== "number" || !Number.isSafeInteger(seq) || seq < 1) return 0;
|
|
32
|
+
if (seq <= this.last) return -1;
|
|
33
|
+
const gap = seq - this.last - 1;
|
|
34
|
+
this.last = seq;
|
|
35
|
+
if (gap > 0) console.warn(`[stream] Frames lost before seq ${seq} of the call ${id}: ${gap}`);
|
|
36
|
+
return gap;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* This function gives the metadata of a stream item. The key `stream-gap` has the gap of the
|
|
42
|
+
* receiver only: the receiver removes a key of the same name that the peer sent.
|
|
43
|
+
*/
|
|
44
|
+
export function itemMetadata(metadata: Record<string, unknown> | undefined, gap: number): Record<string, unknown> {
|
|
45
|
+
const out: Record<string, unknown> = { ...metadata };
|
|
46
|
+
delete out[STREAM_GAP_METADATA_KEY];
|
|
47
|
+
if (gap > 0) out[STREAM_GAP_METADATA_KEY] = gap;
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The number of frames that did not arrive immediately before an item: 0 without a gap. */
|
|
52
|
+
export function gapOf(metadata: Record<string, unknown> | undefined): number {
|
|
53
|
+
const gap = metadata?.[STREAM_GAP_METADATA_KEY];
|
|
54
|
+
return typeof gap === "number" ? gap : 0;
|
|
55
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WebSocket client transport for Node (finding of roadmap R36).
|
|
3
|
+
*
|
|
4
|
+
* This class is the `WebSocketTransport` of the browser entry with one other default: its sockets
|
|
5
|
+
* are of the `WebSocket` class of the `ws` package. A drop by the heartbeat or by the time limit
|
|
6
|
+
* of the handshake then ends the socket at once with `terminate()`. The global `WebSocket` of Node
|
|
7
|
+
* has only `close()`, which waits for the answer of the peer. With a dead peer, that socket stays
|
|
8
|
+
* open until the kernel stops it: 924.6 seconds in the harness cells of R36.
|
|
9
|
+
*
|
|
10
|
+
* Only the Node entry `@procedures/client/node` exports this class. The browser entry does not
|
|
11
|
+
* import `ws`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { WebSocket as NodeWebSocket } from "ws";
|
|
15
|
+
import { WebSocketTransport as PortableWebSocketTransport } from "./transport.js";
|
|
16
|
+
import type { WebSocketTransportOptions } from "./types.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The WebSocket client transport with the `WebSocket` class of the `ws` package as the default
|
|
20
|
+
* class of its sockets. The option `webSocket` gives another class.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* import { Client } from "@procedures/client";
|
|
25
|
+
* import { WebSocketTransport } from "@procedures/client/node";
|
|
26
|
+
*
|
|
27
|
+
* const client = new Client({ transport: new WebSocketTransport({ url: "ws://localhost:3000/ws" }) });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export class WebSocketTransport extends PortableWebSocketTransport {
|
|
31
|
+
constructor(options: WebSocketTransportOptions) {
|
|
32
|
+
super({ ...options, webSocket: options.webSocket ?? NodeWebSocket });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -10,6 +10,7 @@ import type { WebSocketTransportOptions, WebSocketMessage, ServerRequestHandler,
|
|
|
10
10
|
import { WebSocketState } from "./types.js";
|
|
11
11
|
import { withoutInternalKeys } from "../../metadata.js";
|
|
12
12
|
import { IncomingCalls } from "../shared/rpc.js";
|
|
13
|
+
import { StreamSequence, gapOf, itemMetadata } from "../../sequence.js";
|
|
13
14
|
import type { RequestHandler, ServerResponse } from "../../../server/types.js";
|
|
14
15
|
import { methodToPath } from "../../../server/method.js";
|
|
15
16
|
import { optionalMs } from "../../../duration.js";
|
|
@@ -20,6 +21,17 @@ import { heartbeat, type Heartbeat } from "@mark1russell7/service/heartbeat";
|
|
|
20
21
|
import { backoffDelay } from "@mark1russell7/service/backoff";
|
|
21
22
|
import type { MachineTransition } from "../../../machines.js";
|
|
22
23
|
import { WEBSOCKET_CONNECTION_MACHINE } from "../../../machines.js";
|
|
24
|
+
import {
|
|
25
|
+
SubscriptionError,
|
|
26
|
+
remoteEvents,
|
|
27
|
+
subscribeRemote,
|
|
28
|
+
type RemoteEvent,
|
|
29
|
+
type RemoteEventHandler,
|
|
30
|
+
type RemoteEventsOptions,
|
|
31
|
+
type SubscribeOptions,
|
|
32
|
+
type Subscription,
|
|
33
|
+
type SubscriptionHooks,
|
|
34
|
+
} from "../../../events/remote.js";
|
|
23
35
|
|
|
24
36
|
/**
|
|
25
37
|
* Pending request waiting for response.
|
|
@@ -74,6 +86,8 @@ interface PendingRequest {
|
|
|
74
86
|
messageId: string;
|
|
75
87
|
/** The response items of the request, in order */
|
|
76
88
|
queue: ItemQueue<ResponseItem<unknown>>;
|
|
89
|
+
/** The `seq` of the stream frames: a duplicate or a late frame gives no item */
|
|
90
|
+
order: StreamSequence;
|
|
77
91
|
/** The request timeout: it runs until the first frame arrives */
|
|
78
92
|
timeout?: Cancel | undefined;
|
|
79
93
|
}
|
|
@@ -96,6 +110,23 @@ interface PendingRequest {
|
|
|
96
110
|
* const client = new Client({ transport });
|
|
97
111
|
* ```
|
|
98
112
|
*/
|
|
113
|
+
/** The `readyState` of an open socket. The global `WebSocket` and the `ws` package use the same number. */
|
|
114
|
+
const OPEN = 1;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The error handler of a socket that the transport left. A `WebSocket` of the `ws` package that
|
|
118
|
+
* closes during its handshake emits an error. Without a handler, that error ends the process.
|
|
119
|
+
*/
|
|
120
|
+
const ignoreError = (): void => undefined;
|
|
121
|
+
|
|
122
|
+
/** This function removes the handlers of the transport from a socket, and keeps `ignoreError`. */
|
|
123
|
+
function detach(ws: WebSocket): void {
|
|
124
|
+
ws.onopen = null;
|
|
125
|
+
ws.onmessage = null;
|
|
126
|
+
ws.onclose = null;
|
|
127
|
+
ws.onerror = ignoreError;
|
|
128
|
+
}
|
|
129
|
+
|
|
99
130
|
function errorItem<TRes>(id: string, code: string, message: string, retryable: boolean): ResponseItem<TRes> {
|
|
100
131
|
return { id, status: { type: "error", code, message, retryable }, payload: null as TRes, metadata: {} };
|
|
101
132
|
}
|
|
@@ -217,6 +248,8 @@ export class WebSocketTransport implements Transport {
|
|
|
217
248
|
private incoming: IncomingCalls | undefined;
|
|
218
249
|
/** The time limit of the handshake of the socket that opens now */
|
|
219
250
|
private handshakeTimer: Cancel | undefined = undefined;
|
|
251
|
+
/** The signal of the subscriptions of the transport: `close()` aborts it, so they end (EVENTS.md) */
|
|
252
|
+
private readonly subscriptions = new AbortController();
|
|
220
253
|
|
|
221
254
|
constructor(options: WebSocketTransportOptions) {
|
|
222
255
|
this.options = {
|
|
@@ -368,10 +401,7 @@ export class WebSocketTransport implements Transport {
|
|
|
368
401
|
const ws = this.ws;
|
|
369
402
|
this.ws = null;
|
|
370
403
|
if (ws) {
|
|
371
|
-
ws
|
|
372
|
-
ws.onmessage = null;
|
|
373
|
-
ws.onclose = null;
|
|
374
|
-
ws.onerror = null;
|
|
404
|
+
detach(ws);
|
|
375
405
|
try {
|
|
376
406
|
// A socket of the ws package ends at once. close() waits for the answer of a peer that can
|
|
377
407
|
// be dead (finding of roadmap R36).
|
|
@@ -500,7 +530,7 @@ export class WebSocketTransport implements Transport {
|
|
|
500
530
|
return new IncomingCalls({
|
|
501
531
|
handler,
|
|
502
532
|
send: (frame) => ws.send(JSON.stringify(frame)),
|
|
503
|
-
isOpen: () => this.ws === ws && ws.readyState ===
|
|
533
|
+
isOpen: () => this.ws === ws && ws.readyState === OPEN,
|
|
504
534
|
// A call of the server that this client opened: the client trusts it, so it has no
|
|
505
535
|
// credentials (a ProcedureServer applies no token to it)
|
|
506
536
|
requestOf: (frame, signal) => ({
|
|
@@ -548,7 +578,8 @@ export class WebSocketTransport implements Transport {
|
|
|
548
578
|
return;
|
|
549
579
|
}
|
|
550
580
|
|
|
551
|
-
//
|
|
581
|
+
// The frame type `event` is reserved: no side of protocol v1 sends it. A server outside this
|
|
582
|
+
// repository can send it, so the frame still goes to onEvent (EVENTS.md).
|
|
552
583
|
if (message.type === "event") {
|
|
553
584
|
if (this.options.onEvent && message.topic && message.subscriptionId) {
|
|
554
585
|
try {
|
|
@@ -563,7 +594,8 @@ export class WebSocketTransport implements Transport {
|
|
|
563
594
|
// Handle response
|
|
564
595
|
const pending = this.pendingRequests.get(message.id);
|
|
565
596
|
if (!pending) {
|
|
566
|
-
|
|
597
|
+
// A stream frame that left before the cancel of its call is normal: an unsubscribe, a break
|
|
598
|
+
if (message.type !== "stream") console.warn(`[${this.name}] Received response for unknown request: ${message.id}`);
|
|
567
599
|
return;
|
|
568
600
|
}
|
|
569
601
|
|
|
@@ -576,6 +608,9 @@ export class WebSocketTransport implements Transport {
|
|
|
576
608
|
// A stream frame: one item, or the end of the stream (BUGS-2026-07 H5: before, the
|
|
577
609
|
// transport resolved on the first frame and dropped the others)
|
|
578
610
|
if (message.type === "stream") {
|
|
611
|
+
// A duplicate or a late frame: its item came before, or the delivery is at most once
|
|
612
|
+
const gap = pending.order.next(message.seq, message.id);
|
|
613
|
+
if (gap < 0) return;
|
|
579
614
|
if (message.stream?.done) {
|
|
580
615
|
this.pendingRequests.delete(message.id);
|
|
581
616
|
pending.queue.end();
|
|
@@ -585,7 +620,7 @@ export class WebSocketTransport implements Transport {
|
|
|
585
620
|
id: pending.messageId,
|
|
586
621
|
status: { type: "success", code: 200 },
|
|
587
622
|
payload: message.payload,
|
|
588
|
-
metadata: message.metadata
|
|
623
|
+
metadata: itemMetadata(message.metadata, gap),
|
|
589
624
|
});
|
|
590
625
|
return;
|
|
591
626
|
}
|
|
@@ -686,7 +721,7 @@ export class WebSocketTransport implements Transport {
|
|
|
686
721
|
};
|
|
687
722
|
|
|
688
723
|
const queue = new ItemQueue<ResponseItem<unknown>>();
|
|
689
|
-
const pending: PendingRequest = { messageId: message.id, queue };
|
|
724
|
+
const pending: PendingRequest = { messageId: message.id, queue, order: new StreamSequence() };
|
|
690
725
|
pending.timeout = this.clock.after(this.options.requestTimeout, () => {
|
|
691
726
|
if (this.pendingRequests.get(wireId) !== pending) return;
|
|
692
727
|
this.pendingRequests.delete(wireId);
|
|
@@ -719,8 +754,10 @@ export class WebSocketTransport implements Transport {
|
|
|
719
754
|
let taken = 0;
|
|
720
755
|
for await (const item of queue) {
|
|
721
756
|
yield item as ResponseItem<TRes>;
|
|
722
|
-
// The reader took the item: give the server credit, in batches of half the window
|
|
723
|
-
|
|
757
|
+
// The reader took the item: give the server credit, in batches of half the window.
|
|
758
|
+
// The frames of a gap took credit too, and no reader takes them.
|
|
759
|
+
taken += 1 + gapOf(item.metadata);
|
|
760
|
+
if (taken >= window / 2 && this.pendingRequests.get(wireId) === pending) {
|
|
724
761
|
this.sendCredit(wireId, taken);
|
|
725
762
|
taken = 0;
|
|
726
763
|
}
|
|
@@ -835,9 +872,87 @@ export class WebSocketTransport implements Transport {
|
|
|
835
872
|
}
|
|
836
873
|
|
|
837
874
|
/**
|
|
838
|
-
*
|
|
875
|
+
* This function subscribes to topics of the bus of the server (`documentation/EVENTS.md`). The
|
|
876
|
+
* handler gets each event in order. After a drop, the transport sends the subscription again on
|
|
877
|
+
* the new connection. The events between the drop and the new confirmation are lost.
|
|
878
|
+
*
|
|
879
|
+
* @param topics - One topic, or a list of 1 to 64 topics
|
|
880
|
+
* @param handler - The handler of each event. A promise that it gives holds the next event.
|
|
881
|
+
* @param options - `onSubscribed`, `onError`, a signal, the metadata of the call
|
|
882
|
+
* @returns The unsubscribe function, with the `ready` promise of the first confirmation
|
|
883
|
+
*
|
|
884
|
+
* @example
|
|
885
|
+
* ```typescript
|
|
886
|
+
* const unsubscribe = transport.subscribe("orders.created", (order) => console.log(order));
|
|
887
|
+
* await unsubscribe.ready;
|
|
888
|
+
* unsubscribe();
|
|
889
|
+
* ```
|
|
890
|
+
*/
|
|
891
|
+
subscribe<T = unknown>(topics: string | readonly string[], handler: RemoteEventHandler<T>, options: SubscribeOptions = {}): Subscription {
|
|
892
|
+
return subscribeRemote<T>(this, topics, handler, options, this.subscriptionHooks());
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* This function gives the events of topics of the bus of the server as an async iterator. A
|
|
897
|
+
* `break` or the signal ends the subscription. The transport resubscribes after a drop, as for
|
|
898
|
+
* `subscribe()`. An error that ends the subscription ends the iteration.
|
|
899
|
+
*
|
|
900
|
+
* @param topics - One topic, or a list of 1 to 64 topics
|
|
901
|
+
* @param options - A signal, `onSubscribed`, the metadata of the call
|
|
902
|
+
*/
|
|
903
|
+
events<T = unknown>(topics: string | readonly string[], options: RemoteEventsOptions = {}): AsyncIterable<RemoteEvent<T>> {
|
|
904
|
+
return remoteEvents<T>(this, topics, options, this.subscriptionHooks());
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/** The parts of the subscriptions of this transport: its connection, its backoff, its close and `onEvent`. */
|
|
908
|
+
private subscriptionHooks(): SubscriptionHooks {
|
|
909
|
+
return {
|
|
910
|
+
lifetime: this.subscriptions.signal,
|
|
911
|
+
ready: (signal) => this.whenConnected(signal),
|
|
912
|
+
backoff: (attempt) => reconnectDelay(attempt, this.options.reconnect),
|
|
913
|
+
onEvent: (event) => {
|
|
914
|
+
if (!this.options.onEvent) return;
|
|
915
|
+
try {
|
|
916
|
+
this.options.onEvent(event.topic, event.data, event.subscription);
|
|
917
|
+
} catch (error) {
|
|
918
|
+
console.error(`[${this.name}] Event handler error:`, error);
|
|
919
|
+
}
|
|
920
|
+
},
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* This function resolves when the transport is connected, or when the signal aborts. It has no
|
|
926
|
+
* time limit: a subscription waits for each reconnect. It rejects when the transport closed or
|
|
927
|
+
* stopped its reconnects.
|
|
928
|
+
*/
|
|
929
|
+
private whenConnected(signal: AbortSignal): Promise<void> {
|
|
930
|
+
const closed = (): SubscriptionError => new SubscriptionError("CONNECTION_CLOSED", "The WebSocket transport is closed", false);
|
|
931
|
+
if (this.state === WebSocketState.CONNECTED || signal.aborted) return Promise.resolve();
|
|
932
|
+
if (this.state === WebSocketState.DISCONNECTED || this.closing) return Promise.reject(closed());
|
|
933
|
+
return new Promise((resolve, reject) => {
|
|
934
|
+
const finish = (error?: SubscriptionError): void => {
|
|
935
|
+
this.stateListeners.delete(check);
|
|
936
|
+
signal.removeEventListener("abort", onAbort);
|
|
937
|
+
if (error) reject(error);
|
|
938
|
+
else resolve();
|
|
939
|
+
};
|
|
940
|
+
const check = (): void => {
|
|
941
|
+
if (this.state === WebSocketState.CONNECTED) finish();
|
|
942
|
+
else if (this.state === WebSocketState.DISCONNECTED) finish(closed());
|
|
943
|
+
};
|
|
944
|
+
const onAbort = (): void => finish();
|
|
945
|
+
this.stateListeners.add(check);
|
|
946
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Close WebSocket connection. The requests in progress fail, and the subscriptions end.
|
|
839
952
|
*/
|
|
840
953
|
async close(): Promise<void> {
|
|
954
|
+
// The subscriptions end first: each one sends its cancel while the socket is open
|
|
955
|
+
this.subscriptions.abort();
|
|
841
956
|
this.closing = true;
|
|
842
957
|
this.setState(WebSocketState.DISCONNECTING, "close");
|
|
843
958
|
this.stopHeartbeat();
|
|
@@ -854,10 +969,7 @@ export class WebSocketTransport implements Transport {
|
|
|
854
969
|
if (this.ws) {
|
|
855
970
|
const ws = this.ws;
|
|
856
971
|
this.ws = null;
|
|
857
|
-
ws
|
|
858
|
-
ws.onmessage = null;
|
|
859
|
-
ws.onclose = null;
|
|
860
|
-
ws.onerror = null;
|
|
972
|
+
detach(ws);
|
|
861
973
|
ws.close();
|
|
862
974
|
}
|
|
863
975
|
|
|
@@ -152,8 +152,10 @@ export interface WebSocketTransportOptions {
|
|
|
152
152
|
serve?: RequestHandler;
|
|
153
153
|
|
|
154
154
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
155
|
+
* This handler gets each event of each subscription of this transport (`subscribe()` and
|
|
156
|
+
* `events()`), with the id of its subscription (`documentation/EVENTS.md`). It is a wrapper: the
|
|
157
|
+
* handler of a subscription gets the same events. It also gets an `event` frame of a server
|
|
158
|
+
* outside this repository. No side of protocol v1 sends that frame.
|
|
157
159
|
*/
|
|
158
160
|
onEvent?: EventHandler;
|
|
159
161
|
|
|
@@ -170,8 +172,9 @@ export interface WebSocketTransportOptions {
|
|
|
170
172
|
onTransition?: (transition: MachineTransition) => void;
|
|
171
173
|
|
|
172
174
|
/**
|
|
173
|
-
* The class of the sockets. The default is the global `WebSocket`.
|
|
174
|
-
*
|
|
175
|
+
* The class of the sockets. The default is the global `WebSocket`. The `WebSocketTransport` of
|
|
176
|
+
* the Node entry `@procedures/client/node` has the `WebSocket` of the `ws` package as its
|
|
177
|
+
* default. With that class, a drop ends its socket at once with `terminate()`. The global
|
|
175
178
|
* `WebSocket` has only `close()`, which waits for the answer of the peer. With a dead peer, the
|
|
176
179
|
* socket stays open until the kernel stops it, at most 15 minutes (the harness cells of R36).
|
|
177
180
|
*/
|
|
@@ -249,6 +252,13 @@ export interface WebSocketMessage<T = unknown> {
|
|
|
249
252
|
*/
|
|
250
253
|
credit?: number;
|
|
251
254
|
|
|
255
|
+
/**
|
|
256
|
+
* The number of a `stream` frame in its call: 1 in the first frame, and one more in each next
|
|
257
|
+
* frame. The receiver drops a frame whose `seq` is not above the last one. A frame of an older
|
|
258
|
+
* peer has no `seq`.
|
|
259
|
+
*/
|
|
260
|
+
seq?: number;
|
|
261
|
+
|
|
252
262
|
/** Stream indicator (for streaming responses) */
|
|
253
263
|
stream?: {
|
|
254
264
|
/** Is this the last message in the stream? */
|
|
@@ -285,7 +295,8 @@ export type ServerRequestHandler = (
|
|
|
285
295
|
) => Promise<unknown>;
|
|
286
296
|
|
|
287
297
|
/**
|
|
288
|
-
*
|
|
298
|
+
* The handler of the `onEvent` option: it gets the topic, the data and the id of the subscription
|
|
299
|
+
* of each event.
|
|
289
300
|
*/
|
|
290
301
|
export type EventHandler = (
|
|
291
302
|
topic: string,
|
|
@@ -24,6 +24,15 @@ import { optionalMs } from "../../../duration.js";
|
|
|
24
24
|
import { credentialsOf, withoutSecretHeaders } from "../../../server/request-auth.js";
|
|
25
25
|
import { PROTOCOL_VERSION, protocolVersionOf } from "../../../protocol.js";
|
|
26
26
|
import { TIMER_DEFAULTS } from "../../../timers.js";
|
|
27
|
+
import {
|
|
28
|
+
remoteEvents,
|
|
29
|
+
subscribeRemote,
|
|
30
|
+
type RemoteEvent,
|
|
31
|
+
type RemoteEventHandler,
|
|
32
|
+
type RemoteEventsOptions,
|
|
33
|
+
type SubscribeOptions,
|
|
34
|
+
type Subscription,
|
|
35
|
+
} from "../../../events/remote.js";
|
|
27
36
|
|
|
28
37
|
/**
|
|
29
38
|
* WebSocket server transport adapter.
|
|
@@ -389,6 +398,8 @@ export class WebSocketServerTransport implements ServerTransport {
|
|
|
389
398
|
if (channel.incoming.receive(message)) return;
|
|
390
399
|
// A server-response frame of an older client, for a call that no longer exists
|
|
391
400
|
if (message.type === "server-response") return;
|
|
401
|
+
// A late answer of a call that the server stopped: a cancel (an unsubscribe), a time limit
|
|
402
|
+
if (message.type === "stream" || message.type === "response" || message.type === "error") return;
|
|
392
403
|
this.sendError(ws, message.id || "unknown", "Invalid message format");
|
|
393
404
|
}
|
|
394
405
|
|
|
@@ -524,6 +535,38 @@ export class WebSocketServerTransport implements ServerTransport {
|
|
|
524
535
|
};
|
|
525
536
|
}
|
|
526
537
|
|
|
538
|
+
/**
|
|
539
|
+
* This function subscribes to topics of the bus of a client (`documentation/EVENTS.md`). The
|
|
540
|
+
* client serves them with its `serve` option: a `ProcedureServer` with the option `events`. The
|
|
541
|
+
* subscription ends with its connection: a reconnect is a new connection, so the server
|
|
542
|
+
* subscribes again in its `onConnect` handler. The `onError` handler gets the end.
|
|
543
|
+
*
|
|
544
|
+
* @param connectionId - The id of the connection of the client
|
|
545
|
+
* @param topics - One topic, or a list of 1 to 64 topics
|
|
546
|
+
* @param handler - The handler of each event, in order
|
|
547
|
+
* @param options - `onSubscribed`, `onError`, a signal, the metadata of the call
|
|
548
|
+
*/
|
|
549
|
+
subscribe<T = unknown>(
|
|
550
|
+
connectionId: string,
|
|
551
|
+
topics: string | readonly string[],
|
|
552
|
+
handler: RemoteEventHandler<T>,
|
|
553
|
+
options: SubscribeOptions = {},
|
|
554
|
+
): Subscription {
|
|
555
|
+
return subscribeRemote<T>(this.transportFor(connectionId), topics, handler, options, { resubscribe: false });
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* This function gives the events of topics of the bus of a client as an async iterator. The
|
|
560
|
+
* iteration ends with the connection, with an error.
|
|
561
|
+
*
|
|
562
|
+
* @param connectionId - The id of the connection of the client
|
|
563
|
+
* @param topics - One topic, or a list of 1 to 64 topics
|
|
564
|
+
* @param options - A signal, `onSubscribed`, the metadata of the call
|
|
565
|
+
*/
|
|
566
|
+
events<T = unknown>(connectionId: string, topics: string | readonly string[], options: RemoteEventsOptions = {}): AsyncIterable<RemoteEvent<T>> {
|
|
567
|
+
return remoteEvents<T>(this.transportFor(connectionId), topics, options, { resubscribe: false });
|
|
568
|
+
}
|
|
569
|
+
|
|
527
570
|
/**
|
|
528
571
|
* Call a procedure on a specific client (server-to-client RPC). It gives the value of the
|
|
529
572
|
* call, and fails with its error. It is a wrapper of `callConnection` (roadmap R24).
|
|
@@ -550,7 +593,6 @@ export class WebSocketServerTransport implements ServerTransport {
|
|
|
550
593
|
}
|
|
551
594
|
|
|
552
595
|
/**
|
|
553
|
-
* Register a handler for new connections. /**
|
|
554
596
|
* Register a handler for new connections.
|
|
555
597
|
* Returns a function to unregister the handler.
|
|
556
598
|
*/
|
|
@@ -150,6 +150,13 @@ export interface WebSocketMessage<TReq = unknown> {
|
|
|
150
150
|
*/
|
|
151
151
|
credit?: number;
|
|
152
152
|
|
|
153
|
+
/**
|
|
154
|
+
* The number of a `stream` frame in its call: 1 in the first frame, and one more in each next
|
|
155
|
+
* frame. The receiver drops a frame whose `seq` is not above the last one. A frame of an older
|
|
156
|
+
* peer has no `seq`.
|
|
157
|
+
*/
|
|
158
|
+
seq?: number;
|
|
159
|
+
|
|
153
160
|
/** Stream indicator (for streaming responses) */
|
|
154
161
|
stream?: {
|
|
155
162
|
/** Is this the last message in the stream? */
|
|
@@ -15,6 +15,7 @@ import type { Message, ResponseItem } from "../../../client/types.js";
|
|
|
15
15
|
import type { RequestHandler, ServerRequest } from "../../../server/types.js";
|
|
16
16
|
import type { WebSocketMessage } from "../server/types.js";
|
|
17
17
|
import { withoutInternalKeys } from "../../metadata.js";
|
|
18
|
+
import { StreamSequence, gapOf, itemMetadata } from "../../sequence.js";
|
|
18
19
|
import { DELIVERY_METADATA_KEY } from "../../../client/middleware/items.js";
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -143,6 +144,8 @@ interface PendingCall {
|
|
|
143
144
|
messageId: string;
|
|
144
145
|
/** The response items of the call, in order */
|
|
145
146
|
queue: ItemQueue<ResponseItem<unknown>>;
|
|
147
|
+
/** The `seq` of the stream frames: a duplicate or a late frame gives no item */
|
|
148
|
+
order: StreamSequence;
|
|
146
149
|
/** The time limit until the first frame */
|
|
147
150
|
timeout?: ReturnType<typeof setTimeout> | undefined;
|
|
148
151
|
}
|
|
@@ -192,7 +195,7 @@ export class OutgoingCalls {
|
|
|
192
195
|
const wireId = `${this.options.idPrefix}${message.id}~${++this.seq}`;
|
|
193
196
|
const window = this.options.streamWindow;
|
|
194
197
|
const queue = new ItemQueue<ResponseItem<unknown>>();
|
|
195
|
-
const pending: PendingCall = { messageId: message.id, queue };
|
|
198
|
+
const pending: PendingCall = { messageId: message.id, queue, order: new StreamSequence() };
|
|
196
199
|
const limited = Number.isFinite(this.options.requestTimeoutMs) && this.options.requestTimeoutMs > 0;
|
|
197
200
|
if (limited) pending.timeout = setTimeout(() => {
|
|
198
201
|
if (this.pending.get(wireId) !== pending) return;
|
|
@@ -231,8 +234,10 @@ export class OutgoingCalls {
|
|
|
231
234
|
let taken = 0;
|
|
232
235
|
for await (const item of queue) {
|
|
233
236
|
yield item as ResponseItem<TRes>;
|
|
234
|
-
// The reader took the item: give the other side credit, in batches of half the window
|
|
235
|
-
|
|
237
|
+
// The reader took the item: give the other side credit, in batches of half the window.
|
|
238
|
+
// The frames of a gap took credit too, and no reader takes them.
|
|
239
|
+
taken += 1 + gapOf(item.metadata);
|
|
240
|
+
if (taken >= window / 2 && this.pending.get(wireId) === pending) {
|
|
236
241
|
this.sendCredit(wireId, taken);
|
|
237
242
|
taken = 0;
|
|
238
243
|
}
|
|
@@ -262,12 +267,15 @@ export class OutgoingCalls {
|
|
|
262
267
|
pending.timeout = undefined;
|
|
263
268
|
}
|
|
264
269
|
if (frame.type === "stream" && !frame.status) {
|
|
270
|
+
// A duplicate or a late frame: its item came before, or the delivery is at most once
|
|
271
|
+
const gap = pending.order.next(frame.seq, frame.id);
|
|
272
|
+
if (gap < 0) return true;
|
|
265
273
|
if (frame.stream?.done) {
|
|
266
274
|
this.pending.delete(frame.id);
|
|
267
275
|
pending.queue.end();
|
|
268
276
|
return true;
|
|
269
277
|
}
|
|
270
|
-
pending.queue.push({ id: pending.messageId, status: { type: "success", code: 200 }, payload: frame.payload, metadata: frame.metadata
|
|
278
|
+
pending.queue.push({ id: pending.messageId, status: { type: "success", code: 200 }, payload: frame.payload, metadata: itemMetadata(frame.metadata, gap) });
|
|
271
279
|
return true;
|
|
272
280
|
}
|
|
273
281
|
this.pending.delete(frame.id);
|
|
@@ -394,13 +402,15 @@ export class IncomingCalls {
|
|
|
394
402
|
|
|
395
403
|
/**
|
|
396
404
|
* Send a stream (BUGS-2026-07 H5): one `stream` frame for each item, then a frame with
|
|
397
|
-
* `stream.done`. An error of the stream ends it with
|
|
398
|
-
* (a cancel frame, a closed socket), the loop stops,
|
|
399
|
-
* return(). The stream takes its next item only when the
|
|
405
|
+
* `stream.done`. Each `stream` frame has its `seq`, from 1. An error of the stream ends it with
|
|
406
|
+
* an `error` frame. When the signal aborts (a cancel frame, a closed socket), the loop stops,
|
|
407
|
+
* and the handler of the stream gets return(). The stream takes its next item only when the
|
|
408
|
+
* reader has credit for it.
|
|
400
409
|
*/
|
|
401
410
|
private async sendStream(id: string, stream: AsyncIterable<unknown>, signal: AbortSignal, credit: StreamCredit | undefined): Promise<void> {
|
|
402
411
|
const iterator = stream[Symbol.asyncIterator]();
|
|
403
412
|
let done = false;
|
|
413
|
+
let seq = 0;
|
|
404
414
|
try {
|
|
405
415
|
for (;;) {
|
|
406
416
|
if (credit && !(await credit.take(signal))) return;
|
|
@@ -411,11 +421,11 @@ export class IncomingCalls {
|
|
|
411
421
|
break;
|
|
412
422
|
}
|
|
413
423
|
if (signal.aborted || !this.options.isOpen()) return;
|
|
414
|
-
const item: WebSocketMessage = { id, type: "stream", payload: result.value, stream: { done: false } };
|
|
424
|
+
const item: WebSocketMessage = { id, type: "stream", seq: ++seq, payload: result.value, stream: { done: false } };
|
|
415
425
|
if (this.options.sendWithBackpressure) await this.options.sendWithBackpressure(item, signal);
|
|
416
426
|
else this.send(item);
|
|
417
427
|
}
|
|
418
|
-
this.send({ id, type: "stream", stream: { done: true } });
|
|
428
|
+
this.send({ id, type: "stream", seq: ++seq, stream: { done: true } });
|
|
419
429
|
} catch (error) {
|
|
420
430
|
done = true;
|
|
421
431
|
if (signal.aborted) return;
|
package/src/browser.ts
CHANGED
|
@@ -323,6 +323,8 @@ export type { TraceContext } from "./tracing/traceparent.js";
|
|
|
323
323
|
// Wire protocol v1: the version and the one list of error codes (roadmap R25)
|
|
324
324
|
export { PROTOCOL_VERSION, PROTOCOL_HEADER, ERROR_CODES, isProtocolErrorCode, httpStatusOf, protocolVersionOf } from "./protocol.js";
|
|
325
325
|
export type { ProtocolErrorCode, ErrorCodeInfo } from "./protocol.js";
|
|
326
|
+
// The seq of the stream frames: the metadata key of an item after a gap (WIRE-PROTOCOL.md)
|
|
327
|
+
export { STREAM_GAP_METADATA_KEY } from "./adapters/sequence.js";
|
|
326
328
|
// The deadline of a call: the remaining time goes as procedure-timeout (roadmap R10)
|
|
327
329
|
export { TIMEOUT_METADATA_KEY, DEADLINE_METADATA_KEY } from "./client/deadline.js";
|
|
328
330
|
// Typed durations: each public option that is a time takes a Duration (roadmap R11)
|
|
@@ -411,6 +413,19 @@ export type {
|
|
|
411
413
|
EventWaitOptions,
|
|
412
414
|
ChannelMap,
|
|
413
415
|
} from "./events/index.js";
|
|
416
|
+
// The remote events: a peer subscribes to the topics of the bus of another peer (EVENTS.md)
|
|
417
|
+
export { EventPublisher, topicMatcher, EVENTS_SUBSCRIBE_PATH, SubscriptionError, remoteEvents, subscribeRemote } from "./events/index.js";
|
|
418
|
+
export type {
|
|
419
|
+
EventPublisherOptions,
|
|
420
|
+
TopicRule,
|
|
421
|
+
TopicCall,
|
|
422
|
+
EventItem,
|
|
423
|
+
RemoteEvent,
|
|
424
|
+
RemoteEventHandler,
|
|
425
|
+
RemoteEventsOptions,
|
|
426
|
+
SubscribeOptions,
|
|
427
|
+
Subscription,
|
|
428
|
+
} from "./events/index.js";
|
|
414
429
|
|
|
415
430
|
// ============================================================================
|
|
416
431
|
// Components (serializable UI descriptors) moved to @procedures/splay, the UI package
|
|
@@ -213,6 +213,8 @@ console.error(formatError(error, true)); // include stack trace
|
|
|
213
213
|
2. Add metadata to `ERROR_REGISTRY` in `registry.ts` or `HTTP_ERROR_REGISTRY` in `http-errors.ts`
|
|
214
214
|
3. If HTTP-related, add to `HTTP_STATUS_ERROR_MAP` in `http-errors.ts`
|
|
215
215
|
|
|
216
|
+
`ERROR_CODES` of the wire protocol (`src/protocol.ts`, `WIRE-PROTOCOL.md`) is the source of the codes of the protocol. `ERROR_REGISTRY` has an entry for each of them, and that entry takes its HTTP status and its retry flag from `ERROR_CODES`. To add a code of the protocol, add it to `ERROR_CODES`. A code without texts in `registry.ts` gets the meaning of the protocol as its texts. `defineProtocolError` gives a code of the protocol its own texts.
|
|
217
|
+
|
|
216
218
|
Example:
|
|
217
219
|
```typescript
|
|
218
220
|
// 1. Add to ErrorCode enum
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { ErrorCategory, ErrorSeverity, type ErrorMetadata } from "./types.js";
|
|
9
9
|
import { ErrorCode } from "./codes.js";
|
|
10
10
|
import { HTTPStatus } from "./http-status.js";
|
|
11
|
+
import { httpStatusOf } from "../../protocol.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Helper to create HTTP error metadata.
|
|
@@ -595,6 +596,10 @@ export function httpStatusToErrorCode(status: number): ErrorCode {
|
|
|
595
596
|
* ```
|
|
596
597
|
*/
|
|
597
598
|
export function errorCodeToHTTPStatus(code: ErrorCode): HTTPStatus {
|
|
599
|
+
// A code of the wire protocol has the status of ERROR_CODES: before, TIMEOUT gave 408 here and
|
|
600
|
+
// 504 on the wire
|
|
601
|
+
const protocolStatus = httpStatusOf(code);
|
|
602
|
+
if (protocolStatus !== undefined) return protocolStatus as HTTPStatus;
|
|
598
603
|
// Build map from HTTP_STATUS_ERROR_MAP
|
|
599
604
|
const entry = HTTP_STATUS_ERROR_MAP.find(([, errorCode]) => errorCode === code);
|
|
600
605
|
return entry?.[0] ?? HTTPStatus.INTERNAL_SERVER_ERROR;
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* console.log(error.metadata.userMessage); // User-friendly message
|
|
21
21
|
* console.log(error.metadata.devMessage); // Developer diagnostic
|
|
22
22
|
* console.log(error.metadata.suggestions); // Suggested actions
|
|
23
|
-
* console.log(error.metadata.retryable); //
|
|
23
|
+
* console.log(error.metadata.retryable); // false (ERROR_CODES of the protocol)
|
|
24
24
|
*
|
|
25
25
|
* // Format for logging
|
|
26
26
|
* console.log(formatError(error));
|