@supabase/realtime-js 2.12.2 → 2.13.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.
- package/dist/main/RealtimeChannel.d.ts.map +1 -1
- package/dist/main/RealtimeChannel.js +10 -1
- package/dist/main/RealtimeChannel.js.map +1 -1
- package/dist/main/RealtimeClient.d.ts +12 -1
- package/dist/main/RealtimeClient.d.ts.map +1 -1
- package/dist/main/RealtimeClient.js +291 -109
- package/dist/main/RealtimeClient.js.map +1 -1
- package/dist/main/lib/version.d.ts +1 -1
- package/dist/main/lib/version.js +1 -1
- package/dist/module/RealtimeChannel.d.ts.map +1 -1
- package/dist/module/RealtimeChannel.js +10 -1
- package/dist/module/RealtimeChannel.js.map +1 -1
- package/dist/module/RealtimeClient.d.ts +12 -1
- package/dist/module/RealtimeClient.d.ts.map +1 -1
- package/dist/module/RealtimeClient.js +291 -109
- package/dist/module/RealtimeClient.js.map +1 -1
- package/dist/module/lib/version.d.ts +1 -1
- package/dist/module/lib/version.js +1 -1
- package/package.json +1 -1
- package/src/RealtimeChannel.ts +12 -1
- package/src/RealtimeClient.ts +338 -122
- package/src/lib/version.ts +1 -1
package/src/RealtimeClient.ts
CHANGED
|
@@ -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,
|
|
@@ -97,18 +113,18 @@ export default class RealtimeClient {
|
|
|
97
113
|
headers?: { [key: string]: string } = {}
|
|
98
114
|
params?: { [key: string]: string } = {}
|
|
99
115
|
timeout: number = DEFAULT_TIMEOUT
|
|
100
|
-
transport: WebSocketLikeConstructor | null
|
|
101
|
-
heartbeatIntervalMs: number =
|
|
116
|
+
transport: WebSocketLikeConstructor | null = null
|
|
117
|
+
heartbeatIntervalMs: number = CONNECTION_TIMEOUTS.HEARTBEAT_INTERVAL
|
|
102
118
|
heartbeatTimer: ReturnType<typeof setInterval> | undefined = undefined
|
|
103
119
|
pendingHeartbeatRef: string | null = null
|
|
104
120
|
heartbeatCallback: (status: HeartbeatStatus) => void = noop
|
|
105
121
|
ref: number = 0
|
|
106
|
-
reconnectTimer: Timer
|
|
122
|
+
reconnectTimer: Timer | null = null
|
|
107
123
|
logger: Function = noop
|
|
108
124
|
logLevel?: LogLevel
|
|
109
|
-
encode
|
|
110
|
-
decode
|
|
111
|
-
reconnectAfterMs
|
|
125
|
+
encode!: Function
|
|
126
|
+
decode!: Function
|
|
127
|
+
reconnectAfterMs!: Function
|
|
112
128
|
conn: WebSocketLike | null = null
|
|
113
129
|
sendBuffer: Function[] = []
|
|
114
130
|
serializer: Serializer = new Serializer()
|
|
@@ -128,6 +144,9 @@ export default class RealtimeClient {
|
|
|
128
144
|
worker?: boolean
|
|
129
145
|
workerUrl?: string
|
|
130
146
|
workerRef?: Worker
|
|
147
|
+
private _connectionState: RealtimeClientState = 'disconnected'
|
|
148
|
+
private _wasManualDisconnect: boolean = false
|
|
149
|
+
private _authPromise: Promise<void> | null = null
|
|
131
150
|
|
|
132
151
|
/**
|
|
133
152
|
* Initializes the Socket.
|
|
@@ -148,80 +167,48 @@ export default class RealtimeClient {
|
|
|
148
167
|
* @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
168
|
*/
|
|
150
169
|
constructor(endPoint: string, options?: RealtimeClientOptions) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
170
|
+
// Validate required parameters
|
|
171
|
+
if (!options?.params?.apikey) {
|
|
172
|
+
throw new Error('API key is required to connect to Realtime')
|
|
173
173
|
}
|
|
174
|
+
this.apiKey = options.params.apikey
|
|
174
175
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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)
|
|
176
|
+
// Initialize endpoint URLs
|
|
177
|
+
this.endPoint = `${endPoint}/${TRANSPORTS.websocket}`
|
|
178
|
+
this.httpEndpoint = httpEndpointURL(endPoint)
|
|
192
179
|
|
|
180
|
+
this._initializeOptions(options)
|
|
181
|
+
this._setupReconnectionTimer()
|
|
193
182
|
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
183
|
}
|
|
203
184
|
|
|
204
185
|
/**
|
|
205
186
|
* Connects the socket, unless already connected.
|
|
206
187
|
*/
|
|
207
188
|
connect(): void {
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
this.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (this.conn) {
|
|
189
|
+
// Skip if already connecting, disconnecting, or connected
|
|
190
|
+
if (
|
|
191
|
+
this.isConnecting() ||
|
|
192
|
+
this.isDisconnecting() ||
|
|
193
|
+
(this.conn !== null && this.isConnected())
|
|
194
|
+
) {
|
|
215
195
|
return
|
|
216
196
|
}
|
|
197
|
+
|
|
198
|
+
this._setConnectionState('connecting')
|
|
199
|
+
this._setAuthSafely('connect')
|
|
200
|
+
|
|
201
|
+
// Establish WebSocket connection
|
|
217
202
|
if (!this.transport) {
|
|
218
203
|
this.transport = WebSocket
|
|
219
204
|
}
|
|
220
205
|
if (!this.transport) {
|
|
206
|
+
this._setConnectionState('disconnected')
|
|
221
207
|
throw new Error('No transport provided')
|
|
222
208
|
}
|
|
223
|
-
|
|
224
|
-
this.
|
|
209
|
+
|
|
210
|
+
this.conn = new this.transport!(this.endpointURL()) as WebSocketLike
|
|
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
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
this.
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
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
|
-
|
|
438
|
+
|
|
439
|
+
this._setAuthSafely('heartbeat')
|
|
427
440
|
}
|
|
428
441
|
|
|
429
442
|
onHeartbeat(callback: (status: HeartbeatStatus) => void): void {
|
|
@@ -501,57 +514,99 @@ export default class RealtimeClient {
|
|
|
501
514
|
this.channels = this.channels.filter((c) => c.topic !== channel.topic)
|
|
502
515
|
}
|
|
503
516
|
|
|
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
517
|
/** @internal */
|
|
520
518
|
private _onConnMessage(rawMessage: { data: any }) {
|
|
521
519
|
this.decode(rawMessage.data, (msg: RealtimeMessage) => {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
this.heartbeatCallback(msg.payload.status == 'ok' ? 'ok' : 'error')
|
|
520
|
+
// Handle heartbeat responses
|
|
521
|
+
if (msg.topic === 'phoenix' && msg.event === 'phx_reply') {
|
|
522
|
+
this.heartbeatCallback(msg.payload.status === 'ok' ? 'ok' : 'error')
|
|
526
523
|
}
|
|
527
524
|
|
|
528
|
-
|
|
525
|
+
// Handle pending heartbeat reference cleanup
|
|
526
|
+
if (msg.ref && msg.ref === this.pendingHeartbeatRef) {
|
|
529
527
|
this.pendingHeartbeatRef = null
|
|
530
528
|
}
|
|
531
529
|
|
|
530
|
+
// Log incoming message
|
|
531
|
+
const { topic, event, payload, ref } = msg
|
|
532
|
+
const refString = ref ? `(${ref})` : ''
|
|
533
|
+
const status = payload.status || ''
|
|
532
534
|
this.log(
|
|
533
535
|
'receive',
|
|
534
|
-
`${
|
|
535
|
-
(ref && '(' + ref + ')') || ''
|
|
536
|
-
}`,
|
|
536
|
+
`${status} ${topic} ${event} ${refString}`.trim(),
|
|
537
537
|
payload
|
|
538
538
|
)
|
|
539
539
|
|
|
540
|
-
|
|
540
|
+
// Route message to appropriate channels
|
|
541
|
+
this.channels
|
|
541
542
|
.filter((channel: RealtimeChannel) => channel._isMember(topic))
|
|
542
543
|
.forEach((channel: RealtimeChannel) =>
|
|
543
544
|
channel._trigger(event, payload, ref)
|
|
544
545
|
)
|
|
545
546
|
|
|
546
|
-
this.
|
|
547
|
+
this._triggerStateCallbacks('message', msg)
|
|
547
548
|
})
|
|
548
549
|
}
|
|
549
550
|
|
|
551
|
+
/**
|
|
552
|
+
* Clear specific timer
|
|
553
|
+
* @internal
|
|
554
|
+
*/
|
|
555
|
+
private _clearTimer(timer: 'heartbeat' | 'reconnect'): void {
|
|
556
|
+
if (timer === 'heartbeat' && this.heartbeatTimer) {
|
|
557
|
+
clearInterval(this.heartbeatTimer)
|
|
558
|
+
this.heartbeatTimer = undefined
|
|
559
|
+
} else if (timer === 'reconnect') {
|
|
560
|
+
this.reconnectTimer?.reset()
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Clear all timers
|
|
566
|
+
* @internal
|
|
567
|
+
*/
|
|
568
|
+
private _clearAllTimers(): void {
|
|
569
|
+
this._clearTimer('heartbeat')
|
|
570
|
+
this._clearTimer('reconnect')
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Setup connection handlers for WebSocket events
|
|
575
|
+
* @internal
|
|
576
|
+
*/
|
|
577
|
+
private _setupConnectionHandlers(): void {
|
|
578
|
+
if (!this.conn) return
|
|
579
|
+
|
|
580
|
+
this.conn.binaryType = 'arraybuffer'
|
|
581
|
+
this.conn.onopen = () => this._onConnOpen()
|
|
582
|
+
this.conn.onerror = (error: Event) => this._onConnError(error)
|
|
583
|
+
this.conn.onmessage = (event: any) => this._onConnMessage(event)
|
|
584
|
+
this.conn.onclose = (event: any) => this._onConnClose(event)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Teardown connection and cleanup resources
|
|
589
|
+
* @internal
|
|
590
|
+
*/
|
|
591
|
+
private _teardownConnection(): void {
|
|
592
|
+
if (this.conn) {
|
|
593
|
+
this.conn.onopen = null
|
|
594
|
+
this.conn.onerror = null
|
|
595
|
+
this.conn.onmessage = null
|
|
596
|
+
this.conn.onclose = null
|
|
597
|
+
this.conn = null
|
|
598
|
+
}
|
|
599
|
+
this._clearAllTimers()
|
|
600
|
+
this.channels.forEach((channel) => channel.teardown())
|
|
601
|
+
}
|
|
602
|
+
|
|
550
603
|
/** @internal */
|
|
551
604
|
private _onConnOpen() {
|
|
605
|
+
this._setConnectionState('connected')
|
|
552
606
|
this.log('transport', `connected to ${this.endpointURL()}`)
|
|
553
607
|
this.flushSendBuffer()
|
|
554
|
-
this.
|
|
608
|
+
this._clearTimer('reconnect')
|
|
609
|
+
|
|
555
610
|
if (!this.worker) {
|
|
556
611
|
this._startHeartbeat()
|
|
557
612
|
} else {
|
|
@@ -560,7 +615,7 @@ export default class RealtimeClient {
|
|
|
560
615
|
}
|
|
561
616
|
}
|
|
562
617
|
|
|
563
|
-
this.
|
|
618
|
+
this._triggerStateCallbacks('open')
|
|
564
619
|
}
|
|
565
620
|
/** @internal */
|
|
566
621
|
private _startHeartbeat() {
|
|
@@ -596,18 +651,25 @@ export default class RealtimeClient {
|
|
|
596
651
|
}
|
|
597
652
|
/** @internal */
|
|
598
653
|
private _onConnClose(event: any) {
|
|
654
|
+
this._setConnectionState('disconnected')
|
|
599
655
|
this.log('transport', 'close', event)
|
|
600
656
|
this._triggerChanError()
|
|
601
|
-
this.
|
|
602
|
-
|
|
603
|
-
|
|
657
|
+
this._clearTimer('heartbeat')
|
|
658
|
+
|
|
659
|
+
// Only schedule reconnection if it wasn't a manual disconnect
|
|
660
|
+
if (!this._wasManualDisconnect) {
|
|
661
|
+
this.reconnectTimer?.scheduleTimeout()
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
this._triggerStateCallbacks('close', event)
|
|
604
665
|
}
|
|
605
666
|
|
|
606
667
|
/** @internal */
|
|
607
668
|
private _onConnError(error: Event) {
|
|
669
|
+
this._setConnectionState('disconnected')
|
|
608
670
|
this.log('transport', `${error}`)
|
|
609
671
|
this._triggerChanError()
|
|
610
|
-
this.
|
|
672
|
+
this._triggerStateCallbacks('error', error)
|
|
611
673
|
}
|
|
612
674
|
|
|
613
675
|
/** @internal */
|
|
@@ -640,4 +702,158 @@ export default class RealtimeClient {
|
|
|
640
702
|
}
|
|
641
703
|
return result_url
|
|
642
704
|
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Set connection state with proper state management
|
|
708
|
+
* @internal
|
|
709
|
+
*/
|
|
710
|
+
private _setConnectionState(
|
|
711
|
+
state: RealtimeClientState,
|
|
712
|
+
manual = false
|
|
713
|
+
): void {
|
|
714
|
+
this._connectionState = state
|
|
715
|
+
|
|
716
|
+
if (state === 'connecting') {
|
|
717
|
+
this._wasManualDisconnect = false
|
|
718
|
+
} else if (state === 'disconnecting') {
|
|
719
|
+
this._wasManualDisconnect = manual
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Perform the actual auth operation
|
|
725
|
+
* @internal
|
|
726
|
+
*/
|
|
727
|
+
private async _performAuth(token: string | null = null): Promise<void> {
|
|
728
|
+
let tokenToSend: string | null
|
|
729
|
+
|
|
730
|
+
if (token) {
|
|
731
|
+
tokenToSend = token
|
|
732
|
+
} else if (this.accessToken) {
|
|
733
|
+
// Always call the accessToken callback to get fresh token
|
|
734
|
+
tokenToSend = await this.accessToken()
|
|
735
|
+
} else {
|
|
736
|
+
tokenToSend = this.accessTokenValue
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (this.accessTokenValue != tokenToSend) {
|
|
740
|
+
this.accessTokenValue = tokenToSend
|
|
741
|
+
this.channels.forEach((channel) => {
|
|
742
|
+
const payload = {
|
|
743
|
+
access_token: tokenToSend,
|
|
744
|
+
version: DEFAULT_VERSION,
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
tokenToSend && channel.updateJoinPayload(payload)
|
|
748
|
+
|
|
749
|
+
if (channel.joinedOnce && channel._isJoined()) {
|
|
750
|
+
channel._push(CHANNEL_EVENTS.access_token, {
|
|
751
|
+
access_token: tokenToSend,
|
|
752
|
+
})
|
|
753
|
+
}
|
|
754
|
+
})
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Wait for any in-flight auth operations to complete
|
|
760
|
+
* @internal
|
|
761
|
+
*/
|
|
762
|
+
private async _waitForAuthIfNeeded(): Promise<void> {
|
|
763
|
+
if (this._authPromise) {
|
|
764
|
+
await this._authPromise
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Safely call setAuth with standardized error handling
|
|
770
|
+
* @internal
|
|
771
|
+
*/
|
|
772
|
+
private _setAuthSafely(context = 'general'): void {
|
|
773
|
+
this.setAuth().catch((e) => {
|
|
774
|
+
this.log('error', `error setting auth in ${context}`, e)
|
|
775
|
+
})
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Trigger state change callbacks with proper error handling
|
|
780
|
+
* @internal
|
|
781
|
+
*/
|
|
782
|
+
private _triggerStateCallbacks(
|
|
783
|
+
event: keyof typeof this.stateChangeCallbacks,
|
|
784
|
+
data?: any
|
|
785
|
+
): void {
|
|
786
|
+
try {
|
|
787
|
+
this.stateChangeCallbacks[event].forEach((callback) => {
|
|
788
|
+
try {
|
|
789
|
+
callback(data)
|
|
790
|
+
} catch (e) {
|
|
791
|
+
this.log('error', `error in ${event} callback`, e)
|
|
792
|
+
}
|
|
793
|
+
})
|
|
794
|
+
} catch (e) {
|
|
795
|
+
this.log('error', `error triggering ${event} callbacks`, e)
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Setup reconnection timer with proper configuration
|
|
801
|
+
* @internal
|
|
802
|
+
*/
|
|
803
|
+
private _setupReconnectionTimer(): void {
|
|
804
|
+
this.reconnectTimer = new Timer(async () => {
|
|
805
|
+
setTimeout(async () => {
|
|
806
|
+
await this._waitForAuthIfNeeded()
|
|
807
|
+
if (!this.isConnected()) {
|
|
808
|
+
this.connect()
|
|
809
|
+
}
|
|
810
|
+
}, CONNECTION_TIMEOUTS.RECONNECT_DELAY)
|
|
811
|
+
}, this.reconnectAfterMs)
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Initialize client options with defaults
|
|
816
|
+
* @internal
|
|
817
|
+
*/
|
|
818
|
+
private _initializeOptions(options?: RealtimeClientOptions): void {
|
|
819
|
+
// Set defaults
|
|
820
|
+
this.transport = options?.transport ?? null
|
|
821
|
+
this.timeout = options?.timeout ?? DEFAULT_TIMEOUT
|
|
822
|
+
this.heartbeatIntervalMs =
|
|
823
|
+
options?.heartbeatIntervalMs ?? CONNECTION_TIMEOUTS.HEARTBEAT_INTERVAL
|
|
824
|
+
this.worker = options?.worker ?? false
|
|
825
|
+
this.accessToken = options?.accessToken ?? null
|
|
826
|
+
|
|
827
|
+
// Handle special cases
|
|
828
|
+
if (options?.params) this.params = options.params
|
|
829
|
+
if (options?.logger) this.logger = options.logger
|
|
830
|
+
if (options?.logLevel || options?.log_level) {
|
|
831
|
+
this.logLevel = options.logLevel || options.log_level
|
|
832
|
+
this.params = { ...this.params, log_level: this.logLevel as string }
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Set up functions with defaults
|
|
836
|
+
this.reconnectAfterMs =
|
|
837
|
+
options?.reconnectAfterMs ??
|
|
838
|
+
((tries: number) => {
|
|
839
|
+
return RECONNECT_INTERVALS[tries - 1] || DEFAULT_RECONNECT_FALLBACK
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
this.encode =
|
|
843
|
+
options?.encode ??
|
|
844
|
+
((payload: JSON, callback: Function) => {
|
|
845
|
+
return callback(JSON.stringify(payload))
|
|
846
|
+
})
|
|
847
|
+
|
|
848
|
+
this.decode =
|
|
849
|
+
options?.decode ?? this.serializer.decode.bind(this.serializer)
|
|
850
|
+
|
|
851
|
+
// Handle worker setup
|
|
852
|
+
if (this.worker) {
|
|
853
|
+
if (typeof window !== 'undefined' && !window.Worker) {
|
|
854
|
+
throw new Error('Web Worker is not supported')
|
|
855
|
+
}
|
|
856
|
+
this.workerUrl = options?.workerUrl
|
|
857
|
+
}
|
|
858
|
+
}
|
|
643
859
|
}
|
package/src/lib/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.
|
|
1
|
+
export const version = '2.13.0'
|