@procedures/client 0.8.1 → 0.9.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/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/server/addressing.d.ts +78 -0
- package/dist/adapters/http/server/addressing.d.ts.map +1 -0
- package/dist/adapters/http/server/addressing.js +62 -0
- package/dist/adapters/http/server/addressing.js.map +1 -0
- package/dist/adapters/http/server/cors.d.ts.map +1 -1
- package/dist/adapters/http/server/cors.js +3 -0
- package/dist/adapters/http/server/cors.js.map +1 -1
- package/dist/adapters/http/server/handler.d.ts +9 -1
- package/dist/adapters/http/server/handler.d.ts.map +1 -1
- package/dist/adapters/http/server/handler.js +49 -23
- package/dist/adapters/http/server/handler.js.map +1 -1
- package/dist/adapters/http/server/strategies.d.ts +1 -0
- package/dist/adapters/http/server/strategies.d.ts.map +1 -1
- package/dist/adapters/http/server/strategies.js +6 -0
- package/dist/adapters/http/server/strategies.js.map +1 -1
- package/dist/adapters/http/server/transport.d.ts.map +1 -1
- package/dist/adapters/http/server/transport.js +7 -2
- package/dist/adapters/http/server/transport.js.map +1 -1
- package/dist/adapters/http/server/types.d.ts +7 -0
- package/dist/adapters/http/server/types.d.ts.map +1 -1
- package/dist/adapters/http/shared/strategies.d.ts +7 -0
- package/dist/adapters/http/shared/strategies.d.ts.map +1 -1
- package/dist/adapters/http/shared/strategies.js +26 -7
- package/dist/adapters/http/shared/strategies.js.map +1 -1
- package/dist/adapters/local/client/types.d.ts.map +1 -1
- package/dist/adapters/local/client/types.js +3 -1
- package/dist/adapters/local/client/types.js.map +1 -1
- package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/client/transport.js +4 -12
- package/dist/adapters/websocket/client/transport.js.map +1 -1
- package/dist/browser.d.ts +6 -3
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +5 -1
- package/dist/browser.js.map +1 -1
- package/dist/client/client.d.ts +6 -0
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +3 -1
- package/dist/client/client.js.map +1 -1
- package/dist/client/errors/factory.d.ts.map +1 -1
- package/dist/client/errors/factory.js +2 -3
- package/dist/client/errors/factory.js.map +1 -1
- package/dist/client/middleware/cache.d.ts.map +1 -1
- package/dist/client/middleware/cache.js +2 -1
- package/dist/client/middleware/cache.js.map +1 -1
- package/dist/events/publisher.d.ts +25 -2
- package/dist/events/publisher.d.ts.map +1 -1
- package/dist/events/publisher.js +42 -9
- package/dist/events/publisher.js.map +1 -1
- package/dist/node.d.ts +6 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +6 -0
- package/dist/node.js.map +1 -1
- package/dist/procedures/collection/changes.d.ts +3 -3
- package/dist/procedures/collection/changes.d.ts.map +1 -1
- package/dist/procedures/collection/changes.js +4 -4
- package/dist/procedures/collection/changes.js.map +1 -1
- package/dist/procedures/collection/index.d.ts +2 -0
- package/dist/procedures/collection/index.d.ts.map +1 -1
- package/dist/procedures/collection/index.js +2 -0
- package/dist/procedures/collection/index.js.map +1 -1
- package/dist/procedures/collection/partition.d.ts +86 -0
- package/dist/procedures/collection/partition.d.ts.map +1 -0
- package/dist/procedures/collection/partition.js +100 -0
- package/dist/procedures/collection/partition.js.map +1 -0
- package/dist/procedures/collection/procedures.d.ts +4 -4
- package/dist/procedures/collection/procedures.d.ts.map +1 -1
- package/dist/procedures/collection/procedures.js +55 -55
- package/dist/procedures/collection/procedures.js.map +1 -1
- package/dist/procedures/index.d.ts +4 -2
- package/dist/procedures/index.d.ts.map +1 -1
- package/dist/procedures/index.js +3 -1
- package/dist/procedures/index.js.map +1 -1
- package/dist/procedures/invoke.d.ts +17 -2
- package/dist/procedures/invoke.d.ts.map +1 -1
- package/dist/procedures/invoke.js +30 -1
- package/dist/procedures/invoke.js.map +1 -1
- package/dist/procedures/narrowing.d.ts +93 -0
- package/dist/procedures/narrowing.d.ts.map +1 -0
- package/dist/procedures/narrowing.js +328 -0
- package/dist/procedures/narrowing.js.map +1 -0
- package/dist/procedures/types.d.ts +19 -0
- package/dist/procedures/types.d.ts.map +1 -1
- package/dist/procedures/types.js.map +1 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/method.d.ts +8 -2
- package/dist/server/method.d.ts.map +1 -1
- package/dist/server/method.js +16 -5
- package/dist/server/method.js.map +1 -1
- package/dist/server/procedure-server.d.ts +47 -1
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +89 -12
- package/dist/server/procedure-server.js.map +1 -1
- package/dist/server/types.d.ts +16 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +2 -1
- package/dist/server/types.js.map +1 -1
- package/package.json +3 -3
- package/src/adapters/http/server/addressing.ts +118 -0
- package/src/adapters/http/server/cors.ts +3 -0
- package/src/adapters/http/server/handler.ts +57 -23
- package/src/adapters/http/server/strategies.ts +7 -0
- package/src/adapters/http/server/transport.ts +8 -3
- package/src/adapters/http/server/types.ts +8 -0
- package/src/adapters/http/shared/strategies.ts +25 -7
- package/src/adapters/local/client/types.ts +3 -1
- package/src/adapters/websocket/client/transport.ts +4 -12
- package/src/browser.ts +12 -1
- package/src/client/client.ts +8 -1
- package/src/client/errors/factory.ts +2 -3
- package/src/client/middleware/cache.ts +2 -1
- package/src/events/publisher.ts +50 -11
- package/src/node.ts +7 -0
- package/src/procedures/collection/changes.ts +4 -4
- package/src/procedures/collection/index.ts +4 -0
- package/src/procedures/collection/partition.ts +145 -0
- package/src/procedures/collection/procedures.ts +72 -73
- package/src/procedures/index.ts +9 -0
- package/src/procedures/invoke.ts +42 -2
- package/src/procedures/narrowing.ts +357 -0
- package/src/procedures/types.ts +23 -0
- package/src/server/index.ts +2 -1
- package/src/server/method.ts +17 -5
- package/src/server/procedure-server.ts +106 -13
- package/src/server/types.ts +19 -1
|
@@ -27,8 +27,9 @@ import { PROTOCOL_HEADER, PROTOCOL_VERSION, httpStatusOf, protocolVersionOf } fr
|
|
|
27
27
|
import { realClock, type Cancel, type Clock } from "../../../clock.js";
|
|
28
28
|
import { ms, type Duration } from "../../../duration.js";
|
|
29
29
|
import { NDJSON_MEDIA_TYPE as NDJSON, defaultNdjsonError as errorOf, keepAliveMsOf, writeNdjsonStream } from "./ndjson.js";
|
|
30
|
-
import { createPatternServerUrlStrategy } from "./strategies.js";
|
|
31
30
|
import { applyCors, corsOrigins, corsPolicy, type CorsOptions, type CorsPolicy } from "./cors.js";
|
|
31
|
+
import { urlAddressing, type Addressing } from "./addressing.js";
|
|
32
|
+
import { NARROWING_METADATA_KEY, addNarrowing } from "../../../procedures/narrowing.js";
|
|
32
33
|
|
|
33
34
|
/** The parts of a request that a URL strategy of `procedureHandler` reads. */
|
|
34
35
|
export interface UrlRequest {
|
|
@@ -58,9 +59,16 @@ export interface ProcedureHandlerOptions extends BrowserGuardOptions {
|
|
|
58
59
|
readonly basePath?: string | undefined;
|
|
59
60
|
/**
|
|
60
61
|
* The method of a request. The default reads the URL format of `HttpTransport`:
|
|
61
|
-
* `{basePath}/{service}/{operation}`.
|
|
62
|
+
* `{basePath}/{service}/{operation}`. The option `addressing` replaces it.
|
|
62
63
|
*/
|
|
63
64
|
readonly urlStrategy?: NodeUrlStrategy | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* The addressing of a request (CLI-31): a hook of the request and its body that gives the
|
|
67
|
+
* method and the payload of the call. `bodyPathAddressing()` reads the path from the body of a
|
|
68
|
+
* single endpoint.
|
|
69
|
+
* @default urlAddressing({ basePath, urlStrategy })
|
|
70
|
+
*/
|
|
71
|
+
readonly addressing?: Addressing | undefined;
|
|
64
72
|
/**
|
|
65
73
|
* The CORS headers of the requests on the path. `true` uses the defaults: each origin, without
|
|
66
74
|
* credentials. The permitted origins of the browser checks follow the CORS origins.
|
|
@@ -207,6 +215,19 @@ const GUARD_REASONS: Record<string, string> = {
|
|
|
207
215
|
UNSUPPORTED_MEDIA_TYPE: "media-type-not-allowed",
|
|
208
216
|
};
|
|
209
217
|
|
|
218
|
+
/** This function sets a key as an own property, also the key `__proto__` (the `put` of the query language). */
|
|
219
|
+
function put(target: Record<string, unknown>, key: string, value: unknown): void {
|
|
220
|
+
Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* This function sets a key at the root of the metadata. Each source of the narrowing key adds to
|
|
225
|
+
* the key, so no source replaces another (CLI-30).
|
|
226
|
+
*/
|
|
227
|
+
function setMetadata(metadata: Metadata, key: string, value: unknown): void {
|
|
228
|
+
metadata[key] = key === NARROWING_METADATA_KEY ? addNarrowing(metadata[key], value) : value;
|
|
229
|
+
}
|
|
230
|
+
|
|
210
231
|
/** This function writes a JSON response without a framework. */
|
|
211
232
|
function writeJson(res: NodeResponse, status: number, body: unknown): void {
|
|
212
233
|
const text = JSON.stringify(body);
|
|
@@ -286,14 +307,14 @@ export function createProcedureHandler(server: RequestHandler, options: Procedur
|
|
|
286
307
|
assertNamedOrigins(compat.face?.name ?? "procedureHandler", ambientMode, guard.allowedOrigins, compat.face?.originOption ?? "cors.origin");
|
|
287
308
|
const secretHeaders = headerNames(options.secretHeaders);
|
|
288
309
|
const secretQuery = new Set(options.secretQuery ?? (legacy ? [] : ["token"]));
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
310
|
+
// The addressing of a request (CLI-31): the option, or the URL strategy of the shim, or the URL
|
|
311
|
+
// format of HttpTransport
|
|
312
|
+
const compatMethodOf = compat.methodOf;
|
|
313
|
+
const addressing: Addressing =
|
|
314
|
+
options.addressing ??
|
|
315
|
+
urlAddressing(compatMethodOf ? { urlStrategy: (_view, req) => compatMethodOf(req) } : { basePath, urlStrategy: options.urlStrategy });
|
|
316
|
+
// A single endpoint (the path in the body) is the base path itself
|
|
317
|
+
const bareBase = prefix.slice(0, -1);
|
|
297
318
|
const sendJson = compat.sendJson ?? writeJson;
|
|
298
319
|
|
|
299
320
|
/** The controllers of the requests in progress: close() aborts them (deep dive TRN-15) */
|
|
@@ -309,7 +330,11 @@ export function createProcedureHandler(server: RequestHandler, options: Procedur
|
|
|
309
330
|
for (const waiter of waiters) waiter();
|
|
310
331
|
};
|
|
311
332
|
|
|
312
|
-
const matches = (req: IncomingMessage): boolean =>
|
|
333
|
+
const matches = (req: IncomingMessage): boolean => {
|
|
334
|
+
if (compat.matchAll === true) return true;
|
|
335
|
+
const path = pathOf(req);
|
|
336
|
+
return path.startsWith(prefix) || (addressing.takesBasePath === true && path === bareBase);
|
|
337
|
+
};
|
|
313
338
|
|
|
314
339
|
/** The error body of wire v1: the message, the code, the retryable flag and the more fields. */
|
|
315
340
|
const sendError = (res: NodeResponse, status: number, message: string, code: string, retryable = false, more: Record<string, unknown> = {}): void => {
|
|
@@ -349,16 +374,23 @@ export function createProcedureHandler(server: RequestHandler, options: Procedur
|
|
|
349
374
|
}
|
|
350
375
|
};
|
|
351
376
|
|
|
352
|
-
/**
|
|
377
|
+
/**
|
|
378
|
+
* This function converts an HTTP request to a `ServerRequest`, or gives null when the addressing
|
|
379
|
+
* finds no call in the request.
|
|
380
|
+
*/
|
|
353
381
|
const toServerRequest = (req: IncomingMessage, body: unknown): ServerRequest | null => {
|
|
354
|
-
const
|
|
355
|
-
|
|
382
|
+
const { path, query: urlQuery } = urlParts(req.url);
|
|
383
|
+
const view: UrlRequest = { method: (req.method ?? "GET").toUpperCase(), path, url: req.url ?? "/", headers: req.headers, query: queryRecord(urlQuery) };
|
|
384
|
+
const address = addressing(view, body, req);
|
|
385
|
+
if (!address) return null;
|
|
386
|
+
const method = address.method;
|
|
356
387
|
|
|
357
388
|
const id = (req.headers["x-request-id"] as string | undefined) ?? `req-${Date.now()}-${Math.random()}`;
|
|
358
389
|
|
|
359
390
|
// The query without its secret parameters: they go into the credentials (CLI-2a)
|
|
391
|
+
// An own property for each key, also `__proto__`: a key of the request changes no prototype
|
|
360
392
|
const query: Record<string, unknown> = {};
|
|
361
|
-
for (const [key, value] of Object.entries(queryOf(req))) if (!secretQuery.has(key)) query
|
|
393
|
+
for (const [key, value] of Object.entries(queryOf(req))) if (!secretQuery.has(key)) put(query, key, value);
|
|
362
394
|
const params = paramsOf(req);
|
|
363
395
|
|
|
364
396
|
// The metadata of the headers and of the query. The query parameters cannot replace headers,
|
|
@@ -370,17 +402,21 @@ export function createProcedureHandler(server: RequestHandler, options: Procedur
|
|
|
370
402
|
params,
|
|
371
403
|
};
|
|
372
404
|
for (const [key, value] of Object.entries(query)) {
|
|
373
|
-
if (!RESERVED_METADATA.has(key) && !key.startsWith("__")) metadata
|
|
405
|
+
if (!RESERVED_METADATA.has(key) && !key.startsWith("__")) setMetadata(metadata, key, value);
|
|
374
406
|
}
|
|
375
407
|
|
|
376
408
|
// The custom headers at the root of the metadata, as the client's metadataToHeaders() sends
|
|
377
409
|
// them. A secret header (Cookie too) does not go there.
|
|
378
410
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
379
411
|
const lower = key.toLowerCase();
|
|
380
|
-
if (STANDARD_HEADERS.has(lower) || isSecretHeader(lower, secretHeaders) || typeof value !== "string") continue;
|
|
412
|
+
if (STANDARD_HEADERS.has(lower) || isSecretHeader(lower, secretHeaders) || typeof value !== "string" || lower === NARROWING_METADATA_KEY) continue;
|
|
381
413
|
const normalizedKey = HEADER_MAPPINGS[lower] ?? key;
|
|
382
414
|
if (!RESERVED_METADATA.has(normalizedKey) && !normalizedKey.startsWith("__")) metadata[normalizedKey] = value;
|
|
383
415
|
}
|
|
416
|
+
// Each value of the narrowing header is one source, also each line of a repeated header. No
|
|
417
|
+
// source replaces another: a gateway's header and the client's key of X-Metadata both apply (CLI-30)
|
|
418
|
+
const narrowingHeader = req.headersDistinct?.[NARROWING_METADATA_KEY] ?? req.headers[NARROWING_METADATA_KEY];
|
|
419
|
+
for (const value of [narrowingHeader].flat()) if (typeof value === "string") setMetadata(metadata, NARROWING_METADATA_KEY, value);
|
|
384
420
|
|
|
385
421
|
// The custom metadata of the HTTP client: any JSON value, any key (deep dive TRN-11)
|
|
386
422
|
const encoded = req.headers["x-metadata"];
|
|
@@ -391,15 +427,13 @@ export function createProcedureHandler(server: RequestHandler, options: Procedur
|
|
|
391
427
|
const custom = decodeMetadataHeader(encoded);
|
|
392
428
|
if (!custom) throw new BodyError("The X-Metadata header is not URI-encoded JSON of an object", 400);
|
|
393
429
|
for (const [key, value] of Object.entries(custom)) {
|
|
394
|
-
if (!RESERVED_METADATA.has(key) && key !== "auth" && !key.startsWith("__")) metadata
|
|
430
|
+
if (!RESERVED_METADATA.has(key) && key !== "auth" && !key.startsWith("__")) setMetadata(metadata, key, value);
|
|
395
431
|
}
|
|
396
432
|
}
|
|
397
433
|
|
|
398
|
-
// The payload
|
|
399
|
-
// false, "" and null stay (deep dive TRN-10)
|
|
400
|
-
|
|
401
|
-
if (req.method === "GET" && params["id"]) payload = { id: params["id"] };
|
|
402
|
-
if (payload === undefined) payload = {};
|
|
434
|
+
// The payload of the addressing (the body, or the id of a RESTful GET). Only an absent payload
|
|
435
|
+
// becomes {}: 0, false, "" and null stay (deep dive TRN-10)
|
|
436
|
+
const payload: unknown = address.payload === undefined ? {} : address.payload;
|
|
403
437
|
|
|
404
438
|
return {
|
|
405
439
|
id,
|
|
@@ -71,10 +71,17 @@ export function defaultServerUrlStrategy(req: RoutedRequest): Method | null {
|
|
|
71
71
|
* Maps:
|
|
72
72
|
* - POST /rpc/users/get → { service: "users", operation: "get" }
|
|
73
73
|
* - POST /rpc/users/list → { service: "users", operation: "list" }
|
|
74
|
+
* - POST /rpc/morning → { service: "", operation: "morning" } (a path of one segment, CLI-28)
|
|
74
75
|
*/
|
|
75
76
|
export function rpcServerUrlStrategy(req: RoutedRequest): Method | null {
|
|
76
77
|
const path = req.path;
|
|
77
78
|
|
|
79
|
+
// A path of one segment: /rpc/:operation, with the empty service (CLI-28)
|
|
80
|
+
const single = path.match(/^\/rpc\/([^/]+)\/?$/);
|
|
81
|
+
if (single?.[1]) {
|
|
82
|
+
return { service: "", operation: single[1] };
|
|
83
|
+
}
|
|
84
|
+
|
|
78
85
|
// Match pattern: /rpc/:service/:operation
|
|
79
86
|
const match = path.match(/^\/rpc\/([^/]+)\/([^/]+)/);
|
|
80
87
|
if (!match) {
|
|
@@ -42,7 +42,7 @@ export class HttpServerTransport implements ServerTransport {
|
|
|
42
42
|
|
|
43
43
|
private app: Express;
|
|
44
44
|
private httpServer: HttpServer | null = null;
|
|
45
|
-
private options: { port: number; host: string; basePath: string; cors: boolean; httpServer: HttpServer | undefined };
|
|
45
|
+
private options: { port: number; host: string; basePath: string; cors: boolean; httpServer: HttpServer | undefined; takesBasePath: boolean };
|
|
46
46
|
/** The procedure handler that does the work of each request (CLI-3) */
|
|
47
47
|
private readonly handler: NodeProcedureHandler;
|
|
48
48
|
/** The CORS of the app, or undefined when it is off */
|
|
@@ -58,6 +58,8 @@ export class HttpServerTransport implements ServerTransport {
|
|
|
58
58
|
basePath,
|
|
59
59
|
cors: options.cors ?? false,
|
|
60
60
|
httpServer: options.httpServer,
|
|
61
|
+
// A single endpoint with the path in the body is the base path itself (CLI-31)
|
|
62
|
+
takesBasePath: options.addressing?.takesBasePath === true,
|
|
61
63
|
};
|
|
62
64
|
// Route by URL path so the raw server matches the HTTP client's default URL format instead of
|
|
63
65
|
// inferring the operation from the verb (BUGS-2026-07 C3, bug 16)
|
|
@@ -77,6 +79,7 @@ export class HttpServerTransport implements ServerTransport {
|
|
|
77
79
|
allowGet: options.allowGet,
|
|
78
80
|
secretHeaders: options.secretHeaders,
|
|
79
81
|
secretQuery: options.secretQuery,
|
|
82
|
+
addressing: options.addressing,
|
|
80
83
|
},
|
|
81
84
|
{
|
|
82
85
|
legacy: true,
|
|
@@ -116,9 +119,11 @@ export class HttpServerTransport implements ServerTransport {
|
|
|
116
119
|
});
|
|
117
120
|
}
|
|
118
121
|
|
|
119
|
-
// Catch-all route for RPC - use regex to match any path under basePath
|
|
122
|
+
// Catch-all route for RPC - use regex to match any path under basePath, and the base path
|
|
123
|
+
// itself for an addressing that takes it
|
|
124
|
+
const base = this.options.basePath.replace(/\//g, "\\/");
|
|
120
125
|
this.app.all(
|
|
121
|
-
new RegExp(
|
|
126
|
+
new RegExp(this.options.takesBasePath ? `^${base.replace(/\\\/$/, "")}(?:\\/.*)?$` : `^${base}\\/.*`),
|
|
122
127
|
async (req, res) => {
|
|
123
128
|
await this.handler.handle(req, res);
|
|
124
129
|
}
|
|
@@ -10,6 +10,7 @@ import type { Method } from "../../../client/types.js";
|
|
|
10
10
|
import type { BrowserGuardOptions } from "../../../server/browser-guard.js";
|
|
11
11
|
import type { Clock } from "../../../clock.js";
|
|
12
12
|
import type { Duration } from "../../../duration.js";
|
|
13
|
+
import type { Addressing } from "./addressing.js";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* URL strategy for converting HTTP requests to RPC methods.
|
|
@@ -51,6 +52,13 @@ export interface HttpServerTransportOptions extends BrowserGuardOptions {
|
|
|
51
52
|
*/
|
|
52
53
|
urlStrategy?: HttpUrlStrategy;
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* The addressing of a request (CLI-31): a hook of the request and its body that gives the
|
|
57
|
+
* method and the payload of the call. It replaces `urlStrategy`. With `bodyPathAddressing()`,
|
|
58
|
+
* the route takes the base path itself too.
|
|
59
|
+
*/
|
|
60
|
+
addressing?: Addressing;
|
|
61
|
+
|
|
54
62
|
/**
|
|
55
63
|
* Base path for RPC endpoints.
|
|
56
64
|
* @default "/api"
|
|
@@ -60,6 +60,13 @@ export interface UrlPattern {
|
|
|
60
60
|
* Examples:
|
|
61
61
|
* - { service: "users", operation: "get" } → "/users/get"
|
|
62
62
|
* - { service: "users", operation: "get", version: "v2" } → "/v2/users/get"
|
|
63
|
+
* - { service: "", operation: "morning" } → "/morning" (a path of one segment, CLI-28)
|
|
64
|
+
*
|
|
65
|
+
* A path gives one URL segment (a path of one segment) or two (the service with dots, and the
|
|
66
|
+
* operation). Each URL segment is URI-encoded, so a path segment does not make a URL separator.
|
|
67
|
+
* Thus only the version makes three URL segments, and the forms do not collide. A path of one
|
|
68
|
+
* segment has no version in the URL: `/{version}/{operation}` reads as `/{service}/{operation}`.
|
|
69
|
+
* The server ignores the version (wire protocol v1).
|
|
63
70
|
*/
|
|
64
71
|
export const defaultUrlPattern: UrlPattern = {
|
|
65
72
|
template: "/{version?}/{service}/{operation}",
|
|
@@ -67,11 +74,13 @@ export const defaultUrlPattern: UrlPattern = {
|
|
|
67
74
|
format(method: Method, baseUrl: string): string {
|
|
68
75
|
const parts = [baseUrl.replace(/\/$/, "")];
|
|
69
76
|
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
// The version needs a service: without one, it reads as the service (CLI-28)
|
|
78
|
+
if (method.version && method.service !== "") {
|
|
79
|
+
parts.push(encodeURIComponent(method.version));
|
|
72
80
|
}
|
|
73
81
|
|
|
74
|
-
parts.push(method.service
|
|
82
|
+
if (method.service !== "") parts.push(encodeURIComponent(method.service));
|
|
83
|
+
parts.push(encodeURIComponent(method.operation));
|
|
75
84
|
|
|
76
85
|
return parts.join("/");
|
|
77
86
|
},
|
|
@@ -79,11 +88,11 @@ export const defaultUrlPattern: UrlPattern = {
|
|
|
79
88
|
parse(url: string, _httpMethod: HTTPMethod): Method | null {
|
|
80
89
|
// Remove base URL and query params
|
|
81
90
|
const path = url.split("?")[0];
|
|
82
|
-
const segments = path?.split("/").filter(Boolean) ?? [];
|
|
91
|
+
const segments = (path?.split("/").filter(Boolean) ?? []).map(decodeSegment);
|
|
83
92
|
|
|
84
|
-
//
|
|
85
|
-
if (segments.length
|
|
86
|
-
return
|
|
93
|
+
// A path of one segment: /operation, with the empty service (CLI-28)
|
|
94
|
+
if (segments.length === 1) {
|
|
95
|
+
return { service: "", operation: segments[0]! };
|
|
87
96
|
}
|
|
88
97
|
|
|
89
98
|
// Pattern: [version?]/service/operation
|
|
@@ -106,6 +115,15 @@ export const defaultUrlPattern: UrlPattern = {
|
|
|
106
115
|
},
|
|
107
116
|
};
|
|
108
117
|
|
|
118
|
+
/** A URL segment without its URI encoding. A segment that is not valid URI encoding stays as it is. */
|
|
119
|
+
function decodeSegment(segment: string): string {
|
|
120
|
+
try {
|
|
121
|
+
return decodeURIComponent(segment);
|
|
122
|
+
} catch {
|
|
123
|
+
return segment;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
109
127
|
/**
|
|
110
128
|
* RESTful URL Pattern: /{version?}/{service}/{id?}
|
|
111
129
|
*
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { Message, Method } from "../../../client/types.js";
|
|
8
|
+
import { methodName } from "../../../server/method.js";
|
|
8
9
|
import type { ProcedureRegistry } from "../../../procedures/registry.js";
|
|
9
10
|
import type { Interceptor } from "../../../procedures/invoke.js";
|
|
10
11
|
import type { Capability, ClearanceOrder } from "../../../procedures/types.js";
|
|
@@ -23,7 +24,8 @@ export type Handler<TReq = unknown, TRes = unknown> = (
|
|
|
23
24
|
*/
|
|
24
25
|
export function methodKey(method: Method): string {
|
|
25
26
|
const version = method.version || "";
|
|
26
|
-
|
|
27
|
+
// A path of one segment has the key of its operation: "morning", not ".morning" (CLI-28)
|
|
28
|
+
return `${version}:${methodName(method)}`;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/**
|
|
@@ -578,18 +578,10 @@ export class WebSocketTransport implements Transport {
|
|
|
578
578
|
return;
|
|
579
579
|
}
|
|
580
580
|
|
|
581
|
-
// The frame type `event` is reserved: no side of protocol v1 sends it.
|
|
582
|
-
//
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
try {
|
|
586
|
-
this.options.onEvent(message.topic, message.data, message.subscriptionId);
|
|
587
|
-
} catch (error) {
|
|
588
|
-
console.error(`[${this.name}] Event handler error:`, error);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
581
|
+
// The frame type `event` is reserved: no side of protocol v1 sends it. The client ignores it,
|
|
582
|
+
// as a frame of an unknown type. Before 0.9.0, it gave the frame to onEvent, for a server
|
|
583
|
+
// outside this repository (EVENTS.md). onEvent gets the events of the subscriptions only.
|
|
584
|
+
if (message.type === "event") return;
|
|
593
585
|
|
|
594
586
|
// Handle response
|
|
595
587
|
const pending = this.pendingRequests.get(message.id);
|
package/src/browser.ts
CHANGED
|
@@ -87,7 +87,7 @@ export {
|
|
|
87
87
|
HandlerNotFoundError,
|
|
88
88
|
ServerError,
|
|
89
89
|
} from "./server/types.js";
|
|
90
|
-
export { pathToMethod, methodToPath } from "./server/method.js";
|
|
90
|
+
export { pathToMethod, methodToPath, methodName } from "./server/method.js";
|
|
91
91
|
export type {
|
|
92
92
|
ServerRequest,
|
|
93
93
|
ServerResponse,
|
|
@@ -124,6 +124,10 @@ export {
|
|
|
124
124
|
// The change events of the collection writes (roadmap CLI-15)
|
|
125
125
|
collectionChangeTopic,
|
|
126
126
|
collectionTopicReads,
|
|
127
|
+
// The partitions of a collection (roadmap CLI-17)
|
|
128
|
+
PARTITION_PATTERN,
|
|
129
|
+
collectionTopicPartition,
|
|
130
|
+
collectionTopicTarget,
|
|
127
131
|
// The query language of the collection storages (roadmap CLI-14, CLI-34)
|
|
128
132
|
DEFAULT_FIND_LIMIT,
|
|
129
133
|
DEFAULT_QUERY_OPERATORS,
|
|
@@ -275,6 +279,9 @@ export type {
|
|
|
275
279
|
StorageQueryPolicy,
|
|
276
280
|
CollectionProceduresOptions,
|
|
277
281
|
CollectionChangeEventsOptions,
|
|
282
|
+
CollectionPartitions,
|
|
283
|
+
PartitionInput,
|
|
284
|
+
PartitionResolver,
|
|
278
285
|
CollectionChangeEvent,
|
|
279
286
|
ProcedureModule,
|
|
280
287
|
ModuleInput,
|
|
@@ -373,6 +380,10 @@ export type { ProtocolErrorCode, ErrorCodeInfo } from "./protocol.js";
|
|
|
373
380
|
export { STREAM_GAP_METADATA_KEY, STREAM_ITEM_METADATA_KEY } from "./adapters/sequence.js";
|
|
374
381
|
// The deadline of a call: the remaining time goes as procedure-timeout (roadmap R10)
|
|
375
382
|
export { TIMEOUT_METADATA_KEY, DEADLINE_METADATA_KEY } from "./client/deadline.js";
|
|
383
|
+
// The narrowing key of a call: the call asks for a narrower capability on each face (CLI-30)
|
|
384
|
+
export { NARROWING_METADATA_KEY, NARROWING_HEADER, narrowingOf, narrowWithin, addNarrowing } from "./procedures/narrowing.js";
|
|
385
|
+
export type { NarrowingNote, WireNarrowing, NarrowedCapability } from "./procedures/narrowing.js";
|
|
386
|
+
export type { ServerDebugReport } from "./server/types.js";
|
|
376
387
|
// Typed durations: each public option that is a time takes a Duration (roadmap R11)
|
|
377
388
|
export { ms, seconds, minutes, hours, toMs, optionalMs, isDuration } from "./duration.js";
|
|
378
389
|
export type { Duration, TimeUnit } from "./duration.js";
|
package/src/client/client.ts
CHANGED
|
@@ -171,13 +171,20 @@ export interface ExecOptions {
|
|
|
171
171
|
* (`observeHydration()` gets the events of every program in the process.)
|
|
172
172
|
*/
|
|
173
173
|
onRef?: ((event: HydrationEvent) => void) | undefined;
|
|
174
|
+
/**
|
|
175
|
+
* The metadata of the program. A local procedure gets it as `ctx.metadata`, and a remote call
|
|
176
|
+
* sends it. The narrowing key `procedure-narrowing` narrows the capability of each call of the
|
|
177
|
+
* program, local or remote (CLI-30).
|
|
178
|
+
*/
|
|
179
|
+
metadata?: Record<string, unknown> | undefined;
|
|
174
180
|
}
|
|
175
181
|
|
|
176
|
-
/** The caller context of `exec()` and `execStream()`: the observer and the
|
|
182
|
+
/** The caller context of `exec()` and `execStream()`: the observer, the signal and the metadata. */
|
|
177
183
|
function callerOf(options: ExecOptions): CallerContext {
|
|
178
184
|
const caller: CallerContext = {};
|
|
179
185
|
if (options.onRef) caller.onRef = options.onRef;
|
|
180
186
|
if (options.signal) caller.signal = options.signal;
|
|
187
|
+
if (options.metadata) caller.metadata = options.metadata;
|
|
181
188
|
return caller;
|
|
182
189
|
}
|
|
183
190
|
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { getErrorMetadata } from "./registry.js";
|
|
14
14
|
import { httpStatusToErrorCode } from "./http-errors.js";
|
|
15
15
|
import { ErrorCode } from "./codes.js";
|
|
16
|
+
import { methodName } from "../../server/method.js";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Create a rich error from an error code.
|
|
@@ -226,9 +227,7 @@ export function formatError(error: RichError, includeStack = false): string {
|
|
|
226
227
|
|
|
227
228
|
if (error.context.method) {
|
|
228
229
|
const m = error.context.method;
|
|
229
|
-
const methodStr = m.version
|
|
230
|
-
? `${m.version}:${m.service}.${m.operation}`
|
|
231
|
-
: `${m.service}.${m.operation}`;
|
|
230
|
+
const methodStr = m.version ? `${m.version}:${methodName(m)}` : methodName(m);
|
|
232
231
|
lines.push(` Method: ${methodStr}`);
|
|
233
232
|
}
|
|
234
233
|
|
|
@@ -11,6 +11,7 @@ import { LruMap } from "./lru.js";
|
|
|
11
11
|
import { isDuration, optionalMs, type Duration } from "../../duration.js";
|
|
12
12
|
import type { Effects } from "../../procedures/types.js";
|
|
13
13
|
import { TIMER_DEFAULTS } from "../../timers.js";
|
|
14
|
+
import { methodName } from "../../server/method.js";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Cache middleware options.
|
|
@@ -287,7 +288,7 @@ export function createCacheMiddleware(options: CacheOptions): TypedClientMiddlew
|
|
|
287
288
|
const cacheable =
|
|
288
289
|
typeof methods === "function"
|
|
289
290
|
? methods
|
|
290
|
-
: ((names: ReadonlySet<string>) => (method: Method): boolean => names.has(
|
|
291
|
+
: ((names: ReadonlySet<string>) => (method: Method): boolean => names.has(methodName(method)))(
|
|
291
292
|
new Set(methods),
|
|
292
293
|
);
|
|
293
294
|
const effectsOf = options.effects;
|
package/src/events/publisher.ts
CHANGED
|
@@ -28,8 +28,10 @@ import { z } from "zod";
|
|
|
28
28
|
import { createProcedure } from "../procedures/define.js";
|
|
29
29
|
import { InvocationError } from "../procedures/invoke.js";
|
|
30
30
|
import { capabilityRefusal } from "../procedures/capability.js";
|
|
31
|
-
import { collectionTopicReads } from "../procedures/collection/changes.js";
|
|
31
|
+
import { collectionChangeTopic, collectionTopicReads } from "../procedures/collection/changes.js";
|
|
32
|
+
import { collectionTopicTarget, partitionOfCall } from "../procedures/collection/partition.js";
|
|
32
33
|
import type { AnyProcedure, Capability, ProcedureContext } from "../procedures/types.js";
|
|
34
|
+
import type { Principal } from "../server/types.js";
|
|
33
35
|
import type { EventBus, Unsubscribe } from "./types.js";
|
|
34
36
|
import { getGlobalEventBus } from "./bus.js";
|
|
35
37
|
import { EVENTS_SUBSCRIBE_PATH, type EventItem, type SubscribeInput } from "./remote.js";
|
|
@@ -53,6 +55,8 @@ export interface TopicCall {
|
|
|
53
55
|
readonly capability: Capability;
|
|
54
56
|
/** The metadata of the call: the connection id and the headers of a network request. */
|
|
55
57
|
readonly metadata: Readonly<Record<string, unknown>>;
|
|
58
|
+
/** The principal of the call, from the authentication of the host. */
|
|
59
|
+
readonly principal?: Principal | undefined;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
/** The options of an `EventPublisher`. */
|
|
@@ -77,7 +81,12 @@ export interface EventPublisherOptions {
|
|
|
77
81
|
* The resources that a subscriber of a topic reads (roadmap CLI-15). The publisher refuses a
|
|
78
82
|
* topic whose reads the capability of the call does not permit, as for a procedure that reads
|
|
79
83
|
* them: `FORBIDDEN` with the reason `effects-exceeded`.
|
|
80
|
-
*
|
|
84
|
+
*
|
|
85
|
+
* A custom function gets only the topic. For a topic of a collection, give the read of the
|
|
86
|
+
* collection that the publisher listens to: the collection of `collectionTopicTarget(topic,
|
|
87
|
+
* repository)` with the repository of the host. A function that reads the first part of the
|
|
88
|
+
* topic alone checks `collection:a` for the dotted collection `a.b`.
|
|
89
|
+
* @default the read of the collection of `collectionTopicTarget()`, with the repository of the call
|
|
81
90
|
*/
|
|
82
91
|
reads?: ((topic: string) => readonly string[]) | undefined;
|
|
83
92
|
}
|
|
@@ -161,17 +170,18 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
|
|
|
161
170
|
|
|
162
171
|
constructor(
|
|
163
172
|
bus: EventBus,
|
|
164
|
-
topics
|
|
173
|
+
/** The topics of the subscriber, each with the topic of the bus that it listens to */
|
|
174
|
+
routes: ReadonlyArray<readonly [topic: string, busTopic: string]>,
|
|
165
175
|
private readonly buffer: number,
|
|
166
176
|
private readonly signal: AbortSignal | undefined,
|
|
167
177
|
private readonly onClose: (subscription: TopicSubscription) => void,
|
|
168
178
|
/** The output filter of the host for this subscriber: each row of a value passes it */
|
|
169
179
|
private readonly admit: ((item: unknown) => Promise<boolean>) | undefined,
|
|
170
180
|
) {
|
|
171
|
-
this.confirmation = { type: "subscribed", topics };
|
|
181
|
+
this.confirmation = { type: "subscribed", topics: routes.map(([topic]) => topic) };
|
|
172
182
|
// Before, clear() removed the listeners, and the subscriber saw no end (EVENTS.md)
|
|
173
|
-
this.unsubscribes =
|
|
174
|
-
bus.on(
|
|
183
|
+
this.unsubscribes = routes.map(([topic, busTopic]) =>
|
|
184
|
+
bus.on(busTopic, (data) => this.push(topic, data), { onClear: () => this.fail(clearedError(topic), { keepQueue: true }) }),
|
|
175
185
|
);
|
|
176
186
|
signal?.addEventListener("abort", this.onAbort, { once: true });
|
|
177
187
|
}
|
|
@@ -304,6 +314,8 @@ export class EventPublisher {
|
|
|
304
314
|
private readonly known: (topic: string) => boolean;
|
|
305
315
|
private readonly allow: EventPublisherOptions["allow"];
|
|
306
316
|
private readonly reads: (topic: string) => readonly string[];
|
|
317
|
+
/** True when the reads come from the topic rule of the collections (no option `reads`) */
|
|
318
|
+
private readonly defaultReads: boolean;
|
|
307
319
|
private readonly buffer: number;
|
|
308
320
|
private readonly open = new Set<TopicSubscription>();
|
|
309
321
|
|
|
@@ -312,6 +324,7 @@ export class EventPublisher {
|
|
|
312
324
|
this.known = topicMatcher(options.topics);
|
|
313
325
|
this.allow = options.allow;
|
|
314
326
|
this.reads = options.reads ?? collectionTopicReads;
|
|
327
|
+
this.defaultReads = options.reads === undefined;
|
|
315
328
|
this.buffer = Math.max(1, Math.floor(options.buffer ?? 1024));
|
|
316
329
|
this.procedure = createProcedure()
|
|
317
330
|
.path([...EVENTS_SUBSCRIBE_PATH])
|
|
@@ -344,31 +357,57 @@ export class EventPublisher {
|
|
|
344
357
|
for (const subscription of [...this.open]) subscription.fail(stoppedError());
|
|
345
358
|
}
|
|
346
359
|
|
|
360
|
+
/**
|
|
361
|
+
* The topic of the bus for a topic of a subscriber (roadmap CLI-17). A topic of a partitioned
|
|
362
|
+
* collection names the partition that the subscriber asks for, as the input of a procedure does:
|
|
363
|
+
* the resolver of the repository gives the partition, so a subscriber cannot name the topic of
|
|
364
|
+
* another tenant. A collection without partitions refuses a topic with a partition.
|
|
365
|
+
*/
|
|
366
|
+
private async busTopic(topic: string, ctx: ProcedureContext): Promise<string> {
|
|
367
|
+
const target = collectionTopicTarget(topic, ctx.repository);
|
|
368
|
+
if (!target) return topic;
|
|
369
|
+
const partition = await partitionOfCall(ctx, target.collection, target.partition);
|
|
370
|
+
return collectionChangeTopic(target.collection, partition);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* The resources that a subscriber of a topic reads. The default reads the collection of the
|
|
375
|
+
* topic with the rule of the listener (`collectionTopicTarget`), so the read check and the
|
|
376
|
+
* listener name the same collection.
|
|
377
|
+
*/
|
|
378
|
+
private readsOf(topic: string, ctx: ProcedureContext): readonly string[] {
|
|
379
|
+
if (!this.defaultReads) return this.reads(topic);
|
|
380
|
+
const target = collectionTopicTarget(topic, ctx.repository);
|
|
381
|
+
return target ? [`collection:${target.collection}`] : [];
|
|
382
|
+
}
|
|
383
|
+
|
|
347
384
|
/**
|
|
348
385
|
* The stream of one subscription. The publisher examines each topic before it adds a listener:
|
|
349
|
-
* the topic rule
|
|
386
|
+
* the topic rule, the reads, `allow`, and the partition. A call with one bad topic makes no subscription.
|
|
350
387
|
*/
|
|
351
|
-
private subscribe(topics: readonly string[], ctx: ProcedureContext): TopicSubscription {
|
|
388
|
+
private async subscribe(topics: readonly string[], ctx: ProcedureContext): Promise<TopicSubscription> {
|
|
352
389
|
const unique = [...new Set(topics)];
|
|
353
390
|
for (const topic of unique) {
|
|
354
391
|
if (!this.known(topic)) throw new TopicNotFoundError(`The host has no topic "${topic}"`);
|
|
355
392
|
}
|
|
356
393
|
// A subscriber of a topic reads its resources: the capability of the call must permit them
|
|
357
394
|
for (const topic of unique) {
|
|
358
|
-
const reads = this.
|
|
395
|
+
const reads = this.readsOf(topic, ctx);
|
|
359
396
|
if (reads.length === 0) continue;
|
|
360
397
|
const refusal = capabilityRefusal(topicReader(reads), ctx.capability);
|
|
361
398
|
if (refusal) throw new InvocationError("FORBIDDEN", `The topic "${topic}": ${refusal.message}`, ctx.path, refusal.reason);
|
|
362
399
|
}
|
|
363
400
|
if (this.allow) {
|
|
364
|
-
const call: TopicCall = { capability: ctx.capability, metadata: ctx.metadata };
|
|
401
|
+
const call: TopicCall = { capability: ctx.capability, metadata: ctx.metadata, ...(ctx.principal ? { principal: ctx.principal } : {}) };
|
|
365
402
|
for (const topic of unique) {
|
|
366
403
|
if (!this.allow(topic, call)) {
|
|
367
404
|
throw new InvocationError("FORBIDDEN", `The capability of the call does not permit the topic "${topic}"`, ctx.path, "topic-not-allowed");
|
|
368
405
|
}
|
|
369
406
|
}
|
|
370
407
|
}
|
|
371
|
-
const
|
|
408
|
+
const routes: Array<readonly [string, string]> = [];
|
|
409
|
+
for (const topic of unique) routes.push([topic, await this.busTopic(topic, ctx)]);
|
|
410
|
+
const subscription = new TopicSubscription(this.bus, routes, this.buffer, ctx.signal, (closed) => this.open.delete(closed), ctx.admit);
|
|
372
411
|
this.open.add(subscription);
|
|
373
412
|
return subscription.closeIfAborted();
|
|
374
413
|
}
|
package/src/node.ts
CHANGED
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
* gives a request handler `(req, res) => boolean`, and `upgradeHandler(server, options)` gives an
|
|
15
15
|
* upgrade handler `(req, socket, head) => boolean`. Each one takes the requests of its path and
|
|
16
16
|
* gives `false` for the others, so the routes of the application share the listener.
|
|
17
|
+
*
|
|
18
|
+
* The option `addressing` of `procedureHandler` gives the method and the payload of a request
|
|
19
|
+
* (CLI-31). `urlAddressing()` is the default, and `bodyPathAddressing()` reads the path from the
|
|
20
|
+
* body of a single endpoint.
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
23
|
export { WebSocketTransport } from "./adapters/websocket/client/node.js";
|
|
@@ -25,6 +29,9 @@ export type { ProcedureHandlerOptions, NodeProcedureHandler, NodeUrlStrategy, Ur
|
|
|
25
29
|
export type { CorsOptions } from "./adapters/http/server/cors.js";
|
|
26
30
|
export type { RoutedRequest } from "./adapters/http/server/strategies.js";
|
|
27
31
|
export { defaultServerUrlStrategy, rpcServerUrlStrategy } from "./adapters/http/server/strategies.js";
|
|
32
|
+
// The addressing of a request: the URL (the default), or the path in the body of a single endpoint (CLI-31)
|
|
33
|
+
export { urlAddressing, bodyPathAddressing } from "./adapters/http/server/addressing.js";
|
|
34
|
+
export type { Addressing, CallAddress, UrlAddressingOptions, BodyPathAddressingOptions } from "./adapters/http/server/addressing.js";
|
|
28
35
|
export { upgradeHandler } from "./adapters/websocket/server/upgrade.js";
|
|
29
36
|
export type { UpgradeHandlerOptions, NodeUpgradeHandler } from "./adapters/websocket/server/upgrade.js";
|
|
30
37
|
export { WebSocketServerTransport } from "./adapters/websocket/server/transport.js";
|
|
@@ -56,11 +56,11 @@ export interface CollectionChangeEvent {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
* This function gives the topic of the change events of a collection: `collections.<name
|
|
60
|
-
* partition
|
|
59
|
+
* This function gives the topic of the change events of a collection: `collections.<name>`, or
|
|
60
|
+
* `collections.<name>.<partition>` for a partition (roadmap CLI-17).
|
|
61
61
|
*/
|
|
62
|
-
export function collectionChangeTopic(collection: string): string {
|
|
63
|
-
return `collections.${collection}`;
|
|
62
|
+
export function collectionChangeTopic(collection: string, partition?: string): string {
|
|
63
|
+
return partition === undefined ? `collections.${collection}` : `collections.${collection}.${partition}`;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/**
|
|
@@ -70,6 +70,10 @@ export type { CollectionProceduresOptions, CollectionChangeEventsOptions } from
|
|
|
70
70
|
export { collectionChangeTopic, collectionTopicReads } from "./changes.js";
|
|
71
71
|
export type { CollectionChangeEvent } from "./changes.js";
|
|
72
72
|
|
|
73
|
+
// The partitions of a collection (roadmap CLI-17)
|
|
74
|
+
export { PARTITION_PATTERN, collectionTopicPartition, collectionTopicTarget, partitionOfCall, storageOfCall, withPartition } from "./partition.js";
|
|
75
|
+
export type { CollectionPartitions, PartitionInput, PartitionResolver } from "./partition.js";
|
|
76
|
+
|
|
73
77
|
// The storage interface of the core (the shape of client-collections' CollectionStorage)
|
|
74
78
|
export type {
|
|
75
79
|
CollectionStorage,
|