@bian-womp/spark-workbench 0.1.29 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/lib/cjs/index.cjs +322 -225
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/index.d.ts +3 -1
  4. package/lib/cjs/src/index.d.ts.map +1 -1
  5. package/lib/cjs/src/misc/Inspector.d.ts +3 -1
  6. package/lib/cjs/src/misc/Inspector.d.ts.map +1 -1
  7. package/lib/cjs/src/misc/WorkbenchStudio.d.ts +8 -5
  8. package/lib/cjs/src/misc/WorkbenchStudio.d.ts.map +1 -1
  9. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +2 -2
  10. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  11. package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts +2 -2
  12. package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  13. package/lib/cjs/src/misc/hooks.d.ts +2 -2
  14. package/lib/cjs/src/misc/hooks.d.ts.map +1 -1
  15. package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts +29 -0
  16. package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts.map +1 -0
  17. package/lib/cjs/src/runtime/{GraphRunner.d.ts → IGraphRunner.d.ts} +21 -31
  18. package/lib/cjs/src/runtime/IGraphRunner.d.ts.map +1 -0
  19. package/lib/cjs/src/runtime/LocalGraphRunner.d.ts +16 -0
  20. package/lib/cjs/src/runtime/LocalGraphRunner.d.ts.map +1 -0
  21. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts +27 -0
  22. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts.map +1 -0
  23. package/lib/esm/index.js +322 -226
  24. package/lib/esm/index.js.map +1 -1
  25. package/lib/esm/src/index.d.ts +3 -1
  26. package/lib/esm/src/index.d.ts.map +1 -1
  27. package/lib/esm/src/misc/Inspector.d.ts +3 -1
  28. package/lib/esm/src/misc/Inspector.d.ts.map +1 -1
  29. package/lib/esm/src/misc/WorkbenchStudio.d.ts +8 -5
  30. package/lib/esm/src/misc/WorkbenchStudio.d.ts.map +1 -1
  31. package/lib/esm/src/misc/context/WorkbenchContext.d.ts +2 -2
  32. package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  33. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts +2 -2
  34. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  35. package/lib/esm/src/misc/hooks.d.ts +2 -2
  36. package/lib/esm/src/misc/hooks.d.ts.map +1 -1
  37. package/lib/esm/src/runtime/AbstractGraphRunner.d.ts +29 -0
  38. package/lib/esm/src/runtime/AbstractGraphRunner.d.ts.map +1 -0
  39. package/lib/esm/src/runtime/{GraphRunner.d.ts → IGraphRunner.d.ts} +21 -31
  40. package/lib/esm/src/runtime/IGraphRunner.d.ts.map +1 -0
  41. package/lib/esm/src/runtime/LocalGraphRunner.d.ts +16 -0
  42. package/lib/esm/src/runtime/LocalGraphRunner.d.ts.map +1 -0
  43. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts +27 -0
  44. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts.map +1 -0
  45. package/package.json +4 -4
  46. package/lib/cjs/src/runtime/GraphRunner.d.ts.map +0 -1
  47. package/lib/esm/src/runtime/GraphRunner.d.ts.map +0 -1
package/lib/cjs/index.cjs CHANGED
@@ -319,104 +319,295 @@ class CLIWorkbench {
319
319
  }
320
320
  }
321
321
 
