@pushflodev/sdk 1.0.1

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/react.cjs ADDED
@@ -0,0 +1,1300 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ /* @pushflo/sdk - https://pushflo.dev */
7
+
8
+
9
+ // src/utils/EventEmitter.ts
10
+ var TypedEventEmitter = class {
11
+ listeners = /* @__PURE__ */ new Map();
12
+ /**
13
+ * Register an event listener
14
+ */
15
+ on(event, handler) {
16
+ if (!this.listeners.has(event)) {
17
+ this.listeners.set(event, /* @__PURE__ */ new Set());
18
+ }
19
+ this.listeners.get(event).add(handler);
20
+ return this;
21
+ }
22
+ /**
23
+ * Register a one-time event listener
24
+ */
25
+ once(event, handler) {
26
+ const onceHandler = (...args) => {
27
+ this.off(event, onceHandler);
28
+ handler(...args);
29
+ };
30
+ return this.on(event, onceHandler);
31
+ }
32
+ /**
33
+ * Remove an event listener
34
+ */
35
+ off(event, handler) {
36
+ const handlers = this.listeners.get(event);
37
+ if (handlers) {
38
+ handlers.delete(handler);
39
+ if (handlers.size === 0) {
40
+ this.listeners.delete(event);
41
+ }
42
+ }
43
+ return this;
44
+ }
45
+ /**
46
+ * Emit an event to all registered listeners
47
+ */
48
+ emit(event, ...args) {
49
+ const handlers = this.listeners.get(event);
50
+ if (!handlers || handlers.size === 0) {
51
+ return false;
52
+ }
53
+ handlers.forEach((handler) => {
54
+ try {
55
+ handler(...args);
56
+ } catch (error) {
57
+ console.error(`Error in event handler for "${String(event)}":`, error);
58
+ }
59
+ });
60
+ return true;
61
+ }
62
+ /**
63
+ * Remove all listeners for an event, or all listeners if no event specified
64
+ */
65
+ removeAllListeners(event) {
66
+ if (event !== void 0) {
67
+ this.listeners.delete(event);
68
+ } else {
69
+ this.listeners.clear();
70
+ }
71
+ return this;
72
+ }
73
+ /**
74
+ * Get the number of listeners for an event
75
+ */
76
+ listenerCount(event) {
77
+ return this.listeners.get(event)?.size ?? 0;
78
+ }
79
+ /**
80
+ * Get all event names with registered listeners
81
+ */
82
+ eventNames() {
83
+ return Array.from(this.listeners.keys());
84
+ }
85
+ };
86
+
87
+ // src/utils/constants.ts
88
+ var DEFAULTS = {
89
+ /** Default PushFlo API base URL */
90
+ BASE_URL: "https://api.pushflo.dev",
91
+ /** WebSocket endpoint path */
92
+ WS_PATH: "/ws",
93
+ /** Connection timeout in milliseconds */
94
+ CONNECTION_TIMEOUT: 3e4,
95
+ /** Heartbeat interval in milliseconds */
96
+ HEARTBEAT_INTERVAL: 25e3,
97
+ /** Initial reconnection delay in milliseconds */
98
+ RECONNECT_DELAY: 1e3,
99
+ /** Maximum reconnection delay in milliseconds */
100
+ MAX_RECONNECT_DELAY: 3e4,
101
+ /** Reconnection delay multiplier for exponential backoff */
102
+ RECONNECT_MULTIPLIER: 1.5,
103
+ /** Maximum number of reconnection attempts (0 = infinite) */
104
+ MAX_RECONNECT_ATTEMPTS: 0};
105
+ var WS_CLIENT_MESSAGES = {
106
+ SUBSCRIBE: "subscribe",
107
+ UNSUBSCRIBE: "unsubscribe",
108
+ PING: "ping"};
109
+ var WS_SERVER_MESSAGES = {
110
+ CONNECTED: "connected",
111
+ SUBSCRIBED: "subscribed",
112
+ UNSUBSCRIBED: "unsubscribed",
113
+ MESSAGE: "message",
114
+ ERROR: "error",
115
+ PONG: "pong"
116
+ };
117
+ var ERROR_CODES = {
118
+ // Connection errors
119
+ CONNECTION_FAILED: "CONNECTION_FAILED",
120
+ CONNECTION_TIMEOUT: "CONNECTION_TIMEOUT",
121
+ CONNECTION_CLOSED: "CONNECTION_CLOSED",
122
+ // Authentication errors
123
+ INVALID_API_KEY: "INVALID_API_KEY",
124
+ UNAUTHORIZED: "UNAUTHORIZED",
125
+ FORBIDDEN: "FORBIDDEN",
126
+ SERVER_ERROR: "SERVER_ERROR"};
127
+
128
+ // src/utils/logger.ts
129
+ var LOG_LEVELS = {
130
+ debug: 0,
131
+ info: 1,
132
+ warn: 2,
133
+ error: 3
134
+ };
135
+ var Logger = class {
136
+ enabled;
137
+ prefix;
138
+ minLevel;
139
+ constructor(options = {}) {
140
+ this.enabled = options.debug ?? false;
141
+ this.prefix = options.prefix ?? "[PushFlo]";
142
+ this.minLevel = LOG_LEVELS[options.level ?? "debug"];
143
+ }
144
+ /**
145
+ * Enable or disable logging
146
+ */
147
+ setEnabled(enabled) {
148
+ this.enabled = enabled;
149
+ }
150
+ /**
151
+ * Set minimum log level
152
+ */
153
+ setLevel(level) {
154
+ this.minLevel = LOG_LEVELS[level];
155
+ }
156
+ /**
157
+ * Log a debug message
158
+ */
159
+ debug(message, ...args) {
160
+ this.log("debug", message, ...args);
161
+ }
162
+ /**
163
+ * Log an info message
164
+ */
165
+ info(message, ...args) {
166
+ this.log("info", message, ...args);
167
+ }
168
+ /**
169
+ * Log a warning message
170
+ */
171
+ warn(message, ...args) {
172
+ this.log("warn", message, ...args);
173
+ }
174
+ /**
175
+ * Log an error message
176
+ */
177
+ error(message, ...args) {
178
+ this.log("error", message, ...args);
179
+ }
180
+ log(level, message, ...args) {
181
+ if (!this.enabled && level !== "error") {
182
+ return;
183
+ }
184
+ if (LOG_LEVELS[level] < this.minLevel) {
185
+ return;
186
+ }
187
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
188
+ const formattedMessage = `${this.prefix} ${timestamp} [${level.toUpperCase()}] ${message}`;
189
+ switch (level) {
190
+ case "debug":
191
+ console.debug(formattedMessage, ...args);
192
+ break;
193
+ case "info":
194
+ console.info(formattedMessage, ...args);
195
+ break;
196
+ case "warn":
197
+ console.warn(formattedMessage, ...args);
198
+ break;
199
+ case "error":
200
+ console.error(formattedMessage, ...args);
201
+ break;
202
+ }
203
+ }
204
+ };
205
+ function createLogger(options = {}) {
206
+ return new Logger(options);
207
+ }
208
+
209
+ // src/errors/PushFloError.ts
210
+ var PushFloError = class extends Error {
211
+ /** Error code for programmatic handling */
212
+ code;
213
+ /** Whether this error is potentially recoverable through retry */
214
+ retryable;
215
+ /** Original error that caused this error, if any */
216
+ cause;
217
+ constructor(message, code, options = {}) {
218
+ super(message);
219
+ this.name = "PushFloError";
220
+ this.code = code;
221
+ this.retryable = options.retryable ?? false;
222
+ this.cause = options.cause;
223
+ if (Error.captureStackTrace) {
224
+ Error.captureStackTrace(this, this.constructor);
225
+ }
226
+ }
227
+ /**
228
+ * Convert error to JSON-serializable object
229
+ */
230
+ toJSON() {
231
+ return {
232
+ name: this.name,
233
+ message: this.message,
234
+ code: this.code,
235
+ retryable: this.retryable,
236
+ stack: this.stack,
237
+ cause: this.cause?.message
238
+ };
239
+ }
240
+ /**
241
+ * Create a string representation
242
+ */
243
+ toString() {
244
+ return `${this.name} [${this.code}]: ${this.message}`;
245
+ }
246
+ };
247
+
248
+ // src/errors/AuthenticationError.ts
249
+ var AuthenticationError = class _AuthenticationError extends PushFloError {
250
+ constructor(message, code = ERROR_CODES.UNAUTHORIZED, options = {}) {
251
+ super(message, code, { retryable: false, ...options });
252
+ this.name = "AuthenticationError";
253
+ }
254
+ /**
255
+ * Create an invalid API key error
256
+ */
257
+ static invalidKey(keyType) {
258
+ const message = keyType ? `Invalid ${keyType} API key` : "Invalid API key";
259
+ return new _AuthenticationError(message, ERROR_CODES.INVALID_API_KEY);
260
+ }
261
+ /**
262
+ * Create an unauthorized error
263
+ */
264
+ static unauthorized(reason) {
265
+ return new _AuthenticationError(
266
+ reason ?? "Unauthorized - check your API key",
267
+ ERROR_CODES.UNAUTHORIZED
268
+ );
269
+ }
270
+ /**
271
+ * Create a forbidden error
272
+ */
273
+ static forbidden(action) {
274
+ const message = action ? `Access forbidden: insufficient permissions for ${action}` : "Access forbidden: insufficient permissions";
275
+ return new _AuthenticationError(message, ERROR_CODES.FORBIDDEN);
276
+ }
277
+ };
278
+
279
+ // src/utils/retry.ts
280
+ function calculateBackoff(attempt, options = {}) {
281
+ const {
282
+ initialDelay = DEFAULTS.RECONNECT_DELAY,
283
+ maxDelay = DEFAULTS.MAX_RECONNECT_DELAY,
284
+ multiplier = DEFAULTS.RECONNECT_MULTIPLIER
285
+ } = options;
286
+ const exponentialDelay = initialDelay * Math.pow(multiplier, attempt);
287
+ const cappedDelay = Math.min(exponentialDelay, maxDelay);
288
+ const jitter = cappedDelay * 0.25 * (Math.random() * 2 - 1);
289
+ return Math.floor(cappedDelay + jitter);
290
+ }
291
+
292
+ // src/errors/ConnectionError.ts
293
+ var ConnectionError = class _ConnectionError extends PushFloError {
294
+ constructor(message, code = ERROR_CODES.CONNECTION_FAILED, options = {}) {
295
+ super(message, code, { retryable: true, ...options });
296
+ this.name = "ConnectionError";
297
+ }
298
+ /**
299
+ * Create a connection timeout error
300
+ */
301
+ static timeout(timeoutMs) {
302
+ return new _ConnectionError(
303
+ `Connection timed out after ${timeoutMs}ms`,
304
+ ERROR_CODES.CONNECTION_TIMEOUT,
305
+ { retryable: true }
306
+ );
307
+ }
308
+ /**
309
+ * Create a connection closed error
310
+ */
311
+ static closed(reason) {
312
+ return new _ConnectionError(
313
+ reason ? `Connection closed: ${reason}` : "Connection closed unexpectedly",
314
+ ERROR_CODES.CONNECTION_CLOSED,
315
+ { retryable: true }
316
+ );
317
+ }
318
+ /**
319
+ * Create a connection failed error
320
+ */
321
+ static failed(reason, cause) {
322
+ return new _ConnectionError(
323
+ reason ? `Connection failed: ${reason}` : "Failed to connect",
324
+ ERROR_CODES.CONNECTION_FAILED,
325
+ { retryable: true, cause }
326
+ );
327
+ }
328
+ };
329
+
330
+ // src/client/ConnectionStateMachine.ts
331
+ var VALID_TRANSITIONS = [
332
+ { from: "disconnected", to: "connecting" },
333
+ { from: "connecting", to: "connected" },
334
+ { from: "connecting", to: "disconnected" },
335
+ { from: "connecting", to: "error" },
336
+ { from: "connected", to: "disconnected" },
337
+ { from: "connected", to: "error" },
338
+ { from: "error", to: "connecting" },
339
+ { from: "error", to: "disconnected" }
340
+ ];
341
+ var ConnectionStateMachine = class {
342
+ _state = "disconnected";
343
+ listeners = /* @__PURE__ */ new Set();
344
+ /**
345
+ * Get current connection state
346
+ */
347
+ get state() {
348
+ return this._state;
349
+ }
350
+ /**
351
+ * Check if currently in a specific state
352
+ */
353
+ is(state) {
354
+ return this._state === state;
355
+ }
356
+ /**
357
+ * Check if connected
358
+ */
359
+ get isConnected() {
360
+ return this._state === "connected";
361
+ }
362
+ /**
363
+ * Check if connecting
364
+ */
365
+ get isConnecting() {
366
+ return this._state === "connecting";
367
+ }
368
+ /**
369
+ * Check if disconnected
370
+ */
371
+ get isDisconnected() {
372
+ return this._state === "disconnected";
373
+ }
374
+ /**
375
+ * Check if in error state
376
+ */
377
+ get isError() {
378
+ return this._state === "error";
379
+ }
380
+ /**
381
+ * Transition to a new state
382
+ * @returns true if transition was successful
383
+ */
384
+ transition(to) {
385
+ if (this._state === to) {
386
+ return true;
387
+ }
388
+ const isValid = VALID_TRANSITIONS.some((t) => {
389
+ const fromStates = Array.isArray(t.from) ? t.from : [t.from];
390
+ return fromStates.includes(this._state) && t.to === to;
391
+ });
392
+ if (!isValid) {
393
+ console.warn(`Invalid state transition: ${this._state} -> ${to}`);
394
+ return false;
395
+ }
396
+ this._state = to;
397
+ this.listeners.forEach((listener) => {
398
+ try {
399
+ listener(to);
400
+ } catch (error) {
401
+ console.error("Error in connection state listener:", error);
402
+ }
403
+ });
404
+ return true;
405
+ }
406
+ /**
407
+ * Force transition to a state (bypasses validation)
408
+ */
409
+ forceTransition(to) {
410
+ if (this._state === to) {
411
+ return;
412
+ }
413
+ this._state = to;
414
+ this.listeners.forEach((listener) => {
415
+ try {
416
+ listener(to);
417
+ } catch (error) {
418
+ console.error("Error in connection state listener:", error);
419
+ }
420
+ });
421
+ }
422
+ /**
423
+ * Reset to disconnected state
424
+ */
425
+ reset() {
426
+ this.forceTransition("disconnected");
427
+ }
428
+ /**
429
+ * Subscribe to state changes
430
+ */
431
+ onChange(listener) {
432
+ this.listeners.add(listener);
433
+ return () => this.listeners.delete(listener);
434
+ }
435
+ /**
436
+ * Remove all listeners
437
+ */
438
+ removeAllListeners() {
439
+ this.listeners.clear();
440
+ }
441
+ };
442
+
443
+ // src/client/Heartbeat.ts
444
+ var Heartbeat = class {
445
+ intervalId = null;
446
+ timeoutId = null;
447
+ interval;
448
+ pongTimeout;
449
+ onPing;
450
+ onTimeout;
451
+ running = false;
452
+ constructor(options) {
453
+ this.interval = options.interval ?? DEFAULTS.HEARTBEAT_INTERVAL;
454
+ this.pongTimeout = options.pongTimeout ?? this.interval * 2;
455
+ this.onPing = options.onPing;
456
+ this.onTimeout = options.onTimeout;
457
+ }
458
+ /**
459
+ * Start the heartbeat
460
+ */
461
+ start() {
462
+ if (this.running) {
463
+ return;
464
+ }
465
+ this.running = true;
466
+ this.scheduleNextPing();
467
+ }
468
+ /**
469
+ * Stop the heartbeat
470
+ */
471
+ stop() {
472
+ this.running = false;
473
+ this.clearTimers();
474
+ }
475
+ /**
476
+ * Called when a pong is received
477
+ */
478
+ receivedPong() {
479
+ if (!this.running) {
480
+ return;
481
+ }
482
+ if (this.timeoutId !== null) {
483
+ clearTimeout(this.timeoutId);
484
+ this.timeoutId = null;
485
+ }
486
+ this.scheduleNextPing();
487
+ }
488
+ /**
489
+ * Reset the heartbeat (e.g., after any activity)
490
+ */
491
+ reset() {
492
+ if (!this.running) {
493
+ return;
494
+ }
495
+ this.clearTimers();
496
+ this.scheduleNextPing();
497
+ }
498
+ scheduleNextPing() {
499
+ if (!this.running) {
500
+ return;
501
+ }
502
+ this.intervalId = setTimeout(() => {
503
+ if (!this.running) {
504
+ return;
505
+ }
506
+ this.onPing();
507
+ this.timeoutId = setTimeout(() => {
508
+ if (this.running && this.onTimeout) {
509
+ this.onTimeout();
510
+ }
511
+ }, this.pongTimeout);
512
+ }, this.interval);
513
+ }
514
+ clearTimers() {
515
+ if (this.intervalId !== null) {
516
+ clearTimeout(this.intervalId);
517
+ this.intervalId = null;
518
+ }
519
+ if (this.timeoutId !== null) {
520
+ clearTimeout(this.timeoutId);
521
+ this.timeoutId = null;
522
+ }
523
+ }
524
+ };
525
+
526
+ // src/client/WebSocketManager.ts
527
+ var WebSocketManager = class extends TypedEventEmitter {
528
+ options;
529
+ logger;
530
+ stateMachine;
531
+ heartbeat;
532
+ ws = null;
533
+ connectionTimeoutId = null;
534
+ reconnectAttempt = 0;
535
+ reconnectTimeoutId = null;
536
+ intentionalDisconnect = false;
537
+ clientId = null;
538
+ constructor(options) {
539
+ super();
540
+ this.options = {
541
+ apiKey: options.apiKey,
542
+ baseUrl: (options.baseUrl ?? DEFAULTS.BASE_URL).replace(/\/$/, ""),
543
+ connectionTimeout: options.connectionTimeout ?? DEFAULTS.CONNECTION_TIMEOUT,
544
+ heartbeatInterval: options.heartbeatInterval ?? DEFAULTS.HEARTBEAT_INTERVAL,
545
+ autoReconnect: options.autoReconnect ?? true,
546
+ maxReconnectAttempts: options.maxReconnectAttempts ?? DEFAULTS.MAX_RECONNECT_ATTEMPTS,
547
+ reconnectDelay: options.reconnectDelay ?? DEFAULTS.RECONNECT_DELAY,
548
+ maxReconnectDelay: options.maxReconnectDelay ?? DEFAULTS.MAX_RECONNECT_DELAY
549
+ };
550
+ this.logger = createLogger({ debug: options.debug, prefix: "[PushFlo WS]" });
551
+ this.stateMachine = new ConnectionStateMachine();
552
+ this.heartbeat = new Heartbeat({
553
+ interval: this.options.heartbeatInterval,
554
+ onPing: () => this.sendPing(),
555
+ onTimeout: () => this.handleHeartbeatTimeout()
556
+ });
557
+ }
558
+ /**
559
+ * Get current connection state
560
+ */
561
+ get state() {
562
+ return this.stateMachine.state;
563
+ }
564
+ /**
565
+ * Get client ID (available after connected)
566
+ */
567
+ getClientId() {
568
+ return this.clientId;
569
+ }
570
+ /**
571
+ * Subscribe to state changes
572
+ */
573
+ onStateChange(listener) {
574
+ return this.stateMachine.onChange(listener);
575
+ }
576
+ /**
577
+ * Connect to WebSocket server
578
+ */
579
+ async connect() {
580
+ if (this.stateMachine.isConnected) {
581
+ throw new ConnectionError(
582
+ "Already connected",
583
+ "ALREADY_CONNECTED",
584
+ { retryable: false }
585
+ );
586
+ }
587
+ if (this.stateMachine.isConnecting) {
588
+ throw new ConnectionError(
589
+ "Connection in progress",
590
+ "CONNECTION_IN_PROGRESS",
591
+ { retryable: false }
592
+ );
593
+ }
594
+ this.intentionalDisconnect = false;
595
+ this.clearReconnectTimeout();
596
+ return this.establishConnection();
597
+ }
598
+ /**
599
+ * Disconnect from WebSocket server
600
+ */
601
+ disconnect() {
602
+ this.intentionalDisconnect = true;
603
+ this.cleanup();
604
+ this.stateMachine.transition("disconnected");
605
+ this.emit("disconnected", "Disconnected by client");
606
+ }
607
+ /**
608
+ * Send a message to the server
609
+ */
610
+ send(message) {
611
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
612
+ return false;
613
+ }
614
+ try {
615
+ this.ws.send(JSON.stringify(message));
616
+ this.logger.debug("Sent message:", message);
617
+ return true;
618
+ } catch (error) {
619
+ this.logger.error("Failed to send message:", error);
620
+ return false;
621
+ }
622
+ }
623
+ /**
624
+ * Subscribe to a channel
625
+ */
626
+ subscribe(channel) {
627
+ return this.send({
628
+ type: WS_CLIENT_MESSAGES.SUBSCRIBE,
629
+ channel
630
+ });
631
+ }
632
+ /**
633
+ * Unsubscribe from a channel
634
+ */
635
+ unsubscribe(channel) {
636
+ return this.send({
637
+ type: WS_CLIENT_MESSAGES.UNSUBSCRIBE,
638
+ channel
639
+ });
640
+ }
641
+ /**
642
+ * Clean up resources
643
+ */
644
+ destroy() {
645
+ this.intentionalDisconnect = true;
646
+ this.cleanup();
647
+ this.removeAllListeners();
648
+ this.stateMachine.removeAllListeners();
649
+ }
650
+ async establishConnection() {
651
+ this.stateMachine.transition("connecting");
652
+ this.logger.debug("Connecting...");
653
+ return new Promise((resolve, reject) => {
654
+ try {
655
+ const wsUrl = this.buildWsUrl();
656
+ this.logger.debug("WebSocket URL:", wsUrl);
657
+ this.ws = new WebSocket(wsUrl);
658
+ this.connectionTimeoutId = setTimeout(() => {
659
+ if (this.stateMachine.isConnecting) {
660
+ const error = ConnectionError.timeout(this.options.connectionTimeout);
661
+ this.cleanup();
662
+ this.stateMachine.transition("error");
663
+ this.emit("error", error);
664
+ reject(error);
665
+ }
666
+ }, this.options.connectionTimeout);
667
+ this.ws.onopen = () => {
668
+ this.logger.debug("WebSocket opened, waiting for connected message...");
669
+ };
670
+ this.ws.onclose = (event) => {
671
+ this.handleClose(event, reject);
672
+ };
673
+ this.ws.onerror = (event) => {
674
+ this.logger.error("WebSocket error:", event);
675
+ };
676
+ this.ws.onmessage = (event) => {
677
+ this.handleMessage(event, resolve, reject);
678
+ };
679
+ } catch (error) {
680
+ this.cleanup();
681
+ this.stateMachine.transition("error");
682
+ const connError = ConnectionError.failed(
683
+ error instanceof Error ? error.message : "Unknown error",
684
+ error instanceof Error ? error : void 0
685
+ );
686
+ this.emit("error", connError);
687
+ reject(connError);
688
+ }
689
+ });
690
+ }
691
+ buildWsUrl() {
692
+ const baseUrl = this.options.baseUrl;
693
+ const protocol = baseUrl.startsWith("https") ? "wss" : "ws";
694
+ const host = baseUrl.replace(/^https?:\/\//, "");
695
+ return `${protocol}://${host}${DEFAULTS.WS_PATH}?token=${encodeURIComponent(this.options.apiKey)}`;
696
+ }
697
+ handleMessage(event, onConnect, onConnectError) {
698
+ try {
699
+ const message = JSON.parse(event.data);
700
+ this.logger.debug("Received message:", message);
701
+ switch (message.type) {
702
+ case WS_SERVER_MESSAGES.CONNECTED:
703
+ this.handleConnected(message, onConnect);
704
+ break;
705
+ case WS_SERVER_MESSAGES.PONG:
706
+ this.heartbeat.receivedPong();
707
+ break;
708
+ case WS_SERVER_MESSAGES.ERROR:
709
+ this.handleErrorMessage(message, onConnectError);
710
+ break;
711
+ default:
712
+ this.emit("message", message);
713
+ }
714
+ } catch (error) {
715
+ this.logger.error("Failed to parse message:", error);
716
+ }
717
+ }
718
+ handleConnected(message, onConnect) {
719
+ this.clearConnectionTimeout();
720
+ if (message.clientId) {
721
+ this.clientId = message.clientId;
722
+ }
723
+ const connectionInfo = {
724
+ clientId: message.clientId ?? "",
725
+ timestamp: message.timestamp ?? Date.now()
726
+ };
727
+ this.reconnectAttempt = 0;
728
+ this.stateMachine.transition("connected");
729
+ this.heartbeat.start();
730
+ this.logger.debug("Connected:", connectionInfo);
731
+ this.emit("connected", connectionInfo);
732
+ onConnect?.(connectionInfo);
733
+ }
734
+ handleErrorMessage(message, onConnectError) {
735
+ const errorMsg = message.error ?? "Unknown error";
736
+ const code = message.code;
737
+ let error;
738
+ if (code === "UNAUTHORIZED" || code === "INVALID_TOKEN") {
739
+ error = AuthenticationError.unauthorized(errorMsg);
740
+ } else {
741
+ error = new ConnectionError(errorMsg, code ?? "UNKNOWN");
742
+ }
743
+ this.emit("error", error);
744
+ if (this.stateMachine.isConnecting) {
745
+ this.cleanup();
746
+ this.stateMachine.transition("error");
747
+ onConnectError?.(error);
748
+ }
749
+ }
750
+ handleClose(event, onConnectError) {
751
+ this.logger.debug("WebSocket closed:", event.code, event.reason);
752
+ const wasConnected = this.stateMachine.isConnected;
753
+ this.cleanup();
754
+ if (this.intentionalDisconnect) {
755
+ this.stateMachine.transition("disconnected");
756
+ return;
757
+ }
758
+ if (this.stateMachine.isConnecting) {
759
+ this.stateMachine.transition("error");
760
+ const error = ConnectionError.failed(event.reason || "Connection closed");
761
+ this.emit("error", error);
762
+ onConnectError?.(error);
763
+ return;
764
+ }
765
+ this.stateMachine.transition("disconnected");
766
+ this.emit("disconnected", event.reason || void 0);
767
+ if (wasConnected && this.options.autoReconnect) {
768
+ this.scheduleReconnect();
769
+ }
770
+ }
771
+ handleHeartbeatTimeout() {
772
+ this.logger.warn("Heartbeat timeout, reconnecting...");
773
+ this.cleanup();
774
+ this.stateMachine.transition("disconnected");
775
+ this.emit("disconnected", "Heartbeat timeout");
776
+ if (this.options.autoReconnect) {
777
+ this.scheduleReconnect();
778
+ }
779
+ }
780
+ scheduleReconnect() {
781
+ if (this.intentionalDisconnect) {
782
+ return;
783
+ }
784
+ const { maxReconnectAttempts } = this.options;
785
+ if (maxReconnectAttempts > 0 && this.reconnectAttempt >= maxReconnectAttempts) {
786
+ this.logger.error("Max reconnect attempts reached");
787
+ this.stateMachine.transition("error");
788
+ this.emit("error", new ConnectionError(
789
+ "Max reconnection attempts exceeded",
790
+ "MAX_RECONNECT_ATTEMPTS",
791
+ { retryable: false }
792
+ ));
793
+ return;
794
+ }
795
+ const delay = calculateBackoff(this.reconnectAttempt, {
796
+ initialDelay: this.options.reconnectDelay,
797
+ maxDelay: this.options.maxReconnectDelay
798
+ });
799
+ this.logger.debug(`Scheduling reconnect in ${delay}ms (attempt ${this.reconnectAttempt + 1})`);
800
+ this.reconnectTimeoutId = setTimeout(() => {
801
+ this.reconnectAttempt++;
802
+ this.establishConnection().catch((error) => {
803
+ this.logger.error("Reconnect failed:", error);
804
+ this.scheduleReconnect();
805
+ });
806
+ }, delay);
807
+ }
808
+ sendPing() {
809
+ this.send({ type: WS_CLIENT_MESSAGES.PING });
810
+ }
811
+ cleanup() {
812
+ this.clearConnectionTimeout();
813
+ this.heartbeat.stop();
814
+ if (this.ws) {
815
+ this.ws.onopen = null;
816
+ this.ws.onclose = null;
817
+ this.ws.onerror = null;
818
+ this.ws.onmessage = null;
819
+ if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
820
+ this.ws.close();
821
+ }
822
+ this.ws = null;
823
+ }
824
+ }
825
+ clearConnectionTimeout() {
826
+ if (this.connectionTimeoutId !== null) {
827
+ clearTimeout(this.connectionTimeoutId);
828
+ this.connectionTimeoutId = null;
829
+ }
830
+ }
831
+ clearReconnectTimeout() {
832
+ if (this.reconnectTimeoutId !== null) {
833
+ clearTimeout(this.reconnectTimeoutId);
834
+ this.reconnectTimeoutId = null;
835
+ }
836
+ }
837
+ };
838
+
839
+ // src/client/SubscriptionManager.ts
840
+ var SubscriptionManager = class {
841
+ subscriptions = /* @__PURE__ */ new Map();
842
+ /**
843
+ * Add a subscription
844
+ */
845
+ add(channel, options = {}) {
846
+ this.subscriptions.set(channel, {
847
+ channel,
848
+ options,
849
+ confirmed: false
850
+ });
851
+ }
852
+ /**
853
+ * Remove a subscription
854
+ */
855
+ remove(channel) {
856
+ const entry = this.subscriptions.get(channel);
857
+ if (entry) {
858
+ this.subscriptions.delete(channel);
859
+ entry.options.onUnsubscribed?.();
860
+ return true;
861
+ }
862
+ return false;
863
+ }
864
+ /**
865
+ * Check if subscribed to a channel
866
+ */
867
+ has(channel) {
868
+ return this.subscriptions.has(channel);
869
+ }
870
+ /**
871
+ * Get subscription options for a channel
872
+ */
873
+ get(channel) {
874
+ return this.subscriptions.get(channel)?.options;
875
+ }
876
+ /**
877
+ * Mark subscription as confirmed
878
+ */
879
+ confirm(channel) {
880
+ const entry = this.subscriptions.get(channel);
881
+ if (entry) {
882
+ entry.confirmed = true;
883
+ entry.options.onSubscribed?.();
884
+ }
885
+ }
886
+ /**
887
+ * Check if subscription is confirmed
888
+ */
889
+ isConfirmed(channel) {
890
+ return this.subscriptions.get(channel)?.confirmed ?? false;
891
+ }
892
+ /**
893
+ * Handle incoming message
894
+ */
895
+ handleMessage(message) {
896
+ const entry = this.subscriptions.get(message.channel);
897
+ if (entry) {
898
+ entry.options.onMessage?.(message);
899
+ }
900
+ }
901
+ /**
902
+ * Handle subscription error
903
+ */
904
+ handleError(channel, error) {
905
+ const entry = this.subscriptions.get(channel);
906
+ if (entry) {
907
+ entry.options.onError?.(error);
908
+ }
909
+ }
910
+ /**
911
+ * Get all subscribed channel names
912
+ */
913
+ getChannels() {
914
+ return Array.from(this.subscriptions.keys());
915
+ }
916
+ /**
917
+ * Get count of subscriptions
918
+ */
919
+ get size() {
920
+ return this.subscriptions.size;
921
+ }
922
+ /**
923
+ * Clear all subscriptions
924
+ */
925
+ clear() {
926
+ this.subscriptions.forEach((entry) => {
927
+ entry.options.onUnsubscribed?.();
928
+ });
929
+ this.subscriptions.clear();
930
+ }
931
+ /**
932
+ * Reset confirmation status for all subscriptions (e.g., on reconnect)
933
+ */
934
+ resetConfirmations() {
935
+ this.subscriptions.forEach((entry) => {
936
+ entry.confirmed = false;
937
+ });
938
+ }
939
+ };
940
+
941
+ // src/client/PushFloClient.ts
942
+ var PushFloClient = class extends TypedEventEmitter {
943
+ wsManager;
944
+ subscriptions;
945
+ logger;
946
+ connectionChangeListeners = /* @__PURE__ */ new Set();
947
+ constructor(options) {
948
+ super();
949
+ if (!options.publishKey) {
950
+ throw new AuthenticationError(
951
+ "Publish key is required",
952
+ "MISSING_PUBLISH_KEY"
953
+ );
954
+ }
955
+ if (!options.publishKey.startsWith("pub_") && !options.publishKey.startsWith("sec_") && !options.publishKey.startsWith("mgmt_")) {
956
+ throw AuthenticationError.invalidKey("publish");
957
+ }
958
+ this.logger = createLogger({ debug: options.debug, prefix: "[PushFlo]" });
959
+ this.subscriptions = new SubscriptionManager();
960
+ this.wsManager = new WebSocketManager({
961
+ apiKey: options.publishKey,
962
+ baseUrl: options.baseUrl ?? DEFAULTS.BASE_URL,
963
+ connectionTimeout: options.connectionTimeout,
964
+ heartbeatInterval: options.heartbeatInterval,
965
+ autoReconnect: options.autoReconnect,
966
+ maxReconnectAttempts: options.maxReconnectAttempts,
967
+ reconnectDelay: options.reconnectDelay,
968
+ maxReconnectDelay: options.maxReconnectDelay,
969
+ debug: options.debug
970
+ });
971
+ this.setupEventHandlers();
972
+ if (options.autoConnect) {
973
+ this.connect().catch((error) => {
974
+ this.logger.error("Auto-connect failed:", error);
975
+ });
976
+ }
977
+ }
978
+ /**
979
+ * Get current connection state
980
+ */
981
+ get connectionState() {
982
+ return this.wsManager.state;
983
+ }
984
+ /**
985
+ * Get client ID (available after connected)
986
+ */
987
+ get clientId() {
988
+ return this.wsManager.getClientId();
989
+ }
990
+ /**
991
+ * Connect to PushFlo
992
+ */
993
+ async connect() {
994
+ this.logger.debug("Connecting...");
995
+ await this.wsManager.connect();
996
+ const channels = this.subscriptions.getChannels();
997
+ if (channels.length > 0) {
998
+ this.logger.debug("Re-subscribing to channels:", channels);
999
+ this.subscriptions.resetConfirmations();
1000
+ channels.forEach((channel) => {
1001
+ this.wsManager.subscribe(channel);
1002
+ });
1003
+ }
1004
+ }
1005
+ /**
1006
+ * Disconnect from PushFlo
1007
+ */
1008
+ disconnect() {
1009
+ this.logger.debug("Disconnecting...");
1010
+ this.wsManager.disconnect();
1011
+ }
1012
+ /**
1013
+ * Clean up all resources
1014
+ */
1015
+ destroy() {
1016
+ this.logger.debug("Destroying client...");
1017
+ this.subscriptions.clear();
1018
+ this.wsManager.destroy();
1019
+ this.connectionChangeListeners.clear();
1020
+ this.removeAllListeners();
1021
+ }
1022
+ /**
1023
+ * Subscribe to a channel
1024
+ */
1025
+ subscribe(channel, options = {}) {
1026
+ if (!channel) {
1027
+ throw new PushFloError("Channel is required", "INVALID_CHANNEL", { retryable: false });
1028
+ }
1029
+ this.logger.debug("Subscribing to channel:", channel);
1030
+ this.subscriptions.add(channel, options);
1031
+ if (this.wsManager.state === "connected") {
1032
+ this.wsManager.subscribe(channel);
1033
+ }
1034
+ return {
1035
+ channel,
1036
+ unsubscribe: () => this.unsubscribe(channel)
1037
+ };
1038
+ }
1039
+ /**
1040
+ * Unsubscribe from a channel
1041
+ */
1042
+ unsubscribe(channel) {
1043
+ this.logger.debug("Unsubscribing from channel:", channel);
1044
+ this.subscriptions.remove(channel);
1045
+ if (this.wsManager.state === "connected") {
1046
+ this.wsManager.unsubscribe(channel);
1047
+ }
1048
+ }
1049
+ /**
1050
+ * Register a connection state change listener
1051
+ */
1052
+ onConnectionChange(listener) {
1053
+ this.connectionChangeListeners.add(listener);
1054
+ return () => this.connectionChangeListeners.delete(listener);
1055
+ }
1056
+ /**
1057
+ * Get list of subscribed channels
1058
+ */
1059
+ getSubscribedChannels() {
1060
+ return this.subscriptions.getChannels();
1061
+ }
1062
+ /**
1063
+ * Check if subscribed to a channel
1064
+ */
1065
+ isSubscribed(channel) {
1066
+ return this.subscriptions.has(channel);
1067
+ }
1068
+ setupEventHandlers() {
1069
+ this.wsManager.onStateChange((state) => {
1070
+ this.connectionChangeListeners.forEach((listener) => {
1071
+ try {
1072
+ listener(state);
1073
+ } catch (error) {
1074
+ this.logger.error("Error in connection change listener:", error);
1075
+ }
1076
+ });
1077
+ });
1078
+ this.wsManager.on("connected", (info) => {
1079
+ this.emit("connected", info);
1080
+ });
1081
+ this.wsManager.on("disconnected", (reason) => {
1082
+ this.emit("disconnected", reason);
1083
+ });
1084
+ this.wsManager.on("error", (error) => {
1085
+ this.emit("error", error);
1086
+ });
1087
+ this.wsManager.on("message", (message) => {
1088
+ this.handleServerMessage(message);
1089
+ });
1090
+ }
1091
+ handleServerMessage(message) {
1092
+ switch (message.type) {
1093
+ case WS_SERVER_MESSAGES.SUBSCRIBED:
1094
+ if (message.channel) {
1095
+ this.subscriptions.confirm(message.channel);
1096
+ this.logger.debug("Subscribed to channel:", message.channel);
1097
+ }
1098
+ break;
1099
+ case WS_SERVER_MESSAGES.UNSUBSCRIBED:
1100
+ if (message.channel) {
1101
+ this.logger.debug("Unsubscribed from channel:", message.channel);
1102
+ }
1103
+ break;
1104
+ case WS_SERVER_MESSAGES.MESSAGE:
1105
+ if (message.channel && message.message) {
1106
+ const fullMessage = {
1107
+ id: message.message.id,
1108
+ channel: message.channel,
1109
+ eventType: message.message.eventType,
1110
+ clientId: message.message.clientId,
1111
+ content: message.message.content,
1112
+ timestamp: message.message.timestamp
1113
+ };
1114
+ this.subscriptions.handleMessage(fullMessage);
1115
+ this.emit("message", fullMessage);
1116
+ }
1117
+ break;
1118
+ case WS_SERVER_MESSAGES.ERROR:
1119
+ if (message.channel) {
1120
+ const error = new PushFloError(
1121
+ message.error ?? "Unknown error",
1122
+ message.code ?? ERROR_CODES.SERVER_ERROR
1123
+ );
1124
+ this.subscriptions.handleError(message.channel, error);
1125
+ }
1126
+ break;
1127
+ }
1128
+ }
1129
+ };
1130
+ var PushFloContext = react.createContext(null);
1131
+ function PushFloProvider({
1132
+ children,
1133
+ publishKey,
1134
+ baseUrl,
1135
+ debug,
1136
+ connectionTimeout,
1137
+ heartbeatInterval,
1138
+ autoReconnect,
1139
+ maxReconnectAttempts,
1140
+ reconnectDelay,
1141
+ maxReconnectDelay,
1142
+ autoConnect = true
1143
+ }) {
1144
+ const [connectionState, setConnectionState] = react.useState("disconnected");
1145
+ const clientRef = react.useRef(null);
1146
+ const mountedRef = react.useRef(true);
1147
+ react.useEffect(() => {
1148
+ mountedRef.current = true;
1149
+ const client = new PushFloClient({
1150
+ publishKey,
1151
+ baseUrl,
1152
+ debug,
1153
+ connectionTimeout,
1154
+ heartbeatInterval,
1155
+ autoReconnect,
1156
+ maxReconnectAttempts,
1157
+ reconnectDelay,
1158
+ maxReconnectDelay,
1159
+ autoConnect: false
1160
+ // We handle auto-connect ourselves
1161
+ });
1162
+ clientRef.current = client;
1163
+ const unsubscribe2 = client.onConnectionChange((state) => {
1164
+ if (mountedRef.current) {
1165
+ setConnectionState(state);
1166
+ }
1167
+ });
1168
+ if (autoConnect) {
1169
+ client.connect().catch((error) => {
1170
+ if (debug) {
1171
+ console.error("[PushFlo] Auto-connect failed:", error);
1172
+ }
1173
+ });
1174
+ }
1175
+ return () => {
1176
+ mountedRef.current = false;
1177
+ unsubscribe2();
1178
+ client.destroy();
1179
+ clientRef.current = null;
1180
+ };
1181
+ }, [
1182
+ publishKey,
1183
+ baseUrl,
1184
+ debug,
1185
+ connectionTimeout,
1186
+ heartbeatInterval,
1187
+ autoReconnect,
1188
+ maxReconnectAttempts,
1189
+ reconnectDelay,
1190
+ maxReconnectDelay,
1191
+ autoConnect
1192
+ ]);
1193
+ const connect = react.useCallback(async () => {
1194
+ if (clientRef.current) {
1195
+ await clientRef.current.connect();
1196
+ }
1197
+ }, []);
1198
+ const disconnect = react.useCallback(() => {
1199
+ if (clientRef.current) {
1200
+ clientRef.current.disconnect();
1201
+ }
1202
+ }, []);
1203
+ const subscribe = react.useCallback((channel, options) => {
1204
+ if (clientRef.current) {
1205
+ return clientRef.current.subscribe(channel, options);
1206
+ }
1207
+ return null;
1208
+ }, []);
1209
+ const unsubscribe = react.useCallback((channel) => {
1210
+ if (clientRef.current) {
1211
+ clientRef.current.unsubscribe(channel);
1212
+ }
1213
+ }, []);
1214
+ const value = {
1215
+ connectionState,
1216
+ client: clientRef.current,
1217
+ connect,
1218
+ disconnect,
1219
+ subscribe,
1220
+ unsubscribe
1221
+ };
1222
+ return /* @__PURE__ */ jsxRuntime.jsx(PushFloContext.Provider, { value, children });
1223
+ }
1224
+ function usePushFloContext() {
1225
+ const context = react.useContext(PushFloContext);
1226
+ if (!context) {
1227
+ throw new Error("usePushFloContext must be used within a PushFloProvider");
1228
+ }
1229
+ return context;
1230
+ }
1231
+
1232
+ // src/react/usePushFlo.ts
1233
+ function usePushFlo() {
1234
+ const { connectionState, connect, disconnect, subscribe, unsubscribe } = usePushFloContext();
1235
+ return {
1236
+ connectionState,
1237
+ isConnected: connectionState === "connected",
1238
+ isConnecting: connectionState === "connecting",
1239
+ connect,
1240
+ disconnect,
1241
+ subscribe,
1242
+ unsubscribe
1243
+ };
1244
+ }
1245
+ function useChannel(channel, options = {}) {
1246
+ const { connectionState, client } = usePushFloContext();
1247
+ const [messages, setMessages] = react.useState([]);
1248
+ const [isSubscribed, setIsSubscribed] = react.useState(false);
1249
+ const optionsRef = react.useRef(options);
1250
+ optionsRef.current = options;
1251
+ const maxMessages = options.maxMessages ?? 100;
1252
+ const handleMessage = react.useCallback((message) => {
1253
+ setMessages((prev) => {
1254
+ const newMessages = [...prev, message];
1255
+ if (newMessages.length > maxMessages) {
1256
+ return newMessages.slice(-maxMessages);
1257
+ }
1258
+ return newMessages;
1259
+ });
1260
+ optionsRef.current.onMessage?.(message);
1261
+ }, [maxMessages]);
1262
+ const handleError = react.useCallback((error) => {
1263
+ optionsRef.current.onError?.(error);
1264
+ }, []);
1265
+ react.useEffect(() => {
1266
+ if (!client || !channel) {
1267
+ return;
1268
+ }
1269
+ const subscription = client.subscribe(channel, {
1270
+ onMessage: handleMessage,
1271
+ onError: handleError,
1272
+ onSubscribed: () => setIsSubscribed(true),
1273
+ onUnsubscribed: () => setIsSubscribed(false)
1274
+ });
1275
+ if (!subscription) {
1276
+ return;
1277
+ }
1278
+ return () => {
1279
+ subscription.unsubscribe();
1280
+ setIsSubscribed(false);
1281
+ };
1282
+ }, [client, channel, handleMessage, handleError]);
1283
+ const clearMessages = react.useCallback(() => {
1284
+ setMessages([]);
1285
+ }, []);
1286
+ return {
1287
+ messages,
1288
+ lastMessage: messages.length > 0 ? messages[messages.length - 1] : null,
1289
+ connectionState,
1290
+ isSubscribed,
1291
+ clearMessages
1292
+ };
1293
+ }
1294
+
1295
+ exports.PushFloProvider = PushFloProvider;
1296
+ exports.useChannel = useChannel;
1297
+ exports.usePushFlo = usePushFlo;
1298
+ exports.usePushFloContext = usePushFloContext;
1299
+ //# sourceMappingURL=react.cjs.map
1300
+ //# sourceMappingURL=react.cjs.map