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