@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.
- package/lib/cjs/index.cjs +1872 -0
- package/lib/cjs/index.cjs.map +1 -0
- package/lib/cjs/src/builder/GraphBuilder.d.ts +42 -0
- package/lib/cjs/src/builder/GraphBuilder.d.ts.map +1 -0
- package/lib/cjs/src/builder/Registry.d.ts +42 -0
- package/lib/cjs/src/builder/Registry.d.ts.map +1 -0
- package/lib/cjs/src/core/categories.d.ts +32 -0
- package/lib/cjs/src/core/categories.d.ts.map +1 -0
- package/lib/cjs/src/core/types.d.ts +80 -0
- package/lib/cjs/src/core/types.d.ts.map +1 -0
- package/lib/cjs/src/examples/async.d.ts +5 -0
- package/lib/cjs/src/examples/async.d.ts.map +1 -0
- package/lib/cjs/src/examples/engine.d.ts +6 -0
- package/lib/cjs/src/examples/engine.d.ts.map +1 -0
- package/lib/cjs/src/examples/progress.d.ts +5 -0
- package/lib/cjs/src/examples/progress.d.ts.map +1 -0
- package/lib/cjs/src/examples/run.d.ts +2 -0
- package/lib/cjs/src/examples/run.d.ts.map +1 -0
- package/lib/cjs/src/examples/shared.d.ts +8 -0
- package/lib/cjs/src/examples/shared.d.ts.map +1 -0
- package/lib/cjs/src/examples/simple.d.ts +4 -0
- package/lib/cjs/src/examples/simple.d.ts.map +1 -0
- package/lib/cjs/src/examples/validation.d.ts +5 -0
- package/lib/cjs/src/examples/validation.d.ts.map +1 -0
- package/lib/cjs/src/index.d.ts +23 -0
- package/lib/cjs/src/index.d.ts.map +1 -0
- package/lib/cjs/src/plugins/composite.d.ts +22 -0
- package/lib/cjs/src/plugins/composite.d.ts.map +1 -0
- package/lib/cjs/src/plugins/compute.d.ts +5 -0
- package/lib/cjs/src/plugins/compute.d.ts.map +1 -0
- package/lib/cjs/src/runtime/AbstractEngine.d.ts +14 -0
- package/lib/cjs/src/runtime/AbstractEngine.d.ts.map +1 -0
- package/lib/cjs/src/runtime/BatchedEngine.d.ts +17 -0
- package/lib/cjs/src/runtime/BatchedEngine.d.ts.map +1 -0
- package/lib/cjs/src/runtime/Engine.d.ts +14 -0
- package/lib/cjs/src/runtime/Engine.d.ts.map +1 -0
- package/lib/cjs/src/runtime/GraphRuntime.d.ts +127 -0
- package/lib/cjs/src/runtime/GraphRuntime.d.ts.map +1 -0
- package/lib/cjs/src/runtime/HybridEngine.d.ts +21 -0
- package/lib/cjs/src/runtime/HybridEngine.d.ts.map +1 -0
- package/lib/cjs/src/runtime/LocalRunner.d.ts +16 -0
- package/lib/cjs/src/runtime/LocalRunner.d.ts.map +1 -0
- package/lib/cjs/src/runtime/PullEngine.d.ts +8 -0
- package/lib/cjs/src/runtime/PullEngine.d.ts.map +1 -0
- package/lib/cjs/src/runtime/PushEngine.d.ts +6 -0
- package/lib/cjs/src/runtime/PushEngine.d.ts.map +1 -0
- package/lib/cjs/src/runtime/RunnerControl.d.ts +10 -0
- package/lib/cjs/src/runtime/RunnerControl.d.ts.map +1 -0
- package/lib/cjs/src/runtime/StepEngine.d.ts +11 -0
- package/lib/cjs/src/runtime/StepEngine.d.ts.map +1 -0
- package/lib/esm/index.js +1850 -0
- package/lib/esm/index.js.map +1 -0
- package/lib/esm/src/builder/GraphBuilder.d.ts +42 -0
- package/lib/esm/src/builder/GraphBuilder.d.ts.map +1 -0
- package/lib/esm/src/builder/Registry.d.ts +42 -0
- package/lib/esm/src/builder/Registry.d.ts.map +1 -0
- package/lib/esm/src/core/categories.d.ts +32 -0
- package/lib/esm/src/core/categories.d.ts.map +1 -0
- package/lib/esm/src/core/types.d.ts +80 -0
- package/lib/esm/src/core/types.d.ts.map +1 -0
- package/lib/esm/src/examples/async.d.ts +5 -0
- package/lib/esm/src/examples/async.d.ts.map +1 -0
- package/lib/esm/src/examples/engine.d.ts +6 -0
- package/lib/esm/src/examples/engine.d.ts.map +1 -0
- package/lib/esm/src/examples/progress.d.ts +5 -0
- package/lib/esm/src/examples/progress.d.ts.map +1 -0
- package/lib/esm/src/examples/run.d.ts +2 -0
- package/lib/esm/src/examples/run.d.ts.map +1 -0
- package/lib/esm/src/examples/shared.d.ts +8 -0
- package/lib/esm/src/examples/shared.d.ts.map +1 -0
- package/lib/esm/src/examples/simple.d.ts +4 -0
- package/lib/esm/src/examples/simple.d.ts.map +1 -0
- package/lib/esm/src/examples/validation.d.ts +5 -0
- package/lib/esm/src/examples/validation.d.ts.map +1 -0
- package/lib/esm/src/index.d.ts +23 -0
- package/lib/esm/src/index.d.ts.map +1 -0
- package/lib/esm/src/plugins/composite.d.ts +22 -0
- package/lib/esm/src/plugins/composite.d.ts.map +1 -0
- package/lib/esm/src/plugins/compute.d.ts +5 -0
- package/lib/esm/src/plugins/compute.d.ts.map +1 -0
- package/lib/esm/src/runtime/AbstractEngine.d.ts +14 -0
- package/lib/esm/src/runtime/AbstractEngine.d.ts.map +1 -0
- package/lib/esm/src/runtime/BatchedEngine.d.ts +17 -0
- package/lib/esm/src/runtime/BatchedEngine.d.ts.map +1 -0
- package/lib/esm/src/runtime/Engine.d.ts +14 -0
- package/lib/esm/src/runtime/Engine.d.ts.map +1 -0
- package/lib/esm/src/runtime/GraphRuntime.d.ts +127 -0
- package/lib/esm/src/runtime/GraphRuntime.d.ts.map +1 -0
- package/lib/esm/src/runtime/HybridEngine.d.ts +21 -0
- package/lib/esm/src/runtime/HybridEngine.d.ts.map +1 -0
- package/lib/esm/src/runtime/LocalRunner.d.ts +16 -0
- package/lib/esm/src/runtime/LocalRunner.d.ts.map +1 -0
- package/lib/esm/src/runtime/PullEngine.d.ts +8 -0
- package/lib/esm/src/runtime/PullEngine.d.ts.map +1 -0
- package/lib/esm/src/runtime/PushEngine.d.ts +6 -0
- package/lib/esm/src/runtime/PushEngine.d.ts.map +1 -0
- package/lib/esm/src/runtime/RunnerControl.d.ts +10 -0
- package/lib/esm/src/runtime/RunnerControl.d.ts.map +1 -0
- package/lib/esm/src/runtime/StepEngine.d.ts +11 -0
- package/lib/esm/src/runtime/StepEngine.d.ts.map +1 -0
- 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
|