@bian-womp/spark-graph 0.1.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.
Files changed (101) hide show
  1. package/lib/cjs/index.cjs +1872 -0
  2. package/lib/cjs/index.cjs.map +1 -0
  3. package/lib/cjs/src/builder/GraphBuilder.d.ts +42 -0
  4. package/lib/cjs/src/builder/GraphBuilder.d.ts.map +1 -0
  5. package/lib/cjs/src/builder/Registry.d.ts +42 -0
  6. package/lib/cjs/src/builder/Registry.d.ts.map +1 -0
  7. package/lib/cjs/src/core/categories.d.ts +32 -0
  8. package/lib/cjs/src/core/categories.d.ts.map +1 -0
  9. package/lib/cjs/src/core/types.d.ts +80 -0
  10. package/lib/cjs/src/core/types.d.ts.map +1 -0
  11. package/lib/cjs/src/examples/async.d.ts +5 -0
  12. package/lib/cjs/src/examples/async.d.ts.map +1 -0
  13. package/lib/cjs/src/examples/engine.d.ts +6 -0
  14. package/lib/cjs/src/examples/engine.d.ts.map +1 -0
  15. package/lib/cjs/src/examples/progress.d.ts +5 -0
  16. package/lib/cjs/src/examples/progress.d.ts.map +1 -0
  17. package/lib/cjs/src/examples/run.d.ts +2 -0
  18. package/lib/cjs/src/examples/run.d.ts.map +1 -0
  19. package/lib/cjs/src/examples/shared.d.ts +8 -0
  20. package/lib/cjs/src/examples/shared.d.ts.map +1 -0
  21. package/lib/cjs/src/examples/simple.d.ts +4 -0
  22. package/lib/cjs/src/examples/simple.d.ts.map +1 -0
  23. package/lib/cjs/src/examples/validation.d.ts +5 -0
  24. package/lib/cjs/src/examples/validation.d.ts.map +1 -0
  25. package/lib/cjs/src/index.d.ts +23 -0
  26. package/lib/cjs/src/index.d.ts.map +1 -0
  27. package/lib/cjs/src/plugins/composite.d.ts +22 -0
  28. package/lib/cjs/src/plugins/composite.d.ts.map +1 -0
  29. package/lib/cjs/src/plugins/compute.d.ts +5 -0
  30. package/lib/cjs/src/plugins/compute.d.ts.map +1 -0
  31. package/lib/cjs/src/runtime/AbstractEngine.d.ts +14 -0
  32. package/lib/cjs/src/runtime/AbstractEngine.d.ts.map +1 -0
  33. package/lib/cjs/src/runtime/BatchedEngine.d.ts +17 -0
  34. package/lib/cjs/src/runtime/BatchedEngine.d.ts.map +1 -0
  35. package/lib/cjs/src/runtime/Engine.d.ts +14 -0
  36. package/lib/cjs/src/runtime/Engine.d.ts.map +1 -0
  37. package/lib/cjs/src/runtime/GraphRuntime.d.ts +127 -0
  38. package/lib/cjs/src/runtime/GraphRuntime.d.ts.map +1 -0
  39. package/lib/cjs/src/runtime/HybridEngine.d.ts +21 -0
  40. package/lib/cjs/src/runtime/HybridEngine.d.ts.map +1 -0
  41. package/lib/cjs/src/runtime/LocalRunner.d.ts +16 -0
  42. package/lib/cjs/src/runtime/LocalRunner.d.ts.map +1 -0
  43. package/lib/cjs/src/runtime/PullEngine.d.ts +8 -0
  44. package/lib/cjs/src/runtime/PullEngine.d.ts.map +1 -0
  45. package/lib/cjs/src/runtime/PushEngine.d.ts +6 -0
  46. package/lib/cjs/src/runtime/PushEngine.d.ts.map +1 -0
  47. package/lib/cjs/src/runtime/RunnerControl.d.ts +10 -0
  48. package/lib/cjs/src/runtime/RunnerControl.d.ts.map +1 -0
  49. package/lib/cjs/src/runtime/StepEngine.d.ts +11 -0
  50. package/lib/cjs/src/runtime/StepEngine.d.ts.map +1 -0
  51. package/lib/esm/index.js +1850 -0
  52. package/lib/esm/index.js.map +1 -0
  53. package/lib/esm/src/builder/GraphBuilder.d.ts +42 -0
  54. package/lib/esm/src/builder/GraphBuilder.d.ts.map +1 -0
  55. package/lib/esm/src/builder/Registry.d.ts +42 -0
  56. package/lib/esm/src/builder/Registry.d.ts.map +1 -0
  57. package/lib/esm/src/core/categories.d.ts +32 -0
  58. package/lib/esm/src/core/categories.d.ts.map +1 -0
  59. package/lib/esm/src/core/types.d.ts +80 -0
  60. package/lib/esm/src/core/types.d.ts.map +1 -0
  61. package/lib/esm/src/examples/async.d.ts +5 -0
  62. package/lib/esm/src/examples/async.d.ts.map +1 -0
  63. package/lib/esm/src/examples/engine.d.ts +6 -0
  64. package/lib/esm/src/examples/engine.d.ts.map +1 -0
  65. package/lib/esm/src/examples/progress.d.ts +5 -0
  66. package/lib/esm/src/examples/progress.d.ts.map +1 -0
  67. package/lib/esm/src/examples/run.d.ts +2 -0
  68. package/lib/esm/src/examples/run.d.ts.map +1 -0
  69. package/lib/esm/src/examples/shared.d.ts +8 -0
  70. package/lib/esm/src/examples/shared.d.ts.map +1 -0
  71. package/lib/esm/src/examples/simple.d.ts +4 -0
  72. package/lib/esm/src/examples/simple.d.ts.map +1 -0
  73. package/lib/esm/src/examples/validation.d.ts +5 -0
  74. package/lib/esm/src/examples/validation.d.ts.map +1 -0
  75. package/lib/esm/src/index.d.ts +23 -0
  76. package/lib/esm/src/index.d.ts.map +1 -0
  77. package/lib/esm/src/plugins/composite.d.ts +22 -0
  78. package/lib/esm/src/plugins/composite.d.ts.map +1 -0
  79. package/lib/esm/src/plugins/compute.d.ts +5 -0
  80. package/lib/esm/src/plugins/compute.d.ts.map +1 -0
  81. package/lib/esm/src/runtime/AbstractEngine.d.ts +14 -0
  82. package/lib/esm/src/runtime/AbstractEngine.d.ts.map +1 -0
  83. package/lib/esm/src/runtime/BatchedEngine.d.ts +17 -0
  84. package/lib/esm/src/runtime/BatchedEngine.d.ts.map +1 -0
  85. package/lib/esm/src/runtime/Engine.d.ts +14 -0
  86. package/lib/esm/src/runtime/Engine.d.ts.map +1 -0
  87. package/lib/esm/src/runtime/GraphRuntime.d.ts +127 -0
  88. package/lib/esm/src/runtime/GraphRuntime.d.ts.map +1 -0
  89. package/lib/esm/src/runtime/HybridEngine.d.ts +21 -0
  90. package/lib/esm/src/runtime/HybridEngine.d.ts.map +1 -0
  91. package/lib/esm/src/runtime/LocalRunner.d.ts +16 -0
  92. package/lib/esm/src/runtime/LocalRunner.d.ts.map +1 -0
  93. package/lib/esm/src/runtime/PullEngine.d.ts +8 -0
  94. package/lib/esm/src/runtime/PullEngine.d.ts.map +1 -0
  95. package/lib/esm/src/runtime/PushEngine.d.ts +6 -0
  96. package/lib/esm/src/runtime/PushEngine.d.ts.map +1 -0
  97. package/lib/esm/src/runtime/RunnerControl.d.ts +10 -0
  98. package/lib/esm/src/runtime/RunnerControl.d.ts.map +1 -0
  99. package/lib/esm/src/runtime/StepEngine.d.ts +11 -0
  100. package/lib/esm/src/runtime/StepEngine.d.ts.map +1 -0
  101. package/package.json +46 -0
