@bian-womp/spark-remote 0.2.50 → 0.2.52

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/esm/index.js CHANGED
@@ -21,22 +21,23 @@ class SeqGenerator {
21
21
 
22
22
  const OPEN = 1;
23
23
  class WebSocketTransport {
24
- constructor(url) {
24
+ constructor(url, options) {
25
25
  this.listeners = new Set();
26
26
  this.seq = new SeqGenerator();
27
+ this.isAlive = false;
28
+ this.PING_INTERVAL_MS = 30000;
29
+ this.PONG_TIMEOUT_MS = 10000;
27
30
  this.baseUrl = url;
31
+ this.onConnectionLost = options?.onConnectionLost;
28
32
  }
29
33
  async connect(options) {
30
34
  if (this.ws && this.ws.readyState === OPEN)
31
35
  return;
32
- // Build URL with connection params
33
- // Handle both ws:///wss:// URLs and http:///https:// URLs (convert to ws)
34
36
  let url;
35
37
  if (this.baseUrl.startsWith("ws://") || this.baseUrl.startsWith("wss://")) {
36
38
  url = new URL(this.baseUrl);
37
39
  }
38
40
  else {
39
- // Convert http/https to ws/wss
40
41
  const wsBaseUrl = this.baseUrl.replace(/^http/, "ws");
41
42
  url = new URL(wsBaseUrl);
42
43
  }
@@ -54,7 +55,10 @@ class WebSocketTransport {
54
55
  await new Promise((resolve, reject) => {
55
56
  if (!this.ws)
56
57
  return reject(new Error("ws init failed"));
57
- this.ws.onopen = () => resolve();
58
+ this.ws.onopen = () => {
59
+ this.startHeartbeat();
60
+ resolve();
61
+ };
58
62
  this.ws.onerror = (e) => reject(e);
59
63
  this.ws.onmessage = (ev) => {
60
64
  try {
@@ -64,10 +68,72 @@ class WebSocketTransport {
64
68
  }
65
69
  catch { }
66
70
  };
71
+ this.ws.onclose = () => {
72
+ this.stopHeartbeat();
73
+ };
67
74
  });
68
75
  }
76
+ startHeartbeat() {
77
+ this.stopHeartbeat();
78
+ this.isAlive = true;
79
+ if (typeof window !== "undefined" || !this.ws)
80
+ return;
81
+ this.pongHandler = () => {
82
+ this.isAlive = true;
83
+ if (this.pongTimeout) {
84
+ clearTimeout(this.pongTimeout);
85
+ this.pongTimeout = undefined;
86
+ }
87
+ };
88
+ this.ws.on("pong", this.pongHandler);
89
+ this.pingInterval = setInterval(() => {
90
+ if (!this.ws || this.ws.readyState !== OPEN) {
91
+ this.stopHeartbeat();
92
+ return;
93
+ }
94
+ if (!this.isAlive) {
95
+ this.onConnectionLost?.();
96
+ this.stopHeartbeat();
97
+ this.ws.terminate();
98
+ return;
99
+ }
100
+ this.isAlive = false;
101
+ try {
102
+ this.ws.ping();
103
+ }
104
+ catch (err) {
105
+ console.warn("[WebSocketTransport] Failed to send ping:", err);
106
+ this.onConnectionLost?.();
107
+ this.stopHeartbeat();
108
+ return;
109
+ }
110
+ this.pongTimeout = setTimeout(() => {
111
+ if (!this.isAlive) {
112
+ this.onConnectionLost?.();
113
+ this.stopHeartbeat();
114
+ if (this.ws) {
115
+ this.ws.terminate();
116
+ }
117
+ }
118
+ }, this.PONG_TIMEOUT_MS);
119
+ }, this.PING_INTERVAL_MS);
120
+ }
121
+ stopHeartbeat() {
122
+ if (this.pingInterval) {
123
+ clearInterval(this.pingInterval);
124
+ this.pingInterval = undefined;
125
+ }
126
+ if (this.pongTimeout) {
127
+ clearTimeout(this.pongTimeout);
128
+ this.pongTimeout = undefined;
129
+ }
130
+ if (this.ws && this.pongHandler) {
131
+ this.ws.off("pong", this.pongHandler);
132
+ this.pongHandler = undefined;
133
+ }
134
+ this.isAlive = false;
135
+ }
69
136
  async request(msg) {
70
- // For now, just send and wait for the next message with matching seq (simple demo)
71
137
  const seq = this.seq.next();
72
138
  const env = { ...msg, seq };
73
139
  const p = new Promise((resolve) => {
@@ -91,6 +157,7 @@ class WebSocketTransport {
91
157
  return () => this.listeners.delete(cb);
92
158
  }
93
159
  async close() {
160
+ this.stopHeartbeat();
94
161
  if (!this.ws)
95
162
  return;
96
163
  const ws = this.ws;
@@ -345,6 +412,7 @@ async function createRuntimeAdapter(createRegistry, send, extensions) {
345
412
  },
346
413
  snapshotFull: () => {
347
414
  const snap = originalApi.snapshot();
415
+ const extData = originalApi.getExtData();
348
416
  const env = graphRuntime?.getEnvironment?.() ?? {};
349
417
  const def = graphRuntime?.getGraphDef();
350
418
  return {
@@ -352,6 +420,7 @@ async function createRuntimeAdapter(createRegistry, send, extensions) {
352
420
  environment: env,
353
421
  inputs: snap.inputs,
354
422
  outputs: snap.outputs,
423
+ ui: extData?.ui || undefined,
355
424
  };
356
425
  },
357
426
  applySnapshotFull: async (payload) => {
@@ -416,15 +485,6 @@ async function createRuntimeAdapter(createRegistry, send, extensions) {
416
485
  }
417
486
  graphRuntime.setEnvironment(env);
418
487
  },
419
- setInput: (nodeId, handle, value) => {
420
- // If engine exists, use it; otherwise fall back to direct runtime access
421
- if (engine) {
422
- engine.setInput(nodeId, handle, value);
423
- }
424
- else {
425
- graphRuntime?.setInput(nodeId, handle, value);
426
- }
427
- },
428
488
  setInputs: (nodeId, inputs) => {
429
489
  if (engine) {
430
490
  engine.setInputs(nodeId, inputs);
@@ -524,7 +584,6 @@ async function createRuntimeAdapter(createRegistry, send, extensions) {
524
584
  describeRegistry: wrapMethod("describeRegistry", originalApi.describeRegistry),
525
585
  update: wrapMethod("update", originalApi.update),
526
586
  setEnvironment: wrapMethod("setEnvironment", originalApi.setEnvironment),
527
- setInput: wrapMethod("setInput", originalApi.setInput),
528
587
  setInputs: wrapMethod("setInputs", originalApi.setInputs),
529
588
  triggerExternal: wrapMethod("triggerExternal", originalApi.triggerExternal),
530
589
  step: wrapMethod("step", originalApi.step),
@@ -574,11 +633,6 @@ class RemoteEngine {
574
633
  },
575
634
  });
576
635
  }
577
- setInput(nodeId, handle, value) {
578
- this.transport.send({
579
- message: { type: "SetInput", payload: { nodeId, handle, value } },
580
- });
581
- }
582
636
  // Batch inputs for a single network round-trip
583
637
  setInputs(nodeId, inputs) {
584
638
  this.transport.send({
@@ -737,7 +791,13 @@ class RuntimeApiClient {
737
791
  if (!WebSocketTransport) {
738
792
  throw new Error("WebSocketTransport not available");
739
793
  }
740
- return new WebSocketTransport(this.config.url);
794
+ return new WebSocketTransport(this.config.url, {
795
+ onConnectionLost: () => {
796
+ this.dispose().catch((err) => {
797
+ console.warn("[RuntimeApiClient] Error disposing on connection loss:", err);
798
+ });
799
+ },
800
+ });
741
801
  }
742
802
  else if (kind === "remote-unix") {
743
803
  // Dynamic import to avoid bundling in browser builds
@@ -983,6 +1043,12 @@ class RuntimeApiClient {
983
1043
  message: { type: "Flush" },
984
1044
  });
985
1045
  }
1046
+ async setExtData(data) {
1047
+ const transport = await this.ensureConnected();
1048
+ await transport.request({
1049
+ message: { type: "SetExtData", payload: data },
1050
+ });
1051
+ }
986
1052
  /**
987
1053
  * Dispose the client and close the transport connection.
988
1054
  * Idempotent: safe to call multiple times.
@@ -1094,16 +1160,6 @@ class RuntimeApiServer {
1094
1160
  ack();
1095
1161
  break;
1096
1162
  }
1097
- case "SetInput": {
1098
- this.logCommand("SetInput", env, {
1099
- nodeId: msg.payload.nodeId,
1100
- handle: msg.payload.handle,
1101
- value: summarize(msg.payload.value),
1102
- });
1103
- this.runtimeApi.setInput(msg.payload.nodeId, msg.payload.handle, msg.payload.value);
1104
- ack();
1105
- break;
1106
- }
1107
1163
  case "SetInputs": {
1108
1164
  this.logCommand("SetInputs", env, {
1109
1165
  nodeId: msg.payload.nodeId,
@@ -1216,15 +1272,14 @@ class RuntimeApiServer {
1216
1272
  ack();
1217
1273
  break;
1218
1274
  }
1219
- case "Dispose": {
1220
- this.logCommand("Dispose", env);
1221
- this.runtimeApi.dispose();
1275
+ case "SetExtData": {
1276
+ this.runtimeApi.setExtData(msg.payload);
1222
1277
  ack();
1223
1278
  break;
1224
1279
  }
1225
- case "Pause":
1226
- case "Resume": {
1227
- this.logCommand(`${msg.type} (not-impl)`, env);
1280
+ case "Dispose": {
1281
+ this.logCommand("Dispose", env);
1282
+ this.runtimeApi.dispose();
1228
1283
  ack();
1229
1284
  break;
1230
1285
  }