@bian-womp/spark-workbench 0.1.28 → 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 (49) hide show
  1. package/lib/cjs/index.cjs +324 -246
  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/WorkbenchCanvas.d.ts.map +1 -1
  8. package/lib/cjs/src/misc/WorkbenchStudio.d.ts +8 -5
  9. package/lib/cjs/src/misc/WorkbenchStudio.d.ts.map +1 -1
  10. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +2 -2
  11. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  12. package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts +2 -2
  13. package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  14. package/lib/cjs/src/misc/hooks.d.ts +2 -2
  15. package/lib/cjs/src/misc/hooks.d.ts.map +1 -1
  16. package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts +29 -0
  17. package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts.map +1 -0
  18. package/lib/cjs/src/runtime/{GraphRunner.d.ts → IGraphRunner.d.ts} +21 -31
  19. package/lib/cjs/src/runtime/IGraphRunner.d.ts.map +1 -0
  20. package/lib/cjs/src/runtime/LocalGraphRunner.d.ts +16 -0
  21. package/lib/cjs/src/runtime/LocalGraphRunner.d.ts.map +1 -0
  22. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts +27 -0
  23. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts.map +1 -0
  24. package/lib/esm/index.js +325 -248
  25. package/lib/esm/index.js.map +1 -1
  26. package/lib/esm/src/index.d.ts +3 -1
  27. package/lib/esm/src/index.d.ts.map +1 -1
  28. package/lib/esm/src/misc/Inspector.d.ts +3 -1
  29. package/lib/esm/src/misc/Inspector.d.ts.map +1 -1
  30. package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  31. package/lib/esm/src/misc/WorkbenchStudio.d.ts +8 -5
  32. package/lib/esm/src/misc/WorkbenchStudio.d.ts.map +1 -1
  33. package/lib/esm/src/misc/context/WorkbenchContext.d.ts +2 -2
  34. package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  35. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts +2 -2
  36. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  37. package/lib/esm/src/misc/hooks.d.ts +2 -2
  38. package/lib/esm/src/misc/hooks.d.ts.map +1 -1
  39. package/lib/esm/src/runtime/AbstractGraphRunner.d.ts +29 -0
  40. package/lib/esm/src/runtime/AbstractGraphRunner.d.ts.map +1 -0
  41. package/lib/esm/src/runtime/{GraphRunner.d.ts → IGraphRunner.d.ts} +21 -31
  42. package/lib/esm/src/runtime/IGraphRunner.d.ts.map +1 -0
  43. package/lib/esm/src/runtime/LocalGraphRunner.d.ts +16 -0
  44. package/lib/esm/src/runtime/LocalGraphRunner.d.ts.map +1 -0
  45. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts +27 -0
  46. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts.map +1 -0
  47. package/package.json +4 -4
  48. package/lib/cjs/src/runtime/GraphRunner.d.ts.map +0 -1
  49. package/lib/esm/src/runtime/GraphRunner.d.ts.map +0 -1
package/lib/esm/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { GraphBuilder, StepEngine, HybridEngine, PullEngine, BatchedEngine, PushEngine, isTypedOutput, getTypedOutputValue, getTypedOutputTypeId, isInputPrivate, getInputTypeId, createSimpleGraphRegistry, createSimpleGraphDef, createAsyncGraphDef, createAsyncGraphRegistry, createProgressGraphDef, createProgressGraphRegistry, createValidationGraphDef, createValidationGraphRegistry, Registry } from '@bian-womp/spark-graph';
2
- import { RemoteEngine, HttpPollingTransport, WebSocketTransport, RemoteRunner } from '@bian-womp/spark-remote';
2
+ import { HttpPollingTransport, WebSocketTransport, RemoteRunner } from '@bian-womp/spark-remote';
3
3
  import React, { useCallback, useState, useEffect, useMemo, createContext, useContext, useRef, useImperativeHandle } from 'react';
4
4
  import { jsx, jsxs } from 'react/jsx-runtime';
5
5
  import { XCircleIcon, WarningCircleIcon, PlugsConnectedIcon, ClockClockwiseIcon, WifiHighIcon, WifiSlashIcon } from '@phosphor-icons/react';
6
- import ReactFlow, { Handle, Position, useReactFlow, applyNodeChanges, applyEdgeChanges, Background, MiniMap, Controls } from 'reactflow';
6
+ import ReactFlow, { Handle, Position, useReactFlow, Background, MiniMap, Controls } from 'reactflow';
7
7
  import cx from 'classnames';
8
8
 
9
9
  class DefaultUIExtensionRegistry {
@@ -317,104 +317,295 @@ class CLIWorkbench {
317
317
  }
318
318
  }
319
319
 
