@procedures/client 0.46.0 → 0.47.2

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 (148) hide show
  1. package/CHANGELOG.md +99 -68
  2. package/README.md +4 -4
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/adapters/http/server/transport.d.ts +5 -0
  5. package/dist/adapters/http/server/transport.d.ts.map +1 -1
  6. package/dist/adapters/http/server/transport.js +7 -0
  7. package/dist/adapters/http/server/transport.js.map +1 -1
  8. package/dist/adapters/local/client/transport.d.ts.map +1 -1
  9. package/dist/adapters/local/client/transport.js +12 -1
  10. package/dist/adapters/local/client/transport.js.map +1 -1
  11. package/dist/adapters/websocket/client/node.d.ts.map +1 -1
  12. package/dist/adapters/websocket/client/node.js +9 -2
  13. package/dist/adapters/websocket/client/node.js.map +1 -1
  14. package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
  15. package/dist/adapters/websocket/server/transport.js +14 -5
  16. package/dist/adapters/websocket/server/transport.js.map +1 -1
  17. package/dist/browser.d.ts +2 -2
  18. package/dist/browser.d.ts.map +1 -1
  19. package/dist/browser.js +7 -3
  20. package/dist/browser.js.map +1 -1
  21. package/dist/client/client.d.ts +10 -1
  22. package/dist/client/client.d.ts.map +1 -1
  23. package/dist/client/client.js +46 -5
  24. package/dist/client/client.js.map +1 -1
  25. package/dist/client/errors/known-codes.d.ts +16 -0
  26. package/dist/client/errors/known-codes.d.ts.map +1 -0
  27. package/dist/client/errors/known-codes.js +20 -0
  28. package/dist/client/errors/known-codes.js.map +1 -0
  29. package/dist/client/validation/index.d.ts +1 -1
  30. package/dist/client/validation/index.js +1 -1
  31. package/dist/events/publisher.js +1 -1
  32. package/dist/events/publisher.js.map +1 -1
  33. package/dist/procedures/caller.d.ts +1 -1
  34. package/dist/procedures/caller.js +1 -1
  35. package/dist/procedures/collection/admission.d.ts +1 -1
  36. package/dist/procedures/collection/admission.js +1 -1
  37. package/dist/procedures/core/array.js +1 -1
  38. package/dist/procedures/core/array.js.map +1 -1
  39. package/dist/procedures/core/comparison.js +1 -1
  40. package/dist/procedures/core/comparison.js.map +1 -1
  41. package/dist/procedures/core/index.d.ts.map +1 -1
  42. package/dist/procedures/core/index.js +4 -2
  43. package/dist/procedures/core/index.js.map +1 -1
  44. package/dist/procedures/core/math.js +1 -1
  45. package/dist/procedures/core/math.js.map +1 -1
  46. package/dist/procedures/core/meta.js +1 -1
  47. package/dist/procedures/core/meta.js.map +1 -1
  48. package/dist/procedures/core/object.js +1 -1
  49. package/dist/procedures/core/object.js.map +1 -1
  50. package/dist/procedures/core/operands.d.ts.map +1 -1
  51. package/dist/procedures/core/operands.js +7 -2
  52. package/dist/procedures/core/operands.js.map +1 -1
  53. package/dist/procedures/core/outputs.d.ts +1 -1
  54. package/dist/procedures/core/outputs.d.ts.map +1 -1
  55. package/dist/procedures/core/outputs.js +1 -1
  56. package/dist/procedures/core/outputs.js.map +1 -1
  57. package/dist/procedures/core/string.js +1 -1
  58. package/dist/procedures/core/string.js.map +1 -1
  59. package/dist/procedures/core/type.js +1 -1
  60. package/dist/procedures/core/type.js.map +1 -1
  61. package/dist/procedures/define-procedure.d.ts.map +1 -1
  62. package/dist/procedures/define-procedure.js +28 -12
  63. package/dist/procedures/define-procedure.js.map +1 -1
  64. package/dist/procedures/index.d.ts +6 -5
  65. package/dist/procedures/index.d.ts.map +1 -1
  66. package/dist/procedures/index.js +6 -4
  67. package/dist/procedures/index.js.map +1 -1
  68. package/dist/procedures/invoke.d.ts.map +1 -1
  69. package/dist/procedures/invoke.js +14 -1
  70. package/dist/procedures/invoke.js.map +1 -1
  71. package/dist/procedures/module.d.ts +34 -0
  72. package/dist/procedures/module.d.ts.map +1 -1
  73. package/dist/procedures/module.js +90 -0
  74. package/dist/procedures/module.js.map +1 -1
  75. package/dist/procedures/ref.d.ts +10 -1
  76. package/dist/procedures/ref.d.ts.map +1 -1
  77. package/dist/procedures/ref.js +64 -0
  78. package/dist/procedures/ref.js.map +1 -1
  79. package/dist/procedures/registry.d.ts +64 -0
  80. package/dist/procedures/registry.d.ts.map +1 -1
  81. package/dist/procedures/registry.js +60 -0
  82. package/dist/procedures/registry.js.map +1 -1
  83. package/dist/procedures/segments.d.ts.map +1 -1
  84. package/dist/procedures/segments.js +13 -3
  85. package/dist/procedures/segments.js.map +1 -1
  86. package/dist/procedures/types.d.ts +6 -0
  87. package/dist/procedures/types.d.ts.map +1 -1
  88. package/dist/procedures/types.js.map +1 -1
  89. package/dist/server/error-policy.d.ts.map +1 -1
  90. package/dist/server/error-policy.js +2 -1
  91. package/dist/server/error-policy.js.map +1 -1
  92. package/dist/server/face-rules.d.ts +1 -1
  93. package/dist/server/face-rules.js +1 -1
  94. package/dist/server/host-procedures.d.ts.map +1 -1
  95. package/dist/server/host-procedures.js +12 -8
  96. package/dist/server/host-procedures.js.map +1 -1
  97. package/dist/server/procedure-server.d.ts +18 -5
  98. package/dist/server/procedure-server.d.ts.map +1 -1
  99. package/dist/server/procedure-server.js +28 -8
  100. package/dist/server/procedure-server.js.map +1 -1
  101. package/dist/server/server.d.ts +5 -0
  102. package/dist/server/server.d.ts.map +1 -1
  103. package/dist/server/server.js +9 -1
  104. package/dist/server/server.js.map +1 -1
  105. package/dist/testing/transport-contract.d.ts +1 -1
  106. package/dist/testing/transport-contract.js +2 -2
  107. package/dist/testing/transport-contract.js.map +1 -1
  108. package/dist/tracing/traceparent.d.ts +6 -0
  109. package/dist/tracing/traceparent.d.ts.map +1 -1
  110. package/dist/tracing/traceparent.js +27 -6
  111. package/dist/tracing/traceparent.js.map +1 -1
  112. package/package.json +5 -4
  113. package/src/adapters/http/server/transport.ts +8 -0
  114. package/src/adapters/local/client/transport.ts +11 -1
  115. package/src/adapters/websocket/client/node.ts +11 -2
  116. package/src/adapters/websocket/server/transport.ts +17 -5
  117. package/src/browser.ts +11 -0
  118. package/src/client/client.ts +46 -4
  119. package/src/client/errors/known-codes.ts +22 -0
  120. package/src/client/validation/index.ts +1 -1
  121. package/src/events/publisher.ts +1 -1
  122. package/src/procedures/caller.ts +1 -1
  123. package/src/procedures/collection/admission.ts +1 -1
  124. package/src/procedures/core/array.ts +1 -1
  125. package/src/procedures/core/comparison.ts +1 -1
  126. package/src/procedures/core/index.ts +4 -1
  127. package/src/procedures/core/math.ts +1 -1
  128. package/src/procedures/core/meta.ts +1 -1
  129. package/src/procedures/core/object.ts +1 -1
  130. package/src/procedures/core/operands.ts +8 -1
  131. package/src/procedures/core/outputs.ts +1 -1
  132. package/src/procedures/core/string.ts +1 -1
  133. package/src/procedures/core/type.ts +1 -1
  134. package/src/procedures/define-procedure.ts +25 -10
  135. package/src/procedures/index.ts +7 -2
  136. package/src/procedures/invoke.ts +15 -1
  137. package/src/procedures/module.ts +113 -2
  138. package/src/procedures/ref.ts +58 -1
  139. package/src/procedures/registry.ts +99 -0
  140. package/src/procedures/segments.ts +12 -3
  141. package/src/procedures/types.ts +6 -0
  142. package/src/server/error-policy.ts +2 -1
  143. package/src/server/face-rules.ts +1 -1
  144. package/src/server/host-procedures.ts +12 -8
  145. package/src/server/procedure-server.ts +29 -8
  146. package/src/server/server.ts +10 -1
  147. package/src/testing/transport-contract.ts +2 -2
  148. package/src/tracing/traceparent.ts +28 -5
