@tangle-network/sandbox 0.11.1-develop.20260718000408.b3f0f2a → 0.11.1-develop.20260718023623.c7df56e

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,40 +1,65 @@
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"
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";
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
+ }
26
11
  }
27
12
  //#endregion
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
- */
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
+ }
57
+ };
58
+ //#endregion
59
+ //#region src/session-gateway/protocol.ts
35
60
  const DEFAULT_CONFIG = {
36
61
  transport: "websocket",
37
- channels: ["*"],
62
+ channels: [],
38
63
  autoReconnect: true,
39
64
  maxReconnectAttempts: 10,
40
65
  initialReconnectDelayMs: 1e3,
@@ -46,29 +71,47 @@ const DEFAULT_CONFIG = {
46
71
  maxDeduplicationSize: 1e3,
47
72
  enableReplayPersistence: false,
48
73
  replayStorageKeyPrefix: "session_gateway_",
49
- latencyHistorySize: 10
50
- };
51
- const STORAGE_KEYS = {
52
- lastEventId: "last_event_id",
53
- executionId: "execution_id",
54
- sessionId: "session_id"
74
+ latencyHistorySize: 10,
75
+ connectionEstablishmentTimeoutMs: 1e4
55
76
  };
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
- */
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
+ ]);
62
92
  const WIRE_TYPE_MAP = {
63
93
  "sidecar.connected": "runtime.connected",
64
94
  "sidecar.disconnected": "runtime.disconnected",
65
95
  "sidecar.not_found": "runtime.not_found",
66
- "sidecar.ready": "runtime.ready"
96
+ "sidecar.ready": "runtime.ready",
97
+ "sidecar.reconnecting": "runtime.reconnecting",
98
+ "sidecar.removed": "runtime.removed"
67
99
  };
