@stackables/bridge-core 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/{ExecutionTree.d.ts → bridge-core/src/ExecutionTree.d.ts} +59 -2
- package/build/bridge-core/src/ExecutionTree.d.ts.map +1 -0
- package/build/{ExecutionTree.js → bridge-core/src/ExecutionTree.js} +524 -146
- package/build/bridge-core/src/execute-bridge.d.ts.map +1 -0
- package/build/bridge-core/src/index.d.ts.map +1 -0
- package/build/bridge-core/src/merge-documents.d.ts.map +1 -0
- package/build/bridge-core/src/tools/index.d.ts.map +1 -0
- package/build/bridge-core/src/tools/internal.d.ts.map +1 -0
- package/build/bridge-core/src/types.d.ts.map +1 -0
- package/build/bridge-core/src/utils.d.ts.map +1 -0
- package/build/bridge-core/src/version-check.d.ts.map +1 -0
- package/build/bridge-stdlib/src/index.d.ts +34 -0
- package/build/bridge-stdlib/src/index.d.ts.map +1 -0
- package/build/bridge-stdlib/src/index.js +40 -0
- package/build/bridge-stdlib/src/tools/arrays.d.ts +28 -0
- package/build/bridge-stdlib/src/tools/arrays.d.ts.map +1 -0
- package/build/bridge-stdlib/src/tools/arrays.js +50 -0
- package/build/bridge-stdlib/src/tools/audit.d.ts +36 -0
- package/build/bridge-stdlib/src/tools/audit.d.ts.map +1 -0
- package/build/bridge-stdlib/src/tools/audit.js +39 -0
- package/build/bridge-stdlib/src/tools/http-call.d.ts +35 -0
- package/build/bridge-stdlib/src/tools/http-call.d.ts.map +1 -0
- package/build/bridge-stdlib/src/tools/http-call.js +118 -0
- package/build/bridge-stdlib/src/tools/strings.d.ts +13 -0
- package/build/bridge-stdlib/src/tools/strings.d.ts.map +1 -0
- package/build/bridge-stdlib/src/tools/strings.js +12 -0
- package/build/bridge-types/src/index.d.ts +63 -0
- package/build/bridge-types/src/index.d.ts.map +1 -0
- package/build/bridge-types/src/index.js +8 -0
- package/package.json +1 -1
- package/build/ExecutionTree.d.ts.map +0 -1
- package/build/execute-bridge.d.ts.map +0 -1
- package/build/index.d.ts.map +0 -1
- package/build/merge-documents.d.ts.map +0 -1
- package/build/tools/index.d.ts.map +0 -1
- package/build/tools/internal.d.ts.map +0 -1
- package/build/types.d.ts.map +0 -1
- package/build/utils.d.ts.map +0 -1
- package/build/version-check.d.ts.map +0 -1
- /package/build/{execute-bridge.d.ts → bridge-core/src/execute-bridge.d.ts} +0 -0
- /package/build/{execute-bridge.js → bridge-core/src/execute-bridge.js} +0 -0
- /package/build/{index.d.ts → bridge-core/src/index.d.ts} +0 -0
- /package/build/{index.js → bridge-core/src/index.js} +0 -0
- /package/build/{merge-documents.d.ts → bridge-core/src/merge-documents.d.ts} +0 -0
- /package/build/{merge-documents.js → bridge-core/src/merge-documents.js} +0 -0
- /package/build/{tools → bridge-core/src/tools}/index.d.ts +0 -0
- /package/build/{tools → bridge-core/src/tools}/index.js +0 -0
- /package/build/{tools → bridge-core/src/tools}/internal.d.ts +0 -0
- /package/build/{tools → bridge-core/src/tools}/internal.js +0 -0
- /package/build/{types.d.ts → bridge-core/src/types.d.ts} +0 -0
- /package/build/{types.js → bridge-core/src/types.js} +0 -0
- /package/build/{utils.d.ts → bridge-core/src/utils.d.ts} +0 -0
- /package/build/{utils.js → bridge-core/src/utils.js} +0 -0
- /package/build/{version-check.d.ts → bridge-core/src/version-check.d.ts} +0 -0
- /package/build/{version-check.js → bridge-core/src/version-check.js} +0 -0
|
@@ -23,6 +23,23 @@ const BREAK_SYM = Symbol.for("BRIDGE_BREAK");
|
|
|
23
23
|
/** Maximum shadow-tree nesting depth before a BridgePanicError is thrown. */
|
|
24
24
|
export const MAX_EXECUTION_DEPTH = 30;
|
|
25
25
|
const otelTracer = trace.getTracer("@stackables/bridge");
|
|
26
|
+
/**
|
|
27
|
+
* Lazily detect whether the OpenTelemetry tracer is a real (recording)
|
|
28
|
+
* tracer or the default no-op. Probed once on first tool call; result
|
|
29
|
+
* is cached for the lifetime of the process.
|
|
30
|
+
*
|
|
31
|
+
* If the SDK has not been registered by the time the first tool runs,
|
|
32
|
+
* all subsequent calls will skip OTel instrumentation.
|
|
33
|
+
*/
|
|
34
|
+
let _otelActive;
|
|
35
|
+
function isOtelActive() {
|
|
36
|
+
if (_otelActive === undefined) {
|
|
37
|
+
const probe = otelTracer.startSpan("_bridge_probe_");
|
|
38
|
+
_otelActive = probe.isRecording();
|
|
39
|
+
probe.end();
|
|
40
|
+
}
|
|
41
|
+
return _otelActive;
|
|
42
|
+
}
|
|
26
43
|
const otelMeter = metrics.getMeter("@stackables/bridge");
|
|
27
44
|
const toolCallCounter = otelMeter.createCounter("bridge.tool.calls", {
|
|
28
45
|
description: "Total number of tool invocations",
|
|
@@ -51,9 +68,17 @@ function sameTrunk(a, b) {
|
|
|
51
68
|
a.field === b.field &&
|
|
52
69
|
(a.instance ?? undefined) === (b.instance ?? undefined));
|
|
53
70
|
}
|
|
54
|
-
/** Strict path equality */
|
|
71
|
+
/** Strict path equality — manual loop avoids `.every()` closure allocation. See docs/performance.md (#7). */
|
|
55
72
|
function pathEquals(a, b) {
|
|
56
|
-
|
|
73
|
+
if (!a || !b)
|
|
74
|
+
return a === b;
|
|
75
|
+
if (a.length !== b.length)
|
|
76
|
+
return false;
|
|
77
|
+
for (let i = 0; i < a.length; i++) {
|
|
78
|
+
if (a[i] !== b[i])
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
57
82
|
}
|
|
58
83
|
/** Check whether an error is a fatal halt (abort or panic) that must bypass all error boundaries. */
|
|
59
84
|
function isFatalError(err) {
|
|
@@ -62,6 +87,37 @@ function isFatalError(err) {
|
|
|
62
87
|
err?.name === "BridgeAbortError" ||
|
|
63
88
|
err?.name === "BridgePanicError");
|
|
64
89
|
}
|
|
90
|
+
/** Returns `true` when `value` is a thenable (Promise or Promise-like). */
|
|
91
|
+
function isPromise(value) {
|
|
92
|
+
return typeof value?.then === "function";
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Returns the `from` NodeRef when a wire qualifies for the simple-pull fast
|
|
96
|
+
* path (single `from` wire, no safe/falsy/nullish/catch modifiers). Returns
|
|
97
|
+
* `null` otherwise. The result is cached on the wire object so subsequent
|
|
98
|
+
* calls are a single property read. See docs/performance.md (#11).
|
|
99
|
+
*/
|
|
100
|
+
function getSimplePullRef(w) {
|
|
101
|
+
let ref = w.__simplePullRef;
|
|
102
|
+
if (ref !== undefined)
|
|
103
|
+
return ref;
|
|
104
|
+
ref =
|
|
105
|
+
"from" in w &&
|
|
106
|
+
!w.safe &&
|
|
107
|
+
!w.falsyFallbackRefs?.length &&
|
|
108
|
+
w.falsyControl == null &&
|
|
109
|
+
w.falsyFallback == null &&
|
|
110
|
+
w.nullishControl == null &&
|
|
111
|
+
!w.nullishFallbackRef &&
|
|
112
|
+
w.nullishFallback == null &&
|
|
113
|
+
!w.catchControl &&
|
|
114
|
+
!w.catchFallbackRef &&
|
|
115
|
+
w.catchFallback == null
|
|
116
|
+
? w.from
|
|
117
|
+
: null;
|
|
118
|
+
w.__simplePullRef = ref;
|
|
119
|
+
return ref;
|
|
120
|
+
}
|
|
65
121
|
/** Execute a control flow instruction, returning a sentinel or throwing. */
|
|
66
122
|
function applyControlFlow(ctrl) {
|
|
67
123
|
if (ctrl.kind === "throw")
|
|
@@ -126,14 +182,29 @@ export class TraceCollector {
|
|
|
126
182
|
* "true" → true, "false" → false, "null" → null, "42" → 42
|
|
127
183
|
* Plain strings that aren't valid JSON (like "hello", "/search") fall
|
|
128
184
|
* through and are returned as-is.
|
|
185
|
+
*
|
|
186
|
+
* Results are cached in a module-level Map because the same constant
|
|
187
|
+
* strings appear repeatedly across shadow trees. Only safe for
|
|
188
|
+
* immutable values (primitives); callers must not mutate the returned
|
|
189
|
+
* value. See docs/performance.md (#6).
|
|
129
190
|
*/
|
|
191
|
+
const constantCache = new Map();
|
|
130
192
|
function coerceConstant(raw) {
|
|
193
|
+
const cached = constantCache.get(raw);
|
|
194
|
+
if (cached !== undefined)
|
|
195
|
+
return cached;
|
|
196
|
+
let result;
|
|
131
197
|
try {
|
|
132
|
-
|
|
198
|
+
result = JSON.parse(raw);
|
|
133
199
|
}
|
|
134
200
|
catch {
|
|
135
|
-
|
|
201
|
+
result = raw;
|
|
136
202
|
}
|
|
203
|
+
// Hard cap to prevent unbounded growth over long-lived processes.
|
|
204
|
+
if (constantCache.size > 10_000)
|
|
205
|
+
constantCache.clear();
|
|
206
|
+
constantCache.set(raw, result);
|
|
207
|
+
return result;
|
|
137
208
|
}
|
|
138
209
|
const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
139
210
|
function setNested(obj, path, value) {
|
|
@@ -181,6 +252,8 @@ export class ExecutionTree {
|
|
|
181
252
|
toolFns;
|
|
182
253
|
/** Shadow-tree nesting depth (0 for root). */
|
|
183
254
|
depth;
|
|
255
|
+
/** Pre-computed `trunkKey({ ...this.trunk, element: true })`. See docs/performance.md (#4). */
|
|
256
|
+
elementTrunkKey;
|
|
184
257
|
constructor(trunk, document, toolFns, context, parent) {
|
|
185
258
|
this.trunk = trunk;
|
|
186
259
|
this.document = document;
|
|
@@ -190,6 +263,7 @@ export class ExecutionTree {
|
|
|
190
263
|
if (this.depth > MAX_EXECUTION_DEPTH) {
|
|
191
264
|
throw new BridgePanicError(`Maximum execution depth exceeded (${this.depth}) at ${trunkKey(trunk)}. Check for infinite recursion or circular array mappings.`);
|
|
192
265
|
}
|
|
266
|
+
this.elementTrunkKey = `${trunk.module}:${trunk.type}:${trunk.field}:*`;
|
|
193
267
|
this.toolFns = { internal, ...(toolFns ?? {}) };
|
|
194
268
|
const instructions = document.instructions;
|
|
195
269
|
this.bridge = instructions.find((i) => i.kind === "bridge" && i.type === trunk.type && i.field === trunk.field);
|
|
@@ -482,117 +556,161 @@ export class ExecutionTree {
|
|
|
482
556
|
return this.parent.schedule(target, pullChain);
|
|
483
557
|
}
|
|
484
558
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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);
|
|
559
|
+
// ── Sync work: collect and group bridge wires ─────────────────
|
|
560
|
+
// If this target is a pipe fork, also apply bridge wires from its base
|
|
561
|
+
// handle (non-pipe wires, e.g. `c.currency <- i.currency`) as defaults
|
|
562
|
+
// before the fork-specific pipe wires.
|
|
563
|
+
const targetKey = trunkKey(target);
|
|
564
|
+
const pipeFork = this.pipeHandleMap?.get(targetKey);
|
|
565
|
+
const baseTrunk = pipeFork?.baseTrunk;
|
|
566
|
+
const baseWires = baseTrunk
|
|
567
|
+
? (this.bridge?.wires.filter((w) => !("pipe" in w) && sameTrunk(w.to, baseTrunk)) ?? [])
|
|
568
|
+
: [];
|
|
569
|
+
// Fork-specific wires (pipe wires targeting the fork's own instance)
|
|
570
|
+
const forkWires = this.bridge?.wires.filter((w) => sameTrunk(w.to, target)) ?? [];
|
|
571
|
+
// Merge: base provides defaults, fork overrides
|
|
572
|
+
const bridgeWires = [...baseWires, ...forkWires];
|
|
573
|
+
// Look up ToolDef for this target
|
|
574
|
+
const toolName = this.getToolName(target);
|
|
575
|
+
const toolDef = this.resolveToolDefByName(toolName);
|
|
576
|
+
// Group wires by target path so that || (null-fallback) and ??
|
|
577
|
+
// (error-fallback) semantics are honoured via resolveWires().
|
|
578
|
+
const wireGroups = new Map();
|
|
579
|
+
for (const w of bridgeWires) {
|
|
580
|
+
const key = w.to.path.join(".");
|
|
581
|
+
let group = wireGroups.get(key);
|
|
582
|
+
if (!group) {
|
|
583
|
+
group = [];
|
|
584
|
+
wireGroups.set(key, group);
|
|
519
585
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
586
|
+
group.push(w);
|
|
587
|
+
}
|
|
588
|
+
// ── Async path: tool definition requires resolveToolWires + callTool ──
|
|
589
|
+
if (toolDef) {
|
|
590
|
+
return this.scheduleToolDef(toolName, toolDef, wireGroups, pullChain);
|
|
591
|
+
}
|
|
592
|
+
// ── Sync-capable path: no tool definition ──
|
|
593
|
+
// For __local bindings, __define_ pass-throughs, pipe forks backed by
|
|
594
|
+
// sync tools, and logic nodes — resolve bridge wires and return
|
|
595
|
+
// synchronously when all sources are already in state.
|
|
596
|
+
// See docs/performance.md (#12).
|
|
597
|
+
const groupEntries = Array.from(wireGroups.entries());
|
|
598
|
+
const nGroups = groupEntries.length;
|
|
599
|
+
const values = new Array(nGroups);
|
|
600
|
+
let hasAsync = false;
|
|
601
|
+
for (let i = 0; i < nGroups; i++) {
|
|
602
|
+
const v = this.resolveWires(groupEntries[i][1], pullChain);
|
|
603
|
+
values[i] = v;
|
|
604
|
+
if (!hasAsync && isPromise(v))
|
|
605
|
+
hasAsync = true;
|
|
606
|
+
}
|
|
607
|
+
if (!hasAsync) {
|
|
608
|
+
return this.scheduleFinish(target, toolName, groupEntries, values, baseTrunk);
|
|
609
|
+
}
|
|
610
|
+
return Promise.all(values).then((resolved) => this.scheduleFinish(target, toolName, groupEntries, resolved, baseTrunk));
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Assemble input from resolved wire values and either invoke a direct tool
|
|
614
|
+
* function or return the data for pass-through targets (local/define/logic).
|
|
615
|
+
* Returns synchronously when the tool function (if any) returns sync.
|
|
616
|
+
* See docs/performance.md (#12).
|
|
617
|
+
*/
|
|
618
|
+
scheduleFinish(target, toolName, groupEntries, resolvedValues, baseTrunk) {
|
|
619
|
+
const input = {};
|
|
620
|
+
const resolved = [];
|
|
621
|
+
for (let i = 0; i < groupEntries.length; i++) {
|
|
622
|
+
const path = groupEntries[i][1][0].to.path;
|
|
623
|
+
const value = resolvedValues[i];
|
|
624
|
+
resolved.push([path, value]);
|
|
625
|
+
if (path.length === 0 && value != null && typeof value === "object") {
|
|
626
|
+
Object.assign(input, value);
|
|
550
627
|
}
|
|
551
|
-
|
|
552
|
-
|
|
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);
|
|
628
|
+
else {
|
|
629
|
+
setNested(input, path, value);
|
|
565
630
|
}
|
|
566
|
-
|
|
567
|
-
|
|
631
|
+
}
|
|
632
|
+
// Direct tool function lookup by name (simple or dotted).
|
|
633
|
+
// When the handle carries a @version tag, try the versioned key first
|
|
634
|
+
// (e.g. "std.str.toLowerCase@999.1") so user-injected overrides win.
|
|
635
|
+
// For pipe forks, fall back to the baseTrunk's version since forks
|
|
636
|
+
// use synthetic instance numbers (100000+).
|
|
637
|
+
const handleVersion = this.handleVersionMap.get(trunkKey(target)) ??
|
|
638
|
+
(baseTrunk ? this.handleVersionMap.get(trunkKey(baseTrunk)) : undefined);
|
|
639
|
+
let directFn = handleVersion
|
|
640
|
+
? this.lookupToolFn(`${toolName}@${handleVersion}`)
|
|
641
|
+
: undefined;
|
|
642
|
+
if (!directFn) {
|
|
643
|
+
directFn = this.lookupToolFn(toolName);
|
|
644
|
+
}
|
|
645
|
+
if (directFn) {
|
|
646
|
+
return this.callTool(toolName, toolName, directFn, input);
|
|
647
|
+
}
|
|
648
|
+
// Define pass-through: synthetic trunks created by define inlining
|
|
649
|
+
// act as data containers — bridge wires set their values, no tool needed.
|
|
650
|
+
if (target.module.startsWith("__define_")) {
|
|
651
|
+
return input;
|
|
652
|
+
}
|
|
653
|
+
// Local binding or logic node: the wire resolves the source and stores
|
|
654
|
+
// the result — no tool call needed. For path=[] wires the resolved
|
|
655
|
+
// value may be a primitive (boolean from condAnd/condOr, string from
|
|
656
|
+
// a pipe tool like upperCase), so return the resolved value directly.
|
|
657
|
+
if (target.module === "__local" ||
|
|
658
|
+
target.field === "__and" ||
|
|
659
|
+
target.field === "__or") {
|
|
660
|
+
for (const [path, value] of resolved) {
|
|
661
|
+
if (path.length === 0)
|
|
662
|
+
return value;
|
|
568
663
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
664
|
+
return input;
|
|
665
|
+
}
|
|
666
|
+
throw new Error(`No tool found for "${toolName}"`);
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Full async schedule path for targets backed by a ToolDef.
|
|
670
|
+
* Resolves tool wires, bridge wires, and invokes the tool function
|
|
671
|
+
* with error recovery support.
|
|
672
|
+
*/
|
|
673
|
+
async scheduleToolDef(toolName, toolDef, wireGroups, pullChain) {
|
|
674
|
+
// Build input object: tool wires first (base), then bridge wires (override)
|
|
675
|
+
const input = {};
|
|
676
|
+
await this.resolveToolWires(toolDef, input);
|
|
677
|
+
// Resolve bridge wires and apply on top
|
|
678
|
+
const groupEntries = Array.from(wireGroups.entries());
|
|
679
|
+
const resolved = await Promise.all(groupEntries.map(async ([, group]) => {
|
|
680
|
+
const value = await this.resolveWires(group, pullChain);
|
|
681
|
+
return [group[0].to.path, value];
|
|
682
|
+
}));
|
|
683
|
+
for (const [path, value] of resolved) {
|
|
684
|
+
if (path.length === 0 && value != null && typeof value === "object") {
|
|
685
|
+
Object.assign(input, value);
|
|
573
686
|
}
|
|
574
|
-
|
|
575
|
-
|
|
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;
|
|
687
|
+
else {
|
|
688
|
+
setNested(input, path, value);
|
|
586
689
|
}
|
|
587
|
-
|
|
588
|
-
|
|
690
|
+
}
|
|
691
|
+
// Call ToolDef-backed tool function
|
|
692
|
+
const fn = this.lookupToolFn(toolDef.fn);
|
|
693
|
+
if (!fn)
|
|
694
|
+
throw new Error(`Tool function "${toolDef.fn}" not registered`);
|
|
695
|
+
// on error: wrap the tool call with fallback from onError wire
|
|
696
|
+
const onErrorWire = toolDef.wires.find((w) => w.kind === "onError");
|
|
697
|
+
try {
|
|
698
|
+
return await this.callTool(toolName, toolDef.fn, fn, input);
|
|
699
|
+
}
|
|
700
|
+
catch (err) {
|
|
701
|
+
if (!onErrorWire)
|
|
702
|
+
throw err;
|
|
703
|
+
if ("value" in onErrorWire)
|
|
704
|
+
return JSON.parse(onErrorWire.value);
|
|
705
|
+
return this.resolveToolSource(onErrorWire.source, toolDef);
|
|
706
|
+
}
|
|
589
707
|
}
|
|
590
708
|
/**
|
|
591
709
|
* Invoke a tool function, recording both an OpenTelemetry span and (when
|
|
592
710
|
* tracing is enabled) a ToolTrace entry. All three tool-call sites in the
|
|
593
711
|
* engine delegate here so instrumentation lives in exactly one place.
|
|
594
712
|
*/
|
|
595
|
-
|
|
713
|
+
callTool(toolName, fnName, fnImpl, input) {
|
|
596
714
|
// Short-circuit before starting if externally aborted
|
|
597
715
|
if (this.signal?.aborted) {
|
|
598
716
|
throw new BridgeAbortError();
|
|
@@ -603,6 +721,15 @@ export class ExecutionTree {
|
|
|
603
721
|
logger: logger ?? {},
|
|
604
722
|
signal: this.signal,
|
|
605
723
|
};
|
|
724
|
+
// ── Fast path: no instrumentation configured ──────────────────
|
|
725
|
+
// When there is no internal tracer, no logger, and OpenTelemetry
|
|
726
|
+
// has its default no-op provider, skip all instrumentation to
|
|
727
|
+
// avoid closure allocation, template-string building, and no-op
|
|
728
|
+
// metric calls. See docs/performance.md (#5).
|
|
729
|
+
if (!tracer && !logger && !isOtelActive()) {
|
|
730
|
+
return fnImpl(input, toolContext);
|
|
731
|
+
}
|
|
732
|
+
// ── Instrumented path ─────────────────────────────────────────
|
|
606
733
|
const traceStart = tracer?.now();
|
|
607
734
|
const metricAttrs = {
|
|
608
735
|
"bridge.tool.name": toolName,
|
|
@@ -657,7 +784,26 @@ export class ExecutionTree {
|
|
|
657
784
|
});
|
|
658
785
|
}
|
|
659
786
|
shadow() {
|
|
660
|
-
|
|
787
|
+
// Lightweight: bypass the constructor to avoid redundant work that
|
|
788
|
+
// re-derives data identical to the parent (bridge lookup, pipeHandleMap,
|
|
789
|
+
// handleVersionMap, constObj, toolFns spread). See docs/performance.md (#2).
|
|
790
|
+
const child = Object.create(ExecutionTree.prototype);
|
|
791
|
+
child.trunk = this.trunk;
|
|
792
|
+
child.document = this.document;
|
|
793
|
+
child.parent = this;
|
|
794
|
+
child.depth = this.depth + 1;
|
|
795
|
+
if (child.depth > MAX_EXECUTION_DEPTH) {
|
|
796
|
+
throw new BridgePanicError(`Maximum execution depth exceeded (${child.depth}) at ${trunkKey(this.trunk)}. Check for infinite recursion or circular array mappings.`);
|
|
797
|
+
}
|
|
798
|
+
child.state = {};
|
|
799
|
+
child.toolDepCache = new Map();
|
|
800
|
+
child.toolDefCache = new Map();
|
|
801
|
+
// Share read-only pre-computed data from parent
|
|
802
|
+
child.bridge = this.bridge;
|
|
803
|
+
child.pipeHandleMap = this.pipeHandleMap;
|
|
804
|
+
child.handleVersionMap = this.handleVersionMap;
|
|
805
|
+
child.toolFns = this.toolFns;
|
|
806
|
+
child.elementTrunkKey = this.elementTrunkKey;
|
|
661
807
|
child.tracer = this.tracer;
|
|
662
808
|
child.logger = this.logger;
|
|
663
809
|
child.signal = this.signal;
|
|
@@ -667,11 +813,47 @@ export class ExecutionTree {
|
|
|
667
813
|
getTraces() {
|
|
668
814
|
return this.tracer?.traces ?? [];
|
|
669
815
|
}
|
|
670
|
-
|
|
671
|
-
|
|
816
|
+
/**
|
|
817
|
+
* Traverse `ref.path` on an already-resolved value, respecting null guards.
|
|
818
|
+
* Extracted from `pullSingle` so the sync and async paths can share logic.
|
|
819
|
+
*/
|
|
820
|
+
applyPath(resolved, ref) {
|
|
821
|
+
if (!ref.path.length)
|
|
822
|
+
return resolved;
|
|
823
|
+
let result = resolved;
|
|
824
|
+
// Root-level null check
|
|
825
|
+
if (result == null) {
|
|
826
|
+
if (ref.rootSafe)
|
|
827
|
+
return undefined;
|
|
828
|
+
throw new TypeError(`Cannot read properties of ${result} (reading '${ref.path[0]}')`);
|
|
829
|
+
}
|
|
830
|
+
for (let i = 0; i < ref.path.length; i++) {
|
|
831
|
+
const segment = ref.path[i];
|
|
832
|
+
if (UNSAFE_KEYS.has(segment))
|
|
833
|
+
throw new Error(`Unsafe property traversal: ${segment}`);
|
|
834
|
+
if (Array.isArray(result) && !/^\d+$/.test(segment)) {
|
|
835
|
+
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(".")}`);
|
|
836
|
+
}
|
|
837
|
+
result = result[segment];
|
|
838
|
+
if (result == null && i < ref.path.length - 1) {
|
|
839
|
+
const nextSafe = ref.pathSafe?.[i + 1] ?? false;
|
|
840
|
+
if (nextSafe)
|
|
841
|
+
return undefined;
|
|
842
|
+
throw new TypeError(`Cannot read properties of ${result} (reading '${ref.path[i + 1]}')`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return result;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Pull a single value. Returns synchronously when already in state;
|
|
849
|
+
* returns a Promise only when the value is a pending tool call.
|
|
850
|
+
* See docs/performance.md (#10).
|
|
851
|
+
*/
|
|
852
|
+
pullSingle(ref, pullChain = new Set()) {
|
|
853
|
+
// Cache trunkKey on the NodeRef to avoid repeated string allocation
|
|
854
|
+
// for the same AST node. See docs/performance.md (#11).
|
|
855
|
+
const key = (ref.__key ??= trunkKey(ref));
|
|
672
856
|
// ── 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
857
|
if (pullChain.has(key)) {
|
|
676
858
|
throw new BridgePanicError(`Circular dependency detected: "${key}" depends on itself`);
|
|
677
859
|
}
|
|
@@ -693,41 +875,19 @@ export class ExecutionTree {
|
|
|
693
875
|
if (ref.path.length > 0 && ref.module.startsWith("__define_")) {
|
|
694
876
|
const fieldWires = this.bridge?.wires.filter((w) => sameTrunk(w.to, ref) && pathEquals(w.to.path, ref.path)) ?? [];
|
|
695
877
|
if (fieldWires.length > 0) {
|
|
878
|
+
// resolveWires already delivers the value at ref.path — no applyPath.
|
|
696
879
|
return this.resolveWires(fieldWires, nextChain);
|
|
697
880
|
}
|
|
698
881
|
}
|
|
699
882
|
this.state[key] = this.schedule(ref, nextChain);
|
|
700
|
-
value = this.state[key];
|
|
883
|
+
value = this.state[key]; // sync value or Promise (see #12)
|
|
701
884
|
}
|
|
702
|
-
//
|
|
703
|
-
|
|
704
|
-
|
|
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
|
-
}
|
|
885
|
+
// Sync fast path: value is already resolved (not a pending Promise).
|
|
886
|
+
if (!isPromise(value)) {
|
|
887
|
+
return this.applyPath(value, ref);
|
|
729
888
|
}
|
|
730
|
-
|
|
889
|
+
// Async: chain path traversal onto the pending promise.
|
|
890
|
+
return value.then((resolved) => this.applyPath(resolved, ref));
|
|
731
891
|
}
|
|
732
892
|
push(args) {
|
|
733
893
|
this.state[trunkKey(this.trunk)] = args;
|
|
@@ -800,7 +960,26 @@ export class ExecutionTree {
|
|
|
800
960
|
* After layers 1–2b, the overdefinition boundary (`!= null`) decides whether
|
|
801
961
|
* to return or continue to the next wire.
|
|
802
962
|
*/
|
|
803
|
-
|
|
963
|
+
/**
|
|
964
|
+
* Resolve wires, returning synchronously when the hot path allows it.
|
|
965
|
+
*
|
|
966
|
+
* Fast path: single `from` wire with no fallback/catch modifiers, which is
|
|
967
|
+
* the common case for element field wires like `.id <- it.id`. Delegates to
|
|
968
|
+
* `resolveWiresAsync` for anything more complex.
|
|
969
|
+
* See docs/performance.md (#10).
|
|
970
|
+
*/
|
|
971
|
+
resolveWires(wires, pullChain) {
|
|
972
|
+
if (wires.length === 1) {
|
|
973
|
+
const w = wires[0];
|
|
974
|
+
if ("value" in w)
|
|
975
|
+
return coerceConstant(w.value);
|
|
976
|
+
const ref = getSimplePullRef(w);
|
|
977
|
+
if (ref)
|
|
978
|
+
return this.pullSingle(ref, pullChain);
|
|
979
|
+
}
|
|
980
|
+
return this.resolveWiresAsync(wires, pullChain);
|
|
981
|
+
}
|
|
982
|
+
async resolveWiresAsync(wires, pullChain) {
|
|
804
983
|
let lastError;
|
|
805
984
|
for (const w of wires) {
|
|
806
985
|
// Constant wire — always wins, no modifiers
|
|
@@ -984,11 +1163,110 @@ export class ExecutionTree {
|
|
|
984
1163
|
if (item === CONTINUE_SYM)
|
|
985
1164
|
continue;
|
|
986
1165
|
const s = this.shadow();
|
|
987
|
-
s.state[
|
|
1166
|
+
s.state[this.elementTrunkKey] = item;
|
|
988
1167
|
finalShadowTrees.push(s);
|
|
989
1168
|
}
|
|
990
1169
|
return finalShadowTrees;
|
|
991
1170
|
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Resolve pre-grouped wires on this shadow tree without re-filtering.
|
|
1173
|
+
* Called by the parent's `materializeShadows` to skip per-element wire
|
|
1174
|
+
* filtering. Returns synchronously when the wire resolves sync (hot path).
|
|
1175
|
+
* See docs/performance.md (#8, #10).
|
|
1176
|
+
*/
|
|
1177
|
+
resolvePreGrouped(wires) {
|
|
1178
|
+
return this.resolveWires(wires);
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Materialise all output wires into a plain JS object.
|
|
1182
|
+
*
|
|
1183
|
+
* Used by the GraphQL adapter when a bridge field returns a scalar type
|
|
1184
|
+
* (e.g. `JSON`, `JSONObject`). In that case GraphQL won't call sub-field
|
|
1185
|
+
* resolvers, so we need to eagerly resolve every output wire and assemble
|
|
1186
|
+
* the result ourselves — the same logic `run()` uses for object output.
|
|
1187
|
+
*/
|
|
1188
|
+
async collectOutput() {
|
|
1189
|
+
const bridge = this.bridge;
|
|
1190
|
+
if (!bridge)
|
|
1191
|
+
return undefined;
|
|
1192
|
+
const { type, field } = this.trunk;
|
|
1193
|
+
// Shadow tree (array element) — resolve element-level output fields.
|
|
1194
|
+
// For scalar arrays ([JSON!]) GraphQL won't call sub-field resolvers,
|
|
1195
|
+
// so we eagerly materialise each element here.
|
|
1196
|
+
if (this.parent) {
|
|
1197
|
+
const outputFields = new Set();
|
|
1198
|
+
for (const wire of bridge.wires) {
|
|
1199
|
+
if (wire.to.module === SELF_MODULE &&
|
|
1200
|
+
wire.to.type === type &&
|
|
1201
|
+
wire.to.field === field &&
|
|
1202
|
+
wire.to.path.length > 0) {
|
|
1203
|
+
outputFields.add(wire.to.path[0]);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
if (outputFields.size > 0) {
|
|
1207
|
+
const result = {};
|
|
1208
|
+
await Promise.all([...outputFields].map(async (name) => {
|
|
1209
|
+
result[name] = await this.pullOutputField([name]);
|
|
1210
|
+
}));
|
|
1211
|
+
return result;
|
|
1212
|
+
}
|
|
1213
|
+
// Passthrough: return stored element data directly
|
|
1214
|
+
return this.state[this.elementTrunkKey];
|
|
1215
|
+
}
|
|
1216
|
+
// Root wire (`o <- src`) — whole-object passthrough
|
|
1217
|
+
const hasRootWire = bridge.wires.some((w) => "from" in w &&
|
|
1218
|
+
w.to.module === SELF_MODULE &&
|
|
1219
|
+
w.to.type === type &&
|
|
1220
|
+
w.to.field === field &&
|
|
1221
|
+
w.to.path.length === 0);
|
|
1222
|
+
if (hasRootWire) {
|
|
1223
|
+
return this.pullOutputField([]);
|
|
1224
|
+
}
|
|
1225
|
+
// Object output — collect unique top-level field names
|
|
1226
|
+
const outputFields = new Set();
|
|
1227
|
+
for (const wire of bridge.wires) {
|
|
1228
|
+
if (wire.to.module === SELF_MODULE &&
|
|
1229
|
+
wire.to.type === type &&
|
|
1230
|
+
wire.to.field === field &&
|
|
1231
|
+
wire.to.path.length > 0) {
|
|
1232
|
+
outputFields.add(wire.to.path[0]);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
if (outputFields.size === 0)
|
|
1236
|
+
return undefined;
|
|
1237
|
+
const result = {};
|
|
1238
|
+
const resolveField = async (prefix) => {
|
|
1239
|
+
const exactWires = bridge.wires.filter((w) => w.to.module === SELF_MODULE &&
|
|
1240
|
+
w.to.type === type &&
|
|
1241
|
+
w.to.field === field &&
|
|
1242
|
+
pathEquals(w.to.path, prefix));
|
|
1243
|
+
if (exactWires.length > 0) {
|
|
1244
|
+
return this.resolveWires(exactWires);
|
|
1245
|
+
}
|
|
1246
|
+
const subFields = new Set();
|
|
1247
|
+
for (const wire of bridge.wires) {
|
|
1248
|
+
const p = wire.to.path;
|
|
1249
|
+
if (wire.to.module === SELF_MODULE &&
|
|
1250
|
+
wire.to.type === type &&
|
|
1251
|
+
wire.to.field === field &&
|
|
1252
|
+
p.length > prefix.length &&
|
|
1253
|
+
prefix.every((seg, i) => p[i] === seg)) {
|
|
1254
|
+
subFields.add(p[prefix.length]);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
if (subFields.size === 0)
|
|
1258
|
+
return undefined;
|
|
1259
|
+
const obj = {};
|
|
1260
|
+
await Promise.all([...subFields].map(async (sub) => {
|
|
1261
|
+
obj[sub] = await resolveField([...prefix, sub]);
|
|
1262
|
+
}));
|
|
1263
|
+
return obj;
|
|
1264
|
+
};
|
|
1265
|
+
await Promise.all([...outputFields].map(async (name) => {
|
|
1266
|
+
result[name] = await resolveField([name]);
|
|
1267
|
+
}));
|
|
1268
|
+
return result;
|
|
1269
|
+
}
|
|
992
1270
|
/**
|
|
993
1271
|
* Execute the bridge end-to-end without GraphQL.
|
|
994
1272
|
*
|
|
@@ -1055,9 +1333,40 @@ export class ExecutionTree {
|
|
|
1055
1333
|
`Ensure at least one wire targets the output (e.g. \`o.field <- ...\`).`);
|
|
1056
1334
|
}
|
|
1057
1335
|
const result = {};
|
|
1336
|
+
// Resolves a single output field at `prefix` — either via an exact-match
|
|
1337
|
+
// wire (leaf), or by collecting sub-fields from deeper wires (nested object).
|
|
1338
|
+
const resolveField = async (prefix) => {
|
|
1339
|
+
const exactWires = bridge.wires.filter((w) => w.to.module === SELF_MODULE &&
|
|
1340
|
+
w.to.type === type &&
|
|
1341
|
+
w.to.field === field &&
|
|
1342
|
+
pathEquals(w.to.path, prefix));
|
|
1343
|
+
if (exactWires.length > 0) {
|
|
1344
|
+
return this.resolveWires(exactWires);
|
|
1345
|
+
}
|
|
1346
|
+
// No exact wire — gather sub-field names from deeper-path wires
|
|
1347
|
+
// (e.g. `o.why { .temperature <- ... }` produces path ["why","temperature"])
|
|
1348
|
+
const subFields = new Set();
|
|
1349
|
+
for (const wire of bridge.wires) {
|
|
1350
|
+
const p = wire.to.path;
|
|
1351
|
+
if (wire.to.module === SELF_MODULE &&
|
|
1352
|
+
wire.to.type === type &&
|
|
1353
|
+
wire.to.field === field &&
|
|
1354
|
+
p.length > prefix.length &&
|
|
1355
|
+
prefix.every((seg, i) => p[i] === seg)) {
|
|
1356
|
+
subFields.add(p[prefix.length]);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
if (subFields.size === 0)
|
|
1360
|
+
return undefined;
|
|
1361
|
+
const obj = {};
|
|
1362
|
+
await Promise.all([...subFields].map(async (sub) => {
|
|
1363
|
+
obj[sub] = await resolveField([...prefix, sub]);
|
|
1364
|
+
}));
|
|
1365
|
+
return obj;
|
|
1366
|
+
};
|
|
1058
1367
|
await Promise.all([
|
|
1059
1368
|
...[...outputFields].map(async (name) => {
|
|
1060
|
-
result[name] = await
|
|
1369
|
+
result[name] = await resolveField([name]);
|
|
1061
1370
|
}),
|
|
1062
1371
|
...forcePromises,
|
|
1063
1372
|
]);
|
|
@@ -1077,6 +1386,9 @@ export class ExecutionTree {
|
|
|
1077
1386
|
const { type, field } = this.trunk;
|
|
1078
1387
|
const directFields = new Set();
|
|
1079
1388
|
const deepPaths = new Map();
|
|
1389
|
+
// #8: Pre-group wires by exact path — eliminates per-element re-filtering.
|
|
1390
|
+
// Key: wire.to.path joined by \0 (null char is safe — field names are identifiers).
|
|
1391
|
+
const wireGroupsByPath = new Map();
|
|
1080
1392
|
for (const wire of wires) {
|
|
1081
1393
|
const p = wire.to.path;
|
|
1082
1394
|
if (wire.to.module !== SELF_MODULE ||
|
|
@@ -1090,6 +1402,13 @@ export class ExecutionTree {
|
|
|
1090
1402
|
const name = p[pathPrefix.length];
|
|
1091
1403
|
if (p.length === pathPrefix.length + 1) {
|
|
1092
1404
|
directFields.add(name);
|
|
1405
|
+
const pathKey = p.join("\0");
|
|
1406
|
+
let group = wireGroupsByPath.get(pathKey);
|
|
1407
|
+
if (!group) {
|
|
1408
|
+
group = [];
|
|
1409
|
+
wireGroupsByPath.set(pathKey, group);
|
|
1410
|
+
}
|
|
1411
|
+
group.push(wire);
|
|
1093
1412
|
}
|
|
1094
1413
|
else {
|
|
1095
1414
|
let arr = deepPaths.get(name);
|
|
@@ -1100,6 +1419,63 @@ export class ExecutionTree {
|
|
|
1100
1419
|
arr.push(p);
|
|
1101
1420
|
}
|
|
1102
1421
|
}
|
|
1422
|
+
// #9/#10: Fast path — no nested arrays, only direct fields.
|
|
1423
|
+
// Collect all (shadow × field) resolutions. When every value is already in
|
|
1424
|
+
// state (the hot case for element passthrough), resolvePreGrouped returns
|
|
1425
|
+
// synchronously and we skip Promise.all entirely.
|
|
1426
|
+
// See docs/performance.md (#9, #10).
|
|
1427
|
+
if (deepPaths.size === 0) {
|
|
1428
|
+
const directFieldArray = [...directFields];
|
|
1429
|
+
const nFields = directFieldArray.length;
|
|
1430
|
+
const nItems = items.length;
|
|
1431
|
+
// Pre-compute pathKeys and wire groups — only depend on j, not i.
|
|
1432
|
+
// See docs/performance.md (#11).
|
|
1433
|
+
const preGroups = new Array(nFields);
|
|
1434
|
+
for (let j = 0; j < nFields; j++) {
|
|
1435
|
+
const pathKey = [...pathPrefix, directFieldArray[j]].join("\0");
|
|
1436
|
+
preGroups[j] = wireGroupsByPath.get(pathKey);
|
|
1437
|
+
}
|
|
1438
|
+
const rawValues = new Array(nItems * nFields);
|
|
1439
|
+
let hasAsync = false;
|
|
1440
|
+
for (let i = 0; i < nItems; i++) {
|
|
1441
|
+
const shadow = items[i];
|
|
1442
|
+
for (let j = 0; j < nFields; j++) {
|
|
1443
|
+
const v = shadow.resolvePreGrouped(preGroups[j]);
|
|
1444
|
+
rawValues[i * nFields + j] = v;
|
|
1445
|
+
if (!hasAsync && isPromise(v))
|
|
1446
|
+
hasAsync = true;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
const flatValues = hasAsync
|
|
1450
|
+
? await Promise.all(rawValues)
|
|
1451
|
+
: rawValues;
|
|
1452
|
+
const finalResults = [];
|
|
1453
|
+
for (let i = 0; i < items.length; i++) {
|
|
1454
|
+
const obj = {};
|
|
1455
|
+
let doBreak = false;
|
|
1456
|
+
let doSkip = false;
|
|
1457
|
+
for (let j = 0; j < nFields; j++) {
|
|
1458
|
+
const v = flatValues[i * nFields + j];
|
|
1459
|
+
if (v === BREAK_SYM) {
|
|
1460
|
+
doBreak = true;
|
|
1461
|
+
break;
|
|
1462
|
+
}
|
|
1463
|
+
if (v === CONTINUE_SYM) {
|
|
1464
|
+
doSkip = true;
|
|
1465
|
+
break;
|
|
1466
|
+
}
|
|
1467
|
+
obj[directFieldArray[j]] = v;
|
|
1468
|
+
}
|
|
1469
|
+
if (doBreak)
|
|
1470
|
+
break;
|
|
1471
|
+
if (doSkip)
|
|
1472
|
+
continue;
|
|
1473
|
+
finalResults.push(obj);
|
|
1474
|
+
}
|
|
1475
|
+
return finalResults;
|
|
1476
|
+
}
|
|
1477
|
+
// Slow path: deep paths (nested arrays) present.
|
|
1478
|
+
// Uses pre-grouped wires for direct fields (#8), original logic for the rest.
|
|
1103
1479
|
const rawResults = await Promise.all(items.map(async (shadow) => {
|
|
1104
1480
|
const obj = {};
|
|
1105
1481
|
const tasks = [];
|
|
@@ -1114,7 +1490,10 @@ export class ExecutionTree {
|
|
|
1114
1490
|
: children;
|
|
1115
1491
|
}
|
|
1116
1492
|
else {
|
|
1117
|
-
|
|
1493
|
+
// #8: wireGroupsByPath is built in the same branch that populates
|
|
1494
|
+
// directFields, so the group is always present — no fallback needed.
|
|
1495
|
+
const pathKey = fullPath.join("\0");
|
|
1496
|
+
obj[name] = await shadow.resolvePreGrouped(wireGroupsByPath.get(pathKey));
|
|
1118
1497
|
}
|
|
1119
1498
|
})());
|
|
1120
1499
|
}
|
|
@@ -1205,7 +1584,7 @@ export class ExecutionTree {
|
|
|
1205
1584
|
if (item === CONTINUE_SYM)
|
|
1206
1585
|
continue;
|
|
1207
1586
|
const s = this.shadow();
|
|
1208
|
-
s.state[
|
|
1587
|
+
s.state[this.elementTrunkKey] = item;
|
|
1209
1588
|
shadowTrees.push(s);
|
|
1210
1589
|
}
|
|
1211
1590
|
return shadowTrees;
|
|
@@ -1231,7 +1610,7 @@ export class ExecutionTree {
|
|
|
1231
1610
|
if (item === CONTINUE_SYM)
|
|
1232
1611
|
continue;
|
|
1233
1612
|
const s = this.shadow();
|
|
1234
|
-
s.state[
|
|
1613
|
+
s.state[this.elementTrunkKey] = item;
|
|
1235
1614
|
shadowTrees.push(s);
|
|
1236
1615
|
}
|
|
1237
1616
|
return shadowTrees;
|
|
@@ -1242,8 +1621,7 @@ export class ExecutionTree {
|
|
|
1242
1621
|
// where the bridge maps an inner array (e.g. `.stops <- j.stops`) but
|
|
1243
1622
|
// doesn't explicitly wire each scalar field on the element type.
|
|
1244
1623
|
if (this.parent) {
|
|
1245
|
-
const
|
|
1246
|
-
const elementData = this.state[elementKey];
|
|
1624
|
+
const elementData = this.state[this.elementTrunkKey];
|
|
1247
1625
|
if (elementData != null &&
|
|
1248
1626
|
typeof elementData === "object" &&
|
|
1249
1627
|
!Array.isArray(elementData)) {
|
|
@@ -1255,7 +1633,7 @@ export class ExecutionTree {
|
|
|
1255
1633
|
// resolve their own fields via this same fallback path.
|
|
1256
1634
|
return value.map((item) => {
|
|
1257
1635
|
const s = this.shadow();
|
|
1258
|
-
s.state[
|
|
1636
|
+
s.state[this.elementTrunkKey] = item;
|
|
1259
1637
|
return s;
|
|
1260
1638
|
});
|
|
1261
1639
|
}
|