@tangle-network/sandbox 0.11.1-develop.20260718200351.2f5db96 → 0.11.1

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.
@@ -1,65 +1,40 @@
1
- //#region src/session-gateway/callback.ts
2
- function invokeClientCallback(name, callback, onError) {
3
- try {
4
- callback();
5
- } catch (error) {
6
- if (name === "onError") return;
7
- try {
8
- onError?.(`${name} callback failed: ${error instanceof Error ? error.message : String(error)}`, "CALLBACK_ERROR");
9
- } catch {}
10
- }
11
- }
12
- //#endregion
13
- //#region src/session-gateway/connection-waiters.ts
14
- var ConnectionWaiters = class {
15
- waiters = /* @__PURE__ */ new Set();
16
- establishmentTimeout = null;
17
- constructor(timeoutMs) {
18
- this.timeoutMs = timeoutMs;
19
- }
20
- start(onTimeout) {
21
- this.clear();
22
- this.establishmentTimeout = setTimeout(() => {
23
- this.establishmentTimeout = null;
24
- const error = /* @__PURE__ */ new Error(`Connection establishment timed out after ${this.timeoutMs}ms`);
25
- this.rejectAll(error);
26
- onTimeout(error);
27
- }, this.timeoutMs);
28
- }
29
- clear() {
30
- if (this.establishmentTimeout !== null) {
31
- clearTimeout(this.establishmentTimeout);
32
- this.establishmentTimeout = null;
33
- }
34
- }
35
- wait() {
36
- return new Promise((resolve, reject) => {
37
- this.waiters.add({
38
- resolve: () => {
39
- resolve();
40
- },
41
- reject: (error) => {
42
- reject(error);
43
- }
44
- });
45
- });
46
- }
47
- resolveAll() {
48
- this.clear();
49
- for (const waiter of this.waiters) waiter.resolve();
50
- this.waiters.clear();
51
- }
52
- rejectAll(error) {
53
- this.clear();
54
- for (const waiter of this.waiters) waiter.reject(error);
55
- this.waiters.clear();
56
- }
1
+ //#region src/session-gateway/types.ts
2
+ /**
3
+ * Default connection metrics.
4
+ */
5
+ const INITIAL_METRICS = {
6
+ lastPingLatencyMs: null,
7
+ avgPingLatencyMs: null,
8
+ missedPongCount: 0,
9
+ reconnectCount: 0,
10
+ lastPongAt: null,
11
+ lastReconnectAt: null,
12
+ quality: "disconnected"
57
13
  };
14
+ /**
15
+ * Calculate connection quality from metrics.
16
+ */
17
+ function calculateConnectionQuality(metrics, isConnected) {
18
+ if (!isConnected) return "disconnected";
19
+ const { avgPingLatencyMs, missedPongCount, reconnectCount, lastReconnectAt } = metrics;
20
+ const recentReconnect = lastReconnectAt && Date.now() - lastReconnectAt < 300 * 1e3;
21
+ if (missedPongCount >= 2 || recentReconnect && reconnectCount > 1) return "poor";
22
+ if (missedPongCount >= 1 || avgPingLatencyMs !== null && avgPingLatencyMs > 1e3 || recentReconnect) return "degraded";
23
+ if (avgPingLatencyMs !== null && avgPingLatencyMs < 100 && reconnectCount === 0) return "excellent";
24
+ if (avgPingLatencyMs !== null && avgPingLatencyMs <= 300) return "good";
25
+ return "good";
26
+ }
58
27
  //#endregion
59
- //#region src/session-gateway/protocol.ts
28
+ //#region src/session-gateway/client.ts
29
+ /**
30
+ * SessionGatewayClient
31
+ *
32
+ * WebSocket client for Sandbox API session streaming.
33
+ * Browser-safe: no Node.js APIs.
34
+ */
60
35
  const DEFAULT_CONFIG = {
61
36
  transport: "websocket",
62
- channels: [],
37
+ channels: ["*"],
63
38
  autoReconnect: true,
64
39
  maxReconnectAttempts: 10,
65
40
  initialReconnectDelayMs: 1e3,
@@ -71,47 +46,29 @@ const DEFAULT_CONFIG = {
71
46
  maxDeduplicationSize: 1e3,
72
47
  enableReplayPersistence: false,
73
48
  replayStorageKeyPrefix: "session_gateway_",
74
- latencyHistorySize: 10,
75
- connectionEstablishmentTimeoutMs: 1e4
76
- };
77
- const CONTROL_MESSAGE_TYPES = new Set([
78
- "connection.established",
79
- "subscribed",
80
- "unsubscribed",
81
- "pong",
82
- "replay.start",
83
- "replay.progress",
84
- "replay.complete",
85
- "error",
86
- "token.expiring",
87
- "token.expired",
88
- "backpressure.warning",
89
- "terminal.input.error",
90
- "heartbeat"
91
- ]);
92
- const WIRE_TYPE_MAP = {
93
- "sidecar.connected": "runtime.connected",
94
- "sidecar.disconnected": "runtime.disconnected",
95
- "sidecar.not_found": "runtime.not_found",
96
- "sidecar.ready": "runtime.ready",
97
- "sidecar.reconnecting": "runtime.reconnecting",
98
- "sidecar.removed": "runtime.removed"
49
+ latencyHistorySize: 10
99
50
  };
100
- function acceptedSubscriptionChannels(acknowledgement) {
101
- if (acknowledgement.denied?.length) {
102
- const reasons = acknowledgement.denied.map(({ channel, reason }) => `${channel}: ${reason}`).join(", ");
103
- throw new Error(`Subscription denied (${reasons})`);
104
- }
105
- return acknowledgement.channels;
106
- }
107
- //#endregion
108
- //#region src/session-gateway/replay-storage.ts
109
51
  const STORAGE_KEYS = {
110
52
  lastEventId: "last_event_id",
111
53
  executionId: "execution_id",
112
54
  sessionId: "session_id"
113
55
  };
114
- var MemoryReplayStorage = class {
56
+ /**
57
+ * Legacy wire-protocol message type map. The current server emits the
58
+ * `sidecar.*` message types; the SDK surfaces the canonical `runtime.*`
59
+ * names. Kept at module scope so it isn't re-allocated per inbound
60
+ * WebSocket message.
61
+ */
62
+ const WIRE_TYPE_MAP = {
63
+ "sidecar.connected": "runtime.connected",
64
+ "sidecar.disconnected": "runtime.disconnected",
65
+ "sidecar.not_found": "runtime.not_found",
66
+ "sidecar.ready": "runtime.ready"
67
+ };
68
+ /**
69
+ * Memory-based storage fallback when localStorage is not available.
70
+ */
71
+ var MemoryStorage = class {
115
72
  store = /* @__PURE__ */ new Map();
116
73
  getItem(key) {
117
74
  return this.store.get(key) ?? null;
@@ -123,141 +80,15 @@ var MemoryReplayStorage = class {
123
80
  this.store.delete(key);
124
81
  }
125
82
  };
126
- function resolveReplayStorage(configured) {
127
- if (configured) return configured;
128
- if (typeof globalThis !== "undefined" && "localStorage" in globalThis) try {
129
- const testKey = "__session_gateway_storage_test__";
130
- const previous = localStorage.getItem(testKey);
131
- try {
132
- localStorage.setItem(testKey, testKey);
133
- localStorage.removeItem(testKey);
134
- } finally {
135
- if (previous !== null) localStorage.setItem(testKey, previous);
136
- }
137
- return localStorage;
138
- } catch {
139
- return new MemoryReplayStorage();
140
- }
141
- return new MemoryReplayStorage();
142
- }
143
- function endpointIdentity(rawUrl) {
144
- const url = new URL(rawUrl);
145
- url.username = "";
146
- url.password = "";
147
- url.hash = "";
148
- for (const key of [...url.searchParams.keys()]) if (/(^|[-_])(api[-_]?key|auth|authorization|credential|key|password|secret|sig|signature|token)($|[-_])/i.test(key)) url.searchParams.delete(key);
149
- url.searchParams.sort();
150
- return url.toString();
151
- }
152
- var ReplayStateStore = class {
153
- prefix;
154
- constructor(enabled, storage, keyPrefix, endpointUrl, sessionId) {
155
- this.enabled = enabled;
156
- this.storage = storage;
157
- this.prefix = `${keyPrefix}${encodeURIComponent(endpointIdentity(endpointUrl))}:${encodeURIComponent(sessionId)}:`;
158
- }
159
- load() {
160
- if (!this.enabled) return this.emptyState();
161
- try {
162
- return {
163
- lastEventId: this.storage.getItem(this.prefix + STORAGE_KEYS.lastEventId),
164
- executionId: this.storage.getItem(this.prefix + STORAGE_KEYS.executionId),
165
- sessionId: this.storage.getItem(this.prefix + STORAGE_KEYS.sessionId)
166
- };
167
- } catch {
168
- return this.emptyState();
169
- }
170
- }
171
- save(state) {
172
- if (!this.enabled) return;
173
- try {
174
- if (state.lastEventId) this.storage.setItem(this.prefix + STORAGE_KEYS.lastEventId, state.lastEventId);
175
- if (state.executionId) this.storage.setItem(this.prefix + STORAGE_KEYS.executionId, state.executionId);
176
- if (state.sessionId) this.storage.setItem(this.prefix + STORAGE_KEYS.sessionId, state.sessionId);
177
- } catch {}
178
- }
179
- clear() {
180
- if (!this.enabled) return;
181
- try {
182
- this.storage.removeItem(this.prefix + STORAGE_KEYS.lastEventId);
183
- this.storage.removeItem(this.prefix + STORAGE_KEYS.executionId);
184
- this.storage.removeItem(this.prefix + STORAGE_KEYS.sessionId);
185
- } catch {}
186
- }
187
- emptyState() {
188
- return {
189
- lastEventId: null,
190
- executionId: null,
191
- sessionId: null
192
- };
193
- }
194
- };
195
- //#endregion
196
- //#region src/session-gateway/token-refresh.ts
197
- var TokenRefresher = class {
198
- refreshing = false;
199
- constructor(refreshToken, applyToken, onError) {
200
- this.refreshToken = refreshToken;
201
- this.applyToken = applyToken;
202
- this.onError = onError;
203
- }
204
- async refresh() {
205
- if (!this.refreshToken || this.refreshing) return;
206
- this.refreshing = true;
207
- try {
208
- const result = await this.refreshToken();
209
- if (!result.token.trim()) throw new Error("token refresh returned an empty token");
210
- this.applyToken(result.token);
211
- } catch (error) {
212
- invokeClientCallback("onError", () => this.onError?.(`Token refresh failed: ${error instanceof Error ? error.message : String(error)}`, "TOKEN_REFRESH_FAILED"), this.onError);
213
- } finally {
214
- this.refreshing = false;
215
- }
216
- }
217
- };
218
- //#endregion
219
- //#region src/session-gateway/types.ts
220
- /**
221
- * Default connection metrics.
222
- */
223
- const INITIAL_METRICS = {
224
- lastPingLatencyMs: null,
225
- avgPingLatencyMs: null,
226
- missedPongCount: 0,
227
- reconnectCount: 0,
228
- lastPongAt: null,
229
- lastReconnectAt: null,
230
- quality: "disconnected"
231
- };
232
- /**
233
- * Calculate connection quality from metrics.
234
- */
235
- function calculateConnectionQuality(metrics, isConnected) {
236
- if (!isConnected) return "disconnected";
237
- const { avgPingLatencyMs, missedPongCount, reconnectCount, lastReconnectAt } = metrics;
238
- const recentReconnect = lastReconnectAt && Date.now() - lastReconnectAt < 300 * 1e3;
239
- if (missedPongCount >= 2 || recentReconnect && reconnectCount > 1) return "poor";
240
- if (missedPongCount >= 1 || avgPingLatencyMs !== null && avgPingLatencyMs > 1e3 || recentReconnect) return "degraded";
241
- if (avgPingLatencyMs !== null && avgPingLatencyMs < 100 && reconnectCount === 0) return "excellent";
242
- if (avgPingLatencyMs !== null && avgPingLatencyMs <= 300) return "good";
243
- return "good";
244
- }
245
- //#endregion
246
- //#region src/session-gateway/client.ts
247
- const CONNECTION_ESTABLISHMENT_TIMEOUT_CODE = 4008;
248
- const CONNECTION_ERROR_CODE = 4009;
249
- const CONNECTION_ERROR_REASON = "WebSocket connection error";
250
83
  var SessionGatewayClient = class {
251
84
  ws = null;
252
85
  config;
253
86
  handlers;
254
- replayStateStore;
255
- tokenRefresher;
256
87
  currentToken;
88
+ isRefreshingToken = false;
257
89
  state = "disconnected";
258
90
  connectedAt = null;
259
91
  hasConnectedBefore = false;
260
- connectionEstablishedForSocket = false;
261
92
  lastPingAt = null;
262
93
  lastPongAt = null;
263
94
  lastPingSentAt = null;
@@ -271,7 +102,6 @@ var SessionGatewayClient = class {
271
102
  pingInterval = null;
272
103
  pongTimeout = null;
273
104
  pendingCallbacks = /* @__PURE__ */ new Map();
274
- connectionWaiters;
275
105
  messageIdCounter = 0;
276
106
  processedEventIds = /* @__PURE__ */ new Set();
277
107
  pingLatencyHistory = [];
@@ -285,12 +115,22 @@ var SessionGatewayClient = class {
285
115
  received: 0
286
116
  };
287
117
  constructor(config) {
288
- if (!config.token.trim()) throw new Error("Session gateway token must not be empty");
289
- const replayStorage = resolveReplayStorage(config.replayStorage);
118
+ let storage;
119
+ if (config.replayStorage) storage = config.replayStorage;
120
+ else if (typeof globalThis !== "undefined" && "localStorage" in globalThis) try {
121
+ const testKey = "__test__";
122
+ localStorage.setItem(testKey, testKey);
123
+ localStorage.removeItem(testKey);
124
+ storage = localStorage;
125
+ } catch {
126
+ storage = new MemoryStorage();
127
+ }
128
+ else storage = new MemoryStorage();
290
129
  this.config = {
291
130
  url: config.url,
292
131
  sessionId: config.sessionId,
293
132
  token: config.token,
133
+ onTokenRefresh: config.onTokenRefresh,
294
134
  transport: config.transport ?? DEFAULT_CONFIG.transport,
295
135
  channels: config.channels ?? DEFAULT_CONFIG.channels,
296
136
  autoReconnect: config.autoReconnect ?? DEFAULT_CONFIG.autoReconnect,
@@ -303,14 +143,11 @@ var SessionGatewayClient = class {
303
143
  enableDeduplication: config.enableDeduplication ?? DEFAULT_CONFIG.enableDeduplication,
304
144
  maxDeduplicationSize: config.maxDeduplicationSize ?? DEFAULT_CONFIG.maxDeduplicationSize,
305
145
  enableReplayPersistence: config.enableReplayPersistence ?? DEFAULT_CONFIG.enableReplayPersistence,
146
+ replayStorage: storage,
306
147
  replayStorageKeyPrefix: config.replayStorageKeyPrefix ?? DEFAULT_CONFIG.replayStorageKeyPrefix,
307
- latencyHistorySize: config.latencyHistorySize ?? DEFAULT_CONFIG.latencyHistorySize,
308
- connectionEstablishmentTimeoutMs: config.connectionEstablishmentTimeoutMs ?? DEFAULT_CONFIG.connectionEstablishmentTimeoutMs
148
+ latencyHistorySize: config.latencyHistorySize ?? DEFAULT_CONFIG.latencyHistorySize
309
149
  };
310
150
  this.handlers = config.handlers ?? {};
311
- this.connectionWaiters = new ConnectionWaiters(this.config.connectionEstablishmentTimeoutMs);
312
- this.replayStateStore = new ReplayStateStore(this.config.enableReplayPersistence, replayStorage, this.config.replayStorageKeyPrefix, this.config.url, this.config.sessionId);
313
- this.tokenRefresher = new TokenRefresher(config.onTokenRefresh, (token) => this.updateToken(token), this.handlers.onError);
314
151
  this.currentToken = config.token;
315
152
  if (this.config.enableReplayPersistence) this.loadReplayState();
316
153
  }
@@ -318,37 +155,30 @@ var SessionGatewayClient = class {
318
155
  * Connect to the session gateway.
319
156
  */
320
157
  connect() {
321
- if (this.state === "connected" || this.state === "connecting" || this.state === "reconnecting") return;
322
- const nextState = this.hasConnectedBefore ? "reconnecting" : "connecting";
323
- this.setState(nextState);
324
- if (this.getState() !== nextState) return;
158
+ if (this.state === "connected" || this.state === "connecting") return;
159
+ const isReconnect = this.hasConnectedBefore;
160
+ this.setState(isReconnect ? "reconnecting" : "connecting");
325
161
  this.connectWebSocket();
326
162
  }
327
163
  connectWebSocket() {
328
164
  const url = new URL(this.config.url);
329
165
  url.searchParams.set("token", this.currentToken);
330
- this.connectionEstablishedForSocket = false;
331
166
  const socket = new WebSocket(url.toString());
332
167
  this.ws = socket;
333
168
  socket.onopen = () => {
334
169
  if (this.ws === socket) this.handleOpen();
335
170
  };
336
171
  socket.onclose = (event) => this.handleClose(socket, event.code, event.reason);
337
- socket.onerror = () => this.handleError(socket);
172
+ socket.onerror = (_event) => {};
338
173
  socket.onmessage = (event) => {
339
174
  if (this.ws === socket) this.handleMessage(event.data);
340
175
  };
341
- this.connectionWaiters.start((error) => {
342
- this.closeBeforeEstablishment(socket, CONNECTION_ESTABLISHMENT_TIMEOUT_CODE, error.message);
343
- });
344
176
  }
345
177
  /**
346
178
  * Disconnect from the session gateway.
347
179
  */
348
180
  disconnect() {
349
- this.connectionWaiters.rejectAll(/* @__PURE__ */ new Error("Client disconnected"));
350
181
  this.cleanup();
351
- this.connectionEstablishedForSocket = false;
352
182
  const socket = this.ws;
353
183
  this.ws = null;
354
184
  if (socket) socket.close(1e3, "Client disconnect");
@@ -358,10 +188,10 @@ var SessionGatewayClient = class {
358
188
  * Subscribe to additional channels.
359
189
  */
360
190
  async subscribe(channels) {
361
- return acceptedSubscriptionChannels(await this.sendWithResponse({
191
+ return (await this.sendWithResponse({
362
192
  type: "subscribe",
363
193
  channels
364
- }));
194
+ })).channels;
365
195
  }
366
196
  /**
367
197
  * Unsubscribe from channels.
@@ -383,9 +213,7 @@ var SessionGatewayClient = class {
383
213
  /**
384
214
  * Request replay of events since a timestamp.
385
215
  */
386
- async replay(since) {
387
- if (!this.connectionEstablishedForSocket || !this.isConnected()) await this.waitForConnectionEstablished();
388
- if (!this.connectionEstablishedForSocket || !this.isConnected()) throw new Error("Connection closed before replay could be requested");
216
+ replay(since) {
389
217
  this.send({
390
218
  type: "replay",
391
219
  since
@@ -427,7 +255,14 @@ var SessionGatewayClient = class {
427
255
  };
428
256
  this.processedEventIds.clear();
429
257
  this.duplicatesSkipped = 0;
430
- this.replayStateStore.clear();
258
+ if (this.config.enableReplayPersistence) {
259
+ const prefix = this.config.replayStorageKeyPrefix;
260
+ try {
261
+ this.config.replayStorage.removeItem(prefix + STORAGE_KEYS.lastEventId);
262
+ this.config.replayStorage.removeItem(prefix + STORAGE_KEYS.executionId);
263
+ this.config.replayStorage.removeItem(prefix + STORAGE_KEYS.sessionId);
264
+ } catch {}
265
+ }
431
266
  }
432
267
  /**
433
268
  * Get current replay state for UI indicators.
@@ -471,13 +306,12 @@ var SessionGatewayClient = class {
471
306
  resetMetrics() {
472
307
  this.pingLatencyHistory = [];
473
308
  this.connectionMetrics = { ...INITIAL_METRICS };
474
- invokeClientCallback("onMetricsChange", () => this.handlers.onMetricsChange?.(this.connectionMetrics), this.handlers.onError);
309
+ this.handlers.onMetricsChange?.(this.connectionMetrics);
475
310
  }
476
311
  /**
477
312
  * Update the auth token and reconnect with the new token.
478
313
  */
479
314
  updateToken(token) {
480
- if (!token.trim()) throw new Error("Session gateway token must not be empty");
481
315
  this.currentToken = token;
482
316
  if (this.state === "connected" && this.ws) this.ws.close(1e3, "Token refreshed");
483
317
  }
@@ -491,110 +325,73 @@ var SessionGatewayClient = class {
491
325
  if (this.state !== state) {
492
326
  this.state = state;
493
327
  this.updateConnectionQuality();
494
- invokeClientCallback("onStateChange", () => this.handlers.onStateChange?.(state), this.handlers.onError);
328
+ this.handlers.onStateChange?.(state);
495
329
  }
496
330
  }
497
331
  handleOpen() {
498
- this.missedPongCount = 0;
499
- }
500
- handleConnectionEstablished(sessionId) {
501
- if (this.connectionEstablishedForSocket) return;
502
- this.connectionWaiters.clear();
503
332
  const isReconnect = this.hasConnectedBefore;
504
- this.connectionEstablishedForSocket = true;
505
333
  this.hasConnectedBefore = true;
334
+ this.setState("connected");
506
335
  this.connectedAt = Date.now();
507
336
  this.reconnectAttempts = 0;
337
+ this.missedPongCount = 0;
508
338
  this.startPingInterval();
509
339
  this.restoreSubscriptions();
510
- this.setState("connected");
511
- if (!this.connectionEstablishedForSocket || !this.isConnected()) return;
512
- this.connectionWaiters.resolveAll();
513
- invokeClientCallback("onConnect", () => this.handlers.onConnect?.(sessionId), this.handlers.onError);
514
- if (isReconnect && this.connectionEstablishedForSocket && this.isConnected()) {
340
+ if (isReconnect) {
515
341
  this.trackReconnection();
516
- invokeClientCallback("onReconnect", () => this.handlers.onReconnect?.(), this.handlers.onError);
342
+ this.handlers.onReconnect?.();
517
343
  }
518
344
  }
519
345
  handleClose(socket, code, reason) {
520
346
  if (this.ws !== socket) return;
521
- this.connectionWaiters.clear();
522
347
  this.ws = null;
523
- this.connectionEstablishedForSocket = false;
524
348
  this.cleanup();
349
+ this.handlers.onDisconnect?.(code, reason);
525
350
  if (this.config.autoReconnect && this.reconnectAttempts < this.config.maxReconnectAttempts) {
526
351
  this.setState("reconnecting");
527
- if (this.state === "reconnecting") this.scheduleReconnect();
528
- } else {
529
- this.connectionWaiters.rejectAll(/* @__PURE__ */ new Error("Connection closed"));
530
- this.setState("disconnected");
531
- }
532
- invokeClientCallback("onDisconnect", () => this.handlers.onDisconnect?.(code, reason), this.handlers.onError);
533
- }
534
- handleError(socket) {
535
- if (this.ws !== socket) return;
536
- this.connectionWaiters.clear();
537
- this.closeBeforeEstablishment(socket, CONNECTION_ERROR_CODE, CONNECTION_ERROR_REASON);
538
- }
539
- closeBeforeEstablishment(socket, code, reason) {
540
- if (this.ws !== socket || this.connectionEstablishedForSocket) return;
541
- try {
542
- socket.close(code, reason);
543
- } finally {
544
- this.handleClose(socket, code, reason);
545
- }
352
+ this.scheduleReconnect();
353
+ } else this.setState("disconnected");
546
354
  }
547
355
  handleMessage(data) {
548
356
  this.messagesReceived++;
549
357
  try {
550
358
  const raw = JSON.parse(data);
551
359
  if (!raw.type && raw.messageType) raw.type = raw.messageType;
552
- if (typeof raw.type !== "string") {
553
- invokeClientCallback("onError", () => this.handlers.onError?.("Session gateway frame is missing a message type", "INVALID_MESSAGE"), void 0);
554
- return;
555
- }
556
- if (WIRE_TYPE_MAP[raw.type] !== void 0) raw.type = WIRE_TYPE_MAP[raw.type];
360
+ if (typeof raw.type === "string" && WIRE_TYPE_MAP[raw.type] !== void 0) raw.type = WIRE_TYPE_MAP[raw.type];
557
361
  if (raw.data && typeof raw.data === "object") {
558
362
  const d = raw.data;
559
363
  if (d.sidecarId !== void 0 && d.sandboxId === void 0) d.sandboxId = d.sidecarId;
560
- if (raw.type === "error") {
561
- if (raw.message === void 0 && typeof d.message === "string") raw.message = d.message;
562
- if (raw.code === void 0 && typeof d.code === "string") raw.code = d.code;
563
- }
564
364
  }
565
- if (raw.seq === void 0 && typeof raw.sequenceId === "number") raw.seq = raw.sequenceId;
566
365
  if (raw.type === "agent.event" && !raw.data && raw.channel) {
567
- const { type, messageType, channel, id, seq, sequenceId, timestamp, ...rest } = raw;
366
+ const { type, messageType, channel, id, sequenceId, timestamp, ...rest } = raw;
568
367
  raw.data = rest;
569
368
  }
570
369
  const message = raw;
571
- if (CONTROL_MESSAGE_TYPES.has(message.type)) {
572
- if (message.type === "pong") this.handlePong(message.timestamp);
573
- this.deliverMessage(message);
370
+ if (message.type === "pong") {
371
+ this.handlePong(message.timestamp);
372
+ this.dispatchMessage(message);
574
373
  return;
575
374
  }
576
- if ("id" in message && message.id) {
375
+ if ("id" in message && message.id && this.config.enableDeduplication) {
577
376
  const eventId = message.id;
578
- if (this.config.enableDeduplication) {
579
- if (this.processedEventIds.has(eventId)) {
580
- this.duplicatesSkipped++;
581
- return;
582
- }
583
- this.processedEventIds.add(eventId);
584
- if (this.processedEventIds.size > this.config.maxDeduplicationSize) {
585
- const entriesToRemove = Math.floor(this.config.maxDeduplicationSize * .1);
586
- let removed = 0;
587
- for (const oldId of this.processedEventIds) {
588
- if (removed >= entriesToRemove) break;
589
- this.processedEventIds.delete(oldId);
590
- removed++;
591
- }
377
+ if (this.processedEventIds.has(eventId)) {
378
+ this.duplicatesSkipped++;
379
+ return;
380
+ }
381
+ this.processedEventIds.add(eventId);
382
+ if (this.processedEventIds.size > this.config.maxDeduplicationSize) {
383
+ const entriesToRemove = Math.floor(this.config.maxDeduplicationSize * .1);
384
+ let removed = 0;
385
+ for (const oldId of this.processedEventIds) {
386
+ if (removed >= entriesToRemove) break;
387
+ this.processedEventIds.delete(oldId);
388
+ removed++;
592
389
  }
593
390
  }
594
391
  this.lastEventId = eventId;
595
392
  this.saveReplayState();
596
393
  }
597
- this.deliverMessage(message);
394
+ this.dispatchMessage(message);
598
395
  } catch {}
599
396
  }
600
397
  handlePong(timestamp) {
@@ -610,7 +407,6 @@ var SessionGatewayClient = class {
610
407
  }
611
408
  }
612
409
  dispatchMessage(message) {
613
- if (message.type === "error" && message.code === "REPLAY_CURSOR_UNAVAILABLE") this.clearReplayState();
614
410
  if ("id" in message && message.id) {
615
411
  const callback = this.pendingCallbacks.get(message.id);
616
412
  if (callback) {
@@ -622,7 +418,7 @@ var SessionGatewayClient = class {
622
418
  }
623
419
  switch (message.type) {
624
420
  case "connection.established":
625
- this.handleConnectionEstablished(message.data.sessionId);
421
+ this.handlers.onConnect?.(message.data.sessionId);
626
422
  break;
627
423
  case "runtime.connected":
628
424
  this.handlers.onRuntimeConnected?.(message.data.sandboxId, message.data.baseUrl);
@@ -652,7 +448,7 @@ var SessionGatewayClient = class {
652
448
  case "agent.event":
653
449
  this.eventsReceived++;
654
450
  this.trackExecutionContext(message.data);
655
- this.handlers.onAgentEvent?.(message.channel, message.data, message.seq ?? message.sequenceId);
451
+ this.handlers.onAgentEvent?.(message.channel, message.data, message.sequenceId);
656
452
  break;
657
453
  case "replay.start":
658
454
  this.isReplaying = true;
@@ -674,8 +470,8 @@ var SessionGatewayClient = class {
674
470
  this.handlers.onError?.(message.message, message.code);
675
471
  break;
676
472
  case "token.expiring":
677
- invokeClientCallback("onTokenExpiring", () => this.handlers.onTokenExpiring?.(message.data.secondsRemaining), this.handlers.onError);
678
- this.tokenRefresher.refresh();
473
+ this.handlers.onTokenExpiring?.(message.data.secondsRemaining);
474
+ this.handleTokenExpiring();
679
475
  break;
680
476
  case "token.expired":
681
477
  this.handlers.onTokenExpired?.();
@@ -685,10 +481,6 @@ var SessionGatewayClient = class {
685
481
  break;
686
482
  }
687
483
  }
688
- deliverMessage(message) {
689
- invokeClientCallback("message dispatch", () => this.dispatchMessage(message), this.handlers.onError);
690
- invokeClientCallback("onMessage", () => this.handlers.onMessage?.(message), this.handlers.onError);
691
- }
692
484
  trackExecutionContext(data) {
693
485
  if (typeof data !== "object" || data === null) return;
694
486
  const eventData = data;
@@ -701,15 +493,25 @@ var SessionGatewayClient = class {
701
493
  this.saveReplayState();
702
494
  }
703
495
  }
496
+ async handleTokenExpiring() {
497
+ if (!this.config.onTokenRefresh || this.isRefreshingToken) return;
498
+ this.isRefreshingToken = true;
499
+ try {
500
+ const result = await this.config.onTokenRefresh();
501
+ this.updateToken(result.token);
502
+ } catch (error) {
503
+ this.handlers.onError?.(`Token refresh failed: ${error instanceof Error ? error.message : String(error)}`, "TOKEN_REFRESH_FAILED");
504
+ } finally {
505
+ this.isRefreshingToken = false;
506
+ }
507
+ }
704
508
  send(message) {
705
509
  if (this.ws?.readyState === WebSocket.OPEN) {
706
510
  this.ws.send(JSON.stringify(message));
707
511
  this.messagesSent++;
708
512
  }
709
513
  }
710
- async sendWithResponse(message) {
711
- if (!this.connectionEstablishedForSocket || !this.isConnected()) await this.waitForConnectionEstablished();
712
- if (!this.connectionEstablishedForSocket || !this.isConnected()) throw new Error("Connection closed before request could be sent");
514
+ sendWithResponse(message) {
713
515
  return new Promise((resolve, reject) => {
714
516
  const id = `msg_${++this.messageIdCounter}`;
715
517
  this.pendingCallbacks.set(id, {
@@ -728,20 +530,14 @@ var SessionGatewayClient = class {
728
530
  });
729
531
  });
730
532
  }
731
- waitForConnectionEstablished() {
732
- if (this.connectionEstablishedForSocket && this.isConnected()) return Promise.resolve();
733
- if (this.state === "disconnected") return Promise.reject(/* @__PURE__ */ new Error("Client is not connected"));
734
- return this.connectionWaiters.wait();
735
- }
736
533
  restoreSubscriptions() {
737
534
  const channels = this.config.channels;
738
535
  const replayOptions = {};
739
536
  if (this.lastEventId) replayOptions.lastEventId = this.lastEventId;
740
537
  if (this.currentExecutionId) replayOptions.executionId = this.currentExecutionId;
741
538
  if (this.currentSessionId) replayOptions.sessionId = this.currentSessionId;
742
- const hasReplayCursor = this.lastEventId !== null;
743
- if (channels.length === 0 && !hasReplayCursor) return;
744
- if (hasReplayCursor) {
539
+ const hasReplayState = Object.keys(replayOptions).length > 0;
540
+ if (hasReplayState) {
745
541
  this.isReplaying = true;
746
542
  this.replayProgress = {
747
543
  total: 0,
@@ -751,7 +547,7 @@ var SessionGatewayClient = class {
751
547
  this.send({
752
548
  type: "subscribe",
753
549
  channels,
754
- ...hasReplayCursor ? { replayOptions } : {}
550
+ ...hasReplayState ? { replayOptions } : {}
755
551
  });
756
552
  }
757
553
  startPingInterval() {
@@ -807,7 +603,7 @@ var SessionGatewayClient = class {
807
603
  const jitter = delay * .3 * (Math.random() * 2 - 1);
808
604
  this.reconnectTimer = setTimeout(() => {
809
605
  this.reconnectTimer = null;
810
- if (this.state === "reconnecting" && this.ws === null) this.connectWebSocket();
606
+ this.connect();
811
607
  }, delay + jitter);
812
608
  }
813
609
  cleanup() {
@@ -841,7 +637,7 @@ var SessionGatewayClient = class {
841
637
  ...this.connectionMetrics,
842
638
  quality
843
639
  };
844
- invokeClientCallback("onMetricsChange", () => this.handlers.onMetricsChange?.(this.connectionMetrics), this.handlers.onError);
640
+ this.handlers.onMetricsChange?.(this.connectionMetrics);
845
641
  }
846
642
  }
847
643
  trackReconnection() {
@@ -855,17 +651,22 @@ var SessionGatewayClient = class {
855
651
  this.updateConnectionQuality();
856
652
  }
857
653
  loadReplayState() {
858
- const state = this.replayStateStore.load();
859
- this.lastEventId = state.lastEventId;
860
- this.currentExecutionId = state.executionId;
861
- this.currentSessionId = state.sessionId;
654
+ if (!this.config.enableReplayPersistence) return;
655
+ const prefix = this.config.replayStorageKeyPrefix;
656
+ try {
657
+ this.lastEventId = this.config.replayStorage.getItem(prefix + STORAGE_KEYS.lastEventId);
658
+ this.currentExecutionId = this.config.replayStorage.getItem(prefix + STORAGE_KEYS.executionId);
659
+ this.currentSessionId = this.config.replayStorage.getItem(prefix + STORAGE_KEYS.sessionId);
660
+ } catch {}
862
661
  }
863
662
  saveReplayState() {
864
- this.replayStateStore.save({
865
- lastEventId: this.lastEventId,
866
- executionId: this.currentExecutionId,
867
- sessionId: this.currentSessionId
868
- });
663
+ if (!this.config.enableReplayPersistence) return;
664
+ const prefix = this.config.replayStorageKeyPrefix;
665
+ try {
666
+ if (this.lastEventId) this.config.replayStorage.setItem(prefix + STORAGE_KEYS.lastEventId, this.lastEventId);
667
+ if (this.currentExecutionId) this.config.replayStorage.setItem(prefix + STORAGE_KEYS.executionId, this.currentExecutionId);
668
+ if (this.currentSessionId) this.config.replayStorage.setItem(prefix + STORAGE_KEYS.sessionId, this.currentSessionId);
669
+ } catch {}
869
670
  }
870
671
  };
871
672
  //#endregion