@depup/supabase__realtime-js 2.99.3-depup.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/README.md +32 -0
- package/changes.json +14 -0
- package/dist/main/RealtimeChannel.d.ts +370 -0
- package/dist/main/RealtimeChannel.d.ts.map +1 -0
- package/dist/main/RealtimeChannel.js +644 -0
- package/dist/main/RealtimeChannel.js.map +1 -0
- package/dist/main/RealtimeClient.d.ts +238 -0
- package/dist/main/RealtimeClient.d.ts.map +1 -0
- package/dist/main/RealtimeClient.js +870 -0
- package/dist/main/RealtimeClient.js.map +1 -0
- package/dist/main/RealtimePresence.d.ts +76 -0
- package/dist/main/RealtimePresence.d.ts.map +1 -0
- package/dist/main/RealtimePresence.js +237 -0
- package/dist/main/RealtimePresence.js.map +1 -0
- package/dist/main/index.d.ts +6 -0
- package/dist/main/index.d.ts.map +1 -0
- package/dist/main/index.js +18 -0
- package/dist/main/index.js.map +1 -0
- package/dist/main/lib/constants.d.ts +39 -0
- package/dist/main/lib/constants.d.ts.map +1 -0
- package/dist/main/lib/constants.js +48 -0
- package/dist/main/lib/constants.js.map +1 -0
- package/dist/main/lib/push.d.ts +48 -0
- package/dist/main/lib/push.d.ts.map +1 -0
- package/dist/main/lib/push.js +102 -0
- package/dist/main/lib/push.js.map +1 -0
- package/dist/main/lib/serializer.d.ts +33 -0
- package/dist/main/lib/serializer.d.ts.map +1 -0
- package/dist/main/lib/serializer.js +155 -0
- package/dist/main/lib/serializer.js.map +1 -0
- package/dist/main/lib/timer.d.ts +22 -0
- package/dist/main/lib/timer.d.ts.map +1 -0
- package/dist/main/lib/timer.js +39 -0
- package/dist/main/lib/timer.js.map +1 -0
- package/dist/main/lib/transformers.d.ts +109 -0
- package/dist/main/lib/transformers.d.ts.map +1 -0
- package/dist/main/lib/transformers.js +241 -0
- package/dist/main/lib/transformers.js.map +1 -0
- package/dist/main/lib/version.d.ts +2 -0
- package/dist/main/lib/version.d.ts.map +1 -0
- package/dist/main/lib/version.js +11 -0
- package/dist/main/lib/version.js.map +1 -0
- package/dist/main/lib/websocket-factory.d.ts +81 -0
- package/dist/main/lib/websocket-factory.d.ts.map +1 -0
- package/dist/main/lib/websocket-factory.js +131 -0
- package/dist/main/lib/websocket-factory.js.map +1 -0
- package/dist/module/RealtimeChannel.d.ts +370 -0
- package/dist/module/RealtimeChannel.d.ts.map +1 -0
- package/dist/module/RealtimeChannel.js +639 -0
- package/dist/module/RealtimeChannel.js.map +1 -0
- package/dist/module/RealtimeClient.d.ts +238 -0
- package/dist/module/RealtimeClient.d.ts.map +1 -0
- package/dist/module/RealtimeClient.js +866 -0
- package/dist/module/RealtimeClient.js.map +1 -0
- package/dist/module/RealtimePresence.d.ts +76 -0
- package/dist/module/RealtimePresence.d.ts.map +1 -0
- package/dist/module/RealtimePresence.js +233 -0
- package/dist/module/RealtimePresence.js.map +1 -0
- package/dist/module/index.d.ts +6 -0
- package/dist/module/index.d.ts.map +1 -0
- package/dist/module/index.js +6 -0
- package/dist/module/index.js.map +1 -0
- package/dist/module/lib/constants.d.ts +39 -0
- package/dist/module/lib/constants.d.ts.map +1 -0
- package/dist/module/lib/constants.js +45 -0
- package/dist/module/lib/constants.js.map +1 -0
- package/dist/module/lib/push.d.ts +48 -0
- package/dist/module/lib/push.d.ts.map +1 -0
- package/dist/module/lib/push.js +99 -0
- package/dist/module/lib/push.js.map +1 -0
- package/dist/module/lib/serializer.d.ts +33 -0
- package/dist/module/lib/serializer.d.ts.map +1 -0
- package/dist/module/lib/serializer.js +152 -0
- package/dist/module/lib/serializer.js.map +1 -0
- package/dist/module/lib/timer.d.ts +22 -0
- package/dist/module/lib/timer.d.ts.map +1 -0
- package/dist/module/lib/timer.js +36 -0
- package/dist/module/lib/timer.js.map +1 -0
- package/dist/module/lib/transformers.d.ts +109 -0
- package/dist/module/lib/transformers.d.ts.map +1 -0
- package/dist/module/lib/transformers.js +229 -0
- package/dist/module/lib/transformers.js.map +1 -0
- package/dist/module/lib/version.d.ts +2 -0
- package/dist/module/lib/version.d.ts.map +1 -0
- package/dist/module/lib/version.js +8 -0
- package/dist/module/lib/version.js.map +1 -0
- package/dist/module/lib/websocket-factory.d.ts +81 -0
- package/dist/module/lib/websocket-factory.d.ts.map +1 -0
- package/dist/module/lib/websocket-factory.js +127 -0
- package/dist/module/lib/websocket-factory.js.map +1 -0
- package/dist/tsconfig.module.tsbuildinfo +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +85 -0
- package/src/RealtimeChannel.ts +1039 -0
- package/src/RealtimeClient.ts +1023 -0
- package/src/RealtimePresence.ts +346 -0
- package/src/index.ts +53 -0
- package/src/lib/constants.ts +49 -0
- package/src/lib/push.ts +121 -0
- package/src/lib/serializer.ts +203 -0
- package/src/lib/timer.ts +43 -0
- package/src/lib/transformers.ts +270 -0
- package/src/lib/version.ts +7 -0
- package/src/lib/websocket-factory.ts +192 -0
|
@@ -0,0 +1,1023 @@
|
|
|
1
|
+
import WebSocketFactory, { WebSocketLike } from './lib/websocket-factory'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CHANNEL_EVENTS,
|
|
5
|
+
CONNECTION_STATE,
|
|
6
|
+
DEFAULT_VERSION,
|
|
7
|
+
DEFAULT_TIMEOUT,
|
|
8
|
+
SOCKET_STATES,
|
|
9
|
+
TRANSPORTS,
|
|
10
|
+
DEFAULT_VSN,
|
|
11
|
+
VSN_1_0_0,
|
|
12
|
+
VSN_2_0_0,
|
|
13
|
+
WS_CLOSE_NORMAL,
|
|
14
|
+
} from './lib/constants'
|
|
15
|
+
|
|
16
|
+
import Serializer from './lib/serializer'
|
|
17
|
+
import Timer from './lib/timer'
|
|
18
|
+
|
|
19
|
+
import { httpEndpointURL } from './lib/transformers'
|
|
20
|
+
import RealtimeChannel from './RealtimeChannel'
|
|
21
|
+
import type { RealtimeChannelOptions } from './RealtimeChannel'
|
|
22
|
+
|
|
23
|
+
type Fetch = typeof fetch
|
|
24
|
+
|
|
25
|
+
export type Channel = {
|
|
26
|
+
name: string
|
|
27
|
+
inserted_at: string
|
|
28
|
+
updated_at: string
|
|
29
|
+
id: number
|
|
30
|
+
}
|
|
31
|
+
export type LogLevel = 'info' | 'warn' | 'error'
|
|
32
|
+
|
|
33
|
+
export type RealtimeMessage = {
|
|
34
|
+
topic: string
|
|
35
|
+
event: string
|
|
36
|
+
payload: any
|
|
37
|
+
ref: string
|
|
38
|
+
join_ref?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type RealtimeRemoveChannelResponse = 'ok' | 'timed out' | 'error'
|
|
42
|
+
export type HeartbeatStatus = 'sent' | 'ok' | 'error' | 'timeout' | 'disconnected'
|
|
43
|
+
|
|
44
|
+
const noop = () => {}
|
|
45
|
+
|
|
46
|
+
type RealtimeClientState = 'connecting' | 'connected' | 'disconnecting' | 'disconnected'
|
|
47
|
+
|
|
48
|
+
// Connection-related constants
|
|
49
|
+
const CONNECTION_TIMEOUTS = {
|
|
50
|
+
HEARTBEAT_INTERVAL: 25000,
|
|
51
|
+
RECONNECT_DELAY: 10,
|
|
52
|
+
HEARTBEAT_TIMEOUT_FALLBACK: 100,
|
|
53
|
+
} as const
|
|
54
|
+
|
|
55
|
+
const RECONNECT_INTERVALS = [1000, 2000, 5000, 10000] as const
|
|
56
|
+
const DEFAULT_RECONNECT_FALLBACK = 10000
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Minimal WebSocket constructor interface that RealtimeClient can work with.
|
|
60
|
+
* Supply a compatible implementation (native WebSocket, `ws`, etc) when running outside the browser.
|
|
61
|
+
*/
|
|
62
|
+
export interface WebSocketLikeConstructor {
|
|
63
|
+
new (address: string | URL, subprotocols?: string | string[] | undefined): WebSocketLike
|
|
64
|
+
// Allow additional properties that may exist on WebSocket constructors
|
|
65
|
+
[key: string]: any
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface WebSocketLikeError {
|
|
69
|
+
error: any
|
|
70
|
+
message: string
|
|
71
|
+
type: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type RealtimeClientOptions = {
|
|
75
|
+
transport?: WebSocketLikeConstructor
|
|
76
|
+
timeout?: number
|
|
77
|
+
heartbeatIntervalMs?: number
|
|
78
|
+
heartbeatCallback?: (status: HeartbeatStatus, latency?: number) => void
|
|
79
|
+
vsn?: string
|
|
80
|
+
logger?: Function
|
|
81
|
+
encode?: Function
|
|
82
|
+
decode?: Function
|
|
83
|
+
reconnectAfterMs?: Function
|
|
84
|
+
headers?: { [key: string]: string }
|
|
85
|
+
params?: { [key: string]: any }
|
|
86
|
+
//Deprecated: Use it in favour of correct casing `logLevel`
|
|
87
|
+
log_level?: LogLevel
|
|
88
|
+
logLevel?: LogLevel
|
|
89
|
+
fetch?: Fetch
|
|
90
|
+
worker?: boolean
|
|
91
|
+
workerUrl?: string
|
|
92
|
+
accessToken?: () => Promise<string | null>
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const WORKER_SCRIPT = `
|
|
96
|
+
addEventListener("message", (e) => {
|
|
97
|
+
if (e.data.event === "start") {
|
|
98
|
+
setInterval(() => postMessage({ event: "keepAlive" }), e.data.interval);
|
|
99
|
+
}
|
|
100
|
+
});`
|
|
101
|
+
|
|
102
|
+
export default class RealtimeClient {
|
|
103
|
+
accessTokenValue: string | null = null
|
|
104
|
+
apiKey: string | null = null
|
|
105
|
+
private _manuallySetToken: boolean = false
|
|
106
|
+
channels: RealtimeChannel[] = new Array()
|
|
107
|
+
endPoint: string = ''
|
|
108
|
+
httpEndpoint: string = ''
|
|
109
|
+
/** @deprecated headers cannot be set on websocket connections */
|
|
110
|
+
headers?: { [key: string]: string } = {}
|
|
111
|
+
params?: { [key: string]: string } = {}
|
|
112
|
+
timeout: number = DEFAULT_TIMEOUT
|
|
113
|
+
transport: WebSocketLikeConstructor | null = null
|
|
114
|
+
heartbeatIntervalMs: number = CONNECTION_TIMEOUTS.HEARTBEAT_INTERVAL
|
|
115
|
+
heartbeatTimer: ReturnType<typeof setInterval> | undefined = undefined
|
|
116
|
+
pendingHeartbeatRef: string | null = null
|
|
117
|
+
heartbeatCallback: (status: HeartbeatStatus, latency?: number) => void = noop
|
|
118
|
+
ref: number = 0
|
|
119
|
+
reconnectTimer: Timer | null = null
|
|
120
|
+
vsn: string = DEFAULT_VSN
|
|
121
|
+
logger: Function = noop
|
|
122
|
+
logLevel?: LogLevel
|
|
123
|
+
encode!: Function
|
|
124
|
+
decode!: Function
|
|
125
|
+
reconnectAfterMs!: Function
|
|
126
|
+
conn: WebSocketLike | null = null
|
|
127
|
+
sendBuffer: Function[] = []
|
|
128
|
+
serializer: Serializer = new Serializer()
|
|
129
|
+
stateChangeCallbacks: {
|
|
130
|
+
open: Function[]
|
|
131
|
+
close: Function[]
|
|
132
|
+
error: Function[]
|
|
133
|
+
message: Function[]
|
|
134
|
+
} = {
|
|
135
|
+
open: [],
|
|
136
|
+
close: [],
|
|
137
|
+
error: [],
|
|
138
|
+
message: [],
|
|
139
|
+
}
|
|
140
|
+
fetch: Fetch
|
|
141
|
+
accessToken: (() => Promise<string | null>) | null = null
|
|
142
|
+
worker?: boolean
|
|
143
|
+
workerUrl?: string
|
|
144
|
+
workerRef?: Worker
|
|
145
|
+
private _connectionState: RealtimeClientState = 'disconnected'
|
|
146
|
+
private _wasManualDisconnect: boolean = false
|
|
147
|
+
private _authPromise: Promise<void> | null = null
|
|
148
|
+
private _heartbeatSentAt: number | null = null
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Initializes the Socket.
|
|
152
|
+
*
|
|
153
|
+
* @param endPoint The string WebSocket endpoint, ie, "ws://example.com/socket", "wss://example.com", "/socket" (inherited host & protocol)
|
|
154
|
+
* @param httpEndpoint The string HTTP endpoint, ie, "https://example.com", "/" (inherited host & protocol)
|
|
155
|
+
* @param options.transport The Websocket Transport, for example WebSocket. This can be a custom implementation
|
|
156
|
+
* @param options.timeout The default timeout in milliseconds to trigger push timeouts.
|
|
157
|
+
* @param options.params The optional params to pass when connecting.
|
|
158
|
+
* @param options.headers Deprecated: headers cannot be set on websocket connections and this option will be removed in the future.
|
|
159
|
+
* @param options.heartbeatIntervalMs The millisec interval to send a heartbeat message.
|
|
160
|
+
* @param options.heartbeatCallback The optional function to handle heartbeat status and latency.
|
|
161
|
+
* @param options.logger The optional function for specialized logging, ie: logger: (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) }
|
|
162
|
+
* @param options.logLevel Sets the log level for Realtime
|
|
163
|
+
* @param options.encode The function to encode outgoing messages. Defaults to JSON: (payload, callback) => callback(JSON.stringify(payload))
|
|
164
|
+
* @param options.decode The function to decode incoming messages. Defaults to Serializer's decode.
|
|
165
|
+
* @param options.reconnectAfterMs he optional function that returns the millsec reconnect interval. Defaults to stepped backoff off.
|
|
166
|
+
* @param options.worker Use Web Worker to set a side flow. Defaults to false.
|
|
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.
|
|
168
|
+
* @param options.vsn The protocol version to use when connecting. Supported versions are "1.0.0" and "2.0.0". Defaults to "2.0.0".
|
|
169
|
+
* @example
|
|
170
|
+
* ```ts
|
|
171
|
+
* import RealtimeClient from '@supabase/realtime-js'
|
|
172
|
+
*
|
|
173
|
+
* const client = new RealtimeClient('https://xyzcompany.supabase.co/realtime/v1', {
|
|
174
|
+
* params: { apikey: 'public-anon-key' },
|
|
175
|
+
* })
|
|
176
|
+
* client.connect()
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
constructor(endPoint: string, options?: RealtimeClientOptions) {
|
|
180
|
+
// Validate required parameters
|
|
181
|
+
if (!options?.params?.apikey) {
|
|
182
|
+
throw new Error('API key is required to connect to Realtime')
|
|
183
|
+
}
|
|
184
|
+
this.apiKey = options.params.apikey
|
|
185
|
+
|
|
186
|
+
// Initialize endpoint URLs
|
|
187
|
+
this.endPoint = `${endPoint}/${TRANSPORTS.websocket}`
|
|
188
|
+
this.httpEndpoint = httpEndpointURL(endPoint)
|
|
189
|
+
|
|
190
|
+
this._initializeOptions(options)
|
|
191
|
+
this._setupReconnectionTimer()
|
|
192
|
+
this.fetch = this._resolveFetch(options?.fetch)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Connects the socket, unless already connected.
|
|
197
|
+
*/
|
|
198
|
+
connect(): void {
|
|
199
|
+
// Skip if already connecting, disconnecting, or connected
|
|
200
|
+
if (
|
|
201
|
+
this.isConnecting() ||
|
|
202
|
+
this.isDisconnecting() ||
|
|
203
|
+
(this.conn !== null && this.isConnected())
|
|
204
|
+
) {
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this._setConnectionState('connecting')
|
|
209
|
+
|
|
210
|
+
// Trigger auth if needed and not already in progress
|
|
211
|
+
// This ensures auth is called for standalone RealtimeClient usage
|
|
212
|
+
// while avoiding race conditions with SupabaseClient's immediate setAuth call
|
|
213
|
+
if (this.accessToken && !this._authPromise) {
|
|
214
|
+
this._setAuthSafely('connect')
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Establish WebSocket connection
|
|
218
|
+
if (this.transport) {
|
|
219
|
+
// Use custom transport if provided
|
|
220
|
+
this.conn = new this.transport(this.endpointURL()) as WebSocketLike
|
|
221
|
+
} else {
|
|
222
|
+
// Try to use native WebSocket
|
|
223
|
+
try {
|
|
224
|
+
this.conn = WebSocketFactory.createWebSocket(this.endpointURL())
|
|
225
|
+
} catch (error) {
|
|
226
|
+
this._setConnectionState('disconnected')
|
|
227
|
+
const errorMessage = (error as Error).message
|
|
228
|
+
|
|
229
|
+
// Provide helpful error message based on environment
|
|
230
|
+
if (errorMessage.includes('Node.js')) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`${errorMessage}\n\n` +
|
|
233
|
+
'To use Realtime in Node.js, you need to provide a WebSocket implementation:\n\n' +
|
|
234
|
+
'Option 1: Use Node.js 22+ which has native WebSocket support\n' +
|
|
235
|
+
'Option 2: Install and provide the "ws" package:\n\n' +
|
|
236
|
+
' npm install ws\n\n' +
|
|
237
|
+
' import ws from "ws"\n' +
|
|
238
|
+
' const client = new RealtimeClient(url, {\n' +
|
|
239
|
+
' ...options,\n' +
|
|
240
|
+
' transport: ws\n' +
|
|
241
|
+
' })'
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
throw new Error(`WebSocket not available: ${errorMessage}`)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
this._setupConnectionHandlers()
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Returns the URL of the websocket.
|
|
252
|
+
* @returns string The URL of the websocket.
|
|
253
|
+
*/
|
|
254
|
+
endpointURL(): string {
|
|
255
|
+
return this._appendParams(this.endPoint, Object.assign({}, this.params, { vsn: this.vsn }))
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Disconnects the socket.
|
|
260
|
+
*
|
|
261
|
+
* @param code A numeric status code to send on disconnect.
|
|
262
|
+
* @param reason A custom reason for the disconnect.
|
|
263
|
+
*/
|
|
264
|
+
disconnect(code?: number, reason?: string): void {
|
|
265
|
+
if (this.isDisconnecting()) {
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
this._setConnectionState('disconnecting', true)
|
|
270
|
+
|
|
271
|
+
if (this.conn) {
|
|
272
|
+
// Setup fallback timer to prevent hanging in disconnecting state
|
|
273
|
+
const fallbackTimer = setTimeout(() => {
|
|
274
|
+
this._setConnectionState('disconnected')
|
|
275
|
+
}, 100)
|
|
276
|
+
|
|
277
|
+
this.conn.onclose = () => {
|
|
278
|
+
clearTimeout(fallbackTimer)
|
|
279
|
+
this._setConnectionState('disconnected')
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Close the WebSocket connection if close method exists
|
|
283
|
+
if (typeof this.conn.close === 'function') {
|
|
284
|
+
if (code) {
|
|
285
|
+
this.conn.close(code, reason ?? '')
|
|
286
|
+
} else {
|
|
287
|
+
this.conn.close()
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
this._teardownConnection()
|
|
292
|
+
} else {
|
|
293
|
+
this._setConnectionState('disconnected')
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Returns all created channels
|
|
299
|
+
*/
|
|
300
|
+
getChannels(): RealtimeChannel[] {
|
|
301
|
+
return this.channels
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Unsubscribes and removes a single channel
|
|
306
|
+
* @param channel A RealtimeChannel instance
|
|
307
|
+
*/
|
|
308
|
+
async removeChannel(channel: RealtimeChannel): Promise<RealtimeRemoveChannelResponse> {
|
|
309
|
+
const status = await channel.unsubscribe()
|
|
310
|
+
|
|
311
|
+
if (this.channels.length === 0) {
|
|
312
|
+
this.disconnect()
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return status
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Unsubscribes and removes all channels
|
|
320
|
+
*/
|
|
321
|
+
async removeAllChannels(): Promise<RealtimeRemoveChannelResponse[]> {
|
|
322
|
+
const values_1 = await Promise.all(this.channels.map((channel) => channel.unsubscribe()))
|
|
323
|
+
this.channels = []
|
|
324
|
+
this.disconnect()
|
|
325
|
+
return values_1
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Logs the message.
|
|
330
|
+
*
|
|
331
|
+
* For customized logging, `this.logger` can be overridden.
|
|
332
|
+
*/
|
|
333
|
+
log(kind: string, msg: string, data?: any) {
|
|
334
|
+
this.logger(kind, msg, data)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Returns the current state of the socket.
|
|
339
|
+
*/
|
|
340
|
+
connectionState(): CONNECTION_STATE {
|
|
341
|
+
switch (this.conn && this.conn.readyState) {
|
|
342
|
+
case SOCKET_STATES.connecting:
|
|
343
|
+
return CONNECTION_STATE.Connecting
|
|
344
|
+
case SOCKET_STATES.open:
|
|
345
|
+
return CONNECTION_STATE.Open
|
|
346
|
+
case SOCKET_STATES.closing:
|
|
347
|
+
return CONNECTION_STATE.Closing
|
|
348
|
+
default:
|
|
349
|
+
return CONNECTION_STATE.Closed
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Returns `true` is the connection is open.
|
|
355
|
+
*/
|
|
356
|
+
isConnected(): boolean {
|
|
357
|
+
return this.connectionState() === CONNECTION_STATE.Open
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Returns `true` if the connection is currently connecting.
|
|
362
|
+
*/
|
|
363
|
+
isConnecting(): boolean {
|
|
364
|
+
return this._connectionState === 'connecting'
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Returns `true` if the connection is currently disconnecting.
|
|
369
|
+
*/
|
|
370
|
+
isDisconnecting(): boolean {
|
|
371
|
+
return this._connectionState === 'disconnecting'
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Creates (or reuses) a {@link RealtimeChannel} for the provided topic.
|
|
376
|
+
*
|
|
377
|
+
* Topics are automatically prefixed with `realtime:` to match the Realtime service.
|
|
378
|
+
* If a channel with the same topic already exists it will be returned instead of creating
|
|
379
|
+
* a duplicate connection.
|
|
380
|
+
*/
|
|
381
|
+
channel(topic: string, params: RealtimeChannelOptions = { config: {} }): RealtimeChannel {
|
|
382
|
+
const realtimeTopic = `realtime:${topic}`
|
|
383
|
+
const exists = this.getChannels().find((c: RealtimeChannel) => c.topic === realtimeTopic)
|
|
384
|
+
|
|
385
|
+
if (!exists) {
|
|
386
|
+
const chan = new RealtimeChannel(`realtime:${topic}`, params, this)
|
|
387
|
+
this.channels.push(chan)
|
|
388
|
+
|
|
389
|
+
return chan
|
|
390
|
+
} else {
|
|
391
|
+
return exists
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Push out a message if the socket is connected.
|
|
397
|
+
*
|
|
398
|
+
* If the socket is not connected, the message gets enqueued within a local buffer, and sent out when a connection is next established.
|
|
399
|
+
*/
|
|
400
|
+
push(data: RealtimeMessage): void {
|
|
401
|
+
const { topic, event, payload, ref } = data
|
|
402
|
+
const callback = () => {
|
|
403
|
+
this.encode(data, (result: any) => {
|
|
404
|
+
this.conn?.send(result)
|
|
405
|
+
})
|
|
406
|
+
}
|
|
407
|
+
this.log('push', `${topic} ${event} (${ref})`, payload)
|
|
408
|
+
if (this.isConnected()) {
|
|
409
|
+
callback()
|
|
410
|
+
} else {
|
|
411
|
+
this.sendBuffer.push(callback)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Sets the JWT access token used for channel subscription authorization and Realtime RLS.
|
|
417
|
+
*
|
|
418
|
+
* If param is null it will use the `accessToken` callback function or the token set on the client.
|
|
419
|
+
*
|
|
420
|
+
* On callback used, it will set the value of the token internal to the client.
|
|
421
|
+
*
|
|
422
|
+
* When a token is explicitly provided, it will be preserved across channel operations
|
|
423
|
+
* (including removeChannel and resubscribe). The `accessToken` callback will not be
|
|
424
|
+
* invoked until `setAuth()` is called without arguments.
|
|
425
|
+
*
|
|
426
|
+
* @param token A JWT string to override the token set on the client.
|
|
427
|
+
*
|
|
428
|
+
* @example
|
|
429
|
+
* // Use a manual token (preserved across resubscribes, ignores accessToken callback)
|
|
430
|
+
* client.realtime.setAuth('my-custom-jwt')
|
|
431
|
+
*
|
|
432
|
+
* // Switch back to using the accessToken callback
|
|
433
|
+
* client.realtime.setAuth()
|
|
434
|
+
*/
|
|
435
|
+
async setAuth(token: string | null = null): Promise<void> {
|
|
436
|
+
this._authPromise = this._performAuth(token)
|
|
437
|
+
try {
|
|
438
|
+
await this._authPromise
|
|
439
|
+
} finally {
|
|
440
|
+
this._authPromise = null
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Returns true if the current access token was explicitly set via setAuth(token),
|
|
446
|
+
* false if it was obtained via the accessToken callback.
|
|
447
|
+
* @internal
|
|
448
|
+
*/
|
|
449
|
+
_isManualToken(): boolean {
|
|
450
|
+
return this._manuallySetToken
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Sends a heartbeat message if the socket is connected.
|
|
455
|
+
*/
|
|
456
|
+
async sendHeartbeat() {
|
|
457
|
+
if (!this.isConnected()) {
|
|
458
|
+
try {
|
|
459
|
+
this.heartbeatCallback('disconnected')
|
|
460
|
+
} catch (e) {
|
|
461
|
+
this.log('error', 'error in heartbeat callback', e)
|
|
462
|
+
}
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Handle heartbeat timeout and force reconnection if needed
|
|
467
|
+
if (this.pendingHeartbeatRef) {
|
|
468
|
+
this.pendingHeartbeatRef = null
|
|
469
|
+
this._heartbeatSentAt = null
|
|
470
|
+
this.log('transport', 'heartbeat timeout. Attempting to re-establish connection')
|
|
471
|
+
try {
|
|
472
|
+
this.heartbeatCallback('timeout')
|
|
473
|
+
} catch (e) {
|
|
474
|
+
this.log('error', 'error in heartbeat callback', e)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Force reconnection after heartbeat timeout
|
|
478
|
+
this._wasManualDisconnect = false
|
|
479
|
+
this.conn?.close(WS_CLOSE_NORMAL, 'heartbeat timeout')
|
|
480
|
+
|
|
481
|
+
setTimeout(() => {
|
|
482
|
+
if (!this.isConnected()) {
|
|
483
|
+
this.reconnectTimer?.scheduleTimeout()
|
|
484
|
+
}
|
|
485
|
+
}, CONNECTION_TIMEOUTS.HEARTBEAT_TIMEOUT_FALLBACK)
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Send heartbeat message to server
|
|
490
|
+
this._heartbeatSentAt = Date.now()
|
|
491
|
+
this.pendingHeartbeatRef = this._makeRef()
|
|
492
|
+
this.push({
|
|
493
|
+
topic: 'phoenix',
|
|
494
|
+
event: 'heartbeat',
|
|
495
|
+
payload: {},
|
|
496
|
+
ref: this.pendingHeartbeatRef,
|
|
497
|
+
})
|
|
498
|
+
try {
|
|
499
|
+
this.heartbeatCallback('sent')
|
|
500
|
+
} catch (e) {
|
|
501
|
+
this.log('error', 'error in heartbeat callback', e)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
this._setAuthSafely('heartbeat')
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Sets a callback that receives lifecycle events for internal heartbeat messages.
|
|
509
|
+
* Useful for instrumenting connection health (e.g. sent/ok/timeout/disconnected).
|
|
510
|
+
*/
|
|
511
|
+
onHeartbeat(callback: (status: HeartbeatStatus, latency?: number) => void): void {
|
|
512
|
+
this.heartbeatCallback = callback
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Flushes send buffer
|
|
516
|
+
*/
|
|
517
|
+
flushSendBuffer() {
|
|
518
|
+
if (this.isConnected() && this.sendBuffer.length > 0) {
|
|
519
|
+
this.sendBuffer.forEach((callback) => callback())
|
|
520
|
+
this.sendBuffer = []
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Use either custom fetch, if provided, or default fetch to make HTTP requests
|
|
526
|
+
*
|
|
527
|
+
* @internal
|
|
528
|
+
*/
|
|
529
|
+
_resolveFetch = (customFetch?: Fetch): Fetch => {
|
|
530
|
+
if (customFetch) {
|
|
531
|
+
return (...args) => customFetch(...args)
|
|
532
|
+
}
|
|
533
|
+
return (...args) => fetch(...args)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Return the next message ref, accounting for overflows
|
|
538
|
+
*
|
|
539
|
+
* @internal
|
|
540
|
+
*/
|
|
541
|
+
_makeRef(): string {
|
|
542
|
+
let newRef = this.ref + 1
|
|
543
|
+
if (newRef === this.ref) {
|
|
544
|
+
this.ref = 0
|
|
545
|
+
} else {
|
|
546
|
+
this.ref = newRef
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return this.ref.toString()
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Unsubscribe from channels with the specified topic.
|
|
554
|
+
*
|
|
555
|
+
* @internal
|
|
556
|
+
*/
|
|
557
|
+
_leaveOpenTopic(topic: string): void {
|
|
558
|
+
let dupChannel = this.channels.find(
|
|
559
|
+
(c) => c.topic === topic && (c._isJoined() || c._isJoining())
|
|
560
|
+
)
|
|
561
|
+
if (dupChannel) {
|
|
562
|
+
this.log('transport', `leaving duplicate topic "${topic}"`)
|
|
563
|
+
dupChannel.unsubscribe()
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Removes a subscription from the socket.
|
|
569
|
+
*
|
|
570
|
+
* @param channel An open subscription.
|
|
571
|
+
*
|
|
572
|
+
* @internal
|
|
573
|
+
*/
|
|
574
|
+
_remove(channel: RealtimeChannel) {
|
|
575
|
+
this.channels = this.channels.filter((c) => c.topic !== channel.topic)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** @internal */
|
|
579
|
+
private _onConnMessage(rawMessage: { data: any }) {
|
|
580
|
+
this.decode(rawMessage.data, (msg: RealtimeMessage) => {
|
|
581
|
+
// Handle heartbeat responses
|
|
582
|
+
if (
|
|
583
|
+
msg.topic === 'phoenix' &&
|
|
584
|
+
msg.event === 'phx_reply' &&
|
|
585
|
+
msg.ref &&
|
|
586
|
+
msg.ref === this.pendingHeartbeatRef
|
|
587
|
+
) {
|
|
588
|
+
const latency = this._heartbeatSentAt ? Date.now() - this._heartbeatSentAt : undefined
|
|
589
|
+
try {
|
|
590
|
+
this.heartbeatCallback(msg.payload.status === 'ok' ? 'ok' : 'error', latency)
|
|
591
|
+
} catch (e) {
|
|
592
|
+
this.log('error', 'error in heartbeat callback', e)
|
|
593
|
+
}
|
|
594
|
+
this._heartbeatSentAt = null
|
|
595
|
+
this.pendingHeartbeatRef = null
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Log incoming message
|
|
599
|
+
const { topic, event, payload, ref } = msg
|
|
600
|
+
const refString = ref ? `(${ref})` : ''
|
|
601
|
+
const status = payload.status || ''
|
|
602
|
+
this.log('receive', `${status} ${topic} ${event} ${refString}`.trim(), payload)
|
|
603
|
+
|
|
604
|
+
// Route message to appropriate channels
|
|
605
|
+
this.channels
|
|
606
|
+
.filter((channel: RealtimeChannel) => channel._isMember(topic))
|
|
607
|
+
.forEach((channel: RealtimeChannel) => channel._trigger(event, payload, ref))
|
|
608
|
+
|
|
609
|
+
this._triggerStateCallbacks('message', msg)
|
|
610
|
+
})
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Clear specific timer
|
|
615
|
+
* @internal
|
|
616
|
+
*/
|
|
617
|
+
private _clearTimer(timer: 'heartbeat' | 'reconnect'): void {
|
|
618
|
+
if (timer === 'heartbeat' && this.heartbeatTimer) {
|
|
619
|
+
clearInterval(this.heartbeatTimer)
|
|
620
|
+
this.heartbeatTimer = undefined
|
|
621
|
+
} else if (timer === 'reconnect') {
|
|
622
|
+
this.reconnectTimer?.reset()
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Clear all timers
|
|
628
|
+
* @internal
|
|
629
|
+
*/
|
|
630
|
+
private _clearAllTimers(): void {
|
|
631
|
+
this._clearTimer('heartbeat')
|
|
632
|
+
this._clearTimer('reconnect')
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Setup connection handlers for WebSocket events
|
|
637
|
+
* @internal
|
|
638
|
+
*/
|
|
639
|
+
private _setupConnectionHandlers(): void {
|
|
640
|
+
if (!this.conn) return
|
|
641
|
+
|
|
642
|
+
// Set binary type if supported (browsers and most WebSocket implementations)
|
|
643
|
+
if ('binaryType' in this.conn) {
|
|
644
|
+
;(this.conn as any).binaryType = 'arraybuffer'
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
this.conn.onopen = () => this._onConnOpen()
|
|
648
|
+
this.conn.onerror = (error: Event) => this._onConnError(error)
|
|
649
|
+
this.conn.onmessage = (event: any) => this._onConnMessage(event)
|
|
650
|
+
this.conn.onclose = (event: any) => this._onConnClose(event)
|
|
651
|
+
|
|
652
|
+
if (this.conn.readyState === SOCKET_STATES.open) {
|
|
653
|
+
this._onConnOpen()
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Teardown connection and cleanup resources
|
|
659
|
+
* @internal
|
|
660
|
+
*/
|
|
661
|
+
private _teardownConnection(): void {
|
|
662
|
+
if (this.conn) {
|
|
663
|
+
if (
|
|
664
|
+
this.conn.readyState === SOCKET_STATES.open ||
|
|
665
|
+
this.conn.readyState === SOCKET_STATES.connecting
|
|
666
|
+
) {
|
|
667
|
+
try {
|
|
668
|
+
this.conn.close()
|
|
669
|
+
} catch (e) {
|
|
670
|
+
this.log('error', 'Error closing connection', e)
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
this.conn.onopen = null
|
|
675
|
+
this.conn.onerror = null
|
|
676
|
+
this.conn.onmessage = null
|
|
677
|
+
this.conn.onclose = null
|
|
678
|
+
this.conn = null
|
|
679
|
+
}
|
|
680
|
+
this._clearAllTimers()
|
|
681
|
+
this._terminateWorker()
|
|
682
|
+
this.channels.forEach((channel) => channel.teardown())
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/** @internal */
|
|
686
|
+
private _onConnOpen() {
|
|
687
|
+
this._setConnectionState('connected')
|
|
688
|
+
this.log('transport', `connected to ${this.endpointURL()}`)
|
|
689
|
+
|
|
690
|
+
// Wait for any pending auth operations before flushing send buffer
|
|
691
|
+
// This ensures channel join messages include the correct access token
|
|
692
|
+
const authPromise =
|
|
693
|
+
this._authPromise ||
|
|
694
|
+
(this.accessToken && !this.accessTokenValue ? this.setAuth() : Promise.resolve())
|
|
695
|
+
|
|
696
|
+
authPromise
|
|
697
|
+
.then(() => {
|
|
698
|
+
// When subscribe() is called before the accessToken callback has
|
|
699
|
+
// resolved (common on React Native / Expo where token storage is
|
|
700
|
+
// async), the phx_join payload captured at subscribe()-time will
|
|
701
|
+
// have no access_token. By this point auth has settled and
|
|
702
|
+
// this.accessTokenValue holds the real JWT.
|
|
703
|
+
//
|
|
704
|
+
// The stale join messages sitting in sendBuffer captured the old
|
|
705
|
+
// (token-less) payload in a closure, so we cannot simply flush
|
|
706
|
+
// them. Instead we:
|
|
707
|
+
// 1. Patch each channel's joinPush payload with the real token
|
|
708
|
+
// 2. Drop the stale buffered messages
|
|
709
|
+
// 3. Re-send the join for any channel still in "joining" state
|
|
710
|
+
//
|
|
711
|
+
// On browsers this is a harmless no-op: accessTokenValue was
|
|
712
|
+
// already set synchronously before subscribe() ran, so the join
|
|
713
|
+
// payload already had the correct token.
|
|
714
|
+
if (this.accessTokenValue) {
|
|
715
|
+
this.channels.forEach((channel) => {
|
|
716
|
+
channel.updateJoinPayload({ access_token: this.accessTokenValue })
|
|
717
|
+
})
|
|
718
|
+
this.sendBuffer = []
|
|
719
|
+
this.channels.forEach((channel) => {
|
|
720
|
+
if (channel._isJoining()) {
|
|
721
|
+
channel.joinPush.sent = false
|
|
722
|
+
channel.joinPush.send()
|
|
723
|
+
}
|
|
724
|
+
})
|
|
725
|
+
}
|
|
726
|
+
this.flushSendBuffer()
|
|
727
|
+
})
|
|
728
|
+
.catch((e) => {
|
|
729
|
+
this.log('error', 'error waiting for auth on connect', e)
|
|
730
|
+
// Proceed anyway to avoid hanging connections
|
|
731
|
+
this.flushSendBuffer()
|
|
732
|
+
})
|
|
733
|
+
|
|
734
|
+
this._clearTimer('reconnect')
|
|
735
|
+
|
|
736
|
+
if (!this.worker) {
|
|
737
|
+
this._startHeartbeat()
|
|
738
|
+
} else {
|
|
739
|
+
if (!this.workerRef) {
|
|
740
|
+
this._startWorkerHeartbeat()
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
this._triggerStateCallbacks('open')
|
|
745
|
+
}
|
|
746
|
+
/** @internal */
|
|
747
|
+
private _startHeartbeat() {
|
|
748
|
+
this.heartbeatTimer && clearInterval(this.heartbeatTimer)
|
|
749
|
+
this.heartbeatTimer = setInterval(() => this.sendHeartbeat(), this.heartbeatIntervalMs)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/** @internal */
|
|
753
|
+
private _startWorkerHeartbeat() {
|
|
754
|
+
if (this.workerUrl) {
|
|
755
|
+
this.log('worker', `starting worker for from ${this.workerUrl}`)
|
|
756
|
+
} else {
|
|
757
|
+
this.log('worker', `starting default worker`)
|
|
758
|
+
}
|
|
759
|
+
const objectUrl = this._workerObjectUrl(this.workerUrl!)
|
|
760
|
+
this.workerRef = new Worker(objectUrl)
|
|
761
|
+
this.workerRef.onerror = (error) => {
|
|
762
|
+
this.log('worker', 'worker error', (error as ErrorEvent).message)
|
|
763
|
+
this._terminateWorker()
|
|
764
|
+
}
|
|
765
|
+
this.workerRef.onmessage = (event) => {
|
|
766
|
+
if (event.data.event === 'keepAlive') {
|
|
767
|
+
this.sendHeartbeat()
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
this.workerRef.postMessage({
|
|
771
|
+
event: 'start',
|
|
772
|
+
interval: this.heartbeatIntervalMs,
|
|
773
|
+
})
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Terminate the Web Worker and clear the reference
|
|
778
|
+
* @internal
|
|
779
|
+
*/
|
|
780
|
+
private _terminateWorker(): void {
|
|
781
|
+
if (this.workerRef) {
|
|
782
|
+
this.log('worker', 'terminating worker')
|
|
783
|
+
this.workerRef.terminate()
|
|
784
|
+
this.workerRef = undefined
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
/** @internal */
|
|
788
|
+
private _onConnClose(event: any) {
|
|
789
|
+
this._setConnectionState('disconnected')
|
|
790
|
+
this.log('transport', 'close', event)
|
|
791
|
+
this._triggerChanError()
|
|
792
|
+
this._clearTimer('heartbeat')
|
|
793
|
+
|
|
794
|
+
// Only schedule reconnection if it wasn't a manual disconnect
|
|
795
|
+
if (!this._wasManualDisconnect) {
|
|
796
|
+
this.reconnectTimer?.scheduleTimeout()
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
this._triggerStateCallbacks('close', event)
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/** @internal */
|
|
803
|
+
private _onConnError(error: Event) {
|
|
804
|
+
this._setConnectionState('disconnected')
|
|
805
|
+
this.log('transport', `${error}`)
|
|
806
|
+
this._triggerChanError()
|
|
807
|
+
this._triggerStateCallbacks('error', error)
|
|
808
|
+
try {
|
|
809
|
+
this.heartbeatCallback('error')
|
|
810
|
+
} catch (e) {
|
|
811
|
+
this.log('error', 'error in heartbeat callback', e)
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/** @internal */
|
|
816
|
+
private _triggerChanError() {
|
|
817
|
+
this.channels.forEach((channel: RealtimeChannel) => channel._trigger(CHANNEL_EVENTS.error))
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/** @internal */
|
|
821
|
+
private _appendParams(url: string, params: { [key: string]: string }): string {
|
|
822
|
+
if (Object.keys(params).length === 0) {
|
|
823
|
+
return url
|
|
824
|
+
}
|
|
825
|
+
const prefix = url.match(/\?/) ? '&' : '?'
|
|
826
|
+
const query = new URLSearchParams(params)
|
|
827
|
+
return `${url}${prefix}${query}`
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
private _workerObjectUrl(url: string | undefined): string {
|
|
831
|
+
let result_url: string
|
|
832
|
+
if (url) {
|
|
833
|
+
result_url = url
|
|
834
|
+
} else {
|
|
835
|
+
const blob = new Blob([WORKER_SCRIPT], { type: 'application/javascript' })
|
|
836
|
+
result_url = URL.createObjectURL(blob)
|
|
837
|
+
}
|
|
838
|
+
return result_url
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Set connection state with proper state management
|
|
843
|
+
* @internal
|
|
844
|
+
*/
|
|
845
|
+
private _setConnectionState(state: RealtimeClientState, manual = false): void {
|
|
846
|
+
this._connectionState = state
|
|
847
|
+
|
|
848
|
+
if (state === 'connecting') {
|
|
849
|
+
this._wasManualDisconnect = false
|
|
850
|
+
} else if (state === 'disconnecting') {
|
|
851
|
+
this._wasManualDisconnect = manual
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Perform the actual auth operation
|
|
857
|
+
* @internal
|
|
858
|
+
*/
|
|
859
|
+
private async _performAuth(token: string | null = null): Promise<void> {
|
|
860
|
+
let tokenToSend: string | null
|
|
861
|
+
let isManualToken = false
|
|
862
|
+
|
|
863
|
+
if (token) {
|
|
864
|
+
tokenToSend = token
|
|
865
|
+
// Track if this is a manually-provided token
|
|
866
|
+
isManualToken = true
|
|
867
|
+
} else if (this.accessToken) {
|
|
868
|
+
// Call the accessToken callback to get fresh token
|
|
869
|
+
try {
|
|
870
|
+
tokenToSend = await this.accessToken()
|
|
871
|
+
} catch (e) {
|
|
872
|
+
this.log('error', 'Error fetching access token from callback', e)
|
|
873
|
+
// Fall back to cached value if callback fails
|
|
874
|
+
tokenToSend = this.accessTokenValue
|
|
875
|
+
}
|
|
876
|
+
} else {
|
|
877
|
+
tokenToSend = this.accessTokenValue
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Track whether this token was manually set or fetched via callback
|
|
881
|
+
if (isManualToken) {
|
|
882
|
+
this._manuallySetToken = true
|
|
883
|
+
} else if (this.accessToken) {
|
|
884
|
+
// If we used the callback, clear the manual flag
|
|
885
|
+
this._manuallySetToken = false
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
if (this.accessTokenValue != tokenToSend) {
|
|
889
|
+
this.accessTokenValue = tokenToSend
|
|
890
|
+
this.channels.forEach((channel) => {
|
|
891
|
+
const payload = {
|
|
892
|
+
access_token: tokenToSend,
|
|
893
|
+
version: DEFAULT_VERSION,
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
tokenToSend && channel.updateJoinPayload(payload)
|
|
897
|
+
|
|
898
|
+
if (channel.joinedOnce && channel._isJoined()) {
|
|
899
|
+
channel._push(CHANNEL_EVENTS.access_token, {
|
|
900
|
+
access_token: tokenToSend,
|
|
901
|
+
})
|
|
902
|
+
}
|
|
903
|
+
})
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Wait for any in-flight auth operations to complete
|
|
909
|
+
* @internal
|
|
910
|
+
*/
|
|
911
|
+
private async _waitForAuthIfNeeded(): Promise<void> {
|
|
912
|
+
if (this._authPromise) {
|
|
913
|
+
await this._authPromise
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Safely call setAuth with standardized error handling
|
|
919
|
+
* @internal
|
|
920
|
+
*/
|
|
921
|
+
private _setAuthSafely(context = 'general'): void {
|
|
922
|
+
// Only refresh auth if using callback-based tokens
|
|
923
|
+
if (!this._isManualToken()) {
|
|
924
|
+
this.setAuth().catch((e) => {
|
|
925
|
+
this.log('error', `Error setting auth in ${context}`, e)
|
|
926
|
+
})
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Trigger state change callbacks with proper error handling
|
|
932
|
+
* @internal
|
|
933
|
+
*/
|
|
934
|
+
private _triggerStateCallbacks(event: keyof typeof this.stateChangeCallbacks, data?: any): void {
|
|
935
|
+
try {
|
|
936
|
+
this.stateChangeCallbacks[event].forEach((callback) => {
|
|
937
|
+
try {
|
|
938
|
+
callback(data)
|
|
939
|
+
} catch (e) {
|
|
940
|
+
this.log('error', `error in ${event} callback`, e)
|
|
941
|
+
}
|
|
942
|
+
})
|
|
943
|
+
} catch (e) {
|
|
944
|
+
this.log('error', `error triggering ${event} callbacks`, e)
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Setup reconnection timer with proper configuration
|
|
950
|
+
* @internal
|
|
951
|
+
*/
|
|
952
|
+
private _setupReconnectionTimer(): void {
|
|
953
|
+
this.reconnectTimer = new Timer(async () => {
|
|
954
|
+
setTimeout(async () => {
|
|
955
|
+
await this._waitForAuthIfNeeded()
|
|
956
|
+
if (!this.isConnected()) {
|
|
957
|
+
this.connect()
|
|
958
|
+
}
|
|
959
|
+
}, CONNECTION_TIMEOUTS.RECONNECT_DELAY)
|
|
960
|
+
}, this.reconnectAfterMs)
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Initialize client options with defaults
|
|
965
|
+
* @internal
|
|
966
|
+
*/
|
|
967
|
+
private _initializeOptions(options?: RealtimeClientOptions): void {
|
|
968
|
+
// Set defaults
|
|
969
|
+
this.transport = options?.transport ?? null
|
|
970
|
+
this.timeout = options?.timeout ?? DEFAULT_TIMEOUT
|
|
971
|
+
this.heartbeatIntervalMs =
|
|
972
|
+
options?.heartbeatIntervalMs ?? CONNECTION_TIMEOUTS.HEARTBEAT_INTERVAL
|
|
973
|
+
this.worker = options?.worker ?? false
|
|
974
|
+
this.accessToken = options?.accessToken ?? null
|
|
975
|
+
this.heartbeatCallback = options?.heartbeatCallback ?? noop
|
|
976
|
+
this.vsn = options?.vsn ?? DEFAULT_VSN
|
|
977
|
+
|
|
978
|
+
// Handle special cases
|
|
979
|
+
if (options?.params) this.params = options.params
|
|
980
|
+
if (options?.logger) this.logger = options.logger
|
|
981
|
+
if (options?.logLevel || options?.log_level) {
|
|
982
|
+
this.logLevel = options.logLevel || options.log_level
|
|
983
|
+
this.params = { ...this.params, log_level: this.logLevel as string }
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Set up functions with defaults
|
|
987
|
+
this.reconnectAfterMs =
|
|
988
|
+
options?.reconnectAfterMs ??
|
|
989
|
+
((tries: number) => {
|
|
990
|
+
return RECONNECT_INTERVALS[tries - 1] || DEFAULT_RECONNECT_FALLBACK
|
|
991
|
+
})
|
|
992
|
+
|
|
993
|
+
switch (this.vsn) {
|
|
994
|
+
case VSN_1_0_0:
|
|
995
|
+
this.encode =
|
|
996
|
+
options?.encode ??
|
|
997
|
+
((payload: JSON, callback: Function) => {
|
|
998
|
+
return callback(JSON.stringify(payload))
|
|
999
|
+
})
|
|
1000
|
+
|
|
1001
|
+
this.decode =
|
|
1002
|
+
options?.decode ??
|
|
1003
|
+
((payload: string, callback: Function) => {
|
|
1004
|
+
return callback(JSON.parse(payload))
|
|
1005
|
+
})
|
|
1006
|
+
break
|
|
1007
|
+
case VSN_2_0_0:
|
|
1008
|
+
this.encode = options?.encode ?? this.serializer.encode.bind(this.serializer)
|
|
1009
|
+
this.decode = options?.decode ?? this.serializer.decode.bind(this.serializer)
|
|
1010
|
+
break
|
|
1011
|
+
default:
|
|
1012
|
+
throw new Error(`Unsupported serializer version: ${this.vsn}`)
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Handle worker setup
|
|
1016
|
+
if (this.worker) {
|
|
1017
|
+
if (typeof window !== 'undefined' && !window.Worker) {
|
|
1018
|
+
throw new Error('Web Worker is not supported')
|
|
1019
|
+
}
|
|
1020
|
+
this.workerUrl = options?.workerUrl
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|