@bian-womp/spark-workbench 0.2.47 → 0.2.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cjs/index.cjs +308 -161
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/misc/Inspector.d.ts.map +1 -1
- package/lib/cjs/src/misc/WorkbenchStudio.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +2 -2
- package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
- package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts +7 -3
- package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
- package/lib/cjs/src/runtime/IGraphRunner.d.ts +6 -14
- package/lib/cjs/src/runtime/IGraphRunner.d.ts.map +1 -1
- package/lib/cjs/src/runtime/LocalGraphRunner.d.ts +4 -3
- package/lib/cjs/src/runtime/LocalGraphRunner.d.ts.map +1 -1
- package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts +12 -3
- package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
- package/lib/esm/index.js +310 -163
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/misc/Inspector.d.ts.map +1 -1
- package/lib/esm/src/misc/WorkbenchStudio.d.ts.map +1 -1
- package/lib/esm/src/misc/context/WorkbenchContext.d.ts +2 -2
- package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -1
- package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
- package/lib/esm/src/runtime/AbstractGraphRunner.d.ts +7 -3
- package/lib/esm/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
- package/lib/esm/src/runtime/IGraphRunner.d.ts +6 -14
- package/lib/esm/src/runtime/IGraphRunner.d.ts.map +1 -1
- package/lib/esm/src/runtime/LocalGraphRunner.d.ts +4 -3
- package/lib/esm/src/runtime/LocalGraphRunner.d.ts.map +1 -1
- package/lib/esm/src/runtime/RemoteGraphRunner.d.ts +12 -3
- package/lib/esm/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
- package/package.json +4 -4
package/lib/cjs/index.cjs
CHANGED
|
@@ -334,10 +334,12 @@ class AbstractGraphRunner {
|
|
|
334
334
|
this.backend = backend;
|
|
335
335
|
this.listeners = new Map();
|
|
336
336
|
this.stagedInputs = {};
|
|
337
|
+
this.runnerId = "";
|
|
337
338
|
}
|
|
338
339
|
launch(def, opts) {
|
|
340
|
+
// Auto-stop if engine is already running
|
|
339
341
|
if (this.engine) {
|
|
340
|
-
|
|
342
|
+
this.stop();
|
|
341
343
|
}
|
|
342
344
|
}
|
|
343
345
|
setInput(nodeId, handle, value) {
|
|
@@ -389,6 +391,39 @@ class AbstractGraphRunner {
|
|
|
389
391
|
async whenIdle() {
|
|
390
392
|
await this.engine?.whenIdle();
|
|
391
393
|
}
|
|
394
|
+
stop() {
|
|
395
|
+
if (!this.engine)
|
|
396
|
+
return;
|
|
397
|
+
// Dispose engine (cleans up timers, listeners, etc.)
|
|
398
|
+
this.engine.dispose();
|
|
399
|
+
this.engine = undefined;
|
|
400
|
+
// Emit status but keep runtime alive
|
|
401
|
+
if (this.runningKind) {
|
|
402
|
+
this.runningKind = undefined;
|
|
403
|
+
this.emit("status", { running: false, engine: undefined });
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async switchEngine(opts) {
|
|
407
|
+
if (!this.engine || !this.runtime) {
|
|
408
|
+
throw new Error("No engine running to switch from");
|
|
409
|
+
}
|
|
410
|
+
// Wait for current engine to be idle
|
|
411
|
+
await this.whenIdle();
|
|
412
|
+
// Capture current state
|
|
413
|
+
const currentInputs = { ...this.stagedInputs };
|
|
414
|
+
// Stop current engine
|
|
415
|
+
this.stop();
|
|
416
|
+
// Ensure runtime is in a clean state (resumed)
|
|
417
|
+
this.runtime.resume();
|
|
418
|
+
// Create and launch new engine (to be implemented by subclasses)
|
|
419
|
+
await this.createAndLaunchEngine(opts);
|
|
420
|
+
// Re-apply staged inputs to new engine
|
|
421
|
+
for (const [nodeId, map] of Object.entries(currentInputs)) {
|
|
422
|
+
if (this.engine) {
|
|
423
|
+
this.engine.setInputs(nodeId, map);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
392
427
|
on(event, handler) {
|
|
393
428
|
if (!this.listeners.has(event))
|
|
394
429
|
this.listeners.set(event, new Set());
|
|
@@ -420,6 +455,8 @@ class AbstractGraphRunner {
|
|
|
420
455
|
}
|
|
421
456
|
}
|
|
422
457
|
|
|
458
|
+
// Counter for generating readable runner IDs
|
|
459
|
+
let localRunnerCounter = 0;
|
|
423
460
|
class LocalGraphRunner extends AbstractGraphRunner {
|
|
424
461
|
constructor(registry) {
|
|
425
462
|
super(registry, { kind: "local" });
|
|
@@ -438,7 +475,11 @@ class LocalGraphRunner extends AbstractGraphRunner {
|
|
|
438
475
|
this.getEnvironment = () => {
|
|
439
476
|
return this.runtime?.getEnvironment?.();
|
|
440
477
|
};
|
|
441
|
-
this
|
|
478
|
+
// Generate readable ID for this runner instance (e.g., local-001, local-002)
|
|
479
|
+
localRunnerCounter++;
|
|
480
|
+
this.runnerId = `local-${String(localRunnerCounter).padStart(3, "0")}`;
|
|
481
|
+
console.info(`[LocalGraphRunner] Created runner with ID: ${this.runnerId}`);
|
|
482
|
+
this.emit("transport", { runnerId: this.runnerId, state: "local" });
|
|
442
483
|
}
|
|
443
484
|
build(def) {
|
|
444
485
|
const builder = new sparkGraph.GraphBuilder(this.registry);
|
|
@@ -460,37 +501,30 @@ class LocalGraphRunner extends AbstractGraphRunner {
|
|
|
460
501
|
this.build(def);
|
|
461
502
|
if (!this.runtime)
|
|
462
503
|
throw new Error("Runtime not built");
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
}
|
|
504
|
+
// Use the async method to create engine
|
|
505
|
+
this.createAndLaunchEngine(opts).catch((err) => {
|
|
506
|
+
console.error("Failed to launch engine:", err);
|
|
507
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
508
|
+
this.emit("error", {
|
|
509
|
+
kind: "system",
|
|
510
|
+
message: errorMessage,
|
|
511
|
+
err: err instanceof Error ? err : new Error(errorMessage),
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
async createAndLaunchEngine(opts) {
|
|
516
|
+
if (!this.runtime)
|
|
517
|
+
throw new Error("Runtime not built");
|
|
518
|
+
// Use shared engine factory
|
|
519
|
+
this.engine = sparkGraph.createEngine(this.runtime, opts);
|
|
520
|
+
if (!this.engine)
|
|
521
|
+
throw new Error("Failed to create engine");
|
|
488
522
|
this.engine.on("value", (e) => this.emit("value", e));
|
|
489
523
|
this.engine.on("error", (e) => this.emit("error", e));
|
|
490
524
|
this.engine.on("invalidate", (e) => this.emit("invalidate", e));
|
|
491
525
|
this.engine.on("stats", (e) => this.emit("stats", e));
|
|
492
|
-
this.engine.launch(opts
|
|
493
|
-
this.runningKind = opts
|
|
526
|
+
this.engine.launch(opts?.invalidate);
|
|
527
|
+
this.runningKind = opts?.engine ?? "push";
|
|
494
528
|
this.emit("status", { running: true, engine: this.runningKind });
|
|
495
529
|
for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
|
|
496
530
|
this.engine.setInputs(nodeId, map);
|
|
@@ -498,18 +532,18 @@ class LocalGraphRunner extends AbstractGraphRunner {
|
|
|
498
532
|
}
|
|
499
533
|
async step() {
|
|
500
534
|
const eng = this.engine;
|
|
501
|
-
if (eng instanceof sparkGraph.StepEngine)
|
|
535
|
+
if (eng && eng instanceof sparkGraph.StepEngine)
|
|
502
536
|
await eng.step();
|
|
503
537
|
}
|
|
504
538
|
async computeNode(nodeId) {
|
|
505
539
|
const eng = this.engine;
|
|
506
|
-
if (eng instanceof sparkGraph.PullEngine)
|
|
540
|
+
if (eng && eng instanceof sparkGraph.PullEngine)
|
|
507
541
|
await eng.computeNode(nodeId);
|
|
508
542
|
}
|
|
509
|
-
flush() {
|
|
543
|
+
async flush() {
|
|
510
544
|
const eng = this.engine;
|
|
511
|
-
if (eng instanceof sparkGraph.BatchedEngine)
|
|
512
|
-
eng.flush();
|
|
545
|
+
if (eng && eng instanceof sparkGraph.BatchedEngine)
|
|
546
|
+
await eng.flush();
|
|
513
547
|
}
|
|
514
548
|
getOutputs(def) {
|
|
515
549
|
const out = {};
|
|
@@ -597,10 +631,12 @@ class LocalGraphRunner extends AbstractGraphRunner {
|
|
|
597
631
|
dispose() {
|
|
598
632
|
super.dispose();
|
|
599
633
|
this.runtime = undefined;
|
|
600
|
-
this.emit("transport", { state: "local" });
|
|
634
|
+
this.emit("transport", { runnerId: this.runnerId, state: "local" });
|
|
601
635
|
}
|
|
602
636
|
}
|
|
603
637
|
|
|
638
|
+
// Counter for generating readable runner IDs
|
|
639
|
+
let remoteRunnerCounter = 0;
|
|
604
640
|
class RemoteGraphRunner extends AbstractGraphRunner {
|
|
605
641
|
/**
|
|
606
642
|
* Fetch full registry description from remote and register it locally.
|
|
@@ -739,13 +775,20 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
739
775
|
setupClientSubscriptions(client) {
|
|
740
776
|
// Subscribe to transport status changes
|
|
741
777
|
// Convert RuntimeApiClient.TransportStatus to IGraphRunner.TransportStatus
|
|
778
|
+
// Only emit status if it matches this runner's ID
|
|
742
779
|
this.transportStatusUnsubscribe = client.onTransportStatus((status) => {
|
|
780
|
+
if (status.runnerId && status.runnerId !== this.runnerId)
|
|
781
|
+
return;
|
|
743
782
|
// Map remote-unix to undefined since RemoteGraphRunner doesn't support it
|
|
744
783
|
const mappedKind = status.kind === "remote-unix" ? undefined : status.kind;
|
|
745
|
-
|
|
784
|
+
const transportStatus = {
|
|
746
785
|
state: status.state,
|
|
747
786
|
kind: mappedKind,
|
|
748
|
-
|
|
787
|
+
runnerId: this.runnerId,
|
|
788
|
+
};
|
|
789
|
+
// Track current status
|
|
790
|
+
this.currentTransportStatus = transportStatus;
|
|
791
|
+
this.emit("transport", transportStatus);
|
|
749
792
|
});
|
|
750
793
|
}
|
|
751
794
|
// Ensure remote client
|
|
@@ -766,6 +809,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
766
809
|
// Create client with custom event handler if provided
|
|
767
810
|
const client = new sparkRemote.RuntimeApiClient(clientConfig, {
|
|
768
811
|
onCustomEvent: backend.onCustomEvent,
|
|
812
|
+
runnerId: this.runnerId,
|
|
769
813
|
});
|
|
770
814
|
// Setup event subscriptions
|
|
771
815
|
this.setupClientSubscriptions(client);
|
|
@@ -798,6 +842,15 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
798
842
|
this.registryFetching = false;
|
|
799
843
|
this.MAX_REGISTRY_FETCH_ATTEMPTS = 3;
|
|
800
844
|
this.INITIAL_RETRY_DELAY_MS = 1000; // 1 second
|
|
845
|
+
// Generate readable ID for this runner instance (e.g., remote-001, remote-002)
|
|
846
|
+
remoteRunnerCounter++;
|
|
847
|
+
this.runnerId = `remote-${String(remoteRunnerCounter).padStart(3, "0")}`;
|
|
848
|
+
console.info(`[RemoteGraphRunner] Created runner with ID: ${this.runnerId}`);
|
|
849
|
+
// Initialize transport status as "connecting" - will be updated when connection completes
|
|
850
|
+
this.currentTransportStatus = {
|
|
851
|
+
state: "connecting",
|
|
852
|
+
kind: backend.kind,
|
|
853
|
+
};
|
|
801
854
|
// Auto-handle registry-changed invalidations from remote
|
|
802
855
|
// We listen on invalidate and if reason matches, we rehydrate registry and emit a registry event
|
|
803
856
|
this.ensureClient().then(async (client) => {
|
|
@@ -905,30 +958,36 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
905
958
|
catch {
|
|
906
959
|
console.error("Failed to hydrate remote inputs/outputs");
|
|
907
960
|
}
|
|
908
|
-
|
|
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
|
-
}
|
|
961
|
+
await this.createAndLaunchEngine(opts);
|
|
930
962
|
});
|
|
931
963
|
}
|
|
964
|
+
async createAndLaunchEngine(opts) {
|
|
965
|
+
const client = await this.ensureClient();
|
|
966
|
+
// Configure and launch engine on the backend
|
|
967
|
+
await client.launch(opts);
|
|
968
|
+
// Get the remote engine proxy and wire up event listeners
|
|
969
|
+
const eng = client.getEngine();
|
|
970
|
+
if (!this.listenersBound) {
|
|
971
|
+
eng.on("value", (e) => {
|
|
972
|
+
this.valueCache.set(`${e.nodeId}.${e.handle}`, {
|
|
973
|
+
io: e.io,
|
|
974
|
+
value: e.value,
|
|
975
|
+
runtimeTypeId: e.runtimeTypeId,
|
|
976
|
+
});
|
|
977
|
+
this.emit("value", e);
|
|
978
|
+
});
|
|
979
|
+
eng.on("error", (e) => this.emit("error", e));
|
|
980
|
+
eng.on("invalidate", (e) => this.emit("invalidate", e));
|
|
981
|
+
eng.on("stats", (e) => this.emit("stats", e));
|
|
982
|
+
this.listenersBound = true;
|
|
983
|
+
}
|
|
984
|
+
this.engine = eng;
|
|
985
|
+
this.runningKind = opts?.engine ?? "push";
|
|
986
|
+
this.emit("status", { running: true, engine: this.runningKind });
|
|
987
|
+
for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
|
|
988
|
+
this.engine.setInputs(nodeId, map);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
932
991
|
/**
|
|
933
992
|
* Launch using an existing backend runtime that has already been built and hydrated.
|
|
934
993
|
* This is used when resuming from a snapshot where the backend has already applied
|
|
@@ -944,34 +1003,47 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
944
1003
|
// has already been built and hydrated via ApplySnapshotFull.
|
|
945
1004
|
// Calling build() would create a new runtime and lose the restored state.
|
|
946
1005
|
this.lastDef = def;
|
|
947
|
-
|
|
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
|
-
}
|
|
1006
|
+
await this.createAndLaunchEngine(opts);
|
|
970
1007
|
});
|
|
971
1008
|
}
|
|
972
|
-
async
|
|
973
|
-
|
|
974
|
-
|
|
1009
|
+
async switchEngine(opts) {
|
|
1010
|
+
if (!this.engine) {
|
|
1011
|
+
throw new Error("No engine running to switch from");
|
|
1012
|
+
}
|
|
1013
|
+
// Wait for current engine to be idle
|
|
1014
|
+
await this.whenIdle();
|
|
1015
|
+
// Capture current state
|
|
1016
|
+
const currentInputs = { ...this.stagedInputs };
|
|
1017
|
+
// For remote runners, we cannot call this.stop() because it sends a Dispose
|
|
1018
|
+
// command that destroys the graphRuntime on the backend. Instead, we rely on
|
|
1019
|
+
// the backend's launch() method to dispose the old engine and create a new one.
|
|
1020
|
+
// Reconfigure engine on the backend (this will dispose old engine and create new one)
|
|
1021
|
+
const client = await this.ensureClient();
|
|
1022
|
+
await client.launch(opts);
|
|
1023
|
+
// Get the remote engine proxy (should be the same RemoteEngine instance)
|
|
1024
|
+
const eng = client.getEngine();
|
|
1025
|
+
// Update local state to reflect new engine kind
|
|
1026
|
+
// Note: The RemoteEngine instance itself doesn't change, but the backend engine does
|
|
1027
|
+
this.engine = eng;
|
|
1028
|
+
this.runningKind = opts?.engine ?? "push";
|
|
1029
|
+
this.emit("status", { running: true, engine: this.runningKind });
|
|
1030
|
+
// Re-apply staged inputs to new engine
|
|
1031
|
+
for (const [nodeId, map] of Object.entries(currentInputs)) {
|
|
1032
|
+
this.engine.setInputs(nodeId, map);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
async step() {
|
|
1036
|
+
const client = await this.ensureClient();
|
|
1037
|
+
await client.step();
|
|
1038
|
+
}
|
|
1039
|
+
async computeNode(nodeId) {
|
|
1040
|
+
const client = await this.ensureClient();
|
|
1041
|
+
await client.computeNode(nodeId);
|
|
1042
|
+
}
|
|
1043
|
+
async flush() {
|
|
1044
|
+
const client = await this.ensureClient();
|
|
1045
|
+
await client.flush();
|
|
1046
|
+
}
|
|
975
1047
|
triggerExternal(nodeId, event) {
|
|
976
1048
|
this.ensureClient().then(async (client) => {
|
|
977
1049
|
try {
|
|
@@ -1119,6 +1191,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
1119
1191
|
if (this.disposed)
|
|
1120
1192
|
return;
|
|
1121
1193
|
this.disposed = true;
|
|
1194
|
+
console.info(`[RemoteGraphRunner] Disposing runner with ID: ${this.runnerId}`);
|
|
1122
1195
|
super.dispose();
|
|
1123
1196
|
// Clear client promise if any
|
|
1124
1197
|
this.clientPromise = undefined;
|
|
@@ -1138,10 +1211,30 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
1138
1211
|
console.warn("[RemoteGraphRunner] Error disposing client:", err);
|
|
1139
1212
|
});
|
|
1140
1213
|
}
|
|
1141
|
-
|
|
1214
|
+
const disconnectedStatus = {
|
|
1142
1215
|
state: "disconnected",
|
|
1143
1216
|
kind: this.backend.kind,
|
|
1144
|
-
|
|
1217
|
+
runnerId: this.runnerId,
|
|
1218
|
+
};
|
|
1219
|
+
this.currentTransportStatus = disconnectedStatus;
|
|
1220
|
+
this.emit("transport", disconnectedStatus);
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Override on() to emit current transport status immediately when a new listener subscribes.
|
|
1224
|
+
* This ensures listeners don't miss the current status when they attach after connection.
|
|
1225
|
+
*/
|
|
1226
|
+
on(event, handler) {
|
|
1227
|
+
const unsubscribe = super.on(event, handler);
|
|
1228
|
+
// If subscribing to transport events and we have a current status, emit it immediately
|
|
1229
|
+
if (event === "transport" && this.currentTransportStatus) {
|
|
1230
|
+
// Use setTimeout to ensure this happens after the listener is registered
|
|
1231
|
+
// This prevents issues if the handler modifies state synchronously
|
|
1232
|
+
setTimeout(() => {
|
|
1233
|
+
// Type assertion is safe here because we checked event === "transport"
|
|
1234
|
+
handler(this.currentTransportStatus);
|
|
1235
|
+
}, 0);
|
|
1236
|
+
}
|
|
1237
|
+
return unsubscribe;
|
|
1145
1238
|
}
|
|
1146
1239
|
}
|
|
1147
1240
|
|
|
@@ -2327,16 +2420,28 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2327
2420
|
offRunnerRegistry();
|
|
2328
2421
|
offRunnerTransport();
|
|
2329
2422
|
};
|
|
2423
|
+
}, [runner, wb, setRegistry]);
|
|
2424
|
+
const isRunning = React.useCallback(() => runner.isRunning(), [runner]);
|
|
2425
|
+
const engineKind = React.useCallback(() => runner.getRunningEngine(), [runner]);
|
|
2426
|
+
const start = React.useCallback((engine) => {
|
|
2427
|
+
try {
|
|
2428
|
+
runner.launch(wb.export(), { engine });
|
|
2429
|
+
}
|
|
2430
|
+
catch { }
|
|
2330
2431
|
}, [runner, wb]);
|
|
2432
|
+
const stop = React.useCallback(() => runner.stop(), [runner]);
|
|
2433
|
+
const step = React.useCallback(() => runner.step(), [runner]);
|
|
2434
|
+
const flush = React.useCallback(() => runner.flush(), [runner]);
|
|
2331
2435
|
// Push incremental updates into running engine without full reload
|
|
2436
|
+
const isGraphRunning = isRunning();
|
|
2332
2437
|
React.useEffect(() => {
|
|
2333
|
-
if (
|
|
2438
|
+
if (isGraphRunning) {
|
|
2334
2439
|
try {
|
|
2335
2440
|
runner.update(def);
|
|
2336
2441
|
}
|
|
2337
2442
|
catch { }
|
|
2338
2443
|
}
|
|
2339
|
-
}, [runner, def, graphTick]);
|
|
2444
|
+
}, [runner, isGraphRunning, def, graphTick]);
|
|
2340
2445
|
const validationByNode = React.useMemo(() => {
|
|
2341
2446
|
const inputs = {};
|
|
2342
2447
|
const outputs = {};
|
|
@@ -2400,17 +2505,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2400
2505
|
}
|
|
2401
2506
|
return { errors, issues };
|
|
2402
2507
|
}, [validation]);
|
|
2403
|
-
const isRunning = React.useCallback(() => runner.isRunning(), [runner]);
|
|
2404
|
-
const engineKind = React.useCallback(() => runner.getRunningEngine(), [runner]);
|
|
2405
|
-
const start = React.useCallback((engine) => {
|
|
2406
|
-
try {
|
|
2407
|
-
runner.launch(wb.export(), { engine });
|
|
2408
|
-
}
|
|
2409
|
-
catch { }
|
|
2410
|
-
}, [runner, wb]);
|
|
2411
|
-
const stop = React.useCallback(() => runner.dispose(), [runner]);
|
|
2412
|
-
const step = React.useCallback(() => runner.step(), [runner]);
|
|
2413
|
-
const flush = React.useCallback(() => runner.flush(), [runner]);
|
|
2414
2508
|
const value = React.useMemo(() => ({
|
|
2415
2509
|
wb,
|
|
2416
2510
|
runner,
|
|
@@ -2616,6 +2710,42 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
2616
2710
|
outputs: nodeValidationHandles?.outputs?.[selectedNodeId] ?? [],
|
|
2617
2711
|
}
|
|
2618
2712
|
: { inputs: [], outputs: [] };
|
|
2713
|
+
// Render edge status indicator
|
|
2714
|
+
const renderEdgeStatus = React.useCallback(() => {
|
|
2715
|
+
if (!selectedEdge)
|
|
2716
|
+
return null;
|
|
2717
|
+
const status = edgeStatus?.[selectedEdge.id];
|
|
2718
|
+
if (status?.activeRuns > 0) {
|
|
2719
|
+
return (jsxRuntime.jsxs("div", { className: "mt-1 text-xs text-blue-700 bg-blue-50 border border-blue-200 rounded px-2 py-1", children: [jsxRuntime.jsxs("div", { className: "font-semibold", children: ["Running (", status.activeRuns, ")"] }), jsxRuntime.jsx("div", { className: "text-[10px] text-blue-600 mt-1", children: "Note: Edge runIds are not available in stats events" })] }));
|
|
2720
|
+
}
|
|
2721
|
+
return null;
|
|
2722
|
+
}, [selectedEdge, edgeStatus]);
|
|
2723
|
+
// Render linked input display value
|
|
2724
|
+
const renderLinkedInputDisplay = React.useCallback((typeId, current) => {
|
|
2725
|
+
const displayStr = safeToString(typeId, current);
|
|
2726
|
+
const isLong = displayStr.length > 50;
|
|
2727
|
+
const truncated = isLong ? truncateValue(displayStr) : displayStr;
|
|
2728
|
+
return (jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [jsxRuntime.jsx("span", { className: "truncate", children: truncated }), isLong && (jsxRuntime.jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded", onClick: () => copyToClipboard(displayStr), title: "Copy full value", children: jsxRuntime.jsx(react$1.CopyIcon, { size: 14 }) }))] }));
|
|
2729
|
+
}, [safeToString, truncateValue, copyToClipboard]);
|
|
2730
|
+
// Render output validation issues badge
|
|
2731
|
+
const renderOutputValidationBadge = React.useCallback((handle) => {
|
|
2732
|
+
const outIssues = selectedNodeHandleValidation.outputs.filter((m) => m.handle === handle);
|
|
2733
|
+
if (outIssues.length === 0)
|
|
2734
|
+
return null;
|
|
2735
|
+
const outErr = outIssues.some((m) => m.level === "error");
|
|
2736
|
+
const outTitle = outIssues
|
|
2737
|
+
.map((v) => `${v.code}: ${v.message}`)
|
|
2738
|
+
.join("; ");
|
|
2739
|
+
return (jsxRuntime.jsx(IssueBadge, { level: outErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: outTitle }));
|
|
2740
|
+
}, [selectedNodeHandleValidation]);
|
|
2741
|
+
// Render output display value
|
|
2742
|
+
const renderOutputDisplay = React.useCallback((outputValue, effectiveHandle) => {
|
|
2743
|
+
const { typeId, value } = resolveOutputDisplay(outputValue, effectiveHandle);
|
|
2744
|
+
const displayStr = safeToString(typeId, value);
|
|
2745
|
+
const isLong = displayStr.length > 50;
|
|
2746
|
+
const truncated = isLong ? truncateValue(displayStr) : displayStr;
|
|
2747
|
+
return (jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [jsxRuntime.jsx("span", { className: "truncate", children: truncated }), isLong && (jsxRuntime.jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded", onClick: () => copyToClipboard(displayStr), title: "Copy full value", children: jsxRuntime.jsx(react$1.CopyIcon, { size: 14 }) }))] }));
|
|
2748
|
+
}, [safeToString, truncateValue, copyToClipboard]);
|
|
2619
2749
|
// Local drafts and originals for commit-on-blur/enter behavior
|
|
2620
2750
|
const [drafts, setDrafts] = React.useState({});
|
|
2621
2751
|
const [originals, setOriginals] = React.useState({});
|
|
@@ -2673,10 +2803,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
2673
2803
|
return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-auto`, children: [jsxRuntime.jsxs("div", { className: "flex-1 overflow-auto", children: [contextPanel && jsxRuntime.jsx("div", { className: "mb-2", children: contextPanel }), inputValidationErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [inputValidationErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Input Validation Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), jsxRuntime.jsxs("div", { className: "text-[10px] text-red-600 mt-1", children: [err.nodeId, ".", err.handle, " (type: ", err.typeId, ")"] })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeInputValidationError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), inputValidationErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearInputValidationErrors, children: "Clear all" }))] })), systemErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [systemErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: err.code ? `Error ${err.code}` : "Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeSystemError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), systemErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearSystemErrors, children: "Clear all" }))] })), registryErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [registryErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Registry Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), err.attempt && err.maxAttempts && (jsxRuntime.jsxs("div", { className: "text-[10px] text-amber-600 mt-1", children: ["Attempt ", err.attempt, " of ", err.maxAttempts] }))] }), jsxRuntime.jsx("button", { className: "text-amber-500 hover:text-amber-700 text-[10px] px-1", onClick: () => removeRegistryError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), registryErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-amber-600 hover:text-amber-800 underline", onClick: clearRegistryErrors, children: "Clear all" }))] })), jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsxRuntime.jsx("div", { className: "flex-1", 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}` }), !!m.data?.edgeId && (jsxRuntime.jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
|
|
2674
2804
|
e.stopPropagation();
|
|
2675
2805
|
deleteEdgeById(m.data?.edgeId);
|
|
2676
|
-
}, title: "Delete referenced edge", children: "Delete edge" }))] }, 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] }), (() => {
|
|
2677
|
-
const status = edgeStatus?.[selectedEdge.id];
|
|
2678
|
-
return status?.activeRuns > 0 ? (jsxRuntime.jsxs("div", { className: "mt-1 text-xs text-blue-700 bg-blue-50 border border-blue-200 rounded px-2 py-1", children: [jsxRuntime.jsxs("div", { className: "font-semibold", children: ["Running (", status.activeRuns, ")"] }), jsxRuntime.jsx("div", { className: "text-[10px] text-blue-600 mt-1", children: "Note: Edge runIds are not available in stats events" })] })) : null;
|
|
2679
|
-
})(), jsxRuntime.jsx("div", { className: "mt-1", children: jsxRuntime.jsx("button", { className: "text-xs px-2 py-1 border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
|
|
2806
|
+
}, title: "Delete referenced edge", children: "Delete edge" }))] }, 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] }), renderEdgeStatus(), jsxRuntime.jsx("div", { className: "mt-1", children: jsxRuntime.jsx("button", { className: "text-xs px-2 py-1 border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
|
|
2680
2807
|
e.stopPropagation();
|
|
2681
2808
|
deleteEdgeById(selectedEdge.id);
|
|
2682
2809
|
}, title: "Delete this edge", children: "Delete edge" }) }), jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mt-1", children: [jsxRuntime.jsxs("label", { className: "w-20 flex flex-col", children: [jsxRuntime.jsx("span", { children: "Type" }), jsxRuntime.jsx("span", { className: "text-gray-500 text-[11px]", children: "DataTypeId" })] }), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 w-full", value: selectedEdge.typeId ?? "", onChange: (e) => {
|
|
@@ -2755,14 +2882,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
2755
2882
|
? `Default: ${placeholder}`
|
|
2756
2883
|
: "(select)" }), registry.enums
|
|
2757
2884
|
.get(typeId)
|
|
2758
|
-
?.options.map((opt) => (jsxRuntime.jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] }), hasValue && !isLinked && (jsxRuntime.jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded text-gray-500 hover:text-gray-700", onClick: clearInput, title: "Clear input value", children: jsxRuntime.jsx(react$1.XCircleIcon, { size: 16 }) }))] })) : isLinked ? (jsxRuntime.jsx("div", { className: "flex items-center gap-1 flex-1", children: jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: (
|
|
2759
|
-
const displayStr = safeToString(typeId, current);
|
|
2760
|
-
const isLong = displayStr.length > 50;
|
|
2761
|
-
const truncated = isLong
|
|
2762
|
-
? truncateValue(displayStr)
|
|
2763
|
-
: displayStr;
|
|
2764
|
-
return (jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [jsxRuntime.jsx("span", { className: "truncate", children: truncated }), isLong && (jsxRuntime.jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded", onClick: () => copyToClipboard(displayStr), title: "Copy full value", children: jsxRuntime.jsx(react$1.CopyIcon, { size: 14 }) }))] }));
|
|
2765
|
-
})() }) })) : (jsxRuntime.jsxs("div", { className: "flex items-center gap-1 flex-1", children: [jsxRuntime.jsx("input", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1", placeholder: placeholder
|
|
2885
|
+
?.options.map((opt) => (jsxRuntime.jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] }), hasValue && !isLinked && (jsxRuntime.jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded text-gray-500 hover:text-gray-700", onClick: clearInput, title: "Clear input value", children: jsxRuntime.jsx(react$1.XCircleIcon, { size: 16 }) }))] })) : isLinked ? (jsxRuntime.jsx("div", { className: "flex items-center gap-1 flex-1", children: jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: renderLinkedInputDisplay(typeId, current) }) })) : (jsxRuntime.jsxs("div", { className: "flex items-center gap-1 flex-1", children: [jsxRuntime.jsx("input", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1", placeholder: placeholder
|
|
2766
2886
|
? `Default: ${placeholder}`
|
|
2767
2887
|
: undefined, value: displayValue, onChange: (e) => onChangeText(e.target.value), onBlur: commit, onKeyDown: (e) => {
|
|
2768
2888
|
if (e.key === "Enter")
|
|
@@ -2770,24 +2890,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
2770
2890
|
if (e.key === "Escape")
|
|
2771
2891
|
revert();
|
|
2772
2892
|
}, ...commonProps }), hasValue && !isLinked && (jsxRuntime.jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded text-gray-500 hover:text-gray-700", onClick: clearInput, title: "Clear input value", children: jsxRuntime.jsx(react$1.XCircleIcon, { size: 16 }) }))] }))] }, h));
|
|
2773
|
-
}))] }), jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Outputs" }), outputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No outputs" })) : (outputHandles.map((h) => (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-20 flex flex-col", children: [jsxRuntime.jsx("span", { children: prettyHandle(h) }), jsxRuntime.jsx("span", { className: "text-gray-500 text-[11px]", children: outputTypesMap[selectedNodeId]?.[h] ?? "" })] }), jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: (() => {
|
|
2774
|
-
const { typeId, value } = resolveOutputDisplay(nodeOutputs[h], effectiveHandles.outputs[h]);
|
|
2775
|
-
const displayStr = safeToString(typeId, value);
|
|
2776
|
-
const isLong = displayStr.length > 50;
|
|
2777
|
-
const truncated = isLong
|
|
2778
|
-
? truncateValue(displayStr)
|
|
2779
|
-
: displayStr;
|
|
2780
|
-
return (jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [jsxRuntime.jsx("span", { className: "truncate", children: truncated }), isLong && (jsxRuntime.jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded", onClick: () => copyToClipboard(displayStr), title: "Copy full value", children: jsxRuntime.jsx(react$1.CopyIcon, { size: 14 }) }))] }));
|
|
2781
|
-
})() }), (() => {
|
|
2782
|
-
const outIssues = selectedNodeHandleValidation.outputs.filter((m) => m.handle === h);
|
|
2783
|
-
if (outIssues.length === 0)
|
|
2784
|
-
return null;
|
|
2785
|
-
const outErr = outIssues.some((m) => m.level === "error");
|
|
2786
|
-
const outTitle = outIssues
|
|
2787
|
-
.map((v) => `${v.code}: ${v.message}`)
|
|
2788
|
-
.join("; ");
|
|
2789
|
-
return (jsxRuntime.jsx(IssueBadge, { level: outErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: outTitle }));
|
|
2790
|
-
})()] }, h))))] }), selectedNodeValidation.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: selectedNodeValidation.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}` }), !!m.data?.edgeId && (jsxRuntime.jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
|
|
2893
|
+
}))] }), jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Outputs" }), outputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No outputs" })) : (outputHandles.map((h) => (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-20 flex flex-col", children: [jsxRuntime.jsx("span", { children: prettyHandle(h) }), jsxRuntime.jsx("span", { className: "text-gray-500 text-[11px]", children: outputTypesMap[selectedNodeId]?.[h] ?? "" })] }), jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: renderOutputDisplay(nodeOutputs[h], effectiveHandles.outputs[h]) }), renderOutputValidationBadge(h)] }, h))))] }), selectedNodeValidation.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: selectedNodeValidation.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}` }), !!m.data?.edgeId && (jsxRuntime.jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
|
|
2791
2894
|
e.stopPropagation();
|
|
2792
2895
|
deleteEdgeById(m.data?.edgeId);
|
|
2793
2896
|
}, title: "Delete referenced edge", children: "Delete edge" }))] }, i))) })] }))] })) })] }), debug && (jsxRuntime.jsx("div", { className: "mt-3 flex-none min-h-0 h-[50%]", children: jsxRuntime.jsx(DebugEvents, { autoScroll: !!autoScroll, hideWorkbench: !!hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange }) }))] }));
|
|
@@ -3542,6 +3645,44 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3542
3645
|
? computeEffectiveHandles(selectedNode, registry)
|
|
3543
3646
|
: { inputs: {}, outputs: {}, inputDefaults: {} };
|
|
3544
3647
|
const [exampleState, setExampleState] = React.useState(example ?? "");
|
|
3648
|
+
const isGraphRunning = runner.isRunning();
|
|
3649
|
+
const engineKind = runner.getRunningEngine();
|
|
3650
|
+
// Render Start/Stop button based on transport and runner state
|
|
3651
|
+
const renderStartStopButton = React.useCallback(() => {
|
|
3652
|
+
// Check if transport is connecting/retrying
|
|
3653
|
+
const isConnecting = transportStatus.state === "connecting" ||
|
|
3654
|
+
transportStatus.state === "retrying";
|
|
3655
|
+
// Only allow Start/Stop when transport is connected or local
|
|
3656
|
+
const canControl = transportStatus.state === "connected" ||
|
|
3657
|
+
transportStatus.state === "local";
|
|
3658
|
+
if (isConnecting) {
|
|
3659
|
+
return (jsxRuntime.jsxs("button", { className: "border rounded px-2 py-1.5 text-gray-500 border-gray-400 flex items-center gap-1 disabled:opacity-50", disabled: true, title: "Connecting to backend...", children: [jsxRuntime.jsx(react$1.ClockClockwiseIcon, { size: 16, className: "animate-spin" }), jsxRuntime.jsx("span", { className: "font-medium ml-1", children: "Connecting..." })] }));
|
|
3660
|
+
}
|
|
3661
|
+
if (isGraphRunning) {
|
|
3662
|
+
return (jsxRuntime.jsxs("button", { className: "border rounded px-2 py-1.5 text-red-700 border-red-600 flex items-center gap-1 disabled:opacity-50 disabled:text-gray-400 disabled:border-gray-300", onClick: () => runner.stop(), disabled: !canControl, title: canControl ? "Stop engine" : "Waiting for connection", children: [jsxRuntime.jsx(react$1.StopIcon, { size: 16, weight: "fill" }), jsxRuntime.jsx("span", { className: "font-medium ml-1", children: "Stop" })] }));
|
|
3663
|
+
}
|
|
3664
|
+
return (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 disabled:opacity-50", onClick: (evt) => {
|
|
3665
|
+
const kind = engine;
|
|
3666
|
+
if (!kind)
|
|
3667
|
+
return alert("Select an engine first.");
|
|
3668
|
+
if (evt.shiftKey && !confirm("Invalidate and re-run graph?"))
|
|
3669
|
+
return;
|
|
3670
|
+
try {
|
|
3671
|
+
runner.launch(wb.export(), {
|
|
3672
|
+
engine: kind,
|
|
3673
|
+
invalidate: evt.shiftKey,
|
|
3674
|
+
});
|
|
3675
|
+
}
|
|
3676
|
+
catch (err) {
|
|
3677
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3678
|
+
alert(message);
|
|
3679
|
+
}
|
|
3680
|
+
}, disabled: !engine || !canControl, title: !engine
|
|
3681
|
+
? "Select an engine first"
|
|
3682
|
+
: !canControl
|
|
3683
|
+
? "Waiting for connection"
|
|
3684
|
+
: "Start engine", children: [jsxRuntime.jsx(react$1.PlayIcon, { size: 16, weight: "fill" }), jsxRuntime.jsx("span", { className: "font-medium ml-1", children: "Start" })] }));
|
|
3685
|
+
}, [transportStatus, isGraphRunning, runner, engine, wb]);
|
|
3545
3686
|
const defaultExamples = React.useMemo(() => [
|
|
3546
3687
|
{
|
|
3547
3688
|
id: "simple",
|
|
@@ -3590,7 +3731,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3590
3731
|
return backendKind === "local";
|
|
3591
3732
|
});
|
|
3592
3733
|
// Expose init callback with setInitialGraph helper
|
|
3593
|
-
// Note: This runs whenever runner changes
|
|
3734
|
+
// Note: This runs whenever runner changes
|
|
3594
3735
|
React.useEffect(() => {
|
|
3595
3736
|
if (!onInit)
|
|
3596
3737
|
return;
|
|
@@ -3766,7 +3907,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3766
3907
|
applyExample(example);
|
|
3767
3908
|
}, [example, wb]);
|
|
3768
3909
|
React.useEffect(() => {
|
|
3769
|
-
const off = runner.on("transport",
|
|
3910
|
+
const off = runner.on("transport", setTransportStatus);
|
|
3770
3911
|
return () => off();
|
|
3771
3912
|
}, [runner]);
|
|
3772
3913
|
// Track registry readiness for remote backends
|
|
@@ -3787,7 +3928,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3787
3928
|
React.useEffect(() => {
|
|
3788
3929
|
if (!engine)
|
|
3789
3930
|
return;
|
|
3790
|
-
if (
|
|
3931
|
+
if (isGraphRunning)
|
|
3791
3932
|
return;
|
|
3792
3933
|
// Only auto-launch for local backend; require explicit Start for remote
|
|
3793
3934
|
if (backendKind !== "local")
|
|
@@ -3804,7 +3945,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3804
3945
|
catch {
|
|
3805
3946
|
// ignore
|
|
3806
3947
|
}
|
|
3807
|
-
}, [engine, runner, wb, backendKind]);
|
|
3948
|
+
}, [engine, runner, isGraphRunning, wb, backendKind]);
|
|
3808
3949
|
// Registry is automatically fetched by RemoteGraphRunner when it connects
|
|
3809
3950
|
// Run auto layout after registry is hydrated (for remote backends)
|
|
3810
3951
|
React.useEffect(() => {
|
|
@@ -3990,30 +4131,34 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3990
4131
|
return overrides.toElement(baseToElement, { registry });
|
|
3991
4132
|
return baseToElement;
|
|
3992
4133
|
}, [overrides, baseToElement, registry]);
|
|
3993
|
-
return (jsxRuntime.jsxs("div", { className: "w-full h-screen flex flex-col", children: [jsxRuntime.jsxs("div", { className: "p-2 border-b border-gray-300 flex gap-2 items-center", children: [
|
|
3994
|
-
? "Stop engine before switching example"
|
|
3995
|
-
: 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
|
-
? "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) => {
|
|
4134
|
+
return (jsxRuntime.jsxs("div", { className: "w-full h-screen flex flex-col", children: [jsxRuntime.jsxs("div", { className: "p-2 border-b border-gray-300 flex gap-2 items-center", children: [isGraphRunning ? (jsxRuntime.jsxs("span", { className: "ml-2 text-sm text-green-700", children: ["Running: ", engineKind] })) : (jsxRuntime.jsx("span", { className: "ml-2 text-sm text-gray-500", children: "Stopped" })), jsxRuntime.jsxs("span", { className: "ml-2 flex items-center gap-1 text-xs", title: transportStatus.kind || undefined, children: [transportStatus.state === "local" && (jsxRuntime.jsx(react$1.PlugsConnectedIcon, { size: 14, className: "text-gray-500" })), transportStatus.state === "connecting" && (jsxRuntime.jsx(react$1.ClockClockwiseIcon, { size: 14, className: "text-amber-600 animate-pulse" })), transportStatus.state === "connected" && (jsxRuntime.jsx(react$1.WifiHighIcon, { size: 14, className: "text-green-600" })), transportStatus.state === "disconnected" && (jsxRuntime.jsx(react$1.WifiSlashIcon, { size: 14, className: "text-red-600" })), transportStatus.state === "retrying" && (jsxRuntime.jsx(react$1.ClockClockwiseIcon, { size: 14, className: "text-amber-700 animate-pulse" }))] }), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: exampleState, onChange: (e) => applyExample(e.target.value), disabled: isGraphRunning, title: isGraphRunning ? "Stop engine before switching example" : 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: isGraphRunning, title: isGraphRunning ? "Stop engine before switching backend" : 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: "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: "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: engineKind ?? engine ?? "", onChange: async (e) => {
|
|
3998
4135
|
const kind = e.target.value || undefined;
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4136
|
+
const currentEngine = runner.getRunningEngine();
|
|
4137
|
+
// If engine is running and user selected a different engine, switch it
|
|
4138
|
+
if (runner.isRunning() &&
|
|
4139
|
+
currentEngine &&
|
|
4140
|
+
kind &&
|
|
4141
|
+
kind !== currentEngine) {
|
|
4142
|
+
try {
|
|
4143
|
+
await runner.switchEngine({
|
|
4144
|
+
engine: kind,
|
|
4145
|
+
batched: { flushIntervalMs: 0 },
|
|
4146
|
+
hybrid: { windowMs: 250, batchThreshold: 3 },
|
|
4147
|
+
});
|
|
4148
|
+
onEngineChange?.(kind);
|
|
4149
|
+
}
|
|
4150
|
+
catch (err) {
|
|
4151
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4152
|
+
alert(`Failed to switch engine: ${message}`);
|
|
4153
|
+
// Reset dropdown to current engine
|
|
4154
|
+
e.target.value = currentEngine;
|
|
4155
|
+
}
|
|
4011
4156
|
}
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4157
|
+
else {
|
|
4158
|
+
// Normal change when not running
|
|
4159
|
+
onEngineChange?.(kind);
|
|
4015
4160
|
}
|
|
4016
|
-
},
|
|
4161
|
+
}, 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" })] }), engineKind === "step" && (jsxRuntime.jsx("button", { className: "border border-gray-300 rounded p-1", onClick: () => runner.step(), disabled: !isGraphRunning, title: "Step", children: jsxRuntime.jsx(react$1.PlayPauseIcon, { size: 24 }) })), engineKind === "batched" && (jsxRuntime.jsx("button", { className: "border border-gray-300 rounded p-1", onClick: () => runner.flush(), disabled: !isGraphRunning, title: "Flush", children: jsxRuntime.jsx(react$1.LightningIcon, { size: 24 }) })), renderStartStopButton(), jsxRuntime.jsx("button", { className: "border border-gray-300 rounded p-1", onClick: runAutoLayout, children: jsxRuntime.jsx(react$1.TreeStructureIcon, { size: 24 }) }), jsxRuntime.jsx("button", { className: "border border-gray-300 rounded p-1", onClick: () => canvasRef.current?.fitView?.(), title: "Fit View", children: jsxRuntime.jsx(react$1.CornersOutIcon, { size: 24 }) }), jsxRuntime.jsx("button", { className: "border border-gray-300 rounded p-1", onClick: downloadGraph, children: jsxRuntime.jsx(react$1.DownloadSimpleIcon, { size: 24 }) }), jsxRuntime.jsx("button", { className: "border border-gray-300 rounded p-1", onClick: async () => {
|
|
4017
4162
|
try {
|
|
4018
4163
|
const def = wb.export();
|
|
4019
4164
|
const positions = wb.getPositions();
|
|
@@ -4042,7 +4187,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
4042
4187
|
const message = err instanceof Error ? err.message : String(err);
|
|
4043
4188
|
alert(message);
|
|
4044
4189
|
}
|
|
4045
|
-
}, children: jsxRuntime.jsx(react$1.DownloadIcon, { size: 24 }) }), jsxRuntime.jsx("input", { ref: uploadInputRef, type: "file", accept: "application/json,.json", className: "hidden", onChange: onUploadPicked }), jsxRuntime.jsx("button", { className: "
|
|
4190
|
+
}, children: jsxRuntime.jsx(react$1.DownloadIcon, { size: 24 }) }), jsxRuntime.jsx("input", { ref: uploadInputRef, type: "file", accept: "application/json,.json", className: "hidden", onChange: onUploadPicked }), jsxRuntime.jsx("button", { className: "border border-gray-300 rounded p-1", onClick: triggerUpload, children: jsxRuntime.jsx(react$1.UploadIcon, { size: 24 }) }), jsxRuntime.jsxs("label", { className: "flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsxRuntime.jsx(react$1.BugBeetleIcon, { size: 24, weight: debug ? "fill" : undefined })] }), jsxRuntime.jsxs("label", { className: "flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsxRuntime.jsx(react$1.ListBulletsIcon, { size: 24, weight: showValues ? "fill" : undefined })] })] }), 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, getDefaultNodeSize: overrides?.getDefaultNodeSize }) }), jsxRuntime.jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, contextPanel: overrides?.contextPanel })] })] }));
|
|
4046
4191
|
}
|
|
4047
4192
|
function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, backendOptions, overrides, onInit, onChange, }) {
|
|
4048
4193
|
const [registry, setRegistry] = React.useState(sparkGraph.createSimpleGraphRegistry());
|
|
@@ -4106,6 +4251,7 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
|
|
|
4106
4251
|
}
|
|
4107
4252
|
};
|
|
4108
4253
|
}, [runner]);
|
|
4254
|
+
const isGraphRunning = runner.isRunning();
|
|
4109
4255
|
// Track UI registration version to trigger nodeTypes recomputation
|
|
4110
4256
|
const [uiVersion, setUiVersion] = React.useState(0);
|
|
4111
4257
|
// Allow external UI registration (e.g., node renderers) with access to wb
|
|
@@ -4116,11 +4262,12 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
|
|
|
4116
4262
|
setUiVersion((v) => v + 1);
|
|
4117
4263
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
4118
4264
|
}, [wb, runner, overrides]);
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4265
|
+
const onBackendKindChangeWithDispose = React.useCallback(() => (v) => {
|
|
4266
|
+
if (isGraphRunning)
|
|
4267
|
+
runner.dispose();
|
|
4268
|
+
onBackendKindChange(v);
|
|
4269
|
+
}, [isGraphRunning]);
|
|
4270
|
+
return (jsxRuntime.jsx(WorkbenchProvider, { wb: wb, runner: runner, registry: registry, setRegistry: setRegistry, overrides: overrides, uiVersion: uiVersion, children: jsxRuntime.jsx(WorkbenchStudioCanvas, { setRegistry: setRegistry, autoScroll: autoScroll, onAutoScrollChange: onAutoScrollChange, example: example, onExampleChange: onExampleChange, engine: engine, onEngineChange: onEngineChange, backendKind: backendKind, onBackendKindChange: onBackendKindChangeWithDispose, httpBaseUrl: httpBaseUrl, onHttpBaseUrlChange: onHttpBaseUrlChange, wsUrl: wsUrl, onWsUrlChange: onWsUrlChange, debug: debug, onDebugChange: onDebugChange, showValues: showValues, onShowValuesChange: onShowValuesChange, hideWorkbench: hideWorkbench, onHideWorkbenchChange: onHideWorkbenchChange, backendOptions: backendOptions, overrides: overrides, onInit: onInit, onChange: onChange }) }));
|
|
4124
4271
|
}
|
|
4125
4272
|
|
|
4126
4273
|
exports.AbstractWorkbench = AbstractWorkbench;
|