@phpsandbox/sdk 0.0.2

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 (90) hide show
  1. package/README.md +718 -0
  2. package/dist/auth.d.ts +14 -0
  3. package/dist/auth.d.ts.map +1 -0
  4. package/dist/auth.js +12 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/beacon/index.d.ts +169 -0
  7. package/dist/beacon/index.d.ts.map +1 -0
  8. package/dist/beacon/index.js +537 -0
  9. package/dist/beacon/index.js.map +1 -0
  10. package/dist/beacon/navigator.d.ts +141 -0
  11. package/dist/beacon/navigator.d.ts.map +1 -0
  12. package/dist/beacon/navigator.js +246 -0
  13. package/dist/beacon/navigator.js.map +1 -0
  14. package/dist/beacon/types.d.ts +230 -0
  15. package/dist/beacon/types.d.ts.map +1 -0
  16. package/dist/beacon/types.js +2 -0
  17. package/dist/beacon/types.js.map +1 -0
  18. package/dist/browser/phpsandbox-sdk.esm.js +4755 -0
  19. package/dist/browser/phpsandbox-sdk.esm.js.map +7 -0
  20. package/dist/browser/phpsandbox-sdk.esm.min.js +27 -0
  21. package/dist/browser/phpsandbox-sdk.esm.min.js.map +7 -0
  22. package/dist/browser/phpsandbox-sdk.iife.js +4766 -0
  23. package/dist/browser/phpsandbox-sdk.iife.js.map +7 -0
  24. package/dist/browser/phpsandbox-sdk.iife.min.js +27 -0
  25. package/dist/browser/phpsandbox-sdk.iife.min.js.map +7 -0
  26. package/dist/composer.d.ts +45 -0
  27. package/dist/composer.d.ts.map +1 -0
  28. package/dist/composer.js +30 -0
  29. package/dist/composer.js.map +1 -0
  30. package/dist/container.d.ts +66 -0
  31. package/dist/container.d.ts.map +1 -0
  32. package/dist/container.js +56 -0
  33. package/dist/container.js.map +1 -0
  34. package/dist/events/index.d.ts +23 -0
  35. package/dist/events/index.d.ts.map +1 -0
  36. package/dist/events/index.js +46 -0
  37. package/dist/events/index.js.map +1 -0
  38. package/dist/filesystem.d.ts +483 -0
  39. package/dist/filesystem.d.ts.map +1 -0
  40. package/dist/filesystem.js +244 -0
  41. package/dist/filesystem.js.map +1 -0
  42. package/dist/git.d.ts +42 -0
  43. package/dist/git.d.ts.map +1 -0
  44. package/dist/git.js +18 -0
  45. package/dist/git.js.map +1 -0
  46. package/dist/index.d.ts +167 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +265 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/laravel.d.ts +23 -0
  51. package/dist/laravel.d.ts.map +1 -0
  52. package/dist/laravel.js +12 -0
  53. package/dist/laravel.js.map +1 -0
  54. package/dist/log.d.ts +13 -0
  55. package/dist/log.d.ts.map +1 -0
  56. package/dist/log.js +12 -0
  57. package/dist/log.js.map +1 -0
  58. package/dist/lsp.d.ts +57 -0
  59. package/dist/lsp.d.ts.map +1 -0
  60. package/dist/lsp.js +69 -0
  61. package/dist/lsp.js.map +1 -0
  62. package/dist/repl.d.ts +41 -0
  63. package/dist/repl.d.ts.map +1 -0
  64. package/dist/repl.js +27 -0
  65. package/dist/repl.js.map +1 -0
  66. package/dist/shell.d.ts +29 -0
  67. package/dist/shell.d.ts.map +1 -0
  68. package/dist/shell.js +29 -0
  69. package/dist/shell.js.map +1 -0
  70. package/dist/socket/index.d.ts +229 -0
  71. package/dist/socket/index.d.ts.map +1 -0
  72. package/dist/socket/index.js +825 -0
  73. package/dist/socket/index.js.map +1 -0
  74. package/dist/terminal.d.ts +97 -0
  75. package/dist/terminal.d.ts.map +1 -0
  76. package/dist/terminal.js +87 -0
  77. package/dist/terminal.js.map +1 -0
  78. package/dist/types.d.ts +16 -0
  79. package/dist/types.d.ts.map +1 -0
  80. package/dist/types.js +16 -0
  81. package/dist/types.js.map +1 -0
  82. package/dist/utils/disposable.d.ts +7 -0
  83. package/dist/utils/disposable.d.ts.map +1 -0
  84. package/dist/utils/disposable.js +20 -0
  85. package/dist/utils/disposable.js.map +1 -0
  86. package/dist/utils/promise.d.ts +13 -0
  87. package/dist/utils/promise.d.ts.map +1 -0
  88. package/dist/utils/promise.js +21 -0
  89. package/dist/utils/promise.js.map +1 -0
  90. package/package.json +67 -0
