@stackables/bridge 1.4.2 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/ExecutionTree.d.ts +49 -0
- package/build/ExecutionTree.d.ts.map +1 -1
- package/build/ExecutionTree.js +194 -38
- package/build/bridge-format.d.ts.map +1 -1
- package/build/bridge-format.js +11 -0
- package/build/bridge-transform.d.ts +33 -0
- package/build/bridge-transform.d.ts.map +1 -1
- package/build/bridge-transform.js +51 -1
- package/build/index.d.ts +2 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -1
- package/package.json +8 -1
package/build/ExecutionTree.d.ts
CHANGED
|
@@ -11,6 +11,45 @@ type Trunk = {
|
|
|
11
11
|
field: string;
|
|
12
12
|
instance?: number;
|
|
13
13
|
};
|
|
14
|
+
/** Trace verbosity level */
|
|
15
|
+
export type TraceLevel = "basic" | "full";
|
|
16
|
+
/** A single recorded tool invocation. */
|
|
17
|
+
export type ToolTrace = {
|
|
18
|
+
/** Tool name as resolved (e.g. "hereGeo", "std.upperCase") */
|
|
19
|
+
tool: string;
|
|
20
|
+
/** The function that was called (e.g. "httpCall", "upperCase") */
|
|
21
|
+
fn: string;
|
|
22
|
+
/** Input object passed to the tool function (only in "full" level) */
|
|
23
|
+
input?: Record<string, any>;
|
|
24
|
+
/** Resolved output (only in "full" level, on success) */
|
|
25
|
+
output?: any;
|
|
26
|
+
/** Error message (present when the tool threw) */
|
|
27
|
+
error?: string;
|
|
28
|
+
/** Wall-clock duration in milliseconds */
|
|
29
|
+
durationMs: number;
|
|
30
|
+
/** Monotonic timestamp (ms) relative to the first trace in the request */
|
|
31
|
+
startedAt: number;
|
|
32
|
+
};
|
|
33
|
+
/** Shared trace collector — one per request, passed through the tree. */
|
|
34
|
+
export declare class TraceCollector {
|
|
35
|
+
readonly traces: ToolTrace[];
|
|
36
|
+
readonly level: TraceLevel;
|
|
37
|
+
private readonly epoch;
|
|
38
|
+
constructor(level?: TraceLevel);
|
|
39
|
+
/** Returns ms since the collector was created */
|
|
40
|
+
now(): number;
|
|
41
|
+
record(trace: ToolTrace): void;
|
|
42
|
+
/** Build a trace entry, omitting input/output for basic level. */
|
|
43
|
+
entry(base: {
|
|
44
|
+
tool: string;
|
|
45
|
+
fn: string;
|
|
46
|
+
startedAt: number;
|
|
47
|
+
durationMs: number;
|
|
48
|
+
input?: Record<string, any>;
|
|
49
|
+
output?: any;
|
|
50
|
+
error?: string;
|
|
51
|
+
}): ToolTrace;
|
|
52
|
+
}
|
|
14
53
|
export declare class ExecutionTree {
|
|
15
54
|
trunk: Trunk;
|
|
16
55
|
private instructions;
|
|
@@ -22,6 +61,8 @@ export declare class ExecutionTree {
|
|
|
22
61
|
private toolDepCache;
|
|
23
62
|
private toolDefCache;
|
|
24
63
|
private pipeHandleMap;
|
|
64
|
+
/** Shared trace collector — present only when tracing is enabled. */
|
|
65
|
+
tracer?: TraceCollector;
|
|
25
66
|
constructor(trunk: Trunk, instructions: Instruction[], toolFns?: ToolMap | undefined, context?: Record<string, any> | undefined, parent?: ExecutionTree | undefined);
|
|
26
67
|
/** Derive tool name from a trunk */
|
|
27
68
|
private getToolName;
|
|
@@ -38,7 +79,15 @@ export declare class ExecutionTree {
|
|
|
38
79
|
private resolveToolDep;
|
|
39
80
|
schedule(target: Trunk): any;
|
|
40
81
|
shadow(): ExecutionTree;
|
|
82
|
+
/** Returns collected traces (empty array when tracing is disabled). */
|
|
83
|
+
getTraces(): ToolTrace[];
|
|
41
84
|
private pullSingle;
|
|
85
|
+
/**
|
|
86
|
+
* Infer the cost of resolving a NodeRef.
|
|
87
|
+
* Cost 0: memory reads (input, context, const) — no I/O.
|
|
88
|
+
* Cost 1: everything else (tool calls, pipes, defines) — may involve network.
|
|
89
|
+
*/
|
|
90
|
+
private inferCost;
|
|
42
91
|
pull(refs: NodeRef[]): Promise<any>;
|
|
43
92
|
push(args: Record<string, any>): void;
|
|
44
93
|
/** Eagerly schedule tools targeted by forced (<-!) wires. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExecutionTree.d.ts","sourceRoot":"","sources":["../src/ExecutionTree.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,MAAM,EAEN,WAAW,EACX,OAAO,EAGP,OAAO,EAER,MAAM,YAAY,CAAC;AAGpB,gFAAgF;AAChF,UAAU,IAAI;IACZ,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,CAAC;IAChC,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;CACvC;AAED,KAAK,KAAK,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"ExecutionTree.d.ts","sourceRoot":"","sources":["../src/ExecutionTree.ts"],"names":[],"mappings":"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"}
|
package/build/ExecutionTree.js
CHANGED
|
@@ -17,6 +17,40 @@ function sameTrunk(a, b) {
|
|
|
17
17
|
function pathEquals(a, b) {
|
|
18
18
|
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
19
19
|
}
|
|
20
|
+
/** Shared trace collector — one per request, passed through the tree. */
|
|
21
|
+
export class TraceCollector {
|
|
22
|
+
traces = [];
|
|
23
|
+
level;
|
|
24
|
+
epoch = performance.now();
|
|
25
|
+
constructor(level = "full") {
|
|
26
|
+
this.level = level;
|
|
27
|
+
}
|
|
28
|
+
/** Returns ms since the collector was created */
|
|
29
|
+
now() {
|
|
30
|
+
return Math.round((performance.now() - this.epoch) * 100) / 100;
|
|
31
|
+
}
|
|
32
|
+
record(trace) {
|
|
33
|
+
this.traces.push(trace);
|
|
34
|
+
}
|
|
35
|
+
/** Build a trace entry, omitting input/output for basic level. */
|
|
36
|
+
entry(base) {
|
|
37
|
+
if (this.level === "basic") {
|
|
38
|
+
const t = { tool: base.tool, fn: base.fn, durationMs: base.durationMs, startedAt: base.startedAt };
|
|
39
|
+
if (base.error)
|
|
40
|
+
t.error = base.error;
|
|
41
|
+
return t;
|
|
42
|
+
}
|
|
43
|
+
// full
|
|
44
|
+
const t = { tool: base.tool, fn: base.fn, durationMs: base.durationMs, startedAt: base.startedAt };
|
|
45
|
+
if (base.input)
|
|
46
|
+
t.input = structuredClone(base.input);
|
|
47
|
+
if (base.error)
|
|
48
|
+
t.error = base.error;
|
|
49
|
+
else if (base.output !== undefined)
|
|
50
|
+
t.output = base.output;
|
|
51
|
+
return t;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
20
54
|
/** Set a value at a nested path, creating intermediate objects/arrays as needed */
|
|
21
55
|
function setNested(obj, path, value) {
|
|
22
56
|
for (let i = 0; i < path.length - 1; i++) {
|
|
@@ -42,6 +76,8 @@ export class ExecutionTree {
|
|
|
42
76
|
toolDepCache = new Map();
|
|
43
77
|
toolDefCache = new Map();
|
|
44
78
|
pipeHandleMap;
|
|
79
|
+
/** Shared trace collector — present only when tracing is enabled. */
|
|
80
|
+
tracer;
|
|
45
81
|
constructor(trunk, instructions, toolFns, context, parent) {
|
|
46
82
|
this.trunk = trunk;
|
|
47
83
|
this.instructions = instructions;
|
|
@@ -186,10 +222,23 @@ export class ExecutionTree {
|
|
|
186
222
|
throw new Error(`Unknown source "${handle}" in tool "${toolDef.name}"`);
|
|
187
223
|
let value;
|
|
188
224
|
if (dep.kind === "context") {
|
|
189
|
-
|
|
225
|
+
// Walk the full parent chain for context
|
|
226
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
227
|
+
let cursor = this;
|
|
228
|
+
while (cursor && value === undefined) {
|
|
229
|
+
value = cursor.context;
|
|
230
|
+
cursor = cursor.parent;
|
|
231
|
+
}
|
|
190
232
|
}
|
|
191
233
|
else if (dep.kind === "const") {
|
|
192
|
-
|
|
234
|
+
// Walk the full parent chain for const state
|
|
235
|
+
const constKey = trunkKey({ module: SELF_MODULE, type: "Const", field: "const" });
|
|
236
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
237
|
+
let cursor = this;
|
|
238
|
+
while (cursor && value === undefined) {
|
|
239
|
+
value = cursor.state[constKey];
|
|
240
|
+
cursor = cursor.parent;
|
|
241
|
+
}
|
|
193
242
|
}
|
|
194
243
|
else if (dep.kind === "tool") {
|
|
195
244
|
value = await this.resolveToolDep(dep.tool);
|
|
@@ -217,10 +266,19 @@ export class ExecutionTree {
|
|
|
217
266
|
throw new Error(`Tool function "${toolDef.fn}" not registered`);
|
|
218
267
|
// on error: wrap the tool call with fallback from onError wire
|
|
219
268
|
const onErrorWire = toolDef.wires.find((w) => w.kind === "onError");
|
|
269
|
+
const tracer = this.tracer;
|
|
270
|
+
const traceStart = tracer?.now();
|
|
220
271
|
try {
|
|
221
|
-
|
|
272
|
+
const result = await fn(input);
|
|
273
|
+
if (tracer && traceStart != null) {
|
|
274
|
+
tracer.record(tracer.entry({ tool: toolName, fn: toolDef.fn, input, output: result, durationMs: Math.round((tracer.now() - traceStart) * 100) / 100, startedAt: traceStart }));
|
|
275
|
+
}
|
|
276
|
+
return result;
|
|
222
277
|
}
|
|
223
278
|
catch (err) {
|
|
279
|
+
if (tracer && traceStart != null) {
|
|
280
|
+
tracer.record(tracer.entry({ tool: toolName, fn: toolDef.fn, input, error: err.message, durationMs: Math.round((tracer.now() - traceStart) * 100) / 100, startedAt: traceStart }));
|
|
281
|
+
}
|
|
224
282
|
if (!onErrorWire)
|
|
225
283
|
throw err;
|
|
226
284
|
if ("value" in onErrorWire)
|
|
@@ -258,10 +316,23 @@ export class ExecutionTree {
|
|
|
258
316
|
if (toolDef) {
|
|
259
317
|
await this.resolveToolWires(toolDef, input);
|
|
260
318
|
}
|
|
261
|
-
// Resolve bridge wires and apply on top
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
319
|
+
// Resolve bridge wires and apply on top.
|
|
320
|
+
// Group wires by target path so that || (null-fallback) and ??
|
|
321
|
+
// (error-fallback) semantics are honoured via resolveWires().
|
|
322
|
+
const wireGroups = new Map();
|
|
323
|
+
for (const w of bridgeWires) {
|
|
324
|
+
const key = w.to.path.join(".");
|
|
325
|
+
let group = wireGroups.get(key);
|
|
326
|
+
if (!group) {
|
|
327
|
+
group = [];
|
|
328
|
+
wireGroups.set(key, group);
|
|
329
|
+
}
|
|
330
|
+
group.push(w);
|
|
331
|
+
}
|
|
332
|
+
const groupEntries = Array.from(wireGroups.entries());
|
|
333
|
+
const resolved = await Promise.all(groupEntries.map(async ([, group]) => {
|
|
334
|
+
const value = await this.resolveWires(group);
|
|
335
|
+
return [group[0].to.path, value];
|
|
265
336
|
}));
|
|
266
337
|
for (const [path, value] of resolved) {
|
|
267
338
|
if (path.length === 0 && value != null && typeof value === "object") {
|
|
@@ -278,10 +349,19 @@ export class ExecutionTree {
|
|
|
278
349
|
throw new Error(`Tool function "${toolDef.fn}" not registered`);
|
|
279
350
|
// on error: wrap the tool call with fallback from onError wire
|
|
280
351
|
const onErrorWire = toolDef.wires.find((w) => w.kind === "onError");
|
|
352
|
+
const tracer = this.tracer;
|
|
353
|
+
const traceStart = tracer?.now();
|
|
281
354
|
try {
|
|
282
|
-
|
|
355
|
+
const result = await fn(input);
|
|
356
|
+
if (tracer && traceStart != null) {
|
|
357
|
+
tracer.record(tracer.entry({ tool: toolName, fn: toolDef.fn, input, output: result, durationMs: Math.round((tracer.now() - traceStart) * 100) / 100, startedAt: traceStart }));
|
|
358
|
+
}
|
|
359
|
+
return result;
|
|
283
360
|
}
|
|
284
361
|
catch (err) {
|
|
362
|
+
if (tracer && traceStart != null) {
|
|
363
|
+
tracer.record(tracer.entry({ tool: toolName, fn: toolDef.fn, input, error: err.message, durationMs: Math.round((tracer.now() - traceStart) * 100) / 100, startedAt: traceStart }));
|
|
364
|
+
}
|
|
285
365
|
if (!onErrorWire)
|
|
286
366
|
throw err;
|
|
287
367
|
if ("value" in onErrorWire)
|
|
@@ -292,7 +372,21 @@ export class ExecutionTree {
|
|
|
292
372
|
// Direct tool function lookup by name (simple or dotted)
|
|
293
373
|
const directFn = this.lookupToolFn(toolName);
|
|
294
374
|
if (directFn) {
|
|
295
|
-
|
|
375
|
+
const tracer = this.tracer;
|
|
376
|
+
const traceStart = tracer?.now();
|
|
377
|
+
try {
|
|
378
|
+
const result = await directFn(input);
|
|
379
|
+
if (tracer && traceStart != null) {
|
|
380
|
+
tracer.record(tracer.entry({ tool: toolName, fn: toolName, input, output: result, durationMs: Math.round((tracer.now() - traceStart) * 100) / 100, startedAt: traceStart }));
|
|
381
|
+
}
|
|
382
|
+
return result;
|
|
383
|
+
}
|
|
384
|
+
catch (err) {
|
|
385
|
+
if (tracer && traceStart != null) {
|
|
386
|
+
tracer.record(tracer.entry({ tool: toolName, fn: toolName, input, error: err.message, durationMs: Math.round((tracer.now() - traceStart) * 100) / 100, startedAt: traceStart }));
|
|
387
|
+
}
|
|
388
|
+
throw err;
|
|
389
|
+
}
|
|
296
390
|
}
|
|
297
391
|
// Define pass-through: synthetic trunks created by define inlining
|
|
298
392
|
// act as data containers — bridge wires set their values, no tool needed.
|
|
@@ -303,11 +397,24 @@ export class ExecutionTree {
|
|
|
303
397
|
})();
|
|
304
398
|
}
|
|
305
399
|
shadow() {
|
|
306
|
-
|
|
400
|
+
const child = new ExecutionTree(this.trunk, this.instructions, this.toolFns, undefined, this);
|
|
401
|
+
child.tracer = this.tracer;
|
|
402
|
+
return child;
|
|
403
|
+
}
|
|
404
|
+
/** Returns collected traces (empty array when tracing is disabled). */
|
|
405
|
+
getTraces() {
|
|
406
|
+
return this.tracer?.traces ?? [];
|
|
307
407
|
}
|
|
308
408
|
async pullSingle(ref) {
|
|
309
409
|
const key = trunkKey(ref);
|
|
310
|
-
|
|
410
|
+
// Walk the full parent chain — shadow trees may be nested multiple levels
|
|
411
|
+
let value = undefined;
|
|
412
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
413
|
+
let cursor = this;
|
|
414
|
+
while (cursor && value === undefined) {
|
|
415
|
+
value = cursor.state[key];
|
|
416
|
+
cursor = cursor.parent;
|
|
417
|
+
}
|
|
311
418
|
if (value === undefined) {
|
|
312
419
|
this.state[key] = this.schedule(ref);
|
|
313
420
|
value = this.state[key];
|
|
@@ -326,37 +433,62 @@ export class ExecutionTree {
|
|
|
326
433
|
}
|
|
327
434
|
return result;
|
|
328
435
|
}
|
|
436
|
+
/**
|
|
437
|
+
* Infer the cost of resolving a NodeRef.
|
|
438
|
+
* Cost 0: memory reads (input, context, const) — no I/O.
|
|
439
|
+
* Cost 1: everything else (tool calls, pipes, defines) — may involve network.
|
|
440
|
+
*/
|
|
441
|
+
inferCost(ref) {
|
|
442
|
+
// Input args, context, and const live in the state map — free reads
|
|
443
|
+
if (ref.module === SELF_MODULE) {
|
|
444
|
+
const key = trunkKey(ref);
|
|
445
|
+
// Already resolved in state? Free.
|
|
446
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
447
|
+
let cursor = this;
|
|
448
|
+
while (cursor) {
|
|
449
|
+
if (cursor.state[key] !== undefined)
|
|
450
|
+
return 0;
|
|
451
|
+
cursor = cursor.parent;
|
|
452
|
+
}
|
|
453
|
+
// Input/context/const trunks are always cost 0
|
|
454
|
+
if (ref.type === "Context" || ref.type === "Const")
|
|
455
|
+
return 0;
|
|
456
|
+
// Input args trunk: _:Query:fieldName (same as this.trunk) — cost 0
|
|
457
|
+
if (ref.module === SELF_MODULE && ref.type === this.trunk.type && ref.field === this.trunk.field && !ref.element)
|
|
458
|
+
return 0;
|
|
459
|
+
}
|
|
460
|
+
return 1;
|
|
461
|
+
}
|
|
329
462
|
async pull(refs) {
|
|
330
463
|
if (refs.length === 1)
|
|
331
464
|
return this.pullSingle(refs[0]);
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
};
|
|
350
|
-
for (const ref of refs) {
|
|
351
|
-
this.pullSingle(ref).then((value) => {
|
|
352
|
-
if (!hasValue && value != null) {
|
|
353
|
-
hasValue = true;
|
|
354
|
-
resolve(value);
|
|
355
|
-
}
|
|
356
|
-
settle();
|
|
357
|
-
}, (err) => { errors.push(err); settle(); });
|
|
465
|
+
// Cost-sorted sequential evaluation with short-circuit.
|
|
466
|
+
//
|
|
467
|
+
// Sort by inferred cost (stable — preserves declaration order within
|
|
468
|
+
// the same cost tier). This means:
|
|
469
|
+
// • || chains (both sources are tools = same cost) → left-to-right
|
|
470
|
+
// • Overdefinition with mixed costs → cheapest first
|
|
471
|
+
//
|
|
472
|
+
// Evaluate sequentially. Return the first non-null value.
|
|
473
|
+
// If all return null/undefined → return undefined (lets || fire).
|
|
474
|
+
// If all throw → throw AggregateError (lets ?? fire).
|
|
475
|
+
const sorted = [...refs].sort((a, b) => this.inferCost(a) - this.inferCost(b));
|
|
476
|
+
const errors = [];
|
|
477
|
+
for (const ref of sorted) {
|
|
478
|
+
try {
|
|
479
|
+
const value = await this.pullSingle(ref);
|
|
480
|
+
if (value != null)
|
|
481
|
+
return value; // Short-circuit: found data
|
|
358
482
|
}
|
|
359
|
-
|
|
483
|
+
catch (err) {
|
|
484
|
+
errors.push(err);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// All resolved to null/undefined, or all threw
|
|
488
|
+
if (errors.length === refs.length) {
|
|
489
|
+
throw new AggregateError(errors, "All sources failed");
|
|
490
|
+
}
|
|
491
|
+
return undefined;
|
|
360
492
|
}
|
|
361
493
|
push(args) {
|
|
362
494
|
this.state[trunkKey(this.trunk)] = args;
|
|
@@ -458,6 +590,30 @@ export class ExecutionTree {
|
|
|
458
590
|
return s;
|
|
459
591
|
});
|
|
460
592
|
}
|
|
593
|
+
// Fallback: if this shadow tree has stored element data, resolve the
|
|
594
|
+
// requested field directly from it. This handles passthrough arrays
|
|
595
|
+
// where the bridge maps an inner array (e.g. `.stops <- j.stops`) but
|
|
596
|
+
// doesn't explicitly wire each scalar field on the element type.
|
|
597
|
+
if (this.parent) {
|
|
598
|
+
const elementKey = trunkKey({ ...this.trunk, element: true });
|
|
599
|
+
const elementData = this.state[elementKey];
|
|
600
|
+
if (elementData != null && typeof elementData === "object" && !Array.isArray(elementData)) {
|
|
601
|
+
const fieldName = cleanPath[cleanPath.length - 1];
|
|
602
|
+
if (fieldName !== undefined && fieldName in elementData) {
|
|
603
|
+
const value = elementData[fieldName];
|
|
604
|
+
if (array && Array.isArray(value)) {
|
|
605
|
+
// Nested array: wrap items in shadow trees so they can
|
|
606
|
+
// resolve their own fields via this same fallback path.
|
|
607
|
+
return value.map((item) => {
|
|
608
|
+
const s = this.shadow();
|
|
609
|
+
s.state[elementKey] = item;
|
|
610
|
+
return s;
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
return value;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
461
617
|
// Return self to trigger downstream resolvers
|
|
462
618
|
return this;
|
|
463
619
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge-format.d.ts","sourceRoot":"","sources":["../src/bridge-format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAKR,WAAW,EAMd,MAAM,YAAY,CAAC;AA8CpB,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CA0FvD;
|
|
1
|
+
{"version":3,"file":"bridge-format.d.ts","sourceRoot":"","sources":["../src/bridge-format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAKR,WAAW,EAMd,MAAM,YAAY,CAAC;AA8CpB,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CA0FvD;AAmxCD;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAchD;AAID;;GAEG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,WAAW,EAAE,GAAG,MAAM,CA8BnE"}
|
package/build/bridge-format.js
CHANGED
|
@@ -409,6 +409,7 @@ function parseBridgeBlock(block, lineOffset, previousInstructions) {
|
|
|
409
409
|
const [, targetStr, quotedValue, unquotedValue] = constantMatch;
|
|
410
410
|
const value = quotedValue ?? unquotedValue;
|
|
411
411
|
const toRef = resolveAddress(targetStr, handleRes, bridgeType, bridgeField, ln(i));
|
|
412
|
+
assertNoTargetIndices(toRef, ln(i));
|
|
412
413
|
wires.push({ value, to: toRef });
|
|
413
414
|
continue;
|
|
414
415
|
}
|
|
@@ -427,6 +428,7 @@ function parseBridgeBlock(block, lineOffset, previousInstructions) {
|
|
|
427
428
|
assertNotReserved(iterHandle, ln(i), "iterator handle");
|
|
428
429
|
const fromRef = resolveAddress(fromClean, handleRes, bridgeType, bridgeField, ln(i));
|
|
429
430
|
const toRef = resolveAddress(targetStr, handleRes, bridgeType, bridgeField, ln(i));
|
|
431
|
+
assertNoTargetIndices(toRef, ln(i));
|
|
430
432
|
wires.push({ from: fromRef, to: toRef });
|
|
431
433
|
currentArrayToPath = toRef.path;
|
|
432
434
|
currentIterHandle = iterHandle;
|
|
@@ -469,6 +471,7 @@ function parseBridgeBlock(block, lineOffset, previousInstructions) {
|
|
|
469
471
|
fallbackInternalWires = wires.splice(preLen);
|
|
470
472
|
}
|
|
471
473
|
const toRef = resolveAddress(targetStr, handleRes, bridgeType, bridgeField, ln(i));
|
|
474
|
+
assertNoTargetIndices(toRef, ln(i));
|
|
472
475
|
for (let ci = 0; ci < sourceParts.length; ci++) {
|
|
473
476
|
const isFirst = ci === 0;
|
|
474
477
|
const isLast = ci === sourceParts.length - 1;
|
|
@@ -927,6 +930,14 @@ function resolveAddress(address, handles, bridgeType, bridgeField, lineNum) {
|
|
|
927
930
|
throw new Error(`${lineNum != null ? `Line ${lineNum}: ` : ""}Undeclared handle "${prefix}". ` +
|
|
928
931
|
`Add 'with ${prefix}' or 'with ${prefix} as ${prefix}' to the bridge header.`);
|
|
929
932
|
}
|
|
933
|
+
/** Reject explicit array indices on the target (LHS) of a wire. */
|
|
934
|
+
function assertNoTargetIndices(ref, lineNum) {
|
|
935
|
+
if (ref.path.some((seg) => /^\d+$/.test(seg))) {
|
|
936
|
+
throw new Error(`${lineNum != null ? `Line ${lineNum}: ` : ""}` +
|
|
937
|
+
`Explicit array index in wire target is not supported. ` +
|
|
938
|
+
`Use array mapping (\`[] as iter { }\`) instead.`);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
930
941
|
// ── Const block parser ──────────────────────────────────────────────────────
|
|
931
942
|
/**
|
|
932
943
|
* Parse `const` declarations into ConstDef instructions.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type GraphQLSchema } from "graphql";
|
|
2
|
+
import { type ToolTrace, type TraceLevel } from "./ExecutionTree.js";
|
|
2
3
|
import type { Instruction, ToolMap } from "./types.js";
|
|
3
4
|
export type BridgeOptions = {
|
|
4
5
|
/** Tool functions available to the engine.
|
|
@@ -9,8 +10,40 @@ export type BridgeOptions = {
|
|
|
9
10
|
/** Optional function to reshape/restrict the GQL context before it reaches bridge files.
|
|
10
11
|
* By default the full context is exposed via `with context`. */
|
|
11
12
|
contextMapper?: (context: any) => Record<string, any>;
|
|
13
|
+
/** Enable tool-call tracing.
|
|
14
|
+
* - `true` or `"full"`: record tool, fn, input, output/error, timing
|
|
15
|
+
* - `"basic"`: record tool, fn, timing, error (no input/output) */
|
|
16
|
+
trace?: boolean | TraceLevel;
|
|
12
17
|
};
|
|
13
18
|
/** Instructions can be a static array or a function that selects per-request */
|
|
14
19
|
export type InstructionSource = Instruction[] | ((context: any) => Instruction[]);
|
|
15
20
|
export declare function bridgeTransform(schema: GraphQLSchema, instructions: InstructionSource, options?: BridgeOptions): GraphQLSchema;
|
|
21
|
+
/**
|
|
22
|
+
* Read traces that were collected during the current request.
|
|
23
|
+
* Pass the GraphQL context object; returns an empty array when tracing is
|
|
24
|
+
* disabled or no traces were recorded.
|
|
25
|
+
*/
|
|
26
|
+
export declare function getBridgeTraces(context: any): ToolTrace[];
|
|
27
|
+
/**
|
|
28
|
+
* Envelop-compatible plugin for GraphQL Yoga (or any Envelop-based server).
|
|
29
|
+
* When bridge tracing is enabled, this plugin copies the recorded traces into
|
|
30
|
+
* the GraphQL response `extensions.traces` field.
|
|
31
|
+
*
|
|
32
|
+
* Usage:
|
|
33
|
+
* ```ts
|
|
34
|
+
* createYoga({ schema, plugins: [useBridgeTracing()] })
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function useBridgeTracing(): {
|
|
38
|
+
onExecute({ args }: {
|
|
39
|
+
args: {
|
|
40
|
+
contextValue: any;
|
|
41
|
+
};
|
|
42
|
+
}): {
|
|
43
|
+
onExecuteDone({ result, setResult, }: {
|
|
44
|
+
result: any;
|
|
45
|
+
setResult: (r: any) => void;
|
|
46
|
+
}): void;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
16
49
|
//# sourceMappingURL=bridge-transform.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge-transform.d.ts","sourceRoot":"","sources":["../src/bridge-transform.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,aAAa,EAEnB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"bridge-transform.d.ts","sourceRoot":"","sources":["../src/bridge-transform.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,aAAa,EAEnB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAiC,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEpG,OAAO,KAAK,EAAE,WAAW,EAAc,OAAO,EAAE,MAAM,YAAY,CAAC;AAGnE,MAAM,MAAM,aAAa,GAAG;IAC1B;;;kDAG8C;IAC9C,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;qEACiE;IACjE,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACtD;;wEAEoE;IACpE,KAAK,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;CAC9B,CAAC;AAEF,gFAAgF;AAChF,MAAM,MAAM,iBAAiB,GACzB,WAAW,EAAE,GACb,CAAC,CAAC,OAAO,EAAE,GAAG,KAAK,WAAW,EAAE,CAAC,CAAC;AAEtC,wBAAgB,eAAe,CAC7B,MAAM,EAAE,aAAa,EACrB,YAAY,EAAE,iBAAiB,EAC/B,OAAO,CAAC,EAAE,aAAa,GACtB,aAAa,CAuFf;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,GAAG,GAAG,SAAS,EAAE,CAEzD;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB;wBAER;QAAE,IAAI,EAAE;YAAE,YAAY,EAAE,GAAG,CAAA;SAAE,CAAA;KAAE;8CAK5C;YACD,MAAM,EAAE,GAAG,CAAC;YACZ,SAAS,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC;SAC7B;;EAeR"}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { MapperKind, mapSchema } from "@graphql-tools/utils";
|
|
2
2
|
import { GraphQLList, GraphQLNonNull, defaultFieldResolver, } from "graphql";
|
|
3
|
-
import { ExecutionTree } from "./ExecutionTree.js";
|
|
3
|
+
import { ExecutionTree, TraceCollector } from "./ExecutionTree.js";
|
|
4
4
|
import { builtinTools } from "./tools/index.js";
|
|
5
5
|
import { SELF_MODULE } from "./types.js";
|
|
6
6
|
export function bridgeTransform(schema, instructions, options) {
|
|
7
7
|
const userTools = options?.tools;
|
|
8
8
|
const contextMapper = options?.contextMapper;
|
|
9
|
+
const tracing = options?.trace ?? false;
|
|
10
|
+
const traceLevel = tracing === true ? "full" : tracing === false ? false : tracing;
|
|
9
11
|
return mapSchema(schema, {
|
|
10
12
|
[MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName) => {
|
|
11
13
|
let array = false;
|
|
@@ -36,6 +38,11 @@ export function bridgeTransform(schema, instructions, options) {
|
|
|
36
38
|
...(userTools ?? {}),
|
|
37
39
|
};
|
|
38
40
|
source = new ExecutionTree(trunk, activeInstructions, allTools, bridgeContext);
|
|
41
|
+
if (traceLevel) {
|
|
42
|
+
source.tracer = new TraceCollector(traceLevel);
|
|
43
|
+
// Stash tracer on GQL context so the tracing plugin can read it
|
|
44
|
+
context.__bridgeTracer = source.tracer;
|
|
45
|
+
}
|
|
39
46
|
}
|
|
40
47
|
if (source instanceof ExecutionTree &&
|
|
41
48
|
args &&
|
|
@@ -44,6 +51,11 @@ export function bridgeTransform(schema, instructions, options) {
|
|
|
44
51
|
}
|
|
45
52
|
// Kick off forced wires (<-!) at the root entry point
|
|
46
53
|
if (source instanceof ExecutionTree && !info.path.prev) {
|
|
54
|
+
// Ensure input state exists even with no args (prevents
|
|
55
|
+
// recursive scheduling of the input trunk → stack overflow).
|
|
56
|
+
if (!args || Object.keys(args).length === 0) {
|
|
57
|
+
source.push({});
|
|
58
|
+
}
|
|
47
59
|
source.executeForced();
|
|
48
60
|
}
|
|
49
61
|
if (source instanceof ExecutionTree) {
|
|
@@ -55,3 +67,41 @@ export function bridgeTransform(schema, instructions, options) {
|
|
|
55
67
|
},
|
|
56
68
|
});
|
|
57
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Read traces that were collected during the current request.
|
|
72
|
+
* Pass the GraphQL context object; returns an empty array when tracing is
|
|
73
|
+
* disabled or no traces were recorded.
|
|
74
|
+
*/
|
|
75
|
+
export function getBridgeTraces(context) {
|
|
76
|
+
return context?.__bridgeTracer?.traces ?? [];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Envelop-compatible plugin for GraphQL Yoga (or any Envelop-based server).
|
|
80
|
+
* When bridge tracing is enabled, this plugin copies the recorded traces into
|
|
81
|
+
* the GraphQL response `extensions.traces` field.
|
|
82
|
+
*
|
|
83
|
+
* Usage:
|
|
84
|
+
* ```ts
|
|
85
|
+
* createYoga({ schema, plugins: [useBridgeTracing()] })
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export function useBridgeTracing() {
|
|
89
|
+
return {
|
|
90
|
+
onExecute({ args }) {
|
|
91
|
+
return {
|
|
92
|
+
onExecuteDone({ result, setResult, }) {
|
|
93
|
+
const traces = getBridgeTraces(args.contextValue);
|
|
94
|
+
if (traces.length > 0 && result && "data" in result) {
|
|
95
|
+
setResult({
|
|
96
|
+
...result,
|
|
97
|
+
extensions: {
|
|
98
|
+
...(result.extensions ?? {}),
|
|
99
|
+
traces,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
package/build/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { parseBridge } from "./bridge-format.js";
|
|
2
|
-
export { bridgeTransform } from "./bridge-transform.js";
|
|
2
|
+
export { bridgeTransform, getBridgeTraces, useBridgeTracing } from "./bridge-transform.js";
|
|
3
3
|
export type { BridgeOptions, InstructionSource } from "./bridge-transform.js";
|
|
4
4
|
export { builtinTools, std, createHttpCall } from "./tools/index.js";
|
|
5
|
+
export type { ToolTrace, TraceLevel } from "./ExecutionTree.js";
|
|
5
6
|
export type { CacheStore, ConstDef, Instruction, ToolCallFn, ToolDef, ToolMap } from "./types.js";
|
|
6
7
|
//# sourceMappingURL=index.d.ts.map
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC3F,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC9E,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACrE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChE,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC"}
|
package/build/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { parseBridge } from "./bridge-format.js";
|
|
2
|
-
export { bridgeTransform } from "./bridge-transform.js";
|
|
2
|
+
export { bridgeTransform, getBridgeTraces, useBridgeTracing } from "./bridge-transform.js";
|
|
3
3
|
export { builtinTools, std, createHttpCall } from "./tools/index.js";
|
package/package.json
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackables/bridge",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Declarative dataflow for GraphQL",
|
|
5
5
|
"main": "./build/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"types": "./build/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"development": "./src/index.ts",
|
|
11
|
+
"import": "./build/index.js",
|
|
12
|
+
"types": "./build/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
8
15
|
"files": [
|
|
9
16
|
"build",
|
|
10
17
|
"README.md"
|