@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.
- package/CHANGELOG.md +99 -68
- package/README.md +4 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/server/transport.d.ts +5 -0
- package/dist/adapters/http/server/transport.d.ts.map +1 -1
- package/dist/adapters/http/server/transport.js +7 -0
- package/dist/adapters/http/server/transport.js.map +1 -1
- package/dist/adapters/local/client/transport.d.ts.map +1 -1
- package/dist/adapters/local/client/transport.js +12 -1
- package/dist/adapters/local/client/transport.js.map +1 -1
- package/dist/adapters/websocket/client/node.d.ts.map +1 -1
- package/dist/adapters/websocket/client/node.js +9 -2
- package/dist/adapters/websocket/client/node.js.map +1 -1
- package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/server/transport.js +14 -5
- package/dist/adapters/websocket/server/transport.js.map +1 -1
- package/dist/browser.d.ts +2 -2
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +7 -3
- package/dist/browser.js.map +1 -1
- package/dist/client/client.d.ts +10 -1
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +46 -5
- package/dist/client/client.js.map +1 -1
- package/dist/client/errors/known-codes.d.ts +16 -0
- package/dist/client/errors/known-codes.d.ts.map +1 -0
- package/dist/client/errors/known-codes.js +20 -0
- package/dist/client/errors/known-codes.js.map +1 -0
- package/dist/client/validation/index.d.ts +1 -1
- package/dist/client/validation/index.js +1 -1
- package/dist/events/publisher.js +1 -1
- package/dist/events/publisher.js.map +1 -1
- package/dist/procedures/caller.d.ts +1 -1
- package/dist/procedures/caller.js +1 -1
- package/dist/procedures/collection/admission.d.ts +1 -1
- package/dist/procedures/collection/admission.js +1 -1
- package/dist/procedures/core/array.js +1 -1
- package/dist/procedures/core/array.js.map +1 -1
- package/dist/procedures/core/comparison.js +1 -1
- package/dist/procedures/core/comparison.js.map +1 -1
- package/dist/procedures/core/index.d.ts.map +1 -1
- package/dist/procedures/core/index.js +4 -2
- package/dist/procedures/core/index.js.map +1 -1
- package/dist/procedures/core/math.js +1 -1
- package/dist/procedures/core/math.js.map +1 -1
- package/dist/procedures/core/meta.js +1 -1
- package/dist/procedures/core/meta.js.map +1 -1
- package/dist/procedures/core/object.js +1 -1
- package/dist/procedures/core/object.js.map +1 -1
- package/dist/procedures/core/operands.d.ts.map +1 -1
- package/dist/procedures/core/operands.js +7 -2
- package/dist/procedures/core/operands.js.map +1 -1
- package/dist/procedures/core/outputs.d.ts +1 -1
- package/dist/procedures/core/outputs.d.ts.map +1 -1
- package/dist/procedures/core/outputs.js +1 -1
- package/dist/procedures/core/outputs.js.map +1 -1
- package/dist/procedures/core/string.js +1 -1
- package/dist/procedures/core/string.js.map +1 -1
- package/dist/procedures/core/type.js +1 -1
- package/dist/procedures/core/type.js.map +1 -1
- package/dist/procedures/define-procedure.d.ts.map +1 -1
- package/dist/procedures/define-procedure.js +28 -12
- package/dist/procedures/define-procedure.js.map +1 -1
- package/dist/procedures/index.d.ts +6 -5
- package/dist/procedures/index.d.ts.map +1 -1
- package/dist/procedures/index.js +6 -4
- package/dist/procedures/index.js.map +1 -1
- package/dist/procedures/invoke.d.ts.map +1 -1
- package/dist/procedures/invoke.js +14 -1
- package/dist/procedures/invoke.js.map +1 -1
- package/dist/procedures/module.d.ts +34 -0
- package/dist/procedures/module.d.ts.map +1 -1
- package/dist/procedures/module.js +90 -0
- package/dist/procedures/module.js.map +1 -1
- package/dist/procedures/ref.d.ts +10 -1
- package/dist/procedures/ref.d.ts.map +1 -1
- package/dist/procedures/ref.js +64 -0
- package/dist/procedures/ref.js.map +1 -1
- package/dist/procedures/registry.d.ts +64 -0
- package/dist/procedures/registry.d.ts.map +1 -1
- package/dist/procedures/registry.js +60 -0
- package/dist/procedures/registry.js.map +1 -1
- package/dist/procedures/segments.d.ts.map +1 -1
- package/dist/procedures/segments.js +13 -3
- package/dist/procedures/segments.js.map +1 -1
- package/dist/procedures/types.d.ts +6 -0
- package/dist/procedures/types.d.ts.map +1 -1
- package/dist/procedures/types.js.map +1 -1
- package/dist/server/error-policy.d.ts.map +1 -1
- package/dist/server/error-policy.js +2 -1
- package/dist/server/error-policy.js.map +1 -1
- package/dist/server/face-rules.d.ts +1 -1
- package/dist/server/face-rules.js +1 -1
- package/dist/server/host-procedures.d.ts.map +1 -1
- package/dist/server/host-procedures.js +12 -8
- package/dist/server/host-procedures.js.map +1 -1
- package/dist/server/procedure-server.d.ts +18 -5
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +28 -8
- package/dist/server/procedure-server.js.map +1 -1
- package/dist/server/server.d.ts +5 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +9 -1
- package/dist/server/server.js.map +1 -1
- package/dist/testing/transport-contract.d.ts +1 -1
- package/dist/testing/transport-contract.js +2 -2
- package/dist/testing/transport-contract.js.map +1 -1
- package/dist/tracing/traceparent.d.ts +6 -0
- package/dist/tracing/traceparent.d.ts.map +1 -1
- package/dist/tracing/traceparent.js +27 -6
- package/dist/tracing/traceparent.js.map +1 -1
- package/package.json +5 -4
- package/src/adapters/http/server/transport.ts +8 -0
- package/src/adapters/local/client/transport.ts +11 -1
- package/src/adapters/websocket/client/node.ts +11 -2
- package/src/adapters/websocket/server/transport.ts +17 -5
- package/src/browser.ts +11 -0
- package/src/client/client.ts +46 -4
- package/src/client/errors/known-codes.ts +22 -0
- package/src/client/validation/index.ts +1 -1
- package/src/events/publisher.ts +1 -1
- package/src/procedures/caller.ts +1 -1
- package/src/procedures/collection/admission.ts +1 -1
- package/src/procedures/core/array.ts +1 -1
- package/src/procedures/core/comparison.ts +1 -1
- package/src/procedures/core/index.ts +4 -1
- package/src/procedures/core/math.ts +1 -1
- package/src/procedures/core/meta.ts +1 -1
- package/src/procedures/core/object.ts +1 -1
- package/src/procedures/core/operands.ts +8 -1
- package/src/procedures/core/outputs.ts +1 -1
- package/src/procedures/core/string.ts +1 -1
- package/src/procedures/core/type.ts +1 -1
- package/src/procedures/define-procedure.ts +25 -10
- package/src/procedures/index.ts +7 -2
- package/src/procedures/invoke.ts +15 -1
- package/src/procedures/module.ts +113 -2
- package/src/procedures/ref.ts +58 -1
- package/src/procedures/registry.ts +99 -0
- package/src/procedures/segments.ts +12 -3
- package/src/procedures/types.ts +6 -0
- package/src/server/error-policy.ts +2 -1
- package/src/server/face-rules.ts +1 -1
- package/src/server/host-procedures.ts +12 -8
- package/src/server/procedure-server.ts +29 -8
- package/src/server/server.ts +10 -1
- package/src/testing/transport-contract.ts +2 -2
- 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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/procedures/index.ts
CHANGED
|
@@ -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,
|
package/src/procedures/invoke.ts
CHANGED
|
@@ -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
|
|
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
|
}
|
package/src/procedures/module.ts
CHANGED
|
@@ -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
|
package/src/procedures/ref.ts
CHANGED
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
* ```
|
|
58
58
|
*/
|
|
59
59
|
|
|
60
|
-
import type
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|
package/src/procedures/types.ts
CHANGED
|
@@ -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
|
-
|
|
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";
|
package/src/server/face-rules.ts
CHANGED
|
@@ -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: ["
|
|
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
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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()
|