@@ -0,0 +1,825 @@
1
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
2
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
3
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
4
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5
+ };
6
+ var _Transport_instances, _Transport_connect, _Transport_startPeriodicPing;
7
+ import { nanoid } from 'nanoid';
8
+ import { encode, decode } from '@msgpack/msgpack';
9
+ import { ErrorEvent, RateLimitError } from '../types.js';
10
+ import retry from 'async-retry';
11
+ import ReconnectingWebSocket from 'reconnecting-websocket';
12
+ import { timeout } from '../utils/promise.js';
13
+ import WebSocket from 'isomorphic-ws';
14
+ import { NamedDisposable } from '../utils/disposable.js';
15
+ export var SocketEvent;
16
+ (function (SocketEvent) {
17
+ SocketEvent["BootError"] = "Events.BootError";
18
+ SocketEvent["Response"] = "response";
19
+ SocketEvent["Error"] = "error";
20
+ SocketEvent["ClientId"] = "App.Actions.GetClientId";
21
+ })(SocketEvent || (SocketEvent = {}));
22
+ // Add specific error types for better error handling
23
+ export class ConnectionTimeoutError extends Error {
24
+ constructor(message = 'WebSocket connection timeout') {
25
+ super(message);
26
+ this.name = 'ConnectionTimeoutError';
27
+ }
28
+ }
29
+ export class ConnectionFailedError extends Error {
30
+ constructor(message, originalError) {
31
+ super(message);
32
+ this.originalError = originalError;
33
+ this.name = 'ConnectionFailedError';
34
+ }
35
+ }
36
+ export class InvalidMessageError extends Error {
37
+ constructor(message, data) {
38
+ super(message);
39
+ this.data = data;
40
+ this.name = 'InvalidMessageError';
41
+ }
42
+ }
43
+ // Add configuration validation
44
+ export class InvalidConfigurationError extends Error {
45
+ constructor(message) {
46
+ super(message);
47
+ this.name = 'InvalidConfigurationError';
48
+ }
49
+ }
50
+ export class Transport {
51
+ constructor(url, eventEmitter, options = {}) {
52
+ _Transport_instances.add(this);
53
+ this.eventEmitter = eventEmitter;
54
+ this.options = options;
55
+ this.clientId = '';
56
+ this.closed = false;
57
+ this.disposables = new NamedDisposable();
58
+ this.connectPromise = null;
59
+ // Connection health monitoring
60
+ this.connectionStats = {
61
+ connectTime: 0,
62
+ reconnectCount: 0,
63
+ lastPingTime: 0,
64
+ lastPongTime: 0,
65
+ avgResponseTime: 0,
66
+ totalMessages: 0,
67
+ totalErrors: 0,
68
+ connectionStartTime: 0,
69
+ };
70
+ // Message queue for disconnection handling
71
+ this.messageQueue = [];
72
+ this.MAX_QUEUE_SIZE = 100;
73
+ this.QUEUE_TIMEOUT = 30000; // 30 seconds
74
+ // Rate limiting
75
+ this.rateLimiter = {
76
+ requests: [],
77
+ maxRequests: 50,
78
+ windowMs: 1000, // 1 second
79
+ };
80
+ // Validate configuration
81
+ this.validateConfiguration(options);
82
+ this.url = new URL(url);
83
+ this.url.searchParams.set('sdk_version', '0.0.1');
84
+ // Use configurable ping interval
85
+ this.PING_INTERVAL = options.pingInterval ?? 30000;
86
+ // Initialize connection stats
87
+ this.connectionStats.connectionStartTime = Date.now();
88
+ // Always start closed by default (lazy initialization)
89
+ const startClosed = options.startClosed !== false;
90
+ // @ts-expect-error
91
+ this.rws = new ReconnectingWebSocket(this.url.toString(), [], {
92
+ WebSocket: globalThis.WebSocket ?? WebSocket,
93
+ connectionTimeout: options.connectionTimeout ?? 1000,
94
+ maxReconnectionDelay: 2000,
95
+ minReconnectionDelay: 200,
96
+ maxEnqueuedMessages: 0,
97
+ maxRetries: options.maxRetries ?? 50,
98
+ startClosed,
99
+ });
100
+ this.log('debug', 'Transport initialized', {
101
+ url: this.url.toString(),
102
+ options,
103
+ });
104
+ this.registerWatchers();
105
+ this.setupConnectionHealthMonitoring();
106
+ this.startPeriodicMaintenance();
107
+ }
108
+ /**
109
+ * Validate configuration options
110
+ */
111
+ validateConfiguration(options) {
112
+ if (options.pingInterval !== undefined && (options.pingInterval < 1000 || options.pingInterval > 300000)) {
113
+ throw new InvalidConfigurationError('pingInterval must be between 1000ms and 300000ms');
114
+ }
115
+ if (options.connectionTimeout !== undefined && (options.connectionTimeout < 100 || options.connectionTimeout > 30000)) {
116
+ throw new InvalidConfigurationError('connectionTimeout must be between 100ms and 30000ms');
117
+ }
118
+ if (options.maxRetries !== undefined && (options.maxRetries < 0 || options.maxRetries > 100)) {
119
+ throw new InvalidConfigurationError('maxRetries must be between 0 and 100');
120
+ }
121
+ }
122
+ /**
123
+ * Internal logging utility for debugging
124
+ */
125
+ log(level, message, data) {
126
+ if (this.options.debug) {
127
+ const timestamp = new Date().toISOString();
128
+ const logData = data ? JSON.stringify(data, null, 2) : '';
129
+ console[level](`[Transport ${timestamp}] ${message}${logData ? '\n' + logData : ''}`);
130
+ }
131
+ }
132
+ id() {
133
+ return this.clientId;
134
+ }
135
+ async registerWatchers() {
136
+ const onMessage = (ev) => {
137
+ if (!(ev.data instanceof Blob)) {
138
+ throw new Error('Unexpected message type: ' + typeof ev.data);
139
+ }
140
+ ev.data.arrayBuffer().then((buffer) => {
141
+ this.handleRawMessage(decode(buffer));
142
+ });
143
+ };
144
+ this.rws.addEventListener('message', onMessage);
145
+ this.disposables.add('message', {
146
+ dispose: () => {
147
+ this.rws.removeEventListener('message', onMessage);
148
+ },
149
+ });
150
+ }
151
+ async handleRawMessage(ev) {
152
+ if (typeof ev !== 'object' || ev === null) {
153
+ this.log('debug', 'Received invalid message format', { ev });
154
+ return;
155
+ }
156
+ try {
157
+ const { data, event, as } = ev;
158
+ // Validate message structure
159
+ if (!event || typeof event !== 'string') {
160
+ throw new InvalidMessageError('Message missing event field', ev);
161
+ }
162
+ this.log('debug', 'Processing message', { event, hasData: !!data });
163
+ if (event === SocketEvent.ClientId) {
164
+ this.clientId = data.id;
165
+ this.log('info', 'Client ID received', { clientId: this.clientId });
166
+ return;
167
+ }
168
+ if (event === SocketEvent.BootError) {
169
+ this.log('error', 'Boot error received', { data });
170
+ return;
171
+ }
172
+ if (event === SocketEvent.Response) {
173
+ // {"event":"response","data":{"responseEvent":"ping","data":"pong"}}
174
+ const { responseEvent, data: responseData } = data;
175
+ if (!responseEvent) {
176
+ throw new InvalidMessageError('Response message missing responseEvent', ev);
177
+ }
178
+ this.log('debug', 'Response message received', { responseEvent });
179
+ await this.handleMessage(responseEvent, responseData);
180
+ return;
181
+ }
182
+ if (event === SocketEvent.Error) {
183
+ // {"event":"error","data":{"errorEvent":"pingo_error","data":{"code":404,"message":"Action pingo not found"}}}
184
+ const { errorEvent, data: responseData } = data;
185
+ if (!errorEvent) {
186
+ throw new InvalidMessageError('Error message missing errorEvent', ev);
187
+ }
188
+ this.log('debug', 'Error message received', {
189
+ errorEvent,
190
+ errorData: responseData,
191
+ });
192
+ await this.handleMessage(errorEvent, responseData);
193
+ return;
194
+ }
195
+ await this.handleMessage(event, data, as);
196
+ }
197
+ catch (e) {
198
+ this.connectionStats.totalErrors++;
199
+ if (e instanceof InvalidMessageError) {
200
+ this.log('error', 'Invalid message format', {
201
+ error: e.message,
202
+ data: e.data,
203
+ totalErrors: this.connectionStats.totalErrors,
204
+ });
205
+ }
206
+ else {
207
+ this.log('error', 'Failed to parse message', {
208
+ ev,
209
+ error: e instanceof Error ? e.message : String(e),
210
+ totalErrors: this.connectionStats.totalErrors,
211
+ });
212
+ }
213
+ // Don't throw - we want to continue processing other messages
214
+ // But emit an error event for the application to handle
215
+ this.eventEmitter.emit('transport.error', {
216
+ type: 'message_parse_error',
217
+ error: e,
218
+ rawMessage: ev,
219
+ timestamp: Date.now(),
220
+ });
221
+ }
222
+ }
223
+ async handleMessage(event, data, as) {
224
+ if (event === SocketEvent.ClientId) {
225
+ this.clientId = data.id;
226
+ this.eventEmitter.emit(event, data.id);
227
+ return;
228
+ }
229
+ event && this.eventEmitter.emit(as || event, data);
230
+ }
231
+ listen(event, listener, _context) {
232
+ return this.eventEmitter.listen(event, listener);
233
+ }
234
+ removeListener(event, listener) {
235
+ this.eventEmitter.removeListener(event, listener);
236
+ }
237
+ listenOnce(event, listener, context) {
238
+ this.eventEmitter.once(event, listener, context);
239
+ }
240
+ emit(event, ...data) {
241
+ this.eventEmitter.emit(event, ...data);
242
+ }
243
+ get isConnected() {
244
+ return this.status === 'OPEN';
245
+ }
246
+ get isConnecting() {
247
+ return this.status === 'CONNECTING';
248
+ }
249
+ get isDisconnected() {
250
+ return this.status === 'CLOSED';
251
+ }
252
+ get isClosed() {
253
+ return this.closed;
254
+ }
255
+ get status() {
256
+ return {
257
+ 0: 'CONNECTING',
258
+ 1: 'OPEN',
259
+ 2: 'CLOSING',
260
+ 3: 'CLOSED',
261
+ }[this.rws.readyState];
262
+ }
263
+ async call(action, data = {}, options = {}) {
264
+ // Rate limiting check
265
+ if (this.isRateLimited()) {
266
+ throw new RateLimitError('Rate limit exceeded - too many requests');
267
+ }
268
+ // Clear old queued messages periodically
269
+ this.clearOldQueuedMessages();
270
+ const responseEvent = options.responseEvent || `${action}_${nanoid()}_response`;
271
+ const errorEvent = `${responseEvent}_error`;
272
+ let closeHandler;
273
+ const removeListeners = () => {
274
+ if (closeHandler) {
275
+ this.rws.removeEventListener('close', closeHandler);
276
+ this.rws.removeEventListener('error', closeHandler);
277
+ }
278
+ this.eventEmitter.removeListener(responseEvent);
279
+ this.eventEmitter.removeListener(errorEvent);
280
+ };
281
+ const handler = async (resolve, reject) => {
282
+ const abortError = new DOMException('Request aborted', 'AbortError');
283
+ if (options.abortSignal?.aborted) {
284
+ reject(abortError);
285
+ }
286
+ if (options.abortSignal) {
287
+ options.abortSignal.addEventListener('abort', () => {
288
+ reject(abortError);
289
+ });
290
+ }
291
+ // If not connected, queue the message
292
+ if (!this.isConnected && !this.isClosed) {
293
+ this.log('debug', 'Connection not available, queuing message', {
294
+ action,
295
+ });
296
+ // Check queue size limit
297
+ if (this.messageQueue.length >= this.MAX_QUEUE_SIZE) {
298
+ const oldestMessage = this.messageQueue.shift();
299
+ if (oldestMessage) {
300
+ oldestMessage.reject(new Error('Message queue full, oldest message dropped'));
301
+ }
302
+ }
303
+ this.messageQueue.push({
304
+ action,
305
+ data,
306
+ options,
307
+ resolve,
308
+ reject,
309
+ timestamp: Date.now(),
310
+ });
311
+ return;
312
+ }
313
+ const startTime = Date.now();
314
+ this.listenOnce(responseEvent, (response) => {
315
+ // Update response time stats
316
+ const responseTime = Date.now() - startTime;
317
+ this.connectionStats.avgResponseTime = (this.connectionStats.avgResponseTime + responseTime) / 2;
318
+ this.connectionStats.totalMessages++;
319
+ this.log('debug', 'Message response received', {
320
+ action,
321
+ responseTime,
322
+ avgResponseTime: this.connectionStats.avgResponseTime,
323
+ });
324
+ resolve(response);
325
+ });
326
+ this.listenOnce(errorEvent, (e) => {
327
+ this.connectionStats.totalErrors++;
328
+ this.log('error', 'Message error received', { action, error: e });
329
+ reject(new ErrorEvent(e.code, e.message, e));
330
+ });
331
+ closeHandler = (_ev) => {
332
+ if (_ev.code === 1008 && (_ev.reason || '').includes('rate limit')) {
333
+ reject(new RateLimitError(_ev.reason || 'Rate limit exceeded', _ev));
334
+ return;
335
+ }
336
+ reject(new Error(`Connection lost to the notebook during request: ${_ev.reason || 'Unknown reason'}`));
337
+ };
338
+ this.rws.addEventListener('close', closeHandler);
339
+ this.rws.addEventListener('error', closeHandler);
340
+ try {
341
+ this.rws.send(this.pack({ action, data, errorEvent, responseEvent }));
342
+ this.log('debug', 'Message sent', { action, data });
343
+ }
344
+ catch (error) {
345
+ this.log('error', 'Failed to send message', { action, error });
346
+ throw error;
347
+ }
348
+ };
349
+ const send = async () => {
350
+ // Ensure connection is established before making calls
351
+ await __classPrivateFieldGet(this, _Transport_instances, "m", _Transport_connect).call(this);
352
+ const promise = new Promise(handler).finally(removeListeners);
353
+ if (!options.timeout) {
354
+ return promise;
355
+ }
356
+ return timeout(promise, options.timeout).finally(removeListeners);
357
+ };
358
+ return this.sendWithRetry(async () => await send(), options.retries || 10);
359
+ }
360
+ pack(data) {
361
+ return new Blob([encode(data)]);
362
+ }
363
+ sendWithRetry(sender, retries = 10) {
364
+ /**
365
+ * Enhanced retry with exponential backoff and intelligent error handling
366
+ */
367
+ return retry(async (bail, attempt) => {
368
+ try {
369
+ return await sender();
370
+ }
371
+ catch (e) {
372
+ // Don't retry these errors
373
+ if (e instanceof ErrorEvent ||
374
+ e instanceof RateLimitError ||
375
+ e instanceof InvalidConfigurationError ||
376
+ e instanceof InvalidMessageError ||
377
+ e instanceof DOMException) {
378
+ this.log('debug', 'Non-retryable error, bailing', {
379
+ error: e.message,
380
+ attempt,
381
+ });
382
+ bail(e);
383
+ return;
384
+ }
385
+ // Log retry attempt
386
+ this.log('debug', 'Retrying send operation', {
387
+ attempt,
388
+ error: e instanceof Error ? e.message : String(e),
389
+ nextDelay: this.getBackoffDelay(attempt - 1),
390
+ });
391
+ throw e;
392
+ }
393
+ }, {
394
+ retries,
395
+ onRetry: (e, attempt) => {
396
+ this.log('warn', 'Send operation retry', {
397
+ attempt,
398
+ maxRetries: retries,
399
+ error: e instanceof Error ? e.message : String(e),
400
+ });
401
+ },
402
+ // Use exponential backoff with jitter
403
+ minTimeout: 1000,
404
+ factor: 2,
405
+ maxTimeout: 30000,
406
+ randomize: true,
407
+ });
408
+ }
409
+ invoke(action, data = {}, options = {}) {
410
+ if (!options.responseEvent) {
411
+ options.responseEvent = `${action}_${nanoid()}`;
412
+ }
413
+ return this.call('invoke', { action, data }, options);
414
+ }
415
+ disconnect() {
416
+ if (this.closed) {
417
+ console.trace('Transport is already closed, cannot disconnect again');
418
+ return;
419
+ }
420
+ this.close();
421
+ }
422
+ close(code, reason) {
423
+ if (this.closed) {
424
+ return;
425
+ }
426
+ this.log('info', 'Closing transport connection', { code, reason });
427
+ // Clear any pending connection promise
428
+ this.connectPromise = null;
429
+ // Reject all queued messages
430
+ const queuedCount = this.messageQueue.length;
431
+ this.messageQueue.forEach((msg) => {
432
+ console.log('Rejecting queued message due to connection close');
433
+ msg.reject(new Error('Connection closed while message was queued'));
434
+ });
435
+ this.messageQueue = [];
436
+ if (queuedCount > 0) {
437
+ this.log('debug', `Rejected ${queuedCount} queued messages due to connection close`);
438
+ }
439
+ // Clear rate limiter
440
+ this.rateLimiter.requests = [];
441
+ // Dispose all event disposables
442
+ this.disposables.dispose();
443
+ // Close WebSocket connection
444
+ try {
445
+ this.rws.close(code, reason);
446
+ }
447
+ catch (error) {
448
+ this.log('error', 'Error closing WebSocket', {
449
+ error: error instanceof Error ? error.message : String(error),
450
+ });
451
+ }
452
+ // Emit final close event
453
+ this.eventEmitter.emit('transport.closed', {
454
+ code,
455
+ reason,
456
+ metrics: this.getConnectionMetrics(),
457
+ timestamp: Date.now(),
458
+ });
459
+ this.eventEmitter.removeListener('*'); // Remove all event listeners
460
+ this.closed = true;
461
+ this.log('info', 'Transport connection closed successfully');
462
+ }
463
+ onDidConnect(listener) {
464
+ this.rws.addEventListener('open', listener);
465
+ this.disposables.add('connect', {
466
+ dispose: () => {
467
+ this.rws.removeEventListener('open', listener);
468
+ },
469
+ });
470
+ }
471
+ onDidClose(listener) {
472
+ this.rws.addEventListener('close', listener);
473
+ this.disposables.add('close', {
474
+ dispose: () => {
475
+ this.rws.removeEventListener('close', listener);
476
+ },
477
+ });
478
+ }
479
+ /**
480
+ * Setup connection health monitoring
481
+ */
482
+ setupConnectionHealthMonitoring() {
483
+ this.rws.addEventListener('open', () => {
484
+ this.connectionStats.connectTime = Date.now();
485
+ this.connectionStats.reconnectCount++;
486
+ this.log('info', 'Connection established', {
487
+ reconnectCount: this.connectionStats.reconnectCount,
488
+ timeSinceStart: Date.now() - this.connectionStats.connectionStartTime,
489
+ });
490
+ this.processMessageQueue();
491
+ });
492
+ this.rws.addEventListener('close', (event) => {
493
+ this.log('warn', 'Connection closed', {
494
+ code: event.code,
495
+ reason: event.reason,
496
+ wasClean: event.wasClean,
497
+ });
498
+ this.handleConnectionClose(event.code);
499
+ });
500
+ this.rws.addEventListener('error', (event) => {
501
+ this.connectionStats.totalErrors++;
502
+ this.log('error', 'Connection error', {
503
+ error: event,
504
+ totalErrors: this.connectionStats.totalErrors,
505
+ });
506
+ });
507
+ }
508
+ /**
509
+ * Handle different WebSocket close codes appropriately
510
+ */
511
+ handleConnectionClose(code) {
512
+ switch (code) {
513
+ case 1000: // Normal closure
514
+ this.log('info', 'Normal connection closure');
515
+ return 'stop';
516
+ case 1001: // Going away
517
+ this.log('info', 'Connection going away, will reconnect');
518
+ return 'reconnect';
519
+ case 1006: // Abnormal closure
520
+ this.log('warn', 'Abnormal connection closure, will retry');
521
+ return 'retry';
522
+ case 1008: // Policy violation (rate limit)
523
+ this.log('error', 'Connection closed due to policy violation');
524
+ this.clearOldQueuedMessages();
525
+ return 'stop';
526
+ default:
527
+ this.log('warn', `Unknown close code: ${code}, will reconnect`);
528
+ return 'reconnect';
529
+ }
530
+ }
531
+ /**
532
+ * Process queued messages when connection is restored
533
+ */
534
+ processMessageQueue() {
535
+ if (this.messageQueue.length === 0) {
536
+ return;
537
+ }
538
+ this.log('debug', `Processing ${this.messageQueue.length} queued messages`);
539
+ const queue = [...this.messageQueue];
540
+ this.messageQueue = [];
541
+ for (const queuedMessage of queue) {
542
+ // Check if message hasn't timed out
543
+ if (Date.now() - queuedMessage.timestamp > this.QUEUE_TIMEOUT) {
544
+ queuedMessage.reject(new Error('Queued message timed out'));
545
+ continue;
546
+ }
547
+ // Retry the message
548
+ this.call(queuedMessage.action, queuedMessage.data, queuedMessage.options)
549
+ .then(queuedMessage.resolve)
550
+ .catch(queuedMessage.reject);
551
+ }
552
+ }
553
+ /**
554
+ * Clear old queued messages to prevent memory leaks
555
+ */
556
+ clearOldQueuedMessages() {
557
+ const now = Date.now();
558
+ const originalLength = this.messageQueue.length;
559
+ this.messageQueue = this.messageQueue.filter((msg) => {
560
+ const isExpired = now - msg.timestamp > this.QUEUE_TIMEOUT;
561
+ if (isExpired) {
562
+ msg.reject(new Error('Queued message expired'));
563
+ }
564
+ return !isExpired;
565
+ });
566
+ if (originalLength !== this.messageQueue.length) {
567
+ this.log('debug', `Cleared ${originalLength - this.messageQueue.length} expired messages`);
568
+ }
569
+ }
570
+ /**
571
+ * Rate limiting check
572
+ */
573
+ isRateLimited() {
574
+ const now = Date.now();
575
+ // Remove old requests outside the window
576
+ this.rateLimiter.requests = this.rateLimiter.requests.filter((timestamp) => now - timestamp < this.rateLimiter.windowMs);
577
+ // Check if we've exceeded the limit
578
+ if (this.rateLimiter.requests.length >= this.rateLimiter.maxRequests) {
579
+ return true;
580
+ }
581
+ // Add current request
582
+ this.rateLimiter.requests.push(now);
583
+ return false;
584
+ }
585
+ /**
586
+ * Calculate exponential backoff delay
587
+ */
588
+ getBackoffDelay(retryCount) {
589
+ const baseDelay = 1000; // 1 second
590
+ const maxDelay = 30000; // 30 seconds
591
+ const delay = Math.min(baseDelay * Math.pow(2, retryCount), maxDelay);
592
+ // Add jitter to prevent thundering herd
593
+ return delay + Math.random() * 1000;
594
+ }
595
+ /**
596
+ * Get comprehensive connection metrics
597
+ */
598
+ getConnectionMetrics() {
599
+ const now = Date.now();
600
+ const connectionDuration = this.connectionStats.connectTime > 0 ? now - this.connectionStats.connectTime : 0;
601
+ return {
602
+ status: this.status,
603
+ clientId: this.clientId,
604
+ isConnected: this.isConnected,
605
+ isConnecting: this.isConnecting,
606
+ connectionStats: {
607
+ ...this.connectionStats,
608
+ connectionDuration,
609
+ uptime: now - this.connectionStats.connectionStartTime,
610
+ messagesPerSecond: connectionDuration > 0 ? (this.connectionStats.totalMessages / (connectionDuration / 1000)).toFixed(2) : '0',
611
+ errorRate: this.connectionStats.totalMessages > 0
612
+ ? ((this.connectionStats.totalErrors / this.connectionStats.totalMessages) * 100).toFixed(2) + '%'
613
+ : '0%',
614
+ timeSinceLastPing: this.connectionStats.lastPingTime > 0 ? now - this.connectionStats.lastPingTime : 0,
615
+ timeSinceLastPong: this.connectionStats.lastPongTime > 0 ? now - this.connectionStats.lastPongTime : 0,
616
+ },
617
+ messageQueue: {
618
+ length: this.messageQueue.length,
619
+ maxSize: this.MAX_QUEUE_SIZE,
620
+ oldestMessageAge: this.messageQueue.length > 0 ? now - Math.min(...this.messageQueue.map((m) => m.timestamp)) : 0,
621
+ },
622
+ rateLimiter: {
623
+ currentRequests: this.rateLimiter.requests.length,
624
+ maxRequests: this.rateLimiter.maxRequests,
625
+ windowMs: this.rateLimiter.windowMs,
626
+ isLimited: this.isRateLimited(),
627
+ },
628
+ config: {
629
+ pingInterval: this.PING_INTERVAL,
630
+ queueTimeout: this.QUEUE_TIMEOUT,
631
+ url: this.url.toString(),
632
+ },
633
+ };
634
+ }
635
+ /**
636
+ * Get connection health status
637
+ */
638
+ getHealthStatus() {
639
+ const metrics = this.getConnectionMetrics();
640
+ const stats = metrics.connectionStats;
641
+ // Unhealthy conditions
642
+ if (!this.isConnected || stats.timeSinceLastPong > this.PING_INTERVAL * 2 || Number.parseFloat(stats.errorRate) > 50) {
643
+ return 'unhealthy';
644
+ }
645
+ // Degraded conditions
646
+ if (stats.avgResponseTime > 5000 ||
647
+ Number.parseFloat(stats.errorRate) > 10 ||
648
+ stats.timeSinceLastPong > this.PING_INTERVAL * 1.5) {
649
+ return 'degraded';
650
+ }
651
+ return 'healthy';
652
+ }
653
+ /**
654
+ * Reset connection statistics
655
+ */
656
+ resetStats() {
657
+ this.connectionStats = {
658
+ connectTime: Date.now(),
659
+ reconnectCount: 0,
660
+ lastPingTime: 0,
661
+ lastPongTime: 0,
662
+ avgResponseTime: 0,
663
+ totalMessages: 0,
664
+ totalErrors: 0,
665
+ connectionStartTime: Date.now(),
666
+ };
667
+ this.log('debug', 'Connection statistics reset');
668
+ }
669
+ /**
670
+ * Run connection diagnostics
671
+ */
672
+ async runDiagnostics() {
673
+ const metrics = this.getConnectionMetrics();
674
+ const status = this.getHealthStatus();
675
+ const issues = [];
676
+ const recommendations = [];
677
+ // Check for issues
678
+ if (!this.isConnected) {
679
+ issues.push('Connection is not established');
680
+ recommendations.push('Check network connectivity and server availability');
681
+ }
682
+ if (metrics.connectionStats.timeSinceLastPong > this.PING_INTERVAL * 2) {
683
+ issues.push('No pong received recently - connection may be stale');
684
+ recommendations.push('Consider forcing a reconnection');
685
+ }
686
+ if (Number.parseFloat(metrics.connectionStats.errorRate) > 10) {
687
+ issues.push(`High error rate: ${metrics.connectionStats.errorRate}`);
688
+ recommendations.push('Check server logs and network stability');
689
+ }
690
+ if (metrics.messageQueue.length > this.MAX_QUEUE_SIZE * 0.8) {
691
+ issues.push('Message queue is nearly full');
692
+ recommendations.push('Check connection stability and consider reducing message frequency');
693
+ }
694
+ if (metrics.connectionStats.avgResponseTime > 5000) {
695
+ issues.push('High average response time');
696
+ recommendations.push('Check network latency and server performance');
697
+ }
698
+ if (metrics.rateLimiter.isLimited) {
699
+ issues.push('Rate limiting is active');
700
+ recommendations.push('Reduce request frequency or increase rate limit');
701
+ }
702
+ this.log('info', 'Connection diagnostics completed', {
703
+ status,
704
+ issueCount: issues.length,
705
+ recommendationCount: recommendations.length,
706
+ });
707
+ return {
708
+ status,
709
+ metrics,
710
+ issues,
711
+ recommendations,
712
+ };
713
+ }
714
+ /**
715
+ * Periodic maintenance - clean up old data, run health checks
716
+ */
717
+ startPeriodicMaintenance() {
718
+ this.disposables.add('maintenance', () => {
719
+ // Run maintenance every 5 minutes
720
+ const maintenanceInterval = setInterval(() => {
721
+ this.clearOldQueuedMessages();
722
+ // Clean up old rate limiter entries (should already be done, but double-check)
723
+ const now = Date.now();
724
+ this.rateLimiter.requests = this.rateLimiter.requests.filter((timestamp) => now - timestamp < this.rateLimiter.windowMs);
725
+ // Log health status if debug is enabled
726
+ if (this.options.debug) {
727
+ const health = this.getHealthStatus();
728
+ const metrics = this.getConnectionMetrics();
729
+ this.log('debug', 'Periodic health check', {
730
+ health,
731
+ messageCount: metrics.connectionStats.totalMessages,
732
+ errorCount: metrics.connectionStats.totalErrors,
733
+ queueLength: metrics.messageQueue.length,
734
+ });
735
+ }
736
+ }, 5 * 60 * 1000); // 5 minutes
737
+ return {
738
+ dispose: () => {
739
+ clearInterval(maintenanceInterval);
740
+ },
741
+ };
742
+ });
743
+ }
744
+ }
745
+ _Transport_instances = new WeakSet(), _Transport_connect = function _Transport_connect() {
746
+ if (this.isConnected) {
747
+ return Promise.resolve();
748
+ }
749
+ // Return existing connection promise if one is already in progress
750
+ if (this.connectPromise) {
751
+ return this.connectPromise;
752
+ }
753
+ // Create and cache the connection promise
754
+ this.connectPromise = new Promise((resolve, reject) => {
755
+ // Check if connection is already open after potential reconnect
756
+ if (this.isConnected) {
757
+ resolve();
758
+ __classPrivateFieldGet(this, _Transport_instances, "m", _Transport_startPeriodicPing).call(this);
759
+ return;
760
+ }
761
+ // Open the connection if it's closed
762
+ if (this.rws.readyState === 3) {
763
+ this.rws.reconnect();
764
+ }
765
+ let timeoutId;
766
+ const openHandler = () => {
767
+ this.rws.removeEventListener('open', openHandler);
768
+ this.rws.removeEventListener('error', errorHandler);
769
+ clearTimeout(timeoutId);
770
+ // Clear the cached promise on success
771
+ this.connectPromise = null;
772
+ resolve();
773
+ __classPrivateFieldGet(this, _Transport_instances, "m", _Transport_startPeriodicPing).call(this);
774
+ };
775
+ const errorHandler = (error) => {
776
+ this.rws.removeEventListener('open', openHandler);
777
+ this.rws.removeEventListener('error', errorHandler);
778
+ clearTimeout(timeoutId);
779
+ // Clear the cached promise on error so retry is possible
780
+ this.connectPromise = null;
781
+ reject(new Error(`WebSocket connection failed: ${error}`));
782
+ };
783
+ // Add timeout to prevent hanging forever
784
+ timeoutId = setTimeout(() => {
785
+ this.rws.removeEventListener('open', openHandler);
786
+ this.rws.removeEventListener('error', errorHandler);
787
+ // Clear the cached promise on timeout so retry is possible
788
+ this.connectPromise = null;
789
+ reject(new Error('WebSocket connection timeout'));
790
+ }, 10000); // 10 second timeout
791
+ this.rws.addEventListener('open', openHandler);
792
+ this.rws.addEventListener('error', errorHandler);
793
+ });
794
+ return this.connectPromise;
795
+ }, _Transport_startPeriodicPing = function _Transport_startPeriodicPing() {
796
+ this.disposables.add('pingInterval', () => {
797
+ const interval = setInterval(async () => {
798
+ try {
799
+ this.connectionStats.lastPingTime = Date.now();
800
+ this.log('debug', 'Sending periodic ping');
801
+ const startTime = Date.now();
802
+ await this.invoke('ping');
803
+ this.connectionStats.lastPongTime = Date.now();
804
+ const pingTime = this.connectionStats.lastPongTime - startTime;
805
+ this.log('debug', `Ping successful`, { pingTime });
806
+ }
807
+ catch (error) {
808
+ this.log('error', 'Ping failed', {
809
+ error: error instanceof Error ? error.message : String(error),
810
+ });
811
+ // If ping fails consistently, the connection might be dead
812
+ if (Date.now() - this.connectionStats.lastPongTime > this.PING_INTERVAL * 3) {
813
+ this.log('warn', 'Connection appears dead, forcing reconnection');
814
+ this.rws.reconnect();
815
+ }
816
+ }
817
+ }, this.PING_INTERVAL);
818
+ return {
819
+ dispose: () => {
820
+ clearInterval(interval);
821
+ },
822
+ };
823
+ });
824
+ };
825
+ //# sourceMappingURL=index.js.map