@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.
- package/README.md +39 -16
- package/dist/agent/index.d.ts +3 -3
- package/dist/{client-1LKomw8A.js → client-BYpjTOO7.js} +1 -1
- package/dist/{client-mpqiRmdi.d.ts → client-ChixG64E.d.ts} +2 -2
- package/dist/core.d.ts +3 -3
- package/dist/core.js +2 -2
- package/dist/{index-DD8we2b9.d.ts → index-CmLkWZDA.d.ts} +2 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/runtime.d.ts +1 -1
- package/dist/{sandbox-BhbAxbub.d.ts → sandbox-BBkFvFe6.d.ts} +6 -6
- package/dist/{sandbox-Cf25Dgrc.js → sandbox-Ban9d-wW.js} +5 -7
- package/dist/session-gateway/index.d.ts +82 -13
- package/dist/session-gateway/index.js +343 -144
- package/dist/tangle/index.d.ts +1 -1
- package/dist/tangle/index.js +1 -1
- package/dist/{tangle-DcGiYTx3.js → tangle-BbC2o7B8.js} +1 -1
- package/dist/{types-Di5KUpoc.d.ts → types-D6WC8r4K.d.ts} +16 -13
- package/package.json +2 -1
|
@@ -1,40 +1,65 @@
|
|
|
1
|
-
//#region src/session-gateway/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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/
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
|
160
|
-
this.setState(
|
|
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 = (
|
|
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
|
-
}))
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
371
|
-
this.handlePong(message.timestamp);
|
|
372
|
-
this.
|
|
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
|
|
576
|
+
if ("id" in message && message.id) {
|
|
376
577
|
const eventId = message.id;
|
|
377
|
-
if (this.
|
|
378
|
-
this.
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
540
|
-
if (
|
|
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
|
-
...
|
|
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.
|
|
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
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|