@bian-womp/spark-graph 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cjs/index.cjs +1417 -1220
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/core/types.d.ts +1 -1
- package/lib/cjs/src/core/types.d.ts.map +1 -1
- package/lib/cjs/src/index.d.ts +3 -3
- package/lib/cjs/src/index.d.ts.map +1 -1
- package/lib/cjs/src/misc/utils/json.d.ts +9 -0
- package/lib/cjs/src/misc/utils/json.d.ts.map +1 -1
- package/lib/cjs/src/runtime/GraphLifecycleApi.d.ts +4 -1
- package/lib/cjs/src/runtime/GraphLifecycleApi.d.ts.map +1 -1
- package/lib/cjs/src/runtime/GraphRuntime.d.ts +18 -27
- package/lib/cjs/src/runtime/GraphRuntime.d.ts.map +1 -1
- package/lib/cjs/src/runtime/{UnifiedEngine.d.ts → LocalEngine.d.ts} +20 -7
- package/lib/cjs/src/runtime/LocalEngine.d.ts.map +1 -0
- package/lib/cjs/src/runtime/components/EdgePropagator.d.ts +101 -0
- package/lib/cjs/src/runtime/components/EdgePropagator.d.ts.map +1 -0
- package/lib/cjs/src/runtime/components/Graph.d.ts +31 -0
- package/lib/cjs/src/runtime/components/Graph.d.ts.map +1 -0
- package/lib/cjs/src/runtime/components/HandleResolver.d.ts +11 -8
- package/lib/cjs/src/runtime/components/HandleResolver.d.ts.map +1 -1
- package/lib/cjs/src/runtime/components/NodeExecutor.d.ts +108 -0
- package/lib/cjs/src/runtime/components/NodeExecutor.d.ts.map +1 -0
- package/lib/cjs/src/runtime/components/RunContextManager.d.ts +26 -13
- package/lib/cjs/src/runtime/components/RunContextManager.d.ts.map +1 -1
- package/lib/cjs/src/runtime/components/graph-utils.d.ts +22 -0
- package/lib/cjs/src/runtime/components/graph-utils.d.ts.map +1 -0
- package/lib/cjs/src/runtime/components/interfaces.d.ts +9 -13
- package/lib/cjs/src/runtime/components/interfaces.d.ts.map +1 -1
- package/lib/cjs/src/runtime/components/types.d.ts +1 -10
- package/lib/cjs/src/runtime/components/types.d.ts.map +1 -1
- package/lib/esm/index.js +1416 -1220
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/core/types.d.ts +1 -1
- package/lib/esm/src/core/types.d.ts.map +1 -1
- package/lib/esm/src/index.d.ts +3 -3
- package/lib/esm/src/index.d.ts.map +1 -1
- package/lib/esm/src/misc/utils/json.d.ts +9 -0
- package/lib/esm/src/misc/utils/json.d.ts.map +1 -1
- package/lib/esm/src/runtime/GraphLifecycleApi.d.ts +4 -1
- package/lib/esm/src/runtime/GraphLifecycleApi.d.ts.map +1 -1
- package/lib/esm/src/runtime/GraphRuntime.d.ts +18 -27
- package/lib/esm/src/runtime/GraphRuntime.d.ts.map +1 -1
- package/lib/esm/src/runtime/{UnifiedEngine.d.ts → LocalEngine.d.ts} +20 -7
- package/lib/esm/src/runtime/LocalEngine.d.ts.map +1 -0
- package/lib/esm/src/runtime/components/EdgePropagator.d.ts +101 -0
- package/lib/esm/src/runtime/components/EdgePropagator.d.ts.map +1 -0
- package/lib/esm/src/runtime/components/Graph.d.ts +31 -0
- package/lib/esm/src/runtime/components/Graph.d.ts.map +1 -0
- package/lib/esm/src/runtime/components/HandleResolver.d.ts +11 -8
- package/lib/esm/src/runtime/components/HandleResolver.d.ts.map +1 -1
- package/lib/esm/src/runtime/components/NodeExecutor.d.ts +108 -0
- package/lib/esm/src/runtime/components/NodeExecutor.d.ts.map +1 -0
- package/lib/esm/src/runtime/components/RunContextManager.d.ts +26 -13
- package/lib/esm/src/runtime/components/RunContextManager.d.ts.map +1 -1
- package/lib/esm/src/runtime/components/graph-utils.d.ts +22 -0
- package/lib/esm/src/runtime/components/graph-utils.d.ts.map +1 -0
- package/lib/esm/src/runtime/components/interfaces.d.ts +9 -13
- package/lib/esm/src/runtime/components/interfaces.d.ts.map +1 -1
- package/lib/esm/src/runtime/components/types.d.ts +1 -10
- package/lib/esm/src/runtime/components/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/lib/cjs/src/runtime/AbstractEngine.d.ts +0 -28
- package/lib/cjs/src/runtime/AbstractEngine.d.ts.map +0 -1
- package/lib/cjs/src/runtime/UnifiedEngine.d.ts.map +0 -1
- package/lib/cjs/src/runtime/components/ExecutionScheduler.d.ts +0 -56
- package/lib/cjs/src/runtime/components/ExecutionScheduler.d.ts.map +0 -1
- package/lib/cjs/src/runtime/components/GraphStructure.d.ts +0 -36
- package/lib/cjs/src/runtime/components/GraphStructure.d.ts.map +0 -1
- package/lib/cjs/src/runtime/components/ValuePropagator.d.ts +0 -46
- package/lib/cjs/src/runtime/components/ValuePropagator.d.ts.map +0 -1
- package/lib/esm/src/runtime/AbstractEngine.d.ts +0 -28
- package/lib/esm/src/runtime/AbstractEngine.d.ts.map +0 -1
- package/lib/esm/src/runtime/UnifiedEngine.d.ts.map +0 -1
- package/lib/esm/src/runtime/components/ExecutionScheduler.d.ts +0 -56
- package/lib/esm/src/runtime/components/ExecutionScheduler.d.ts.map +0 -1
- package/lib/esm/src/runtime/components/GraphStructure.d.ts +0 -36
- package/lib/esm/src/runtime/components/GraphStructure.d.ts.map +0 -1
- package/lib/esm/src/runtime/components/ValuePropagator.d.ts +0 -46
- package/lib/esm/src/runtime/components/ValuePropagator.d.ts.map +0 -1
package/lib/cjs/index.cjs
CHANGED
|
@@ -405,48 +405,14 @@ class Registry {
|
|
|
405
405
|
Registry.idCounter = 0;
|
|
406
406
|
|
|
407
407
|
/**
|
|
408
|
-
*
|
|
409
|
-
*/
|
|
410
|
-
/**
|
|
411
|
-
* Type guard to check if a value is a Promise
|
|
412
|
-
*/
|
|
413
|
-
function isPromise(value) {
|
|
414
|
-
return !!value && typeof value.then === "function";
|
|
415
|
-
}
|
|
416
|
-
/**
|
|
417
|
-
* Unwrap a value that might be a Promise
|
|
418
|
-
*/
|
|
419
|
-
async function unwrapMaybePromise(value) {
|
|
420
|
-
return isPromise(value) ? await value : value;
|
|
421
|
-
}
|
|
422
|
-
/**
|
|
423
|
-
* Shallow/deep-ish equality check to avoid unnecessary runs on identical values
|
|
424
|
-
*/
|
|
425
|
-
function valuesEqual(a, b) {
|
|
426
|
-
if (a === b)
|
|
427
|
-
return true;
|
|
428
|
-
if (typeof a !== typeof b)
|
|
429
|
-
return false;
|
|
430
|
-
if (a && b && typeof a === "object") {
|
|
431
|
-
try {
|
|
432
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
433
|
-
}
|
|
434
|
-
catch {
|
|
435
|
-
return false;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
return false;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* GraphStructure component - manages nodes, edges, and handle resolution
|
|
408
|
+
* Graph component - manages nodes, edges, and handle resolution
|
|
443
409
|
*/
|
|
444
|
-
class
|
|
410
|
+
class Graph {
|
|
445
411
|
constructor(registry) {
|
|
412
|
+
this.registry = registry;
|
|
446
413
|
this.nodes = new Map();
|
|
447
414
|
this.edges = [];
|
|
448
415
|
this.resolvedByNode = new Map();
|
|
449
|
-
this.registry = registry;
|
|
450
416
|
}
|
|
451
417
|
// Node accessors
|
|
452
418
|
getNode(nodeId) {
|
|
@@ -488,99 +454,21 @@ class GraphStructure {
|
|
|
488
454
|
getResolvedHandlesMap() {
|
|
489
455
|
return this.resolvedByNode;
|
|
490
456
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
try {
|
|
505
|
-
if (typeof desc.resolveHandles === "function") {
|
|
506
|
-
const maybe = desc.resolveHandles({
|
|
507
|
-
nodeId: n.nodeId,
|
|
508
|
-
environment: environment || {},
|
|
509
|
-
params: n.params,
|
|
510
|
-
inputs: undefined,
|
|
511
|
-
});
|
|
512
|
-
// Only use sync results here; async results are applied via recompute later
|
|
513
|
-
if (isPromise(maybe)) {
|
|
514
|
-
// mark node as pending async recompute
|
|
515
|
-
pending.add(n.nodeId);
|
|
516
|
-
}
|
|
517
|
-
else {
|
|
518
|
-
dyn = maybe || {};
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
catch {
|
|
523
|
-
// ignore dynamic resolution errors at this stage
|
|
524
|
-
}
|
|
525
|
-
// Merge base with dynamic and overrides (allow partial resolvedHandles)
|
|
526
|
-
const inputs = {
|
|
527
|
-
...desc.inputs,
|
|
528
|
-
...dyn.inputs,
|
|
529
|
-
...overrideInputs,
|
|
530
|
-
};
|
|
531
|
-
const outputs = {
|
|
532
|
-
...desc.outputs,
|
|
533
|
-
...dyn.outputs,
|
|
534
|
-
...overrideOutputs,
|
|
535
|
-
};
|
|
536
|
-
const inputDefaults = {
|
|
537
|
-
...desc.inputDefaults,
|
|
538
|
-
...dyn.inputDefaults,
|
|
539
|
-
...overrideDefaults,
|
|
540
|
-
};
|
|
541
|
-
out.set(n.nodeId, { inputs, outputs, inputDefaults });
|
|
457
|
+
/**
|
|
458
|
+
* Check if all inbound edges for a node have values
|
|
459
|
+
*/
|
|
460
|
+
allInboundHaveValue(nodeId) {
|
|
461
|
+
const node = this.nodes.get(nodeId);
|
|
462
|
+
if (!node)
|
|
463
|
+
return false;
|
|
464
|
+
const inbound = this.edges.filter((e) => e.target.nodeId === nodeId);
|
|
465
|
+
if (inbound.length === 0)
|
|
466
|
+
return true;
|
|
467
|
+
for (const e of inbound) {
|
|
468
|
+
if (!(e.target.handle in node.inputs))
|
|
469
|
+
return false;
|
|
542
470
|
}
|
|
543
|
-
return
|
|
544
|
-
}
|
|
545
|
-
static buildEdges(def, registry, resolvedByNode) {
|
|
546
|
-
return def.edges.map((e) => {
|
|
547
|
-
const srcNode = def.nodes.find((n) => n.nodeId === e.source.nodeId);
|
|
548
|
-
const dstNode = def.nodes.find((n) => n.nodeId === e.target.nodeId);
|
|
549
|
-
let effectiveTypeId = e.typeId; // Start with original
|
|
550
|
-
let srcDeclared;
|
|
551
|
-
let dstDeclared;
|
|
552
|
-
if (srcNode) {
|
|
553
|
-
const resolved = resolvedByNode.get(srcNode.nodeId);
|
|
554
|
-
if (resolved)
|
|
555
|
-
srcDeclared = resolved.outputs[e.source.handle];
|
|
556
|
-
}
|
|
557
|
-
if (!effectiveTypeId) {
|
|
558
|
-
// Infer if not explicitly set
|
|
559
|
-
effectiveTypeId = Array.isArray(srcDeclared)
|
|
560
|
-
? srcDeclared[0]
|
|
561
|
-
: srcDeclared;
|
|
562
|
-
}
|
|
563
|
-
if (dstNode) {
|
|
564
|
-
const resolved = resolvedByNode.get(dstNode.nodeId);
|
|
565
|
-
if (resolved)
|
|
566
|
-
dstDeclared = getInputTypeId(resolved.inputs, e.target.handle);
|
|
567
|
-
}
|
|
568
|
-
const { convert, convertAsync } = GraphStructure.buildEdgeConverters(srcDeclared, dstDeclared, registry, `buildEdges: ${srcNode?.typeId || ""}.${e.source.nodeId}.${e.source.handle} -> ${dstNode?.typeId || ""}.${e.target.nodeId}.${e.target.handle}`);
|
|
569
|
-
return {
|
|
570
|
-
id: e.id,
|
|
571
|
-
source: { ...e.source },
|
|
572
|
-
target: { ...e.target },
|
|
573
|
-
typeId: e.typeId, // Preserve original (may be undefined)
|
|
574
|
-
effectiveTypeId: effectiveTypeId ?? "untyped", // Always present
|
|
575
|
-
convert,
|
|
576
|
-
convertAsync,
|
|
577
|
-
srcUnionTypes: Array.isArray(srcDeclared)
|
|
578
|
-
? [...srcDeclared]
|
|
579
|
-
: undefined,
|
|
580
|
-
dstDeclared,
|
|
581
|
-
stats: { runs: 0, inFlight: false, progress: 0 },
|
|
582
|
-
};
|
|
583
|
-
});
|
|
471
|
+
return true;
|
|
584
472
|
}
|
|
585
473
|
// Clear all data
|
|
586
474
|
clear() {
|
|
@@ -588,70 +476,6 @@ class GraphStructure {
|
|
|
588
476
|
this.edges = [];
|
|
589
477
|
this.resolvedByNode.clear();
|
|
590
478
|
}
|
|
591
|
-
// Static helper: build edge converters for type coercion
|
|
592
|
-
static buildEdgeConverters(srcDeclared, dstDeclared, registry, edgeLabel) {
|
|
593
|
-
if (!dstDeclared || !srcDeclared) {
|
|
594
|
-
return {};
|
|
595
|
-
}
|
|
596
|
-
const isUnion = Array.isArray(srcDeclared);
|
|
597
|
-
const srcTypes = isUnion ? srcDeclared : [srcDeclared];
|
|
598
|
-
// Helper to get the coercion for a specific type
|
|
599
|
-
const getCoercion = (typeId) => {
|
|
600
|
-
return registry.resolveCoercion(typeId, dstDeclared);
|
|
601
|
-
};
|
|
602
|
-
// Resolve coercions for all source types
|
|
603
|
-
const coercions = srcTypes.map(getCoercion);
|
|
604
|
-
const hasAsync = coercions.some((r) => r?.kind === "async");
|
|
605
|
-
// Helper to extract and validate typed output for unions
|
|
606
|
-
const extractPayload = (v) => {
|
|
607
|
-
const typeId = getTypedOutputTypeId(v);
|
|
608
|
-
const payload = getTypedOutputValue(v);
|
|
609
|
-
if (isUnion) {
|
|
610
|
-
if (!typeId) {
|
|
611
|
-
throw new Error(`Typed output required for union source (${edgeLabel}); allowed: ${srcTypes.join("|")}`);
|
|
612
|
-
}
|
|
613
|
-
if (!srcTypes.includes(typeId)) {
|
|
614
|
-
throw new Error(`Invalid typed output ${typeId} (${edgeLabel}); allowed: ${srcTypes.join("|")}`);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
else if (typeId) {
|
|
618
|
-
// Warn if typed output is used for non-union source
|
|
619
|
-
console.warn(`Typed output ${typeId} is fed even though source is not union (${edgeLabel}): ${srcDeclared} -> ${dstDeclared}`);
|
|
620
|
-
}
|
|
621
|
-
return { typeId: typeId || srcTypes[0], payload };
|
|
622
|
-
};
|
|
623
|
-
if (hasAsync) {
|
|
624
|
-
return {
|
|
625
|
-
convertAsync: async (v, signal) => {
|
|
626
|
-
const { typeId, payload } = extractPayload(v);
|
|
627
|
-
const res = getCoercion(typeId);
|
|
628
|
-
if (!res)
|
|
629
|
-
return payload;
|
|
630
|
-
if (res.kind === "async") {
|
|
631
|
-
return await res.convertAsync(payload, signal);
|
|
632
|
-
}
|
|
633
|
-
return res.convert(payload);
|
|
634
|
-
},
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
// Sync path
|
|
638
|
-
const firstCoercion = coercions.find((r) => r?.kind === "sync");
|
|
639
|
-
if (!firstCoercion) {
|
|
640
|
-
return {};
|
|
641
|
-
}
|
|
642
|
-
return {
|
|
643
|
-
convert: (v) => {
|
|
644
|
-
const { typeId, payload } = extractPayload(v);
|
|
645
|
-
const res = getCoercion(typeId);
|
|
646
|
-
if (!res)
|
|
647
|
-
return payload;
|
|
648
|
-
if (res.kind === "async") {
|
|
649
|
-
throw new Error(`Async coercion required but convert used (${edgeLabel})`);
|
|
650
|
-
}
|
|
651
|
-
return res.convert(payload);
|
|
652
|
-
},
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
479
|
}
|
|
656
480
|
|
|
657
481
|
/**
|
|
@@ -691,25 +515,30 @@ class EventEmitter {
|
|
|
691
515
|
* RunContextManager component - manages run-context lifecycle
|
|
692
516
|
*/
|
|
693
517
|
class RunContextManager {
|
|
694
|
-
constructor() {
|
|
518
|
+
constructor(graph) {
|
|
519
|
+
this.graph = graph;
|
|
695
520
|
this.runContexts = new Map();
|
|
696
521
|
this.runContextCounter = 0;
|
|
522
|
+
this.graph = graph;
|
|
697
523
|
}
|
|
698
524
|
/**
|
|
699
525
|
* Create a new run-context for runFromHere
|
|
700
526
|
*/
|
|
701
|
-
createRunContext(startNodeId, options) {
|
|
527
|
+
createRunContext(startNodeId, resolve, options) {
|
|
702
528
|
const id = `rc-${++this.runContextCounter}`;
|
|
703
529
|
const ctx = {
|
|
704
530
|
id,
|
|
705
531
|
startNodes: new Set([startNodeId]),
|
|
706
532
|
cancelledNodes: new Set(),
|
|
707
|
-
|
|
533
|
+
pendingNodes: 0,
|
|
534
|
+
pendingEdges: 0,
|
|
535
|
+
pendingResolvers: 0,
|
|
708
536
|
skipPropagateValues: options?.skipPropagateValues ?? false,
|
|
709
537
|
propagate: options?.propagate ?? true,
|
|
538
|
+
resolve,
|
|
710
539
|
};
|
|
711
540
|
this.runContexts.set(id, ctx);
|
|
712
|
-
return
|
|
541
|
+
return id;
|
|
713
542
|
}
|
|
714
543
|
/**
|
|
715
544
|
* Get a run-context by ID
|
|
@@ -729,18 +558,60 @@ class RunContextManager {
|
|
|
729
558
|
hasActiveRunContexts() {
|
|
730
559
|
return this.runContexts.size > 0;
|
|
731
560
|
}
|
|
561
|
+
startNodeRun(id, nodeId) {
|
|
562
|
+
const ctx = this.runContexts.get(id);
|
|
563
|
+
if (!ctx)
|
|
564
|
+
return;
|
|
565
|
+
ctx.pendingNodes++;
|
|
566
|
+
}
|
|
567
|
+
finishNodeRun(id, nodeId) {
|
|
568
|
+
const ctx = this.runContexts.get(id);
|
|
569
|
+
if (!ctx)
|
|
570
|
+
return;
|
|
571
|
+
ctx.pendingNodes--;
|
|
572
|
+
this.finishRunContextIfPossible(id);
|
|
573
|
+
}
|
|
574
|
+
startEdgeConversion(id, edgeId) {
|
|
575
|
+
const ctx = this.runContexts.get(id);
|
|
576
|
+
if (!ctx)
|
|
577
|
+
return;
|
|
578
|
+
ctx.pendingEdges++;
|
|
579
|
+
}
|
|
580
|
+
finishEdgeConversion(id, edgeId) {
|
|
581
|
+
const ctx = this.runContexts.get(id);
|
|
582
|
+
if (!ctx)
|
|
583
|
+
return;
|
|
584
|
+
ctx.pendingEdges--;
|
|
585
|
+
this.finishRunContextIfPossible(id);
|
|
586
|
+
}
|
|
587
|
+
startHandleResolution(id, nodeId) {
|
|
588
|
+
const ctx = this.runContexts.get(id);
|
|
589
|
+
if (!ctx)
|
|
590
|
+
return;
|
|
591
|
+
ctx.pendingResolvers++;
|
|
592
|
+
}
|
|
593
|
+
finishHandleResolution(id, nodeId) {
|
|
594
|
+
const ctx = this.runContexts.get(id);
|
|
595
|
+
if (!ctx)
|
|
596
|
+
return;
|
|
597
|
+
ctx.pendingResolvers--;
|
|
598
|
+
this.finishRunContextIfPossible(id);
|
|
599
|
+
}
|
|
732
600
|
/**
|
|
733
|
-
* Finish and remove a run-context when
|
|
601
|
+
* Finish and remove a run-context when all pending operations reach zero
|
|
734
602
|
*/
|
|
735
|
-
|
|
603
|
+
finishRunContextIfPossible(id) {
|
|
736
604
|
const ctx = this.runContexts.get(id);
|
|
737
605
|
if (!ctx)
|
|
738
606
|
return;
|
|
739
|
-
if (ctx.
|
|
607
|
+
if (ctx.pendingNodes > 0 ||
|
|
608
|
+
ctx.pendingEdges > 0 ||
|
|
609
|
+
ctx.pendingResolvers > 0) {
|
|
740
610
|
return; // Still has pending work
|
|
611
|
+
}
|
|
741
612
|
// Clean up activeRunContexts from all nodes
|
|
742
|
-
for (const node of
|
|
743
|
-
node.
|
|
613
|
+
for (const node of this.graph.getNodes().values()) {
|
|
614
|
+
node.activeRunContextIds.delete(id);
|
|
744
615
|
}
|
|
745
616
|
this.runContexts.delete(id);
|
|
746
617
|
if (ctx.resolve)
|
|
@@ -753,7 +624,7 @@ class RunContextManager {
|
|
|
753
624
|
* @param edges - All edges in the graph (for downstream traversal)
|
|
754
625
|
* @param nodes - All nodes in the graph (for clearing activeRunContexts)
|
|
755
626
|
*/
|
|
756
|
-
cancelNodeInRunContexts(nodeId, includeDownstream
|
|
627
|
+
cancelNodeInRunContexts(nodeId, includeDownstream) {
|
|
757
628
|
const toCancel = new Set([nodeId]);
|
|
758
629
|
if (includeDownstream) {
|
|
759
630
|
// Collect all downstream nodes
|
|
@@ -764,7 +635,7 @@ class RunContextManager {
|
|
|
764
635
|
if (visited.has(cur))
|
|
765
636
|
continue;
|
|
766
637
|
visited.add(cur);
|
|
767
|
-
for (const e of
|
|
638
|
+
for (const e of this.graph.getEdges()) {
|
|
768
639
|
if (e.source.nodeId === cur) {
|
|
769
640
|
const targetId = e.target.nodeId;
|
|
770
641
|
if (!visited.has(targetId)) {
|
|
@@ -783,9 +654,9 @@ class RunContextManager {
|
|
|
783
654
|
}
|
|
784
655
|
// Clear activeRunContexts for cancelled nodes
|
|
785
656
|
for (const id of toCancel) {
|
|
786
|
-
const node =
|
|
657
|
+
const node = this.graph.getNode(id);
|
|
787
658
|
if (node)
|
|
788
|
-
node.
|
|
659
|
+
node.activeRunContextIds.clear();
|
|
789
660
|
}
|
|
790
661
|
}
|
|
791
662
|
/**
|
|
@@ -813,17 +684,214 @@ const LOG_LEVEL_VALUES = {
|
|
|
813
684
|
silent: 4,
|
|
814
685
|
};
|
|
815
686
|
|
|
687
|
+
/**
|
|
688
|
+
* Shared utility functions for runtime components
|
|
689
|
+
*/
|
|
690
|
+
/**
|
|
691
|
+
* Type guard to check if a value is a Promise
|
|
692
|
+
*/
|
|
693
|
+
function isPromise(value) {
|
|
694
|
+
return !!value && typeof value.then === "function";
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Unwrap a value that might be a Promise
|
|
698
|
+
*/
|
|
699
|
+
async function unwrapMaybePromise(value) {
|
|
700
|
+
return isPromise(value) ? await value : value;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Shallow/deep-ish equality check to avoid unnecessary runs on identical values
|
|
704
|
+
*/
|
|
705
|
+
function valuesEqual(a, b) {
|
|
706
|
+
if (a === b)
|
|
707
|
+
return true;
|
|
708
|
+
if (typeof a !== typeof b)
|
|
709
|
+
return false;
|
|
710
|
+
if (a && b && typeof a === "object") {
|
|
711
|
+
try {
|
|
712
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
713
|
+
}
|
|
714
|
+
catch {
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function tryHandleResolving(def, registry, environment) {
|
|
722
|
+
const out = new Map();
|
|
723
|
+
const pending = new Set();
|
|
724
|
+
for (const n of def.nodes) {
|
|
725
|
+
const desc = registry.nodes.get(n.typeId);
|
|
726
|
+
if (!desc)
|
|
727
|
+
continue;
|
|
728
|
+
const overrideInputs = n.resolvedHandles?.inputs;
|
|
729
|
+
const overrideOutputs = n.resolvedHandles?.outputs;
|
|
730
|
+
const overrideDefaults = n.resolvedHandles?.inputDefaults;
|
|
731
|
+
// Resolve dynamic handles if available (initial pass: inputs may be undefined)
|
|
732
|
+
let dyn = {};
|
|
733
|
+
try {
|
|
734
|
+
if (typeof desc.resolveHandles === "function") {
|
|
735
|
+
const maybe = desc.resolveHandles({
|
|
736
|
+
nodeId: n.nodeId,
|
|
737
|
+
environment: environment || {},
|
|
738
|
+
params: n.params,
|
|
739
|
+
inputs: undefined,
|
|
740
|
+
});
|
|
741
|
+
// Only use sync results here; async results are applied via recompute later
|
|
742
|
+
if (isPromise(maybe)) {
|
|
743
|
+
// mark node as pending async recompute
|
|
744
|
+
pending.add(n.nodeId);
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
dyn = maybe || {};
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
catch {
|
|
752
|
+
// ignore dynamic resolution errors at this stage
|
|
753
|
+
}
|
|
754
|
+
// Merge base with dynamic and overrides (allow partial resolvedHandles)
|
|
755
|
+
const inputs = {
|
|
756
|
+
...desc.inputs,
|
|
757
|
+
...dyn.inputs,
|
|
758
|
+
...overrideInputs,
|
|
759
|
+
};
|
|
760
|
+
const outputs = {
|
|
761
|
+
...desc.outputs,
|
|
762
|
+
...dyn.outputs,
|
|
763
|
+
...overrideOutputs,
|
|
764
|
+
};
|
|
765
|
+
const inputDefaults = {
|
|
766
|
+
...desc.inputDefaults,
|
|
767
|
+
...dyn.inputDefaults,
|
|
768
|
+
...overrideDefaults,
|
|
769
|
+
};
|
|
770
|
+
out.set(n.nodeId, { inputs, outputs, inputDefaults });
|
|
771
|
+
}
|
|
772
|
+
return { resolved: out, pending };
|
|
773
|
+
}
|
|
774
|
+
function buildEdges(def, registry, resolvedByNode) {
|
|
775
|
+
return def.edges.map((e) => {
|
|
776
|
+
const srcNode = def.nodes.find((n) => n.nodeId === e.source.nodeId);
|
|
777
|
+
const dstNode = def.nodes.find((n) => n.nodeId === e.target.nodeId);
|
|
778
|
+
const { srcDeclared, dstDeclared, effectiveTypeId } = extractEdgeTypes(e.source.nodeId, e.source.handle, e.target.nodeId, e.target.handle, resolvedByNode, e.typeId);
|
|
779
|
+
const { convert, convertAsync } = buildEdgeConverters(srcDeclared, dstDeclared, registry, `buildEdges: ${srcNode?.typeId || ""}.${e.source.nodeId}.${e.source.handle} -> ${dstNode?.typeId || ""}.${e.target.nodeId}.${e.target.handle}`);
|
|
780
|
+
return {
|
|
781
|
+
id: e.id,
|
|
782
|
+
source: { ...e.source },
|
|
783
|
+
target: { ...e.target },
|
|
784
|
+
typeId: e.typeId, // Preserve original (may be undefined)
|
|
785
|
+
effectiveTypeId, // Always present
|
|
786
|
+
convert,
|
|
787
|
+
convertAsync,
|
|
788
|
+
srcUnionTypes: Array.isArray(srcDeclared) ? [...srcDeclared] : undefined,
|
|
789
|
+
dstDeclared,
|
|
790
|
+
stats: { runs: 0, inFlight: false, progress: 0 },
|
|
791
|
+
};
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Extract edge type information from resolved handles
|
|
796
|
+
* Used by both buildEdges and updateNodeHandles to avoid duplication
|
|
797
|
+
*/
|
|
798
|
+
function extractEdgeTypes(sourceNodeId, sourceHandle, targetNodeId, targetHandle, resolvedByNode, explicitTypeId) {
|
|
799
|
+
const srcResolved = resolvedByNode.get(sourceNodeId);
|
|
800
|
+
const dstResolved = resolvedByNode.get(targetNodeId);
|
|
801
|
+
const srcDeclared = srcResolved
|
|
802
|
+
? srcResolved.outputs[sourceHandle]
|
|
803
|
+
: undefined;
|
|
804
|
+
const dstDeclared = dstResolved
|
|
805
|
+
? getInputTypeId(dstResolved.inputs, targetHandle)
|
|
806
|
+
: undefined;
|
|
807
|
+
let effectiveTypeId = explicitTypeId;
|
|
808
|
+
if (!effectiveTypeId) {
|
|
809
|
+
// Infer if not explicitly set
|
|
810
|
+
effectiveTypeId = Array.isArray(srcDeclared) ? srcDeclared[0] : srcDeclared;
|
|
811
|
+
}
|
|
812
|
+
return {
|
|
813
|
+
srcDeclared,
|
|
814
|
+
dstDeclared,
|
|
815
|
+
effectiveTypeId: effectiveTypeId ?? "untyped",
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
// Static helper: build edge converters for type coercion
|
|
819
|
+
function buildEdgeConverters(srcDeclared, dstDeclared, registry, edgeLabel) {
|
|
820
|
+
if (!dstDeclared || !srcDeclared) {
|
|
821
|
+
return {};
|
|
822
|
+
}
|
|
823
|
+
const isUnion = Array.isArray(srcDeclared);
|
|
824
|
+
const srcTypes = isUnion ? srcDeclared : [srcDeclared];
|
|
825
|
+
// Helper to get the coercion for a specific type
|
|
826
|
+
const getCoercion = (typeId) => {
|
|
827
|
+
return registry.resolveCoercion(typeId, dstDeclared);
|
|
828
|
+
};
|
|
829
|
+
// Resolve coercions for all source types
|
|
830
|
+
const coercions = srcTypes.map(getCoercion);
|
|
831
|
+
const hasAsync = coercions.some((r) => r?.kind === "async");
|
|
832
|
+
// Helper to extract and validate typed output for unions
|
|
833
|
+
const extractPayload = (v) => {
|
|
834
|
+
const typeId = getTypedOutputTypeId(v);
|
|
835
|
+
const payload = getTypedOutputValue(v);
|
|
836
|
+
if (isUnion) {
|
|
837
|
+
if (!typeId) {
|
|
838
|
+
throw new Error(`Typed output required for union source (${edgeLabel}); allowed: ${srcTypes.join("|")}`);
|
|
839
|
+
}
|
|
840
|
+
if (!srcTypes.includes(typeId)) {
|
|
841
|
+
throw new Error(`Invalid typed output ${typeId} (${edgeLabel}); allowed: ${srcTypes.join("|")}`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
else if (typeId) {
|
|
845
|
+
// Warn if typed output is used for non-union source
|
|
846
|
+
console.warn(`Typed output ${typeId} is fed even though source is not union (${edgeLabel}): ${srcDeclared} -> ${dstDeclared}`);
|
|
847
|
+
}
|
|
848
|
+
return { typeId: typeId || srcTypes[0], payload };
|
|
849
|
+
};
|
|
850
|
+
if (hasAsync) {
|
|
851
|
+
return {
|
|
852
|
+
convertAsync: async (v, signal) => {
|
|
853
|
+
const { typeId, payload } = extractPayload(v);
|
|
854
|
+
const res = getCoercion(typeId);
|
|
855
|
+
if (!res)
|
|
856
|
+
return payload;
|
|
857
|
+
if (res.kind === "async") {
|
|
858
|
+
return await res.convertAsync(payload, signal);
|
|
859
|
+
}
|
|
860
|
+
return res.convert(payload);
|
|
861
|
+
},
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
// Sync path
|
|
865
|
+
const firstCoercion = coercions.find((r) => r?.kind === "sync");
|
|
866
|
+
if (!firstCoercion) {
|
|
867
|
+
return {};
|
|
868
|
+
}
|
|
869
|
+
return {
|
|
870
|
+
convert: (v) => {
|
|
871
|
+
const { typeId, payload } = extractPayload(v);
|
|
872
|
+
const res = getCoercion(typeId);
|
|
873
|
+
if (!res)
|
|
874
|
+
return payload;
|
|
875
|
+
if (res.kind === "async") {
|
|
876
|
+
throw new Error(`Async coercion required but convert used (${edgeLabel})`);
|
|
877
|
+
}
|
|
878
|
+
return res.convert(payload);
|
|
879
|
+
},
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
|
|
816
883
|
/**
|
|
817
884
|
* HandleResolver component - manages dynamic handle resolution
|
|
818
885
|
*/
|
|
819
886
|
class HandleResolver {
|
|
820
|
-
constructor(
|
|
821
|
-
this.
|
|
822
|
-
this.environment = {};
|
|
823
|
-
this.graphStructure = graphStructure;
|
|
887
|
+
constructor(graph, eventEmitter, runContextManager, edgePropagator, registry, environment) {
|
|
888
|
+
this.graph = graph;
|
|
824
889
|
this.eventEmitter = eventEmitter;
|
|
825
|
-
this.
|
|
890
|
+
this.runContextManager = runContextManager;
|
|
891
|
+
this.edgePropagator = edgePropagator;
|
|
826
892
|
this.registry = registry;
|
|
893
|
+
this.recomputeTokenByNode = new Map();
|
|
894
|
+
this.environment = {};
|
|
827
895
|
this.environment = environment ?? {};
|
|
828
896
|
}
|
|
829
897
|
setRegistry(registry) {
|
|
@@ -839,106 +907,162 @@ class HandleResolver {
|
|
|
839
907
|
// If no registry or node not found, skip
|
|
840
908
|
if (!this.registry)
|
|
841
909
|
return;
|
|
842
|
-
const node = this.
|
|
910
|
+
const node = this.graph.getNode(nodeId);
|
|
843
911
|
if (!node)
|
|
844
912
|
return;
|
|
913
|
+
// Track resolver start for all active run-contexts
|
|
914
|
+
if (node.activeRunContextIds && node.activeRunContextIds.size > 0) {
|
|
915
|
+
for (const runContextId of node.activeRunContextIds) {
|
|
916
|
+
this.runContextManager.startHandleResolution(runContextId, nodeId);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
845
919
|
setTimeout(() => {
|
|
846
|
-
void this.recomputeHandlesForNode(nodeId);
|
|
920
|
+
void this.recomputeHandlesForNode(nodeId, node.activeRunContextIds);
|
|
847
921
|
}, 0);
|
|
848
922
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
async recomputeHandlesForNode(nodeId) {
|
|
853
|
-
const registry = this.registry;
|
|
854
|
-
const node = this.graphStructure.getNode(nodeId);
|
|
855
|
-
if (!node)
|
|
856
|
-
return;
|
|
857
|
-
const desc = registry.nodes.get(node.typeId);
|
|
858
|
-
if (!desc)
|
|
923
|
+
// Update resolved handles for a single node and refresh edge converters/types that touch it
|
|
924
|
+
updateNodeHandles(nodeId, handles) {
|
|
925
|
+
if (!this.registry)
|
|
859
926
|
return;
|
|
860
|
-
const
|
|
861
|
-
if (
|
|
927
|
+
const node = this.graph.getNode(nodeId);
|
|
928
|
+
if (!node)
|
|
862
929
|
return;
|
|
863
|
-
|
|
864
|
-
this.
|
|
865
|
-
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
930
|
+
this.graph.setResolvedHandles(nodeId, handles);
|
|
931
|
+
const edges = this.graph.getEdges();
|
|
932
|
+
const resolvedByNode = this.graph.getResolvedHandlesMap();
|
|
933
|
+
for (const e of edges) {
|
|
934
|
+
// Only update edges that touch the changed node
|
|
935
|
+
const touchesChangedNode = e.source.nodeId === nodeId || e.target.nodeId === nodeId;
|
|
936
|
+
if (!touchesChangedNode)
|
|
937
|
+
continue;
|
|
938
|
+
const srcNode = this.graph.getNode(e.source.nodeId);
|
|
939
|
+
const dstNode = this.graph.getNode(e.target.nodeId);
|
|
940
|
+
const oldDstDeclared = e.dstDeclared;
|
|
941
|
+
// Extract edge types using shared helper (handles both source and target updates)
|
|
942
|
+
const { srcDeclared, dstDeclared, effectiveTypeId } = extractEdgeTypes(e.source.nodeId, e.source.handle, e.target.nodeId, e.target.handle, resolvedByNode, e.typeId);
|
|
943
|
+
// Update edge properties
|
|
944
|
+
if (!e.typeId) {
|
|
945
|
+
e.effectiveTypeId = effectiveTypeId;
|
|
946
|
+
}
|
|
947
|
+
e.dstDeclared = dstDeclared;
|
|
948
|
+
e.srcUnionTypes = Array.isArray(srcDeclared)
|
|
949
|
+
? [...srcDeclared]
|
|
950
|
+
: undefined;
|
|
951
|
+
// Update converters
|
|
952
|
+
const conv = buildEdgeConverters(srcDeclared, dstDeclared, this.registry, `updateNodeHandles: ${srcNode?.typeId || ""}.${e.source.nodeId}.${e.source.handle} -> ${dstNode?.typeId || ""}.${e.target.nodeId}.${e.target.handle}`);
|
|
953
|
+
e.convert = conv.convert;
|
|
954
|
+
e.convertAsync = conv.convertAsync;
|
|
955
|
+
if (e.target.nodeId === nodeId &&
|
|
956
|
+
oldDstDeclared === undefined &&
|
|
957
|
+
dstDeclared !== undefined) {
|
|
958
|
+
const srcNode = this.graph.getNode(e.source.nodeId);
|
|
959
|
+
if (srcNode) {
|
|
960
|
+
const srcValue = srcNode.outputs[e.source.handle];
|
|
961
|
+
if (srcValue !== undefined) {
|
|
962
|
+
this.edgePropagator.propagate(e.source.nodeId, e.source.handle, srcValue, srcNode.activeRunContextIds);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
this.edgePropagator.invalidateDownstream(nodeId);
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Recompute dynamic handles for a single node using current inputs/environment
|
|
971
|
+
*/
|
|
972
|
+
async recomputeHandlesForNode(nodeId, activeRunContextIds) {
|
|
973
|
+
try {
|
|
974
|
+
if (!this.registry)
|
|
975
|
+
return;
|
|
976
|
+
const node = this.graph.getNode(nodeId);
|
|
977
|
+
if (!node)
|
|
978
|
+
return;
|
|
979
|
+
const desc = this.registry.nodes.get(node.typeId);
|
|
980
|
+
if (!desc)
|
|
981
|
+
return;
|
|
982
|
+
const resolveHandles = desc.resolveHandles;
|
|
983
|
+
if (typeof resolveHandles !== "function")
|
|
984
|
+
return;
|
|
985
|
+
const token = (this.recomputeTokenByNode.get(nodeId) ?? 0) + 1;
|
|
986
|
+
this.recomputeTokenByNode.set(nodeId, token);
|
|
987
|
+
// Log resolveHandles-start
|
|
988
|
+
const nodeLogLevel = node.logLevel ?? "info";
|
|
989
|
+
const nodeLogValue = LOG_LEVEL_VALUES[nodeLogLevel] ?? 1;
|
|
990
|
+
const shouldLog = nodeLogValue <= LOG_LEVEL_VALUES.debug && nodeLogLevel !== "silent";
|
|
991
|
+
if (shouldLog) {
|
|
992
|
+
console.info(`[node:${nodeId}:${node.typeId}] resolveHandles-start`);
|
|
993
|
+
}
|
|
994
|
+
let resolved;
|
|
995
|
+
try {
|
|
996
|
+
const res = resolveHandles({
|
|
997
|
+
nodeId,
|
|
998
|
+
environment: this.environment || {},
|
|
999
|
+
params: node.params,
|
|
1000
|
+
inputs: node.inputs || {},
|
|
1001
|
+
});
|
|
1002
|
+
resolved = await unwrapMaybePromise(res);
|
|
1003
|
+
}
|
|
1004
|
+
catch {
|
|
1005
|
+
// Log resolveHandles-done even on error
|
|
1006
|
+
if (shouldLog) {
|
|
1007
|
+
console.info(`[node:${nodeId}:${node.typeId}] resolveHandles-done (error)`);
|
|
1008
|
+
}
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
// Log resolveHandles-done
|
|
1012
|
+
if (shouldLog) {
|
|
1013
|
+
console.info(`[node:${nodeId}:${node.typeId}] resolveHandles-done`);
|
|
1014
|
+
}
|
|
1015
|
+
// If a newer recompute was scheduled, drop this result
|
|
1016
|
+
if ((this.recomputeTokenByNode.get(nodeId) ?? 0) !== token)
|
|
1017
|
+
return;
|
|
1018
|
+
const before = this.graph.getResolvedHandles(nodeId);
|
|
1019
|
+
if (!before)
|
|
1020
|
+
return;
|
|
1021
|
+
// Re-fetch desc to ensure we have the latest (node might have been updated)
|
|
1022
|
+
const nodeDesc = this.registry.nodes.get(node.typeId);
|
|
1023
|
+
if (!nodeDesc)
|
|
1024
|
+
return;
|
|
1025
|
+
const inputs = { ...nodeDesc.inputs, ...resolved?.inputs };
|
|
1026
|
+
const outputs = { ...nodeDesc.outputs, ...resolved?.outputs };
|
|
1027
|
+
const inputDefaults = {
|
|
1028
|
+
...nodeDesc.inputDefaults,
|
|
1029
|
+
...resolved?.inputDefaults,
|
|
1030
|
+
};
|
|
1031
|
+
const after = { inputs, outputs, inputDefaults };
|
|
1032
|
+
// Compare shallow-structurally via JSON
|
|
1033
|
+
if (JSON.stringify(before) === JSON.stringify(after))
|
|
1034
|
+
return;
|
|
1035
|
+
// Call GraphRuntime's updateNodeHandles to update edges and re-propagate values
|
|
1036
|
+
// Note: updateNodeHandles will set the resolved handles internally
|
|
1037
|
+
this.updateNodeHandles(nodeId, after);
|
|
1038
|
+
// Notify graph updated with the changed handles
|
|
1039
|
+
this.eventEmitter.emit("invalidate", {
|
|
1040
|
+
reason: "graph-updated",
|
|
1041
|
+
resolvedHandles: { [nodeId]: after },
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
finally {
|
|
1045
|
+
// Track resolver finish after successful completion
|
|
1046
|
+
if (activeRunContextIds && activeRunContextIds.size > 0) {
|
|
1047
|
+
for (const runContextId of activeRunContextIds) {
|
|
1048
|
+
this.runContextManager.finishHandleResolution(runContextId, nodeId);
|
|
1049
|
+
}
|
|
886
1050
|
}
|
|
887
|
-
return;
|
|
888
|
-
}
|
|
889
|
-
// Log resolveHandles-done
|
|
890
|
-
if (shouldLog) {
|
|
891
|
-
console.info(`[node:${nodeId}:${node.typeId}] resolveHandles-done`);
|
|
892
1051
|
}
|
|
893
|
-
// If a newer recompute was scheduled, drop this result
|
|
894
|
-
if ((this.recomputeTokenByNode.get(nodeId) ?? 0) !== token)
|
|
895
|
-
return;
|
|
896
|
-
const resolved = this.graphStructure.getResolvedHandles(nodeId);
|
|
897
|
-
if (!resolved)
|
|
898
|
-
return; // Node was deleted
|
|
899
|
-
// Re-fetch desc to ensure we have the latest (node might have been updated)
|
|
900
|
-
const nodeDesc = registry.nodes.get(node.typeId);
|
|
901
|
-
if (!nodeDesc)
|
|
902
|
-
return;
|
|
903
|
-
const inputs = { ...nodeDesc.inputs, ...r?.inputs };
|
|
904
|
-
const outputs = { ...nodeDesc.outputs, ...r?.outputs };
|
|
905
|
-
const inputDefaults = {
|
|
906
|
-
...nodeDesc.inputDefaults,
|
|
907
|
-
...r?.inputDefaults,
|
|
908
|
-
};
|
|
909
|
-
const next = { inputs, outputs, inputDefaults };
|
|
910
|
-
const before = resolved;
|
|
911
|
-
// Compare shallow-structurally via JSON
|
|
912
|
-
if (JSON.stringify(before) === JSON.stringify(next))
|
|
913
|
-
return;
|
|
914
|
-
this.graphStructure.setResolvedHandles(nodeId, next);
|
|
915
|
-
// Call GraphRuntime's updateNodeHandles to update edges and re-propagate values
|
|
916
|
-
this.runtimeCoordinator.updateNodeHandles(nodeId, next, registry);
|
|
917
|
-
// Notify graph updated with the changed handles
|
|
918
|
-
this.eventEmitter.emit("invalidate", {
|
|
919
|
-
reason: "graph-updated",
|
|
920
|
-
resolvedHandles: { [nodeId]: next },
|
|
921
|
-
});
|
|
922
1052
|
}
|
|
923
1053
|
}
|
|
924
1054
|
|
|
925
1055
|
/**
|
|
926
|
-
*
|
|
1056
|
+
* EdgePropagator component - handles value propagation through edges
|
|
927
1057
|
*/
|
|
928
|
-
class
|
|
929
|
-
constructor(
|
|
930
|
-
this.
|
|
931
|
-
this.graphStructure = graphStructure;
|
|
1058
|
+
class EdgePropagator {
|
|
1059
|
+
constructor(graph, eventEmitter, runContextManager, handleResolver, nodeExecutor) {
|
|
1060
|
+
this.graph = graph;
|
|
932
1061
|
this.eventEmitter = eventEmitter;
|
|
933
1062
|
this.runContextManager = runContextManager;
|
|
934
1063
|
this.handleResolver = handleResolver;
|
|
935
|
-
this.
|
|
936
|
-
|
|
937
|
-
/**
|
|
938
|
-
* Set the execution scheduler (called after construction to resolve circular dependency)
|
|
939
|
-
*/
|
|
940
|
-
setExecutionScheduler(executionScheduler) {
|
|
941
|
-
this.executionScheduler = executionScheduler;
|
|
1064
|
+
this.nodeExecutor = nodeExecutor;
|
|
1065
|
+
this.arrayInputBuckets = new Map();
|
|
942
1066
|
}
|
|
943
1067
|
/**
|
|
944
1068
|
* Propagate value through edges
|
|
@@ -947,10 +1071,24 @@ class ValuePropagator {
|
|
|
947
1071
|
*/
|
|
948
1072
|
propagate(srcNodeId, srcHandle, value, runContextIds) {
|
|
949
1073
|
// Set source output
|
|
950
|
-
|
|
1074
|
+
if (!this.setSourceOutput(srcNodeId, srcHandle, value)) {
|
|
1075
|
+
return; // Node was removed
|
|
1076
|
+
}
|
|
1077
|
+
// Find outgoing edges
|
|
1078
|
+
const outEdges = this.findOutgoingEdges(srcNodeId, srcHandle);
|
|
1079
|
+
// Process each edge
|
|
1080
|
+
for (const edge of outEdges) {
|
|
1081
|
+
this.propagateToEdge(edge, value, srcNodeId, runContextIds);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Set source output value and emit event
|
|
1086
|
+
*/
|
|
1087
|
+
setSourceOutput(srcNodeId, srcHandle, value) {
|
|
1088
|
+
const srcNode = this.graph.getNode(srcNodeId);
|
|
951
1089
|
if (!srcNode) {
|
|
952
1090
|
// Node was removed (e.g., graph updated) but an async emit arrived late; ignore
|
|
953
|
-
return;
|
|
1091
|
+
return false;
|
|
954
1092
|
}
|
|
955
1093
|
srcNode.outputs[srcHandle] = value;
|
|
956
1094
|
this.eventEmitter.emit("value", {
|
|
@@ -960,221 +1098,306 @@ class ValuePropagator {
|
|
|
960
1098
|
io: "output",
|
|
961
1099
|
runtimeTypeId: getTypedOutputTypeId(value),
|
|
962
1100
|
});
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1101
|
+
return true;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Find all outgoing edges from a source node handle
|
|
1105
|
+
*/
|
|
1106
|
+
findOutgoingEdges(srcNodeId, srcHandle) {
|
|
1107
|
+
const edges = this.graph.getEdges();
|
|
1108
|
+
return edges.filter((e) => e.source.nodeId === srcNodeId && e.source.handle === srcHandle);
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Propagate value to a single edge
|
|
1112
|
+
*/
|
|
1113
|
+
propagateToEdge(edge, value, srcNodeId, runContextIds) {
|
|
1114
|
+
// Filter run-contexts
|
|
1115
|
+
const effectiveRunContexts = runContextIds && runContextIds.size > 0
|
|
1116
|
+
? this.filterEffectiveRunContexts(edge, srcNodeId, runContextIds)
|
|
1117
|
+
: undefined;
|
|
1118
|
+
if (runContextIds &&
|
|
1119
|
+
runContextIds.size > 0 &&
|
|
1120
|
+
!(effectiveRunContexts && effectiveRunContexts.size > 0)) {
|
|
1121
|
+
return; // No valid run-contexts for this edge
|
|
1122
|
+
}
|
|
1123
|
+
// Validate union types
|
|
1124
|
+
if (!this.validateUnionType(edge, value, srcNodeId)) {
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
// Clone value per edge to isolate conversions
|
|
1128
|
+
let nextVal = structuredClone(value);
|
|
1129
|
+
// Apply conversion and propagate
|
|
1130
|
+
if (edge.convertAsync) {
|
|
1131
|
+
this.handleAsyncConversion(edge, nextVal, effectiveRunContexts);
|
|
1132
|
+
}
|
|
1133
|
+
else {
|
|
1134
|
+
this.handleSyncConversion(edge, nextVal, effectiveRunContexts);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Filter run-contexts to exclude cancelled nodes
|
|
1139
|
+
*/
|
|
1140
|
+
filterEffectiveRunContexts(edge, srcNodeId, runContextIds) {
|
|
1141
|
+
const effectiveRunContexts = new Set();
|
|
1142
|
+
for (const id of runContextIds) {
|
|
1143
|
+
const ctx = this.runContextManager.getRunContext(id);
|
|
1144
|
+
if (!ctx)
|
|
997
1145
|
continue;
|
|
1146
|
+
if (ctx.cancelledNodes.has(srcNodeId))
|
|
1147
|
+
continue;
|
|
1148
|
+
if (ctx.cancelledNodes.has(edge.target.nodeId))
|
|
1149
|
+
continue;
|
|
1150
|
+
effectiveRunContexts.add(id);
|
|
1151
|
+
}
|
|
1152
|
+
return effectiveRunContexts.size > 0 ? effectiveRunContexts : undefined;
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Validate union type requirements
|
|
1156
|
+
*/
|
|
1157
|
+
validateUnionType(edge, value, srcNodeId) {
|
|
1158
|
+
const isUnion = Array.isArray(edge.srcUnionTypes);
|
|
1159
|
+
const isTyped = isTypedOutput(value);
|
|
1160
|
+
if (isUnion && !isTyped) {
|
|
1161
|
+
const err = new Error(`Output ${srcNodeId}.${edge.source.handle} requires typed value for union output (allowed: ${edge.srcUnionTypes.join("|")})`);
|
|
1162
|
+
this.eventEmitter.emit("error", {
|
|
1163
|
+
kind: "edge-convert",
|
|
1164
|
+
edgeId: edge.id,
|
|
1165
|
+
source: { nodeId: edge.source.nodeId, handle: edge.source.handle },
|
|
1166
|
+
target: { nodeId: edge.target.nodeId, handle: edge.target.handle },
|
|
1167
|
+
err,
|
|
1168
|
+
});
|
|
1169
|
+
return false;
|
|
1170
|
+
}
|
|
1171
|
+
return true;
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Handle synchronous conversion
|
|
1175
|
+
*/
|
|
1176
|
+
handleSyncConversion(edge, value, effectiveRunContexts) {
|
|
1177
|
+
let convertedValue = value;
|
|
1178
|
+
if (edge.convert) {
|
|
1179
|
+
convertedValue = edge.convert(value);
|
|
1180
|
+
}
|
|
1181
|
+
this.applyToTarget(edge, convertedValue, effectiveRunContexts);
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Handle asynchronous conversion
|
|
1185
|
+
*/
|
|
1186
|
+
handleAsyncConversion(edge, value, effectiveRunContexts) {
|
|
1187
|
+
if (!edge.convertAsync)
|
|
1188
|
+
return;
|
|
1189
|
+
// Track edge run-context IDs for pendingEdges tracking
|
|
1190
|
+
const edgeRunContextIds = effectiveRunContexts
|
|
1191
|
+
? Array.from(effectiveRunContexts)
|
|
1192
|
+
: undefined;
|
|
1193
|
+
if (edgeRunContextIds) {
|
|
1194
|
+
for (const id of edgeRunContextIds) {
|
|
1195
|
+
this.runContextManager.startEdgeConversion(id, edge.id);
|
|
998
1196
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
let forHandle = forNode.get(e.target.handle);
|
|
1019
|
-
if (!forHandle) {
|
|
1020
|
-
forHandle = new Map();
|
|
1021
|
-
forNode.set(e.target.handle, forHandle);
|
|
1022
|
-
}
|
|
1023
|
-
forHandle.set(e.id, toArray(v));
|
|
1024
|
-
const merged = [];
|
|
1025
|
-
for (const ed of edges) {
|
|
1026
|
-
if (ed.target.nodeId === e.target.nodeId &&
|
|
1027
|
-
ed.target.handle === e.target.handle) {
|
|
1028
|
-
const part = forHandle.get(ed.id);
|
|
1029
|
-
if (part && part.length)
|
|
1030
|
-
merged.push(...part);
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
next = merged;
|
|
1034
|
-
}
|
|
1035
|
-
const prev = dstNode.inputs[e.target.handle];
|
|
1036
|
-
const same = valuesEqual(prev, next);
|
|
1037
|
-
if (!same) {
|
|
1038
|
-
// Check skipPropagateValues (only in run-context mode)
|
|
1039
|
-
let shouldSkipPropagateValues = false;
|
|
1040
|
-
if (isRunContextAware && effectiveRunContexts) {
|
|
1041
|
-
for (const id of effectiveRunContexts) {
|
|
1042
|
-
const ctx = this.runContextManager.getRunContext(id);
|
|
1043
|
-
if (ctx && ctx.skipPropagateValues) {
|
|
1044
|
-
shouldSkipPropagateValues = true;
|
|
1045
|
-
break;
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
// Set input values unless skipPropagateValues is enabled
|
|
1050
|
-
if (!shouldSkipPropagateValues) {
|
|
1051
|
-
dstNode.inputs[e.target.handle] = next;
|
|
1052
|
-
this.eventEmitter.emit("value", {
|
|
1053
|
-
nodeId: e.target.nodeId,
|
|
1054
|
-
handle: e.target.handle,
|
|
1055
|
-
value: next,
|
|
1056
|
-
io: "input",
|
|
1057
|
-
runtimeTypeId: getTypedOutputTypeId(next),
|
|
1058
|
-
});
|
|
1059
|
-
this.handleResolver.scheduleRecomputeHandles(e.target.nodeId);
|
|
1060
|
-
}
|
|
1061
|
-
// Check propagate flag (only in run-context mode; auto mode always propagates)
|
|
1062
|
-
let shouldPropagate = true; // Default: always propagate in auto mode
|
|
1063
|
-
if (isRunContextAware && effectiveRunContexts) {
|
|
1064
|
-
shouldPropagate = false;
|
|
1065
|
-
for (const id of effectiveRunContexts) {
|
|
1066
|
-
const ctx = this.runContextManager.getRunContext(id);
|
|
1067
|
-
if (ctx && ctx.propagate) {
|
|
1068
|
-
shouldPropagate = true;
|
|
1069
|
-
break;
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
// Schedule downstream execution if propagation is enabled
|
|
1074
|
-
if (!this.runtimeCoordinator.isPaused() &&
|
|
1075
|
-
shouldPropagate &&
|
|
1076
|
-
this.executionScheduler.allInboundHaveValue(e.target.nodeId)) {
|
|
1077
|
-
if (isRunContextAware && effectiveRunContexts) {
|
|
1078
|
-
this.executionScheduler.scheduleInputsChangedWithRunContexts(e.target.nodeId, effectiveRunContexts);
|
|
1079
|
-
}
|
|
1080
|
-
else {
|
|
1081
|
-
this.executionScheduler.scheduleInputsChanged(e.target.nodeId);
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
};
|
|
1086
|
-
// Handle async conversion
|
|
1087
|
-
if (e.convertAsync) {
|
|
1088
|
-
this.eventEmitter.emit("stats", {
|
|
1089
|
-
kind: "edge-start",
|
|
1090
|
-
edgeId: e.id,
|
|
1091
|
-
typeId: e.typeId,
|
|
1092
|
-
source: { nodeId: e.source.nodeId, handle: e.source.handle },
|
|
1093
|
-
target: { nodeId: e.target.nodeId, handle: e.target.handle },
|
|
1094
|
-
});
|
|
1095
|
-
const controller = new AbortController();
|
|
1096
|
-
const startAt = Date.now();
|
|
1097
|
-
e.stats.runs += 1;
|
|
1098
|
-
e.stats.inFlight = true;
|
|
1099
|
-
if (!isRunContextAware) {
|
|
1100
|
-
// Only track progress in auto mode
|
|
1101
|
-
e.stats.progress = 0;
|
|
1102
|
-
}
|
|
1103
|
-
const sig = controller.signal;
|
|
1104
|
-
e.convertAsync(nextVal, sig)
|
|
1105
|
-
.then((converted) => {
|
|
1106
|
-
if (!sig.aborted) {
|
|
1107
|
-
applyToTarget(converted);
|
|
1108
|
-
e.stats.inFlight = false;
|
|
1109
|
-
const duration = Date.now() - startAt;
|
|
1110
|
-
e.stats.lastDurationMs = duration;
|
|
1111
|
-
if (!isRunContextAware) {
|
|
1112
|
-
// More detailed stats in auto mode
|
|
1113
|
-
e.stats.lastEndAt = Date.now();
|
|
1114
|
-
e.stats.lastError = undefined;
|
|
1115
|
-
this.eventEmitter.emit("stats", {
|
|
1116
|
-
kind: "edge-done",
|
|
1117
|
-
edgeId: e.id,
|
|
1118
|
-
typeId: e.typeId,
|
|
1119
|
-
source: { nodeId: e.source.nodeId, handle: e.source.handle },
|
|
1120
|
-
target: { nodeId: e.target.nodeId, handle: e.target.handle },
|
|
1121
|
-
durationMs: duration,
|
|
1122
|
-
});
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
})
|
|
1126
|
-
.catch((err) => {
|
|
1127
|
-
if (sig.aborted)
|
|
1128
|
-
return;
|
|
1129
|
-
e.stats.inFlight = false;
|
|
1130
|
-
if (!isRunContextAware) {
|
|
1131
|
-
e.stats.lastError = err;
|
|
1132
|
-
}
|
|
1133
|
-
this.eventEmitter.emit("error", {
|
|
1134
|
-
kind: "edge-convert",
|
|
1135
|
-
edgeId: e.id,
|
|
1136
|
-
source: { nodeId: e.source.nodeId, handle: e.source.handle },
|
|
1137
|
-
target: { nodeId: e.target.nodeId, handle: e.target.handle },
|
|
1138
|
-
err,
|
|
1139
|
-
});
|
|
1140
|
-
});
|
|
1197
|
+
}
|
|
1198
|
+
this.eventEmitter.emit("stats", {
|
|
1199
|
+
kind: "edge-start",
|
|
1200
|
+
edgeId: edge.id,
|
|
1201
|
+
typeId: edge.typeId,
|
|
1202
|
+
source: { nodeId: edge.source.nodeId, handle: edge.source.handle },
|
|
1203
|
+
target: { nodeId: edge.target.nodeId, handle: edge.target.handle },
|
|
1204
|
+
});
|
|
1205
|
+
const controller = new AbortController();
|
|
1206
|
+
const startAt = Date.now();
|
|
1207
|
+
edge.stats.runs += 1;
|
|
1208
|
+
edge.stats.inFlight = true;
|
|
1209
|
+
edge.stats.progress = 0;
|
|
1210
|
+
edge
|
|
1211
|
+
.convertAsync(value, controller.signal)
|
|
1212
|
+
.then((converted) => {
|
|
1213
|
+
if (!controller.signal.aborted) {
|
|
1214
|
+
this.applyToTarget(edge, converted, effectiveRunContexts);
|
|
1215
|
+
this.updateEdgeStatsOnSuccess(edge, startAt);
|
|
1141
1216
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1217
|
+
})
|
|
1218
|
+
.catch((err) => {
|
|
1219
|
+
if (controller.signal.aborted)
|
|
1220
|
+
return;
|
|
1221
|
+
this.handleEdgeConversionError(edge, err);
|
|
1222
|
+
})
|
|
1223
|
+
.finally(() => {
|
|
1224
|
+
this.finishEdgeConversion(edgeRunContextIds, edge.id);
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Apply value to target node input
|
|
1229
|
+
*/
|
|
1230
|
+
applyToTarget(edge, value, effectiveRunContexts) {
|
|
1231
|
+
const dstNode = this.graph.getNode(edge.target.nodeId);
|
|
1232
|
+
if (!dstNode)
|
|
1233
|
+
return;
|
|
1234
|
+
// Skip writing to unresolved handles
|
|
1235
|
+
if (edge.dstDeclared === undefined)
|
|
1236
|
+
return;
|
|
1237
|
+
// Handle array types
|
|
1238
|
+
const processedValue = this.processArrayInput(edge, value);
|
|
1239
|
+
// Check if value changed
|
|
1240
|
+
const prev = dstNode.inputs[edge.target.handle];
|
|
1241
|
+
if (valuesEqual(prev, processedValue)) {
|
|
1242
|
+
return; // No change
|
|
1243
|
+
}
|
|
1244
|
+
// Set input value (respecting skipPropagateValues)
|
|
1245
|
+
const shouldSetValue = this.shouldSetInputValue(effectiveRunContexts);
|
|
1246
|
+
if (shouldSetValue) {
|
|
1247
|
+
this.setTargetInput(edge, dstNode, processedValue);
|
|
1248
|
+
}
|
|
1249
|
+
// Schedule downstream execution
|
|
1250
|
+
this.executeDownstream(edge.target.nodeId, effectiveRunContexts);
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Process array input by merging values from all edges
|
|
1254
|
+
*/
|
|
1255
|
+
processArrayInput(edge, value) {
|
|
1256
|
+
const dstIsArray = typeof edge.dstDeclared === "string" && edge.dstDeclared.endsWith("[]");
|
|
1257
|
+
if (!dstIsArray) {
|
|
1258
|
+
return value;
|
|
1259
|
+
}
|
|
1260
|
+
const toArray = (x) => Array.isArray(x) ? x : x === undefined ? [] : [x];
|
|
1261
|
+
let forNode = this.arrayInputBuckets.get(edge.target.nodeId);
|
|
1262
|
+
if (!forNode) {
|
|
1263
|
+
forNode = new Map();
|
|
1264
|
+
this.arrayInputBuckets.set(edge.target.nodeId, forNode);
|
|
1265
|
+
}
|
|
1266
|
+
let forHandle = forNode.get(edge.target.handle);
|
|
1267
|
+
if (!forHandle) {
|
|
1268
|
+
forHandle = new Map();
|
|
1269
|
+
forNode.set(edge.target.handle, forHandle);
|
|
1270
|
+
}
|
|
1271
|
+
forHandle.set(edge.id, toArray(value));
|
|
1272
|
+
// Merge all parts for this handle
|
|
1273
|
+
const edges = this.graph.getEdges();
|
|
1274
|
+
const merged = [];
|
|
1275
|
+
for (const ed of edges) {
|
|
1276
|
+
if (ed.target.nodeId === edge.target.nodeId &&
|
|
1277
|
+
ed.target.handle === edge.target.handle) {
|
|
1278
|
+
const part = forHandle.get(ed.id);
|
|
1279
|
+
if (part && part.length)
|
|
1280
|
+
merged.push(...part);
|
|
1148
1281
|
}
|
|
1149
1282
|
}
|
|
1283
|
+
return merged;
|
|
1150
1284
|
}
|
|
1151
1285
|
/**
|
|
1152
|
-
*
|
|
1286
|
+
* Check if input value should be set (respecting skipPropagateValues)
|
|
1153
1287
|
*/
|
|
1154
|
-
|
|
1155
|
-
|
|
1288
|
+
shouldSetInputValue(effectiveRunContexts) {
|
|
1289
|
+
if (!effectiveRunContexts) {
|
|
1290
|
+
return true; // Auto mode always sets values
|
|
1291
|
+
}
|
|
1292
|
+
// Check skipPropagateValues (only in run-context mode)
|
|
1293
|
+
for (const id of effectiveRunContexts) {
|
|
1294
|
+
const ctx = this.runContextManager.getRunContext(id);
|
|
1295
|
+
if (ctx && ctx.skipPropagateValues) {
|
|
1296
|
+
return false;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return true;
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Set target input value and emit event
|
|
1303
|
+
*/
|
|
1304
|
+
setTargetInput(edge, dstNode, value) {
|
|
1305
|
+
dstNode.inputs[edge.target.handle] = value;
|
|
1306
|
+
this.eventEmitter.emit("value", {
|
|
1307
|
+
nodeId: edge.target.nodeId,
|
|
1308
|
+
handle: edge.target.handle,
|
|
1309
|
+
value,
|
|
1310
|
+
io: "input",
|
|
1311
|
+
runtimeTypeId: getTypedOutputTypeId(value),
|
|
1312
|
+
});
|
|
1313
|
+
this.handleResolver.scheduleRecomputeHandles(edge.target.nodeId);
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Execute downstream if conditions are met
|
|
1317
|
+
*/
|
|
1318
|
+
executeDownstream(targetNodeId, effectiveRunContexts) {
|
|
1319
|
+
// Determine if we should propagate
|
|
1320
|
+
const shouldPropagate = this.shouldPropagateExecution(effectiveRunContexts);
|
|
1321
|
+
if (shouldPropagate && this.graph.allInboundHaveValue(targetNodeId)) {
|
|
1322
|
+
this.nodeExecutor.execute(targetNodeId, effectiveRunContexts);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Check if execution should propagate
|
|
1327
|
+
*/
|
|
1328
|
+
shouldPropagateExecution(effectiveRunContexts) {
|
|
1329
|
+
if (!effectiveRunContexts) {
|
|
1330
|
+
return true; // Auto mode always propagates
|
|
1331
|
+
}
|
|
1332
|
+
// Check propagate flag (only in run-context mode)
|
|
1333
|
+
for (const id of effectiveRunContexts) {
|
|
1334
|
+
const ctx = this.runContextManager.getRunContext(id);
|
|
1335
|
+
if (ctx && ctx.propagate) {
|
|
1336
|
+
return true;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
return false;
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Update edge stats on successful conversion
|
|
1343
|
+
*/
|
|
1344
|
+
updateEdgeStatsOnSuccess(edge, startAt) {
|
|
1345
|
+
edge.stats.inFlight = false;
|
|
1346
|
+
const duration = Date.now() - startAt;
|
|
1347
|
+
edge.stats.lastDurationMs = duration;
|
|
1348
|
+
this.eventEmitter.emit("stats", {
|
|
1349
|
+
kind: "edge-done",
|
|
1350
|
+
edgeId: edge.id,
|
|
1351
|
+
typeId: edge.typeId,
|
|
1352
|
+
source: { nodeId: edge.source.nodeId, handle: edge.source.handle },
|
|
1353
|
+
target: { nodeId: edge.target.nodeId, handle: edge.target.handle },
|
|
1354
|
+
durationMs: duration,
|
|
1355
|
+
});
|
|
1356
|
+
edge.stats.lastEndAt = Date.now();
|
|
1357
|
+
edge.stats.lastError = undefined;
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Handle edge conversion error
|
|
1361
|
+
*/
|
|
1362
|
+
handleEdgeConversionError(edge, err) {
|
|
1363
|
+
edge.stats.inFlight = false;
|
|
1364
|
+
edge.stats.lastError = err;
|
|
1365
|
+
this.eventEmitter.emit("error", {
|
|
1366
|
+
kind: "edge-convert",
|
|
1367
|
+
edgeId: edge.id,
|
|
1368
|
+
source: { nodeId: edge.source.nodeId, handle: edge.source.handle },
|
|
1369
|
+
target: { nodeId: edge.target.nodeId, handle: edge.target.handle },
|
|
1370
|
+
err,
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Finish edge conversion and decrement pending edges
|
|
1375
|
+
*/
|
|
1376
|
+
finishEdgeConversion(edgeRunContextIds, edgeId) {
|
|
1377
|
+
if (!edgeRunContextIds)
|
|
1378
|
+
return;
|
|
1379
|
+
for (const id of edgeRunContextIds) {
|
|
1380
|
+
this.runContextManager.finishEdgeConversion(id, edgeId);
|
|
1381
|
+
}
|
|
1156
1382
|
}
|
|
1157
1383
|
/**
|
|
1158
1384
|
* Re-emit all outputs from a node (used when graph updates)
|
|
1159
1385
|
* Only re-emits outputs that are valid according to resolved handles
|
|
1160
1386
|
*/
|
|
1161
|
-
|
|
1162
|
-
const node = this.
|
|
1387
|
+
invalidateDownstream(nodeId) {
|
|
1388
|
+
const node = this.graph.getNode(nodeId);
|
|
1163
1389
|
if (!node)
|
|
1164
1390
|
return;
|
|
1165
1391
|
// Get resolved handles to filter out invalid outputs
|
|
1166
|
-
const resolved = this.
|
|
1392
|
+
const resolved = this.graph.getResolvedHandles(nodeId);
|
|
1167
1393
|
const validOutputHandles = resolved?.outputs
|
|
1168
1394
|
? new Set(Object.keys(resolved.outputs))
|
|
1169
1395
|
: new Set();
|
|
1170
1396
|
// Use node's activeRunContexts to propagate to new nodes that were added
|
|
1171
|
-
const runContextIds = node.activeRunContexts.size > 0
|
|
1172
|
-
? new Set(node.activeRunContexts)
|
|
1173
|
-
: undefined;
|
|
1174
1397
|
for (const [handle, value] of Object.entries(node.outputs)) {
|
|
1175
1398
|
// Only re-emit if this handle is still valid
|
|
1176
1399
|
if (validOutputHandles.has(handle)) {
|
|
1177
|
-
this.propagate(nodeId, handle, value,
|
|
1400
|
+
this.propagate(nodeId, handle, value, node.activeRunContextIds);
|
|
1178
1401
|
}
|
|
1179
1402
|
}
|
|
1180
1403
|
}
|
|
@@ -1193,52 +1416,35 @@ class ValuePropagator {
|
|
|
1193
1416
|
}
|
|
1194
1417
|
|
|
1195
1418
|
/**
|
|
1196
|
-
*
|
|
1419
|
+
* NodeExecutor component - handles node execution scheduling and lifecycle
|
|
1197
1420
|
*/
|
|
1198
|
-
class
|
|
1199
|
-
constructor(
|
|
1200
|
-
this.
|
|
1201
|
-
this.graphStructure = graphStructure;
|
|
1421
|
+
class NodeExecutor {
|
|
1422
|
+
constructor(graph, eventEmitter, runContextManager, edgePropagator, runtime, environment) {
|
|
1423
|
+
this.graph = graph;
|
|
1202
1424
|
this.eventEmitter = eventEmitter;
|
|
1203
1425
|
this.runContextManager = runContextManager;
|
|
1204
|
-
this.
|
|
1205
|
-
this.
|
|
1426
|
+
this.edgePropagator = edgePropagator;
|
|
1427
|
+
this.runtime = runtime;
|
|
1428
|
+
this.environment = {};
|
|
1206
1429
|
this.environment = environment ?? {};
|
|
1207
1430
|
}
|
|
1208
1431
|
setEnvironment(environment) {
|
|
1209
1432
|
this.environment = environment;
|
|
1210
1433
|
}
|
|
1211
|
-
/**
|
|
1212
|
-
* Check if all inbound edges for a node have values
|
|
1213
|
-
*/
|
|
1214
|
-
allInboundHaveValue(nodeId) {
|
|
1215
|
-
const node = this.graphStructure.getNode(nodeId);
|
|
1216
|
-
if (!node)
|
|
1217
|
-
return false;
|
|
1218
|
-
const edges = this.graphStructure.getEdges();
|
|
1219
|
-
const inbound = edges.filter((e) => e.target.nodeId === nodeId);
|
|
1220
|
-
if (inbound.length === 0)
|
|
1221
|
-
return true;
|
|
1222
|
-
for (const e of inbound) {
|
|
1223
|
-
if (!(e.target.handle in node.inputs))
|
|
1224
|
-
return false;
|
|
1225
|
-
}
|
|
1226
|
-
return true;
|
|
1227
|
-
}
|
|
1228
1434
|
/**
|
|
1229
1435
|
* Compute effective inputs for a node by merging real inputs with defaults
|
|
1230
1436
|
*/
|
|
1231
1437
|
getEffectiveInputs(nodeId) {
|
|
1232
|
-
const node = this.
|
|
1438
|
+
const node = this.graph.getNode(nodeId);
|
|
1233
1439
|
if (!node)
|
|
1234
1440
|
return {};
|
|
1235
|
-
const registry = this.
|
|
1441
|
+
const registry = this.graph.getRegistry();
|
|
1236
1442
|
if (!registry)
|
|
1237
1443
|
return {};
|
|
1238
1444
|
const desc = registry.nodes.get(node.typeId);
|
|
1239
1445
|
if (!desc)
|
|
1240
1446
|
return {};
|
|
1241
|
-
const resolved = this.
|
|
1447
|
+
const resolved = this.graph.getResolvedHandles(nodeId);
|
|
1242
1448
|
const regDefaults = desc.inputDefaults ?? {};
|
|
1243
1449
|
const dynDefaults = resolved?.inputDefaults ?? {};
|
|
1244
1450
|
// Identify which handles are dynamically resolved (not in registry statics)
|
|
@@ -1252,7 +1458,7 @@ class ExecutionScheduler {
|
|
|
1252
1458
|
// Start with real inputs only (no defaults)
|
|
1253
1459
|
const effective = { ...node.inputs };
|
|
1254
1460
|
// Build set of inbound handles (wired inputs)
|
|
1255
|
-
const edges = this.
|
|
1461
|
+
const edges = this.graph.getEdges();
|
|
1256
1462
|
const inbound = new Set(edges
|
|
1257
1463
|
.filter((e) => e.target.nodeId === nodeId)
|
|
1258
1464
|
.map((e) => e.target.handle));
|
|
@@ -1279,7 +1485,7 @@ class ExecutionScheduler {
|
|
|
1279
1485
|
createExecutionContext(nodeId, node, inputs, runId, abortSignal, runContextIds, options) {
|
|
1280
1486
|
const emitHandler = options?.emitHandler ??
|
|
1281
1487
|
((handle, value) => {
|
|
1282
|
-
this.
|
|
1488
|
+
this.edgePropagator.propagate(nodeId, handle, value, runContextIds);
|
|
1283
1489
|
});
|
|
1284
1490
|
const reportProgress = options?.reportProgress ??
|
|
1285
1491
|
((p) => {
|
|
@@ -1314,20 +1520,18 @@ class ExecutionScheduler {
|
|
|
1314
1520
|
}
|
|
1315
1521
|
}
|
|
1316
1522
|
};
|
|
1317
|
-
// Store run-context IDs for use in scheduleInputsChanged
|
|
1318
|
-
const storedRunContextIds = runContextIds;
|
|
1319
1523
|
return {
|
|
1320
1524
|
nodeId,
|
|
1321
1525
|
state: node.state,
|
|
1322
1526
|
setState: (next) => Object.assign(node.state, next),
|
|
1323
1527
|
emit: emitHandler,
|
|
1324
1528
|
invalidateDownstream: () => {
|
|
1325
|
-
this.
|
|
1529
|
+
this.edgePropagator.invalidateDownstream(nodeId);
|
|
1326
1530
|
},
|
|
1327
|
-
|
|
1328
|
-
if (this.allInboundHaveValue(nodeId)) {
|
|
1531
|
+
execute: () => {
|
|
1532
|
+
if (this.graph.allInboundHaveValue(nodeId)) {
|
|
1329
1533
|
// Preserve run-context IDs when scheduling from execution context
|
|
1330
|
-
this.
|
|
1534
|
+
this.execute(nodeId, runContextIds);
|
|
1331
1535
|
}
|
|
1332
1536
|
},
|
|
1333
1537
|
getInput: (handle) => inputs[handle],
|
|
@@ -1339,232 +1543,345 @@ class ExecutionScheduler {
|
|
|
1339
1543
|
};
|
|
1340
1544
|
}
|
|
1341
1545
|
/**
|
|
1342
|
-
*
|
|
1546
|
+
* Internal method for executing inputs changed (also used by GraphRuntime)
|
|
1547
|
+
*/
|
|
1548
|
+
execute(nodeId, runContextIds) {
|
|
1549
|
+
const node = this.graph.getNode(nodeId);
|
|
1550
|
+
if (!node)
|
|
1551
|
+
return;
|
|
1552
|
+
const runMode = this.runtime.getRunMode();
|
|
1553
|
+
if (!runMode) {
|
|
1554
|
+
console.warn("NodeExecutor.execute: no runMode, skipping execution");
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
if (runMode === "manual" && (!runContextIds || runContextIds.size === 0)) {
|
|
1558
|
+
console.warn("NodeExecutor.execute: no runContextIds provided, skipping execution");
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
if (runMode === "auto" && runContextIds && runContextIds.size > 0) {
|
|
1562
|
+
console.warn("NodeExecutor.execute: runContextIds provided in auto-mode, ignoring");
|
|
1563
|
+
runContextIds = undefined;
|
|
1564
|
+
}
|
|
1565
|
+
// Early validation for auto-mode paused state
|
|
1566
|
+
if (this.runtime.isPaused())
|
|
1567
|
+
return;
|
|
1568
|
+
// Attach run-context IDs if provided
|
|
1569
|
+
this.attachRunContexts(node, runContextIds);
|
|
1570
|
+
// Handle debouncing
|
|
1571
|
+
const now = Date.now();
|
|
1572
|
+
if (this.shouldDebounce(node, now)) {
|
|
1573
|
+
this.handleDebouncedSchedule(node, nodeId, now);
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
// Prepare execution plan
|
|
1577
|
+
const executionPlan = this.prepareExecutionPlan(node, nodeId, runContextIds, now);
|
|
1578
|
+
// Route to appropriate concurrency handler
|
|
1579
|
+
this.routeToConcurrencyHandler(node, nodeId, executionPlan);
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Attach run-context IDs to the node
|
|
1583
|
+
*/
|
|
1584
|
+
attachRunContexts(node, runContextIds) {
|
|
1585
|
+
if (!runContextIds)
|
|
1586
|
+
return;
|
|
1587
|
+
node.activeRunContextIds = new Set([
|
|
1588
|
+
...node.activeRunContextIds,
|
|
1589
|
+
...runContextIds,
|
|
1590
|
+
]);
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Check if execution should be debounced
|
|
1594
|
+
*/
|
|
1595
|
+
shouldDebounce(node, now) {
|
|
1596
|
+
const policy = node.policy ?? {};
|
|
1597
|
+
return !!(policy.debounceMs &&
|
|
1598
|
+
node.lastScheduledAt &&
|
|
1599
|
+
now - node.lastScheduledAt < policy.debounceMs);
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* Handle debounced scheduling by replacing the latest queued item
|
|
1603
|
+
*/
|
|
1604
|
+
handleDebouncedSchedule(node, nodeId, now) {
|
|
1605
|
+
const effectiveInputs = this.getEffectiveInputs(nodeId);
|
|
1606
|
+
node.queue.splice(0, node.queue.length);
|
|
1607
|
+
node.runSeq += 1;
|
|
1608
|
+
const rid = `${nodeId}:${node.runSeq}:${now}`;
|
|
1609
|
+
node.queue.push({ runId: rid, inputs: effectiveInputs });
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* Prepare execution plan with all necessary information
|
|
1613
|
+
*/
|
|
1614
|
+
prepareExecutionPlan(node, nodeId, runContextIds, now) {
|
|
1615
|
+
node.lastScheduledAt = now;
|
|
1616
|
+
node.runSeq += 1;
|
|
1617
|
+
const runId = `${nodeId}:${node.runSeq}:${now}`;
|
|
1618
|
+
node.latestRunId = runId;
|
|
1619
|
+
const effectiveInputs = this.getEffectiveInputs(nodeId);
|
|
1620
|
+
// Take a shallow snapshot of the current policy for this run
|
|
1621
|
+
const policySnapshot = node.policy ? { ...node.policy } : undefined;
|
|
1622
|
+
return {
|
|
1623
|
+
runId,
|
|
1624
|
+
effectiveInputs,
|
|
1625
|
+
runContextIdsForRun: runContextIds,
|
|
1626
|
+
timestamp: now,
|
|
1627
|
+
policy: policySnapshot,
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Route execution to appropriate concurrency handler
|
|
1632
|
+
*/
|
|
1633
|
+
routeToConcurrencyHandler(node, nodeId, plan) {
|
|
1634
|
+
const mode = plan.policy?.asyncConcurrency ?? "switch";
|
|
1635
|
+
switch (mode) {
|
|
1636
|
+
case "drop":
|
|
1637
|
+
this.handleDropMode(node, nodeId, plan);
|
|
1638
|
+
break;
|
|
1639
|
+
case "queue":
|
|
1640
|
+
this.handleQueueMode(node, nodeId, plan);
|
|
1641
|
+
break;
|
|
1642
|
+
case "switch":
|
|
1643
|
+
case "merge":
|
|
1644
|
+
default:
|
|
1645
|
+
this.startRun(node, nodeId, plan);
|
|
1646
|
+
break;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Handle drop mode - drop execution if node is already running, otherwise start run
|
|
1651
|
+
*/
|
|
1652
|
+
handleDropMode(node, nodeId, plan) {
|
|
1653
|
+
// Drop if node is already running
|
|
1654
|
+
if (node.activeControllers.size > 0) {
|
|
1655
|
+
return; // Don't increment pendingCount if we're dropping this run
|
|
1656
|
+
}
|
|
1657
|
+
// Start run if node is not running
|
|
1658
|
+
this.startRun(node, nodeId, plan);
|
|
1659
|
+
}
|
|
1660
|
+
/**
|
|
1661
|
+
* Handle queue mode - add to queue and process sequentially
|
|
1662
|
+
*/
|
|
1663
|
+
handleQueueMode(node, nodeId, plan) {
|
|
1664
|
+
const maxQ = plan.policy?.maxQueue ?? 8;
|
|
1665
|
+
node.queue.push({ runId: plan.runId, inputs: plan.effectiveInputs });
|
|
1666
|
+
if (node.queue.length > maxQ)
|
|
1667
|
+
node.queue.shift();
|
|
1668
|
+
this.processQueue(node, nodeId);
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Process queued executions sequentially
|
|
1672
|
+
*/
|
|
1673
|
+
processQueue(node, nodeId) {
|
|
1674
|
+
const processNext = () => {
|
|
1675
|
+
if (node.activeControllers.size > 0)
|
|
1676
|
+
return;
|
|
1677
|
+
const next = node.queue.shift();
|
|
1678
|
+
if (!next)
|
|
1679
|
+
return;
|
|
1680
|
+
node.latestRunId = next.runId;
|
|
1681
|
+
const policySnapshot = node.policy ? { ...node.policy } : undefined;
|
|
1682
|
+
const plan = {
|
|
1683
|
+
runId: next.runId,
|
|
1684
|
+
effectiveInputs: next.inputs,
|
|
1685
|
+
runContextIdsForRun: node.activeRunContextIds,
|
|
1686
|
+
timestamp: Date.now(),
|
|
1687
|
+
policy: policySnapshot,
|
|
1688
|
+
};
|
|
1689
|
+
this.startRun(node, nodeId, plan, () => {
|
|
1690
|
+
setTimeout(processNext, 0);
|
|
1691
|
+
});
|
|
1692
|
+
};
|
|
1693
|
+
processNext();
|
|
1694
|
+
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Start a node execution run
|
|
1697
|
+
*/
|
|
1698
|
+
startRun(node, nodeId, plan, onDone) {
|
|
1699
|
+
// Track run-contexts
|
|
1700
|
+
this.trackRunContextStart(nodeId, plan.runContextIdsForRun);
|
|
1701
|
+
// Setup execution controller
|
|
1702
|
+
const controller = this.createExecutionController(node, plan.runId);
|
|
1703
|
+
// Handle concurrency mode
|
|
1704
|
+
this.applyConcurrencyMode(node, controller, plan);
|
|
1705
|
+
// Setup timeout if needed
|
|
1706
|
+
const timeoutId = this.setupTimeout(node, controller, plan);
|
|
1707
|
+
// Create execution context
|
|
1708
|
+
const ctx = this.createExecutionContext(nodeId, node, plan.effectiveInputs, plan.runId, controller.signal, plan.runContextIdsForRun, this.createEmitAndProgressHandlers(node, nodeId, plan));
|
|
1709
|
+
// Execute
|
|
1710
|
+
this.executeNode(node, nodeId, ctx, plan, controller, timeoutId, onDone);
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Track run-context start for pending nodes
|
|
1714
|
+
*/
|
|
1715
|
+
trackRunContextStart(nodeId, runContextIdsForRun) {
|
|
1716
|
+
if (runContextIdsForRun && runContextIdsForRun.size > 0) {
|
|
1717
|
+
for (const id of runContextIdsForRun) {
|
|
1718
|
+
this.runContextManager.startNodeRun(id, nodeId);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
/**
|
|
1723
|
+
* Create execution controller and update node stats
|
|
1724
|
+
*/
|
|
1725
|
+
createExecutionController(node, runId) {
|
|
1726
|
+
const controller = new AbortController();
|
|
1727
|
+
node.stats.runs += 1;
|
|
1728
|
+
node.stats.active += 1;
|
|
1729
|
+
node.stats.lastStartAt = Date.now();
|
|
1730
|
+
node.stats.progress = 0;
|
|
1731
|
+
node.activeControllers.add(controller);
|
|
1732
|
+
node.controllerRunIds.set(controller, runId);
|
|
1733
|
+
return controller;
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* Apply concurrency mode (switch mode aborts other controllers)
|
|
1737
|
+
*/
|
|
1738
|
+
applyConcurrencyMode(node, controller, plan) {
|
|
1739
|
+
const mode = plan.policy?.asyncConcurrency ?? "switch";
|
|
1740
|
+
if (mode === "switch") {
|
|
1741
|
+
for (const c of Array.from(node.activeControllers)) {
|
|
1742
|
+
if (c !== controller)
|
|
1743
|
+
c.abort("switch");
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Setup timeout for execution if configured
|
|
1749
|
+
*/
|
|
1750
|
+
setupTimeout(node, controller, plan) {
|
|
1751
|
+
const policy = plan.policy ?? {};
|
|
1752
|
+
if (policy.timeoutMs && policy.timeoutMs > 0) {
|
|
1753
|
+
return setTimeout(() => controller.abort("timeout"), policy.timeoutMs);
|
|
1754
|
+
}
|
|
1755
|
+
return undefined;
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Create emit and progress handlers for execution context
|
|
1759
|
+
*/
|
|
1760
|
+
createEmitAndProgressHandlers(node, nodeId, plan) {
|
|
1761
|
+
const policy = plan.policy ?? {};
|
|
1762
|
+
return {
|
|
1763
|
+
emitHandler: (handle, value) => {
|
|
1764
|
+
const m = policy.asyncConcurrency ?? "switch";
|
|
1765
|
+
// Drop emits from runs that were explicitly cancelled due to a
|
|
1766
|
+
// snapshot/undo/redo operation, regardless of asyncConcurrency.
|
|
1767
|
+
if (node.snapshotCancelledRunIds?.has(plan.runId))
|
|
1768
|
+
return;
|
|
1769
|
+
if (m !== "merge" && plan.runId !== node.latestRunId)
|
|
1770
|
+
return;
|
|
1771
|
+
this.edgePropagator.propagate(nodeId, handle, value, plan.runContextIdsForRun);
|
|
1772
|
+
},
|
|
1773
|
+
reportProgress: (p) => {
|
|
1774
|
+
node.stats.progress = Math.max(0, Math.min(1, Number(p) || 0));
|
|
1775
|
+
this.eventEmitter.emit("stats", {
|
|
1776
|
+
kind: "node-progress",
|
|
1777
|
+
nodeId,
|
|
1778
|
+
typeId: node.typeId,
|
|
1779
|
+
runId: plan.runId,
|
|
1780
|
+
progress: node.stats.progress,
|
|
1781
|
+
});
|
|
1782
|
+
},
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Execute the node with retry logic and cleanup
|
|
1787
|
+
*/
|
|
1788
|
+
executeNode(node, nodeId, ctx, plan, controller, timeoutId, onDone) {
|
|
1789
|
+
// Fire node-start event
|
|
1790
|
+
this.eventEmitter.emit("stats", {
|
|
1791
|
+
kind: "node-start",
|
|
1792
|
+
nodeId,
|
|
1793
|
+
typeId: node.typeId,
|
|
1794
|
+
runId: plan.runId,
|
|
1795
|
+
});
|
|
1796
|
+
ctx.log("debug", "node-start");
|
|
1797
|
+
const exec = async (attempt) => {
|
|
1798
|
+
let hadError = false;
|
|
1799
|
+
try {
|
|
1800
|
+
if (node.lifecycle?.prepare) {
|
|
1801
|
+
ctx.log("debug", "prepare-start");
|
|
1802
|
+
node.lifecycle.prepare(node.params ?? {}, ctx);
|
|
1803
|
+
ctx.log("debug", "prepare-done");
|
|
1804
|
+
}
|
|
1805
|
+
await node.runtime.onInputsChanged?.(plan.effectiveInputs, ctx);
|
|
1806
|
+
}
|
|
1807
|
+
catch (err) {
|
|
1808
|
+
// Suppress errors caused by expected cancellations
|
|
1809
|
+
if (controller.signal.aborted) {
|
|
1810
|
+
const reason = controller.signal.reason;
|
|
1811
|
+
if (reason === "switch" ||
|
|
1812
|
+
reason === "snapshot" ||
|
|
1813
|
+
reason === "node-deleted" ||
|
|
1814
|
+
reason === "user-cancelled") {
|
|
1815
|
+
return; // Cancellation events are emitted separately, skip error handling
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
hadError = true;
|
|
1819
|
+
node.stats.lastError = err;
|
|
1820
|
+
const retry = plan.policy?.retry;
|
|
1821
|
+
if (retry && attempt < (retry.attempts ?? 0)) {
|
|
1822
|
+
const delay = retry.backoffMs ? retry.backoffMs(attempt) : 0;
|
|
1823
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
1824
|
+
return exec(attempt + 1);
|
|
1825
|
+
}
|
|
1826
|
+
this.eventEmitter.emit("error", {
|
|
1827
|
+
kind: "node-run",
|
|
1828
|
+
nodeId,
|
|
1829
|
+
runId: plan.runId,
|
|
1830
|
+
err,
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
finally {
|
|
1834
|
+
this.cleanupExecution(node, nodeId, ctx, plan, controller, timeoutId, hadError, onDone);
|
|
1835
|
+
}
|
|
1836
|
+
};
|
|
1837
|
+
exec(0);
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* Cleanup after execution completes
|
|
1343
1841
|
*/
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
*/
|
|
1350
|
-
scheduleInputsChangedWithRunContexts(nodeId, runContextIds) {
|
|
1351
|
-
this.scheduleInputsChangedInternal(nodeId, runContextIds);
|
|
1352
|
-
}
|
|
1353
|
-
/**
|
|
1354
|
-
* Internal method for scheduling (also used by GraphRuntime)
|
|
1355
|
-
*/
|
|
1356
|
-
scheduleInputsChangedInternal(nodeId, runContextIds) {
|
|
1357
|
-
const node = this.graphStructure.getNode(nodeId);
|
|
1358
|
-
if (!node)
|
|
1359
|
-
return;
|
|
1360
|
-
if (this.runtimeCoordinator.isPaused())
|
|
1361
|
-
return;
|
|
1362
|
-
// If run-context IDs are provided, attach them to the node
|
|
1363
|
-
if (runContextIds) {
|
|
1364
|
-
for (const id of runContextIds) {
|
|
1365
|
-
node.activeRunContexts.add(id);
|
|
1842
|
+
cleanupExecution(node, nodeId, ctx, plan, controller, timeoutId, hadError, onDone) {
|
|
1843
|
+
// Decrement pendingNodes count for all relevant run-contexts
|
|
1844
|
+
if (plan.runContextIdsForRun && plan.runContextIdsForRun.size > 0) {
|
|
1845
|
+
for (const id of plan.runContextIdsForRun) {
|
|
1846
|
+
this.runContextManager.finishNodeRun(id, nodeId);
|
|
1366
1847
|
}
|
|
1367
1848
|
}
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
// Compute effective inputs (real inputs + defaults) for this execution
|
|
1371
|
-
const effectiveInputs = this.getEffectiveInputs(nodeId);
|
|
1372
|
-
if (policy.debounceMs &&
|
|
1373
|
-
node.lastScheduledAt &&
|
|
1374
|
-
now - node.lastScheduledAt < policy.debounceMs) {
|
|
1375
|
-
// debounce: replace latest queued
|
|
1376
|
-
node.queue.splice(0, node.queue.length);
|
|
1377
|
-
node.runSeq += 1;
|
|
1378
|
-
const rid = `${nodeId}:${node.runSeq}:${now}`;
|
|
1379
|
-
node.queue.push({ runId: rid, inputs: effectiveInputs });
|
|
1849
|
+
// Skip cleanup if node was deleted (cleanup already handled)
|
|
1850
|
+
if (!this.graph.hasNode(nodeId)) {
|
|
1380
1851
|
return;
|
|
1381
1852
|
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
node.
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
const controller = new AbortController();
|
|
1401
|
-
node.stats.runs += 1;
|
|
1402
|
-
node.stats.active += 1;
|
|
1403
|
-
node.stats.lastStartAt = now;
|
|
1404
|
-
node.stats.progress = 0;
|
|
1405
|
-
node.activeControllers.add(controller);
|
|
1406
|
-
node.controllerRunIds.set(controller, runId);
|
|
1407
|
-
const mode = policy.asyncConcurrency ?? "switch";
|
|
1408
|
-
if (mode === "switch") {
|
|
1409
|
-
for (const c of Array.from(node.activeControllers)) {
|
|
1410
|
-
if (c !== controller)
|
|
1411
|
-
c.abort("switch");
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
let timeoutId;
|
|
1415
|
-
if (policy.timeoutMs && policy.timeoutMs > 0) {
|
|
1416
|
-
timeoutId = setTimeout(() => controller.abort("timeout"), policy.timeoutMs);
|
|
1417
|
-
}
|
|
1418
|
-
const ctx = this.createExecutionContext(nodeId, node, capturedInputs, runId, controller.signal, runContextIdsForRun, {
|
|
1419
|
-
emitHandler: (handle, value) => {
|
|
1420
|
-
const m = policy.asyncConcurrency ?? "switch";
|
|
1421
|
-
// Drop emits from runs that were explicitly cancelled due to a
|
|
1422
|
-
// snapshot/undo/redo operation, regardless of asyncConcurrency.
|
|
1423
|
-
if (node.snapshotCancelledRunIds?.has(runId))
|
|
1424
|
-
return;
|
|
1425
|
-
if (m !== "merge" && runId !== node.latestRunId)
|
|
1426
|
-
return;
|
|
1427
|
-
this.valuePropagator.propagate(nodeId, handle, value, runContextIdsForRun);
|
|
1428
|
-
},
|
|
1429
|
-
reportProgress: (p) => {
|
|
1430
|
-
node.stats.progress = Math.max(0, Math.min(1, Number(p) || 0));
|
|
1431
|
-
this.eventEmitter.emit("stats", {
|
|
1432
|
-
kind: "node-progress",
|
|
1433
|
-
nodeId,
|
|
1434
|
-
typeId: node.typeId,
|
|
1435
|
-
runId,
|
|
1436
|
-
progress: node.stats.progress,
|
|
1437
|
-
});
|
|
1438
|
-
},
|
|
1439
|
-
});
|
|
1440
|
-
const exec = async (attempt) => {
|
|
1441
|
-
let hadError = false;
|
|
1442
|
-
try {
|
|
1443
|
-
if (node.lifecycle?.prepare) {
|
|
1444
|
-
ctx.log("debug", "prepare-start");
|
|
1445
|
-
node.lifecycle.prepare(node.params ?? {}, ctx);
|
|
1446
|
-
ctx.log("debug", "prepare-done");
|
|
1447
|
-
}
|
|
1448
|
-
await node.runtime.onInputsChanged?.(capturedInputs, ctx);
|
|
1449
|
-
}
|
|
1450
|
-
catch (err) {
|
|
1451
|
-
// Suppress errors caused by expected cancellations
|
|
1452
|
-
if (controller.signal.aborted) {
|
|
1453
|
-
const reason = controller.signal.reason;
|
|
1454
|
-
if (reason === "switch" ||
|
|
1455
|
-
reason === "snapshot" ||
|
|
1456
|
-
reason === "node-deleted" ||
|
|
1457
|
-
reason === "user-cancelled") {
|
|
1458
|
-
return; // Cancellation events are emitted separately, skip error handling
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
hadError = true;
|
|
1462
|
-
node.stats.lastError = err;
|
|
1463
|
-
const retry = policy.retry;
|
|
1464
|
-
if (retry && attempt < (retry.attempts ?? 0)) {
|
|
1465
|
-
const delay = retry.backoffMs ? retry.backoffMs(attempt) : 0;
|
|
1466
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
1467
|
-
return exec(attempt + 1);
|
|
1468
|
-
}
|
|
1469
|
-
this.eventEmitter.emit("error", {
|
|
1470
|
-
kind: "node-run",
|
|
1471
|
-
nodeId,
|
|
1472
|
-
runId,
|
|
1473
|
-
err,
|
|
1474
|
-
});
|
|
1475
|
-
}
|
|
1476
|
-
finally {
|
|
1477
|
-
// Skip cleanup if node was deleted (cleanup already handled)
|
|
1478
|
-
if (!this.graphStructure.hasNode(nodeId)) {
|
|
1479
|
-
return;
|
|
1480
|
-
}
|
|
1481
|
-
if (timeoutId)
|
|
1482
|
-
clearTimeout(timeoutId);
|
|
1483
|
-
node.activeControllers.delete(controller);
|
|
1484
|
-
node.controllerRunIds.delete(controller);
|
|
1485
|
-
node.stats.active = Math.max(0, node.activeControllers.size);
|
|
1486
|
-
node.stats.lastEndAt = Date.now();
|
|
1487
|
-
node.stats.lastDurationMs =
|
|
1488
|
-
node.stats.lastStartAt && node.stats.lastEndAt
|
|
1489
|
-
? node.stats.lastEndAt - node.stats.lastStartAt
|
|
1490
|
-
: undefined;
|
|
1491
|
-
if (!hadError)
|
|
1492
|
-
node.stats.lastError = undefined;
|
|
1493
|
-
// Only emit node-done if not cancelled (cancellation events emitted separately)
|
|
1494
|
-
const isCancelled = controller.signal.aborted &&
|
|
1495
|
-
(controller.signal.reason === "snapshot" ||
|
|
1496
|
-
controller.signal.reason === "node-deleted" ||
|
|
1497
|
-
controller.signal.reason === "user-cancelled");
|
|
1498
|
-
if (!isCancelled) {
|
|
1499
|
-
this.eventEmitter.emit("stats", {
|
|
1500
|
-
kind: "node-done",
|
|
1501
|
-
nodeId,
|
|
1502
|
-
typeId: node.typeId,
|
|
1503
|
-
runId,
|
|
1504
|
-
durationMs: node.stats.lastDurationMs,
|
|
1505
|
-
});
|
|
1506
|
-
}
|
|
1507
|
-
ctx.log("debug", "node-done", {
|
|
1508
|
-
durationMs: node.stats.lastDurationMs,
|
|
1509
|
-
hadError,
|
|
1510
|
-
});
|
|
1511
|
-
// Decrement pending count for all relevant run-contexts
|
|
1512
|
-
if (runContextIdsForRun && runContextIdsForRun.size > 0) {
|
|
1513
|
-
for (const id of runContextIdsForRun) {
|
|
1514
|
-
const ctx = this.runContextManager.getRunContext(id);
|
|
1515
|
-
if (ctx) {
|
|
1516
|
-
ctx.pending--;
|
|
1517
|
-
if (ctx.pending === 0) {
|
|
1518
|
-
this.runContextManager.finishRunContext(id, this.graphStructure.getNodes());
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
if (onDone)
|
|
1524
|
-
onDone();
|
|
1525
|
-
}
|
|
1526
|
-
};
|
|
1527
|
-
// Fire node-start event
|
|
1853
|
+
if (timeoutId)
|
|
1854
|
+
clearTimeout(timeoutId);
|
|
1855
|
+
node.activeControllers.delete(controller);
|
|
1856
|
+
node.controllerRunIds.delete(controller);
|
|
1857
|
+
node.stats.active = Math.max(0, node.activeControllers.size);
|
|
1858
|
+
node.stats.lastEndAt = Date.now();
|
|
1859
|
+
node.stats.lastDurationMs =
|
|
1860
|
+
node.stats.lastStartAt && node.stats.lastEndAt
|
|
1861
|
+
? node.stats.lastEndAt - node.stats.lastStartAt
|
|
1862
|
+
: undefined;
|
|
1863
|
+
if (!hadError)
|
|
1864
|
+
node.stats.lastError = undefined;
|
|
1865
|
+
// Only emit node-done if not cancelled (cancellation events emitted separately)
|
|
1866
|
+
const isCancelled = controller.signal.aborted &&
|
|
1867
|
+
(controller.signal.reason === "snapshot" ||
|
|
1868
|
+
controller.signal.reason === "node-deleted" ||
|
|
1869
|
+
controller.signal.reason === "user-cancelled");
|
|
1870
|
+
if (!isCancelled) {
|
|
1528
1871
|
this.eventEmitter.emit("stats", {
|
|
1529
|
-
kind: "node-
|
|
1872
|
+
kind: "node-done",
|
|
1530
1873
|
nodeId,
|
|
1531
1874
|
typeId: node.typeId,
|
|
1532
|
-
runId,
|
|
1875
|
+
runId: plan.runId,
|
|
1876
|
+
durationMs: node.stats.lastDurationMs,
|
|
1533
1877
|
});
|
|
1534
|
-
ctx.log("debug", "node-start");
|
|
1535
|
-
exec(0);
|
|
1536
|
-
};
|
|
1537
|
-
const mode = policy.asyncConcurrency ?? "switch";
|
|
1538
|
-
if (mode === "drop" && node.activeControllers.size > 0) {
|
|
1539
|
-
// Don't increment pendingCount if we're dropping this run
|
|
1540
|
-
return;
|
|
1541
|
-
}
|
|
1542
|
-
if (mode === "queue") {
|
|
1543
|
-
const maxQ = policy.maxQueue ?? 8;
|
|
1544
|
-
node.queue.push({ runId: rid, inputs: effectiveInputs });
|
|
1545
|
-
if (node.queue.length > maxQ)
|
|
1546
|
-
node.queue.shift();
|
|
1547
|
-
const processNext = () => {
|
|
1548
|
-
if (node.activeControllers.size > 0)
|
|
1549
|
-
return;
|
|
1550
|
-
const next = node.queue.shift();
|
|
1551
|
-
if (!next)
|
|
1552
|
-
return;
|
|
1553
|
-
node.latestRunId = next.runId;
|
|
1554
|
-
// Use node's activeRunContexts for queued items
|
|
1555
|
-
const queuedRunContextIds = node.activeRunContexts.size > 0
|
|
1556
|
-
? new Set(node.activeRunContexts)
|
|
1557
|
-
: undefined;
|
|
1558
|
-
startRun(next.runId, next.inputs, queuedRunContextIds, () => {
|
|
1559
|
-
// After finishing, schedule next
|
|
1560
|
-
setTimeout(processNext, 0);
|
|
1561
|
-
});
|
|
1562
|
-
};
|
|
1563
|
-
processNext();
|
|
1564
|
-
return;
|
|
1565
1878
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1879
|
+
ctx.log("debug", "node-done", {
|
|
1880
|
+
durationMs: node.stats.lastDurationMs,
|
|
1881
|
+
hadError,
|
|
1882
|
+
});
|
|
1883
|
+
if (onDone)
|
|
1884
|
+
onDone();
|
|
1568
1885
|
}
|
|
1569
1886
|
/**
|
|
1570
1887
|
* Cancel all active runs for a node
|
|
@@ -1612,7 +1929,7 @@ class ExecutionScheduler {
|
|
|
1612
1929
|
const toCancel = new Set(nodeIds);
|
|
1613
1930
|
const visited = new Set();
|
|
1614
1931
|
const queue = [...nodeIds];
|
|
1615
|
-
const edges = this.
|
|
1932
|
+
const edges = this.graph.getEdges();
|
|
1616
1933
|
// Collect all downstream nodes to cancel
|
|
1617
1934
|
for (let i = 0; i < queue.length; i++) {
|
|
1618
1935
|
const nodeId = queue[i];
|
|
@@ -1631,7 +1948,7 @@ class ExecutionScheduler {
|
|
|
1631
1948
|
}
|
|
1632
1949
|
// Cancel runs for all affected nodes
|
|
1633
1950
|
for (const nodeId of toCancel) {
|
|
1634
|
-
const node = this.
|
|
1951
|
+
const node = this.graph.getNode(nodeId);
|
|
1635
1952
|
if (!node)
|
|
1636
1953
|
continue;
|
|
1637
1954
|
this.cancelNodeActiveRuns(node, reason);
|
|
@@ -1642,8 +1959,8 @@ class ExecutionScheduler {
|
|
|
1642
1959
|
}
|
|
1643
1960
|
// Cancel nodes in run-contexts (exclude them from active run-contexts)
|
|
1644
1961
|
for (const nodeId of toCancel) {
|
|
1645
|
-
|
|
1646
|
-
|
|
1962
|
+
// includeDownstream = false (already collected above)
|
|
1963
|
+
this.runContextManager.cancelNodeInRunContexts(nodeId, false);
|
|
1647
1964
|
}
|
|
1648
1965
|
}
|
|
1649
1966
|
}
|
|
@@ -1652,45 +1969,30 @@ class ExecutionScheduler {
|
|
|
1652
1969
|
class GraphRuntime {
|
|
1653
1970
|
constructor() {
|
|
1654
1971
|
// State
|
|
1655
|
-
this.paused = false;
|
|
1656
1972
|
this.environment = {};
|
|
1973
|
+
this.runMode = null;
|
|
1974
|
+
this.pauseRefCount = 0;
|
|
1657
1975
|
// Initialize components
|
|
1658
|
-
this.
|
|
1976
|
+
this.graph = new Graph();
|
|
1659
1977
|
this.eventEmitter = new EventEmitter();
|
|
1660
|
-
this.runContextManager = new RunContextManager();
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
undefined // environment set later
|
|
1666
|
-
);
|
|
1667
|
-
// ValuePropagator needs IHandleResolver and IRuntimeCoordinator
|
|
1668
|
-
// ExecutionScheduler needs IValuePropagator - circular dependency!
|
|
1669
|
-
// Solution: Create ValuePropagator first (without executionScheduler),
|
|
1670
|
-
// then create ExecutionScheduler with ValuePropagator,
|
|
1671
|
-
// then wire ValuePropagator with ExecutionScheduler via setExecutionScheduler
|
|
1672
|
-
this.valuePropagator = new ValuePropagator(this.graphStructure, this.eventEmitter, this.runContextManager, this.handleResolver, // IHandleResolver
|
|
1673
|
-
this // IRuntimeCoordinator
|
|
1674
|
-
);
|
|
1675
|
-
// Create ExecutionScheduler with ValuePropagator
|
|
1676
|
-
this.executionScheduler = new ExecutionScheduler(this.graphStructure, this.eventEmitter, this.runContextManager, this.valuePropagator, // IValuePropagator
|
|
1677
|
-
this, // IRuntimeCoordinator
|
|
1678
|
-
this.environment);
|
|
1679
|
-
// Wire ValuePropagator with ExecutionScheduler to resolve circular dependency
|
|
1680
|
-
this.valuePropagator.setExecutionScheduler(this.executionScheduler);
|
|
1978
|
+
this.runContextManager = new RunContextManager(this.graph);
|
|
1979
|
+
this.handleResolver = new HandleResolver(this.graph, this.eventEmitter, this.runContextManager, this);
|
|
1980
|
+
this.edgePropagator = new EdgePropagator(this.graph, this.eventEmitter, this.runContextManager, this.handleResolver, this);
|
|
1981
|
+
// Create NodeExecutor with EdgePropagator
|
|
1982
|
+
this.nodeExecutor = new NodeExecutor(this.graph, this.eventEmitter, this.runContextManager, this, this);
|
|
1681
1983
|
}
|
|
1682
1984
|
static create(def, registry, opts) {
|
|
1683
1985
|
const gr = new GraphRuntime();
|
|
1684
1986
|
gr.environment = opts?.environment ?? {};
|
|
1685
1987
|
// Set registry and environment on components
|
|
1686
|
-
gr.
|
|
1988
|
+
gr.graph.setRegistry(registry);
|
|
1687
1989
|
gr.handleResolver.setRegistry(registry);
|
|
1688
1990
|
gr.handleResolver.setEnvironment(gr.environment);
|
|
1689
|
-
gr.
|
|
1991
|
+
gr.nodeExecutor.setEnvironment(gr.environment);
|
|
1690
1992
|
// Precompute per-node resolved handles (use def-provided overrides; do not compute dynamically here)
|
|
1691
|
-
const initial =
|
|
1692
|
-
for (const [nodeId, handles] of initial.
|
|
1693
|
-
gr.
|
|
1993
|
+
const initial = tryHandleResolving(def, registry, gr.environment);
|
|
1994
|
+
for (const [nodeId, handles] of initial.resolved) {
|
|
1995
|
+
gr.graph.setResolvedHandles(nodeId, handles);
|
|
1694
1996
|
}
|
|
1695
1997
|
// Instantiate nodes
|
|
1696
1998
|
for (const n of def.nodes) {
|
|
@@ -1731,13 +2033,13 @@ class GraphRuntime {
|
|
|
1731
2033
|
queued: 0,
|
|
1732
2034
|
progress: 0,
|
|
1733
2035
|
},
|
|
1734
|
-
|
|
2036
|
+
activeRunContextIds: new Set(),
|
|
1735
2037
|
};
|
|
1736
|
-
gr.
|
|
2038
|
+
gr.graph.setNode(n.nodeId, rn);
|
|
1737
2039
|
}
|
|
1738
2040
|
// Instantiate edges
|
|
1739
|
-
const edges =
|
|
1740
|
-
gr.
|
|
2041
|
+
const edges = buildEdges(def, registry, gr.graph.getResolvedHandlesMap());
|
|
2042
|
+
gr.graph.setEdges(edges);
|
|
1741
2043
|
// Schedule async recompute only for nodes that indicated Promise-based resolveHandles
|
|
1742
2044
|
for (const nodeId of initial.pending) {
|
|
1743
2045
|
gr.handleResolver.scheduleRecomputeHandles(nodeId);
|
|
@@ -1748,12 +2050,12 @@ class GraphRuntime {
|
|
|
1748
2050
|
return this.eventEmitter.on(event, handler);
|
|
1749
2051
|
}
|
|
1750
2052
|
setInputs(nodeId, inputs) {
|
|
1751
|
-
const node = this.
|
|
2053
|
+
const node = this.graph.getNode(nodeId);
|
|
1752
2054
|
if (!node)
|
|
1753
2055
|
throw new Error(`Node not found: ${nodeId}`);
|
|
1754
2056
|
let anyChanged = false;
|
|
1755
|
-
const edges = this.
|
|
1756
|
-
const registry = this.
|
|
2057
|
+
const edges = this.graph.getEdges();
|
|
2058
|
+
const registry = this.graph.getRegistry();
|
|
1757
2059
|
for (const [handle, value] of Object.entries(inputs)) {
|
|
1758
2060
|
const hasInbound = edges.some((e) => e.target.nodeId === nodeId && e.target.handle === handle);
|
|
1759
2061
|
if (hasInbound)
|
|
@@ -1761,7 +2063,7 @@ class GraphRuntime {
|
|
|
1761
2063
|
// Validate input value against declared type
|
|
1762
2064
|
if (value !== undefined && registry) {
|
|
1763
2065
|
const desc = registry.nodes.get(node.typeId);
|
|
1764
|
-
const resolved = this.
|
|
2066
|
+
const resolved = this.graph.getResolvedHandles(nodeId);
|
|
1765
2067
|
// Get typeId from resolved handles first, then registry statics
|
|
1766
2068
|
const typeId = resolved
|
|
1767
2069
|
? getInputTypeId(resolved.inputs, handle)
|
|
@@ -1800,100 +2102,23 @@ class GraphRuntime {
|
|
|
1800
2102
|
anyChanged = true;
|
|
1801
2103
|
}
|
|
1802
2104
|
}
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
this.handleResolver.scheduleRecomputeHandles(nodeId);
|
|
2105
|
+
// In auto mode, input updates can trigger execution; in manual mode they never should.
|
|
2106
|
+
if (anyChanged) {
|
|
2107
|
+
this.handleResolver.scheduleRecomputeHandles(nodeId);
|
|
2108
|
+
if (this.runMode === "auto" && this.graph.allInboundHaveValue(nodeId)) {
|
|
2109
|
+
this.execute(nodeId);
|
|
2110
|
+
}
|
|
1810
2111
|
}
|
|
1811
2112
|
}
|
|
1812
2113
|
getOutput(nodeId, output) {
|
|
1813
|
-
const node = this.
|
|
2114
|
+
const node = this.graph.getNode(nodeId);
|
|
1814
2115
|
return node?.outputs[output];
|
|
1815
2116
|
}
|
|
1816
|
-
// Update resolved handles for a single node and refresh edge converters/types that touch it
|
|
1817
|
-
updateNodeHandles(nodeId, handles, registry) {
|
|
1818
|
-
const node = this.graphStructure.getNode(nodeId);
|
|
1819
|
-
if (!node)
|
|
1820
|
-
return;
|
|
1821
|
-
const oldResolved = this.graphStructure.getResolvedHandles(nodeId);
|
|
1822
|
-
this.graphStructure.setResolvedHandles(nodeId, handles);
|
|
1823
|
-
// Clear outputs that are no longer valid handles
|
|
1824
|
-
const oldOutputs = oldResolved?.outputs ?? {};
|
|
1825
|
-
const newOutputs = handles.outputs ?? {};
|
|
1826
|
-
const oldOutputHandles = new Set(Object.keys(oldOutputs));
|
|
1827
|
-
const newOutputHandles = new Set(Object.keys(newOutputs));
|
|
1828
|
-
for (const handle of oldOutputHandles) {
|
|
1829
|
-
if (!newOutputHandles.has(handle)) {
|
|
1830
|
-
// Output handle was removed - clear it and emit undefined to invalidate downstream
|
|
1831
|
-
delete node.outputs[handle];
|
|
1832
|
-
this.eventEmitter.emit("value", {
|
|
1833
|
-
nodeId,
|
|
1834
|
-
handle,
|
|
1835
|
-
value: undefined,
|
|
1836
|
-
io: "output",
|
|
1837
|
-
});
|
|
1838
|
-
}
|
|
1839
|
-
}
|
|
1840
|
-
const edges = this.graphStructure.getEdges();
|
|
1841
|
-
// Recompute edge converter/type for edges where this node is source or target
|
|
1842
|
-
for (const e of edges) {
|
|
1843
|
-
const srcNode = this.graphStructure.getNode(e.source.nodeId);
|
|
1844
|
-
const dstNode = this.graphStructure.getNode(e.target.nodeId);
|
|
1845
|
-
let srcDeclared = e.effectiveTypeId; // Use effectiveTypeId as fallback
|
|
1846
|
-
let dstDeclared = e.dstDeclared;
|
|
1847
|
-
const oldDstDeclared = dstDeclared; // Track old value to detect resolution
|
|
1848
|
-
if (e.source.nodeId === nodeId) {
|
|
1849
|
-
const resolved = this.graphStructure.getResolvedHandles(nodeId);
|
|
1850
|
-
srcDeclared = resolved
|
|
1851
|
-
? resolved.outputs[e.source.handle]
|
|
1852
|
-
: srcDeclared;
|
|
1853
|
-
// Update effectiveTypeId if original wasn't explicit
|
|
1854
|
-
if (!e.typeId) {
|
|
1855
|
-
e.effectiveTypeId = Array.isArray(srcDeclared)
|
|
1856
|
-
? srcDeclared?.[0] ?? "untyped"
|
|
1857
|
-
: srcDeclared ?? "untyped";
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
if (e.target.nodeId === nodeId) {
|
|
1861
|
-
const resolved = this.graphStructure.getResolvedHandles(nodeId);
|
|
1862
|
-
if (resolved) {
|
|
1863
|
-
dstDeclared = getInputTypeId(resolved.inputs, e.target.handle);
|
|
1864
|
-
e.dstDeclared = dstDeclared;
|
|
1865
|
-
}
|
|
1866
|
-
}
|
|
1867
|
-
const conv = GraphStructure.buildEdgeConverters(srcDeclared, dstDeclared, registry, `updateNodeHandles: ${srcNode?.typeId || ""}.${e.source.nodeId}.${e.source.handle} -> ${dstNode?.typeId || ""}.${e.target.nodeId}.${e.target.handle}`);
|
|
1868
|
-
e.convert = conv.convert;
|
|
1869
|
-
e.convertAsync = conv.convertAsync;
|
|
1870
|
-
// If target handle was just resolved (was undefined, now has a type), re-propagate values
|
|
1871
|
-
if (e.target.nodeId === nodeId &&
|
|
1872
|
-
oldDstDeclared === undefined &&
|
|
1873
|
-
dstDeclared !== undefined) {
|
|
1874
|
-
const srcNode = this.graphStructure.getNode(e.source.nodeId);
|
|
1875
|
-
if (srcNode) {
|
|
1876
|
-
const srcValue = srcNode.outputs[e.source.handle];
|
|
1877
|
-
if (srcValue !== undefined) {
|
|
1878
|
-
// Re-propagate through the now-resolved edge converter
|
|
1879
|
-
// Preserve run-contexts if source node has them
|
|
1880
|
-
const runContextIds = srcNode.activeRunContexts.size > 0
|
|
1881
|
-
? new Set(srcNode.activeRunContexts)
|
|
1882
|
-
: undefined;
|
|
1883
|
-
this.valuePropagator.propagate(e.source.nodeId, e.source.handle, srcValue, runContextIds);
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
// Re-emit only valid outputs (after clearing removed ones)
|
|
1889
|
-
this.valuePropagator.reemitNodeOutputs(nodeId);
|
|
1890
|
-
}
|
|
1891
2117
|
launch(invalidate = false) {
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
const effectiveInputs = this.executionScheduler.getEffectiveInputs(node.nodeId);
|
|
2118
|
+
for (const node of this.graph.getNodes().values()) {
|
|
2119
|
+
const effectiveInputs = this.nodeExecutor.getEffectiveInputs(node.nodeId);
|
|
1895
2120
|
const ctrl = new AbortController();
|
|
1896
|
-
const ctx = this.
|
|
2121
|
+
const ctx = this.nodeExecutor.createExecutionContext(node.nodeId, node, effectiveInputs, `${node.nodeId}:init`, ctrl.signal);
|
|
1897
2122
|
if (node.lifecycle?.prepare) {
|
|
1898
2123
|
ctx.log("debug", "prepare-start");
|
|
1899
2124
|
node.lifecycle.prepare(node.params ?? {}, ctx);
|
|
@@ -1901,40 +2126,27 @@ class GraphRuntime {
|
|
|
1901
2126
|
}
|
|
1902
2127
|
node.runtime.onActivated?.();
|
|
1903
2128
|
}
|
|
1904
|
-
if (invalidate) {
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
this.executionScheduler.scheduleInputsChangedInternal(nodeId);
|
|
2129
|
+
if (this.runMode === "auto" && invalidate) {
|
|
2130
|
+
for (const nodeId of this.graph.getNodes().keys()) {
|
|
2131
|
+
if (this.graph.allInboundHaveValue(nodeId))
|
|
2132
|
+
this.execute(nodeId);
|
|
1909
2133
|
}
|
|
1910
2134
|
}
|
|
1911
2135
|
}
|
|
1912
2136
|
triggerExternal(nodeId, event) {
|
|
1913
|
-
const node = this.
|
|
2137
|
+
const node = this.graph.getNode(nodeId);
|
|
1914
2138
|
if (!node)
|
|
1915
2139
|
return;
|
|
1916
|
-
// Forward event to node's onExternalEvent handler for custom actions
|
|
1917
2140
|
node.runtime.onExternalEvent?.(event, node.state);
|
|
1918
2141
|
}
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
this.runContextManager.resolveAll();
|
|
1922
|
-
for (const node of this.graphStructure.getNodes().values()) {
|
|
1923
|
-
node.runtime.onDeactivated?.();
|
|
1924
|
-
node.runtime.dispose?.();
|
|
1925
|
-
node.lifecycle?.dispose?.({
|
|
1926
|
-
state: node.state,
|
|
1927
|
-
setState: (next) => Object.assign(node.state, next),
|
|
1928
|
-
});
|
|
1929
|
-
}
|
|
1930
|
-
this.graphStructure.clear();
|
|
2142
|
+
cancelNodeRuns(nodeIds) {
|
|
2143
|
+
this.nodeExecutor.cancelNodeRuns(nodeIds);
|
|
1931
2144
|
}
|
|
1932
2145
|
getNodeIds() {
|
|
1933
|
-
return Array.from(this.
|
|
2146
|
+
return Array.from(this.graph.getNodes().keys());
|
|
1934
2147
|
}
|
|
1935
|
-
// Unsafe helpers for serializer: read-only accessors and hydration
|
|
1936
2148
|
getNodeData(nodeId) {
|
|
1937
|
-
const node = this.
|
|
2149
|
+
const node = this.graph.getNode(nodeId);
|
|
1938
2150
|
if (!node)
|
|
1939
2151
|
return undefined;
|
|
1940
2152
|
return {
|
|
@@ -1951,16 +2163,14 @@ class GraphRuntime {
|
|
|
1951
2163
|
setEnvironment(env) {
|
|
1952
2164
|
this.environment = { ...env };
|
|
1953
2165
|
this.handleResolver.setEnvironment(this.environment);
|
|
1954
|
-
this.
|
|
1955
|
-
|
|
1956
|
-
for (const nodeId of this.graphStructure.getNodes().keys()) {
|
|
2166
|
+
this.nodeExecutor.setEnvironment(this.environment);
|
|
2167
|
+
for (const nodeId of this.graph.getNodes().keys()) {
|
|
1957
2168
|
this.handleResolver.scheduleRecomputeHandles(nodeId);
|
|
1958
2169
|
}
|
|
1959
2170
|
}
|
|
1960
|
-
// Export a GraphDefinition reflecting the current runtime view
|
|
1961
2171
|
getGraphDef() {
|
|
1962
|
-
const nodes = Array.from(this.
|
|
1963
|
-
const resolved = this.
|
|
2172
|
+
const nodes = Array.from(this.graph.getNodes().values()).map((n) => {
|
|
2173
|
+
const resolved = this.graph.getResolvedHandles(n.nodeId);
|
|
1964
2174
|
return {
|
|
1965
2175
|
nodeId: n.nodeId,
|
|
1966
2176
|
typeId: n.typeId,
|
|
@@ -1968,18 +2178,15 @@ class GraphRuntime {
|
|
|
1968
2178
|
resolvedHandles: resolved ? { ...resolved } : undefined,
|
|
1969
2179
|
};
|
|
1970
2180
|
});
|
|
1971
|
-
const edges = this.
|
|
1972
|
-
.getEdges()
|
|
1973
|
-
.map((e) => ({
|
|
2181
|
+
const edges = this.graph.getEdges().map((e) => ({
|
|
1974
2182
|
id: e.id,
|
|
1975
2183
|
source: { nodeId: e.source.nodeId, handle: e.source.handle },
|
|
1976
2184
|
target: { nodeId: e.target.nodeId, handle: e.target.handle },
|
|
1977
|
-
typeId: e.typeId,
|
|
2185
|
+
typeId: e.typeId,
|
|
1978
2186
|
}));
|
|
1979
2187
|
return { nodes, edges };
|
|
1980
2188
|
}
|
|
1981
2189
|
async whenIdle() {
|
|
1982
|
-
// If we have active run-contexts, wait for all of them to complete
|
|
1983
2190
|
const allRunContexts = this.runContextManager.getAllRunContexts();
|
|
1984
2191
|
if (allRunContexts.size > 0) {
|
|
1985
2192
|
await new Promise((resolve) => {
|
|
@@ -1995,7 +2202,7 @@ class GraphRuntime {
|
|
|
1995
2202
|
});
|
|
1996
2203
|
}
|
|
1997
2204
|
const isIdle = () => {
|
|
1998
|
-
for (const n of this.
|
|
2205
|
+
for (const n of this.graph.getNodes().values()) {
|
|
1999
2206
|
if (n.activeControllers.size > 0)
|
|
2000
2207
|
return false;
|
|
2001
2208
|
if (n.queue.length > 0)
|
|
@@ -2015,97 +2222,51 @@ class GraphRuntime {
|
|
|
2015
2222
|
setTimeout(check, 10);
|
|
2016
2223
|
});
|
|
2017
2224
|
}
|
|
2018
|
-
/**
|
|
2019
|
-
* Run this node and optionally all dynamically reachable downstream nodes as a run-context.
|
|
2020
|
-
* Includes nodes added later behind the same path (via re-emits).
|
|
2021
|
-
* @param startNodeId - The node to start execution from
|
|
2022
|
-
* @param options - Execution options
|
|
2023
|
-
* @param options.skipPropagateValues - If true, don't set inputs of linked nodes (default: false)
|
|
2024
|
-
* @param options.propagate - If false, don't schedule downstream nodes (default: true)
|
|
2025
|
-
*/
|
|
2026
2225
|
async runFromHereContext(startNodeId, options) {
|
|
2027
|
-
const node = this.
|
|
2028
|
-
if (!node) {
|
|
2029
|
-
// Node doesn't exist - resolve immediately
|
|
2030
|
-
return;
|
|
2031
|
-
}
|
|
2032
|
-
const ctx = this.runContextManager.createRunContext(startNodeId, options);
|
|
2033
|
-
// Create promise that resolves when context finishes
|
|
2034
|
-
const promise = new Promise((resolve) => {
|
|
2035
|
-
ctx.resolve = resolve;
|
|
2036
|
-
});
|
|
2037
|
-
// Temporarily unpause if needed
|
|
2038
|
-
const wasPaused = this.paused;
|
|
2039
|
-
if (wasPaused) {
|
|
2040
|
-
this.paused = false;
|
|
2041
|
-
}
|
|
2042
|
-
try {
|
|
2043
|
-
// Seed the start node with this run-context
|
|
2044
|
-
node.activeRunContexts.add(ctx.id);
|
|
2045
|
-
this.scheduleInputsChangedWithRunContexts(startNodeId, new Set([ctx.id]));
|
|
2046
|
-
await promise;
|
|
2047
|
-
}
|
|
2048
|
-
finally {
|
|
2049
|
-
// Restore pause state if it was paused and no other run-contexts are active
|
|
2050
|
-
if (wasPaused && this.runContextManager.getAllRunContexts().size === 0) {
|
|
2051
|
-
this.paused = true;
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
2054
|
-
}
|
|
2055
|
-
/**
|
|
2056
|
-
* Schedule a node with run-context IDs attached
|
|
2057
|
-
*/
|
|
2058
|
-
scheduleInputsChangedWithRunContexts(nodeId, runContextIds) {
|
|
2059
|
-
const node = this.graphStructure.getNode(nodeId);
|
|
2226
|
+
const node = this.graph.getNode(startNodeId);
|
|
2060
2227
|
if (!node)
|
|
2061
2228
|
return;
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
node.
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
}
|
|
2068
|
-
pause() {
|
|
2069
|
-
this.paused = true;
|
|
2070
|
-
}
|
|
2071
|
-
isPaused() {
|
|
2072
|
-
return this.paused;
|
|
2229
|
+
return new Promise((resolve) => {
|
|
2230
|
+
const id = this.runContextManager.createRunContext(startNodeId, resolve, options);
|
|
2231
|
+
node.activeRunContextIds.add(id);
|
|
2232
|
+
this.execute(startNodeId, new Set([id]));
|
|
2233
|
+
});
|
|
2073
2234
|
}
|
|
2074
|
-
|
|
2075
|
-
this.
|
|
2235
|
+
setRunMode(runMode) {
|
|
2236
|
+
this.runMode = runMode;
|
|
2076
2237
|
}
|
|
2077
|
-
|
|
2078
|
-
this.
|
|
2238
|
+
getRunMode() {
|
|
2239
|
+
return this.runMode;
|
|
2079
2240
|
}
|
|
2080
|
-
|
|
2081
|
-
this.
|
|
2241
|
+
requestPause() {
|
|
2242
|
+
this.pauseRefCount++;
|
|
2243
|
+
let released = false;
|
|
2244
|
+
return () => {
|
|
2245
|
+
if (released)
|
|
2246
|
+
return;
|
|
2247
|
+
released = true;
|
|
2248
|
+
this.pauseRefCount--;
|
|
2249
|
+
};
|
|
2082
2250
|
}
|
|
2083
|
-
|
|
2084
|
-
this.
|
|
2251
|
+
isPaused() {
|
|
2252
|
+
return this.pauseRefCount > 0;
|
|
2085
2253
|
}
|
|
2086
2254
|
copyOutputs(fromNodeId, toNodeId, options) {
|
|
2087
|
-
// Get outputs from source node
|
|
2088
2255
|
const fromNode = this.getNodeData(fromNodeId);
|
|
2089
2256
|
if (!fromNode?.outputs)
|
|
2090
2257
|
return;
|
|
2091
|
-
|
|
2092
|
-
// hydrate already pauses internally, so we don't need to handle dry option here
|
|
2093
|
-
// reemit: !options?.dry means don't propagate downstream if dry mode
|
|
2094
|
-
this.hydrate({ outputs: { [toNodeId]: { ...fromNode.outputs } } }, { reemit: !options?.dry });
|
|
2258
|
+
this.hydrate({ outputs: { [toNodeId]: { ...fromNode.outputs } } }, { invalidate: !options?.dry });
|
|
2095
2259
|
}
|
|
2096
|
-
// Hydrate inputs/outputs without triggering computation; optionally re-emit outputs downstream
|
|
2097
2260
|
hydrate(payload, opts) {
|
|
2098
|
-
const
|
|
2099
|
-
this.paused = true;
|
|
2261
|
+
const releasePause = this.requestPause();
|
|
2100
2262
|
try {
|
|
2101
2263
|
const ins = payload?.inputs || {};
|
|
2102
2264
|
for (const [nodeId, map] of Object.entries(ins)) {
|
|
2103
|
-
const node = this.
|
|
2265
|
+
const node = this.graph.getNode(nodeId);
|
|
2104
2266
|
if (!node)
|
|
2105
2267
|
continue;
|
|
2106
2268
|
for (const [h, v] of Object.entries(map || {})) {
|
|
2107
2269
|
node.inputs[h] = structuredClone(v);
|
|
2108
|
-
// emit input value event
|
|
2109
2270
|
this.eventEmitter.emit("value", {
|
|
2110
2271
|
nodeId,
|
|
2111
2272
|
handle: h,
|
|
@@ -2117,12 +2278,11 @@ class GraphRuntime {
|
|
|
2117
2278
|
}
|
|
2118
2279
|
const outs = payload?.outputs || {};
|
|
2119
2280
|
for (const [nodeId, map] of Object.entries(outs)) {
|
|
2120
|
-
const node = this.
|
|
2281
|
+
const node = this.graph.getNode(nodeId);
|
|
2121
2282
|
if (!node)
|
|
2122
2283
|
continue;
|
|
2123
2284
|
for (const [h, v] of Object.entries(map || {})) {
|
|
2124
2285
|
node.outputs[h] = structuredClone(v);
|
|
2125
|
-
// emit output value event
|
|
2126
2286
|
this.eventEmitter.emit("value", {
|
|
2127
2287
|
nodeId,
|
|
2128
2288
|
handle: h,
|
|
@@ -2132,238 +2292,245 @@ class GraphRuntime {
|
|
|
2132
2292
|
});
|
|
2133
2293
|
}
|
|
2134
2294
|
}
|
|
2135
|
-
if (opts?.
|
|
2136
|
-
for (const nodeId of this.
|
|
2137
|
-
this.
|
|
2295
|
+
if (opts?.invalidate) {
|
|
2296
|
+
for (const nodeId of this.graph.getNodes().keys()) {
|
|
2297
|
+
this.invalidateDownstream(nodeId);
|
|
2138
2298
|
}
|
|
2139
2299
|
}
|
|
2140
2300
|
}
|
|
2141
2301
|
finally {
|
|
2142
|
-
|
|
2302
|
+
releasePause();
|
|
2143
2303
|
}
|
|
2144
2304
|
}
|
|
2145
|
-
// Incrementally update nodes/edges to match new definition without full rebuild
|
|
2146
2305
|
update(def, registry) {
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
}
|
|
2306
|
+
{
|
|
2307
|
+
// Delete nodes that are no longer in the definition
|
|
2308
|
+
const afterIds = new Set(def.nodes.map((n) => n.nodeId));
|
|
2309
|
+
const beforeIds = new Set(this.graph.getNodes().keys());
|
|
2310
|
+
for (const nodeId of Array.from(beforeIds)) {
|
|
2311
|
+
if (!afterIds.has(nodeId)) {
|
|
2312
|
+
const node = this.graph.getNode(nodeId);
|
|
2313
|
+
this.nodeExecutor.cancelNodeActiveRuns(node, "node-deleted");
|
|
2314
|
+
this.runContextManager.cancelNodeInRunContexts(nodeId, true);
|
|
2315
|
+
node.runtime.onDeactivated?.();
|
|
2316
|
+
node.runtime.dispose?.();
|
|
2317
|
+
node.lifecycle?.dispose?.({
|
|
2318
|
+
state: node.state,
|
|
2319
|
+
setState: (next) => Object.assign(node.state, next),
|
|
2320
|
+
});
|
|
2321
|
+
this.graph.deleteNode(nodeId);
|
|
2322
|
+
this.edgePropagator.clearArrayBuckets(nodeId);
|
|
2165
2323
|
}
|
|
2166
|
-
// Cleanup node resources
|
|
2167
|
-
node.runtime.onDeactivated?.();
|
|
2168
|
-
node.runtime.dispose?.();
|
|
2169
|
-
node.lifecycle?.dispose?.({
|
|
2170
|
-
state: node.state,
|
|
2171
|
-
setState: (next) => Object.assign(node.state, next),
|
|
2172
|
-
});
|
|
2173
|
-
this.graphStructure.deleteNode(nodeId);
|
|
2174
|
-
this.valuePropagator.clearArrayBuckets(nodeId);
|
|
2175
2324
|
}
|
|
2176
2325
|
}
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
const
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
};
|
|
2220
|
-
this.graphStructure.setNode(n.nodeId, rn);
|
|
2221
|
-
// Activate new node
|
|
2222
|
-
const effectiveInputs = this.executionScheduler.getEffectiveInputs(rn.nodeId);
|
|
2223
|
-
const ctrl = new AbortController();
|
|
2224
|
-
const ctx = this.executionScheduler.createExecutionContext(rn.nodeId, rn, effectiveInputs, `${rn.nodeId}:init`, ctrl.signal);
|
|
2225
|
-
if (rn.lifecycle?.prepare) {
|
|
2226
|
-
ctx.log("debug", "prepare-start");
|
|
2227
|
-
rn.lifecycle.prepare(rn.params ?? {}, ctx);
|
|
2228
|
-
ctx.log("debug", "prepare-done");
|
|
2229
|
-
}
|
|
2230
|
-
rn.runtime.onActivated?.();
|
|
2231
|
-
}
|
|
2232
|
-
else {
|
|
2233
|
-
// update params/policy
|
|
2234
|
-
existing.params = n.params;
|
|
2235
|
-
if (!existing.stats) {
|
|
2236
|
-
existing.stats = {
|
|
2237
|
-
runs: 0,
|
|
2238
|
-
active: 0,
|
|
2239
|
-
queued: 0,
|
|
2240
|
-
progress: 0,
|
|
2326
|
+
{
|
|
2327
|
+
// Add or update nodes that are in the definition
|
|
2328
|
+
for (const n of def.nodes) {
|
|
2329
|
+
const existing = this.graph.getNode(n.nodeId);
|
|
2330
|
+
if (!existing) {
|
|
2331
|
+
const desc = registry.nodes.get(n.typeId);
|
|
2332
|
+
if (!desc)
|
|
2333
|
+
throw new Error(`Unknown node type: ${n.typeId}`);
|
|
2334
|
+
const cat = registry.categories.get(desc.categoryId);
|
|
2335
|
+
if (!cat)
|
|
2336
|
+
throw new Error(`Unknown category: ${desc.categoryId}`);
|
|
2337
|
+
if (cat.validateImpl)
|
|
2338
|
+
cat.validateImpl(desc.impl);
|
|
2339
|
+
const runtime = cat.createRuntime({
|
|
2340
|
+
nodeId: n.nodeId,
|
|
2341
|
+
impl: desc.impl,
|
|
2342
|
+
});
|
|
2343
|
+
const newNode = {
|
|
2344
|
+
typeId: n.typeId,
|
|
2345
|
+
nodeId: n.nodeId,
|
|
2346
|
+
lifecycle: desc.lifecycle,
|
|
2347
|
+
inputs: {},
|
|
2348
|
+
outputs: {},
|
|
2349
|
+
state: {},
|
|
2350
|
+
runtime,
|
|
2351
|
+
params: n.params,
|
|
2352
|
+
policy: {
|
|
2353
|
+
...cat.policy,
|
|
2354
|
+
...desc.policy,
|
|
2355
|
+
...n.params?.policy,
|
|
2356
|
+
},
|
|
2357
|
+
runSeq: 0,
|
|
2358
|
+
activeControllers: new Set(),
|
|
2359
|
+
controllerRunIds: new Map(),
|
|
2360
|
+
queue: [],
|
|
2361
|
+
stats: {
|
|
2362
|
+
runs: 0,
|
|
2363
|
+
active: 0,
|
|
2364
|
+
queued: 0,
|
|
2365
|
+
progress: 0,
|
|
2366
|
+
},
|
|
2367
|
+
activeRunContextIds: new Set(),
|
|
2241
2368
|
};
|
|
2369
|
+
this.graph.setNode(n.nodeId, newNode);
|
|
2370
|
+
const effectiveInputs = this.nodeExecutor.getEffectiveInputs(newNode.nodeId);
|
|
2371
|
+
const ctrl = new AbortController();
|
|
2372
|
+
const ctx = this.nodeExecutor.createExecutionContext(newNode.nodeId, newNode, effectiveInputs, `${newNode.nodeId}:init`, ctrl.signal);
|
|
2373
|
+
if (newNode.lifecycle?.prepare) {
|
|
2374
|
+
ctx.log("debug", "prepare-start");
|
|
2375
|
+
newNode.lifecycle.prepare(newNode.params ?? {}, ctx);
|
|
2376
|
+
ctx.log("debug", "prepare-done");
|
|
2377
|
+
}
|
|
2378
|
+
newNode.runtime.onActivated?.();
|
|
2379
|
+
}
|
|
2380
|
+
else {
|
|
2381
|
+
existing.params = n.params;
|
|
2382
|
+
if (!existing.stats) {
|
|
2383
|
+
existing.stats = {
|
|
2384
|
+
runs: 0,
|
|
2385
|
+
active: 0,
|
|
2386
|
+
queued: 0,
|
|
2387
|
+
progress: 0,
|
|
2388
|
+
};
|
|
2389
|
+
}
|
|
2242
2390
|
}
|
|
2243
2391
|
}
|
|
2244
2392
|
}
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
}
|
|
2253
|
-
// Capture previous per-handle target sets before rebuilding edges
|
|
2254
|
-
const prevOutTargets = new Map();
|
|
2255
|
-
for (const e of edges) {
|
|
2256
|
-
const tmap = prevOutTargets.get(e.source.nodeId) ?? new Map();
|
|
2257
|
-
const tset = tmap.get(e.source.handle) ?? new Set();
|
|
2258
|
-
tset.add(`${e.target.nodeId}.${e.target.handle}`);
|
|
2259
|
-
tmap.set(e.source.handle, tset);
|
|
2260
|
-
prevOutTargets.set(e.source.nodeId, tmap);
|
|
2261
|
-
}
|
|
2262
|
-
// Precompute per-node resolved handles for updated graph (include dynamic)
|
|
2263
|
-
const resolved = GraphStructure.computeResolvedHandleMap(def, registry, this.environment);
|
|
2264
|
-
// Check which handles changed and emit events for those
|
|
2265
|
-
const changedHandles = {};
|
|
2266
|
-
for (const [nodeId, newHandles] of resolved.map) {
|
|
2267
|
-
const oldHandles = this.graphStructure.getResolvedHandles(nodeId);
|
|
2268
|
-
if (!oldHandles ||
|
|
2269
|
-
JSON.stringify(oldHandles) !== JSON.stringify(newHandles)) {
|
|
2270
|
-
changedHandles[nodeId] = newHandles;
|
|
2393
|
+
{
|
|
2394
|
+
const beforeEdges = this.graph.getEdges();
|
|
2395
|
+
const beforeInbound = new Map();
|
|
2396
|
+
for (const e of beforeEdges) {
|
|
2397
|
+
const set = beforeInbound.get(e.target.nodeId) ?? new Set();
|
|
2398
|
+
set.add(e.target.handle);
|
|
2399
|
+
beforeInbound.set(e.target.nodeId, set);
|
|
2271
2400
|
}
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
const currSet = nextInbound.get(nodeId) ?? new Set();
|
|
2291
|
-
const node = this.graphStructure.getNode(nodeId);
|
|
2292
|
-
if (!node)
|
|
2293
|
-
continue;
|
|
2294
|
-
let changed = false;
|
|
2295
|
-
for (const handle of Array.from(prevSet)) {
|
|
2296
|
-
if (!currSet.has(handle)) {
|
|
2297
|
-
if (handle in node.inputs) {
|
|
2298
|
-
delete node.inputs[handle];
|
|
2299
|
-
changed = true;
|
|
2401
|
+
const beforeOutTargets = new Map();
|
|
2402
|
+
for (const e of beforeEdges) {
|
|
2403
|
+
const tmap = beforeOutTargets.get(e.source.nodeId) ??
|
|
2404
|
+
new Map();
|
|
2405
|
+
const tset = tmap.get(e.source.handle) ?? new Set();
|
|
2406
|
+
tset.add(`${e.target.nodeId}.${e.target.handle}`);
|
|
2407
|
+
tmap.set(e.source.handle, tset);
|
|
2408
|
+
beforeOutTargets.set(e.source.nodeId, tmap);
|
|
2409
|
+
}
|
|
2410
|
+
{
|
|
2411
|
+
// Update handles and edges
|
|
2412
|
+
const result = tryHandleResolving(def, registry, this.environment);
|
|
2413
|
+
const changedHandles = {};
|
|
2414
|
+
for (const [nodeId, newHandles] of result.resolved) {
|
|
2415
|
+
const oldHandles = this.graph.getResolvedHandles(nodeId);
|
|
2416
|
+
if (!oldHandles ||
|
|
2417
|
+
JSON.stringify(oldHandles) !== JSON.stringify(newHandles)) {
|
|
2418
|
+
changedHandles[nodeId] = newHandles;
|
|
2300
2419
|
}
|
|
2301
2420
|
}
|
|
2421
|
+
for (const [nodeId, handles] of result.resolved) {
|
|
2422
|
+
this.graph.setResolvedHandles(nodeId, handles);
|
|
2423
|
+
}
|
|
2424
|
+
const afterEdges = buildEdges(def, registry, this.graph.getResolvedHandlesMap());
|
|
2425
|
+
this.graph.setEdges(afterEdges);
|
|
2426
|
+
for (const nodeId of result.pending) {
|
|
2427
|
+
this.handleResolver.scheduleRecomputeHandles(nodeId);
|
|
2428
|
+
}
|
|
2429
|
+
if (Object.keys(changedHandles).length > 0) {
|
|
2430
|
+
this.eventEmitter.emit("invalidate", {
|
|
2431
|
+
reason: "graph-updated",
|
|
2432
|
+
resolvedHandles: changedHandles,
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2302
2435
|
}
|
|
2303
|
-
|
|
2304
|
-
//
|
|
2305
|
-
|
|
2306
|
-
this.
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
if (
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2436
|
+
{
|
|
2437
|
+
// Update inputs and propagate changes
|
|
2438
|
+
const afterInbound = new Map();
|
|
2439
|
+
const afterEdges = this.graph.getEdges();
|
|
2440
|
+
for (const e of afterEdges) {
|
|
2441
|
+
const set = afterInbound.get(e.target.nodeId) ?? new Set();
|
|
2442
|
+
set.add(e.target.handle);
|
|
2443
|
+
afterInbound.set(e.target.nodeId, set);
|
|
2444
|
+
}
|
|
2445
|
+
// Propagate changes on edges removed
|
|
2446
|
+
for (const [nodeId, beforeSet] of beforeInbound) {
|
|
2447
|
+
const currSet = afterInbound.get(nodeId) ?? new Set();
|
|
2448
|
+
const node = this.graph.getNode(nodeId);
|
|
2449
|
+
if (!node)
|
|
2450
|
+
continue;
|
|
2451
|
+
let changed = false;
|
|
2452
|
+
for (const handle of Array.from(beforeSet)) {
|
|
2453
|
+
if (!currSet.has(handle)) {
|
|
2454
|
+
if (handle in node.inputs) {
|
|
2455
|
+
delete node.inputs[handle];
|
|
2456
|
+
changed = true;
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
if (changed) {
|
|
2461
|
+
this.edgePropagator.clearArrayBuckets(nodeId);
|
|
2462
|
+
if (this.runMode === "auto" &&
|
|
2463
|
+
this.graph.allInboundHaveValue(nodeId)) {
|
|
2464
|
+
this.execute(nodeId);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
// Propagate changes on edges added
|
|
2469
|
+
const afterOutTargets = new Map();
|
|
2470
|
+
for (const e of afterEdges) {
|
|
2471
|
+
const targetMap = afterOutTargets.get(e.source.nodeId) ??
|
|
2472
|
+
new Map();
|
|
2473
|
+
const targetSet = targetMap.get(e.source.handle) ?? new Set();
|
|
2474
|
+
targetSet.add(`${e.target.nodeId}.${e.target.handle}`);
|
|
2475
|
+
targetMap.set(e.source.handle, targetSet);
|
|
2476
|
+
afterOutTargets.set(e.source.nodeId, targetMap);
|
|
2477
|
+
}
|
|
2478
|
+
const setsEqual = (a, b) => {
|
|
2479
|
+
if (!a && !b)
|
|
2480
|
+
return true;
|
|
2481
|
+
if (!a || !b)
|
|
2482
|
+
return false;
|
|
2483
|
+
if (a.size !== b.size)
|
|
2484
|
+
return false;
|
|
2485
|
+
for (const v of a)
|
|
2486
|
+
if (!b.has(v))
|
|
2487
|
+
return false;
|
|
2488
|
+
return true;
|
|
2489
|
+
};
|
|
2490
|
+
const nodesToCheck = new Set([
|
|
2491
|
+
...Array.from(beforeOutTargets.keys()),
|
|
2492
|
+
...Array.from(afterOutTargets.keys()),
|
|
2493
|
+
]);
|
|
2494
|
+
for (const nodeId of nodesToCheck) {
|
|
2495
|
+
const beforeMap = beforeOutTargets.get(nodeId) ?? new Map();
|
|
2496
|
+
const afterMap = afterOutTargets.get(nodeId) ?? new Map();
|
|
2497
|
+
const handles = new Set([
|
|
2498
|
+
...Array.from(beforeMap.keys()),
|
|
2499
|
+
...Array.from(afterMap.keys()),
|
|
2500
|
+
]);
|
|
2501
|
+
for (const handle of handles) {
|
|
2502
|
+
const beforeTargetSet = beforeMap.get(handle) ?? new Set();
|
|
2503
|
+
const afterTargetSet = afterMap.get(handle) ?? new Set();
|
|
2504
|
+
if (!setsEqual(beforeTargetSet, afterTargetSet)) {
|
|
2505
|
+
const val = this.getOutput(nodeId, handle);
|
|
2506
|
+
if (val !== undefined)
|
|
2507
|
+
this.propagate(nodeId, handle, val);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2350
2510
|
}
|
|
2351
2511
|
}
|
|
2352
2512
|
}
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2513
|
+
}
|
|
2514
|
+
dispose() {
|
|
2515
|
+
this.runContextManager.resolveAll();
|
|
2516
|
+
for (const node of this.graph.getNodes().values()) {
|
|
2517
|
+
node.runtime.onDeactivated?.();
|
|
2518
|
+
node.runtime.dispose?.();
|
|
2519
|
+
node.lifecycle?.dispose?.({
|
|
2520
|
+
state: node.state,
|
|
2521
|
+
setState: (next) => Object.assign(node.state, next),
|
|
2362
2522
|
});
|
|
2363
2523
|
}
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2524
|
+
this.graph.clear();
|
|
2525
|
+
}
|
|
2526
|
+
execute(nodeId, runContextIds) {
|
|
2527
|
+
this.nodeExecutor.execute(nodeId, runContextIds);
|
|
2528
|
+
}
|
|
2529
|
+
propagate(srcNodeId, srcHandle, value, runContextIds) {
|
|
2530
|
+
this.edgePropagator.propagate(srcNodeId, srcHandle, value, runContextIds);
|
|
2531
|
+
}
|
|
2532
|
+
invalidateDownstream(nodeId) {
|
|
2533
|
+
this.edgePropagator.invalidateDownstream(nodeId);
|
|
2367
2534
|
}
|
|
2368
2535
|
}
|
|
2369
2536
|
|
|
@@ -2585,21 +2752,25 @@ class GraphBuilder {
|
|
|
2585
2752
|
}
|
|
2586
2753
|
}
|
|
2587
2754
|
|
|
2588
|
-
|
|
2589
|
-
|
|
2755
|
+
/**
|
|
2756
|
+
* Unified Engine implementation that handles both manual and auto run modes.
|
|
2757
|
+
* - Manual mode: Nodes execute only when explicitly called via computeNode/runFromHere (unless paused)
|
|
2758
|
+
* - Auto mode: Nodes automatically execute when inputs change (unless paused)
|
|
2759
|
+
*/
|
|
2760
|
+
class LocalEngine {
|
|
2761
|
+
constructor(graphRuntime, runMode) {
|
|
2590
2762
|
this.graphRuntime = graphRuntime;
|
|
2763
|
+
this.setRunMode(runMode ?? "manual");
|
|
2591
2764
|
}
|
|
2592
2765
|
setInputs(nodeId, inputs, options) {
|
|
2593
2766
|
if (options?.dry) {
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
this.graphRuntime.pause();
|
|
2767
|
+
// Use requestPause to temporarily pause without affecting base run mode
|
|
2768
|
+
const releasePause = this.graphRuntime.requestPause();
|
|
2597
2769
|
try {
|
|
2598
2770
|
this.graphRuntime.setInputs(nodeId, inputs);
|
|
2599
2771
|
}
|
|
2600
2772
|
finally {
|
|
2601
|
-
|
|
2602
|
-
this.graphRuntime.resume();
|
|
2773
|
+
releasePause();
|
|
2603
2774
|
}
|
|
2604
2775
|
}
|
|
2605
2776
|
else {
|
|
@@ -2608,15 +2779,13 @@ class AbstractEngine {
|
|
|
2608
2779
|
}
|
|
2609
2780
|
triggerExternal(nodeId, event, options) {
|
|
2610
2781
|
if (options?.dry) {
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
this.graphRuntime.pause();
|
|
2782
|
+
// Use requestPause to temporarily pause without affecting base run mode
|
|
2783
|
+
const releasePause = this.graphRuntime.requestPause();
|
|
2614
2784
|
try {
|
|
2615
2785
|
this.graphRuntime.triggerExternal(nodeId, event);
|
|
2616
2786
|
}
|
|
2617
2787
|
finally {
|
|
2618
|
-
|
|
2619
|
-
this.graphRuntime.resume();
|
|
2788
|
+
releasePause();
|
|
2620
2789
|
}
|
|
2621
2790
|
}
|
|
2622
2791
|
else {
|
|
@@ -2641,19 +2810,6 @@ class AbstractEngine {
|
|
|
2641
2810
|
dispose() {
|
|
2642
2811
|
// this.graphRuntime.dispose();
|
|
2643
2812
|
}
|
|
2644
|
-
}
|
|
2645
|
-
|
|
2646
|
-
/**
|
|
2647
|
-
* Unified Engine implementation that handles both manual and auto run modes.
|
|
2648
|
-
* - Manual mode: Runtime is paused, nodes execute only when explicitly called via computeNode/runFromHere
|
|
2649
|
-
* - Auto mode: Runtime is resumed, nodes automatically execute when inputs change
|
|
2650
|
-
*/
|
|
2651
|
-
class UnifiedEngine extends AbstractEngine {
|
|
2652
|
-
constructor(graphRuntime, runMode) {
|
|
2653
|
-
super(graphRuntime);
|
|
2654
|
-
this.runMode = "manual";
|
|
2655
|
-
this.setRunMode(runMode ?? "manual");
|
|
2656
|
-
}
|
|
2657
2813
|
launch(invalidate, runMode) {
|
|
2658
2814
|
if (runMode)
|
|
2659
2815
|
this.setRunMode(runMode);
|
|
@@ -2680,20 +2836,8 @@ class UnifiedEngine extends AbstractEngine {
|
|
|
2680
2836
|
async runFromHere(nodeId) {
|
|
2681
2837
|
await this.graphRuntime.runFromHereContext(nodeId);
|
|
2682
2838
|
}
|
|
2683
|
-
getRunMode() {
|
|
2684
|
-
return this.runMode;
|
|
2685
|
-
}
|
|
2686
2839
|
setRunMode(runMode) {
|
|
2687
|
-
|
|
2688
|
-
return;
|
|
2689
|
-
this.runMode = runMode;
|
|
2690
|
-
// Update runtime pause/resume state based on new mode
|
|
2691
|
-
if (runMode === "manual") {
|
|
2692
|
-
this.graphRuntime.pause();
|
|
2693
|
-
}
|
|
2694
|
-
else {
|
|
2695
|
-
this.graphRuntime.resume();
|
|
2696
|
-
}
|
|
2840
|
+
this.graphRuntime.setRunMode(runMode);
|
|
2697
2841
|
}
|
|
2698
2842
|
}
|
|
2699
2843
|
|
|
@@ -4240,6 +4384,58 @@ function setValueAtPath(obj, pathSegments, newValue) {
|
|
|
4240
4384
|
}
|
|
4241
4385
|
return true;
|
|
4242
4386
|
}
|
|
4387
|
+
/**
|
|
4388
|
+
* Sets a value at a path, creating intermediate objects as needed.
|
|
4389
|
+
* Mutates the root object in place.
|
|
4390
|
+
* @param root - The root object to modify (must be an object, will be initialized if needed)
|
|
4391
|
+
* @param pathSegments - The path segments to traverse
|
|
4392
|
+
* @param value - The value to set, or null to delete the path
|
|
4393
|
+
* @throws Error if path cannot be created (e.g., array indices not supported, invalid parent types)
|
|
4394
|
+
*/
|
|
4395
|
+
function setValueAtPathWithCreation(root, pathSegments, value) {
|
|
4396
|
+
if (value === null) {
|
|
4397
|
+
const result = getValueAtPath(root, pathSegments);
|
|
4398
|
+
if (result && result.parent !== null && !Array.isArray(result.parent)) {
|
|
4399
|
+
delete result.parent[result.key];
|
|
4400
|
+
}
|
|
4401
|
+
return;
|
|
4402
|
+
}
|
|
4403
|
+
if (!root || typeof root !== "object" || Array.isArray(root)) {
|
|
4404
|
+
throw new Error("Root must be an object");
|
|
4405
|
+
}
|
|
4406
|
+
let current = root;
|
|
4407
|
+
for (let i = 0; i < pathSegments.length - 1; i++) {
|
|
4408
|
+
const segment = pathSegments[i];
|
|
4409
|
+
if (typeof segment === "string") {
|
|
4410
|
+
if (!current ||
|
|
4411
|
+
typeof current !== "object" ||
|
|
4412
|
+
Array.isArray(current) ||
|
|
4413
|
+
!(segment in current) ||
|
|
4414
|
+
typeof current[segment] !== "object" ||
|
|
4415
|
+
current[segment] === null ||
|
|
4416
|
+
Array.isArray(current[segment])) {
|
|
4417
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
4418
|
+
throw new Error(`Cannot create path: parent at segment ${i} is not an object`);
|
|
4419
|
+
}
|
|
4420
|
+
current[segment] = {};
|
|
4421
|
+
}
|
|
4422
|
+
current = current[segment];
|
|
4423
|
+
}
|
|
4424
|
+
else {
|
|
4425
|
+
throw new Error("Array indices not supported in extData paths");
|
|
4426
|
+
}
|
|
4427
|
+
}
|
|
4428
|
+
const lastSegment = pathSegments[pathSegments.length - 1];
|
|
4429
|
+
if (typeof lastSegment === "string") {
|
|
4430
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
4431
|
+
throw new Error(`Cannot set value: parent at final segment is not an object`);
|
|
4432
|
+
}
|
|
4433
|
+
current[lastSegment] = value;
|
|
4434
|
+
}
|
|
4435
|
+
else {
|
|
4436
|
+
throw new Error("Array indices not supported in extData paths");
|
|
4437
|
+
}
|
|
4438
|
+
}
|
|
4243
4439
|
function findMatchingPaths(obj, pathSegments, currentPath = []) {
|
|
4244
4440
|
if (pathSegments.length === 0) {
|
|
4245
4441
|
return [{ path: currentPath, value: obj }];
|
|
@@ -4781,8 +4977,8 @@ exports.CompositeCategory = CompositeCategory;
|
|
|
4781
4977
|
exports.ComputeCategory = ComputeCategory;
|
|
4782
4978
|
exports.GraphBuilder = GraphBuilder;
|
|
4783
4979
|
exports.GraphRuntime = GraphRuntime;
|
|
4980
|
+
exports.LocalEngine = LocalEngine;
|
|
4784
4981
|
exports.Registry = Registry;
|
|
4785
|
-
exports.UnifiedEngine = UnifiedEngine;
|
|
4786
4982
|
exports.buildValueConverter = buildValueConverter;
|
|
4787
4983
|
exports.computeGraphCenter = computeGraphCenter;
|
|
4788
4984
|
exports.createAsyncGraphDef = createAsyncGraphDef;
|
|
@@ -4811,5 +5007,6 @@ exports.parseJsonPath = parseJsonPath;
|
|
|
4811
5007
|
exports.registerDelayNode = registerDelayNode;
|
|
4812
5008
|
exports.registerProgressNodes = registerProgressNodes;
|
|
4813
5009
|
exports.setValueAtPath = setValueAtPath;
|
|
5010
|
+
exports.setValueAtPathWithCreation = setValueAtPathWithCreation;
|
|
4814
5011
|
exports.typed = typed;
|
|
4815
5012
|
//# sourceMappingURL=index.cjs.map
|