@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.
- package/README.md +16 -39
- package/dist/agent/index.d.ts +3 -3
- package/dist/{client-L4c2I5tL.d.ts → client-DBioX_CL.d.ts} +2 -2
- package/dist/{client-yksqE5VH.js → client-D_67csnK.js} +7 -15
- package/dist/collaboration/index.js +1 -1
- package/dist/{collaboration-CeNQA9H_.js → collaboration-DDnuTcZd.js} +1 -1
- package/dist/core.d.ts +4 -4
- package/dist/core.js +3 -3
- package/dist/{errors-hbpL_-S6.js → errors-Cbs78OlF.js} +3 -3
- package/dist/{errors-B-8AMQdY.d.ts → errors-ntvaf0_F.d.ts} +1 -1
- package/dist/{index-XaVF9qK-.d.ts → index-Dj8LzvId.d.ts} +2 -2
- package/dist/index.d.ts +6 -6
- package/dist/index.js +8 -8
- package/dist/intelligence/index.js +1 -1
- package/dist/{runtime-api-r8vQVMe4.js → runtime-api-kukaWPtF.js} +5 -6
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.js +2 -2
- package/dist/{sandbox-C0qo2TZO.d.ts → sandbox-BRoxCvpS.d.ts} +29 -81
- package/dist/{sandbox-C8WoMyT0.js → sandbox-DaLPTCRA.js} +32 -133
- package/dist/session-gateway/index.d.ts +13 -82
- package/dist/session-gateway/index.js +144 -343
- package/dist/tangle/index.d.ts +1 -1
- package/dist/tangle/index.js +1 -1
- package/dist/{tangle-m1lRkQlR.js → tangle-DHOkbr5a.js} +4 -4
- package/dist/{types-DACKEpON.d.ts → types-D2C72ySk.d.ts} +30 -83
- package/package.json +3 -4
|
@@ -1,65 +1,40 @@
|
|
|
1
|
-
//#region src/session-gateway/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
289
|
-
|
|
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"
|
|
322
|
-
const
|
|
323
|
-
this.setState(
|
|
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 = () =>
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
366
|
+
const { type, messageType, channel, id, sequenceId, timestamp, ...rest } = raw;
|
|
568
367
|
raw.data = rest;
|
|
569
368
|
}
|
|
570
369
|
const message = raw;
|
|
571
|
-
if (
|
|
572
|
-
|
|
573
|
-
this.
|
|
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.
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
678
|
-
this.
|
|
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
|
-
|
|
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
|
|
743
|
-
if (
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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.
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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
|