@phpsandbox/sdk 0.0.51 → 0.0.53

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.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -6
  3. package/dist/auth.d.ts +0 -1
  4. package/dist/auth.js +0 -1
  5. package/dist/beacon/index.d.ts +4 -5
  6. package/dist/beacon/index.js +11 -12
  7. package/dist/beacon/navigator.d.ts +1 -2
  8. package/dist/beacon/navigator.js +0 -1
  9. package/dist/beacon/types.d.ts +12 -13
  10. package/dist/beacon/types.js +0 -1
  11. package/dist/browser/phpsandbox-sdk.esm.js +313 -481
  12. package/dist/browser/phpsandbox-sdk.esm.min.js +2 -3
  13. package/dist/browser/phpsandbox-sdk.iife.js +308 -481
  14. package/dist/browser/phpsandbox-sdk.iife.min.js +2 -3
  15. package/dist/composer.d.ts +2 -3
  16. package/dist/composer.js +0 -1
  17. package/dist/config.d.ts +0 -1
  18. package/dist/config.js +0 -1
  19. package/dist/container.d.ts +0 -1
  20. package/dist/container.js +0 -1
  21. package/dist/errors/index.d.ts +0 -1
  22. package/dist/errors/index.js +0 -1
  23. package/dist/events/index.d.ts +15 -15
  24. package/dist/events/index.js +16 -9
  25. package/dist/filesystem.d.ts +6 -7
  26. package/dist/filesystem.js +36 -14
  27. package/dist/git.d.ts +20 -9
  28. package/dist/git.js +31 -3
  29. package/dist/index.d.ts +3 -2
  30. package/dist/index.js +38 -25
  31. package/dist/laravel.d.ts +1 -2
  32. package/dist/laravel.js +1 -2
  33. package/dist/log.d.ts +3 -4
  34. package/dist/log.js +1 -2
  35. package/dist/lsp.d.ts +10 -11
  36. package/dist/lsp.js +9 -10
  37. package/dist/repl.d.ts +3 -4
  38. package/dist/repl.js +2 -3
  39. package/dist/services.d.ts +0 -1
  40. package/dist/services.js +0 -1
  41. package/dist/shell.d.ts +1 -2
  42. package/dist/shell.js +1 -2
  43. package/dist/socket/index.d.ts +23 -129
  44. package/dist/socket/index.js +60 -350
  45. package/dist/terminal.d.ts +2 -3
  46. package/dist/terminal.js +45 -26
  47. package/dist/types.d.ts +0 -1
  48. package/dist/types.js +0 -1
  49. package/dist/utils/disposable.d.ts +0 -1
  50. package/dist/utils/disposable.js +0 -1
  51. package/dist/utils/promise.d.ts +0 -1
  52. package/dist/utils/promise.js +0 -1
  53. package/dist/utils/retry.d.ts +0 -1
  54. package/dist/utils/retry.js +0 -1
  55. package/dist/utils/websocket.d.ts +0 -1
  56. package/dist/utils/websocket.js +0 -1
  57. package/docs/api-summary.md +136 -0
  58. package/docs/cdn-usage.md +80 -0
  59. package/docs/getting-started.md +152 -0
  60. package/examples/README.md +206 -0
  61. package/examples/basic.ts +24 -0
  62. package/examples/get.ts +21 -0
  63. package/examples/git-merge-smoke.ts +399 -0
  64. package/examples/laravel-load-test.ts +2829 -0
  65. package/examples/notebook-readonly-smoke.ts +823 -0
  66. package/package.json +13 -7
  67. package/dist/auth.d.ts.map +0 -1
  68. package/dist/auth.js.map +0 -1
  69. package/dist/beacon/index.d.ts.map +0 -1
  70. package/dist/beacon/index.js.map +0 -1
  71. package/dist/beacon/navigator.d.ts.map +0 -1
  72. package/dist/beacon/navigator.js.map +0 -1
  73. package/dist/beacon/types.d.ts.map +0 -1
  74. package/dist/beacon/types.js.map +0 -1
  75. package/dist/browser/phpsandbox-sdk.esm.js.map +0 -7
  76. package/dist/browser/phpsandbox-sdk.esm.min.js.map +0 -7
  77. package/dist/browser/phpsandbox-sdk.iife.js.map +0 -7
  78. package/dist/browser/phpsandbox-sdk.iife.min.js.map +0 -7
  79. package/dist/composer.d.ts.map +0 -1
  80. package/dist/composer.js.map +0 -1
  81. package/dist/config.d.ts.map +0 -1
  82. package/dist/config.js.map +0 -1
  83. package/dist/container.d.ts.map +0 -1
  84. package/dist/container.js.map +0 -1
  85. package/dist/errors/index.d.ts.map +0 -1
  86. package/dist/errors/index.js.map +0 -1
  87. package/dist/events/index.d.ts.map +0 -1
  88. package/dist/events/index.js.map +0 -1
  89. package/dist/filesystem.d.ts.map +0 -1
  90. package/dist/filesystem.js.map +0 -1
  91. package/dist/git.d.ts.map +0 -1
  92. package/dist/git.js.map +0 -1
  93. package/dist/index.d.ts.map +0 -1
  94. package/dist/index.js.map +0 -1
  95. package/dist/laravel.d.ts.map +0 -1
  96. package/dist/laravel.js.map +0 -1
  97. package/dist/log.d.ts.map +0 -1
  98. package/dist/log.js.map +0 -1
  99. package/dist/lsp.d.ts.map +0 -1
  100. package/dist/lsp.js.map +0 -1
  101. package/dist/repl.d.ts.map +0 -1
  102. package/dist/repl.js.map +0 -1
  103. package/dist/services.d.ts.map +0 -1
  104. package/dist/services.js.map +0 -1
  105. package/dist/shell.d.ts.map +0 -1
  106. package/dist/shell.js.map +0 -1
  107. package/dist/socket/index.d.ts.map +0 -1
  108. package/dist/socket/index.js.map +0 -1
  109. package/dist/terminal.d.ts.map +0 -1
  110. package/dist/terminal.js.map +0 -1
  111. package/dist/types.d.ts.map +0 -1
  112. package/dist/types.js.map +0 -1
  113. package/dist/utils/disposable.d.ts.map +0 -1
  114. package/dist/utils/disposable.js.map +0 -1
  115. package/dist/utils/promise.d.ts.map +0 -1
  116. package/dist/utils/promise.js.map +0 -1
  117. package/dist/utils/retry.d.ts.map +0 -1
  118. package/dist/utils/retry.js.map +0 -1
  119. package/dist/utils/websocket.d.ts.map +0 -1
  120. package/dist/utils/websocket.js.map +0 -1
@@ -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 ReconnectingWebSocket from 'reconnecting-websocket';
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
- if (!(ev.data instanceof Blob)) {
173
- const error = new Error('Unexpected message type: ' + typeof ev.data);
174
- this.connectionStats.totalErrors++;
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 error = new NotebookUnavailableError(data?.message || 'Notebook is no longer available', data || {});
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, data.id);
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 = async (resolve, reject) => {
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
- options.abortSignal.addEventListener('abort', () => {
375
+ abortHandler = () => {
386
376
  reject(abortError);
387
- });
377
+ };
378
+ options.abortSignal.addEventListener('abort', abortHandler, { once: true });
388
379
  }
389
- // If not connected, queue the message
390
- if (!this.isConnected && !this.isClosed) {
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
- reject(new SDKErrorEvent(e.code, e.message, e));
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
- throw error;
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
- return this.sendWithRetry(async () => await send(), options.retries || 10);
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 = 10) {
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
- this.disposables.add('connect', {
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
- this.disposables.add('close', {
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 ('code' in event && (event.code === 1008 || event.code === 4000)) {
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.connectionStats.lastPongTime = Date.now();
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
@@ -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): 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): 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