@volter/world-core 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +29 -0
- package/app-route.cjs +154 -0
- package/app-route.d.cts +7 -0
- package/attach.cjs +80 -0
- package/dist/app-route.cjs +154 -0
- package/dist/app-route.d.cts +7 -0
- package/dist/attach.cjs +80 -0
- package/dist/generated/pack-facts.json +4306 -0
- package/dist/inject.cjs +1097 -0
- package/dist/network-policy.cjs +92 -0
- package/dist/network-policy.d.cts +10 -0
- package/dist/src/actions.d.ts +276 -0
- package/dist/src/actions.js +436 -0
- package/dist/src/ancestry.d.ts +22 -0
- package/dist/src/ancestry.js +238 -0
- package/dist/src/args.d.ts +3 -0
- package/dist/src/args.js +12 -0
- package/dist/src/blob-store.d.ts +55 -0
- package/dist/src/blob-store.js +186 -0
- package/dist/src/brand-tokens.d.ts +2 -0
- package/dist/src/brand-tokens.js +17 -0
- package/dist/src/changeset.d.ts +431 -0
- package/dist/src/changeset.js +0 -0
- package/dist/src/client-bundle.d.ts +1 -0
- package/dist/src/client-bundle.js +28 -0
- package/dist/src/credential.d.ts +38 -0
- package/dist/src/credential.js +114 -0
- package/dist/src/derived-core.d.ts +452 -0
- package/dist/src/derived-core.js +782 -0
- package/dist/src/derived.d.ts +84 -0
- package/dist/src/derived.js +122 -0
- package/dist/src/emit.d.ts +106 -0
- package/dist/src/emit.js +157 -0
- package/dist/src/executor.d.ts +120 -0
- package/dist/src/executor.js +387 -0
- package/dist/src/file-response.d.ts +3 -0
- package/dist/src/file-response.js +22 -0
- package/dist/src/fork.d.ts +26 -0
- package/dist/src/fork.js +68 -0
- package/dist/src/git/history.d.ts +36 -0
- package/dist/src/git/history.js +298 -0
- package/dist/src/git/index.d.ts +6 -0
- package/dist/src/git/index.js +6 -0
- package/dist/src/git/inflate.d.ts +11 -0
- package/dist/src/git/inflate.js +194 -0
- package/dist/src/git/objects.d.ts +64 -0
- package/dist/src/git/objects.js +161 -0
- package/dist/src/git/pack.d.ts +14 -0
- package/dist/src/git/pack.js +199 -0
- package/dist/src/git/refs.d.ts +19 -0
- package/dist/src/git/refs.js +35 -0
- package/dist/src/git/smart-http.d.ts +45 -0
- package/dist/src/git/smart-http.js +223 -0
- package/dist/src/hash.d.ts +38 -0
- package/dist/src/hash.js +48 -0
- package/dist/src/head.d.ts +140 -0
- package/dist/src/head.js +313 -0
- package/dist/src/history.d.ts +76 -0
- package/dist/src/history.js +322 -0
- package/dist/src/index.d.ts +73 -0
- package/dist/src/index.js +98 -0
- package/dist/src/lifecycle.d.ts +1 -0
- package/dist/src/lifecycle.js +8 -0
- package/dist/src/log.d.ts +254 -0
- package/dist/src/log.js +801 -0
- package/dist/src/mirror-shell.d.ts +2 -0
- package/dist/src/mirror-shell.js +13 -0
- package/dist/src/observe.d.ts +49 -0
- package/dist/src/observe.js +148 -0
- package/dist/src/pack-assets.d.ts +30 -0
- package/dist/src/pack-assets.js +88 -0
- package/dist/src/packRegistry.d.ts +374 -0
- package/dist/src/packRegistry.js +142 -0
- package/dist/src/placeholder-remote.d.ts +22 -0
- package/dist/src/placeholder-remote.js +86 -0
- package/dist/src/proxy.d.ts +25 -0
- package/dist/src/proxy.js +155 -0
- package/dist/src/rateBudget.d.ts +367 -0
- package/dist/src/rateBudget.js +925 -0
- package/dist/src/references.d.ts +18 -0
- package/dist/src/references.js +27 -0
- package/dist/src/remote-execute.d.ts +22 -0
- package/dist/src/remote-execute.js +1 -0
- package/dist/src/resource-blob.d.ts +10 -0
- package/dist/src/resource-blob.js +56 -0
- package/dist/src/scenario.d.ts +197 -0
- package/dist/src/scenario.js +425 -0
- package/dist/src/schemas.d.ts +78 -0
- package/dist/src/schemas.js +50 -0
- package/dist/src/serve-http.d.ts +48 -0
- package/dist/src/serve-http.js +340 -0
- package/dist/src/serve.d.ts +147 -0
- package/dist/src/serve.js +507 -0
- package/dist/src/shared-blob-index.d.ts +4 -0
- package/dist/src/shared-blob-index.js +126 -0
- package/dist/src/state-system.d.ts +70 -0
- package/dist/src/state-system.js +90 -0
- package/dist/src/storage.d.ts +101 -0
- package/dist/src/storage.js +337 -0
- package/dist/src/twin-fetch.d.ts +64 -0
- package/dist/src/twin-fetch.js +91 -0
- package/dist/src/types.d.ts +40 -0
- package/dist/src/types.js +1 -0
- package/dist/src/v1-removed.d.ts +159 -0
- package/dist/src/v1-removed.js +124 -0
- package/dist/src/volter-home.d.ts +5 -0
- package/dist/src/volter-home.js +10 -0
- package/dist/src/world-clock.d.ts +4 -0
- package/dist/src/world-clock.js +32 -0
- package/dist/src/world-env.d.ts +3 -0
- package/dist/src/world-env.js +22 -0
- package/dist/src/world-store-sql.d.ts +27 -0
- package/dist/src/world-store-sql.js +86 -0
- package/dist/src/world-store.d.ts +168 -0
- package/dist/src/world-store.js +475 -0
- package/dist/src/worldConfig.d.ts +9 -0
- package/dist/src/worldConfig.js +17 -0
- package/dist/stream-bridge.cjs +80 -0
- package/dist/vendor-hosts.cjs +200 -0
- package/generated/pack-facts.json +4306 -0
- package/inject.cjs +1097 -0
- package/network-policy.cjs +92 -0
- package/network-policy.d.cts +10 -0
- package/package.json +103 -0
- package/src/actions.ts +564 -0
- package/src/ancestry.ts +213 -0
- package/src/args.ts +14 -0
- package/src/blob-store.ts +185 -0
- package/src/brand-tokens.ts +17 -0
- package/src/changeset.ts +1032 -0
- package/src/client-bundle.ts +29 -0
- package/src/credential.ts +140 -0
- package/src/derived-core.ts +1004 -0
- package/src/derived.ts +176 -0
- package/src/emit.ts +242 -0
- package/src/executor.ts +431 -0
- package/src/file-response.ts +22 -0
- package/src/fork.ts +89 -0
- package/src/git/history.ts +177 -0
- package/src/git/index.ts +6 -0
- package/src/git/inflate.ts +125 -0
- package/src/git/objects.ts +110 -0
- package/src/git/pack.ts +105 -0
- package/src/git/refs.ts +25 -0
- package/src/git/smart-http.ts +149 -0
- package/src/hash.ts +66 -0
- package/src/head.ts +318 -0
- package/src/history.ts +246 -0
- package/src/index.ts +323 -0
- package/src/lifecycle.ts +8 -0
- package/src/log.ts +793 -0
- package/src/mirror-shell.ts +15 -0
- package/src/observe.ts +130 -0
- package/src/pack-assets.ts +81 -0
- package/src/packRegistry.ts +408 -0
- package/src/placeholder-remote.ts +81 -0
- package/src/proxy.ts +183 -0
- package/src/rateBudget.ts +1115 -0
- package/src/references.ts +46 -0
- package/src/remote-execute.ts +26 -0
- package/src/resource-blob.ts +57 -0
- package/src/scenario.ts +479 -0
- package/src/schemas.ts +56 -0
- package/src/serve-http.ts +299 -0
- package/src/serve.ts +618 -0
- package/src/shared-blob-index.ts +108 -0
- package/src/state-system.ts +115 -0
- package/src/storage.ts +407 -0
- package/src/twin-fetch.ts +147 -0
- package/src/types.ts +50 -0
- package/src/v1-removed.ts +172 -0
- package/src/volter-home.ts +11 -0
- package/src/world-clock.ts +33 -0
- package/src/world-env.ts +18 -0
- package/src/world-store-sql.ts +118 -0
- package/src/world-store.ts +572 -0
- package/src/worldConfig.ts +27 -0
- package/stream-bridge.cjs +80 -0
- package/vendor-hosts.cjs +200 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
// THE scenario engine — System 2 of the twin programming model (one grammar, per-pack
|
|
2
|
+
// vocabulary). See company-repo BRIEFS/TWIN-PROGRAMMING-MODEL.md (LOCKED, 2026-08-27).
|
|
3
|
+
//
|
|
4
|
+
// A HANDLER is an MSW-shaped data rule: { on, respond, once?, scope?, phase?, advancePhase? }.
|
|
5
|
+
// Handlers are evaluated IN ORDER; the FIRST handler whose `on` conditions ALL hold fires.
|
|
6
|
+
// No match → the caller serves its labeled deterministic stub and records the MISS (with the
|
|
7
|
+
// request's extracted features — the authoring signal). The handler FILE in the world dir is
|
|
8
|
+
// the only write surface; `engine.use(...)` exists for in-process tests only (LIFO over the
|
|
9
|
+
// baseline, removable). There are NO runtime write doors — a running world is never mutated.
|
|
10
|
+
//
|
|
11
|
+
// The GRAMMAR (structure, ordering, once/scope/phase, strict validation, extractors,
|
|
12
|
+
// placeholders, miss records) is this module's and identical for every vendor. The
|
|
13
|
+
// VOCABULARY (which `on` keys exist and how each matches; what `respond` may contain; which
|
|
14
|
+
// routes are stateful and therefore refuse success-shaped handlers) is the pack's, declared
|
|
15
|
+
// through a PackScenarioAdapter. Determinism: the engine is a pure state machine — same
|
|
16
|
+
// handler list + same request sequence → same decisions, byte for byte.
|
|
17
|
+
//
|
|
18
|
+
// STRICT EVERYWHERE (the gemini discipline): unknown top-level keys, unknown `on` keys,
|
|
19
|
+
// unknown placeholder names, malformed extractors — all THROW with the valid vocabulary in
|
|
20
|
+
// the message. A typo must fail loudly at load, never silently mis-match at serve.
|
|
21
|
+
export class ScenarioError extends Error {
|
|
22
|
+
}
|
|
23
|
+
// `$comment` is allowed (and ignored) at document and handler level — JSON has no comments
|
|
24
|
+
// and scenario files are hand-authored story documents.
|
|
25
|
+
const HANDLER_KEYS = new Set(["id", "on", "respond", "fault", "once", "scope", "phase", "advancePhase", "$comment"]);
|
|
26
|
+
const DOCUMENT_KEYS = new Set(["extractors", "handlers", "$comment"]);
|
|
27
|
+
/** Strict-loud parse of a scenario DOCUMENT (the per-vendor handlers/<vendor>.json content).
|
|
28
|
+
* The caller does file IO; this validates. Every refusal names the valid vocabulary. */
|
|
29
|
+
export function parseScenarioDocument(raw, adapter) {
|
|
30
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
31
|
+
throw new ScenarioError(`${adapter.vendor} scenario: the document is an object { extractors?, handlers }`);
|
|
32
|
+
const doc = raw;
|
|
33
|
+
for (const key of Object.keys(doc)) {
|
|
34
|
+
if (!DOCUMENT_KEYS.has(key))
|
|
35
|
+
throw new ScenarioError(`${adapter.vendor} scenario: unknown key "${key}" (valid: ${[...DOCUMENT_KEYS].join(", ")})`);
|
|
36
|
+
}
|
|
37
|
+
const extractors = {};
|
|
38
|
+
if (doc.extractors !== undefined) {
|
|
39
|
+
if (typeof doc.extractors !== "object" || doc.extractors === null || Array.isArray(doc.extractors))
|
|
40
|
+
throw new ScenarioError(`${adapter.vendor} scenario: extractors is an object of named specs`);
|
|
41
|
+
for (const [name, spec] of Object.entries(doc.extractors)) {
|
|
42
|
+
extractors[name] = parseExtractor(name, spec, adapter);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!Array.isArray(doc.handlers))
|
|
46
|
+
throw new ScenarioError(`${adapter.vendor} scenario: handlers is an array`);
|
|
47
|
+
const handlers = doc.handlers.map((h, i) => parseHandler(h, i, adapter, extractors));
|
|
48
|
+
return { ...(doc.extractors !== undefined ? { extractors } : {}), handlers };
|
|
49
|
+
}
|
|
50
|
+
function parseExtractor(name, raw, adapter) {
|
|
51
|
+
if (typeof raw !== "object" || raw === null)
|
|
52
|
+
throw new ScenarioError(`${adapter.vendor} scenario: extractor "${name}" is an object`);
|
|
53
|
+
const spec = raw;
|
|
54
|
+
const packKind = typeof spec.kind === "string" ? adapter.extractorKinds?.[spec.kind] : undefined;
|
|
55
|
+
if (packKind !== undefined) {
|
|
56
|
+
const refusal = packKind.validate(spec, name);
|
|
57
|
+
if (refusal)
|
|
58
|
+
throw new ScenarioError(`${adapter.vendor} scenario: extractor "${name}": ${refusal}`);
|
|
59
|
+
return spec;
|
|
60
|
+
}
|
|
61
|
+
if (spec.kind === "feature") {
|
|
62
|
+
if (typeof spec.feature !== "string" || spec.feature.length === 0)
|
|
63
|
+
throw new ScenarioError(`${adapter.vendor} scenario: extractor "${name}" (feature) needs a feature name`);
|
|
64
|
+
for (const key of Object.keys(spec))
|
|
65
|
+
if (key !== "kind" && key !== "feature")
|
|
66
|
+
throw new ScenarioError(`${adapter.vendor} scenario: extractor "${name}": unknown key "${key}"`);
|
|
67
|
+
return { kind: "feature", feature: spec.feature };
|
|
68
|
+
}
|
|
69
|
+
if (spec.kind === "textPattern") {
|
|
70
|
+
if (adapter.text === undefined)
|
|
71
|
+
throw new ScenarioError(`${adapter.vendor} scenario: extractor "${name}" uses textPattern but this pack exposes no request text`);
|
|
72
|
+
if (typeof spec.pattern !== "string")
|
|
73
|
+
throw new ScenarioError(`${adapter.vendor} scenario: extractor "${name}" (textPattern) needs a pattern`);
|
|
74
|
+
try {
|
|
75
|
+
new RegExp(spec.pattern);
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
throw new ScenarioError(`${adapter.vendor} scenario: extractor "${name}": invalid pattern: ${e instanceof Error ? e.message : String(e)}`);
|
|
79
|
+
}
|
|
80
|
+
if (spec.as !== undefined && spec.as !== "string" && spec.as !== "number")
|
|
81
|
+
throw new ScenarioError(`${adapter.vendor} scenario: extractor "${name}": as is "string" or "number"`);
|
|
82
|
+
for (const key of Object.keys(spec))
|
|
83
|
+
if (!["kind", "pattern", "as"].includes(key))
|
|
84
|
+
throw new ScenarioError(`${adapter.vendor} scenario: extractor "${name}": unknown key "${key}"`);
|
|
85
|
+
return { kind: "textPattern", pattern: spec.pattern, ...(spec.as !== undefined ? { as: spec.as } : {}) };
|
|
86
|
+
}
|
|
87
|
+
throw new ScenarioError(`${adapter.vendor} scenario: extractor "${name}": kind is one of ${[...new Set(["feature", "textPattern", ...Object.keys(adapter.extractorKinds ?? {})])].join(", ")}`);
|
|
88
|
+
}
|
|
89
|
+
function parseHandler(raw, index, adapter, extractors) {
|
|
90
|
+
const at = `handler ${index + 1}`;
|
|
91
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
92
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at} is an object { on, respond, ... }`);
|
|
93
|
+
const h = raw;
|
|
94
|
+
for (const key of Object.keys(h)) {
|
|
95
|
+
if (!HANDLER_KEYS.has(key))
|
|
96
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: unknown key "${key}" (valid: ${[...HANDLER_KEYS].join(", ")})`);
|
|
97
|
+
}
|
|
98
|
+
if (typeof h.on !== "object" || h.on === null || Array.isArray(h.on))
|
|
99
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: \`on\` is an object of conditions ({} matches all)`);
|
|
100
|
+
// `nthCall` is the one kernel-builtin condition (1-based per-scope call index); every other
|
|
101
|
+
// `on` key is the pack's declared vocabulary.
|
|
102
|
+
const validOn = ["nthCall", ...Object.keys(adapter.matchers)];
|
|
103
|
+
for (const [key, value] of Object.entries(h.on)) {
|
|
104
|
+
if (!validOn.includes(key))
|
|
105
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: unknown \`on\` key "${key}" (valid here: ${validOn.join(", ")})`);
|
|
106
|
+
if (key === "nthCall" && (!Number.isInteger(value) || value < 1))
|
|
107
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: nthCall is a positive integer`);
|
|
108
|
+
}
|
|
109
|
+
const onRefusal = adapter.validateOn?.(h.on);
|
|
110
|
+
if (onRefusal)
|
|
111
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: ${onRefusal}`);
|
|
112
|
+
if (h.fault !== undefined) {
|
|
113
|
+
const f = h.fault;
|
|
114
|
+
if (typeof f !== "object" || f === null || Array.isArray(f))
|
|
115
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault is an object { kind: "slow" | "status" | "drop", ... }`);
|
|
116
|
+
const kinds = ["slow", "status", "drop"];
|
|
117
|
+
if (!kinds.includes(f.kind))
|
|
118
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.kind is one of ${kinds.join(", ")}, got ${JSON.stringify(f.kind)}`);
|
|
119
|
+
const keysFor = { slow: ["kind", "ms"], status: ["kind", "status", "retryAfterSeconds", "message"], drop: ["kind", "holdMs"] };
|
|
120
|
+
for (const key of Object.keys(f))
|
|
121
|
+
if (!keysFor[f.kind].includes(key))
|
|
122
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.${key} is not a key of a ${String(f.kind)} fault (valid: ${keysFor[f.kind].join(", ")})`);
|
|
123
|
+
if (f.kind === "slow" && !(Number.isInteger(f.ms) && f.ms >= 0 && f.ms <= 600_000))
|
|
124
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.ms is an integer 0..600000 (milliseconds)`);
|
|
125
|
+
if (f.kind === "status" && !(Number.isInteger(f.status) && f.status >= 400 && f.status <= 599))
|
|
126
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.status is an integer 400..599 — a fault is a refusal or a failure, never a success`);
|
|
127
|
+
if (f.kind === "status" && f.retryAfterSeconds !== undefined && !(Number.isInteger(f.retryAfterSeconds) && f.retryAfterSeconds >= 0))
|
|
128
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.retryAfterSeconds is a non-negative integer`);
|
|
129
|
+
if (f.kind === "status" && f.message !== undefined && typeof f.message !== "string")
|
|
130
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.message is a string`);
|
|
131
|
+
if (f.kind === "drop" && f.holdMs !== undefined && !(Number.isInteger(f.holdMs) && f.holdMs >= 0))
|
|
132
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.holdMs is a non-negative integer`);
|
|
133
|
+
if (f.kind !== "slow" && "respond" in h)
|
|
134
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: a ${String(f.kind)} fault serves no content — drop \`respond\` (only a slow fault may carry one)`);
|
|
135
|
+
}
|
|
136
|
+
if (!("respond" in h) && h.fault === undefined)
|
|
137
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: \`respond\` is required (or a \`fault\`)`);
|
|
138
|
+
if (h.id !== undefined && (typeof h.id !== "string" || h.id.length === 0))
|
|
139
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: id is a non-empty string`);
|
|
140
|
+
if (h.once !== undefined && typeof h.once !== "boolean")
|
|
141
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: once is boolean`);
|
|
142
|
+
if (h.scope !== undefined && h.scope !== "world" && h.scope !== "session")
|
|
143
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: scope is "world" or "session"`);
|
|
144
|
+
if (h.scope === "session" && adapter.scopeKey === undefined)
|
|
145
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: scope "session" but this pack derives no session key`);
|
|
146
|
+
for (const key of ["phase", "advancePhase"]) {
|
|
147
|
+
if (h[key] !== undefined && (typeof h[key] !== "string" || h[key].length === 0))
|
|
148
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: ${key} is a non-empty string`);
|
|
149
|
+
}
|
|
150
|
+
for (const name of "respond" in h ? placeholderNames(h.respond) : []) {
|
|
151
|
+
if (extractors[name] === undefined)
|
|
152
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: respond references "{{${name}}}" but no extractor "${name}" is declared`);
|
|
153
|
+
}
|
|
154
|
+
const refusal = "respond" in h ? adapter.validateRespond?.(h.respond, h) : null;
|
|
155
|
+
if (refusal)
|
|
156
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: ${refusal}`);
|
|
157
|
+
return h;
|
|
158
|
+
}
|
|
159
|
+
const PLACEHOLDER_SRC = "\\{\\{([a-zA-Z0-9_.-]+)(\\|(?:min|max):-?[\\d.]+)?\\}\\}";
|
|
160
|
+
function placeholderNames(value, out = new Set()) {
|
|
161
|
+
if (typeof value === "string") {
|
|
162
|
+
for (const m of value.matchAll(new RegExp(PLACEHOLDER_SRC, "g")))
|
|
163
|
+
out.add(m[1]);
|
|
164
|
+
// A lone unparseable {{...}} is almost certainly a typo'd placeholder — fail loudly.
|
|
165
|
+
const braces = /\{\{[^}]*\}\}/g.exec(value);
|
|
166
|
+
if (braces && !new RegExp(`^${PLACEHOLDER_SRC}$`).test(braces[0])) {
|
|
167
|
+
throw new ScenarioError(`malformed placeholder ${JSON.stringify(braces[0])} (expected {{name}} or {{name|min:N}}/{{name|max:N}})`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else if (Array.isArray(value)) {
|
|
171
|
+
for (const v of value)
|
|
172
|
+
placeholderNames(v, out);
|
|
173
|
+
}
|
|
174
|
+
else if (typeof value === "object" && value !== null) {
|
|
175
|
+
for (const v of Object.values(value))
|
|
176
|
+
placeholderNames(v, out);
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
function adapterKind(adapter, kind) {
|
|
181
|
+
return adapter.extractorKinds?.[kind];
|
|
182
|
+
}
|
|
183
|
+
/** `{{name|min:N}}` / `{{name|max:N}}` — numeric clamps on an extracted value. */
|
|
184
|
+
function applyTransform(value, transform, vendor, where) {
|
|
185
|
+
if (!transform)
|
|
186
|
+
return value;
|
|
187
|
+
const [op, rawN] = transform.slice(1).split(":");
|
|
188
|
+
if (typeof value !== "number")
|
|
189
|
+
throw new ScenarioError(`${vendor} scenario: ${where} applies |${op}:${rawN} to a non-numeric extractor value ${JSON.stringify(value)}`);
|
|
190
|
+
const n = Number(rawN);
|
|
191
|
+
return op === "min" ? Math.min(value, n) : Math.max(value, n);
|
|
192
|
+
}
|
|
193
|
+
const MISS_KEEP = 20;
|
|
194
|
+
/** The engine: pure state machine over registered handlers. One instance per twin server. */
|
|
195
|
+
export class ScenarioEngine {
|
|
196
|
+
adapter;
|
|
197
|
+
extractors;
|
|
198
|
+
baseline = [];
|
|
199
|
+
overrides = [];
|
|
200
|
+
scopes = new Map();
|
|
201
|
+
missCount = 0;
|
|
202
|
+
recentMisses = [];
|
|
203
|
+
constructor(adapter, document) {
|
|
204
|
+
this.adapter = adapter;
|
|
205
|
+
this.extractors = document?.extractors ?? {};
|
|
206
|
+
if (document) {
|
|
207
|
+
this.baseline = document.handlers.map((handler, i) => ({ handler, id: handler.id ?? `handler-${i + 1}`, source: "file", matches: 0 }));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** In-process test overrides: LIFO over the baseline; returns a remover. NOT a runtime
|
|
211
|
+
* door — nothing outside this process can reach it, by design. */
|
|
212
|
+
use(...handlers) {
|
|
213
|
+
const parsed = handlers.map((h, i) => parseHandler(h, i, this.adapter, this.extractors));
|
|
214
|
+
const registered = parsed.map((handler, i) => ({ handler, id: handler.id ?? `use-${this.overrides.length + i + 1}`, source: "use", matches: 0 }));
|
|
215
|
+
this.overrides = [...registered, ...this.overrides]; // LIFO: newest first
|
|
216
|
+
return () => { this.overrides = this.overrides.filter((r) => !registered.includes(r)); };
|
|
217
|
+
}
|
|
218
|
+
/** Decide one request. Mutates scope state (calls, once, phase) exactly like serving. */
|
|
219
|
+
next(req) {
|
|
220
|
+
const scopeOf = (h) => (h.scope === "session" ? `session:${this.adapter.scopeKey(req)}` : "world");
|
|
221
|
+
// The call counter ticks once per request on the WORLD scope (and the session scope when
|
|
222
|
+
// one exists) — before matching, so nthCall-style matchers see 1-based "this call".
|
|
223
|
+
this.scope("world").calls += 1;
|
|
224
|
+
if (this.adapter.scopeKey)
|
|
225
|
+
this.scope(`session:${this.adapter.scopeKey(req)}`).calls += 1;
|
|
226
|
+
for (const r of [...this.overrides, ...this.baseline]) {
|
|
227
|
+
const scope = this.scope(scopeOf(r.handler));
|
|
228
|
+
if (r.handler.phase !== undefined && scope.phase !== r.handler.phase)
|
|
229
|
+
continue;
|
|
230
|
+
if (r.handler.once && scope.onceFired.has(r.id))
|
|
231
|
+
continue;
|
|
232
|
+
if (!this.matches(r.handler, req, scope))
|
|
233
|
+
continue;
|
|
234
|
+
if (r.handler.once)
|
|
235
|
+
scope.onceFired.add(r.id);
|
|
236
|
+
if (r.handler.advancePhase !== undefined)
|
|
237
|
+
scope.phase = r.handler.advancePhase;
|
|
238
|
+
r.matches += 1;
|
|
239
|
+
// `ruleId` is the row's STABLE id — the authored `id` or the parser's `handler-<n>`
|
|
240
|
+
// default — the same name the status door reports, so a pack stamping the fired rule
|
|
241
|
+
// (deepgram's `scenario_rule`) and the operator reading /twin/scenario see one vocabulary.
|
|
242
|
+
try {
|
|
243
|
+
return { kind: "handler", handler: r.handler, respond: "respond" in r.handler ? this.substitute(r.handler.respond, req) : undefined, ruleId: r.id, ...(r.handler.fault !== undefined ? { fault: r.handler.fault } : {}) };
|
|
244
|
+
}
|
|
245
|
+
catch (e) {
|
|
246
|
+
// The handler that demanded the value is named: an authoring fault is fixed by finding it.
|
|
247
|
+
if (e instanceof ScenarioError)
|
|
248
|
+
throw new ScenarioError(`${e.message} [${r.id}${typeof r.handler.$comment === "string" ? `: ${String(r.handler.$comment).slice(0, 80)}` : ""}]`);
|
|
249
|
+
throw e;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const miss = { features: this.adapter.features(req), phase: this.scope("world").phase };
|
|
253
|
+
this.missCount += 1;
|
|
254
|
+
this.recentMisses = [...this.recentMisses.slice(-(MISS_KEEP - 1)), miss];
|
|
255
|
+
return { kind: "miss", miss };
|
|
256
|
+
}
|
|
257
|
+
/** Decide one request AND honor its fault: a `slow` is awaited here and the decision is
|
|
258
|
+
* returned for the pack to serve; a `status` returns the rendered refusal to serve as-is; a
|
|
259
|
+
* `drop` never resolves within its hold. The one call a realizer makes so every vendor's
|
|
260
|
+
* outage reads the same. */
|
|
261
|
+
async serve(req, signal) {
|
|
262
|
+
throwIfScenarioAborted(signal);
|
|
263
|
+
const decision = this.next(req);
|
|
264
|
+
throwIfScenarioAborted(signal);
|
|
265
|
+
if (decision.kind !== "handler" || decision.fault === undefined)
|
|
266
|
+
return decision;
|
|
267
|
+
const result = await scenarioFaultResult(decision.fault, () => this.adapter.renderFault?.(decision.fault, req), signal);
|
|
268
|
+
throwIfScenarioAborted(signal);
|
|
269
|
+
return result === null ? decision : { kind: "fault", result, ruleId: decision.ruleId };
|
|
270
|
+
}
|
|
271
|
+
/** For GET /twin/scenario — active handlers with match counts, and the recent misses. */
|
|
272
|
+
status() {
|
|
273
|
+
const row = (r) => ({ id: r.id, ...(r.handler.phase !== undefined ? { phase: r.handler.phase } : {}), ...(r.handler.once !== undefined ? { once: r.handler.once } : {}), ...(r.handler.scope !== undefined ? { scope: r.handler.scope } : {}), matches: r.matches, source: r.source });
|
|
274
|
+
return { vendor: this.adapter.vendor, handlers: [...this.overrides, ...this.baseline].map(row), misses: this.missCount, recentMisses: [...this.recentMisses] };
|
|
275
|
+
}
|
|
276
|
+
scope(key) {
|
|
277
|
+
let s = this.scopes.get(key);
|
|
278
|
+
if (!s) {
|
|
279
|
+
s = { calls: 0, phase: undefined, onceFired: new Set() };
|
|
280
|
+
this.scopes.set(key, s);
|
|
281
|
+
}
|
|
282
|
+
return s;
|
|
283
|
+
}
|
|
284
|
+
matches(handler, req, scope) {
|
|
285
|
+
for (const [key, condition] of Object.entries(handler.on)) {
|
|
286
|
+
if (key === "nthCall") {
|
|
287
|
+
if (scope.calls !== condition)
|
|
288
|
+
return false;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const matcher = this.adapter.matchers[key];
|
|
292
|
+
if (!matcher)
|
|
293
|
+
return false; // unreachable post-parse; belt over braces
|
|
294
|
+
if (!matcher(req, condition))
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
extractValue(name, req, cache) {
|
|
300
|
+
const cached = cache.get(name);
|
|
301
|
+
if (cached !== undefined)
|
|
302
|
+
return cached;
|
|
303
|
+
const spec = this.extractors[name];
|
|
304
|
+
const packKind = adapterKind(this.adapter, spec.kind);
|
|
305
|
+
let value;
|
|
306
|
+
if (packKind !== undefined) {
|
|
307
|
+
value = packKind.extract(spec, req, name);
|
|
308
|
+
}
|
|
309
|
+
else if (spec.kind === "feature") {
|
|
310
|
+
const v = this.adapter.features(req)[spec.feature];
|
|
311
|
+
if (v === undefined)
|
|
312
|
+
throw new ScenarioError(`${this.adapter.vendor} scenario: placeholder "{{${name}}}": the request has no feature "${spec.feature}"`);
|
|
313
|
+
value = typeof v === "number" || typeof v === "string" ? v : String(v);
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
const text = this.adapter.text(req);
|
|
317
|
+
const tp = spec;
|
|
318
|
+
const m = new RegExp(tp.pattern).exec(text);
|
|
319
|
+
if (!m)
|
|
320
|
+
throw new ScenarioError(`${this.adapter.vendor} scenario: placeholder "{{${name}}}": pattern did not match the request (the handler demanded a value the request never stated)`);
|
|
321
|
+
const captured = m[1] ?? m[0];
|
|
322
|
+
if (tp.as === "number") {
|
|
323
|
+
const n = Number(captured);
|
|
324
|
+
if (Number.isNaN(n))
|
|
325
|
+
throw new ScenarioError(`${this.adapter.vendor} scenario: placeholder "{{${name}}}": captured "${captured}" is not a number`);
|
|
326
|
+
value = n;
|
|
327
|
+
}
|
|
328
|
+
else
|
|
329
|
+
value = captured;
|
|
330
|
+
}
|
|
331
|
+
cache.set(name, value);
|
|
332
|
+
return value;
|
|
333
|
+
}
|
|
334
|
+
substitute(value, req, cache = new Map()) {
|
|
335
|
+
if (typeof value === "string") {
|
|
336
|
+
// A WHOLE-string placeholder yields the typed value (numbers stay numbers).
|
|
337
|
+
const whole = new RegExp(`^${PLACEHOLDER_SRC}$`).exec(value);
|
|
338
|
+
if (whole)
|
|
339
|
+
return applyTransform(this.extractValue(whole[1], req, cache), whole[2], this.adapter.vendor, value);
|
|
340
|
+
return value.replace(new RegExp(PLACEHOLDER_SRC, "g"), (all, name, transform) => String(applyTransform(this.extractValue(name, req, cache), transform, this.adapter.vendor, all)));
|
|
341
|
+
}
|
|
342
|
+
if (Array.isArray(value))
|
|
343
|
+
return value.map((v) => this.substitute(v, req, cache));
|
|
344
|
+
if (typeof value === "object" && value !== null) {
|
|
345
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, this.substitute(v, req, cache)]));
|
|
346
|
+
}
|
|
347
|
+
return value;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/** The GET /twin manifest for a STATEFUL twin (no scenario engine): what it stores, how
|
|
351
|
+
* state and identity work, where behavior scripting actually lives. Education ships
|
|
352
|
+
* INSIDE the twin — in-world agents have no repos or skills, only HTTP. */
|
|
353
|
+
/** Honor a fault outside the engine (hand-written serve paths): `slow` waits and returns null
|
|
354
|
+
* (serve normally now); `status` returns what to serve; `drop` holds the request for `holdMs`
|
|
355
|
+
* (default 300 000) and then throws, so the socket is released and nothing is ever answered. */
|
|
356
|
+
function throwIfScenarioAborted(signal) {
|
|
357
|
+
signal?.throwIfAborted();
|
|
358
|
+
}
|
|
359
|
+
function waitForScenarioFault(ms, signal) {
|
|
360
|
+
throwIfScenarioAborted(signal);
|
|
361
|
+
return new Promise((resolve, reject) => {
|
|
362
|
+
const cleanup = () => signal?.removeEventListener('abort', onAbort);
|
|
363
|
+
const onAbort = () => {
|
|
364
|
+
clearTimeout(timer);
|
|
365
|
+
cleanup();
|
|
366
|
+
reject(signal?.reason);
|
|
367
|
+
};
|
|
368
|
+
const timer = setTimeout(() => { cleanup(); resolve(); }, ms);
|
|
369
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
370
|
+
if (signal?.aborted)
|
|
371
|
+
onAbort();
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
export async function scenarioFaultResult(fault, render, signal) {
|
|
375
|
+
throwIfScenarioAborted(signal);
|
|
376
|
+
if (fault.kind === "slow") {
|
|
377
|
+
await waitForScenarioFault(fault.ms, signal);
|
|
378
|
+
throwIfScenarioAborted(signal);
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
if (fault.kind === "drop") {
|
|
382
|
+
await waitForScenarioFault(fault.holdMs ?? 300_000, signal);
|
|
383
|
+
throwIfScenarioAborted(signal);
|
|
384
|
+
throw new ScenarioError("scenario drop: the request was held unanswered for its hold and is now released without a response");
|
|
385
|
+
}
|
|
386
|
+
const rendered = render?.();
|
|
387
|
+
throwIfScenarioAborted(signal);
|
|
388
|
+
const headers = { ...(rendered?.headers ?? {}) };
|
|
389
|
+
if (fault.retryAfterSeconds !== undefined)
|
|
390
|
+
headers["retry-after"] = String(fault.retryAfterSeconds);
|
|
391
|
+
return { status: fault.status, headers, body: rendered?.body ?? { error: { message: fault.message ?? `twin fault: ${fault.status}`, type: "twin_fault" } } };
|
|
392
|
+
}
|
|
393
|
+
export function statefulTwinManifest(input) {
|
|
394
|
+
return {
|
|
395
|
+
twin: true,
|
|
396
|
+
vendor: input.vendor,
|
|
397
|
+
twinOf: input.twinOf,
|
|
398
|
+
program: {
|
|
399
|
+
state: `Stateful: it stores ${input.stores}. Create state through the vendor's OWN API with the real SDK or plain fetch pointed here — there is no fixture language and no write door besides the vendor's.`,
|
|
400
|
+
identity: input.identity ?? 'Authenticate as the vendor does; the twin accepts any non-sentinel credential.',
|
|
401
|
+
time: 'Writes are stamped from the WORLD CLOCK (volter-world clock <world> set/advance) — deterministic history is clock-set, seed, clock-advance.',
|
|
402
|
+
behavior: 'This twin is state, not scripting — answers are functions of what you seeded. Judgment/fault scripting lives on the scripted vendor twins (their GET /twin explains).',
|
|
403
|
+
...(input.notes ? { notes: input.notes } : {}),
|
|
404
|
+
},
|
|
405
|
+
doors: { manifest: 'GET /twin' },
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
/** The GET /twin manifest — the discovery door's body. Education ships INSIDE the twin:
|
|
409
|
+
* in-world agents have no repos or skills, only HTTP. */
|
|
410
|
+
export function twinManifest(input) {
|
|
411
|
+
const status = input.engine?.status();
|
|
412
|
+
return {
|
|
413
|
+
twin: true,
|
|
414
|
+
vendor: input.vendor,
|
|
415
|
+
twinOf: input.twinOf,
|
|
416
|
+
program: {
|
|
417
|
+
state: input.stateSentence,
|
|
418
|
+
behavior: input.behaviorSentence,
|
|
419
|
+
invariant: "A handler never fakes a SUCCESS on a route whose data this twin stores — seed that through the vendor's own API instead. Serving is deterministic.",
|
|
420
|
+
...(input.exampleHandler ? { exampleHandler: input.exampleHandler } : {}),
|
|
421
|
+
},
|
|
422
|
+
doors: { manifest: "GET /twin", scenario: "GET /twin/scenario (read-only: active handlers + match/miss counts)" },
|
|
423
|
+
...(status ? { activeHandlers: status.handlers.length, misses: status.misses } : {}),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const WorldActorSchema: z.ZodObject<{
|
|
3
|
+
id: z.ZodOptional<z.ZodString>;
|
|
4
|
+
name: z.ZodOptional<z.ZodString>;
|
|
5
|
+
kind: z.ZodEnum<{
|
|
6
|
+
human: "human";
|
|
7
|
+
agent: "agent";
|
|
8
|
+
bot: "bot";
|
|
9
|
+
system: "system";
|
|
10
|
+
}>;
|
|
11
|
+
}, z.core.$strip>;
|
|
12
|
+
export declare const WorldSubjectSchema: z.ZodObject<{
|
|
13
|
+
type: z.ZodString;
|
|
14
|
+
id: z.ZodString;
|
|
15
|
+
}, z.core.$strip>;
|
|
16
|
+
export declare const WorldExternalRefSchema: z.ZodObject<{
|
|
17
|
+
provider: z.ZodString;
|
|
18
|
+
id: z.ZodOptional<z.ZodString>;
|
|
19
|
+
url: z.ZodOptional<z.ZodString>;
|
|
20
|
+
cursor: z.ZodOptional<z.ZodString>;
|
|
21
|
+
rawRef: z.ZodOptional<z.ZodString>;
|
|
22
|
+
}, z.core.$strip>;
|
|
23
|
+
export declare const WorldServiceEventSchema: z.ZodObject<{
|
|
24
|
+
id: z.ZodString;
|
|
25
|
+
service: z.ZodString;
|
|
26
|
+
type: z.ZodString;
|
|
27
|
+
schemaVersion: z.ZodNumber;
|
|
28
|
+
idempotencyKey: z.ZodString;
|
|
29
|
+
occurredAt: z.ZodString;
|
|
30
|
+
observedAt: z.ZodString;
|
|
31
|
+
origin: z.ZodEnum<{
|
|
32
|
+
agent: "agent";
|
|
33
|
+
virtual: "virtual";
|
|
34
|
+
external: "external";
|
|
35
|
+
connector: "connector";
|
|
36
|
+
replay: "replay";
|
|
37
|
+
migration: "migration";
|
|
38
|
+
}>;
|
|
39
|
+
actor: z.ZodOptional<z.ZodObject<{
|
|
40
|
+
id: z.ZodOptional<z.ZodString>;
|
|
41
|
+
name: z.ZodOptional<z.ZodString>;
|
|
42
|
+
kind: z.ZodEnum<{
|
|
43
|
+
human: "human";
|
|
44
|
+
agent: "agent";
|
|
45
|
+
bot: "bot";
|
|
46
|
+
system: "system";
|
|
47
|
+
}>;
|
|
48
|
+
}, z.core.$strip>>;
|
|
49
|
+
subject: z.ZodObject<{
|
|
50
|
+
type: z.ZodString;
|
|
51
|
+
id: z.ZodString;
|
|
52
|
+
}, z.core.$strip>;
|
|
53
|
+
causationId: z.ZodOptional<z.ZodString>;
|
|
54
|
+
correlationId: z.ZodOptional<z.ZodString>;
|
|
55
|
+
external: z.ZodOptional<z.ZodObject<{
|
|
56
|
+
provider: z.ZodString;
|
|
57
|
+
id: z.ZodOptional<z.ZodString>;
|
|
58
|
+
url: z.ZodOptional<z.ZodString>;
|
|
59
|
+
cursor: z.ZodOptional<z.ZodString>;
|
|
60
|
+
rawRef: z.ZodOptional<z.ZodString>;
|
|
61
|
+
}, z.core.$strip>>;
|
|
62
|
+
data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
63
|
+
raw: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
64
|
+
}, z.core.$strip>;
|
|
65
|
+
export declare const GenericWorldStateSchema: z.ZodObject<{
|
|
66
|
+
version: z.ZodLiteral<1>;
|
|
67
|
+
service: z.ZodString;
|
|
68
|
+
rebuiltAt: z.ZodString;
|
|
69
|
+
eventCount: z.ZodNumber;
|
|
70
|
+
latestEventId: z.ZodOptional<z.ZodString>;
|
|
71
|
+
subjects: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
72
|
+
type: z.ZodString;
|
|
73
|
+
id: z.ZodString;
|
|
74
|
+
latestEventId: z.ZodString;
|
|
75
|
+
latestType: z.ZodString;
|
|
76
|
+
updatedAt: z.ZodString;
|
|
77
|
+
}, z.core.$strip>>;
|
|
78
|
+
}, z.core.$strip>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const WorldActorSchema = z.object({
|
|
3
|
+
id: z.string().min(1).optional(),
|
|
4
|
+
name: z.string().min(1).optional(),
|
|
5
|
+
kind: z.enum(['human', 'agent', 'bot', 'system']),
|
|
6
|
+
});
|
|
7
|
+
export const WorldSubjectSchema = z.object({
|
|
8
|
+
type: z.string().min(1),
|
|
9
|
+
id: z.string().min(1),
|
|
10
|
+
});
|
|
11
|
+
export const WorldExternalRefSchema = z.object({
|
|
12
|
+
provider: z.string().min(1),
|
|
13
|
+
id: z.string().min(1).optional(),
|
|
14
|
+
url: z.string().min(1).optional(),
|
|
15
|
+
cursor: z.string().min(1).optional(),
|
|
16
|
+
rawRef: z.string().min(1).optional(),
|
|
17
|
+
});
|
|
18
|
+
export const WorldServiceEventSchema = z.object({
|
|
19
|
+
id: z.string().min(1),
|
|
20
|
+
service: z.string().min(1),
|
|
21
|
+
type: z.string().min(1),
|
|
22
|
+
schemaVersion: z.number().int().positive(),
|
|
23
|
+
idempotencyKey: z.string().min(1),
|
|
24
|
+
occurredAt: z.string().min(1),
|
|
25
|
+
observedAt: z.string().min(1),
|
|
26
|
+
origin: z.enum(['virtual', 'external', 'agent', 'connector', 'replay', 'migration']),
|
|
27
|
+
actor: WorldActorSchema.optional(),
|
|
28
|
+
subject: WorldSubjectSchema,
|
|
29
|
+
causationId: z.string().min(1).optional(),
|
|
30
|
+
correlationId: z.string().min(1).optional(),
|
|
31
|
+
external: WorldExternalRefSchema.optional(),
|
|
32
|
+
data: z.record(z.string(), z.unknown()),
|
|
33
|
+
raw: z.record(z.string(), z.unknown()).optional(),
|
|
34
|
+
});
|
|
35
|
+
// NOTE: WorldAnnotationSchema moved to @volter/tracker/world-annotations — annotating
|
|
36
|
+
// world events as sources/noise is a tracker (verification) concern, not the twin's.
|
|
37
|
+
export const GenericWorldStateSchema = z.object({
|
|
38
|
+
version: z.literal(1),
|
|
39
|
+
service: z.string().min(1),
|
|
40
|
+
rebuiltAt: z.string().min(1),
|
|
41
|
+
eventCount: z.number().int().nonnegative(),
|
|
42
|
+
latestEventId: z.string().min(1).optional(),
|
|
43
|
+
subjects: z.record(z.string(), z.object({
|
|
44
|
+
type: z.string().min(1),
|
|
45
|
+
id: z.string().min(1),
|
|
46
|
+
latestEventId: z.string().min(1),
|
|
47
|
+
latestType: z.string().min(1),
|
|
48
|
+
updatedAt: z.string().min(1),
|
|
49
|
+
})),
|
|
50
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type HttpHandler = (request: Request) => Response | Promise<Response>;
|
|
2
|
+
export type WebSocketPeer = {
|
|
3
|
+
send: (data: string | Uint8Array) => unknown;
|
|
4
|
+
close: (code?: number, reason?: string) => unknown;
|
|
5
|
+
};
|
|
6
|
+
export type WebSocketUpgrade = {
|
|
7
|
+
accepts: (request: Request) => boolean;
|
|
8
|
+
open: (peer: WebSocketPeer, request: Request) => void;
|
|
9
|
+
message: (peer: WebSocketPeer, data: string | Uint8Array) => void | Promise<void>;
|
|
10
|
+
close: (peer: WebSocketPeer) => void;
|
|
11
|
+
};
|
|
12
|
+
export type ServeHttpOptions = {
|
|
13
|
+
/** Multiplexing infrastructure fronts leave vendor journaling to the downstream twins. */
|
|
14
|
+
twinRequestJournal?: boolean;
|
|
15
|
+
/** Optional persistent transport, with the same callbacks on Node and Bun. */
|
|
16
|
+
upgrade?: WebSocketUpgrade;
|
|
17
|
+
tls?: {
|
|
18
|
+
key: string | Uint8Array;
|
|
19
|
+
cert: string | Uint8Array;
|
|
20
|
+
};
|
|
21
|
+
/** the interface to listen on: loopback unless a caller names another. A twin is reached by its
|
|
22
|
+
* World over loopback; a front that is meant to be reached (a served world, a host) names its own. */
|
|
23
|
+
hostname?: string;
|
|
24
|
+
port?: number;
|
|
25
|
+
fetch: HttpHandler;
|
|
26
|
+
/** seconds a connection may idle (Bun honors it; Node's keep-alive timeout is set from it) */
|
|
27
|
+
idleTimeout?: number;
|
|
28
|
+
/** the largest request body accepted, in bytes (Bun's default is 128 MB; Node sets none) */
|
|
29
|
+
maxRequestBodySize?: number;
|
|
30
|
+
/** what a thrown handler answers with; absent, a 500 carrying the error (as Bun.serve) */
|
|
31
|
+
error?: (error: Error) => Response | Promise<Response>;
|
|
32
|
+
};
|
|
33
|
+
export type HttpServer = {
|
|
34
|
+
hostname: string;
|
|
35
|
+
port: number;
|
|
36
|
+
/** as Bun's: a URL, whose string form ends in `/` — packs build `${server.url}path` on that */
|
|
37
|
+
url: URL;
|
|
38
|
+
/** stop listening; `closeActive` (default true) drops in-flight connections as well */
|
|
39
|
+
stop: (closeActive?: boolean) => Promise<void>;
|
|
40
|
+
};
|
|
41
|
+
export declare function setServeDecorator(fn: ((options: ServeHttpOptions) => HttpHandler) | undefined): void;
|
|
42
|
+
/** The path a World asks a service it booted, when that service's port answers, which boot it belongs to. */
|
|
43
|
+
export declare const WORLD_BOOT_PATH = "/__volter/world-boot";
|
|
44
|
+
export declare function serveHttp(options: ServeHttpOptions): Promise<HttpServer>;
|
|
45
|
+
/** A Node builtin without a static import — `process.getBuiltinModule` (Node 22.3+, Bun): what a
|
|
46
|
+
* module that also rides into a browser bundle reaches `node:crypto` with. A bare `require()`
|
|
47
|
+
* is Bun's alone; Node's ESM has none (found 2026-09-07 by the pages running under node). */
|
|
48
|
+
export declare function nodeBuiltin<T = unknown>(name: string): T;
|