322
- class GraphRunner {
322
+ class AbstractGraphRunner {
323
323
  constructor(registry, backend) {
324
324
  this.registry = registry;
325
+ this.backend = backend;
325
326
  this.listeners = new Map();
326
327
  this.stagedInputs = {};
327
- this.backend = { kind: "local" };
328
- if (backend)
329
- this.backend = backend;
330
- // Emit initial transport status
331
- if (this.backend.kind === "local")
332
- this.emit("transport", { state: "local" });
333
328
  }
334
- build(def) {
335
- if (this.backend.kind === "local") {
336
- const builder = new sparkGraph.GraphBuilder(this.registry);
337
- this.runtime = builder.build(def);
338
- // Signal UI that freshly built graph should be considered invalidated
339
- this.emit("invalidate", { reason: "graph-built" });
329
+ launch(def, opts) {
330
+ if (this.engine) {
331
+ throw new Error("Engine already running. Stop the current engine first.");
332
+ }
333
+ }
334
+ setInput(nodeId, handle, value) {
335
+ if (!this.stagedInputs[nodeId])
336
+ this.stagedInputs[nodeId] = {};
337
+ this.stagedInputs[nodeId][handle] = value;
338
+ if (this.engine) {
339
+ this.engine.setInput(nodeId, handle, value);
340
+ }
341
+ else {
342
+ // Emit a value event so UI updates even when engine isn't running
343
+ this.emit("value", { nodeId, handle, value, io: "input" });
344
+ }
345
+ }
346
+ // Batch update multiple inputs on a node and trigger a single run
347
+ setInputs(nodeId, inputs) {
348
+ if (!inputs)
340
349
  return;
350
+ if (!this.stagedInputs[nodeId])
351
+ this.stagedInputs[nodeId] = {};
352
+ Object.assign(this.stagedInputs[nodeId], inputs);
353
+ if (this.engine) {
354
+ // Running: set all inputs
355
+ this.engine.setInputs(nodeId, inputs);
356
+ }
357
+ else {
358
+ // Not running: emit a single synthetic value event per handle; UI will coalesce
359
+ console.warn("Engine does not exists");
360
+ for (const [handle, value] of Object.entries(inputs)) {
361
+ this.emit("value", { nodeId, handle, value, io: "input" });
362
+ }
341
363
  }
342
- // Remote: no-op here; build is performed on remote server during launch
364
+ }
365
+ async whenIdle() {
366
+ await this.engine?.whenIdle();
367
+ }
368
+ on(event, handler) {
369
+ if (!this.listeners.has(event))
370
+ this.listeners.set(event, new Set());
371
+ const set = this.listeners.get(event);
372
+ set.add(handler);
373
+ return () => set.delete(handler);
374
+ }
375
+ emit(event, payload) {
376
+ const set = this.listeners.get(event);
377
+ if (set)
378
+ for (const h of Array.from(set))
379
+ h(payload);
380
+ }
381
+ dispose() {
382
+ this.engine?.dispose();
383
+ this.engine = undefined;
384
+ this.runtime?.dispose();
385
+ this.runtime = undefined;
386
+ if (this.runningKind) {
387
+ this.runningKind = undefined;
388
+ this.emit("status", { running: false, engine: undefined });
389
+ }
390
+ }
391
+ isRunning() {
392
+ return !!this.engine;
393
+ }
394
+ getRunningEngine() {
395
+ return this.runningKind;
396
+ }
397
+ }
398
+
399
+ class LocalGraphRunner extends AbstractGraphRunner {
400
+ constructor(registry) {
401
+ super(registry, { kind: "local" });
402
+ this.emit("transport", { state: "local" });
403
+ }
404
+ build(def) {
405
+ const builder = new sparkGraph.GraphBuilder(this.registry);
406
+ this.runtime = builder.build(def);
407
+ // Signal UI that freshly built graph should be considered invalidated
408
+ this.emit("invalidate", { reason: "graph-built" });
343
409
  }
344
410
  update(def) {
345
- if (this.backend.kind === "local") {
346
- if (!this.runtime)
347
- return;
348
- // Prevent mid-run churn while wiring changes are applied
349
- this.runtime.pause();
350
- this.runtime.update(def, this.registry);
351
- this.runtime.resume();
352
- this.emit("invalidate", { reason: "graph-updated" });
411
+ if (!this.runtime)
353
412
  return;
413
+ // Prevent mid-run churn while wiring changes are applied
414
+ this.runtime.pause();
415
+ this.runtime.update(def, this.registry);
416
+ this.runtime.resume();
417
+ this.emit("invalidate", { reason: "graph-updated" });
418
+ }
419
+ launch(def, opts) {
420
+ super.launch(def, opts);
421
+ this.build(def);
422
+ if (!this.runtime)
423
+ throw new Error("Runtime not built");
424
+ const rt = this.runtime;
425
+ switch (opts.engine) {
426
+ case "push":
427
+ this.engine = new sparkGraph.PushEngine(rt);
428
+ break;
429
+ case "batched":
430
+ this.engine = new sparkGraph.BatchedEngine(rt, {
431
+ flushIntervalMs: opts.batched?.flushIntervalMs ?? 0,
432
+ });
433
+ break;
434
+ case "pull":
435
+ this.engine = new sparkGraph.PullEngine(rt);
436
+ break;
437
+ case "hybrid":
438
+ this.engine = new sparkGraph.HybridEngine(rt, {
439
+ windowMs: opts.hybrid?.windowMs ?? 250,
440
+ batchThreshold: opts.hybrid?.batchThreshold ?? 3,
441
+ });
442
+ break;
443
+ case "step":
444
+ this.engine = new sparkGraph.StepEngine(rt);
445
+ break;
446
+ default:
447
+ throw new Error("Unknown engine kind");
448
+ }
449
+ this.engine.on("value", (e) => this.emit("value", e));
450
+ this.engine.on("error", (e) => this.emit("error", e));
451
+ this.engine.on("invalidate", (e) => this.emit("invalidate", e));
452
+ this.engine.on("stats", (e) => this.emit("stats", e));
453
+ this.engine.launch();
454
+ this.runningKind = opts.engine;
455
+ this.emit("status", { running: true, engine: this.runningKind });
456
+ for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
457
+ this.engine.setInputs(nodeId, map);
458
+ }
459
+ }
460
+ async step() {
461
+ const eng = this.engine;
462
+ if (eng instanceof sparkGraph.StepEngine)
463
+ await eng.step();
464
+ }
465
+ async computeNode(nodeId) {
466
+ const eng = this.engine;
467
+ if (eng instanceof sparkGraph.PullEngine)
468
+ await eng.computeNode(nodeId);
469
+ }
470
+ flush() {
471
+ const eng = this.engine;
472
+ if (eng instanceof sparkGraph.BatchedEngine)
473
+ eng.flush();
474
+ }
475
+ getOutputs(def) {
476
+ const out = {};
477
+ if (!this.runtime)
478
+ return out;
479
+ for (const n of def.nodes) {
480
+ const desc = this.registry.nodes.get(n.typeId);
481
+ const handles = Object.keys(desc?.outputs ?? {});
482
+ for (const h of handles) {
483
+ const v = this.runtime.getOutput(n.nodeId, h);
484
+ if (v !== undefined) {
485
+ if (!out[n.nodeId])
486
+ out[n.nodeId] = {};
487
+ out[n.nodeId][h] = v;
488
+ }
489
+ }
354
490
  }
491
+ return out;
492
+ }
493
+ getInputs(def) {
494
+ const out = {};
495
+ for (const n of def.nodes) {
496
+ const staged = this.stagedInputs[n.nodeId] ?? {};
497
+ const runtimeInputs = this.runtime
498
+ ? this.runtime.getNodeData?.(n.nodeId)?.inputs ?? {}
499
+ : {};
500
+ if (this.isRunning()) {
501
+ out[n.nodeId] = runtimeInputs;
502
+ }
503
+ else {
504
+ const merged = { ...runtimeInputs, ...staged };
505
+ if (Object.keys(merged).length > 0)
506
+ out[n.nodeId] = merged;
507
+ }
508
+ }
509
+ return out;
510
+ }
511
+ dispose() {
512
+ super.dispose();
513
+ this.runtime = undefined;
514
+ this.emit("transport", { state: "local" });
515
+ }
516
+ }
517
+
518
+ class RemoteGraphRunner extends AbstractGraphRunner {
519
+ constructor(registry, backend) {
520
+ super(registry, backend);
521
+ this.valueCache = new Map();
522
+ this.listenersBound = false;
523
+ // Auto-handle registry-changed invalidations from remote
524
+ // We listen on invalidate and if reason matches, we rehydrate registry and emit a registry event
525
+ this.ensureRemoteRunner().then(async (runner) => {
526
+ const eng = runner.getEngine();
527
+ if (!this.listenersBound) {
528
+ eng.on("invalidate", async (e) => {
529
+ if (e.reason === "registry-changed") {
530
+ try {
531
+ const deltas = Array.isArray(e.deltas) ? e.deltas : [];
532
+ for (const d of deltas) {
533
+ if (!d || typeof d !== "object")
534
+ continue;
535
+ if (d.kind === "register-enum") {
536
+ this.registry.registerEnum({
537
+ id: d.id,
538
+ displayName: d.displayName,
539
+ options: d.options,
540
+ opts: d.opts,
541
+ });
542
+ }
543
+ else if (d.kind === "register-type") {
544
+ if (!this.registry.types.has(d.id)) {
545
+ this.registry.registerType({
546
+ id: d.id,
547
+ displayName: d.displayName,
548
+ validate: (_v) => true,
549
+ });
550
+ }
551
+ }
552
+ else if (d.kind === "register-node") {
553
+ if (!this.registry.nodes.has(d.desc?.id)) {
554
+ this.registry.registerNode({
555
+ id: String(d.desc?.id || ""),
556
+ categoryId: String(d.desc?.categoryId || "compute"),
557
+ displayName: d.desc?.displayName,
558
+ inputs: d.desc?.inputs || {},
559
+ outputs: d.desc?.outputs || {},
560
+ impl: () => { },
561
+ });
562
+ }
563
+ }
564
+ }
565
+ this.emit("registry", this.registry);
566
+ // Trigger update so validation/UI refreshes using last known graph
567
+ try {
568
+ if (this.lastDef)
569
+ this.update(this.lastDef);
570
+ }
571
+ catch {
572
+ console.error("Failed to update graph definition after registry changed");
573
+ }
574
+ }
575
+ catch {
576
+ console.error("Failed to handle registry changed event");
577
+ }
578
+ }
579
+ });
580
+ }
581
+ });
582
+ }
583
+ build(def) {
584
+ console.warn("Unsupported operation for remote runner");
585
+ }
586
+ update(def) {
355
587
  // Remote: forward update; ignore errors (fire-and-forget)
356
- void this.ensureRemote().then(async (rc) => {
588
+ this.ensureRemoteRunner().then(async (runner) => {
357
589
  try {
358
- await rc.runner.update(def);
590
+ await runner.update(def);
359
591
  this.emit("invalidate", { reason: "graph-updated" });
592
+ this.lastDef = def;
360
593
  }
361
594
  catch { }
362
595
  });
363
596
  }
364
597
  launch(def, opts) {
365
- if (this.engine) {
366
- throw new Error("Engine already running. Stop the current engine first.");
367
- }
368
- if (this.backend.kind === "local") {
369
- this.build(def);
370
- if (!this.runtime)
371
- throw new Error("Runtime not built");
372
- const rt = this.runtime;
373
- switch (opts.engine) {
374
- case "push":
375
- this.engine = new sparkGraph.PushEngine(rt);
376
- break;
377
- case "batched":
378
- this.engine = new sparkGraph.BatchedEngine(rt, {
379
- flushIntervalMs: opts.batched?.flushIntervalMs ?? 0,
380
- });
381
- break;
382
- case "pull":
383
- this.engine = new sparkGraph.PullEngine(rt);
384
- break;
385
- case "hybrid":
386
- this.engine = new sparkGraph.HybridEngine(rt, {
387
- windowMs: opts.hybrid?.windowMs ?? 250,
388
- batchThreshold: opts.hybrid?.batchThreshold ?? 3,
389
- });
390
- break;
391
- case "step":
392
- this.engine = new sparkGraph.StepEngine(rt);
393
- break;
394
- default:
395
- throw new Error("Unknown engine kind");
396
- }
397
- this.engine.on("value", (e) => this.emit("value", e));
398
- this.engine.on("error", (e) => this.emit("error", e));
399
- this.engine.on("invalidate", (e) => this.emit("invalidate", e));
400
- this.engine.on("stats", (e) => this.emit("stats", e));
401
- this.engine.launch();
402
- this.runningKind = opts.engine;
403
- this.emit("status", { running: true, engine: this.runningKind });
404
- for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
405
- this.engine.setInputs(nodeId, map);
406
- }
407
- return;
408
- }
598
+ super.launch(def, opts);
409
599
  // Remote: build remotely then launch
410
- void this.ensureRemote().then(async (rc) => {
411
- await rc.runner.build(def);
600
+ this.ensureRemoteRunner().then(async (runner) => {
601
+ await runner.build(def);
412
602
  // Signal UI after remote build as well
413
603
  this.emit("invalidate", { reason: "graph-built" });
604
+ this.lastDef = def;
414
605
  // Hydrate current remote inputs/outputs (including defaults) into cache
415
606
  try {
416
- const snap = await rc.runner.snapshot();
607
+ const snap = await runner.snapshot();
417
608
  for (const [nodeId, map] of Object.entries(snap.inputs || {})) {
418
609
  for (const [handle, value] of Object.entries(map || {})) {
419
- rc.valueCache.set(`${nodeId}.${handle}`, {
610
+ this.valueCache.set(`${nodeId}.${handle}`, {
420
611
  io: "input",
421
612
  value,
422
613
  });
@@ -425,7 +616,7 @@ class GraphRunner {
425
616
  }
426
617
  for (const [nodeId, map] of Object.entries(snap.outputs || {})) {
427
618
  for (const [handle, value] of Object.entries(map || {})) {
428
- rc.valueCache.set(`${nodeId}.${handle}`, {
619
+ this.valueCache.set(`${nodeId}.${handle}`, {
429
620
  io: "output",
430
621
  value,
431
622
  });
@@ -436,10 +627,10 @@ class GraphRunner {
436
627
  catch {
437
628
  console.error("Failed to hydrate remote inputs/outputs");
438
629
  }
439
- const eng = rc.runner.getEngine();
440
- if (!rc.listenersBound) {
630
+ const eng = runner.getEngine();
631
+ if (!this.listenersBound) {
441
632
  eng.on("value", (e) => {
442
- rc.valueCache.set(`${e.nodeId}.${e.handle}`, {
633
+ this.valueCache.set(`${e.nodeId}.${e.handle}`, {
443
634
  io: e.io,
444
635
  value: e.value,
445
636
  runtimeTypeId: e.runtimeTypeId,
@@ -449,7 +640,7 @@ class GraphRunner {
449
640
  eng.on("error", (e) => this.emit("error", e));
450
641
  eng.on("invalidate", (e) => this.emit("invalidate", e));
451
642
  eng.on("stats", (e) => this.emit("stats", e));
452
- rc.listenersBound = true;
643
+ this.listenersBound = true;
453
644
  }
454
645
  this.engine = eng;
455
646
  this.engine.launch();
@@ -460,85 +651,18 @@ class GraphRunner {
460
651
  }
461
652
  });
462
653
  }
463
- setInput(nodeId, handle, value) {
464
- if (!this.stagedInputs[nodeId])
465
- this.stagedInputs[nodeId] = {};
466
- this.stagedInputs[nodeId][handle] = value;
467
- if (this.engine) {
468
- this.engine.setInput(nodeId, handle, value);
469
- }
470
- else {
471
- // Emit a value event so UI updates even when engine isn't running
472
- this.emit("value", { nodeId, handle, value, io: "input" });
473
- }
474
- }
475
- // Batch update multiple inputs on a node and trigger a single run
476
- setInputs(nodeId, inputs) {
477
- if (!inputs)
478
- return;
479
- if (!this.stagedInputs[nodeId])
480
- this.stagedInputs[nodeId] = {};
481
- Object.assign(this.stagedInputs[nodeId], inputs);
482
- // Local running: pause, set all inputs, resume, schedule a single recompute
483
- if (this.backend.kind === "local" && this.engine && this.runtime) {
484
- this.engine.setInputs(nodeId, inputs);
485
- }
486
- // Remote running: forward inputs individually (no batch API available)
487
- else if (this.engine &&
488
- this.backend.kind !== "local" &&
489
- this.engine instanceof sparkRemote.RemoteEngine) {
490
- this.engine.setInputs(nodeId, inputs);
491
- }
492
- // Not running: emit value events so UI reflects staged values
493
- else if (!this.engine) {
494
- // Not running: emit a single synthetic value event per handle; UI will coalesce
495
- console.warn("Remote engine does not exists");
496
- for (const [handle, value] of Object.entries(inputs)) {
497
- this.emit("value", { nodeId, handle, value, io: "input" });
498
- }
499
- }
500
- }
501
654
  async step() {
502
- if (this.backend.kind !== "local")
503
- return; // unsupported remotely
504
- const eng = this.engine;
505
- if (eng instanceof sparkGraph.StepEngine)
506
- await eng.step();
655
+ console.warn("Unsupported operation for remote runner");
507
656
  }
508
657
  async computeNode(nodeId) {
509
- if (this.backend.kind !== "local")
510
- return; // unsupported remotely
511
- const eng = this.engine;
512
- if (eng instanceof sparkGraph.PullEngine)
513
- await eng.computeNode(nodeId);
658
+ console.warn("Unsupported operation for remote runner");
514
659
  }
515
660
  flush() {
516
- if (this.backend.kind !== "local")
517
- return; // unsupported remotely
518
- const eng = this.engine;
519
- if (eng instanceof sparkGraph.BatchedEngine)
520
- eng.flush();
661
+ console.warn("Unsupported operation for remote runner");
521
662
  }
522
663
  getOutputs(def) {
523
664
  const out = {};
524
- if (this.backend.kind === "local") {
525
- if (!this.runtime)
526
- return out;
527
- for (const n of def.nodes) {
528
- const desc = this.registry.nodes.get(n.typeId);
529
- const handles = Object.keys(desc?.outputs ?? {});
530
- for (const h of handles) {
531
- const v = this.runtime.getOutput(n.nodeId, h);
532
- if (v !== undefined) {
533
- if (!out[n.nodeId])
534
- out[n.nodeId] = {};
535
- out[n.nodeId][h] = v;
536
- }
537
- }
538
- }
539
- return out;
540
- }
541
- const cache = this.remote?.valueCache;
665
+ const cache = this.valueCache;
542
666
  if (!cache)
543
667
  return out;
544
668
  for (const n of def.nodes) {
@@ -558,31 +682,14 @@ class GraphRunner {
558
682
  }
559
683
  getInputs(def) {
560
684
  const out = {};
561
- if (this.backend.kind === "local") {
562
- for (const n of def.nodes) {
563
- const staged = this.stagedInputs[n.nodeId] ?? {};
564
- const runtimeInputs = this.runtime
565
- ? this.runtime.getNodeData?.(n.nodeId)?.inputs ?? {}
566
- : {};
567
- if (this.isRunning()) {
568
- out[n.nodeId] = runtimeInputs;
569
- }
570
- else {
571
- const merged = { ...runtimeInputs, ...staged };
572
- if (Object.keys(merged).length > 0)
573
- out[n.nodeId] = merged;
574
- }
575
- }
576
- return out;
577
- }
578
- const cache = this.remote?.valueCache;
685
+ const cache = this.valueCache;
579
686
  for (const n of def.nodes) {
580
687
  const staged = this.stagedInputs[n.nodeId] ?? {};
581
688
  const desc = this.registry.nodes.get(n.typeId);
582
689
  const handles = Object.keys(desc?.inputs ?? {});
583
690
  const cur = {};
584
691
  for (const h of handles) {
585
- const rec = cache?.get(`${n.nodeId}.${h}`);
692
+ const rec = cache.get(`${n.nodeId}.${h}`);
586
693
  if (rec && rec.io === "input")
587
694
  cur[h] = rec.value;
588
695
  }
@@ -592,52 +699,21 @@ class GraphRunner {
592
699
  }
593
700
  return out;
594
701
  }
595
- async whenIdle() {
596
- await this.engine?.whenIdle();
597
- }
598
- on(event, handler) {
599
- if (!this.listeners.has(event))
600
- this.listeners.set(event, new Set());
601
- const set = this.listeners.get(event);
602
- set.add(handler);
603
- return () => set.delete(handler);
604
- }
605
- emit(event, payload) {
606
- const set = this.listeners.get(event);
607
- if (set)
608
- for (const h of Array.from(set))
609
- h(payload);
610
- }
611
702
  dispose() {
612
- this.engine?.dispose();
613
- this.engine = undefined;
614
- this.runtime?.dispose();
615
- this.runtime = undefined;
616
- this.remote = undefined;
617
- if (this.runningKind) {
618
- this.runningKind = undefined;
619
- this.emit("status", { running: false, engine: undefined });
620
- }
621
- const kind = this.backend.kind === "local"
622
- ? undefined
623
- : this.backend.kind;
703
+ super.dispose();
704
+ this.runner = undefined;
705
+ this.transport = undefined;
624
706
  this.emit("transport", {
625
- state: this.backend.kind === "local" ? "local" : "disconnected",
626
- kind,
707
+ state: "disconnected",
708
+ kind: this.backend.kind,
627
709
  });
628
710
  }
629
- isRunning() {
630
- return !!this.engine;
631
- }
632
- getRunningEngine() {
633
- return this.runningKind;
634
- }
635
711
  // Ensure remote transport/runner
636
- async ensureRemote() {
637
- if (this.remote)
638
- return this.remote;
712
+ async ensureRemoteRunner() {
713
+ if (this.runner)
714
+ return this.runner;
639
715
  let transport;
640
- const kind = this.backend.kind === "remote-http" ? "remote-http" : "remote-ws";
716
+ const kind = this.backend.kind;
641
717
  this.emit("transport", { state: "connecting", kind });
642
718
  if (this.backend.kind === "remote-http") {
643
719
  if (!sparkRemote.HttpPollingTransport)
@@ -655,14 +731,12 @@ class GraphRunner {
655
731
  throw new Error("Remote backend not configured");
656
732
  }
657
733
  const runner = new sparkRemote.RemoteRunner(transport);
658
- this.remote = {
659
- runner,
660
- transport,
661
- valueCache: new Map(),
662
- listenersBound: false,
663
- };
734
+ this.runner = runner;
735
+ this.transport = transport;
736
+ this.valueCache.clear();
737
+ this.listenersBound = false;
664
738
  this.emit("transport", { state: "connected", kind });
665
- return this.remote;
739
+ return runner;
666
740
  }
667
741
  }
668
742
 
@@ -1335,6 +1409,23 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
1335
1409
  });
1336
1410
  const off7 = wb.on("error", add("workbench", "error"));
1337
1411
  wb.refreshValidation();
1412
+ // Registry updates: swap registry and refresh graph validation/UI
1413
+ const offReg = runner.on("registry", (newReg) => {
1414
+ try {
1415
+ setRegistry(newReg);
1416
+ wb.setRegistry(newReg);
1417
+ // Trigger a graph update so the UI revalidates with new types/enums/nodes
1418
+ try {
1419
+ runner.update(wb.export());
1420
+ }
1421
+ catch {
1422
+ console.error("Failed to update graph definition after registry changed");
1423
+ }
1424
+ }
1425
+ catch {
1426
+ console.error("Failed to handle registry changed event");
1427
+ }
1428
+ });
1338
1429
  return () => {
1339
1430
  off1();
1340
1431
  off2();
@@ -1347,6 +1438,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
1347
1438
  off5b();
1348
1439
  off6();
1349
1440
  off7();
1441
+ offReg();
1350
1442
  };
1351
1443
  }, [runner, wb]);
1352
1444
  // Push incremental updates into running engine without full reload
@@ -1528,7 +1620,7 @@ function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWork
1528
1620
  return (jsxRuntime.jsxs("div", { className: "flex flex-col h-full min-h-0", children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Events" }), jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [jsxRuntime.jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: hideWorkbench, onChange: (e) => onHideWorkbenchChange?.(e.target.checked) }), jsxRuntime.jsx("span", { children: "Hide workbench" })] }), jsxRuntime.jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: autoScroll, onChange: (e) => onAutoScrollChange?.(e.target.checked) }), jsxRuntime.jsx("span", { children: "Auto scroll" })] }), jsxRuntime.jsx("button", { onClick: clearEvents, className: "text-xs px-2 py-0.5 border border-gray-300 rounded", children: "Clear" })] })] }), jsxRuntime.jsx("div", { ref: scrollRef, className: "flex-1 overflow-auto text-[11px] leading-4 divide-y divide-gray-200", children: rows.map((ev, idx) => (jsxRuntime.jsxs("div", { className: "opacity-85 odd:bg-gray-50 px-2 py-1", children: [jsxRuntime.jsxs("div", { className: "flex items-baseline gap-2", children: [jsxRuntime.jsx("span", { className: "w-8 shrink-0 text-right text-gray-500 select-none", children: idx + 1 }), jsxRuntime.jsxs("span", { className: "text-gray-500", children: [new Date(ev.at).toLocaleTimeString(), " \u00B7 ", ev.source, ":", ev.type] })] }), jsxRuntime.jsx("pre", { className: "m-0 whitespace-pre-wrap ml-10", children: renderPayload(ev.payload) })] }, `${ev.at}:${idx}`))) })] }));
1529
1621
  }
1530
1622
 
1531
- function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toString, toElement, setInput, }) {
1623
+ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toString, toElement, contextPanel, setInput, }) {
1532
1624
  const safeToString = (typeId, value) => {
1533
1625
  try {
1534
1626
  if (typeof toString === "function") {
@@ -1617,7 +1709,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
1617
1709
  setOriginals(nextOriginals);
1618
1710
  }, [selectedNodeId, selectedDesc, valuesTick]);
1619
1711
  const widthClass = debug ? "w-[480px]" : "w-[320px]";
1620
- return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-hidden`, children: [jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsx("div", { className: "flex-1 overflow-auto", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxRuntime.jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedEdge.typeId] })] }), selectedEdgeValidation.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: selectedEdgeValidation.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : (jsxRuntime.jsxs("div", { children: [selectedNode && (jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Node: ", selectedNode.nodeId] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedNode.typeId] }), !!selectedNodeStatus?.lastError && (jsxRuntime.jsx("div", { className: "mt-2 text-sm text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 break-words", children: String(selectedNodeStatus.lastError?.message ??
1712
+ return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-hidden`, children: [contextPanel && (jsxRuntime.jsx("div", { className: "mb-2", children: contextPanel })), jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsx("div", { className: "flex-1 overflow-auto", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxRuntime.jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedEdge.typeId] })] }), selectedEdgeValidation.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: selectedEdgeValidation.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : (jsxRuntime.jsxs("div", { children: [selectedNode && (jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Node: ", selectedNode.nodeId] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedNode.typeId] }), !!selectedNodeStatus?.lastError && (jsxRuntime.jsx("div", { className: "mt-2 text-sm text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 break-words", children: String(selectedNodeStatus.lastError?.message ??
1621
1713
  selectedNodeStatus.lastError) }))] })), jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Inputs" }), inputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No inputs" })) : (inputHandles.map((h) => {
1622
1714
  const typeId = sparkGraph.getInputTypeId(selectedDesc?.inputs, h);
1623
1715
  const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
@@ -2441,18 +2533,22 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2441
2533
  catch (err) {
2442
2534
  alert(String(err?.message ?? err));
2443
2535
  }
2444
- }, disabled: !engine, children: "Start" })), jsxRuntime.jsx("button", { className: "border border-gray-300 rounded px-2 py-1.5", onClick: runAutoLayout, children: "Auto Layout" }), jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: () => canvasRef.current?.fitView?.(), title: "Fit View", children: "Fit View" }), jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: downloadGraph, children: "Download Graph" }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Debug events" })] }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Show values in nodes" })] })] }), jsxRuntime.jsxs("div", { className: "flex flex-1 min-h-0", children: [jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: jsxRuntime.jsx(WorkbenchCanvas, { ref: canvasRef, showValues: showValues, toString: toString, toElement: toElement }) }), jsxRuntime.jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, toElement: toElement })] })] }));
2536
+ }, disabled: !engine, children: "Start" })), jsxRuntime.jsx("button", { className: "border border-gray-300 rounded px-2 py-1.5", onClick: runAutoLayout, children: "Auto Layout" }), jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: () => canvasRef.current?.fitView?.(), title: "Fit View", children: "Fit View" }), jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: downloadGraph, children: "Download Graph" }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Debug events" })] }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Show values in nodes" })] })] }), jsxRuntime.jsxs("div", { className: "flex flex-1 min-h-0", children: [jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: jsxRuntime.jsx(WorkbenchCanvas, { ref: canvasRef, showValues: showValues, toString: toString, toElement: toElement }) }), jsxRuntime.jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, toElement: toElement, contextPanel: overrides?.contextPanel })] })] }));
2445
2537
  }
