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