@stackables/bridge 1.5.0 → 1.6.1

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.
@@ -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. */
@@ -48,6 +97,14 @@ export declare class ExecutionTree {
48
97
  * `??` (fallback/fallbackRef): fires when all sources reject (throw/error). */
49
98
  private resolveWires;
50
99
  response(ipath: Path, array: boolean): Promise<any>;
100
+ /**
101
+ * Find define output wires for a specific field path.
102
+ *
103
+ * Looks for whole-object define forward wires (`o <- defineHandle`)
104
+ * at path=[] for this trunk, then searches the define's output wires
105
+ * for ones matching the requested field path.
106
+ */
107
+ private findDefineFieldWires;
51
108
  }
52
109
  export {};
53
110
  //# sourceMappingURL=ExecutionTree.d.ts.map
@@ -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;AAsChF,qBAAa,aAAa;IAQf,KAAK,EAAE,KAAK;IACnB,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,OAAO,CAAC;IAChB,OAAO,CAAC,OAAO,CAAC;IAChB,OAAO,CAAC,MAAM,CAAC;IAXjB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAM;IAChC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,OAAO,CAAC,YAAY,CAAwC;IAC5D,OAAO,CAAC,YAAY,CAA0C;IAC9D,OAAO,CAAC,aAAa,CAAsE;gBAGlF,KAAK,EAAE,KAAK,EACX,YAAY,EAAE,WAAW,EAAE,EAC3B,OAAO,CAAC,EAAE,OAAO,YAAA,EACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,YAAA,EAC7B,MAAM,CAAC,EAAE,aAAa,YAAA;IA8BhC,oCAAoC;IACpC,OAAO,CAAC,WAAW;IAKnB;uGACmG;IACnG,OAAO,CAAC,YAAY;IAsBpB,oEAAoE;IACpE,OAAO,CAAC,oBAAoB;IA8D5B,mEAAmE;YACrD,gBAAgB;IA0B9B,2EAA2E;YAC7D,iBAAiB;IAyC/B,kDAAkD;IAClD,OAAO,CAAC,cAAc;IAgCtB,QAAQ,CAAC,MAAM,EAAE,KAAK,GAAG,GAAG;IA8F5B,MAAM,IAAI,aAAa;YAUT,UAAU;IAmClB,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC;IAqCzC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAI9B,6DAA6D;IAC7D,aAAa,IAAI,IAAI;IAqBrB;;oFAEgF;IAChF,OAAO,CAAC,YAAY;IA8Cd,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC;CA+E1D"}
1
+ {"version":3,"file":"ExecutionTree.d.ts","sourceRoot":"","sources":["../src/ExecutionTree.ts"],"names":[],"mappings":"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"}
@@ -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;
@@ -230,10 +266,19 @@ export class ExecutionTree {
230
266
  throw new Error(`Tool function "${toolDef.fn}" not registered`);
231
267
  // on error: wrap the tool call with fallback from onError wire
232
268
  const onErrorWire = toolDef.wires.find((w) => w.kind === "onError");
269
+ const tracer = this.tracer;
270
+ const traceStart = tracer?.now();
233
271
  try {
234
- return await fn(input);
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;
235
277
  }
236
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
+ }
237
282
  if (!onErrorWire)
238
283
  throw err;
239
284
  if ("value" in onErrorWire)
@@ -304,10 +349,19 @@ export class ExecutionTree {
304
349
  throw new Error(`Tool function "${toolDef.fn}" not registered`);
305
350
  // on error: wrap the tool call with fallback from onError wire
306
351
  const onErrorWire = toolDef.wires.find((w) => w.kind === "onError");
352
+ const tracer = this.tracer;
353
+ const traceStart = tracer?.now();
307
354
  try {
308
- return await fn(input);
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;
309
360
  }
310
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
+ }
311
365
  if (!onErrorWire)
312
366
  throw err;
313
367
  if ("value" in onErrorWire)
@@ -318,7 +372,21 @@ export class ExecutionTree {
318
372
  // Direct tool function lookup by name (simple or dotted)
319
373
  const directFn = this.lookupToolFn(toolName);
320
374
  if (directFn) {
321
- return directFn(input);
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
+ }
322
390
  }
323
391
  // Define pass-through: synthetic trunks created by define inlining
324
392
  // act as data containers — bridge wires set their values, no tool needed.
@@ -329,7 +397,13 @@ export class ExecutionTree {
329
397
  })();
330
398
  }
