@bian-womp/spark-workbench 0.2.46 → 0.2.48

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 (29) hide show
  1. package/lib/cjs/index.cjs +153 -86
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/misc/WorkbenchStudio.d.ts.map +1 -1
  4. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +2 -2
  5. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  6. package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  7. package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts +6 -3
  8. package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
  9. package/lib/cjs/src/runtime/IGraphRunner.d.ts +4 -14
  10. package/lib/cjs/src/runtime/IGraphRunner.d.ts.map +1 -1
  11. package/lib/cjs/src/runtime/LocalGraphRunner.d.ts +3 -3
  12. package/lib/cjs/src/runtime/LocalGraphRunner.d.ts.map +1 -1
  13. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts +5 -3
  14. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
  15. package/lib/esm/index.js +154 -87
  16. package/lib/esm/index.js.map +1 -1
  17. package/lib/esm/src/misc/WorkbenchStudio.d.ts.map +1 -1
  18. package/lib/esm/src/misc/context/WorkbenchContext.d.ts +2 -2
  19. package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  20. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  21. package/lib/esm/src/runtime/AbstractGraphRunner.d.ts +6 -3
  22. package/lib/esm/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
  23. package/lib/esm/src/runtime/IGraphRunner.d.ts +4 -14
  24. package/lib/esm/src/runtime/IGraphRunner.d.ts.map +1 -1
  25. package/lib/esm/src/runtime/LocalGraphRunner.d.ts +3 -3
  26. package/lib/esm/src/runtime/LocalGraphRunner.d.ts.map +1 -1
  27. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts +5 -3
  28. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
  29. package/package.json +4 -4
package/lib/cjs/index.cjs CHANGED
@@ -336,8 +336,9 @@ class AbstractGraphRunner {
336
336
  this.stagedInputs = {};
337
337
  }
338
338
  launch(def, opts) {
339
+ // Auto-stop if engine is already running
339
340
  if (this.engine) {
340
- throw new Error("Engine already running. Stop the current engine first.");
341
+ this.stop();
341
342
  }
342
343
  }
