@stackables/bridge-compiler 1.0.1 → 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.
@@ -1,4 +1,4 @@
1
- import type { Bridge, BridgeDocument, ToolMap } from "./types.ts";
1
+ import type { Bridge, BridgeDocument, ToolMap, Wire } from "./types.ts";
2
2
  /** Fatal panic error — bypasses all error boundaries (`?.` and `catch`). */
3
3
  export declare class BridgePanicError extends Error {
4
4
  constructor(message: string);
@@ -32,6 +32,13 @@ type Trunk = {
32
32
  field: string;
33
33
  instance?: number;
34
34
  };
35
+ /**
36
+ * A value that may already be resolved (synchronous) or still pending (asynchronous).
37
+ * Using this instead of always returning `Promise<T>` lets callers skip
38
+ * microtask scheduling when the value is immediately available.
39
+ * See docs/performance.md (#10).
40
+ */
41
+ type MaybePromise<T> = T | Promise<T>;
35
42
  /** Trace verbosity level.
36
43
  * - `"off"` (default) — no collection, zero overhead
37
44
  * - `"basic"` — tool, fn, timing, errors; no input/output
@@ -101,6 +108,8 @@ export declare class ExecutionTree {
101
108
  private toolFns?;
102
109
  /** Shadow-tree nesting depth (0 for root). */
103
110
  private depth;
111
+ /** Pre-computed `trunkKey({ ...this.trunk, element: true })`. See docs/performance.md (#4). */
112
+ private elementTrunkKey;
104
113
  constructor(trunk: Trunk, document: BridgeDocument, toolFns?: ToolMap, context?: Record<string, any> | undefined, parent?: ExecutionTree | undefined);
105
114
  /** Derive tool name from a trunk */
106
115
  private getToolName;
@@ -115,7 +124,20 @@ export declare class ExecutionTree {
115
124
  private resolveToolSource;
116
125
  /** Call a tool dependency (cached per request) */
117
126
  private resolveToolDep;
118
- schedule(target: Trunk, pullChain?: Set<string>): any;
127
+ schedule(target: Trunk, pullChain?: Set<string>): MaybePromise<any>;
128
+ /**
129
+ * Assemble input from resolved wire values and either invoke a direct tool
130
+ * function or return the data for pass-through targets (local/define/logic).
131
+ * Returns synchronously when the tool function (if any) returns sync.
132
+ * See docs/performance.md (#12).
133
+ */
134
+ private scheduleFinish;
135
+ /**
136
+ * Full async schedule path for targets backed by a ToolDef.
137
+ * Resolves tool wires, bridge wires, and invokes the tool function
138
+ * with error recovery support.
139
+ */
140
+ private scheduleToolDef;
119
141
  /**
120
142
  * Invoke a tool function, recording both an OpenTelemetry span and (when
121
143
  * tracing is enabled) a ToolTrace entry. All three tool-call sites in the
@@ -125,6 +147,16 @@ export declare class ExecutionTree {
125
147
  shadow(): ExecutionTree;
126
148
  /** Returns collected traces (empty array when tracing is disabled). */
127
149
  getTraces(): ToolTrace[];
150
+ /**
151
+ * Traverse `ref.path` on an already-resolved value, respecting null guards.
152
+ * Extracted from `pullSingle` so the sync and async paths can share logic.
153
+ */
154
+ private applyPath;
155
+ /**
156
+ * Pull a single value. Returns synchronously when already in state;
157
+ * returns a Promise only when the value is a pending tool call.
158
+ * See docs/performance.md (#10).
159
+ */
128
160
  private pullSingle;
129
161
  push(args: Record<string, any>): void;
130
162
  /** Store the aggregated promise for critical forced handles so
@@ -163,7 +195,16 @@ export declare class ExecutionTree {
163
195
  * After layers 1–2b, the overdefinition boundary (`!= null`) decides whether
164
196
  * to return or continue to the next wire.
165
197
  */
198
+ /**
199
+ * Resolve wires, returning synchronously when the hot path allows it.
200
+ *
201
+ * Fast path: single `from` wire with no fallback/catch modifiers, which is
202
+ * the common case for element field wires like `.id <- it.id`. Delegates to
203
+ * `resolveWiresAsync` for anything more complex.
204
+ * See docs/performance.md (#10).
205
+ */
166
206
  private resolveWires;
207
+ private resolveWiresAsync;
167
208
  /**
168
209
  * Resolve an output field by path for use outside of a GraphQL resolver.
169
210
  *
@@ -179,6 +220,22 @@ export declare class ExecutionTree {
179
220
  * in a shadow tree (mirrors `response()` array handling).
180
221
  */
181
222
  pullOutputField(path: string[], array?: boolean): Promise<unknown>;
223
+ /**
224
+ * Resolve pre-grouped wires on this shadow tree without re-filtering.
225
+ * Called by the parent's `materializeShadows` to skip per-element wire
226
+ * filtering. Returns synchronously when the wire resolves sync (hot path).
227
+ * See docs/performance.md (#8, #10).
228
+ */
229
+ resolvePreGrouped(wires: Wire[]): MaybePromise<unknown>;
230
+ /**
231
+ * Materialise all output wires into a plain JS object.
232
+ *
233
+ * Used by the GraphQL adapter when a bridge field returns a scalar type
234
+ * (e.g. `JSON`, `JSONObject`). In that case GraphQL won't call sub-field
235
+ * resolvers, so we need to eagerly resolve every output wire and assemble
236
+ * the result ourselves — the same logic `run()` uses for object output.
237
+ */
238
+ collectOutput(): Promise<unknown>;
182
239
  /**
183
240
  * Execute the bridge end-to-end without GraphQL.
184
241
  *
@@ -1 +1 @@
1
- {"version":3,"file":"ExecutionTree.d.ts","sourceRoot":"","sources":["../../../../bridge-core/src/ExecutionTree.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ExecutionTree.d.ts","sourceRoot":"","sources":["../../../../bridge-core/src/ExecutionTree.ts"],"names":[],"mappings":"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"}
@@ -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
- return a.length === b.length && a.every((v, i) => v === b[i]);
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
- return JSON.parse(raw);
198
+ result = JSON.parse(raw);
133
199
  }
134
200
  catch {
135
- return raw;
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
- 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
- }
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);
532
585
  }
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
- }
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
- // 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);
628
+ else {
629
+ setNested(input, path, value);
565
630
  }
566
- if (directFn) {
567
- return this.callTool(toolName, toolName, directFn, input);
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
- // 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;
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
- // 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;
687
+ else {
688
+ setNested(input, path, value);
586
689
  }
587
- throw new Error(`No tool found for "${toolName}"`);
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
- async callTool(toolName, fnName, fnImpl, input) {
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
- const child = new ExecutionTree(this.trunk, this.document, this.toolFns, undefined, this);
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
- async pullSingle(ref, pullChain = new Set()) {
671
- const key = trunkKey(ref);
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];
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;
883
+ value = this.state[key]; // sync value or Promise (see #12)
706
884
  }
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]}')`);
885
+ // Sync fast path: value is already resolved (not a pending Promise).
886
+ if (!isPromise(value)) {
887
+ return this.applyPath(value, ref);
713
888
  }
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;
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
- async resolveWires(wires, pullChain) {
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[trunkKey({ ...this.trunk, element: true })] = item;
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
  *
@@ -1108,6 +1386,9 @@ export class ExecutionTree {
1108
1386
  const { type, field } = this.trunk;
1109
1387
  const directFields = new Set();
1110
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();
1111
1392
  for (const wire of wires) {
1112
1393
  const p = wire.to.path;
1113
1394
  if (wire.to.module !== SELF_MODULE ||
@@ -1121,6 +1402,13 @@ export class ExecutionTree {
1121
1402
  const name = p[pathPrefix.length];
1122
1403
  if (p.length === pathPrefix.length + 1) {
1123
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);
1124
1412
  }
1125
1413
  else {
1126
1414
  let arr = deepPaths.get(name);
@@ -1131,6 +1419,63 @@ export class ExecutionTree {
1131
1419
  arr.push(p);
1132
1420
  }
1133
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.
1134
1479
  const rawResults = await Promise.all(items.map(async (shadow) => {
1135
1480
  const obj = {};
1136
1481
  const tasks = [];
@@ -1145,7 +1490,10 @@ export class ExecutionTree {
1145
1490
  : children;
1146
1491
  }
1147
1492
  else {
1148
- obj[name] = await shadow.pullOutputField(fullPath);
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));
1149
1497
  }
1150
1498
  })());
1151
1499
  }
@@ -1236,7 +1584,7 @@ export class ExecutionTree {
1236
1584
  if (item === CONTINUE_SYM)
1237
1585
  continue;
1238
1586
  const s = this.shadow();
1239
- s.state[trunkKey({ ...this.trunk, element: true })] = item;
1587
+ s.state[this.elementTrunkKey] = item;
1240
1588
  shadowTrees.push(s);
1241
1589
  }
1242
1590
  return shadowTrees;
@@ -1262,7 +1610,7 @@ export class ExecutionTree {
1262
1610
  if (item === CONTINUE_SYM)
1263
1611
  continue;
1264
1612
  const s = this.shadow();
1265
- s.state[trunkKey({ ...this.trunk, element: true })] = item;
1613
+ s.state[this.elementTrunkKey] = item;
1266
1614
  shadowTrees.push(s);
1267
1615
  }
1268
1616
  return shadowTrees;
@@ -1273,8 +1621,7 @@ export class ExecutionTree {
1273
1621
  // where the bridge maps an inner array (e.g. `.stops <- j.stops`) but
1274
1622
  // doesn't explicitly wire each scalar field on the element type.
1275
1623
  if (this.parent) {
1276
- const elementKey = trunkKey({ ...this.trunk, element: true });
1277
- const elementData = this.state[elementKey];
1624
+ const elementData = this.state[this.elementTrunkKey];
1278
1625
  if (elementData != null &&
1279
1626
  typeof elementData === "object" &&
1280
1627
  !Array.isArray(elementData)) {
@@ -1286,7 +1633,7 @@ export class ExecutionTree {
1286
1633
  // resolve their own fields via this same fallback path.
1287
1634
  return value.map((item) => {
1288
1635
  const s = this.shadow();
1289
- s.state[elementKey] = item;
1636
+ s.state[this.elementTrunkKey] = item;
1290
1637
  return s;
1291
1638
  });
1292
1639
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackables/bridge-compiler",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Bridge DSL parser, serializer, and language service",
5
5
  "main": "./build/index.js",
6
6
  "type": "module",
@@ -23,7 +23,7 @@
23
23
  "license": "MIT",
24
24
  "dependencies": {
25
25
  "chevrotain": "^11.1.2",
26
- "@stackables/bridge-core": "1.0.1",
26
+ "@stackables/bridge-core": "1.0.2",
27
27
  "@stackables/bridge-stdlib": "1.5.0"
28
28
  },
29
29
  "devDependencies": {