@bian-womp/spark-workbench 0.2.37 → 0.2.39
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 +536 -417
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/index.d.ts +2 -1
- package/lib/cjs/src/index.d.ts.map +1 -1
- package/lib/cjs/src/misc/DefaultNode.d.ts.map +1 -1
- package/lib/cjs/src/misc/Inspector.d.ts +1 -2
- 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/mapping.d.ts +19 -0
- package/lib/cjs/src/misc/mapping.d.ts.map +1 -1
- package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts +32 -17
- package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
- package/lib/esm/index.js +533 -419
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/index.d.ts +2 -1
- package/lib/esm/src/index.d.ts.map +1 -1
- package/lib/esm/src/misc/DefaultNode.d.ts.map +1 -1
- package/lib/esm/src/misc/Inspector.d.ts +1 -2
- 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/mapping.d.ts +19 -0
- package/lib/esm/src/misc/mapping.d.ts.map +1 -1
- package/lib/esm/src/runtime/RemoteGraphRunner.d.ts +32 -17
- package/lib/esm/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
- package/package.json +4 -4
package/lib/cjs/index.cjs
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
var sparkGraph = require('@bian-womp/spark-graph');
|
|
4
4
|
var sparkRemote = require('@bian-womp/spark-remote');
|
|
5
|
-
var React = require('react');
|
|
6
5
|
var react = require('@xyflow/react');
|
|
6
|
+
var React = require('react');
|
|
7
|
+
var cx = require('classnames');
|
|
7
8
|
var jsxRuntime = require('react/jsx-runtime');
|
|
8
9
|
var react$1 = require('@phosphor-icons/react');
|
|
9
|
-
var cx = require('classnames');
|
|
10
10
|
var isEqual = require('lodash/isEqual');
|
|
11
11
|
|
|
12
12
|
class DefaultUIExtensionRegistry {
|
|
@@ -602,8 +602,200 @@ class LocalGraphRunner extends AbstractGraphRunner {
|
|
|
602
602
|
}
|
|
603
603
|
|
|
604
604
|
class RemoteGraphRunner extends AbstractGraphRunner {
|
|
605
|
+
/**
|
|
606
|
+
* Fetch full registry description from remote and register it locally.
|
|
607
|
+
* Called automatically on first connection with retry mechanism.
|
|
608
|
+
*/
|
|
609
|
+
async fetchRegistry(client, attempt = 1) {
|
|
610
|
+
if (this.registryFetching) {
|
|
611
|
+
// Already fetching, don't start another fetch
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
this.registryFetching = true;
|
|
615
|
+
try {
|
|
616
|
+
const desc = await client.describeRegistry();
|
|
617
|
+
// Register types
|
|
618
|
+
for (const t of desc.types) {
|
|
619
|
+
if (t.options) {
|
|
620
|
+
this.registry.registerEnum({
|
|
621
|
+
id: t.id,
|
|
622
|
+
options: t.options,
|
|
623
|
+
bakeTarget: t.bakeTarget,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
if (!this.registry.types.has(t.id)) {
|
|
628
|
+
this.registry.registerType({
|
|
629
|
+
id: t.id,
|
|
630
|
+
displayName: t.displayName,
|
|
631
|
+
validate: (_v) => true,
|
|
632
|
+
bakeTarget: t.bakeTarget,
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
// Register categories
|
|
638
|
+
for (const c of desc.categories || []) {
|
|
639
|
+
if (!this.registry.categories.has(c.id)) {
|
|
640
|
+
// Create placeholder category descriptor
|
|
641
|
+
const category = {
|
|
642
|
+
id: c.id,
|
|
643
|
+
displayName: c.displayName,
|
|
644
|
+
createRuntime: () => ({
|
|
645
|
+
async onInputsChanged() { },
|
|
646
|
+
}),
|
|
647
|
+
policy: { asyncConcurrency: "switch" },
|
|
648
|
+
};
|
|
649
|
+
this.registry.categories.register(category);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// Register coercions
|
|
653
|
+
for (const c of desc.coercions) {
|
|
654
|
+
if (c.async) {
|
|
655
|
+
this.registry.registerAsyncCoercion(c.from, c.to, async (v) => v, {
|
|
656
|
+
nonTransitive: c.nonTransitive,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
this.registry.registerCoercion(c.from, c.to, (v) => v, {
|
|
661
|
+
nonTransitive: c.nonTransitive,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
// Register nodes
|
|
666
|
+
for (const n of desc.nodes) {
|
|
667
|
+
if (!this.registry.nodes.has(n.id)) {
|
|
668
|
+
this.registry.registerNode({
|
|
669
|
+
id: n.id,
|
|
670
|
+
categoryId: n.categoryId,
|
|
671
|
+
displayName: n.displayName,
|
|
672
|
+
inputs: n.inputs || {},
|
|
673
|
+
outputs: n.outputs || {},
|
|
674
|
+
impl: () => { },
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
this.registryFetched = true;
|
|
679
|
+
this.registryFetching = false;
|
|
680
|
+
this.emit("registry", this.registry);
|
|
681
|
+
}
|
|
682
|
+
catch (err) {
|
|
683
|
+
this.registryFetching = false;
|
|
684
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
685
|
+
// Retry with exponential backoff if attempts remaining
|
|
686
|
+
if (attempt < this.MAX_REGISTRY_FETCH_ATTEMPTS) {
|
|
687
|
+
const delayMs = this.INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
|
|
688
|
+
console.warn(`Failed to fetch registry (attempt ${attempt}/${this.MAX_REGISTRY_FETCH_ATTEMPTS}), retrying in ${delayMs}ms...`, error);
|
|
689
|
+
// Emit error event for UI feedback
|
|
690
|
+
this.emit("error", {
|
|
691
|
+
kind: "registry",
|
|
692
|
+
message: `Registry fetch failed (attempt ${attempt}/${this.MAX_REGISTRY_FETCH_ATTEMPTS}), retrying...`,
|
|
693
|
+
err: error,
|
|
694
|
+
attempt,
|
|
695
|
+
maxAttempts: this.MAX_REGISTRY_FETCH_ATTEMPTS,
|
|
696
|
+
});
|
|
697
|
+
// Retry after delay
|
|
698
|
+
setTimeout(() => {
|
|
699
|
+
this.fetchRegistry(client, attempt + 1).catch(() => {
|
|
700
|
+
// Final failure handled below
|
|
701
|
+
});
|
|
702
|
+
}, delayMs);
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
// Max attempts reached, emit final error
|
|
706
|
+
console.error(`Failed to fetch registry after ${this.MAX_REGISTRY_FETCH_ATTEMPTS} attempts:`, error);
|
|
707
|
+
this.emit("error", {
|
|
708
|
+
kind: "registry",
|
|
709
|
+
message: `Failed to fetch registry after ${this.MAX_REGISTRY_FETCH_ATTEMPTS} attempts. Please check your connection and try refreshing.`,
|
|
710
|
+
err: error,
|
|
711
|
+
attempt: this.MAX_REGISTRY_FETCH_ATTEMPTS,
|
|
712
|
+
maxAttempts: this.MAX_REGISTRY_FETCH_ATTEMPTS,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Build RuntimeApiClient config from RemoteExecutionBackend config.
|
|
719
|
+
*/
|
|
720
|
+
buildClientConfig(backend) {
|
|
721
|
+
if (backend.kind === "remote-http") {
|
|
722
|
+
return {
|
|
723
|
+
kind: "remote-http",
|
|
724
|
+
baseUrl: backend.baseUrl,
|
|
725
|
+
connectOptions: backend.connectOptions,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
return {
|
|
730
|
+
kind: "remote-ws",
|
|
731
|
+
url: backend.url,
|
|
732
|
+
connectOptions: backend.connectOptions,
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Setup event subscriptions for the client.
|
|
738
|
+
*/
|
|
739
|
+
setupClientSubscriptions(client, backend) {
|
|
740
|
+
// Subscribe to custom events (for additional listeners if needed)
|
|
741
|
+
if (backend.onCustomEvent) {
|
|
742
|
+
this.customEventUnsubscribe = client.subscribeCustomEvents(backend.onCustomEvent);
|
|
743
|
+
}
|
|
744
|
+
// Subscribe to transport status changes
|
|
745
|
+
// Convert RuntimeApiClient.TransportStatus to IGraphRunner.TransportStatus
|
|
746
|
+
this.transportStatusUnsubscribe = client.onTransportStatus((status) => {
|
|
747
|
+
// Map remote-unix to undefined since RemoteGraphRunner doesn't support it
|
|
748
|
+
const mappedKind = status.kind === "remote-unix" ? undefined : status.kind;
|
|
749
|
+
this.emit("transport", {
|
|
750
|
+
state: status.state,
|
|
751
|
+
kind: mappedKind,
|
|
752
|
+
});
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
// Ensure remote client
|
|
756
|
+
async ensureClient() {
|
|
757
|
+
if (this.disposed) {
|
|
758
|
+
throw new Error("Cannot ensure client: RemoteGraphRunner has been disposed");
|
|
759
|
+
}
|
|
760
|
+
if (this.client)
|
|
761
|
+
return this.client;
|
|
762
|
+
// If already connecting, wait for that connection to complete
|
|
763
|
+
if (this.clientPromise)
|
|
764
|
+
return this.clientPromise;
|
|
765
|
+
const backend = this.backend;
|
|
766
|
+
// Create connection promise to prevent concurrent connections
|
|
767
|
+
this.clientPromise = (async () => {
|
|
768
|
+
// Build client config from backend config
|
|
769
|
+
const clientConfig = this.buildClientConfig(backend);
|
|
770
|
+
// Create client with custom event handler if provided
|
|
771
|
+
const client = new sparkRemote.RuntimeApiClient(clientConfig, {
|
|
772
|
+
onCustomEvent: backend.onCustomEvent,
|
|
773
|
+
});
|
|
774
|
+
// Setup event subscriptions
|
|
775
|
+
this.setupClientSubscriptions(client, backend);
|
|
776
|
+
// Connect the client (this will create and connect transport)
|
|
777
|
+
await client.connect();
|
|
778
|
+
this.client = client;
|
|
779
|
+
this.valueCache.clear();
|
|
780
|
+
this.listenersBound = false;
|
|
781
|
+
// Auto-fetch registry on first connection (only once)
|
|
782
|
+
if (!this.registryFetched && !this.registryFetching) {
|
|
783
|
+
// Log loading state (UI can listen to transport status for loading indication)
|
|
784
|
+
console.info("Loading registry from remote...");
|
|
785
|
+
this.fetchRegistry(client).catch((err) => {
|
|
786
|
+
console.error("[RemoteGraphRunner] Failed to fetch registry:", err);
|
|
787
|
+
// Error handling is done inside fetchRegistry, but we catch unhandled rejections
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
// Clear promise on success
|
|
791
|
+
this.clientPromise = undefined;
|
|
792
|
+
return client;
|
|
793
|
+
})();
|
|
794
|
+
return this.clientPromise;
|
|
795
|
+
}
|
|
605
796
|
constructor(registry, backend) {
|
|
606
797
|
super(registry, backend);
|
|
798
|
+
this.disposed = false;
|
|
607
799
|
this.valueCache = new Map();
|
|
608
800
|
this.listenersBound = false;
|
|
609
801
|
this.registryFetched = false;
|
|
@@ -612,8 +804,8 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
612
804
|
this.INITIAL_RETRY_DELAY_MS = 1000; // 1 second
|
|
613
805
|
// Auto-handle registry-changed invalidations from remote
|
|
614
806
|
// We listen on invalidate and if reason matches, we rehydrate registry and emit a registry event
|
|
615
|
-
this.
|
|
616
|
-
const eng =
|
|
807
|
+
this.ensureClient().then(async (client) => {
|
|
808
|
+
const eng = client.getEngine();
|
|
617
809
|
if (!this.listenersBound) {
|
|
618
810
|
eng.on("invalidate", async (e) => {
|
|
619
811
|
if (e.reason === "registry-changed") {
|
|
@@ -672,14 +864,12 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
672
864
|
}
|
|
673
865
|
});
|
|
674
866
|
}
|
|
675
|
-
build(def) {
|
|
676
|
-
console.warn("Unsupported operation for remote runner");
|
|
677
|
-
}
|
|
867
|
+
build(def) { }
|
|
678
868
|
update(def) {
|
|
679
869
|
// Remote: forward update; ignore errors (fire-and-forget)
|
|
680
|
-
this.
|
|
870
|
+
this.ensureClient().then(async (client) => {
|
|
681
871
|
try {
|
|
682
|
-
await
|
|
872
|
+
await client.update(def);
|
|
683
873
|
this.emit("invalidate", { reason: "graph-updated" });
|
|
684
874
|
this.lastDef = def;
|
|
685
875
|
}
|
|
@@ -689,14 +879,14 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
689
879
|
launch(def, opts) {
|
|
690
880
|
super.launch(def, opts);
|
|
691
881
|
// Remote: build remotely then launch
|
|
692
|
-
this.
|
|
693
|
-
await
|
|
882
|
+
this.ensureClient().then(async (client) => {
|
|
883
|
+
await client.build(def);
|
|
694
884
|
// Signal UI after remote build as well
|
|
695
885
|
this.emit("invalidate", { reason: "graph-built" });
|
|
696
886
|
this.lastDef = def;
|
|
697
887
|
// Hydrate current remote inputs/outputs (including defaults) into cache
|
|
698
888
|
try {
|
|
699
|
-
const snap = await
|
|
889
|
+
const snap = await client.snapshot();
|
|
700
890
|
for (const [nodeId, map] of Object.entries(snap.inputs || {})) {
|
|
701
891
|
for (const [handle, value] of Object.entries(map || {})) {
|
|
702
892
|
this.valueCache.set(`${nodeId}.${handle}`, {
|
|
@@ -719,7 +909,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
719
909
|
catch {
|
|
720
910
|
console.error("Failed to hydrate remote inputs/outputs");
|
|
721
911
|
}
|
|
722
|
-
const eng =
|
|
912
|
+
const eng = client.getEngine();
|
|
723
913
|
if (!this.listenersBound) {
|
|
724
914
|
eng.on("value", (e) => {
|
|
725
915
|
this.valueCache.set(`${e.nodeId}.${e.handle}`, {
|
|
@@ -743,36 +933,70 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
743
933
|
}
|
|
744
934
|
});
|
|
745
935
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
936
|
+
/**
|
|
937
|
+
* Launch using an existing backend runtime that has already been built and hydrated.
|
|
938
|
+
* This is used when resuming from a snapshot where the backend has already applied
|
|
939
|
+
* ApplySnapshotFull (which builds the graph and hydrates inputs/outputs).
|
|
940
|
+
* Unlike launch(), this method does NOT call client.build() to avoid destroying
|
|
941
|
+
* the runtime state that was just restored.
|
|
942
|
+
*/
|
|
943
|
+
launchExisting(def, opts) {
|
|
944
|
+
super.launch(def, opts);
|
|
945
|
+
// Remote: attach to existing runtime and launch (do NOT rebuild)
|
|
946
|
+
this.ensureClient().then(async (client) => {
|
|
947
|
+
// NOTE: We do NOT call client.build(def) here because the backend runtime
|
|
948
|
+
// has already been built and hydrated via ApplySnapshotFull.
|
|
949
|
+
// Calling build() would create a new runtime and lose the restored state.
|
|
950
|
+
this.lastDef = def;
|
|
951
|
+
// Attach to the existing engine
|
|
952
|
+
const eng = client.getEngine();
|
|
953
|
+
if (!this.listenersBound) {
|
|
954
|
+
eng.on("value", (e) => {
|
|
955
|
+
this.valueCache.set(`${e.nodeId}.${e.handle}`, {
|
|
956
|
+
io: e.io,
|
|
957
|
+
value: e.value,
|
|
958
|
+
runtimeTypeId: e.runtimeTypeId,
|
|
959
|
+
});
|
|
960
|
+
this.emit("value", e);
|
|
961
|
+
});
|
|
962
|
+
eng.on("error", (e) => this.emit("error", e));
|
|
963
|
+
eng.on("invalidate", (e) => this.emit("invalidate", e));
|
|
964
|
+
eng.on("stats", (e) => this.emit("stats", e));
|
|
965
|
+
this.listenersBound = true;
|
|
966
|
+
}
|
|
967
|
+
this.engine = eng;
|
|
968
|
+
this.engine.launch(opts.invalidate);
|
|
969
|
+
this.runningKind = "push";
|
|
970
|
+
this.emit("status", { running: true, engine: this.runningKind });
|
|
971
|
+
for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
|
|
972
|
+
this.engine.setInputs(nodeId, map);
|
|
973
|
+
}
|
|
974
|
+
});
|
|
754
975
|
}
|
|
976
|
+
async step() { }
|
|
977
|
+
async computeNode(nodeId) { }
|
|
978
|
+
flush() { }
|
|
755
979
|
triggerExternal(nodeId, event) {
|
|
756
|
-
this.
|
|
980
|
+
this.ensureClient().then(async (client) => {
|
|
757
981
|
try {
|
|
758
|
-
await
|
|
982
|
+
await client.getEngine().triggerExternal(nodeId, event);
|
|
759
983
|
}
|
|
760
984
|
catch { }
|
|
761
985
|
});
|
|
762
986
|
}
|
|
763
987
|
async coerce(from, to, value) {
|
|
764
|
-
const
|
|
988
|
+
const client = await this.ensureClient();
|
|
765
989
|
try {
|
|
766
|
-
return await
|
|
990
|
+
return await client.coerce(from, to, value);
|
|
767
991
|
}
|
|
768
992
|
catch {
|
|
769
993
|
return value;
|
|
770
994
|
}
|
|
771
995
|
}
|
|
772
996
|
async snapshotFull() {
|
|
773
|
-
const
|
|
997
|
+
const client = await this.ensureClient();
|
|
774
998
|
try {
|
|
775
|
-
return await
|
|
999
|
+
return await client.snapshotFull();
|
|
776
1000
|
}
|
|
777
1001
|
catch {
|
|
778
1002
|
return { def: undefined, environment: {}, inputs: {}, outputs: {} };
|
|
@@ -780,17 +1004,17 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
780
1004
|
}
|
|
781
1005
|
async applySnapshotFull(payload) {
|
|
782
1006
|
// Hydrate local cache first so UI can display values immediately
|
|
783
|
-
this.
|
|
1007
|
+
this.hydrateSnapshotFull(payload);
|
|
784
1008
|
// Then sync with backend
|
|
785
|
-
const
|
|
786
|
-
await
|
|
1009
|
+
const client = await this.ensureClient();
|
|
1010
|
+
await client.applySnapshotFull(payload);
|
|
787
1011
|
}
|
|
788
1012
|
/**
|
|
789
1013
|
* Hydrates the local valueCache from a snapshot and emits value events.
|
|
790
1014
|
* This ensures the UI can display inputs/outputs immediately without waiting
|
|
791
1015
|
* for value events from the remote backend.
|
|
792
1016
|
*/
|
|
793
|
-
|
|
1017
|
+
hydrateSnapshotFull(snapshot) {
|
|
794
1018
|
// Hydrate inputs
|
|
795
1019
|
for (const [nodeId, map] of Object.entries(snapshot.inputs || {})) {
|
|
796
1020
|
for (const [handle, value] of Object.entries(map || {})) {
|
|
@@ -813,20 +1037,25 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
813
1037
|
}
|
|
814
1038
|
}
|
|
815
1039
|
setEnvironment(env, opts) {
|
|
816
|
-
|
|
817
|
-
if (
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1040
|
+
// Use client if available, otherwise ensure client and then set environment
|
|
1041
|
+
if (this.client) {
|
|
1042
|
+
this.client.setEnvironment(env, opts).catch(() => { });
|
|
1043
|
+
}
|
|
1044
|
+
else {
|
|
1045
|
+
// If client not ready yet, ensure it and then set environment
|
|
1046
|
+
this.ensureClient()
|
|
1047
|
+
.then((client) => {
|
|
1048
|
+
client.setEnvironment(env, opts).catch(() => { });
|
|
1049
|
+
})
|
|
1050
|
+
.catch(() => { });
|
|
1051
|
+
}
|
|
825
1052
|
}
|
|
826
1053
|
getEnvironment() {
|
|
827
|
-
//
|
|
828
|
-
//
|
|
829
|
-
//
|
|
1054
|
+
// Interface requires sync return, but RuntimeApiClient.getEnvironment() is async.
|
|
1055
|
+
// Returns undefined synchronously; callers needing the actual value should:
|
|
1056
|
+
// - Use snapshotFull() which includes environment
|
|
1057
|
+
// - Call client.getEnvironment() directly if they have access to the client
|
|
1058
|
+
// This is a limitation of the sync interface signature.
|
|
830
1059
|
return undefined;
|
|
831
1060
|
}
|
|
832
1061
|
getOutputs(def) {
|
|
@@ -890,183 +1119,233 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
890
1119
|
return out;
|
|
891
1120
|
}
|
|
892
1121
|
dispose() {
|
|
1122
|
+
// Idempotent: allow multiple calls safely
|
|
1123
|
+
if (this.disposed)
|
|
1124
|
+
return;
|
|
1125
|
+
this.disposed = true;
|
|
893
1126
|
super.dispose();
|
|
894
|
-
|
|
895
|
-
this.
|
|
1127
|
+
// Clear client promise if any
|
|
1128
|
+
this.clientPromise = undefined;
|
|
1129
|
+
// Unsubscribe from custom events and transport status
|
|
1130
|
+
if (this.customEventUnsubscribe) {
|
|
1131
|
+
this.customEventUnsubscribe();
|
|
1132
|
+
this.customEventUnsubscribe = undefined;
|
|
1133
|
+
}
|
|
1134
|
+
if (this.transportStatusUnsubscribe) {
|
|
1135
|
+
this.transportStatusUnsubscribe();
|
|
1136
|
+
this.transportStatusUnsubscribe = undefined;
|
|
1137
|
+
}
|
|
1138
|
+
// Dispose client (which will close transport)
|
|
1139
|
+
const clientToDispose = this.client;
|
|
1140
|
+
this.client = undefined;
|
|
896
1141
|
this.registryFetched = false; // Reset so registry is fetched again on reconnect
|
|
897
1142
|
this.registryFetching = false; // Reset fetching state
|
|
1143
|
+
if (clientToDispose) {
|
|
1144
|
+
clientToDispose.dispose().catch((err) => {
|
|
1145
|
+
console.warn("[RemoteGraphRunner] Error disposing client:", err);
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
898
1148
|
this.emit("transport", {
|
|
899
1149
|
state: "disconnected",
|
|
900
1150
|
kind: this.backend.kind,
|
|
901
1151
|
});
|
|
902
1152
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
try {
|
|
914
|
-
const desc = await runner.describeRegistry();
|
|
915
|
-
// Register types
|
|
916
|
-
for (const t of desc.types) {
|
|
917
|
-
if (t.options) {
|
|
918
|
-
this.registry.registerEnum({
|
|
919
|
-
id: t.id,
|
|
920
|
-
options: t.options,
|
|
921
|
-
bakeTarget: t.bakeTarget,
|
|
922
|
-
});
|
|
923
|
-
}
|
|
924
|
-
else {
|
|
925
|
-
if (!this.registry.types.has(t.id)) {
|
|
926
|
-
this.registry.registerType({
|
|
927
|
-
id: t.id,
|
|
928
|
-
displayName: t.displayName,
|
|
929
|
-
validate: (_v) => true,
|
|
930
|
-
bakeTarget: t.bakeTarget,
|
|
931
|
-
});
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
// Register categories
|
|
936
|
-
for (const c of desc.categories || []) {
|
|
937
|
-
if (!this.registry.categories.has(c.id)) {
|
|
938
|
-
// Create placeholder category descriptor
|
|
939
|
-
const category = {
|
|
940
|
-
id: c.id,
|
|
941
|
-
displayName: c.displayName,
|
|
942
|
-
createRuntime: () => ({
|
|
943
|
-
async onInputsChanged() { },
|
|
944
|
-
}),
|
|
945
|
-
policy: { asyncConcurrency: "switch" },
|
|
946
|
-
};
|
|
947
|
-
this.registry.categories.register(category);
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
// Register coercions
|
|
951
|
-
for (const c of desc.coercions) {
|
|
952
|
-
if (c.async) {
|
|
953
|
-
this.registry.registerAsyncCoercion(c.from, c.to, async (v) => v, {
|
|
954
|
-
nonTransitive: c.nonTransitive,
|
|
955
|
-
});
|
|
956
|
-
}
|
|
957
|
-
else {
|
|
958
|
-
this.registry.registerCoercion(c.from, c.to, (v) => v, {
|
|
959
|
-
nonTransitive: c.nonTransitive,
|
|
960
|
-
});
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
// Register nodes
|
|
964
|
-
for (const n of desc.nodes) {
|
|
965
|
-
if (!this.registry.nodes.has(n.id)) {
|
|
966
|
-
this.registry.registerNode({
|
|
967
|
-
id: n.id,
|
|
968
|
-
categoryId: n.categoryId,
|
|
969
|
-
displayName: n.displayName,
|
|
970
|
-
inputs: n.inputs || {},
|
|
971
|
-
outputs: n.outputs || {},
|
|
972
|
-
impl: () => { },
|
|
973
|
-
});
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
this.registryFetched = true;
|
|
977
|
-
this.registryFetching = false;
|
|
978
|
-
this.emit("registry", this.registry);
|
|
979
|
-
}
|
|
980
|
-
catch (err) {
|
|
981
|
-
this.registryFetching = false;
|
|
982
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
983
|
-
// Retry with exponential backoff if attempts remaining
|
|
984
|
-
if (attempt < this.MAX_REGISTRY_FETCH_ATTEMPTS) {
|
|
985
|
-
const delayMs = this.INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
|
|
986
|
-
console.warn(`Failed to fetch registry (attempt ${attempt}/${this.MAX_REGISTRY_FETCH_ATTEMPTS}), retrying in ${delayMs}ms...`, error);
|
|
987
|
-
// Emit error event for UI feedback
|
|
988
|
-
this.emit("error", {
|
|
989
|
-
kind: "registry",
|
|
990
|
-
message: `Registry fetch failed (attempt ${attempt}/${this.MAX_REGISTRY_FETCH_ATTEMPTS}), retrying...`,
|
|
991
|
-
err: error,
|
|
992
|
-
attempt,
|
|
993
|
-
maxAttempts: this.MAX_REGISTRY_FETCH_ATTEMPTS,
|
|
994
|
-
});
|
|
995
|
-
// Retry after delay
|
|
996
|
-
setTimeout(() => {
|
|
997
|
-
this.fetchRegistry(runner, attempt + 1).catch(() => {
|
|
998
|
-
// Final failure handled below
|
|
999
|
-
});
|
|
1000
|
-
}, delayMs);
|
|
1001
|
-
}
|
|
1002
|
-
else {
|
|
1003
|
-
// Max attempts reached, emit final error
|
|
1004
|
-
console.error(`Failed to fetch registry after ${this.MAX_REGISTRY_FETCH_ATTEMPTS} attempts:`, error);
|
|
1005
|
-
this.emit("error", {
|
|
1006
|
-
kind: "registry",
|
|
1007
|
-
message: `Failed to fetch registry after ${this.MAX_REGISTRY_FETCH_ATTEMPTS} attempts. Please check your connection and try refreshing.`,
|
|
1008
|
-
err: error,
|
|
1009
|
-
attempt: this.MAX_REGISTRY_FETCH_ATTEMPTS,
|
|
1010
|
-
maxAttempts: this.MAX_REGISTRY_FETCH_ATTEMPTS,
|
|
1011
|
-
});
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function formatDataUrlAsLabel(dataUrl) {
|
|
1156
|
+
try {
|
|
1157
|
+
const semi = dataUrl.indexOf(";");
|
|
1158
|
+
const comma = dataUrl.indexOf(",");
|
|
1159
|
+
const mime = dataUrl.slice(5, semi > 0 ? semi : undefined).toUpperCase();
|
|
1160
|
+
const b64 = comma >= 0 ? dataUrl.slice(comma + 1) : "";
|
|
1161
|
+
const bytes = Math.floor((b64.length * 3) / 4);
|
|
1162
|
+
return `${mime} Data (${bytes} bytes)`;
|
|
1014
1163
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1164
|
+
catch {
|
|
1165
|
+
return dataUrl.length > 64 ? dataUrl.slice(0, 64) + "…" : dataUrl;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
function resolveOutputDisplay(raw, declared) {
|
|
1169
|
+
if (sparkGraph.isTypedOutput(raw)) {
|
|
1170
|
+
return {
|
|
1171
|
+
typeId: sparkGraph.getTypedOutputTypeId(raw),
|
|
1172
|
+
value: sparkGraph.getTypedOutputValue(raw),
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
let typeId = undefined;
|
|
1176
|
+
if (Array.isArray(declared)) {
|
|
1177
|
+
typeId = declared.length === 1 ? declared[0] : undefined;
|
|
1178
|
+
}
|
|
1179
|
+
else if (typeof declared === "string") {
|
|
1180
|
+
typeId = declared.includes("|") ? undefined : declared;
|
|
1181
|
+
}
|
|
1182
|
+
return { typeId, value: raw };
|
|
1183
|
+
}
|
|
1184
|
+
function formatDeclaredTypeSignature(declared) {
|
|
1185
|
+
if (Array.isArray(declared))
|
|
1186
|
+
return declared.join(" | ");
|
|
1187
|
+
return declared ?? "";
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Formats a handle ID for display in the UI.
|
|
1191
|
+
* For handles with format "prefix:middle:suffix:extra" (4 parts), displays only the middle part.
|
|
1192
|
+
* Otherwise returns the handle ID as-is.
|
|
1193
|
+
*/
|
|
1194
|
+
function prettyHandle(id) {
|
|
1195
|
+
try {
|
|
1196
|
+
const parts = String(id).split(":");
|
|
1197
|
+
// If there are exactly 3 colons (4 parts), display only the second part
|
|
1198
|
+
if (parts.length === 4)
|
|
1199
|
+
return parts[1] || id;
|
|
1200
|
+
return id;
|
|
1201
|
+
}
|
|
1202
|
+
catch {
|
|
1203
|
+
return id;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
// Pre-format common structures for display; return undefined to defer to caller
|
|
1207
|
+
function preformatValueForDisplay(typeId, value, registry) {
|
|
1208
|
+
if (value === undefined || value === null)
|
|
1209
|
+
return "";
|
|
1210
|
+
// Unwrap typed outputs
|
|
1211
|
+
if (sparkGraph.isTypedOutput(value)) {
|
|
1212
|
+
return preformatValueForDisplay(sparkGraph.getTypedOutputTypeId(value), sparkGraph.getTypedOutputValue(value), registry);
|
|
1213
|
+
}
|
|
1214
|
+
// Enums
|
|
1215
|
+
if (typeId && typeId.startsWith("enum:") && registry) {
|
|
1216
|
+
const n = Number(value);
|
|
1217
|
+
const label = registry.enums.get(typeId)?.valueToLabel.get(n);
|
|
1218
|
+
if (label)
|
|
1219
|
+
return label;
|
|
1220
|
+
}
|
|
1221
|
+
// Use deep summarization for strings, arrays and nested objects to avoid huge HTML payloads
|
|
1222
|
+
const summarized = summarizeDeep(value);
|
|
1223
|
+
if (typeof summarized === "string")
|
|
1224
|
+
return summarized;
|
|
1225
|
+
// Resource-like objects with url/title (after summarization)
|
|
1226
|
+
if (summarized && typeof summarized === "object") {
|
|
1227
|
+
const urlMaybe = summarized.url;
|
|
1228
|
+
if (typeof urlMaybe === "string") {
|
|
1229
|
+
const title = summarized.title || "";
|
|
1230
|
+
const shortUrl = urlMaybe.length > 32 ? urlMaybe.slice(0, 32) + "…" : urlMaybe;
|
|
1231
|
+
return title ? `${title} (${shortUrl})` : shortUrl;
|
|
1053
1232
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1233
|
+
}
|
|
1234
|
+
return undefined;
|
|
1235
|
+
}
|
|
1236
|
+
function summarizeDeep(value) {
|
|
1237
|
+
// Strings: summarize data URLs and trim extremely long strings
|
|
1238
|
+
if (typeof value === "string") {
|
|
1239
|
+
if (value.startsWith("data:"))
|
|
1240
|
+
return formatDataUrlAsLabel(value);
|
|
1241
|
+
return value.length > 512 ? value.slice(0, 512) + "…" : value;
|
|
1242
|
+
}
|
|
1243
|
+
// Typed output wrapper
|
|
1244
|
+
if (sparkGraph.isTypedOutput(value)) {
|
|
1245
|
+
return summarizeDeep(sparkGraph.getTypedOutputValue(value));
|
|
1246
|
+
}
|
|
1247
|
+
// Arrays
|
|
1248
|
+
if (Array.isArray(value)) {
|
|
1249
|
+
return value.map((v) => summarizeDeep(v));
|
|
1250
|
+
}
|
|
1251
|
+
// Objects
|
|
1252
|
+
if (value && typeof value === "object") {
|
|
1253
|
+
const obj = value;
|
|
1254
|
+
const out = {};
|
|
1255
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
1256
|
+
// Special-case any 'url' field
|
|
1257
|
+
if (typeof v === "string" &&
|
|
1258
|
+
k.toLowerCase() === "url" &&
|
|
1259
|
+
v.startsWith("data:")) {
|
|
1260
|
+
out[k] = formatDataUrlAsLabel(v);
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
out[k] = summarizeDeep(v);
|
|
1067
1264
|
}
|
|
1068
|
-
return
|
|
1265
|
+
return out;
|
|
1069
1266
|
}
|
|
1267
|
+
return value;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// Shared UI constants for node layout to keep mapping and rendering in sync
|
|
1271
|
+
const NODE_HEADER_HEIGHT_PX = 24;
|
|
1272
|
+
const NODE_ROW_HEIGHT_PX = 22;
|
|
1273
|
+
|
|
1274
|
+
function computeEffectiveHandles(node, registry) {
|
|
1275
|
+
const desc = registry.nodes.get(node.typeId);
|
|
1276
|
+
const resolved = node.resolvedHandles || {};
|
|
1277
|
+
const inputs = { ...desc?.inputs, ...resolved.inputs };
|
|
1278
|
+
const outputs = { ...desc?.outputs, ...resolved.outputs };
|
|
1279
|
+
const inputDefaults = { ...desc?.inputDefaults, ...resolved.inputDefaults };
|
|
1280
|
+
return { inputs, outputs, inputDefaults };
|
|
1281
|
+
}
|
|
1282
|
+
function countVisibleHandles(handles) {
|
|
1283
|
+
const inputIds = Object.keys(handles.inputs).filter((k) => !sparkGraph.isInputPrivate(handles.inputs, k));
|
|
1284
|
+
const outputIds = Object.keys(handles.outputs);
|
|
1285
|
+
return { inputsCount: inputIds.length, outputsCount: outputIds.length };
|
|
1286
|
+
}
|
|
1287
|
+
function estimateNodeSize(args) {
|
|
1288
|
+
const { node, registry, showValues, overrides } = args;
|
|
1289
|
+
const { inputs, outputs } = computeEffectiveHandles(node, registry);
|
|
1290
|
+
// Count only non-private inputs for rows on left
|
|
1291
|
+
const { inputsCount, outputsCount } = countVisibleHandles({
|
|
1292
|
+
inputs,
|
|
1293
|
+
outputs,
|
|
1294
|
+
});
|
|
1295
|
+
const rows = Math.max(inputsCount, outputsCount);
|
|
1296
|
+
const baseWidth = showValues ? 320 : 240;
|
|
1297
|
+
const width = overrides?.width ?? baseWidth;
|
|
1298
|
+
const height = overrides?.height ?? NODE_HEADER_HEIGHT_PX + rows * NODE_ROW_HEIGHT_PX;
|
|
1299
|
+
return { width, height, inputsCount, outputsCount, rowCount: rows };
|
|
1300
|
+
}
|
|
1301
|
+
function layoutNode(args) {
|
|
1302
|
+
const { node, registry, showValues, overrides } = args;
|
|
1303
|
+
const { inputs, outputs } = computeEffectiveHandles(node, registry);
|
|
1304
|
+
const inputOrder = Object.keys(inputs).filter((k) => !sparkGraph.isInputPrivate(inputs, k));
|
|
1305
|
+
const outputOrder = Object.keys(outputs);
|
|
1306
|
+
const { width, height } = estimateNodeSize({
|
|
1307
|
+
node,
|
|
1308
|
+
registry,
|
|
1309
|
+
showValues,
|
|
1310
|
+
overrides,
|
|
1311
|
+
});
|
|
1312
|
+
const HEADER = NODE_HEADER_HEIGHT_PX;
|
|
1313
|
+
const ROW = NODE_ROW_HEIGHT_PX;
|
|
1314
|
+
const handles = [
|
|
1315
|
+
...inputOrder.map((id, i) => ({
|
|
1316
|
+
id,
|
|
1317
|
+
type: "target",
|
|
1318
|
+
position: react.Position.Left,
|
|
1319
|
+
x: 0,
|
|
1320
|
+
y: HEADER + i * ROW,
|
|
1321
|
+
width: 1,
|
|
1322
|
+
height: ROW + 2,
|
|
1323
|
+
})),
|
|
1324
|
+
...outputOrder.map((id, i) => ({
|
|
1325
|
+
id,
|
|
1326
|
+
type: "source",
|
|
1327
|
+
position: react.Position.Right,
|
|
1328
|
+
x: width - 1,
|
|
1329
|
+
y: HEADER + i * ROW,
|
|
1330
|
+
width: 1,
|
|
1331
|
+
height: ROW + 2,
|
|
1332
|
+
})),
|
|
1333
|
+
];
|
|
1334
|
+
const handleLayout = [
|
|
1335
|
+
...inputOrder.map((id, i) => ({
|
|
1336
|
+
id,
|
|
1337
|
+
type: "target",
|
|
1338
|
+
position: react.Position.Left,
|
|
1339
|
+
y: HEADER + i * ROW + ROW / 2,
|
|
1340
|
+
})),
|
|
1341
|
+
...outputOrder.map((id, i) => ({
|
|
1342
|
+
id,
|
|
1343
|
+
type: "source",
|
|
1344
|
+
position: react.Position.Right,
|
|
1345
|
+
y: HEADER + i * ROW + ROW / 2,
|
|
1346
|
+
})),
|
|
1347
|
+
];
|
|
1348
|
+
return { width, height, inputOrder, outputOrder, handles, handleLayout };
|
|
1070
1349
|
}
|
|
1071
1350
|
|
|
1072
1351
|
function useWorkbenchBridge(wb) {
|
|
@@ -1313,202 +1592,6 @@ function useQueryParamString(key, defaultValue) {
|
|
|
1313
1592
|
return [val, set];
|
|
1314
1593
|
}
|
|
1315
1594
|
|
|
1316
|
-
function formatDataUrlAsLabel(dataUrl) {
|
|
1317
|
-
try {
|
|
1318
|
-
const semi = dataUrl.indexOf(";");
|
|
1319
|
-
const comma = dataUrl.indexOf(",");
|
|
1320
|
-
const mime = dataUrl.slice(5, semi > 0 ? semi : undefined).toUpperCase();
|
|
1321
|
-
const b64 = comma >= 0 ? dataUrl.slice(comma + 1) : "";
|
|
1322
|
-
const bytes = Math.floor((b64.length * 3) / 4);
|
|
1323
|
-
return `${mime} Data (${bytes} bytes)`;
|
|
1324
|
-
}
|
|
1325
|
-
catch {
|
|
1326
|
-
return dataUrl.length > 64 ? dataUrl.slice(0, 64) + "…" : dataUrl;
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
function resolveOutputDisplay(raw, declared) {
|
|
1330
|
-
if (sparkGraph.isTypedOutput(raw)) {
|
|
1331
|
-
return {
|
|
1332
|
-
typeId: sparkGraph.getTypedOutputTypeId(raw),
|
|
1333
|
-
value: sparkGraph.getTypedOutputValue(raw),
|
|
1334
|
-
};
|
|
1335
|
-
}
|
|
1336
|
-
let typeId = undefined;
|
|
1337
|
-
if (Array.isArray(declared)) {
|
|
1338
|
-
typeId = declared.length === 1 ? declared[0] : undefined;
|
|
1339
|
-
}
|
|
1340
|
-
else if (typeof declared === "string") {
|
|
1341
|
-
typeId = declared.includes("|") ? undefined : declared;
|
|
1342
|
-
}
|
|
1343
|
-
return { typeId, value: raw };
|
|
1344
|
-
}
|
|
1345
|
-
function formatDeclaredTypeSignature(declared) {
|
|
1346
|
-
if (Array.isArray(declared))
|
|
1347
|
-
return declared.join(" | ");
|
|
1348
|
-
return declared ?? "";
|
|
1349
|
-
}
|
|
1350
|
-
/**
|
|
1351
|
-
* Formats a handle ID for display in the UI.
|
|
1352
|
-
* For handles with format "prefix:middle:suffix:extra" (4 parts), displays only the middle part.
|
|
1353
|
-
* Otherwise returns the handle ID as-is.
|
|
1354
|
-
*/
|
|
1355
|
-
function prettyHandle(id) {
|
|
1356
|
-
try {
|
|
1357
|
-
const parts = String(id).split(":");
|
|
1358
|
-
// If there are exactly 3 colons (4 parts), display only the second part
|
|
1359
|
-
if (parts.length === 4)
|
|
1360
|
-
return parts[1] || id;
|
|
1361
|
-
return id;
|
|
1362
|
-
}
|
|
1363
|
-
catch {
|
|
1364
|
-
return id;
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
// Pre-format common structures for display; return undefined to defer to caller
|
|
1368
|
-
function preformatValueForDisplay(typeId, value, registry) {
|
|
1369
|
-
if (value === undefined || value === null)
|
|
1370
|
-
return "";
|
|
1371
|
-
// Unwrap typed outputs
|
|
1372
|
-
if (sparkGraph.isTypedOutput(value)) {
|
|
1373
|
-
return preformatValueForDisplay(sparkGraph.getTypedOutputTypeId(value), sparkGraph.getTypedOutputValue(value), registry);
|
|
1374
|
-
}
|
|
1375
|
-
// Enums
|
|
1376
|
-
if (typeId && typeId.startsWith("enum:") && registry) {
|
|
1377
|
-
const n = Number(value);
|
|
1378
|
-
const label = registry.enums.get(typeId)?.valueToLabel.get(n);
|
|
1379
|
-
if (label)
|
|
1380
|
-
return label;
|
|
1381
|
-
}
|
|
1382
|
-
// Use deep summarization for strings, arrays and nested objects to avoid huge HTML payloads
|
|
1383
|
-
const summarized = summarizeDeep(value);
|
|
1384
|
-
if (typeof summarized === "string")
|
|
1385
|
-
return summarized;
|
|
1386
|
-
// Resource-like objects with url/title (after summarization)
|
|
1387
|
-
if (summarized && typeof summarized === "object") {
|
|
1388
|
-
const urlMaybe = summarized.url;
|
|
1389
|
-
if (typeof urlMaybe === "string") {
|
|
1390
|
-
const title = summarized.title || "";
|
|
1391
|
-
const shortUrl = urlMaybe.length > 32 ? urlMaybe.slice(0, 32) + "…" : urlMaybe;
|
|
1392
|
-
return title ? `${title} (${shortUrl})` : shortUrl;
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
return undefined;
|
|
1396
|
-
}
|
|
1397
|
-
function summarizeDeep(value) {
|
|
1398
|
-
// Strings: summarize data URLs and trim extremely long strings
|
|
1399
|
-
if (typeof value === "string") {
|
|
1400
|
-
if (value.startsWith("data:"))
|
|
1401
|
-
return formatDataUrlAsLabel(value);
|
|
1402
|
-
return value.length > 512 ? value.slice(0, 512) + "…" : value;
|
|
1403
|
-
}
|
|
1404
|
-
// Typed output wrapper
|
|
1405
|
-
if (sparkGraph.isTypedOutput(value)) {
|
|
1406
|
-
return summarizeDeep(sparkGraph.getTypedOutputValue(value));
|
|
1407
|
-
}
|
|
1408
|
-
// Arrays
|
|
1409
|
-
if (Array.isArray(value)) {
|
|
1410
|
-
return value.map((v) => summarizeDeep(v));
|
|
1411
|
-
}
|
|
1412
|
-
// Objects
|
|
1413
|
-
if (value && typeof value === "object") {
|
|
1414
|
-
const obj = value;
|
|
1415
|
-
const out = {};
|
|
1416
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
1417
|
-
// Special-case any 'url' field
|
|
1418
|
-
if (typeof v === "string" &&
|
|
1419
|
-
k.toLowerCase() === "url" &&
|
|
1420
|
-
v.startsWith("data:")) {
|
|
1421
|
-
out[k] = formatDataUrlAsLabel(v);
|
|
1422
|
-
continue;
|
|
1423
|
-
}
|
|
1424
|
-
out[k] = summarizeDeep(v);
|
|
1425
|
-
}
|
|
1426
|
-
return out;
|
|
1427
|
-
}
|
|
1428
|
-
return value;
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
// Shared UI constants for node layout to keep mapping and rendering in sync
|
|
1432
|
-
const NODE_HEADER_HEIGHT_PX = 24;
|
|
1433
|
-
const NODE_ROW_HEIGHT_PX = 22;
|
|
1434
|
-
|
|
1435
|
-
function computeEffectiveHandles(node, registry) {
|
|
1436
|
-
const desc = registry.nodes.get(node.typeId);
|
|
1437
|
-
const resolved = node.resolvedHandles || {};
|
|
1438
|
-
const inputs = { ...desc?.inputs, ...resolved.inputs };
|
|
1439
|
-
const outputs = { ...desc?.outputs, ...resolved.outputs };
|
|
1440
|
-
const inputDefaults = { ...desc?.inputDefaults, ...resolved.inputDefaults };
|
|
1441
|
-
return { inputs, outputs, inputDefaults };
|
|
1442
|
-
}
|
|
1443
|
-
function countVisibleHandles(handles) {
|
|
1444
|
-
const inputIds = Object.keys(handles.inputs).filter((k) => !sparkGraph.isInputPrivate(handles.inputs, k));
|
|
1445
|
-
const outputIds = Object.keys(handles.outputs);
|
|
1446
|
-
return { inputsCount: inputIds.length, outputsCount: outputIds.length };
|
|
1447
|
-
}
|
|
1448
|
-
function estimateNodeSize(args) {
|
|
1449
|
-
const { node, registry, showValues, overrides } = args;
|
|
1450
|
-
const { inputs, outputs } = computeEffectiveHandles(node, registry);
|
|
1451
|
-
// Count only non-private inputs for rows on left
|
|
1452
|
-
const { inputsCount, outputsCount } = countVisibleHandles({
|
|
1453
|
-
inputs,
|
|
1454
|
-
outputs,
|
|
1455
|
-
});
|
|
1456
|
-
const rows = Math.max(inputsCount, outputsCount);
|
|
1457
|
-
const baseWidth = showValues ? 320 : 240;
|
|
1458
|
-
const width = overrides?.width ?? baseWidth;
|
|
1459
|
-
const height = overrides?.height ?? NODE_HEADER_HEIGHT_PX + rows * NODE_ROW_HEIGHT_PX;
|
|
1460
|
-
return { width, height, inputsCount, outputsCount, rowCount: rows };
|
|
1461
|
-
}
|
|
1462
|
-
function layoutNode(args) {
|
|
1463
|
-
const { node, registry, showValues, overrides } = args;
|
|
1464
|
-
const { inputs, outputs } = computeEffectiveHandles(node, registry);
|
|
1465
|
-
const inputOrder = Object.keys(inputs).filter((k) => !sparkGraph.isInputPrivate(inputs, k));
|
|
1466
|
-
const outputOrder = Object.keys(outputs);
|
|
1467
|
-
const { width, height } = estimateNodeSize({
|
|
1468
|
-
node,
|
|
1469
|
-
registry,
|
|
1470
|
-
showValues,
|
|
1471
|
-
overrides,
|
|
1472
|
-
});
|
|
1473
|
-
const HEADER = NODE_HEADER_HEIGHT_PX;
|
|
1474
|
-
const ROW = NODE_ROW_HEIGHT_PX;
|
|
1475
|
-
const handles = [
|
|
1476
|
-
...inputOrder.map((id, i) => ({
|
|
1477
|
-
id,
|
|
1478
|
-
type: "target",
|
|
1479
|
-
position: react.Position.Left,
|
|
1480
|
-
x: 0,
|
|
1481
|
-
y: HEADER + i * ROW,
|
|
1482
|
-
width: 1,
|
|
1483
|
-
height: ROW + 2,
|
|
1484
|
-
})),
|
|
1485
|
-
...outputOrder.map((id, i) => ({
|
|
1486
|
-
id,
|
|
1487
|
-
type: "source",
|
|
1488
|
-
position: react.Position.Right,
|
|
1489
|
-
x: width - 1,
|
|
1490
|
-
y: HEADER + i * ROW,
|
|
1491
|
-
width: 1,
|
|
1492
|
-
height: ROW + 2,
|
|
1493
|
-
})),
|
|
1494
|
-
];
|
|
1495
|
-
const handleLayout = [
|
|
1496
|
-
...inputOrder.map((id, i) => ({
|
|
1497
|
-
id,
|
|
1498
|
-
type: "target",
|
|
1499
|
-
position: react.Position.Left,
|
|
1500
|
-
y: HEADER + i * ROW + ROW / 2,
|
|
1501
|
-
})),
|
|
1502
|
-
...outputOrder.map((id, i) => ({
|
|
1503
|
-
id,
|
|
1504
|
-
type: "source",
|
|
1505
|
-
position: react.Position.Right,
|
|
1506
|
-
y: HEADER + i * ROW + ROW / 2,
|
|
1507
|
-
})),
|
|
1508
|
-
];
|
|
1509
|
-
return { width, height, inputOrder, outputOrder, handles, handleLayout };
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
1595
|
function toReactFlow(def, positions, registry, opts) {
|
|
1513
1596
|
const EDGE_STYLE_MISSING = { stroke: "#f59e0b", strokeWidth: 2 }; // amber-500
|
|
1514
1597
|
const EDGE_STYLE_ERROR = { stroke: "#ef4444", strokeWidth: 2 };
|
|
@@ -1734,6 +1817,33 @@ function getNodeBorderClassNames(args) {
|
|
|
1734
1817
|
: "";
|
|
1735
1818
|
return [borderWidth, borderStyle, borderColor, ring].join(" ").trim();
|
|
1736
1819
|
}
|
|
1820
|
+
/**
|
|
1821
|
+
* Shared utility to generate handle className based on validation and value state.
|
|
1822
|
+
* - Linked handles (with inbound edges) get black borders
|
|
1823
|
+
* - Handles with values (but not linked) get darker gray borders
|
|
1824
|
+
* - Handles with only defaults (no value, not linked) get lighter gray borders
|
|
1825
|
+
* - Validation errors (red/amber) take precedence over value-based styling.
|
|
1826
|
+
*/
|
|
1827
|
+
function getHandleClassName(args) {
|
|
1828
|
+
const { kind, id, validation } = args;
|
|
1829
|
+
const vIssues = (kind === "input" ? validation.inputs : validation.outputs)?.filter((v) => v.handle === id) || [];
|
|
1830
|
+
const hasAny = vIssues.length > 0;
|
|
1831
|
+
const hasErr = vIssues.some((v) => v.level === "error");
|
|
1832
|
+
// Determine border color based on priority:
|
|
1833
|
+
// 1. Validation errors (red/amber) - highest priority
|
|
1834
|
+
// 2. Gray border
|
|
1835
|
+
let borderColor;
|
|
1836
|
+
if (hasAny && hasErr) {
|
|
1837
|
+
borderColor = "!border-red-500";
|
|
1838
|
+
}
|
|
1839
|
+
else if (hasAny) {
|
|
1840
|
+
borderColor = "!border-amber-500";
|
|
1841
|
+
}
|
|
1842
|
+
else {
|
|
1843
|
+
borderColor = "!border-gray-500 dark:!border-gray-400";
|
|
1844
|
+
}
|
|
1845
|
+
return cx("!w-3 !h-3 !bg-white !dark:bg-stone-900", borderColor, kind === "output" && "!rounded-none");
|
|
1846
|
+
}
|
|
1737
1847
|
|
|
1738
1848
|
const WorkbenchContext = React.createContext(null);
|
|
1739
1849
|
function useWorkbenchContext() {
|
|
@@ -2456,7 +2566,7 @@ function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWork
|
|
|
2456
2566
|
return (jsxRuntime.jsxs("div", { className: "flex flex-col h-full min-h-0", children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Events" }), jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [jsxRuntime.jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: hideWorkbench, onChange: (e) => onHideWorkbenchChange?.(e.target.checked) }), jsxRuntime.jsx("span", { children: "Hide workbench" })] }), jsxRuntime.jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: autoScroll, onChange: (e) => onAutoScrollChange?.(e.target.checked) }), jsxRuntime.jsx("span", { children: "Auto scroll" })] }), jsxRuntime.jsx("button", { onClick: handleCopyLogs, className: "p-2 border border-gray-300 rounded flex items-center justify-center", title: copied ? "Copied!" : "Copy logs as formatted JSON", children: jsxRuntime.jsx(react$1.CopyIcon, { size: 14 }) }), jsxRuntime.jsx("button", { onClick: clearEvents, className: "p-2 border border-gray-300 rounded flex items-center justify-center", title: "Clear all events", children: jsxRuntime.jsx(react$1.TrashIcon, { size: 14 }) })] })] }), jsxRuntime.jsx("div", { ref: scrollRef, className: "flex-1 overflow-auto text-[11px] leading-4 divide-y divide-gray-200", children: rows.map((ev) => (jsxRuntime.jsxs("div", { className: "opacity-85 odd:bg-gray-50 px-2 py-1", children: [jsxRuntime.jsxs("div", { className: "flex items-baseline gap-2", children: [jsxRuntime.jsx("span", { className: "w-12 shrink-0 text-right text-gray-500 select-none", children: ev.no }), jsxRuntime.jsxs("span", { className: "text-gray-500", children: [new Date(ev.at).toLocaleTimeString(), " \u00B7 ", ev.source, ":", ev.type] })] }), jsxRuntime.jsx("pre", { className: "m-0 whitespace-pre-wrap ml-12", children: renderPayload(ev.payload) })] }, `${ev.at}:${ev.no}`))) })] }));
|
|
2457
2567
|
}
|
|
2458
2568
|
|
|
2459
|
-
function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toString,
|
|
2569
|
+
function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toString, contextPanel, setInput, }) {
|
|
2460
2570
|
const safeToString = (typeId, value) => {
|
|
2461
2571
|
try {
|
|
2462
2572
|
if (typeof toString === "function") {
|
|
@@ -2834,15 +2944,17 @@ function DefaultNodeContent({ data, isConnectable, }) {
|
|
|
2834
2944
|
const status = data.status ?? { activeRuns: 0 };
|
|
2835
2945
|
const validation = data.validation ?? {
|
|
2836
2946
|
inputs: [],
|
|
2837
|
-
outputs: []
|
|
2947
|
+
outputs: [],
|
|
2948
|
+
issues: [],
|
|
2949
|
+
};
|
|
2838
2950
|
const isRunning = !!status.activeRuns;
|
|
2839
2951
|
const pct = Math.round(Math.max(0, Math.min(1, Number(status.progress) || 0)) * 100);
|
|
2840
|
-
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: cx("h-px", (isRunning || pct > 0) && "bg-blue-200 dark:bg-blue-900"), children: jsxRuntime.jsx("div", { className: cx("h-px transition-all", (isRunning || pct > 0) && "bg-blue-500"), style: { width: isRunning || pct > 0 ? `${pct}%` : 0 } }) }), jsxRuntime.jsx(NodeHandles, { data: data, isConnectable: isConnectable, getClassName: ({ kind, id }) => {
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
}, renderLabel: ({ kind, id: handleId }) => {
|
|
2952
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: cx("h-px", (isRunning || pct > 0) && "bg-blue-200 dark:bg-blue-900"), children: jsxRuntime.jsx("div", { className: cx("h-px transition-all", (isRunning || pct > 0) && "bg-blue-500"), style: { width: isRunning || pct > 0 ? `${pct}%` : 0 } }) }), jsxRuntime.jsx(NodeHandles, { data: data, isConnectable: isConnectable, getClassName: ({ kind, id }) => getHandleClassName({
|
|
2953
|
+
kind,
|
|
2954
|
+
id,
|
|
2955
|
+
validation,
|
|
2956
|
+
inputConnected: data.inputConnected,
|
|
2957
|
+
}), renderLabel: ({ kind, id: handleId }) => {
|
|
2846
2958
|
const entries = kind === "input" ? inputEntries : outputEntries;
|
|
2847
2959
|
const entry = entries.find((e) => e.id === handleId);
|
|
2848
2960
|
if (!entry)
|
|
@@ -3448,9 +3560,6 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3448
3560
|
state: "local",
|
|
3449
3561
|
});
|
|
3450
3562
|
const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
|
|
3451
|
-
selectedNode
|
|
3452
|
-
? registry.nodes.get(selectedNode.typeId)
|
|
3453
|
-
: undefined;
|
|
3454
3563
|
const effectiveHandles = selectedNode
|
|
3455
3564
|
? computeEffectiveHandles(selectedNode, registry)
|
|
3456
3565
|
: { inputs: {}, outputs: {}, inputDefaults: {} };
|
|
@@ -3741,7 +3850,12 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3741
3850
|
const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId && e.target.handle === handle);
|
|
3742
3851
|
if (isLinked)
|
|
3743
3852
|
return;
|
|
3744
|
-
|
|
3853
|
+
// If raw is undefined, pass it through to delete the input value
|
|
3854
|
+
if (raw === undefined) {
|
|
3855
|
+
runner.setInput(selectedNodeId, handle, undefined);
|
|
3856
|
+
return;
|
|
3857
|
+
}
|
|
3858
|
+
const typeId = sparkGraph.getInputTypeId(effectiveHandles.inputs, handle);
|
|
3745
3859
|
let value = raw;
|
|
3746
3860
|
const parseArray = (s, map) => {
|
|
3747
3861
|
const str = String(s).trim();
|
|
@@ -3950,7 +4064,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3950
4064
|
const message = err instanceof Error ? err.message : String(err);
|
|
3951
4065
|
alert(message);
|
|
3952
4066
|
}
|
|
3953
|
-
}, 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: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: triggerUpload, children: jsxRuntime.jsx(react$1.UploadIcon, { size: 24 }) }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsxRuntime.jsx(react$1.BugBeetleIcon, { size: 24, weight: debug ? "fill" : undefined })] }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsxRuntime.jsx(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,
|
|
4067
|
+
}, 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: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: triggerUpload, children: jsxRuntime.jsx(react$1.UploadIcon, { size: 24 }) }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsxRuntime.jsx(react$1.BugBeetleIcon, { size: 24, weight: debug ? "fill" : undefined })] }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsxRuntime.jsx(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 })] })] }));
|
|
3954
4068
|
}
|
|
3955
4069
|
function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, backendOptions, overrides, onInit, onChange, }) {
|
|
3956
4070
|
const [registry, setRegistry] = React.useState(sparkGraph.createSimpleGraphRegistry());
|
|
@@ -4046,9 +4160,14 @@ exports.WorkbenchCanvas = WorkbenchCanvas;
|
|
|
4046
4160
|
exports.WorkbenchContext = WorkbenchContext;
|
|
4047
4161
|
exports.WorkbenchProvider = WorkbenchProvider;
|
|
4048
4162
|
exports.WorkbenchStudio = WorkbenchStudio;
|
|
4163
|
+
exports.computeEffectiveHandles = computeEffectiveHandles;
|
|
4164
|
+
exports.countVisibleHandles = countVisibleHandles;
|
|
4165
|
+
exports.estimateNodeSize = estimateNodeSize;
|
|
4049
4166
|
exports.formatDataUrlAsLabel = formatDataUrlAsLabel;
|
|
4050
4167
|
exports.formatDeclaredTypeSignature = formatDeclaredTypeSignature;
|
|
4168
|
+
exports.getHandleClassName = getHandleClassName;
|
|
4051
4169
|
exports.getNodeBorderClassNames = getNodeBorderClassNames;
|
|
4170
|
+
exports.layoutNode = layoutNode;
|
|
4052
4171
|
exports.preformatValueForDisplay = preformatValueForDisplay;
|
|
4053
4172
|
exports.prettyHandle = prettyHandle;
|
|
4054
4173
|
exports.resolveOutputDisplay = resolveOutputDisplay;
|