@bian-womp/spark-workbench 0.2.37 → 0.2.38
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 +340 -213
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/index.d.ts +1 -0
- 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 +337 -215
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/index.d.ts +1 -0
- 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
|
@@ -4,9 +4,9 @@ var sparkGraph = require('@bian-womp/spark-graph');
|
|
|
4
4
|
var sparkRemote = require('@bian-womp/spark-remote');
|
|
5
5
|
var React = require('react');
|
|
6
6
|
var react = require('@xyflow/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") {
|
|
@@ -677,9 +869,9 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
677
869
|
}
|
|
678
870
|
update(def) {
|
|
679
871
|
// Remote: forward update; ignore errors (fire-and-forget)
|
|
680
|
-
this.
|
|
872
|
+
this.ensureClient().then(async (client) => {
|
|
681
873
|
try {
|
|
682
|
-
await
|
|
874
|
+
await client.update(def);
|
|
683
875
|
this.emit("invalidate", { reason: "graph-updated" });
|
|
684
876
|
this.lastDef = def;
|
|
685
877
|
}
|
|
@@ -689,14 +881,14 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
689
881
|
launch(def, opts) {
|
|
690
882
|
super.launch(def, opts);
|
|
691
883
|
// Remote: build remotely then launch
|
|
692
|
-
this.
|
|
693
|
-
await
|
|
884
|
+
this.ensureClient().then(async (client) => {
|
|
885
|
+
await client.build(def);
|
|
694
886
|
// Signal UI after remote build as well
|
|
695
887
|
this.emit("invalidate", { reason: "graph-built" });
|
|
696
888
|
this.lastDef = def;
|
|
697
889
|
// Hydrate current remote inputs/outputs (including defaults) into cache
|
|
698
890
|
try {
|
|
699
|
-
const snap = await
|
|
891
|
+
const snap = await client.snapshot();
|
|
700
892
|
for (const [nodeId, map] of Object.entries(snap.inputs || {})) {
|
|
701
893
|
for (const [handle, value] of Object.entries(map || {})) {
|
|
702
894
|
this.valueCache.set(`${nodeId}.${handle}`, {
|
|
@@ -719,7 +911,47 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
719
911
|
catch {
|
|
720
912
|
console.error("Failed to hydrate remote inputs/outputs");
|
|
721
913
|
}
|
|
722
|
-
const eng =
|
|
914
|
+
const eng = client.getEngine();
|
|
915
|
+
if (!this.listenersBound) {
|
|
916
|
+
eng.on("value", (e) => {
|
|
917
|
+
this.valueCache.set(`${e.nodeId}.${e.handle}`, {
|
|
918
|
+
io: e.io,
|
|
919
|
+
value: e.value,
|
|
920
|
+
runtimeTypeId: e.runtimeTypeId,
|
|
921
|
+
});
|
|
922
|
+
this.emit("value", e);
|
|
923
|
+
});
|
|
924
|
+
eng.on("error", (e) => this.emit("error", e));
|
|
925
|
+
eng.on("invalidate", (e) => this.emit("invalidate", e));
|
|
926
|
+
eng.on("stats", (e) => this.emit("stats", e));
|
|
927
|
+
this.listenersBound = true;
|
|
928
|
+
}
|
|
929
|
+
this.engine = eng;
|
|
930
|
+
this.engine.launch(opts.invalidate);
|
|
931
|
+
this.runningKind = "push";
|
|
932
|
+
this.emit("status", { running: true, engine: this.runningKind });
|
|
933
|
+
for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
|
|
934
|
+
this.engine.setInputs(nodeId, map);
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Launch using an existing backend runtime that has already been built and hydrated.
|
|
940
|
+
* This is used when resuming from a snapshot where the backend has already applied
|
|
941
|
+
* ApplySnapshotFull (which builds the graph and hydrates inputs/outputs).
|
|
942
|
+
* Unlike launch(), this method does NOT call client.build() to avoid destroying
|
|
943
|
+
* the runtime state that was just restored.
|
|
944
|
+
*/
|
|
945
|
+
launchExisting(def, opts) {
|
|
946
|
+
super.launch(def, opts);
|
|
947
|
+
// Remote: attach to existing runtime and launch (do NOT rebuild)
|
|
948
|
+
this.ensureClient().then(async (client) => {
|
|
949
|
+
// NOTE: We do NOT call client.build(def) here because the backend runtime
|
|
950
|
+
// has already been built and hydrated via ApplySnapshotFull.
|
|
951
|
+
// Calling build() would create a new runtime and lose the restored state.
|
|
952
|
+
this.lastDef = def;
|
|
953
|
+
// Attach to the existing engine
|
|
954
|
+
const eng = client.getEngine();
|
|
723
955
|
if (!this.listenersBound) {
|
|
724
956
|
eng.on("value", (e) => {
|
|
725
957
|
this.valueCache.set(`${e.nodeId}.${e.handle}`, {
|
|
@@ -753,26 +985,26 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
753
985
|
console.warn("Unsupported operation for remote runner");
|
|
754
986
|
}
|
|
755
987
|
triggerExternal(nodeId, event) {
|
|
756
|
-
this.
|
|
988
|
+
this.ensureClient().then(async (client) => {
|
|
757
989
|
try {
|
|
758
|
-
await
|
|
990
|
+
await client.getEngine().triggerExternal(nodeId, event);
|
|
759
991
|
}
|
|
760
992
|
catch { }
|
|
761
993
|
});
|
|
762
994
|
}
|
|
763
995
|
async coerce(from, to, value) {
|
|
764
|
-
const
|
|
996
|
+
const client = await this.ensureClient();
|
|
765
997
|
try {
|
|
766
|
-
return await
|
|
998
|
+
return await client.coerce(from, to, value);
|
|
767
999
|
}
|
|
768
1000
|
catch {
|
|
769
1001
|
return value;
|
|
770
1002
|
}
|
|
771
1003
|
}
|
|
772
1004
|
async snapshotFull() {
|
|
773
|
-
const
|
|
1005
|
+
const client = await this.ensureClient();
|
|
774
1006
|
try {
|
|
775
|
-
return await
|
|
1007
|
+
return await client.snapshotFull();
|
|
776
1008
|
}
|
|
777
1009
|
catch {
|
|
778
1010
|
return { def: undefined, environment: {}, inputs: {}, outputs: {} };
|
|
@@ -780,17 +1012,17 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
780
1012
|
}
|
|
781
1013
|
async applySnapshotFull(payload) {
|
|
782
1014
|
// Hydrate local cache first so UI can display values immediately
|
|
783
|
-
this.
|
|
1015
|
+
this.hydrateSnapshotFull(payload);
|
|
784
1016
|
// Then sync with backend
|
|
785
|
-
const
|
|
786
|
-
await
|
|
1017
|
+
const client = await this.ensureClient();
|
|
1018
|
+
await client.applySnapshotFull(payload);
|
|
787
1019
|
}
|
|
788
1020
|
/**
|
|
789
1021
|
* Hydrates the local valueCache from a snapshot and emits value events.
|
|
790
1022
|
* This ensures the UI can display inputs/outputs immediately without waiting
|
|
791
1023
|
* for value events from the remote backend.
|
|
792
1024
|
*/
|
|
793
|
-
|
|
1025
|
+
hydrateSnapshotFull(snapshot) {
|
|
794
1026
|
// Hydrate inputs
|
|
795
1027
|
for (const [nodeId, map] of Object.entries(snapshot.inputs || {})) {
|
|
796
1028
|
for (const [handle, value] of Object.entries(map || {})) {
|
|
@@ -813,20 +1045,25 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
813
1045
|
}
|
|
814
1046
|
}
|
|
815
1047
|
setEnvironment(env, opts) {
|
|
816
|
-
|
|
817
|
-
if (
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1048
|
+
// Use client if available, otherwise ensure client and then set environment
|
|
1049
|
+
if (this.client) {
|
|
1050
|
+
this.client.setEnvironment(env, opts).catch(() => { });
|
|
1051
|
+
}
|
|
1052
|
+
else {
|
|
1053
|
+
// If client not ready yet, ensure it and then set environment
|
|
1054
|
+
this.ensureClient()
|
|
1055
|
+
.then((client) => {
|
|
1056
|
+
client.setEnvironment(env, opts).catch(() => { });
|
|
1057
|
+
})
|
|
1058
|
+
.catch(() => { });
|
|
1059
|
+
}
|
|
825
1060
|
}
|
|
826
1061
|
getEnvironment() {
|
|
827
|
-
//
|
|
828
|
-
//
|
|
829
|
-
//
|
|
1062
|
+
// Interface requires sync return, but RuntimeApiClient.getEnvironment() is async.
|
|
1063
|
+
// Returns undefined synchronously; callers needing the actual value should:
|
|
1064
|
+
// - Use snapshotFull() which includes environment
|
|
1065
|
+
// - Call client.getEnvironment() directly if they have access to the client
|
|
1066
|
+
// This is a limitation of the sync interface signature.
|
|
830
1067
|
return undefined;
|
|
831
1068
|
}
|
|
832
1069
|
getOutputs(def) {
|
|
@@ -890,183 +1127,37 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
890
1127
|
return out;
|
|
891
1128
|
}
|
|
892
1129
|
dispose() {
|
|
1130
|
+
// Idempotent: allow multiple calls safely
|
|
1131
|
+
if (this.disposed)
|
|
1132
|
+
return;
|
|
1133
|
+
this.disposed = true;
|
|
893
1134
|
super.dispose();
|
|
894
|
-
|
|
895
|
-
this.
|
|
1135
|
+
// Clear client promise if any
|
|
1136
|
+
this.clientPromise = undefined;
|
|
1137
|
+
// Unsubscribe from custom events and transport status
|
|
1138
|
+
if (this.customEventUnsubscribe) {
|
|
1139
|
+
this.customEventUnsubscribe();
|
|
1140
|
+
this.customEventUnsubscribe = undefined;
|
|
1141
|
+
}
|
|
1142
|
+
if (this.transportStatusUnsubscribe) {
|
|
1143
|
+
this.transportStatusUnsubscribe();
|
|
1144
|
+
this.transportStatusUnsubscribe = undefined;
|
|
1145
|
+
}
|
|
1146
|
+
// Dispose client (which will close transport)
|
|
1147
|
+
const clientToDispose = this.client;
|
|
1148
|
+
this.client = undefined;
|
|
896
1149
|
this.registryFetched = false; // Reset so registry is fetched again on reconnect
|
|
897
1150
|
this.registryFetching = false; // Reset fetching state
|
|
1151
|
+
if (clientToDispose) {
|
|
1152
|
+
clientToDispose.dispose().catch((err) => {
|
|
1153
|
+
console.warn("[RemoteGraphRunner] Error disposing client:", err);
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
898
1156
|
this.emit("transport", {
|
|
899
1157
|
state: "disconnected",
|
|
900
1158
|
kind: this.backend.kind,
|
|
901
1159
|
});
|
|
902
1160
|
}
|
|
903
|
-
/**
|
|
904
|
-
* Fetch full registry description from remote and register it locally.
|
|
905
|
-
* Called automatically on first connection with retry mechanism.
|
|
906
|
-
*/
|
|
907
|
-
async fetchRegistry(runner, attempt = 1) {
|
|
908
|
-
if (this.registryFetching) {
|
|
909
|
-
// Already fetching, don't start another fetch
|
|
910
|
-
return;
|
|
911
|
-
}
|
|
912
|
-
this.registryFetching = true;
|
|
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
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
// Ensure remote transport/runner
|
|
1016
|
-
async ensureRemoteRunner() {
|
|
1017
|
-
if (this.runner)
|
|
1018
|
-
return this.runner;
|
|
1019
|
-
let transport;
|
|
1020
|
-
const kind = this.backend.kind;
|
|
1021
|
-
const backend = this.backend;
|
|
1022
|
-
const connectOptions = backend.connectOptions;
|
|
1023
|
-
this.emit("transport", { state: "connecting", kind });
|
|
1024
|
-
if (backend.kind === "remote-http") {
|
|
1025
|
-
if (!sparkRemote.HttpPollingTransport)
|
|
1026
|
-
throw new Error("HttpPollingTransport not available");
|
|
1027
|
-
transport = new sparkRemote.HttpPollingTransport(backend.baseUrl);
|
|
1028
|
-
await transport.connect(connectOptions);
|
|
1029
|
-
}
|
|
1030
|
-
else if (backend.kind === "remote-ws") {
|
|
1031
|
-
if (!sparkRemote.WebSocketTransport)
|
|
1032
|
-
throw new Error("WebSocketTransport not available");
|
|
1033
|
-
transport = new sparkRemote.WebSocketTransport(backend.url);
|
|
1034
|
-
await transport.connect(connectOptions);
|
|
1035
|
-
}
|
|
1036
|
-
else {
|
|
1037
|
-
throw new Error("Remote backend not configured");
|
|
1038
|
-
}
|
|
1039
|
-
// Subscribe to custom events if handler provided
|
|
1040
|
-
if (backend.onCustomEvent) {
|
|
1041
|
-
transport.subscribe((event) => {
|
|
1042
|
-
// Filter out standard runtime events, pass others to custom handler
|
|
1043
|
-
const msg = event.message;
|
|
1044
|
-
if (msg && typeof msg === "object" && "type" in msg) {
|
|
1045
|
-
const type = msg.type;
|
|
1046
|
-
// Standard runtime events: stats, value, error, invalidate
|
|
1047
|
-
// Custom events are anything else (e.g., flow-opened, flow-latest)
|
|
1048
|
-
if (!["stats", "value", "error", "invalidate"].includes(type)) {
|
|
1049
|
-
backend.onCustomEvent?.(event);
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
});
|
|
1053
|
-
}
|
|
1054
|
-
const runner = new sparkRemote.RemoteRunner(transport);
|
|
1055
|
-
this.runner = runner;
|
|
1056
|
-
this.transport = transport;
|
|
1057
|
-
this.valueCache.clear();
|
|
1058
|
-
this.listenersBound = false;
|
|
1059
|
-
this.emit("transport", { state: "connected", kind });
|
|
1060
|
-
// Auto-fetch registry on first connection (only once)
|
|
1061
|
-
if (!this.registryFetched && !this.registryFetching) {
|
|
1062
|
-
// Log loading state (UI can listen to transport status for loading indication)
|
|
1063
|
-
console.info("Loading registry from remote...");
|
|
1064
|
-
this.fetchRegistry(runner).catch(() => {
|
|
1065
|
-
// Error handling is done inside fetchRegistry
|
|
1066
|
-
});
|
|
1067
|
-
}
|
|
1068
|
-
return runner;
|
|
1069
|
-
}
|
|
1070
1161
|
}
|
|
1071
1162
|
|
|
1072
1163
|
function useWorkbenchBridge(wb) {
|
|
@@ -1734,6 +1825,33 @@ function getNodeBorderClassNames(args) {
|
|
|
1734
1825
|
: "";
|
|
1735
1826
|
return [borderWidth, borderStyle, borderColor, ring].join(" ").trim();
|
|
1736
1827
|
}
|
|
1828
|
+
/**
|
|
1829
|
+
* Shared utility to generate handle className based on validation and value state.
|
|
1830
|
+
* - Linked handles (with inbound edges) get black borders
|
|
1831
|
+
* - Handles with values (but not linked) get darker gray borders
|
|
1832
|
+
* - Handles with only defaults (no value, not linked) get lighter gray borders
|
|
1833
|
+
* - Validation errors (red/amber) take precedence over value-based styling.
|
|
1834
|
+
*/
|
|
1835
|
+
function getHandleClassName(args) {
|
|
1836
|
+
const { kind, id, validation } = args;
|
|
1837
|
+
const vIssues = (kind === "input" ? validation.inputs : validation.outputs)?.filter((v) => v.handle === id) || [];
|
|
1838
|
+
const hasAny = vIssues.length > 0;
|
|
1839
|
+
const hasErr = vIssues.some((v) => v.level === "error");
|
|
1840
|
+
// Determine border color based on priority:
|
|
1841
|
+
// 1. Validation errors (red/amber) - highest priority
|
|
1842
|
+
// 2. Gray border
|
|
1843
|
+
let borderColor;
|
|
1844
|
+
if (hasAny && hasErr) {
|
|
1845
|
+
borderColor = "!border-red-500";
|
|
1846
|
+
}
|
|
1847
|
+
else if (hasAny) {
|
|
1848
|
+
borderColor = "!border-amber-500";
|
|
1849
|
+
}
|
|
1850
|
+
else {
|
|
1851
|
+
borderColor = "!border-gray-500 dark:!border-gray-400";
|
|
1852
|
+
}
|
|
1853
|
+
return cx("!w-3 !h-3 !bg-white !dark:bg-stone-900", borderColor, kind === "output" && "!rounded-none");
|
|
1854
|
+
}
|
|
1737
1855
|
|
|
1738
1856
|
const WorkbenchContext = React.createContext(null);
|
|
1739
1857
|
function useWorkbenchContext() {
|
|
@@ -2456,7 +2574,7 @@ function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWork
|
|
|
2456
2574
|
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
2575
|
}
|
|
2458
2576
|
|
|
2459
|
-
function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toString,
|
|
2577
|
+
function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toString, contextPanel, setInput, }) {
|
|
2460
2578
|
const safeToString = (typeId, value) => {
|
|
2461
2579
|
try {
|
|
2462
2580
|
if (typeof toString === "function") {
|
|
@@ -2834,15 +2952,17 @@ function DefaultNodeContent({ data, isConnectable, }) {
|
|
|
2834
2952
|
const status = data.status ?? { activeRuns: 0 };
|
|
2835
2953
|
const validation = data.validation ?? {
|
|
2836
2954
|
inputs: [],
|
|
2837
|
-
outputs: []
|
|
2955
|
+
outputs: [],
|
|
2956
|
+
issues: [],
|
|
2957
|
+
};
|
|
2838
2958
|
const isRunning = !!status.activeRuns;
|
|
2839
2959
|
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 }) => {
|
|
2960
|
+
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({
|
|
2961
|
+
kind,
|
|
2962
|
+
id,
|
|
2963
|
+
validation,
|
|
2964
|
+
inputConnected: data.inputConnected,
|
|
2965
|
+
}), renderLabel: ({ kind, id: handleId }) => {
|
|
2846
2966
|
const entries = kind === "input" ? inputEntries : outputEntries;
|
|
2847
2967
|
const entry = entries.find((e) => e.id === handleId);
|
|
2848
2968
|
if (!entry)
|
|
@@ -3448,9 +3568,6 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3448
3568
|
state: "local",
|
|
3449
3569
|
});
|
|
3450
3570
|
const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
|
|
3451
|
-
selectedNode
|
|
3452
|
-
? registry.nodes.get(selectedNode.typeId)
|
|
3453
|
-
: undefined;
|
|
3454
3571
|
const effectiveHandles = selectedNode
|
|
3455
3572
|
? computeEffectiveHandles(selectedNode, registry)
|
|
3456
3573
|
: { inputs: {}, outputs: {}, inputDefaults: {} };
|
|
@@ -3741,7 +3858,12 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3741
3858
|
const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId && e.target.handle === handle);
|
|
3742
3859
|
if (isLinked)
|
|
3743
3860
|
return;
|
|
3744
|
-
|
|
3861
|
+
// If raw is undefined, pass it through to delete the input value
|
|
3862
|
+
if (raw === undefined) {
|
|
3863
|
+
runner.setInput(selectedNodeId, handle, undefined);
|
|
3864
|
+
return;
|
|
3865
|
+
}
|
|
3866
|
+
const typeId = sparkGraph.getInputTypeId(effectiveHandles.inputs, handle);
|
|
3745
3867
|
let value = raw;
|
|
3746
3868
|
const parseArray = (s, map) => {
|
|
3747
3869
|
const str = String(s).trim();
|
|
@@ -3950,7 +4072,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3950
4072
|
const message = err instanceof Error ? err.message : String(err);
|
|
3951
4073
|
alert(message);
|
|
3952
4074
|
}
|
|
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,
|
|
4075
|
+
}, 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
4076
|
}
|
|
3955
4077
|
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
4078
|
const [registry, setRegistry] = React.useState(sparkGraph.createSimpleGraphRegistry());
|
|
@@ -4046,9 +4168,14 @@ exports.WorkbenchCanvas = WorkbenchCanvas;
|
|
|
4046
4168
|
exports.WorkbenchContext = WorkbenchContext;
|
|
4047
4169
|
exports.WorkbenchProvider = WorkbenchProvider;
|
|
4048
4170
|
exports.WorkbenchStudio = WorkbenchStudio;
|
|
4171
|
+
exports.computeEffectiveHandles = computeEffectiveHandles;
|
|
4172
|
+
exports.countVisibleHandles = countVisibleHandles;
|
|
4173
|
+
exports.estimateNodeSize = estimateNodeSize;
|
|
4049
4174
|
exports.formatDataUrlAsLabel = formatDataUrlAsLabel;
|
|
4050
4175
|
exports.formatDeclaredTypeSignature = formatDeclaredTypeSignature;
|
|
4176
|
+
exports.getHandleClassName = getHandleClassName;
|
|
4051
4177
|
exports.getNodeBorderClassNames = getNodeBorderClassNames;
|
|
4178
|
+
exports.layoutNode = layoutNode;
|
|
4052
4179
|
exports.preformatValueForDisplay = preformatValueForDisplay;
|
|
4053
4180
|
exports.prettyHandle = prettyHandle;
|
|
4054
4181
|
exports.resolveOutputDisplay = resolveOutputDisplay;
|