@supabase/realtime-js 2.12.2 → 2.14.0

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 (40) hide show
  1. package/dist/main/RealtimeChannel.d.ts.map +1 -1
  2. package/dist/main/RealtimeChannel.js +10 -1
  3. package/dist/main/RealtimeChannel.js.map +1 -1
  4. package/dist/main/RealtimeClient.d.ts +13 -2
  5. package/dist/main/RealtimeClient.d.ts.map +1 -1
  6. package/dist/main/RealtimeClient.js +311 -115
  7. package/dist/main/RealtimeClient.js.map +1 -1
  8. package/dist/main/index.d.ts +2 -1
  9. package/dist/main/index.d.ts.map +1 -1
  10. package/dist/main/index.js +3 -1
  11. package/dist/main/index.js.map +1 -1
  12. package/dist/main/lib/version.d.ts +1 -1
  13. package/dist/main/lib/version.js +1 -1
  14. package/dist/main/lib/websocket-factory.d.ts +41 -0
  15. package/dist/main/lib/websocket-factory.d.ts.map +1 -0
  16. package/dist/main/lib/websocket-factory.js +132 -0
  17. package/dist/main/lib/websocket-factory.js.map +1 -0
  18. package/dist/module/RealtimeChannel.d.ts.map +1 -1
  19. package/dist/module/RealtimeChannel.js +10 -1
  20. package/dist/module/RealtimeChannel.js.map +1 -1
  21. package/dist/module/RealtimeClient.d.ts +13 -2
  22. package/dist/module/RealtimeClient.d.ts.map +1 -1
  23. package/dist/module/RealtimeClient.js +312 -115
  24. package/dist/module/RealtimeClient.js.map +1 -1
  25. package/dist/module/index.d.ts +2 -1
  26. package/dist/module/index.d.ts.map +1 -1
  27. package/dist/module/index.js +2 -1
  28. package/dist/module/index.js.map +1 -1
  29. package/dist/module/lib/version.d.ts +1 -1
  30. package/dist/module/lib/version.js +1 -1
  31. package/dist/module/lib/websocket-factory.d.ts +41 -0
  32. package/dist/module/lib/websocket-factory.d.ts.map +1 -0
  33. package/dist/module/lib/websocket-factory.js +128 -0
  34. package/dist/module/lib/websocket-factory.js.map +1 -0
  35. package/package.json +3 -4
  36. package/src/RealtimeChannel.ts +12 -1
  37. package/src/RealtimeClient.ts +358 -132
  38. package/src/index.ts +3 -0
  39. package/src/lib/version.ts +1 -1
  40. package/src/lib/websocket-factory.ts +189 -0
@@ -1,4 +1,4 @@
1
- import { WebSocket } from 'isows'
1
+ import WebSocketFactory, { WebSocketLike } from './lib/websocket-factory'
2
2
 
