@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.
- package/README.md +718 -0
- package/dist/auth.d.ts +14 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +12 -0
- package/dist/auth.js.map +1 -0
- package/dist/beacon/index.d.ts +169 -0
- package/dist/beacon/index.d.ts.map +1 -0
- package/dist/beacon/index.js +537 -0
- package/dist/beacon/index.js.map +1 -0
- package/dist/beacon/navigator.d.ts +141 -0
- package/dist/beacon/navigator.d.ts.map +1 -0
- package/dist/beacon/navigator.js +246 -0
- package/dist/beacon/navigator.js.map +1 -0
- package/dist/beacon/types.d.ts +230 -0
- package/dist/beacon/types.d.ts.map +1 -0
- package/dist/beacon/types.js +2 -0
- package/dist/beacon/types.js.map +1 -0
- package/dist/browser/phpsandbox-sdk.esm.js +4755 -0
- package/dist/browser/phpsandbox-sdk.esm.js.map +7 -0
- package/dist/browser/phpsandbox-sdk.esm.min.js +27 -0
- package/dist/browser/phpsandbox-sdk.esm.min.js.map +7 -0
- package/dist/browser/phpsandbox-sdk.iife.js +4766 -0
- package/dist/browser/phpsandbox-sdk.iife.js.map +7 -0
- package/dist/browser/phpsandbox-sdk.iife.min.js +27 -0
- package/dist/browser/phpsandbox-sdk.iife.min.js.map +7 -0
- package/dist/composer.d.ts +45 -0
- package/dist/composer.d.ts.map +1 -0
- package/dist/composer.js +30 -0
- package/dist/composer.js.map +1 -0
- package/dist/container.d.ts +66 -0
- package/dist/container.d.ts.map +1 -0
- package/dist/container.js +56 -0
- package/dist/container.js.map +1 -0
- package/dist/events/index.d.ts +23 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +46 -0
- package/dist/events/index.js.map +1 -0
- package/dist/filesystem.d.ts +483 -0
- package/dist/filesystem.d.ts.map +1 -0
- package/dist/filesystem.js +244 -0
- package/dist/filesystem.js.map +1 -0
- package/dist/git.d.ts +42 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +18 -0
- package/dist/git.js.map +1 -0
- package/dist/index.d.ts +167 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +265 -0
- package/dist/index.js.map +1 -0
- package/dist/laravel.d.ts +23 -0
- package/dist/laravel.d.ts.map +1 -0
- package/dist/laravel.js +12 -0
- package/dist/laravel.js.map +1 -0
- package/dist/log.d.ts +13 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +12 -0
- package/dist/log.js.map +1 -0
- package/dist/lsp.d.ts +57 -0
- package/dist/lsp.d.ts.map +1 -0
- package/dist/lsp.js +69 -0
- package/dist/lsp.js.map +1 -0
- package/dist/repl.d.ts +41 -0
- package/dist/repl.d.ts.map +1 -0
- package/dist/repl.js +27 -0
- package/dist/repl.js.map +1 -0
- package/dist/shell.d.ts +29 -0
- package/dist/shell.d.ts.map +1 -0
- package/dist/shell.js +29 -0
- package/dist/shell.js.map +1 -0
- package/dist/socket/index.d.ts +229 -0
- package/dist/socket/index.d.ts.map +1 -0
- package/dist/socket/index.js +825 -0
- package/dist/socket/index.js.map +1 -0
- package/dist/terminal.d.ts +97 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +87 -0
- package/dist/terminal.js.map +1 -0
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/disposable.d.ts +7 -0
- package/dist/utils/disposable.d.ts.map +1 -0
- package/dist/utils/disposable.js +20 -0
- package/dist/utils/disposable.js.map +1 -0
- package/dist/utils/promise.d.ts +13 -0
- package/dist/utils/promise.d.ts.map +1 -0
- package/dist/utils/promise.js +21 -0
- package/dist/utils/promise.js.map +1 -0
- 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
|