@@ -0,0 +1,1850 @@
1
+ class CategoryRegistry {
2
+ constructor() {
3
+ this.categories = new Map();
4
+ }
5
+ register(cat) {
6
+ this.categories.set(cat.id, cat);
7
+ return this;
8
+ }
9
+ get(id) {
10
+ return this.categories.get(id);
11
+ }
12
+ has(id) {
13
+ return this.categories.has(id);
14
+ }
15
+ }
16
+ class Registry {
17
+ constructor() {
18
+ this.types = new Map();
19
+ this.nodes = new Map();
20
+ this.categories = new CategoryRegistry();
21
+ this.serializers = new Map();
22
+ this.coercions = new Map();
23
+ this.asyncCoercions = new Map();
24
+ this.enums = new Map();
25
+ }
26
+ registerType(desc) {
27
+ this.types.set(desc.id, desc);
28
+ return this;
29
+ }
30
+ registerNode(desc) {
31
+ this.nodes.set(desc.id, desc);
32
+ return this;
33
+ }
34
+ registerSerializer(typeId, s) {
35
+ this.serializers.set(typeId, s);
36
+ return this;
37
+ }
38
+ // Register a type coercion from one type id to another
39
+ registerCoercion(fromTypeId, toTypeId, convert) {
40
+ this.coercions.set(`${fromTypeId}->${toTypeId}`, convert);
41
+ return this;
42
+ }
43
+ // Register an async type coercion from one type id to another
44
+ registerAsyncCoercion(fromTypeId, toTypeId, convertAsync) {
45
+ this.asyncCoercions.set(`${fromTypeId}->${toTypeId}`, convertAsync);
46
+ return this;
47
+ }
48
+ canCoerce(fromTypeId, toTypeId) {
49
+ if (!fromTypeId || !toTypeId)
50
+ return false;
51
+ if (fromTypeId === toTypeId)
52
+ return true;
53
+ const key = `${fromTypeId}->${toTypeId}`;
54
+ return this.coercions.has(key) || this.asyncCoercions.has(key);
55
+ }
56
+ getCoercion(fromTypeId, toTypeId) {
57
+ if (fromTypeId === toTypeId)
58
+ return (v) => v;
59
+ return this.coercions.get(`${fromTypeId}->${toTypeId}`);
60
+ }
61
+ getAsyncCoercion(fromTypeId, toTypeId) {
62
+ if (fromTypeId === toTypeId)
63
+ return undefined;
64
+ return this.asyncCoercions.get(`${fromTypeId}->${toTypeId}`);
65
+ }
66
+ // Enum support
67
+ registerEnum(enumTypeId, options, labelType, valueType) {
68
+ const valueToLabel = new Map();
69
+ const labelToValue = new Map();
70
+ for (const { value, label } of options) {
71
+ valueToLabel.set(value, label);
72
+ labelToValue.set(label.toLowerCase(), value);
73
+ }
74
+ this.enums.set(enumTypeId, {
75
+ options,
76
+ valueToLabel,
77
+ labelToValue,
78
+ });
79
+ // Register type descriptor and serializer for enum (stored as number)
80
+ this.registerType({
81
+ id: enumTypeId,
82
+ validate: (v) => typeof v === "number" && valueToLabel.has(Number(v)),
83
+ });
84
+ this.registerSerializer(enumTypeId, {
85
+ serialize: (v) => v,
86
+ deserialize: (d) => Number(d),
87
+ });
88
+ // Coercions: string -> enum (by label), float -> enum (by numeric value), enum -> string (label)
89
+ this.registerCoercion(labelType, enumTypeId, (value) => {
90
+ const s = String(value ?? "")
91
+ .trim()
92
+ .toLowerCase();
93
+ const rec = this.enums.get(enumTypeId);
94
+ if (!rec)
95
+ return value;
96
+ if (rec.labelToValue.has(s))
97
+ return rec.labelToValue.get(s);
98
+ const asNum = Number(s);
99
+ if (Number.isFinite(asNum) && rec.valueToLabel.has(asNum))
100
+ return asNum;
101
+ return Array.from(rec.valueToLabel.keys())[0] ?? 0;
102
+ });
103
+ this.registerCoercion(valueType, enumTypeId, (value) => {
104
+ const n = Number(value);
105
+ const rec = this.enums.get(enumTypeId);
106
+ if (!rec)
107
+ return value;
108
+ return rec.valueToLabel.has(n)
109
+ ? n
110
+ : Array.from(rec.valueToLabel.keys())[0] ?? 0;
111
+ });
112
+ this.registerCoercion(enumTypeId, labelType, (value) => {
113
+ const n = Number(value);
114
+ const rec = this.enums.get(enumTypeId);
115
+ if (!rec)
116
+ return String(value);
117
+ return rec.valueToLabel.get(n) ?? String(n);
118
+ });
119
+ return this;
120
+ }
121
+ getEnumOptions(enumTypeId) {
122
+ return this.enums.get(enumTypeId)?.options ?? [];
123
+ }
124
+ getEnumLabel(enumTypeId, value) {
125
+ return this.enums.get(enumTypeId)?.valueToLabel.get(value);
126
+ }
127
+ getEnumValue(enumTypeId, label) {
128
+ return this.enums.get(enumTypeId)?.labelToValue.get(label.toLowerCase());
129
+ }
130
+ }
131
+
132
+ class GraphRuntime {
133
+ constructor() {
134
+ this.nodes = new Map();
135
+ this.edges = [];
136
+ this.listeners = new Map();
137
+ this.environment = {};
138
+ this.paused = false;
139
+ }
140
+ static create(def, registry, opts) {
141
+ const gr = new GraphRuntime();
142
+ gr.environment = opts?.environment ?? {};
143
+ // Instantiate nodes
144
+ for (const n of def.nodes) {
145
+ const desc = registry.nodes.get(n.typeId);
146
+ if (!desc)
147
+ throw new Error(`Unknown node type: ${n.typeId}`);
148
+ const cat = registry.categories.get(desc.categoryId);
149
+ if (!cat)
150
+ throw new Error(`Unknown category: ${desc.categoryId}`);
151
+ if (cat.validateImpl)
152
+ cat.validateImpl(desc.impl);
153
+ const runtime = cat.createRuntime({
154
+ nodeId: n.nodeId,
155
+ impl: desc.impl,
156
+ });
157
+ const rn = {
158
+ typeId: n.typeId,
159
+ nodeId: n.nodeId,
160
+ lifecycle: desc.lifecycle,
161
+ inputs: {},
162
+ outputs: {},
163
+ state: {},
164
+ runtime,
165
+ params: n.params,
166
+ policy: {
167
+ ...cat.policy,
168
+ ...(n.params && n.params.policy ? n.params.policy : {}),
169
+ },
170
+ activeControllers: new Set(),
171
+ queue: [],
172
+ stats: {
173
+ runs: 0,
174
+ active: 0,
175
+ queued: 0,
176
+ progress: 0,
177
+ },
178
+ };
179
+ gr.nodes.set(n.nodeId, rn);
180
+ }
181
+ // Instantiate edges
182
+ gr.edges = def.edges.map((e) => {
183
+ // infer type from source output if missing
184
+ const srcNode = def.nodes.find((n) => n.nodeId === e.source.nodeId);
185
+ const dstNode = def.nodes.find((n) => n.nodeId === e.target.nodeId);
186
+ let effectiveTypeId = e.typeId;
187
+ let srcDeclared;
188
+ let dstDeclared;
189
+ if (srcNode) {
190
+ const srcDesc = registry.nodes.get(srcNode.typeId);
191
+ if (srcDesc) {
192
+ srcDeclared = srcDesc.outputs[e.source.handle];
193
+ }
194
+ }
195
+ if (!effectiveTypeId)
196
+ effectiveTypeId = srcDeclared;
197
+ if (dstNode) {
198
+ const dstDesc = registry.nodes.get(dstNode.typeId);
199
+ if (dstDesc) {
200
+ dstDeclared = dstDesc.inputs[e.target.handle];
201
+ }
202
+ }
203
+ // Attach convert if source/target differ but coercible
204
+ let convert = undefined;
205
+ let convertAsync = undefined;
206
+ if (srcDeclared && dstDeclared && srcDeclared !== dstDeclared) {
207
+ const fn = registry.getCoercion(srcDeclared, dstDeclared);
208
+ if (fn)
209
+ convert = convert ?? fn;
210
+ const afn = registry.getAsyncCoercion(srcDeclared, dstDeclared);
211
+ if (afn)
212
+ convertAsync = convertAsync ?? afn;
213
+ }
214
+ return {
215
+ id: e.id,
216
+ source: { ...e.source },
217
+ target: { ...e.target },
218
+ typeId: effectiveTypeId ?? "untyped",
219
+ convert,
220
+ convertAsync,
221
+ stats: { runs: 0, inFlight: false, progress: 0 },
222
+ };
223
+ });
224
+ return gr;
225
+ }
226
+ on(event, handler) {
227
+ if (!this.listeners.has(event))
228
+ this.listeners.set(event, new Set());
229
+ const set = this.listeners.get(event);
230
+ set.add(handler);
231
+ return () => set.delete(handler);
232
+ }
233
+ emit(event, payload) {
234
+ const set = this.listeners.get(event);
235
+ if (set)
236
+ for (const h of Array.from(set))
237
+ h(payload);
238
+ }
239
+ setInput(nodeId, handle, value) {
240
+ const node = this.nodes.get(nodeId);
241
+ if (!node)
242
+ throw new Error(`Node not found: ${nodeId}`);
243
+ // If this input has an inbound edge, prefer propagated runtime value over manual input
244
+ const hasInbound = this.edges.some((e) => e.target.nodeId === nodeId && e.target.handle === handle);
245
+ if (hasInbound)
246
+ return; // respect linked value
247
+ node.inputs[handle] = value;
248
+ // Emit value event for input updates
249
+ this.emit("value", { nodeId, handle, value, io: "input" });
250
+ if (!this.paused)
251
+ this.scheduleInputsChanged(nodeId);
252
+ }
253
+ getOutput(nodeId, output) {
254
+ const node = this.nodes.get(nodeId);
255
+ return node?.outputs[output];
256
+ }
257
+ scheduleInputsChanged(nodeId) {
258
+ const node = this.nodes.get(nodeId);
259
+ if (!node)
260
+ return;
261
+ if (this.paused)
262
+ return;
263
+ const now = Date.now();
264
+ const policy = node.policy ?? {};
265
+ if (policy.debounceMs &&
266
+ node.lastScheduledAt &&
267
+ now - node.lastScheduledAt < policy.debounceMs) {
268
+ // debounce: replace latest queued
269
+ node.queue.splice(0, node.queue.length);
270
+ const rid = `${nodeId}:${now}`;
271
+ node.queue.push({ runId: rid, inputs: { ...node.inputs } });
272
+ return;
273
+ }
274
+ node.lastScheduledAt = now;
275
+ const rid = `${nodeId}:${now}:${Math.random().toString(36).slice(2, 8)}`;
276
+ node.latestRunId = rid;
277
+ const startRun = (runId, capturedInputs, onDone) => {
278
+ const controller = new AbortController();
279
+ node.stats.runs += 1;
280
+ node.stats.active += 1;
281
+ node.stats.lastStartAt = now;
282
+ node.stats.progress = 0;
283
+ node.activeControllers.add(controller);
284
+ const mode = policy.asyncConcurrency ?? "switch";
285
+ if (mode === "switch") {
286
+ for (const c of Array.from(node.activeControllers)) {
287
+ if (c !== controller)
288
+ c.abort("switch");
289
+ }
290
+ }
291
+ let timeoutId;
292
+ if (policy.timeoutMs && policy.timeoutMs > 0) {
293
+ timeoutId = setTimeout(() => controller.abort("timeout"), policy.timeoutMs);
294
+ }
295
+ const ctx = {
296
+ state: node.state,
297
+ setState: (next) => Object.assign(node.state, next),
298
+ emit: (handle, value) => {
299
+ const m = policy.asyncConcurrency ?? "switch";
300
+ if (m !== "merge" && runId !== node.latestRunId)
301
+ return;
302
+ this.propagate(nodeId, handle, value);
303
+ },
304
+ invalidateDownstream: () => this.invalidateDownstream(nodeId),
305
+ getInput: (handle) => capturedInputs[handle],
306
+ environment: this.environment,
307
+ runId: runId,
308
+ abortSignal: controller.signal,
309
+ createAbortController: () => new AbortController(),
310
+ reportProgress: (p) => {
311
+ node.stats.progress = Math.max(0, Math.min(1, Number(p) || 0));
312
+ this.emit("stats", {
313
+ kind: "node-progress",
314
+ nodeId,
315
+ runId,
316
+ progress: node.stats.progress,
317
+ });
318
+ },
319
+ };
320
+ const exec = async (attempt) => {
321
+ try {
322
+ await node.runtime.onInputsChanged?.(capturedInputs, ctx);
323
+ }
324
+ catch (err) {
325
+ // Suppress errors caused by expected cancellations (switch)
326
+ if (controller.signal.aborted) {
327
+ const reason = controller.signal.reason;
328
+ if (reason === "switch") {
329
+ return; // ignore switched runs
330
+ }
331
+ }
332
+ const retry = policy.retry;
333
+ if (retry && attempt < (retry.attempts ?? 0)) {
334
+ const delay = retry.backoffMs ? retry.backoffMs(attempt) : 0;
335
+ await new Promise((r) => setTimeout(r, delay));
336
+ return exec(attempt + 1);
337
+ }
338
+ this.emit("error", { nodeId, runId, err });
339
+ }
340
+ finally {
341
+ if (timeoutId)
342
+ clearTimeout(timeoutId);
343
+ node.activeControllers.delete(controller);
344
+ node.stats.active = Math.max(0, node.activeControllers.size);
345
+ node.stats.lastEndAt = Date.now();
346
+ node.stats.lastDurationMs =
347
+ node.stats.lastStartAt && node.stats.lastEndAt
348
+ ? node.stats.lastEndAt - node.stats.lastStartAt
349
+ : undefined;
350
+ this.emit("stats", {
351
+ kind: "node-done",
352
+ nodeId,
353
+ runId,
354
+ durationMs: node.stats.lastDurationMs,
355
+ });
356
+ if (onDone)
357
+ onDone();
358
+ }
359
+ };
360
+ // fire node-start event
361
+ this.emit("stats", { kind: "node-start", nodeId, runId });
362
+ void exec(0);
363
+ };
364
+ const mode = policy.asyncConcurrency ?? "switch";
365
+ if (mode === "drop" && node.activeControllers.size > 0)
366
+ return;
367
+ if (mode === "queue") {
368
+ const maxQ = policy.maxQueue ?? 8;
369
+ node.queue.push({ runId: rid, inputs: { ...node.inputs } });
370
+ if (node.queue.length > maxQ)
371
+ node.queue.shift();
372
+ const processNext = () => {
373
+ if (node.activeControllers.size > 0)
374
+ return;
375
+ const next = node.queue.shift();
376
+ if (!next)
377
+ return;
378
+ node.latestRunId = next.runId;
379
+ startRun(next.runId, next.inputs, () => {
380
+ // After finishing, schedule next
381
+ setTimeout(processNext, 0);
382
+ });
383
+ };
384
+ processNext();
385
+ return;
386
+ }
387
+ // switch or merge
388
+ startRun(rid, { ...node.inputs });
389
+ }
390
+ invalidateDownstream(nodeId) {
391
+ // Notifies dependents; for now we propagate current outputs
392
+ for (const e of this.edges.filter((e) => e.source.nodeId === nodeId)) {
393
+ const value = this.getOutput(nodeId, e.source.handle);
394
+ if (value !== undefined)
395
+ this.propagate(nodeId, e.source.handle, value);
396
+ }
397
+ }
398
+ propagate(srcNodeId, srcHandle, value) {
399
+ // set source output
400
+ const srcNode = this.nodes.get(srcNodeId);
401
+ srcNode.outputs[srcHandle] = value;
402
+ this.emit("value", {
403
+ nodeId: srcNodeId,
404
+ handle: srcHandle,
405
+ value,
406
+ io: "output",
407
+ });
408
+ // fan-out along all edges from this output
409
+ const outEdges = this.edges.filter((e) => e.source.nodeId === srcNodeId && e.source.handle === srcHandle);
410
+ for (const e of outEdges) {
411
+ let nextVal = value;
412
+ const applyToTarget = (v) => {
413
+ const dstNode = this.nodes.get(e.target.nodeId);
414
+ if (!dstNode)
415
+ return;
416
+ dstNode.inputs[e.target.handle] = v;
417
+ // Emit value event for input updates
418
+ this.emit("value", {
419
+ nodeId: e.target.nodeId,
420
+ handle: e.target.handle,
421
+ value: v,
422
+ io: "input",
423
+ });
424
+ if (!this.paused)
425
+ this.scheduleInputsChanged(e.target.nodeId);
426
+ };
427
+ if (e.convertAsync) {
428
+ // emit edge-start before launching async conversion
429
+ this.emit("stats", {
430
+ kind: "edge-start",
431
+ edgeId: e.id,
432
+ source: { nodeId: e.source.nodeId, handle: e.source.handle },
433
+ target: { nodeId: e.target.nodeId, handle: e.target.handle },
434
+ });
435
+ const controller = new AbortController();
436
+ const startAt = Date.now();
437
+ e.stats.runs += 1;
438
+ e.stats.inFlight = true;
439
+ e.stats.progress = 0;
440
+ const sig = controller.signal;
441
+ // Fire async conversion
442
+ void e
443
+ .convertAsync(nextVal, sig)
444
+ .then((v) => {
445
+ e.stats.inFlight = false;
446
+ e.stats.lastEndAt = Date.now();
447
+ e.stats.lastDurationMs = e.stats.lastEndAt - startAt;
448
+ this.emit("stats", {
449
+ kind: "edge-done",
450
+ edgeId: e.id,
451
+ source: { nodeId: e.source.nodeId, handle: e.source.handle },
452
+ target: { nodeId: e.target.nodeId, handle: e.target.handle },
453
+ durationMs: e.stats.lastDurationMs,
454
+ });
455
+ applyToTarget(v);
456
+ })
457
+ .catch((err) => {
458
+ if (sig.aborted)
459
+ return;
460
+ e.stats.inFlight = false;
461
+ e.stats.lastError = err;
462
+ this.emit("error", {
463
+ kind: "edge-convert",
464
+ edgeId: e.id,
465
+ source: { nodeId: e.source.nodeId, handle: e.source.handle },
466
+ target: { nodeId: e.target.nodeId, handle: e.target.handle },
467
+ err,
468
+ });
469
+ });
470
+ }
471
+ else {
472
+ if (e.convert)
473
+ nextVal = e.convert(nextVal);
474
+ applyToTarget(nextVal);
475
+ }
476
+ }
477
+ }
478
+ launch() {
479
+ // call onActivated for nodes that implement it
480
+ for (const node of this.nodes.values()) {
481
+ const ctrl = new AbortController();
482
+ const ctx = {
483
+ state: node.state,
484
+ setState: (next) => Object.assign(node.state, next),
485
+ emit: (handle, value) => this.propagate(node.nodeId, handle, value),
486
+ invalidateDownstream: () => this.invalidateDownstream(node.nodeId),
487
+ getInput: (handle) => node.inputs[handle],
488
+ environment: this.environment,
489
+ runId: `${node.nodeId}:activation`,
490
+ abortSignal: ctrl.signal,
491
+ createAbortController: () => new AbortController(),
492
+ reportProgress: (p) => {
493
+ node.stats.progress = Math.max(0, Math.min(1, Number(p) || 0));
494
+ },
495
+ };
496
+ node.lifecycle?.init?.(node.params ?? {}, {
497
+ state: node.state,
498
+ setState: (next) => Object.assign(node.state, next),
499
+ });
500
+ node.runtime.onActivated?.(ctx);
501
+ }
502
+ }
503
+ triggerExternal(nodeId, event) {
504
+ const node = this.nodes.get(nodeId);
505
+ if (!node)
506
+ return;
507
+ const ctrl = new AbortController();
508
+ const ctx = {
509
+ state: node.state,
510
+ setState: (next) => Object.assign(node.state, next),
511
+ emit: (handle, value) => this.propagate(nodeId, handle, value),
512
+ invalidateDownstream: () => this.invalidateDownstream(nodeId),
513
+ getInput: (handle) => node.inputs[handle],
514
+ environment: this.environment,
515
+ runId: `${nodeId}:external`,
516
+ abortSignal: ctrl.signal,
517
+ createAbortController: () => new AbortController(),
518
+ reportProgress: (p) => {
519
+ node.stats.progress = Math.max(0, Math.min(1, Number(p) || 0));
520
+ },
521
+ };
522
+ node.runtime.onExternalEvent?.(event, ctx);
523
+ }
524
+ dispose() {
525
+ for (const node of this.nodes.values()) {
526
+ node.runtime.onDeactivated?.();
527
+ node.runtime.dispose?.();
528
+ node.lifecycle?.dispose?.({
529
+ state: node.state,
530
+ setState: (next) => Object.assign(node.state, next),
531
+ });
532
+ }
533
+ this.nodes.clear();
534
+ this.edges = [];
535
+ this.listeners.clear();
536
+ }
537
+ // Unsafe helpers for serializer: read-only accessors and hydration
538
+ __unsafe_getNodeData(nodeId) {
539
+ const node = this.nodes.get(nodeId);
540
+ if (!node)
541
+ return undefined;
542
+ return {
543
+ inputs: { ...node.inputs },
544
+ outputs: { ...node.outputs },
545
+ state: node.state,
546
+ params: node.params,
547
+ stats: { ...node.stats },
548
+ };
549
+ }
550
+ __unsafe_getEnvironment() {
551
+ return { ...this.environment };
552
+ }
553
+ setEnvironment(env) {
554
+ this.environment = { ...env };
555
+ }
556
+ __unsafe_setEnvironment(env) {
557
+ this.setEnvironment(env);
558
+ }
559
+ __unsafe_hydrateNode(nodeId, data, opts) {
560
+ const node = this.nodes.get(nodeId);
561
+ if (!node)
562
+ return;
563
+ if (opts?.replace) {
564
+ node.inputs = {};
565
+ node.outputs = {};
566
+ node.state = {};
567
+ }
568
+ if (data.inputs)
569
+ Object.assign(node.inputs, data.inputs);
570
+ if (data.outputs)
571
+ Object.assign(node.outputs, data.outputs);
572
+ if (data.state !== undefined)
573
+ node.state = data.state;
574
+ if (data.params)
575
+ node.params = data.params;
576
+ }
577
+ async whenIdle() {
578
+ const isIdle = () => {
579
+ for (const n of this.nodes.values()) {
580
+ if (n.activeControllers.size > 0)
581
+ return false;
582
+ if (n.queue.length > 0)
583
+ return false;
584
+ }
585
+ return true;
586
+ };
587
+ if (isIdle())
588
+ return;
589
+ await new Promise((resolve) => {
590
+ const check = () => {
591
+ if (isIdle())
592
+ resolve();
593
+ else
594
+ setTimeout(check, 10);
595
+ };
596
+ setTimeout(check, 10);
597
+ });
598
+ }
599
+ pause() {
600
+ this.paused = true;
601
+ }
602
+ resume() {
603
+ this.paused = false;
604
+ }
605
+ __unsafe_invalidateDownstream(nodeId) {
606
+ this.invalidateDownstream(nodeId);
607
+ }
608
+ __unsafe_reemitNodeOutputs(nodeId) {
609
+ const node = this.nodes.get(nodeId);
610
+ if (!node)
611
+ return;
612
+ for (const [handle, value] of Object.entries(node.outputs)) {
613
+ this.propagate(nodeId, handle, value);
614
+ }
615
+ }
616
+ __unsafe_scheduleInputsChanged(nodeId) {
617
+ this.scheduleInputsChanged(nodeId);
618
+ }
619
+ // Incrementally update nodes/edges to match new definition without full rebuild
620
+ update(def, registry) {
621
+ // Handle node additions and removals
622
+ const desiredIds = new Set(def.nodes.map((n) => n.nodeId));
623
+ const currentIds = new Set(this.nodes.keys());
624
+ // Remove nodes not present
625
+ for (const nodeId of Array.from(currentIds)) {
626
+ if (!desiredIds.has(nodeId)) {
627
+ const node = this.nodes.get(nodeId);
628
+ node.runtime.onDeactivated?.();
629
+ node.runtime.dispose?.();
630
+ node.lifecycle?.dispose?.({
631
+ state: node.state,
632
+ setState: (next) => Object.assign(node.state, next),
633
+ });
634
+ this.nodes.delete(nodeId);
635
+ }
636
+ }
637
+ // Add or update existing nodes
638
+ for (const n of def.nodes) {
639
+ const existing = this.nodes.get(n.nodeId);
640
+ if (!existing) {
641
+ // create new runtime node
642
+ const desc = registry.nodes.get(n.typeId);
643
+ if (!desc)
644
+ throw new Error(`Unknown node type: ${n.typeId}`);
645
+ const cat = registry.categories.get(desc.categoryId);
646
+ if (!cat)
647
+ throw new Error(`Unknown category: ${desc.categoryId}`);
648
+ if (cat.validateImpl)
649
+ cat.validateImpl(desc.impl);
650
+ const runtime = cat.createRuntime({
651
+ nodeId: n.nodeId,
652
+ impl: desc.impl,
653
+ });
654
+ const rn = {
655
+ typeId: n.typeId,
656
+ nodeId: n.nodeId,
657
+ lifecycle: desc.lifecycle,
658
+ inputs: {},
659
+ outputs: {},
660
+ state: {},
661
+ runtime,
662
+ params: n.params,
663
+ policy: {
664
+ ...cat.policy,
665
+ ...(n.params && n.params.policy ? n.params.policy : {}),
666
+ },
667
+ activeControllers: new Set(),
668
+ queue: [],
669
+ stats: {
670
+ runs: 0,
671
+ active: 0,
672
+ queued: 0,
673
+ progress: 0,
674
+ },
675
+ };
676
+ this.nodes.set(n.nodeId, rn);
677
+ // Activate new node
678
+ const ctrl = new AbortController();
679
+ const ctx = {
680
+ state: rn.state,
681
+ setState: (next) => Object.assign(rn.state, next),
682
+ emit: (handle, value) => this.propagate(rn.nodeId, handle, value),
683
+ invalidateDownstream: () => this.invalidateDownstream(rn.nodeId),
684
+ getInput: (handle) => rn.inputs[handle],
685
+ environment: this.environment,
686
+ runId: `${rn.nodeId}:activation`,
687
+ abortSignal: ctrl.signal,
688
+ createAbortController: () => new AbortController(),
689
+ };
690
+ rn.lifecycle?.init?.(rn.params ?? {}, {
691
+ state: rn.state,
692
+ setState: (next) => Object.assign(rn.state, next),
693
+ });
694
+ rn.runtime.onActivated?.(ctx);
695
+ }
696
+ else {
697
+ // update params/policy
698
+ existing.params = n.params;
699
+ if (!existing.stats) {
700
+ existing.stats = {
701
+ runs: 0,
702
+ active: 0,
703
+ queued: 0,
704
+ progress: 0,
705
+ };
706
+ }
707
+ }
708
+ }
709
+ // Capture previous inbound map before rebuilding edges
710
+ const prevInbound = new Map();
711
+ for (const e of this.edges) {
712
+ const set = prevInbound.get(e.target.nodeId) ?? new Set();
713
+ set.add(e.target.handle);
714
+ prevInbound.set(e.target.nodeId, set);
715
+ }
716
+ // Rebuild edges mapping with coercions
717
+ this.edges = def.edges.map((e) => {
718
+ const srcNode = def.nodes.find((nn) => nn.nodeId === e.source.nodeId);
719
+ const dstNode = def.nodes.find((nn) => nn.nodeId === e.target.nodeId);
720
+ let effectiveTypeId = e.typeId;
721
+ let srcDeclared;
722
+ let dstDeclared;
723
+ if (srcNode) {
724
+ const srcDesc = registry.nodes.get(srcNode.typeId);
725
+ if (srcDesc) {
726
+ srcDeclared = srcDesc.outputs[e.source.handle];
727
+ }
728
+ }
729
+ if (!effectiveTypeId)
730
+ effectiveTypeId = srcDeclared;
731
+ if (dstNode) {
732
+ const dstDesc = registry.nodes.get(dstNode.typeId);
733
+ if (dstDesc) {
734
+ dstDeclared = dstDesc.inputs[e.target.handle];
735
+ }
736
+ }
737
+ let convert = undefined;
738
+ let convertAsync = undefined;
739
+ if (srcDeclared && dstDeclared && srcDeclared !== dstDeclared) {
740
+ const fn = registry.getCoercion(srcDeclared, dstDeclared);
741
+ if (fn)
742
+ convert = convert ?? fn;
743
+ const afn = registry.getAsyncCoercion(srcDeclared, dstDeclared);
744
+ if (afn)
745
+ convertAsync = convertAsync ?? afn;
746
+ }
747
+ return {
748
+ id: e.id,
749
+ source: { ...e.source },
750
+ target: { ...e.target },
751
+ typeId: effectiveTypeId ?? "untyped",
752
+ convert,
753
+ convertAsync,
754
+ stats: { runs: 0, inFlight: false, progress: 0 },
755
+ };
756
+ });
757
+ // Build new inbound map
758
+ const nextInbound = new Map();
759
+ for (const e of this.edges) {
760
+ const set = nextInbound.get(e.target.nodeId) ?? new Set();
761
+ set.add(e.target.handle);
762
+ nextInbound.set(e.target.nodeId, set);
763
+ }
764
+ // For inputs that lost inbound connections, clear and schedule recompute
765
+ for (const [nodeId, prevSet] of prevInbound) {
766
+ const currSet = nextInbound.get(nodeId) ?? new Set();
767
+ const node = this.nodes.get(nodeId);
768
+ if (!node)
769
+ continue;
770
+ let changed = false;
771
+ for (const handle of Array.from(prevSet)) {
772
+ if (!currSet.has(handle)) {
773
+ if (handle in node.inputs) {
774
+ delete node.inputs[handle];
775
+ changed = true;
776
+ }
777
+ }
778
+ }
779
+ if (changed)
780
+ this.scheduleInputsChanged(nodeId);
781
+ }
782
+ // Re-emit existing outputs to populate new edges
783
+ for (const nodeId of this.nodes.keys()) {
784
+ this.__unsafe_reemitNodeOutputs(nodeId);
785
+ }
786
+ }
787
+ }
788
+
789
+ class GraphBuilder {
790
+ constructor(registry) {
791
+ this.registry = registry;
792
+ }
793
+ validate(def) {
794
+ const issues = [];
795
+ const nodeIds = new Set();
796
+ const edgeIds = new Set();
797
+ // nodes exist, ids unique, and categories registered
798
+ for (const n of def.nodes) {
799
+ if (nodeIds.has(n.nodeId)) {
800
+ issues.push({
801
+ level: "error",
802
+ code: "NODE_ID_DUP",
803
+ message: `Duplicate nodeId ${n.nodeId}`,
804
+ data: { nodeId: n.nodeId },
805
+ });
806
+ }
807
+ else {
808
+ nodeIds.add(n.nodeId);
809
+ }
810
+ const nodeType = this.registry.nodes.get(n.typeId);
811
+ if (!nodeType) {
812
+ issues.push({
813
+ level: "error",
814
+ code: "NODE_TYPE_MISSING",
815
+ message: `Unknown node type ${n.typeId}`,
816
+ data: { typeId: n.typeId, nodeId: n.nodeId },
817
+ });
818
+ continue;
819
+ }
820
+ if (!this.registry.categories.has(nodeType.categoryId)) {
821
+ issues.push({
822
+ level: "error",
823
+ code: "CATEGORY_MISSING",
824
+ message: `Unknown category ${nodeType.categoryId} for node type ${n.typeId}`,
825
+ });
826
+ }
827
+ }
828
+ // edges validation: nodes exist, handles exist, type exists
829
+ const inboundCounts = new Map();
830
+ for (const e of def.edges) {
831
+ if (edgeIds.has(e.id)) {
832
+ issues.push({
833
+ level: "error",
834
+ code: "EDGE_ID_DUP",
835
+ message: `Duplicate edge id ${e.id}`,
836
+ data: { edgeId: e.id },
837
+ });
838
+ }
839
+ else {
840
+ edgeIds.add(e.id);
841
+ }
842
+ const srcNode = def.nodes.find((nn) => nn.nodeId === e.source.nodeId);
843
+ const dstNode = def.nodes.find((nn) => nn.nodeId === e.target.nodeId);
844
+ if (!srcNode)
845
+ issues.push({
846
+ level: "error",
847
+ code: "EDGE_SOURCE_MISSING",
848
+ message: `Edge ${e.id} source node missing`,
849
+ data: { edgeId: e.id },
850
+ });
851
+ if (!dstNode)
852
+ issues.push({
853
+ level: "error",
854
+ code: "EDGE_TARGET_MISSING",
855
+ message: `Edge ${e.id} target node missing`,
856
+ data: { edgeId: e.id },
857
+ });
858
+ // infer edge type from source output if missing
859
+ let effectiveTypeId = e.typeId;
860
+ if (!effectiveTypeId && srcNode) {
861
+ const srcType = this.registry.nodes.get(srcNode.typeId);
862
+ if (srcType) {
863
+ effectiveTypeId = srcType.outputs[e.source.handle];
864
+ }
865
+ }
866
+ const type = effectiveTypeId
867
+ ? this.registry.types.get(effectiveTypeId)
868
+ : undefined;
869
+ if (!type) {
870
+ issues.push({
871
+ level: "error",
872
+ code: "TYPE_MISSING",
873
+ message: `Edge ${e.id} type missing or unknown`,
874
+ data: { edgeId: e.id },
875
+ });
876
+ }
877
+ if (srcNode) {
878
+ const srcType = this.registry.nodes.get(srcNode.typeId);
879
+ if (srcType && !(e.source.handle in srcType.outputs)) {
880
+ issues.push({
881
+ level: "error",
882
+ code: "OUTPUT_MISSING",
883
+ message: `Edge ${e.id} source output ${e.source.handle} missing on ${srcNode.typeId}`,
884
+ data: {
885
+ edgeId: e.id,
886
+ nodeId: srcNode.nodeId,
887
+ output: e.source.handle,
888
+ },
889
+ });
890
+ }
891
+ if (srcType) {
892
+ const declared = srcType.outputs[e.source.handle];
893
+ if (declared &&
894
+ effectiveTypeId &&
895
+ declared !== effectiveTypeId &&
896
+ !this.registry.canCoerce(declared, effectiveTypeId)) {
897
+ issues.push({
898
+ level: "error",
899
+ code: "TYPE_MISMATCH_OUTPUT",
900
+ message: `Edge ${e.id} type ${effectiveTypeId} mismatches source output ${srcNode.typeId}.${e.source.handle} (${declared}) and no coercion exists`,
901
+ data: {
902
+ edgeId: e.id,
903
+ nodeId: srcNode.nodeId,
904
+ output: e.source.handle,
905
+ declared,
906
+ effectiveTypeId,
907
+ },
908
+ });
909
+ }
910
+ }
911
+ }
912
+ if (dstNode) {
913
+ const dstType = this.registry.nodes.get(dstNode.typeId);
914
+ if (dstType && !(e.target.handle in dstType.inputs)) {
915
+ issues.push({
916
+ level: "error",
917
+ code: "INPUT_MISSING",
918
+ message: `Edge ${e.id} target input ${e.target.handle} missing on ${dstNode.typeId}`,
919
+ data: {
920
+ edgeId: e.id,
921
+ nodeId: dstNode.nodeId,
922
+ input: e.target.handle,
923
+ },
924
+ });
925
+ }
926
+ if (dstType) {
927
+ const declared = dstType.inputs[e.target.handle];
928
+ if (declared &&
929
+ effectiveTypeId &&
930
+ declared !== effectiveTypeId &&
931
+ !this.registry.canCoerce(effectiveTypeId, declared)) {
932
+ issues.push({
933
+ level: "error",
934
+ code: "TYPE_MISMATCH_INPUT",
935
+ message: `Edge ${e.id} type ${effectiveTypeId} mismatches target input ${dstNode.typeId}.${e.target.handle} (${declared}) and no coercion exists`,
936
+ data: {
937
+ edgeId: e.id,
938
+ nodeId: dstNode.nodeId,
939
+ input: e.target.handle,
940
+ declared,
941
+ effectiveTypeId,
942
+ },
943
+ });
944
+ }
945
+ }
946
+ }
947
+ // Track multiple inbound edges targeting the same input handle
948
+ const inboundKey = `${e.target.nodeId}::${e.target.handle}`;
949
+ inboundCounts.set(inboundKey, (inboundCounts.get(inboundKey) ?? 0) + 1);
950
+ }
951
+ for (const [key, count] of inboundCounts) {
952
+ if (count > 1) {
953
+ issues.push({
954
+ level: "warning",
955
+ code: "MULTI_INBOUND",
956
+ message: `Input ${key} has ${count} inbound edges (last-write wins).`,
957
+ data: { nodeId: key.split("::")[0], input: key.split("::")[1] },
958
+ });
959
+ }
960
+ }
961
+ return { ok: issues.every((i) => i.level !== "error"), issues };
962
+ }
963
+ build(def, opts) {
964
+ return GraphRuntime.create(def, this.registry, opts);
965
+ }
966
+ wrapAsNode(def, exposure, nodeTypeId, displayName) {
967
+ // Infer exposed handle types from inner graph using registry
968
+ const inputTypes = {};
969
+ const outputTypes = {};
970
+ for (const [outerIn, map] of Object.entries(exposure.inputs)) {
971
+ const innerNode = def.nodes.find((n) => n.nodeId === map.nodeId);
972
+ const innerDesc = innerNode
973
+ ? this.registry.nodes.get(innerNode.typeId)
974
+ : undefined;
975
+ const typeId = innerDesc ? innerDesc.inputs[map.handle] : undefined;
976
+ inputTypes[outerIn] = typeId ?? "untyped";
977
+ }
978
+ for (const [outerOut, map] of Object.entries(exposure.outputs)) {
979
+ const innerNode = def.nodes.find((n) => n.nodeId === map.nodeId);
980
+ const innerDesc = innerNode
981
+ ? this.registry.nodes.get(innerNode.typeId)
982
+ : undefined;
983
+ const typeId = innerDesc ? innerDesc.outputs[map.handle] : undefined;
984
+ outputTypes[outerOut] = typeId ?? "untyped";
985
+ }
986
+ return {
987
+ id: nodeTypeId,
988
+ displayName,
989
+ categoryId: "composite",
990
+ inputs: inputTypes,
991
+ outputs: outputTypes,
992
+ impl: { def, exposure },
993
+ };
994
+ }
995
+ }
996
+
997
+ class AbstractEngine {
998
+ constructor(graphRuntime) {
999
+ this.graphRuntime = graphRuntime;
1000
+ }
1001
+ launch() {
1002
+ this.graphRuntime.launch();
1003
+ }
1004
+ setInput(nodeId, handle, value) {
1005
+ this.graphRuntime.setInput(nodeId, handle, value);
1006
+ }
1007
+ triggerExternal(nodeId, event) {
1008
+ this.graphRuntime.triggerExternal(nodeId, event);
1009
+ }
1010
+ on(event, handler) {
1011
+ return this.graphRuntime.on(event, handler);
1012
+ }
1013
+ getOutput(nodeId, output) {
1014
+ return this.graphRuntime.getOutput(nodeId, output);
1015
+ }
1016
+ whenIdle() {
1017
+ return this.graphRuntime.whenIdle();
1018
+ }
1019
+ dispose() {
1020
+ this.graphRuntime.dispose();
1021
+ }
1022
+ }
1023
+
1024
+ class PushEngine extends AbstractEngine {
1025
+ constructor(graphRuntime) {
1026
+ super(graphRuntime);
1027
+ }
1028
+ }
1029
+
1030
+ class LocalRunner {
1031
+ constructor(registry) {
1032
+ this.registry = registry;
1033
+ this.builder = new GraphBuilder(registry);
1034
+ }
1035
+ async build(def, opts) {
1036
+ const rt = this.builder.build(def, opts);
1037
+ this.engine = new PushEngine(rt);
1038
+ }
1039
+ async update(def) {
1040
+ // If engine exists and is a PushEngine backed by GraphRuntime, use runtime.update
1041
+ // Otherwise rebuild.
1042
+ const eng = this.engine;
1043
+ if (eng && eng.graphRuntime) {
1044
+ eng.graphRuntime.update(def, this.registry);
1045
+ return;
1046
+ }
1047
+ await this.build(def);
1048
+ }
1049
+ getEngine() {
1050
+ if (!this.engine)
1051
+ throw new Error("Engine not built. Call build(def) first.");
1052
+ return this.engine;
1053
+ }
1054
+ }
1055
+
1056
+ class BatchedEngine extends AbstractEngine {
1057
+ constructor(graphRuntime, opts = {}) {
1058
+ super(graphRuntime);
1059
+ this.opts = opts;
1060
+ this.dirtyNodes = new Set();
1061
+ }
1062
+ launch() {
1063
+ this.graphRuntime.pause();
1064
+ if (this.opts.flushIntervalMs && this.opts.flushIntervalMs > 0) {
1065
+ this.timer = setInterval(() => this.flush(), this.opts.flushIntervalMs);
1066
+ }
1067
+ }
1068
+ setInput(nodeId, handle, value) {
1069
+ super.setInput(nodeId, handle, value);
1070
+ this.dirtyNodes.add(nodeId);
1071
+ }
1072
+ triggerExternal(nodeId, event) {
1073
+ super.triggerExternal(nodeId, event);
1074
+ this.dirtyNodes.add(nodeId);
1075
+ }
1076
+ async flush() {
1077
+ if (this.dirtyNodes.size === 0)
1078
+ return;
1079
+ // Resume, schedule dirty nodes, wait idle, then pause again
1080
+ const nodes = Array.from(this.dirtyNodes);
1081
+ this.dirtyNodes.clear();
1082
+ this.graphRuntime.resume();
1083
+ for (const n of nodes)
1084
+ this.graphRuntime.__unsafe_scheduleInputsChanged(n);
1085
+ await this.graphRuntime.whenIdle();
1086
+ this.graphRuntime.pause();
1087
+ }
1088
+ dispose() {
1089
+ if (this.timer)
1090
+ clearInterval(this.timer);
1091
+ super.dispose();
1092
+ }
1093
+ }
1094
+
1095
+ // PullEngine computes only when asked, otherwise holds inputs without scheduling
1096
+ class PullEngine extends AbstractEngine {
1097
+ constructor(graphRuntime) {
1098
+ super(graphRuntime);
1099
+ this.graphRuntime.pause();
1100
+ }
1101
+ launch() { }
1102
+ // Pull API
1103
+ async computeNode(nodeId) {
1104
+ this.graphRuntime.resume();
1105
+ this.graphRuntime.__unsafe_scheduleInputsChanged(nodeId);
1106
+ await this.graphRuntime.whenIdle();
1107
+ this.graphRuntime.pause();
1108
+ }
1109
+ }
1110
+
1111
+ class HybridEngine extends AbstractEngine {
1112
+ constructor(graphRuntime, opts = {}) {
1113
+ super(graphRuntime);
1114
+ this.opts = opts;
1115
+ this.windowStart = 0;
1116
+ this.countInWindow = 0;
1117
+ this.batching = false;
1118
+ this.dirtyNodes = new Set();
1119
+ this.windowStart = Date.now();
1120
+ }
1121
+ updateWindow() {
1122
+ const now = Date.now();
1123
+ const windowMs = this.opts.windowMs ?? 250;
1124
+ if (now - this.windowStart > windowMs) {
1125
+ this.windowStart = now;
1126
+ this.countInWindow = 0;
1127
+ if (this.batching) {
1128
+ this.graphRuntime.resume();
1129
+ this.batching = false;
1130
+ // schedule all dirty nodes accumulated during batching
1131
+ const nodes = Array.from(this.dirtyNodes);
1132
+ this.dirtyNodes.clear();
1133
+ for (const n of nodes)
1134
+ this.graphRuntime.__unsafe_scheduleInputsChanged(n);
1135
+ if (this.flushTimer) {
1136
+ clearTimeout(this.flushTimer);
1137
+ this.flushTimer = undefined;
1138
+ }
1139
+ }
1140
+ }
1141
+ }
1142
+ launch() {
1143
+ this.graphRuntime.resume();
1144
+ }
1145
+ setInput(nodeId, handle, value) {
1146
+ this.updateWindow();
1147
+ this.countInWindow += 1;
1148
+ const threshold = this.opts.batchThreshold ?? 5;
1149
+ if (!this.batching && this.countInWindow >= threshold) {
1150
+ this.graphRuntime.pause();
1151
+ this.batching = true;
1152
+ // ensure flush even if no more inputs arrive
1153
+ const windowMs = this.opts.windowMs ?? 250;
1154
+ if (this.flushTimer)
1155
+ clearTimeout(this.flushTimer);
1156
+ this.flushTimer = setTimeout(() => {
1157
+ if (!this.batching)
1158
+ return;
1159
+ this.graphRuntime.resume();
1160
+ this.batching = false;
1161
+ const nodes = Array.from(this.dirtyNodes);
1162
+ this.dirtyNodes.clear();
1163
+ for (const n of nodes)
1164
+ this.graphRuntime.__unsafe_scheduleInputsChanged(n);
1165
+ this.flushTimer = undefined;
1166
+ }, windowMs);
1167
+ }
1168
+ super.setInput(nodeId, handle, value);
1169
+ this.dirtyNodes.add(nodeId);
1170
+ if (!this.batching)
1171
+ this.graphRuntime.__unsafe_scheduleInputsChanged(nodeId);
1172
+ }
1173
+ triggerExternal(nodeId, event) {
1174
+ super.triggerExternal(nodeId, event);
1175
+ this.dirtyNodes.add(nodeId);
1176
+ }
1177
+ dispose() {
1178
+ if (this.flushTimer) {
1179
+ clearTimeout(this.flushTimer);
1180
+ this.flushTimer = undefined;
1181
+ }
1182
+ super.dispose();
1183
+ }
1184
+ }
1185
+
1186
+ // StepEngine: expose explicit step() to process pending changes once
1187
+ class StepEngine extends AbstractEngine {
1188
+ constructor(graphRuntime) {
1189
+ super(graphRuntime);
1190
+ this.dirtyNodes = new Set();
1191
+ this.graphRuntime.pause();
1192
+ }
1193
+ launch() { }
1194
+ setInput(nodeId, handle, value) {
1195
+ super.setInput(nodeId, handle, value);
1196
+ this.dirtyNodes.add(nodeId);
1197
+ }
1198
+ triggerExternal(nodeId, event) {
1199
+ super.triggerExternal(nodeId, event);
1200
+ this.dirtyNodes.add(nodeId);
1201
+ }
1202
+ async step() {
1203
+ // resume first so scheduling isn't ignored due to pause
1204
+ const nodes = Array.from(this.dirtyNodes);
1205
+ this.dirtyNodes.clear();
1206
+ this.graphRuntime.resume();
1207
+ for (const n of nodes)
1208
+ this.graphRuntime.__unsafe_scheduleInputsChanged(n);
1209
+ await this.graphRuntime.whenIdle();
1210
+ this.graphRuntime.pause();
1211
+ }
1212
+ }
1213
+
1214
+ const ComputeCategory = {
1215
+ id: "compute",
1216
+ displayName: "Compute",
1217
+ createRuntime: ({ impl }) => ({
1218
+ async onInputsChanged(inputs, ctx) {
1219
+ const out = await impl(inputs, ctx);
1220
+ if (out && typeof out === "object") {
1221
+ for (const [h, v] of Object.entries(out))
1222
+ ctx.emit(h, v);
1223
+ }
1224
+ },
1225
+ }),
1226
+ policy: { mode: "push", asyncConcurrency: "switch" },
1227
+ };
1228
+
1229
+ const CompositeCategory = (registry) => ({
1230
+ id: "composite",
1231
+ displayName: "Composite",
1232
+ validateImpl: (impl) => {
1233
+ if (!impl || !impl.def)
1234
+ throw new Error("Composite impl requires def");
1235
+ },
1236
+ createRuntime: ({ impl }) => {
1237
+ let inner;
1238
+ let unsub;
1239
+ return {
1240
+ onActivated: () => {
1241
+ inner = GraphRuntime.create(impl.def, registry);
1242
+ // Wire inner outputs to outer emits
1243
+ unsub = inner.on("value", (e) => {
1244
+ for (const [outHandle, map] of Object.entries(impl.exposure.outputs)) {
1245
+ if (e.nodeId === map.nodeId && e.handle === map.handle) ;
1246
+ }
1247
+ });
1248
+ inner.launch();
1249
+ },
1250
+ onInputsChanged: (inputs, ctx) => {
1251
+ if (!inner)
1252
+ return;
1253
+ // map outer input => inner node input
1254
+ for (const [inHandle, map] of Object.entries(impl.exposure.inputs)) {
1255
+ if (inHandle in inputs)
1256
+ inner.setInput(map.nodeId, map.handle, inputs[inHandle]);
1257
+ }
1258
+ // pull inner exposed outputs and emit
1259
+ for (const [outHandle, map] of Object.entries(impl.exposure.outputs)) {
1260
+ const v = inner.getOutput(map.nodeId, map.handle);
1261
+ if (v !== undefined)
1262
+ ctx.emit(outHandle, v);
1263
+ }
1264
+ },
1265
+ onDeactivated: () => {
1266
+ if (unsub)
1267
+ unsub();
1268
+ },
1269
+ dispose: () => {
1270
+ if (unsub)
1271
+ unsub();
1272
+ inner?.dispose();
1273
+ },
1274
+ };
1275
+ },
1276
+ policy: { mode: "hybrid" },
1277
+ });
1278
+
1279
+ function setupBasicGraphRegistry() {
1280
+ const registry = new Registry();
1281
+ registry.categories.register(ComputeCategory);
1282
+ const floatType = {
1283
+ id: "float",
1284
+ validate: (v) => typeof v === "number" && !Number.isNaN(v),
1285
+ };
1286
+ registry.registerType(floatType);
1287
+ registry.registerSerializer("float", {
1288
+ serialize: (v) => v,
1289
+ deserialize: (d) => Number(d),
1290
+ });
1291
+ const boolType = {
1292
+ id: "bool",
1293
+ validate: (v) => typeof v === "boolean",
1294
+ };
1295
+ const stringType = {
1296
+ id: "string",
1297
+ validate: (v) => typeof v === "string",
1298
+ };
1299
+ const vec3Type = {
1300
+ id: "vec3",
1301
+ validate: (v) => Array.isArray(v) &&
1302
+ v.length === 3 &&
1303
+ v.every((x) => typeof x === "number"),
1304
+ };
1305
+ const floatArrayType = {
1306
+ id: "float[]",
1307
+ validate: (v) => Array.isArray(v) && v.every((x) => typeof x === "number"),
1308
+ };
1309
+ const boolArrayType = {
1310
+ id: "bool[]",
1311
+ validate: (v) => Array.isArray(v) && v.every((x) => typeof x === "boolean"),
1312
+ };
1313
+ const vec3ArrayType = {
1314
+ id: "vec3[]",
1315
+ validate: (v) => Array.isArray(v) &&
1316
+ v.every((x) => Array.isArray(x) &&
1317
+ x.length === 3 &&
1318
+ x.every((n) => typeof n === "number")),
1319
+ };
1320
+ [
1321
+ boolType,
1322
+ stringType,
1323
+ vec3Type,
1324
+ floatType,
1325
+ floatArrayType,
1326
+ boolArrayType,
1327
+ vec3ArrayType,
1328
+ ].forEach((t) => {
1329
+ registry.registerType(t);
1330
+ registry.registerSerializer(t.id, {
1331
+ serialize: (v) => v,
1332
+ deserialize: (d) => d,
1333
+ });
1334
+ });
1335
+ // Helpers
1336
+ const asArray = (v) => Array.isArray(v) ? v : [Number(v)];
1337
+ // Register core coercions: float <-> float[]
1338
+ registry.registerCoercion("float", "float[]", (v) => Array.isArray(v) ? v : [Number(v)]);
1339
+ registry.registerCoercion("float[]", "float", (v) => Array.isArray(v) ? Number(v[0] ?? 0) : Number(v));
1340
+ // float[] -> vec3[] : map x to [x,0,0]
1341
+ registry.registerCoercion("float[]", "vec3[]", (v) => {
1342
+ const arr = asArray(v);
1343
+ return arr.map((x) => [Number(x) || 0, 0, 0]);
1344
+ });
1345
+ // Example async coercion: simulate expensive conversion float[] -> vec3[] by computing magnitudes
1346
+ registry.registerCoercion("float[]", "vec3[]", (v) => {
1347
+ // synchronous fallback; async version can be provided on edge via convertAsync
1348
+ if (!Array.isArray(v))
1349
+ return [];
1350
+ return v.map((t) => Math.hypot(Number(t?.[0] ?? 0), Number(t?.[1] ?? 0), Number(t?.[2] ?? 0)));
1351
+ });
1352
+ // Async coercion variant for vec3[] -> float[] (chunked + abortable)
1353
+ registry.registerAsyncCoercion("vec3[]", "float[]", async (value, signal) => {
1354
+ const arr = Array.isArray(value)
1355
+ ? value
1356
+ : [];
1357
+ const out = new Array(arr.length);
1358
+ for (let i = 0; i < arr.length; i++) {
1359
+ if (signal.aborted)
1360
+ throw new DOMException("Aborted", "AbortError");
1361
+ const v = arr[i] ?? [0, 0, 0];
1362
+ await new Promise((r) => setTimeout(r, 1000));
1363
+ out[i] = Math.hypot(Number(v[0] ?? 0), Number(v[1] ?? 0), Number(v[2] ?? 0));
1364
+ }
1365
+ return out;
1366
+ });
1367
+ const broadcast = (a, b) => {
1368
+ const aa = asArray(a);
1369
+ const bb = asArray(b);
1370
+ if (aa.length === bb.length)
1371
+ return [aa, bb];
1372
+ if (aa.length === 1)
1373
+ return [new Array(bb.length).fill(aa[0]), bb];
1374
+ if (bb.length === 1)
1375
+ return [aa, new Array(aa.length).fill(bb[0])];
1376
+ const len = Math.max(aa.length, bb.length);
1377
+ return [new Array(len).fill(aa[0] ?? 0), new Array(len).fill(bb[0] ?? 0)];
1378
+ };
1379
+ const clamp = (x, min, max) => Math.min(max, Math.max(min, x));
1380
+ const lerp = (a, b, t) => a + (b - a) * t;
1381
+ const lcg = (seed) => {
1382
+ let s = seed >>> 0 || 1;
1383
+ return () => (s = (s * 1664525 + 1013904223) >>> 0) / 0xffffffff;
1384
+ };
1385
+ // Number
1386
+ registry.registerNode({
1387
+ id: "number",
1388
+ categoryId: "compute",
1389
+ inputs: { Value: "float" },
1390
+ outputs: { Result: "float" },
1391
+ impl: (ins) => ({ Result: Number(ins.Value) }),
1392
+ });
1393
+ // Integer
1394
+ registry.registerNode({
1395
+ id: "integer",
1396
+ categoryId: "compute",
1397
+ inputs: { Value: "float" },
1398
+ outputs: { Result: "float" },
1399
+ impl: (ins) => ({
1400
+ Result: Math.trunc(Number(ins.Value)),
1401
+ }),
1402
+ });
1403
+ // Number to String
1404
+ registry.registerNode({
1405
+ id: "numberToString",
1406
+ categoryId: "compute",
1407
+ inputs: { Value: "float" },
1408
+ outputs: { Text: "string" },
1409
+ impl: (ins) => ({ Text: String(ins.Value) }),
1410
+ });
1411
+ // Enums: Math Operation
1412
+ registry.registerEnum("enum:math.operation", [
1413
+ { value: 0, label: "Add" },
1414
+ { value: 1, label: "Subtract" },
1415
+ { value: 2, label: "Multiply" },
1416
+ { value: 3, label: "Divide" },
1417
+ { value: 4, label: "Min" },
1418
+ { value: 5, label: "Max" },
1419
+ { value: 6, label: "Modulo" },
1420
+ { value: 7, label: "Power" },
1421
+ ], "string", "float");
1422
+ // Enums: Compare Operation
1423
+ registry.registerEnum("enum:compare.operation", [
1424
+ { value: 0, label: "LessThan" },
1425
+ { value: 1, label: "LessThanOrEqual" },
1426
+ { value: 2, label: "GreaterThan" },
1427
+ { value: 3, label: "GreaterThanOrEqual" },
1428
+ { value: 4, label: "Equal" },
1429
+ { value: 5, label: "NotEqual" },
1430
+ ], "string", "float");
1431
+ // Clamp
1432
+ registry.registerNode({
1433
+ id: "clamp",
1434
+ categoryId: "compute",
1435
+ inputs: { Value: "float[]", Min: "float", Max: "float" },
1436
+ outputs: { Value: "float[]" },
1437
+ impl: (ins) => {
1438
+ const vals = asArray(ins.Value);
1439
+ const min = Number(ins.Min ?? 0);
1440
+ const max = Number(ins.Max ?? 1);
1441
+ return { Value: vals.map((v) => clamp(Number(v), min, max)) };
1442
+ },
1443
+ });
1444
+ // Interpolate (lerp)
1445
+ registry.registerNode({
1446
+ id: "interpolate",
1447
+ categoryId: "compute",
1448
+ inputs: { ValueA: "float[]", ValueB: "float[]", Factor: "float" },
1449
+ outputs: { Value: "float[]" },
1450
+ impl: (ins) => {
1451
+ const [a, b] = broadcast(ins.ValueA, ins.ValueB);
1452
+ const t = Number(ins.Factor ?? 0);
1453
+ const len = Math.max(a.length, b.length);
1454
+ const out = new Array(len)
1455
+ .fill(0)
1456
+ .map((_, i) => lerp(Number(a[i] ?? 0), Number(b[i] ?? 0), t));
1457
+ return { Value: out };
1458
+ },
1459
+ });
1460
+ // Map Range (linear)
1461
+ registry.registerNode({
1462
+ id: "mapRange",
1463
+ categoryId: "compute",
1464
+ inputs: {
1465
+ Mode: "string",
1466
+ Clamp: "bool",
1467
+ Value: "float[]",
1468
+ FromMin: "float",
1469
+ FromMax: "float",
1470
+ ToMin: "float",
1471
+ ToMax: "float",
1472
+ },
1473
+ outputs: { Value: "float[]" },
1474
+ impl: (ins) => {
1475
+ const vals = asArray(ins.Value);
1476
+ const fromMin = Number(ins.FromMin ?? 0);
1477
+ const fromMax = Number(ins.FromMax ?? 1);
1478
+ const toMin = Number(ins.ToMin ?? 0);
1479
+ const toMax = Number(ins.ToMax ?? 1);
1480
+ const doClamp = Boolean(ins.Clamp);
1481
+ const out = vals.map((v) => {
1482
+ const t = (Number(v) - fromMin) / (fromMax - fromMin || 1);
1483
+ const r = toMin + t * (toMax - toMin);
1484
+ return doClamp
1485
+ ? clamp(r, Math.min(toMin, toMax), Math.max(toMin, toMax))
1486
+ : r;
1487
+ });
1488
+ return { Value: out };
1489
+ },
1490
+ });
1491
+ // Math (subset) - scalar version for simple examples
1492
+ registry.registerNode({
1493
+ id: "math",
1494
+ categoryId: "compute",
1495
+ inputs: { Operation: "enum:math.operation", A: "float[]", B: "float[]" },
1496
+ outputs: { Result: "float[]" },
1497
+ impl: (ins) => {
1498
+ // Gracefully handle missing inputs by treating them as zeros
1499
+ const a = ins.A === undefined ? [] : asArray(ins.A);
1500
+ const b = ins.B === undefined ? [] : asArray(ins.B);
1501
+ const len = Math.max(a.length, b.length);
1502
+ const op = Number(ins.Operation ?? 0) | 0;
1503
+ const ops = [
1504
+ (x, y) => x + y,
1505
+ (x, y) => x - y,
1506
+ (x, y) => x * y,
1507
+ (x, y) => x / (y || 1),
1508
+ (x, y) => Math.min(x, y),
1509
+ (x, y) => Math.max(x, y),
1510
+ (x, y) => (y ? x % y : 0),
1511
+ (x, y) => Math.pow(x, y),
1512
+ ];
1513
+ const fn = ops[op] ?? ops[0];
1514
+ const out = new Array(len).fill(0).map((_, i) => {
1515
+ const ax = a.length === 1 && len > 1 ? a[0] : a[i] ?? 0;
1516
+ const bx = b.length === 1 && len > 1 ? b[0] : b[i] ?? 0;
1517
+ return fn(Number(ax), Number(bx));
1518
+ });
1519
+ return { Result: out };
1520
+ },
1521
+ });
1522
+ // Compare
1523
+ registry.registerNode({
1524
+ id: "compare",
1525
+ categoryId: "compute",
1526
+ inputs: { Operation: "enum:compare.operation", A: "float[]", B: "float[]" },
1527
+ outputs: { Result: "bool[]" },
1528
+ impl: (ins) => {
1529
+ const [a, b] = broadcast(ins.A, ins.B);
1530
+ const op = Number(ins.Operation ?? 4) | 0; // default Equal
1531
+ const ops = [
1532
+ (x, y) => x < y,
1533
+ (x, y) => x <= y,
1534
+ (x, y) => x > y,
1535
+ (x, y) => x >= y,
1536
+ (x, y) => x === y,
1537
+ (x, y) => x !== y,
1538
+ ];
1539
+ const fn = ops[op] ?? ops[4];
1540
+ return { Result: a.map((x, i) => fn(Number(x), Number(b[i] ?? 0))) };
1541
+ },
1542
+ });
1543
+ // Combine XYZ
1544
+ registry.registerNode({
1545
+ id: "combineXYZ",
1546
+ categoryId: "compute",
1547
+ inputs: { X: "float[]", Y: "float[]", Z: "float[]" },
1548
+ outputs: { XYZ: "vec3[]" },
1549
+ impl: (ins) => {
1550
+ const [x, y] = broadcast(ins.X, ins.Y);
1551
+ const [xx, z] = broadcast(x, ins.Z);
1552
+ const len = Math.max(xx.length, z.length);
1553
+ const out = new Array(len)
1554
+ .fill(0)
1555
+ .map((_, i) => [Number(xx[i] ?? 0), Number(y[i] ?? 0), Number(z[i] ?? 0)]);
1556
+ return { XYZ: out };
1557
+ },
1558
+ });
1559
+ // Separate XYZ
1560
+ registry.registerNode({
1561
+ id: "separateXYZ",
1562
+ categoryId: "compute",
1563
+ inputs: { XYZ: "vec3[]" },
1564
+ outputs: { X: "float[]", Y: "float[]", Z: "float[]" },
1565
+ impl: (ins) => {
1566
+ const arr = ins.XYZ ?? [];
1567
+ const X = arr.map((v) => Number(v?.[0] ?? 0));
1568
+ const Y = arr.map((v) => Number(v?.[1] ?? 0));
1569
+ const Z = arr.map((v) => Number(v?.[2] ?? 0));
1570
+ return { X, Y, Z };
1571
+ },
1572
+ });
1573
+ // Indices
1574
+ registry.registerNode({
1575
+ id: "indices",
1576
+ categoryId: "compute",
1577
+ inputs: { Domain: "float" },
1578
+ outputs: { Indices: "float[]" },
1579
+ impl: (ins) => {
1580
+ const n = Math.trunc(ins.Domain);
1581
+ return { Indices: Array.from({ length: n }, (_, i) => i) };
1582
+ },
1583
+ });
1584
+ // Random Numbers
1585
+ registry.registerNode({
1586
+ id: "randomNumbers",
1587
+ categoryId: "compute",
1588
+ inputs: { Domain: "float", Min: "float", Max: "float", Seed: "float" },
1589
+ outputs: { Values: "float[]" },
1590
+ impl: (ins) => {
1591
+ const len = Math.trunc(ins.Domain);
1592
+ const min = Number(ins.Min ?? 0);
1593
+ const max = Number(ins.Max ?? 1);
1594
+ const rng = lcg(Number(ins.Seed ?? 1));
1595
+ const out = Array.from({ length: len }, () => min + rng() * (max - min));
1596
+ return { Values: out };
1597
+ },
1598
+ });
1599
+ // Random Vectors
1600
+ registry.registerNode({
1601
+ id: "randomVectors",
1602
+ categoryId: "compute",
1603
+ inputs: { Domain: "float", Min: "vec3", Max: "vec3", Seed: "float" },
1604
+ outputs: { Values: "vec3[]" },
1605
+ impl: (ins) => {
1606
+ const len = Math.trunc(ins.Domain);
1607
+ const min = ins.Min ?? [0, 0, 0];
1608
+ const max = ins.Max ?? [1, 1, 1];
1609
+ const rng = lcg(Number(ins.Seed ?? 1));
1610
+ const out = Array.from({ length: len }, () => [
1611
+ min[0] + rng() * (max[0] - min[0]),
1612
+ min[1] + rng() * (max[1] - min[1]),
1613
+ min[2] + rng() * (max[2] - min[2]),
1614
+ ]);
1615
+ return { Values: out };
1616
+ },
1617
+ });
1618
+ return registry;
1619
+ }
1620
+ function makeBasicGraphDefinition() {
1621
+ return {
1622
+ nodes: [
1623
+ { nodeId: "n1", typeId: "math" },
1624
+ { nodeId: "n2", typeId: "math" },
1625
+ ],
1626
+ edges: [
1627
+ {
1628
+ id: "e1",
1629
+ source: { nodeId: "n1", handle: "Result" },
1630
+ target: { nodeId: "n2", handle: "A" },
1631
+ },
1632
+ ],
1633
+ };
1634
+ }
1635
+ function registerDelayNode(registry) {
1636
+ registry.registerNode({
1637
+ id: "delay",
1638
+ categoryId: "compute",
1639
+ inputs: { x: "float", ms: "float" },
1640
+ outputs: { out: "float" },
1641
+ impl: async (ins, ctx) => {
1642
+ const ms = Number(ins.ms ?? 200);
1643
+ const xRaw = ins.x;
1644
+ if (xRaw === undefined || xRaw === null || Number.isNaN(Number(xRaw))) {
1645
+ return; // wait until x is present to avoid NaN emissions
1646
+ }
1647
+ await new Promise((resolve, reject) => {
1648
+ const id = setTimeout(resolve, ms);
1649
+ const onAbort = () => {
1650
+ clearTimeout(id);
1651
+ reject(new DOMException("Aborted", "AbortError"));
1652
+ };
1653
+ if (ctx.abortSignal.aborted)
1654
+ return onAbort();
1655
+ ctx.abortSignal.addEventListener("abort", onAbort, { once: true });
1656
+ });
1657
+ return { out: Number(xRaw) };
1658
+ },
1659
+ });
1660
+ }
1661
+ function sleepWithAbort(ms, signal) {
1662
+ return new Promise((resolve, reject) => {
1663
+ const id = setTimeout(() => {
1664
+ cleanup();
1665
+ resolve();
1666
+ }, ms);
1667
+ const onAbort = () => {
1668
+ clearTimeout(id);
1669
+ cleanup();
1670
+ reject(new DOMException("Aborted", "AbortError"));
1671
+ };
1672
+ const cleanup = () => {
1673
+ signal.removeEventListener("abort", onAbort);
1674
+ };
1675
+ if (signal.aborted)
1676
+ return onAbort();
1677
+ signal.addEventListener("abort", onAbort);
1678
+ });
1679
+ }
1680
+ function registerProgressNodes(registry) {
1681
+ registry.registerNode({
1682
+ id: "progressWorker",
1683
+ categoryId: "compute",
1684
+ inputs: { Steps: "float", DelayMs: "float", ShouldError: "bool" },
1685
+ outputs: { Done: "string" },
1686
+ impl: async (ins, ctx) => {
1687
+ const steps = Math.max(1, Math.trunc(Number(ins.Steps ?? 10)));
1688
+ const delayMs = Math.max(0, Math.trunc(Number(ins.DelayMs ?? 50)));
1689
+ const shouldError = Boolean(ins.ShouldError);
1690
+ for (let i = 0; i < steps; i++) {
1691
+ ctx.reportProgress?.(i / steps);
1692
+ await sleepWithAbort(delayMs, ctx.abortSignal);
1693
+ if (shouldError && i >= Math.floor(steps * 0.7)) {
1694
+ ctx.reportProgress?.(i / steps);
1695
+ throw new Error("progressWorker: simulated failure at 70% progress");
1696
+ }
1697
+ }
1698
+ ctx.reportProgress?.(1);
1699
+ return { Done: `Completed ${steps} steps` };
1700
+ },
1701
+ });
1702
+ }
1703
+
1704
+ function createSimpleGraphDef() {
1705
+ return makeBasicGraphDefinition();
1706
+ }
1707
+ function createSimpleGraphRegistry() {
1708
+ return setupBasicGraphRegistry();
1709
+ }
1710
+
1711
+ function createAsyncGraphDef() {
1712
+ const def = {
1713
+ nodes: [
1714
+ {
1715
+ nodeId: "n1",
1716
+ typeId: "math",
1717
+ },
1718
+ {
1719
+ nodeId: "n2",
1720
+ typeId: "delay",
1721
+ params: { policy: { asyncConcurrency: "queue", maxQueue: 4 } },
1722
+ },
1723
+ {
1724
+ nodeId: "n3",
1725
+ typeId: "separateXYZ",
1726
+ },
1727
+ {
1728
+ nodeId: "n4",
1729
+ typeId: "combineXYZ",
1730
+ },
1731
+ ],
1732
+ edges: [
1733
+ {
1734
+ id: "e1",
1735
+ source: { nodeId: "n1", handle: "Result" },
1736
+ target: { nodeId: "n2", handle: "x" },
1737
+ },
1738
+ // Demonstrate async edge conversion: vec3[] -> float[] using coercion
1739
+ {
1740
+ id: "e2",
1741
+ source: { nodeId: "n4", handle: "XYZ" },
1742
+ target: { nodeId: "n1", handle: "A" },
1743
+ typeId: "vec3[]",
1744
+ // convertAsync,
1745
+ },
1746
+ {
1747
+ id: "e3",
1748
+ source: { nodeId: "n3", handle: "X" },
1749
+ target: { nodeId: "n4", handle: "X" },
1750
+ },
1751
+ {
1752
+ id: "e4",
1753
+ source: { nodeId: "n3", handle: "Y" },
1754
+ target: { nodeId: "n4", handle: "Y" },
1755
+ },
1756
+ {
1757
+ id: "e5",
1758
+ source: { nodeId: "n3", handle: "Z" },
1759
+ target: { nodeId: "n4", handle: "Z" },
1760
+ },
1761
+ ],
1762
+ };
1763
+ return def;
1764
+ }
1765
+ function createAsyncGraphRegistry() {
1766
+ const registry = setupBasicGraphRegistry();
1767
+ registerDelayNode(registry);
1768
+ return registry;
1769
+ }
1770
+
1771
+ function createProgressGraphDef() {
1772
+ const def = {
1773
+ nodes: [
1774
+ { nodeId: "steps", typeId: "number" },
1775
+ { nodeId: "delay", typeId: "number" },
1776
+ { nodeId: "work", typeId: "progressWorker" },
1777
+ ],
1778
+ edges: [
1779
+ {
1780
+ id: "e1",
1781
+ source: { nodeId: "steps", handle: "Result" },
1782
+ target: { nodeId: "work", handle: "Steps" },
1783
+ },
1784
+ {
1785
+ id: "e2",
1786
+ source: { nodeId: "delay", handle: "Result" },
1787
+ target: { nodeId: "work", handle: "DelayMs" },
1788
+ },
1789
+ // not wiring ShouldError to show manual input driven error later
1790
+ ],
1791
+ };
1792
+ return def;
1793
+ }
1794
+ function createProgressGraphRegistry() {
1795
+ const registry = setupBasicGraphRegistry();
1796
+ registerProgressNodes(registry);
1797
+ return registry;
1798
+ }
1799
+
1800
+ function createValidationGraphDef() {
1801
+ // Intentionally build a graph with validation issues:
1802
+ // - Unknown edge type (wire number to boolean input without coercion)
1803
+ // - Missing target input handle
1804
+ // - Multi inbound to same input
1805
+ const def = {
1806
+ nodes: [
1807
+ { nodeId: "nA", typeId: "number" },
1808
+ { nodeId: "nB", typeId: "number" },
1809
+ { nodeId: "nC", typeId: "math" },
1810
+ { nodeId: "s1", typeId: "numberToString" },
1811
+ { nodeId: "cmp", typeId: "compare" },
1812
+ // Global validation issue: unknown node type (no nodeId/edgeId in data)
1813
+ { nodeId: "bad", typeId: "unknownType" },
1814
+ ],
1815
+ edges: [
1816
+ // Valid: nA.Result -> nC.A (number)
1817
+ {
1818
+ id: "e1",
1819
+ source: { nodeId: "nA", handle: "Result" },
1820
+ target: { nodeId: "nC", handle: "A" },
1821
+ },
1822
+ // Invalid input name (INPUT_MISSING)
1823
+ {
1824
+ id: "e2",
1825
+ source: { nodeId: "nB", handle: "Result" },
1826
+ target: { nodeId: "nC", handle: "NonExistent" },
1827
+ },
1828
+ // Multi inbound to same input (warning): another edge to A
1829
+ {
1830
+ id: "e3",
1831
+ source: { nodeId: "nB", handle: "Result" },
1832
+ target: { nodeId: "nC", handle: "A" },
1833
+ },
1834
+ // Type mismatch to highlight coercion/validation (string -> float[] should error)
1835
+ {
1836
+ id: "e4",
1837
+ source: { nodeId: "s1", handle: "Text" },
1838
+ target: { nodeId: "cmp", handle: "A" },
1839
+ },
1840
+ ],
1841
+ };
1842
+ return def;
1843
+ }
1844
+ function createValidationGraphRegistry() {
1845
+ const registry = setupBasicGraphRegistry();
1846
+ return registry;
1847
+ }
1848
+
1849
+ export { BatchedEngine, CompositeCategory, ComputeCategory, GraphBuilder, GraphRuntime, HybridEngine, LocalRunner, PullEngine, PushEngine, Registry, StepEngine, createAsyncGraphDef, createAsyncGraphRegistry, createProgressGraphDef, createProgressGraphRegistry, createSimpleGraphDef, createSimpleGraphRegistry, createValidationGraphDef, createValidationGraphRegistry, registerDelayNode, registerProgressNodes };
1850
+ //# sourceMappingURL=index.js.map