@stackables/bridge-graphql 1.0.1 → 1.0.3
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/build/bridge-transform.d.ts.map +1 -0
- package/build/{bridge-graphql/src/bridge-transform.js → bridge-transform.js} +17 -2
- package/build/index.d.ts.map +1 -0
- package/package.json +3 -3
- package/build/bridge-core/src/ExecutionTree.d.ts +0 -213
- package/build/bridge-core/src/ExecutionTree.d.ts.map +0 -1
- package/build/bridge-core/src/ExecutionTree.js +0 -1323
- package/build/bridge-core/src/execute-bridge.d.ts +0 -69
- package/build/bridge-core/src/execute-bridge.d.ts.map +0 -1
- package/build/bridge-core/src/execute-bridge.js +0 -52
- package/build/bridge-core/src/index.d.ts +0 -18
- package/build/bridge-core/src/index.d.ts.map +0 -1
- package/build/bridge-core/src/index.js +0 -20
- package/build/bridge-core/src/merge-documents.d.ts +0 -25
- package/build/bridge-core/src/merge-documents.d.ts.map +0 -1
- package/build/bridge-core/src/merge-documents.js +0 -91
- package/build/bridge-core/src/tools/index.d.ts +0 -2
- package/build/bridge-core/src/tools/index.d.ts.map +0 -1
- package/build/bridge-core/src/tools/index.js +0 -1
- package/build/bridge-core/src/tools/internal.d.ts +0 -71
- package/build/bridge-core/src/tools/internal.d.ts.map +0 -1
- package/build/bridge-core/src/tools/internal.js +0 -59
- package/build/bridge-core/src/types.d.ts +0 -349
- package/build/bridge-core/src/types.d.ts.map +0 -1
- package/build/bridge-core/src/types.js +0 -3
- package/build/bridge-core/src/utils.d.ts +0 -9
- package/build/bridge-core/src/utils.d.ts.map +0 -1
- package/build/bridge-core/src/utils.js +0 -23
- package/build/bridge-core/src/version-check.d.ts +0 -64
- package/build/bridge-core/src/version-check.d.ts.map +0 -1
- package/build/bridge-core/src/version-check.js +0 -205
- package/build/bridge-graphql/src/bridge-transform.d.ts.map +0 -1
- package/build/bridge-graphql/src/index.d.ts.map +0 -1
- package/build/bridge-stdlib/src/index.d.ts +0 -34
- package/build/bridge-stdlib/src/index.d.ts.map +0 -1
- package/build/bridge-stdlib/src/index.js +0 -40
- package/build/bridge-stdlib/src/tools/arrays.d.ts +0 -28
- package/build/bridge-stdlib/src/tools/arrays.d.ts.map +0 -1
- package/build/bridge-stdlib/src/tools/arrays.js +0 -50
- package/build/bridge-stdlib/src/tools/audit.d.ts +0 -36
- package/build/bridge-stdlib/src/tools/audit.d.ts.map +0 -1
- package/build/bridge-stdlib/src/tools/audit.js +0 -39
- package/build/bridge-stdlib/src/tools/http-call.d.ts +0 -35
- package/build/bridge-stdlib/src/tools/http-call.d.ts.map +0 -1
- package/build/bridge-stdlib/src/tools/http-call.js +0 -118
- package/build/bridge-stdlib/src/tools/strings.d.ts +0 -13
- package/build/bridge-stdlib/src/tools/strings.d.ts.map +0 -1
- package/build/bridge-stdlib/src/tools/strings.js +0 -12
- package/build/bridge-types/src/index.d.ts +0 -63
- package/build/bridge-types/src/index.d.ts.map +0 -1
- package/build/bridge-types/src/index.js +0 -8
- /package/build/{bridge-graphql/src/bridge-transform.d.ts → bridge-transform.d.ts} +0 -0
- /package/build/{bridge-graphql/src/index.d.ts → index.d.ts} +0 -0
- /package/build/{bridge-graphql/src/index.js → index.js} +0 -0
|
@@ -1,1323 +0,0 @@
|
|
|
1
|
-
import { SpanStatusCode, metrics, trace } from "@opentelemetry/api";
|
|
2
|
-
import { parsePath } from "./utils.js";
|
|
3
|
-
import { internal } from "./tools/index.js";
|
|
4
|
-
import { SELF_MODULE } from "./types.js";
|
|
5
|
-
/** Fatal panic error — bypasses all error boundaries (`?.` and `catch`). */
|
|
6
|
-
export class BridgePanicError extends Error {
|
|
7
|
-
constructor(message) {
|
|
8
|
-
super(message);
|
|
9
|
-
this.name = "BridgePanicError";
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
/** Abort error — raised when an external AbortSignal cancels execution. */
|
|
13
|
-
export class BridgeAbortError extends Error {
|
|
14
|
-
constructor(message = "Execution aborted by external signal") {
|
|
15
|
-
super(message);
|
|
16
|
-
this.name = "BridgeAbortError";
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
/** Sentinel for `continue` — skip the current array element */
|
|
20
|
-
const CONTINUE_SYM = Symbol.for("BRIDGE_CONTINUE");
|
|
21
|
-
/** Sentinel for `break` — halt array iteration */
|
|
22
|
-
const BREAK_SYM = Symbol.for("BRIDGE_BREAK");
|
|
23
|
-
/** Maximum shadow-tree nesting depth before a BridgePanicError is thrown. */
|
|
24
|
-
export const MAX_EXECUTION_DEPTH = 30;
|
|
25
|
-
const otelTracer = trace.getTracer("@stackables/bridge");
|
|
26
|
-
const otelMeter = metrics.getMeter("@stackables/bridge");
|
|
27
|
-
const toolCallCounter = otelMeter.createCounter("bridge.tool.calls", {
|
|
28
|
-
description: "Total number of tool invocations",
|
|
29
|
-
});
|
|
30
|
-
const toolDurationHistogram = otelMeter.createHistogram("bridge.tool.duration", {
|
|
31
|
-
description: "Tool call duration in milliseconds",
|
|
32
|
-
unit: "ms",
|
|
33
|
-
});
|
|
34
|
-
const toolErrorCounter = otelMeter.createCounter("bridge.tool.errors", {
|
|
35
|
-
description: "Total number of tool invocation errors",
|
|
36
|
-
});
|
|
37
|
-
/** Round milliseconds to 2 decimal places */
|
|
38
|
-
function roundMs(ms) {
|
|
39
|
-
return Math.round(ms * 100) / 100;
|
|
40
|
-
}
|
|
41
|
-
/** Stable string key for the state map */
|
|
42
|
-
function trunkKey(ref) {
|
|
43
|
-
if (ref.element)
|
|
44
|
-
return `${ref.module}:${ref.type}:${ref.field}:*`;
|
|
45
|
-
return `${ref.module}:${ref.type}:${ref.field}${ref.instance != null ? `:${ref.instance}` : ""}`;
|
|
46
|
-
}
|
|
47
|
-
/** Match two trunks (ignoring path and element) */
|
|
48
|
-
function sameTrunk(a, b) {
|
|
49
|
-
return (a.module === b.module &&
|
|
50
|
-
a.type === b.type &&
|
|
51
|
-
a.field === b.field &&
|
|
52
|
-
(a.instance ?? undefined) === (b.instance ?? undefined));
|
|
53
|
-
}
|
|
54
|
-
/** Strict path equality */
|
|
55
|
-
function pathEquals(a, b) {
|
|
56
|
-
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
57
|
-
}
|
|
58
|
-
/** Check whether an error is a fatal halt (abort or panic) that must bypass all error boundaries. */
|
|
59
|
-
function isFatalError(err) {
|
|
60
|
-
return (err instanceof BridgePanicError ||
|
|
61
|
-
err instanceof BridgeAbortError ||
|
|
62
|
-
err?.name === "BridgeAbortError" ||
|
|
63
|
-
err?.name === "BridgePanicError");
|
|
64
|
-
}
|
|
65
|
-
/** Execute a control flow instruction, returning a sentinel or throwing. */
|
|
66
|
-
function applyControlFlow(ctrl) {
|
|
67
|
-
if (ctrl.kind === "throw")
|
|
68
|
-
throw new Error(ctrl.message);
|
|
69
|
-
if (ctrl.kind === "panic")
|
|
70
|
-
throw new BridgePanicError(ctrl.message);
|
|
71
|
-
if (ctrl.kind === "continue")
|
|
72
|
-
return CONTINUE_SYM;
|
|
73
|
-
/* ctrl.kind === "break" */
|
|
74
|
-
return BREAK_SYM;
|
|
75
|
-
}
|
|
76
|
-
/** Shared trace collector — one per request, passed through the tree. */
|
|
77
|
-
export class TraceCollector {
|
|
78
|
-
traces = [];
|
|
79
|
-
level;
|
|
80
|
-
epoch = performance.now();
|
|
81
|
-
constructor(level = "full") {
|
|
82
|
-
this.level = level;
|
|
83
|
-
}
|
|
84
|
-
/** Returns ms since the collector was created */
|
|
85
|
-
now() {
|
|
86
|
-
return roundMs(performance.now() - this.epoch);
|
|
87
|
-
}
|
|
88
|
-
record(trace) {
|
|
89
|
-
this.traces.push(trace);
|
|
90
|
-
}
|
|
91
|
-
/** Build a trace entry, omitting input/output for basic level. */
|
|
92
|
-
entry(base) {
|
|
93
|
-
if (this.level === "basic") {
|
|
94
|
-
const t = {
|
|
95
|
-
tool: base.tool,
|
|
96
|
-
fn: base.fn,
|
|
97
|
-
durationMs: base.durationMs,
|
|
98
|
-
startedAt: base.startedAt,
|
|
99
|
-
};
|
|
100
|
-
if (base.error)
|
|
101
|
-
t.error = base.error;
|
|
102
|
-
return t;
|
|
103
|
-
}
|
|
104
|
-
// full
|
|
105
|
-
const t = {
|
|
106
|
-
tool: base.tool,
|
|
107
|
-
fn: base.fn,
|
|
108
|
-
durationMs: base.durationMs,
|
|
109
|
-
startedAt: base.startedAt,
|
|
110
|
-
};
|
|
111
|
-
if (base.input)
|
|
112
|
-
t.input = structuredClone(base.input);
|
|
113
|
-
if (base.error)
|
|
114
|
-
t.error = base.error;
|
|
115
|
-
else if (base.output !== undefined)
|
|
116
|
-
t.output = base.output;
|
|
117
|
-
return t;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
/** Set a value at a nested path, creating intermediate objects/arrays as needed */
|
|
121
|
-
/**
|
|
122
|
-
* Coerce a constant wire value string to its proper JS type.
|
|
123
|
-
*
|
|
124
|
-
* The parser stores all bare constants as strings (because the Wire type
|
|
125
|
-
* uses `value: string`). JSON.parse recovers the original type:
|
|
126
|
-
* "true" → true, "false" → false, "null" → null, "42" → 42
|
|
127
|
-
* Plain strings that aren't valid JSON (like "hello", "/search") fall
|
|
128
|
-
* through and are returned as-is.
|
|
129
|
-
*/
|
|
130
|
-
function coerceConstant(raw) {
|
|
131
|
-
try {
|
|
132
|
-
return JSON.parse(raw);
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
return raw;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
139
|
-
function setNested(obj, path, value) {
|
|
140
|
-
for (let i = 0; i < path.length - 1; i++) {
|
|
141
|
-
const key = path[i];
|
|
142
|
-
if (UNSAFE_KEYS.has(key))
|
|
143
|
-
throw new Error(`Unsafe assignment key: ${key}`);
|
|
144
|
-
const nextKey = path[i + 1];
|
|
145
|
-
if (obj[key] == null) {
|
|
146
|
-
obj[key] = /^\d+$/.test(nextKey) ? [] : {};
|
|
147
|
-
}
|
|
148
|
-
obj = obj[key];
|
|
149
|
-
}
|
|
150
|
-
if (path.length > 0) {
|
|
151
|
-
const finalKey = path[path.length - 1];
|
|
152
|
-
if (UNSAFE_KEYS.has(finalKey))
|
|
153
|
-
throw new Error(`Unsafe assignment key: ${finalKey}`);
|
|
154
|
-
obj[finalKey] = value;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
export class ExecutionTree {
|
|
158
|
-
trunk;
|
|
159
|
-
document;
|
|
160
|
-
context;
|
|
161
|
-
parent;
|
|
162
|
-
state = {};
|
|
163
|
-
bridge;
|
|
164
|
-
toolDepCache = new Map();
|
|
165
|
-
toolDefCache = new Map();
|
|
166
|
-
pipeHandleMap;
|
|
167
|
-
/**
|
|
168
|
-
* Maps trunk keys to `@version` strings from handle bindings.
|
|
169
|
-
* Populated in the constructor so `schedule()` can prefer versioned
|
|
170
|
-
* tool lookups (e.g. `std.str.toLowerCase@999.1`) over the default.
|
|
171
|
-
*/
|
|
172
|
-
handleVersionMap = new Map();
|
|
173
|
-
/** Promise that resolves when all critical `force` handles have settled. */
|
|
174
|
-
forcedExecution;
|
|
175
|
-
/** Shared trace collector — present only when tracing is enabled. */
|
|
176
|
-
tracer;
|
|
177
|
-
/** Structured logger passed from BridgeOptions. Defaults to no-ops. */
|
|
178
|
-
logger;
|
|
179
|
-
/** External abort signal — cancels execution when triggered. */
|
|
180
|
-
signal;
|
|
181
|
-
toolFns;
|
|
182
|
-
/** Shadow-tree nesting depth (0 for root). */
|
|
183
|
-
depth;
|
|
184
|
-
constructor(trunk, document, toolFns, context, parent) {
|
|
185
|
-
this.trunk = trunk;
|
|
186
|
-
this.document = document;
|
|
187
|
-
this.context = context;
|
|
188
|
-
this.parent = parent;
|
|
189
|
-
this.depth = parent ? parent.depth + 1 : 0;
|
|
190
|
-
if (this.depth > MAX_EXECUTION_DEPTH) {
|
|
191
|
-
throw new BridgePanicError(`Maximum execution depth exceeded (${this.depth}) at ${trunkKey(trunk)}. Check for infinite recursion or circular array mappings.`);
|
|
192
|
-
}
|
|
193
|
-
this.toolFns = { internal, ...(toolFns ?? {}) };
|
|
194
|
-
const instructions = document.instructions;
|
|
195
|
-
this.bridge = instructions.find((i) => i.kind === "bridge" && i.type === trunk.type && i.field === trunk.field);
|
|
196
|
-
if (this.bridge?.pipeHandles) {
|
|
197
|
-
this.pipeHandleMap = new Map(this.bridge.pipeHandles.map((ph) => [ph.key, ph]));
|
|
198
|
-
}
|
|
199
|
-
// Build handle→version map from bridge handle bindings
|
|
200
|
-
if (this.bridge) {
|
|
201
|
-
const instanceCounters = new Map();
|
|
202
|
-
for (const h of this.bridge.handles) {
|
|
203
|
-
if (h.kind !== "tool")
|
|
204
|
-
continue;
|
|
205
|
-
const name = h.name;
|
|
206
|
-
const lastDot = name.lastIndexOf(".");
|
|
207
|
-
let module, field, counterKey, type;
|
|
208
|
-
if (lastDot !== -1) {
|
|
209
|
-
module = name.substring(0, lastDot);
|
|
210
|
-
field = name.substring(lastDot + 1);
|
|
211
|
-
counterKey = `${module}:${field}`;
|
|
212
|
-
type = this.trunk.type;
|
|
213
|
-
}
|
|
214
|
-
else {
|
|
215
|
-
module = SELF_MODULE;
|
|
216
|
-
field = name;
|
|
217
|
-
counterKey = `Tools:${name}`;
|
|
218
|
-
type = "Tools";
|
|
219
|
-
}
|
|
220
|
-
const instance = (instanceCounters.get(counterKey) ?? 0) + 1;
|
|
221
|
-
instanceCounters.set(counterKey, instance);
|
|
222
|
-
if (h.version) {
|
|
223
|
-
const key = trunkKey({ module, type, field, instance });
|
|
224
|
-
this.handleVersionMap.set(key, h.version);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
if (context) {
|
|
229
|
-
this.state[trunkKey({ module: SELF_MODULE, type: "Context", field: "context" })] = context;
|
|
230
|
-
}
|
|
231
|
-
// Collect const definitions into a single namespace object
|
|
232
|
-
const constObj = {};
|
|
233
|
-
for (const inst of instructions) {
|
|
234
|
-
if (inst.kind === "const") {
|
|
235
|
-
constObj[inst.name] = JSON.parse(inst.value);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
if (Object.keys(constObj).length > 0) {
|
|
239
|
-
this.state[trunkKey({ module: SELF_MODULE, type: "Const", field: "const" })] = constObj;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
/** Derive tool name from a trunk */
|
|
243
|
-
getToolName(target) {
|
|
244
|
-
if (target.module === SELF_MODULE)
|
|
245
|
-
return target.field;
|
|
246
|
-
return `${target.module}.${target.field}`;
|
|
247
|
-
}
|
|
248
|
-
/** Deep-lookup a tool function by dotted name (e.g. "std.str.toUpperCase").
|
|
249
|
-
* Falls back to a flat key lookup for backward compat (e.g. "hereapi.geocode" as literal key). */
|
|
250
|
-
lookupToolFn(name) {
|
|
251
|
-
if (name.includes(".")) {
|
|
252
|
-
// Try namespace traversal first
|
|
253
|
-
const parts = name.split(".");
|
|
254
|
-
let current = this.toolFns;
|
|
255
|
-
for (const part of parts) {
|
|
256
|
-
if (UNSAFE_KEYS.has(part))
|
|
257
|
-
return undefined;
|
|
258
|
-
if (current == null || typeof current !== "object") {
|
|
259
|
-
current = undefined;
|
|
260
|
-
break;
|
|
261
|
-
}
|
|
262
|
-
current = current[part];
|
|
263
|
-
}
|
|
264
|
-
if (typeof current === "function")
|
|
265
|
-
return current;
|
|
266
|
-
// Fall back to flat key (e.g. "hereapi.geocode" as a literal property name)
|
|
267
|
-
const flat = this.toolFns?.[name];
|
|
268
|
-
if (typeof flat === "function")
|
|
269
|
-
return flat;
|
|
270
|
-
// Try versioned namespace keys (e.g. "std.str@999.1" → { toLowerCase })
|
|
271
|
-
// For "std.str.toLowerCase@999.1", check:
|
|
272
|
-
// toolFns["std.str@999.1"]?.toLowerCase
|
|
273
|
-
// toolFns["std@999.1"]?.str?.toLowerCase
|
|
274
|
-
const atIdx = name.lastIndexOf("@");
|
|
275
|
-
if (atIdx > 0) {
|
|
276
|
-
const baseName = name.substring(0, atIdx);
|
|
277
|
-
const version = name.substring(atIdx + 1);
|
|
278
|
-
const nameParts = baseName.split(".");
|
|
279
|
-
for (let i = nameParts.length - 1; i >= 1; i--) {
|
|
280
|
-
const nsKey = nameParts.slice(0, i).join(".") + "@" + version;
|
|
281
|
-
const remainder = nameParts.slice(i);
|
|
282
|
-
let ns = this.toolFns?.[nsKey];
|
|
283
|
-
if (ns != null && typeof ns === "object") {
|
|
284
|
-
for (const part of remainder) {
|
|
285
|
-
if (ns == null || typeof ns !== "object") {
|
|
286
|
-
ns = undefined;
|
|
287
|
-
break;
|
|
288
|
-
}
|
|
289
|
-
ns = ns[part];
|
|
290
|
-
}
|
|
291
|
-
if (typeof ns === "function")
|
|
292
|
-
return ns;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
return undefined;
|
|
297
|
-
}
|
|
298
|
-
// Try root level first
|
|
299
|
-
const fn = this.toolFns?.[name];
|
|
300
|
-
if (typeof fn === "function")
|
|
301
|
-
return fn;
|
|
302
|
-
// Fall back to std namespace (builtins are callable without std. prefix)
|
|
303
|
-
const stdFn = this.toolFns?.std?.[name];
|
|
304
|
-
if (typeof stdFn === "function")
|
|
305
|
-
return stdFn;
|
|
306
|
-
// Fall back to internal namespace (engine-internal tools: math ops, concat, etc.)
|
|
307
|
-
const internalFn = this.toolFns?.internal?.[name];
|
|
308
|
-
return typeof internalFn === "function" ? internalFn : undefined;
|
|
309
|
-
}
|
|
310
|
-
/** Resolve a ToolDef by name, merging the extends chain (cached) */
|
|
311
|
-
resolveToolDefByName(name) {
|
|
312
|
-
if (this.toolDefCache.has(name))
|
|
313
|
-
return this.toolDefCache.get(name) ?? undefined;
|
|
314
|
-
const toolDefs = this.document.instructions.filter((i) => i.kind === "tool");
|
|
315
|
-
const base = toolDefs.find((t) => t.name === name);
|
|
316
|
-
if (!base) {
|
|
317
|
-
this.toolDefCache.set(name, null);
|
|
318
|
-
return undefined;
|
|
319
|
-
}
|
|
320
|
-
// Build extends chain: root → ... → leaf
|
|
321
|
-
const chain = [base];
|
|
322
|
-
let current = base;
|
|
323
|
-
while (current.extends) {
|
|
324
|
-
const parent = toolDefs.find((t) => t.name === current.extends);
|
|
325
|
-
if (!parent)
|
|
326
|
-
throw new Error(`Tool "${current.name}" extends unknown tool "${current.extends}"`);
|
|
327
|
-
chain.unshift(parent);
|
|
328
|
-
current = parent;
|
|
329
|
-
}
|
|
330
|
-
// Merge: root provides base, each child overrides
|
|
331
|
-
const merged = {
|
|
332
|
-
kind: "tool",
|
|
333
|
-
name,
|
|
334
|
-
fn: chain[0].fn, // fn from root ancestor
|
|
335
|
-
deps: [],
|
|
336
|
-
wires: [],
|
|
337
|
-
};
|
|
338
|
-
for (const def of chain) {
|
|
339
|
-
// Merge deps (dedupe by handle)
|
|
340
|
-
for (const dep of def.deps) {
|
|
341
|
-
if (!merged.deps.some((d) => d.handle === dep.handle)) {
|
|
342
|
-
merged.deps.push(dep);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
// Merge wires (child overrides parent by target; onError replaces onError)
|
|
346
|
-
for (const wire of def.wires) {
|
|
347
|
-
if (wire.kind === "onError") {
|
|
348
|
-
const idx = merged.wires.findIndex((w) => w.kind === "onError");
|
|
349
|
-
if (idx >= 0)
|
|
350
|
-
merged.wires[idx] = wire;
|
|
351
|
-
else
|
|
352
|
-
merged.wires.push(wire);
|
|
353
|
-
}
|
|
354
|
-
else {
|
|
355
|
-
const idx = merged.wires.findIndex((w) => "target" in w && w.target === wire.target);
|
|
356
|
-
if (idx >= 0)
|
|
357
|
-
merged.wires[idx] = wire;
|
|
358
|
-
else
|
|
359
|
-
merged.wires.push(wire);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
this.toolDefCache.set(name, merged);
|
|
364
|
-
return merged;
|
|
365
|
-
}
|
|
366
|
-
/** Resolve a tool definition's wires into a nested input object */
|
|
367
|
-
async resolveToolWires(toolDef, input) {
|
|
368
|
-
// Constants applied synchronously
|
|
369
|
-
for (const wire of toolDef.wires) {
|
|
370
|
-
if (wire.kind === "constant") {
|
|
371
|
-
setNested(input, parsePath(wire.target), coerceConstant(wire.value));
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
// Pull wires resolved in parallel (independent deps shouldn't wait on each other)
|
|
375
|
-
const pullWires = toolDef.wires.filter((w) => w.kind === "pull");
|
|
376
|
-
if (pullWires.length > 0) {
|
|
377
|
-
const resolved = await Promise.all(pullWires.map(async (wire) => ({
|
|
378
|
-
target: wire.target,
|
|
379
|
-
value: await this.resolveToolSource(wire.source, toolDef),
|
|
380
|
-
})));
|
|
381
|
-
for (const { target, value } of resolved) {
|
|
382
|
-
setNested(input, parsePath(target), value);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
/** Resolve a source reference from a tool wire against its dependencies */
|
|
387
|
-
async resolveToolSource(source, toolDef) {
|
|
388
|
-
const dotIdx = source.indexOf(".");
|
|
389
|
-
const handle = dotIdx === -1 ? source : source.substring(0, dotIdx);
|
|
390
|
-
const restPath = dotIdx === -1 ? [] : source.substring(dotIdx + 1).split(".");
|
|
391
|
-
const dep = toolDef.deps.find((d) => d.handle === handle);
|
|
392
|
-
if (!dep)
|
|
393
|
-
throw new Error(`Unknown source "${handle}" in tool "${toolDef.name}"`);
|
|
394
|
-
let value;
|
|
395
|
-
if (dep.kind === "context") {
|
|
396
|
-
// Walk the full parent chain for context
|
|
397
|
-
let cursor = this;
|
|
398
|
-
while (cursor && value === undefined) {
|
|
399
|
-
value = cursor.context;
|
|
400
|
-
cursor = cursor.parent;
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
else if (dep.kind === "const") {
|
|
404
|
-
// Walk the full parent chain for const state
|
|
405
|
-
const constKey = trunkKey({
|
|
406
|
-
module: SELF_MODULE,
|
|
407
|
-
type: "Const",
|
|
408
|
-
field: "const",
|
|
409
|
-
});
|
|
410
|
-
let cursor = this;
|
|
411
|
-
while (cursor && value === undefined) {
|
|
412
|
-
value = cursor.state[constKey];
|
|
413
|
-
cursor = cursor.parent;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
else if (dep.kind === "tool") {
|
|
417
|
-
value = await this.resolveToolDep(dep.tool);
|
|
418
|
-
}
|
|
419
|
-
for (const segment of restPath) {
|
|
420
|
-
value = value?.[segment];
|
|
421
|
-
}
|
|
422
|
-
return value;
|
|
423
|
-
}
|
|
424
|
-
/** Call a tool dependency (cached per request) */
|
|
425
|
-
resolveToolDep(toolName) {
|
|
426
|
-
// Check parent first (shadow trees delegate)
|
|
427
|
-
if (this.parent)
|
|
428
|
-
return this.parent.resolveToolDep(toolName);
|
|
429
|
-
if (this.toolDepCache.has(toolName))
|
|
430
|
-
return this.toolDepCache.get(toolName);
|
|
431
|
-
const promise = (async () => {
|
|
432
|
-
const toolDef = this.resolveToolDefByName(toolName);
|
|
433
|
-
if (!toolDef)
|
|
434
|
-
throw new Error(`Tool dependency "${toolName}" not found`);
|
|
435
|
-
const input = {};
|
|
436
|
-
await this.resolveToolWires(toolDef, input);
|
|
437
|
-
const fn = this.lookupToolFn(toolDef.fn);
|
|
438
|
-
if (!fn)
|
|
439
|
-
throw new Error(`Tool function "${toolDef.fn}" not registered`);
|
|
440
|
-
// on error: wrap the tool call with fallback from onError wire
|
|
441
|
-
const onErrorWire = toolDef.wires.find((w) => w.kind === "onError");
|
|
442
|
-
try {
|
|
443
|
-
return await this.callTool(toolName, toolDef.fn, fn, input);
|
|
444
|
-
}
|
|
445
|
-
catch (err) {
|
|
446
|
-
if (!onErrorWire)
|
|
447
|
-
throw err;
|
|
448
|
-
if ("value" in onErrorWire)
|
|
449
|
-
return JSON.parse(onErrorWire.value);
|
|
450
|
-
return this.resolveToolSource(onErrorWire.source, toolDef);
|
|
451
|
-
}
|
|
452
|
-
})();
|
|
453
|
-
this.toolDepCache.set(toolName, promise);
|
|
454
|
-
return promise;
|
|
455
|
-
}
|
|
456
|
-
schedule(target, pullChain) {
|
|
457
|
-
// Delegate to parent (shadow trees don't schedule directly) unless
|
|
458
|
-
// the target fork has bridge wires sourced from element data,
|
|
459
|
-
// or a __local binding whose source chain touches element data.
|
|
460
|
-
if (this.parent) {
|
|
461
|
-
const forkWires = this.bridge?.wires.filter((w) => sameTrunk(w.to, target)) ?? [];
|
|
462
|
-
const hasElementSource = forkWires.some((w) => ("from" in w && !!w.from.element) ||
|
|
463
|
-
("condAnd" in w &&
|
|
464
|
-
(!!w.condAnd.leftRef.element || !!w.condAnd.rightRef?.element)) ||
|
|
465
|
-
("condOr" in w &&
|
|
466
|
-
(!!w.condOr.leftRef.element || !!w.condOr.rightRef?.element)));
|
|
467
|
-
// For __local trunks, also check transitively: if the source is a
|
|
468
|
-
// pipe fork whose own wires reference element data, keep it local.
|
|
469
|
-
const hasTransitiveElementSource = target.module === "__local" &&
|
|
470
|
-
forkWires.some((w) => {
|
|
471
|
-
if (!("from" in w))
|
|
472
|
-
return false;
|
|
473
|
-
const srcTrunk = {
|
|
474
|
-
module: w.from.module,
|
|
475
|
-
type: w.from.type,
|
|
476
|
-
field: w.from.field,
|
|
477
|
-
instance: w.from.instance,
|
|
478
|
-
};
|
|
479
|
-
return (this.bridge?.wires.some((iw) => sameTrunk(iw.to, srcTrunk) && "from" in iw && !!iw.from.element) ?? false);
|
|
480
|
-
});
|
|
481
|
-
if (!hasElementSource && !hasTransitiveElementSource) {
|
|
482
|
-
return this.parent.schedule(target, pullChain);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
return (async () => {
|
|
486
|
-
// If this target is a pipe fork, also apply bridge wires from its base
|
|
487
|
-
// handle (non-pipe wires, e.g. `c.currency <- i.currency`) as defaults
|
|
488
|
-
// before the fork-specific pipe wires.
|
|
489
|
-
const targetKey = trunkKey(target);
|
|
490
|
-
const pipeFork = this.pipeHandleMap?.get(targetKey);
|
|
491
|
-
const baseTrunk = pipeFork?.baseTrunk;
|
|
492
|
-
const baseWires = baseTrunk
|
|
493
|
-
? (this.bridge?.wires.filter((w) => !("pipe" in w) && sameTrunk(w.to, baseTrunk)) ?? [])
|
|
494
|
-
: [];
|
|
495
|
-
// Fork-specific wires (pipe wires targeting the fork's own instance)
|
|
496
|
-
const forkWires = this.bridge?.wires.filter((w) => sameTrunk(w.to, target)) ?? [];
|
|
497
|
-
// Merge: base provides defaults, fork overrides
|
|
498
|
-
const bridgeWires = [...baseWires, ...forkWires];
|
|
499
|
-
// Look up ToolDef for this target
|
|
500
|
-
const toolName = this.getToolName(target);
|
|
501
|
-
const toolDef = this.resolveToolDefByName(toolName);
|
|
502
|
-
// Build input object: tool wires first (base), then bridge wires (override)
|
|
503
|
-
const input = {};
|
|
504
|
-
if (toolDef) {
|
|
505
|
-
await this.resolveToolWires(toolDef, input);
|
|
506
|
-
}
|
|
507
|
-
// Resolve bridge wires and apply on top.
|
|
508
|
-
// Group wires by target path so that || (null-fallback) and ??
|
|
509
|
-
// (error-fallback) semantics are honoured via resolveWires().
|
|
510
|
-
const wireGroups = new Map();
|
|
511
|
-
for (const w of bridgeWires) {
|
|
512
|
-
const key = w.to.path.join(".");
|
|
513
|
-
let group = wireGroups.get(key);
|
|
514
|
-
if (!group) {
|
|
515
|
-
group = [];
|
|
516
|
-
wireGroups.set(key, group);
|
|
517
|
-
}
|
|
518
|
-
group.push(w);
|
|
519
|
-
}
|
|
520
|
-
const groupEntries = Array.from(wireGroups.entries());
|
|
521
|
-
const resolved = await Promise.all(groupEntries.map(async ([, group]) => {
|
|
522
|
-
const value = await this.resolveWires(group, pullChain);
|
|
523
|
-
return [group[0].to.path, value];
|
|
524
|
-
}));
|
|
525
|
-
for (const [path, value] of resolved) {
|
|
526
|
-
if (path.length === 0 && value != null && typeof value === "object") {
|
|
527
|
-
Object.assign(input, value);
|
|
528
|
-
}
|
|
529
|
-
else {
|
|
530
|
-
setNested(input, path, value);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
// Call ToolDef-backed tool function
|
|
534
|
-
if (toolDef) {
|
|
535
|
-
const fn = this.lookupToolFn(toolDef.fn);
|
|
536
|
-
if (!fn)
|
|
537
|
-
throw new Error(`Tool function "${toolDef.fn}" not registered`);
|
|
538
|
-
// on error: wrap the tool call with fallback from onError wire
|
|
539
|
-
const onErrorWire = toolDef.wires.find((w) => w.kind === "onError");
|
|
540
|
-
try {
|
|
541
|
-
return await this.callTool(toolName, toolDef.fn, fn, input);
|
|
542
|
-
}
|
|
543
|
-
catch (err) {
|
|
544
|
-
if (!onErrorWire)
|
|
545
|
-
throw err;
|
|
546
|
-
if ("value" in onErrorWire)
|
|
547
|
-
return JSON.parse(onErrorWire.value);
|
|
548
|
-
return this.resolveToolSource(onErrorWire.source, toolDef);
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
// Direct tool function lookup by name (simple or dotted).
|
|
552
|
-
// When the handle carries a @version tag, try the versioned key first
|
|
553
|
-
// (e.g. "std.str.toLowerCase@999.1") so user-injected overrides win.
|
|
554
|
-
// For pipe forks, fall back to the baseTrunk's version since forks
|
|
555
|
-
// use synthetic instance numbers (100000+).
|
|
556
|
-
const handleVersion = this.handleVersionMap.get(trunkKey(target)) ??
|
|
557
|
-
(baseTrunk
|
|
558
|
-
? this.handleVersionMap.get(trunkKey(baseTrunk))
|
|
559
|
-
: undefined);
|
|
560
|
-
let directFn = handleVersion
|
|
561
|
-
? this.lookupToolFn(`${toolName}@${handleVersion}`)
|
|
562
|
-
: undefined;
|
|
563
|
-
if (!directFn) {
|
|
564
|
-
directFn = this.lookupToolFn(toolName);
|
|
565
|
-
}
|
|
566
|
-
if (directFn) {
|
|
567
|
-
return this.callTool(toolName, toolName, directFn, input);
|
|
568
|
-
}
|
|
569
|
-
// Define pass-through: synthetic trunks created by define inlining
|
|
570
|
-
// act as data containers — bridge wires set their values, no tool needed.
|
|
571
|
-
if (target.module.startsWith("__define_")) {
|
|
572
|
-
return input;
|
|
573
|
-
}
|
|
574
|
-
// Local binding or logic node: the wire resolves the source and stores
|
|
575
|
-
// the result — no tool call needed. For path=[] wires the resolved
|
|
576
|
-
// value may be a primitive (boolean from condAnd/condOr, string from
|
|
577
|
-
// a pipe tool like upperCase), so return the resolved value directly.
|
|
578
|
-
if (target.module === "__local" ||
|
|
579
|
-
target.field === "__and" ||
|
|
580
|
-
target.field === "__or") {
|
|
581
|
-
for (const [path, value] of resolved) {
|
|
582
|
-
if (path.length === 0)
|
|
583
|
-
return value;
|
|
584
|
-
}
|
|
585
|
-
return input;
|
|
586
|
-
}
|
|
587
|
-
throw new Error(`No tool found for "${toolName}"`);
|
|
588
|
-
})();
|
|
589
|
-
}
|
|
590
|
-
/**
|
|
591
|
-
* Invoke a tool function, recording both an OpenTelemetry span and (when
|
|
592
|
-
* tracing is enabled) a ToolTrace entry. All three tool-call sites in the
|
|
593
|
-
* engine delegate here so instrumentation lives in exactly one place.
|
|
594
|
-
*/
|
|
595
|
-
async callTool(toolName, fnName, fnImpl, input) {
|
|
596
|
-
// Short-circuit before starting if externally aborted
|
|
597
|
-
if (this.signal?.aborted) {
|
|
598
|
-
throw new BridgeAbortError();
|
|
599
|
-
}
|
|
600
|
-
const tracer = this.tracer;
|
|
601
|
-
const logger = this.logger;
|
|
602
|
-
const toolContext = {
|
|
603
|
-
logger: logger ?? {},
|
|
604
|
-
signal: this.signal,
|
|
605
|
-
};
|
|
606
|
-
const traceStart = tracer?.now();
|
|
607
|
-
const metricAttrs = {
|
|
608
|
-
"bridge.tool.name": toolName,
|
|
609
|
-
"bridge.tool.fn": fnName,
|
|
610
|
-
};
|
|
611
|
-
return otelTracer.startActiveSpan(`bridge.tool.${toolName}.${fnName}`, { attributes: metricAttrs }, async (span) => {
|
|
612
|
-
const wallStart = performance.now();
|
|
613
|
-
try {
|
|
614
|
-
const result = await fnImpl(input, toolContext);
|
|
615
|
-
const durationMs = roundMs(performance.now() - wallStart);
|
|
616
|
-
toolCallCounter.add(1, metricAttrs);
|
|
617
|
-
toolDurationHistogram.record(durationMs, metricAttrs);
|
|
618
|
-
if (tracer && traceStart != null) {
|
|
619
|
-
tracer.record(tracer.entry({
|
|
620
|
-
tool: toolName,
|
|
621
|
-
fn: fnName,
|
|
622
|
-
input,
|
|
623
|
-
output: result,
|
|
624
|
-
durationMs: roundMs(tracer.now() - traceStart),
|
|
625
|
-
startedAt: traceStart,
|
|
626
|
-
}));
|
|
627
|
-
}
|
|
628
|
-
logger?.debug?.("[bridge] tool %s (%s) completed in %dms", toolName, fnName, durationMs);
|
|
629
|
-
return result;
|
|
630
|
-
}
|
|
631
|
-
catch (err) {
|
|
632
|
-
const durationMs = roundMs(performance.now() - wallStart);
|
|
633
|
-
toolCallCounter.add(1, metricAttrs);
|
|
634
|
-
toolDurationHistogram.record(durationMs, metricAttrs);
|
|
635
|
-
toolErrorCounter.add(1, metricAttrs);
|
|
636
|
-
if (tracer && traceStart != null) {
|
|
637
|
-
tracer.record(tracer.entry({
|
|
638
|
-
tool: toolName,
|
|
639
|
-
fn: fnName,
|
|
640
|
-
input,
|
|
641
|
-
error: err.message,
|
|
642
|
-
durationMs: roundMs(tracer.now() - traceStart),
|
|
643
|
-
startedAt: traceStart,
|
|
644
|
-
}));
|
|
645
|
-
}
|
|
646
|
-
span.recordException(err);
|
|
647
|
-
span.setStatus({
|
|
648
|
-
code: SpanStatusCode.ERROR,
|
|
649
|
-
message: err.message,
|
|
650
|
-
});
|
|
651
|
-
logger?.error?.("[bridge] tool %s (%s) failed: %s", toolName, fnName, err.message);
|
|
652
|
-
throw err;
|
|
653
|
-
}
|
|
654
|
-
finally {
|
|
655
|
-
span.end();
|
|
656
|
-
}
|
|
657
|
-
});
|
|
658
|
-
}
|
|
659
|
-
shadow() {
|
|
660
|
-
const child = new ExecutionTree(this.trunk, this.document, this.toolFns, undefined, this);
|
|
661
|
-
child.tracer = this.tracer;
|
|
662
|
-
child.logger = this.logger;
|
|
663
|
-
child.signal = this.signal;
|
|
664
|
-
return child;
|
|
665
|
-
}
|
|
666
|
-
/** Returns collected traces (empty array when tracing is disabled). */
|
|
667
|
-
getTraces() {
|
|
668
|
-
return this.tracer?.traces ?? [];
|
|
669
|
-
}
|
|
670
|
-
async pullSingle(ref, pullChain = new Set()) {
|
|
671
|
-
const key = trunkKey(ref);
|
|
672
|
-
// ── Cycle detection ─────────────────────────────────────────────
|
|
673
|
-
// If this exact key is already in our active pull chain, it is a
|
|
674
|
-
// circular dependency that would deadlock (await-on-self).
|
|
675
|
-
if (pullChain.has(key)) {
|
|
676
|
-
throw new BridgePanicError(`Circular dependency detected: "${key}" depends on itself`);
|
|
677
|
-
}
|
|
678
|
-
// Walk the full parent chain — shadow trees may be nested multiple levels
|
|
679
|
-
let value = undefined;
|
|
680
|
-
let cursor = this;
|
|
681
|
-
while (cursor && value === undefined) {
|
|
682
|
-
value = cursor.state[key];
|
|
683
|
-
cursor = cursor.parent;
|
|
684
|
-
}
|
|
685
|
-
if (value === undefined) {
|
|
686
|
-
const nextChain = new Set(pullChain).add(key);
|
|
687
|
-
// ── Lazy define field resolution ────────────────────────────────
|
|
688
|
-
// For define trunks (__define_in_* / __define_out_*) with a specific
|
|
689
|
-
// field path, resolve ONLY the wire(s) targeting that field instead
|
|
690
|
-
// of scheduling the entire trunk. This avoids triggering unrelated
|
|
691
|
-
// dependency chains (e.g. requesting "city" should not fire the
|
|
692
|
-
// lat/lon coalesce chains that call the geo tool).
|
|
693
|
-
if (ref.path.length > 0 && ref.module.startsWith("__define_")) {
|
|
694
|
-
const fieldWires = this.bridge?.wires.filter((w) => sameTrunk(w.to, ref) && pathEquals(w.to.path, ref.path)) ?? [];
|
|
695
|
-
if (fieldWires.length > 0) {
|
|
696
|
-
return this.resolveWires(fieldWires, nextChain);
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
this.state[key] = this.schedule(ref, nextChain);
|
|
700
|
-
value = this.state[key];
|
|
701
|
-
}
|
|
702
|
-
// Always await in case the stored value is a Promise (e.g. from schedule()).
|
|
703
|
-
const resolved = await Promise.resolve(value);
|
|
704
|
-
if (!ref.path.length) {
|
|
705
|
-
return resolved;
|
|
706
|
-
}
|
|
707
|
-
let result = resolved;
|
|
708
|
-
// Root-level null check: if root data is null/undefined
|
|
709
|
-
if (result == null && ref.path.length > 0) {
|
|
710
|
-
if (ref.rootSafe)
|
|
711
|
-
return undefined;
|
|
712
|
-
throw new TypeError(`Cannot read properties of ${result} (reading '${ref.path[0]}')`);
|
|
713
|
-
}
|
|
714
|
-
for (let i = 0; i < ref.path.length; i++) {
|
|
715
|
-
const segment = ref.path[i];
|
|
716
|
-
if (UNSAFE_KEYS.has(segment))
|
|
717
|
-
throw new Error(`Unsafe property traversal: ${segment}`);
|
|
718
|
-
if (Array.isArray(result) && !/^\d+$/.test(segment)) {
|
|
719
|
-
this.logger?.warn?.(`[bridge] Accessing ".${segment}" on an array (${result.length} items) — did you mean to use pickFirst or array mapping? Source: ${trunkKey(ref)}.${ref.path.join(".")}`);
|
|
720
|
-
}
|
|
721
|
-
result = result[segment];
|
|
722
|
-
// Check for null/undefined AFTER access, before next segment
|
|
723
|
-
if (result == null && i < ref.path.length - 1) {
|
|
724
|
-
const nextSafe = ref.pathSafe?.[i + 1] ?? false;
|
|
725
|
-
if (nextSafe)
|
|
726
|
-
return undefined;
|
|
727
|
-
throw new TypeError(`Cannot read properties of ${result} (reading '${ref.path[i + 1]}')`);
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
return result;
|
|
731
|
-
}
|
|
732
|
-
push(args) {
|
|
733
|
-
this.state[trunkKey(this.trunk)] = args;
|
|
734
|
-
}
|
|
735
|
-
/** Store the aggregated promise for critical forced handles so
|
|
736
|
-
* `response()` can await it exactly once per bridge execution. */
|
|
737
|
-
setForcedExecution(p) {
|
|
738
|
-
this.forcedExecution = p;
|
|
739
|
-
}
|
|
740
|
-
/** Return the critical forced-execution promise (if any). */
|
|
741
|
-
getForcedExecution() {
|
|
742
|
-
return this.forcedExecution;
|
|
743
|
-
}
|
|
744
|
-
/**
|
|
745
|
-
* Eagerly schedule tools targeted by `force <handle>` statements.
|
|
746
|
-
*
|
|
747
|
-
* Returns an array of promises for **critical** forced handles (those
|
|
748
|
-
* without `?? null`). Fire-and-forget handles (`catchError: true`) are
|
|
749
|
-
* scheduled but their errors are silently suppressed.
|
|
750
|
-
*
|
|
751
|
-
* Callers must `await Promise.all(...)` the returned promises so that a
|
|
752
|
-
* critical force failure propagates as a standard error.
|
|
753
|
-
*/
|
|
754
|
-
executeForced() {
|
|
755
|
-
const forces = this.bridge?.forces;
|
|
756
|
-
if (!forces || forces.length === 0)
|
|
757
|
-
return [];
|
|
758
|
-
const critical = [];
|
|
759
|
-
const scheduled = new Set();
|
|
760
|
-
for (const f of forces) {
|
|
761
|
-
const trunk = {
|
|
762
|
-
module: f.module,
|
|
763
|
-
type: f.type,
|
|
764
|
-
field: f.field,
|
|
765
|
-
instance: f.instance,
|
|
766
|
-
};
|
|
767
|
-
const key = trunkKey(trunk);
|
|
768
|
-
if (scheduled.has(key) || this.state[key] !== undefined)
|
|
769
|
-
continue;
|
|
770
|
-
scheduled.add(key);
|
|
771
|
-
this.state[key] = this.schedule(trunk);
|
|
772
|
-
if (f.catchError) {
|
|
773
|
-
// Fire-and-forget: suppress unhandled rejection.
|
|
774
|
-
Promise.resolve(this.state[key]).catch(() => { });
|
|
775
|
-
}
|
|
776
|
-
else {
|
|
777
|
-
// Critical: caller must await and let failure propagate.
|
|
778
|
-
critical.push(Promise.resolve(this.state[key]));
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
return critical;
|
|
782
|
-
}
|
|
783
|
-
/**
|
|
784
|
-
* Resolve a set of matched wires.
|
|
785
|
-
*
|
|
786
|
-
* Architecture: two distinct resolution axes —
|
|
787
|
-
*
|
|
788
|
-
* **Falsy Gate** (`||`, within a wire): `falsyFallbackRefs` + `falsyFallback`
|
|
789
|
-
* → truthy check — falsy values (0, "", false) trigger fallback chain.
|
|
790
|
-
*
|
|
791
|
-
* **Overdefinition** (across wires): multiple wires target the same path
|
|
792
|
-
* → nullish check — only null/undefined falls through to the next wire.
|
|
793
|
-
*
|
|
794
|
-
* Per-wire layers:
|
|
795
|
-
* Layer 1 — Execution (pullSingle + safe modifier)
|
|
796
|
-
* Layer 2a — Falsy Gate (falsyFallbackRefs → falsyFallback / falsyControl)
|
|
797
|
-
* Layer 2b — Nullish Gate (nullishFallbackRef / nullishFallback / nullishControl)
|
|
798
|
-
* Layer 3 — Catch (catchFallbackRef / catchFallback / catchControl)
|
|
799
|
-
*
|
|
800
|
-
* After layers 1–2b, the overdefinition boundary (`!= null`) decides whether
|
|
801
|
-
* to return or continue to the next wire.
|
|
802
|
-
*/
|
|
803
|
-
async resolveWires(wires, pullChain) {
|
|
804
|
-
let lastError;
|
|
805
|
-
for (const w of wires) {
|
|
806
|
-
// Constant wire — always wins, no modifiers
|
|
807
|
-
if ("value" in w)
|
|
808
|
-
return coerceConstant(w.value);
|
|
809
|
-
try {
|
|
810
|
-
// --- Layer 1: Execution ---
|
|
811
|
-
let resolvedValue;
|
|
812
|
-
if ("cond" in w) {
|
|
813
|
-
const condValue = await this.pullSingle(w.cond, pullChain);
|
|
814
|
-
if (condValue) {
|
|
815
|
-
if (w.thenRef !== undefined)
|
|
816
|
-
resolvedValue = await this.pullSingle(w.thenRef, pullChain);
|
|
817
|
-
else if (w.thenValue !== undefined)
|
|
818
|
-
resolvedValue = coerceConstant(w.thenValue);
|
|
819
|
-
}
|
|
820
|
-
else {
|
|
821
|
-
if (w.elseRef !== undefined)
|
|
822
|
-
resolvedValue = await this.pullSingle(w.elseRef, pullChain);
|
|
823
|
-
else if (w.elseValue !== undefined)
|
|
824
|
-
resolvedValue = coerceConstant(w.elseValue);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
else if ("condAnd" in w) {
|
|
828
|
-
const { leftRef, rightRef, rightValue, safe: isSafe, rightSafe, } = w.condAnd;
|
|
829
|
-
const leftVal = isSafe
|
|
830
|
-
? await this.pullSingle(leftRef, pullChain).catch((e) => {
|
|
831
|
-
if (isFatalError(e))
|
|
832
|
-
throw e;
|
|
833
|
-
return undefined;
|
|
834
|
-
})
|
|
835
|
-
: await this.pullSingle(leftRef, pullChain);
|
|
836
|
-
if (!leftVal) {
|
|
837
|
-
resolvedValue = false;
|
|
838
|
-
}
|
|
839
|
-
else if (rightRef !== undefined) {
|
|
840
|
-
const rightVal = rightSafe
|
|
841
|
-
? await this.pullSingle(rightRef, pullChain).catch((e) => {
|
|
842
|
-
if (isFatalError(e))
|
|
843
|
-
throw e;
|
|
844
|
-
return undefined;
|
|
845
|
-
})
|
|
846
|
-
: await this.pullSingle(rightRef, pullChain);
|
|
847
|
-
resolvedValue = Boolean(rightVal);
|
|
848
|
-
}
|
|
849
|
-
else if (rightValue !== undefined) {
|
|
850
|
-
resolvedValue = Boolean(coerceConstant(rightValue));
|
|
851
|
-
}
|
|
852
|
-
else {
|
|
853
|
-
resolvedValue = Boolean(leftVal);
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
else if ("condOr" in w) {
|
|
857
|
-
const { leftRef, rightRef, rightValue, safe: isSafe, rightSafe, } = w.condOr;
|
|
858
|
-
const leftVal = isSafe
|
|
859
|
-
? await this.pullSingle(leftRef, pullChain).catch((e) => {
|
|
860
|
-
if (isFatalError(e))
|
|
861
|
-
throw e;
|
|
862
|
-
return undefined;
|
|
863
|
-
})
|
|
864
|
-
: await this.pullSingle(leftRef, pullChain);
|
|
865
|
-
if (leftVal) {
|
|
866
|
-
resolvedValue = true;
|
|
867
|
-
}
|
|
868
|
-
else if (rightRef !== undefined) {
|
|
869
|
-
const rightVal = rightSafe
|
|
870
|
-
? await this.pullSingle(rightRef, pullChain).catch((e) => {
|
|
871
|
-
if (isFatalError(e))
|
|
872
|
-
throw e;
|
|
873
|
-
return undefined;
|
|
874
|
-
})
|
|
875
|
-
: await this.pullSingle(rightRef, pullChain);
|
|
876
|
-
resolvedValue = Boolean(rightVal);
|
|
877
|
-
}
|
|
878
|
-
else if (rightValue !== undefined) {
|
|
879
|
-
resolvedValue = Boolean(coerceConstant(rightValue));
|
|
880
|
-
}
|
|
881
|
-
else {
|
|
882
|
-
resolvedValue = Boolean(leftVal);
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
else if ("from" in w) {
|
|
886
|
-
if (w.safe) {
|
|
887
|
-
try {
|
|
888
|
-
resolvedValue = await this.pullSingle(w.from, pullChain);
|
|
889
|
-
}
|
|
890
|
-
catch (err) {
|
|
891
|
-
if (isFatalError(err))
|
|
892
|
-
throw err;
|
|
893
|
-
resolvedValue = undefined;
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
else {
|
|
897
|
-
resolvedValue = await this.pullSingle(w.from, pullChain);
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
else {
|
|
901
|
-
continue;
|
|
902
|
-
}
|
|
903
|
-
// --- Layer 2a: Falsy Gate (||) ---
|
|
904
|
-
if (!resolvedValue && w.falsyFallbackRefs?.length) {
|
|
905
|
-
for (const ref of w.falsyFallbackRefs) {
|
|
906
|
-
// Assign the fallback value regardless of whether it is truthy or falsy.
|
|
907
|
-
// e.g. `false || 0` will correctly update resolvedValue to `0`.
|
|
908
|
-
resolvedValue = await this.pullSingle(ref, pullChain);
|
|
909
|
-
// If it is truthy, we are done! Short-circuit the || chain.
|
|
910
|
-
if (resolvedValue)
|
|
911
|
-
break;
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
if (!resolvedValue) {
|
|
915
|
-
if (w.falsyControl) {
|
|
916
|
-
resolvedValue = applyControlFlow(w.falsyControl);
|
|
917
|
-
}
|
|
918
|
-
else if (w.falsyFallback != null) {
|
|
919
|
-
resolvedValue = coerceConstant(w.falsyFallback);
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
// --- Layer 2b: Nullish Gate (??) ---
|
|
923
|
-
if (resolvedValue == null) {
|
|
924
|
-
if (w.nullishControl) {
|
|
925
|
-
resolvedValue = applyControlFlow(w.nullishControl);
|
|
926
|
-
}
|
|
927
|
-
else if (w.nullishFallbackRef) {
|
|
928
|
-
resolvedValue = await this.pullSingle(w.nullishFallbackRef, pullChain);
|
|
929
|
-
}
|
|
930
|
-
else if (w.nullishFallback != null) {
|
|
931
|
-
resolvedValue = coerceConstant(w.nullishFallback);
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
// --- Overdefinition Boundary ---
|
|
935
|
-
if (resolvedValue != null)
|
|
936
|
-
return resolvedValue;
|
|
937
|
-
}
|
|
938
|
-
catch (err) {
|
|
939
|
-
// --- Layer 3: Catch ---
|
|
940
|
-
if (isFatalError(err))
|
|
941
|
-
throw err;
|
|
942
|
-
if (w.catchControl)
|
|
943
|
-
return applyControlFlow(w.catchControl);
|
|
944
|
-
if (w.catchFallbackRef)
|
|
945
|
-
return this.pullSingle(w.catchFallbackRef, pullChain);
|
|
946
|
-
if (w.catchFallback != null)
|
|
947
|
-
return coerceConstant(w.catchFallback);
|
|
948
|
-
lastError = err;
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
if (lastError)
|
|
952
|
-
throw lastError;
|
|
953
|
-
return undefined;
|
|
954
|
-
}
|
|
955
|
-
/**
|
|
956
|
-
* Resolve an output field by path for use outside of a GraphQL resolver.
|
|
957
|
-
*
|
|
958
|
-
* This is the non-GraphQL equivalent of what `response()` does per field:
|
|
959
|
-
* it finds all wires targeting `this.trunk` at `path` and resolves them.
|
|
960
|
-
*
|
|
961
|
-
* Used by `executeBridge()` so standalone bridge execution does not need to
|
|
962
|
-
* fabricate GraphQL Path objects to pull output data.
|
|
963
|
-
*
|
|
964
|
-
* @param path - Output field path, e.g. `["lat"]`. Pass `[]` for whole-output
|
|
965
|
-
* array bridges (`o <- items[] as x { ... }`).
|
|
966
|
-
* @param array - When `true` and the result is an array, wraps each element
|
|
967
|
-
* in a shadow tree (mirrors `response()` array handling).
|
|
968
|
-
*/
|
|
969
|
-
async pullOutputField(path, array = false) {
|
|
970
|
-
const matches = this.bridge?.wires.filter((w) => sameTrunk(w.to, this.trunk) && pathEquals(w.to.path, path)) ?? [];
|
|
971
|
-
if (matches.length === 0)
|
|
972
|
-
return undefined;
|
|
973
|
-
const result = this.resolveWires(matches);
|
|
974
|
-
if (!array)
|
|
975
|
-
return result;
|
|
976
|
-
const resolved = await result;
|
|
977
|
-
if (resolved === BREAK_SYM || resolved === CONTINUE_SYM)
|
|
978
|
-
return [];
|
|
979
|
-
const items = resolved;
|
|
980
|
-
const finalShadowTrees = [];
|
|
981
|
-
for (const item of items) {
|
|
982
|
-
if (item === BREAK_SYM)
|
|
983
|
-
break;
|
|
984
|
-
if (item === CONTINUE_SYM)
|
|
985
|
-
continue;
|
|
986
|
-
const s = this.shadow();
|
|
987
|
-
s.state[trunkKey({ ...this.trunk, element: true })] = item;
|
|
988
|
-
finalShadowTrees.push(s);
|
|
989
|
-
}
|
|
990
|
-
return finalShadowTrees;
|
|
991
|
-
}
|
|
992
|
-
/**
|
|
993
|
-
* Execute the bridge end-to-end without GraphQL.
|
|
994
|
-
*
|
|
995
|
-
* Injects `input` as the trunk arguments, runs forced wires, then pulls
|
|
996
|
-
* and materialises every output field into a plain JS object (or array of
|
|
997
|
-
* objects for array-mapped bridges).
|
|
998
|
-
*
|
|
999
|
-
* This is the single entry-point used by `executeBridge()`.
|
|
1000
|
-
*/
|
|
1001
|
-
async run(input) {
|
|
1002
|
-
const bridge = this.bridge;
|
|
1003
|
-
if (!bridge) {
|
|
1004
|
-
throw new Error(`No bridge definition found for ${this.trunk.type}.${this.trunk.field}`);
|
|
1005
|
-
}
|
|
1006
|
-
this.push(input);
|
|
1007
|
-
const forcePromises = this.executeForced();
|
|
1008
|
-
const { type, field } = this.trunk;
|
|
1009
|
-
// Is there a root-level wire targeting the output with path []?
|
|
1010
|
-
const hasRootWire = bridge.wires.some((w) => "from" in w &&
|
|
1011
|
-
w.to.module === SELF_MODULE &&
|
|
1012
|
-
w.to.type === type &&
|
|
1013
|
-
w.to.field === field &&
|
|
1014
|
-
w.to.path.length === 0);
|
|
1015
|
-
// Array-mapped output (`o <- items[] as x { ... }`) has BOTH a root wire
|
|
1016
|
-
// AND element-level wires (from.element === true). A plain passthrough
|
|
1017
|
-
// (`o <- api.user`) only has the root wire.
|
|
1018
|
-
// Local bindings (from.__local) are also element-scoped.
|
|
1019
|
-
// Pipe fork output wires in element context (e.g. concat template strings)
|
|
1020
|
-
// may have to.element === true instead.
|
|
1021
|
-
const hasElementWires = bridge.wires.some((w) => "from" in w &&
|
|
1022
|
-
(w.from.element === true ||
|
|
1023
|
-
w.from.module === "__local" ||
|
|
1024
|
-
w.to.element === true) &&
|
|
1025
|
-
w.to.module === SELF_MODULE &&
|
|
1026
|
-
w.to.type === type &&
|
|
1027
|
-
w.to.field === field);
|
|
1028
|
-
if (hasRootWire && hasElementWires) {
|
|
1029
|
-
const [shadows] = await Promise.all([
|
|
1030
|
-
this.pullOutputField([], true),
|
|
1031
|
-
...forcePromises,
|
|
1032
|
-
]);
|
|
1033
|
-
return this.materializeShadows(shadows, []);
|
|
1034
|
-
}
|
|
1035
|
-
// Whole-object passthrough: `o <- api.user`
|
|
1036
|
-
if (hasRootWire) {
|
|
1037
|
-
const [result] = await Promise.all([
|
|
1038
|
-
this.pullOutputField([]),
|
|
1039
|
-
...forcePromises,
|
|
1040
|
-
]);
|
|
1041
|
-
return result;
|
|
1042
|
-
}
|
|
1043
|
-
// Object output — collect unique top-level field names
|
|
1044
|
-
const outputFields = new Set();
|
|
1045
|
-
for (const wire of bridge.wires) {
|
|
1046
|
-
if (wire.to.module === SELF_MODULE &&
|
|
1047
|
-
wire.to.type === type &&
|
|
1048
|
-
wire.to.field === field &&
|
|
1049
|
-
wire.to.path.length > 0) {
|
|
1050
|
-
outputFields.add(wire.to.path[0]);
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
if (outputFields.size === 0) {
|
|
1054
|
-
throw new Error(`Bridge "${type}.${field}" has no output wires. ` +
|
|
1055
|
-
`Ensure at least one wire targets the output (e.g. \`o.field <- ...\`).`);
|
|
1056
|
-
}
|
|
1057
|
-
const result = {};
|
|
1058
|
-
// Resolves a single output field at `prefix` — either via an exact-match
|
|
1059
|
-
// wire (leaf), or by collecting sub-fields from deeper wires (nested object).
|
|
1060
|
-
const resolveField = async (prefix) => {
|
|
1061
|
-
const exactWires = bridge.wires.filter((w) => w.to.module === SELF_MODULE &&
|
|
1062
|
-
w.to.type === type &&
|
|
1063
|
-
w.to.field === field &&
|
|
1064
|
-
pathEquals(w.to.path, prefix));
|
|
1065
|
-
if (exactWires.length > 0) {
|
|
1066
|
-
return this.resolveWires(exactWires);
|
|
1067
|
-
}
|
|
1068
|
-
// No exact wire — gather sub-field names from deeper-path wires
|
|
1069
|
-
// (e.g. `o.why { .temperature <- ... }` produces path ["why","temperature"])
|
|
1070
|
-
const subFields = new Set();
|
|
1071
|
-
for (const wire of bridge.wires) {
|
|
1072
|
-
const p = wire.to.path;
|
|
1073
|
-
if (wire.to.module === SELF_MODULE &&
|
|
1074
|
-
wire.to.type === type &&
|
|
1075
|
-
wire.to.field === field &&
|
|
1076
|
-
p.length > prefix.length &&
|
|
1077
|
-
prefix.every((seg, i) => p[i] === seg)) {
|
|
1078
|
-
subFields.add(p[prefix.length]);
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
if (subFields.size === 0)
|
|
1082
|
-
return undefined;
|
|
1083
|
-
const obj = {};
|
|
1084
|
-
await Promise.all([...subFields].map(async (sub) => {
|
|
1085
|
-
obj[sub] = await resolveField([...prefix, sub]);
|
|
1086
|
-
}));
|
|
1087
|
-
return obj;
|
|
1088
|
-
};
|
|
1089
|
-
await Promise.all([
|
|
1090
|
-
...[...outputFields].map(async (name) => {
|
|
1091
|
-
result[name] = await resolveField([name]);
|
|
1092
|
-
}),
|
|
1093
|
-
...forcePromises,
|
|
1094
|
-
]);
|
|
1095
|
-
return result;
|
|
1096
|
-
}
|
|
1097
|
-
/**
|
|
1098
|
-
* Recursively convert shadow trees into plain JS objects.
|
|
1099
|
-
*
|
|
1100
|
-
* Wire categories at each level (prefix = P):
|
|
1101
|
-
* Leaf — `to.path = [...P, name]`, no deeper paths → scalar
|
|
1102
|
-
* Array — direct wire AND deeper paths → pull as array, recurse
|
|
1103
|
-
* Nested object — only deeper paths, no direct wire → pull each
|
|
1104
|
-
* full path and assemble via setNested
|
|
1105
|
-
*/
|
|
1106
|
-
async materializeShadows(items, pathPrefix) {
|
|
1107
|
-
const wires = this.bridge.wires;
|
|
1108
|
-
const { type, field } = this.trunk;
|
|
1109
|
-
const directFields = new Set();
|
|
1110
|
-
const deepPaths = new Map();
|
|
1111
|
-
for (const wire of wires) {
|
|
1112
|
-
const p = wire.to.path;
|
|
1113
|
-
if (wire.to.module !== SELF_MODULE ||
|
|
1114
|
-
wire.to.type !== type ||
|
|
1115
|
-
wire.to.field !== field)
|
|
1116
|
-
continue;
|
|
1117
|
-
if (p.length <= pathPrefix.length)
|
|
1118
|
-
continue;
|
|
1119
|
-
if (!pathPrefix.every((seg, i) => p[i] === seg))
|
|
1120
|
-
continue;
|
|
1121
|
-
const name = p[pathPrefix.length];
|
|
1122
|
-
if (p.length === pathPrefix.length + 1) {
|
|
1123
|
-
directFields.add(name);
|
|
1124
|
-
}
|
|
1125
|
-
else {
|
|
1126
|
-
let arr = deepPaths.get(name);
|
|
1127
|
-
if (!arr) {
|
|
1128
|
-
arr = [];
|
|
1129
|
-
deepPaths.set(name, arr);
|
|
1130
|
-
}
|
|
1131
|
-
arr.push(p);
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
const rawResults = await Promise.all(items.map(async (shadow) => {
|
|
1135
|
-
const obj = {};
|
|
1136
|
-
const tasks = [];
|
|
1137
|
-
for (const name of directFields) {
|
|
1138
|
-
const fullPath = [...pathPrefix, name];
|
|
1139
|
-
const hasDeeper = deepPaths.has(name);
|
|
1140
|
-
tasks.push((async () => {
|
|
1141
|
-
if (hasDeeper) {
|
|
1142
|
-
const children = await shadow.pullOutputField(fullPath, true);
|
|
1143
|
-
obj[name] = Array.isArray(children)
|
|
1144
|
-
? await this.materializeShadows(children, fullPath)
|
|
1145
|
-
: children;
|
|
1146
|
-
}
|
|
1147
|
-
else {
|
|
1148
|
-
obj[name] = await shadow.pullOutputField(fullPath);
|
|
1149
|
-
}
|
|
1150
|
-
})());
|
|
1151
|
-
}
|
|
1152
|
-
for (const [name, paths] of deepPaths) {
|
|
1153
|
-
if (directFields.has(name))
|
|
1154
|
-
continue;
|
|
1155
|
-
tasks.push((async () => {
|
|
1156
|
-
const nested = {};
|
|
1157
|
-
await Promise.all(paths.map(async (fullPath) => {
|
|
1158
|
-
const value = await shadow.pullOutputField(fullPath);
|
|
1159
|
-
setNested(nested, fullPath.slice(pathPrefix.length + 1), value);
|
|
1160
|
-
}));
|
|
1161
|
-
obj[name] = nested;
|
|
1162
|
-
})());
|
|
1163
|
-
}
|
|
1164
|
-
await Promise.all(tasks);
|
|
1165
|
-
// Check if any field resolved to a sentinel — propagate it
|
|
1166
|
-
for (const v of Object.values(obj)) {
|
|
1167
|
-
if (v === CONTINUE_SYM)
|
|
1168
|
-
return CONTINUE_SYM;
|
|
1169
|
-
if (v === BREAK_SYM)
|
|
1170
|
-
return BREAK_SYM;
|
|
1171
|
-
}
|
|
1172
|
-
return obj;
|
|
1173
|
-
}));
|
|
1174
|
-
// Filter sentinels from the final result
|
|
1175
|
-
const finalResults = [];
|
|
1176
|
-
for (const item of rawResults) {
|
|
1177
|
-
if (item === BREAK_SYM)
|
|
1178
|
-
break;
|
|
1179
|
-
if (item === CONTINUE_SYM)
|
|
1180
|
-
continue;
|
|
1181
|
-
finalResults.push(item);
|
|
1182
|
-
}
|
|
1183
|
-
return finalResults;
|
|
1184
|
-
}
|
|
1185
|
-
async response(ipath, array) {
|
|
1186
|
-
// Build path segments from GraphQL resolver info
|
|
1187
|
-
const pathSegments = [];
|
|
1188
|
-
let index = ipath;
|
|
1189
|
-
while (index.prev) {
|
|
1190
|
-
pathSegments.unshift(`${index.key}`);
|
|
1191
|
-
index = index.prev;
|
|
1192
|
-
}
|
|
1193
|
-
if (pathSegments.length === 0) {
|
|
1194
|
-
// Direct output for scalar/list return types (e.g. [String!])
|
|
1195
|
-
const directOutput = this.bridge?.wires.filter((w) => sameTrunk(w.to, this.trunk) &&
|
|
1196
|
-
w.to.path.length === 1 &&
|
|
1197
|
-
w.to.path[0] === this.trunk.field) ?? [];
|
|
1198
|
-
if (directOutput.length > 0) {
|
|
1199
|
-
return this.resolveWires(directOutput);
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
// Strip numeric indices (array positions) from path for wire matching
|
|
1203
|
-
const cleanPath = pathSegments.filter((p) => !/^\d+$/.test(p));
|
|
1204
|
-
// Find wires whose target matches this trunk + path
|
|
1205
|
-
const matches = this.bridge?.wires.filter((w) => (w.to.element ? !!this.parent : true) &&
|
|
1206
|
-
sameTrunk(w.to, this.trunk) &&
|
|
1207
|
-
pathEquals(w.to.path, cleanPath)) ?? [];
|
|
1208
|
-
if (matches.length > 0) {
|
|
1209
|
-
// ── Lazy define resolution ──────────────────────────────────────
|
|
1210
|
-
// When ALL matches at the root object level (path=[]) are
|
|
1211
|
-
// whole-object wires sourced from define output modules, defer
|
|
1212
|
-
// resolution to field-by-field GraphQL traversal. This avoids
|
|
1213
|
-
// eagerly scheduling every tool inside the define block — only
|
|
1214
|
-
// fields actually requested by the query will trigger their
|
|
1215
|
-
// dependency chains.
|
|
1216
|
-
if (cleanPath.length === 0 &&
|
|
1217
|
-
!array &&
|
|
1218
|
-
matches.every((w) => "from" in w &&
|
|
1219
|
-
w.from.module.startsWith("__define_out_") &&
|
|
1220
|
-
w.from.path.length === 0)) {
|
|
1221
|
-
return this;
|
|
1222
|
-
}
|
|
1223
|
-
const response = this.resolveWires(matches);
|
|
1224
|
-
if (!array) {
|
|
1225
|
-
return response;
|
|
1226
|
-
}
|
|
1227
|
-
// Array: create shadow trees for per-element resolution
|
|
1228
|
-
const resolved = await response;
|
|
1229
|
-
if (resolved === BREAK_SYM || resolved === CONTINUE_SYM)
|
|
1230
|
-
return [];
|
|
1231
|
-
const items = resolved;
|
|
1232
|
-
const shadowTrees = [];
|
|
1233
|
-
for (const item of items) {
|
|
1234
|
-
if (item === BREAK_SYM)
|
|
1235
|
-
break;
|
|
1236
|
-
if (item === CONTINUE_SYM)
|
|
1237
|
-
continue;
|
|
1238
|
-
const s = this.shadow();
|
|
1239
|
-
s.state[trunkKey({ ...this.trunk, element: true })] = item;
|
|
1240
|
-
shadowTrees.push(s);
|
|
1241
|
-
}
|
|
1242
|
-
return shadowTrees;
|
|
1243
|
-
}
|
|
1244
|
-
// ── Resolve field from deferred define ────────────────────────────
|
|
1245
|
-
// No direct wires for this field path — check whether a define
|
|
1246
|
-
// forward wire exists at the root level (`o <- defineHandle`) and
|
|
1247
|
-
// resolve only the matching field wire from the define's output.
|
|
1248
|
-
if (cleanPath.length > 0) {
|
|
1249
|
-
const defineFieldWires = this.findDefineFieldWires(cleanPath);
|
|
1250
|
-
if (defineFieldWires.length > 0) {
|
|
1251
|
-
const response = this.resolveWires(defineFieldWires);
|
|
1252
|
-
if (!array)
|
|
1253
|
-
return response;
|
|
1254
|
-
const resolved = await response;
|
|
1255
|
-
if (resolved === BREAK_SYM || resolved === CONTINUE_SYM)
|
|
1256
|
-
return [];
|
|
1257
|
-
const items = resolved;
|
|
1258
|
-
const shadowTrees = [];
|
|
1259
|
-
for (const item of items) {
|
|
1260
|
-
if (item === BREAK_SYM)
|
|
1261
|
-
break;
|
|
1262
|
-
if (item === CONTINUE_SYM)
|
|
1263
|
-
continue;
|
|
1264
|
-
const s = this.shadow();
|
|
1265
|
-
s.state[trunkKey({ ...this.trunk, element: true })] = item;
|
|
1266
|
-
shadowTrees.push(s);
|
|
1267
|
-
}
|
|
1268
|
-
return shadowTrees;
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
// Fallback: if this shadow tree has stored element data, resolve the
|
|
1272
|
-
// requested field directly from it. This handles passthrough arrays
|
|
1273
|
-
// where the bridge maps an inner array (e.g. `.stops <- j.stops`) but
|
|
1274
|
-
// doesn't explicitly wire each scalar field on the element type.
|
|
1275
|
-
if (this.parent) {
|
|
1276
|
-
const elementKey = trunkKey({ ...this.trunk, element: true });
|
|
1277
|
-
const elementData = this.state[elementKey];
|
|
1278
|
-
if (elementData != null &&
|
|
1279
|
-
typeof elementData === "object" &&
|
|
1280
|
-
!Array.isArray(elementData)) {
|
|
1281
|
-
const fieldName = cleanPath[cleanPath.length - 1];
|
|
1282
|
-
if (fieldName !== undefined && fieldName in elementData) {
|
|
1283
|
-
const value = elementData[fieldName];
|
|
1284
|
-
if (array && Array.isArray(value)) {
|
|
1285
|
-
// Nested array: wrap items in shadow trees so they can
|
|
1286
|
-
// resolve their own fields via this same fallback path.
|
|
1287
|
-
return value.map((item) => {
|
|
1288
|
-
const s = this.shadow();
|
|
1289
|
-
s.state[elementKey] = item;
|
|
1290
|
-
return s;
|
|
1291
|
-
});
|
|
1292
|
-
}
|
|
1293
|
-
return value;
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
// Return self to trigger downstream resolvers
|
|
1298
|
-
return this;
|
|
1299
|
-
}
|
|
1300
|
-
/**
|
|
1301
|
-
* Find define output wires for a specific field path.
|
|
1302
|
-
*
|
|
1303
|
-
* Looks for whole-object define forward wires (`o <- defineHandle`)
|
|
1304
|
-
* at path=[] for this trunk, then searches the define's output wires
|
|
1305
|
-
* for ones matching the requested field path.
|
|
1306
|
-
*/
|
|
1307
|
-
findDefineFieldWires(cleanPath) {
|
|
1308
|
-
const forwards = this.bridge?.wires.filter((w) => "from" in w &&
|
|
1309
|
-
sameTrunk(w.to, this.trunk) &&
|
|
1310
|
-
w.to.path.length === 0 &&
|
|
1311
|
-
w.from.module.startsWith("__define_out_") &&
|
|
1312
|
-
w.from.path.length === 0) ?? [];
|
|
1313
|
-
if (forwards.length === 0)
|
|
1314
|
-
return [];
|
|
1315
|
-
const result = [];
|
|
1316
|
-
for (const fw of forwards) {
|
|
1317
|
-
const defOutTrunk = fw.from;
|
|
1318
|
-
const fieldWires = this.bridge?.wires.filter((w) => sameTrunk(w.to, defOutTrunk) && pathEquals(w.to.path, cleanPath)) ?? [];
|
|
1319
|
-
result.push(...fieldWires);
|
|
1320
|
-
}
|
|
1321
|
-
return result;
|
|
1322
|
-
}
|
|
1323
|
-
}
|