@bian-womp/spark-graph 0.2.93 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cjs/index.cjs +1462 -994
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/core/order.d.ts +7 -0
- package/lib/cjs/src/core/order.d.ts.map +1 -0
- package/lib/cjs/src/examples/async.d.ts.map +1 -1
- package/lib/cjs/src/examples/progress.d.ts.map +1 -1
- package/lib/cjs/src/examples/runMode.d.ts +2 -0
- package/lib/cjs/src/examples/runMode.d.ts.map +1 -0
- package/lib/cjs/src/examples/shared.d.ts.map +1 -1
- package/lib/cjs/src/examples/simple.d.ts.map +1 -1
- package/lib/cjs/src/examples/snapshot.d.ts.map +1 -1
- package/lib/cjs/src/examples/validation.d.ts.map +1 -1
- package/lib/cjs/src/index.d.ts +4 -7
- package/lib/cjs/src/index.d.ts.map +1 -1
- package/lib/cjs/src/misc/base.d.ts +41 -0
- package/lib/cjs/src/misc/base.d.ts.map +1 -1
- package/lib/cjs/src/runtime/AbstractEngine.d.ts +18 -4
- package/lib/cjs/src/runtime/AbstractEngine.d.ts.map +1 -1
- package/lib/cjs/src/runtime/Engine.d.ts +17 -3
- package/lib/cjs/src/runtime/Engine.d.ts.map +1 -1
- package/lib/cjs/src/runtime/GraphLifecycleApi.d.ts +37 -0
- package/lib/cjs/src/runtime/GraphLifecycleApi.d.ts.map +1 -0
- package/lib/cjs/src/runtime/GraphRuntime.d.ts +29 -29
- package/lib/cjs/src/runtime/GraphRuntime.d.ts.map +1 -1
- package/lib/cjs/src/runtime/UnifiedEngine.d.ts +32 -0
- package/lib/cjs/src/runtime/UnifiedEngine.d.ts.map +1 -0
- package/lib/cjs/src/runtime/components/EventEmitter.d.ts +12 -0
- package/lib/cjs/src/runtime/components/EventEmitter.d.ts.map +1 -0
- package/lib/cjs/src/runtime/components/ExecutionScheduler.d.ts +56 -0
- package/lib/cjs/src/runtime/components/ExecutionScheduler.d.ts.map +1 -0
- package/lib/cjs/src/runtime/components/GraphStructure.d.ts +36 -0
- package/lib/cjs/src/runtime/components/GraphStructure.d.ts.map +1 -0
- package/lib/cjs/src/runtime/components/HandleResolver.d.ts +27 -0
- package/lib/cjs/src/runtime/components/HandleResolver.d.ts.map +1 -0
- package/lib/cjs/src/runtime/components/RunContextManager.d.ts +55 -0
- package/lib/cjs/src/runtime/components/RunContextManager.d.ts.map +1 -0
- package/lib/cjs/src/runtime/components/ValuePropagator.d.ts +46 -0
- package/lib/cjs/src/runtime/components/ValuePropagator.d.ts.map +1 -0
- package/lib/cjs/src/runtime/components/interfaces.d.ts +34 -0
- package/lib/cjs/src/runtime/components/interfaces.d.ts.map +1 -0
- package/lib/cjs/src/runtime/components/types.d.ts +43 -0
- package/lib/cjs/src/runtime/components/types.d.ts.map +1 -0
- package/lib/cjs/src/runtime/utils.d.ts +16 -0
- package/lib/cjs/src/runtime/utils.d.ts.map +1 -0
- package/lib/esm/index.js +1459 -989
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/core/order.d.ts +7 -0
- package/lib/esm/src/core/order.d.ts.map +1 -0
- package/lib/esm/src/examples/async.d.ts.map +1 -1
- package/lib/esm/src/examples/progress.d.ts.map +1 -1
- package/lib/esm/src/examples/runMode.d.ts +2 -0
- package/lib/esm/src/examples/runMode.d.ts.map +1 -0
- package/lib/esm/src/examples/shared.d.ts.map +1 -1
- package/lib/esm/src/examples/simple.d.ts.map +1 -1
- package/lib/esm/src/examples/snapshot.d.ts.map +1 -1
- package/lib/esm/src/examples/validation.d.ts.map +1 -1
- package/lib/esm/src/index.d.ts +4 -7
- package/lib/esm/src/index.d.ts.map +1 -1
- package/lib/esm/src/misc/base.d.ts +41 -0
- package/lib/esm/src/misc/base.d.ts.map +1 -1
- package/lib/esm/src/runtime/AbstractEngine.d.ts +18 -4
- package/lib/esm/src/runtime/AbstractEngine.d.ts.map +1 -1
- package/lib/esm/src/runtime/Engine.d.ts +17 -3
- package/lib/esm/src/runtime/Engine.d.ts.map +1 -1
- package/lib/esm/src/runtime/GraphLifecycleApi.d.ts +37 -0
- package/lib/esm/src/runtime/GraphLifecycleApi.d.ts.map +1 -0
- package/lib/esm/src/runtime/GraphRuntime.d.ts +29 -29
- package/lib/esm/src/runtime/GraphRuntime.d.ts.map +1 -1
- package/lib/esm/src/runtime/UnifiedEngine.d.ts +32 -0
- package/lib/esm/src/runtime/UnifiedEngine.d.ts.map +1 -0
- package/lib/esm/src/runtime/components/EventEmitter.d.ts +12 -0
- package/lib/esm/src/runtime/components/EventEmitter.d.ts.map +1 -0
- package/lib/esm/src/runtime/components/ExecutionScheduler.d.ts +56 -0
- package/lib/esm/src/runtime/components/ExecutionScheduler.d.ts.map +1 -0
- package/lib/esm/src/runtime/components/GraphStructure.d.ts +36 -0
- package/lib/esm/src/runtime/components/GraphStructure.d.ts.map +1 -0
- package/lib/esm/src/runtime/components/HandleResolver.d.ts +27 -0
- package/lib/esm/src/runtime/components/HandleResolver.d.ts.map +1 -0
- package/lib/esm/src/runtime/components/RunContextManager.d.ts +55 -0
- package/lib/esm/src/runtime/components/RunContextManager.d.ts.map +1 -0
- package/lib/esm/src/runtime/components/ValuePropagator.d.ts +46 -0
- package/lib/esm/src/runtime/components/ValuePropagator.d.ts.map +1 -0
- package/lib/esm/src/runtime/components/interfaces.d.ts +34 -0
- package/lib/esm/src/runtime/components/interfaces.d.ts.map +1 -0
- package/lib/esm/src/runtime/components/types.d.ts +43 -0
- package/lib/esm/src/runtime/components/types.d.ts.map +1 -0
- package/lib/esm/src/runtime/utils.d.ts +16 -0
- package/lib/esm/src/runtime/utils.d.ts.map +1 -0
- package/package.json +2 -2
- package/lib/cjs/src/examples/engine.d.ts +0 -6
- package/lib/cjs/src/examples/engine.d.ts.map +0 -1
- package/lib/cjs/src/runtime/BatchedEngine.d.ts +0 -17
- package/lib/cjs/src/runtime/BatchedEngine.d.ts.map +0 -1
- package/lib/cjs/src/runtime/EngineFactory.d.ts +0 -10
- package/lib/cjs/src/runtime/EngineFactory.d.ts.map +0 -1
- package/lib/cjs/src/runtime/HybridEngine.d.ts +0 -21
- package/lib/cjs/src/runtime/HybridEngine.d.ts.map +0 -1
- package/lib/cjs/src/runtime/PullEngine.d.ts +0 -8
- package/lib/cjs/src/runtime/PullEngine.d.ts.map +0 -1
- package/lib/cjs/src/runtime/PushEngine.d.ts +0 -7
- package/lib/cjs/src/runtime/PushEngine.d.ts.map +0 -1
- package/lib/cjs/src/runtime/StepEngine.d.ts +0 -11
- package/lib/cjs/src/runtime/StepEngine.d.ts.map +0 -1
- package/lib/esm/src/examples/engine.d.ts +0 -6
- package/lib/esm/src/examples/engine.d.ts.map +0 -1
- package/lib/esm/src/runtime/BatchedEngine.d.ts +0 -17
- package/lib/esm/src/runtime/BatchedEngine.d.ts.map +0 -1
- package/lib/esm/src/runtime/EngineFactory.d.ts +0 -10
- package/lib/esm/src/runtime/EngineFactory.d.ts.map +0 -1
- package/lib/esm/src/runtime/HybridEngine.d.ts +0 -21
- package/lib/esm/src/runtime/HybridEngine.d.ts.map +0 -1
- package/lib/esm/src/runtime/PullEngine.d.ts +0 -8
- package/lib/esm/src/runtime/PullEngine.d.ts.map +0 -1
- package/lib/esm/src/runtime/PushEngine.d.ts +0 -7
- package/lib/esm/src/runtime/PushEngine.d.ts.map +0 -1
- package/lib/esm/src/runtime/StepEngine.d.ts +0 -11
- package/lib/esm/src/runtime/StepEngine.d.ts.map +0 -1
package/lib/cjs/index.cjs
CHANGED
|
@@ -404,186 +404,191 @@ class Registry {
|
|
|
404
404
|
}
|
|
405
405
|
Registry.idCounter = 0;
|
|
406
406
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
// Helper: typed promise detection and unwrapping for T | Promise<T>
|
|
407
|
+
/**
|
|
408
|
+
* Shared utility functions for runtime components
|
|
409
|
+
*/
|
|
410
|
+
/**
|
|
411
|
+
* Type guard to check if a value is a Promise
|
|
412
|
+
*/
|
|
416
413
|
function isPromise(value) {
|
|
417
414
|
return !!value && typeof value.then === "function";
|
|
418
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* Unwrap a value that might be a Promise
|
|
418
|
+
*/
|
|
419
419
|
async function unwrapMaybePromise(value) {
|
|
420
420
|
return isPromise(value) ? await value : value;
|
|
421
421
|
}
|
|
422
|
-
|
|
423
|
-
|
|
422
|
+
/**
|
|
423
|
+
* Shallow/deep-ish equality check to avoid unnecessary runs on identical values
|
|
424
|
+
*/
|
|
425
|
+
function valuesEqual(a, b) {
|
|
426
|
+
if (a === b)
|
|
427
|
+
return true;
|
|
428
|
+
if (typeof a !== typeof b)
|
|
429
|
+
return false;
|
|
430
|
+
if (a && b && typeof a === "object") {
|
|
431
|
+
try {
|
|
432
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* GraphStructure component - manages nodes, edges, and handle resolution
|
|
443
|
+
*/
|
|
444
|
+
class GraphStructure {
|
|
445
|
+
constructor(registry) {
|
|
424
446
|
this.nodes = new Map();
|
|
425
447
|
this.edges = [];
|
|
426
|
-
// Current resolved handles per node (registry statics merged with per-node overrides)
|
|
427
448
|
this.resolvedByNode = new Map();
|
|
428
|
-
this.
|
|
429
|
-
this.environment = {};
|
|
430
|
-
// Token to guard async resolveHandles recomputes per node
|
|
431
|
-
this.recomputeTokenByNode = new Map();
|
|
432
|
-
this.paused = false;
|
|
433
|
-
// For array-typed target inputs, keep per-edge contributions so successive runs
|
|
434
|
-
// from the same source replace their slice instead of accumulating forever.
|
|
435
|
-
// Structure: nodeId -> handle -> edgeId -> values[]
|
|
436
|
-
this.arrayInputBuckets = new Map();
|
|
449
|
+
this.registry = registry;
|
|
437
450
|
}
|
|
438
|
-
//
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
return true;
|
|
442
|
-
if (typeof a !== typeof b)
|
|
443
|
-
return false;
|
|
444
|
-
if (a && b && typeof a === "object") {
|
|
445
|
-
try {
|
|
446
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
447
|
-
}
|
|
448
|
-
catch {
|
|
449
|
-
return false;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
return false;
|
|
451
|
+
// Node accessors
|
|
452
|
+
getNode(nodeId) {
|
|
453
|
+
return this.nodes.get(nodeId);
|
|
453
454
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
gr.registry = registry;
|
|
457
|
-
gr.environment = opts?.environment ?? {};
|
|
458
|
-
// Precompute per-node resolved handles (use def-provided overrides; do not compute dynamically here)
|
|
459
|
-
const initial = GraphRuntime.computeResolvedHandleMap(def, registry, gr.environment);
|
|
460
|
-
gr.resolvedByNode = initial.map;
|
|
461
|
-
// Instantiate nodes
|
|
462
|
-
for (const n of def.nodes) {
|
|
463
|
-
const desc = registry.nodes.get(n.typeId);
|
|
464
|
-
if (!desc)
|
|
465
|
-
throw new Error(`Unknown node type: ${n.typeId}`);
|
|
466
|
-
const cat = registry.categories.get(desc.categoryId);
|
|
467
|
-
if (!cat)
|
|
468
|
-
throw new Error(`Unknown category: ${desc.categoryId}`);
|
|
469
|
-
if (cat.validateImpl)
|
|
470
|
-
cat.validateImpl(desc.impl);
|
|
471
|
-
const runtime = cat.createRuntime({
|
|
472
|
-
nodeId: n.nodeId,
|
|
473
|
-
impl: desc.impl,
|
|
474
|
-
});
|
|
475
|
-
const rn = {
|
|
476
|
-
typeId: n.typeId,
|
|
477
|
-
nodeId: n.nodeId,
|
|
478
|
-
lifecycle: desc.lifecycle,
|
|
479
|
-
inputs: {},
|
|
480
|
-
outputs: {},
|
|
481
|
-
state: {},
|
|
482
|
-
runtime,
|
|
483
|
-
params: n.params,
|
|
484
|
-
policy: {
|
|
485
|
-
...cat.policy,
|
|
486
|
-
...desc.policy,
|
|
487
|
-
...n.params?.policy,
|
|
488
|
-
},
|
|
489
|
-
logLevel: desc.logLevel,
|
|
490
|
-
runSeq: 0,
|
|
491
|
-
activeControllers: new Set(),
|
|
492
|
-
controllerRunIds: new Map(),
|
|
493
|
-
queue: [],
|
|
494
|
-
stats: {
|
|
495
|
-
runs: 0,
|
|
496
|
-
active: 0,
|
|
497
|
-
queued: 0,
|
|
498
|
-
progress: 0,
|
|
499
|
-
},
|
|
500
|
-
};
|
|
501
|
-
gr.nodes.set(n.nodeId, rn);
|
|
502
|
-
}
|
|
503
|
-
// Instantiate edges
|
|
504
|
-
gr.edges = GraphRuntime.buildEdges(def, registry, gr.resolvedByNode);
|
|
505
|
-
// Schedule async recompute only for nodes that indicated Promise-based resolveHandles
|
|
506
|
-
for (const nodeId of initial.pending)
|
|
507
|
-
gr.scheduleRecomputeHandles(nodeId);
|
|
508
|
-
return gr;
|
|
455
|
+
getNodes() {
|
|
456
|
+
return this.nodes;
|
|
509
457
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
this.listeners.set(event, new Set());
|
|
513
|
-
const set = this.listeners.get(event);
|
|
514
|
-
set.add(handler);
|
|
515
|
-
return () => set.delete(handler);
|
|
458
|
+
setNode(nodeId, node) {
|
|
459
|
+
this.nodes.set(nodeId, node);
|
|
516
460
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
if (set)
|
|
520
|
-
for (const h of Array.from(set))
|
|
521
|
-
h(payload);
|
|
461
|
+
deleteNode(nodeId) {
|
|
462
|
+
this.nodes.delete(nodeId);
|
|
522
463
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
464
|
+
hasNode(nodeId) {
|
|
465
|
+
return this.nodes.has(nodeId);
|
|
466
|
+
}
|
|
467
|
+
// Edge accessors
|
|
468
|
+
getEdges() {
|
|
469
|
+
return this.edges;
|
|
470
|
+
}
|
|
471
|
+
setEdges(edges) {
|
|
472
|
+
this.edges = edges;
|
|
473
|
+
}
|
|
474
|
+
// Registry accessor
|
|
475
|
+
getRegistry() {
|
|
476
|
+
return this.registry;
|
|
477
|
+
}
|
|
478
|
+
setRegistry(registry) {
|
|
479
|
+
this.registry = registry;
|
|
480
|
+
}
|
|
481
|
+
// Resolved handles accessors
|
|
482
|
+
getResolvedHandles(nodeId) {
|
|
483
|
+
return this.resolvedByNode.get(nodeId);
|
|
484
|
+
}
|
|
485
|
+
setResolvedHandles(nodeId, handles) {
|
|
486
|
+
this.resolvedByNode.set(nodeId, handles);
|
|
487
|
+
}
|
|
488
|
+
getResolvedHandlesMap() {
|
|
489
|
+
return this.resolvedByNode;
|
|
490
|
+
}
|
|
491
|
+
// Static factory methods
|
|
492
|
+
static computeResolvedHandleMap(def, registry, environment) {
|
|
493
|
+
const out = new Map();
|
|
494
|
+
const pending = new Set();
|
|
495
|
+
for (const n of def.nodes) {
|
|
496
|
+
const desc = registry.nodes.get(n.typeId);
|
|
497
|
+
if (!desc)
|
|
531
498
|
continue;
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
:
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
value,
|
|
553
|
-
message: errorMessage,
|
|
554
|
-
});
|
|
555
|
-
// Skip storing invalid value
|
|
556
|
-
continue;
|
|
499
|
+
const overrideInputs = n.resolvedHandles?.inputs;
|
|
500
|
+
const overrideOutputs = n.resolvedHandles?.outputs;
|
|
501
|
+
const overrideDefaults = n.resolvedHandles?.inputDefaults;
|
|
502
|
+
// Resolve dynamic handles if available (initial pass: inputs may be undefined)
|
|
503
|
+
let dyn = {};
|
|
504
|
+
try {
|
|
505
|
+
if (typeof desc.resolveHandles === "function") {
|
|
506
|
+
const maybe = desc.resolveHandles({
|
|
507
|
+
nodeId: n.nodeId,
|
|
508
|
+
environment: environment || {},
|
|
509
|
+
params: n.params,
|
|
510
|
+
inputs: undefined,
|
|
511
|
+
});
|
|
512
|
+
// Only use sync results here; async results are applied via recompute later
|
|
513
|
+
if (isPromise(maybe)) {
|
|
514
|
+
// mark node as pending async recompute
|
|
515
|
+
pending.add(n.nodeId);
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
dyn = maybe || {};
|
|
557
519
|
}
|
|
558
520
|
}
|
|
559
521
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
if (!same) {
|
|
563
|
-
if (value === undefined) {
|
|
564
|
-
delete node.inputs[handle];
|
|
565
|
-
}
|
|
566
|
-
else {
|
|
567
|
-
node.inputs[handle] = value;
|
|
568
|
-
}
|
|
569
|
-
// Emit value event for input updates
|
|
570
|
-
this.emit("value", { nodeId, handle, value, io: "input" });
|
|
571
|
-
anyChanged = true;
|
|
522
|
+
catch {
|
|
523
|
+
// ignore dynamic resolution errors at this stage
|
|
572
524
|
}
|
|
525
|
+
// Merge base with dynamic and overrides (allow partial resolvedHandles)
|
|
526
|
+
const inputs = {
|
|
527
|
+
...desc.inputs,
|
|
528
|
+
...dyn.inputs,
|
|
529
|
+
...overrideInputs,
|
|
530
|
+
};
|
|
531
|
+
const outputs = {
|
|
532
|
+
...desc.outputs,
|
|
533
|
+
...dyn.outputs,
|
|
534
|
+
...overrideOutputs,
|
|
535
|
+
};
|
|
536
|
+
const inputDefaults = {
|
|
537
|
+
...desc.inputDefaults,
|
|
538
|
+
...dyn.inputDefaults,
|
|
539
|
+
...overrideDefaults,
|
|
540
|
+
};
|
|
541
|
+
out.set(n.nodeId, { inputs, outputs, inputDefaults });
|
|
573
542
|
}
|
|
574
|
-
|
|
575
|
-
// Only schedule if all inbound inputs are present (or there are none)
|
|
576
|
-
if (anyChanged && this.allInboundHaveValue(nodeId))
|
|
577
|
-
this.scheduleInputsChangedInternal(nodeId);
|
|
578
|
-
// Recompute dynamic handles for this node when its direct inputs change
|
|
579
|
-
if (anyChanged)
|
|
580
|
-
this.scheduleRecomputeHandles(nodeId);
|
|
581
|
-
}
|
|
543
|
+
return { map: out, pending };
|
|
582
544
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
545
|
+
static buildEdges(def, registry, resolvedByNode) {
|
|
546
|
+
return def.edges.map((e) => {
|
|
547
|
+
const srcNode = def.nodes.find((n) => n.nodeId === e.source.nodeId);
|
|
548
|
+
const dstNode = def.nodes.find((n) => n.nodeId === e.target.nodeId);
|
|
549
|
+
let effectiveTypeId = e.typeId; // Start with original
|
|
550
|
+
let srcDeclared;
|
|
551
|
+
let dstDeclared;
|
|
552
|
+
if (srcNode) {
|
|
553
|
+
const resolved = resolvedByNode.get(srcNode.nodeId);
|
|
554
|
+
if (resolved)
|
|
555
|
+
srcDeclared = resolved.outputs[e.source.handle];
|
|
556
|
+
}
|
|
557
|
+
if (!effectiveTypeId) {
|
|
558
|
+
// Infer if not explicitly set
|
|
559
|
+
effectiveTypeId = Array.isArray(srcDeclared)
|
|
560
|
+
? srcDeclared[0]
|
|
561
|
+
: srcDeclared;
|
|
562
|
+
}
|
|
563
|
+
if (dstNode) {
|
|
564
|
+
const resolved = resolvedByNode.get(dstNode.nodeId);
|
|
565
|
+
if (resolved)
|
|
566
|
+
dstDeclared = getInputTypeId(resolved.inputs, e.target.handle);
|
|
567
|
+
}
|
|
568
|
+
const { convert, convertAsync } = GraphStructure.buildEdgeConverters(srcDeclared, dstDeclared, registry, `buildEdges: ${srcNode?.typeId || ""}.${e.source.nodeId}.${e.source.handle} -> ${dstNode?.typeId || ""}.${e.target.nodeId}.${e.target.handle}`);
|
|
569
|
+
return {
|
|
570
|
+
id: e.id,
|
|
571
|
+
source: { ...e.source },
|
|
572
|
+
target: { ...e.target },
|
|
573
|
+
typeId: e.typeId, // Preserve original (may be undefined)
|
|
574
|
+
effectiveTypeId: effectiveTypeId ?? "untyped", // Always present
|
|
575
|
+
convert,
|
|
576
|
+
convertAsync,
|
|
577
|
+
srcUnionTypes: Array.isArray(srcDeclared)
|
|
578
|
+
? [...srcDeclared]
|
|
579
|
+
: undefined,
|
|
580
|
+
dstDeclared,
|
|
581
|
+
stats: { runs: 0, inFlight: false, progress: 0 },
|
|
582
|
+
};
|
|
583
|
+
});
|
|
586
584
|
}
|
|
585
|
+
// Clear all data
|
|
586
|
+
clear() {
|
|
587
|
+
this.nodes.clear();
|
|
588
|
+
this.edges = [];
|
|
589
|
+
this.resolvedByNode.clear();
|
|
590
|
+
}
|
|
591
|
+
// Static helper: build edge converters for type coercion
|
|
587
592
|
static buildEdgeConverters(srcDeclared, dstDeclared, registry, edgeLabel) {
|
|
588
593
|
if (!dstDeclared || !srcDeclared) {
|
|
589
594
|
return {};
|
|
@@ -647,27 +652,652 @@ class GraphRuntime {
|
|
|
647
652
|
},
|
|
648
653
|
};
|
|
649
654
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Event emitter component for GraphRuntime
|
|
659
|
+
* Handles all event listener management and emission
|
|
660
|
+
*/
|
|
661
|
+
class EventEmitter {
|
|
662
|
+
constructor() {
|
|
663
|
+
this.listeners = new Map();
|
|
664
|
+
}
|
|
665
|
+
on(event, handler) {
|
|
666
|
+
if (!this.listeners.has(event))
|
|
667
|
+
this.listeners.set(event, new Set());
|
|
668
|
+
const set = this.listeners.get(event);
|
|
669
|
+
set.add(handler);
|
|
670
|
+
return () => set.delete(handler);
|
|
671
|
+
}
|
|
672
|
+
emit(event, payload) {
|
|
673
|
+
const set = this.listeners.get(event);
|
|
674
|
+
if (!set)
|
|
675
|
+
return;
|
|
676
|
+
for (const handler of Array.from(set)) {
|
|
677
|
+
try {
|
|
678
|
+
handler(payload);
|
|
679
|
+
}
|
|
680
|
+
catch (err) {
|
|
681
|
+
console.error(`Error in ${event} handler:`, err);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
clear() {
|
|
686
|
+
this.listeners.clear();
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* RunContextManager component - manages run-context lifecycle
|
|
692
|
+
*/
|
|
693
|
+
class RunContextManager {
|
|
694
|
+
constructor() {
|
|
695
|
+
this.runContexts = new Map();
|
|
696
|
+
this.runContextCounter = 0;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Create a new run-context for runFromHere
|
|
700
|
+
*/
|
|
701
|
+
createRunContext(startNodeId, options) {
|
|
702
|
+
const id = `rc-${++this.runContextCounter}`;
|
|
703
|
+
const ctx = {
|
|
704
|
+
id,
|
|
705
|
+
startNodes: new Set([startNodeId]),
|
|
706
|
+
cancelledNodes: new Set(),
|
|
707
|
+
pending: 0,
|
|
708
|
+
skipPropagateValues: options?.skipPropagateValues ?? false,
|
|
709
|
+
propagate: options?.propagate ?? true,
|
|
710
|
+
};
|
|
711
|
+
this.runContexts.set(id, ctx);
|
|
712
|
+
return ctx;
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Get a run-context by ID
|
|
716
|
+
*/
|
|
717
|
+
getRunContext(id) {
|
|
718
|
+
return this.runContexts.get(id);
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Get all run-contexts
|
|
722
|
+
*/
|
|
723
|
+
getAllRunContexts() {
|
|
724
|
+
return this.runContexts;
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Check if there are any active run-contexts
|
|
728
|
+
*/
|
|
729
|
+
hasActiveRunContexts() {
|
|
730
|
+
return this.runContexts.size > 0;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Finish and remove a run-context when its pending count reaches zero
|
|
734
|
+
*/
|
|
735
|
+
finishRunContext(id, nodes) {
|
|
736
|
+
const ctx = this.runContexts.get(id);
|
|
737
|
+
if (!ctx)
|
|
738
|
+
return;
|
|
739
|
+
if (ctx.pending > 0)
|
|
740
|
+
return; // Still has pending work
|
|
741
|
+
// Clean up activeRunContexts from all nodes
|
|
742
|
+
for (const node of nodes.values()) {
|
|
743
|
+
node.activeRunContexts.delete(id);
|
|
744
|
+
}
|
|
745
|
+
this.runContexts.delete(id);
|
|
746
|
+
if (ctx.resolve)
|
|
747
|
+
ctx.resolve();
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Cancel a node for all run-contexts (called from UI or update())
|
|
751
|
+
* @param nodeId - The node to cancel
|
|
752
|
+
* @param includeDownstream - Whether to also cancel downstream nodes
|
|
753
|
+
* @param edges - All edges in the graph (for downstream traversal)
|
|
754
|
+
* @param nodes - All nodes in the graph (for clearing activeRunContexts)
|
|
755
|
+
*/
|
|
756
|
+
cancelNodeInRunContexts(nodeId, includeDownstream, edges, nodes) {
|
|
757
|
+
const toCancel = new Set([nodeId]);
|
|
758
|
+
if (includeDownstream) {
|
|
759
|
+
// Collect all downstream nodes
|
|
760
|
+
const queue = [nodeId];
|
|
761
|
+
const visited = new Set();
|
|
762
|
+
for (let i = 0; i < queue.length; i++) {
|
|
763
|
+
const cur = queue[i];
|
|
764
|
+
if (visited.has(cur))
|
|
765
|
+
continue;
|
|
766
|
+
visited.add(cur);
|
|
767
|
+
for (const e of edges) {
|
|
768
|
+
if (e.source.nodeId === cur) {
|
|
769
|
+
const targetId = e.target.nodeId;
|
|
770
|
+
if (!visited.has(targetId)) {
|
|
771
|
+
toCancel.add(targetId);
|
|
772
|
+
queue.push(targetId);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
// Mark nodes as cancelled in all run-contexts
|
|
779
|
+
for (const ctx of this.runContexts.values()) {
|
|
780
|
+
for (const id of toCancel) {
|
|
781
|
+
ctx.cancelledNodes.add(id);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
// Clear activeRunContexts for cancelled nodes
|
|
785
|
+
for (const id of toCancel) {
|
|
786
|
+
const node = nodes.get(id);
|
|
787
|
+
if (node)
|
|
788
|
+
node.activeRunContexts.clear();
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Resolve all pending run-context promises (for cleanup)
|
|
793
|
+
*/
|
|
794
|
+
resolveAll() {
|
|
795
|
+
for (const ctx of this.runContexts.values()) {
|
|
796
|
+
if (ctx.resolve)
|
|
797
|
+
ctx.resolve();
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Clear all run-contexts
|
|
802
|
+
*/
|
|
803
|
+
clear() {
|
|
804
|
+
this.runContexts.clear();
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const LOG_LEVEL_VALUES = {
|
|
809
|
+
debug: 0,
|
|
810
|
+
info: 1,
|
|
811
|
+
warn: 2,
|
|
812
|
+
error: 3,
|
|
813
|
+
silent: 4,
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* HandleResolver component - manages dynamic handle resolution
|
|
818
|
+
*/
|
|
819
|
+
class HandleResolver {
|
|
820
|
+
constructor(graphStructure, eventEmitter, runtimeCoordinator, registry, environment) {
|
|
821
|
+
this.recomputeTokenByNode = new Map();
|
|
822
|
+
this.environment = {};
|
|
823
|
+
this.graphStructure = graphStructure;
|
|
824
|
+
this.eventEmitter = eventEmitter;
|
|
825
|
+
this.runtimeCoordinator = runtimeCoordinator;
|
|
826
|
+
this.registry = registry;
|
|
827
|
+
this.environment = environment ?? {};
|
|
828
|
+
}
|
|
829
|
+
setRegistry(registry) {
|
|
830
|
+
this.registry = registry;
|
|
831
|
+
}
|
|
832
|
+
setEnvironment(environment) {
|
|
833
|
+
this.environment = environment;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Schedule async recomputation of handles for a node
|
|
837
|
+
*/
|
|
838
|
+
scheduleRecomputeHandles(nodeId) {
|
|
839
|
+
// If no registry or node not found, skip
|
|
840
|
+
if (!this.registry)
|
|
841
|
+
return;
|
|
842
|
+
const node = this.graphStructure.getNode(nodeId);
|
|
843
|
+
if (!node)
|
|
844
|
+
return;
|
|
845
|
+
setTimeout(() => {
|
|
846
|
+
void this.recomputeHandlesForNode(nodeId);
|
|
847
|
+
}, 0);
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Recompute dynamic handles for a single node using current inputs/environment
|
|
851
|
+
*/
|
|
852
|
+
async recomputeHandlesForNode(nodeId) {
|
|
853
|
+
const registry = this.registry;
|
|
854
|
+
const node = this.graphStructure.getNode(nodeId);
|
|
855
|
+
if (!node)
|
|
856
|
+
return;
|
|
857
|
+
const desc = registry.nodes.get(node.typeId);
|
|
858
|
+
if (!desc)
|
|
859
|
+
return;
|
|
860
|
+
const resolveHandles = desc.resolveHandles;
|
|
861
|
+
if (typeof resolveHandles !== "function")
|
|
862
|
+
return;
|
|
863
|
+
const token = (this.recomputeTokenByNode.get(nodeId) ?? 0) + 1;
|
|
864
|
+
this.recomputeTokenByNode.set(nodeId, token);
|
|
865
|
+
// Log resolveHandles-start
|
|
866
|
+
const nodeLogLevel = node.logLevel ?? "info";
|
|
867
|
+
const nodeLogValue = LOG_LEVEL_VALUES[nodeLogLevel] ?? 1;
|
|
868
|
+
const shouldLog = nodeLogValue <= LOG_LEVEL_VALUES.debug && nodeLogLevel !== "silent";
|
|
869
|
+
if (shouldLog) {
|
|
870
|
+
console.info(`[node:${nodeId}:${node.typeId}] resolveHandles-start`);
|
|
871
|
+
}
|
|
872
|
+
let r;
|
|
873
|
+
try {
|
|
874
|
+
const res = resolveHandles({
|
|
875
|
+
nodeId,
|
|
876
|
+
environment: this.environment || {},
|
|
877
|
+
params: node.params,
|
|
878
|
+
inputs: node.inputs || {},
|
|
879
|
+
});
|
|
880
|
+
r = await unwrapMaybePromise(res);
|
|
881
|
+
}
|
|
882
|
+
catch {
|
|
883
|
+
// Log resolveHandles-done even on error
|
|
884
|
+
if (shouldLog) {
|
|
885
|
+
console.info(`[node:${nodeId}:${node.typeId}] resolveHandles-done (error)`);
|
|
886
|
+
}
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
// Log resolveHandles-done
|
|
890
|
+
if (shouldLog) {
|
|
891
|
+
console.info(`[node:${nodeId}:${node.typeId}] resolveHandles-done`);
|
|
892
|
+
}
|
|
893
|
+
// If a newer recompute was scheduled, drop this result
|
|
894
|
+
if ((this.recomputeTokenByNode.get(nodeId) ?? 0) !== token)
|
|
895
|
+
return;
|
|
896
|
+
const resolved = this.graphStructure.getResolvedHandles(nodeId);
|
|
897
|
+
if (!resolved)
|
|
898
|
+
return; // Node was deleted
|
|
899
|
+
// Re-fetch desc to ensure we have the latest (node might have been updated)
|
|
900
|
+
const nodeDesc = registry.nodes.get(node.typeId);
|
|
901
|
+
if (!nodeDesc)
|
|
902
|
+
return;
|
|
903
|
+
const inputs = { ...nodeDesc.inputs, ...r?.inputs };
|
|
904
|
+
const outputs = { ...nodeDesc.outputs, ...r?.outputs };
|
|
905
|
+
const inputDefaults = {
|
|
906
|
+
...nodeDesc.inputDefaults,
|
|
907
|
+
...r?.inputDefaults,
|
|
908
|
+
};
|
|
909
|
+
const next = { inputs, outputs, inputDefaults };
|
|
910
|
+
const before = resolved;
|
|
911
|
+
// Compare shallow-structurally via JSON
|
|
912
|
+
if (JSON.stringify(before) === JSON.stringify(next))
|
|
913
|
+
return;
|
|
914
|
+
this.graphStructure.setResolvedHandles(nodeId, next);
|
|
915
|
+
// Call GraphRuntime's updateNodeHandles to update edges and re-propagate values
|
|
916
|
+
this.runtimeCoordinator.updateNodeHandles(nodeId, next, registry);
|
|
917
|
+
// Notify graph updated with the changed handles
|
|
918
|
+
this.eventEmitter.emit("invalidate", {
|
|
919
|
+
reason: "graph-updated",
|
|
920
|
+
resolvedHandles: { [nodeId]: next },
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* ValuePropagator component - handles value propagation through edges
|
|
927
|
+
*/
|
|
928
|
+
class ValuePropagator {
|
|
929
|
+
constructor(graphStructure, eventEmitter, runContextManager, handleResolver, runtimeCoordinator) {
|
|
930
|
+
this.arrayInputBuckets = new Map();
|
|
931
|
+
this.graphStructure = graphStructure;
|
|
932
|
+
this.eventEmitter = eventEmitter;
|
|
933
|
+
this.runContextManager = runContextManager;
|
|
934
|
+
this.handleResolver = handleResolver;
|
|
935
|
+
this.runtimeCoordinator = runtimeCoordinator;
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Set the execution scheduler (called after construction to resolve circular dependency)
|
|
939
|
+
*/
|
|
940
|
+
setExecutionScheduler(executionScheduler) {
|
|
941
|
+
this.executionScheduler = executionScheduler;
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Propagate value through edges
|
|
945
|
+
* @param runContextIds - Optional set of run-context IDs. If provided, propagation is run-context aware.
|
|
946
|
+
* If undefined or empty, behaves like auto mode (always propagates values and execution).
|
|
947
|
+
*/
|
|
948
|
+
propagate(srcNodeId, srcHandle, value, runContextIds) {
|
|
949
|
+
// Set source output
|
|
950
|
+
const srcNode = this.graphStructure.getNode(srcNodeId);
|
|
951
|
+
if (!srcNode) {
|
|
952
|
+
// Node was removed (e.g., graph updated) but an async emit arrived late; ignore
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
srcNode.outputs[srcHandle] = value;
|
|
956
|
+
this.eventEmitter.emit("value", {
|
|
957
|
+
nodeId: srcNodeId,
|
|
958
|
+
handle: srcHandle,
|
|
959
|
+
value,
|
|
960
|
+
io: "output",
|
|
961
|
+
runtimeTypeId: getTypedOutputTypeId(value),
|
|
962
|
+
});
|
|
963
|
+
// Fan-out along all edges from this output
|
|
964
|
+
const edges = this.graphStructure.getEdges();
|
|
965
|
+
const outEdges = edges.filter((e) => e.source.nodeId === srcNodeId && e.source.handle === srcHandle);
|
|
966
|
+
const isRunContextAware = runContextIds !== undefined && runContextIds.size > 0;
|
|
967
|
+
for (const e of outEdges) {
|
|
968
|
+
// Filter run-contexts: skip any where src or dst is cancelled (only in run-context mode)
|
|
969
|
+
let effectiveRunContexts;
|
|
970
|
+
if (isRunContextAware) {
|
|
971
|
+
effectiveRunContexts = new Set();
|
|
972
|
+
for (const id of runContextIds) {
|
|
973
|
+
const ctx = this.runContextManager.getRunContext(id);
|
|
974
|
+
if (!ctx)
|
|
975
|
+
continue;
|
|
976
|
+
if (ctx.cancelledNodes.has(srcNodeId))
|
|
977
|
+
continue;
|
|
978
|
+
if (ctx.cancelledNodes.has(e.target.nodeId))
|
|
979
|
+
continue;
|
|
980
|
+
effectiveRunContexts.add(id);
|
|
981
|
+
}
|
|
982
|
+
if (effectiveRunContexts.size === 0)
|
|
983
|
+
continue; // No valid run-contexts for this edge
|
|
984
|
+
}
|
|
985
|
+
// If source declares a union for this handle, require typed output
|
|
986
|
+
const isUnion = Array.isArray(e.srcUnionTypes);
|
|
987
|
+
const isTyped = isTypedOutput(value);
|
|
988
|
+
if (isUnion && !isTyped) {
|
|
989
|
+
const err = new Error(`Output ${srcNodeId}.${srcHandle} requires typed value for union output (allowed: ${e.srcUnionTypes.join("|")})`);
|
|
990
|
+
this.eventEmitter.emit("error", {
|
|
991
|
+
kind: "edge-convert",
|
|
992
|
+
edgeId: e.id,
|
|
993
|
+
source: { nodeId: e.source.nodeId, handle: e.source.handle },
|
|
994
|
+
target: { nodeId: e.target.nodeId, handle: e.target.handle },
|
|
995
|
+
err,
|
|
996
|
+
});
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
// Clone per edge to isolate conversions
|
|
1000
|
+
let nextVal = structuredClone(value);
|
|
1001
|
+
const applyToTarget = (v) => {
|
|
1002
|
+
const dstNode = this.graphStructure.getNode(e.target.nodeId);
|
|
1003
|
+
if (!dstNode)
|
|
1004
|
+
return;
|
|
1005
|
+
// Skip writing to unresolved handles
|
|
1006
|
+
if (e.dstDeclared === undefined)
|
|
1007
|
+
return;
|
|
1008
|
+
const dstIsArray = typeof e.dstDeclared === "string" && e.dstDeclared.endsWith("[]");
|
|
1009
|
+
let next = v;
|
|
1010
|
+
// Handle array types
|
|
1011
|
+
if (dstIsArray) {
|
|
1012
|
+
const toArray = (x) => Array.isArray(x) ? x : x === undefined ? [] : [x];
|
|
1013
|
+
let forNode = this.arrayInputBuckets.get(e.target.nodeId);
|
|
1014
|
+
if (!forNode) {
|
|
1015
|
+
forNode = new Map();
|
|
1016
|
+
this.arrayInputBuckets.set(e.target.nodeId, forNode);
|
|
1017
|
+
}
|
|
1018
|
+
let forHandle = forNode.get(e.target.handle);
|
|
1019
|
+
if (!forHandle) {
|
|
1020
|
+
forHandle = new Map();
|
|
1021
|
+
forNode.set(e.target.handle, forHandle);
|
|
1022
|
+
}
|
|
1023
|
+
forHandle.set(e.id, toArray(v));
|
|
1024
|
+
const merged = [];
|
|
1025
|
+
for (const ed of edges) {
|
|
1026
|
+
if (ed.target.nodeId === e.target.nodeId &&
|
|
1027
|
+
ed.target.handle === e.target.handle) {
|
|
1028
|
+
const part = forHandle.get(ed.id);
|
|
1029
|
+
if (part && part.length)
|
|
1030
|
+
merged.push(...part);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
next = merged;
|
|
1034
|
+
}
|
|
1035
|
+
const prev = dstNode.inputs[e.target.handle];
|
|
1036
|
+
const same = valuesEqual(prev, next);
|
|
1037
|
+
if (!same) {
|
|
1038
|
+
// Check skipPropagateValues (only in run-context mode)
|
|
1039
|
+
let shouldSkipPropagateValues = false;
|
|
1040
|
+
if (isRunContextAware && effectiveRunContexts) {
|
|
1041
|
+
for (const id of effectiveRunContexts) {
|
|
1042
|
+
const ctx = this.runContextManager.getRunContext(id);
|
|
1043
|
+
if (ctx && ctx.skipPropagateValues) {
|
|
1044
|
+
shouldSkipPropagateValues = true;
|
|
1045
|
+
break;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
// Set input values unless skipPropagateValues is enabled
|
|
1050
|
+
if (!shouldSkipPropagateValues) {
|
|
1051
|
+
dstNode.inputs[e.target.handle] = next;
|
|
1052
|
+
this.eventEmitter.emit("value", {
|
|
1053
|
+
nodeId: e.target.nodeId,
|
|
1054
|
+
handle: e.target.handle,
|
|
1055
|
+
value: next,
|
|
1056
|
+
io: "input",
|
|
1057
|
+
runtimeTypeId: getTypedOutputTypeId(next),
|
|
1058
|
+
});
|
|
1059
|
+
this.handleResolver.scheduleRecomputeHandles(e.target.nodeId);
|
|
1060
|
+
}
|
|
1061
|
+
// Check propagate flag (only in run-context mode; auto mode always propagates)
|
|
1062
|
+
let shouldPropagate = true; // Default: always propagate in auto mode
|
|
1063
|
+
if (isRunContextAware && effectiveRunContexts) {
|
|
1064
|
+
shouldPropagate = false;
|
|
1065
|
+
for (const id of effectiveRunContexts) {
|
|
1066
|
+
const ctx = this.runContextManager.getRunContext(id);
|
|
1067
|
+
if (ctx && ctx.propagate) {
|
|
1068
|
+
shouldPropagate = true;
|
|
1069
|
+
break;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
// Schedule downstream execution if propagation is enabled
|
|
1074
|
+
if (!this.runtimeCoordinator.isPaused() &&
|
|
1075
|
+
shouldPropagate &&
|
|
1076
|
+
this.executionScheduler.allInboundHaveValue(e.target.nodeId)) {
|
|
1077
|
+
if (isRunContextAware && effectiveRunContexts) {
|
|
1078
|
+
this.executionScheduler.scheduleInputsChangedWithRunContexts(e.target.nodeId, effectiveRunContexts);
|
|
1079
|
+
}
|
|
1080
|
+
else {
|
|
1081
|
+
this.executionScheduler.scheduleInputsChanged(e.target.nodeId);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
// Handle async conversion
|
|
1087
|
+
if (e.convertAsync) {
|
|
1088
|
+
this.eventEmitter.emit("stats", {
|
|
1089
|
+
kind: "edge-start",
|
|
1090
|
+
edgeId: e.id,
|
|
1091
|
+
typeId: e.typeId,
|
|
1092
|
+
source: { nodeId: e.source.nodeId, handle: e.source.handle },
|
|
1093
|
+
target: { nodeId: e.target.nodeId, handle: e.target.handle },
|
|
1094
|
+
});
|
|
1095
|
+
const controller = new AbortController();
|
|
1096
|
+
const startAt = Date.now();
|
|
1097
|
+
e.stats.runs += 1;
|
|
1098
|
+
e.stats.inFlight = true;
|
|
1099
|
+
if (!isRunContextAware) {
|
|
1100
|
+
// Only track progress in auto mode
|
|
1101
|
+
e.stats.progress = 0;
|
|
1102
|
+
}
|
|
1103
|
+
const sig = controller.signal;
|
|
1104
|
+
e.convertAsync(nextVal, sig)
|
|
1105
|
+
.then((converted) => {
|
|
1106
|
+
if (!sig.aborted) {
|
|
1107
|
+
applyToTarget(converted);
|
|
1108
|
+
e.stats.inFlight = false;
|
|
1109
|
+
const duration = Date.now() - startAt;
|
|
1110
|
+
e.stats.lastDurationMs = duration;
|
|
1111
|
+
if (!isRunContextAware) {
|
|
1112
|
+
// More detailed stats in auto mode
|
|
1113
|
+
e.stats.lastEndAt = Date.now();
|
|
1114
|
+
e.stats.lastError = undefined;
|
|
1115
|
+
this.eventEmitter.emit("stats", {
|
|
1116
|
+
kind: "edge-done",
|
|
1117
|
+
edgeId: e.id,
|
|
1118
|
+
typeId: e.typeId,
|
|
1119
|
+
source: { nodeId: e.source.nodeId, handle: e.source.handle },
|
|
1120
|
+
target: { nodeId: e.target.nodeId, handle: e.target.handle },
|
|
1121
|
+
durationMs: duration,
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
})
|
|
1126
|
+
.catch((err) => {
|
|
1127
|
+
if (sig.aborted)
|
|
1128
|
+
return;
|
|
1129
|
+
e.stats.inFlight = false;
|
|
1130
|
+
if (!isRunContextAware) {
|
|
1131
|
+
e.stats.lastError = err;
|
|
1132
|
+
}
|
|
1133
|
+
this.eventEmitter.emit("error", {
|
|
1134
|
+
kind: "edge-convert",
|
|
1135
|
+
edgeId: e.id,
|
|
1136
|
+
source: { nodeId: e.source.nodeId, handle: e.source.handle },
|
|
1137
|
+
target: { nodeId: e.target.nodeId, handle: e.target.handle },
|
|
1138
|
+
err,
|
|
1139
|
+
});
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
else {
|
|
1143
|
+
// Synchronous conversion
|
|
1144
|
+
if (e.convert) {
|
|
1145
|
+
nextVal = e.convert(nextVal);
|
|
1146
|
+
}
|
|
1147
|
+
applyToTarget(nextVal);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Propagate value in run-context aware mode (convenience method)
|
|
1153
|
+
*/
|
|
1154
|
+
propagateInRunContexts(srcNodeId, srcHandle, value, runContextIds) {
|
|
1155
|
+
this.propagate(srcNodeId, srcHandle, value, runContextIds);
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Re-emit all outputs from a node (used when graph updates)
|
|
1159
|
+
* Only re-emits outputs that are valid according to resolved handles
|
|
1160
|
+
*/
|
|
1161
|
+
reemitNodeOutputs(nodeId) {
|
|
1162
|
+
const node = this.graphStructure.getNode(nodeId);
|
|
1163
|
+
if (!node)
|
|
1164
|
+
return;
|
|
1165
|
+
// Get resolved handles to filter out invalid outputs
|
|
1166
|
+
const resolved = this.graphStructure.getResolvedHandles(nodeId);
|
|
1167
|
+
const validOutputHandles = resolved?.outputs
|
|
1168
|
+
? new Set(Object.keys(resolved.outputs))
|
|
1169
|
+
: new Set();
|
|
1170
|
+
// Use node's activeRunContexts to propagate to new nodes that were added
|
|
1171
|
+
const runContextIds = node.activeRunContexts.size > 0
|
|
1172
|
+
? new Set(node.activeRunContexts)
|
|
1173
|
+
: undefined;
|
|
1174
|
+
for (const [handle, value] of Object.entries(node.outputs)) {
|
|
1175
|
+
// Only re-emit if this handle is still valid
|
|
1176
|
+
if (validOutputHandles.has(handle)) {
|
|
1177
|
+
this.propagate(nodeId, handle, value, runContextIds);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Clear array input buckets for a node (when node is deleted)
|
|
1183
|
+
*/
|
|
1184
|
+
clearArrayBuckets(nodeId) {
|
|
1185
|
+
this.arrayInputBuckets.delete(nodeId);
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Clear all array buckets
|
|
1189
|
+
*/
|
|
1190
|
+
clearAllArrayBuckets() {
|
|
1191
|
+
this.arrayInputBuckets.clear();
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* ExecutionScheduler component - handles node execution scheduling and lifecycle
|
|
1197
|
+
*/
|
|
1198
|
+
class ExecutionScheduler {
|
|
1199
|
+
constructor(graphStructure, eventEmitter, runContextManager, valuePropagator, runtimeCoordinator, environment) {
|
|
1200
|
+
this.environment = {};
|
|
1201
|
+
this.graphStructure = graphStructure;
|
|
1202
|
+
this.eventEmitter = eventEmitter;
|
|
1203
|
+
this.runContextManager = runContextManager;
|
|
1204
|
+
this.valuePropagator = valuePropagator;
|
|
1205
|
+
this.runtimeCoordinator = runtimeCoordinator;
|
|
1206
|
+
this.environment = environment ?? {};
|
|
1207
|
+
}
|
|
1208
|
+
setEnvironment(environment) {
|
|
1209
|
+
this.environment = environment;
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Check if all inbound edges for a node have values
|
|
1213
|
+
*/
|
|
1214
|
+
allInboundHaveValue(nodeId) {
|
|
1215
|
+
const node = this.graphStructure.getNode(nodeId);
|
|
1216
|
+
if (!node)
|
|
1217
|
+
return false;
|
|
1218
|
+
const edges = this.graphStructure.getEdges();
|
|
1219
|
+
const inbound = edges.filter((e) => e.target.nodeId === nodeId);
|
|
1220
|
+
if (inbound.length === 0)
|
|
1221
|
+
return true;
|
|
1222
|
+
for (const e of inbound) {
|
|
1223
|
+
if (!(e.target.handle in node.inputs))
|
|
1224
|
+
return false;
|
|
1225
|
+
}
|
|
1226
|
+
return true;
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Compute effective inputs for a node by merging real inputs with defaults
|
|
1230
|
+
*/
|
|
1231
|
+
getEffectiveInputs(nodeId) {
|
|
1232
|
+
const node = this.graphStructure.getNode(nodeId);
|
|
1233
|
+
if (!node)
|
|
1234
|
+
return {};
|
|
1235
|
+
const registry = this.graphStructure.getRegistry();
|
|
1236
|
+
if (!registry)
|
|
1237
|
+
return {};
|
|
1238
|
+
const desc = registry.nodes.get(node.typeId);
|
|
1239
|
+
if (!desc)
|
|
1240
|
+
return {};
|
|
1241
|
+
const resolved = this.graphStructure.getResolvedHandles(nodeId);
|
|
1242
|
+
const regDefaults = desc.inputDefaults ?? {};
|
|
1243
|
+
const dynDefaults = resolved?.inputDefaults ?? {};
|
|
1244
|
+
// Identify which handles are dynamically resolved (not in registry statics)
|
|
1245
|
+
const staticHandles = new Set(Object.keys(desc.inputs ?? {}));
|
|
1246
|
+
const dynamicHandles = new Set(Object.keys(resolved?.inputs ?? {}).filter((h) => !staticHandles.has(h)));
|
|
1247
|
+
// Precedence: dynamic > registry
|
|
1248
|
+
const mergedDefaults = {
|
|
1249
|
+
...regDefaults,
|
|
1250
|
+
...dynDefaults,
|
|
1251
|
+
};
|
|
1252
|
+
// Start with real inputs only (no defaults)
|
|
1253
|
+
const effective = { ...node.inputs };
|
|
1254
|
+
// Build set of inbound handles (wired inputs)
|
|
1255
|
+
const edges = this.graphStructure.getEdges();
|
|
1256
|
+
const inbound = new Set(edges
|
|
1257
|
+
.filter((e) => e.target.nodeId === nodeId)
|
|
1258
|
+
.map((e) => e.target.handle));
|
|
1259
|
+
// Apply defaults only for:
|
|
1260
|
+
// 1. Unbound handles that have no explicit value
|
|
1261
|
+
// 2. Static handles (not dynamically resolved)
|
|
1262
|
+
for (const [handle, defaultValue] of Object.entries(mergedDefaults)) {
|
|
1263
|
+
if (defaultValue === undefined)
|
|
1264
|
+
continue;
|
|
1265
|
+
if (inbound.has(handle))
|
|
1266
|
+
continue; // Don't override wired inputs
|
|
1267
|
+
if (effective[handle] !== undefined)
|
|
1268
|
+
continue; // Already has value
|
|
1269
|
+
if (dynamicHandles.has(handle))
|
|
1270
|
+
continue; // Skip defaults for dynamic handles
|
|
1271
|
+
// Clone to avoid shared references
|
|
1272
|
+
effective[handle] = structuredClone(defaultValue);
|
|
1273
|
+
}
|
|
1274
|
+
return effective;
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Create an execution context for a node
|
|
1278
|
+
*/
|
|
1279
|
+
createExecutionContext(nodeId, node, inputs, runId, abortSignal, runContextIds, options) {
|
|
1280
|
+
const emitHandler = options?.emitHandler ??
|
|
1281
|
+
((handle, value) => {
|
|
1282
|
+
this.valuePropagator.propagate(nodeId, handle, value, runContextIds);
|
|
1283
|
+
});
|
|
1284
|
+
const reportProgress = options?.reportProgress ??
|
|
1285
|
+
((p) => {
|
|
1286
|
+
node.stats.progress = Math.max(0, Math.min(1, Number(p) || 0));
|
|
1287
|
+
});
|
|
1288
|
+
// Create log function that respects node's logLevel
|
|
1289
|
+
const log = (level, message, context) => {
|
|
1290
|
+
const nodeLogLevel = node.logLevel ?? "info";
|
|
1291
|
+
const nodeLogValue = LOG_LEVEL_VALUES[nodeLogLevel] ?? 1;
|
|
1292
|
+
const requestedValue = LOG_LEVEL_VALUES[level] ?? 1;
|
|
1293
|
+
// Only log if requested level >= node's logLevel
|
|
1294
|
+
if (requestedValue >= nodeLogValue && nodeLogLevel !== "silent") {
|
|
1295
|
+
const contextStr = context
|
|
1296
|
+
? ` ${Object.entries(context)
|
|
666
1297
|
.map(([k, v]) => `${k}=${JSON.stringify(v)}`)
|
|
667
1298
|
.join(" ")}`
|
|
668
1299
|
: "";
|
|
669
1300
|
const fullMessage = `[node:${runId || nodeId}:${node.typeId}] ${message}${contextStr}`;
|
|
670
|
-
// For other levels, use appropriate console method
|
|
671
1301
|
switch (level) {
|
|
672
1302
|
case "debug":
|
|
673
1303
|
console.info(fullMessage);
|
|
@@ -684,15 +1314,20 @@ class GraphRuntime {
|
|
|
684
1314
|
}
|
|
685
1315
|
}
|
|
686
1316
|
};
|
|
1317
|
+
// Store run-context IDs for use in scheduleInputsChanged
|
|
1318
|
+
const storedRunContextIds = runContextIds;
|
|
687
1319
|
return {
|
|
688
1320
|
nodeId,
|
|
689
1321
|
state: node.state,
|
|
690
1322
|
setState: (next) => Object.assign(node.state, next),
|
|
691
1323
|
emit: emitHandler,
|
|
692
|
-
invalidateDownstream: () =>
|
|
1324
|
+
invalidateDownstream: () => {
|
|
1325
|
+
this.runtimeCoordinator.invalidateDownstream(nodeId);
|
|
1326
|
+
},
|
|
693
1327
|
scheduleInputsChanged: () => {
|
|
694
1328
|
if (this.allInboundHaveValue(nodeId)) {
|
|
695
|
-
|
|
1329
|
+
// Preserve run-context IDs when scheduling from execution context
|
|
1330
|
+
this.scheduleInputsChangedInternal(nodeId, storedRunContextIds);
|
|
696
1331
|
}
|
|
697
1332
|
},
|
|
698
1333
|
getInput: (handle) => inputs[handle],
|
|
@@ -703,12 +1338,33 @@ class GraphRuntime {
|
|
|
703
1338
|
log,
|
|
704
1339
|
};
|
|
705
1340
|
}
|
|
706
|
-
|
|
707
|
-
|
|
1341
|
+
/**
|
|
1342
|
+
* Schedule a node for execution when its inputs change
|
|
1343
|
+
*/
|
|
1344
|
+
scheduleInputsChanged(nodeId) {
|
|
1345
|
+
this.scheduleInputsChangedInternal(nodeId);
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Schedule a node for execution with run-context IDs
|
|
1349
|
+
*/
|
|
1350
|
+
scheduleInputsChangedWithRunContexts(nodeId, runContextIds) {
|
|
1351
|
+
this.scheduleInputsChangedInternal(nodeId, runContextIds);
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Internal method for scheduling (also used by GraphRuntime)
|
|
1355
|
+
*/
|
|
1356
|
+
scheduleInputsChangedInternal(nodeId, runContextIds) {
|
|
1357
|
+
const node = this.graphStructure.getNode(nodeId);
|
|
708
1358
|
if (!node)
|
|
709
1359
|
return;
|
|
710
|
-
if (this.
|
|
1360
|
+
if (this.runtimeCoordinator.isPaused())
|
|
711
1361
|
return;
|
|
1362
|
+
// If run-context IDs are provided, attach them to the node
|
|
1363
|
+
if (runContextIds) {
|
|
1364
|
+
for (const id of runContextIds) {
|
|
1365
|
+
node.activeRunContexts.add(id);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
712
1368
|
const now = Date.now();
|
|
713
1369
|
const policy = node.policy ?? {};
|
|
714
1370
|
// Compute effective inputs (real inputs + defaults) for this execution
|
|
@@ -727,7 +1383,20 @@ class GraphRuntime {
|
|
|
727
1383
|
node.runSeq += 1;
|
|
728
1384
|
const rid = `${nodeId}:${node.runSeq}:${now}`;
|
|
729
1385
|
node.latestRunId = rid;
|
|
730
|
-
|
|
1386
|
+
// Get run-context IDs for this node (use provided or from node's activeRunContexts)
|
|
1387
|
+
const runContextIdsForRun = runContextIds ||
|
|
1388
|
+
(node.activeRunContexts.size > 0
|
|
1389
|
+
? new Set(node.activeRunContexts)
|
|
1390
|
+
: undefined);
|
|
1391
|
+
const startRun = (runId, capturedInputs, runContextIdsForRun, onDone) => {
|
|
1392
|
+
// Increment pending count for all relevant run-contexts (node will run)
|
|
1393
|
+
if (runContextIdsForRun && runContextIdsForRun.size > 0) {
|
|
1394
|
+
for (const id of runContextIdsForRun) {
|
|
1395
|
+
const ctx = this.runContextManager.getRunContext(id);
|
|
1396
|
+
if (ctx)
|
|
1397
|
+
ctx.pending++;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
731
1400
|
const controller = new AbortController();
|
|
732
1401
|
node.stats.runs += 1;
|
|
733
1402
|
node.stats.active += 1;
|
|
@@ -746,7 +1415,7 @@ class GraphRuntime {
|
|
|
746
1415
|
if (policy.timeoutMs && policy.timeoutMs > 0) {
|
|
747
1416
|
timeoutId = setTimeout(() => controller.abort("timeout"), policy.timeoutMs);
|
|
748
1417
|
}
|
|
749
|
-
const ctx = this.createExecutionContext(nodeId, node, capturedInputs, runId, controller.signal, {
|
|
1418
|
+
const ctx = this.createExecutionContext(nodeId, node, capturedInputs, runId, controller.signal, runContextIdsForRun, {
|
|
750
1419
|
emitHandler: (handle, value) => {
|
|
751
1420
|
const m = policy.asyncConcurrency ?? "switch";
|
|
752
1421
|
// Drop emits from runs that were explicitly cancelled due to a
|
|
@@ -755,11 +1424,11 @@ class GraphRuntime {
|
|
|
755
1424
|
return;
|
|
756
1425
|
if (m !== "merge" && runId !== node.latestRunId)
|
|
757
1426
|
return;
|
|
758
|
-
this.propagate(nodeId, handle, value);
|
|
1427
|
+
this.valuePropagator.propagate(nodeId, handle, value, runContextIdsForRun);
|
|
759
1428
|
},
|
|
760
1429
|
reportProgress: (p) => {
|
|
761
1430
|
node.stats.progress = Math.max(0, Math.min(1, Number(p) || 0));
|
|
762
|
-
this.emit("stats", {
|
|
1431
|
+
this.eventEmitter.emit("stats", {
|
|
763
1432
|
kind: "node-progress",
|
|
764
1433
|
nodeId,
|
|
765
1434
|
typeId: node.typeId,
|
|
@@ -784,7 +1453,8 @@ class GraphRuntime {
|
|
|
784
1453
|
const reason = controller.signal.reason;
|
|
785
1454
|
if (reason === "switch" ||
|
|
786
1455
|
reason === "snapshot" ||
|
|
787
|
-
reason === "node-deleted"
|
|
1456
|
+
reason === "node-deleted" ||
|
|
1457
|
+
reason === "user-cancelled") {
|
|
788
1458
|
return; // Cancellation events are emitted separately, skip error handling
|
|
789
1459
|
}
|
|
790
1460
|
}
|
|
@@ -796,11 +1466,16 @@ class GraphRuntime {
|
|
|
796
1466
|
await new Promise((r) => setTimeout(r, delay));
|
|
797
1467
|
return exec(attempt + 1);
|
|
798
1468
|
}
|
|
799
|
-
this.emit("error", {
|
|
1469
|
+
this.eventEmitter.emit("error", {
|
|
1470
|
+
kind: "node-run",
|
|
1471
|
+
nodeId,
|
|
1472
|
+
runId,
|
|
1473
|
+
err,
|
|
1474
|
+
});
|
|
800
1475
|
}
|
|
801
1476
|
finally {
|
|
802
1477
|
// Skip cleanup if node was deleted (cleanup already handled)
|
|
803
|
-
if (!this.
|
|
1478
|
+
if (!this.graphStructure.hasNode(nodeId)) {
|
|
804
1479
|
return;
|
|
805
1480
|
}
|
|
806
1481
|
if (timeoutId)
|
|
@@ -818,9 +1493,10 @@ class GraphRuntime {
|
|
|
818
1493
|
// Only emit node-done if not cancelled (cancellation events emitted separately)
|
|
819
1494
|
const isCancelled = controller.signal.aborted &&
|
|
820
1495
|
(controller.signal.reason === "snapshot" ||
|
|
821
|
-
controller.signal.reason === "node-deleted"
|
|
1496
|
+
controller.signal.reason === "node-deleted" ||
|
|
1497
|
+
controller.signal.reason === "user-cancelled");
|
|
822
1498
|
if (!isCancelled) {
|
|
823
|
-
this.emit("stats", {
|
|
1499
|
+
this.eventEmitter.emit("stats", {
|
|
824
1500
|
kind: "node-done",
|
|
825
1501
|
nodeId,
|
|
826
1502
|
typeId: node.typeId,
|
|
@@ -832,12 +1508,24 @@ class GraphRuntime {
|
|
|
832
1508
|
durationMs: node.stats.lastDurationMs,
|
|
833
1509
|
hadError,
|
|
834
1510
|
});
|
|
1511
|
+
// Decrement pending count for all relevant run-contexts
|
|
1512
|
+
if (runContextIdsForRun && runContextIdsForRun.size > 0) {
|
|
1513
|
+
for (const id of runContextIdsForRun) {
|
|
1514
|
+
const ctx = this.runContextManager.getRunContext(id);
|
|
1515
|
+
if (ctx) {
|
|
1516
|
+
ctx.pending--;
|
|
1517
|
+
if (ctx.pending === 0) {
|
|
1518
|
+
this.runContextManager.finishRunContext(id, this.graphStructure.getNodes());
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
835
1523
|
if (onDone)
|
|
836
1524
|
onDone();
|
|
837
1525
|
}
|
|
838
1526
|
};
|
|
839
|
-
//
|
|
840
|
-
this.emit("stats", {
|
|
1527
|
+
// Fire node-start event
|
|
1528
|
+
this.eventEmitter.emit("stats", {
|
|
841
1529
|
kind: "node-start",
|
|
842
1530
|
nodeId,
|
|
843
1531
|
typeId: node.typeId,
|
|
@@ -847,8 +1535,10 @@ class GraphRuntime {
|
|
|
847
1535
|
exec(0);
|
|
848
1536
|
};
|
|
849
1537
|
const mode = policy.asyncConcurrency ?? "switch";
|
|
850
|
-
if (mode === "drop" && node.activeControllers.size > 0)
|
|
1538
|
+
if (mode === "drop" && node.activeControllers.size > 0) {
|
|
1539
|
+
// Don't increment pendingCount if we're dropping this run
|
|
851
1540
|
return;
|
|
1541
|
+
}
|
|
852
1542
|
if (mode === "queue") {
|
|
853
1543
|
const maxQ = policy.maxQueue ?? 8;
|
|
854
1544
|
node.queue.push({ runId: rid, inputs: effectiveInputs });
|
|
@@ -861,347 +1551,302 @@ class GraphRuntime {
|
|
|
861
1551
|
if (!next)
|
|
862
1552
|
return;
|
|
863
1553
|
node.latestRunId = next.runId;
|
|
864
|
-
|
|
1554
|
+
// Use node's activeRunContexts for queued items
|
|
1555
|
+
const queuedRunContextIds = node.activeRunContexts.size > 0
|
|
1556
|
+
? new Set(node.activeRunContexts)
|
|
1557
|
+
: undefined;
|
|
1558
|
+
startRun(next.runId, next.inputs, queuedRunContextIds, () => {
|
|
865
1559
|
// After finishing, schedule next
|
|
866
|
-
setTimeout(processNext, 0);
|
|
867
|
-
});
|
|
868
|
-
};
|
|
869
|
-
processNext();
|
|
870
|
-
return;
|
|
871
|
-
}
|
|
872
|
-
// switch or merge
|
|
873
|
-
startRun(rid, effectiveInputs);
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
const registry = this.registry;
|
|
898
|
-
if (!registry)
|
|
899
|
-
return {};
|
|
900
|
-
const desc = registry.nodes.get(node.typeId);
|
|
901
|
-
if (!desc)
|
|
902
|
-
return {};
|
|
903
|
-
const resolved = this.resolvedByNode.get(nodeId);
|
|
904
|
-
const regDefaults = desc.inputDefaults ?? {};
|
|
905
|
-
const dynDefaults = resolved?.inputDefaults ?? {};
|
|
906
|
-
// Identify which handles are dynamically resolved (not in registry statics)
|
|
907
|
-
// Dynamic handles are metadata-only and should not receive defaults in execution
|
|
908
|
-
const staticHandles = new Set(Object.keys(desc.inputs ?? {}));
|
|
909
|
-
const dynamicHandles = new Set(Object.keys(resolved?.inputs ?? {}).filter((h) => !staticHandles.has(h)));
|
|
910
|
-
// Precedence: dynamic > registry
|
|
911
|
-
const mergedDefaults = {
|
|
912
|
-
...regDefaults,
|
|
913
|
-
...dynDefaults,
|
|
914
|
-
};
|
|
915
|
-
// Start with real inputs only (no defaults)
|
|
916
|
-
const effective = { ...node.inputs };
|
|
917
|
-
// Build set of inbound handles (wired inputs)
|
|
918
|
-
const inbound = new Set(this.edges
|
|
919
|
-
.filter((e) => e.target.nodeId === nodeId)
|
|
920
|
-
.map((e) => e.target.handle));
|
|
921
|
-
// Apply defaults only for:
|
|
922
|
-
// 1. Unbound handles that have no explicit value
|
|
923
|
-
// 2. Static handles (not dynamically resolved)
|
|
924
|
-
// This prevents dynamic handles (like geo:*) from getting defaults in execution
|
|
925
|
-
// Dynamic handles are metadata-only for UI display, not execution values
|
|
926
|
-
for (const [handle, defaultValue] of Object.entries(mergedDefaults)) {
|
|
927
|
-
if (defaultValue === undefined)
|
|
928
|
-
continue;
|
|
929
|
-
if (inbound.has(handle))
|
|
930
|
-
continue; // Don't override wired inputs
|
|
931
|
-
if (effective[handle] !== undefined)
|
|
932
|
-
continue; // Already has value
|
|
933
|
-
if (dynamicHandles.has(handle))
|
|
934
|
-
continue; // Skip defaults for dynamic handles
|
|
935
|
-
// Clone to avoid shared references
|
|
936
|
-
effective[handle] = structuredClone(defaultValue);
|
|
937
|
-
}
|
|
938
|
-
return effective;
|
|
939
|
-
}
|
|
940
|
-
invalidateDownstreamInternal(nodeId) {
|
|
941
|
-
// Notifies dependents; for now we propagate current outputs
|
|
942
|
-
for (const e of this.edges.filter((e) => e.source.nodeId === nodeId)) {
|
|
943
|
-
const value = this.getOutput(nodeId, e.source.handle);
|
|
944
|
-
if (value !== undefined)
|
|
945
|
-
this.propagate(nodeId, e.source.handle, value);
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
propagate(srcNodeId, srcHandle, value) {
|
|
949
|
-
// set source output
|
|
950
|
-
const srcNode = this.nodes.get(srcNodeId);
|
|
951
|
-
if (!srcNode) {
|
|
952
|
-
// Node was removed (e.g., graph updated) but an async emit arrived late; ignore
|
|
953
|
-
return;
|
|
954
|
-
}
|
|
955
|
-
srcNode.outputs[srcHandle] = value;
|
|
956
|
-
this.emit("value", {
|
|
957
|
-
nodeId: srcNodeId,
|
|
958
|
-
handle: srcHandle,
|
|
959
|
-
value,
|
|
960
|
-
io: "output",
|
|
961
|
-
runtimeTypeId: getTypedOutputTypeId(value),
|
|
962
|
-
});
|
|
963
|
-
// fan-out along all edges from this output
|
|
964
|
-
const outEdges = this.edges.filter((e) => e.source.nodeId === srcNodeId && e.source.handle === srcHandle);
|
|
965
|
-
for (const e of outEdges) {
|
|
966
|
-
// If source declares a union for this handle, require typed output
|
|
967
|
-
const isUnion = Array.isArray(e.srcUnionTypes);
|
|
968
|
-
const isTyped = isTypedOutput(value);
|
|
969
|
-
if (isUnion && !isTyped) {
|
|
970
|
-
const err = new Error(`Output ${srcNodeId}.${srcHandle} requires typed value for union output (allowed: ${e.srcUnionTypes.join("|")})`);
|
|
971
|
-
this.emit("error", {
|
|
972
|
-
kind: "edge-convert",
|
|
973
|
-
edgeId: e.id,
|
|
974
|
-
source: { nodeId: e.source.nodeId, handle: e.source.handle },
|
|
975
|
-
target: { nodeId: e.target.nodeId, handle: e.target.handle },
|
|
976
|
-
err,
|
|
977
|
-
});
|
|
978
|
-
continue;
|
|
979
|
-
}
|
|
980
|
-
// Clone per edge to isolate conversions from mutating the shared source value
|
|
981
|
-
let nextVal = structuredClone(value);
|
|
982
|
-
const applyToTarget = (v) => {
|
|
983
|
-
const dstNode = this.nodes.get(e.target.nodeId);
|
|
984
|
-
if (!dstNode)
|
|
985
|
-
return;
|
|
986
|
-
// Skip writing to unresolved handles - wait for handle resolution
|
|
987
|
-
if (e.dstDeclared === undefined)
|
|
988
|
-
return;
|
|
989
|
-
const dstIsArray = typeof e.dstDeclared === "string" && e.dstDeclared.endsWith("[]");
|
|
990
|
-
let next = v;
|
|
991
|
-
// If target input is an array type, merge per-edge contributions deterministically
|
|
992
|
-
if (dstIsArray) {
|
|
993
|
-
const toArray = (x) => Array.isArray(x) ? x : x === undefined ? [] : [x];
|
|
994
|
-
// Update this edge's contribution
|
|
995
|
-
let forNode = this.arrayInputBuckets.get(e.target.nodeId);
|
|
996
|
-
if (!forNode) {
|
|
997
|
-
forNode = new Map();
|
|
998
|
-
this.arrayInputBuckets.set(e.target.nodeId, forNode);
|
|
999
|
-
}
|
|
1000
|
-
let forHandle = forNode.get(e.target.handle);
|
|
1001
|
-
if (!forHandle) {
|
|
1002
|
-
forHandle = new Map();
|
|
1003
|
-
forNode.set(e.target.handle, forHandle);
|
|
1004
|
-
}
|
|
1005
|
-
forHandle.set(e.id, toArray(v));
|
|
1006
|
-
// Compute merged array in the order of current edges list
|
|
1007
|
-
const merged = [];
|
|
1008
|
-
for (const ed of this.edges) {
|
|
1009
|
-
if (ed.target.nodeId === e.target.nodeId &&
|
|
1010
|
-
ed.target.handle === e.target.handle) {
|
|
1011
|
-
const part = forHandle.get(ed.id);
|
|
1012
|
-
if (part && part.length)
|
|
1013
|
-
merged.push(...part);
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
next = merged;
|
|
1017
|
-
}
|
|
1018
|
-
const prev = dstNode.inputs[e.target.handle];
|
|
1019
|
-
const same = this.valuesEqual(prev, next);
|
|
1020
|
-
if (!same) {
|
|
1021
|
-
dstNode.inputs[e.target.handle] = next;
|
|
1022
|
-
// Emit value event for input updates
|
|
1023
|
-
this.emit("value", {
|
|
1024
|
-
nodeId: e.target.nodeId,
|
|
1025
|
-
handle: e.target.handle,
|
|
1026
|
-
value: next,
|
|
1027
|
-
io: "input",
|
|
1028
|
-
runtimeTypeId: getTypedOutputTypeId(next),
|
|
1029
|
-
});
|
|
1030
|
-
// Recompute dynamic handles for the destination node on input change
|
|
1031
|
-
this.scheduleRecomputeHandles(e.target.nodeId);
|
|
1032
|
-
if (!this.paused && this.allInboundHaveValue(e.target.nodeId))
|
|
1033
|
-
this.scheduleInputsChangedInternal(e.target.nodeId);
|
|
1034
|
-
}
|
|
1035
|
-
};
|
|
1036
|
-
if (e.convertAsync) {
|
|
1037
|
-
// emit edge-start before launching async conversion
|
|
1038
|
-
this.emit("stats", {
|
|
1039
|
-
kind: "edge-start",
|
|
1040
|
-
edgeId: e.id,
|
|
1041
|
-
typeId: e.typeId,
|
|
1042
|
-
source: { nodeId: e.source.nodeId, handle: e.source.handle },
|
|
1043
|
-
target: { nodeId: e.target.nodeId, handle: e.target.handle },
|
|
1044
|
-
});
|
|
1045
|
-
const controller = new AbortController();
|
|
1046
|
-
const startAt = Date.now();
|
|
1047
|
-
e.stats.runs += 1;
|
|
1048
|
-
e.stats.inFlight = true;
|
|
1049
|
-
e.stats.progress = 0;
|
|
1050
|
-
const sig = controller.signal;
|
|
1051
|
-
// Fire async conversion using edge's convertAsync (dynamic union aware)
|
|
1052
|
-
e.convertAsync(nextVal, sig)
|
|
1053
|
-
.then((v) => {
|
|
1054
|
-
e.stats.inFlight = false;
|
|
1055
|
-
e.stats.lastEndAt = Date.now();
|
|
1056
|
-
e.stats.lastDurationMs = e.stats.lastEndAt - startAt;
|
|
1057
|
-
// Clear lastError on successful conversion
|
|
1058
|
-
e.stats.lastError = undefined;
|
|
1059
|
-
this.emit("stats", {
|
|
1060
|
-
kind: "edge-done",
|
|
1061
|
-
edgeId: e.id,
|
|
1062
|
-
typeId: e.typeId,
|
|
1063
|
-
source: { nodeId: e.source.nodeId, handle: e.source.handle },
|
|
1064
|
-
target: { nodeId: e.target.nodeId, handle: e.target.handle },
|
|
1065
|
-
durationMs: e.stats.lastDurationMs,
|
|
1066
|
-
});
|
|
1067
|
-
applyToTarget(v);
|
|
1068
|
-
})
|
|
1069
|
-
.catch((err) => {
|
|
1070
|
-
if (sig.aborted)
|
|
1071
|
-
return;
|
|
1072
|
-
e.stats.inFlight = false;
|
|
1073
|
-
e.stats.lastError = err;
|
|
1074
|
-
this.emit("error", {
|
|
1075
|
-
kind: "edge-convert",
|
|
1076
|
-
edgeId: e.id,
|
|
1077
|
-
source: { nodeId: e.source.nodeId, handle: e.source.handle },
|
|
1078
|
-
target: { nodeId: e.target.nodeId, handle: e.target.handle },
|
|
1079
|
-
err,
|
|
1080
|
-
});
|
|
1560
|
+
setTimeout(processNext, 0);
|
|
1561
|
+
});
|
|
1562
|
+
};
|
|
1563
|
+
processNext();
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
// switch or merge
|
|
1567
|
+
startRun(rid, effectiveInputs, runContextIdsForRun);
|
|
1568
|
+
}
|
|
1569
|
+
/**
|
|
1570
|
+
* Cancel all active runs for a node
|
|
1571
|
+
*/
|
|
1572
|
+
cancelNodeActiveRuns(node, reason) {
|
|
1573
|
+
for (const controller of Array.from(node.activeControllers)) {
|
|
1574
|
+
const runId = node.controllerRunIds.get(controller);
|
|
1575
|
+
if (runId) {
|
|
1576
|
+
// Track cancelled runIds for snapshot and user-cancelled operations
|
|
1577
|
+
// (to drop emits from cancelled runs)
|
|
1578
|
+
if (reason === "snapshot" || reason === "user-cancelled") {
|
|
1579
|
+
if (!node.snapshotCancelledRunIds) {
|
|
1580
|
+
node.snapshotCancelledRunIds = new Set();
|
|
1581
|
+
}
|
|
1582
|
+
node.snapshotCancelledRunIds.add(runId);
|
|
1583
|
+
}
|
|
1584
|
+
// Emit cancellation event
|
|
1585
|
+
this.eventEmitter.emit("stats", {
|
|
1586
|
+
kind: "node-done",
|
|
1587
|
+
nodeId: node.nodeId,
|
|
1588
|
+
typeId: node.typeId,
|
|
1589
|
+
runId,
|
|
1590
|
+
cancelled: true,
|
|
1081
1591
|
});
|
|
1082
1592
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1593
|
+
try {
|
|
1594
|
+
controller.abort(reason);
|
|
1595
|
+
}
|
|
1596
|
+
catch {
|
|
1597
|
+
// ignore abort errors
|
|
1087
1598
|
}
|
|
1088
1599
|
}
|
|
1600
|
+
node.activeControllers.clear();
|
|
1601
|
+
node.controllerRunIds.clear();
|
|
1602
|
+
node.stats.active = 0;
|
|
1603
|
+
node.queue = [];
|
|
1089
1604
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1605
|
+
/**
|
|
1606
|
+
* Cancel runs for multiple nodes.
|
|
1607
|
+
* Can be called for snapshot/undo/redo operations or user-initiated cancellation.
|
|
1608
|
+
*/
|
|
1609
|
+
cancelNodeRuns(nodeIds, reason = "user-cancelled") {
|
|
1610
|
+
if (nodeIds.length === 0)
|
|
1611
|
+
return;
|
|
1612
|
+
const toCancel = new Set(nodeIds);
|
|
1613
|
+
const visited = new Set();
|
|
1614
|
+
const queue = [...nodeIds];
|
|
1615
|
+
const edges = this.graphStructure.getEdges();
|
|
1616
|
+
// Collect all downstream nodes to cancel
|
|
1617
|
+
for (let i = 0; i < queue.length; i++) {
|
|
1618
|
+
const nodeId = queue[i];
|
|
1619
|
+
if (visited.has(nodeId))
|
|
1620
|
+
continue;
|
|
1621
|
+
visited.add(nodeId);
|
|
1622
|
+
for (const edge of edges) {
|
|
1623
|
+
if (edge.source.nodeId === nodeId) {
|
|
1624
|
+
const targetId = edge.target.nodeId;
|
|
1625
|
+
if (!visited.has(targetId)) {
|
|
1626
|
+
toCancel.add(targetId);
|
|
1627
|
+
queue.push(targetId);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
// Cancel runs for all affected nodes
|
|
1633
|
+
for (const nodeId of toCancel) {
|
|
1634
|
+
const node = this.graphStructure.getNode(nodeId);
|
|
1635
|
+
if (!node)
|
|
1636
|
+
continue;
|
|
1637
|
+
this.cancelNodeActiveRuns(node, reason);
|
|
1638
|
+
node.runSeq += 1;
|
|
1639
|
+
const now = Date.now();
|
|
1640
|
+
const suffix = reason === "snapshot" ? "snapshot" : "cancelled";
|
|
1641
|
+
node.latestRunId = `${nodeId}:${node.runSeq}:${now}:${suffix}`;
|
|
1642
|
+
}
|
|
1643
|
+
// Cancel nodes in run-contexts (exclude them from active run-contexts)
|
|
1644
|
+
for (const nodeId of toCancel) {
|
|
1645
|
+
this.runContextManager.cancelNodeInRunContexts(nodeId, false, // includeDownstream = false (already collected above)
|
|
1646
|
+
edges, this.graphStructure.getNodes());
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
// Types are now imported from components/types.ts (re-exported above)
|
|
1652
|
+
class GraphRuntime {
|
|
1653
|
+
constructor() {
|
|
1654
|
+
// State
|
|
1655
|
+
this.paused = false;
|
|
1656
|
+
this.environment = {};
|
|
1657
|
+
// Initialize components
|
|
1658
|
+
this.graphStructure = new GraphStructure();
|
|
1659
|
+
this.eventEmitter = new EventEmitter();
|
|
1660
|
+
this.runContextManager = new RunContextManager();
|
|
1661
|
+
// Initialize components with interface-based dependencies
|
|
1662
|
+
// HandleResolver only needs IRuntimeCoordinator (this)
|
|
1663
|
+
this.handleResolver = new HandleResolver(this.graphStructure, this.eventEmitter, this, // IRuntimeCoordinator
|
|
1664
|
+
undefined, // registry set later
|
|
1665
|
+
undefined // environment set later
|
|
1666
|
+
);
|
|
1667
|
+
// ValuePropagator needs IHandleResolver and IRuntimeCoordinator
|
|
1668
|
+
// ExecutionScheduler needs IValuePropagator - circular dependency!
|
|
1669
|
+
// Solution: Create ValuePropagator first (without executionScheduler),
|
|
1670
|
+
// then create ExecutionScheduler with ValuePropagator,
|
|
1671
|
+
// then wire ValuePropagator with ExecutionScheduler via setExecutionScheduler
|
|
1672
|
+
this.valuePropagator = new ValuePropagator(this.graphStructure, this.eventEmitter, this.runContextManager, this.handleResolver, // IHandleResolver
|
|
1673
|
+
this // IRuntimeCoordinator
|
|
1674
|
+
);
|
|
1675
|
+
// Create ExecutionScheduler with ValuePropagator
|
|
1676
|
+
this.executionScheduler = new ExecutionScheduler(this.graphStructure, this.eventEmitter, this.runContextManager, this.valuePropagator, // IValuePropagator
|
|
1677
|
+
this, // IRuntimeCoordinator
|
|
1678
|
+
this.environment);
|
|
1679
|
+
// Wire ValuePropagator with ExecutionScheduler to resolve circular dependency
|
|
1680
|
+
this.valuePropagator.setExecutionScheduler(this.executionScheduler);
|
|
1681
|
+
}
|
|
1682
|
+
static create(def, registry, opts) {
|
|
1683
|
+
const gr = new GraphRuntime();
|
|
1684
|
+
gr.environment = opts?.environment ?? {};
|
|
1685
|
+
// Set registry and environment on components
|
|
1686
|
+
gr.graphStructure.setRegistry(registry);
|
|
1687
|
+
gr.handleResolver.setRegistry(registry);
|
|
1688
|
+
gr.handleResolver.setEnvironment(gr.environment);
|
|
1689
|
+
gr.executionScheduler.setEnvironment(gr.environment);
|
|
1690
|
+
// Precompute per-node resolved handles (use def-provided overrides; do not compute dynamically here)
|
|
1691
|
+
const initial = GraphStructure.computeResolvedHandleMap(def, registry, gr.environment);
|
|
1692
|
+
for (const [nodeId, handles] of initial.map) {
|
|
1693
|
+
gr.graphStructure.setResolvedHandles(nodeId, handles);
|
|
1694
|
+
}
|
|
1695
|
+
// Instantiate nodes
|
|
1094
1696
|
for (const n of def.nodes) {
|
|
1095
1697
|
const desc = registry.nodes.get(n.typeId);
|
|
1096
1698
|
if (!desc)
|
|
1699
|
+
throw new Error(`Unknown node type: ${n.typeId}`);
|
|
1700
|
+
const cat = registry.categories.get(desc.categoryId);
|
|
1701
|
+
if (!cat)
|
|
1702
|
+
throw new Error(`Unknown category: ${desc.categoryId}`);
|
|
1703
|
+
if (cat.validateImpl)
|
|
1704
|
+
cat.validateImpl(desc.impl);
|
|
1705
|
+
const runtime = cat.createRuntime({
|
|
1706
|
+
nodeId: n.nodeId,
|
|
1707
|
+
impl: desc.impl,
|
|
1708
|
+
});
|
|
1709
|
+
const rn = {
|
|
1710
|
+
typeId: n.typeId,
|
|
1711
|
+
nodeId: n.nodeId,
|
|
1712
|
+
lifecycle: desc.lifecycle,
|
|
1713
|
+
inputs: {},
|
|
1714
|
+
outputs: {},
|
|
1715
|
+
state: {},
|
|
1716
|
+
runtime,
|
|
1717
|
+
params: n.params,
|
|
1718
|
+
policy: {
|
|
1719
|
+
...cat.policy,
|
|
1720
|
+
...desc.policy,
|
|
1721
|
+
...n.params?.policy,
|
|
1722
|
+
},
|
|
1723
|
+
logLevel: desc.logLevel,
|
|
1724
|
+
runSeq: 0,
|
|
1725
|
+
activeControllers: new Set(),
|
|
1726
|
+
controllerRunIds: new Map(),
|
|
1727
|
+
queue: [],
|
|
1728
|
+
stats: {
|
|
1729
|
+
runs: 0,
|
|
1730
|
+
active: 0,
|
|
1731
|
+
queued: 0,
|
|
1732
|
+
progress: 0,
|
|
1733
|
+
},
|
|
1734
|
+
activeRunContexts: new Set(),
|
|
1735
|
+
};
|
|
1736
|
+
gr.graphStructure.setNode(n.nodeId, rn);
|
|
1737
|
+
}
|
|
1738
|
+
// Instantiate edges
|
|
1739
|
+
const edges = GraphStructure.buildEdges(def, registry, gr.graphStructure.getResolvedHandlesMap());
|
|
1740
|
+
gr.graphStructure.setEdges(edges);
|
|
1741
|
+
// Schedule async recompute only for nodes that indicated Promise-based resolveHandles
|
|
1742
|
+
for (const nodeId of initial.pending) {
|
|
1743
|
+
gr.handleResolver.scheduleRecomputeHandles(nodeId);
|
|
1744
|
+
}
|
|
1745
|
+
return gr;
|
|
1746
|
+
}
|
|
1747
|
+
on(event, handler) {
|
|
1748
|
+
return this.eventEmitter.on(event, handler);
|
|
1749
|
+
}
|
|
1750
|
+
setInputs(nodeId, inputs) {
|
|
1751
|
+
const node = this.graphStructure.getNode(nodeId);
|
|
1752
|
+
if (!node)
|
|
1753
|
+
throw new Error(`Node not found: ${nodeId}`);
|
|
1754
|
+
let anyChanged = false;
|
|
1755
|
+
const edges = this.graphStructure.getEdges();
|
|
1756
|
+
const registry = this.graphStructure.getRegistry();
|
|
1757
|
+
for (const [handle, value] of Object.entries(inputs)) {
|
|
1758
|
+
const hasInbound = edges.some((e) => e.target.nodeId === nodeId && e.target.handle === handle);
|
|
1759
|
+
if (hasInbound)
|
|
1097
1760
|
continue;
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1761
|
+
// Validate input value against declared type
|
|
1762
|
+
if (value !== undefined && registry) {
|
|
1763
|
+
const desc = registry.nodes.get(node.typeId);
|
|
1764
|
+
const resolved = this.graphStructure.getResolvedHandles(nodeId);
|
|
1765
|
+
// Get typeId from resolved handles first, then registry statics
|
|
1766
|
+
const typeId = resolved
|
|
1767
|
+
? getInputTypeId(resolved.inputs, handle)
|
|
1768
|
+
: desc
|
|
1769
|
+
? getInputTypeId(desc.inputs, handle)
|
|
1770
|
+
: undefined;
|
|
1771
|
+
if (typeId) {
|
|
1772
|
+
const typeDesc = registry.types.get(typeId);
|
|
1773
|
+
if (typeDesc?.validate && !typeDesc.validate(value)) {
|
|
1774
|
+
// Emit error event for invalid input value and reject it
|
|
1775
|
+
const errorMessage = `Invalid value for input ${nodeId}.${handle} (type ${typeId}): ${JSON.stringify(value)}`;
|
|
1776
|
+
this.eventEmitter.emit("error", {
|
|
1777
|
+
kind: "input-validation",
|
|
1778
|
+
nodeId,
|
|
1779
|
+
handle,
|
|
1780
|
+
typeId,
|
|
1781
|
+
value,
|
|
1782
|
+
message: errorMessage,
|
|
1783
|
+
});
|
|
1784
|
+
// Skip storing invalid value
|
|
1785
|
+
continue;
|
|
1118
1786
|
}
|
|
1119
1787
|
}
|
|
1120
1788
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1789
|
+
const prev = node.inputs[handle];
|
|
1790
|
+
const same = valuesEqual(prev, value);
|
|
1791
|
+
if (!same) {
|
|
1792
|
+
if (value === undefined) {
|
|
1793
|
+
delete node.inputs[handle];
|
|
1794
|
+
}
|
|
1795
|
+
else {
|
|
1796
|
+
node.inputs[handle] = value;
|
|
1797
|
+
}
|
|
1798
|
+
// Emit value event for input updates
|
|
1799
|
+
this.eventEmitter.emit("value", { nodeId, handle, value, io: "input" });
|
|
1800
|
+
anyChanged = true;
|
|
1123
1801
|
}
|
|
1124
|
-
// Merge base with dynamic and overrides (allow partial resolvedHandles)
|
|
1125
|
-
const inputs = {
|
|
1126
|
-
...desc.inputs,
|
|
1127
|
-
...dyn.inputs,
|
|
1128
|
-
...overrideInputs,
|
|
1129
|
-
};
|
|
1130
|
-
const outputs = {
|
|
1131
|
-
...desc.outputs,
|
|
1132
|
-
...dyn.outputs,
|
|
1133
|
-
...overrideOutputs,
|
|
1134
|
-
};
|
|
1135
|
-
const inputDefaults = {
|
|
1136
|
-
...desc.inputDefaults,
|
|
1137
|
-
...dyn.inputDefaults,
|
|
1138
|
-
...overrideDefaults,
|
|
1139
|
-
};
|
|
1140
|
-
out.set(n.nodeId, { inputs, outputs, inputDefaults });
|
|
1141
1802
|
}
|
|
1142
|
-
|
|
1803
|
+
if (!this.paused) {
|
|
1804
|
+
// Only schedule if all inbound inputs are present (or there are none)
|
|
1805
|
+
if (anyChanged && this.executionScheduler.allInboundHaveValue(nodeId))
|
|
1806
|
+
this.executionScheduler.scheduleInputsChangedInternal(nodeId);
|
|
1807
|
+
// Recompute dynamic handles for this node when its direct inputs change
|
|
1808
|
+
if (anyChanged)
|
|
1809
|
+
this.handleResolver.scheduleRecomputeHandles(nodeId);
|
|
1810
|
+
}
|
|
1143
1811
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
return
|
|
1147
|
-
const srcNode = def.nodes.find((n) => n.nodeId === e.source.nodeId);
|
|
1148
|
-
const dstNode = def.nodes.find((n) => n.nodeId === e.target.nodeId);
|
|
1149
|
-
let effectiveTypeId = e.typeId; // Start with original
|
|
1150
|
-
let srcDeclared;
|
|
1151
|
-
let dstDeclared;
|
|
1152
|
-
if (srcNode) {
|
|
1153
|
-
const resolved = resolvedByNode.get(srcNode.nodeId);
|
|
1154
|
-
if (resolved)
|
|
1155
|
-
srcDeclared = resolved.outputs[e.source.handle];
|
|
1156
|
-
}
|
|
1157
|
-
if (!effectiveTypeId) {
|
|
1158
|
-
// Infer if not explicitly set
|
|
1159
|
-
effectiveTypeId = Array.isArray(srcDeclared)
|
|
1160
|
-
? srcDeclared[0]
|
|
1161
|
-
: srcDeclared;
|
|
1162
|
-
}
|
|
1163
|
-
if (dstNode) {
|
|
1164
|
-
const resolved = resolvedByNode.get(dstNode.nodeId);
|
|
1165
|
-
if (resolved)
|
|
1166
|
-
dstDeclared = getInputTypeId(resolved.inputs, e.target.handle);
|
|
1167
|
-
}
|
|
1168
|
-
const { convert, convertAsync } = GraphRuntime.buildEdgeConverters(srcDeclared, dstDeclared, registry, `buildEdges: ${srcNode?.typeId || ""}.${e.source.nodeId}.${e.source.handle} -> ${dstNode?.typeId || ""}.${e.target.nodeId}.${e.target.handle}`);
|
|
1169
|
-
return {
|
|
1170
|
-
id: e.id,
|
|
1171
|
-
source: { ...e.source },
|
|
1172
|
-
target: { ...e.target },
|
|
1173
|
-
typeId: e.typeId, // Preserve original (may be undefined)
|
|
1174
|
-
effectiveTypeId: effectiveTypeId ?? "untyped", // Always present
|
|
1175
|
-
convert,
|
|
1176
|
-
convertAsync,
|
|
1177
|
-
srcUnionTypes: Array.isArray(srcDeclared)
|
|
1178
|
-
? [...srcDeclared]
|
|
1179
|
-
: undefined,
|
|
1180
|
-
dstDeclared,
|
|
1181
|
-
stats: { runs: 0, inFlight: false, progress: 0 },
|
|
1182
|
-
};
|
|
1183
|
-
});
|
|
1812
|
+
getOutput(nodeId, output) {
|
|
1813
|
+
const node = this.graphStructure.getNode(nodeId);
|
|
1814
|
+
return node?.outputs[output];
|
|
1184
1815
|
}
|
|
1185
|
-
|
|
1186
|
-
|
|
1816
|
+
// Update resolved handles for a single node and refresh edge converters/types that touch it
|
|
1817
|
+
updateNodeHandles(nodeId, handles, registry) {
|
|
1818
|
+
const node = this.graphStructure.getNode(nodeId);
|
|
1187
1819
|
if (!node)
|
|
1188
1820
|
return;
|
|
1189
|
-
|
|
1190
|
-
|
|
1821
|
+
const oldResolved = this.graphStructure.getResolvedHandles(nodeId);
|
|
1822
|
+
this.graphStructure.setResolvedHandles(nodeId, handles);
|
|
1823
|
+
// Clear outputs that are no longer valid handles
|
|
1824
|
+
const oldOutputs = oldResolved?.outputs ?? {};
|
|
1825
|
+
const newOutputs = handles.outputs ?? {};
|
|
1826
|
+
const oldOutputHandles = new Set(Object.keys(oldOutputs));
|
|
1827
|
+
const newOutputHandles = new Set(Object.keys(newOutputs));
|
|
1828
|
+
for (const handle of oldOutputHandles) {
|
|
1829
|
+
if (!newOutputHandles.has(handle)) {
|
|
1830
|
+
// Output handle was removed - clear it and emit undefined to invalidate downstream
|
|
1831
|
+
delete node.outputs[handle];
|
|
1832
|
+
this.eventEmitter.emit("value", {
|
|
1833
|
+
nodeId,
|
|
1834
|
+
handle,
|
|
1835
|
+
value: undefined,
|
|
1836
|
+
io: "output",
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1191
1839
|
}
|
|
1192
|
-
|
|
1193
|
-
// Update resolved handles for a single node and refresh edge converters/types that touch it
|
|
1194
|
-
updateNodeHandles(nodeId, handles, registry) {
|
|
1195
|
-
this.resolvedByNode.set(nodeId, handles);
|
|
1840
|
+
const edges = this.graphStructure.getEdges();
|
|
1196
1841
|
// Recompute edge converter/type for edges where this node is source or target
|
|
1197
|
-
for (const e of
|
|
1198
|
-
const srcNode = this.
|
|
1199
|
-
const dstNode = this.
|
|
1842
|
+
for (const e of edges) {
|
|
1843
|
+
const srcNode = this.graphStructure.getNode(e.source.nodeId);
|
|
1844
|
+
const dstNode = this.graphStructure.getNode(e.target.nodeId);
|
|
1200
1845
|
let srcDeclared = e.effectiveTypeId; // Use effectiveTypeId as fallback
|
|
1201
1846
|
let dstDeclared = e.dstDeclared;
|
|
1202
1847
|
const oldDstDeclared = dstDeclared; // Track old value to detect resolution
|
|
1203
1848
|
if (e.source.nodeId === nodeId) {
|
|
1204
|
-
const resolved = this.
|
|
1849
|
+
const resolved = this.graphStructure.getResolvedHandles(nodeId);
|
|
1205
1850
|
srcDeclared = resolved
|
|
1206
1851
|
? resolved.outputs[e.source.handle]
|
|
1207
1852
|
: srcDeclared;
|
|
@@ -1213,38 +1858,42 @@ class GraphRuntime {
|
|
|
1213
1858
|
}
|
|
1214
1859
|
}
|
|
1215
1860
|
if (e.target.nodeId === nodeId) {
|
|
1216
|
-
const resolved = this.
|
|
1861
|
+
const resolved = this.graphStructure.getResolvedHandles(nodeId);
|
|
1217
1862
|
if (resolved) {
|
|
1218
1863
|
dstDeclared = getInputTypeId(resolved.inputs, e.target.handle);
|
|
1219
1864
|
e.dstDeclared = dstDeclared;
|
|
1220
1865
|
}
|
|
1221
1866
|
}
|
|
1222
|
-
const conv =
|
|
1867
|
+
const conv = GraphStructure.buildEdgeConverters(srcDeclared, dstDeclared, registry, `updateNodeHandles: ${srcNode?.typeId || ""}.${e.source.nodeId}.${e.source.handle} -> ${dstNode?.typeId || ""}.${e.target.nodeId}.${e.target.handle}`);
|
|
1223
1868
|
e.convert = conv.convert;
|
|
1224
1869
|
e.convertAsync = conv.convertAsync;
|
|
1225
1870
|
// If target handle was just resolved (was undefined, now has a type), re-propagate values
|
|
1226
1871
|
if (e.target.nodeId === nodeId &&
|
|
1227
1872
|
oldDstDeclared === undefined &&
|
|
1228
1873
|
dstDeclared !== undefined) {
|
|
1229
|
-
const srcNode = this.
|
|
1874
|
+
const srcNode = this.graphStructure.getNode(e.source.nodeId);
|
|
1230
1875
|
if (srcNode) {
|
|
1231
1876
|
const srcValue = srcNode.outputs[e.source.handle];
|
|
1232
1877
|
if (srcValue !== undefined) {
|
|
1233
1878
|
// Re-propagate through the now-resolved edge converter
|
|
1234
|
-
|
|
1879
|
+
// Preserve run-contexts if source node has them
|
|
1880
|
+
const runContextIds = srcNode.activeRunContexts.size > 0
|
|
1881
|
+
? new Set(srcNode.activeRunContexts)
|
|
1882
|
+
: undefined;
|
|
1883
|
+
this.valuePropagator.propagate(e.source.nodeId, e.source.handle, srcValue, runContextIds);
|
|
1235
1884
|
}
|
|
1236
1885
|
}
|
|
1237
1886
|
}
|
|
1238
1887
|
}
|
|
1239
|
-
//
|
|
1240
|
-
this.
|
|
1888
|
+
// Re-emit only valid outputs (after clearing removed ones)
|
|
1889
|
+
this.valuePropagator.reemitNodeOutputs(nodeId);
|
|
1241
1890
|
}
|
|
1242
1891
|
launch(invalidate = false) {
|
|
1243
1892
|
// call onActivated for nodes that implement it
|
|
1244
|
-
for (const node of this.
|
|
1245
|
-
const effectiveInputs = this.getEffectiveInputs(node.nodeId);
|
|
1893
|
+
for (const node of this.graphStructure.getNodes().values()) {
|
|
1894
|
+
const effectiveInputs = this.executionScheduler.getEffectiveInputs(node.nodeId);
|
|
1246
1895
|
const ctrl = new AbortController();
|
|
1247
|
-
const ctx = this.createExecutionContext(node.nodeId, node, effectiveInputs, `${node.nodeId}:init`, ctrl.signal);
|
|
1896
|
+
const ctx = this.executionScheduler.createExecutionContext(node.nodeId, node, effectiveInputs, `${node.nodeId}:init`, ctrl.signal);
|
|
1248
1897
|
if (node.lifecycle?.prepare) {
|
|
1249
1898
|
ctx.log("debug", "prepare-start");
|
|
1250
1899
|
node.lifecycle.prepare(node.params ?? {}, ctx);
|
|
@@ -1254,31 +1903,23 @@ class GraphRuntime {
|
|
|
1254
1903
|
}
|
|
1255
1904
|
if (invalidate) {
|
|
1256
1905
|
// After activation, schedule nodes that have all inbound inputs present
|
|
1257
|
-
for (const nodeId of this.
|
|
1258
|
-
if (this.allInboundHaveValue(nodeId))
|
|
1259
|
-
this.scheduleInputsChangedInternal(nodeId);
|
|
1906
|
+
for (const nodeId of this.graphStructure.getNodes().keys()) {
|
|
1907
|
+
if (this.executionScheduler.allInboundHaveValue(nodeId))
|
|
1908
|
+
this.executionScheduler.scheduleInputsChangedInternal(nodeId);
|
|
1260
1909
|
}
|
|
1261
1910
|
}
|
|
1262
1911
|
}
|
|
1263
1912
|
triggerExternal(nodeId, event) {
|
|
1264
|
-
const node = this.
|
|
1913
|
+
const node = this.graphStructure.getNode(nodeId);
|
|
1265
1914
|
if (!node)
|
|
1266
1915
|
return;
|
|
1267
|
-
//
|
|
1268
|
-
|
|
1269
|
-
typeof event === "object" &&
|
|
1270
|
-
event.type === "invalidate") {
|
|
1271
|
-
if (this.allInboundHaveValue(nodeId))
|
|
1272
|
-
this.scheduleInputsChangedInternal(nodeId);
|
|
1273
|
-
else
|
|
1274
|
-
this.invalidateDownstreamInternal(nodeId);
|
|
1275
|
-
}
|
|
1276
|
-
else {
|
|
1277
|
-
node.runtime.onExternalEvent?.(event, node.state);
|
|
1278
|
-
}
|
|
1916
|
+
// Forward event to node's onExternalEvent handler for custom actions
|
|
1917
|
+
node.runtime.onExternalEvent?.(event, node.state);
|
|
1279
1918
|
}
|
|
1280
1919
|
dispose() {
|
|
1281
|
-
|
|
1920
|
+
// Resolve all pending run-context promises before cleanup
|
|
1921
|
+
this.runContextManager.resolveAll();
|
|
1922
|
+
for (const node of this.graphStructure.getNodes().values()) {
|
|
1282
1923
|
node.runtime.onDeactivated?.();
|
|
1283
1924
|
node.runtime.dispose?.();
|
|
1284
1925
|
node.lifecycle?.dispose?.({
|
|
@@ -1286,17 +1927,14 @@ class GraphRuntime {
|
|
|
1286
1927
|
setState: (next) => Object.assign(node.state, next),
|
|
1287
1928
|
});
|
|
1288
1929
|
}
|
|
1289
|
-
this.
|
|
1290
|
-
this.edges = [];
|
|
1291
|
-
this.listeners.clear();
|
|
1292
|
-
this.arrayInputBuckets.clear();
|
|
1930
|
+
this.graphStructure.clear();
|
|
1293
1931
|
}
|
|
1294
1932
|
getNodeIds() {
|
|
1295
|
-
return Array.from(this.
|
|
1933
|
+
return Array.from(this.graphStructure.getNodes().keys());
|
|
1296
1934
|
}
|
|
1297
1935
|
// Unsafe helpers for serializer: read-only accessors and hydration
|
|
1298
1936
|
getNodeData(nodeId) {
|
|
1299
|
-
const node = this.
|
|
1937
|
+
const node = this.graphStructure.getNode(nodeId);
|
|
1300
1938
|
if (!node)
|
|
1301
1939
|
return undefined;
|
|
1302
1940
|
return {
|
|
@@ -1312,15 +1950,17 @@ class GraphRuntime {
|
|
|
1312
1950
|
}
|
|
1313
1951
|
setEnvironment(env) {
|
|
1314
1952
|
this.environment = { ...env };
|
|
1953
|
+
this.handleResolver.setEnvironment(this.environment);
|
|
1954
|
+
this.executionScheduler.setEnvironment(this.environment);
|
|
1315
1955
|
// Recompute dynamic handles for all nodes when environment changes
|
|
1316
|
-
for (const nodeId of this.
|
|
1317
|
-
this.scheduleRecomputeHandles(nodeId);
|
|
1956
|
+
for (const nodeId of this.graphStructure.getNodes().keys()) {
|
|
1957
|
+
this.handleResolver.scheduleRecomputeHandles(nodeId);
|
|
1318
1958
|
}
|
|
1319
1959
|
}
|
|
1320
1960
|
// Export a GraphDefinition reflecting the current runtime view
|
|
1321
1961
|
getGraphDef() {
|
|
1322
|
-
const nodes = Array.from(this.
|
|
1323
|
-
const resolved = this.
|
|
1962
|
+
const nodes = Array.from(this.graphStructure.getNodes().values()).map((n) => {
|
|
1963
|
+
const resolved = this.graphStructure.getResolvedHandles(n.nodeId);
|
|
1324
1964
|
return {
|
|
1325
1965
|
nodeId: n.nodeId,
|
|
1326
1966
|
typeId: n.typeId,
|
|
@@ -1328,7 +1968,9 @@ class GraphRuntime {
|
|
|
1328
1968
|
resolvedHandles: resolved ? { ...resolved } : undefined,
|
|
1329
1969
|
};
|
|
1330
1970
|
});
|
|
1331
|
-
const edges = this.
|
|
1971
|
+
const edges = this.graphStructure
|
|
1972
|
+
.getEdges()
|
|
1973
|
+
.map((e) => ({
|
|
1332
1974
|
id: e.id,
|
|
1333
1975
|
source: { nodeId: e.source.nodeId, handle: e.source.handle },
|
|
1334
1976
|
target: { nodeId: e.target.nodeId, handle: e.target.handle },
|
|
@@ -1337,8 +1979,23 @@ class GraphRuntime {
|
|
|
1337
1979
|
return { nodes, edges };
|
|
1338
1980
|
}
|
|
1339
1981
|
async whenIdle() {
|
|
1982
|
+
// If we have active run-contexts, wait for all of them to complete
|
|
1983
|
+
const allRunContexts = this.runContextManager.getAllRunContexts();
|
|
1984
|
+
if (allRunContexts.size > 0) {
|
|
1985
|
+
await new Promise((resolve) => {
|
|
1986
|
+
const check = () => {
|
|
1987
|
+
if (this.runContextManager.getAllRunContexts().size === 0) {
|
|
1988
|
+
resolve();
|
|
1989
|
+
}
|
|
1990
|
+
else {
|
|
1991
|
+
setTimeout(check, 10);
|
|
1992
|
+
}
|
|
1993
|
+
};
|
|
1994
|
+
setTimeout(check, 10);
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1340
1997
|
const isIdle = () => {
|
|
1341
|
-
for (const n of this.
|
|
1998
|
+
for (const n of this.graphStructure.getNodes().values()) {
|
|
1342
1999
|
if (n.activeControllers.size > 0)
|
|
1343
2000
|
return false;
|
|
1344
2001
|
if (n.queue.length > 0)
|
|
@@ -1358,6 +2015,56 @@ class GraphRuntime {
|
|
|
1358
2015
|
setTimeout(check, 10);
|
|
1359
2016
|
});
|
|
1360
2017
|
}
|
|
2018
|
+
/**
|
|
2019
|
+
* Run this node and optionally all dynamically reachable downstream nodes as a run-context.
|
|
2020
|
+
* Includes nodes added later behind the same path (via re-emits).
|
|
2021
|
+
* @param startNodeId - The node to start execution from
|
|
2022
|
+
* @param options - Execution options
|
|
2023
|
+
* @param options.skipPropagateValues - If true, don't set inputs of linked nodes (default: false)
|
|
2024
|
+
* @param options.propagate - If false, don't schedule downstream nodes (default: true)
|
|
2025
|
+
*/
|
|
2026
|
+
async runFromHereContext(startNodeId, options) {
|
|
2027
|
+
const node = this.graphStructure.getNode(startNodeId);
|
|
2028
|
+
if (!node) {
|
|
2029
|
+
// Node doesn't exist - resolve immediately
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
const ctx = this.runContextManager.createRunContext(startNodeId, options);
|
|
2033
|
+
// Create promise that resolves when context finishes
|
|
2034
|
+
const promise = new Promise((resolve) => {
|
|
2035
|
+
ctx.resolve = resolve;
|
|
2036
|
+
});
|
|
2037
|
+
// Temporarily unpause if needed
|
|
2038
|
+
const wasPaused = this.paused;
|
|
2039
|
+
if (wasPaused) {
|
|
2040
|
+
this.paused = false;
|
|
2041
|
+
}
|
|
2042
|
+
try {
|
|
2043
|
+
// Seed the start node with this run-context
|
|
2044
|
+
node.activeRunContexts.add(ctx.id);
|
|
2045
|
+
this.scheduleInputsChangedWithRunContexts(startNodeId, new Set([ctx.id]));
|
|
2046
|
+
await promise;
|
|
2047
|
+
}
|
|
2048
|
+
finally {
|
|
2049
|
+
// Restore pause state if it was paused and no other run-contexts are active
|
|
2050
|
+
if (wasPaused && this.runContextManager.getAllRunContexts().size === 0) {
|
|
2051
|
+
this.paused = true;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
/**
|
|
2056
|
+
* Schedule a node with run-context IDs attached
|
|
2057
|
+
*/
|
|
2058
|
+
scheduleInputsChangedWithRunContexts(nodeId, runContextIds) {
|
|
2059
|
+
const node = this.graphStructure.getNode(nodeId);
|
|
2060
|
+
if (!node)
|
|
2061
|
+
return;
|
|
2062
|
+
// Attach run-contexts to the node
|
|
2063
|
+
for (const id of runContextIds) {
|
|
2064
|
+
node.activeRunContexts.add(id);
|
|
2065
|
+
}
|
|
2066
|
+
this.executionScheduler.scheduleInputsChangedInternal(nodeId, runContextIds);
|
|
2067
|
+
}
|
|
1361
2068
|
pause() {
|
|
1362
2069
|
this.paused = true;
|
|
1363
2070
|
}
|
|
@@ -1368,80 +2075,23 @@ class GraphRuntime {
|
|
|
1368
2075
|
this.paused = false;
|
|
1369
2076
|
}
|
|
1370
2077
|
invalidateDownstream(nodeId) {
|
|
1371
|
-
this.
|
|
2078
|
+
this.valuePropagator.reemitNodeOutputs(nodeId);
|
|
1372
2079
|
}
|
|
1373
2080
|
scheduleInputsChanged(nodeId) {
|
|
1374
|
-
this.scheduleInputsChangedInternal(nodeId);
|
|
1375
|
-
}
|
|
1376
|
-
/**
|
|
1377
|
-
* Cancel all active runs for a node and emit cancellation events
|
|
1378
|
-
* @param node - The node to cancel runs for
|
|
1379
|
-
* @param reason - The cancellation reason ("snapshot" | "node-deleted")
|
|
1380
|
-
*/
|
|
1381
|
-
cancelNodeActiveRuns(node, reason) {
|
|
1382
|
-
for (const controller of Array.from(node.activeControllers)) {
|
|
1383
|
-
const runId = node.controllerRunIds.get(controller);
|
|
1384
|
-
if (runId) {
|
|
1385
|
-
// Track cancelled runIds for snapshot operations
|
|
1386
|
-
if (reason === "snapshot") {
|
|
1387
|
-
if (!node.snapshotCancelledRunIds) {
|
|
1388
|
-
node.snapshotCancelledRunIds = new Set();
|
|
1389
|
-
}
|
|
1390
|
-
node.snapshotCancelledRunIds.add(runId);
|
|
1391
|
-
}
|
|
1392
|
-
// Emit cancellation event
|
|
1393
|
-
this.emit("stats", {
|
|
1394
|
-
kind: "node-done",
|
|
1395
|
-
nodeId: node.nodeId,
|
|
1396
|
-
typeId: node.typeId,
|
|
1397
|
-
runId,
|
|
1398
|
-
cancelled: true,
|
|
1399
|
-
});
|
|
1400
|
-
}
|
|
1401
|
-
try {
|
|
1402
|
-
controller.abort(reason);
|
|
1403
|
-
}
|
|
1404
|
-
catch {
|
|
1405
|
-
// ignore abort errors
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
node.activeControllers.clear();
|
|
1409
|
-
node.controllerRunIds.clear();
|
|
1410
|
-
node.stats.active = 0;
|
|
1411
|
-
node.queue = [];
|
|
2081
|
+
this.executionScheduler.scheduleInputsChangedInternal(nodeId);
|
|
1412
2082
|
}
|
|
1413
2083
|
cancelNodeRuns(nodeIds) {
|
|
1414
|
-
|
|
2084
|
+
this.executionScheduler.cancelNodeRuns(nodeIds);
|
|
2085
|
+
}
|
|
2086
|
+
copyOutputs(fromNodeId, toNodeId, options) {
|
|
2087
|
+
// Get outputs from source node
|
|
2088
|
+
const fromNode = this.getNodeData(fromNodeId);
|
|
2089
|
+
if (!fromNode?.outputs)
|
|
1415
2090
|
return;
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
for (let i = 0; i < queue.length; i++) {
|
|
1421
|
-
const nodeId = queue[i];
|
|
1422
|
-
if (visited.has(nodeId))
|
|
1423
|
-
continue;
|
|
1424
|
-
visited.add(nodeId);
|
|
1425
|
-
for (const edge of this.edges) {
|
|
1426
|
-
if (edge.source.nodeId === nodeId) {
|
|
1427
|
-
const targetId = edge.target.nodeId;
|
|
1428
|
-
if (!visited.has(targetId)) {
|
|
1429
|
-
toCancel.add(targetId);
|
|
1430
|
-
queue.push(targetId);
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
// Cancel runs for all affected nodes
|
|
1436
|
-
for (const nodeId of toCancel) {
|
|
1437
|
-
const node = this.nodes.get(nodeId);
|
|
1438
|
-
if (!node)
|
|
1439
|
-
continue;
|
|
1440
|
-
this.cancelNodeActiveRuns(node, "snapshot");
|
|
1441
|
-
node.runSeq += 1;
|
|
1442
|
-
const now = Date.now();
|
|
1443
|
-
node.latestRunId = `${nodeId}:${node.runSeq}:${now}:snapshot`;
|
|
1444
|
-
}
|
|
2091
|
+
// Copy outputs to target node using hydrate
|
|
2092
|
+
// hydrate already pauses internally, so we don't need to handle dry option here
|
|
2093
|
+
// reemit: !options?.dry means don't propagate downstream if dry mode
|
|
2094
|
+
this.hydrate({ outputs: { [toNodeId]: { ...fromNode.outputs } } }, { reemit: !options?.dry });
|
|
1445
2095
|
}
|
|
1446
2096
|
// Hydrate inputs/outputs without triggering computation; optionally re-emit outputs downstream
|
|
1447
2097
|
hydrate(payload, opts) {
|
|
@@ -1450,13 +2100,13 @@ class GraphRuntime {
|
|
|
1450
2100
|
try {
|
|
1451
2101
|
const ins = payload?.inputs || {};
|
|
1452
2102
|
for (const [nodeId, map] of Object.entries(ins)) {
|
|
1453
|
-
const node = this.
|
|
2103
|
+
const node = this.graphStructure.getNode(nodeId);
|
|
1454
2104
|
if (!node)
|
|
1455
2105
|
continue;
|
|
1456
2106
|
for (const [h, v] of Object.entries(map || {})) {
|
|
1457
2107
|
node.inputs[h] = structuredClone(v);
|
|
1458
2108
|
// emit input value event
|
|
1459
|
-
this.emit("value", {
|
|
2109
|
+
this.eventEmitter.emit("value", {
|
|
1460
2110
|
nodeId,
|
|
1461
2111
|
handle: h,
|
|
1462
2112
|
value: node.inputs[h],
|
|
@@ -1467,13 +2117,13 @@ class GraphRuntime {
|
|
|
1467
2117
|
}
|
|
1468
2118
|
const outs = payload?.outputs || {};
|
|
1469
2119
|
for (const [nodeId, map] of Object.entries(outs)) {
|
|
1470
|
-
const node = this.
|
|
2120
|
+
const node = this.graphStructure.getNode(nodeId);
|
|
1471
2121
|
if (!node)
|
|
1472
2122
|
continue;
|
|
1473
2123
|
for (const [h, v] of Object.entries(map || {})) {
|
|
1474
2124
|
node.outputs[h] = structuredClone(v);
|
|
1475
2125
|
// emit output value event
|
|
1476
|
-
this.emit("value", {
|
|
2126
|
+
this.eventEmitter.emit("value", {
|
|
1477
2127
|
nodeId,
|
|
1478
2128
|
handle: h,
|
|
1479
2129
|
value: node.outputs[h],
|
|
@@ -1483,8 +2133,9 @@ class GraphRuntime {
|
|
|
1483
2133
|
}
|
|
1484
2134
|
}
|
|
1485
2135
|
if (opts?.reemit) {
|
|
1486
|
-
for (const nodeId of this.
|
|
1487
|
-
this.reemitNodeOutputs(nodeId);
|
|
2136
|
+
for (const nodeId of this.graphStructure.getNodes().keys()) {
|
|
2137
|
+
this.valuePropagator.reemitNodeOutputs(nodeId);
|
|
2138
|
+
}
|
|
1488
2139
|
}
|
|
1489
2140
|
}
|
|
1490
2141
|
finally {
|
|
@@ -1495,13 +2146,23 @@ class GraphRuntime {
|
|
|
1495
2146
|
update(def, registry) {
|
|
1496
2147
|
// Handle node additions and removals
|
|
1497
2148
|
const desiredIds = new Set(def.nodes.map((n) => n.nodeId));
|
|
1498
|
-
const currentIds = new Set(this.
|
|
2149
|
+
const currentIds = new Set(this.graphStructure.getNodes().keys());
|
|
1499
2150
|
// Remove nodes not present
|
|
1500
2151
|
for (const nodeId of Array.from(currentIds)) {
|
|
1501
2152
|
if (!desiredIds.has(nodeId)) {
|
|
1502
|
-
const node = this.
|
|
2153
|
+
const node = this.graphStructure.getNode(nodeId);
|
|
1503
2154
|
// Cancel all active runs and emit cancellation events
|
|
1504
|
-
this.cancelNodeActiveRuns(node, "node-deleted");
|
|
2155
|
+
this.executionScheduler.cancelNodeActiveRuns(node, "node-deleted");
|
|
2156
|
+
// Cancel node in all run-contexts (marks it as cancelled and clears activeRunContexts)
|
|
2157
|
+
this.runContextManager.cancelNodeInRunContexts(nodeId,
|
|
2158
|
+
/* includeDownstream */ true, this.graphStructure.getEdges(), this.graphStructure.getNodes());
|
|
2159
|
+
// Check for run-context completion (they may finish if pending reaches 0)
|
|
2160
|
+
const allRunContexts = this.runContextManager.getAllRunContexts();
|
|
2161
|
+
for (const ctx of Array.from(allRunContexts.values())) {
|
|
2162
|
+
if (ctx.pending === 0) {
|
|
2163
|
+
this.runContextManager.finishRunContext(ctx.id, this.graphStructure.getNodes());
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
1505
2166
|
// Cleanup node resources
|
|
1506
2167
|
node.runtime.onDeactivated?.();
|
|
1507
2168
|
node.runtime.dispose?.();
|
|
@@ -1509,13 +2170,13 @@ class GraphRuntime {
|
|
|
1509
2170
|
state: node.state,
|
|
1510
2171
|
setState: (next) => Object.assign(node.state, next),
|
|
1511
2172
|
});
|
|
1512
|
-
this.
|
|
1513
|
-
this.
|
|
2173
|
+
this.graphStructure.deleteNode(nodeId);
|
|
2174
|
+
this.valuePropagator.clearArrayBuckets(nodeId);
|
|
1514
2175
|
}
|
|
1515
2176
|
}
|
|
1516
2177
|
// Add or update existing nodes
|
|
1517
2178
|
for (const n of def.nodes) {
|
|
1518
|
-
const existing = this.
|
|
2179
|
+
const existing = this.graphStructure.getNode(n.nodeId);
|
|
1519
2180
|
if (!existing) {
|
|
1520
2181
|
// create new runtime node
|
|
1521
2182
|
const desc = registry.nodes.get(n.typeId);
|
|
@@ -1554,12 +2215,13 @@ class GraphRuntime {
|
|
|
1554
2215
|
queued: 0,
|
|
1555
2216
|
progress: 0,
|
|
1556
2217
|
},
|
|
2218
|
+
activeRunContexts: new Set(),
|
|
1557
2219
|
};
|
|
1558
|
-
this.
|
|
2220
|
+
this.graphStructure.setNode(n.nodeId, rn);
|
|
1559
2221
|
// Activate new node
|
|
1560
|
-
const effectiveInputs = this.getEffectiveInputs(rn.nodeId);
|
|
2222
|
+
const effectiveInputs = this.executionScheduler.getEffectiveInputs(rn.nodeId);
|
|
1561
2223
|
const ctrl = new AbortController();
|
|
1562
|
-
const ctx = this.createExecutionContext(rn.nodeId, rn, effectiveInputs, `${rn.nodeId}:init`, ctrl.signal);
|
|
2224
|
+
const ctx = this.executionScheduler.createExecutionContext(rn.nodeId, rn, effectiveInputs, `${rn.nodeId}:init`, ctrl.signal);
|
|
1563
2225
|
if (rn.lifecycle?.prepare) {
|
|
1564
2226
|
ctx.log("debug", "prepare-start");
|
|
1565
2227
|
rn.lifecycle.prepare(rn.params ?? {}, ctx);
|
|
@@ -1581,15 +2243,16 @@ class GraphRuntime {
|
|
|
1581
2243
|
}
|
|
1582
2244
|
}
|
|
1583
2245
|
// Capture previous inbound map before rebuilding edges
|
|
2246
|
+
const edges = this.graphStructure.getEdges();
|
|
1584
2247
|
const prevInbound = new Map();
|
|
1585
|
-
for (const e of
|
|
2248
|
+
for (const e of edges) {
|
|
1586
2249
|
const set = prevInbound.get(e.target.nodeId) ?? new Set();
|
|
1587
2250
|
set.add(e.target.handle);
|
|
1588
2251
|
prevInbound.set(e.target.nodeId, set);
|
|
1589
2252
|
}
|
|
1590
2253
|
// Capture previous per-handle target sets before rebuilding edges
|
|
1591
2254
|
const prevOutTargets = new Map();
|
|
1592
|
-
for (const e of
|
|
2255
|
+
for (const e of edges) {
|
|
1593
2256
|
const tmap = prevOutTargets.get(e.source.nodeId) ?? new Map();
|
|
1594
2257
|
const tset = tmap.get(e.source.handle) ?? new Set();
|
|
1595
2258
|
tset.add(`${e.target.nodeId}.${e.target.handle}`);
|
|
@@ -1597,22 +2260,27 @@ class GraphRuntime {
|
|
|
1597
2260
|
prevOutTargets.set(e.source.nodeId, tmap);
|
|
1598
2261
|
}
|
|
1599
2262
|
// Precompute per-node resolved handles for updated graph (include dynamic)
|
|
1600
|
-
const resolved =
|
|
2263
|
+
const resolved = GraphStructure.computeResolvedHandleMap(def, registry, this.environment);
|
|
1601
2264
|
// Check which handles changed and emit events for those
|
|
1602
2265
|
const changedHandles = {};
|
|
1603
2266
|
for (const [nodeId, newHandles] of resolved.map) {
|
|
1604
|
-
const oldHandles = this.
|
|
2267
|
+
const oldHandles = this.graphStructure.getResolvedHandles(nodeId);
|
|
1605
2268
|
if (!oldHandles ||
|
|
1606
2269
|
JSON.stringify(oldHandles) !== JSON.stringify(newHandles)) {
|
|
1607
2270
|
changedHandles[nodeId] = newHandles;
|
|
1608
2271
|
}
|
|
1609
2272
|
}
|
|
1610
|
-
|
|
2273
|
+
// Update resolved handles
|
|
2274
|
+
for (const [nodeId, handles] of resolved.map) {
|
|
2275
|
+
this.graphStructure.setResolvedHandles(nodeId, handles);
|
|
2276
|
+
}
|
|
1611
2277
|
// Rebuild edges mapping with coercions
|
|
1612
|
-
|
|
2278
|
+
const newEdges = GraphStructure.buildEdges(def, registry, this.graphStructure.getResolvedHandlesMap());
|
|
2279
|
+
this.graphStructure.setEdges(newEdges);
|
|
1613
2280
|
// Build new inbound map
|
|
1614
2281
|
const nextInbound = new Map();
|
|
1615
|
-
|
|
2282
|
+
const updatedEdges = this.graphStructure.getEdges();
|
|
2283
|
+
for (const e of updatedEdges) {
|
|
1616
2284
|
const set = nextInbound.get(e.target.nodeId) ?? new Set();
|
|
1617
2285
|
set.add(e.target.handle);
|
|
1618
2286
|
nextInbound.set(e.target.nodeId, set);
|
|
@@ -1620,7 +2288,7 @@ class GraphRuntime {
|
|
|
1620
2288
|
// For inputs that lost inbound connections, clear and schedule recompute
|
|
1621
2289
|
for (const [nodeId, prevSet] of prevInbound) {
|
|
1622
2290
|
const currSet = nextInbound.get(nodeId) ?? new Set();
|
|
1623
|
-
const node = this.
|
|
2291
|
+
const node = this.graphStructure.getNode(nodeId);
|
|
1624
2292
|
if (!node)
|
|
1625
2293
|
continue;
|
|
1626
2294
|
let changed = false;
|
|
@@ -1633,22 +2301,14 @@ class GraphRuntime {
|
|
|
1633
2301
|
}
|
|
1634
2302
|
}
|
|
1635
2303
|
if (changed) {
|
|
1636
|
-
// Clear buckets for handles that lost inbound
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
for (const handle of Array.from(prevSet)) {
|
|
1640
|
-
if (!currSet.has(handle))
|
|
1641
|
-
bucketsForNode.delete(handle);
|
|
1642
|
-
}
|
|
1643
|
-
if (bucketsForNode.size === 0)
|
|
1644
|
-
this.arrayInputBuckets.delete(nodeId);
|
|
1645
|
-
}
|
|
1646
|
-
this.scheduleInputsChangedInternal(nodeId);
|
|
2304
|
+
// Clear buckets for handles that lost inbound (handled by ValuePropagator)
|
|
2305
|
+
this.valuePropagator.clearArrayBuckets(nodeId);
|
|
2306
|
+
this.executionScheduler.scheduleInputsChangedInternal(nodeId);
|
|
1647
2307
|
}
|
|
1648
2308
|
}
|
|
1649
2309
|
// Re-emit outputs when per-handle target sets change (precise and simple)
|
|
1650
2310
|
const nextOutTargets = new Map();
|
|
1651
|
-
for (const e of
|
|
2311
|
+
for (const e of updatedEdges) {
|
|
1652
2312
|
const tmap = nextOutTargets.get(e.source.nodeId) ?? new Map();
|
|
1653
2313
|
const tset = tmap.get(e.source.handle) ?? new Set();
|
|
1654
2314
|
tset.add(`${e.target.nodeId}.${e.target.handle}`);
|
|
@@ -1684,119 +2344,26 @@ class GraphRuntime {
|
|
|
1684
2344
|
if (!setsEqualStr(pset, nset)) {
|
|
1685
2345
|
const val = this.getOutput(nodeId, handle);
|
|
1686
2346
|
if (val !== undefined)
|
|
1687
|
-
this.propagate(nodeId, handle, val);
|
|
1688
|
-
else if (this.allInboundHaveValue(nodeId))
|
|
1689
|
-
this.scheduleInputsChangedInternal(nodeId);
|
|
2347
|
+
this.valuePropagator.propagate(nodeId, handle, val);
|
|
2348
|
+
else if (this.executionScheduler.allInboundHaveValue(nodeId))
|
|
2349
|
+
this.executionScheduler.scheduleInputsChangedInternal(nodeId);
|
|
1690
2350
|
}
|
|
1691
2351
|
}
|
|
1692
2352
|
}
|
|
1693
2353
|
// Prune array bucket contributions for edges that no longer exist
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
const m = validPerTarget.get(ed.target.nodeId) ?? new Map();
|
|
1697
|
-
const s = m.get(ed.target.handle) ?? new Set();
|
|
1698
|
-
s.add(ed.id);
|
|
1699
|
-
m.set(ed.target.handle, s);
|
|
1700
|
-
validPerTarget.set(ed.target.nodeId, m);
|
|
1701
|
-
}
|
|
1702
|
-
for (const [nodeId, byHandle] of Array.from(this.arrayInputBuckets)) {
|
|
1703
|
-
const validHandles = validPerTarget.get(nodeId) ?? new Map();
|
|
1704
|
-
for (const [handle, perEdge] of Array.from(byHandle)) {
|
|
1705
|
-
const validEdgeIds = validHandles.get(handle) ?? new Set();
|
|
1706
|
-
for (const edgeId of Array.from(perEdge.keys())) {
|
|
1707
|
-
if (!validEdgeIds.has(edgeId))
|
|
1708
|
-
perEdge.delete(edgeId);
|
|
1709
|
-
}
|
|
1710
|
-
if (perEdge.size === 0)
|
|
1711
|
-
byHandle.delete(handle);
|
|
1712
|
-
}
|
|
1713
|
-
if (byHandle.size === 0)
|
|
1714
|
-
this.arrayInputBuckets.delete(nodeId);
|
|
1715
|
-
}
|
|
2354
|
+
// This is handled by ValuePropagator - array buckets are managed there
|
|
2355
|
+
// The buckets will be cleaned up automatically when edges are removed
|
|
1716
2356
|
// Schedule async recompute for nodes that indicated Promise-based resolveHandles in this update
|
|
1717
2357
|
// Emit event for changed handles (if any)
|
|
1718
2358
|
if (Object.keys(changedHandles).length > 0) {
|
|
1719
|
-
this.emit("invalidate", {
|
|
2359
|
+
this.eventEmitter.emit("invalidate", {
|
|
1720
2360
|
reason: "graph-updated",
|
|
1721
2361
|
resolvedHandles: changedHandles,
|
|
1722
2362
|
});
|
|
1723
2363
|
}
|
|
1724
2364
|
for (const nodeId of resolved.pending) {
|
|
1725
|
-
this.scheduleRecomputeHandles(nodeId);
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
// Schedule a recomputation of dynamic handles for a node (async to avoid mutating during propagation)
|
|
1729
|
-
scheduleRecomputeHandles(nodeId) {
|
|
1730
|
-
// If no registry or node not found, skip
|
|
1731
|
-
if (!this.registry)
|
|
1732
|
-
return;
|
|
1733
|
-
const node = this.nodes.get(nodeId);
|
|
1734
|
-
if (!node)
|
|
1735
|
-
return;
|
|
1736
|
-
setTimeout(() => {
|
|
1737
|
-
void this.recomputeHandlesForNode(nodeId);
|
|
1738
|
-
}, 0);
|
|
1739
|
-
}
|
|
1740
|
-
// Recompute dynamic handles for a single node using current inputs/environment
|
|
1741
|
-
async recomputeHandlesForNode(nodeId) {
|
|
1742
|
-
const registry = this.registry;
|
|
1743
|
-
const node = this.nodes.get(nodeId);
|
|
1744
|
-
if (!node)
|
|
1745
|
-
return;
|
|
1746
|
-
const desc = registry.nodes.get(node.typeId);
|
|
1747
|
-
if (!desc)
|
|
1748
|
-
return;
|
|
1749
|
-
const resolveHandles = desc.resolveHandles;
|
|
1750
|
-
if (typeof resolveHandles !== "function")
|
|
1751
|
-
return;
|
|
1752
|
-
const token = (this.recomputeTokenByNode.get(nodeId) ?? 0) + 1;
|
|
1753
|
-
this.recomputeTokenByNode.set(nodeId, token);
|
|
1754
|
-
// Log resolveHandles-start
|
|
1755
|
-
const nodeLogLevel = node.logLevel ?? "info";
|
|
1756
|
-
const nodeLogValue = LOG_LEVEL_VALUES[nodeLogLevel] ?? 1;
|
|
1757
|
-
const shouldLog = nodeLogValue <= LOG_LEVEL_VALUES.debug && nodeLogLevel !== "silent";
|
|
1758
|
-
if (shouldLog) {
|
|
1759
|
-
console.info(`[node:${nodeId}:${node.typeId}] resolveHandles-start`);
|
|
1760
|
-
}
|
|
1761
|
-
let r;
|
|
1762
|
-
try {
|
|
1763
|
-
const res = resolveHandles({
|
|
1764
|
-
nodeId,
|
|
1765
|
-
environment: this.environment || {},
|
|
1766
|
-
params: node.params,
|
|
1767
|
-
inputs: node.inputs || {},
|
|
1768
|
-
});
|
|
1769
|
-
r = await unwrapMaybePromise(res);
|
|
1770
|
-
}
|
|
1771
|
-
catch {
|
|
1772
|
-
// Log resolveHandles-done even on error
|
|
1773
|
-
if (shouldLog) {
|
|
1774
|
-
console.info(`[node:${nodeId}:${node.typeId}] resolveHandles-done (error)`);
|
|
1775
|
-
}
|
|
1776
|
-
return;
|
|
1777
|
-
}
|
|
1778
|
-
// Log resolveHandles-done
|
|
1779
|
-
if (shouldLog) {
|
|
1780
|
-
console.info(`[node:${nodeId}:${node.typeId}] resolveHandles-done`);
|
|
2365
|
+
this.handleResolver.scheduleRecomputeHandles(nodeId);
|
|
1781
2366
|
}
|
|
1782
|
-
// If a newer recompute was scheduled, drop this result
|
|
1783
|
-
if ((this.recomputeTokenByNode.get(nodeId) ?? 0) !== token)
|
|
1784
|
-
return;
|
|
1785
|
-
const inputs = { ...desc.inputs, ...r?.inputs };
|
|
1786
|
-
const outputs = { ...desc.outputs, ...r?.outputs };
|
|
1787
|
-
const inputDefaults = { ...desc.inputDefaults, ...r?.inputDefaults };
|
|
1788
|
-
const next = { inputs, outputs, inputDefaults };
|
|
1789
|
-
const before = this.resolvedByNode.get(nodeId);
|
|
1790
|
-
// Compare shallow-structurally via JSON
|
|
1791
|
-
if (JSON.stringify(before) === JSON.stringify(next))
|
|
1792
|
-
return;
|
|
1793
|
-
this.resolvedByNode.set(nodeId, next);
|
|
1794
|
-
this.updateNodeHandles(nodeId, next, registry);
|
|
1795
|
-
// Notify graph updated with the changed handles
|
|
1796
|
-
this.emit("invalidate", {
|
|
1797
|
-
reason: "graph-updated",
|
|
1798
|
-
resolvedHandles: { [nodeId]: next },
|
|
1799
|
-
});
|
|
1800
2367
|
}
|
|
1801
2368
|
}
|
|
1802
2369
|
|
|
@@ -2022,14 +2589,39 @@ class AbstractEngine {
|
|
|
2022
2589
|
constructor(graphRuntime) {
|
|
2023
2590
|
this.graphRuntime = graphRuntime;
|
|
2024
2591
|
}
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2592
|
+
setInputs(nodeId, inputs, options) {
|
|
2593
|
+
if (options?.dry) {
|
|
2594
|
+
const wasPaused = this.graphRuntime.isPaused();
|
|
2595
|
+
if (!wasPaused)
|
|
2596
|
+
this.graphRuntime.pause();
|
|
2597
|
+
try {
|
|
2598
|
+
this.graphRuntime.setInputs(nodeId, inputs);
|
|
2599
|
+
}
|
|
2600
|
+
finally {
|
|
2601
|
+
if (!wasPaused)
|
|
2602
|
+
this.graphRuntime.resume();
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
else {
|
|
2606
|
+
this.graphRuntime.setInputs(nodeId, inputs);
|
|
2607
|
+
}
|
|
2030
2608
|
}
|
|
2031
|
-
triggerExternal(nodeId, event) {
|
|
2032
|
-
|
|
2609
|
+
triggerExternal(nodeId, event, options) {
|
|
2610
|
+
if (options?.dry) {
|
|
2611
|
+
const wasPaused = this.graphRuntime.isPaused();
|
|
2612
|
+
if (!wasPaused)
|
|
2613
|
+
this.graphRuntime.pause();
|
|
2614
|
+
try {
|
|
2615
|
+
this.graphRuntime.triggerExternal(nodeId, event);
|
|
2616
|
+
}
|
|
2617
|
+
finally {
|
|
2618
|
+
if (!wasPaused)
|
|
2619
|
+
this.graphRuntime.resume();
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
else {
|
|
2623
|
+
this.graphRuntime.triggerExternal(nodeId, event);
|
|
2624
|
+
}
|
|
2033
2625
|
}
|
|
2034
2626
|
on(event, handler) {
|
|
2035
2627
|
return this.graphRuntime.on(event, handler);
|
|
@@ -2040,205 +2632,68 @@ class AbstractEngine {
|
|
|
2040
2632
|
whenIdle() {
|
|
2041
2633
|
return this.graphRuntime.whenIdle();
|
|
2042
2634
|
}
|
|
2635
|
+
cancelNodeRuns(nodeIds) {
|
|
2636
|
+
this.graphRuntime.cancelNodeRuns(nodeIds);
|
|
2637
|
+
}
|
|
2638
|
+
copyOutputs(fromNodeId, toNodeId, options) {
|
|
2639
|
+
this.graphRuntime.copyOutputs(fromNodeId, toNodeId, options);
|
|
2640
|
+
}
|
|
2043
2641
|
dispose() {
|
|
2044
2642
|
// this.graphRuntime.dispose();
|
|
2045
2643
|
}
|
|
2046
2644
|
}
|
|
2047
2645
|
|
|
2048
|
-
|
|
2049
|
-
|
|
2646
|
+
/**
|
|
2647
|
+
* Unified Engine implementation that handles both manual and auto run modes.
|
|
2648
|
+
* - Manual mode: Runtime is paused, nodes execute only when explicitly called via computeNode/runFromHere
|
|
2649
|
+
* - Auto mode: Runtime is resumed, nodes automatically execute when inputs change
|
|
2650
|
+
*/
|
|
2651
|
+
class UnifiedEngine extends AbstractEngine {
|
|
2652
|
+
constructor(graphRuntime, runMode) {
|
|
2050
2653
|
super(graphRuntime);
|
|
2654
|
+
this.runMode = "manual";
|
|
2655
|
+
this.setRunMode(runMode ?? "manual");
|
|
2051
2656
|
}
|
|
2052
|
-
launch() {
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
}
|
|
2057
|
-
|
|
2058
|
-
class BatchedEngine extends AbstractEngine {
|
|
2059
|
-
constructor(graphRuntime, opts = {}) {
|
|
2060
|
-
super(graphRuntime);
|
|
2061
|
-
this.opts = opts;
|
|
2062
|
-
this.dirtyNodes = new Set();
|
|
2657
|
+
launch(invalidate, runMode) {
|
|
2658
|
+
if (runMode)
|
|
2659
|
+
this.setRunMode(runMode);
|
|
2660
|
+
this.graphRuntime.launch(invalidate);
|
|
2063
2661
|
}
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2662
|
+
/**
|
|
2663
|
+
* Run only this node, no downstream propagation.
|
|
2664
|
+
* Works in both modes, but typically only used in manual mode.
|
|
2665
|
+
* @param nodeId - The node to run
|
|
2666
|
+
* @param options - Execution options
|
|
2667
|
+
* @param options.skipPropagateValues - If true, don't set inputs of linked nodes (default: false)
|
|
2668
|
+
*/
|
|
2669
|
+
async computeNode(nodeId, options) {
|
|
2670
|
+
await this.graphRuntime.runFromHereContext(nodeId, {
|
|
2671
|
+
skipPropagateValues: options?.skipPropagateValues ?? false,
|
|
2672
|
+
propagate: false, // Don't schedule downstream nodes
|
|
2673
|
+
});
|
|
2069
2674
|
}
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2675
|
+
/**
|
|
2676
|
+
* Run this node and all dynamically reachable downstream nodes.
|
|
2677
|
+
* Works in both modes, but typically only used in manual mode.
|
|
2678
|
+
* Uses run-context system for dynamic graph updates.
|
|
2679
|
+
*/
|
|
2680
|
+
async runFromHere(nodeId) {
|
|
2681
|
+
await this.graphRuntime.runFromHereContext(nodeId);
|
|
2073
2682
|
}
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
this.dirtyNodes.add(nodeId);
|
|
2683
|
+
getRunMode() {
|
|
2684
|
+
return this.runMode;
|
|
2077
2685
|
}
|
|
2078
|
-
|
|
2079
|
-
if (this.
|
|
2686
|
+
setRunMode(runMode) {
|
|
2687
|
+
if (this.runMode === runMode)
|
|
2080
2688
|
return;
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
this.graphRuntime.resume();
|
|
2085
|
-
for (const n of nodes)
|
|
2086
|
-
this.graphRuntime.scheduleInputsChanged(n);
|
|
2087
|
-
await this.graphRuntime.whenIdle();
|
|
2088
|
-
this.graphRuntime.pause();
|
|
2089
|
-
}
|
|
2090
|
-
dispose() {
|
|
2091
|
-
if (this.timer)
|
|
2092
|
-
clearInterval(this.timer);
|
|
2093
|
-
super.dispose();
|
|
2094
|
-
}
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
// PullEngine computes only when asked, otherwise holds inputs without scheduling
|
|
2098
|
-
class PullEngine extends AbstractEngine {
|
|
2099
|
-
constructor(graphRuntime) {
|
|
2100
|
-
super(graphRuntime);
|
|
2101
|
-
this.graphRuntime.pause();
|
|
2102
|
-
}
|
|
2103
|
-
launch() { }
|
|
2104
|
-
// Pull API
|
|
2105
|
-
async computeNode(nodeId) {
|
|
2106
|
-
this.graphRuntime.resume();
|
|
2107
|
-
this.graphRuntime.scheduleInputsChanged(nodeId);
|
|
2108
|
-
await this.graphRuntime.whenIdle();
|
|
2109
|
-
this.graphRuntime.pause();
|
|
2110
|
-
}
|
|
2111
|
-
}
|
|
2112
|
-
|
|
2113
|
-
class HybridEngine extends AbstractEngine {
|
|
2114
|
-
constructor(graphRuntime, opts = {}) {
|
|
2115
|
-
super(graphRuntime);
|
|
2116
|
-
this.opts = opts;
|
|
2117
|
-
this.windowStart = 0;
|
|
2118
|
-
this.countInWindow = 0;
|
|
2119
|
-
this.batching = false;
|
|
2120
|
-
this.dirtyNodes = new Set();
|
|
2121
|
-
this.windowStart = Date.now();
|
|
2122
|
-
}
|
|
2123
|
-
updateWindow() {
|
|
2124
|
-
const now = Date.now();
|
|
2125
|
-
const windowMs = this.opts.windowMs ?? 250;
|
|
2126
|
-
if (now - this.windowStart > windowMs) {
|
|
2127
|
-
this.windowStart = now;
|
|
2128
|
-
this.countInWindow = 0;
|
|
2129
|
-
if (this.batching) {
|
|
2130
|
-
this.graphRuntime.resume();
|
|
2131
|
-
this.batching = false;
|
|
2132
|
-
// schedule all dirty nodes accumulated during batching
|
|
2133
|
-
const nodes = Array.from(this.dirtyNodes);
|
|
2134
|
-
this.dirtyNodes.clear();
|
|
2135
|
-
for (const n of nodes)
|
|
2136
|
-
this.graphRuntime.scheduleInputsChanged(n);
|
|
2137
|
-
if (this.flushTimer) {
|
|
2138
|
-
clearTimeout(this.flushTimer);
|
|
2139
|
-
this.flushTimer = undefined;
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
|
-
}
|
|
2143
|
-
}
|
|
2144
|
-
launch() {
|
|
2145
|
-
this.graphRuntime.resume();
|
|
2146
|
-
}
|
|
2147
|
-
setInputs(nodeId, inputs) {
|
|
2148
|
-
this.updateWindow();
|
|
2149
|
-
this.countInWindow += 1;
|
|
2150
|
-
const threshold = this.opts.batchThreshold ?? 5;
|
|
2151
|
-
if (!this.batching && this.countInWindow >= threshold) {
|
|
2689
|
+
this.runMode = runMode;
|
|
2690
|
+
// Update runtime pause/resume state based on new mode
|
|
2691
|
+
if (runMode === "manual") {
|
|
2152
2692
|
this.graphRuntime.pause();
|
|
2153
|
-
this.batching = true;
|
|
2154
|
-
// ensure flush even if no more inputs arrive
|
|
2155
|
-
const windowMs = this.opts.windowMs ?? 250;
|
|
2156
|
-
if (this.flushTimer)
|
|
2157
|
-
clearTimeout(this.flushTimer);
|
|
2158
|
-
this.flushTimer = setTimeout(() => {
|
|
2159
|
-
if (!this.batching)
|
|
2160
|
-
return;
|
|
2161
|
-
this.graphRuntime.resume();
|
|
2162
|
-
this.batching = false;
|
|
2163
|
-
const nodes = Array.from(this.dirtyNodes);
|
|
2164
|
-
this.dirtyNodes.clear();
|
|
2165
|
-
for (const n of nodes)
|
|
2166
|
-
this.graphRuntime.scheduleInputsChanged(n);
|
|
2167
|
-
this.flushTimer = undefined;
|
|
2168
|
-
}, windowMs);
|
|
2169
2693
|
}
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
if (!this.batching)
|
|
2173
|
-
this.graphRuntime.scheduleInputsChanged(nodeId);
|
|
2174
|
-
}
|
|
2175
|
-
triggerExternal(nodeId, event) {
|
|
2176
|
-
super.triggerExternal(nodeId, event);
|
|
2177
|
-
this.dirtyNodes.add(nodeId);
|
|
2178
|
-
}
|
|
2179
|
-
dispose() {
|
|
2180
|
-
if (this.flushTimer) {
|
|
2181
|
-
clearTimeout(this.flushTimer);
|
|
2182
|
-
this.flushTimer = undefined;
|
|
2694
|
+
else {
|
|
2695
|
+
this.graphRuntime.resume();
|
|
2183
2696
|
}
|
|
2184
|
-
super.dispose();
|
|
2185
|
-
}
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
// StepEngine: expose explicit step() to process pending changes once
|
|
2189
|
-
class StepEngine extends AbstractEngine {
|
|
2190
|
-
constructor(graphRuntime) {
|
|
2191
|
-
super(graphRuntime);
|
|
2192
|
-
this.dirtyNodes = new Set();
|
|
2193
|
-
this.graphRuntime.pause();
|
|
2194
|
-
}
|
|
2195
|
-
launch() { }
|
|
2196
|
-
setInputs(nodeId, inputs) {
|
|
2197
|
-
super.setInputs(nodeId, inputs);
|
|
2198
|
-
this.dirtyNodes.add(nodeId);
|
|
2199
|
-
}
|
|
2200
|
-
triggerExternal(nodeId, event) {
|
|
2201
|
-
super.triggerExternal(nodeId, event);
|
|
2202
|
-
this.dirtyNodes.add(nodeId);
|
|
2203
|
-
}
|
|
2204
|
-
async step() {
|
|
2205
|
-
// resume first so scheduling isn't ignored due to pause
|
|
2206
|
-
const nodes = Array.from(this.dirtyNodes);
|
|
2207
|
-
this.dirtyNodes.clear();
|
|
2208
|
-
this.graphRuntime.resume();
|
|
2209
|
-
for (const n of nodes)
|
|
2210
|
-
this.graphRuntime.scheduleInputsChanged(n);
|
|
2211
|
-
await this.graphRuntime.whenIdle();
|
|
2212
|
-
this.graphRuntime.pause();
|
|
2213
|
-
}
|
|
2214
|
-
}
|
|
2215
|
-
|
|
2216
|
-
/**
|
|
2217
|
-
* Creates an Engine instance for the given GraphRuntime based on engine configuration.
|
|
2218
|
-
* This is the single source of truth for engine creation, used by both local and remote runners.
|
|
2219
|
-
*/
|
|
2220
|
-
function createEngine(runtime, config) {
|
|
2221
|
-
const engineKind = config?.engine ?? "push";
|
|
2222
|
-
const batched = config?.batched ?? { flushIntervalMs: 0 };
|
|
2223
|
-
const hybrid = config?.hybrid ?? { windowMs: 250, batchThreshold: 3 };
|
|
2224
|
-
switch (engineKind) {
|
|
2225
|
-
case "push":
|
|
2226
|
-
return new PushEngine(runtime);
|
|
2227
|
-
case "batched":
|
|
2228
|
-
return new BatchedEngine(runtime, {
|
|
2229
|
-
flushIntervalMs: batched.flushIntervalMs,
|
|
2230
|
-
});
|
|
2231
|
-
case "pull":
|
|
2232
|
-
return new PullEngine(runtime);
|
|
2233
|
-
case "hybrid":
|
|
2234
|
-
return new HybridEngine(runtime, {
|
|
2235
|
-
windowMs: hybrid.windowMs,
|
|
2236
|
-
batchThreshold: hybrid.batchThreshold,
|
|
2237
|
-
});
|
|
2238
|
-
case "step":
|
|
2239
|
-
return new StepEngine(runtime);
|
|
2240
|
-
default:
|
|
2241
|
-
throw new Error(`Unknown engine kind: ${engineKind}`);
|
|
2242
2697
|
}
|
|
2243
2698
|
}
|
|
2244
2699
|
|
|
@@ -2481,6 +2936,48 @@ const isJson = (v) => {
|
|
|
2481
2936
|
return Object.values(v).every(isJson);
|
|
2482
2937
|
return false;
|
|
2483
2938
|
};
|
|
2939
|
+
// Export operation constants for use in examples and tests
|
|
2940
|
+
const BaseMathOperation = {
|
|
2941
|
+
Add: 0,
|
|
2942
|
+
Subtract: 1,
|
|
2943
|
+
Multiply: 2,
|
|
2944
|
+
Divide: 3,
|
|
2945
|
+
Min: 4,
|
|
2946
|
+
Max: 5,
|
|
2947
|
+
Modulo: 6,
|
|
2948
|
+
Power: 7,
|
|
2949
|
+
Round: 8,
|
|
2950
|
+
Floor: 9,
|
|
2951
|
+
Ceil: 10,
|
|
2952
|
+
Abs: 11,
|
|
2953
|
+
Sum: 12,
|
|
2954
|
+
Avg: 13,
|
|
2955
|
+
MinAll: 14,
|
|
2956
|
+
MaxAll: 15,
|
|
2957
|
+
Sin: 16,
|
|
2958
|
+
Cos: 17,
|
|
2959
|
+
Tan: 18,
|
|
2960
|
+
Asin: 19,
|
|
2961
|
+
Acos: 20,
|
|
2962
|
+
Atan: 21,
|
|
2963
|
+
Sqrt: 22,
|
|
2964
|
+
Exp: 23,
|
|
2965
|
+
Log: 24,
|
|
2966
|
+
};
|
|
2967
|
+
const BaseCompareOperation = {
|
|
2968
|
+
LessThan: 0,
|
|
2969
|
+
LessThanOrEqual: 1,
|
|
2970
|
+
GreaterThan: 2,
|
|
2971
|
+
GreaterThanOrEqual: 3,
|
|
2972
|
+
Equal: 4,
|
|
2973
|
+
NotEqual: 5,
|
|
2974
|
+
};
|
|
2975
|
+
const BaseLogicOperation = {
|
|
2976
|
+
Not: 0,
|
|
2977
|
+
And: 1,
|
|
2978
|
+
Or: 2,
|
|
2979
|
+
Xor: 3,
|
|
2980
|
+
};
|
|
2484
2981
|
function setupBasicGraphRegistry(id) {
|
|
2485
2982
|
const registry = new Registry(id);
|
|
2486
2983
|
registry.categories.register(ComputeCategory);
|
|
@@ -2565,33 +3062,6 @@ function setupBasicGraphRegistry(id) {
|
|
|
2565
3062
|
return undefined;
|
|
2566
3063
|
}
|
|
2567
3064
|
});
|
|
2568
|
-
const BaseMathOperation = {
|
|
2569
|
-
Add: 0,
|
|
2570
|
-
Subtract: 1,
|
|
2571
|
-
Multiply: 2,
|
|
2572
|
-
Divide: 3,
|
|
2573
|
-
Min: 4,
|
|
2574
|
-
Max: 5,
|
|
2575
|
-
Modulo: 6,
|
|
2576
|
-
Power: 7,
|
|
2577
|
-
Round: 8,
|
|
2578
|
-
Floor: 9,
|
|
2579
|
-
Ceil: 10,
|
|
2580
|
-
Abs: 11,
|
|
2581
|
-
Sum: 12,
|
|
2582
|
-
Avg: 13,
|
|
2583
|
-
MinAll: 14,
|
|
2584
|
-
MaxAll: 15,
|
|
2585
|
-
Sin: 16,
|
|
2586
|
-
Cos: 17,
|
|
2587
|
-
Tan: 18,
|
|
2588
|
-
Asin: 19,
|
|
2589
|
-
Acos: 20,
|
|
2590
|
-
Atan: 21,
|
|
2591
|
-
Sqrt: 22,
|
|
2592
|
-
Exp: 23,
|
|
2593
|
-
Log: 24,
|
|
2594
|
-
};
|
|
2595
3065
|
// Enums: Math Operation
|
|
2596
3066
|
registry.registerEnum({
|
|
2597
3067
|
id: "enum:base.math.operation",
|
|
@@ -2600,14 +3070,6 @@ function setupBasicGraphRegistry(id) {
|
|
|
2600
3070
|
label,
|
|
2601
3071
|
})),
|
|
2602
3072
|
});
|
|
2603
|
-
const BaseCompareOperation = {
|
|
2604
|
-
LessThan: 0,
|
|
2605
|
-
LessThanOrEqual: 1,
|
|
2606
|
-
GreaterThan: 2,
|
|
2607
|
-
GreaterThanOrEqual: 3,
|
|
2608
|
-
Equal: 4,
|
|
2609
|
-
NotEqual: 5,
|
|
2610
|
-
};
|
|
2611
3073
|
// Enums: Compare Operation
|
|
2612
3074
|
registry.registerEnum({
|
|
2613
3075
|
id: "enum:base.compare.operation",
|
|
@@ -2616,12 +3078,6 @@ function setupBasicGraphRegistry(id) {
|
|
|
2616
3078
|
label,
|
|
2617
3079
|
})),
|
|
2618
3080
|
});
|
|
2619
|
-
const BaseLogicOperation = {
|
|
2620
|
-
Not: 0,
|
|
2621
|
-
And: 1,
|
|
2622
|
-
Or: 2,
|
|
2623
|
-
Xor: 3,
|
|
2624
|
-
};
|
|
2625
3081
|
// Enums: Logic Operation
|
|
2626
3082
|
registry.registerEnum({
|
|
2627
3083
|
id: "enum:base.logic.operation",
|
|
@@ -3452,7 +3908,8 @@ function installLogging(engine) {
|
|
|
3452
3908
|
console.log(`[progress] ${s.runId || s.nodeId}: ${pct}%`);
|
|
3453
3909
|
}
|
|
3454
3910
|
else if (s.kind === "node-done") {
|
|
3455
|
-
|
|
3911
|
+
const cancelled = s.cancelled ? " (cancelled)" : "";
|
|
3912
|
+
console.log(`[done] ${s.runId || s.nodeId} in ${s.durationMs ?? 0}ms${cancelled}`);
|
|
3456
3913
|
}
|
|
3457
3914
|
else if (s.kind === "node-start") {
|
|
3458
3915
|
console.log(`[start] ${s.runId || s.nodeId}`);
|
|
@@ -3471,6 +3928,21 @@ function installLogging(engine) {
|
|
|
3471
3928
|
else if (e.kind === "edge-convert") {
|
|
3472
3929
|
console.warn(`[error] ${e.edgeId} ${e.source.nodeId}.${e.source.handle} -> ${e.target.nodeId}.${e.target.handle}`, e.err?.message ?? e.err);
|
|
3473
3930
|
}
|
|
3931
|
+
else if (e.kind === "input-validation") {
|
|
3932
|
+
console.warn(`[error] input-validation: ${e.nodeId}.${e.handle} (type ${e.typeId})`, e.message);
|
|
3933
|
+
}
|
|
3934
|
+
else if (e.kind === "registry") {
|
|
3935
|
+
console.warn(`[error] registry:`, e.err?.message ?? e.err, e.attempt !== undefined
|
|
3936
|
+
? `(attempt ${e.attempt}/${e.maxAttempts ?? "?"})`
|
|
3937
|
+
: "");
|
|
3938
|
+
}
|
|
3939
|
+
else if (e.kind === "system") {
|
|
3940
|
+
console.warn(`[error] system: ${e.message}`, e.code ? `(code: ${e.code})` : "", e.err ? e.err?.message ?? e.err : "", e.details ? JSON.stringify(e.details) : "");
|
|
3941
|
+
}
|
|
3942
|
+
else {
|
|
3943
|
+
// Log any other error kinds (shouldn't happen, but handle gracefully)
|
|
3944
|
+
console.warn(`[error] unknown error kind:`, e);
|
|
3945
|
+
}
|
|
3474
3946
|
});
|
|
3475
3947
|
}
|
|
3476
3948
|
|
|
@@ -3548,7 +4020,6 @@ function createAsyncGraphDef() {
|
|
|
3548
4020
|
source: { nodeId: "n4", handle: "XYZ" },
|
|
3549
4021
|
target: { nodeId: "n1", handle: "A" },
|
|
3550
4022
|
typeId: "base.vec3[]",
|
|
3551
|
-
// convertAsync,
|
|
3552
4023
|
},
|
|
3553
4024
|
{
|
|
3554
4025
|
id: "e3",
|
|
@@ -3606,9 +4077,9 @@ function createProgressGraphRegistry(id) {
|
|
|
3606
4077
|
|
|
3607
4078
|
function createValidationGraphDef() {
|
|
3608
4079
|
// Intentionally build a graph with validation issues:
|
|
3609
|
-
// - Unknown
|
|
4080
|
+
// - Unknown node type
|
|
3610
4081
|
// - Missing target input handle
|
|
3611
|
-
// - Multi inbound to same input
|
|
4082
|
+
// - Multi inbound to same input (warning)
|
|
3612
4083
|
const def = {
|
|
3613
4084
|
nodes: [
|
|
3614
4085
|
{ nodeId: "nA", typeId: "base.input.number" },
|
|
@@ -3616,7 +4087,7 @@ function createValidationGraphDef() {
|
|
|
3616
4087
|
{ nodeId: "nC", typeId: "base.math" },
|
|
3617
4088
|
{ nodeId: "s1", typeId: "base.object.toString" },
|
|
3618
4089
|
{ nodeId: "cmp", typeId: "base.compare" },
|
|
3619
|
-
//
|
|
4090
|
+
// Validation issue: unknown node type
|
|
3620
4091
|
{ nodeId: "bad", typeId: "unknownType" },
|
|
3621
4092
|
],
|
|
3622
4093
|
edges: [
|
|
@@ -3638,7 +4109,6 @@ function createValidationGraphDef() {
|
|
|
3638
4109
|
source: { nodeId: "nB", handle: "Result" },
|
|
3639
4110
|
target: { nodeId: "nC", handle: "A" },
|
|
3640
4111
|
},
|
|
3641
|
-
// Type mismatch to highlight coercion/validation (string -> float[] should error)
|
|
3642
4112
|
{
|
|
3643
4113
|
id: "e4",
|
|
3644
4114
|
source: { nodeId: "s1", handle: "Text" },
|
|
@@ -4304,21 +4774,19 @@ function buildValueConverter(config) {
|
|
|
4304
4774
|
};
|
|
4305
4775
|
}
|
|
4306
4776
|
|
|
4307
|
-
exports.
|
|
4777
|
+
exports.BaseCompareOperation = BaseCompareOperation;
|
|
4778
|
+
exports.BaseLogicOperation = BaseLogicOperation;
|
|
4779
|
+
exports.BaseMathOperation = BaseMathOperation;
|
|
4308
4780
|
exports.CompositeCategory = CompositeCategory;
|
|
4309
4781
|
exports.ComputeCategory = ComputeCategory;
|
|
4310
4782
|
exports.GraphBuilder = GraphBuilder;
|
|
4311
4783
|
exports.GraphRuntime = GraphRuntime;
|
|
4312
|
-
exports.HybridEngine = HybridEngine;
|
|
4313
|
-
exports.PullEngine = PullEngine;
|
|
4314
|
-
exports.PushEngine = PushEngine;
|
|
4315
4784
|
exports.Registry = Registry;
|
|
4316
|
-
exports.
|
|
4785
|
+
exports.UnifiedEngine = UnifiedEngine;
|
|
4317
4786
|
exports.buildValueConverter = buildValueConverter;
|
|
4318
4787
|
exports.computeGraphCenter = computeGraphCenter;
|
|
4319
4788
|
exports.createAsyncGraphDef = createAsyncGraphDef;
|
|
4320
4789
|
exports.createAsyncGraphRegistry = createAsyncGraphRegistry;
|
|
4321
|
-
exports.createEngine = createEngine;
|
|
4322
4790
|
exports.createProgressGraphDef = createProgressGraphDef;
|
|
4323
4791
|
exports.createProgressGraphRegistry = createProgressGraphRegistry;
|
|
4324
4792
|
exports.createSimpleGraphDef = createSimpleGraphDef;
|