@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/browser.d.ts +2 -2
  4. package/dist/browser.d.ts.map +1 -1
  5. package/dist/browser.js +2 -0
  6. package/dist/browser.js.map +1 -1
  7. package/dist/client/client.d.ts +10 -3
  8. package/dist/client/client.d.ts.map +1 -1
  9. package/dist/client/client.js +14 -2
  10. package/dist/client/client.js.map +1 -1
  11. package/dist/events/publisher.d.ts +9 -4
  12. package/dist/events/publisher.d.ts.map +1 -1
  13. package/dist/events/publisher.js +39 -11
  14. package/dist/events/publisher.js.map +1 -1
  15. package/dist/procedures/collection/partition.d.ts +21 -0
  16. package/dist/procedures/collection/partition.d.ts.map +1 -1
  17. package/dist/procedures/collection/partition.js +27 -0
  18. package/dist/procedures/collection/partition.js.map +1 -1
  19. package/dist/procedures/envelope.d.ts +119 -0
  20. package/dist/procedures/envelope.d.ts.map +1 -0
  21. package/dist/procedures/envelope.js +371 -0
  22. package/dist/procedures/envelope.js.map +1 -0
  23. package/dist/procedures/index.d.ts +2 -0
  24. package/dist/procedures/index.d.ts.map +1 -1
  25. package/dist/procedures/index.js +2 -0
  26. package/dist/procedures/index.js.map +1 -1
  27. package/dist/server/procedure-server.d.ts +13 -1
  28. package/dist/server/procedure-server.d.ts.map +1 -1
  29. package/dist/server/procedure-server.js +16 -2
  30. package/dist/server/procedure-server.js.map +1 -1
  31. package/dist/server/types.d.ts +11 -0
  32. package/dist/server/types.d.ts.map +1 -1
  33. package/dist/server/types.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/browser.ts +9 -0
  36. package/src/client/client.ts +17 -4
  37. package/src/events/publisher.ts +47 -15
  38. package/src/procedures/collection/partition.ts +32 -0
  39. package/src/procedures/envelope.ts +468 -0
  40. package/src/procedures/index.ts +4 -0
  41. package/src/server/procedure-server.ts +23 -3
  42. 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
+ }
@@ -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
- const request: ServerRequest<unknown> = { id: "connection", method: { service: "", operation: "" }, payload: undefined, metadata: {}, credentials };
996
- return (await this.admit(request)) === undefined;
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
  /**
@@ -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
  /**