3
3
  import {
4
4
  CHANNEL_EVENTS,
@@ -46,6 +46,22 @@ export type HeartbeatStatus =
46
46
 
47
47
  const noop = () => {}
48
48
 
49
+ type RealtimeClientState =
50
+ | 'connecting'
51
+ | 'connected'
52
+ | 'disconnecting'
53
+ | 'disconnected'
54
+
55
+ // Connection-related constants
56
+ const CONNECTION_TIMEOUTS = {
57
+ HEARTBEAT_INTERVAL: 25000,
58
+ RECONNECT_DELAY: 10,
59
+ HEARTBEAT_TIMEOUT_FALLBACK: 100,
60
+ } as const
61
+
62
+ const RECONNECT_INTERVALS = [1000, 2000, 5000, 10000] as const
63
+ const DEFAULT_RECONNECT_FALLBACK = 10000
64
+
49
65
  export interface WebSocketLikeConstructor {
50
66
  new (
51
67
  address: string | URL,
@@ -53,8 +69,6 @@ export interface WebSocketLikeConstructor {
53
69
  ): WebSocketLike
54
70
  }
55
71
 
56
- export type WebSocketLike = WebSocket
57
-
58
72
  export interface WebSocketLikeError {
59
73
  error: any
60
74
  message: string
@@ -97,18 +111,18 @@ export default class RealtimeClient {
97
111
  headers?: { [key: string]: string } = {}
98
112
  params?: { [key: string]: string } = {}
99
113
  timeout: number = DEFAULT_TIMEOUT
100
- transport: WebSocketLikeConstructor | null
101
- heartbeatIntervalMs: number = 25000
114
+ transport: WebSocketLikeConstructor | null = null
115
+ heartbeatIntervalMs: number = CONNECTION_TIMEOUTS.HEARTBEAT_INTERVAL
102
116
  heartbeatTimer: ReturnType<typeof setInterval> | undefined = undefined
103
117
  pendingHeartbeatRef: string | null = null
104
118
  heartbeatCallback: (status: HeartbeatStatus) => void = noop
105
119
  ref: number = 0
106
- reconnectTimer: Timer
120
+ reconnectTimer: Timer | null = null
107
121
  logger: Function = noop
108
122
  logLevel?: LogLevel
109
- encode: Function
110
- decode: Function
111
- reconnectAfterMs: Function
123
+ encode!: Function
124
+ decode!: Function
125
+ reconnectAfterMs!: Function
112
126
  conn: WebSocketLike | null = null
113
127
  sendBuffer: Function[] = []
114
128
  serializer: Serializer = new Serializer()
@@ -128,6 +142,9 @@ export default class RealtimeClient {
128
142
  worker?: boolean
129
143
  workerUrl?: string
130
144
  workerRef?: Worker
145
+ private _connectionState: RealtimeClientState = 'disconnected'
146
+ private _wasManualDisconnect: boolean = false
147
+ private _authPromise: Promise<void> | null = null
131
148
 
132
149
  /**
133
150
  * Initializes the Socket.
@@ -148,80 +165,50 @@ export default class RealtimeClient {
148
165
  * @param options.workerUrl The URL of the worker script. Defaults to https://realtime.supabase.com/worker.js that includes a heartbeat event call to keep the connection alive.
149
166
  */
150
167
  constructor(endPoint: string, options?: RealtimeClientOptions) {
151
- this.endPoint = `${endPoint}/${TRANSPORTS.websocket}`
152
- this.httpEndpoint = httpEndpointURL(endPoint)
153
- if (options?.transport) {
154
- this.transport = options.transport
155
- } else {
156
- this.transport = null
157
- }
158
- if (options?.params) this.params = options.params
159
- if (options?.timeout) this.timeout = options.timeout
160
- if (options?.logger) this.logger = options.logger
161
- if (options?.logLevel || options?.log_level) {
162
- this.logLevel = options.logLevel || options.log_level
163
- this.params = { ...this.params, log_level: this.logLevel as string }
164
- }
165
-
166
- if (options?.heartbeatIntervalMs)
167
- this.heartbeatIntervalMs = options.heartbeatIntervalMs
168
-
169
- const accessTokenValue = options?.params?.apikey
170
- if (accessTokenValue) {
171
- this.accessTokenValue = accessTokenValue
172
- this.apiKey = accessTokenValue
168
+ // Validate required parameters
169
+ if (!options?.params?.apikey) {
170
+ throw new Error('API key is required to connect to Realtime')
173
171
  }
172
+ this.apiKey = options.params.apikey
174
173
 
175
- this.reconnectAfterMs = options?.reconnectAfterMs
176
- ? options.reconnectAfterMs
177
- : (tries: number) => {
178
- return [1000, 2000, 5000, 10000][tries - 1] || 10000
179
- }
180
- this.encode = options?.encode
181
- ? options.encode
182
- : (payload: JSON, callback: Function) => {
183
- return callback(JSON.stringify(payload))
184
- }
185
- this.decode = options?.decode
186
- ? options.decode
187
- : this.serializer.decode.bind(this.serializer)
188
- this.reconnectTimer = new Timer(async () => {
189
- this.disconnect()
190
- this.connect()
191
- }, this.reconnectAfterMs)
174
+ // Initialize endpoint URLs
175
+ this.endPoint = `${endPoint}/${TRANSPORTS.websocket}`
176
+ this.httpEndpoint = httpEndpointURL(endPoint)
192
177
 
178
+ this._initializeOptions(options)
179
+ this._setupReconnectionTimer()
193
180
  this.fetch = this._resolveFetch(options?.fetch)
194
- if (options?.worker) {
195
- if (typeof window !== 'undefined' && !window.Worker) {
196
- throw new Error('Web Worker is not supported')
197
- }
198
- this.worker = options?.worker || false
199
- this.workerUrl = options?.workerUrl
200
- }
201
- this.accessToken = options?.accessToken || null
202
181
  }
203
182
 
204
183
  /**
205
184
  * Connects the socket, unless already connected.
206
185
  */
207
186
  connect(): void {
208
- // Set the token on connect
209
- setTimeout(() => {
210
- this.setAuth().catch((e) => {
211
- this.log('error', 'error setting auth', e)
212
- })
213
- }, 0)
214
- if (this.conn) {
187
+ // Skip if already connecting, disconnecting, or connected
188
+ if (
189
+ this.isConnecting() ||
190
+ this.isDisconnecting() ||
191
+ (this.conn !== null && this.isConnected())
192
+ ) {
215
193
  return
216
194
  }
195
+
196
+ this._setConnectionState('connecting')
197
+ this._setAuthSafely('connect')
198
+
199
+ // Establish WebSocket connection
217
200
  if (!this.transport) {
218
- this.transport = WebSocket
219
- }
220
- if (!this.transport) {
221
- throw new Error('No transport provided')
201
+ try {
202
+ this.conn = WebSocketFactory.createWebSocket(this.endpointURL())
203
+ } catch (error) {
204
+ this._setConnectionState('disconnected')
205
+ throw new Error(`WebSocket not available: ${(error as Error).message}`)
206
+ }
207
+ } else {
208
+ // Use custom transport if provided
209
+ this.conn = new this.transport!(this.endpointURL()) as WebSocketLike
222
210
  }
223
- this.conn = new this.transport(this.endpointURL()) as WebSocketLike
224
- this.setupConnection()
211
+ this._setupConnectionHandlers()
225
212
  }
226
213
 
227
214
  /**
@@ -242,19 +229,33 @@ export default class RealtimeClient {
242
229
  * @param reason A custom reason for the disconnect.
243
230
  */
244
231
  disconnect(code?: number, reason?: string): void {
232
+ if (this.isDisconnecting()) {
233
+ return
234
+ }
235
+
236
+ this._setConnectionState('disconnecting', true)
237
+
245
238
  if (this.conn) {
246
- this.conn.onclose = function () {} // noop
239
+ // Setup fallback timer to prevent hanging in disconnecting state
240
+ const fallbackTimer = setTimeout(() => {
241
+ this._setConnectionState('disconnected')
242
+ }, 100)
243
+
244
+ this.conn.onclose = () => {
245
+ clearTimeout(fallbackTimer)
246
+ this._setConnectionState('disconnected')
247
+ }
248
+
249
+ // Close the WebSocket connection
247
250
  if (code) {
248
251
  this.conn.close(code, reason ?? '')
249
252
  } else {
250
253
  this.conn.close()
251
254
  }
252
- this.conn = null
253
255
 
254
- // remove open handles
255
- this.heartbeatTimer && clearInterval(this.heartbeatTimer)
256
- this.reconnectTimer.reset()
257
- this.channels.forEach((channel) => channel.teardown())
256
+ this._teardownConnection()
257
+ } else {
258
+ this._setConnectionState('disconnected')
258
259
  }
259
260
  }
260
261
 
@@ -325,6 +326,20 @@ export default class RealtimeClient {
325
326
  return this.connectionState() === CONNECTION_STATE.Open
326
327
  }
327
328
 
329
+ /**
330
+ * Returns `true` if the connection is currently connecting.
331
+ */
332
+ isConnecting(): boolean {
333
+ return this._connectionState === 'connecting'
334
+ }
335
+
336
+ /**
337
+ * Returns `true` if the connection is currently disconnecting.
338
+ */
339
+ isDisconnecting(): boolean {
340
+ return this._connectionState === 'disconnecting'
341
+ }
342
+
328
343
  channel(
329
344
  topic: string,
330
345
  params: RealtimeChannelOptions = { config: {} }
@@ -374,27 +389,11 @@ export default class RealtimeClient {
374
389
  * @param token A JWT string to override the token set on the client.
375
390
  */
376
391
  async setAuth(token: string | null = null): Promise<void> {
377
- let tokenToSend =
378
- token ||
379
- (this.accessToken && (await this.accessToken())) ||
380
- this.accessTokenValue
381
-
382
- if (this.accessTokenValue != tokenToSend) {
383
- this.accessTokenValue = tokenToSend
384
- this.channels.forEach((channel) => {
385
- const payload = {
386
- access_token: tokenToSend,
387
- version: DEFAULT_VERSION,
388
- }
389
-
390
- tokenToSend && channel.updateJoinPayload(payload)
391
-
392
- if (channel.joinedOnce && channel._isJoined()) {
393
- channel._push(CHANNEL_EVENTS.access_token, {
394
- access_token: tokenToSend,
395
- })
396
- }
397
- })
392
+ this._authPromise = this._performAuth(token)
393
+ try {
394
+ await this._authPromise
395
+ } finally {
396
+ this._authPromise = null
398
397
  }
399
398
  }
400
399
  /**
@@ -405,6 +404,8 @@ export default class RealtimeClient {
405
404
  this.heartbeatCallback('disconnected')
406
405
  return
407
406
  }
407
+
408
+ // Handle heartbeat timeout and force reconnection if needed
408
409
  if (this.pendingHeartbeatRef) {
409
410
  this.pendingHeartbeatRef = null
410
411
  this.log(
@@ -412,9 +413,20 @@ export default class RealtimeClient {
412
413
  'heartbeat timeout. Attempting to re-establish connection'
413
414
  )
414
415
  this.heartbeatCallback('timeout')
415
- this.conn?.close(WS_CLOSE_NORMAL, 'hearbeat timeout')
416
+
417
+ // Force reconnection after heartbeat timeout
418
+ this._wasManualDisconnect = false
419
+ this.conn?.close(WS_CLOSE_NORMAL, 'heartbeat timeout')
420
+
421
+ setTimeout(() => {
422
+ if (!this.isConnected()) {
423
+ this.reconnectTimer?.scheduleTimeout()
424
+ }
425
+ }, CONNECTION_TIMEOUTS.HEARTBEAT_TIMEOUT_FALLBACK)
416
426
  return
417
427
  }
428
+
429
+ // Send heartbeat message to server
418
430
  this.pendingHeartbeatRef = this._makeRef()
419
431
  this.push({
420
432
  topic: 'phoenix',
@@ -423,7 +435,8 @@ export default class RealtimeClient {
423
435
  ref: this.pendingHeartbeatRef,
424
436
  })
425
437
  this.heartbeatCallback('sent')
426
- await this.setAuth()
438
+
439
+ this._setAuthSafely('heartbeat')
427
440
  }
428
441
 
429
442
  onHeartbeat(callback: (status: HeartbeatStatus) => void): void {
@@ -449,10 +462,16 @@ export default class RealtimeClient {
449
462
  if (customFetch) {
450
463
  _fetch = customFetch
451
464
  } else if (typeof fetch === 'undefined') {
465
+ // Node.js environment without native fetch
452
466
  _fetch = (...args) =>
453
- import('@supabase/node-fetch' as any).then(({ default: fetch }) =>
454
- fetch(...args)
455
- )
467
+ import('@supabase/node-fetch' as any)
468
+ .then(({ default: fetch }) => fetch(...args))
469
+ .catch((error) => {
470
+ throw new Error(
471
+ `Failed to load @supabase/node-fetch: ${error.message}. ` +
472
+ `This is required for HTTP requests in Node.js environments without native fetch.`
473
+ )
474
+ })
456
475
  } else {
457
476
  _fetch = fetch
458
477
  }
@@ -501,57 +520,103 @@ export default class RealtimeClient {
501
520
  this.channels = this.channels.filter((c) => c.topic !== channel.topic)
502
521
  }
503
522
 
504
- /**
505
- * Sets up connection handlers.
506
- *
507
- * @internal
508
- */
509
- private setupConnection(): void {
510
- if (this.conn) {
511
- this.conn.binaryType = 'arraybuffer'
512
- this.conn.onopen = () => this._onConnOpen()
513
- this.conn.onerror = (error: Event) => this._onConnError(error)
514
- this.conn.onmessage = (event: any) => this._onConnMessage(event)
515
- this.conn.onclose = (event: any) => this._onConnClose(event)
516
- }
517
- }
518
-
519
523
  /** @internal */
520
524
  private _onConnMessage(rawMessage: { data: any }) {
521
525
  this.decode(rawMessage.data, (msg: RealtimeMessage) => {
522
- let { topic, event, payload, ref } = msg
523
-
524
- if (topic === 'phoenix' && event === 'phx_reply') {
525
- this.heartbeatCallback(msg.payload.status == 'ok' ? 'ok' : 'error')
526
+ // Handle heartbeat responses
527
+ if (msg.topic === 'phoenix' && msg.event === 'phx_reply') {
528
+ this.heartbeatCallback(msg.payload.status === 'ok' ? 'ok' : 'error')
526
529
  }
527
530
 
528
- if (ref && ref === this.pendingHeartbeatRef) {
531
+ // Handle pending heartbeat reference cleanup
532
+ if (msg.ref && msg.ref === this.pendingHeartbeatRef) {
529
533
  this.pendingHeartbeatRef = null
530
534
  }
531
535
 
536
+ // Log incoming message
537
+ const { topic, event, payload, ref } = msg
538
+ const refString = ref ? `(${ref})` : ''
539
+ const status = payload.status || ''
532
540
  this.log(
533
541
  'receive',
534
- `${payload.status || ''} ${topic} ${event} ${
535
- (ref && '(' + ref + ')') || ''
536
- }`,
542
+ `${status} ${topic} ${event} ${refString}`.trim(),
537
543
  payload
538
544
  )
539
545
 
540
- Array.from(this.channels)
546
+ // Route message to appropriate channels
547
+ this.channels
541
548
  .filter((channel: RealtimeChannel) => channel._isMember(topic))
542
549
  .forEach((channel: RealtimeChannel) =>
543
550
  channel._trigger(event, payload, ref)
544
551
  )
545
552
 
546
- this.stateChangeCallbacks.message.forEach((callback) => callback(msg))
553
+ this._triggerStateCallbacks('message', msg)
547
554
  })
548
555
  }
549
556
 
557
+ /**
558
+ * Clear specific timer
559
+ * @internal
560
+ */
561
+ private _clearTimer(timer: 'heartbeat' | 'reconnect'): void {
562
+ if (timer === 'heartbeat' && this.heartbeatTimer) {
563
+ clearInterval(this.heartbeatTimer)
564
+ this.heartbeatTimer = undefined
565
+ } else if (timer === 'reconnect') {
566
+ this.reconnectTimer?.reset()
567
+ }
568
+ }
569
+
570
+ /**
571
+ * Clear all timers
572
+ * @internal
573
+ */
574
+ private _clearAllTimers(): void {
575
+ this._clearTimer('heartbeat')
576
+ this._clearTimer('reconnect')
577
+ }
578
+
579
+ /**
580
+ * Setup connection handlers for WebSocket events
581
+ * @internal
582
+ */
583
+ private _setupConnectionHandlers(): void {
584
+ if (!this.conn) return
585
+
586
+ // Set binary type if supported (browsers and most WebSocket implementations)
587
+ if ('binaryType' in this.conn) {
588
+ ;(this.conn as any).binaryType = 'arraybuffer'
589
+ }
590
+
591
+ this.conn.onopen = () => this._onConnOpen()
592
+ this.conn.onerror = (error: Event) => this._onConnError(error)
593
+ this.conn.onmessage = (event: any) => this._onConnMessage(event)
594
+ this.conn.onclose = (event: any) => this._onConnClose(event)
595
+ }
596
+
597
+ /**
598
+ * Teardown connection and cleanup resources
599
+ * @internal
600
+ */
601
+ private _teardownConnection(): void {
602
+ if (this.conn) {
603
+ this.conn.onopen = null
604
+ this.conn.onerror = null
605
+ this.conn.onmessage = null
606
+ this.conn.onclose = null
607
+ this.conn = null
608
+ }
609
+ this._clearAllTimers()
610
+ this.channels.forEach((channel) => channel.teardown())
611
+ }
612
+
550
613
  /** @internal */
551
614
  private _onConnOpen() {
615
+ this._setConnectionState('connected')
552
616
  this.log('transport', `connected to ${this.endpointURL()}`)
553
617
  this.flushSendBuffer()
554
- this.reconnectTimer.reset()
618
+ this._clearTimer('reconnect')
619
+
555
620
  if (!this.worker) {
556
621
  this._startHeartbeat()
557
622
  } else {
@@ -560,7 +625,7 @@ export default class RealtimeClient {
560
625
  }
561
626
  }
562
627
 
563
- this.stateChangeCallbacks.open.forEach((callback) => callback())
628
+ this._triggerStateCallbacks('open')
564
629
  }
565
630
  /** @internal */
566
631
  private _startHeartbeat() {
@@ -596,18 +661,25 @@ export default class RealtimeClient {
596
661
  }
597
662
  /** @internal */
598
663
  private _onConnClose(event: any) {
664
+ this._setConnectionState('disconnected')
599
665
  this.log('transport', 'close', event)
600
666
  this._triggerChanError()
601
- this.heartbeatTimer && clearInterval(this.heartbeatTimer)
602
- this.reconnectTimer.scheduleTimeout()
603
- this.stateChangeCallbacks.close.forEach((callback) => callback(event))
667
+ this._clearTimer('heartbeat')
668
+
669
+ // Only schedule reconnection if it wasn't a manual disconnect
670
+ if (!this._wasManualDisconnect) {
671
+ this.reconnectTimer?.scheduleTimeout()
672
+ }
673
+
674
+ this._triggerStateCallbacks('close', event)
604
675
  }
605
676
 
606
677
  /** @internal */
607
678
  private _onConnError(error: Event) {
679
+ this._setConnectionState('disconnected')
608
680
  this.log('transport', `${error}`)
609
681
  this._triggerChanError()
610
- this.stateChangeCallbacks.error.forEach((callback) => callback(error))
682
+ this._triggerStateCallbacks('error', error)
611
683
  }
612
684
 
613
685
  /** @internal */
@@ -640,4 +712,158 @@ export default class RealtimeClient {
640
712
  }
641
713
  return result_url
642
714
  }
715
+
716
+ /**
717
+ * Set connection state with proper state management
718
+ * @internal
719
+ */
720
+ private _setConnectionState(
721
+ state: RealtimeClientState,
722
+ manual = false
723
+ ): void {
724
+ this._connectionState = state
725
+
726
+ if (state === 'connecting') {
727
+ this._wasManualDisconnect = false
728
+ } else if (state === 'disconnecting') {
729
+ this._wasManualDisconnect = manual
730
+ }
731
+ }
732
+
733
+ /**
734
+ * Perform the actual auth operation
735
+ * @internal
736
+ */
737
+ private async _performAuth(token: string | null = null): Promise<void> {
738
+ let tokenToSend: string | null
739
+
740
+ if (token) {
741
+ tokenToSend = token
742
+ } else if (this.accessToken) {
743
+ // Always call the accessToken callback to get fresh token
744
+ tokenToSend = await this.accessToken()
745
+ } else {
746
+ tokenToSend = this.accessTokenValue
747
+ }
748
+
749
+ if (this.accessTokenValue != tokenToSend) {
750
+ this.accessTokenValue = tokenToSend
751
+ this.channels.forEach((channel) => {
752
+ const payload = {
753
+ access_token: tokenToSend,
754
+ version: DEFAULT_VERSION,
755
+ }
756
+
757
+ tokenToSend && channel.updateJoinPayload(payload)
758
+
759
+ if (channel.joinedOnce && channel._isJoined()) {
760
+ channel._push(CHANNEL_EVENTS.access_token, {
761
+ access_token: tokenToSend,
762
+ })
763
+ }
764
+ })
765
+ }
766
+ }
767
+
768
+ /**
769
+ * Wait for any in-flight auth operations to complete
770
+ * @internal
771
+ */
772
+ private async _waitForAuthIfNeeded(): Promise<void> {
773
+ if (this._authPromise) {
774
+ await this._authPromise
775
+ }
776
+ }
777
+
778
+ /**
779
+ * Safely call setAuth with standardized error handling
780
+ * @internal
781
+ */
782
+ private _setAuthSafely(context = 'general'): void {
783
+ this.setAuth().catch((e) => {
784
+ this.log('error', `error setting auth in ${context}`, e)
785
+ })
786
+ }
787
+
788
+ /**
789
+ * Trigger state change callbacks with proper error handling
790
+ * @internal
791
+ */
792
+ private _triggerStateCallbacks(
793
+ event: keyof typeof this.stateChangeCallbacks,
794
+ data?: any
795
+ ): void {
796
+ try {
797
+ this.stateChangeCallbacks[event].forEach((callback) => {
798
+ try {
799
+ callback(data)
800
+ } catch (e) {
801
+ this.log('error', `error in ${event} callback`, e)
802
+ }
803
+ })
804
+ } catch (e) {
805
+ this.log('error', `error triggering ${event} callbacks`, e)
806
+ }
807
+ }
808
+
809
+ /**
810
+ * Setup reconnection timer with proper configuration
811
+ * @internal
812
+ */
813
+ private _setupReconnectionTimer(): void {
814
+ this.reconnectTimer = new Timer(async () => {
815
+ setTimeout(async () => {
816
+ await this._waitForAuthIfNeeded()
817
+ if (!this.isConnected()) {
818
+ this.connect()
819
+ }
820
+ }, CONNECTION_TIMEOUTS.RECONNECT_DELAY)
821
+ }, this.reconnectAfterMs)
822
+ }
823
+
824
+ /**
825
+ * Initialize client options with defaults
826
+ * @internal
827
+ */
828
+ private _initializeOptions(options?: RealtimeClientOptions): void {
829
+ // Set defaults
830
+ this.transport = options?.transport ?? null
831
+ this.timeout = options?.timeout ?? DEFAULT_TIMEOUT
832
+ this.heartbeatIntervalMs =
833
+ options?.heartbeatIntervalMs ?? CONNECTION_TIMEOUTS.HEARTBEAT_INTERVAL
834
+ this.worker = options?.worker ?? false
835
+ this.accessToken = options?.accessToken ?? null
836
+
837
+ // Handle special cases
838
+ if (options?.params) this.params = options.params
839
+ if (options?.logger) this.logger = options.logger
840
+ if (options?.logLevel || options?.log_level) {
841
+ this.logLevel = options.logLevel || options.log_level
842
+ this.params = { ...this.params, log_level: this.logLevel as string }
843
+ }
844
+
845
+ // Set up functions with defaults
846
+ this.reconnectAfterMs =
847
+ options?.reconnectAfterMs ??
848
+ ((tries: number) => {
849
+ return RECONNECT_INTERVALS[tries - 1] || DEFAULT_RECONNECT_FALLBACK
850
+ })
851
+
852
+ this.encode =
853
+ options?.encode ??
854
+ ((payload: JSON, callback: Function) => {
855
+ return callback(JSON.stringify(payload))
856
+ })
857
+
858
+ this.decode =
859
+ options?.decode ?? this.serializer.decode.bind(this.serializer)
860
+
861
+ // Handle worker setup
862
+ if (this.worker) {
863
+ if (typeof window !== 'undefined' && !window.Worker) {
864
+ throw new Error('Web Worker is not supported')
865
+ }
866
+ this.workerUrl = options?.workerUrl
867
+ }
868
+ }
643
869
  }
package/src/index.ts CHANGED
@@ -22,6 +22,7 @@ import RealtimePresence, {
22
22
  RealtimePresenceLeavePayload,
23
23
  REALTIME_PRESENCE_LISTEN_EVENTS,
24
24
  } from './RealtimePresence'
25
+ import WebSocketFactory, { WebSocketLike } from './lib/websocket-factory'
25
26
 
26
27
  export {
27
28
  RealtimePresence,
@@ -45,4 +46,6 @@ export {
45
46
  REALTIME_PRESENCE_LISTEN_EVENTS,
46
47
  REALTIME_SUBSCRIBE_STATES,
47
48
  REALTIME_CHANNEL_STATES,
49
+ WebSocketFactory,
50
+ WebSocketLike,
48
51
  }
@@ -1 +1 @@
1
- export const version = '2.12.2'
1
+ export const version = '2.14.0'