@phpsandbox/sdk 0.0.51 → 0.0.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1 -1
- package/dist/auth.d.ts +0 -1
- package/dist/auth.js +0 -1
- package/dist/beacon/index.d.ts +4 -5
- package/dist/beacon/index.js +11 -12
- package/dist/beacon/navigator.d.ts +1 -2
- package/dist/beacon/navigator.js +0 -1
- package/dist/beacon/types.d.ts +12 -13
- package/dist/beacon/types.js +0 -1
- package/dist/browser/phpsandbox-sdk.esm.js +282 -479
- package/dist/browser/phpsandbox-sdk.esm.min.js +2 -3
- package/dist/browser/phpsandbox-sdk.iife.js +277 -479
- package/dist/browser/phpsandbox-sdk.iife.min.js +2 -3
- package/dist/composer.d.ts +2 -3
- package/dist/composer.js +0 -1
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -1
- package/dist/container.d.ts +0 -1
- package/dist/container.js +0 -1
- package/dist/errors/index.d.ts +0 -1
- package/dist/errors/index.js +0 -1
- package/dist/events/index.d.ts +15 -15
- package/dist/events/index.js +16 -9
- package/dist/filesystem.d.ts +6 -7
- package/dist/filesystem.js +36 -14
- package/dist/git.d.ts +0 -1
- package/dist/git.js +0 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +38 -25
- package/dist/laravel.d.ts +1 -2
- package/dist/laravel.js +1 -2
- package/dist/log.d.ts +3 -4
- package/dist/log.js +1 -2
- package/dist/lsp.d.ts +10 -11
- package/dist/lsp.js +9 -10
- package/dist/repl.d.ts +3 -4
- package/dist/repl.js +2 -3
- package/dist/services.d.ts +0 -1
- package/dist/services.js +0 -1
- package/dist/shell.d.ts +1 -2
- package/dist/shell.js +1 -2
- package/dist/socket/index.d.ts +23 -129
- package/dist/socket/index.js +60 -350
- package/dist/terminal.d.ts +2 -3
- package/dist/terminal.js +45 -26
- package/dist/types.d.ts +0 -1
- package/dist/types.js +0 -1
- package/dist/utils/disposable.d.ts +0 -1
- package/dist/utils/disposable.js +0 -1
- package/dist/utils/promise.d.ts +0 -1
- package/dist/utils/promise.js +0 -1
- package/dist/utils/retry.d.ts +0 -1
- package/dist/utils/retry.js +0 -1
- package/dist/utils/websocket.d.ts +0 -1
- package/dist/utils/websocket.js +0 -1
- package/docs/api-summary.md +136 -0
- package/docs/cdn-usage.md +80 -0
- package/docs/getting-started.md +152 -0
- package/examples/README.md +206 -0
- package/examples/basic.ts +24 -0
- package/examples/get.ts +21 -0
- package/examples/git-merge-smoke.ts +415 -0
- package/examples/laravel-load-test.ts +2829 -0
- package/examples/notebook-readonly-smoke.ts +823 -0
- package/package.json +13 -7
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/beacon/index.d.ts.map +0 -1
- package/dist/beacon/index.js.map +0 -1
- package/dist/beacon/navigator.d.ts.map +0 -1
- package/dist/beacon/navigator.js.map +0 -1
- package/dist/beacon/types.d.ts.map +0 -1
- package/dist/beacon/types.js.map +0 -1
- package/dist/browser/phpsandbox-sdk.esm.js.map +0 -7
- package/dist/browser/phpsandbox-sdk.esm.min.js.map +0 -7
- package/dist/browser/phpsandbox-sdk.iife.js.map +0 -7
- package/dist/browser/phpsandbox-sdk.iife.min.js.map +0 -7
- package/dist/composer.d.ts.map +0 -1
- package/dist/composer.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/container.d.ts.map +0 -1
- package/dist/container.js.map +0 -1
- package/dist/errors/index.d.ts.map +0 -1
- package/dist/errors/index.js.map +0 -1
- package/dist/events/index.d.ts.map +0 -1
- package/dist/events/index.js.map +0 -1
- package/dist/filesystem.d.ts.map +0 -1
- package/dist/filesystem.js.map +0 -1
- package/dist/git.d.ts.map +0 -1
- package/dist/git.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/laravel.d.ts.map +0 -1
- package/dist/laravel.js.map +0 -1
- package/dist/log.d.ts.map +0 -1
- package/dist/log.js.map +0 -1
- package/dist/lsp.d.ts.map +0 -1
- package/dist/lsp.js.map +0 -1
- package/dist/repl.d.ts.map +0 -1
- package/dist/repl.js.map +0 -1
- package/dist/services.d.ts.map +0 -1
- package/dist/services.js.map +0 -1
- package/dist/shell.d.ts.map +0 -1
- package/dist/shell.js.map +0 -1
- package/dist/socket/index.d.ts.map +0 -1
- package/dist/socket/index.js.map +0 -1
- package/dist/terminal.d.ts.map +0 -1
- package/dist/terminal.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/utils/disposable.d.ts.map +0 -1
- package/dist/utils/disposable.js.map +0 -1
- package/dist/utils/promise.d.ts.map +0 -1
- package/dist/utils/promise.js.map +0 -1
- package/dist/utils/retry.d.ts.map +0 -1
- package/dist/utils/retry.js.map +0 -1
- package/dist/utils/websocket.d.ts.map +0 -1
- package/dist/utils/websocket.js.map +0 -1
package/dist/socket/index.js
CHANGED
|
@@ -8,12 +8,14 @@ import { nanoid } from 'nanoid';
|
|
|
8
8
|
import { encode, decode } from '@msgpack/msgpack';
|
|
9
9
|
import { ErrorEvent as SDKErrorEvent, RateLimitError } from '../types.js';
|
|
10
10
|
import { retry } from '../utils/retry.js';
|
|
11
|
-
import
|
|
11
|
+
import * as ReconnectingWebSocketModule from 'reconnecting-websocket';
|
|
12
12
|
import { timeout } from '../utils/promise.js';
|
|
13
13
|
import { describeWebSocketEvent } from '../utils/websocket.js';
|
|
14
14
|
import WebSocket from 'isomorphic-ws';
|
|
15
15
|
import { NamedDisposable } from '../utils/disposable.js';
|
|
16
16
|
import { NotebookUnavailableError, PromiseTimeoutError, SendTimeoutError } from '../errors/index.js';
|
|
17
|
+
const ReconnectingWebSocket = (ReconnectingWebSocketModule.default
|
|
18
|
+
?? ReconnectingWebSocketModule);
|
|
17
19
|
export var SocketEvent;
|
|
18
20
|
(function (SocketEvent) {
|
|
19
21
|
SocketEvent["BootError"] = "Events.BootError";
|
|
@@ -60,21 +62,6 @@ export class Transport {
|
|
|
60
62
|
this.connectPromise = null;
|
|
61
63
|
this.terminalError = null;
|
|
62
64
|
this.pingIntervalStarted = false;
|
|
63
|
-
// Connection health monitoring
|
|
64
|
-
this.connectionStats = {
|
|
65
|
-
connectTime: 0,
|
|
66
|
-
reconnectCount: 0,
|
|
67
|
-
lastPingTime: 0,
|
|
68
|
-
lastPongTime: 0,
|
|
69
|
-
avgResponseTime: 0,
|
|
70
|
-
totalMessages: 0,
|
|
71
|
-
totalErrors: 0,
|
|
72
|
-
connectionStartTime: 0,
|
|
73
|
-
};
|
|
74
|
-
// Message queue for disconnection handling
|
|
75
|
-
this.messageQueue = [];
|
|
76
|
-
this.MAX_QUEUE_SIZE = 100;
|
|
77
|
-
this.QUEUE_TIMEOUT = 30000; // 30 seconds
|
|
78
65
|
this.incomingMessageChain = Promise.resolve();
|
|
79
66
|
// Validate configuration
|
|
80
67
|
this.validateConfiguration(options);
|
|
@@ -82,11 +69,8 @@ export class Transport {
|
|
|
82
69
|
// Use configurable ping interval
|
|
83
70
|
this.PING_INTERVAL = options.pingInterval ?? 30000;
|
|
84
71
|
this.CONNECT_WAIT_TIMEOUT = options.connectWaitTimeout ?? 30000;
|
|
85
|
-
// Initialize connection stats
|
|
86
|
-
this.connectionStats.connectionStartTime = Date.now();
|
|
87
72
|
// Always start closed by default (lazy initialization)
|
|
88
73
|
const startClosed = options.startClosed !== false;
|
|
89
|
-
// @ts-expect-error
|
|
90
74
|
this.rws = new ReconnectingWebSocket(this.url.toString(), [], {
|
|
91
75
|
WebSocket: options.webSocket ?? globalThis.WebSocket ?? WebSocket,
|
|
92
76
|
connectionTimeout: options.connectionTimeout,
|
|
@@ -101,8 +85,6 @@ export class Transport {
|
|
|
101
85
|
options,
|
|
102
86
|
});
|
|
103
87
|
this.registerWatchers();
|
|
104
|
-
this.setupConnectionHealthMonitoring();
|
|
105
|
-
this.startPeriodicMaintenance();
|
|
106
88
|
}
|
|
107
89
|
/**
|
|
108
90
|
* Validate configuration options
|
|
@@ -149,7 +131,6 @@ export class Transport {
|
|
|
149
131
|
.catch(() => undefined)
|
|
150
132
|
.then(() => this.processIncomingMessage(ev))
|
|
151
133
|
.catch((error) => {
|
|
152
|
-
this.connectionStats.totalErrors++;
|
|
153
134
|
this.log('error', 'Failed to process queued WebSocket frame', {
|
|
154
135
|
error: error instanceof Error ? error.message : String(error),
|
|
155
136
|
});
|
|
@@ -169,11 +150,13 @@ export class Transport {
|
|
|
169
150
|
});
|
|
170
151
|
}
|
|
171
152
|
async processIncomingMessage(ev) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
this.
|
|
153
|
+
let buffer;
|
|
154
|
+
try {
|
|
155
|
+
buffer = await this.toArrayBuffer(ev.data);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
175
158
|
this.log('error', 'Unexpected WebSocket message type', {
|
|
176
|
-
error: error.message,
|
|
159
|
+
error: error instanceof Error ? error.message : String(error),
|
|
177
160
|
messageType: typeof ev.data,
|
|
178
161
|
});
|
|
179
162
|
this.eventEmitter.emit('transport.error', {
|
|
@@ -184,7 +167,6 @@ export class Transport {
|
|
|
184
167
|
});
|
|
185
168
|
return;
|
|
186
169
|
}
|
|
187
|
-
const buffer = await ev.data.arrayBuffer();
|
|
188
170
|
if (buffer.byteLength === 0) {
|
|
189
171
|
this.log('warn', 'Ignoring empty WebSocket frame');
|
|
190
172
|
return;
|
|
@@ -194,7 +176,6 @@ export class Transport {
|
|
|
194
176
|
decoded = decode(buffer);
|
|
195
177
|
}
|
|
196
178
|
catch (error) {
|
|
197
|
-
this.connectionStats.totalErrors++;
|
|
198
179
|
this.log('error', 'Failed to decode WebSocket frame', {
|
|
199
180
|
error: error instanceof Error ? error.message : String(error),
|
|
200
181
|
byteLength: buffer.byteLength,
|
|
@@ -211,7 +192,6 @@ export class Transport {
|
|
|
211
192
|
await this.handleRawMessage(decoded);
|
|
212
193
|
}
|
|
213
194
|
catch (error) {
|
|
214
|
-
this.connectionStats.totalErrors++;
|
|
215
195
|
this.log('error', 'Failed to process WebSocket frame', {
|
|
216
196
|
error: error instanceof Error ? error.message : String(error),
|
|
217
197
|
byteLength: buffer.byteLength,
|
|
@@ -224,6 +204,19 @@ export class Transport {
|
|
|
224
204
|
});
|
|
225
205
|
}
|
|
226
206
|
}
|
|
207
|
+
async toArrayBuffer(data) {
|
|
208
|
+
if (data instanceof Blob) {
|
|
209
|
+
return data.arrayBuffer();
|
|
210
|
+
}
|
|
211
|
+
if (data instanceof ArrayBuffer) {
|
|
212
|
+
return data;
|
|
213
|
+
}
|
|
214
|
+
if (ArrayBuffer.isView(data)) {
|
|
215
|
+
const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
216
|
+
return bytes.slice().buffer;
|
|
217
|
+
}
|
|
218
|
+
throw new Error('Unexpected message type: ' + typeof data);
|
|
219
|
+
}
|
|
227
220
|
async handleRawMessage(ev) {
|
|
228
221
|
if (this.closed) {
|
|
229
222
|
this.log('debug', 'Ignoring message received after transport was closed');
|
|
@@ -246,7 +239,8 @@ export class Transport {
|
|
|
246
239
|
return;
|
|
247
240
|
}
|
|
248
241
|
if (event === SocketEvent.BootError) {
|
|
249
|
-
const
|
|
242
|
+
const errorData = data;
|
|
243
|
+
const error = new NotebookUnavailableError(errorData?.message || 'Notebook is no longer available', errorData || {});
|
|
250
244
|
this.log('error', 'Boot error received', { data });
|
|
251
245
|
this.terminate(error, 4000, error.message);
|
|
252
246
|
return;
|
|
@@ -277,19 +271,16 @@ export class Transport {
|
|
|
277
271
|
await this.handleMessage(event, data, as);
|
|
278
272
|
}
|
|
279
273
|
catch (e) {
|
|
280
|
-
this.connectionStats.totalErrors++;
|
|
281
274
|
if (e instanceof InvalidMessageError) {
|
|
282
275
|
this.log('error', 'Invalid message format', {
|
|
283
276
|
error: e.message,
|
|
284
277
|
data: e.data,
|
|
285
|
-
totalErrors: this.connectionStats.totalErrors,
|
|
286
278
|
});
|
|
287
279
|
}
|
|
288
280
|
else {
|
|
289
281
|
this.log('error', 'Failed to parse message', {
|
|
290
282
|
ev,
|
|
291
283
|
error: e instanceof Error ? e.message : String(e),
|
|
292
|
-
totalErrors: this.connectionStats.totalErrors,
|
|
293
284
|
});
|
|
294
285
|
}
|
|
295
286
|
// Don't throw - we want to continue processing other messages
|
|
@@ -305,7 +296,7 @@ export class Transport {
|
|
|
305
296
|
async handleMessage(event, data, as) {
|
|
306
297
|
if (event === SocketEvent.ClientId) {
|
|
307
298
|
this.clientId = data.id;
|
|
308
|
-
this.eventEmitter.emit(event,
|
|
299
|
+
this.eventEmitter.emit(event, this.clientId);
|
|
309
300
|
return;
|
|
310
301
|
}
|
|
311
302
|
event && this.eventEmitter.emit(as || event, data);
|
|
@@ -314,7 +305,7 @@ export class Transport {
|
|
|
314
305
|
return this.eventEmitter.listen(event, listener);
|
|
315
306
|
}
|
|
316
307
|
onDidBootError(listener) {
|
|
317
|
-
this.eventEmitter.listen('transport.boot_error', (event) => {
|
|
308
|
+
return this.eventEmitter.listen('transport.boot_error', (event) => {
|
|
318
309
|
listener(event.error);
|
|
319
310
|
});
|
|
320
311
|
}
|
|
@@ -322,12 +313,7 @@ export class Transport {
|
|
|
322
313
|
this.eventEmitter.removeListener(event, listener);
|
|
323
314
|
}
|
|
324
315
|
listenOnce(event, listener, context) {
|
|
325
|
-
this.eventEmitter.once(event, listener, context);
|
|
326
|
-
this.disposables.add(`once_${event}_${nanoid()}`, {
|
|
327
|
-
dispose: () => {
|
|
328
|
-
this.eventEmitter.removeListener(event, listener);
|
|
329
|
-
},
|
|
330
|
-
});
|
|
316
|
+
return this.eventEmitter.once(event, listener, context);
|
|
331
317
|
}
|
|
332
318
|
emit(event, ...data) {
|
|
333
319
|
this.eventEmitter.emit(event, ...data);
|
|
@@ -359,20 +345,23 @@ export class Transport {
|
|
|
359
345
|
if (this.terminalError) {
|
|
360
346
|
throw this.terminalError;
|
|
361
347
|
}
|
|
362
|
-
// Clear old queued messages periodically
|
|
363
|
-
this.clearOldQueuedMessages();
|
|
364
348
|
const responseEvent = options.responseEvent || `${action}_${nanoid()}_response`;
|
|
365
349
|
const errorEvent = `${responseEvent}_error`;
|
|
366
350
|
let closeHandler;
|
|
351
|
+
let abortHandler;
|
|
367
352
|
const removeListeners = () => {
|
|
368
353
|
if (closeHandler) {
|
|
369
354
|
this.rws.removeEventListener('close', closeHandler);
|
|
370
355
|
this.rws.removeEventListener('error', closeHandler);
|
|
371
356
|
}
|
|
357
|
+
if (abortHandler) {
|
|
358
|
+
options.abortSignal?.removeEventListener('abort', abortHandler);
|
|
359
|
+
abortHandler = undefined;
|
|
360
|
+
}
|
|
372
361
|
this.eventEmitter.removeListener(responseEvent);
|
|
373
362
|
this.eventEmitter.removeListener(errorEvent);
|
|
374
363
|
};
|
|
375
|
-
const handler =
|
|
364
|
+
const handler = (resolve, reject) => {
|
|
376
365
|
if (this.terminalError) {
|
|
377
366
|
reject(this.terminalError);
|
|
378
367
|
return;
|
|
@@ -380,51 +369,28 @@ export class Transport {
|
|
|
380
369
|
const abortError = new DOMException('Request aborted', 'AbortError');
|
|
381
370
|
if (options.abortSignal?.aborted) {
|
|
382
371
|
reject(abortError);
|
|
372
|
+
return;
|
|
383
373
|
}
|
|
384
374
|
if (options.abortSignal) {
|
|
385
|
-
|
|
375
|
+
abortHandler = () => {
|
|
386
376
|
reject(abortError);
|
|
387
|
-
}
|
|
377
|
+
};
|
|
378
|
+
options.abortSignal.addEventListener('abort', abortHandler, { once: true });
|
|
388
379
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
this.log('debug', 'Connection not available, queuing message', {
|
|
392
|
-
action,
|
|
393
|
-
});
|
|
394
|
-
// Check queue size limit
|
|
395
|
-
if (this.messageQueue.length >= this.MAX_QUEUE_SIZE) {
|
|
396
|
-
const oldestMessage = this.messageQueue.shift();
|
|
397
|
-
if (oldestMessage) {
|
|
398
|
-
oldestMessage.reject(new Error('Message queue full, oldest message dropped'));
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
this.messageQueue.push({
|
|
402
|
-
action,
|
|
403
|
-
data,
|
|
404
|
-
options,
|
|
405
|
-
resolve,
|
|
406
|
-
reject,
|
|
407
|
-
timestamp: Date.now(),
|
|
408
|
-
});
|
|
380
|
+
if (!this.isConnected || this.isClosed) {
|
|
381
|
+
reject(new ConnectionFailedError(`Connection is not open while sending ${action}. Current status: ${this.status}`));
|
|
409
382
|
return;
|
|
410
383
|
}
|
|
411
|
-
const startTime = Date.now();
|
|
412
384
|
this.listenOnce(responseEvent, (response) => {
|
|
413
|
-
// Update response time stats
|
|
414
|
-
const responseTime = Date.now() - startTime;
|
|
415
|
-
this.connectionStats.avgResponseTime = (this.connectionStats.avgResponseTime + responseTime) / 2;
|
|
416
|
-
this.connectionStats.totalMessages++;
|
|
417
385
|
this.log('debug', 'Message response received', {
|
|
418
386
|
action,
|
|
419
|
-
responseTime,
|
|
420
|
-
avgResponseTime: this.connectionStats.avgResponseTime,
|
|
421
387
|
});
|
|
422
388
|
resolve(response);
|
|
423
389
|
});
|
|
424
390
|
this.listenOnce(errorEvent, (e) => {
|
|
425
|
-
this.connectionStats.totalErrors++;
|
|
426
391
|
this.log('error', 'Message error received', { action, error: e });
|
|
427
|
-
|
|
392
|
+
const error = e;
|
|
393
|
+
reject(new SDKErrorEvent(error.code, error.message, error));
|
|
428
394
|
});
|
|
429
395
|
closeHandler = (_ev) => {
|
|
430
396
|
if (_ev.code === 1008 && (_ev.reason || '').includes('rate limit')) {
|
|
@@ -442,12 +408,18 @@ export class Transport {
|
|
|
442
408
|
}
|
|
443
409
|
catch (error) {
|
|
444
410
|
this.log('error', 'Failed to send message', { action, error });
|
|
445
|
-
|
|
411
|
+
reject(error);
|
|
446
412
|
}
|
|
447
413
|
};
|
|
448
414
|
const send = async () => {
|
|
415
|
+
if (options.abortSignal?.aborted) {
|
|
416
|
+
throw new DOMException('Request aborted', 'AbortError');
|
|
417
|
+
}
|
|
449
418
|
// Ensure connection is established before making calls
|
|
450
419
|
await __classPrivateFieldGet(this, _Transport_instances, "m", _Transport_connect).call(this);
|
|
420
|
+
if (options.abortSignal?.aborted) {
|
|
421
|
+
throw new DOMException('Request aborted', 'AbortError');
|
|
422
|
+
}
|
|
451
423
|
const promise = new Promise(handler).finally(removeListeners);
|
|
452
424
|
if (!options.timeout) {
|
|
453
425
|
return promise;
|
|
@@ -459,7 +431,8 @@ export class Transport {
|
|
|
459
431
|
throw error;
|
|
460
432
|
}).finally(removeListeners);
|
|
461
433
|
};
|
|
462
|
-
|
|
434
|
+
const retries = options.retries === false ? 0 : options.retries ?? 0;
|
|
435
|
+
return this.sendWithRetry(async () => await send(), retries);
|
|
463
436
|
}
|
|
464
437
|
send(action, data = {}) {
|
|
465
438
|
if (this.terminalError) {
|
|
@@ -473,12 +446,10 @@ export class Transport {
|
|
|
473
446
|
}
|
|
474
447
|
try {
|
|
475
448
|
this.rws.send(this.pack({ action, data }));
|
|
476
|
-
this.connectionStats.totalMessages++;
|
|
477
449
|
this.log('debug', 'One-way message sent', { action, data });
|
|
478
450
|
return true;
|
|
479
451
|
}
|
|
480
452
|
catch (error) {
|
|
481
|
-
this.connectionStats.totalErrors++;
|
|
482
453
|
this.log('error', 'Failed to send one-way message', { action, error });
|
|
483
454
|
return false;
|
|
484
455
|
}
|
|
@@ -486,7 +457,7 @@ export class Transport {
|
|
|
486
457
|
pack(data) {
|
|
487
458
|
return new Blob([encode(data)]);
|
|
488
459
|
}
|
|
489
|
-
sendWithRetry(sender, retries
|
|
460
|
+
sendWithRetry(sender, retries) {
|
|
490
461
|
/**
|
|
491
462
|
* Enhanced retry with exponential backoff and intelligent error handling
|
|
492
463
|
*/
|
|
@@ -523,7 +494,6 @@ export class Transport {
|
|
|
523
494
|
this.log('debug', 'Retrying send operation', {
|
|
524
495
|
attempt,
|
|
525
496
|
error: e instanceof Error ? e.message : String(e),
|
|
526
|
-
nextDelay: this.getBackoffDelay(attempt - 1),
|
|
527
497
|
});
|
|
528
498
|
throw e;
|
|
529
499
|
}
|
|
@@ -571,7 +541,6 @@ export class Transport {
|
|
|
571
541
|
}
|
|
572
542
|
disconnect() {
|
|
573
543
|
if (this.closed) {
|
|
574
|
-
console.trace('Transport is already closed, cannot disconnect again');
|
|
575
544
|
return;
|
|
576
545
|
}
|
|
577
546
|
this.close();
|
|
@@ -593,16 +562,6 @@ export class Transport {
|
|
|
593
562
|
this.log('info', 'Closing transport connection', { code, reason });
|
|
594
563
|
// Clear any pending connection promise
|
|
595
564
|
this.connectPromise = null;
|
|
596
|
-
// Reject all queued messages
|
|
597
|
-
const queuedCount = this.messageQueue.length;
|
|
598
|
-
const closeError = this.terminalError || new Error('Connection closed while message was queued');
|
|
599
|
-
this.messageQueue.forEach((msg) => {
|
|
600
|
-
msg.reject(closeError);
|
|
601
|
-
});
|
|
602
|
-
this.messageQueue = [];
|
|
603
|
-
if (queuedCount > 0) {
|
|
604
|
-
this.log('debug', `Rejected ${queuedCount} queued messages due to connection close`);
|
|
605
|
-
}
|
|
606
565
|
// Dispose all event disposables
|
|
607
566
|
this.disposables.dispose();
|
|
608
567
|
// Close WebSocket connection
|
|
@@ -618,7 +577,6 @@ export class Transport {
|
|
|
618
577
|
this.eventEmitter.emit('transport.closed', {
|
|
619
578
|
code,
|
|
620
579
|
reason,
|
|
621
|
-
metrics: this.getConnectionMetrics(),
|
|
622
580
|
timestamp: Date.now(),
|
|
623
581
|
});
|
|
624
582
|
this.eventEmitter.removeListener('*'); // Remove all event listeners
|
|
@@ -627,261 +585,23 @@ export class Transport {
|
|
|
627
585
|
}
|
|
628
586
|
onDidConnect(listener) {
|
|
629
587
|
this.rws.addEventListener('open', listener);
|
|
630
|
-
|
|
588
|
+
const disposable = {
|
|
631
589
|
dispose: () => {
|
|
632
590
|
this.rws.removeEventListener('open', listener);
|
|
633
591
|
},
|
|
634
|
-
}
|
|
592
|
+
};
|
|
593
|
+
this.disposables.add(`connect_${nanoid()}`, disposable);
|
|
594
|
+
return disposable;
|
|
635
595
|
}
|
|
636
596
|
onDidClose(listener) {
|
|
637
597
|
this.rws.addEventListener('close', listener);
|
|
638
|
-
|
|
598
|
+
const disposable = {
|
|
639
599
|
dispose: () => {
|
|
640
600
|
this.rws.removeEventListener('close', listener);
|
|
641
601
|
},
|
|
642
|
-
});
|
|
643
|
-
}
|
|
644
|
-
/**
|
|
645
|
-
* Setup connection health monitoring
|
|
646
|
-
*/
|
|
647
|
-
setupConnectionHealthMonitoring() {
|
|
648
|
-
this.rws.addEventListener('open', () => {
|
|
649
|
-
this.connectionStats.connectTime = Date.now();
|
|
650
|
-
this.connectionStats.reconnectCount++;
|
|
651
|
-
this.log('info', 'Connection established', {
|
|
652
|
-
reconnectCount: this.connectionStats.reconnectCount,
|
|
653
|
-
timeSinceStart: Date.now() - this.connectionStats.connectionStartTime,
|
|
654
|
-
});
|
|
655
|
-
this.processMessageQueue();
|
|
656
|
-
});
|
|
657
|
-
this.rws.addEventListener('close', (event) => {
|
|
658
|
-
this.log('warn', 'Connection closed', {
|
|
659
|
-
code: event.code,
|
|
660
|
-
reason: event.reason,
|
|
661
|
-
wasClean: event.wasClean,
|
|
662
|
-
});
|
|
663
|
-
this.handleConnectionClose(event.code);
|
|
664
|
-
});
|
|
665
|
-
this.rws.addEventListener('error', (event) => {
|
|
666
|
-
this.connectionStats.totalErrors++;
|
|
667
|
-
const detail = describeWebSocketEvent(event);
|
|
668
|
-
this.log('error', 'Connection error', {
|
|
669
|
-
error: detail,
|
|
670
|
-
rawError: event,
|
|
671
|
-
totalErrors: this.connectionStats.totalErrors,
|
|
672
|
-
});
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
/**
|
|
676
|
-
* Handle different WebSocket close codes appropriately
|
|
677
|
-
*/
|
|
678
|
-
handleConnectionClose(code) {
|
|
679
|
-
switch (code) {
|
|
680
|
-
case 1000: // Normal closure
|
|
681
|
-
this.log('info', 'Normal connection closure');
|
|
682
|
-
return 'stop';
|
|
683
|
-
case 1001: // Going away
|
|
684
|
-
this.log('info', 'Connection going away, will reconnect');
|
|
685
|
-
return 'reconnect';
|
|
686
|
-
case 1006: // Abnormal closure
|
|
687
|
-
this.log('warn', 'Abnormal connection closure, will retry');
|
|
688
|
-
return 'retry';
|
|
689
|
-
case 1008: // Policy violation (rate limit)
|
|
690
|
-
this.log('error', 'Connection closed due to policy violation');
|
|
691
|
-
this.clearOldQueuedMessages();
|
|
692
|
-
return 'stop';
|
|
693
|
-
case 4000:
|
|
694
|
-
this.log('info', 'Connection closed due to terminal notebook error');
|
|
695
|
-
return 'stop';
|
|
696
|
-
default:
|
|
697
|
-
this.log('warn', `Unknown close code: ${code}, will reconnect`);
|
|
698
|
-
return 'reconnect';
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
/**
|
|
702
|
-
* Process queued messages when connection is restored
|
|
703
|
-
*/
|
|
704
|
-
processMessageQueue() {
|
|
705
|
-
if (this.messageQueue.length === 0) {
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
this.log('debug', `Processing ${this.messageQueue.length} queued messages`);
|
|
709
|
-
const queue = [...this.messageQueue];
|
|
710
|
-
this.messageQueue = [];
|
|
711
|
-
for (const queuedMessage of queue) {
|
|
712
|
-
// Check if message hasn't timed out
|
|
713
|
-
if (Date.now() - queuedMessage.timestamp > this.QUEUE_TIMEOUT) {
|
|
714
|
-
queuedMessage.reject(new Error('Queued message timed out'));
|
|
715
|
-
continue;
|
|
716
|
-
}
|
|
717
|
-
// Retry the message
|
|
718
|
-
this.call(queuedMessage.action, queuedMessage.data, queuedMessage.options)
|
|
719
|
-
.then(queuedMessage.resolve)
|
|
720
|
-
.catch(queuedMessage.reject);
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
/**
|
|
724
|
-
* Clear old queued messages to prevent memory leaks
|
|
725
|
-
*/
|
|
726
|
-
clearOldQueuedMessages() {
|
|
727
|
-
const now = Date.now();
|
|
728
|
-
const originalLength = this.messageQueue.length;
|
|
729
|
-
this.messageQueue = this.messageQueue.filter((msg) => {
|
|
730
|
-
const isExpired = now - msg.timestamp > this.QUEUE_TIMEOUT;
|
|
731
|
-
if (isExpired) {
|
|
732
|
-
msg.reject(new Error('Queued message expired'));
|
|
733
|
-
}
|
|
734
|
-
return !isExpired;
|
|
735
|
-
});
|
|
736
|
-
if (originalLength !== this.messageQueue.length) {
|
|
737
|
-
this.log('debug', `Cleared ${originalLength - this.messageQueue.length} expired messages`);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
/**
|
|
741
|
-
* Calculate exponential backoff delay
|
|
742
|
-
*/
|
|
743
|
-
getBackoffDelay(retryCount) {
|
|
744
|
-
const baseDelay = 1000; // 1 second
|
|
745
|
-
const maxDelay = 30000; // 30 seconds
|
|
746
|
-
const delay = Math.min(baseDelay * Math.pow(2, retryCount), maxDelay);
|
|
747
|
-
// Add jitter to prevent thundering herd
|
|
748
|
-
return delay + Math.random() * 1000;
|
|
749
|
-
}
|
|
750
|
-
/**
|
|
751
|
-
* Get comprehensive connection metrics
|
|
752
|
-
*/
|
|
753
|
-
getConnectionMetrics() {
|
|
754
|
-
const now = Date.now();
|
|
755
|
-
const connectionDuration = this.connectionStats.connectTime > 0 ? now - this.connectionStats.connectTime : 0;
|
|
756
|
-
return {
|
|
757
|
-
status: this.status,
|
|
758
|
-
clientId: this.clientId,
|
|
759
|
-
isConnected: this.isConnected,
|
|
760
|
-
isConnecting: this.isConnecting,
|
|
761
|
-
connectionStats: {
|
|
762
|
-
...this.connectionStats,
|
|
763
|
-
connectionDuration,
|
|
764
|
-
uptime: now - this.connectionStats.connectionStartTime,
|
|
765
|
-
messagesPerSecond: connectionDuration > 0 ? (this.connectionStats.totalMessages / (connectionDuration / 1000)).toFixed(2) : '0',
|
|
766
|
-
errorRate: this.connectionStats.totalMessages > 0
|
|
767
|
-
? ((this.connectionStats.totalErrors / this.connectionStats.totalMessages) * 100).toFixed(2) + '%'
|
|
768
|
-
: '0%',
|
|
769
|
-
timeSinceLastPing: this.connectionStats.lastPingTime > 0 ? now - this.connectionStats.lastPingTime : 0,
|
|
770
|
-
timeSinceLastPong: this.connectionStats.lastPongTime > 0 ? now - this.connectionStats.lastPongTime : 0,
|
|
771
|
-
},
|
|
772
|
-
messageQueue: {
|
|
773
|
-
length: this.messageQueue.length,
|
|
774
|
-
maxSize: this.MAX_QUEUE_SIZE,
|
|
775
|
-
oldestMessageAge: this.messageQueue.length > 0 ? now - Math.min(...this.messageQueue.map((m) => m.timestamp)) : 0,
|
|
776
|
-
},
|
|
777
|
-
config: {
|
|
778
|
-
pingInterval: this.PING_INTERVAL,
|
|
779
|
-
queueTimeout: this.QUEUE_TIMEOUT,
|
|
780
|
-
url: this.url.toString(),
|
|
781
|
-
},
|
|
782
602
|
};
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
* Get connection health status
|
|
786
|
-
*/
|
|
787
|
-
getHealthStatus() {
|
|
788
|
-
const metrics = this.getConnectionMetrics();
|
|
789
|
-
const stats = metrics.connectionStats;
|
|
790
|
-
// Unhealthy conditions
|
|
791
|
-
if (!this.isConnected || stats.timeSinceLastPong > this.PING_INTERVAL * 2 || Number.parseFloat(stats.errorRate) > 50) {
|
|
792
|
-
return 'unhealthy';
|
|
793
|
-
}
|
|
794
|
-
// Degraded conditions
|
|
795
|
-
if (stats.avgResponseTime > 5000 ||
|
|
796
|
-
Number.parseFloat(stats.errorRate) > 10 ||
|
|
797
|
-
stats.timeSinceLastPong > this.PING_INTERVAL * 1.5) {
|
|
798
|
-
return 'degraded';
|
|
799
|
-
}
|
|
800
|
-
return 'healthy';
|
|
801
|
-
}
|
|
802
|
-
/**
|
|
803
|
-
* Reset connection statistics
|
|
804
|
-
*/
|
|
805
|
-
resetStats() {
|
|
806
|
-
this.connectionStats = {
|
|
807
|
-
connectTime: Date.now(),
|
|
808
|
-
reconnectCount: 0,
|
|
809
|
-
lastPingTime: 0,
|
|
810
|
-
lastPongTime: 0,
|
|
811
|
-
avgResponseTime: 0,
|
|
812
|
-
totalMessages: 0,
|
|
813
|
-
totalErrors: 0,
|
|
814
|
-
connectionStartTime: Date.now(),
|
|
815
|
-
};
|
|
816
|
-
this.log('debug', 'Connection statistics reset');
|
|
817
|
-
}
|
|
818
|
-
/**
|
|
819
|
-
* Run connection diagnostics
|
|
820
|
-
*/
|
|
821
|
-
async runDiagnostics() {
|
|
822
|
-
const metrics = this.getConnectionMetrics();
|
|
823
|
-
const status = this.getHealthStatus();
|
|
824
|
-
const issues = [];
|
|
825
|
-
const recommendations = [];
|
|
826
|
-
// Check for issues
|
|
827
|
-
if (!this.isConnected) {
|
|
828
|
-
issues.push('Connection is not established');
|
|
829
|
-
recommendations.push('Check network connectivity and server availability');
|
|
830
|
-
}
|
|
831
|
-
if (metrics.connectionStats.timeSinceLastPong > this.PING_INTERVAL * 2) {
|
|
832
|
-
issues.push('No pong received recently - connection may be stale');
|
|
833
|
-
recommendations.push('Consider forcing a reconnection');
|
|
834
|
-
}
|
|
835
|
-
if (Number.parseFloat(metrics.connectionStats.errorRate) > 10) {
|
|
836
|
-
issues.push(`High error rate: ${metrics.connectionStats.errorRate}`);
|
|
837
|
-
recommendations.push('Check server logs and network stability');
|
|
838
|
-
}
|
|
839
|
-
if (metrics.messageQueue.length > this.MAX_QUEUE_SIZE * 0.8) {
|
|
840
|
-
issues.push('Message queue is nearly full');
|
|
841
|
-
recommendations.push('Check connection stability and consider reducing message frequency');
|
|
842
|
-
}
|
|
843
|
-
if (metrics.connectionStats.avgResponseTime > 5000) {
|
|
844
|
-
issues.push('High average response time');
|
|
845
|
-
recommendations.push('Check network latency and server performance');
|
|
846
|
-
}
|
|
847
|
-
this.log('info', 'Connection diagnostics completed', {
|
|
848
|
-
status,
|
|
849
|
-
issueCount: issues.length,
|
|
850
|
-
recommendationCount: recommendations.length,
|
|
851
|
-
});
|
|
852
|
-
return {
|
|
853
|
-
status,
|
|
854
|
-
metrics,
|
|
855
|
-
issues,
|
|
856
|
-
recommendations,
|
|
857
|
-
};
|
|
858
|
-
}
|
|
859
|
-
/**
|
|
860
|
-
* Periodic maintenance - clean up old data, run health checks
|
|
861
|
-
*/
|
|
862
|
-
startPeriodicMaintenance() {
|
|
863
|
-
this.disposables.add('maintenance', () => {
|
|
864
|
-
// Run maintenance every 5 minutes
|
|
865
|
-
const maintenanceInterval = setInterval(() => {
|
|
866
|
-
this.clearOldQueuedMessages();
|
|
867
|
-
// Log health status if debug is enabled
|
|
868
|
-
if (this.options.debug) {
|
|
869
|
-
const health = this.getHealthStatus();
|
|
870
|
-
const metrics = this.getConnectionMetrics();
|
|
871
|
-
this.log('debug', 'Periodic health check', {
|
|
872
|
-
health,
|
|
873
|
-
messageCount: metrics.connectionStats.totalMessages,
|
|
874
|
-
errorCount: metrics.connectionStats.totalErrors,
|
|
875
|
-
queueLength: metrics.messageQueue.length,
|
|
876
|
-
});
|
|
877
|
-
}
|
|
878
|
-
}, 5 * 60 * 1000); // 5 minutes
|
|
879
|
-
return {
|
|
880
|
-
dispose: () => {
|
|
881
|
-
clearInterval(maintenanceInterval);
|
|
882
|
-
},
|
|
883
|
-
};
|
|
884
|
-
});
|
|
603
|
+
this.disposables.add(`close_${nanoid()}`, disposable);
|
|
604
|
+
return disposable;
|
|
885
605
|
}
|
|
886
606
|
}
|
|
887
607
|
_Transport_instances = new WeakSet(), _Transport_connect = function _Transport_connect() {
|
|
@@ -927,7 +647,7 @@ _Transport_instances = new WeakSet(), _Transport_connect = function _Transport_c
|
|
|
927
647
|
};
|
|
928
648
|
const closeHandler = (event) => {
|
|
929
649
|
const detail = describeWebSocketEvent(event);
|
|
930
|
-
if (
|
|
650
|
+
if (event.code === 1008 || event.code === 4000) {
|
|
931
651
|
cleanup();
|
|
932
652
|
this.connectPromise = null;
|
|
933
653
|
reject(new ConnectionFailedError(`WebSocket connection failed: ${detail}`, event));
|
|
@@ -961,23 +681,14 @@ _Transport_instances = new WeakSet(), _Transport_connect = function _Transport_c
|
|
|
961
681
|
this.disposables.add('pingInterval', () => {
|
|
962
682
|
const interval = setInterval(async () => {
|
|
963
683
|
try {
|
|
964
|
-
this.connectionStats.lastPingTime = Date.now();
|
|
965
684
|
this.log('debug', 'Sending periodic ping');
|
|
966
|
-
const startTime = Date.now();
|
|
967
685
|
await this.invoke('ping');
|
|
968
|
-
this.
|
|
969
|
-
const pingTime = this.connectionStats.lastPongTime - startTime;
|
|
970
|
-
this.log('debug', `Ping successful`, { pingTime });
|
|
686
|
+
this.log('debug', 'Ping successful');
|
|
971
687
|
}
|
|
972
688
|
catch (error) {
|
|
973
689
|
this.log('error', 'Ping failed', {
|
|
974
690
|
error: error instanceof Error ? error.message : String(error),
|
|
975
691
|
});
|
|
976
|
-
// If ping fails consistently, the connection might be dead
|
|
977
|
-
if (Date.now() - this.connectionStats.lastPongTime > this.PING_INTERVAL * 3) {
|
|
978
|
-
this.log('warn', 'Connection appears dead, forcing reconnection');
|
|
979
|
-
this.rws.reconnect();
|
|
980
|
-
}
|
|
981
692
|
}
|
|
982
693
|
}, this.PING_INTERVAL);
|
|
983
694
|
return {
|
|
@@ -988,4 +699,3 @@ _Transport_instances = new WeakSet(), _Transport_connect = function _Transport_c
|
|
|
988
699
|
};
|
|
989
700
|
});
|
|
990
701
|
};
|
|
991
|
-
//# sourceMappingURL=index.js.map
|
package/dist/terminal.d.ts
CHANGED
|
@@ -94,9 +94,9 @@ export default class Terminal {
|
|
|
94
94
|
list(): Promise<Task[]>;
|
|
95
95
|
start(): Promise<void>;
|
|
96
96
|
create(input: TerminalCreateInput): Promise<Task>;
|
|
97
|
-
onStarted(handler: (t: Task) => void):
|
|
97
|
+
onStarted(handler: (t: Task) => void): Disposable;
|
|
98
98
|
resize(id: string, size: [number, number]): Promise<boolean>;
|
|
99
|
-
onOutput(id: string, handler: (data: TerminalEvents['terminal.output']) => void):
|
|
99
|
+
onOutput(id: string, handler: (data: TerminalEvents['terminal.output']) => void): Disposable;
|
|
100
100
|
input(id: string, input: string): Promise<boolean>;
|
|
101
101
|
listen<T extends keyof TerminalEvents>(event: T, handler: (data: TerminalEvents[T]) => void): Disposable;
|
|
102
102
|
attach(id: string, opts?: {
|
|
@@ -107,4 +107,3 @@ export default class Terminal {
|
|
|
107
107
|
private processHandle;
|
|
108
108
|
}
|
|
109
109
|
export {};
|
|
110
|
-
//# sourceMappingURL=terminal.d.ts.map
|