@bian-womp/spark-graph 0.1.8 → 0.1.10

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 CHANGED
@@ -24,9 +24,12 @@ class Registry {
24
24
  this.serializers = new Map();
25
25
  this.coercions = new Map();
26
26
  this.asyncCoercions = new Map();
27
+ this.resolvedCache = new Map();
27
28
  }
28
29
  registerType(desc, opts) {
29
30
  this.types.set(desc.id, desc);
31
+ // Any structural change invalidates resolution cache
32
+ this.resolvedCache.clear();
30
33
  if (!this.serializers.has(desc.id)) {
31
34
  this.registerSerializer(desc.id, {
32
35
  serialize: (v) => v,
@@ -83,46 +86,231 @@ class Registry {
83
86
  return this;
84
87
  }
85
88
  // Register a type coercion from one type id to another
86
- registerCoercion(fromTypeId, toTypeId, convert) {
87
- this.coercions.set(`${fromTypeId}->${toTypeId}`, convert);
89
+ registerCoercion(fromTypeId, toTypeId, convert, opts) {
90
+ this.coercions.set(`${fromTypeId}->${toTypeId}`, {
91
+ convert,
92
+ nonTransitive: !!opts?.nonTransitive,
93
+ });
94
+ // If both source and target have array variants, add derived array->array coercion
95
+ const fromArr = `${fromTypeId}[]`;
96
+ const toArr = `${toTypeId}[]`;
97
+ const arrKey = `${fromArr}->${toArr}`;
98
+ if (this.types.has(fromArr) && this.types.has(toArr)) {
99
+ if (!this.coercions.has(arrKey) && !this.asyncCoercions.has(arrKey)) {
100
+ this.coercions.set(arrKey, {
101
+ convert: (value) => {
102
+ if (Array.isArray(value))
103
+ return value.map((v) => convert(v));
104
+ // Best-effort: coerce single to array-of-single
105
+ return [convert(value)];
106
+ },
107
+ nonTransitive: !!opts?.nonTransitive,
108
+ });
109
+ }
110
+ }
111
+ this.resolvedCache.clear();
88
112
  return this;
89
113
  }
90
114
  // Register an async type coercion from one type id to another
91
- registerAsyncCoercion(fromTypeId, toTypeId, convertAsync) {
92
- this.asyncCoercions.set(`${fromTypeId}->${toTypeId}`, convertAsync);
115
+ registerAsyncCoercion(fromTypeId, toTypeId, convertAsync, opts) {
116
+ this.asyncCoercions.set(`${fromTypeId}->${toTypeId}`, {
117
+ convertAsync,
118
+ nonTransitive: !!opts?.nonTransitive,
119
+ });
120
+ // If both source and target have array variants, add derived array->array async coercion
121
+ const fromArr = `${fromTypeId}[]`;
122
+ const toArr = `${toTypeId}[]`;
123
+ const arrKey = `${fromArr}->${toArr}`;
124
+ if (this.types.has(fromArr) && this.types.has(toArr)) {
125
+ if (!this.coercions.has(arrKey) && !this.asyncCoercions.has(arrKey)) {
126
+ this.asyncCoercions.set(arrKey, {
127
+ convertAsync: async (value, signal) => {
128
+ if (Array.isArray(value)) {
129
+ const out = [];
130
+ for (let i = 0; i < value.length; i++) {
131
+ if (signal?.aborted)
132
+ throw signal.reason ?? new Error("aborted");
133
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
134
+ out.push(await convertAsync(value[i], signal));
135
+ }
136
+ return out;
137
+ }
138
+ return [await convertAsync(value, signal)];
139
+ },
140
+ nonTransitive: !!opts?.nonTransitive,
141
+ });
142
+ }
143
+ }
144
+ this.resolvedCache.clear();
93
145
  return this;
94
146
  }
95
147
  canCoerce(fromTypeId, toTypeId) {
96
148
  if (!fromTypeId || !toTypeId)
97
149
  return false;
98
- if (fromTypeId === toTypeId)
99
- return true;
100
- const key = `${fromTypeId}->${toTypeId}`;
101
- return this.coercions.has(key) || this.asyncCoercions.has(key);
150
+ return !!this.resolveCoercion(fromTypeId, toTypeId);
102
151
  }
103
152
  getCoercion(fromTypeId, toTypeId) {
104
- if (fromTypeId === toTypeId)
105
- return (v) => v;
106
- return this.coercions.get(`${fromTypeId}->${toTypeId}`);
153
+ const resolved = this.resolveCoercion(fromTypeId, toTypeId);
154
+ if (!resolved)
155
+ return undefined;
156
+ if (resolved.kind === "sync")
157
+ return resolved.convert;
158
+ return undefined;
107
159
  }
108
160
  getAsyncCoercion(fromTypeId, toTypeId) {
109
- if (fromTypeId === toTypeId)
161
+ const resolved = this.resolveCoercion(fromTypeId, toTypeId);
162
+ if (!resolved)
110
163
  return undefined;
111
- return this.asyncCoercions.get(`${fromTypeId}->${toTypeId}`);
164
+ if (resolved.kind === "async")
165
+ return resolved.convertAsync;
166
+ return undefined;
112
167
  }
113
168
  // Introspection for dynamic discovery
114
169
  listCoercions() {
115
170
  const out = [];
116
- for (const key of this.coercions.keys()) {
171
+ for (const [key, rec] of this.coercions.entries()) {
117
172
  const [from, to] = key.split("->");
118
- out.push({ from, to, async: false });
173
+ out.push({
174
+ from,
175
+ to,
176
+ async: false,
177
+ nonTransitive: rec.nonTransitive,
178
+ });
119
179
  }
120
- for (const key of this.asyncCoercions.keys()) {
180
+ for (const [key, rec] of this.asyncCoercions.entries()) {
121
181
  const [from, to] = key.split("->");
122
- out.push({ from, to, async: true });
182
+ out.push({ from, to, async: true, nonTransitive: rec.nonTransitive });
123
183
  }
124
184
  return out;
125
185
  }
186
+ resolveCoercion(fromTypeId, toTypeId) {
187
+ const cacheKey = `${fromTypeId}->${toTypeId}`;
188
+ const cached = this.resolvedCache.get(cacheKey);
189
+ if (cached)
190
+ return cached;
191
+ if (fromTypeId === toTypeId) {
192
+ const res = { kind: "sync", convert: (v) => v };
193
+ this.resolvedCache.set(cacheKey, res);
194
+ return res;
195
+ }
196
+ // Direct edges (regardless of nonTransitive)
197
+ const directSync = this.coercions.get(cacheKey);
198
+ if (directSync) {
199
+ const res = {
200
+ kind: "sync",
201
+ convert: directSync.convert,
202
+ };
203
+ this.resolvedCache.set(cacheKey, res);
204
+ return res;
205
+ }
206
+ const directAsync = this.asyncCoercions.get(cacheKey);
207
+ if (directAsync) {
208
+ const res = {
209
+ kind: "async",
210
+ convertAsync: directAsync.convertAsync,
211
+ };
212
+ this.resolvedCache.set(cacheKey, res);
213
+ return res;
214
+ }
215
+ // Build adjacency from transitive-allowed edges only
216
+ const getNeighbors = (from) => {
217
+ const out = [];
218
+ for (const [key, rec] of this.coercions.entries()) {
219
+ if (rec.nonTransitive)
220
+ continue;
221
+ const [src, dst] = key.split("->");
222
+ if (src === from)
223
+ out.push({ from: src, to: dst, kind: "sync", convert: rec.convert });
224
+ }
225
+ for (const [key, rec] of this.asyncCoercions.entries()) {
226
+ if (rec.nonTransitive)
227
+ continue;
228
+ const [src, dst] = key.split("->");
229
+ if (src === from)
230
+ out.push({
231
+ from: src,
232
+ to: dst,
233
+ kind: "async",
234
+ convertAsync: rec.convertAsync,
235
+ });
236
+ }
237
+ return out;
238
+ };
239
+ const betterOf = (a, b) => {
240
+ if (!a)
241
+ return true;
242
+ if (b.edges !== a.edges)
243
+ return b.edges < a.edges;
244
+ return b.async < a.async;
245
+ };
246
+ const best = new Map();
247
+ const queue = [];
248
+ const push = (e) => {
249
+ // simple insertion to keep queue roughly ordered by cost
250
+ let i = 0;
251
+ while (i < queue.length) {
252
+ const q = queue[i];
253
+ if (e.cost.edges < q.cost.edges ||
254
+ (e.cost.edges === q.cost.edges && e.cost.async < q.cost.async)) {
255
+ break;
256
+ }
257
+ i++;
258
+ }
259
+ queue.splice(i, 0, e);
260
+ };
261
+ push({ node: fromTypeId, cost: { edges: 0, async: 0 }, path: [] });
262
+ best.set(fromTypeId, { edges: 0, async: 0 });
263
+ while (queue.length > 0) {
264
+ const cur = queue.shift();
265
+ if (cur.node === toTypeId) {
266
+ // Compose
267
+ const hasAsync = cur.path.some((s) => s.kind === "async");
268
+ if (!hasAsync) {
269
+ const convert = (value) => {
270
+ let acc = value;
271
+ for (const step of cur.path) {
272
+ // all sync by construction
273
+ acc = step.convert(acc);
274
+ }
275
+ return acc;
276
+ };
277
+ const res = { kind: "sync", convert };
278
+ this.resolvedCache.set(cacheKey, res);
279
+ return res;
280
+ }
281
+ else {
282
+ const convertAsync = async (value, signal) => {
283
+ let acc = value;
284
+ for (const step of cur.path) {
285
+ if (step.kind === "sync") {
286
+ acc = step.convert(acc);
287
+ }
288
+ else {
289
+ acc = await step.convertAsync(acc, signal);
290
+ }
291
+ }
292
+ return acc;
293
+ };
294
+ const res = { kind: "async", convertAsync };
295
+ this.resolvedCache.set(cacheKey, res);
296
+ return res;
297
+ }
298
+ }
299
+ // expand neighbors
300
+ for (const step of getNeighbors(cur.node)) {
301
+ const nextCost = {
302
+ edges: cur.cost.edges + 1,
303
+ async: cur.cost.async + (step.kind === "async" ? 1 : 0),
304
+ };
305
+ const prev = best.get(step.to);
306
+ if (betterOf(prev, nextCost)) {
307
+ best.set(step.to, nextCost);
308
+ push({ node: step.to, cost: nextCost, path: [...cur.path, step] });
309
+ }
310
+ }
311
+ }
312
+ return undefined;
313
+ }
126
314
  // Enum support
127
315
  registerEnum(desc) {
128
316
  const { id, displayName, options, opts } = desc;
@@ -1331,79 +1519,89 @@ const CompositeCategory = (registry) => ({
1331
1519
  policy: { mode: "hybrid" },
1332
1520
  });
1333
1521
 
1522
+ // Helpers
1523
+ const asArray = (v) => Array.isArray(v) ? v : [Number(v)];
1524
+ const broadcast = (a, b) => {
1525
+ const aa = asArray(a);
1526
+ const bb = asArray(b);
1527
+ if (aa.length === bb.length)
1528
+ return [aa, bb];
1529
+ if (aa.length === 1)
1530
+ return [new Array(bb.length).fill(aa[0]), bb];
1531
+ if (bb.length === 1)
1532
+ return [aa, new Array(aa.length).fill(bb[0])];
1533
+ const len = Math.max(aa.length, bb.length);
1534
+ return [new Array(len).fill(aa[0] ?? 0), new Array(len).fill(bb[0] ?? 0)];
1535
+ };
1536
+ const clamp = (x, min, max) => Math.min(max, Math.max(min, x));
1537
+ const lerp = (a, b, t) => a + (b - a) * t;
1538
+ const lcg = (seed) => {
1539
+ let s = seed >>> 0 || 1;
1540
+ return () => (s = (s * 1664525 + 1013904223) >>> 0) / 0xffffffff;
1541
+ };
1334
1542
  function setupBasicGraphRegistry() {
1335
1543
  const registry = new Registry();
1336
1544
  registry.categories.register(ComputeCategory);
1337
- const floatType = {
1545
+ registry.registerType({
1338
1546
  id: "base.float",
1339
1547
  validate: (v) => typeof v === "number" && !Number.isNaN(v),
1340
- };
1341
- registry.registerType(floatType);
1342
- registry.registerSerializer("base.float", {
1343
- serialize: (v) => v,
1344
- deserialize: (d) => Number(d),
1345
- });
1346
- const boolType = {
1548
+ }, { withArray: true, arrayPickFirstDefined: true });
1549
+ registry.registerType({
1347
1550
  id: "base.bool",
1348
1551
  validate: (v) => typeof v === "boolean",
1349
- };
1350
- const stringType = {
1552
+ }, { withArray: true, arrayPickFirstDefined: true });
1553
+ registry.registerType({
1351
1554
  id: "base.string",
1352
1555
  validate: (v) => typeof v === "string",
1353
- };
1354
- const vec3Type = {
1556
+ }, { withArray: true, arrayPickFirstDefined: true });
1557
+ registry.registerType({
1355
1558
  id: "base.vec3",
1356
1559
  validate: (v) => Array.isArray(v) &&
1357
1560
  v.length === 3 &&
1358
1561
  v.every((x) => typeof x === "number"),
1359
- };
1360
- [boolType, stringType, vec3Type, floatType].forEach((t) => {
1361
- registry.registerType(t, { withArray: true, arrayPickFirstDefined: true });
1362
- registry.registerSerializer(t.id, {
1363
- serialize: (v) => v,
1364
- deserialize: (d) => d,
1365
- });
1562
+ }, { withArray: true, arrayPickFirstDefined: true });
1563
+ // float -> vec3 : map x to [x,0,0]
1564
+ registry.registerCoercion("base.float", "base.vec3", (v) => {
1565
+ return [Number(v) || 0, 0, 0];
1366
1566
  });
1367
- // Helpers
1368
- const asArray = (v) => Array.isArray(v) ? v : [Number(v)];
1369
- // float[] -> vec3[] : map x to [x,0,0]
1370
- registry.registerCoercion("base.float[]", "base.vec3[]", (v) => {
1371
- const arr = asArray(v);
1372
- return arr.map((x) => [Number(x) || 0, 0, 0]);
1567
+ // Async coercion variant for vec3 -> float (chunked + abortable)
1568
+ registry.registerAsyncCoercion("base.vec3", "base.float", async (value, signal) => {
1569
+ if (signal.aborted)
1570
+ throw new DOMException("Aborted", "AbortError");
1571
+ const v = value;
1572
+ await new Promise((r) => setTimeout(r, 1000));
1573
+ return Math.hypot(Number(v[0] ?? 0), Number(v[1] ?? 0), Number(v[2] ?? 0));
1373
1574
  });
1374
- // Async coercion variant for vec3[] -> float[] (chunked + abortable)
1375
- registry.registerAsyncCoercion("base.vec3[]", "base.float[]", async (value, signal) => {
1376
- const arr = Array.isArray(value)
1377
- ? value
1378
- : [];
1379
- const out = new Array(arr.length);
1380
- for (let i = 0; i < arr.length; i++) {
1381
- if (signal.aborted)
1382
- throw new DOMException("Aborted", "AbortError");
1383
- const v = arr[i] ?? [0, 0, 0];
1384
- await new Promise((r) => setTimeout(r, 1000));
1385
- out[i] = Math.hypot(Number(v[0] ?? 0), Number(v[1] ?? 0), Number(v[2] ?? 0));
1386
- }
1387
- return out;
1575
+ registry.registerCoercion("base.bool", "base.float", (v) => (v ? 1 : 0));
1576
+ registry.registerCoercion("base.float", "base.bool", (v) => !!v);
1577
+ registry.registerCoercion("base.float", "base.string", (v) => String(v));
1578
+ registry.registerCoercion("base.string", "base.float", (v) => Number(v));
1579
+ // Enums: Math Operation
1580
+ registry.registerEnum({
1581
+ id: "base.enum:math.operation",
1582
+ options: [
1583
+ { value: 0, label: "Add" },
1584
+ { value: 1, label: "Subtract" },
1585
+ { value: 2, label: "Multiply" },
1586
+ { value: 3, label: "Divide" },
1587
+ { value: 4, label: "Min" },
1588
+ { value: 5, label: "Max" },
1589
+ { value: 6, label: "Modulo" },
1590
+ { value: 7, label: "Power" },
1591
+ ],
1592
+ });
1593
+ // Enums: Compare Operation
1594
+ registry.registerEnum({
1595
+ id: "base.enum:compare.operation",
1596
+ options: [
1597
+ { value: 0, label: "LessThan" },
1598
+ { value: 1, label: "LessThanOrEqual" },
1599
+ { value: 2, label: "GreaterThan" },
1600
+ { value: 3, label: "GreaterThanOrEqual" },
1601
+ { value: 4, label: "Equal" },
1602
+ { value: 5, label: "NotEqual" },
1603
+ ],
1388
1604
  });
1389
- const broadcast = (a, b) => {
1390
- const aa = asArray(a);
1391
- const bb = asArray(b);
1392
- if (aa.length === bb.length)
1393
- return [aa, bb];
1394
- if (aa.length === 1)
1395
- return [new Array(bb.length).fill(aa[0]), bb];
1396
- if (bb.length === 1)
1397
- return [aa, new Array(aa.length).fill(bb[0])];
1398
- const len = Math.max(aa.length, bb.length);
1399
- return [new Array(len).fill(aa[0] ?? 0), new Array(len).fill(bb[0] ?? 0)];
1400
- };
1401
- const clamp = (x, min, max) => Math.min(max, Math.max(min, x));
1402
- const lerp = (a, b, t) => a + (b - a) * t;
1403
- const lcg = (seed) => {
1404
- let s = seed >>> 0 || 1;
1405
- return () => (s = (s * 1664525 + 1013904223) >>> 0) / 0xffffffff;
1406
- };
1407
1605
  // Number
1408
1606
  registry.registerNode({
1409
1607
  id: "base.number",
@@ -1430,32 +1628,6 @@ function setupBasicGraphRegistry() {
1430
1628
  outputs: { Text: "base.string" },
1431
1629
  impl: (ins) => ({ Text: String(ins.Value) }),
1432
1630
  });
1433
- // Enums: Math Operation
1434
- registry.registerEnum({
1435
- id: "base.enum:math.operation",
1436
- options: [
1437
- { value: 0, label: "Add" },
1438
- { value: 1, label: "Subtract" },
1439
- { value: 2, label: "Multiply" },
1440
- { value: 3, label: "Divide" },
1441
- { value: 4, label: "Min" },
1442
- { value: 5, label: "Max" },
1443
- { value: 6, label: "Modulo" },
1444
- { value: 7, label: "Power" },
1445
- ],
1446
- });
1447
- // Enums: Compare Operation
1448
- registry.registerEnum({
1449
- id: "base.enum:compare.operation",
1450
- options: [
1451
- { value: 0, label: "LessThan" },
1452
- { value: 1, label: "LessThanOrEqual" },
1453
- { value: 2, label: "GreaterThan" },
1454
- { value: 3, label: "GreaterThanOrEqual" },
1455
- { value: 4, label: "Equal" },
1456
- { value: 5, label: "NotEqual" },
1457
- ],
1458
- });
1459
1631
  // Clamp
1460
1632
  registry.registerNode({
1461
1633
  id: "base.clamp",
@@ -1667,21 +1839,6 @@ function setupBasicGraphRegistry() {
1667
1839
  });
1668
1840
  return registry;
1669
1841
  }
1670
- function makeBasicGraphDefinition() {
1671
- return {
1672
- nodes: [
1673
- { nodeId: "n1", typeId: "base.math" },
1674
- { nodeId: "n2", typeId: "base.math" },
1675
- ],
1676
- edges: [
1677
- {
1678
- id: "e1",
1679
- source: { nodeId: "n1", handle: "Result" },
1680
- target: { nodeId: "n2", handle: "A" },
1681
- },
1682
- ],
1683
- };
1684
- }
1685
1842
  function registerDelayNode(registry) {
1686
1843
  registry.registerNode({
1687
1844
  id: "async.delay",
@@ -1755,6 +1912,37 @@ function registerProgressNodes(registry) {
1755
1912
  });
1756
1913
  }
1757
1914
 
1915
+ function makeBasicGraphDefinition() {
1916
+ return {
1917
+ nodes: [
1918
+ { nodeId: "n1", typeId: "base.math" },
1919
+ { nodeId: "n2", typeId: "base.math" },
1920
+ // Transitivity demo nodes
1921
+ { nodeId: "n3", typeId: "base.compare" },
1922
+ { nodeId: "n4", typeId: "base.randomXYZs" },
1923
+ ],
1924
+ edges: [
1925
+ {
1926
+ id: "e1",
1927
+ source: { nodeId: "n1", handle: "Result" },
1928
+ target: { nodeId: "n2", handle: "A" },
1929
+ },
1930
+ // Feed n2 result to comparer A
1931
+ {
1932
+ id: "e2",
1933
+ source: { nodeId: "n2", handle: "Result" },
1934
+ target: { nodeId: "n3", handle: "A" },
1935
+ },
1936
+ // Transitive coercion edge: bool[] (n3.Result) -> vec3 (n4.Min)
1937
+ // Path: bool[] -> float[] -> vec3[] -> vec3
1938
+ {
1939
+ id: "e3",
1940
+ source: { nodeId: "n3", handle: "Result" },
1941
+ target: { nodeId: "n4", handle: "Min" },
1942
+ },
1943
+ ],
1944
+ };
1945
+ }
1758
1946
  function createSimpleGraphDef() {
1759
1947
  return makeBasicGraphDefinition();
1760
1948
  }