68
- /**
69
- * Memory-based storage fallback when localStorage is not available.
70
- */
71
- var MemoryStorage = class {
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
+ const STORAGE_KEYS = {
110
+ lastEventId: "last_event_id",
111
+ executionId: "execution_id",
112
+ sessionId: "session_id"
113
+ };
114
+ var MemoryReplayStorage = class {
72
115
  store = /* @__PURE__ */ new Map();
73
116
  getItem(key) {
74
117
  return this.store.get(key) ?? null;
@@ -80,15 +123,141 @@ var MemoryStorage = class {
80
123
  this.store.delete(key);
81
124
  }
82
125
  };
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";
83
250
  var SessionGatewayClient = class {
84
251
  ws = null;
85
252
  config;
86
253
  handlers;
254
+ replayStateStore;
255
+ tokenRefresher;
87
256
  currentToken;
88
- isRefreshingToken = false;
89
257
  state = "disconnected";
90
258
  connectedAt = null;
91
259
  hasConnectedBefore = false;
260
+ connectionEstablishedForSocket = false;
92
261
  lastPingAt = null;
93
262
  lastPongAt = null;
94
263
  lastPingSentAt = null;
@@ -102,6 +271,7 @@ var SessionGatewayClient = class {
102
271
  pingInterval = null;
103
272
  pongTimeout = null;
104
273
  pendingCallbacks = /* @__PURE__ */ new Map();
274
+ connectionWaiters;
105
275
  messageIdCounter = 0;
106
276
  processedEventIds = /* @__PURE__ */ new Set();
107
277
  pingLatencyHistory = [];
@@ -115,22 +285,12 @@ var SessionGatewayClient = class {
115
285
  received: 0
116
286
  };
117
287
  constructor(config) {
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();
288
+ if (!config.token.trim()) throw new Error("Session gateway token must not be empty");
289
+ const replayStorage = resolveReplayStorage(config.replayStorage);
129
290
  this.config = {
130
291
  url: config.url,
131
292
  sessionId: config.sessionId,
132
293
  token: config.token,
133
- onTokenRefresh: config.onTokenRefresh,
134
294
  transport: config.transport ?? DEFAULT_CONFIG.transport,
135
295
  channels: config.channels ?? DEFAULT_CONFIG.channels,
136
296
  autoReconnect: config.autoReconnect ?? DEFAULT_CONFIG.autoReconnect,
@@ -143,11 +303,14 @@ var SessionGatewayClient = class {
143
303
  enableDeduplication: config.enableDeduplication ?? DEFAULT_CONFIG.enableDeduplication,
144
304
  maxDeduplicationSize: config.maxDeduplicationSize ?? DEFAULT_CONFIG.maxDeduplicationSize,
145
305
  enableReplayPersistence: config.enableReplayPersistence ?? DEFAULT_CONFIG.enableReplayPersistence,
146
- replayStorage: storage,
147
306
  replayStorageKeyPrefix: config.replayStorageKeyPrefix ?? DEFAULT_CONFIG.replayStorageKeyPrefix,
148
- latencyHistorySize: config.latencyHistorySize ?? DEFAULT_CONFIG.latencyHistorySize
307
+ latencyHistorySize: config.latencyHistorySize ?? DEFAULT_CONFIG.latencyHistorySize,
308
+ connectionEstablishmentTimeoutMs: config.connectionEstablishmentTimeoutMs ?? DEFAULT_CONFIG.connectionEstablishmentTimeoutMs
149
309
  };
150
310
  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);
151
314
  this.currentToken = config.token;
152
315
  if (this.config.enableReplayPersistence) this.loadReplayState();
153
316
  }
@@ -155,30 +318,37 @@ var SessionGatewayClient = class {
155
318
  * Connect to the session gateway.
156
319
  */
157
320
  connect() {
158
- if (this.state === "connected" || this.state === "connecting") return;
159
- const isReconnect = this.hasConnectedBefore;
160
- this.setState(isReconnect ? "reconnecting" : "connecting");
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;
161
325
  this.connectWebSocket();
162
326
  }
163
327
  connectWebSocket() {
164
328
  const url = new URL(this.config.url);
165
329
  url.searchParams.set("token", this.currentToken);
330
+ this.connectionEstablishedForSocket = false;
166
331
  const socket = new WebSocket(url.toString());
167
332
  this.ws = socket;
168
333
  socket.onopen = () => {
169
334
  if (this.ws === socket) this.handleOpen();
170
335
  };
171
336
  socket.onclose = (event) => this.handleClose(socket, event.code, event.reason);
172
- socket.onerror = (_event) => {};
337
+ socket.onerror = () => this.handleError(socket);
173
338
  socket.onmessage = (event) => {
174
339
  if (this.ws === socket) this.handleMessage(event.data);
175
340
  };
341
+ this.connectionWaiters.start((error) => {
342
+ this.closeBeforeEstablishment(socket, CONNECTION_ESTABLISHMENT_TIMEOUT_CODE, error.message);
343
+ });
176
344
  }
177
345
  /**
178
346
  * Disconnect from the session gateway.
179
347
  */
180
348
  disconnect() {
349
+ this.connectionWaiters.rejectAll(/* @__PURE__ */ new Error("Client disconnected"));
181
350
  this.cleanup();
351
+ this.connectionEstablishedForSocket = false;
182
352
  const socket = this.ws;
183
353
  this.ws = null;
184
354
  if (socket) socket.close(1e3, "Client disconnect");
@@ -188,10 +358,10 @@ var SessionGatewayClient = class {
188
358
  * Subscribe to additional channels.
189
359
  */
190
360
  async subscribe(channels) {
191
- return (await this.sendWithResponse({
361
+ return acceptedSubscriptionChannels(await this.sendWithResponse({
192
362
  type: "subscribe",
193
363
  channels
194
- })).channels;
364
+ }));
195
365
  }
196
366
  /**
197
367
  * Unsubscribe from channels.
@@ -213,7 +383,9 @@ var SessionGatewayClient = class {
213
383
  /**
214
384
  * Request replay of events since a timestamp.
215
385
  */
216
- replay(since) {
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");
217
389
  this.send({
218
390
  type: "replay",
219
391
  since
@@ -255,14 +427,7 @@ var SessionGatewayClient = class {
255
427
  };
256
428
  this.processedEventIds.clear();
257
429
  this.duplicatesSkipped = 0;
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
- }
430
+ this.replayStateStore.clear();
266
431
  }
267
432
  /**
268
433
  * Get current replay state for UI indicators.
@@ -306,12 +471,13 @@ var SessionGatewayClient = class {
306
471
  resetMetrics() {
307
472
  this.pingLatencyHistory = [];
308
473
  this.connectionMetrics = { ...INITIAL_METRICS };
309
- this.handlers.onMetricsChange?.(this.connectionMetrics);
474
+ invokeClientCallback("onMetricsChange", () => this.handlers.onMetricsChange?.(this.connectionMetrics), this.handlers.onError);
310
475
  }
311
476
  /**
312
477
  * Update the auth token and reconnect with the new token.
313
478
  */
314
479
  updateToken(token) {
480
+ if (!token.trim()) throw new Error("Session gateway token must not be empty");
315
481
  this.currentToken = token;
316
482
  if (this.state === "connected" && this.ws) this.ws.close(1e3, "Token refreshed");
317
483
  }
@@ -325,73 +491,110 @@ var SessionGatewayClient = class {
325
491
  if (this.state !== state) {
326
492
  this.state = state;
327
493
  this.updateConnectionQuality();
328
- this.handlers.onStateChange?.(state);
494
+ invokeClientCallback("onStateChange", () => this.handlers.onStateChange?.(state), this.handlers.onError);
329
495
  }
330
496
  }
331
497
  handleOpen() {
498
+ this.missedPongCount = 0;
499
+ }
500
+ handleConnectionEstablished(sessionId) {
501
+ if (this.connectionEstablishedForSocket) return;
502
+ this.connectionWaiters.clear();
332
503
  const isReconnect = this.hasConnectedBefore;
504
+ this.connectionEstablishedForSocket = true;
333
505
  this.hasConnectedBefore = true;
334
- this.setState("connected");
335
506
  this.connectedAt = Date.now();
336
507
  this.reconnectAttempts = 0;
337
- this.missedPongCount = 0;
338
508
  this.startPingInterval();
339
509
  this.restoreSubscriptions();
340
- if (isReconnect) {
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()) {
341
515
  this.trackReconnection();
342
- this.handlers.onReconnect?.();
516
+ invokeClientCallback("onReconnect", () => this.handlers.onReconnect?.(), this.handlers.onError);
343
517
  }
344
518
  }
345
519
  handleClose(socket, code, reason) {
346
520
  if (this.ws !== socket) return;
521
+ this.connectionWaiters.clear();
347
522
  this.ws = null;
523
+ this.connectionEstablishedForSocket = false;
348
524
  this.cleanup();
349
- this.handlers.onDisconnect?.(code, reason);
350
525
  if (this.config.autoReconnect && this.reconnectAttempts < this.config.maxReconnectAttempts) {
351
526
  this.setState("reconnecting");
352
- this.scheduleReconnect();
353
- } else this.setState("disconnected");
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
+ }
354
546
  }
355
547
  handleMessage(data) {
356
548
  this.messagesReceived++;
357
549
  try {
358
550
  const raw = JSON.parse(data);
359
551
  if (!raw.type && raw.messageType) raw.type = raw.messageType;
360
- if (typeof raw.type === "string" && WIRE_TYPE_MAP[raw.type] !== void 0) raw.type = WIRE_TYPE_MAP[raw.type];
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];
361
557
  if (raw.data && typeof raw.data === "object") {
362
558
  const d = raw.data;
363
559
  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
+ }
364
564
  }
565
+ if (raw.seq === void 0 && typeof raw.sequenceId === "number") raw.seq = raw.sequenceId;
365
566
  if (raw.type === "agent.event" && !raw.data && raw.channel) {
366
- const { type, messageType, channel, id, sequenceId, timestamp, ...rest } = raw;
567
+ const { type, messageType, channel, id, seq, sequenceId, timestamp, ...rest } = raw;
367
568
  raw.data = rest;
368
569
  }
369
570
  const message = raw;
370
- if (message.type === "pong") {
371
- this.handlePong(message.timestamp);
372
- this.dispatchMessage(message);
571
+ if (CONTROL_MESSAGE_TYPES.has(message.type)) {
572
+ if (message.type === "pong") this.handlePong(message.timestamp);
573
+ this.deliverMessage(message);
373
574
  return;
374
575
  }
375
- if ("id" in message && message.id && this.config.enableDeduplication) {
576
+ if ("id" in message && message.id) {
376
577
  const eventId = message.id;
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++;
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
+ }
389
592
  }
390
593
  }
391
594
  this.lastEventId = eventId;
392
595
  this.saveReplayState();
393
596
  }
394
- this.dispatchMessage(message);
597
+ this.deliverMessage(message);
395
598
  } catch {}
396
599
  }
397
600
  handlePong(timestamp) {
@@ -407,6 +610,7 @@ var SessionGatewayClient = class {
407
610
  }
408
611
  }
409
612
  dispatchMessage(message) {
613
+ if (message.type === "error" && message.code === "REPLAY_CURSOR_UNAVAILABLE") this.clearReplayState();
410
614
  if ("id" in message && message.id) {
411
615
  const callback = this.pendingCallbacks.get(message.id);
412
616
  if (callback) {
@@ -418,7 +622,7 @@ var SessionGatewayClient = class {
418
622
  }
419
623
  switch (message.type) {
420
624
  case "connection.established":
421
- this.handlers.onConnect?.(message.data.sessionId);
625
+ this.handleConnectionEstablished(message.data.sessionId);
422
626
  break;
423
627
  case "runtime.connected":
424
628
  this.handlers.onRuntimeConnected?.(message.data.sandboxId, message.data.baseUrl);
@@ -448,7 +652,7 @@ var SessionGatewayClient = class {
448
652
  case "agent.event":
449
653
  this.eventsReceived++;
450
654
  this.trackExecutionContext(message.data);
451
- this.handlers.onAgentEvent?.(message.channel, message.data, message.sequenceId);
655
+ this.handlers.onAgentEvent?.(message.channel, message.data, message.seq ?? message.sequenceId);
452
656
  break;
453
657
  case "replay.start":
454
658
  this.isReplaying = true;
@@ -470,8 +674,8 @@ var SessionGatewayClient = class {
470
674
  this.handlers.onError?.(message.message, message.code);
471
675
  break;
472
676
  case "token.expiring":
473
- this.handlers.onTokenExpiring?.(message.data.secondsRemaining);
474
- this.handleTokenExpiring();
677
+ invokeClientCallback("onTokenExpiring", () => this.handlers.onTokenExpiring?.(message.data.secondsRemaining), this.handlers.onError);
678
+ this.tokenRefresher.refresh();
475
679
  break;
476
680
  case "token.expired":
477
681
  this.handlers.onTokenExpired?.();
@@ -481,6 +685,10 @@ var SessionGatewayClient = class {
481
685
  break;
482
686
  }
483
687
  }
688
+ deliverMessage(message) {
689
+ invokeClientCallback("message dispatch", () => this.dispatchMessage(message), this.handlers.onError);
690
+ invokeClientCallback("onMessage", () => this.handlers.onMessage?.(message), this.handlers.onError);
691
+ }
484
692
  trackExecutionContext(data) {
485
693
  if (typeof data !== "object" || data === null) return;
486
694
  const eventData = data;
@@ -493,25 +701,15 @@ var SessionGatewayClient = class {
493
701
  this.saveReplayState();
494
702
  }
495
703
  }
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
- }
508
704
  send(message) {
509
705
  if (this.ws?.readyState === WebSocket.OPEN) {
510
706
  this.ws.send(JSON.stringify(message));
511
707
  this.messagesSent++;
512
708
  }
513
709
  }
514
- sendWithResponse(message) {
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");
515
713
  return new Promise((resolve, reject) => {
516
714
  const id = `msg_${++this.messageIdCounter}`;
517
715
  this.pendingCallbacks.set(id, {
@@ -530,14 +728,20 @@ var SessionGatewayClient = class {
530
728
  });
531
729
  });
532
730
  }
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
+ }
533
736
  restoreSubscriptions() {
534
737
  const channels = this.config.channels;
535
738
  const replayOptions = {};
536
739
  if (this.lastEventId) replayOptions.lastEventId = this.lastEventId;
537
740
  if (this.currentExecutionId) replayOptions.executionId = this.currentExecutionId;
538
741
  if (this.currentSessionId) replayOptions.sessionId = this.currentSessionId;
539
- const hasReplayState = Object.keys(replayOptions).length > 0;
540
- if (hasReplayState) {
742
+ const hasReplayCursor = this.lastEventId !== null;
743
+ if (channels.length === 0 && !hasReplayCursor) return;
744
+ if (hasReplayCursor) {
541
745
  this.isReplaying = true;
542
746
  this.replayProgress = {
543
747
  total: 0,
@@ -547,7 +751,7 @@ var SessionGatewayClient = class {
547
751
  this.send({
548
752
  type: "subscribe",
549
753
  channels,
550
- ...hasReplayState ? { replayOptions } : {}
754
+ ...hasReplayCursor ? { replayOptions } : {}
551
755
  });
552
756
  }
553
757
  startPingInterval() {
@@ -603,7 +807,7 @@ var SessionGatewayClient = class {
603
807
  const jitter = delay * .3 * (Math.random() * 2 - 1);
604
808
  this.reconnectTimer = setTimeout(() => {
605
809
  this.reconnectTimer = null;
606
- this.connect();
810
+ if (this.state === "reconnecting" && this.ws === null) this.connectWebSocket();
607
811
  }, delay + jitter);
608
812
  }
609
813
  cleanup() {
@@ -637,7 +841,7 @@ var SessionGatewayClient = class {
637
841
  ...this.connectionMetrics,
638
842
  quality
639
843
  };
640
- this.handlers.onMetricsChange?.(this.connectionMetrics);
844
+ invokeClientCallback("onMetricsChange", () => this.handlers.onMetricsChange?.(this.connectionMetrics), this.handlers.onError);
641
845
  }
642
846
  }
643
847
  trackReconnection() {
@@ -651,22 +855,17 @@ var SessionGatewayClient = class {
651
855
  this.updateConnectionQuality();
652
856
  }
653
857
  loadReplayState() {
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 {}
858
+ const state = this.replayStateStore.load();
859
+ this.lastEventId = state.lastEventId;
860
+ this.currentExecutionId = state.executionId;
861
+ this.currentSessionId = state.sessionId;
661
862
  }
662
863
  saveReplayState() {
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 {}
864
+ this.replayStateStore.save({
865
+ lastEventId: this.lastEventId,
866
+ executionId: this.currentExecutionId,
867
+ sessionId: this.currentSessionId
868
+ });
670
869
  }
671
870
  };
672
871
  //#endregion