@@ -26,7 +26,7 @@
26
26
  */
27
27
 
28
28
  import { defineProcedure, validateProcedure } from "./define.js";
29
- import { PROCEDURE_REGISTRY, assertValidPath } from "./registry.js";
29
+ import { PROCEDURE_REGISTRY, assertValidPath, loadAllIn, resolveIn } from "./registry.js";
30
30
  import type {
31
31
  Procedure,
32
32
  ProcedurePath,
@@ -36,13 +36,14 @@ import type {
36
36
  ProcedureRegistryLike,
37
37
  Effects,
38
38
  } from "./types.js";
39
- import { z } from "zod";
39
+ import * as z from "zod";
40
40
  import { anySchema } from "./core/schemas.js";
41
41
  import { keyToPath, pathToKey } from "./types.js";
42
42
  import {
43
43
  createRefScope,
44
44
  isAnyProcedureRef,
45
45
  markDataDriven,
46
+ preloadRefPaths,
46
47
  rawInputRule,
47
48
  RUNS_REFS_TAG,
48
49
  } from "./ref.js";
@@ -211,7 +212,11 @@ function createAggregationHandler(
211
212
  const scope = createRefScope();
212
213
  scope.outputs.set("input", input);
213
214
  const executor = <TIn, TOut>(path: ProcedurePath, value: TIn): Promise<TOut> => ctx.client.call<TIn, TOut>(path, value);
214
- const options = { scope, rawInput: rawInputRule(ctx.registry ?? PROCEDURE_REGISTRY), onRef: ctx.onRef };
215
+ const registry = ctx.registry ?? PROCEDURE_REGISTRY;
216
+ // A lazy registry loads the paths of the body before the raw-input rule reads them (roadmap O93)
217
+ const preload = preloadRefPaths(registry, aggregation);
218
+ if (preload) await preload;
219
+ const options = { scope, rawInput: rawInputRule(registry), onRef: ctx.onRef };
215
220
  if (isAnyProcedureRef(aggregation)) {
216
221
  return runProgram(aggregation, { ...options, executor });
217
222
  }
@@ -327,7 +332,8 @@ export const defineProcedureProcedure: Procedure<
327
332
  }
328
333
 
329
334
  const pathKey = pathToKey(path);
330
- const existing = registry.get(path);
335
+ // A lazy registry loads the package of the path first: a procedure of a package is not new (roadmap O93)
336
+ const existing = await resolveIn(registry, path);
331
337
  // A procedure outside the expose rule of the host is not the caller's to replace (R17)
332
338
  if (existing && ctx?.expose && !ctx.expose(path)) {
333
339
  throw new InvocationError(
@@ -473,7 +479,8 @@ export const getProcedureProcedure: Procedure<
473
479
  },
474
480
  handler: async (input: ProcedureGetInput, ctx?: ProcedureContext) => {
475
481
  const path = typeof input.path === "string" ? keyToPath(input.path) : input.path;
476
- const procedure = (ctx?.registry ?? PROCEDURE_REGISTRY).get(path);
482
+ // A lazy registry loads the package of the path first (roadmap O93)
483
+ const procedure = await resolveIn(ctx?.registry ?? PROCEDURE_REGISTRY, path);
477
484
  // A procedure outside the expose rule of the host is not found (R17)
478
485
  if (!procedure || (ctx?.expose && !ctx.expose(procedure.path))) {
479
486
  return { found: false as const };
@@ -513,7 +520,10 @@ export const listProceduresProcedure: Procedure<
513
520
  output: "json",
514
521
  },
515
522
  handler: async (input: ProcedureListInput, ctx?: ProcedureContext) => {
516
- let all = (ctx?.registry ?? PROCEDURE_REGISTRY).getAll();
523
+ const registry = ctx?.registry ?? PROCEDURE_REGISTRY;
524
+ // A lazy registry loads each package first: the list has each procedure (roadmap O93)
525
+ await loadAllIn(registry);
526
+ let all = registry.getAll();
517
527
  // Only the procedures that the host exposes (R17)
518
528
  if (ctx?.expose) all = all.filter((procedure) => ctx.expose!(procedure.path));
519
529
  if (input.runtime) all = all.filter(isRuntimeDefined);
@@ -539,11 +549,15 @@ export const manifestProcedure: Procedure<{ namespace?: string | undefined }, Pr
539
549
  shorts: { namespace: "n" },
540
550
  output: "json",
541
551
  },
542
- handler: async (input: { namespace?: string | undefined }, ctx?: ProcedureContext) =>
543
- describeRegistry(ctx?.registry ?? PROCEDURE_REGISTRY, {
552
+ handler: async (input: { namespace?: string | undefined }, ctx?: ProcedureContext) => {
553
+ const registry = ctx?.registry ?? PROCEDURE_REGISTRY;
554
+ // A lazy registry loads each package first: the manifest has each procedure (roadmap O93)
555
+ await loadAllIn(registry);
556
+ return describeRegistry(registry, {
544
557
  ...(ctx?.expose ? { expose: ctx.expose } : {}),
545
558
  ...(input.namespace ? { include: (procedure: AnyProcedure) => procedure.path[0] === input.namespace } : {}),
546
- }),
559
+ });
560
+ },
547
561
  });
548
562
 
549
563
  /**
@@ -568,7 +582,8 @@ export const deleteProcedureProcedure: Procedure<
568
582
  if (ctx?.expose && !ctx.expose(input.path)) {
569
583
  return { deleted: false };
570
584
  }
571
- const existing = registry.get(input.path);
585
+ // A lazy registry loads the package of the path first (roadmap O93)
586
+ const existing = await resolveIn(registry, input.path);
572
587
  if (!existing) {
573
588
  return { deleted: false };
574
589
  }
@@ -114,7 +114,8 @@ export { NARROWING_METADATA_KEY, NARROWING_HEADER, narrowingOf, narrowWithin, ad
114
114
  export type { NarrowingNote, WireNarrowing, NarrowedCapability } from "./narrowing.js";
115
115
 
116
116
  // Registry
117
- export { ProcedureRegistry, RegistryError, PROCEDURE_REGISTRY, assertValidPath } from "./registry.js";
117
+ export { ProcedureRegistry, RegistryError, PROCEDURE_REGISTRY, assertValidPath, resolveIn, loadAllIn } from "./registry.js";
118
+ export type { RegistryResolver, ProcedureRegistryOptions, ResolvingRegistry } from "./registry.js";
118
119
 
119
120
  // Definition helpers
120
121
  export {
@@ -223,9 +224,10 @@ export {
223
224
  moduleClosure,
224
225
  registerModules,
225
226
  createRegistry,
227
+ createLazyRegistry,
226
228
  registerClientProcedures,
227
229
  } from "./module.js";
228
- export type { ModuleInput, PackageDefinition } from "./module.js";
230
+ export type { ModuleInput, PackageDefinition, LazyRegistryOptions } from "./module.js";
229
231
  // One call for a registry of some modules, `invokePath` and `outputValue`
230
232
  export { callerOf } from "./caller.js";
231
233
  export type { Caller, CallerOptions } from "./caller.js";
@@ -262,6 +264,7 @@ export {
262
264
  RAW_INPUT_TAG,
263
265
  takesRawInput,
264
266
  rawInputRule,
267
+ preloadRefPaths,
265
268
  MAX_DATA_DEPTH,
266
269
  LITERAL_KEY,
267
270
  isLiteral,
@@ -338,6 +341,8 @@ export {
338
341
  // Every core procedure (math, comparison, string, type, object, array, meta too). Not registered by default.
339
342
  allCoreProcedures,
340
343
  allCoreModules,
344
+ // Only the math procedures: a bundle that imports only these gets no other core procedure (roadmap O95)
345
+ mathProcedures,
341
346
  chainProcedure,
342
347
  traceProcedure,
343
348
  type TraceRow,
@@ -440,6 +440,13 @@ export async function invokeProcedure<TOutput = unknown>(
440
440
  return dispatch(0, invocation, options) as Promise<ProcedureOutput<TOutput>>;
441
441
  }
442
442
 
443
+ /** True when the declared effects of a procedure read or write the registry. */
444
+ function usesRegistry(procedure: AnyProcedure): boolean {
445
+ const effects = (procedure.metadata as { effects?: { reads?: unknown; writes?: unknown } } | undefined)?.effects;
446
+ const names = (list: unknown): boolean => Array.isArray(list) && list.includes("registry");
447
+ return names(effects?.reads) || names(effects?.writes);
448
+ }
449
+
443
450
  /** The end of the chain: the validation, the context, the handler and the output. */
444
451
  async function runInvocation<TOutput>(invocation: Invocation, options: InvokeOptions): Promise<ProcedureOutput<TOutput>> {
445
452
  const { procedure, path, input } = invocation;
@@ -472,6 +479,11 @@ async function runInvocation<TOutput>(invocation: Invocation, options: InvokeOpt
472
479
  data = parsed.value === input ? input : withScope(parsed.value, scopeOf(input));
473
480
  }
474
481
 
482
+ // A procedure that declares a read or a write of the registry sees each procedure of a lazy
483
+ // registry (roadmap O93). An eager registry does not wait here.
484
+ const registry = options.registry;
485
+ if (registry?.lazy && usesRegistry(procedure)) await registry.loadAll();
486
+
475
487
  // The handler starts here, before the first await (the site's trace depends on it)
476
488
  const result: unknown = await handler(data, createProcedureContext(procedure, { ...options, capability: invocation.capability }, invocation));
477
489
  // The output filter examines only what leaves the host
@@ -508,7 +520,9 @@ export async function invokePath<TOutput = unknown>(
508
520
  input: unknown,
509
521
  options: InvokeOptions = {},
510
522
  ): Promise<ProcedureOutput<TOutput>> {
511
- const procedure = (options.registry ?? PROCEDURE_REGISTRY).get(path);
523
+ const registry = options.registry ?? PROCEDURE_REGISTRY;
524
+ // A miss of a lazy registry loads the packages of the path (roadmap O93). A hit does not wait.
525
+ const procedure = registry.get(path) ?? (registry.lazy ? await registry.resolve(path) : undefined);
512
526
  if (!procedure) {
513
527
  throw new InvocationError("NOT_FOUND", `Procedure not found: ${pathToKey(path)}`, path);
514
528
  }
@@ -18,10 +18,10 @@
18
18
  * rule of a server all use the registry of the invocation (`ctx.registry`).
19
19
  */
20
20
 
21
- import { PROCEDURE_REGISTRY, ProcedureRegistry, RegistryError } from "./registry.js";
21
+ import { PROCEDURE_REGISTRY, ProcedureRegistry, RegistryError, type RegistryResolver } from "./registry.js";
22
22
  import { coreProcedures } from "./core/index.js";
23
23
  import { metaProcedures as definitionProcedures } from "./define-procedure.js";
24
- import { pathToKey, type AnyProcedure, type ProcedureModule, type RegistrationOptions } from "./types.js";
24
+ import { pathToKey, type AnyProcedure, type ProcedureModule, type ProcedurePath, type RegistrationOptions } from "./types.js";
25
25
 
26
26
  /**
27
27
  * The procedures of `@procedures/client`: the control flow (`client.chain`, `client.map` and the
@@ -139,6 +139,117 @@ export function createRegistry(...modules: ModuleInput[]): ProcedureRegistry {
139
139
  return registerModules(new ProcedureRegistry(), [clientModule, ...modules]);
140
140
  }
141
141
 
142
+ /** The options of `createLazyRegistry()` (roadmap O93). */
143
+ export interface LazyRegistryOptions {
144
+ /** The modules that the registry registers at once, after `clientModule`: the core procedures. */
145
+ readonly modules?: readonly ModuleInput[] | undefined;
146
+ /**
147
+ * The index: the key of each path (`pathToKey()`) and the names of the packages that give it.
148
+ * Two packages can give one path.
149
+ */
150
+ readonly index: Readonly<Record<string, readonly string[]>>;
151
+ /** This function gives the module of a package, or undefined for a package without a build. */
152
+ readonly load: (name: string) => Promise<ProcedureModule | undefined>;
153
+ /**
154
+ * The names of all packages, in the order of the eager load. `loadAll()` loads them in this
155
+ * order. The default is the names of the index.
156
+ */
157
+ readonly packages?: readonly string[] | undefined;
158
+ /** This function gets each warning. The default is `console.warn`. */
159
+ readonly warn?: ((message: string) => void) | undefined;
160
+ }
161
+
162
+ /** The first line of the message of an error. */
163
+ function firstLine(error: unknown): string {
164
+ return (error instanceof Error ? error.message : String(error)).split("\n")[0] ?? "";
165
+ }
166
+
167
+ /**
168
+ * This function makes a lazy registry (roadmap O93). The registry has the procedures of
169
+ * `clientModule` and of `options.modules` at once. A miss of `resolve()` loads only the packages
170
+ * that the index names for the path.
171
+ *
172
+ * - Concurrent resolves of one package start one load.
173
+ * - A path that is not in the index loads each package, with one warning for each path.
174
+ * - A package that did not load gives a warning. The resolve of a path that is still missing
175
+ * then rejects with a `RegistryError`, with the load error as its `cause`. A later resolve tries
176
+ * that package again.
177
+ * - The registration goes through `registerModules()`: the modules that a module requires come
178
+ * with it, and the same handler from two modules registers one time.
179
+ * - `loadAll()` loads each package in the order of `options.packages`.
180
+ */
181
+ export function createLazyRegistry(options: LazyRegistryOptions): ProcedureRegistry {
182
+ const warn = options.warn ?? ((message: string): void => console.warn(message));
183
+ const packages = options.packages ?? [...new Set(Object.values(options.index).flat())];
184
+ const loaded = new Set<string>();
185
+ const inFlight = new Map<string, Promise<void>>();
186
+ const warnedPaths = new Set<string>();
187
+
188
+ function loadPackage(name: string): Promise<void> {
189
+ if (loaded.has(name)) return Promise.resolve();
190
+ const current = inFlight.get(name);
191
+ if (current) return current;
192
+ const started = (async (): Promise<void> => {
193
+ const module = await options.load(name);
194
+ if (module) registerModules(registry, [module]);
195
+ })();
196
+ inFlight.set(name, started);
197
+ // A failure is not recorded as a load: the next resolve tries the package again
198
+ started.then(
199
+ () => {
200
+ loaded.add(name);
201
+ inFlight.delete(name);
202
+ },
203
+ (error: unknown) => {
204
+ inFlight.delete(name);
205
+ warn(`the procedures of ${name} did not load: ${firstLine(error)}`);
206
+ },
207
+ );
208
+ return started;
209
+ }
210
+
211
+ const resolver: RegistryResolver = {
212
+ async resolve(path: ProcedurePath): Promise<void> {
213
+ const key = pathToKey(path);
214
+ const named = Object.hasOwn(options.index, key) ? options.index[key] : undefined;
215
+ let targets: readonly string[];
216
+ if (named !== undefined && named.length > 0) {
217
+ targets = named;
218
+ } else {
219
+ if (!warnedPaths.has(key)) {
220
+ warnedPaths.add(key);
221
+ warn(`the index has no package for ${key}: each package loads`);
222
+ }
223
+ targets = packages;
224
+ }
225
+ let failure: { name: string; error: unknown } | undefined;
226
+ // One package after the other, in the order of the index: the order of the eager load
227
+ for (const name of targets) {
228
+ try {
229
+ await loadPackage(name);
230
+ } catch (error) {
231
+ failure ??= { name, error };
232
+ }
233
+ }
234
+ if (failure === undefined || registry.has(path)) return;
235
+ const cause = failure.error instanceof Error ? failure.error : new Error(String(failure.error));
236
+ throw new RegistryError(`The procedure ${key} did not load: the package ${failure.name} did not load: ${firstLine(cause)}`, path, cause);
237
+ },
238
+ async loadAll(): Promise<void> {
239
+ for (const name of packages) {
240
+ try {
241
+ await loadPackage(name);
242
+ } catch {
243
+ // loadPackage() gave the warning. The other packages still load, as in the eager load.
244
+ }
245
+ }
246
+ },
247
+ };
248
+
249
+ const registry = new ProcedureRegistry({ resolver });
250
+ return registerModules(registry, [clientModule, ...(options.modules ?? [])]);
251
+ }
252
+
142
253
  /**
143
254
  * This function registers the procedures of `clientModule` in a registry: the global registry by
144
255
  * default. A procedure that the registry has already stays. The `./register` entry of the package
@@ -57,7 +57,8 @@
57
57
  * ```
58
58
  */
59
59
 
60
- import type { ProcedurePath } from "./types.js";
60
+ import { pathToKey, type ProcedurePath } from "./types.js";
61
+ import type { ResolvingRegistry } from "./registry.js";
61
62
 
62
63
  // =============================================================================
63
64
  // Procedure Reference Constants
@@ -854,6 +855,62 @@ export function rawInputRule(
854
855
 
855
856
  const RAW_INPUT_RULES = new WeakMap<object, (path: ProcedurePath) => boolean>();
856
857
 
858
+ /** The `$proc` paths of a value that the registry does not have yet, by key. */
859
+ function missingRefPaths(registry: ResolvingRegistry, value: unknown): ProcedurePath[] {
860
+ const missing = new Map<string, ProcedurePath>();
861
+ const seen = new Set<object>();
862
+ const visit = (node: unknown): void => {
863
+ if (node === null || typeof node !== "object" || seen.has(node)) return;
864
+ seen.add(node);
865
+ if (isLiteral(node)) return;
866
+ if (Array.isArray(node)) {
867
+ for (const item of node) visit(item);
868
+ return;
869
+ }
870
+ if (isAnyProcedureRef(node)) {
871
+ try {
872
+ const ref = normalizeRef(node);
873
+ const path: unknown = ref.path;
874
+ if (Array.isArray(path) && path.length > 0 && path.every((segment) => typeof segment === "string") && registry.get(path) === undefined) {
875
+ missing.set(pathToKey(path), path);
876
+ }
877
+ visit(ref.input);
878
+ } catch {
879
+ // The run reports a ref that is not correct
880
+ }
881
+ return;
882
+ }
883
+ if (!isPlainObject(node)) return;
884
+ for (const field of Object.values(node)) visit(field);
885
+ };
886
+ visit(value);
887
+ return [...missing.values()];
888
+ }
889
+
890
+ /**
891
+ * This function loads the procedures of the `$proc` paths of a value into a lazy registry (roadmap
892
+ * O93). A run starts it before it reads the raw-input rule, because the rule is sync and reads only
893
+ * the procedures that have loaded. The function gives undefined, with no wait, for an eager
894
+ * registry and for a value whose paths the registry has. A package that does not load does not
895
+ * stop the run: the call of its path reports the error.
896
+ */
897
+ export function preloadRefPaths(registry: ResolvingRegistry | undefined, value: unknown): Promise<void> | undefined {
898
+ if (registry?.lazy !== true || registry.resolve === undefined) return undefined;
899
+ const missing = missingRefPaths(registry, value);
900
+ if (missing.length === 0) return undefined;
901
+ const resolve = registry.resolve.bind(registry);
902
+ return (async (): Promise<void> => {
903
+ // One path after the other: the order of the registrations does not depend on the time of each load
904
+ for (const path of missing) {
905
+ try {
906
+ await resolve(path);
907
+ } catch {
908
+ // The call of the path reports the load error
909
+ }
910
+ }
911
+ })();
912
+ }
913
+
857
914
  // =============================================================================
858
915
  // Template Extraction
859
916
  // =============================================================================
@@ -76,6 +76,60 @@ function samePath(a: ProcedurePath, b: ProcedurePath): boolean {
76
76
  return a.length === b.length && a.every((segment, index) => segment === b[index]);
77
77
  }
78
78
 
79
+ // =============================================================================
80
+ // Lazy Resolution (roadmap O93)
81
+ // =============================================================================
82
+
83
+ /**
84
+ * The resolver of a lazy registry: it loads procedures into the registry on demand.
85
+ * `createLazyRegistry()` gives one. An eager registry has no resolver.
86
+ */
87
+ export interface RegistryResolver {
88
+ /**
89
+ * This method loads the packages that give the path. It rejects when a package did not load.
90
+ * A later call tries that package again.
91
+ */
92
+ resolve(path: ProcedurePath): Promise<void>;
93
+ /** This method loads each package. A package that did not load gives a warning, not a rejection. */
94
+ loadAll(): Promise<void>;
95
+ }
96
+
97
+ /** The options of `new ProcedureRegistry()`. */
98
+ export interface ProcedureRegistryOptions {
99
+ /**
100
+ * The resolver of a lazy registry. Without it, the registry is eager: `resolve()` gives what
101
+ * `get()` gives, and `loadAll()` does nothing.
102
+ */
103
+ resolver?: RegistryResolver | undefined;
104
+ }
105
+
106
+ /** The reads of a registry that `resolveIn()` and `loadAllIn()` use. A lazy registry has all of them. */
107
+ export interface ResolvingRegistry {
108
+ get(path: ProcedurePath): AnyProcedure | undefined;
109
+ readonly lazy?: boolean;
110
+ resolve?(path: ProcedurePath): Promise<AnyProcedure | undefined>;
111
+ loadAll?(): Promise<void>;
112
+ }
113
+
114
+ /**
115
+ * This function gives the procedure at a path of a registry. On a miss, a lazy registry loads the
116
+ * packages of the path. An eager registry gives what `get()` gives.
117
+ */
118
+ export async function resolveIn(registry: ResolvingRegistry, path: ProcedurePath): Promise<AnyProcedure | undefined> {
119
+ const found = registry.get(path);
120
+ if (found !== undefined || registry.lazy !== true || registry.resolve === undefined) return found;
121
+ return registry.resolve(path);
122
+ }
123
+
124
+ /**
125
+ * This function loads each package of a lazy registry. A procedure that enumerates its registry
126
+ * (`procedure.list`, `procedure.manifest`, `mcp.listTools`) starts it first. On an eager
127
+ * registry, it does nothing.
128
+ */
129
+ export async function loadAllIn(registry: Pick<ResolvingRegistry, "lazy" | "loadAll">): Promise<void> {
130
+ if (registry.lazy === true && registry.loadAll !== undefined) await registry.loadAll();
131
+ }
132
+
79
133
  // =============================================================================
80
134
  // Procedure Registry
81
135
  // =============================================================================
@@ -113,6 +167,17 @@ export class ProcedureRegistry {
113
167
  /** Registered module names for debugging */
114
168
  private readonly registeredModules = new Set<string>();
115
169
 
170
+ /** The resolver of a lazy registry, or undefined for an eager registry (roadmap O93) */
171
+ private readonly resolver: RegistryResolver | undefined;
172
+
173
+ /**
174
+ * This constructor makes an eager registry. With `options.resolver`, the registry is lazy: a
175
+ * miss of `resolve()` loads the packages of the path.
176
+ */
177
+ constructor(options: ProcedureRegistryOptions = {}) {
178
+ this.resolver = options.resolver;
179
+ }
180
+
116
181
  // ---------------------------------------------------------------------------
117
182
  // Registration
118
183
  // ---------------------------------------------------------------------------
@@ -278,6 +343,40 @@ export class ProcedureRegistry {
278
343
  return this.procedures.size;
279
344
  }
280
345
 
346
+ // ---------------------------------------------------------------------------
347
+ // Lazy Resolution (roadmap O93)
348
+ // ---------------------------------------------------------------------------
349
+
350
+ /**
351
+ * This property is true for a lazy registry. The sync reads (`get`, `has`, `getAll`,
352
+ * `getByPrefix`, `size`) of a lazy registry give only the procedures that have loaded.
353
+ */
354
+ get lazy(): boolean {
355
+ return this.resolver !== undefined;
356
+ }
357
+
358
+ /**
359
+ * This method gives the procedure at a path. A registered path gives its procedure at once. On
360
+ * a miss, a lazy registry loads the packages of the path first. An eager registry gives undefined.
361
+ *
362
+ * @throws RegistryError when the path is still missing and a package of the path did not load.
363
+ * The load error is the `cause`.
364
+ */
365
+ async resolve(path: ProcedurePath): Promise<AnyProcedure | undefined> {
366
+ const found = this.get(path);
367
+ if (found || !this.resolver) return found;
368
+ await this.resolver.resolve(path);
369
+ return this.get(path);
370
+ }
371
+
372
+ /**
373
+ * This method loads each package of a lazy registry. After it, the sync reads give each
374
+ * procedure. On an eager registry, it does nothing.
375
+ */
376
+ async loadAll(): Promise<void> {
377
+ if (this.resolver) await this.resolver.loadAll();
378
+ }
379
+
281
380
  /**
282
381
  * Get all registered module names.
283
382
  */
@@ -24,7 +24,8 @@
24
24
  * ```
25
25
  */
26
26
  export function encodeSegment(segment: string): string {
27
- return segment.replace(/[%.]/g, (char) => (char === "%" ? "%25" : "%2E"));
27
+ // Almost each segment has no `.` and no `%`: a test costs less than a replace with a callback (roadmap O90)
28
+ return /[%.]/.test(segment) ? segment.replace(/[%.]/g, (char) => (char === "%" ? "%25" : "%2E")) : segment;
28
29
  }
29
30
 
30
31
  /**
@@ -39,7 +40,8 @@ export function encodeSegment(segment: string): string {
39
40
  * ```
40
41
  */
41
42
  export function decodeSegment(segment: string): string {
42
- return segment.replace(/%(25|2[Ee])/g, (_sequence, code: string) => (code === "25" ? "%" : "."));
43
+ // Almost each segment has no `%`: a test costs less than a replace with a callback (roadmap O90)
44
+ return segment.includes("%") ? segment.replace(/%(25|2[Ee])/g, (_sequence, code: string) => (code === "25" ? "%" : ".")) : segment;
43
45
  }
44
46
 
45
47
  /**
@@ -56,7 +58,14 @@ export function decodeSegment(segment: string): string {
56
58
  export function segmentsToTopic(segments: readonly string[]): string {
57
59
  // A segment that is not a string becomes text as with join(): null and undefined give "" (a
58
60
  // 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(".");
61
+ // The loop makes no array for each key: a key is made for each step of a call (roadmap O90)
62
+ let topic = "";
63
+ for (let index = 0; index < segments.length; index++) {
64
+ const segment: unknown = segments[index];
65
+ if (index > 0) topic += ".";
66
+ topic += encodeSegment(segment === null || segment === undefined ? "" : String(segment));
67
+ }
68
+ return topic;
60
69
  }
61
70
 
62
71
  /**
@@ -442,6 +442,12 @@ export interface ProcedureRegistryLike {
442
442
  getAll(): AnyProcedure[];
443
443
  register(procedure: AnyProcedure, options?: RegistrationOptions): void;
444
444
  unregister(path: ProcedurePath): boolean;
445
+ /** This property is true for a lazy registry (roadmap O93). Its sync reads give only the procedures that have loaded. */
446
+ readonly lazy?: boolean;
447
+ /** This method gives the procedure at a path. A lazy registry loads the packages of the path on a miss. */
448
+ resolve?(path: ProcedurePath): Promise<AnyProcedure | undefined>;
449
+ /** This method loads each package of a lazy registry. A procedure that enumerates the registry starts it first. */
450
+ loadAll?(): Promise<void>;
445
451
  }
446
452
 
447
453
  /**
@@ -22,7 +22,8 @@
22
22
 
23
23
  import type { ProcedurePath } from "../procedures/types.js";
24
24
  import { closableUnread } from "../procedures/invoke.js";
25
- import { isKnownErrorCode } from "../client/errors/registry.js";
25
+ // The codes only: the texts of ERROR_REGISTRY stay out of a bundle of the server (roadmap O95)
26
+ import { isKnownCode as isKnownErrorCode } from "../client/errors/known-codes.js";
26
27
  import { errorCodeOf } from "../protocol.js";
27
28
  import { errorResponse, realErrorOf } from "./error-response.js";
28
29
  import { methodToPath } from "./method.js";
@@ -73,7 +73,7 @@ function exposeRule(expose: NonNullable<FaceRulesOptions["expose"]>): (path: Pro
73
73
  *
74
74
  * @example
75
75
  * ```typescript
76
- * const rules = faceRules({ narrowing: { effects: { writes: [] } }, expose: ["graphapi.*"] }, policy, "read");
76
+ * const rules = faceRules({ narrowing: { effects: { writes: [] } }, expose: ["api.*"] }, policy, "read");
77
77
  * const handler = { ...inner, handle: (request) => inner.handle(rules.apply(request)) };
78
78
  * ```
79
79
  */
@@ -9,7 +9,7 @@
9
9
  * checks that have no token.
10
10
  */
11
11
 
12
- import { z } from "zod";
12
+ import * as z from "zod";
13
13
  import { createProcedure } from "../procedures/define.js";
14
14
  import { PURE_EFFECTS } from "../procedures/effects.js";
15
15
  import type { AnyProcedure } from "../procedures/types.js";
@@ -69,13 +69,17 @@ export function hostProcedures(source: HostSource): AnyProcedure[] {
69
69
  .input(z.object({}))
70
70
  .output(healthOutput)
71
71
  .meta({ description: "The health of the host: its state, its uptime and its calls in flight", effects: PURE_EFFECTS })
72
- .handler(async () => ({
73
- status: source.server.stopping ? ("stopping" as const) : ("ok" as const),
74
- uptimeMs: Date.now() - source.startedAt,
75
- procedures: source.server.procedureCount,
76
- callsInFlight: source.server.inFlight(),
77
- peerId: source.id,
78
- }))
72
+ .handler(async () => {
73
+ // A lazy registry loads each package first: the count has each procedure (roadmap O93)
74
+ await source.server.loadAll();
75
+ return {
76
+ status: source.server.stopping ? ("stopping" as const) : ("ok" as const),
77
+ uptimeMs: Date.now() - source.startedAt,
78
+ procedures: source.server.procedureCount,
79
+ callsInFlight: source.server.inFlight(),
80
+ peerId: source.id,
81
+ };
82
+ })
79
83
  .build();
80
84
 
81
85
  const info = createProcedure()