331
399
  shadow() {
332
- return new ExecutionTree(this.trunk, this.instructions, this.toolFns, undefined, this);
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 ?? [];
333
407
  }
334
408
  async pullSingle(ref) {
335
409
  const key = trunkKey(ref);
@@ -342,6 +416,18 @@ export class ExecutionTree {
342
416
  cursor = cursor.parent;
343
417
  }
344
418
  if (value === undefined) {
419
+ // ── Lazy define field resolution ────────────────────────────────
420
+ // For define trunks (__define_in_* / __define_out_*) with a specific
421
+ // field path, resolve ONLY the wire(s) targeting that field instead
422
+ // of scheduling the entire trunk. This avoids triggering unrelated
423
+ // dependency chains (e.g. requesting "city" should not fire the
424
+ // lat/lon coalesce chains that call the geo tool).
425
+ if (ref.path.length > 0 && ref.module.startsWith("__define_")) {
426
+ const fieldWires = this.bridge?.wires.filter((w) => sameTrunk(w.to, ref) && pathEquals(w.to.path, ref.path)) ?? [];
427
+ if (fieldWires.length > 0) {
428
+ return this.resolveWires(fieldWires);
429
+ }
430
+ }
345
431
  this.state[key] = this.schedule(ref);
346
432
  value = this.state[key];
347
433
  }
@@ -359,37 +445,62 @@ export class ExecutionTree {
359
445
  }
360
446
  return result;
361
447
  }
