@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.
- 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 +13 -2
- package/dist/main/RealtimeClient.d.ts.map +1 -1
- package/dist/main/RealtimeClient.js +311 -115
- package/dist/main/RealtimeClient.js.map +1 -1
- package/dist/main/index.d.ts +2 -1
- package/dist/main/index.d.ts.map +1 -1
- package/dist/main/index.js +3 -1
- package/dist/main/index.js.map +1 -1
- package/dist/main/lib/version.d.ts +1 -1
- package/dist/main/lib/version.js +1 -1
- package/dist/main/lib/websocket-factory.d.ts +41 -0
- package/dist/main/lib/websocket-factory.d.ts.map +1 -0
- package/dist/main/lib/websocket-factory.js +132 -0
- package/dist/main/lib/websocket-factory.js.map +1 -0
- 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 +13 -2
- package/dist/module/RealtimeClient.d.ts.map +1 -1
- package/dist/module/RealtimeClient.js +312 -115
- package/dist/module/RealtimeClient.js.map +1 -1
- package/dist/module/index.d.ts +2 -1
- package/dist/module/index.d.ts.map +1 -1
- package/dist/module/index.js +2 -1
- package/dist/module/index.js.map +1 -1
- package/dist/module/lib/version.d.ts +1 -1
- package/dist/module/lib/version.js +1 -1
- package/dist/module/lib/websocket-factory.d.ts +41 -0
- package/dist/module/lib/websocket-factory.d.ts.map +1 -0
- package/dist/module/lib/websocket-factory.js +128 -0
- package/dist/module/lib/websocket-factory.js.map +1 -0
- package/package.json +3 -4
- package/src/RealtimeChannel.ts +12 -1
- package/src/RealtimeClient.ts +358 -132
- package/src/index.ts +3 -0
- package/src/lib/version.ts +1 -1
- package/src/lib/websocket-factory.ts +189 -0
package/src/RealtimeClient.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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 =
|
|
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
|
|
110
|
-
decode
|
|
111
|
-
reconnectAfterMs
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
-
//
|
|
209
|
-
|
|
210
|
-
this.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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.
|
|
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
|
-
|
|
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 {
|
|
@@ -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)
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
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
|
-
`${
|
|
535
|
-
(ref && '(' + ref + ')') || ''
|
|
536
|
-
}`,
|
|
542
|
+
`${status} ${topic} ${event} ${refString}`.trim(),
|
|
537
543
|
payload
|
|
538
544
|
)
|
|
539
545
|
|
|
540
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
602
|
-
|
|
603
|
-
|
|
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.
|
|
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
|
}
|
package/src/lib/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.
|
|
1
|
+
export const version = '2.14.0'
|