@bian-womp/spark-workbench 0.2.36 → 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 +405 -221
- 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/context/WorkbenchContext.d.ts +4 -1
- package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
- package/lib/cjs/src/misc/mapping.d.ts +19 -0
- package/lib/cjs/src/misc/mapping.d.ts.map +1 -1
- package/lib/cjs/src/runtime/AbstractGraphRunner.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 +402 -223
- 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/context/WorkbenchContext.d.ts +4 -1
- package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -1
- package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
- package/lib/esm/src/misc/mapping.d.ts +19 -0
- package/lib/esm/src/misc/mapping.d.ts.map +1 -1
- package/lib/esm/src/runtime/AbstractGraphRunner.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 {
|
|
@@ -343,7 +343,12 @@ class AbstractGraphRunner {
|
|
|
343
343
|
setInput(nodeId, handle, value) {
|
|
344
344
|
if (!this.stagedInputs[nodeId])
|
|
345
345
|
this.stagedInputs[nodeId] = {};
|
|
346
|
-
|
|
346
|
+
if (value === undefined) {
|
|
347
|
+
delete this.stagedInputs[nodeId][handle];
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
this.stagedInputs[nodeId][handle] = value;
|
|
351
|
+
}
|
|
347
352
|
if (this.engine) {
|
|
348
353
|
this.engine.setInput(nodeId, handle, value);
|
|
349
354
|
}
|
|
@@ -361,7 +366,14 @@ class AbstractGraphRunner {
|
|
|
361
366
|
return;
|
|
362
367
|
if (!this.stagedInputs[nodeId])
|
|
363
368
|
this.stagedInputs[nodeId] = {};
|
|
364
|
-
|
|
369
|
+
for (const [handle, value] of Object.entries(inputs)) {
|
|
370
|
+
if (value === undefined) {
|
|
371
|
+
delete this.stagedInputs[nodeId][handle];
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
this.stagedInputs[nodeId][handle] = value;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
365
377
|
if (this.engine) {
|
|
366
378
|
// Running: set all inputs
|
|
367
379
|
this.engine.setInputs(nodeId, inputs);
|
|
@@ -590,8 +602,200 @@ class LocalGraphRunner extends AbstractGraphRunner {
|
|
|
590
602
|
}
|
|
591
603
|
|
|
592
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
|
+
}
|
|
593
796
|
constructor(registry, backend) {
|
|
594
797
|
super(registry, backend);
|
|
798
|
+
this.disposed = false;
|
|
595
799
|
this.valueCache = new Map();
|
|
596
800
|
this.listenersBound = false;
|
|
597
801
|
this.registryFetched = false;
|
|
@@ -600,8 +804,8 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
600
804
|
this.INITIAL_RETRY_DELAY_MS = 1000; // 1 second
|
|
601
805
|
// Auto-handle registry-changed invalidations from remote
|
|
602
806
|
// We listen on invalidate and if reason matches, we rehydrate registry and emit a registry event
|
|
603
|
-
this.
|
|
604
|
-
const eng =
|
|
807
|
+
this.ensureClient().then(async (client) => {
|
|
808
|
+
const eng = client.getEngine();
|
|
605
809
|
if (!this.listenersBound) {
|
|
606
810
|
eng.on("invalidate", async (e) => {
|
|
607
811
|
if (e.reason === "registry-changed") {
|
|
@@ -665,9 +869,9 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
665
869
|
}
|
|
666
870
|
update(def) {
|
|
667
871
|
// Remote: forward update; ignore errors (fire-and-forget)
|
|
668
|
-
this.
|
|
872
|
+
this.ensureClient().then(async (client) => {
|
|
669
873
|
try {
|
|
670
|
-
await
|
|
874
|
+
await client.update(def);
|
|
671
875
|
this.emit("invalidate", { reason: "graph-updated" });
|
|
672
876
|
this.lastDef = def;
|
|
673
877
|
}
|
|
@@ -677,14 +881,14 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
677
881
|
launch(def, opts) {
|
|
678
882
|
super.launch(def, opts);
|
|
679
883
|
// Remote: build remotely then launch
|
|
680
|
-
this.
|
|
681
|
-
await
|
|
884
|
+
this.ensureClient().then(async (client) => {
|
|
885
|
+
await client.build(def);
|
|
682
886
|
// Signal UI after remote build as well
|
|
683
887
|
this.emit("invalidate", { reason: "graph-built" });
|
|
684
888
|
this.lastDef = def;
|
|
685
889
|
// Hydrate current remote inputs/outputs (including defaults) into cache
|
|
686
890
|
try {
|
|
687
|
-
const snap = await
|
|
891
|
+
const snap = await client.snapshot();
|
|
688
892
|
for (const [nodeId, map] of Object.entries(snap.inputs || {})) {
|
|
689
893
|
for (const [handle, value] of Object.entries(map || {})) {
|
|
690
894
|
this.valueCache.set(`${nodeId}.${handle}`, {
|
|
@@ -707,7 +911,47 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
707
911
|
catch {
|
|
708
912
|
console.error("Failed to hydrate remote inputs/outputs");
|
|
709
913
|
}
|
|
710
|
-
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();
|
|
711
955
|
if (!this.listenersBound) {
|
|
712
956
|
eng.on("value", (e) => {
|
|
713
957
|
this.valueCache.set(`${e.nodeId}.${e.handle}`, {
|
|
@@ -741,26 +985,26 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
741
985
|
console.warn("Unsupported operation for remote runner");
|
|
742
986
|
}
|
|
743
987
|
triggerExternal(nodeId, event) {
|
|
744
|
-
this.
|
|
988
|
+
this.ensureClient().then(async (client) => {
|
|
745
989
|
try {
|
|
746
|
-
await
|
|
990
|
+
await client.getEngine().triggerExternal(nodeId, event);
|
|
747
991
|
}
|
|
748
992
|
catch { }
|
|
749
993
|
});
|
|
750
994
|
}
|
|
751
995
|
async coerce(from, to, value) {
|
|
752
|
-
const
|
|
996
|
+
const client = await this.ensureClient();
|
|
753
997
|
try {
|
|
754
|
-
return await
|
|
998
|
+
return await client.coerce(from, to, value);
|
|
755
999
|
}
|
|
756
1000
|
catch {
|
|
757
1001
|
return value;
|
|
758
1002
|
}
|
|
759
1003
|
}
|
|
760
1004
|
async snapshotFull() {
|
|
761
|
-
const
|
|
1005
|
+
const client = await this.ensureClient();
|
|
762
1006
|
try {
|
|
763
|
-
return await
|
|
1007
|
+
return await client.snapshotFull();
|
|
764
1008
|
}
|
|
765
1009
|
catch {
|
|
766
1010
|
return { def: undefined, environment: {}, inputs: {}, outputs: {} };
|
|
@@ -768,17 +1012,17 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
768
1012
|
}
|
|
769
1013
|
async applySnapshotFull(payload) {
|
|
770
1014
|
// Hydrate local cache first so UI can display values immediately
|
|
771
|
-
this.
|
|
1015
|
+
this.hydrateSnapshotFull(payload);
|
|
772
1016
|
// Then sync with backend
|
|
773
|
-
const
|
|
774
|
-
await
|
|
1017
|
+
const client = await this.ensureClient();
|
|
1018
|
+
await client.applySnapshotFull(payload);
|
|
775
1019
|
}
|
|
776
1020
|
/**
|
|
777
1021
|
* Hydrates the local valueCache from a snapshot and emits value events.
|
|
778
1022
|
* This ensures the UI can display inputs/outputs immediately without waiting
|
|
779
1023
|
* for value events from the remote backend.
|
|
780
1024
|
*/
|
|
781
|
-
|
|
1025
|
+
hydrateSnapshotFull(snapshot) {
|
|
782
1026
|
// Hydrate inputs
|
|
783
1027
|
for (const [nodeId, map] of Object.entries(snapshot.inputs || {})) {
|
|
784
1028
|
for (const [handle, value] of Object.entries(map || {})) {
|
|
@@ -801,20 +1045,25 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
801
1045
|
}
|
|
802
1046
|
}
|
|
803
1047
|
setEnvironment(env, opts) {
|
|
804
|
-
|
|
805
|
-
if (
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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
|
+
}
|
|
813
1060
|
}
|
|
814
1061
|
getEnvironment() {
|
|
815
|
-
//
|
|
816
|
-
//
|
|
817
|
-
//
|
|
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.
|
|
818
1067
|
return undefined;
|
|
819
1068
|
}
|
|
820
1069
|
getOutputs(def) {
|
|
@@ -878,183 +1127,37 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
878
1127
|
return out;
|
|
879
1128
|
}
|
|
880
1129
|
dispose() {
|
|
1130
|
+
// Idempotent: allow multiple calls safely
|
|
1131
|
+
if (this.disposed)
|
|
1132
|
+
return;
|
|
1133
|
+
this.disposed = true;
|
|
881
1134
|
super.dispose();
|
|
882
|
-
|
|
883
|
-
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;
|
|
884
1149
|
this.registryFetched = false; // Reset so registry is fetched again on reconnect
|
|
885
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
|
+
}
|
|
886
1156
|
this.emit("transport", {
|
|
887
1157
|
state: "disconnected",
|
|
888
1158
|
kind: this.backend.kind,
|
|
889
1159
|
});
|
|
890
1160
|
}
|
|
891
|
-
/**
|
|
892
|
-
* Fetch full registry description from remote and register it locally.
|
|
893
|
-
* Called automatically on first connection with retry mechanism.
|
|
894
|
-
*/
|
|
895
|
-
async fetchRegistry(runner, attempt = 1) {
|
|
896
|
-
if (this.registryFetching) {
|
|
897
|
-
// Already fetching, don't start another fetch
|
|
898
|
-
return;
|
|
899
|
-
}
|
|
900
|
-
this.registryFetching = true;
|
|
901
|
-
try {
|
|
902
|
-
const desc = await runner.describeRegistry();
|
|
903
|
-
// Register types
|
|
904
|
-
for (const t of desc.types) {
|
|
905
|
-
if (t.options) {
|
|
906
|
-
this.registry.registerEnum({
|
|
907
|
-
id: t.id,
|
|
908
|
-
options: t.options,
|
|
909
|
-
bakeTarget: t.bakeTarget,
|
|
910
|
-
});
|
|
911
|
-
}
|
|
912
|
-
else {
|
|
913
|
-
if (!this.registry.types.has(t.id)) {
|
|
914
|
-
this.registry.registerType({
|
|
915
|
-
id: t.id,
|
|
916
|
-
displayName: t.displayName,
|
|
917
|
-
validate: (_v) => true,
|
|
918
|
-
bakeTarget: t.bakeTarget,
|
|
919
|
-
});
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
// Register categories
|
|
924
|
-
for (const c of desc.categories || []) {
|
|
925
|
-
if (!this.registry.categories.has(c.id)) {
|
|
926
|
-
// Create placeholder category descriptor
|
|
927
|
-
const category = {
|
|
928
|
-
id: c.id,
|
|
929
|
-
displayName: c.displayName,
|
|
930
|
-
createRuntime: () => ({
|
|
931
|
-
async onInputsChanged() { },
|
|
932
|
-
}),
|
|
933
|
-
policy: { asyncConcurrency: "switch" },
|
|
934
|
-
};
|
|
935
|
-
this.registry.categories.register(category);
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
// Register coercions
|
|
939
|
-
for (const c of desc.coercions) {
|
|
940
|
-
if (c.async) {
|
|
941
|
-
this.registry.registerAsyncCoercion(c.from, c.to, async (v) => v, {
|
|
942
|
-
nonTransitive: c.nonTransitive,
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
|
-
else {
|
|
946
|
-
this.registry.registerCoercion(c.from, c.to, (v) => v, {
|
|
947
|
-
nonTransitive: c.nonTransitive,
|
|
948
|
-
});
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
// Register nodes
|
|
952
|
-
for (const n of desc.nodes) {
|
|
953
|
-
if (!this.registry.nodes.has(n.id)) {
|
|
954
|
-
this.registry.registerNode({
|
|
955
|
-
id: n.id,
|
|
956
|
-
categoryId: n.categoryId,
|
|
957
|
-
displayName: n.displayName,
|
|
958
|
-
inputs: n.inputs || {},
|
|
959
|
-
outputs: n.outputs || {},
|
|
960
|
-
impl: () => { },
|
|
961
|
-
});
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
this.registryFetched = true;
|
|
965
|
-
this.registryFetching = false;
|
|
966
|
-
this.emit("registry", this.registry);
|
|
967
|
-
}
|
|
968
|
-
catch (err) {
|
|
969
|
-
this.registryFetching = false;
|
|
970
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
971
|
-
// Retry with exponential backoff if attempts remaining
|
|
972
|
-
if (attempt < this.MAX_REGISTRY_FETCH_ATTEMPTS) {
|
|
973
|
-
const delayMs = this.INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
|
|
974
|
-
console.warn(`Failed to fetch registry (attempt ${attempt}/${this.MAX_REGISTRY_FETCH_ATTEMPTS}), retrying in ${delayMs}ms...`, error);
|
|
975
|
-
// Emit error event for UI feedback
|
|
976
|
-
this.emit("error", {
|
|
977
|
-
kind: "registry",
|
|
978
|
-
message: `Registry fetch failed (attempt ${attempt}/${this.MAX_REGISTRY_FETCH_ATTEMPTS}), retrying...`,
|
|
979
|
-
err: error,
|
|
980
|
-
attempt,
|
|
981
|
-
maxAttempts: this.MAX_REGISTRY_FETCH_ATTEMPTS,
|
|
982
|
-
});
|
|
983
|
-
// Retry after delay
|
|
984
|
-
setTimeout(() => {
|
|
985
|
-
this.fetchRegistry(runner, attempt + 1).catch(() => {
|
|
986
|
-
// Final failure handled below
|
|
987
|
-
});
|
|
988
|
-
}, delayMs);
|
|
989
|
-
}
|
|
990
|
-
else {
|
|
991
|
-
// Max attempts reached, emit final error
|
|
992
|
-
console.error(`Failed to fetch registry after ${this.MAX_REGISTRY_FETCH_ATTEMPTS} attempts:`, error);
|
|
993
|
-
this.emit("error", {
|
|
994
|
-
kind: "registry",
|
|
995
|
-
message: `Failed to fetch registry after ${this.MAX_REGISTRY_FETCH_ATTEMPTS} attempts. Please check your connection and try refreshing.`,
|
|
996
|
-
err: error,
|
|
997
|
-
attempt: this.MAX_REGISTRY_FETCH_ATTEMPTS,
|
|
998
|
-
maxAttempts: this.MAX_REGISTRY_FETCH_ATTEMPTS,
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
// Ensure remote transport/runner
|
|
1004
|
-
async ensureRemoteRunner() {
|
|
1005
|
-
if (this.runner)
|
|
1006
|
-
return this.runner;
|
|
1007
|
-
let transport;
|
|
1008
|
-
const kind = this.backend.kind;
|
|
1009
|
-
const backend = this.backend;
|
|
1010
|
-
const connectOptions = backend.connectOptions;
|
|
1011
|
-
this.emit("transport", { state: "connecting", kind });
|
|
1012
|
-
if (backend.kind === "remote-http") {
|
|
1013
|
-
if (!sparkRemote.HttpPollingTransport)
|
|
1014
|
-
throw new Error("HttpPollingTransport not available");
|
|
1015
|
-
transport = new sparkRemote.HttpPollingTransport(backend.baseUrl);
|
|
1016
|
-
await transport.connect(connectOptions);
|
|
1017
|
-
}
|
|
1018
|
-
else if (backend.kind === "remote-ws") {
|
|
1019
|
-
if (!sparkRemote.WebSocketTransport)
|
|
1020
|
-
throw new Error("WebSocketTransport not available");
|
|
1021
|
-
transport = new sparkRemote.WebSocketTransport(backend.url);
|
|
1022
|
-
await transport.connect(connectOptions);
|
|
1023
|
-
}
|
|
1024
|
-
else {
|
|
1025
|
-
throw new Error("Remote backend not configured");
|
|
1026
|
-
}
|
|
1027
|
-
// Subscribe to custom events if handler provided
|
|
1028
|
-
if (backend.onCustomEvent) {
|
|
1029
|
-
transport.subscribe((event) => {
|
|
1030
|
-
// Filter out standard runtime events, pass others to custom handler
|
|
1031
|
-
const msg = event.message;
|
|
1032
|
-
if (msg && typeof msg === "object" && "type" in msg) {
|
|
1033
|
-
const type = msg.type;
|
|
1034
|
-
// Standard runtime events: stats, value, error, invalidate
|
|
1035
|
-
// Custom events are anything else (e.g., flow-opened, flow-latest)
|
|
1036
|
-
if (!["stats", "value", "error", "invalidate"].includes(type)) {
|
|
1037
|
-
backend.onCustomEvent?.(event);
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
});
|
|
1041
|
-
}
|
|
1042
|
-
const runner = new sparkRemote.RemoteRunner(transport);
|
|
1043
|
-
this.runner = runner;
|
|
1044
|
-
this.transport = transport;
|
|
1045
|
-
this.valueCache.clear();
|
|
1046
|
-
this.listenersBound = false;
|
|
1047
|
-
this.emit("transport", { state: "connected", kind });
|
|
1048
|
-
// Auto-fetch registry on first connection (only once)
|
|
1049
|
-
if (!this.registryFetched && !this.registryFetching) {
|
|
1050
|
-
// Log loading state (UI can listen to transport status for loading indication)
|
|
1051
|
-
console.info("Loading registry from remote...");
|
|
1052
|
-
this.fetchRegistry(runner).catch(() => {
|
|
1053
|
-
// Error handling is done inside fetchRegistry
|
|
1054
|
-
});
|
|
1055
|
-
}
|
|
1056
|
-
return runner;
|
|
1057
|
-
}
|
|
1058
1161
|
}
|
|
1059
1162
|
|
|
1060
1163
|
function useWorkbenchBridge(wb) {
|
|
@@ -1722,6 +1825,33 @@ function getNodeBorderClassNames(args) {
|
|
|
1722
1825
|
: "";
|
|
1723
1826
|
return [borderWidth, borderStyle, borderColor, ring].join(" ").trim();
|
|
1724
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
|
+
}
|
|
1725
1855
|
|
|
1726
1856
|
const WorkbenchContext = React.createContext(null);
|
|
1727
1857
|
function useWorkbenchContext() {
|
|
@@ -1738,14 +1868,19 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
1738
1868
|
const clearEvents = React.useCallback(() => setEvents([]), []);
|
|
1739
1869
|
const [systemErrors, setSystemErrors] = React.useState([]);
|
|
1740
1870
|
const [registryErrors, setRegistryErrors] = React.useState([]);
|
|
1871
|
+
const [inputValidationErrors, setInputValidationErrors] = React.useState([]);
|
|
1741
1872
|
const clearSystemErrors = React.useCallback(() => setSystemErrors([]), []);
|
|
1742
1873
|
const clearRegistryErrors = React.useCallback(() => setRegistryErrors([]), []);
|
|
1874
|
+
const clearInputValidationErrors = React.useCallback(() => setInputValidationErrors([]), []);
|
|
1743
1875
|
const removeSystemError = React.useCallback((index) => {
|
|
1744
1876
|
setSystemErrors((prev) => prev.filter((_, idx) => idx !== index));
|
|
1745
1877
|
}, []);
|
|
1746
1878
|
const removeRegistryError = React.useCallback((index) => {
|
|
1747
1879
|
setRegistryErrors((prev) => prev.filter((_, idx) => idx !== index));
|
|
1748
1880
|
}, []);
|
|
1881
|
+
const removeInputValidationError = React.useCallback((index) => {
|
|
1882
|
+
setInputValidationErrors((prev) => prev.filter((_, idx) => idx !== index));
|
|
1883
|
+
}, []);
|
|
1749
1884
|
// Fallback progress animation: drive progress to 100% over ~2 minutes
|
|
1750
1885
|
const FALLBACK_TOTAL_MS = 2 * 60 * 1000;
|
|
1751
1886
|
const [fallbackStarts, setFallbackStarts] = React.useState({});
|
|
@@ -1801,7 +1936,8 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
1801
1936
|
const out = {};
|
|
1802
1937
|
// Local: runtimeTypeId is not stored; derive from typed wrapper in outputsMap
|
|
1803
1938
|
for (const n of def.nodes) {
|
|
1804
|
-
const
|
|
1939
|
+
const effectiveHandles = computeEffectiveHandles(n, registry);
|
|
1940
|
+
const outputsDecl = effectiveHandles.outputs;
|
|
1805
1941
|
const handles = Object.keys(outputsDecl);
|
|
1806
1942
|
const cur = {};
|
|
1807
1943
|
for (const h of handles) {
|
|
@@ -1962,10 +2098,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
1962
2098
|
const offRunnerValue = runner.on("value", (e) => {
|
|
1963
2099
|
if (e?.io === "input") {
|
|
1964
2100
|
const nodeId = e?.nodeId;
|
|
2101
|
+
const handle = e?.handle;
|
|
1965
2102
|
setNodeStatus((s) => ({
|
|
1966
2103
|
...s,
|
|
1967
2104
|
[nodeId]: { ...s[nodeId], invalidated: true },
|
|
1968
2105
|
}));
|
|
2106
|
+
// Clear validation errors for this input when a valid value is set
|
|
2107
|
+
setInputValidationErrors((prev) => prev.filter((err) => !(err.nodeId === nodeId && err.handle === handle)));
|
|
1969
2108
|
}
|
|
1970
2109
|
return add("runner", "value")(e);
|
|
1971
2110
|
});
|
|
@@ -1974,6 +2113,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
1974
2113
|
const nodeError = e;
|
|
1975
2114
|
const registryError = e;
|
|
1976
2115
|
const systemError = e;
|
|
2116
|
+
const inputValidationError = e;
|
|
1977
2117
|
if (edgeError.kind === "edge-convert") {
|
|
1978
2118
|
const edgeId = edgeError.edgeId;
|
|
1979
2119
|
setEdgeStatus((s) => ({
|
|
@@ -2009,6 +2149,18 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2009
2149
|
return [...prev, registryError];
|
|
2010
2150
|
});
|
|
2011
2151
|
}
|
|
2152
|
+
else if (inputValidationError.kind === "input-validation") {
|
|
2153
|
+
// Track input validation errors for UI display
|
|
2154
|
+
setInputValidationErrors((prev) => {
|
|
2155
|
+
// Avoid duplicates by checking nodeId, handle, and typeId
|
|
2156
|
+
if (prev.some((err) => err.nodeId === inputValidationError.nodeId &&
|
|
2157
|
+
err.handle === inputValidationError.handle &&
|
|
2158
|
+
err.typeId === inputValidationError.typeId)) {
|
|
2159
|
+
return prev;
|
|
2160
|
+
}
|
|
2161
|
+
return [...prev, inputValidationError];
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2012
2164
|
else if (systemError.kind === "system") {
|
|
2013
2165
|
// Track custom errors for UI display
|
|
2014
2166
|
setSystemErrors((prev) => {
|
|
@@ -2132,7 +2284,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2132
2284
|
}
|
|
2133
2285
|
return add("runner", "stats")(s);
|
|
2134
2286
|
});
|
|
2135
|
-
const offWbGraphChanged = wb.on("graphChanged",
|
|
2287
|
+
const offWbGraphChanged = wb.on("graphChanged", (event) => {
|
|
2288
|
+
// Clear validation errors for removed nodes
|
|
2289
|
+
if (event.change?.type === "removeNode") {
|
|
2290
|
+
const removedNodeId = event.change.nodeId;
|
|
2291
|
+
setInputValidationErrors((prev) => prev.filter((err) => err.nodeId !== removedNodeId));
|
|
2292
|
+
}
|
|
2293
|
+
return add("workbench", "graphChanged")(event);
|
|
2294
|
+
});
|
|
2136
2295
|
const offWbGraphUiChanged = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
|
|
2137
2296
|
const offWbValidationChanged = wb.on("validationChanged", add("workbench", "validationChanged"));
|
|
2138
2297
|
// Ensure newly added nodes start as invalidated until first evaluation
|
|
@@ -2302,10 +2461,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2302
2461
|
clearEvents,
|
|
2303
2462
|
systemErrors,
|
|
2304
2463
|
registryErrors,
|
|
2464
|
+
inputValidationErrors,
|
|
2305
2465
|
clearSystemErrors,
|
|
2306
2466
|
clearRegistryErrors,
|
|
2467
|
+
clearInputValidationErrors,
|
|
2307
2468
|
removeSystemError,
|
|
2308
2469
|
removeRegistryError,
|
|
2470
|
+
removeInputValidationError,
|
|
2309
2471
|
isRunning,
|
|
2310
2472
|
engineKind,
|
|
2311
2473
|
start,
|
|
@@ -2330,10 +2492,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2330
2492
|
valuesTick,
|
|
2331
2493
|
systemErrors,
|
|
2332
2494
|
registryErrors,
|
|
2495
|
+
inputValidationErrors,
|
|
2333
2496
|
clearSystemErrors,
|
|
2334
2497
|
clearRegistryErrors,
|
|
2498
|
+
clearInputValidationErrors,
|
|
2335
2499
|
removeSystemError,
|
|
2336
2500
|
removeRegistryError,
|
|
2501
|
+
removeInputValidationError,
|
|
2337
2502
|
inputsMap,
|
|
2338
2503
|
inputDefaultsMap,
|
|
2339
2504
|
outputsMap,
|
|
@@ -2409,7 +2574,7 @@ function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWork
|
|
|
2409
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}`))) })] }));
|
|
2410
2575
|
}
|
|
2411
2576
|
|
|
2412
|
-
function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toString,
|
|
2577
|
+
function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toString, contextPanel, setInput, }) {
|
|
2413
2578
|
const safeToString = (typeId, value) => {
|
|
2414
2579
|
try {
|
|
2415
2580
|
if (typeof toString === "function") {
|
|
@@ -2421,7 +2586,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
2421
2586
|
return String(value ?? "");
|
|
2422
2587
|
}
|
|
2423
2588
|
};
|
|
2424
|
-
const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, nodeStatus, edgeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, updateEdgeType, systemErrors, registryErrors, clearSystemErrors, clearRegistryErrors, removeSystemError, removeRegistryError, } = useWorkbenchContext();
|
|
2589
|
+
const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, nodeStatus, edgeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, updateEdgeType, systemErrors, registryErrors, inputValidationErrors, clearSystemErrors, clearRegistryErrors, clearInputValidationErrors, removeSystemError, removeRegistryError, removeInputValidationError, } = useWorkbenchContext();
|
|
2425
2590
|
const nodeValidationIssues = validationByNode.issues;
|
|
2426
2591
|
const edgeValidationIssues = validationByEdge.issues;
|
|
2427
2592
|
const nodeValidationHandles = validationByNode;
|
|
@@ -2532,7 +2697,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
2532
2697
|
}
|
|
2533
2698
|
catch { }
|
|
2534
2699
|
};
|
|
2535
|
-
return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-auto`, children: [jsxRuntime.jsxs("div", { className: "flex-1 overflow-auto", children: [contextPanel && jsxRuntime.jsx("div", { className: "mb-2", children: contextPanel }), systemErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [systemErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: err.code ? `Error ${err.code}` : "Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeSystemError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), systemErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearSystemErrors, children: "Clear all" }))] })), registryErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [registryErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Registry Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), err.attempt && err.maxAttempts && (jsxRuntime.jsxs("div", { className: "text-[10px] text-amber-600 mt-1", children: ["Attempt ", err.attempt, " of ", err.maxAttempts] }))] }), jsxRuntime.jsx("button", { className: "text-amber-500 hover:text-amber-700 text-[10px] px-1", onClick: () => removeRegistryError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), registryErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-amber-600 hover:text-amber-800 underline", onClick: clearRegistryErrors, children: "Clear all" }))] })), jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsxRuntime.jsx("div", { className: "flex-1", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (jsxRuntime.jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
|
|
2700
|
+
return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-auto`, children: [jsxRuntime.jsxs("div", { className: "flex-1 overflow-auto", children: [contextPanel && jsxRuntime.jsx("div", { className: "mb-2", children: contextPanel }), inputValidationErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [inputValidationErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Input Validation Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), jsxRuntime.jsxs("div", { className: "text-[10px] text-red-600 mt-1", children: [err.nodeId, ".", err.handle, " (type: ", err.typeId, ")"] })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeInputValidationError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), inputValidationErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearInputValidationErrors, children: "Clear all" }))] })), systemErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [systemErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: err.code ? `Error ${err.code}` : "Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeSystemError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), systemErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearSystemErrors, children: "Clear all" }))] })), registryErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [registryErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Registry Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), err.attempt && err.maxAttempts && (jsxRuntime.jsxs("div", { className: "text-[10px] text-amber-600 mt-1", children: ["Attempt ", err.attempt, " of ", err.maxAttempts] }))] }), jsxRuntime.jsx("button", { className: "text-amber-500 hover:text-amber-700 text-[10px] px-1", onClick: () => removeRegistryError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), registryErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-amber-600 hover:text-amber-800 underline", onClick: clearRegistryErrors, children: "Clear all" }))] })), jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsxRuntime.jsx("div", { className: "flex-1", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (jsxRuntime.jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
|
|
2536
2701
|
e.stopPropagation();
|
|
2537
2702
|
deleteEdgeById(m.data?.edgeId);
|
|
2538
2703
|
}, title: "Delete referenced edge", children: "Delete edge" }))] }, i))) })] }))] })) : selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxRuntime.jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), (() => {
|
|
@@ -2570,13 +2735,20 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
2570
2735
|
const current = nodeInputs[h];
|
|
2571
2736
|
const hasValue = current !== undefined && current !== null;
|
|
2572
2737
|
const value = drafts[h] ?? safeToString(typeId, current);
|
|
2573
|
-
const displayValue =
|
|
2738
|
+
const displayValue = value;
|
|
2574
2739
|
const placeholder = hasDefault ? defaultStr : undefined;
|
|
2575
2740
|
const onChangeText = (text) => setDrafts((d) => ({ ...d, [h]: text }));
|
|
2576
2741
|
const commit = () => {
|
|
2577
2742
|
const draft = drafts[h];
|
|
2578
2743
|
if (draft === undefined)
|
|
2579
2744
|
return;
|
|
2745
|
+
// Only commit if draft differs from current value
|
|
2746
|
+
const currentDisplay = safeToString(typeId, current);
|
|
2747
|
+
if (draft === currentDisplay) {
|
|
2748
|
+
// No change, just sync originals without calling setInput
|
|
2749
|
+
setOriginals((o) => ({ ...o, [h]: draft }));
|
|
2750
|
+
return;
|
|
2751
|
+
}
|
|
2580
2752
|
setInput(h, draft);
|
|
2581
2753
|
setOriginals((o) => ({ ...o, [h]: draft }));
|
|
2582
2754
|
};
|
|
@@ -2780,15 +2952,17 @@ function DefaultNodeContent({ data, isConnectable, }) {
|
|
|
2780
2952
|
const status = data.status ?? { activeRuns: 0 };
|
|
2781
2953
|
const validation = data.validation ?? {
|
|
2782
2954
|
inputs: [],
|
|
2783
|
-
outputs: []
|
|
2955
|
+
outputs: [],
|
|
2956
|
+
issues: [],
|
|
2957
|
+
};
|
|
2784
2958
|
const isRunning = !!status.activeRuns;
|
|
2785
2959
|
const pct = Math.round(Math.max(0, Math.min(1, Number(status.progress) || 0)) * 100);
|
|
2786
|
-
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 }) => {
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
}, 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 }) => {
|
|
2792
2966
|
const entries = kind === "input" ? inputEntries : outputEntries;
|
|
2793
2967
|
const entry = entries.find((e) => e.id === handleId);
|
|
2794
2968
|
if (!entry)
|
|
@@ -3394,9 +3568,9 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3394
3568
|
state: "local",
|
|
3395
3569
|
});
|
|
3396
3570
|
const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
|
|
3397
|
-
const
|
|
3398
|
-
?
|
|
3399
|
-
:
|
|
3571
|
+
const effectiveHandles = selectedNode
|
|
3572
|
+
? computeEffectiveHandles(selectedNode, registry)
|
|
3573
|
+
: { inputs: {}, outputs: {}, inputDefaults: {} };
|
|
3400
3574
|
const [exampleState, setExampleState] = React.useState(example ?? "");
|
|
3401
3575
|
const defaultExamples = React.useMemo(() => [
|
|
3402
3576
|
{
|
|
@@ -3684,7 +3858,12 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3684
3858
|
const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId && e.target.handle === handle);
|
|
3685
3859
|
if (isLinked)
|
|
3686
3860
|
return;
|
|
3687
|
-
|
|
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);
|
|
3688
3867
|
let value = raw;
|
|
3689
3868
|
const parseArray = (s, map) => {
|
|
3690
3869
|
const str = String(s).trim();
|
|
@@ -3761,7 +3940,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3761
3940
|
}
|
|
3762
3941
|
}
|
|
3763
3942
|
runner.setInput(selectedNodeId, handle, value);
|
|
3764
|
-
}, [selectedNodeId, def.edges,
|
|
3943
|
+
}, [selectedNodeId, def.edges, effectiveHandles, runner]);
|
|
3765
3944
|
const setInput = React.useMemo(() => {
|
|
3766
3945
|
if (overrides?.setInput) {
|
|
3767
3946
|
return overrides.setInput(baseSetInput, {
|
|
@@ -3893,7 +4072,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3893
4072
|
const message = err instanceof Error ? err.message : String(err);
|
|
3894
4073
|
alert(message);
|
|
3895
4074
|
}
|
|
3896
|
-
}, 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 })] })] }));
|
|
3897
4076
|
}
|
|
3898
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, }) {
|
|
3899
4078
|
const [registry, setRegistry] = React.useState(sparkGraph.createSimpleGraphRegistry());
|
|
@@ -3989,9 +4168,14 @@ exports.WorkbenchCanvas = WorkbenchCanvas;
|
|
|
3989
4168
|
exports.WorkbenchContext = WorkbenchContext;
|
|
3990
4169
|
exports.WorkbenchProvider = WorkbenchProvider;
|
|
3991
4170
|
exports.WorkbenchStudio = WorkbenchStudio;
|
|
4171
|
+
exports.computeEffectiveHandles = computeEffectiveHandles;
|
|
4172
|
+
exports.countVisibleHandles = countVisibleHandles;
|
|
4173
|
+
exports.estimateNodeSize = estimateNodeSize;
|
|
3992
4174
|
exports.formatDataUrlAsLabel = formatDataUrlAsLabel;
|
|
3993
4175
|
exports.formatDeclaredTypeSignature = formatDeclaredTypeSignature;
|
|
4176
|
+
exports.getHandleClassName = getHandleClassName;
|
|
3994
4177
|
exports.getNodeBorderClassNames = getNodeBorderClassNames;
|
|
4178
|
+
exports.layoutNode = layoutNode;
|
|
3995
4179
|
exports.preformatValueForDisplay = preformatValueForDisplay;
|
|
3996
4180
|
exports.prettyHandle = prettyHandle;
|
|
3997
4181
|
exports.resolveOutputDisplay = resolveOutputDisplay;
|