2446
- function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, overrides, onInit, onChange, }) {
2538
+ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, overrides, onInit, onChange, contextPanel, }) {
2447
2539
  const [registry, setRegistry] = React.useState(sparkGraph.createSimpleGraphRegistry());
2448
2540
  const [wb] = React.useState(() => new InMemoryWorkbench({ ui: new DefaultUIExtensionRegistry() }));
2449
2541
  const runner = React.useMemo(() => {
2450
- const backend = backendKind === "remote-http"
2451
- ? { kind: "remote-http", baseUrl: httpBaseUrl }
2452
- : backendKind === "remote-ws"
2453
- ? { kind: "remote-ws", url: wsUrl }
2454
- : { kind: "local" };
2455
- return new GraphRunner(registry, backend);
2542
+ if (backendKind === "remote-http") {
2543
+ return new RemoteGraphRunner(registry, {
2544
+ kind: "remote-http",
2545
+ baseUrl: httpBaseUrl,
2546
+ });
2547
+ }
2548
+ if (backendKind === "remote-ws") {
2549
+ return new RemoteGraphRunner(registry, { kind: "remote-ws", url: wsUrl });
2550
+ }
2551
+ return new LocalGraphRunner(registry);
2456
2552
  }, [registry, backendKind, httpBaseUrl, wsUrl]);
2457
2553
  // Allow external UI registration (e.g., node renderers) with access to wb
2458
2554
  React.useEffect(() => {
@@ -2470,9 +2566,10 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
2470
2566
  exports.AbstractWorkbench = AbstractWorkbench;
2471
2567
  exports.CLIWorkbench = CLIWorkbench;
2472
2568
  exports.DefaultUIExtensionRegistry = DefaultUIExtensionRegistry;
2473
- exports.GraphRunner = GraphRunner;
2474
2569
  exports.InMemoryWorkbench = InMemoryWorkbench;
2475
2570
  exports.Inspector = Inspector;
2571
+ exports.LocalGraphRunner = LocalGraphRunner;
2572
+ exports.RemoteGraphRunner = RemoteGraphRunner;
2476
2573
  exports.WorkbenchCanvas = WorkbenchCanvas;
2477
2574
  exports.WorkbenchContext = WorkbenchContext;
2478
2575
  exports.WorkbenchProvider = WorkbenchProvider;