@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
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "./types.js";
|
|
16
16
|
import { HTTP_ERROR_REGISTRY } from "./http-errors.js";
|
|
17
17
|
import { ErrorCode } from "./codes.js";
|
|
18
|
+
import { ERROR_CODES, type ProtocolErrorCode } from "../../protocol.js";
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Create error metadata with defaults.
|
|
@@ -47,12 +48,72 @@ function defineError(
|
|
|
47
48
|
return result;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
/** The texts of an entry. For a code of the protocol, `ERROR_CODES` gives the status and the retry flag. */
|
|
52
|
+
type ProtocolErrorTexts = Omit<ErrorMetadata, "code" | "httpStatus" | "retryable">;
|
|
53
|
+
|
|
50
54
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
|
|
55
|
+
* This function makes the entry of a code of the wire protocol. The HTTP status and the retry
|
|
56
|
+
* flag come from `ERROR_CODES` (`protocol.ts`, `WIRE-PROTOCOL.md`), the source of these facts.
|
|
57
|
+
*/
|
|
58
|
+
function defineProtocolError(code: ProtocolErrorCode, texts: ProtocolErrorTexts): ErrorMetadata {
|
|
59
|
+
const info = ERROR_CODES[code];
|
|
60
|
+
return defineError(code, { ...texts, httpStatus: info.http, retryable: info.retryable });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The category of each code of the protocol, for a code that the registry does not describe. */
|
|
64
|
+
const PROTOCOL_CATEGORIES: Readonly<Record<ProtocolErrorCode, ErrorCategory>> = {
|
|
65
|
+
NOT_FOUND: ErrorCategory.CLIENT,
|
|
66
|
+
NO_HANDLER: ErrorCategory.SERVER,
|
|
67
|
+
NOT_EXPOSED: ErrorCategory.AUTH,
|
|
68
|
+
NOT_DECLARED: ErrorCategory.AUTH,
|
|
69
|
+
FORBIDDEN: ErrorCategory.AUTH,
|
|
70
|
+
UNAUTHORIZED: ErrorCategory.AUTH,
|
|
71
|
+
VALIDATION_ERROR: ErrorCategory.CLIENT,
|
|
72
|
+
INVALID_REQUEST: ErrorCategory.PROTOCOL,
|
|
73
|
+
PROTOCOL_VERSION: ErrorCategory.PROTOCOL,
|
|
74
|
+
OUTPUT_VALIDATION_ERROR: ErrorCategory.SERVER,
|
|
75
|
+
NO_OUTPUT: ErrorCategory.SERVER,
|
|
76
|
+
HANDLER_ERROR: ErrorCategory.SERVER,
|
|
77
|
+
INTERNAL_ERROR: ErrorCategory.SERVER,
|
|
78
|
+
NOT_ACCEPTABLE: ErrorCategory.CLIENT,
|
|
79
|
+
DUPLICATE: ErrorCategory.CLIENT,
|
|
80
|
+
DUPLICATE_REQUEST: ErrorCategory.CLIENT,
|
|
81
|
+
ABORTED: ErrorCategory.CANCELLED,
|
|
82
|
+
TIMEOUT: ErrorCategory.TIMEOUT,
|
|
83
|
+
UNAVAILABLE: ErrorCategory.SERVER,
|
|
84
|
+
CONNECTION_CLOSED: ErrorCategory.NETWORK,
|
|
85
|
+
CONNECTION_NOT_FOUND: ErrorCategory.CLIENT,
|
|
86
|
+
TOPIC_NOT_FOUND: ErrorCategory.CLIENT,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* This function gives the registry with one entry for each code of the protocol. The status and
|
|
91
|
+
* the retry flag of such an entry come from `ERROR_CODES`, so the two lists agree (before, the
|
|
92
|
+
* registry gave `TIMEOUT` the status 408 and two codes another retry flag). A code without texts
|
|
93
|
+
* here gets the meaning of the protocol as its texts.
|
|
94
|
+
*/
|
|
95
|
+
function withProtocolCodes(described: ErrorRegistry): ErrorRegistry {
|
|
96
|
+
const registry: ErrorRegistry = { ...described };
|
|
97
|
+
for (const code of Object.keys(ERROR_CODES) as ProtocolErrorCode[]) {
|
|
98
|
+
const info = ERROR_CODES[code];
|
|
99
|
+
const meaning = `${info.meaning}.`;
|
|
100
|
+
const texts: ProtocolErrorTexts = described[code] ?? {
|
|
101
|
+
summary: info.meaning,
|
|
102
|
+
detail: meaning,
|
|
103
|
+
category: PROTOCOL_CATEGORIES[code],
|
|
104
|
+
severity: info.retryable ? ErrorSeverity.WARNING : ErrorSeverity.ERROR,
|
|
105
|
+
userMessage: meaning,
|
|
106
|
+
devMessage: meaning,
|
|
107
|
+
};
|
|
108
|
+
registry[code] = { ...texts, code, httpStatus: info.http, retryable: info.retryable };
|
|
109
|
+
}
|
|
110
|
+
return registry;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The entries with their own texts. `ERROR_REGISTRY` adds the codes of the protocol.
|
|
54
115
|
*/
|
|
55
|
-
|
|
116
|
+
const DESCRIBED_ERRORS = {
|
|
56
117
|
//
|
|
57
118
|
// ═══ Network Errors ═══
|
|
58
119
|
//
|
|
@@ -108,16 +169,14 @@ export const ERROR_REGISTRY: ErrorRegistry = {
|
|
|
108
169
|
// ═══ Timeout Errors ═══
|
|
109
170
|
//
|
|
110
171
|
|
|
111
|
-
TIMEOUT:
|
|
172
|
+
TIMEOUT: defineProtocolError("TIMEOUT", {
|
|
112
173
|
summary: "Request timed out",
|
|
113
174
|
detail:
|
|
114
175
|
"The request exceeded the configured timeout duration. This may indicate slow server response, network congestion, or overly aggressive timeout settings.",
|
|
115
176
|
category: ErrorCategory.TIMEOUT,
|
|
116
|
-
retryable: true,
|
|
117
177
|
severity: ErrorSeverity.WARNING,
|
|
118
178
|
userMessage: "The request took too long to complete. Please try again.",
|
|
119
179
|
devMessage: "Request exceeded timeout threshold. Consider increasing timeout or optimizing server response time.",
|
|
120
|
-
httpStatus: 408,
|
|
121
180
|
suggestions: [
|
|
122
181
|
"Try again with a longer timeout",
|
|
123
182
|
"Check server performance",
|
|
@@ -159,11 +218,10 @@ export const ERROR_REGISTRY: ErrorRegistry = {
|
|
|
159
218
|
// ═══ Cancellation Errors ═══
|
|
160
219
|
//
|
|
161
220
|
|
|
162
|
-
ABORTED:
|
|
221
|
+
ABORTED: defineProtocolError("ABORTED", {
|
|
163
222
|
summary: "Request was aborted",
|
|
164
223
|
detail: "The request was explicitly cancelled by the client via AbortSignal. This is user-initiated.",
|
|
165
224
|
category: ErrorCategory.CANCELLED,
|
|
166
|
-
retryable: false,
|
|
167
225
|
severity: ErrorSeverity.INFO,
|
|
168
226
|
userMessage: "The request was cancelled.",
|
|
169
227
|
devMessage: "Request aborted via AbortSignal. This is expected and not an error condition.",
|
|
@@ -269,15 +327,13 @@ export const ERROR_REGISTRY: ErrorRegistry = {
|
|
|
269
327
|
],
|
|
270
328
|
}),
|
|
271
329
|
|
|
272
|
-
UNAUTHORIZED:
|
|
330
|
+
UNAUTHORIZED: defineProtocolError("UNAUTHORIZED", {
|
|
273
331
|
summary: "Unauthorized",
|
|
274
332
|
detail: "Authentication is required but was not provided or is invalid.",
|
|
275
333
|
category: ErrorCategory.AUTH,
|
|
276
|
-
retryable: false,
|
|
277
334
|
severity: ErrorSeverity.ERROR,
|
|
278
335
|
userMessage: "Authentication required. Please sign in.",
|
|
279
336
|
devMessage: "Missing or invalid authentication credentials. Check auth token/API key.",
|
|
280
|
-
httpStatus: 401,
|
|
281
337
|
suggestions: [
|
|
282
338
|
"Provide valid authentication credentials",
|
|
283
339
|
"Check token hasn't expired",
|
|
@@ -285,15 +341,13 @@ export const ERROR_REGISTRY: ErrorRegistry = {
|
|
|
285
341
|
],
|
|
286
342
|
}),
|
|
287
343
|
|
|
288
|
-
FORBIDDEN:
|
|
344
|
+
FORBIDDEN: defineProtocolError("FORBIDDEN", {
|
|
289
345
|
summary: "Forbidden",
|
|
290
346
|
detail: "The authenticated user doesn't have permission to access this resource.",
|
|
291
347
|
category: ErrorCategory.AUTH,
|
|
292
|
-
retryable: false,
|
|
293
348
|
severity: ErrorSeverity.ERROR,
|
|
294
349
|
userMessage: "You don't have permission to access this resource.",
|
|
295
350
|
devMessage: "Insufficient permissions. Check user roles and resource ACLs.",
|
|
296
|
-
httpStatus: 403,
|
|
297
351
|
suggestions: [
|
|
298
352
|
"Verify user has required permissions",
|
|
299
353
|
"Check resource access control lists",
|
|
@@ -301,15 +355,13 @@ export const ERROR_REGISTRY: ErrorRegistry = {
|
|
|
301
355
|
],
|
|
302
356
|
}),
|
|
303
357
|
|
|
304
|
-
NOT_FOUND:
|
|
358
|
+
NOT_FOUND: defineProtocolError("NOT_FOUND", {
|
|
305
359
|
summary: "Not found",
|
|
306
360
|
detail: "The requested resource does not exist.",
|
|
307
361
|
category: ErrorCategory.CLIENT,
|
|
308
|
-
retryable: false,
|
|
309
362
|
severity: ErrorSeverity.ERROR,
|
|
310
363
|
userMessage: "The requested item was not found.",
|
|
311
364
|
devMessage: "Resource not found. Verify ID/path is correct and resource exists.",
|
|
312
|
-
httpStatus: 404,
|
|
313
365
|
suggestions: [
|
|
314
366
|
"Verify the resource ID is correct",
|
|
315
367
|
"Check the resource hasn't been deleted",
|
|
@@ -402,17 +454,15 @@ export const ERROR_REGISTRY: ErrorRegistry = {
|
|
|
402
454
|
// ═══ Server Errors (5xx) ═══
|
|
403
455
|
//
|
|
404
456
|
|
|
405
|
-
INTERNAL_ERROR:
|
|
457
|
+
INTERNAL_ERROR: defineProtocolError("INTERNAL_ERROR", {
|
|
406
458
|
summary: "Internal server error",
|
|
407
459
|
detail: "The server encountered an unexpected condition. This is a server-side bug.",
|
|
408
460
|
category: ErrorCategory.SERVER,
|
|
409
|
-
retryable: true,
|
|
410
461
|
severity: ErrorSeverity.CRITICAL,
|
|
411
462
|
userMessage: "Server error occurred. Please try again later.",
|
|
412
463
|
devMessage: "Internal server error. Check server logs for details.",
|
|
413
|
-
|
|
464
|
+
// The protocol says that a retry does not help (ERROR_CODES: retryable false)
|
|
414
465
|
suggestions: [
|
|
415
|
-
"Retry the request",
|
|
416
466
|
"Check server logs",
|
|
417
467
|
"Contact server team",
|
|
418
468
|
],
|
|
@@ -554,6 +604,14 @@ export const ERROR_REGISTRY: ErrorRegistry = {
|
|
|
554
604
|
...HTTP_ERROR_REGISTRY,
|
|
555
605
|
} as const satisfies ErrorRegistry;
|
|
556
606
|
|
|
607
|
+
/**
|
|
608
|
+
* Universal Error Registry
|
|
609
|
+
*
|
|
610
|
+
* Protocol-agnostic error codes with rich metadata. Each code of the wire protocol has an entry,
|
|
611
|
+
* and `ERROR_CODES` gives its HTTP status and its retry flag.
|
|
612
|
+
*/
|
|
613
|
+
export const ERROR_REGISTRY: ErrorRegistry = withProtocolCodes(DESCRIBED_ERRORS);
|
|
614
|
+
|
|
557
615
|
/**
|
|
558
616
|
* Get error metadata by code.
|
|
559
617
|
*
|
|
@@ -83,7 +83,6 @@ export class RateLimitError extends Error {
|
|
|
83
83
|
*/
|
|
84
84
|
interface QueuedRequest {
|
|
85
85
|
resolve: () => void;
|
|
86
|
-
reject: (error: Error) => void;
|
|
87
86
|
timestamp: number;
|
|
88
87
|
}
|
|
89
88
|
|
|
@@ -208,7 +207,6 @@ export function createRateLimitMiddleware(
|
|
|
208
207
|
signal?.removeEventListener("abort", onAbort);
|
|
209
208
|
resolve(true);
|
|
210
209
|
},
|
|
211
|
-
reject: () => resolve(false),
|
|
212
210
|
timestamp: Date.now(),
|
|
213
211
|
};
|
|
214
212
|
// A queued request whose caller aborts leaves the queue (before, it kept its place)
|
|
@@ -170,19 +170,12 @@ async function* runWithTimeout<TReq, TRes>(
|
|
|
170
170
|
phase = "done";
|
|
171
171
|
return;
|
|
172
172
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
yield timeoutItem();
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
173
|
+
// An item or an error of the transport comes before the abort. `nextOrAbort` adds its abort
|
|
174
|
+
// listener before it calls `next()`, so a later abort gives ABORTED_FIRST, not an item.
|
|
178
175
|
yield result.value;
|
|
179
176
|
}
|
|
180
177
|
} catch (error) {
|
|
181
178
|
phase = "done";
|
|
182
|
-
if (timedOut()) {
|
|
183
|
-
yield timeoutItem();
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
179
|
throw error;
|
|
187
180
|
} finally {
|
|
188
181
|
clearTimeout(timeoutId);
|
package/src/events/index.ts
CHANGED
|
@@ -55,6 +55,22 @@ export {
|
|
|
55
55
|
withEventBusSignal,
|
|
56
56
|
} from "./bus.js";
|
|
57
57
|
|
|
58
|
+
// The remote events: a peer subscribes to the topics of the bus of another peer (EVENTS.md)
|
|
59
|
+
export { EventPublisher, topicMatcher, MAX_SUBSCRIPTION_TOPICS, MAX_TOPIC_LENGTH } from "./publisher.js";
|
|
60
|
+
export type { EventPublisherOptions, TopicRule, TopicCall } from "./publisher.js";
|
|
61
|
+
export { EVENTS_SUBSCRIBE_PATH, SubscriptionError, remoteEvents, subscribeRemote } from "./remote.js";
|
|
62
|
+
export type {
|
|
63
|
+
EventItem,
|
|
64
|
+
EventTransport,
|
|
65
|
+
RemoteEvent,
|
|
66
|
+
RemoteEventHandler,
|
|
67
|
+
RemoteEventsOptions,
|
|
68
|
+
SubscribeInput,
|
|
69
|
+
SubscribeOptions,
|
|
70
|
+
Subscription,
|
|
71
|
+
SubscriptionHooks,
|
|
72
|
+
} from "./remote.js";
|
|
73
|
+
|
|
58
74
|
// Utilities
|
|
59
75
|
export {
|
|
60
76
|
createChannelPrefix,
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The publisher of the remote events (`documentation/EVENTS.md`).
|
|
3
|
+
*
|
|
4
|
+
* An `EventPublisher` serves the procedure `events.subscribe` from an event bus. A host gives it
|
|
5
|
+
* to `ProcedureServer` (the option `events`) or to `LocalTransport`. Each subscription is a stream
|
|
6
|
+
* without an end: its first item confirms the listeners, and each other item is one event of the
|
|
7
|
+
* bus.
|
|
8
|
+
*
|
|
9
|
+
* - The topic rule (`topics`) is the expose rule of the topics. A topic outside it gives
|
|
10
|
+
* `TOPIC_NOT_FOUND`.
|
|
11
|
+
* - `allow` examines each topic with the capability of the call. A refusal gives `FORBIDDEN` with
|
|
12
|
+
* the reason `topic-not-allowed`.
|
|
13
|
+
* - The procedure declares that it reads the resource `events`, so the capability of the host
|
|
14
|
+
* applies to it as to each procedure.
|
|
15
|
+
*
|
|
16
|
+
* The delivery is at most once. The queue of a subscription holds at most `buffer` events. When
|
|
17
|
+
* it is full, the publisher drops the oldest event, and the next event tells the number of lost
|
|
18
|
+
* events (`dropped`). The bus never waits for a subscriber.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
import { createProcedure } from "../procedures/define.js";
|
|
23
|
+
import { InvocationError } from "../procedures/invoke.js";
|
|
24
|
+
import type { AnyProcedure, Capability, ProcedureContext } from "../procedures/types.js";
|
|
25
|
+
import type { EventBus, Unsubscribe } from "./types.js";
|
|
26
|
+
import { getGlobalEventBus } from "./bus.js";
|
|
27
|
+
import { EVENTS_SUBSCRIBE_PATH, type EventItem, type SubscribeInput } from "./remote.js";
|
|
28
|
+
|
|
29
|
+
/** The number of topics that one subscription can name. */
|
|
30
|
+
export const MAX_SUBSCRIPTION_TOPICS = 64;
|
|
31
|
+
|
|
32
|
+
/** The number of characters of a topic name. */
|
|
33
|
+
export const MAX_TOPIC_LENGTH = 256;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The topics that a publisher serves: a list of patterns, or a function. A pattern is an exact
|
|
37
|
+
* name (`orders.created`), a prefix that ends with `*` (`orders.*`), or `*` for each topic.
|
|
38
|
+
*/
|
|
39
|
+
export type TopicRule = readonly string[] | ((topic: string) => boolean);
|
|
40
|
+
|
|
41
|
+
/** What `allow` knows about the call of a subscription. */
|
|
42
|
+
export interface TopicCall {
|
|
43
|
+
/** The capability of the call: its clearance, its permitted effects and its deadline. */
|
|
44
|
+
readonly capability: Capability;
|
|
45
|
+
/** The metadata of the call: the connection id and the headers of a network request. */
|
|
46
|
+
readonly metadata: Readonly<Record<string, unknown>>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The options of an `EventPublisher`. */
|
|
50
|
+
export interface EventPublisherOptions {
|
|
51
|
+
/** The bus that the subscriptions read. The default is the global bus of `getGlobalEventBus()`. */
|
|
52
|
+
bus?: EventBus | undefined;
|
|
53
|
+
/** The topics that a peer can subscribe to. A topic outside the rule gives `TOPIC_NOT_FOUND`. */
|
|
54
|
+
topics: TopicRule;
|
|
55
|
+
/**
|
|
56
|
+
* This function tells if a call can subscribe to a topic of the rule, for example from the
|
|
57
|
+
* clearance of its capability. A refusal gives `FORBIDDEN` with the reason `topic-not-allowed`.
|
|
58
|
+
* Without it, each call can subscribe to each topic of the rule.
|
|
59
|
+
*/
|
|
60
|
+
allow?: ((topic: string, call: TopicCall) => boolean) | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* The number of events that one subscription holds for a slow subscriber. When the queue is
|
|
63
|
+
* full, the publisher drops the oldest event.
|
|
64
|
+
* @default 1024
|
|
65
|
+
*/
|
|
66
|
+
buffer?: number | undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** This function gives the test of a topic rule. */
|
|
70
|
+
export function topicMatcher(rule: TopicRule): (topic: string) => boolean {
|
|
71
|
+
if (typeof rule === "function") return rule;
|
|
72
|
+
const exact = new Set<string>();
|
|
73
|
+
const prefixes: string[] = [];
|
|
74
|
+
let all = false;
|
|
75
|
+
for (const pattern of rule) {
|
|
76
|
+
if (pattern === "*") all = true;
|
|
77
|
+
else if (pattern.endsWith("*")) prefixes.push(pattern.slice(0, -1));
|
|
78
|
+
else exact.add(pattern);
|
|
79
|
+
}
|
|
80
|
+
return (topic) => all || exact.has(topic) || prefixes.some((prefix) => topic.startsWith(prefix));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The error of a topic outside the topic rule. */
|
|
84
|
+
class TopicNotFoundError extends Error {
|
|
85
|
+
override readonly name = "TopicNotFoundError";
|
|
86
|
+
readonly code = "TOPIC_NOT_FOUND";
|
|
87
|
+
readonly retryable = false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The error that ends each subscription when the publisher stops. A resubscribe can succeed later. */
|
|
91
|
+
function stoppedError(): Error {
|
|
92
|
+
return Object.assign(new Error("The publisher of the events stops"), { code: "UNAVAILABLE", retryable: true });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const topicSchema = z.string().min(1).max(MAX_TOPIC_LENGTH);
|
|
96
|
+
|
|
97
|
+
const subscribeInput = z.object({
|
|
98
|
+
topics: z.array(topicSchema).min(1).max(MAX_SUBSCRIPTION_TOPICS).describe("The topics of the subscription"),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const eventItemSchema = z.discriminatedUnion("type", [
|
|
102
|
+
z.object({
|
|
103
|
+
type: z.literal("subscribed"),
|
|
104
|
+
topics: z.array(z.string()).describe("The topics that the publisher listens to for this subscription"),
|
|
105
|
+
}),
|
|
106
|
+
z.object({
|
|
107
|
+
type: z.literal("event"),
|
|
108
|
+
topic: z.string(),
|
|
109
|
+
data: z.unknown(),
|
|
110
|
+
seq: z.number().int().describe("The number of the event in the subscription, from 1"),
|
|
111
|
+
dropped: z.number().int().optional().describe("The events that the publisher dropped immediately before this one"),
|
|
112
|
+
}),
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* One subscription: the listeners on the bus and the queue of its events. It is the async
|
|
117
|
+
* iterator of the stream of the call. `return()`, the abort of the signal of the call and
|
|
118
|
+
* `fail()` remove the listeners.
|
|
119
|
+
*/
|
|
120
|
+
class TopicSubscription implements AsyncIterableIterator<EventItem> {
|
|
121
|
+
private confirmation: EventItem | undefined;
|
|
122
|
+
private readonly queue: Array<Extract<EventItem, { type: "event" }>> = [];
|
|
123
|
+
private seq = 0;
|
|
124
|
+
private dropped = 0;
|
|
125
|
+
private waiter: { resolve: (result: IteratorResult<EventItem>) => void; reject: (error: unknown) => void } | undefined;
|
|
126
|
+
private closed = false;
|
|
127
|
+
private failure: unknown;
|
|
128
|
+
private readonly unsubscribes: Unsubscribe[];
|
|
129
|
+
|
|
130
|
+
constructor(
|
|
131
|
+
bus: EventBus,
|
|
132
|
+
topics: readonly string[],
|
|
133
|
+
private readonly buffer: number,
|
|
134
|
+
private readonly signal: AbortSignal | undefined,
|
|
135
|
+
private readonly onClose: (subscription: TopicSubscription) => void,
|
|
136
|
+
) {
|
|
137
|
+
this.confirmation = { type: "subscribed", topics };
|
|
138
|
+
this.unsubscribes = topics.map((topic) => bus.on(topic, (data) => this.push(topic, data)));
|
|
139
|
+
signal?.addEventListener("abort", this.onAbort, { once: true });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** This function closes a subscription whose call aborted before it started. */
|
|
143
|
+
closeIfAborted(): this {
|
|
144
|
+
if (this.signal?.aborted) this.close();
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private readonly onAbort = (): void => this.close();
|
|
149
|
+
|
|
150
|
+
/** This function takes one event of the bus. It never waits. */
|
|
151
|
+
private push(topic: string, data: unknown): void {
|
|
152
|
+
if (this.closed) return;
|
|
153
|
+
const event = { type: "event" as const, topic, data, seq: ++this.seq };
|
|
154
|
+
const waiter = this.waiter;
|
|
155
|
+
if (waiter && this.queue.length === 0) {
|
|
156
|
+
this.waiter = undefined;
|
|
157
|
+
waiter.resolve({ value: this.withDropped(event), done: false });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (this.queue.length >= this.buffer) {
|
|
161
|
+
// A slow subscriber: the oldest event goes, and the next event tells the loss
|
|
162
|
+
this.queue.shift();
|
|
163
|
+
this.dropped++;
|
|
164
|
+
}
|
|
165
|
+
this.queue.push(event);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The event with the number of events that the publisher dropped before it. */
|
|
169
|
+
private withDropped(event: Extract<EventItem, { type: "event" }>): EventItem {
|
|
170
|
+
if (this.dropped === 0) return event;
|
|
171
|
+
const dropped = this.dropped;
|
|
172
|
+
this.dropped = 0;
|
|
173
|
+
return { ...event, dropped };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
next(): Promise<IteratorResult<EventItem>> {
|
|
177
|
+
if (this.confirmation) {
|
|
178
|
+
const confirmation = this.confirmation;
|
|
179
|
+
this.confirmation = undefined;
|
|
180
|
+
return Promise.resolve({ value: confirmation, done: false });
|
|
181
|
+
}
|
|
182
|
+
const event = this.queue.shift();
|
|
183
|
+
if (event) return Promise.resolve({ value: this.withDropped(event), done: false });
|
|
184
|
+
if (this.failure !== undefined) {
|
|
185
|
+
const failure = this.failure;
|
|
186
|
+
this.failure = undefined;
|
|
187
|
+
return Promise.reject(failure);
|
|
188
|
+
}
|
|
189
|
+
if (this.closed) return Promise.resolve({ value: undefined, done: true });
|
|
190
|
+
return new Promise((resolve, reject) => {
|
|
191
|
+
this.waiter = { resolve, reject };
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return(): Promise<IteratorResult<EventItem>> {
|
|
196
|
+
this.close();
|
|
197
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
[Symbol.asyncIterator](): AsyncIterableIterator<EventItem> {
|
|
201
|
+
return this;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** This function ends the subscription with an error: the reader gets it at its next item. */
|
|
205
|
+
fail(error: unknown): void {
|
|
206
|
+
if (this.closed) return;
|
|
207
|
+
const waiter = this.waiter;
|
|
208
|
+
this.waiter = undefined;
|
|
209
|
+
this.release();
|
|
210
|
+
this.queue.length = 0;
|
|
211
|
+
this.confirmation = undefined;
|
|
212
|
+
if (waiter) waiter.reject(error);
|
|
213
|
+
else this.failure = error;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** This function ends the subscription without an error. */
|
|
217
|
+
close(): void {
|
|
218
|
+
if (this.closed) return;
|
|
219
|
+
const waiter = this.waiter;
|
|
220
|
+
this.waiter = undefined;
|
|
221
|
+
this.release();
|
|
222
|
+
this.queue.length = 0;
|
|
223
|
+
this.confirmation = undefined;
|
|
224
|
+
waiter?.resolve({ value: undefined, done: true });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** This function removes the listeners from the bus and from the signal. */
|
|
228
|
+
private release(): void {
|
|
229
|
+
this.closed = true;
|
|
230
|
+
for (const unsubscribe of this.unsubscribes) unsubscribe();
|
|
231
|
+
this.signal?.removeEventListener("abort", this.onAbort);
|
|
232
|
+
this.onClose(this);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The publisher of the remote events: it serves `events.subscribe` from a bus, under a topic
|
|
238
|
+
* rule. `ProcedureServer` makes one from its option `events`.
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* ```typescript
|
|
242
|
+
* const publisher = new EventPublisher({ bus, topics: ["orders.*"] });
|
|
243
|
+
* const server = new ProcedureServer({ registry, autoRegister: true, events: publisher });
|
|
244
|
+
* publisher.publish("orders.created", { order: 1 });
|
|
245
|
+
* ```
|
|
246
|
+
*/
|
|
247
|
+
export class EventPublisher {
|
|
248
|
+
/** The bus that the subscriptions read. */
|
|
249
|
+
readonly bus: EventBus;
|
|
250
|
+
/** The procedure `events.subscribe` of this publisher. A host pins it, outside its registry. */
|
|
251
|
+
readonly procedure: AnyProcedure;
|
|
252
|
+
private readonly known: (topic: string) => boolean;
|
|
253
|
+
private readonly allow: EventPublisherOptions["allow"];
|
|
254
|
+
private readonly buffer: number;
|
|
255
|
+
private readonly open = new Set<TopicSubscription>();
|
|
256
|
+
|
|
257
|
+
constructor(options: EventPublisherOptions) {
|
|
258
|
+
this.bus = options.bus ?? getGlobalEventBus();
|
|
259
|
+
this.known = topicMatcher(options.topics);
|
|
260
|
+
this.allow = options.allow;
|
|
261
|
+
this.buffer = Math.max(1, Math.floor(options.buffer ?? 1024));
|
|
262
|
+
this.procedure = createProcedure()
|
|
263
|
+
.path([...EVENTS_SUBSCRIBE_PATH])
|
|
264
|
+
.input(subscribeInput)
|
|
265
|
+
.output(eventItemSchema)
|
|
266
|
+
.meta({
|
|
267
|
+
description: "A subscription to topics of the event bus of the host: a stream without an end, with one item for each event",
|
|
268
|
+
effects: { reads: ["events"], writes: [] },
|
|
269
|
+
stream: { unbounded: true },
|
|
270
|
+
})
|
|
271
|
+
.handler((async (input: SubscribeInput, ctx: ProcedureContext) => this.subscribe(input.topics, ctx)) as never)
|
|
272
|
+
.build() as AnyProcedure;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** This function publishes an event on the bus: each subscription of the topic gets it. */
|
|
276
|
+
publish<T>(topic: string, data: T): void {
|
|
277
|
+
this.bus.emit(topic, data);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** The number of open subscriptions. */
|
|
281
|
+
get subscriptions(): number {
|
|
282
|
+
return this.open.size;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* This function ends each open subscription with a retryable `UNAVAILABLE`: the publisher stops.
|
|
287
|
+
* A subscriber resubscribes after its backoff. `ProcedureServer.stop()` calls it first.
|
|
288
|
+
*/
|
|
289
|
+
end(): void {
|
|
290
|
+
for (const subscription of [...this.open]) subscription.fail(stoppedError());
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The stream of one subscription. The publisher examines each topic before it adds a listener:
|
|
295
|
+
* the topic rule first, then `allow`. A call with one bad topic makes no subscription.
|
|
296
|
+
*/
|
|
297
|
+
private subscribe(topics: readonly string[], ctx: ProcedureContext): TopicSubscription {
|
|
298
|
+
const unique = [...new Set(topics)];
|
|
299
|
+
for (const topic of unique) {
|
|
300
|
+
if (!this.known(topic)) throw new TopicNotFoundError(`The host has no topic "${topic}"`);
|
|
301
|
+
}
|
|
302
|
+
if (this.allow) {
|
|
303
|
+
const call: TopicCall = { capability: ctx.capability, metadata: ctx.metadata };
|
|
304
|
+
for (const topic of unique) {
|
|
305
|
+
if (!this.allow(topic, call)) {
|
|
306
|
+
throw new InvocationError("FORBIDDEN", `The capability of the call does not permit the topic "${topic}"`, ctx.path, "topic-not-allowed");
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const subscription = new TopicSubscription(this.bus, unique, this.buffer, ctx.signal, (closed) => this.open.delete(closed));
|
|
311
|
+
this.open.add(subscription);
|
|
312
|
+
return subscription.closeIfAborted();
|
|
313
|
+
}
|
|
314
|
+
}
|