@procedures/client 0.33.0 → 0.35.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 +59 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/server/addressing.d.ts.map +1 -1
- package/dist/adapters/http/server/addressing.js +6 -2
- package/dist/adapters/http/server/addressing.js.map +1 -1
- package/dist/adapters/local/client/transport.js +2 -1
- package/dist/adapters/local/client/transport.js.map +1 -1
- package/dist/browser.d.ts +3 -2
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +5 -3
- package/dist/browser.js.map +1 -1
- package/dist/events/publisher.d.ts +6 -8
- package/dist/events/publisher.d.ts.map +1 -1
- package/dist/events/publisher.js +7 -31
- package/dist/events/publisher.js.map +1 -1
- package/dist/procedures/collection/changes.d.ts +2 -2
- package/dist/procedures/collection/changes.js +2 -2
- package/dist/procedures/collection/index.js +1 -1
- package/dist/procedures/collection/index.js.map +1 -1
- package/dist/procedures/collection/partition.d.ts +8 -56
- package/dist/procedures/collection/partition.d.ts.map +1 -1
- package/dist/procedures/collection/partition.js +7 -73
- package/dist/procedures/collection/partition.js.map +1 -1
- package/dist/procedures/collection/topic.d.ts +27 -39
- package/dist/procedures/collection/topic.d.ts.map +1 -1
- package/dist/procedures/collection/topic.js +55 -38
- package/dist/procedures/collection/topic.js.map +1 -1
- package/dist/procedures/core/schemas.d.ts +26 -10
- package/dist/procedures/core/schemas.d.ts.map +1 -1
- package/dist/procedures/core/schemas.js +34 -16
- package/dist/procedures/core/schemas.js.map +1 -1
- package/dist/procedures/define-procedure.d.ts.map +1 -1
- package/dist/procedures/define-procedure.js +5 -5
- package/dist/procedures/define-procedure.js.map +1 -1
- package/dist/procedures/index.d.ts +4 -3
- package/dist/procedures/index.d.ts.map +1 -1
- package/dist/procedures/index.js +5 -3
- package/dist/procedures/index.js.map +1 -1
- package/dist/procedures/module.d.ts +22 -0
- package/dist/procedures/module.d.ts.map +1 -1
- package/dist/procedures/module.js +18 -0
- package/dist/procedures/module.js.map +1 -1
- package/dist/procedures/registry.d.ts +2 -1
- package/dist/procedures/registry.d.ts.map +1 -1
- package/dist/procedures/registry.js +4 -2
- package/dist/procedures/registry.js.map +1 -1
- package/dist/procedures/segments.d.ts +61 -0
- package/dist/procedures/segments.d.ts.map +1 -0
- package/dist/procedures/segments.js +71 -0
- package/dist/procedures/segments.js.map +1 -0
- package/dist/procedures/types.d.ts +5 -2
- package/dist/procedures/types.d.ts.map +1 -1
- package/dist/procedures/types.js +8 -4
- package/dist/procedures/types.js.map +1 -1
- package/dist/server/host-policy.d.ts +1 -1
- package/dist/server/host-policy.d.ts.map +1 -1
- package/dist/server/host-policy.js +5 -3
- package/dist/server/host-policy.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/method.d.ts +18 -3
- package/dist/server/method.d.ts.map +1 -1
- package/dist/server/method.js +23 -6
- package/dist/server/method.js.map +1 -1
- package/dist/server/procedure-server.d.ts +5 -23
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +4 -29
- package/dist/server/procedure-server.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/http/server/addressing.ts +6 -2
- package/src/adapters/local/client/transport.ts +2 -2
- package/src/browser.ts +6 -1
- package/src/events/publisher.ts +14 -40
- package/src/procedures/collection/changes.ts +2 -2
- package/src/procedures/collection/index.ts +1 -1
- package/src/procedures/collection/partition.ts +10 -87
- package/src/procedures/collection/topic.ts +48 -51
- package/src/procedures/core/schemas.ts +35 -16
- package/src/procedures/define-procedure.ts +5 -7
- package/src/procedures/index.ts +6 -2
- package/src/procedures/module.ts +34 -0
- package/src/procedures/registry.ts +4 -2
- package/src/procedures/segments.ts +74 -0
- package/src/procedures/types.ts +8 -4
- package/src/server/host-policy.ts +5 -4
- package/src/server/index.ts +2 -0
- package/src/server/method.ts +23 -6
- package/src/server/procedure-server.ts +6 -34
|
@@ -1,95 +1,91 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The topics of the change events of a collection (roadmap CLI-15, CLI-17 and CLI-
|
|
3
|
-
* has the one function that makes a topic, the functions that read it, and the topic rule
|
|
4
|
-
* collection. Each part of the client that makes or reads the topic of a collection or of a
|
|
2
|
+
* The topics of the change events of a collection (roadmap CLI-15, CLI-17, CLI-42 and CLI-75). This
|
|
3
|
+
* module has the one function that makes a topic, the functions that read it, and the topic rule
|
|
4
|
+
* of a collection. Each part of the client that makes or reads the topic of a collection or of a
|
|
5
5
|
* partition uses them.
|
|
6
6
|
*
|
|
7
|
-
* - `collections.<name>` is the topic of a collection
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* partition after that dot can have each
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* - `collections.<name>` is the topic of a collection, and `collections.<name>.<partition>` is the
|
|
8
|
+
* topic of a partition.
|
|
9
|
+
* - In each topic, the name encodes `%` as `%25` and `.` as `%2E` (`encodeSegment()`). Thus the
|
|
10
|
+
* first dot after `collections.` ends the name, and the partition after that dot can have each
|
|
11
|
+
* character, also `.`, `/` and `%`. The partition stays as it is.
|
|
12
|
+
* - A name without `.` and `%` stays as it is. Thus each topic of such a name stays the same.
|
|
13
|
+
* - Before CLI-75, the plain topic kept a dotted name as it is: `collections.a.b` was the topic of
|
|
14
|
+
* the collection `a.b` and also of the partition `b` of `a`. Now the topic of `a.b` is
|
|
15
|
+
* `collections.a%2Eb`, so the two topics are not the same.
|
|
13
16
|
* - Only the canonical form of a topic counts: a parse gives a collection and a partition only when
|
|
14
17
|
* `collectionChangeTopic(collection, partition)` gives the same topic. Thus `%2e` is no alias of
|
|
15
18
|
* `%2E`, and a rule or an `allow` function that compares the strings sees each topic one way.
|
|
16
|
-
* - The topic of a collection whose name has a dot, `collections.a.b`, has the same form as the
|
|
17
|
-
* topic of the partition `b` of the collection `a`. Only the repository knows the collection, so
|
|
18
|
-
* `collectionTopicTarget()` decides with it.
|
|
19
|
-
* - `ProcedureServer` refuses a storage of `a.b` while `a` has partitions, in each order (roadmap
|
|
20
|
-
* CLI-57, decision D23). Thus the readers of `a.b` do not get the events of a partition of `a`.
|
|
21
19
|
*/
|
|
20
|
+
import { encodeSegment, decodeSegment } from "../segments.js";
|
|
22
21
|
import type { RepositoryProvider } from "../types.js";
|
|
23
22
|
|
|
24
23
|
/** The prefix of the topic of each collection. */
|
|
25
24
|
const PREFIX = "collections.";
|
|
26
25
|
|
|
27
|
-
/** This function encodes the name of a collection for a topic with a partition: `%` and `.` become `%25` and `%2E`. */
|
|
28
|
-
function encodeTopicName(name: string): string {
|
|
29
|
-
return name.replace(/[%.]/g, (char) => (char === "%" ? "%25" : "%2E"));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** This function decodes the name of a topic with a partition. The parse then checks the canonical form. */
|
|
33
|
-
function decodeTopicName(segment: string): string {
|
|
34
|
-
return segment.replace(/%(25|2E)/g, (_sequence, code: string) => (code === "25" ? "%" : "."));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
26
|
/**
|
|
38
27
|
* This function gives the topic of the change events of a collection: `collections.<name>`, or
|
|
39
|
-
* `collections.<name>.<partition>` for a partition (roadmap CLI-17).
|
|
40
|
-
*
|
|
41
|
-
*
|
|
28
|
+
* `collections.<name>.<partition>` for a partition (roadmap CLI-17). The name encodes `%` and `.`
|
|
29
|
+
* (roadmap CLI-42 and CLI-75), so `collectionTopicPartition()` gives back the same collection and
|
|
30
|
+
* the same partition.
|
|
42
31
|
*
|
|
43
32
|
* @example
|
|
44
33
|
* ```typescript
|
|
45
34
|
* collectionChangeTopic("notes", "t1/board:b1"); // "collections.notes.t1/board:b1"
|
|
46
35
|
* collectionChangeTopic("graph.nodes", "acme/space"); // "collections.graph%2Enodes.acme/space"
|
|
47
|
-
* collectionChangeTopic("graph.nodes"); // "collections.graph
|
|
36
|
+
* collectionChangeTopic("graph.nodes"); // "collections.graph%2Enodes"
|
|
48
37
|
* ```
|
|
49
38
|
*/
|
|
50
39
|
export function collectionChangeTopic(collection: string, partition?: string): string {
|
|
51
|
-
|
|
40
|
+
const name = `${PREFIX}${encodeSegment(collection)}`;
|
|
41
|
+
return partition === undefined ? name : `${name}.${partition}`;
|
|
52
42
|
}
|
|
53
43
|
|
|
54
44
|
/**
|
|
55
45
|
* This function gives the collection and the partition of a topic `collections.<name>` or
|
|
56
|
-
* `collections.<name>.<partition>`, or undefined for another topic.
|
|
57
|
-
*
|
|
46
|
+
* `collections.<name>.<partition>`, or undefined for another topic. A host reads the collection
|
|
47
|
+
* of a topic with it, and not with its own pattern.
|
|
58
48
|
*
|
|
59
|
-
* - The first dot after the name ends the name.
|
|
60
|
-
*
|
|
49
|
+
* - The first dot after the name ends the name. The name is decoded: `%2E` is a dot and `%25` is a
|
|
50
|
+
* `%` (roadmap CLI-42 and CLI-75).
|
|
61
51
|
* - Each character after that dot is the partition. A partition has one character at least.
|
|
62
52
|
* - A topic that is not in the canonical form gives undefined: `collectionChangeTopic()` of the
|
|
63
53
|
* result always gives the topic again. Thus `collections.a%2eb.t1` names no collection.
|
|
64
54
|
* - For each name and each partition, `collectionTopicPartition(collectionChangeTopic(name,
|
|
65
55
|
* partition))` gives the same name and the same partition.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* collectionTopicPartition("collections.e2e%2Etabs"); // { collection: "e2e.tabs", partition: undefined }
|
|
60
|
+
* collectionTopicPartition("collections.e2e%2Etabs.t1"); // { collection: "e2e.tabs", partition: "t1" }
|
|
61
|
+
* ```
|
|
66
62
|
*/
|
|
67
63
|
export function collectionTopicPartition(topic: string): { collection: string; partition: string | undefined } | undefined {
|
|
68
64
|
if (!topic.startsWith(PREFIX)) return undefined;
|
|
69
65
|
const rest = topic.slice(PREFIX.length);
|
|
70
66
|
const dot = rest.indexOf(".");
|
|
71
|
-
if (dot ===
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
const partition = rest.slice(dot + 1);
|
|
67
|
+
if (dot === 0 || dot === rest.length - 1 || rest === "") return undefined;
|
|
68
|
+
const collection = decodeSegment(dot === -1 ? rest : rest.slice(0, dot));
|
|
69
|
+
const partition = dot === -1 ? undefined : rest.slice(dot + 1);
|
|
75
70
|
// Only the canonical form counts: an alias of an escape names no collection
|
|
76
71
|
return collectionChangeTopic(collection, partition) === topic ? { collection, partition } : undefined;
|
|
77
72
|
}
|
|
78
73
|
|
|
79
74
|
/**
|
|
80
|
-
* This function gives the collection and the partition part of a topic of a call
|
|
81
|
-
*
|
|
75
|
+
* This function gives the collection and the partition part of a topic of a call: one rule for
|
|
76
|
+
* the read check and for the listener of the publisher. It gives the result of
|
|
77
|
+
* `collectionTopicPartition()`.
|
|
82
78
|
*
|
|
83
|
-
* -
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
79
|
+
* - Before CLI-75, a collection of the repository whose name was the whole rest of the topic won,
|
|
80
|
+
* because the plain topic of a dotted name did not encode the name. Now each topic encodes the
|
|
81
|
+
* name, so the function does not read the repository.
|
|
82
|
+
*
|
|
83
|
+
* @param repository - Deprecated: not used since roadmap CLI-75. The parameter stays for the
|
|
84
|
+
* callers, and a later MINOR removes it. (TSDoc has no `@deprecated` for one parameter, and the
|
|
85
|
+
* block tag would deprecate the whole function, which stays.)
|
|
88
86
|
*/
|
|
89
87
|
export function collectionTopicTarget(topic: string, repository: RepositoryProvider | undefined): { collection: string; partition: string | undefined } | undefined {
|
|
90
|
-
|
|
91
|
-
const whole = topic.slice(PREFIX.length);
|
|
92
|
-
if (whole !== "" && repository?.hasCollection(whole)) return { collection: whole, partition: undefined };
|
|
88
|
+
void repository;
|
|
93
89
|
return collectionTopicPartition(topic);
|
|
94
90
|
}
|
|
95
91
|
|
|
@@ -97,21 +93,22 @@ export function collectionTopicTarget(topic: string, repository: RepositoryProvi
|
|
|
97
93
|
* This function gives the topic rule of a collection for `EventPublisher` (the option `topics`):
|
|
98
94
|
* the topic of the collection, and the pattern of the topics of its partitions (roadmap CLI-42).
|
|
99
95
|
* Build the rule of a collection with it. Do not join the parts yourself: a dotted name needs its
|
|
100
|
-
* encoded form
|
|
96
|
+
* encoded form.
|
|
101
97
|
*
|
|
102
98
|
* - The rule always has the two forms. For a collection with partitions, the plain topic asks for
|
|
103
99
|
* no partition, and the resolver of the partition decides.
|
|
104
100
|
* - For a collection without partitions, a topic of a partition passes the rule. The publisher then
|
|
105
101
|
* refuses it with `FORBIDDEN` (reason `partition-not-allowed`).
|
|
106
|
-
* - The
|
|
102
|
+
* - The rule matches only the canonical form of a topic, so an alias of an escape gets
|
|
107
103
|
* `TOPIC_NOT_FOUND`.
|
|
108
104
|
*
|
|
109
105
|
* @example
|
|
110
106
|
* ```typescript
|
|
111
|
-
* collectionTopicRule("graph.nodes"); // ["collections.graph
|
|
107
|
+
* collectionTopicRule("graph.nodes"); // ["collections.graph%2Enodes", "collections.graph%2Enodes.*"]
|
|
112
108
|
* new EventPublisher({ bus, topics: ["orders.*", ...collectionTopicRule("graph.nodes")] });
|
|
113
109
|
* ```
|
|
114
110
|
*/
|
|
115
111
|
export function collectionTopicRule(collection: string): string[] {
|
|
116
|
-
|
|
112
|
+
const topic = collectionChangeTopic(collection);
|
|
113
|
+
return [topic, `${topic}.*`];
|
|
117
114
|
}
|
|
@@ -47,8 +47,30 @@ export const anySchema: ZodLike<any> & StandardSchemaV1<unknown, any> & { readon
|
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
50
|
+
* This function gives a pass-through schema for the type `T`: it validates nothing and has no JSON
|
|
51
|
+
* Schema. The type parameter only gives the TypeScript type.
|
|
52
|
+
*
|
|
53
|
+
* Use it for internal procedures where a real schema adds nothing. Give a real schema (zod) to a
|
|
54
|
+
* procedure that an MCP server or the CLI exposes. Then the host validates its input, and its
|
|
55
|
+
* tools and help describe the fields (roadmap 2.1).
|
|
56
|
+
*
|
|
57
|
+
* ```ts
|
|
58
|
+
* createProcedure().path(["a", "b"]).input(typed<{ id: string }>()).output(typed<number>())
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function typed<T>(): ZodLikeSchema<T> & StandardSchemaV1<unknown, T> {
|
|
62
|
+
return {
|
|
63
|
+
parse: (data: unknown) => data as T,
|
|
64
|
+
safeParse: (data: unknown) => ({ success: true as const, data: data as T }),
|
|
65
|
+
_output: undefined as unknown as T,
|
|
66
|
+
"~standard": passThrough<T>(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* This function gives a pass-through schema for the type `T`.
|
|
72
|
+
*
|
|
73
|
+
* @deprecated Use `typed<T>()`. A later MINOR removes `typedAnySchema` (tracker item O58, decision D13).
|
|
52
74
|
*/
|
|
53
75
|
export function typedAnySchema<T>(): ZodLike<T> & StandardSchemaV1<unknown, T> {
|
|
54
76
|
return {
|
|
@@ -63,11 +85,16 @@ export function typedAnySchema<T>(): ZodLike<T> & StandardSchemaV1<unknown, T> {
|
|
|
63
85
|
// =============================================================================
|
|
64
86
|
|
|
65
87
|
/**
|
|
66
|
-
*
|
|
67
|
-
* Standard Schema already, so the function returns it unchanged: the validation and
|
|
68
|
-
* Schema come from zod itself.
|
|
88
|
+
* This function gives a zod schema the type of a procedure schema (`ZodLikeSchema<T>`). A zod 4
|
|
89
|
+
* schema is a Standard Schema already, so the function returns it unchanged: the validation and
|
|
90
|
+
* the JSON Schema come from zod itself.
|
|
69
91
|
*
|
|
70
92
|
* An object with only a `parse` method gets `safeParse` and `~standard` around that method.
|
|
93
|
+
*
|
|
94
|
+
* @deprecated Give the zod schema to `input()` and `output()` directly: they take each Standard
|
|
95
|
+
* Schema and infer its type. To check a schema against a type, write `const s: z.ZodType<T> = ...`.
|
|
96
|
+
* For a parse-only object, write a Standard Schema (`~standard`). A later MINOR removes
|
|
97
|
+
* `zodAdapter` (tracker item O58, decision D13).
|
|
71
98
|
*/
|
|
72
99
|
export function zodAdapter<T>(schema: { parse: (data: unknown) => T }): ZodLikeSchema<T> {
|
|
73
100
|
if (isStandardSchema(schema) && typeof (schema as { safeParse?: unknown }).safeParse === "function") {
|
|
@@ -106,18 +133,10 @@ export function zodAdapter<T>(schema: { parse: (data: unknown) => T }): ZodLikeS
|
|
|
106
133
|
// =============================================================================
|
|
107
134
|
|
|
108
135
|
/**
|
|
109
|
-
*
|
|
110
|
-
* gives the TypeScript type.
|
|
136
|
+
* This function gives a pass-through schema for the type `T`.
|
|
111
137
|
*
|
|
112
|
-
* Use
|
|
113
|
-
* server exposes, or that a user calls from the CLI, takes a real schema (zod), so its input
|
|
114
|
-
* is validated and its tools and help describe the fields (roadmap 2.1).
|
|
138
|
+
* @deprecated Use `typed<T>()`. A later MINOR removes `outputSchema` (tracker item O58, decision D13).
|
|
115
139
|
*/
|
|
116
140
|
export function outputSchema<T>(): ZodLikeSchema<T> & StandardSchemaV1<unknown, T> {
|
|
117
|
-
return
|
|
118
|
-
parse: (data: unknown) => data as T,
|
|
119
|
-
safeParse: (data: unknown) => ({ success: true as const, data: data as T }),
|
|
120
|
-
_output: undefined as unknown as T,
|
|
121
|
-
"~standard": passThrough<T>(),
|
|
122
|
-
};
|
|
141
|
+
return typed<T>();
|
|
123
142
|
}
|
|
@@ -37,7 +37,7 @@ import type {
|
|
|
37
37
|
Effects,
|
|
38
38
|
} from "./types.js";
|
|
39
39
|
import { z } from "zod";
|
|
40
|
-
import { anySchema
|
|
40
|
+
import { anySchema } from "./core/schemas.js";
|
|
41
41
|
import { keyToPath, pathToKey } from "./types.js";
|
|
42
42
|
import {
|
|
43
43
|
createRefScope,
|
|
@@ -326,7 +326,7 @@ export const defineProcedureProcedure: Procedure<
|
|
|
326
326
|
throw new Error('procedure.define: the aggregation must be a procedure ref: { "$proc": [...], "input": {...} }');
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
-
const pathKey = path
|
|
329
|
+
const pathKey = pathToKey(path);
|
|
330
330
|
const existing = registry.get(path);
|
|
331
331
|
// A procedure outside the expose rule of the host is not the caller's to replace (R17)
|
|
332
332
|
if (existing && ctx?.expose && !ctx.expose(path)) {
|
|
@@ -463,7 +463,7 @@ export const getProcedureProcedure: Procedure<
|
|
|
463
463
|
ProcedureMetadata
|
|
464
464
|
> = defineProcedure({
|
|
465
465
|
path: ["procedure", "get"],
|
|
466
|
-
input:
|
|
466
|
+
input: getInputSchema,
|
|
467
467
|
output: GetOutputSchema,
|
|
468
468
|
metadata: {
|
|
469
469
|
description: "Describe a procedure by path or key",
|
|
@@ -503,7 +503,7 @@ export const listProceduresProcedure: Procedure<
|
|
|
503
503
|
ProcedureMetadata
|
|
504
504
|
> = defineProcedure({
|
|
505
505
|
path: ["procedure", "list"],
|
|
506
|
-
input:
|
|
506
|
+
input: listInputSchema,
|
|
507
507
|
output: ListOutputSchema,
|
|
508
508
|
metadata: {
|
|
509
509
|
description: "List the registered procedures",
|
|
@@ -530,9 +530,7 @@ export const listProceduresProcedure: Procedure<
|
|
|
530
530
|
*/
|
|
531
531
|
export const manifestProcedure: Procedure<{ namespace?: string | undefined }, ProcedureManifest, ProcedureMetadata> = defineProcedure({
|
|
532
532
|
path: ["procedure", "manifest"],
|
|
533
|
-
input:
|
|
534
|
-
z.object({ namespace: z.string().optional().describe("Only the procedures under this first path segment") }) as z.ZodType<{ namespace?: string | undefined }>,
|
|
535
|
-
),
|
|
533
|
+
input: z.object({ namespace: z.string().optional().describe("Only the procedures under this first path segment") }) as z.ZodType<{ namespace?: string | undefined }>,
|
|
536
534
|
output: ManifestOutputSchema,
|
|
537
535
|
metadata: {
|
|
538
536
|
description: "The manifest of the registry: each exposed procedure with its schemas and effects",
|
package/src/procedures/index.ts
CHANGED
|
@@ -38,6 +38,8 @@ export type {
|
|
|
38
38
|
|
|
39
39
|
// Type utilities
|
|
40
40
|
export { pathToKey, keyToPath } from "./types.js";
|
|
41
|
+
// The escape of a segment with a dot, for a method name, a key and a topic (roadmap CLI-75)
|
|
42
|
+
export { encodeSegment, decodeSegment, segmentsToTopic, topicToSegments } from "./segments.js";
|
|
41
43
|
|
|
42
44
|
// Capabilities (CAPABILITIES-2026-10)
|
|
43
45
|
export {
|
|
@@ -134,7 +136,7 @@ export {
|
|
|
134
136
|
collectionTopicPartition,
|
|
135
137
|
collectionTopicTarget,
|
|
136
138
|
collectionTopicRule,
|
|
137
|
-
// The refusal of a storage whose topic is the topic of a partition (roadmap CLI-57)
|
|
139
|
+
// The refusal of a storage whose topic is the topic of a partition (roadmap CLI-57). Deprecated since CLI-75
|
|
138
140
|
CollectionTopicCollisionError,
|
|
139
141
|
// The rules of the rows of a collection (roadmap CLI-41)
|
|
140
142
|
admittedStorage,
|
|
@@ -212,12 +214,13 @@ export {
|
|
|
212
214
|
export {
|
|
213
215
|
clientModule,
|
|
214
216
|
defineModule,
|
|
217
|
+
definePackage,
|
|
215
218
|
moduleClosure,
|
|
216
219
|
registerModules,
|
|
217
220
|
createRegistry,
|
|
218
221
|
registerClientProcedures,
|
|
219
222
|
} from "./module.js";
|
|
220
|
-
export type { ModuleInput } from "./module.js";
|
|
223
|
+
export type { ModuleInput, PackageDefinition } from "./module.js";
|
|
221
224
|
// One call for a registry of some modules, `invokePath` and `outputValue`
|
|
222
225
|
export { callerOf } from "./caller.js";
|
|
223
226
|
export type { Caller, CallerOptions } from "./caller.js";
|
|
@@ -345,6 +348,7 @@ export {
|
|
|
345
348
|
tryCatchProcedure,
|
|
346
349
|
// Schemas and result types
|
|
347
350
|
anySchema,
|
|
351
|
+
typed,
|
|
348
352
|
typedAnySchema,
|
|
349
353
|
zodAdapter,
|
|
350
354
|
outputSchema,
|
package/src/procedures/module.ts
CHANGED
|
@@ -42,6 +42,40 @@ export function defineModule(
|
|
|
42
42
|
return { name, procedures: [...procedures], requires };
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/** The module of a package and the function that registers it. `definePackage` gives one. */
|
|
46
|
+
export interface PackageDefinition {
|
|
47
|
+
/** The module: the procedures as data. The `./module` entry of the package exports it. */
|
|
48
|
+
readonly module: ProcedureModule;
|
|
49
|
+
/**
|
|
50
|
+
* This function registers the procedures of the module, and of the modules that it uses, in a
|
|
51
|
+
* registry. The default is the global registry. The `./register` entry of the package starts it.
|
|
52
|
+
*/
|
|
53
|
+
readonly register: (registry?: ProcedureRegistry) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* This function gives the module of a package and its `register` function. The call registers
|
|
58
|
+
* nothing, so the `./module` entry of a package can use it.
|
|
59
|
+
*
|
|
60
|
+
* ```ts
|
|
61
|
+
* // src/module.ts
|
|
62
|
+
* const vitest: PackageDefinition = definePackage("@procedures/vitest", [vitestRunProcedure]);
|
|
63
|
+
* export const vitestModule: ProcedureModule = vitest.module;
|
|
64
|
+
* export const registerVitestProcedures: PackageDefinition["register"] = vitest.register;
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function definePackage(
|
|
68
|
+
name: string,
|
|
69
|
+
procedures: readonly AnyProcedure[],
|
|
70
|
+
requires: readonly ProcedureModule[] = []
|
|
71
|
+
): PackageDefinition {
|
|
72
|
+
const module = defineModule(name, procedures, requires);
|
|
73
|
+
const register = (registry: ProcedureRegistry = PROCEDURE_REGISTRY): void => {
|
|
74
|
+
registerModules(registry, [module]);
|
|
75
|
+
};
|
|
76
|
+
return { module, register };
|
|
77
|
+
}
|
|
78
|
+
|
|
45
79
|
/**
|
|
46
80
|
* The modules and their required modules, each one time, with each required module before the
|
|
47
81
|
* module that requires it.
|
|
@@ -53,7 +53,8 @@ const RESERVED_SEGMENTS: ReadonlySet<string> = new Set(["__proto__", "constructo
|
|
|
53
53
|
* This function checks a procedure path: at least one segment, each a non-empty string, and
|
|
54
54
|
* none of `__proto__`, `constructor` or `prototype`. A reserved segment reaches
|
|
55
55
|
* `Object.prototype` in a tree walk (deep dive CORE-6). A segment can contain a dot
|
|
56
|
-
* (`["
|
|
56
|
+
* (`["collections", "e2e.tabs", "find"]`). The key encodes the dot (roadmap CLI-75), so two
|
|
57
|
+
* different paths do not have the same key.
|
|
57
58
|
*
|
|
58
59
|
* @throws RegistryError when the path is not valid
|
|
59
60
|
*/
|
|
@@ -141,7 +142,8 @@ export class ProcedureRegistry {
|
|
|
141
142
|
if (hintRefusal) throw new RegistryError(`${hintRefusal} (${key})`, path);
|
|
142
143
|
|
|
143
144
|
const replaced = this.procedures.get(key);
|
|
144
|
-
// The key
|
|
145
|
+
// The key encodes each segment (roadmap CLI-75), so ["a.b", "c"] and ["a", "b.c"] have different
|
|
146
|
+
// keys. The check stays as a guard: two different paths must never share one key
|
|
145
147
|
if (replaced && !samePath(replaced.path, path)) {
|
|
146
148
|
throw new RegistryError(
|
|
147
149
|
`Procedure path ${JSON.stringify(path)} has the same key as ${JSON.stringify(replaced.path)}: ${key}`,
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one escape of a segment of a procedure path or of a topic (roadmap CLI-75, decision D36).
|
|
3
|
+
*
|
|
4
|
+
* A method name, a registry key and a topic join their segments with ".". Before, a segment with a
|
|
5
|
+
* dot did not come back: `["collections", "e2e.tabs", "find"]` went over the wire as
|
|
6
|
+
* `collections.e2e.tabs.find` and came back as four segments. Now each segment encodes `%` as
|
|
7
|
+
* `%25` and then `.` as `%2E` before the join.
|
|
8
|
+
*
|
|
9
|
+
* - A segment without `.` and `%` encodes to itself. Thus the wire text of each such name stays the same.
|
|
10
|
+
* - The decode changes `%2E` and `%2e` to `.`, and `%25` to `%`. It keeps each other `%` sequence,
|
|
11
|
+
* so the raw `%` of an old client stays a `%`.
|
|
12
|
+
* - For each segment, `decodeSegment(encodeSegment(segment))` gives the same segment.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* This function encodes one segment for a text with dots between the segments. It encodes `%` as
|
|
17
|
+
* `%25` and then `.` as `%2E`. A segment without `.` and `%` stays the same.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* encodeSegment("notes"); // "notes"
|
|
22
|
+
* encodeSegment("e2e.tabs"); // "e2e%2Etabs"
|
|
23
|
+
* encodeSegment("50%"); // "50%25"
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function encodeSegment(segment: string): string {
|
|
27
|
+
return segment.replace(/[%.]/g, (char) => (char === "%" ? "%25" : "%2E"));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* This function decodes one segment of a text with dots between the segments. `%2E` and `%2e`
|
|
32
|
+
* become `.`, and `%25` becomes `%`. Each other `%` sequence stays the same.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* decodeSegment("e2e%2Etabs"); // "e2e.tabs"
|
|
37
|
+
* decodeSegment("50%25"); // "50%"
|
|
38
|
+
* decodeSegment("50%"); // "50%"
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function decodeSegment(segment: string): string {
|
|
42
|
+
return segment.replace(/%(25|2[Ee])/g, (_sequence, code: string) => (code === "25" ? "%" : "."));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* This function gives the text of a list of raw segments: it encodes each segment and joins them
|
|
47
|
+
* with dots. Use it for a topic, so that a segment with a dot stays one segment.
|
|
48
|
+
* `topicToSegments()` gives the segments again.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* segmentsToTopic(["orders", "created"]); // "orders.created"
|
|
53
|
+
* segmentsToTopic(["collections", "e2e.tabs"]); // "collections.e2e%2Etabs"
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export function segmentsToTopic(segments: readonly string[]): string {
|
|
57
|
+
// A segment that is not a string becomes text as with join(): null and undefined give "" (a
|
|
58
|
+
// caller can check a path that is not valid, for example the compiler)
|
|
59
|
+
return segments.map((segment: unknown) => encodeSegment(segment === null || segment === undefined ? "" : String(segment))).join(".");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* This function gives the raw segments of a text with dots between the segments: it splits the
|
|
64
|
+
* text on each dot and decodes each part. For each list of segments with one segment or more,
|
|
65
|
+
* `topicToSegments(segmentsToTopic(segments))` gives the same list.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* topicToSegments("collections.e2e%2Etabs"); // ["collections", "e2e.tabs"]
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export function topicToSegments(topic: string): string[] {
|
|
73
|
+
return topic.split(".").map(decodeSegment);
|
|
74
|
+
}
|
package/src/procedures/types.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { EventBus } from "../events/types.js";
|
|
|
15
15
|
import type { HydrationEvent } from "./ref.js";
|
|
16
16
|
import type { Principal } from "../server/types.js";
|
|
17
17
|
import type { Invocation } from "./invoke.js";
|
|
18
|
+
import { segmentsToTopic, topicToSegments } from "./segments.js";
|
|
18
19
|
|
|
19
20
|
// =============================================================================
|
|
20
21
|
// Procedure Path
|
|
@@ -33,17 +34,20 @@ import type { Invocation } from "./invoke.js";
|
|
|
33
34
|
export type ProcedurePath = string[];
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
|
-
*
|
|
37
|
+
* This function gives the key of a path: the segments with dots between them. Each segment encodes
|
|
38
|
+
* `%` as `%25` and `.` as `%2E` (`encodeSegment()`, roadmap CLI-75). Thus the key of
|
|
39
|
+
* `["a.b", "c"]` is `a%2Eb.c`, and not the key of `["a", "b", "c"]`. A plain path keeps its key.
|
|
37
40
|
*/
|
|
38
41
|
export function pathToKey(path: ProcedurePath): string {
|
|
39
|
-
return path
|
|
42
|
+
return segmentsToTopic(path);
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
/**
|
|
43
|
-
*
|
|
46
|
+
* This function gives the path of a key: it splits the key on each dot and decodes each segment
|
|
47
|
+
* (`decodeSegment()`, roadmap CLI-75). For each path, `keyToPath(pathToKey(path))` gives the path.
|
|
44
48
|
*/
|
|
45
49
|
export function keyToPath(key: string): ProcedurePath {
|
|
46
|
-
return key
|
|
50
|
+
return topicToSegments(key);
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
// =============================================================================
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* keyed limiter for each limit, and refuses a call above a limit with `RATE_LIMITED`.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import type
|
|
25
|
+
import { pathToKey, type Capability, type ClearanceOrder, type ProcedurePath, type ResourcesOf } from "../procedures/types.js";
|
|
26
26
|
import type { AdmitItem, Interceptor, Invocation } from "../procedures/invoke.js";
|
|
27
27
|
import { narrowCapability } from "../procedures/capability.js";
|
|
28
28
|
import type { PrincipalOf, ServerRequest } from "./types.js";
|
|
@@ -53,7 +53,8 @@ export function exposePatterns(patterns: readonly string[]): (path: ProcedurePat
|
|
|
53
53
|
}
|
|
54
54
|
return (path) => {
|
|
55
55
|
if (all) return true;
|
|
56
|
-
|
|
56
|
+
// The key encodes a dot in a segment (roadmap CLI-75), as the method name does
|
|
57
|
+
const key = pathToKey(path);
|
|
57
58
|
return exact.has(key) || prefixes.some((prefix) => key.startsWith(prefix));
|
|
58
59
|
};
|
|
59
60
|
}
|
|
@@ -62,7 +63,7 @@ export function exposePatterns(patterns: readonly string[]): (path: ProcedurePat
|
|
|
62
63
|
export function surfaceRule(surface: ToolSurface): (path: ProcedurePath) => boolean {
|
|
63
64
|
const namespaces = new Set(surface.namespaces);
|
|
64
65
|
const excluded = new Set(surface.exclude ?? []);
|
|
65
|
-
return (path) => namespaces.has(path[0] ?? "") && !excluded.has(path
|
|
66
|
+
return (path) => namespaces.has(path[0] ?? "") && !excluded.has(pathToKey(path));
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
/** The options of `hostPolicy()`. */
|
|
@@ -162,7 +163,7 @@ function exposeOf(options: HostPolicyOptions): ((path: ProcedurePath) => boolean
|
|
|
162
163
|
: surfaceRule(expose as ToolSurface);
|
|
163
164
|
const excluded = new Set(options.exclude ?? []);
|
|
164
165
|
if (excluded.size === 0) return rule;
|
|
165
|
-
return (path) => !excluded.has(path
|
|
166
|
+
return (path) => !excluded.has(pathToKey(path)) && (rule ? rule(path) : true);
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
/** This function builds the policy of a host from its options. */
|
package/src/server/index.ts
CHANGED
|
@@ -44,6 +44,8 @@ export type { ServerOptions } from "./server.js";
|
|
|
44
44
|
export { ProcedureServer, createProcedureServer, STREAM_METADATA_KEY, isUnboundedStream } from "./procedure-server.js";
|
|
45
45
|
export type { ProcedureServerOptions, RegisterProcedureOptions, StorageConfig, StopOptions, StopParts, StorageHandle, UseStorageOptions } from "./procedure-server.js";
|
|
46
46
|
export { pathToMethod, methodToPath, methodName } from "./method.js";
|
|
47
|
+
// The escape of a segment with a dot, for a method name, a key and a topic (roadmap CLI-75)
|
|
48
|
+
export { encodeSegment, decodeSegment, segmentsToTopic, topicToSegments } from "../procedures/segments.js";
|
|
47
49
|
export {
|
|
48
50
|
HandlerNotFoundError,
|
|
49
51
|
ServerError,
|
package/src/server/method.ts
CHANGED
|
@@ -8,26 +8,41 @@
|
|
|
8
8
|
* (deep dive architecture review 3.3).
|
|
9
9
|
*
|
|
10
10
|
* `methodToPath` splits both the service and the operation on ".", so it reads both forms.
|
|
11
|
+
*
|
|
12
|
+
* Each segment encodes `%` as `%25` and then `.` as `%2E` before the join (`encodeSegment()`,
|
|
13
|
+
* roadmap CLI-75). Thus the path `["collections", "e2e.tabs", "find"]` is the method
|
|
14
|
+
* `{ service: "collections.e2e%2Etabs", operation: "find" }`, and `methodToPath` gives the same
|
|
15
|
+
* three segments again. A segment without `.` and `%` stays the same, so a plain path keeps its method.
|
|
11
16
|
*/
|
|
12
17
|
|
|
13
18
|
import type { Method } from "../client/types.js";
|
|
14
19
|
import type { ProcedurePath } from "../procedures/types.js";
|
|
20
|
+
import { encodeSegment, topicToSegments } from "../procedures/segments.js";
|
|
15
21
|
|
|
16
22
|
/**
|
|
17
23
|
* This function gives the transport method of a procedure path. The last segment is the
|
|
18
|
-
* operation, and the other segments, joined with dots, are the service.
|
|
19
|
-
* for example `["morning"]`, has
|
|
24
|
+
* operation, and the other segments, joined with dots, are the service. Each segment is encoded
|
|
25
|
+
* first (`encodeSegment()`, roadmap CLI-75). A path of one segment, for example `["morning"]`, has
|
|
26
|
+
* the empty service (CLI-28). An empty path throws.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* pathToMethod(["docker", "compose", "up"]); // { service: "docker.compose", operation: "up" }
|
|
31
|
+
* pathToMethod(["collections", "e2e.tabs", "find"]); // { service: "collections.e2e%2Etabs", operation: "find" }
|
|
32
|
+
* ```
|
|
20
33
|
*/
|
|
21
34
|
export function pathToMethod(path: readonly string[]): Method {
|
|
22
35
|
if (path.length === 0) {
|
|
23
36
|
throw new Error("Invalid procedure path: it needs one segment or more");
|
|
24
37
|
}
|
|
25
|
-
|
|
38
|
+
const segments = path.map(encodeSegment);
|
|
39
|
+
return { service: segments.slice(0, -1).join("."), operation: segments[segments.length - 1]! };
|
|
26
40
|
}
|
|
27
41
|
|
|
28
42
|
/**
|
|
29
43
|
* The name of a method with dots: `docker.compose.up`, or only the operation for the empty
|
|
30
|
-
* service of a path of one segment (`morning`).
|
|
44
|
+
* service of a path of one segment (`morning`). The segments of the method are already encoded,
|
|
45
|
+
* so the name of `["collections", "e2e.tabs", "find"]` is `collections.e2e%2Etabs.find`.
|
|
31
46
|
*/
|
|
32
47
|
export function methodName(method: Pick<Method, "service" | "operation">): string {
|
|
33
48
|
return method.service === "" ? method.operation : `${method.service}.${method.operation}`;
|
|
@@ -36,10 +51,12 @@ export function methodName(method: Pick<Method, "service" | "operation">): strin
|
|
|
36
51
|
/**
|
|
37
52
|
* This function gives the procedure path of a transport method. It accepts both encodings:
|
|
38
53
|
* `{ service: "a.b", operation: "c" }` and `{ service: "a", operation: "b.c" }` give `["a", "b", "c"]`.
|
|
54
|
+
* It splits on each dot and then decodes each segment (`decodeSegment()`, roadmap CLI-75): `%2E`
|
|
55
|
+
* gives `.` and `%25` gives `%`.
|
|
39
56
|
*/
|
|
40
57
|
export function methodToPath(method: Pick<Method, "service" | "operation">): ProcedurePath {
|
|
41
58
|
// The empty service of a path of one segment has no segment. Each other empty segment stays, so
|
|
42
59
|
// a method with ".." or a stray dot names no procedure: the registry has no empty segment (CLI-28)
|
|
43
|
-
const service = method.service === "" ? [] : method.service
|
|
44
|
-
return [...service, ...method.operation
|
|
60
|
+
const service = method.service === "" ? [] : topicToSegments(method.service);
|
|
61
|
+
return [...service, ...topicToSegments(method.operation)];
|
|
45
62
|
}
|