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