343
344
  setInput(nodeId, handle, value) {
@@ -389,6 +390,39 @@ class AbstractGraphRunner {
389
390
  async whenIdle() {
390
391
  await this.engine?.whenIdle();
391
392
  }
393
+ stop() {
394
+ if (!this.engine)
395
+ return;
396
+ // Dispose engine (cleans up timers, listeners, etc.)
397
+ this.engine.dispose();
398
+ this.engine = undefined;
399
+ // Emit status but keep runtime alive
400
+ if (this.runningKind) {
401
+ this.runningKind = undefined;
402
+ this.emit("status", { running: false, engine: undefined });
403
+ }
404
+ }
405
+ async switchEngine(opts) {
406
+ if (!this.engine || !this.runtime) {
407
+ throw new Error("No engine running to switch from");
408
+ }
409
+ // Wait for current engine to be idle
410
+ await this.whenIdle();
411
+ // Capture current state
412
+ const currentInputs = { ...this.stagedInputs };
413
+ // Stop current engine
414
+ this.stop();
415
+ // Ensure runtime is in a clean state (resumed)
416
+ this.runtime.resume();
417
+ // Create and launch new engine (to be implemented by subclasses)
418
+ await this.createAndLaunchEngine(opts);
419
+ // Re-apply staged inputs to new engine
420
+ for (const [nodeId, map] of Object.entries(currentInputs)) {
421
+ if (this.engine) {
422
+ this.engine.setInputs(nodeId, map);
423
+ }
424
+ }
425
+ }
392
426
  on(event, handler) {
393
427
  if (!this.listeners.has(event))
394
428
  this.listeners.set(event, new Set());
@@ -460,37 +494,30 @@ class LocalGraphRunner extends AbstractGraphRunner {
460
494
  this.build(def);
461
495
  if (!this.runtime)
462
496
  throw new Error("Runtime not built");
463
- const rt = this.runtime;
464
- switch (opts.engine) {
465
- case "push":
466
- this.engine = new sparkGraph.PushEngine(rt);
467
- break;
468
- case "batched":
469
- this.engine = new sparkGraph.BatchedEngine(rt, {
470
- flushIntervalMs: opts.batched?.flushIntervalMs ?? 0,
471
- });
472
- break;
473
- case "pull":
474
- this.engine = new sparkGraph.PullEngine(rt);
475
- break;
476
- case "hybrid":
477
- this.engine = new sparkGraph.HybridEngine(rt, {
478
- windowMs: opts.hybrid?.windowMs ?? 250,
479
- batchThreshold: opts.hybrid?.batchThreshold ?? 3,
480
- });
481
- break;
482
- case "step":
483
- this.engine = new sparkGraph.StepEngine(rt);
484
- break;
485
- default:
486
- throw new Error("Unknown engine kind");
487
- }
497
+ // Use the async method to create engine
498
+ this.createAndLaunchEngine(opts).catch((err) => {
499
+ console.error("Failed to launch engine:", err);
500
+ const errorMessage = err instanceof Error ? err.message : String(err);
501
+ this.emit("error", {
502
+ kind: "system",
503
+ message: errorMessage,
504
+ err: err instanceof Error ? err : new Error(errorMessage),
505
+ });
506
+ });
507
+ }
508
+ async createAndLaunchEngine(opts) {
509
+ if (!this.runtime)
510
+ throw new Error("Runtime not built");
511
+ // Use shared engine factory
512
+ this.engine = sparkGraph.createEngine(this.runtime, opts);
513
+ if (!this.engine)
514
+ throw new Error("Failed to create engine");
488
515
  this.engine.on("value", (e) => this.emit("value", e));
489
516
  this.engine.on("error", (e) => this.emit("error", e));
490
517
  this.engine.on("invalidate", (e) => this.emit("invalidate", e));
491
518
  this.engine.on("stats", (e) => this.emit("stats", e));
492
- this.engine.launch(opts.invalidate);
493
- this.runningKind = opts.engine;
519
+ this.engine.launch(opts?.invalidate);
520
+ this.runningKind = opts?.engine ?? "push";
494
521
  this.emit("status", { running: true, engine: this.runningKind });
495
522
  for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
496
523
  this.engine.setInputs(nodeId, map);
@@ -498,18 +525,18 @@ class LocalGraphRunner extends AbstractGraphRunner {
498
525
  }
499
526
  async step() {
500
527
  const eng = this.engine;
501
- if (eng instanceof sparkGraph.StepEngine)
528
+ if (eng && eng instanceof sparkGraph.StepEngine)
502
529
  await eng.step();
503
530
  }
504
531
  async computeNode(nodeId) {
505
532
  const eng = this.engine;
506
- if (eng instanceof sparkGraph.PullEngine)
533
+ if (eng && eng instanceof sparkGraph.PullEngine)
507
534
  await eng.computeNode(nodeId);
508
535
  }
509
- flush() {
536
+ async flush() {
510
537
  const eng = this.engine;
511
- if (eng instanceof sparkGraph.BatchedEngine)
512
- eng.flush();
538
+ if (eng && eng instanceof sparkGraph.BatchedEngine)
539
+ await eng.flush();
513
540
  }
514
541
  getOutputs(def) {
515
542
  const out = {};
@@ -905,30 +932,36 @@ class RemoteGraphRunner extends AbstractGraphRunner {
905
932
  catch {
906
933
  console.error("Failed to hydrate remote inputs/outputs");
907
934
  }
908
- const eng = client.getEngine();
909
- if (!this.listenersBound) {
910
- eng.on("value", (e) => {
911
- this.valueCache.set(`${e.nodeId}.${e.handle}`, {
912
- io: e.io,
913
- value: e.value,
914
- runtimeTypeId: e.runtimeTypeId,
915
- });
916
- this.emit("value", e);
917
- });
918
- eng.on("error", (e) => this.emit("error", e));
919
- eng.on("invalidate", (e) => this.emit("invalidate", e));
920
- eng.on("stats", (e) => this.emit("stats", e));
921
- this.listenersBound = true;
922
- }
923
- this.engine = eng;
924
- this.engine.launch(opts.invalidate);
925
- this.runningKind = "push";
926
- this.emit("status", { running: true, engine: this.runningKind });
927
- for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
928
- this.engine.setInputs(nodeId, map);
929
- }
935
+ await this.createAndLaunchEngine(opts);
930
936
  });
931
937
  }
938
+ async createAndLaunchEngine(opts) {
939
+ const client = await this.ensureClient();
940
+ // Configure and launch engine on the backend
941
+ await client.launch(opts);
942
+ // Get the remote engine proxy and wire up event listeners
943
+ const eng = client.getEngine();
944
+ if (!this.listenersBound) {
945
+ eng.on("value", (e) => {
946
+ this.valueCache.set(`${e.nodeId}.${e.handle}`, {
947
+ io: e.io,
948
+ value: e.value,
949
+ runtimeTypeId: e.runtimeTypeId,
950
+ });
951
+ this.emit("value", e);
952
+ });
953
+ eng.on("error", (e) => this.emit("error", e));
954
+ eng.on("invalidate", (e) => this.emit("invalidate", e));
955
+ eng.on("stats", (e) => this.emit("stats", e));
956
+ this.listenersBound = true;
957
+ }
958
+ this.engine = eng;
959
+ this.runningKind = opts?.engine ?? "push";
960
+ this.emit("status", { running: true, engine: this.runningKind });
961
+ for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
962
+ this.engine.setInputs(nodeId, map);
963
+ }
964
+ }
932
965
  /**
933
966
  * Launch using an existing backend runtime that has already been built and hydrated.
934
967
  * This is used when resuming from a snapshot where the backend has already applied
@@ -944,34 +977,44 @@ class RemoteGraphRunner extends AbstractGraphRunner {
944
977
  // has already been built and hydrated via ApplySnapshotFull.
945
978
  // Calling build() would create a new runtime and lose the restored state.
946
979
  this.lastDef = def;
947
- // Attach to the existing engine
948
- const eng = client.getEngine();
949
- if (!this.listenersBound) {
950
- eng.on("value", (e) => {
951
- this.valueCache.set(`${e.nodeId}.${e.handle}`, {
952
- io: e.io,
953
- value: e.value,
954
- runtimeTypeId: e.runtimeTypeId,
955
- });
956
- this.emit("value", e);
957
- });
958
- eng.on("error", (e) => this.emit("error", e));
959
- eng.on("invalidate", (e) => this.emit("invalidate", e));
960
- eng.on("stats", (e) => this.emit("stats", e));
961
- this.listenersBound = true;
962
- }
963
- this.engine = eng;
964
- this.engine.launch(opts.invalidate);
965
- this.runningKind = "push";
966
- this.emit("status", { running: true, engine: this.runningKind });
967
- for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
968
- this.engine.setInputs(nodeId, map);
969
- }
980
+ await this.createAndLaunchEngine(opts);
970
981
  });
971
982
  }
972
- async step() { }
973
- async computeNode(nodeId) { }
974
- flush() { }
983
+ async switchEngine(opts) {
984
+ if (!this.engine) {
985
+ throw new Error("No engine running to switch from");
986
+ }
987
+ // Wait for current engine to be idle
988
+ await this.whenIdle();
989
+ // Capture current state
990
+ const currentInputs = { ...this.stagedInputs };
991
+ // Stop current engine
992
+ this.stop();
993
+ // Create and launch new engine with the specified kind
994
+ const client = await this.ensureClient();
995
+ await client.launch(opts);
996
+ // Get the remote engine proxy
997
+ const eng = client.getEngine();
998
+ this.engine = eng;
999
+ this.runningKind = opts?.engine ?? "push";
1000
+ this.emit("status", { running: true, engine: this.runningKind });
1001
+ // Re-apply staged inputs to new engine
1002
+ for (const [nodeId, map] of Object.entries(currentInputs)) {
1003
+ this.engine.setInputs(nodeId, map);
1004
+ }
1005
+ }
1006
+ async step() {
1007
+ const client = await this.ensureClient();
1008
+ await client.step();
1009
+ }
1010
+ async computeNode(nodeId) {
1011
+ const client = await this.ensureClient();
1012
+ await client.computeNode(nodeId);
1013
+ }
1014
+ async flush() {
1015
+ const client = await this.ensureClient();
1016
+ await client.flush();
1017
+ }
975
1018
  triggerExternal(nodeId, event) {
976
1019
  this.ensureClient().then(async (client) => {
977
1020
  try {
@@ -2327,7 +2370,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2327
2370
  offRunnerRegistry();
2328
2371
  offRunnerTransport();
2329
2372
  };
2330
- }, [runner, wb]);
2373
+ }, [runner, wb, setRegistry]);
2331
2374
  // Push incremental updates into running engine without full reload
2332
2375
  React.useEffect(() => {
2333
2376
  if (runner.isRunning()) {
@@ -2408,7 +2451,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2408
2451
  }
2409
2452
  catch { }
2410
2453
  }, [runner, wb]);
2411
- const stop = React.useCallback(() => runner.dispose(), [runner]);
2454
+ const stop = React.useCallback(() => runner.stop(), [runner]);
2412
2455
  const step = React.useCallback(() => runner.step(), [runner]);
2413
2456
  const flush = React.useCallback(() => runner.flush(), [runner]);
2414
2457
  const value = React.useMemo(() => ({
@@ -3994,10 +4037,34 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3994
4037
  ? "Stop engine before switching example"
3995
4038
  : undefined, children: [jsxRuntime.jsx("option", { value: "", children: "Select Example\u2026" }), examples.map((ex) => (jsxRuntime.jsx("option", { value: ex.id, children: ex.label }, ex.id)))] }), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: backendKind, onChange: (e) => onBackendKindChange(e.target.value), disabled: runner.isRunning(), title: runner.isRunning()
3996
4039
  ? "Stop engine before switching backend"
3997
- : undefined, children: [jsxRuntime.jsx("option", { value: "local", children: "Local" }), jsxRuntime.jsx("option", { value: "remote-http", children: "Remote (HTTP)" }), jsxRuntime.jsx("option", { value: "remote-ws", children: "Remote (WebSocket)" })] }), backendKind === "remote-http" && !!onHttpBaseUrlChange && (jsxRuntime.jsx("input", { className: "ml-2 border border-gray-300 rounded px-2 py-1 w-72", placeholder: "http://127.0.0.1:18080", value: httpBaseUrl, onChange: (e) => onHttpBaseUrlChange(e.target.value) })), backendKind === "remote-ws" && !!onWsUrlChange && (jsxRuntime.jsx("input", { className: "ml-2 border border-gray-300 rounded px-2 py-1 w-72", placeholder: "ws://127.0.0.1:18081", value: wsUrl, onChange: (e) => onWsUrlChange(e.target.value) })), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: runner.getRunningEngine() ?? engine ?? "", onChange: (e) => {
4040
+ : undefined, children: [jsxRuntime.jsx("option", { value: "local", children: "Local" }), jsxRuntime.jsx("option", { value: "remote-http", children: "Remote (HTTP)" }), jsxRuntime.jsx("option", { value: "remote-ws", children: "Remote (WebSocket)" })] }), backendKind === "remote-http" && !!onHttpBaseUrlChange && (jsxRuntime.jsx("input", { className: "ml-2 border border-gray-300 rounded px-2 py-1 w-72", placeholder: "http://127.0.0.1:18080", value: httpBaseUrl, onChange: (e) => onHttpBaseUrlChange(e.target.value) })), backendKind === "remote-ws" && !!onWsUrlChange && (jsxRuntime.jsx("input", { className: "ml-2 border border-gray-300 rounded px-2 py-1 w-72", placeholder: "ws://127.0.0.1:18081", value: wsUrl, onChange: (e) => onWsUrlChange(e.target.value) })), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: runner.getRunningEngine() ?? engine ?? "", onChange: async (e) => {
3998
4041
  const kind = e.target.value || undefined;
3999
- onEngineChange?.(kind);
4000
- }, children: [jsxRuntime.jsx("option", { value: "", children: "Select Engine\u2026" }), jsxRuntime.jsx("option", { value: "push", children: "Push" }), jsxRuntime.jsx("option", { value: "batched", children: "Batched" }), jsxRuntime.jsx("option", { value: "pull", children: "Pull" }), jsxRuntime.jsx("option", { value: "hybrid", children: "Hybrid" }), jsxRuntime.jsx("option", { value: "step", children: "Step" })] }), runner.getRunningEngine() === "step" && (jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded p-1", onClick: () => runner.step(), disabled: !runner.isRunning(), title: "Step", children: jsxRuntime.jsx(react$1.PlayPauseIcon, { size: 24 }) })), runner.getRunningEngine() === "batched" && (jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded p-1", onClick: () => runner.flush(), disabled: !runner.isRunning(), title: "Flush", children: jsxRuntime.jsx(react$1.LightningIcon, { size: 24 }) })), runner.isRunning() ? (jsxRuntime.jsxs("button", { className: "border rounded px-2 py-1.5 text-red-700 border-red-600 flex items-center gap-1", onClick: () => runner.dispose(), title: "Stop engine", children: [jsxRuntime.jsx(react$1.StopIcon, { size: 16, weight: "fill" }), jsxRuntime.jsx("span", { className: "font-medium ml-1", children: "Stop" })] })) : (jsxRuntime.jsxs("button", { className: "border rounded px-2 py-1.5 text-green-700 border-green-600 flex items-center gap-1 disabled:text-gray-400 disabled:border-gray-300", onClick: (evt) => {
4042
+ const currentEngine = runner.getRunningEngine();
4043
+ // If engine is running and user selected a different engine, switch it
4044
+ if (runner.isRunning() &&
4045
+ currentEngine &&
4046
+ kind &&
4047
+ kind !== currentEngine) {
4048
+ try {
4049
+ await runner.switchEngine({
4050
+ engine: kind,
4051
+ batched: { flushIntervalMs: 0 },
4052
+ hybrid: { windowMs: 250, batchThreshold: 3 },
4053
+ });
4054
+ onEngineChange?.(kind);
4055
+ }
4056
+ catch (err) {
4057
+ const message = err instanceof Error ? err.message : String(err);
4058
+ alert(`Failed to switch engine: ${message}`);
4059
+ // Reset dropdown to current engine
4060
+ e.target.value = currentEngine;
4061
+ }
4062
+ }
4063
+ else {
4064
+ // Normal change when not running
4065
+ onEngineChange?.(kind);
4066
+ }
4067
+ }, children: [jsxRuntime.jsx("option", { value: "", children: "Select Engine\u2026" }), jsxRuntime.jsx("option", { value: "push", children: "Push" }), jsxRuntime.jsx("option", { value: "batched", children: "Batched" }), jsxRuntime.jsx("option", { value: "pull", children: "Pull" }), jsxRuntime.jsx("option", { value: "hybrid", children: "Hybrid" }), jsxRuntime.jsx("option", { value: "step", children: "Step" })] }), runner.getRunningEngine() === "step" && (jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded p-1", onClick: () => runner.step(), disabled: !runner.isRunning(), title: "Step", children: jsxRuntime.jsx(react$1.PlayPauseIcon, { size: 24 }) })), runner.getRunningEngine() === "batched" && (jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded p-1", onClick: () => runner.flush(), disabled: !runner.isRunning(), title: "Flush", children: jsxRuntime.jsx(react$1.LightningIcon, { size: 24 }) })), runner.isRunning() ? (jsxRuntime.jsxs("button", { className: "border rounded px-2 py-1.5 text-red-700 border-red-600 flex items-center gap-1", onClick: () => runner.stop(), title: "Stop engine", children: [jsxRuntime.jsx(react$1.StopIcon, { size: 16, weight: "fill" }), jsxRuntime.jsx("span", { className: "font-medium ml-1", children: "Stop" })] })) : (jsxRuntime.jsxs("button", { className: "border rounded px-2 py-1.5 text-green-700 border-green-600 flex items-center gap-1 disabled:text-gray-400 disabled:border-gray-300", onClick: (evt) => {
4001
4068
  const kind = engine;
4002
4069
  if (!kind)
4003
4070
  return alert("Select an engine first.");