@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.
Files changed (2) hide show
  1. package/dist/index.js +1685 -10
  2. package/package.json +4 -1
package/dist/index.js CHANGED
@@ -1,7 +1,5 @@
1
1
  import './global.css';
2
- import require$$0, { useState, useEffect, createContext, useRef, useContext, useCallback, useMemo, Activity } from 'react';
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 = require$$0,
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 Brook(config));
2151
+ const [client, setClient] = useState(new Package(config));
477
2152
  useEffect(() => {
478
2153
  client.cleanup();
479
2154
  if (apiKey) {
480
- const brook = new Brook(config);
2155
+ const brook = new Package(config);
481
2156
  setClient(brook);
482
2157
  return () => {
483
2158
  brook.cleanup();