320
- class GraphRunner {
320
+ class AbstractGraphRunner {
321
321
  constructor(registry, backend) {
322
322
  this.registry = registry;
323
+ this.backend = backend;
323
324
  this.listeners = new Map();
324
325
  this.stagedInputs = {};
325
- this.backend = { kind: "local" };
326
- if (backend)
327
- this.backend = backend;
328
- // Emit initial transport status
329
- if (this.backend.kind === "local")
330
- this.emit("transport", { state: "local" });
331
326
  }
332
- build(def) {
333
- if (this.backend.kind === "local") {
334
- const builder = new GraphBuilder(this.registry);
335
- this.runtime = builder.build(def);
336
- // Signal UI that freshly built graph should be considered invalidated
337
- this.emit("invalidate", { reason: "graph-built" });
327
+ launch(def, opts) {
328
+ if (this.engine) {
329
+ throw new Error("Engine already running. Stop the current engine first.");
330
+ }
331
+ }
332
+ setInput(nodeId, handle, value) {
333
+ if (!this.stagedInputs[nodeId])
334
+ this.stagedInputs[nodeId] = {};
335
+ this.stagedInputs[nodeId][handle] = value;
336
+ if (this.engine) {
337
+ this.engine.setInput(nodeId, handle, value);
338
+ }
339
+ else {
340
+ // Emit a value event so UI updates even when engine isn't running
341
+ this.emit("value", { nodeId, handle, value, io: "input" });
342
+ }
343
+ }
344
+ // Batch update multiple inputs on a node and trigger a single run
345
+ setInputs(nodeId, inputs) {
346
+ if (!inputs)
338
347
  return;
348
+ if (!this.stagedInputs[nodeId])
349
+ this.stagedInputs[nodeId] = {};
350
+ Object.assign(this.stagedInputs[nodeId], inputs);
351
+ if (this.engine) {
352
+ // Running: set all inputs
353
+ this.engine.setInputs(nodeId, inputs);
339
354
  }
340
- // Remote: no-op here; build is performed on remote server during launch
355
+ else {
356
+ // Not running: emit a single synthetic value event per handle; UI will coalesce
357
+ console.warn("Engine does not exists");
358
+ for (const [handle, value] of Object.entries(inputs)) {
359
+ this.emit("value", { nodeId, handle, value, io: "input" });
360
+ }
361
+ }
362
+ }
363
+ async whenIdle() {
364
+ await this.engine?.whenIdle();
365
+ }
366
+ on(event, handler) {
367
+ if (!this.listeners.has(event))
368
+ this.listeners.set(event, new Set());
369
+ const set = this.listeners.get(event);
370
+ set.add(handler);
371
+ return () => set.delete(handler);
372
+ }
373
+ emit(event, payload) {
374
+ const set = this.listeners.get(event);
375
+ if (set)
376
+ for (const h of Array.from(set))
377
+ h(payload);
378
+ }
379
+ dispose() {
380
+ this.engine?.dispose();
381
+ this.engine = undefined;
382
+ this.runtime?.dispose();
383
+ this.runtime = undefined;
384
+ if (this.runningKind) {
385
+ this.runningKind = undefined;
386
+ this.emit("status", { running: false, engine: undefined });
387
+ }
388
+ }
389
+ isRunning() {
390
+ return !!this.engine;
391
+ }
392
+ getRunningEngine() {
393
+ return this.runningKind;
394
+ }
395
+ }
396
+
397
+ class LocalGraphRunner extends AbstractGraphRunner {
398
+ constructor(registry) {
399
+ super(registry, { kind: "local" });
400
+ this.emit("transport", { state: "local" });
401
+ }
402
+ build(def) {
403
+ const builder = new GraphBuilder(this.registry);
404
+ this.runtime = builder.build(def);
405
+ // Signal UI that freshly built graph should be considered invalidated
406
+ this.emit("invalidate", { reason: "graph-built" });
341
407
  }
342
408
  update(def) {
343
- if (this.backend.kind === "local") {
344
- if (!this.runtime)
345
- return;
346
- // Prevent mid-run churn while wiring changes are applied
347
- this.runtime.pause();
348
- this.runtime.update(def, this.registry);
349
- this.runtime.resume();
350
- this.emit("invalidate", { reason: "graph-updated" });
409
+ if (!this.runtime)
351
410
  return;
411
+ // Prevent mid-run churn while wiring changes are applied
412
+ this.runtime.pause();
413
+ this.runtime.update(def, this.registry);
414
+ this.runtime.resume();
415
+ this.emit("invalidate", { reason: "graph-updated" });
416
+ }
417
+ launch(def, opts) {
418
+ super.launch(def, opts);
419
+ this.build(def);
420
+ if (!this.runtime)
421
+ throw new Error("Runtime not built");
422
+ const rt = this.runtime;
423
+ switch (opts.engine) {
424
+ case "push":
425
+ this.engine = new PushEngine(rt);
426
+ break;
427
+ case "batched":
428
+ this.engine = new BatchedEngine(rt, {
429
+ flushIntervalMs: opts.batched?.flushIntervalMs ?? 0,
430
+ });
431
+ break;
432
+ case "pull":
433
+ this.engine = new PullEngine(rt);
434
+ break;
435
+ case "hybrid":
436
+ this.engine = new HybridEngine(rt, {
437
+ windowMs: opts.hybrid?.windowMs ?? 250,
438
+ batchThreshold: opts.hybrid?.batchThreshold ?? 3,
439
+ });
440
+ break;
441
+ case "step":
442
+ this.engine = new StepEngine(rt);
443
+ break;
444
+ default:
445
+ throw new Error("Unknown engine kind");
352
446
  }
447
+ this.engine.on("value", (e) => this.emit("value", e));
448
+ this.engine.on("error", (e) => this.emit("error", e));
449
+ this.engine.on("invalidate", (e) => this.emit("invalidate", e));
450
+ this.engine.on("stats", (e) => this.emit("stats", e));
451
+ this.engine.launch();
452
+ this.runningKind = opts.engine;
453
+ this.emit("status", { running: true, engine: this.runningKind });
454
+ for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
455
+ this.engine.setInputs(nodeId, map);
456
+ }
457
+ }
458
+ async step() {
459
+ const eng = this.engine;
460
+ if (eng instanceof StepEngine)
461
+ await eng.step();
462
+ }
463
+ async computeNode(nodeId) {
464
+ const eng = this.engine;
465
+ if (eng instanceof PullEngine)
466
+ await eng.computeNode(nodeId);
467
+ }
468
+ flush() {
469
+ const eng = this.engine;
470
+ if (eng instanceof BatchedEngine)
471
+ eng.flush();
472
+ }
473
+ getOutputs(def) {
474
+ const out = {};
475
+ if (!this.runtime)
476
+ return out;
477
+ for (const n of def.nodes) {
478
+ const desc = this.registry.nodes.get(n.typeId);
479
+ const handles = Object.keys(desc?.outputs ?? {});
480
+ for (const h of handles) {
481
+ const v = this.runtime.getOutput(n.nodeId, h);
482
+ if (v !== undefined) {
483
+ if (!out[n.nodeId])
484
+ out[n.nodeId] = {};
485
+ out[n.nodeId][h] = v;
486
+ }
487
+ }
488
+ }
489
+ return out;
490
+ }
491
+ getInputs(def) {
492
+ const out = {};
493
+ for (const n of def.nodes) {
494
+ const staged = this.stagedInputs[n.nodeId] ?? {};
495
+ const runtimeInputs = this.runtime
496
+ ? this.runtime.getNodeData?.(n.nodeId)?.inputs ?? {}
497
+ : {};
498
+ if (this.isRunning()) {
499
+ out[n.nodeId] = runtimeInputs;
500
+ }
501
+ else {
502
+ const merged = { ...runtimeInputs, ...staged };
503
+ if (Object.keys(merged).length > 0)
504
+ out[n.nodeId] = merged;
505
+ }
506
+ }
507
+ return out;
508
+ }
509
+ dispose() {
510
+ super.dispose();
511
+ this.runtime = undefined;
512
+ this.emit("transport", { state: "local" });
513
+ }
514
+ }
515
+
516
+ class RemoteGraphRunner extends AbstractGraphRunner {
517
+ constructor(registry, backend) {
518
+ super(registry, backend);
519
+ this.valueCache = new Map();
520
+ this.listenersBound = false;
521
+ // Auto-handle registry-changed invalidations from remote
522
+ // We listen on invalidate and if reason matches, we rehydrate registry and emit a registry event
523
+ this.ensureRemoteRunner().then(async (runner) => {
524
+ const eng = runner.getEngine();
525
+ if (!this.listenersBound) {
526
+ eng.on("invalidate", async (e) => {
527
+ if (e.reason === "registry-changed") {
528
+ try {
529
+ const deltas = Array.isArray(e.deltas) ? e.deltas : [];
530
+ for (const d of deltas) {
531
+ if (!d || typeof d !== "object")
532
+ continue;
533
+ if (d.kind === "register-enum") {
534
+ this.registry.registerEnum({
535
+ id: d.id,
536
+ displayName: d.displayName,
537
+ options: d.options,
538
+ opts: d.opts,
539
+ });
540
+ }
541
+ else if (d.kind === "register-type") {
542
+ if (!this.registry.types.has(d.id)) {
543
+ this.registry.registerType({
544
+ id: d.id,
545
+ displayName: d.displayName,
546
+ validate: (_v) => true,
547
+ });
548
+ }
549
+ }
550
+ else if (d.kind === "register-node") {
551
+ if (!this.registry.nodes.has(d.desc?.id)) {
552
+ this.registry.registerNode({
553
+ id: String(d.desc?.id || ""),
554
+ categoryId: String(d.desc?.categoryId || "compute"),
555
+ displayName: d.desc?.displayName,
556
+ inputs: d.desc?.inputs || {},
557
+ outputs: d.desc?.outputs || {},
558
+ impl: () => { },
559
+ });
560
+ }
561
+ }
562
+ }
563
+ this.emit("registry", this.registry);
564
+ // Trigger update so validation/UI refreshes using last known graph
565
+ try {
566
+ if (this.lastDef)
567
+ this.update(this.lastDef);
568
+ }
569
+ catch {
570
+ console.error("Failed to update graph definition after registry changed");
571
+ }
572
+ }
573
+ catch {
574
+ console.error("Failed to handle registry changed event");
575
+ }
576
+ }
577
+ });
578
+ }
579
+ });
580
+ }
581
+ build(def) {
582
+ console.warn("Unsupported operation for remote runner");
583
+ }
584
+ update(def) {
353
585
  // Remote: forward update; ignore errors (fire-and-forget)
354
- void this.ensureRemote().then(async (rc) => {
586
+ this.ensureRemoteRunner().then(async (runner) => {
355
587
  try {
356
- await rc.runner.update(def);
588
+ await runner.update(def);
357
589
  this.emit("invalidate", { reason: "graph-updated" });
590
+ this.lastDef = def;
358
591
  }
359
592
  catch { }
360
593
  });
361
594
  }
362
595
  launch(def, opts) {
363
- if (this.engine) {
364
- throw new Error("Engine already running. Stop the current engine first.");
365
- }
366
- if (this.backend.kind === "local") {
367
- this.build(def);
368
- if (!this.runtime)
369
- throw new Error("Runtime not built");
370
- const rt = this.runtime;
371
- switch (opts.engine) {
372
- case "push":
373
- this.engine = new PushEngine(rt);
374
- break;
375
- case "batched":
376
- this.engine = new BatchedEngine(rt, {
377
- flushIntervalMs: opts.batched?.flushIntervalMs ?? 0,
378
- });
379
- break;
380
- case "pull":
381
- this.engine = new PullEngine(rt);
382
- break;
383
- case "hybrid":
384
- this.engine = new HybridEngine(rt, {
385
- windowMs: opts.hybrid?.windowMs ?? 250,
386
- batchThreshold: opts.hybrid?.batchThreshold ?? 3,
387
- });
388
- break;
389
- case "step":
390
- this.engine = new StepEngine(rt);
391
- break;
392
- default:
393
- throw new Error("Unknown engine kind");
394
- }
395
- this.engine.on("value", (e) => this.emit("value", e));
396
- this.engine.on("error", (e) => this.emit("error", e));
397
- this.engine.on("invalidate", (e) => this.emit("invalidate", e));
398
- this.engine.on("stats", (e) => this.emit("stats", e));
399
- this.engine.launch();
400
- this.runningKind = opts.engine;
401
- this.emit("status", { running: true, engine: this.runningKind });
402
- for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
403
- this.engine.setInputs(nodeId, map);
404
- }
405
- return;
406
- }
596
+ super.launch(def, opts);
407
597
  // Remote: build remotely then launch
408
- void this.ensureRemote().then(async (rc) => {
409
- await rc.runner.build(def);
598
+ this.ensureRemoteRunner().then(async (runner) => {
599
+ await runner.build(def);
410
600
  // Signal UI after remote build as well
411
601
  this.emit("invalidate", { reason: "graph-built" });
602
+ this.lastDef = def;
412
603
  // Hydrate current remote inputs/outputs (including defaults) into cache
413
604
  try {
414
- const snap = await rc.runner.snapshot();
605
+ const snap = await runner.snapshot();
415
606
  for (const [nodeId, map] of Object.entries(snap.inputs || {})) {
416
607
  for (const [handle, value] of Object.entries(map || {})) {
417
- rc.valueCache.set(`${nodeId}.${handle}`, {
608
+ this.valueCache.set(`${nodeId}.${handle}`, {
418
609
  io: "input",
419
610
  value,
420
611
  });
@@ -423,7 +614,7 @@ class GraphRunner {
423
614
  }
424
615
  for (const [nodeId, map] of Object.entries(snap.outputs || {})) {
425
616
  for (const [handle, value] of Object.entries(map || {})) {
426
- rc.valueCache.set(`${nodeId}.${handle}`, {
617
+ this.valueCache.set(`${nodeId}.${handle}`, {
427
618
  io: "output",
428
619
  value,
429
620
  });
@@ -434,10 +625,10 @@ class GraphRunner {
434
625
  catch {
435
626
  console.error("Failed to hydrate remote inputs/outputs");
436
627
  }
437
- const eng = rc.runner.getEngine();
438
- if (!rc.listenersBound) {
628
+ const eng = runner.getEngine();
629
+ if (!this.listenersBound) {
439
630
  eng.on("value", (e) => {
440
- rc.valueCache.set(`${e.nodeId}.${e.handle}`, {
631
+ this.valueCache.set(`${e.nodeId}.${e.handle}`, {
441
632
  io: e.io,
442
633
  value: e.value,
443
634
  runtimeTypeId: e.runtimeTypeId,
@@ -447,7 +638,7 @@ class GraphRunner {
447
638
  eng.on("error", (e) => this.emit("error", e));
448
639
  eng.on("invalidate", (e) => this.emit("invalidate", e));
449
640
  eng.on("stats", (e) => this.emit("stats", e));
450
- rc.listenersBound = true;
641
+ this.listenersBound = true;
451
642
  }
452
643
  this.engine = eng;
453
644
  this.engine.launch();
@@ -458,85 +649,18 @@ class GraphRunner {
458
649
  }
459
650
  });
460
651
  }
461
- setInput(nodeId, handle, value) {
462
- if (!this.stagedInputs[nodeId])
463
- this.stagedInputs[nodeId] = {};
464
- this.stagedInputs[nodeId][handle] = value;
465
- if (this.engine) {
466
- this.engine.setInput(nodeId, handle, value);
467
- }
468
- else {
469
- // Emit a value event so UI updates even when engine isn't running
470
- this.emit("value", { nodeId, handle, value, io: "input" });
471
- }
472
- }
473
- // Batch update multiple inputs on a node and trigger a single run
474
- setInputs(nodeId, inputs) {
475
- if (!inputs)
476
- return;
477
- if (!this.stagedInputs[nodeId])
478
- this.stagedInputs[nodeId] = {};
479
- Object.assign(this.stagedInputs[nodeId], inputs);
480
- // Local running: pause, set all inputs, resume, schedule a single recompute
481
- if (this.backend.kind === "local" && this.engine && this.runtime) {
482
- this.engine.setInputs(nodeId, inputs);
483
- }
484
- // Remote running: forward inputs individually (no batch API available)
485
- else if (this.engine &&
486
- this.backend.kind !== "local" &&
487
- this.engine instanceof RemoteEngine) {
488
- this.engine.setInputs(nodeId, inputs);
489
- }
490
- // Not running: emit value events so UI reflects staged values
491
- else if (!this.engine) {
492
- // Not running: emit a single synthetic value event per handle; UI will coalesce
493
- console.warn("Remote engine does not exists");
494
- for (const [handle, value] of Object.entries(inputs)) {
495
- this.emit("value", { nodeId, handle, value, io: "input" });
496
- }
497
- }
498
- }
499
652
  async step() {
500
- if (this.backend.kind !== "local")
501
- return; // unsupported remotely
502
- const eng = this.engine;
503
- if (eng instanceof StepEngine)
504
- await eng.step();
653
+ console.warn("Unsupported operation for remote runner");
505
654
  }
506
655
  async computeNode(nodeId) {
507
- if (this.backend.kind !== "local")
508
- return; // unsupported remotely
509
- const eng = this.engine;
510
- if (eng instanceof PullEngine)
511
- await eng.computeNode(nodeId);
656
+ console.warn("Unsupported operation for remote runner");
512
657
  }
513
658
  flush() {
514
- if (this.backend.kind !== "local")
515
- return; // unsupported remotely
516
- const eng = this.engine;
517
- if (eng instanceof BatchedEngine)
518
- eng.flush();
659
+ console.warn("Unsupported operation for remote runner");
519
660
  }
520
661
  getOutputs(def) {
521
662
  const out = {};
522
- if (this.backend.kind === "local") {
523
- if (!this.runtime)
524
- return out;
525
- for (const n of def.nodes) {
526
- const desc = this.registry.nodes.get(n.typeId);
527
- const handles = Object.keys(desc?.outputs ?? {});
528
- for (const h of handles) {
529
- const v = this.runtime.getOutput(n.nodeId, h);
530
- if (v !== undefined) {
531
- if (!out[n.nodeId])
532
- out[n.nodeId] = {};
533
- out[n.nodeId][h] = v;
534
- }
535
- }
536
- }
537
- return out;
538
- }
539
- const cache = this.remote?.valueCache;
663
+ const cache = this.valueCache;
540
664
  if (!cache)
541
665
  return out;
542
666
  for (const n of def.nodes) {
@@ -556,31 +680,14 @@ class GraphRunner {
556
680
  }
557
681
  getInputs(def) {
558
682
  const out = {};
559
- if (this.backend.kind === "local") {
560
- for (const n of def.nodes) {
561
- const staged = this.stagedInputs[n.nodeId] ?? {};
562
- const runtimeInputs = this.runtime
563
- ? this.runtime.getNodeData?.(n.nodeId)?.inputs ?? {}
564
- : {};
565
- if (this.isRunning()) {
566
- out[n.nodeId] = runtimeInputs;
567
- }
568
- else {
569
- const merged = { ...runtimeInputs, ...staged };
570
- if (Object.keys(merged).length > 0)
571
- out[n.nodeId] = merged;
572
- }
573
- }
574
- return out;
575
- }
576
- const cache = this.remote?.valueCache;
683
+ const cache = this.valueCache;
577
684
  for (const n of def.nodes) {
578
685
  const staged = this.stagedInputs[n.nodeId] ?? {};
579
686
  const desc = this.registry.nodes.get(n.typeId);
580
687
  const handles = Object.keys(desc?.inputs ?? {});
581
688
  const cur = {};
582
689
  for (const h of handles) {
583
- const rec = cache?.get(`${n.nodeId}.${h}`);
690
+ const rec = cache.get(`${n.nodeId}.${h}`);
584
691
  if (rec && rec.io === "input")
585
692
  cur[h] = rec.value;
586
693
  }
@@ -590,52 +697,21 @@ class GraphRunner {
590
697
  }
591
698
  return out;
592
699
  }
593
- async whenIdle() {
594
- await this.engine?.whenIdle();
595
- }
596
- on(event, handler) {
597
- if (!this.listeners.has(event))
598
- this.listeners.set(event, new Set());
599
- const set = this.listeners.get(event);
600
- set.add(handler);
601
- return () => set.delete(handler);
602
- }
603
- emit(event, payload) {
604
- const set = this.listeners.get(event);
605
- if (set)
606
- for (const h of Array.from(set))
607
- h(payload);
608
- }
609
700
  dispose() {
610
- this.engine?.dispose();
611
- this.engine = undefined;
612
- this.runtime?.dispose();
613
- this.runtime = undefined;
614
- this.remote = undefined;
615
- if (this.runningKind) {
616
- this.runningKind = undefined;
617
- this.emit("status", { running: false, engine: undefined });
618
- }
619
- const kind = this.backend.kind === "local"
620
- ? undefined
621
- : this.backend.kind;
701
+ super.dispose();
702
+ this.runner = undefined;
703
+ this.transport = undefined;
622
704
  this.emit("transport", {
623
- state: this.backend.kind === "local" ? "local" : "disconnected",
624
- kind,
705
+ state: "disconnected",
706
+ kind: this.backend.kind,
625
707
  });
626
708
  }
627
- isRunning() {
628
- return !!this.engine;
629
- }
630
- getRunningEngine() {
631
- return this.runningKind;
632
- }
633
709
  // Ensure remote transport/runner
634
- async ensureRemote() {
635
- if (this.remote)
636
- return this.remote;
710
+ async ensureRemoteRunner() {
711
+ if (this.runner)
712
+ return this.runner;
637
713
  let transport;
638
- const kind = this.backend.kind === "remote-http" ? "remote-http" : "remote-ws";
714
+ const kind = this.backend.kind;
639
715
  this.emit("transport", { state: "connecting", kind });
640
716
  if (this.backend.kind === "remote-http") {
641
717
  if (!HttpPollingTransport)
@@ -653,14 +729,12 @@ class GraphRunner {
653
729
  throw new Error("Remote backend not configured");
654
730
  }
655
731
  const runner = new RemoteRunner(transport);
656
- this.remote = {
657
- runner,
658
- transport,
659
- valueCache: new Map(),
660
- listenersBound: false,
661
- };
732
+ this.runner = runner;
733
+ this.transport = transport;
734
+ this.valueCache.clear();
735
+ this.listenersBound = false;
662
736
  this.emit("transport", { state: "connected", kind });
663
- return this.remote;
737
+ return runner;
664
738
  }
665
739
  }
666
740
 
@@ -1333,6 +1407,23 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
1333
1407
  });
1334
1408
  const off7 = wb.on("error", add("workbench", "error"));
1335
1409
  wb.refreshValidation();
1410
+ // Registry updates: swap registry and refresh graph validation/UI
1411
+ const offReg = runner.on("registry", (newReg) => {
1412
+ try {
1413
+ setRegistry(newReg);
1414
+ wb.setRegistry(newReg);
1415
+ // Trigger a graph update so the UI revalidates with new types/enums/nodes
1416
+ try {
1417
+ runner.update(wb.export());
1418
+ }
1419
+ catch {
1420
+ console.error("Failed to update graph definition after registry changed");
1421
+ }
1422
+ }
1423
+ catch {
1424
+ console.error("Failed to handle registry changed event");
1425
+ }
1426
+ });
1336
1427
  return () => {
1337
1428
  off1();
1338
1429
  off2();
@@ -1345,6 +1436,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
1345
1436
  off5b();
1346
1437
  off6();
1347
1438
  off7();
1439
+ offReg();
1348
1440
  };
1349
1441
  }, [runner, wb]);
1350
1442
  // Push incremental updates into running engine without full reload
@@ -1526,7 +1618,7 @@ function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWork
1526
1618
  return (jsxs("div", { className: "flex flex-col h-full min-h-0", children: [jsxs("div", { className: "flex items-center justify-between mb-1", children: [jsx("div", { className: "font-semibold", children: "Events" }), jsxs("div", { className: "flex items-center gap-2", children: [jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsx("input", { type: "checkbox", checked: hideWorkbench, onChange: (e) => onHideWorkbenchChange?.(e.target.checked) }), jsx("span", { children: "Hide workbench" })] }), jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsx("input", { type: "checkbox", checked: autoScroll, onChange: (e) => onAutoScrollChange?.(e.target.checked) }), jsx("span", { children: "Auto scroll" })] }), jsx("button", { onClick: clearEvents, className: "text-xs px-2 py-0.5 border border-gray-300 rounded", children: "Clear" })] })] }), jsx("div", { ref: scrollRef, className: "flex-1 overflow-auto text-[11px] leading-4 divide-y divide-gray-200", children: rows.map((ev, idx) => (jsxs("div", { className: "opacity-85 odd:bg-gray-50 px-2 py-1", children: [jsxs("div", { className: "flex items-baseline gap-2", children: [jsx("span", { className: "w-8 shrink-0 text-right text-gray-500 select-none", children: idx + 1 }), jsxs("span", { className: "text-gray-500", children: [new Date(ev.at).toLocaleTimeString(), " \u00B7 ", ev.source, ":", ev.type] })] }), jsx("pre", { className: "m-0 whitespace-pre-wrap ml-10", children: renderPayload(ev.payload) })] }, `${ev.at}:${idx}`))) })] }));
1527
1619
  }
1528
1620
 
1529
- function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toString, toElement, setInput, }) {
1621
+ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toString, toElement, contextPanel, setInput, }) {
1530
1622
  const safeToString = (typeId, value) => {
1531
1623
  try {
1532
1624
  if (typeof toString === "function") {
@@ -1615,7 +1707,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
1615
1707
  setOriginals(nextOriginals);
1616
1708
  }, [selectedNodeId, selectedDesc, valuesTick]);
1617
1709
  const widthClass = debug ? "w-[480px]" : "w-[320px]";
1618
- return (jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-hidden`, children: [jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsx("div", { className: "flex-1 overflow-auto", children: !selectedNode && !selectedEdge ? (jsxs("div", { children: [jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxs("li", { className: "flex items-center gap-1", children: [jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : selectedEdge ? (jsxs("div", { children: [jsxs("div", { className: "mb-2", children: [jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), jsxs("div", { children: ["Type: ", selectedEdge.typeId] })] }), selectedEdgeValidation.length > 0 && (jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsx("ul", { className: "list-disc ml-4", children: selectedEdgeValidation.map((m, i) => (jsxs("li", { className: "flex items-center gap-1", children: [jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : (jsxs("div", { children: [selectedNode && (jsxs("div", { className: "mb-2", children: [jsxs("div", { children: ["Node: ", selectedNode.nodeId] }), jsxs("div", { children: ["Type: ", selectedNode.typeId] }), !!selectedNodeStatus?.lastError && (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 ??
1710
+ return (jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-hidden`, children: [contextPanel && (jsx("div", { className: "mb-2", children: contextPanel })), jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsx("div", { className: "flex-1 overflow-auto", children: !selectedNode && !selectedEdge ? (jsxs("div", { children: [jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxs("li", { className: "flex items-center gap-1", children: [jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : selectedEdge ? (jsxs("div", { children: [jsxs("div", { className: "mb-2", children: [jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), jsxs("div", { children: ["Type: ", selectedEdge.typeId] })] }), selectedEdgeValidation.length > 0 && (jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsx("ul", { className: "list-disc ml-4", children: selectedEdgeValidation.map((m, i) => (jsxs("li", { className: "flex items-center gap-1", children: [jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : (jsxs("div", { children: [selectedNode && (jsxs("div", { className: "mb-2", children: [jsxs("div", { children: ["Node: ", selectedNode.nodeId] }), jsxs("div", { children: ["Type: ", selectedNode.typeId] }), !!selectedNodeStatus?.lastError && (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 ??
1619
1711
  selectedNodeStatus.lastError) }))] })), jsxs("div", { className: "mb-2", children: [jsx("div", { className: "font-semibold mb-1", children: "Inputs" }), inputHandles.length === 0 ? (jsx("div", { className: "text-gray-500", children: "No inputs" })) : (inputHandles.map((h) => {
1620
1712
  const typeId = getInputTypeId(selectedDesc?.inputs, h);
1621
1713
  const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
@@ -1921,7 +2013,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
1921
2013
  catch { }
1922
2014
  },
1923
2015
  }));
1924
- const { onConnect, onNodesChange: wbOnNodesChange, onEdgesChange: wbOnEdgesChange, onEdgesDelete, onNodesDelete, onSelectionChange, } = useWorkbenchBridge(wb);
2016
+ const { onConnect, onNodesChange, onEdgesChange, onEdgesDelete, onNodesDelete, onSelectionChange, } = useWorkbenchBridge(wb);
1925
2017
  const { nodeTypes, resolveNodeType } = useMemo(() => {
1926
2018
  // Build nodeTypes map using UI extension registry
1927
2019
  const ui = wb.getUI();
@@ -1942,7 +2034,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
1942
2034
  return { nodeTypes: types, resolveNodeType: resolver };
1943
2035
  // registry is stable; ui renderers expected to be set up before mount
1944
2036
  }, [wb, registry]);
1945
- const { nodes: computedNodes, edges: computedEdges } = useMemo(() => {
2037
+ const { nodes, edges } = useMemo(() => {
1946
2038
  const def = wb.export();
1947
2039
  const sel = wb.getSelection();
1948
2040
  const out = toReactFlow(def, wb.getPositions(), registry, {
@@ -1975,25 +2067,6 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
1975
2067
  nodeTypes,
1976
2068
  resolveNodeType,
1977
2069
  ]);
1978
- // Local controlled state per requested pattern
1979
- const [nodes, setNodes] = useState(computedNodes);
1980
- const [edges, setEdges] = useState(computedEdges);
1981
- // Sync from computed graph to local state when source changes
1982
- useEffect(() => {
1983
- setNodes(computedNodes);
1984
- }, [computedNodes]);
1985
- useEffect(() => {
1986
- setEdges(computedEdges);
1987
- }, [computedEdges]);
1988
- // Bridge RF changes to both local state and workbench
1989
- const onNodesChange = useCallback((changes) => {
1990
- setNodes((nds) => applyNodeChanges(changes, nds));
1991
- wbOnNodesChange(changes);
1992
- }, [wbOnNodesChange]);
1993
- const onEdgesChange = useCallback((changes) => {
1994
- setEdges((eds) => applyEdgeChanges(changes, eds));
1995
- wbOnEdgesChange(changes);
1996
- }, [wbOnEdgesChange]);
1997
2070
  const [menuOpen, setMenuOpen] = useState(false);
1998
2071
  const [menuPos, setMenuPos] = useState(null);
1999
2072
  const [nodeMenuOpen, setNodeMenuOpen] = useState(false);
@@ -2458,18 +2531,22 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2458
2531
  catch (err) {
2459
2532
  alert(String(err?.message ?? err));
2460
2533
  }
2461
- }, disabled: !engine, children: "Start" })), jsx("button", { className: "border border-gray-300 rounded px-2 py-1.5", onClick: runAutoLayout, children: "Auto Layout" }), 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" }), jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: downloadGraph, children: "Download Graph" }), jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsx("span", { children: "Debug events" })] }), jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsx("span", { children: "Show values in nodes" })] })] }), jsxs("div", { className: "flex flex-1 min-h-0", children: [jsx("div", { className: "flex-1 min-w-0", children: jsx(WorkbenchCanvas, { ref: canvasRef, showValues: showValues, toString: toString, toElement: toElement }) }), jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, toElement: toElement })] })] }));
2534
+ }, disabled: !engine, children: "Start" })), jsx("button", { className: "border border-gray-300 rounded px-2 py-1.5", onClick: runAutoLayout, children: "Auto Layout" }), 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" }), jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: downloadGraph, children: "Download Graph" }), jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsx("span", { children: "Debug events" })] }), jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsx("span", { children: "Show values in nodes" })] })] }), jsxs("div", { className: "flex flex-1 min-h-0", children: [jsx("div", { className: "flex-1 min-w-0", children: jsx(WorkbenchCanvas, { ref: canvasRef, showValues: showValues, toString: toString, toElement: toElement }) }), jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, toElement: toElement, contextPanel: overrides?.contextPanel })] })] }));
2462
2535
  }
2463
- function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, overrides, onInit, onChange, }) {
2536
+ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, overrides, onInit, onChange, contextPanel, }) {
2464
2537
  const [registry, setRegistry] = useState(createSimpleGraphRegistry());
2465
2538
  const [wb] = useState(() => new InMemoryWorkbench({ ui: new DefaultUIExtensionRegistry() }));
2466
2539
  const runner = useMemo(() => {
2467
- const backend = backendKind === "remote-http"
2468
- ? { kind: "remote-http", baseUrl: httpBaseUrl }
2469
- : backendKind === "remote-ws"
2470
- ? { kind: "remote-ws", url: wsUrl }
2471
- : { kind: "local" };
2472
- return new GraphRunner(registry, backend);
2540
+ if (backendKind === "remote-http") {
2541
+ return new RemoteGraphRunner(registry, {
2542
+ kind: "remote-http",
2543
+ baseUrl: httpBaseUrl,
2544
+ });
2545
+ }
2546
+ if (backendKind === "remote-ws") {
2547
+ return new RemoteGraphRunner(registry, { kind: "remote-ws", url: wsUrl });
2548
+ }
2549
+ return new LocalGraphRunner(registry);
2473
2550
  }, [registry, backendKind, httpBaseUrl, wsUrl]);
2474
2551
  // Allow external UI registration (e.g., node renderers) with access to wb
2475
2552
  useEffect(() => {
@@ -2484,5 +2561,5 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
2484
2561
  }, httpBaseUrl: httpBaseUrl, onHttpBaseUrlChange: onHttpBaseUrlChange, wsUrl: wsUrl, onWsUrlChange: onWsUrlChange, debug: debug, onDebugChange: onDebugChange, showValues: showValues, onShowValuesChange: onShowValuesChange, hideWorkbench: hideWorkbench, onHideWorkbenchChange: onHideWorkbenchChange, overrides: overrides, onInit: onInit, onChange: onChange }) }));
2485
2562
  }
2486
2563
 
2487
- export { AbstractWorkbench, CLIWorkbench, DefaultUIExtensionRegistry, GraphRunner, InMemoryWorkbench, Inspector, WorkbenchCanvas, WorkbenchContext, WorkbenchProvider, WorkbenchStudio, formatDataUrlAsLabel, formatDeclaredTypeSignature, getNodeBorderClassNames, preformatValueForDisplay, resolveOutputDisplay, summarizeDeep, toReactFlow, useQueryParamBoolean, useQueryParamString, useWorkbenchBridge, useWorkbenchContext, useWorkbenchGraphTick, useWorkbenchGraphUiTick, useWorkbenchVersionTick };
2564
+ export { AbstractWorkbench, CLIWorkbench, DefaultUIExtensionRegistry, InMemoryWorkbench, Inspector, LocalGraphRunner, RemoteGraphRunner, WorkbenchCanvas, WorkbenchContext, WorkbenchProvider, WorkbenchStudio, formatDataUrlAsLabel, formatDeclaredTypeSignature, getNodeBorderClassNames, preformatValueForDisplay, resolveOutputDisplay, summarizeDeep, toReactFlow, useQueryParamBoolean, useQueryParamString, useWorkbenchBridge, useWorkbenchContext, useWorkbenchGraphTick, useWorkbenchGraphUiTick, useWorkbenchVersionTick };
2488
2565
  //# sourceMappingURL=index.js.map