@procedures/client 0.22.0 → 0.23.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 +61 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.d.ts +2 -2
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +2 -0
- package/dist/browser.js.map +1 -1
- package/dist/client/client.d.ts +10 -3
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +14 -2
- package/dist/client/client.js.map +1 -1
- package/dist/events/publisher.d.ts +9 -4
- package/dist/events/publisher.d.ts.map +1 -1
- package/dist/events/publisher.js +39 -11
- package/dist/events/publisher.js.map +1 -1
- package/dist/procedures/collection/partition.d.ts +21 -0
- package/dist/procedures/collection/partition.d.ts.map +1 -1
- package/dist/procedures/collection/partition.js +27 -0
- package/dist/procedures/collection/partition.js.map +1 -1
- package/dist/procedures/envelope.d.ts +119 -0
- package/dist/procedures/envelope.d.ts.map +1 -0
- package/dist/procedures/envelope.js +371 -0
- package/dist/procedures/envelope.js.map +1 -0
- package/dist/procedures/index.d.ts +2 -0
- package/dist/procedures/index.d.ts.map +1 -1
- package/dist/procedures/index.js +2 -0
- package/dist/procedures/index.js.map +1 -1
- package/dist/server/procedure-server.d.ts +13 -1
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +16 -2
- package/dist/server/procedure-server.js.map +1 -1
- package/dist/server/types.d.ts +11 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js.map +1 -1
- package/package.json +1 -1
- package/src/browser.ts +9 -0
- package/src/client/client.ts +17 -4
- package/src/events/publisher.ts +47 -15
- package/src/procedures/collection/partition.ts +32 -0
- package/src/procedures/envelope.ts +468 -0
- package/src/procedures/index.ts +4 -0
- package/src/server/procedure-server.ts +23 -3
- package/src/server/types.ts +11 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The envelope form of a program (coeval, decisions D6, D9 and D10, roadmap item M7).
|
|
3
|
+
*
|
|
4
|
+
* A document of coeval is an envelope: `{ type, value }`. The header can also have `name` and
|
|
5
|
+
* `own`. In the envelope form of a program:
|
|
6
|
+
*
|
|
7
|
+
* - A call is `{ type: path, value: input }`. The type is the path of a procedure, as an array of
|
|
8
|
+
* keys. A ref into the world, `{ type: "ref", value: ["world", ...path] }`, is also a type.
|
|
9
|
+
* - A ref is `{ type: "ref", value: ["names", name, ...fields] }`. The name `last` reads the result
|
|
10
|
+
* of the step before.
|
|
11
|
+
* - A literal is `{ type: "literal", value }`. Nothing in its value runs.
|
|
12
|
+
*
|
|
13
|
+
* The envelope form is a surface. `fromEnvelopeProgram()` changes it into the `$proc` form, and
|
|
14
|
+
* `hydrateInput()` starts that form. In the input of a `$proc` program, an envelope is data. Thus
|
|
15
|
+
* the root of a program decides its form, and the old programs do not change.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
LITERAL_KEY,
|
|
20
|
+
MAX_DATA_DEPTH,
|
|
21
|
+
OUTPUT_REF_KEY,
|
|
22
|
+
PROCEDURE_JSON_KEY,
|
|
23
|
+
PROCEDURE_NAME_KEY,
|
|
24
|
+
PROCEDURE_WHEN_KEY,
|
|
25
|
+
isAnyProcedureRef,
|
|
26
|
+
isLiteral,
|
|
27
|
+
isOutputRef,
|
|
28
|
+
isPlainObject,
|
|
29
|
+
isProcedureRef,
|
|
30
|
+
normalizeRef,
|
|
31
|
+
type AnyProcedureRef,
|
|
32
|
+
type OutputRef,
|
|
33
|
+
type ProcedureRefJson,
|
|
34
|
+
} from "./ref.js";
|
|
35
|
+
import type { ProcedurePath } from "./types.js";
|
|
36
|
+
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// Types
|
|
39
|
+
// =============================================================================
|
|
40
|
+
|
|
41
|
+
/** This type is a path of keys from a root of the context (coeval D10). */
|
|
42
|
+
export type EnvelopePath = readonly (string | number)[];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* This document reads a value: `{ type: "ref", value: [root, step, ...] }` (coeval D10). A client
|
|
46
|
+
* program reads only the root `names`. A ref into the world is only the type of a call.
|
|
47
|
+
*/
|
|
48
|
+
export interface EnvelopeRef {
|
|
49
|
+
readonly type: "ref";
|
|
50
|
+
readonly value: EnvelopePath;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** This document gives its value as data. Nothing in the value runs. */
|
|
54
|
+
export interface EnvelopeLiteral {
|
|
55
|
+
readonly type: "literal";
|
|
56
|
+
readonly value: unknown;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* This document is a call in the envelope form (coeval D9). The type is the path of the
|
|
61
|
+
* procedure, or a ref into the world. The value is the input. A value of `null` gives no input.
|
|
62
|
+
*/
|
|
63
|
+
export interface EnvelopeProgram {
|
|
64
|
+
/** The path of the procedure, or a ref into the world: `{ type: "ref", value: ["world", ...path] }`. */
|
|
65
|
+
readonly type: readonly string[] | EnvelopeRef;
|
|
66
|
+
/** The input of the call. `null` gives a call without input. */
|
|
67
|
+
readonly value: unknown;
|
|
68
|
+
/** The name of the result. A later step of `client.chain` reads it as `["names", name]`. */
|
|
69
|
+
readonly name?: string | undefined;
|
|
70
|
+
/** The own entries of the document. A client program has none: the conversion refuses this key. */
|
|
71
|
+
readonly own?: unknown;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// =============================================================================
|
|
75
|
+
// The shapes of the envelope form
|
|
76
|
+
// =============================================================================
|
|
77
|
+
|
|
78
|
+
/** The keys of an envelope (coeval D6). */
|
|
79
|
+
const HEADER_KEYS: ReadonlySet<string> = new Set(["type", "value", "name", "own"]);
|
|
80
|
+
|
|
81
|
+
/** The keys that make an object a construct of the `$proc` form. */
|
|
82
|
+
const NATIVE_KEYS: readonly string[] = [PROCEDURE_JSON_KEY, OUTPUT_REF_KEY, LITERAL_KEY];
|
|
83
|
+
|
|
84
|
+
/** The keys of a `$proc` call that the envelope form keeps (`$when` gives its own error). */
|
|
85
|
+
const CALL_KEYS: ReadonlySet<string> = new Set([PROCEDURE_JSON_KEY, "input", PROCEDURE_NAME_KEY, PROCEDURE_WHEN_KEY]);
|
|
86
|
+
|
|
87
|
+
/** The keys of a runtime `ProcedureRef` that the envelope form keeps. */
|
|
88
|
+
const RUNTIME_CALL_KEYS: ReadonlySet<string> = new Set(["path", "input", PROCEDURE_NAME_KEY, PROCEDURE_WHEN_KEY]);
|
|
89
|
+
|
|
90
|
+
const REF_TYPE = "ref";
|
|
91
|
+
const LITERAL_TYPE = "literal";
|
|
92
|
+
const NAMES_ROOT = "names";
|
|
93
|
+
const WORLD_ROOT = "world";
|
|
94
|
+
/** The name of the result of the step before, in the envelope form. */
|
|
95
|
+
const LAST_NAME = "last";
|
|
96
|
+
/** The name of the result of the step before, in the `$proc` form. */
|
|
97
|
+
const NATIVE_LAST = "$last";
|
|
98
|
+
|
|
99
|
+
/** An object with the shape of an envelope. */
|
|
100
|
+
type EnvelopeShape = Record<string, unknown> & { type: unknown; value: unknown; name?: unknown; own?: unknown };
|
|
101
|
+
|
|
102
|
+
/** The kind of an envelope. `bad-type` is an envelope whose type is a ref that is not into the world. */
|
|
103
|
+
type EnvelopeKind = "call" | "ref" | "literal" | "bad-type";
|
|
104
|
+
|
|
105
|
+
/** This function gives true if a value is a plain object with `type`, `value` and only header keys. */
|
|
106
|
+
function isEnvelopeShape(value: unknown): value is EnvelopeShape {
|
|
107
|
+
return (
|
|
108
|
+
isPlainObject(value) &&
|
|
109
|
+
Object.hasOwn(value, "type") &&
|
|
110
|
+
Object.hasOwn(value, "value") &&
|
|
111
|
+
Object.keys(value).every((key) => HEADER_KEYS.has(key))
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** This function gives true if a value is a procedure path: an array of one or more strings. */
|
|
116
|
+
function isProcedurePath(value: unknown): value is readonly string[] {
|
|
117
|
+
return Array.isArray(value) && value.length > 0 && value.every((key) => typeof key === "string");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** This function gives the procedure path of the type of a call, or undefined for another type. */
|
|
121
|
+
function callPath(type: unknown): ProcedurePath | undefined {
|
|
122
|
+
if (isProcedurePath(type)) return [...type];
|
|
123
|
+
if (!isEnvelopeShape(type) || type.type !== REF_TYPE || Object.keys(type).length !== 2) return undefined;
|
|
124
|
+
const path = type.value;
|
|
125
|
+
if (!Array.isArray(path) || path[0] !== WORLD_ROOT) return undefined;
|
|
126
|
+
const steps: unknown[] = path.slice(1);
|
|
127
|
+
return isProcedurePath(steps) ? [...steps] : undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** This function gives the kind of an envelope, or undefined for data. */
|
|
131
|
+
function envelopeKind(value: unknown): EnvelopeKind | undefined {
|
|
132
|
+
if (!isEnvelopeShape(value)) return undefined;
|
|
133
|
+
if (callPath(value.type)) return "call";
|
|
134
|
+
if (value.type === REF_TYPE) return "ref";
|
|
135
|
+
if (value.type === LITERAL_TYPE) return "literal";
|
|
136
|
+
if (isPlainObject(value.type) && value.type["type"] === REF_TYPE) return "bad-type";
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** This function gives true if a plain object has a key of the `$proc` form. */
|
|
141
|
+
function hasNativeKey(value: Record<string, unknown>): boolean {
|
|
142
|
+
return NATIVE_KEYS.some((key) => Object.hasOwn(value, key));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** This function gives a short JSON text of a value for an error message. */
|
|
146
|
+
function preview(value: unknown): string {
|
|
147
|
+
let text: string | undefined;
|
|
148
|
+
try {
|
|
149
|
+
text = JSON.stringify(value);
|
|
150
|
+
} catch {
|
|
151
|
+
text = undefined;
|
|
152
|
+
}
|
|
153
|
+
text ??= String(value);
|
|
154
|
+
return text.length > 80 ? `${text.slice(0, 77)}...` : text;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* This function gives the result of `fn` one level deeper in a walk of a program. It refuses an
|
|
159
|
+
* object that contains itself, and a program deeper than `MAX_DATA_DEPTH`, as hydration does.
|
|
160
|
+
*/
|
|
161
|
+
function within<T>(value: object, path: Set<object>, depth: number, fn: () => T): T {
|
|
162
|
+
if (depth > MAX_DATA_DEPTH) {
|
|
163
|
+
throw new Error(`The program has more than ${MAX_DATA_DEPTH} levels of nesting`);
|
|
164
|
+
}
|
|
165
|
+
if (path.has(value)) {
|
|
166
|
+
throw new Error("The program is cyclic (an object contains itself)");
|
|
167
|
+
}
|
|
168
|
+
path.add(value);
|
|
169
|
+
try {
|
|
170
|
+
return fn();
|
|
171
|
+
} finally {
|
|
172
|
+
path.delete(value);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// =============================================================================
|
|
177
|
+
// The envelope form to the $proc form
|
|
178
|
+
// =============================================================================
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* This function gives true if a value is a program in the envelope form. The value is a plain
|
|
182
|
+
* object with the keys `type` and `value`, and only the header keys `name` and `own` in
|
|
183
|
+
* addition. The type is a procedure path (an array of one or more strings), or a ref into the
|
|
184
|
+
* world: `{ type: "ref", value: ["world", ...path] }`.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```typescript
|
|
188
|
+
* isEnvelopeProgram({ type: ["client", "add"], value: { a: 1, b: 2 } }); // true
|
|
189
|
+
* isEnvelopeProgram({ $proc: ["client", "add"], input: { a: 1, b: 2 } }); // false
|
|
190
|
+
* ```
|
|
191
|
+
*/
|
|
192
|
+
export function isEnvelopeProgram(value: unknown): value is EnvelopeProgram {
|
|
193
|
+
return envelopeKind(value) === "call";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* This function changes a program in the envelope form into the `$proc` form. `hydrateInput()`
|
|
198
|
+
* and `client.exec()` start the result.
|
|
199
|
+
*
|
|
200
|
+
* The conversion:
|
|
201
|
+
* - A call gives `{ $proc: path, input }`. A value of `null` gives a call without `input`. The
|
|
202
|
+
* header `name` gives `$name`.
|
|
203
|
+
* - A ref `["names", "last"]` gives `{ $ref: "$last" }`. A ref `["names", name, ...fields]` gives
|
|
204
|
+
* `{ $ref: "name.field" }`.
|
|
205
|
+
* - A literal gives `{ $literal: value }`.
|
|
206
|
+
* - An object with a `$proc`, `$ref` or `$literal` key is data: the function puts it in a
|
|
207
|
+
* `$literal`. A runtime `ProcedureRef` is data too.
|
|
208
|
+
* - The function copies the arrays and the other plain objects. Other values stay the same.
|
|
209
|
+
*
|
|
210
|
+
* The function throws an error for a ref with a root other than `names`, and for a bad key of a
|
|
211
|
+
* ref path. It also throws an error for a header on a ref or a literal, and for the key `own`.
|
|
212
|
+
* The envelope form has no `$when`: a program that needs it keeps the `$proc` form.
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* ```typescript
|
|
216
|
+
* fromEnvelopeProgram({
|
|
217
|
+
* type: ["client", "multiply"],
|
|
218
|
+
* value: { a: { type: "ref", value: ["names", "item"] }, b: 10 },
|
|
219
|
+
* });
|
|
220
|
+
* // { $proc: ["client", "multiply"], input: { a: { $ref: "item" }, b: 10 } }
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
export function fromEnvelopeProgram(program: EnvelopeProgram): ProcedureRefJson {
|
|
224
|
+
if (!isEnvelopeProgram(program)) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`fromEnvelopeProgram() requires a call: { "type": [procedure path], "value": input }. Got ${preview(program)}`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
const path = new Set<object>();
|
|
230
|
+
return within(program, path, 0, () => fromCall(program as EnvelopeShape, path, 0));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** This function changes a call envelope into a `$proc` call. */
|
|
234
|
+
function fromCall(call: EnvelopeShape, path: Set<object>, depth: number): ProcedureRefJson {
|
|
235
|
+
const procedurePath = callPath(call.type)!;
|
|
236
|
+
if (call.own !== undefined) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`A client program has no own entries: remove the key "own" of the call of ${preview(procedurePath)}`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (call.name !== undefined && typeof call.name !== "string") {
|
|
242
|
+
throw new Error(`The name of a call must be a string. The call of ${preview(procedurePath)} has the name ${preview(call.name)}`);
|
|
243
|
+
}
|
|
244
|
+
const native: { $proc: ProcedurePath; input?: unknown; $name?: string } = { $proc: procedurePath };
|
|
245
|
+
// A call without input gets undefined, as a $proc call without `input` (PROGRAM-FORMAT section 1)
|
|
246
|
+
if (call.value !== null && call.value !== undefined) native.input = fromValue(call.value, path, depth + 1);
|
|
247
|
+
if (call.name !== undefined) native.$name = call.name;
|
|
248
|
+
return native;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** This function refuses a header on an envelope that is not a call. */
|
|
252
|
+
function refuseHeader(envelope: EnvelopeShape): void {
|
|
253
|
+
for (const key of ["name", "own"]) {
|
|
254
|
+
if (envelope[key] !== undefined) {
|
|
255
|
+
throw new Error(`Only a call has a header. The ${String(envelope.type)} ${preview(envelope)} has the key "${key}"`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** This function gives the text of one key of a ref path. */
|
|
261
|
+
function keyText(key: unknown, refPath: readonly unknown[]): string {
|
|
262
|
+
if (typeof key === "string" && !key.includes(".")) return key;
|
|
263
|
+
if (typeof key === "number" && Number.isSafeInteger(key) && key >= 0) return String(key);
|
|
264
|
+
throw new Error(
|
|
265
|
+
`Each key of a ref path must be a string without a dot or an integer of 0 or more. ` +
|
|
266
|
+
`The key ${preview(key)} of the ref ${preview(refPath)} is not`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** This function changes a ref envelope into a `$ref` object. */
|
|
271
|
+
function fromRef(ref: EnvelopeShape): { $ref: string } {
|
|
272
|
+
refuseHeader(ref);
|
|
273
|
+
const refPath = ref.value;
|
|
274
|
+
if (!Array.isArray(refPath) || refPath.length === 0) {
|
|
275
|
+
throw new Error(`The value of a ref must be a path: [root, step, ...]. Got ${preview(refPath)}`);
|
|
276
|
+
}
|
|
277
|
+
const [root, ...steps] = refPath as unknown[];
|
|
278
|
+
if (root !== NAMES_ROOT) {
|
|
279
|
+
const where = root === WORLD_ROOT ? ` A ref into the world is only the type of a call.` : "";
|
|
280
|
+
throw new Error(
|
|
281
|
+
`A client program reads only the root "names". The ref ${preview(refPath)} has the root ${preview(root)}.${where}`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
if (steps.length === 0) {
|
|
285
|
+
throw new Error(`A ref to the root "names" needs a name: ["names", name, ...fields]. Got ${preview(refPath)}`);
|
|
286
|
+
}
|
|
287
|
+
const [name, ...fields] = steps.map((key) => keyText(key, refPath));
|
|
288
|
+
return { $ref: [name === LAST_NAME ? NATIVE_LAST : name, ...fields].join(".") };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** This function changes a value of an envelope program into the `$proc` form. */
|
|
292
|
+
function fromValue(value: unknown, path: Set<object>, depth: number): unknown {
|
|
293
|
+
if (value === null || typeof value !== "object") return value;
|
|
294
|
+
if (Array.isArray(value)) {
|
|
295
|
+
return within(value, path, depth, () => value.map((item) => fromValue(item, path, depth + 1)));
|
|
296
|
+
}
|
|
297
|
+
// The $proc form is data in an envelope program: a runtime ref, and each object that
|
|
298
|
+
// hydration reads as a call or a $ref
|
|
299
|
+
if (isProcedureRef(value)) return { $literal: value };
|
|
300
|
+
if (!isPlainObject(value)) {
|
|
301
|
+
// Values that are not plain data (Date, Map, class instances) stay the same object
|
|
302
|
+
return isAnyProcedureRef(value) || isOutputRef(value) ? { $literal: value } : value;
|
|
303
|
+
}
|
|
304
|
+
switch (envelopeKind(value)) {
|
|
305
|
+
case "call":
|
|
306
|
+
return within(value, path, depth, () => fromCall(value as EnvelopeShape, path, depth));
|
|
307
|
+
case "ref":
|
|
308
|
+
return fromRef(value as EnvelopeShape);
|
|
309
|
+
case "literal":
|
|
310
|
+
refuseHeader(value as EnvelopeShape);
|
|
311
|
+
return { $literal: value["value"] };
|
|
312
|
+
case "bad-type":
|
|
313
|
+
throw new Error(
|
|
314
|
+
`The type of a call must be a procedure path or a ref into the world: ["world", ...path]. Got ${preview(value["type"])}`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
// An object of the $proc form is data in an envelope program
|
|
318
|
+
if (hasNativeKey(value)) return { $literal: value };
|
|
319
|
+
return within(value, path, depth, () =>
|
|
320
|
+
// Object.fromEntries makes own properties, also for a key such as "__proto__"
|
|
321
|
+
Object.fromEntries(Object.entries(value).map(([key, item]) => [key, fromValue(item, path, depth + 1)]))
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// =============================================================================
|
|
326
|
+
// The $proc form to the envelope form
|
|
327
|
+
// =============================================================================
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* This function changes a program in the `$proc` form into the envelope form, for a tool that
|
|
331
|
+
* shows it. `fromEnvelopeProgram()` changes the result back into the same program.
|
|
332
|
+
*
|
|
333
|
+
* The conversion:
|
|
334
|
+
* - A call gives `{ type: path, value: input }`. A call without input gives the value `null`.
|
|
335
|
+
* `$name` gives the header `name`.
|
|
336
|
+
* - `{ $ref: "$last" }` gives `["names", "last"]`. `{ $ref: "a.b" }` gives `["names", "a", "b"]`.
|
|
337
|
+
* - `{ $literal: value }` gives a literal.
|
|
338
|
+
* - Data that the envelope form reads in a different way gives a literal. This data is an object
|
|
339
|
+
* with the shape of a call, a ref or a literal. It is also an object with a `$proc`, `$ref` or
|
|
340
|
+
* `$literal` key.
|
|
341
|
+
*
|
|
342
|
+
* The function throws an error for a program that the envelope form cannot keep:
|
|
343
|
+
* - a call with `$when`, or with a key other than the path, the input and the name,
|
|
344
|
+
* - a `$ref` object with other keys, or a ref to a step with the name `last`,
|
|
345
|
+
* - such data that holds a call, a `$ref` or a `$literal`.
|
|
346
|
+
*
|
|
347
|
+
* These programs keep the `$proc` form.
|
|
348
|
+
*
|
|
349
|
+
* The round trip gives the same program, with two exceptions. A call with the input `null`, and
|
|
350
|
+
* data that the envelope form reads in a different way, come back in a `$literal`. They give
|
|
351
|
+
* the same values.
|
|
352
|
+
*
|
|
353
|
+
* @example
|
|
354
|
+
* ```typescript
|
|
355
|
+
* toEnvelopeProgram({ $proc: ["client", "add"], input: { a: { $ref: "$last" }, b: 1 }, $name: "sum" });
|
|
356
|
+
* // { type: ["client", "add"], value: { a: { type: "ref", value: ["names", "last"] }, b: 1 }, name: "sum" }
|
|
357
|
+
* ```
|
|
358
|
+
*/
|
|
359
|
+
export function toEnvelopeProgram(program: AnyProcedureRef): EnvelopeProgram {
|
|
360
|
+
if (!isAnyProcedureRef(program)) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
`toEnvelopeProgram() requires a call: { "$proc": [procedure path], "input": input }. Got ${preview(program)}`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
const path = new Set<object>();
|
|
366
|
+
return within(program, path, 0, () => toCall(program, path, 0));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** This function changes a `$proc` call into a call envelope. */
|
|
370
|
+
function toCall(call: AnyProcedureRef, path: Set<object>, depth: number): EnvelopeProgram {
|
|
371
|
+
const { path: procedurePath, input, $name, $when } = normalizeRef(call);
|
|
372
|
+
if ($when !== undefined) {
|
|
373
|
+
throw new Error(
|
|
374
|
+
`The call of ${preview(procedurePath)} has "$when": ${preview($when)}. ` +
|
|
375
|
+
`The envelope form has no $when: keep this program in the $proc form`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
if (!isProcedurePath(procedurePath)) {
|
|
379
|
+
throw new Error(`The path of a call must be one or more strings. Got ${preview(procedurePath)}`);
|
|
380
|
+
}
|
|
381
|
+
// A key that the envelope form cannot keep has no envelope form: the conversion does not drop it
|
|
382
|
+
const keys = isProcedureRef(call) ? RUNTIME_CALL_KEYS : CALL_KEYS;
|
|
383
|
+
const other = Object.keys(call).find((key) => !keys.has(key));
|
|
384
|
+
if (other !== undefined) {
|
|
385
|
+
throw new Error(
|
|
386
|
+
`The call of ${preview(procedurePath)} has the key "${other}". The envelope form keeps only ` +
|
|
387
|
+
`the path, the input and the name: keep this program in the $proc form`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
if ($name !== undefined && typeof $name !== "string") {
|
|
391
|
+
throw new Error(`The $name of a call must be a string. The call of ${preview(procedurePath)} has ${preview($name)}`);
|
|
392
|
+
}
|
|
393
|
+
const value =
|
|
394
|
+
input === undefined
|
|
395
|
+
? null
|
|
396
|
+
: input === null
|
|
397
|
+
? { type: LITERAL_TYPE, value: null }
|
|
398
|
+
: toValue(input, path, depth + 1);
|
|
399
|
+
return { type: [...procedurePath], value, ...($name !== undefined ? { name: $name } : {}) };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** This function changes a `$ref` object into a ref envelope. */
|
|
403
|
+
function toRef(ref: OutputRef): EnvelopeRef {
|
|
404
|
+
const refText = ref.$ref;
|
|
405
|
+
if (Object.keys(ref).length !== 1) {
|
|
406
|
+
throw new Error(
|
|
407
|
+
`The $ref object ${preview(ref)} has other keys. The envelope form keeps only the path: keep this program in the $proc form`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
const [name, ...fields] = refText.split(".");
|
|
411
|
+
if (name === LAST_NAME) {
|
|
412
|
+
throw new Error(
|
|
413
|
+
`The ref "${refText}" reads a step with the name "last". In the envelope form, ` +
|
|
414
|
+
`["names", "last"] reads the result of the step before: give the step a different name`
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
return { type: REF_TYPE, value: [NAMES_ROOT, name === NATIVE_LAST ? LAST_NAME : name!, ...fields] };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** This function gives true if a value of the `$proc` form is a call, a `$ref` or a `$literal`. */
|
|
421
|
+
function isNativeConstruct(value: unknown): boolean {
|
|
422
|
+
return isAnyProcedureRef(value) || isOutputRef(value) || isLiteral(value);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** This function gives true if the arrays and plain objects in a value hold a construct of the `$proc` form. */
|
|
426
|
+
function holdsNativeConstruct(value: unknown, path: Set<object>, depth: number): boolean {
|
|
427
|
+
const children = Array.isArray(value) ? value : isPlainObject(value) ? Object.values(value) : [];
|
|
428
|
+
if (children.length === 0) return false;
|
|
429
|
+
return within(value as object, path, depth, () =>
|
|
430
|
+
children.some((child) => isNativeConstruct(child) || holdsNativeConstruct(child, path, depth + 1))
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** This function changes a value of a `$proc` program into the envelope form. */
|
|
435
|
+
function toValue(value: unknown, path: Set<object>, depth: number): unknown {
|
|
436
|
+
if (value === null || typeof value !== "object") return value;
|
|
437
|
+
// The same order as hydration: a $literal, then a $ref, then a call
|
|
438
|
+
if (isLiteral(value)) return { type: LITERAL_TYPE, value: value.$literal };
|
|
439
|
+
if (isOutputRef(value)) return toRef(value);
|
|
440
|
+
if (isAnyProcedureRef(value)) return within(value, path, depth, () => toCall(value, path, depth));
|
|
441
|
+
if (Array.isArray(value)) {
|
|
442
|
+
return within(value, path, depth, () => value.map((item) => toValue(item, path, depth + 1)));
|
|
443
|
+
}
|
|
444
|
+
// Values that are not plain data (Date, Map, class instances) stay the same object
|
|
445
|
+
if (!isPlainObject(value)) return value;
|
|
446
|
+
if (envelopeKind(value) !== undefined || hasNativeKey(value)) {
|
|
447
|
+
// The envelope form reads this data in a different way: it becomes a literal. A literal
|
|
448
|
+
// stops the hydration of its value, so a value that holds a construct has no envelope form.
|
|
449
|
+
if (holdsNativeConstruct(value, path, depth)) {
|
|
450
|
+
throw new Error(
|
|
451
|
+
`The data ${preview(value)} has the shape of a construct of the envelope form, and it holds a ` +
|
|
452
|
+
`call, a $ref or a $literal. It has no envelope form: keep this program in the $proc form`
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
return { type: LITERAL_TYPE, value };
|
|
456
|
+
}
|
|
457
|
+
const converted = within(value, path, depth, () =>
|
|
458
|
+
Object.fromEntries(Object.entries(value).map(([key, item]) => [key, toValue(item, path, depth + 1)]))
|
|
459
|
+
);
|
|
460
|
+
// A $ref in the type of such data gives a type that is a ref, but not into the world
|
|
461
|
+
if (envelopeKind(converted) !== undefined) {
|
|
462
|
+
throw new Error(
|
|
463
|
+
`The data ${preview(value)} has a type that the envelope form reads as a ref. It has no envelope form: ` +
|
|
464
|
+
`keep this program in the $proc form`
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
return converted;
|
|
468
|
+
}
|
package/src/procedures/index.ts
CHANGED
|
@@ -287,6 +287,10 @@ export type {
|
|
|
287
287
|
RefScope,
|
|
288
288
|
} from "./ref.js";
|
|
289
289
|
|
|
290
|
+
// The envelope form of a program (coeval D6, D9 and D10, roadmap item M7)
|
|
291
|
+
export { isEnvelopeProgram, fromEnvelopeProgram, toEnvelopeProgram } from "./envelope.js";
|
|
292
|
+
export type { EnvelopeProgram, EnvelopeRef, EnvelopeLiteral, EnvelopePath } from "./envelope.js";
|
|
293
|
+
|
|
290
294
|
// Core language procedures (chain, parallel, if, etc.)
|
|
291
295
|
export {
|
|
292
296
|
coreProcedures,
|
|
@@ -147,7 +147,8 @@ export interface ProcedureServerOptions extends ServerOptions {
|
|
|
147
147
|
/**
|
|
148
148
|
* This function gives the principal of a request, or undefined to refuse it with
|
|
149
149
|
* `UNAUTHORIZED`. It runs after the token check. The capability function can read
|
|
150
|
-
* `request.principal`.
|
|
150
|
+
* `request.principal`. Like the request hook, it can run more than one time for one request or
|
|
151
|
+
* one task: `admits()` and `identify()` run it too (roadmap CLI-69).
|
|
151
152
|
*/
|
|
152
153
|
authenticate?: ((request: ServerRequest<unknown>) => Principal | undefined | Promise<Principal | undefined>) | undefined;
|
|
153
154
|
|
|
@@ -159,6 +160,9 @@ export interface ProcedureServerOptions extends ServerOptions {
|
|
|
159
160
|
* `request.principal`. Undefined or an error refuses the request with `UNAUTHORIZED`. A local
|
|
160
161
|
* request (`LOCAL_CREDENTIALS`) does not go through the hook. A server with a hook refuses a
|
|
161
162
|
* request without credentials, and the hook does not run for it (CLI-1).
|
|
163
|
+
*
|
|
164
|
+
* The hook can run more than one time for one request or one task: `admits()` and `identify()`
|
|
165
|
+
* run it too (CLI-69).
|
|
162
166
|
*/
|
|
163
167
|
principalOf?: PrincipalOf | undefined;
|
|
164
168
|
|
|
@@ -297,6 +301,11 @@ async function* releasedAtEnd<T>(items: AsyncIterable<T>, release: () => void):
|
|
|
297
301
|
}
|
|
298
302
|
}
|
|
299
303
|
|
|
304
|
+
/** The request of credentials without a call: `admits()` and `identify()` give it to the checks of each call. */
|
|
305
|
+
function credentialsRequest(id: string, credentials: Credentials): ServerRequest<unknown> {
|
|
306
|
+
return { id, method: { service: "", operation: "" }, payload: undefined, metadata: {}, credentials };
|
|
307
|
+
}
|
|
308
|
+
|
|
300
309
|
/** A signal that aborts when one of the signals aborts. */
|
|
301
310
|
function joinSignals(first: AbortSignal | undefined, second: AbortSignal): AbortSignal {
|
|
302
311
|
if (!first) return second;
|
|
@@ -992,8 +1001,19 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
|
|
|
992
1001
|
* connection.
|
|
993
1002
|
*/
|
|
994
1003
|
async admits(credentials: Credentials): Promise<boolean> {
|
|
995
|
-
|
|
996
|
-
|
|
1004
|
+
return (await this.admit(credentialsRequest("connection", credentials))) === undefined;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* This method gives the principal of a request with these credentials, before a call (CLI-69).
|
|
1009
|
+
* It uses the path of each call: the request hook, the token check and the authenticate
|
|
1010
|
+
* function. It gives undefined when the server refuses the credentials, or when the request has
|
|
1011
|
+
* no principal. The MCP face uses it for the fair share of its task store. Each use runs the
|
|
1012
|
+
* request hook and the authenticate function one more time.
|
|
1013
|
+
*/
|
|
1014
|
+
async identify(credentials: Credentials): Promise<Principal | undefined> {
|
|
1015
|
+
const request = credentialsRequest("identify", credentials);
|
|
1016
|
+
return (await this.admit(request)) === undefined ? request.principal : undefined;
|
|
997
1017
|
}
|
|
998
1018
|
|
|
999
1019
|
/**
|
package/src/server/types.ts
CHANGED
|
@@ -122,6 +122,10 @@ export interface RequestIdentity {
|
|
|
122
122
|
* The request hook of a server: it reads the credentials of a network request (a cookie, a header,
|
|
123
123
|
* the query) and gives the identity of the request. Undefined, or an error, refuses the request
|
|
124
124
|
* with `UNAUTHORIZED`. The hook runs before the token check and before `authenticate`.
|
|
125
|
+
*
|
|
126
|
+
* The hook can run more than one time for one request or one task. `admits()` runs it at a
|
|
127
|
+
* WebSocket upgrade, and `identify()` runs it before the MCP face makes a task (roadmap CLI-69).
|
|
128
|
+
* Thus a hook with a side effect, for example a count or a log line, sees each of these runs.
|
|
125
129
|
*/
|
|
126
130
|
export type PrincipalOf = (
|
|
127
131
|
credentials: Credentials,
|
|
@@ -169,6 +173,13 @@ export interface RequestHandler {
|
|
|
169
173
|
* trusts such a request only from a named origin. Absent: the handler reads no cookies.
|
|
170
174
|
*/
|
|
171
175
|
readonly ambientCredentials?: AmbientCredentials | undefined;
|
|
176
|
+
/**
|
|
177
|
+
* This function gives the principal of a request with these credentials, before a call
|
|
178
|
+
* (CLI-69). It uses the checks of each call, and gives undefined when they refuse the
|
|
179
|
+
* credentials or give no principal. `ProcedureServer` has it. Absent: a face knows the principal
|
|
180
|
+
* only in the call.
|
|
181
|
+
*/
|
|
182
|
+
identify?(credentials: Credentials): Promise<Principal | undefined>;
|
|
172
183
|
}
|
|
173
184
|
|
|
174
185
|
/**
|