@aptly-sdk/hq 0.0.5 → 0.0.6
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/dist/index.js +1685 -10
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import './global.css';
|
|
2
|
-
import
|
|
3
|
-
import Brook from '@aptly-sdk/brook';
|
|
4
|
-
import { BrookProvider as BrookProvider$1, useStream } from '@aptly-sdk/brook/react';
|
|
2
|
+
import React, { useEffect, createContext, useRef, useState, useContext, useCallback, useMemo, Activity } from 'react';
|
|
5
3
|
|
|
6
4
|
function getProject(apiKey) {
|
|
7
5
|
return new Promise(resolve => {
|
|
@@ -44,6 +42,1683 @@ var client = {
|
|
|
44
42
|
}
|
|
45
43
|
};
|
|
46
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Exponential backoff implementation with 1.5x multiplier and jitter.
|
|
47
|
+
* Prevents thundering herd problem during reconnection scenarios.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Manages exponential backoff timing for reconnection attempts.
|
|
52
|
+
*/
|
|
53
|
+
class ExponentialBackoff {
|
|
54
|
+
/**
|
|
55
|
+
* Creates a new ExponentialBackoff instance.
|
|
56
|
+
*
|
|
57
|
+
* @param {Object} options - Configuration options.
|
|
58
|
+
* @param {number} [options.initialDelay=3000] - Initial delay in milliseconds.
|
|
59
|
+
* @param {number} [options.multiplier=1.5] - Backoff multiplier factor.
|
|
60
|
+
* @param {number} [options.maxDelay=30000] - Maximum delay cap in milliseconds.
|
|
61
|
+
* @param {number} [options.jitter=0.1] - Jitter factor (0-1) to add randomness.
|
|
62
|
+
* @param {number} [options.maxAttempts=Infinity] - Maximum number of attempts.
|
|
63
|
+
* @param {Log} log - Log instance.
|
|
64
|
+
*/
|
|
65
|
+
constructor(options = {}) {
|
|
66
|
+
this.initialDelay = options.initialDelay || 3000; // 3 seconds
|
|
67
|
+
this.multiplier = options.multiplier || 1.5; // 1.5x multiplier as per PRP
|
|
68
|
+
this.maxDelay = options.maxDelay || 30000; // 30 seconds max
|
|
69
|
+
this.jitter = options.jitter || 0.1; // 10% jitter
|
|
70
|
+
this.maxAttempts = options.maxAttempts || Infinity;
|
|
71
|
+
this.attempts = 0;
|
|
72
|
+
this.currentDelay = this.initialDelay;
|
|
73
|
+
this.log = options.log;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Gets the next delay value with exponential backoff and jitter.
|
|
78
|
+
*
|
|
79
|
+
* @returns {number} Delay in milliseconds for the next attempt.
|
|
80
|
+
*/
|
|
81
|
+
getDelay() {
|
|
82
|
+
if (this.attempts >= this.maxAttempts) {
|
|
83
|
+
throw new Error('Maximum reconnection attempts exceeded');
|
|
84
|
+
}
|
|
85
|
+
this.attempts += 1;
|
|
86
|
+
|
|
87
|
+
// Calculate base delay with exponential backoff
|
|
88
|
+
let delay = this.currentDelay;
|
|
89
|
+
|
|
90
|
+
// Apply jitter to prevent thundering herd
|
|
91
|
+
// Jitter adds randomness between -jitter% and +jitter%
|
|
92
|
+
const jitterAmount = delay * this.jitter;
|
|
93
|
+
const jitterOffset = (Math.random() * 2 - 1) * jitterAmount;
|
|
94
|
+
delay += jitterOffset;
|
|
95
|
+
|
|
96
|
+
// Ensure delay is positive and within bounds
|
|
97
|
+
delay = Math.max(100, Math.min(delay, this.maxDelay));
|
|
98
|
+
|
|
99
|
+
// Update current delay for next attempt
|
|
100
|
+
this.currentDelay = Math.min(this.currentDelay * this.multiplier, this.maxDelay);
|
|
101
|
+
return Math.floor(delay);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resets the backoff state after a successful connection.
|
|
106
|
+
*
|
|
107
|
+
* @returns {void}
|
|
108
|
+
*/
|
|
109
|
+
reset() {
|
|
110
|
+
this.attempts = 0;
|
|
111
|
+
this.currentDelay = this.initialDelay;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Gets the current number of attempts made.
|
|
116
|
+
*
|
|
117
|
+
* @returns {number} Number of connection attempts.
|
|
118
|
+
*/
|
|
119
|
+
getAttempts() {
|
|
120
|
+
return this.attempts;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Checks if maximum attempts have been reached.
|
|
125
|
+
*
|
|
126
|
+
* @returns {boolean} True if max attempts exceeded.
|
|
127
|
+
*/
|
|
128
|
+
isMaxAttemptsReached() {
|
|
129
|
+
return this.attempts >= this.maxAttempts;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Gets the next delay without incrementing the attempt counter.
|
|
134
|
+
* Useful for previewing the next delay value.
|
|
135
|
+
*
|
|
136
|
+
* @returns {number} Next delay in milliseconds.
|
|
137
|
+
*/
|
|
138
|
+
peekNextDelay() {
|
|
139
|
+
let delay = this.currentDelay * this.multiplier;
|
|
140
|
+
delay = Math.min(delay, this.maxDelay);
|
|
141
|
+
|
|
142
|
+
// Apply jitter
|
|
143
|
+
const jitterAmount = delay * this.jitter;
|
|
144
|
+
const jitterOffset = (Math.random() * 2 - 1) * jitterAmount;
|
|
145
|
+
delay += jitterOffset;
|
|
146
|
+
return Math.max(100, Math.floor(delay));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Creates a promise that resolves after the backoff delay.
|
|
151
|
+
* Convenient for async/await usage patterns.
|
|
152
|
+
*
|
|
153
|
+
* @returns {Promise<number>} Promise resolving to the delay used.
|
|
154
|
+
*/
|
|
155
|
+
async wait() {
|
|
156
|
+
const delay = this.getDelay();
|
|
157
|
+
return new Promise(resolve => {
|
|
158
|
+
setTimeout(() => {
|
|
159
|
+
resolve(delay);
|
|
160
|
+
}, delay);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Gets configuration summary for debugging.
|
|
166
|
+
*
|
|
167
|
+
* @returns {Object} Configuration and state information.
|
|
168
|
+
*/
|
|
169
|
+
getStatus() {
|
|
170
|
+
return {
|
|
171
|
+
attempts: this.attempts,
|
|
172
|
+
currentDelay: this.currentDelay,
|
|
173
|
+
nextDelay: this.attempts < this.maxAttempts ? this.peekNextDelay() : null,
|
|
174
|
+
maxAttempts: this.maxAttempts,
|
|
175
|
+
config: {
|
|
176
|
+
initialDelay: this.initialDelay,
|
|
177
|
+
multiplier: this.multiplier,
|
|
178
|
+
maxDelay: this.maxDelay,
|
|
179
|
+
jitter: this.jitter
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* WebSocket connection manager with automatic reconnection and fault tolerance.
|
|
187
|
+
* Provides reliable real-time connectivity with exponential backoff strategy.
|
|
188
|
+
*/
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Connection states for status tracking.
|
|
193
|
+
*/
|
|
194
|
+
const CONNECTION_STATES = {
|
|
195
|
+
DISCONNECTED: 'disconnected',
|
|
196
|
+
CONNECTING: 'connecting',
|
|
197
|
+
AUTHENTICATING: 'authenticating',
|
|
198
|
+
CONNECTED: 'connected',
|
|
199
|
+
RECONNECTING: 'reconnecting',
|
|
200
|
+
FAILED: 'failed',
|
|
201
|
+
UNAUTHORIZED: 'unauthorized'
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Manages WebSocket connections with automatic reconnection and state management.
|
|
206
|
+
*/
|
|
207
|
+
class Connection {
|
|
208
|
+
/**
|
|
209
|
+
* Creates a new Connection instance.
|
|
210
|
+
*
|
|
211
|
+
* @param {Object} config - Connection configuration.
|
|
212
|
+
* @param {string} config.endpoint - WebSocket server endpoint.
|
|
213
|
+
* @param {string} config.apiKey - API key for authentication.
|
|
214
|
+
* @param {number} [config.reconnectTimeout=3000] - Initial reconnection delay.
|
|
215
|
+
* @param {EventTarget} connectivity - Event target for connectivity events.
|
|
216
|
+
*/
|
|
217
|
+
constructor(config, connectivity, log) {
|
|
218
|
+
this.config = config;
|
|
219
|
+
this.connectivity = connectivity;
|
|
220
|
+
this.ws = null;
|
|
221
|
+
this.state = CONNECTION_STATES.DISCONNECTED;
|
|
222
|
+
this.clientId = this.generateClientId();
|
|
223
|
+
this.log = log;
|
|
224
|
+
|
|
225
|
+
// Reconnection management
|
|
226
|
+
this.backoff = new ExponentialBackoff({
|
|
227
|
+
initialDelay: config.reconnectTimeout || 3000,
|
|
228
|
+
multiplier: 1.5,
|
|
229
|
+
maxDelay: 30000,
|
|
230
|
+
maxAttempts: 20,
|
|
231
|
+
log: this.log
|
|
232
|
+
});
|
|
233
|
+
this.reconnectTimeoutId = null;
|
|
234
|
+
this.shouldReconnect = false;
|
|
235
|
+
this.messageHandlers = new Set();
|
|
236
|
+
this.connectionPromise = null;
|
|
237
|
+
|
|
238
|
+
// Message queue for offline messages
|
|
239
|
+
this.messageQueue = [];
|
|
240
|
+
this.maxQueueSize = 10000;
|
|
241
|
+
|
|
242
|
+
// Heartbeat management
|
|
243
|
+
this.heartbeatInterval = null;
|
|
244
|
+
this.lastPongTime = null;
|
|
245
|
+
this.lastPingTime = null;
|
|
246
|
+
this.heartbeatIntervalMs = 30000; // 30 seconds
|
|
247
|
+
|
|
248
|
+
// Authentication management
|
|
249
|
+
this.isAuthenticated = false;
|
|
250
|
+
this.authTimeout = 30000; // 30 seconds to authenticate
|
|
251
|
+
this.authTimeoutId = null;
|
|
252
|
+
this.authPromise = null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Establishes WebSocket connection with retry logic.
|
|
257
|
+
*
|
|
258
|
+
* @returns {Promise<boolean>} Promise resolving to connection success status.
|
|
259
|
+
*/
|
|
260
|
+
async connect() {
|
|
261
|
+
// If already connecting, return the existing promise
|
|
262
|
+
if (this.connectionPromise) {
|
|
263
|
+
return this.connectionPromise;
|
|
264
|
+
}
|
|
265
|
+
this.shouldReconnect = true;
|
|
266
|
+
this.connectionPromise = this.attemptConnection();
|
|
267
|
+
try {
|
|
268
|
+
const result = await this.connectionPromise;
|
|
269
|
+
this.connectionPromise = null;
|
|
270
|
+
return result;
|
|
271
|
+
} catch (error) {
|
|
272
|
+
this.connectionPromise = null;
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Attempts to establish WebSocket connection.
|
|
279
|
+
*
|
|
280
|
+
* @returns {Promise<boolean>} Promise resolving to connection success.
|
|
281
|
+
* @private
|
|
282
|
+
*/
|
|
283
|
+
attemptConnection() {
|
|
284
|
+
return new Promise((resolve, reject) => {
|
|
285
|
+
try {
|
|
286
|
+
this.setState(CONNECTION_STATES.CONNECTING);
|
|
287
|
+
|
|
288
|
+
// Create WebSocket connection
|
|
289
|
+
this.ws = new WebSocket(this.config.endpoint);
|
|
290
|
+
|
|
291
|
+
// Connection timeout
|
|
292
|
+
const connectionTimeout = setTimeout(() => {
|
|
293
|
+
if (this.ws && this.ws.readyState === WebSocket.CONNECTING) {
|
|
294
|
+
this.ws.close();
|
|
295
|
+
reject(new Error('Connection timeout'));
|
|
296
|
+
}
|
|
297
|
+
}, 10000);
|
|
298
|
+
|
|
299
|
+
// Connection opened successfully
|
|
300
|
+
this.ws.onopen = () => {
|
|
301
|
+
clearTimeout(connectionTimeout);
|
|
302
|
+
this.setState(CONNECTION_STATES.AUTHENTICATING);
|
|
303
|
+
this.backoff.reset();
|
|
304
|
+
|
|
305
|
+
// Start authentication process
|
|
306
|
+
this.startAuthentication().then(() => {
|
|
307
|
+
this.setState(CONNECTION_STATES.CONNECTED);
|
|
308
|
+
// this.startHeartbeat(); -- This is removed due to heartbeat is managed in server.
|
|
309
|
+
this.flushMessageQueue();
|
|
310
|
+
resolve(true);
|
|
311
|
+
}).catch(authError => {
|
|
312
|
+
this.log.error('Authentication failed:', authError);
|
|
313
|
+
this.ws.close(1000, 'Authentication failed');
|
|
314
|
+
this.setState(CONNECTION_STATES.UNAUTHORIZED);
|
|
315
|
+
reject(authError);
|
|
316
|
+
});
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// Handle incoming messages
|
|
320
|
+
this.ws.onmessage = event => {
|
|
321
|
+
this.handleMessage(event);
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// Handle connection close
|
|
325
|
+
this.ws.onclose = event => {
|
|
326
|
+
clearTimeout(connectionTimeout);
|
|
327
|
+
this.stopHeartbeat();
|
|
328
|
+
this.handleDisconnection(event);
|
|
329
|
+
|
|
330
|
+
// If this was during initial connection, reject the promise
|
|
331
|
+
if (this.state === CONNECTION_STATES.CONNECTING) {
|
|
332
|
+
reject(new Error(`Connection failed: ${event.code} ${event.reason}`));
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// Handle connection errors
|
|
337
|
+
this.ws.onerror = error => {
|
|
338
|
+
clearTimeout(connectionTimeout);
|
|
339
|
+
|
|
340
|
+
// If this was during initial connection, reject the promise
|
|
341
|
+
if (this.state === CONNECTION_STATES.CONNECTING) {
|
|
342
|
+
reject(new Error('WebSocket connection error'));
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
} catch (error) {
|
|
346
|
+
reject(error);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Closes the WebSocket connection and stops reconnection attempts.
|
|
353
|
+
*
|
|
354
|
+
* @returns {void}
|
|
355
|
+
*/
|
|
356
|
+
disconnect() {
|
|
357
|
+
this.shouldReconnect = false;
|
|
358
|
+
if (this.reconnectTimeoutId) {
|
|
359
|
+
clearTimeout(this.reconnectTimeoutId);
|
|
360
|
+
this.reconnectTimeoutId = null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Clear auth timeout
|
|
364
|
+
if (this.authTimeoutId) {
|
|
365
|
+
clearTimeout(this.authTimeoutId);
|
|
366
|
+
this.authTimeoutId = null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Reset authentication state
|
|
370
|
+
this.isAuthenticated = false;
|
|
371
|
+
this.authPromise = null;
|
|
372
|
+
this.stopHeartbeat();
|
|
373
|
+
if (this.ws) {
|
|
374
|
+
try {
|
|
375
|
+
// Detach handlers to avoid side effects during shutdown
|
|
376
|
+
this.ws.onopen = null;
|
|
377
|
+
this.ws.onmessage = null;
|
|
378
|
+
this.ws.onclose = null;
|
|
379
|
+
this.ws.onerror = null;
|
|
380
|
+
|
|
381
|
+
// Prefer terminate when available to avoid lingering close timers
|
|
382
|
+
if (typeof this.ws.terminate === 'function') {
|
|
383
|
+
this.ws.terminate();
|
|
384
|
+
} else {
|
|
385
|
+
this.ws.close();
|
|
386
|
+
}
|
|
387
|
+
} catch (e) {
|
|
388
|
+
// Ignore errors during shutdown
|
|
389
|
+
}
|
|
390
|
+
this.ws = null;
|
|
391
|
+
}
|
|
392
|
+
this.setState(CONNECTION_STATES.DISCONNECTED);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Sends a message through the WebSocket connection.
|
|
397
|
+
*
|
|
398
|
+
* @param {Object} message - Message object to send.
|
|
399
|
+
* @returns {void}
|
|
400
|
+
*/
|
|
401
|
+
send(message) {
|
|
402
|
+
const messageStr = JSON.stringify(message);
|
|
403
|
+
if (this.isConnected()) {
|
|
404
|
+
try {
|
|
405
|
+
this.ws.send(messageStr);
|
|
406
|
+
} catch (error) {
|
|
407
|
+
this.log.error('Failed to send message:', error);
|
|
408
|
+
// Queue the message for retry
|
|
409
|
+
this.queueMessage(message);
|
|
410
|
+
}
|
|
411
|
+
} else {
|
|
412
|
+
// Queue message if not connected
|
|
413
|
+
this.queueMessage(message);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Adds a message handler for incoming WebSocket messages.
|
|
419
|
+
*
|
|
420
|
+
* @param {Function} handler - Message handler function.
|
|
421
|
+
* @returns {void}
|
|
422
|
+
*/
|
|
423
|
+
addMessageHandler(handler) {
|
|
424
|
+
this.messageHandlers.add(handler);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Removes a message handler.
|
|
429
|
+
*
|
|
430
|
+
* @param {Function} handler - Message handler function to remove.
|
|
431
|
+
* @returns {void}
|
|
432
|
+
*/
|
|
433
|
+
removeMessageHandler(handler) {
|
|
434
|
+
this.messageHandlers.delete(handler);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Gets the current connection state.
|
|
439
|
+
*
|
|
440
|
+
* @returns {string} Current connection state.
|
|
441
|
+
*/
|
|
442
|
+
getState() {
|
|
443
|
+
return this.state;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Checks if the connection is currently established and authenticated.
|
|
448
|
+
*
|
|
449
|
+
* @returns {boolean} True if connected and authenticated.
|
|
450
|
+
*/
|
|
451
|
+
isConnected() {
|
|
452
|
+
return this.state === CONNECTION_STATES.CONNECTED && this.isAuthenticated && this.ws && this.ws.readyState === WebSocket.OPEN;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Gets the client ID for this connection.
|
|
457
|
+
*
|
|
458
|
+
* @returns {string} Unique client identifier.
|
|
459
|
+
*/
|
|
460
|
+
getClientId() {
|
|
461
|
+
return this.clientId;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Handles incoming WebSocket messages.
|
|
466
|
+
*
|
|
467
|
+
* @param {MessageEvent} event - WebSocket message event.
|
|
468
|
+
* @returns {void}
|
|
469
|
+
* @private
|
|
470
|
+
*/
|
|
471
|
+
handleMessage(event) {
|
|
472
|
+
try {
|
|
473
|
+
const message = JSON.parse(event.data);
|
|
474
|
+
|
|
475
|
+
// Handle authentication messages during auth flow
|
|
476
|
+
if (!this.isAuthenticated) {
|
|
477
|
+
this.handleAuthMessage(message);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Handle heartbeat responses
|
|
482
|
+
if (message.type === 'heartbeat') {
|
|
483
|
+
this.handleReceiveHeartbeat();
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Forward message to registered handlers
|
|
488
|
+
this.messageHandlers.forEach(handler => {
|
|
489
|
+
try {
|
|
490
|
+
handler(message);
|
|
491
|
+
} catch (error) {
|
|
492
|
+
this.log.error('Message handler error:', error);
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
} catch (error) {
|
|
496
|
+
this.log.error('Failed to parse WebSocket message:', error);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Starts the post-connection authentication process.
|
|
502
|
+
*
|
|
503
|
+
* @returns {Promise<void>} Promise resolving when authentication completes.
|
|
504
|
+
* @private
|
|
505
|
+
*/
|
|
506
|
+
startAuthentication() {
|
|
507
|
+
return new Promise((resolve, reject) => {
|
|
508
|
+
this.authPromise = {
|
|
509
|
+
resolve,
|
|
510
|
+
reject
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// Set authentication timeout
|
|
514
|
+
this.authTimeoutId = setTimeout(() => {
|
|
515
|
+
const error = new Error('Authentication timeout');
|
|
516
|
+
this.authPromise = null;
|
|
517
|
+
reject(error);
|
|
518
|
+
}, this.authTimeout);
|
|
519
|
+
|
|
520
|
+
// Send authentication credentials immediately after connection
|
|
521
|
+
// The server expects immediate authentication, not a request-response flow
|
|
522
|
+
this.sendAuthCredentials();
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Handles authentication-related messages from the server.
|
|
528
|
+
*
|
|
529
|
+
* @param {Object} message - Parsed WebSocket message.
|
|
530
|
+
* @returns {void}
|
|
531
|
+
* @private
|
|
532
|
+
*/
|
|
533
|
+
handleAuthMessage(message) {
|
|
534
|
+
if (!this.authPromise) {
|
|
535
|
+
return; // No active authentication process
|
|
536
|
+
}
|
|
537
|
+
switch (message.type) {
|
|
538
|
+
case 'auth_required':
|
|
539
|
+
this.sendAuthCredentials();
|
|
540
|
+
break;
|
|
541
|
+
case 'auth_success':
|
|
542
|
+
this.handleAuthSuccess(message);
|
|
543
|
+
break;
|
|
544
|
+
case 'connected':
|
|
545
|
+
this.handleConnected(message);
|
|
546
|
+
break;
|
|
547
|
+
case 'auth_timeout':
|
|
548
|
+
case 'error':
|
|
549
|
+
this.handleAuthError(message);
|
|
550
|
+
break;
|
|
551
|
+
case 'heartbeat':
|
|
552
|
+
// Allow heartbeat during auth
|
|
553
|
+
this.lastPongTime = Date.now();
|
|
554
|
+
this.handleReceiveHeartbeat();
|
|
555
|
+
break;
|
|
556
|
+
default:
|
|
557
|
+
this.log.warn('Unexpected message during authentication:', message.type);
|
|
558
|
+
if (message.error === 'Invalid API key') {
|
|
559
|
+
this.setState(CONNECTION_STATES.UNAUTHORIZED);
|
|
560
|
+
}
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Sends authentication credentials to the server.
|
|
567
|
+
*
|
|
568
|
+
* @returns {void}
|
|
569
|
+
* @private
|
|
570
|
+
*/
|
|
571
|
+
sendAuthCredentials() {
|
|
572
|
+
const authMessage = {
|
|
573
|
+
type: 'auth',
|
|
574
|
+
apiKey: this.config.apiKey,
|
|
575
|
+
timestamp: Date.now()
|
|
576
|
+
};
|
|
577
|
+
try {
|
|
578
|
+
this.ws.send(JSON.stringify(authMessage));
|
|
579
|
+
} catch (error) {
|
|
580
|
+
this.log.error('Failed to send auth credentials:', error);
|
|
581
|
+
if (this.authPromise) {
|
|
582
|
+
this.authPromise.reject(error);
|
|
583
|
+
this.authPromise = null;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Handles successful authentication response.
|
|
590
|
+
*
|
|
591
|
+
* @param {Object} message - Auth success message.
|
|
592
|
+
* @returns {void}
|
|
593
|
+
* @private
|
|
594
|
+
*/
|
|
595
|
+
handleAuthSuccess(message) {
|
|
596
|
+
// Wait for connected message to complete authentication
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Handles connected message after successful authentication.
|
|
601
|
+
*
|
|
602
|
+
* @param {Object} message - Connected message.
|
|
603
|
+
* @returns {void}
|
|
604
|
+
* @private
|
|
605
|
+
*/
|
|
606
|
+
handleConnected(message) {
|
|
607
|
+
// Clear auth timeout
|
|
608
|
+
if (this.authTimeoutId) {
|
|
609
|
+
clearTimeout(this.authTimeoutId);
|
|
610
|
+
this.authTimeoutId = null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Mark as authenticated
|
|
614
|
+
this.isAuthenticated = true;
|
|
615
|
+
|
|
616
|
+
// Resolve authentication promise
|
|
617
|
+
if (this.authPromise) {
|
|
618
|
+
this.authPromise.resolve();
|
|
619
|
+
this.authPromise = null;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Handles authentication errors.
|
|
625
|
+
*
|
|
626
|
+
* @param {Object} message - Error message.
|
|
627
|
+
* @returns {void}
|
|
628
|
+
* @private
|
|
629
|
+
*/
|
|
630
|
+
handleAuthError(message) {
|
|
631
|
+
this.log.error('Authentication error:', message);
|
|
632
|
+
|
|
633
|
+
// Clear auth timeout
|
|
634
|
+
if (this.authTimeoutId) {
|
|
635
|
+
clearTimeout(this.authTimeoutId);
|
|
636
|
+
this.authTimeoutId = null;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Reject authentication promise
|
|
640
|
+
if (this.authPromise) {
|
|
641
|
+
const error = new Error(message.error || message.message || 'Authentication failed');
|
|
642
|
+
this.authPromise.reject(error);
|
|
643
|
+
this.authPromise = null;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Handles WebSocket disconnection and manages reconnection.
|
|
649
|
+
*
|
|
650
|
+
* @param {CloseEvent} event - WebSocket close event.
|
|
651
|
+
* @returns {void}
|
|
652
|
+
* @private
|
|
653
|
+
*/
|
|
654
|
+
handleDisconnection(event) {
|
|
655
|
+
this.log.info('WebSocket disconnected:', event.code, event.reason);
|
|
656
|
+
|
|
657
|
+
// Reset authentication state on disconnection
|
|
658
|
+
this.isAuthenticated = false;
|
|
659
|
+
this.authPromise = null;
|
|
660
|
+
if (this.authTimeoutId) {
|
|
661
|
+
clearTimeout(this.authTimeoutId);
|
|
662
|
+
this.authTimeoutId = null;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Don't reconnect if it was a deliberate disconnection
|
|
666
|
+
if (!this.shouldReconnect || event.code === 1000) {
|
|
667
|
+
this.setState(CONNECTION_STATES.DISCONNECTED);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Check if we've exceeded max attempts
|
|
672
|
+
if (this.backoff.isMaxAttemptsReached()) {
|
|
673
|
+
this.setState(CONNECTION_STATES.FAILED);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Start reconnection process
|
|
678
|
+
this.setState(CONNECTION_STATES.RECONNECTING);
|
|
679
|
+
this.scheduleReconnection();
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Schedules the next reconnection attempt with exponential backoff.
|
|
684
|
+
*
|
|
685
|
+
* @returns {void}
|
|
686
|
+
* @private
|
|
687
|
+
*/
|
|
688
|
+
scheduleReconnection() {
|
|
689
|
+
if (this.reconnectTimeoutId) {
|
|
690
|
+
clearTimeout(this.reconnectTimeoutId);
|
|
691
|
+
}
|
|
692
|
+
const delay = this.backoff.getDelay();
|
|
693
|
+
this.reconnectTimeoutId = setTimeout(async () => {
|
|
694
|
+
if (this.shouldReconnect && this.state === CONNECTION_STATES.RECONNECTING) {
|
|
695
|
+
try {
|
|
696
|
+
await this.attemptConnection();
|
|
697
|
+
} catch (error) {
|
|
698
|
+
this.log.error('Reconnection attempt failed:', error);
|
|
699
|
+
// Schedule next attempt if we haven't exceeded max attempts
|
|
700
|
+
if (!this.backoff.isMaxAttemptsReached()) {
|
|
701
|
+
this.scheduleReconnection();
|
|
702
|
+
} else {
|
|
703
|
+
this.setState(CONNECTION_STATES.FAILED);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}, delay);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Sets the connection state and emits connectivity events.
|
|
712
|
+
*
|
|
713
|
+
* @param {string} newState - New connection state.
|
|
714
|
+
* @returns {void}
|
|
715
|
+
* @private
|
|
716
|
+
*/
|
|
717
|
+
setState(newState) {
|
|
718
|
+
if (this.state !== newState) {
|
|
719
|
+
this.state = newState;
|
|
720
|
+
|
|
721
|
+
// Emit connectivity event
|
|
722
|
+
const event = new CustomEvent('connectivity', {
|
|
723
|
+
detail: {
|
|
724
|
+
status: newState,
|
|
725
|
+
timestamp: Date.now(),
|
|
726
|
+
clientId: this.clientId
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
this.connectivity.dispatchEvent(event);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Queues a message for sending when connection is re-established.
|
|
735
|
+
*
|
|
736
|
+
* @param {Object} message - Message to queue.
|
|
737
|
+
* @returns {void}
|
|
738
|
+
* @private
|
|
739
|
+
*/
|
|
740
|
+
queueMessage(message) {
|
|
741
|
+
if (this.messageQueue.length >= this.maxQueueSize) {
|
|
742
|
+
this.messageQueue.shift(); // Remove oldest message
|
|
743
|
+
}
|
|
744
|
+
this.messageQueue.push({
|
|
745
|
+
message,
|
|
746
|
+
timestamp: Date.now()
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Sends all queued messages after reconnection.
|
|
752
|
+
*
|
|
753
|
+
* @returns {void}
|
|
754
|
+
* @private
|
|
755
|
+
*/
|
|
756
|
+
flushMessageQueue() {
|
|
757
|
+
if (this.messageQueue.length === 0) {
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
const messages = [...this.messageQueue];
|
|
761
|
+
this.messageQueue = [];
|
|
762
|
+
messages.forEach(({
|
|
763
|
+
message
|
|
764
|
+
}) => {
|
|
765
|
+
this.send(message);
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Starts heartbeat monitoring to detect connection health.
|
|
771
|
+
*
|
|
772
|
+
* @deprecated
|
|
773
|
+
* @returns {void}
|
|
774
|
+
* @private
|
|
775
|
+
*/
|
|
776
|
+
startHeartbeat() {
|
|
777
|
+
this.stopHeartbeat();
|
|
778
|
+
this.lastPongTime = Date.now();
|
|
779
|
+
this.heartbeatInterval = setInterval(() => {
|
|
780
|
+
if (this.isConnected()) {
|
|
781
|
+
// Check if we've received a recent pong
|
|
782
|
+
const timeSinceLastPong = Date.now() - this.lastPongTime;
|
|
783
|
+
if (timeSinceLastPong > this.heartbeatIntervalMs * 2) {
|
|
784
|
+
this.log.warn('Heartbeat timeout detected, closing connection');
|
|
785
|
+
this.ws.close();
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Send heartbeat
|
|
790
|
+
this.send({
|
|
791
|
+
type: 'heartbeat',
|
|
792
|
+
timestamp: Date.now()
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
}, this.heartbeatIntervalMs);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Stops heartbeat monitoring.
|
|
800
|
+
*
|
|
801
|
+
* @returns {void}
|
|
802
|
+
* @private
|
|
803
|
+
*/
|
|
804
|
+
stopHeartbeat() {
|
|
805
|
+
if (this.heartbeatInterval) {
|
|
806
|
+
clearInterval(this.heartbeatInterval);
|
|
807
|
+
this.heartbeatInterval = null;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Generates a unique client ID.
|
|
813
|
+
*
|
|
814
|
+
* @returns {string} Unique client identifier.
|
|
815
|
+
* @private
|
|
816
|
+
*/
|
|
817
|
+
generateClientId() {
|
|
818
|
+
return `client_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Gets connection statistics for debugging.
|
|
823
|
+
*
|
|
824
|
+
* @returns {Object} Connection statistics.
|
|
825
|
+
*/
|
|
826
|
+
getStats() {
|
|
827
|
+
return {
|
|
828
|
+
state: this.state,
|
|
829
|
+
clientId: this.clientId,
|
|
830
|
+
isAuthenticated: this.isAuthenticated,
|
|
831
|
+
reconnectAttempts: this.backoff.getAttempts(),
|
|
832
|
+
queuedMessages: this.messageQueue.length,
|
|
833
|
+
lastPongTime: this.lastPongTime,
|
|
834
|
+
backoffStatus: this.backoff.getStatus()
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
handleReceiveHeartbeat() {
|
|
838
|
+
this.lastPingTime = Date.now();
|
|
839
|
+
this.ws.send(JSON.stringify({
|
|
840
|
+
type: 'heartbeat',
|
|
841
|
+
payload: {
|
|
842
|
+
type: 'response'
|
|
843
|
+
}
|
|
844
|
+
}));
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// import * as log from './utils/logx.js';
|
|
849
|
+
|
|
850
|
+
class Stream {
|
|
851
|
+
constructor({
|
|
852
|
+
channelName,
|
|
853
|
+
connection,
|
|
854
|
+
offset,
|
|
855
|
+
log
|
|
856
|
+
}) {
|
|
857
|
+
this.offset = offset;
|
|
858
|
+
this.state = '';
|
|
859
|
+
this.channelName = channelName;
|
|
860
|
+
this.connection = connection;
|
|
861
|
+
this.handler;
|
|
862
|
+
this.log = log;
|
|
863
|
+
}
|
|
864
|
+
start(handler) {
|
|
865
|
+
if (typeof handler !== 'function') {
|
|
866
|
+
throw new Error('Stream callback must be a function');
|
|
867
|
+
}
|
|
868
|
+
this.handler = handler;
|
|
869
|
+
const subscribeMessage = {
|
|
870
|
+
type: 'subscribe',
|
|
871
|
+
channel: this.channelName,
|
|
872
|
+
fromOffset: this.offset
|
|
873
|
+
};
|
|
874
|
+
this.connection.send(subscribeMessage);
|
|
875
|
+
this.state = 'streaming';
|
|
876
|
+
this.log.info('Streaming started');
|
|
877
|
+
}
|
|
878
|
+
stop() {
|
|
879
|
+
this.state = 'stopped';
|
|
880
|
+
}
|
|
881
|
+
getStatus() {
|
|
882
|
+
const wsConnectionState = this.connection.getState();
|
|
883
|
+
if (wsConnectionState !== 'connected') return wsConnectionState;
|
|
884
|
+
if (this.state) return this.state || wsConnectionState;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Channel class for pub/sub messaging with offset tracking and message replay.
|
|
890
|
+
* Provides stream subscription, publishing, and message history functionality.
|
|
891
|
+
*/
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Represents a pub/sub channel with real-time messaging capabilities.
|
|
895
|
+
*/
|
|
896
|
+
class Channel {
|
|
897
|
+
/**
|
|
898
|
+
* Creates a new Channel instance.
|
|
899
|
+
*
|
|
900
|
+
* @param {string} name - Channel name/topic.
|
|
901
|
+
* @param {Connection} connection - WebSocket connection manager.
|
|
902
|
+
*/
|
|
903
|
+
constructor(name, connection, log) {
|
|
904
|
+
if (!name || typeof name !== 'string') {
|
|
905
|
+
throw new Error('Channel name is required and must be a string');
|
|
906
|
+
}
|
|
907
|
+
this.name = name;
|
|
908
|
+
this.connection = connection;
|
|
909
|
+
this.log = log;
|
|
910
|
+
|
|
911
|
+
// Stream management
|
|
912
|
+
this.streamHandlers = new Set();
|
|
913
|
+
this.messageHandler = this.handleMessage.bind(this);
|
|
914
|
+
this.streams = new Map();
|
|
915
|
+
|
|
916
|
+
// Subscribe to connection messages
|
|
917
|
+
this.connection.addMessageHandler(this.messageHandler);
|
|
918
|
+
this.offset = 0;
|
|
919
|
+
this.localStorage;
|
|
920
|
+
if (globalThis.window && window.localStorage) {
|
|
921
|
+
this.localStorage = window.localStorage;
|
|
922
|
+
const storedOffset = this.localStorage.getItem(`${this.name}_offset`);
|
|
923
|
+
if (storedOffset) {
|
|
924
|
+
this.offset = Number(storedOffset);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
stream(callback) {
|
|
929
|
+
if (typeof callback !== 'function') {
|
|
930
|
+
throw new Error('Stream callback must be a function');
|
|
931
|
+
}
|
|
932
|
+
const previousSize = this.streams.size;
|
|
933
|
+
const stream = new Stream({
|
|
934
|
+
channelName: this.name,
|
|
935
|
+
connection: this.connection,
|
|
936
|
+
offset: this.offset,
|
|
937
|
+
log: this.log
|
|
938
|
+
});
|
|
939
|
+
this.streams.set(callback, stream);
|
|
940
|
+
if (this.streams.size > previousSize) {
|
|
941
|
+
this.log.info('Streaming next success!', this.streams.size, this.offset);
|
|
942
|
+
}
|
|
943
|
+
stream.start(callback);
|
|
944
|
+
return () => {
|
|
945
|
+
this.unstream(callback);
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
unstream(callback) {
|
|
949
|
+
const stream = this.streams.get(callback);
|
|
950
|
+
if (stream) {
|
|
951
|
+
const previousSize = this.streams.size;
|
|
952
|
+
stream.stop();
|
|
953
|
+
this.streams.delete(callback);
|
|
954
|
+
if (previousSize > this.streams.size) {
|
|
955
|
+
this.log.info('Unstreaming next success!', this.streams.size);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
if (this.streams.size === 0) {
|
|
959
|
+
this.log.info('No streams left, closing the channel.');
|
|
960
|
+
|
|
961
|
+
// sends unsubscribe message to the ws connection but this will not close the ws connection.
|
|
962
|
+
// the ws will just stop receiving message for this channel/topic.
|
|
963
|
+
// channel will be automatically reopened when new stream is created.
|
|
964
|
+
this.close();
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Publishes a message to the channel via WebSocket.
|
|
970
|
+
*
|
|
971
|
+
* @param {*} message - Message payload to publish.
|
|
972
|
+
* @returns {Promise<void>} Promise resolving when message is published.
|
|
973
|
+
*/
|
|
974
|
+
async publish(message) {
|
|
975
|
+
if (!this.connection.isConnected()) {
|
|
976
|
+
throw new Error('Connection is not established');
|
|
977
|
+
}
|
|
978
|
+
return new Promise((resolve, reject) => {
|
|
979
|
+
const publishMessage = {
|
|
980
|
+
type: 'publish',
|
|
981
|
+
channel: this.name,
|
|
982
|
+
message,
|
|
983
|
+
timestamp: Date.now()
|
|
984
|
+
};
|
|
985
|
+
try {
|
|
986
|
+
this.connection.send(publishMessage);
|
|
987
|
+
|
|
988
|
+
// Note: We don't wait for confirmation in this implementation
|
|
989
|
+
// In production, you might want to wait for a 'published' response
|
|
990
|
+
resolve();
|
|
991
|
+
} catch (error) {
|
|
992
|
+
reject(error);
|
|
993
|
+
}
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Alias for publish() method for backward compatibility.
|
|
999
|
+
*
|
|
1000
|
+
* @param {*} message - Message payload to publish.
|
|
1001
|
+
* @returns {Promise<void>} Promise resolving when message is published.
|
|
1002
|
+
*/
|
|
1003
|
+
async send(message) {
|
|
1004
|
+
return this.publish(message);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Unsubscribes from the channel and stops receiving messages.
|
|
1009
|
+
*
|
|
1010
|
+
* @returns {void}
|
|
1011
|
+
*/
|
|
1012
|
+
close() {
|
|
1013
|
+
// unsubscribe to the ws connection
|
|
1014
|
+
const unsubscribeMessage = {
|
|
1015
|
+
type: 'unsubscribe',
|
|
1016
|
+
channel: this.name,
|
|
1017
|
+
timestamp: Date.now()
|
|
1018
|
+
};
|
|
1019
|
+
this.connection.send(unsubscribeMessage);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Handles incoming WebSocket messages for this channel.
|
|
1024
|
+
*
|
|
1025
|
+
* @param {Object} message - WebSocket message object.
|
|
1026
|
+
* @returns {void}
|
|
1027
|
+
* @private
|
|
1028
|
+
*/
|
|
1029
|
+
handleMessage(message) {
|
|
1030
|
+
// if no streams are active, do not process the message
|
|
1031
|
+
if (this.streams.size === 0) {
|
|
1032
|
+
this.log.info('No streams active, skipping message:', message);
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// Handle subscription confirmation
|
|
1037
|
+
if (message.type === 'subscribed' && message.channel === this.name) {
|
|
1038
|
+
this.log.info(`Subscribed to channel: ${this.name}`);
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Handle unsubscription confirmation
|
|
1043
|
+
if (message.type === 'unsubscribed' && message.channel === this.name) {
|
|
1044
|
+
this.log.info(`Unsubscribed from channel: ${this.name}`);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Handle incoming messages - check if message has channel field or assume it's for this channel
|
|
1049
|
+
if (message.type === 'message') {
|
|
1050
|
+
if (!message.channel || message.channel === this.name) {
|
|
1051
|
+
// Add channel to message if missing
|
|
1052
|
+
if (!message.channel) {
|
|
1053
|
+
message.channel = this.name;
|
|
1054
|
+
}
|
|
1055
|
+
this.processIncomingMessage(message);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Handle publish confirmation
|
|
1061
|
+
if (message.type === 'published' && message.channel === this.name) {
|
|
1062
|
+
this.log.info('Message published to channel');
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* Processes an incoming message and forwards it to stream handlers.
|
|
1068
|
+
*
|
|
1069
|
+
* @param {Object} message - Incoming message object.
|
|
1070
|
+
* @returns {void}
|
|
1071
|
+
* @private
|
|
1072
|
+
*/
|
|
1073
|
+
processIncomingMessage(message) {
|
|
1074
|
+
// Create metadata object for the callback
|
|
1075
|
+
const metadata = {
|
|
1076
|
+
offset: message.offset,
|
|
1077
|
+
timestamp: message.timestamp,
|
|
1078
|
+
replay: message.replay || false,
|
|
1079
|
+
channel: message.channel
|
|
1080
|
+
};
|
|
1081
|
+
if (message.offset) {
|
|
1082
|
+
if (message.offset > this.offset) {
|
|
1083
|
+
this.setOffset(message.offset);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// Forward to all stream handlers (process all messages)
|
|
1088
|
+
this.streams.forEach(stream => {
|
|
1089
|
+
try {
|
|
1090
|
+
stream.handler(message.data, metadata);
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
this.log.error('Stream handler error:', error);
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Re-subscribes to the channel after reconnection.
|
|
1099
|
+
* Called automatically when connection is re-established.
|
|
1100
|
+
*
|
|
1101
|
+
* @returns {void}
|
|
1102
|
+
*/
|
|
1103
|
+
resubscribe() {
|
|
1104
|
+
if (this.streams.size > 0) ;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Gets channel statistics for debugging.
|
|
1109
|
+
*
|
|
1110
|
+
* @returns {Object} Channel statistics and state information.
|
|
1111
|
+
*/
|
|
1112
|
+
getStats() {
|
|
1113
|
+
return {
|
|
1114
|
+
name: this.name,
|
|
1115
|
+
streamHandlers: this.streamHandlers.size,
|
|
1116
|
+
connectionState: this.connection.getState()
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
setOffset(offset) {
|
|
1120
|
+
this.offset = offset;
|
|
1121
|
+
if (this.localStorage) {
|
|
1122
|
+
this.localStorage.setItem(`${this.name}_offset`, this.offset);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
class Log {
|
|
1128
|
+
constructor(config) {
|
|
1129
|
+
this.verbose = config.verbose;
|
|
1130
|
+
}
|
|
1131
|
+
info(...args) {
|
|
1132
|
+
if (this.verbose) {
|
|
1133
|
+
console.log(...args);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
error(...args) {
|
|
1137
|
+
if (this.verbose) {
|
|
1138
|
+
console.error(...args);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
warn(...args) {
|
|
1142
|
+
if (this.verbose) {
|
|
1143
|
+
console.warn(...args);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
debug(...args) {
|
|
1147
|
+
if (this.verbose) {
|
|
1148
|
+
console.debug(...args);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* Main Package class for the realtime pub/sub SDK.
|
|
1155
|
+
* Provides WebSocket connectivity, channel management, and real-time messaging.
|
|
1156
|
+
*/
|
|
1157
|
+
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* Main SDK class that provides pub/sub functionality with automatic reconnection.
|
|
1161
|
+
*/
|
|
1162
|
+
class Package {
|
|
1163
|
+
/**
|
|
1164
|
+
* @param {Object} config - Client configuration.
|
|
1165
|
+
* @param {string} config.apiKey - API key for authentication.
|
|
1166
|
+
*/
|
|
1167
|
+
constructor(config = {}) {
|
|
1168
|
+
if (!config.apiKey) {
|
|
1169
|
+
throw new Error('API key is required');
|
|
1170
|
+
}
|
|
1171
|
+
this.config = config;
|
|
1172
|
+
this.config.endpoint = config.host || 'wss://connect.aptly.cloud';
|
|
1173
|
+
this.config.verbose = config.verbose || false;
|
|
1174
|
+
this.log = new Log(config);
|
|
1175
|
+
|
|
1176
|
+
// Create connectivity event target for status monitoring (internal)
|
|
1177
|
+
this._connectivity = new EventTarget();
|
|
1178
|
+
|
|
1179
|
+
// Initialize connection manager
|
|
1180
|
+
this.connection = new Connection(config, this._connectivity, this.log);
|
|
1181
|
+
this._connectivityHandlers = new Set();
|
|
1182
|
+
|
|
1183
|
+
// Channel management
|
|
1184
|
+
this.channels = new Map();
|
|
1185
|
+
|
|
1186
|
+
// Set up connection event handlers
|
|
1187
|
+
this.setupConnectionHandlers();
|
|
1188
|
+
|
|
1189
|
+
// Create realtime interface
|
|
1190
|
+
this.realtime = {
|
|
1191
|
+
channel: this.createChannel.bind(this)
|
|
1192
|
+
};
|
|
1193
|
+
this.log.info('Package initialized');
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* Establishes connection to the WebSocket server.
|
|
1198
|
+
*
|
|
1199
|
+
* @returns {Promise<boolean>} Promise resolving to connection success status.
|
|
1200
|
+
*/
|
|
1201
|
+
async connect() {
|
|
1202
|
+
try {
|
|
1203
|
+
this.log.info('Connecting to server');
|
|
1204
|
+
const connected = await this.connection.connect();
|
|
1205
|
+
this.log.info('Connected to server', connected);
|
|
1206
|
+
if (connected) {
|
|
1207
|
+
// Re-subscribe to existing channels after reconnection
|
|
1208
|
+
this.resubscribeChannels();
|
|
1209
|
+
}
|
|
1210
|
+
return connected;
|
|
1211
|
+
} catch (error) {
|
|
1212
|
+
this.log.error('Failed to connect:', error);
|
|
1213
|
+
throw error;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* Disconnects from the WebSocket server and cleans up resources.
|
|
1219
|
+
*
|
|
1220
|
+
* @returns {void}
|
|
1221
|
+
*/
|
|
1222
|
+
disconnect() {
|
|
1223
|
+
// Close all channels
|
|
1224
|
+
this.channels.forEach(channel => {
|
|
1225
|
+
channel.close();
|
|
1226
|
+
});
|
|
1227
|
+
this.channels.clear();
|
|
1228
|
+
|
|
1229
|
+
// Disconnect the connection
|
|
1230
|
+
this.connection.disconnect();
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Creates or retrieves a channel instance for the given channel name.
|
|
1235
|
+
*
|
|
1236
|
+
* @param {string} name - Channel name/topic.
|
|
1237
|
+
* @returns {Channel} Channel instance for the specified name.
|
|
1238
|
+
*/
|
|
1239
|
+
createChannel(name) {
|
|
1240
|
+
if (!name || typeof name !== 'string') {
|
|
1241
|
+
throw new Error('Channel name is required and must be a string');
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// Return existing channel if already created
|
|
1245
|
+
if (this.channels.has(name)) {
|
|
1246
|
+
return this.channels.get(name);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// Create new channel instance
|
|
1250
|
+
const channel = new Channel(name, this.connection, this.log);
|
|
1251
|
+
this.channels.set(name, channel);
|
|
1252
|
+
return channel;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* Gets the current connection status.
|
|
1257
|
+
*
|
|
1258
|
+
* @returns {string} Current connection status.
|
|
1259
|
+
*/
|
|
1260
|
+
getConnectionStatus() {
|
|
1261
|
+
return this.connection.getState();
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* Checks if the connection is authenticated.
|
|
1266
|
+
*
|
|
1267
|
+
* @returns {boolean} True if authenticated.
|
|
1268
|
+
*/
|
|
1269
|
+
isAuthenticated() {
|
|
1270
|
+
return this.connection.isAuthenticated;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* Gets the client ID for this connection.
|
|
1275
|
+
*
|
|
1276
|
+
* @returns {string} Unique client identifier.
|
|
1277
|
+
*/
|
|
1278
|
+
getClientId() {
|
|
1279
|
+
return this.connection.getClientId();
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/**
|
|
1283
|
+
* Gets all active channel names.
|
|
1284
|
+
*
|
|
1285
|
+
* @returns {Array<string>} Array of active channel names.
|
|
1286
|
+
*/
|
|
1287
|
+
getActiveChannels() {
|
|
1288
|
+
return Array.from(this.channels.keys());
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* Gets statistics for all channels and the connection.
|
|
1293
|
+
*
|
|
1294
|
+
* @returns {Object} Statistics object with connection and channel information.
|
|
1295
|
+
*/
|
|
1296
|
+
getStats() {
|
|
1297
|
+
const channelStats = {};
|
|
1298
|
+
this.channels.forEach((channel, name) => {
|
|
1299
|
+
channelStats[name] = channel.getStats();
|
|
1300
|
+
});
|
|
1301
|
+
return {
|
|
1302
|
+
connection: this.connection.getStats(),
|
|
1303
|
+
channels: channelStats,
|
|
1304
|
+
activeChannels: this.getActiveChannels().length
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/**
|
|
1309
|
+
* Publishes a message to a channel via HTTP API (fallback method).
|
|
1310
|
+
*
|
|
1311
|
+
* @param {string} channel - Channel name to publish to.
|
|
1312
|
+
* @param {*} message - Message payload to publish.
|
|
1313
|
+
* @returns {Promise<Object>} Promise resolving to publish response.
|
|
1314
|
+
*/
|
|
1315
|
+
async publishHttp(channel, message) {
|
|
1316
|
+
if (!channel || !message) {
|
|
1317
|
+
throw new Error('Channel and message are required');
|
|
1318
|
+
}
|
|
1319
|
+
const httpEndpoint = this.config.endpoint.replace('ws://', 'http://').replace('wss://', 'https://');
|
|
1320
|
+
const url = new URL('/realtime', httpEndpoint);
|
|
1321
|
+
const response = await fetch(url.toString(), {
|
|
1322
|
+
method: 'POST',
|
|
1323
|
+
headers: {
|
|
1324
|
+
'Content-Type': 'application/json',
|
|
1325
|
+
'x-api-key': this.config.apiKey
|
|
1326
|
+
},
|
|
1327
|
+
body: JSON.stringify({
|
|
1328
|
+
channel,
|
|
1329
|
+
message
|
|
1330
|
+
})
|
|
1331
|
+
});
|
|
1332
|
+
if (!response.ok) {
|
|
1333
|
+
throw new Error(`HTTP publish failed: ${response.status} ${response.statusText}`);
|
|
1334
|
+
}
|
|
1335
|
+
return response.json();
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
/**
|
|
1339
|
+
* Checks if the client is currently connected to the server.
|
|
1340
|
+
*
|
|
1341
|
+
* @returns {boolean} True if connected.
|
|
1342
|
+
*/
|
|
1343
|
+
isConnected() {
|
|
1344
|
+
return this.connection.isConnected();
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
/**
|
|
1348
|
+
* Sets up connection event handlers for automatic channel management.
|
|
1349
|
+
*
|
|
1350
|
+
* @returns {void}
|
|
1351
|
+
* @private
|
|
1352
|
+
*/
|
|
1353
|
+
setupConnectionHandlers() {
|
|
1354
|
+
// Listen for connection state changes
|
|
1355
|
+
const handler = event => {
|
|
1356
|
+
const {
|
|
1357
|
+
status
|
|
1358
|
+
} = event.detail;
|
|
1359
|
+
|
|
1360
|
+
// Re-subscribe channels when connection is re-established
|
|
1361
|
+
// todo add conidtion only when previously connected
|
|
1362
|
+
if (status === 'connected') {
|
|
1363
|
+
this.resubscribeChannels();
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
this._connectivity.addEventListener('connectivity', handler);
|
|
1367
|
+
this._connectivityHandlers.add(handler);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/**
|
|
1371
|
+
* Re-subscribes all active channels after reconnection.
|
|
1372
|
+
*
|
|
1373
|
+
* @returns {void}
|
|
1374
|
+
* @private
|
|
1375
|
+
*/
|
|
1376
|
+
resubscribeChannels() {
|
|
1377
|
+
this.channels.forEach(channel => {
|
|
1378
|
+
if (channel.streams && channel.streams.size > 0) {
|
|
1379
|
+
channel.resubscribe();
|
|
1380
|
+
}
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
onConnectivityChange(callback) {
|
|
1384
|
+
if (typeof callback !== 'function') {
|
|
1385
|
+
throw new Error('Connectivity callback must be a function');
|
|
1386
|
+
}
|
|
1387
|
+
function handler(event) {
|
|
1388
|
+
callback(event.detail.status);
|
|
1389
|
+
}
|
|
1390
|
+
this._connectivity.addEventListener('connectivity', handler);
|
|
1391
|
+
this.log.info('connectivity listener added');
|
|
1392
|
+
this._connectivityHandlers.add(handler);
|
|
1393
|
+
return () => {
|
|
1394
|
+
this._connectivity.removeEventListener('connectivity', handler);
|
|
1395
|
+
this.log.info('connectivity listener removed');
|
|
1396
|
+
this._connectivityHandlers.delete(handler);
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/**
|
|
1401
|
+
* Removes all event listeners and cleans up resources.
|
|
1402
|
+
* Should be called when the Package instance is no longer needed.
|
|
1403
|
+
*
|
|
1404
|
+
* @returns {void}
|
|
1405
|
+
*/
|
|
1406
|
+
cleanup() {
|
|
1407
|
+
this.disconnect();
|
|
1408
|
+
this._connectivityHandlers.forEach(handler => {
|
|
1409
|
+
this._connectivity.removeEventListener('connectivity', handler);
|
|
1410
|
+
});
|
|
1411
|
+
this._connectivityHandlers.clear();
|
|
1412
|
+
|
|
1413
|
+
// Remove all connectivity listeners
|
|
1414
|
+
// Note: EventTarget doesn't have a removeAllListeners method,
|
|
1415
|
+
// so we rely on garbage collection when the instance is destroyed
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// Attach the connectivity subscription method to match the API pattern
|
|
1420
|
+
Object.defineProperty(Package.prototype, 'connectivity', {
|
|
1421
|
+
get() {
|
|
1422
|
+
return {
|
|
1423
|
+
subscribe: this.subscribeToConnectivity.bind(this)
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
/**
|
|
1429
|
+
* BrookProvider component that provides Brook client context to React components.
|
|
1430
|
+
* Manages the client instance and connection state across the component tree.
|
|
1431
|
+
*/
|
|
1432
|
+
|
|
1433
|
+
const BrookContext = /*#__PURE__*/createContext(null);
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* BrookProvider component that wraps the application with Brook client context.
|
|
1437
|
+
*
|
|
1438
|
+
* @param {Object} props - Component props.
|
|
1439
|
+
* @param {Object} props.config - Brook client instance.
|
|
1440
|
+
* @param {React.ReactNode} props.children - Child components to wrap.
|
|
1441
|
+
* @returns {React.ReactElement} Provider component.
|
|
1442
|
+
*/
|
|
1443
|
+
function BrookProvider$1({
|
|
1444
|
+
config,
|
|
1445
|
+
children
|
|
1446
|
+
}) {
|
|
1447
|
+
if (!config) {
|
|
1448
|
+
throw new Error('BrookProvider requires a config prop with Brook client instance');
|
|
1449
|
+
}
|
|
1450
|
+
useEffect(() => {
|
|
1451
|
+
config.connect();
|
|
1452
|
+
}, [config]);
|
|
1453
|
+
const contextValue = {
|
|
1454
|
+
client: config
|
|
1455
|
+
};
|
|
1456
|
+
return /*#__PURE__*/React.createElement(BrookContext.Provider, {
|
|
1457
|
+
value: contextValue
|
|
1458
|
+
}, children);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
/**
|
|
1462
|
+
* Hook to access the Brook context.
|
|
1463
|
+
*
|
|
1464
|
+
* @returns {Object} Brook context containing client and connection state.
|
|
1465
|
+
* @throws {Error} If used outside of BrookProvider.
|
|
1466
|
+
*/
|
|
1467
|
+
const useBrookContext = () => {
|
|
1468
|
+
const context = useContext(BrookContext);
|
|
1469
|
+
if (!context) {
|
|
1470
|
+
throw new Error('Hook must be used within a BrookProvider');
|
|
1471
|
+
}
|
|
1472
|
+
return context;
|
|
1473
|
+
};
|
|
1474
|
+
|
|
1475
|
+
function useConnection() {
|
|
1476
|
+
const ctx = useBrookContext();
|
|
1477
|
+
const [status, setStatus] = useState('disconnected');
|
|
1478
|
+
useEffect(() => {
|
|
1479
|
+
const client = ctx.client;
|
|
1480
|
+
if (client) {
|
|
1481
|
+
setStatus(client.getConnectionStatus());
|
|
1482
|
+
}
|
|
1483
|
+
if (client?.onConnectivityChange) {
|
|
1484
|
+
const unsubscribe = client.onConnectivityChange(status => {
|
|
1485
|
+
setStatus(status);
|
|
1486
|
+
});
|
|
1487
|
+
return () => unsubscribe?.();
|
|
1488
|
+
}
|
|
1489
|
+
}, [ctx]);
|
|
1490
|
+
return {
|
|
1491
|
+
status
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
/**
|
|
1496
|
+
* Timer node for linked list implementation.
|
|
1497
|
+
* Represents a single timeout operation in the queue.
|
|
1498
|
+
*/
|
|
1499
|
+
class TimerNode {
|
|
1500
|
+
/**
|
|
1501
|
+
* Creates a new timer node.
|
|
1502
|
+
*
|
|
1503
|
+
* @param {Function} callback - Function to execute when timer fires.
|
|
1504
|
+
* @param {number} delay - Delay in milliseconds before execution.
|
|
1505
|
+
*/
|
|
1506
|
+
constructor(callback, delay) {
|
|
1507
|
+
this.callback = callback;
|
|
1508
|
+
this.delay = delay;
|
|
1509
|
+
this.next = null;
|
|
1510
|
+
this.timeoutId = null;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
/**
|
|
1514
|
+
* Sets the next node in the queue.
|
|
1515
|
+
*
|
|
1516
|
+
* @param {TimerNode} next - The next timer node.
|
|
1517
|
+
*/
|
|
1518
|
+
setNext(next) {
|
|
1519
|
+
this.next = next;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
/**
|
|
1523
|
+
* Executes this timer node and schedules the next one.
|
|
1524
|
+
*
|
|
1525
|
+
* @param {Function} onComplete - Callback when this node completes.
|
|
1526
|
+
*/
|
|
1527
|
+
execute(onComplete) {
|
|
1528
|
+
this.timeoutId = setTimeout(() => {
|
|
1529
|
+
try {
|
|
1530
|
+
this.callback?.();
|
|
1531
|
+
} catch (error) {
|
|
1532
|
+
console.error('Timer callback error:', error);
|
|
1533
|
+
}
|
|
1534
|
+
onComplete(this.next);
|
|
1535
|
+
}, this.delay);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
/**
|
|
1539
|
+
* Cancels the timeout if it hasn't executed yet.
|
|
1540
|
+
*/
|
|
1541
|
+
cancel() {
|
|
1542
|
+
if (this.timeoutId) {
|
|
1543
|
+
clearTimeout(this.timeoutId);
|
|
1544
|
+
this.timeoutId = null;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/**
|
|
1550
|
+
* Sequential timer implementation using a linked list.
|
|
1551
|
+
* Executes timeouts one after another in the order they were added.
|
|
1552
|
+
*/
|
|
1553
|
+
class Timer {
|
|
1554
|
+
/**
|
|
1555
|
+
* Creates a new Timer instance.
|
|
1556
|
+
*/
|
|
1557
|
+
constructor() {
|
|
1558
|
+
this.head = null;
|
|
1559
|
+
this.tail = null;
|
|
1560
|
+
this.isExecuting = false;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
/**
|
|
1564
|
+
* Adds a timeout to the execution queue.
|
|
1565
|
+
*
|
|
1566
|
+
* @param {Function} callback - Function to execute when timer fires.
|
|
1567
|
+
* @param {number} delay - Delay in milliseconds before execution.
|
|
1568
|
+
* @returns {TimerNode} The created timer node for potential cancellation.
|
|
1569
|
+
*/
|
|
1570
|
+
setTimeout(callback, delay) {
|
|
1571
|
+
const node = new TimerNode(callback, delay);
|
|
1572
|
+
|
|
1573
|
+
// Add to queue
|
|
1574
|
+
if (!this.head) {
|
|
1575
|
+
this.head = node;
|
|
1576
|
+
this.tail = node;
|
|
1577
|
+
this._startExecution();
|
|
1578
|
+
} else {
|
|
1579
|
+
this.tail.setNext(node);
|
|
1580
|
+
this.tail = node;
|
|
1581
|
+
}
|
|
1582
|
+
return node;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
/**
|
|
1586
|
+
* Starts executing the timer queue.
|
|
1587
|
+
*
|
|
1588
|
+
* @private
|
|
1589
|
+
*/
|
|
1590
|
+
_startExecution() {
|
|
1591
|
+
if (this.isExecuting || !this.head) return;
|
|
1592
|
+
this.isExecuting = true;
|
|
1593
|
+
this._executeNext(this.head);
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
/**
|
|
1597
|
+
* Executes the next timer in the queue.
|
|
1598
|
+
*
|
|
1599
|
+
* @param {TimerNode} currentNode - The current node to execute.
|
|
1600
|
+
* @private
|
|
1601
|
+
*/
|
|
1602
|
+
_executeNext(currentNode) {
|
|
1603
|
+
if (!currentNode) {
|
|
1604
|
+
this._onQueueComplete();
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
currentNode.execute(nextNode => {
|
|
1608
|
+
this.head = nextNode;
|
|
1609
|
+
if (!nextNode) {
|
|
1610
|
+
this.tail = null;
|
|
1611
|
+
}
|
|
1612
|
+
this._executeNext(nextNode);
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
/**
|
|
1617
|
+
* Called when the timer queue is complete.
|
|
1618
|
+
*
|
|
1619
|
+
* @private
|
|
1620
|
+
*/
|
|
1621
|
+
_onQueueComplete() {
|
|
1622
|
+
this.isExecuting = false;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
/**
|
|
1626
|
+
* Clears all pending timers in the queue.
|
|
1627
|
+
*/
|
|
1628
|
+
clear() {
|
|
1629
|
+
let current = this.head;
|
|
1630
|
+
while (current) {
|
|
1631
|
+
current.cancel();
|
|
1632
|
+
current = current.next;
|
|
1633
|
+
}
|
|
1634
|
+
this.head = null;
|
|
1635
|
+
this.tail = null;
|
|
1636
|
+
this.isExecuting = false;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
/**
|
|
1640
|
+
* Checks if the timer queue is currently executing.
|
|
1641
|
+
*
|
|
1642
|
+
* @returns {boolean} True if executing, false otherwise.
|
|
1643
|
+
*/
|
|
1644
|
+
isRunning() {
|
|
1645
|
+
return this.isExecuting;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
/**
|
|
1649
|
+
* Gets the number of pending timers in the queue.
|
|
1650
|
+
*
|
|
1651
|
+
* @returns {number} Number of pending timers.
|
|
1652
|
+
*/
|
|
1653
|
+
getPendingCount() {
|
|
1654
|
+
let count = 0;
|
|
1655
|
+
let current = this.head;
|
|
1656
|
+
while (current) {
|
|
1657
|
+
count++;
|
|
1658
|
+
current = current.next;
|
|
1659
|
+
}
|
|
1660
|
+
return count;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
function useStream(topic, callback) {
|
|
1665
|
+
const ctx = useBrookContext();
|
|
1666
|
+
const unsubscribeRef = useRef(null);
|
|
1667
|
+
const topicRef = useRef(topic);
|
|
1668
|
+
const {
|
|
1669
|
+
status
|
|
1670
|
+
} = useConnection();
|
|
1671
|
+
const [streaming, setStreaming] = useState(false);
|
|
1672
|
+
function subscribe() {
|
|
1673
|
+
ctx?.client?.log?.info('Subscribing to topic:', topic);
|
|
1674
|
+
if (topicRef.current !== topic) {
|
|
1675
|
+
// unsubscribe from the previous topic
|
|
1676
|
+
unsubscribe();
|
|
1677
|
+
}
|
|
1678
|
+
if (unsubscribeRef.current) {
|
|
1679
|
+
ctx?.client?.log?.info('Already subscribed to channel. Aborting action.');
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
if (!topic) {
|
|
1683
|
+
ctx?.client?.log?.warn('No topic to subscribe to.');
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
const channel = ctx.client.realtime.channel(topic);
|
|
1687
|
+
ctx?.client?.log?.info(`Subscribing to channel: ${topic} from useStream hook`);
|
|
1688
|
+
const timer = new Timer();
|
|
1689
|
+
unsubscribeRef.current = channel.stream((message, metadata) => {
|
|
1690
|
+
timer.setTimeout(() => {
|
|
1691
|
+
callback(message, metadata);
|
|
1692
|
+
}, 50);
|
|
1693
|
+
});
|
|
1694
|
+
setStreaming(true);
|
|
1695
|
+
}
|
|
1696
|
+
function unsubscribe() {
|
|
1697
|
+
if (unsubscribeRef.current) {
|
|
1698
|
+
ctx?.client?.log?.info('Unsubscribing.');
|
|
1699
|
+
unsubscribeRef.current();
|
|
1700
|
+
unsubscribeRef.current = null;
|
|
1701
|
+
ctx?.client?.log?.info('Unsubscribed.');
|
|
1702
|
+
setStreaming(false);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
useEffect(() => {
|
|
1706
|
+
// do nothing
|
|
1707
|
+
if (!topic) return;
|
|
1708
|
+
|
|
1709
|
+
// otherwise, automatically subscribe
|
|
1710
|
+
if (status === 'connected') {
|
|
1711
|
+
subscribe();
|
|
1712
|
+
}
|
|
1713
|
+
}, [topic, status]);
|
|
1714
|
+
useEffect(() => unsubscribe, []);
|
|
1715
|
+
return {
|
|
1716
|
+
streaming,
|
|
1717
|
+
unsubscribe,
|
|
1718
|
+
subscribe
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
|
|
47
1722
|
var jsxRuntime = {exports: {}};
|
|
48
1723
|
|
|
49
1724
|
var reactJsxRuntime_production = {};
|
|
@@ -380,7 +2055,7 @@ function requireReactJsxRuntime_development () {
|
|
|
380
2055
|
object.$$typeof === REACT_ELEMENT_TYPE
|
|
381
2056
|
);
|
|
382
2057
|
}
|
|
383
|
-
var React =
|
|
2058
|
+
var React$1 = React,
|
|
384
2059
|
REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"),
|
|
385
2060
|
REACT_PORTAL_TYPE = Symbol.for("react.portal"),
|
|
386
2061
|
REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"),
|
|
@@ -396,7 +2071,7 @@ function requireReactJsxRuntime_development () {
|
|
|
396
2071
|
REACT_ACTIVITY_TYPE = Symbol.for("react.activity"),
|
|
397
2072
|
REACT_CLIENT_REFERENCE = Symbol.for("react.client.reference"),
|
|
398
2073
|
ReactSharedInternals =
|
|
399
|
-
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
|
|
2074
|
+
React$1.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
|
|
400
2075
|
hasOwnProperty = Object.prototype.hasOwnProperty,
|
|
401
2076
|
isArrayImpl = Array.isArray,
|
|
402
2077
|
createTask = console.createTask
|
|
@@ -404,15 +2079,15 @@ function requireReactJsxRuntime_development () {
|
|
|
404
2079
|
: function () {
|
|
405
2080
|
return null;
|
|
406
2081
|
};
|
|
407
|
-
React = {
|
|
2082
|
+
React$1 = {
|
|
408
2083
|
react_stack_bottom_frame: function (callStackForError) {
|
|
409
2084
|
return callStackForError();
|
|
410
2085
|
}
|
|
411
2086
|
};
|
|
412
2087
|
var specialPropKeyWarningShown;
|
|
413
2088
|
var didWarnAboutElementRef = {};
|
|
414
|
-
var unknownOwnerDebugStack = React.react_stack_bottom_frame.bind(
|
|
415
|
-
React,
|
|
2089
|
+
var unknownOwnerDebugStack = React$1.react_stack_bottom_frame.bind(
|
|
2090
|
+
React$1,
|
|
416
2091
|
UnknownOwner
|
|
417
2092
|
)();
|
|
418
2093
|
var unknownOwnerDebugTask = createTask(getTaskName(UnknownOwner));
|
|
@@ -473,11 +2148,11 @@ const BrookProvider = ({
|
|
|
473
2148
|
const config = {
|
|
474
2149
|
apiKey: apiKey || "apiKey"
|
|
475
2150
|
};
|
|
476
|
-
const [client, setClient] = useState(new
|
|
2151
|
+
const [client, setClient] = useState(new Package(config));
|
|
477
2152
|
useEffect(() => {
|
|
478
2153
|
client.cleanup();
|
|
479
2154
|
if (apiKey) {
|
|
480
|
-
const brook = new
|
|
2155
|
+
const brook = new Package(config);
|
|
481
2156
|
setClient(brook);
|
|
482
2157
|
return () => {
|
|
483
2158
|
brook.cleanup();
|