448
+ /**
449
+ * Infer the cost of resolving a NodeRef.
450
+ * Cost 0: memory reads (input, context, const) — no I/O.
451
+ * Cost 1: everything else (tool calls, pipes, defines) — may involve network.
452
+ */
453
+ inferCost(ref) {
454
+ // Input args, context, and const live in the state map — free reads
455
+ if (ref.module === SELF_MODULE) {
456
+ const key = trunkKey(ref);
457
+ // Already resolved in state? Free.
458
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
459
+ let cursor = this;
460
+ while (cursor) {
461
+ if (cursor.state[key] !== undefined)
462
+ return 0;
463
+ cursor = cursor.parent;
464
+ }
465
+ // Input/context/const trunks are always cost 0
466
+ if (ref.type === "Context" || ref.type === "Const")
467
+ return 0;
468
+ // Input args trunk: _:Query:fieldName (same as this.trunk) — cost 0
469
+ if (ref.module === SELF_MODULE && ref.type === this.trunk.type && ref.field === this.trunk.field && !ref.element)
470
+ return 0;
471
+ }
472
+ return 1;
473
+ }
362
474
  async pull(refs) {
363
475
  if (refs.length === 1)
364
476
  return this.pullSingle(refs[0]);
365
- // Multiple sources: all start in parallel.
366
- // Return the first that resolves to a non-null/undefined value.
367
- // If all resolve to null/undefined resolve undefined (lets || fire).
368
- // If all reject throw AggregateError (lets ?? fire).
369
- return new Promise((resolve, reject) => {
370
- let remaining = refs.length;
371
- let hasValue = false;
372
- const errors = [];
373
- const settle = () => {
374
- if (--remaining === 0 && !hasValue) {
375
- if (errors.length === refs.length) {
376
- reject(new AggregateError(errors, "All sources failed"));
377
- }
378
- else {
379
- resolve(undefined); // all resolved to null/undefined
380
- }
381
- }
382
- };
383
- for (const ref of refs) {
384
- this.pullSingle(ref).then((value) => {
385
- if (!hasValue && value != null) {
386
- hasValue = true;
387
- resolve(value);
388
- }
389
- settle();
390
- }, (err) => { errors.push(err); settle(); });
477
+ // Cost-sorted sequential evaluation with short-circuit.
478
+ //
479
+ // Sort by inferred cost (stable preserves declaration order within
480
+ // the same cost tier). This means:
481
+ // • || chains (both sources are tools = same cost) left-to-right
482
+ // • Overdefinition with mixed costs → cheapest first
483
+ //
484
+ // Evaluate sequentially. Return the first non-null value.
485
+ // If all return null/undefined → return undefined (lets || fire).
486
+ // If all throw throw AggregateError (lets ?? fire).
487
+ const sorted = [...refs].sort((a, b) => this.inferCost(a) - this.inferCost(b));
488
+ const errors = [];
489
+ for (const ref of sorted) {
490
+ try {
491
+ const value = await this.pullSingle(ref);
492
+ if (value != null)
493
+ return value; // Short-circuit: found data
391
494
  }
392
- });
495
+ catch (err) {
496
+ errors.push(err);
497
+ }
498
+ }
499
+ // All resolved to null/undefined, or all threw
500
+ if (errors.length === refs.length) {
501
+ throw new AggregateError(errors, "All sources failed");
502
+ }
503
+ return undefined;
393
504
  }
394
505
  push(args) {
395
506
  this.state[trunkKey(this.trunk)] = args;
@@ -479,6 +590,20 @@ export class ExecutionTree {
479
590
  sameTrunk(w.to, this.trunk) &&
480
591
  pathEquals(w.to.path, cleanPath)) ?? [];
481
592
  if (matches.length > 0) {
593
+ // ── Lazy define resolution ──────────────────────────────────────
594
+ // When ALL matches at the root object level (path=[]) are
595
+ // whole-object wires sourced from define output modules, defer
596
+ // resolution to field-by-field GraphQL traversal. This avoids
597
+ // eagerly scheduling every tool inside the define block — only
598
+ // fields actually requested by the query will trigger their
599
+ // dependency chains.
600
+ if (cleanPath.length === 0 &&
601
+ !array &&
602
+ matches.every((w) => "from" in w &&
603
+ w.from.module.startsWith("__define_out_") &&
604
+ w.from.path.length === 0)) {
605
+ return this;
606
+ }
482
607
  const response = this.resolveWires(matches);
483
608
  if (!array) {
484
609
  return response;
@@ -491,6 +616,24 @@ export class ExecutionTree {
491
616
  return s;
492
617
  });
493
618
  }
619
+ // ── Resolve field from deferred define ────────────────────────────
620
+ // No direct wires for this field path — check whether a define
621
+ // forward wire exists at the root level (`o <- defineHandle`) and
622
+ // resolve only the matching field wire from the define's output.
623
+ if (cleanPath.length > 0) {
624
+ const defineFieldWires = this.findDefineFieldWires(cleanPath);
625
+ if (defineFieldWires.length > 0) {
626
+ const response = this.resolveWires(defineFieldWires);
627
+ if (!array)
628
+ return response;
629
+ const items = (await response);
630
+ return items.map((item) => {
631
+ const s = this.shadow();
632
+ s.state[trunkKey({ ...this.trunk, element: true })] = item;
633
+ return s;
634
+ });
635
+ }
636
+ }
494
637
  // Fallback: if this shadow tree has stored element data, resolve the
495
638
  // requested field directly from it. This handles passthrough arrays
496
639
  // where the bridge maps an inner array (e.g. `.stops <- j.stops`) but
@@ -518,4 +661,28 @@ export class ExecutionTree {
518
661
  // Return self to trigger downstream resolvers
519
662
  return this;
520
663
  }
664
+ /**
665
+ * Find define output wires for a specific field path.
666
+ *
667
+ * Looks for whole-object define forward wires (`o <- defineHandle`)
668
+ * at path=[] for this trunk, then searches the define's output wires
669
+ * for ones matching the requested field path.
670
+ */
671
+ findDefineFieldWires(cleanPath) {
672
+ const forwards = this.bridge?.wires.filter((w) => "from" in w &&
673
+ sameTrunk(w.to, this.trunk) &&
674
+ w.to.path.length === 0 &&
675
+ w.from.module.startsWith("__define_out_") &&
676
+ w.from.path.length === 0) ?? [];
677
+ if (forwards.length === 0)
678
+ return [];
679
+ const result = [];
680
+ for (const fw of forwards) {
681
+ const defOutTrunk = fw.from;
682
+ const fieldWires = this.bridge?.wires.filter((w) => sameTrunk(w.to, defOutTrunk) &&
683
+ pathEquals(w.to.path, cleanPath)) ?? [];
684
+ result.push(...fieldWires);
685
+ }
686
+ return result;
687
+ }
521
688
  }
@@ -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;AAGjB,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;CACvD,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,CA+Ef"}
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 &&
@@ -60,3 +67,41 @@ export function bridgeTransform(schema, instructions, options) {
60
67
  },
61
68
  });
62
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
@@ -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;AACxD,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC9E,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACrE,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,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,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackables/bridge",
3
- "version": "1.5.0",
3
+ "version": "1.6.1",
4
4
  "description": "Declarative dataflow for GraphQL",
5
5
  "main": "./build/index.js",
6
6
  "type": "module",