@supabase/realtime-js 2.99.3-canary.0 → 2.99.3
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 +28 -35
- package/dist/main/RealtimeChannel.d.ts.map +1 -1
- package/dist/main/RealtimeChannel.js +301 -140
- package/dist/main/RealtimeChannel.js.map +1 -1
- package/dist/main/RealtimeClient.d.ts +57 -38
- package/dist/main/RealtimeClient.d.ts.map +1 -1
- package/dist/main/RealtimeClient.js +520 -232
- package/dist/main/RealtimeClient.js.map +1 -1
- package/dist/main/RealtimePresence.d.ts +24 -8
- package/dist/main/RealtimePresence.d.ts.map +1 -1
- package/dist/main/RealtimePresence.js +202 -6
- package/dist/main/RealtimePresence.js.map +1 -1
- package/dist/main/lib/constants.d.ts +35 -39
- package/dist/main/lib/constants.d.ts.map +1 -1
- package/dist/main/lib/constants.js +35 -30
- package/dist/main/lib/constants.js.map +1 -1
- 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/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/version.d.ts +1 -1
- package/dist/main/lib/version.d.ts.map +1 -1
- package/dist/main/lib/version.js +1 -1
- package/dist/main/lib/version.js.map +1 -1
- package/dist/main/lib/websocket-factory.d.ts +9 -0
- package/dist/main/lib/websocket-factory.d.ts.map +1 -1
- package/dist/main/lib/websocket-factory.js +12 -0
- package/dist/main/lib/websocket-factory.js.map +1 -1
- package/dist/module/RealtimeChannel.d.ts +28 -35
- package/dist/module/RealtimeChannel.d.ts.map +1 -1
- package/dist/module/RealtimeChannel.js +302 -141
- package/dist/module/RealtimeChannel.js.map +1 -1
- package/dist/module/RealtimeClient.d.ts +57 -38
- package/dist/module/RealtimeClient.d.ts.map +1 -1
- package/dist/module/RealtimeClient.js +521 -233
- package/dist/module/RealtimeClient.js.map +1 -1
- package/dist/module/RealtimePresence.d.ts +24 -8
- package/dist/module/RealtimePresence.d.ts.map +1 -1
- package/dist/module/RealtimePresence.js +202 -5
- package/dist/module/RealtimePresence.js.map +1 -1
- package/dist/module/lib/constants.d.ts +35 -39
- package/dist/module/lib/constants.d.ts.map +1 -1
- package/dist/module/lib/constants.js +35 -30
- package/dist/module/lib/constants.js.map +1 -1
- 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/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/version.d.ts +1 -1
- package/dist/module/lib/version.d.ts.map +1 -1
- package/dist/module/lib/version.js +1 -1
- package/dist/module/lib/version.js.map +1 -1
- package/dist/module/lib/websocket-factory.d.ts +9 -0
- package/dist/module/lib/websocket-factory.d.ts.map +1 -1
- package/dist/module/lib/websocket-factory.js +12 -0
- package/dist/module/lib/websocket-factory.js.map +1 -1
- package/dist/tsconfig.module.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/RealtimeChannel.ts +364 -201
- package/src/RealtimeClient.ts +583 -296
- package/src/RealtimePresence.ts +287 -10
- package/src/lib/constants.ts +37 -50
- package/src/lib/push.ts +121 -0
- package/src/lib/timer.ts +43 -0
- package/src/lib/version.ts +1 -1
- package/src/lib/websocket-factory.ts +13 -0
- package/dist/main/phoenix/channelAdapter.d.ts +0 -32
- package/dist/main/phoenix/channelAdapter.d.ts.map +0 -1
- package/dist/main/phoenix/channelAdapter.js +0 -103
- package/dist/main/phoenix/channelAdapter.js.map +0 -1
- package/dist/main/phoenix/presenceAdapter.d.ts +0 -53
- package/dist/main/phoenix/presenceAdapter.d.ts.map +0 -1
- package/dist/main/phoenix/presenceAdapter.js +0 -93
- package/dist/main/phoenix/presenceAdapter.js.map +0 -1
- package/dist/main/phoenix/socketAdapter.d.ts +0 -38
- package/dist/main/phoenix/socketAdapter.d.ts.map +0 -1
- package/dist/main/phoenix/socketAdapter.js +0 -114
- package/dist/main/phoenix/socketAdapter.js.map +0 -1
- package/dist/main/phoenix/types.d.ts +0 -5
- package/dist/main/phoenix/types.d.ts.map +0 -1
- package/dist/main/phoenix/types.js +0 -3
- package/dist/main/phoenix/types.js.map +0 -1
- package/dist/module/phoenix/channelAdapter.d.ts +0 -32
- package/dist/module/phoenix/channelAdapter.d.ts.map +0 -1
- package/dist/module/phoenix/channelAdapter.js +0 -100
- package/dist/module/phoenix/channelAdapter.js.map +0 -1
- package/dist/module/phoenix/presenceAdapter.d.ts +0 -53
- package/dist/module/phoenix/presenceAdapter.d.ts.map +0 -1
- package/dist/module/phoenix/presenceAdapter.js +0 -90
- package/dist/module/phoenix/presenceAdapter.js.map +0 -1
- package/dist/module/phoenix/socketAdapter.d.ts +0 -38
- package/dist/module/phoenix/socketAdapter.d.ts.map +0 -1
- package/dist/module/phoenix/socketAdapter.js +0 -111
- package/dist/module/phoenix/socketAdapter.js.map +0 -1
- package/dist/module/phoenix/types.d.ts +0 -5
- package/dist/module/phoenix/types.d.ts.map +0 -1
- package/dist/module/phoenix/types.js +0 -2
- package/dist/module/phoenix/types.js.map +0 -1
- package/src/phoenix/channelAdapter.ts +0 -147
- package/src/phoenix/presenceAdapter.ts +0 -116
- package/src/phoenix/socketAdapter.ts +0 -168
- package/src/phoenix/types.ts +0 -32
package/src/RealtimeClient.ts
CHANGED
|
@@ -5,27 +5,29 @@ import {
|
|
|
5
5
|
CONNECTION_STATE,
|
|
6
6
|
DEFAULT_VERSION,
|
|
7
7
|
DEFAULT_TIMEOUT,
|
|
8
|
+
SOCKET_STATES,
|
|
9
|
+
TRANSPORTS,
|
|
8
10
|
DEFAULT_VSN,
|
|
9
11
|
VSN_1_0_0,
|
|
10
12
|
VSN_2_0_0,
|
|
13
|
+
WS_CLOSE_NORMAL,
|
|
11
14
|
} from './lib/constants'
|
|
12
15
|
|
|
13
16
|
import Serializer from './lib/serializer'
|
|
17
|
+
import Timer from './lib/timer'
|
|
18
|
+
|
|
14
19
|
import { httpEndpointURL } from './lib/transformers'
|
|
15
20
|
import RealtimeChannel from './RealtimeChannel'
|
|
16
21
|
import type { RealtimeChannelOptions } from './RealtimeChannel'
|
|
17
|
-
import SocketAdapter from './phoenix/socketAdapter'
|
|
18
|
-
import type {
|
|
19
|
-
Message,
|
|
20
|
-
SocketOptions,
|
|
21
|
-
HeartbeatCallback,
|
|
22
|
-
Encode,
|
|
23
|
-
Decode,
|
|
24
|
-
Vsn,
|
|
25
|
-
} from './phoenix/types'
|
|
26
22
|
|
|
27
23
|
type Fetch = typeof fetch
|
|
28
24
|
|
|
25
|
+
export type Channel = {
|
|
26
|
+
name: string
|
|
27
|
+
inserted_at: string
|
|
28
|
+
updated_at: string
|
|
29
|
+
id: number
|
|
30
|
+
}
|
|
29
31
|
export type LogLevel = 'info' | 'warn' | 'error'
|
|
30
32
|
|
|
31
33
|
export type RealtimeMessage = {
|
|
@@ -38,7 +40,10 @@ export type RealtimeMessage = {
|
|
|
38
40
|
|
|
39
41
|
export type RealtimeRemoveChannelResponse = 'ok' | 'timed out' | 'error'
|
|
40
42
|
export type HeartbeatStatus = 'sent' | 'ok' | 'error' | 'timeout' | 'disconnected'
|
|
41
|
-
|
|
43
|
+
|
|
44
|
+
const noop = () => {}
|
|
45
|
+
|
|
46
|
+
type RealtimeClientState = 'connecting' | 'connected' | 'disconnecting' | 'disconnected'
|
|
42
47
|
|
|
43
48
|
// Connection-related constants
|
|
44
49
|
const CONNECTION_TIMEOUTS = {
|
|
@@ -60,16 +65,22 @@ export interface WebSocketLikeConstructor {
|
|
|
60
65
|
[key: string]: any
|
|
61
66
|
}
|
|
62
67
|
|
|
68
|
+
export interface WebSocketLikeError {
|
|
69
|
+
error: any
|
|
70
|
+
message: string
|
|
71
|
+
type: string
|
|
72
|
+
}
|
|
73
|
+
|
|
63
74
|
export type RealtimeClientOptions = {
|
|
64
75
|
transport?: WebSocketLikeConstructor
|
|
65
76
|
timeout?: number
|
|
66
77
|
heartbeatIntervalMs?: number
|
|
67
78
|
heartbeatCallback?: (status: HeartbeatStatus, latency?: number) => void
|
|
68
|
-
vsn?:
|
|
69
|
-
logger?:
|
|
70
|
-
encode?:
|
|
71
|
-
decode?:
|
|
72
|
-
reconnectAfterMs?:
|
|
79
|
+
vsn?: string
|
|
80
|
+
logger?: Function
|
|
81
|
+
encode?: Function
|
|
82
|
+
decode?: Function
|
|
83
|
+
reconnectAfterMs?: Function
|
|
73
84
|
headers?: { [key: string]: string }
|
|
74
85
|
params?: { [key: string]: any }
|
|
75
86
|
//Deprecated: Use it in favour of correct casing `logLevel`
|
|
@@ -89,101 +100,52 @@ const WORKER_SCRIPT = `
|
|
|
89
100
|
});`
|
|
90
101
|
|
|
91
102
|
export default class RealtimeClient {
|
|
92
|
-
/** @internal */
|
|
93
|
-
socketAdapter: SocketAdapter
|
|
94
|
-
channels: RealtimeChannel[] = new Array()
|
|
95
|
-
|
|
96
103
|
accessTokenValue: string | null = null
|
|
97
|
-
accessToken: (() => Promise<string | null>) | null = null
|
|
98
104
|
apiKey: string | null = null
|
|
99
|
-
|
|
105
|
+
private _manuallySetToken: boolean = false
|
|
106
|
+
channels: RealtimeChannel[] = new Array()
|
|
107
|
+
endPoint: string = ''
|
|
100
108
|
httpEndpoint: string = ''
|
|
101
109
|
/** @deprecated headers cannot be set on websocket connections */
|
|
102
110
|
headers?: { [key: string]: string } = {}
|
|
103
111
|
params?: { [key: string]: string } = {}
|
|
104
|
-
|
|
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
|
|
105
118
|
ref: number = 0
|
|
106
|
-
|
|
119
|
+
reconnectTimer: Timer | null = null
|
|
120
|
+
vsn: string = DEFAULT_VSN
|
|
121
|
+
logger: Function = noop
|
|
107
122
|
logLevel?: LogLevel
|
|
108
|
-
|
|
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
|
+
}
|
|
109
140
|
fetch: Fetch
|
|
141
|
+
accessToken: (() => Promise<string | null>) | null = null
|
|
110
142
|
worker?: boolean
|
|
111
143
|
workerUrl?: string
|
|
112
144
|
workerRef?: Worker
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
get endPoint() {
|
|
117
|
-
return this.socketAdapter.endPoint
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
get timeout() {
|
|
121
|
-
return this.socketAdapter.timeout
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
get transport() {
|
|
125
|
-
return this.socketAdapter.transport
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
get heartbeatCallback() {
|
|
129
|
-
return this.socketAdapter.heartbeatCallback
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
get heartbeatIntervalMs() {
|
|
133
|
-
return this.socketAdapter.heartbeatIntervalMs
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
get heartbeatTimer() {
|
|
137
|
-
if (this.worker) {
|
|
138
|
-
return this._workerHeartbeatTimer
|
|
139
|
-
}
|
|
140
|
-
return this.socketAdapter.heartbeatTimer
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
get pendingHeartbeatRef() {
|
|
144
|
-
if (this.worker) {
|
|
145
|
-
return this._pendingWorkerHeartbeatRef
|
|
146
|
-
}
|
|
147
|
-
return this.socketAdapter.pendingHeartbeatRef
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
get reconnectTimer() {
|
|
151
|
-
return this.socketAdapter.reconnectTimer
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
get vsn() {
|
|
155
|
-
return this.socketAdapter.vsn
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
get encode() {
|
|
159
|
-
return this.socketAdapter.encode
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
get decode() {
|
|
163
|
-
return this.socketAdapter.decode
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
get reconnectAfterMs() {
|
|
167
|
-
return this.socketAdapter.reconnectAfterMs
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
get sendBuffer() {
|
|
171
|
-
return this.socketAdapter.sendBuffer
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
get stateChangeCallbacks(): {
|
|
175
|
-
open: [string, Function][]
|
|
176
|
-
close: [string, Function][]
|
|
177
|
-
error: [string, Function][]
|
|
178
|
-
message: [string, Function][]
|
|
179
|
-
} {
|
|
180
|
-
return this.socketAdapter.stateChangeCallbacks
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
private _manuallySetToken: boolean = false
|
|
145
|
+
private _connectionState: RealtimeClientState = 'disconnected'
|
|
146
|
+
private _wasManualDisconnect: boolean = false
|
|
184
147
|
private _authPromise: Promise<void> | null = null
|
|
185
|
-
private
|
|
186
|
-
private _pendingWorkerHeartbeatRef: string | null = null
|
|
148
|
+
private _heartbeatSentAt: number | null = null
|
|
187
149
|
|
|
188
150
|
/**
|
|
189
151
|
* Initializes the Socket.
|
|
@@ -221,11 +183,12 @@ export default class RealtimeClient {
|
|
|
221
183
|
}
|
|
222
184
|
this.apiKey = options.params.apikey
|
|
223
185
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
this.socketAdapter = new SocketAdapter(endPoint, socketAdapterOptions)
|
|
186
|
+
// Initialize endpoint URLs
|
|
187
|
+
this.endPoint = `${endPoint}/${TRANSPORTS.websocket}`
|
|
227
188
|
this.httpEndpoint = httpEndpointURL(endPoint)
|
|
228
189
|
|
|
190
|
+
this._initializeOptions(options)
|
|
191
|
+
this._setupReconnectionTimer()
|
|
229
192
|
this.fetch = this._resolveFetch(options?.fetch)
|
|
230
193
|
}
|
|
231
194
|
|
|
@@ -234,10 +197,16 @@ export default class RealtimeClient {
|
|
|
234
197
|
*/
|
|
235
198
|
connect(): void {
|
|
236
199
|
// Skip if already connecting, disconnecting, or connected
|
|
237
|
-
if (
|
|
200
|
+
if (
|
|
201
|
+
this.isConnecting() ||
|
|
202
|
+
this.isDisconnecting() ||
|
|
203
|
+
(this.conn !== null && this.isConnected())
|
|
204
|
+
) {
|
|
238
205
|
return
|
|
239
206
|
}
|
|
240
207
|
|
|
208
|
+
this._setConnectionState('connecting')
|
|
209
|
+
|
|
241
210
|
// Trigger auth if needed and not already in progress
|
|
242
211
|
// This ensures auth is called for standalone RealtimeClient usage
|
|
243
212
|
// while avoiding race conditions with SupabaseClient's immediate setAuth call
|
|
@@ -245,32 +214,37 @@ export default class RealtimeClient {
|
|
|
245
214
|
this._setAuthSafely('connect')
|
|
246
215
|
}
|
|
247
216
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
this.
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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}`)
|
|
269
245
|
}
|
|
270
|
-
throw new Error(`WebSocket not available: ${errorMessage}`)
|
|
271
246
|
}
|
|
272
|
-
|
|
273
|
-
this._handleNodeJsRaceCondition()
|
|
247
|
+
this._setupConnectionHandlers()
|
|
274
248
|
}
|
|
275
249
|
|
|
276
250
|
/**
|
|
@@ -278,7 +252,7 @@ export default class RealtimeClient {
|
|
|
278
252
|
* @returns string The URL of the websocket.
|
|
279
253
|
*/
|
|
280
254
|
endpointURL(): string {
|
|
281
|
-
return this.
|
|
255
|
+
return this._appendParams(this.endPoint, Object.assign({}, this.params, { vsn: this.vsn }))
|
|
282
256
|
}
|
|
283
257
|
|
|
284
258
|
/**
|
|
@@ -287,18 +261,37 @@ export default class RealtimeClient {
|
|
|
287
261
|
* @param code A numeric status code to send on disconnect.
|
|
288
262
|
* @param reason A custom reason for the disconnect.
|
|
289
263
|
*/
|
|
290
|
-
|
|
264
|
+
disconnect(code?: number, reason?: string): void {
|
|
291
265
|
if (this.isDisconnecting()) {
|
|
292
|
-
return
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
+
}
|
|
302
295
|
}
|
|
303
296
|
|
|
304
297
|
/**
|
|
@@ -309,16 +302,12 @@ export default class RealtimeClient {
|
|
|
309
302
|
}
|
|
310
303
|
|
|
311
304
|
/**
|
|
312
|
-
* Unsubscribes
|
|
305
|
+
* Unsubscribes and removes a single channel
|
|
313
306
|
* @param channel A RealtimeChannel instance
|
|
314
307
|
*/
|
|
315
308
|
async removeChannel(channel: RealtimeChannel): Promise<RealtimeRemoveChannelResponse> {
|
|
316
309
|
const status = await channel.unsubscribe()
|
|
317
310
|
|
|
318
|
-
if (status === 'ok') {
|
|
319
|
-
channel.teardown()
|
|
320
|
-
}
|
|
321
|
-
|
|
322
311
|
if (this.channels.length === 0) {
|
|
323
312
|
this.disconnect()
|
|
324
313
|
}
|
|
@@ -327,55 +316,59 @@ export default class RealtimeClient {
|
|
|
327
316
|
}
|
|
328
317
|
|
|
329
318
|
/**
|
|
330
|
-
* Unsubscribes
|
|
319
|
+
* Unsubscribes and removes all channels
|
|
331
320
|
*/
|
|
332
321
|
async removeAllChannels(): Promise<RealtimeRemoveChannelResponse[]> {
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
channel.teardown()
|
|
336
|
-
return result
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
const result = await Promise.all(promises)
|
|
322
|
+
const values_1 = await Promise.all(this.channels.map((channel) => channel.unsubscribe()))
|
|
323
|
+
this.channels = []
|
|
340
324
|
this.disconnect()
|
|
341
|
-
return
|
|
325
|
+
return values_1
|
|
342
326
|
}
|
|
343
327
|
|
|
344
328
|
/**
|
|
345
329
|
* Logs the message.
|
|
346
330
|
*
|
|
347
|
-
* For customized logging, `this.logger` can be overridden
|
|
331
|
+
* For customized logging, `this.logger` can be overridden.
|
|
348
332
|
*/
|
|
349
333
|
log(kind: string, msg: string, data?: any) {
|
|
350
|
-
this.
|
|
334
|
+
this.logger(kind, msg, data)
|
|
351
335
|
}
|
|
352
336
|
|
|
353
337
|
/**
|
|
354
338
|
* Returns the current state of the socket.
|
|
355
339
|
*/
|
|
356
|
-
connectionState() {
|
|
357
|
-
|
|
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
|
+
}
|
|
358
351
|
}
|
|
359
352
|
|
|
360
353
|
/**
|
|
361
354
|
* Returns `true` is the connection is open.
|
|
362
355
|
*/
|
|
363
356
|
isConnected(): boolean {
|
|
364
|
-
return this.
|
|
357
|
+
return this.connectionState() === CONNECTION_STATE.Open
|
|
365
358
|
}
|
|
366
359
|
|
|
367
360
|
/**
|
|
368
361
|
* Returns `true` if the connection is currently connecting.
|
|
369
362
|
*/
|
|
370
363
|
isConnecting(): boolean {
|
|
371
|
-
return this.
|
|
364
|
+
return this._connectionState === 'connecting'
|
|
372
365
|
}
|
|
373
366
|
|
|
374
367
|
/**
|
|
375
368
|
* Returns `true` if the connection is currently disconnecting.
|
|
376
369
|
*/
|
|
377
370
|
isDisconnecting(): boolean {
|
|
378
|
-
return this.
|
|
371
|
+
return this._connectionState === 'disconnecting'
|
|
379
372
|
}
|
|
380
373
|
|
|
381
374
|
/**
|
|
@@ -405,7 +398,18 @@ export default class RealtimeClient {
|
|
|
405
398
|
* If the socket is not connected, the message gets enqueued within a local buffer, and sent out when a connection is next established.
|
|
406
399
|
*/
|
|
407
400
|
push(data: RealtimeMessage): void {
|
|
408
|
-
|
|
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
|
+
}
|
|
409
413
|
}
|
|
410
414
|
|
|
411
415
|
/**
|
|
@@ -450,15 +454,71 @@ export default class RealtimeClient {
|
|
|
450
454
|
* Sends a heartbeat message if the socket is connected.
|
|
451
455
|
*/
|
|
452
456
|
async sendHeartbeat() {
|
|
453
|
-
this.
|
|
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')
|
|
454
505
|
}
|
|
455
506
|
|
|
456
507
|
/**
|
|
457
508
|
* Sets a callback that receives lifecycle events for internal heartbeat messages.
|
|
458
509
|
* Useful for instrumenting connection health (e.g. sent/ok/timeout/disconnected).
|
|
459
510
|
*/
|
|
460
|
-
onHeartbeat(callback:
|
|
461
|
-
this.
|
|
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
|
+
}
|
|
462
522
|
}
|
|
463
523
|
|
|
464
524
|
/**
|
|
@@ -479,11 +539,33 @@ export default class RealtimeClient {
|
|
|
479
539
|
* @internal
|
|
480
540
|
*/
|
|
481
541
|
_makeRef(): string {
|
|
482
|
-
|
|
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()
|
|
483
550
|
}
|
|
484
551
|
|
|
485
552
|
/**
|
|
486
|
-
*
|
|
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.
|
|
487
569
|
*
|
|
488
570
|
* @param channel An open subscription.
|
|
489
571
|
*
|
|
@@ -493,6 +575,283 @@ export default class RealtimeClient {
|
|
|
493
575
|
this.channels = this.channels.filter((c) => c.topic !== channel.topic)
|
|
494
576
|
}
|
|
495
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
|
+
|
|
496
855
|
/**
|
|
497
856
|
* Perform the actual auth operation
|
|
498
857
|
* @internal
|
|
@@ -536,8 +895,8 @@ export default class RealtimeClient {
|
|
|
536
895
|
|
|
537
896
|
tokenToSend && channel.updateJoinPayload(payload)
|
|
538
897
|
|
|
539
|
-
if (channel.joinedOnce && channel.
|
|
540
|
-
channel.
|
|
898
|
+
if (channel.joinedOnce && channel._isJoined()) {
|
|
899
|
+
channel._push(CHANNEL_EVENTS.access_token, {
|
|
541
900
|
access_token: tokenToSend,
|
|
542
901
|
})
|
|
543
902
|
}
|
|
@@ -568,150 +927,89 @@ export default class RealtimeClient {
|
|
|
568
927
|
}
|
|
569
928
|
}
|
|
570
929
|
|
|
571
|
-
/**
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
+
}
|
|
580
942
|
})
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
this._startWorkerHeartbeat()
|
|
584
|
-
}
|
|
585
|
-
})
|
|
586
|
-
this.socketAdapter.onClose(() => {
|
|
587
|
-
if (this.worker && this.workerRef) {
|
|
588
|
-
this._terminateWorker()
|
|
589
|
-
}
|
|
590
|
-
})
|
|
591
|
-
this.socketAdapter.onMessage((message: Message<any>) => {
|
|
592
|
-
if (message.ref && message.ref === this._pendingWorkerHeartbeatRef) {
|
|
593
|
-
this._pendingWorkerHeartbeatRef = null
|
|
594
|
-
}
|
|
595
|
-
})
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
/** @internal */
|
|
599
|
-
private _handleNodeJsRaceCondition() {
|
|
600
|
-
if (this.socketAdapter.isConnected()) {
|
|
601
|
-
// hack: ensure onConnOpen is called
|
|
602
|
-
this.socketAdapter.getSocket().onConnOpen()
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
/** @internal */
|
|
607
|
-
private _wrapHeartbeatCallback(heartbeatCallback?: HeartbeatCallback): HeartbeatCallback {
|
|
608
|
-
return (status, latency) => {
|
|
609
|
-
if (status == 'sent') this._setAuthSafely()
|
|
610
|
-
if (heartbeatCallback) heartbeatCallback(status, latency)
|
|
943
|
+
} catch (e) {
|
|
944
|
+
this.log('error', `error triggering ${event} callbacks`, e)
|
|
611
945
|
}
|
|
612
946
|
}
|
|
613
947
|
|
|
614
|
-
/** @internal */
|
|
615
|
-
private _startWorkerHeartbeat() {
|
|
616
|
-
if (this.workerUrl) {
|
|
617
|
-
this.log('worker', `starting worker for from ${this.workerUrl}`)
|
|
618
|
-
} else {
|
|
619
|
-
this.log('worker', `starting default worker`)
|
|
620
|
-
}
|
|
621
|
-
const objectUrl = this._workerObjectUrl(this.workerUrl!)
|
|
622
|
-
this.workerRef = new Worker(objectUrl)
|
|
623
|
-
this.workerRef.onerror = (error) => {
|
|
624
|
-
this.log('worker', 'worker error', (error as ErrorEvent).message)
|
|
625
|
-
this._terminateWorker()
|
|
626
|
-
this.disconnect()
|
|
627
|
-
}
|
|
628
|
-
this.workerRef.onmessage = (event) => {
|
|
629
|
-
if (event.data.event === 'keepAlive') {
|
|
630
|
-
this.sendHeartbeat()
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
this.workerRef.postMessage({
|
|
634
|
-
event: 'start',
|
|
635
|
-
interval: this.heartbeatIntervalMs,
|
|
636
|
-
})
|
|
637
|
-
}
|
|
638
|
-
|
|
639
948
|
/**
|
|
640
|
-
*
|
|
949
|
+
* Setup reconnection timer with proper configuration
|
|
641
950
|
* @internal
|
|
642
951
|
*/
|
|
643
|
-
private
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
private _workerObjectUrl(url: string | undefined): string {
|
|
653
|
-
let result_url: string
|
|
654
|
-
if (url) {
|
|
655
|
-
result_url = url
|
|
656
|
-
} else {
|
|
657
|
-
const blob = new Blob([WORKER_SCRIPT], { type: 'application/javascript' })
|
|
658
|
-
result_url = URL.createObjectURL(blob)
|
|
659
|
-
}
|
|
660
|
-
return result_url
|
|
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)
|
|
661
961
|
}
|
|
662
962
|
|
|
663
963
|
/**
|
|
664
|
-
* Initialize
|
|
964
|
+
* Initialize client options with defaults
|
|
665
965
|
* @internal
|
|
666
966
|
*/
|
|
667
|
-
private _initializeOptions(options?: RealtimeClientOptions):
|
|
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
|
|
668
973
|
this.worker = options?.worker ?? false
|
|
669
974
|
this.accessToken = options?.accessToken ?? null
|
|
975
|
+
this.heartbeatCallback = options?.heartbeatCallback ?? noop
|
|
976
|
+
this.vsn = options?.vsn ?? DEFAULT_VSN
|
|
670
977
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
result.reconnectAfterMs =
|
|
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 =
|
|
682
988
|
options?.reconnectAfterMs ??
|
|
683
989
|
((tries: number) => {
|
|
684
990
|
return RECONNECT_INTERVALS[tries - 1] || DEFAULT_RECONNECT_FALLBACK
|
|
685
991
|
})
|
|
686
992
|
|
|
687
|
-
|
|
688
|
-
let defaultDecode: Decode<void>
|
|
689
|
-
|
|
690
|
-
switch (result.vsn) {
|
|
993
|
+
switch (this.vsn) {
|
|
691
994
|
case VSN_1_0_0:
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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
|
+
})
|
|
698
1006
|
break
|
|
699
1007
|
case VSN_2_0_0:
|
|
700
|
-
|
|
701
|
-
|
|
1008
|
+
this.encode = options?.encode ?? this.serializer.encode.bind(this.serializer)
|
|
1009
|
+
this.decode = options?.decode ?? this.serializer.decode.bind(this.serializer)
|
|
702
1010
|
break
|
|
703
1011
|
default:
|
|
704
|
-
throw new Error(`Unsupported serializer version: ${
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
result.encode = options?.encode ?? defaultEncode
|
|
708
|
-
result.decode = options?.decode ?? defaultDecode
|
|
709
|
-
|
|
710
|
-
result.beforeReconnect = this._reconnectAuth.bind(this)
|
|
711
|
-
|
|
712
|
-
if (options?.logLevel || options?.log_level) {
|
|
713
|
-
this.logLevel = options.logLevel || options.log_level
|
|
714
|
-
result.params = { ...result.params, log_level: this.logLevel as string }
|
|
1012
|
+
throw new Error(`Unsupported serializer version: ${this.vsn}`)
|
|
715
1013
|
}
|
|
716
1014
|
|
|
717
1015
|
// Handle worker setup
|
|
@@ -720,17 +1018,6 @@ export default class RealtimeClient {
|
|
|
720
1018
|
throw new Error('Web Worker is not supported')
|
|
721
1019
|
}
|
|
722
1020
|
this.workerUrl = options?.workerUrl
|
|
723
|
-
result.autoSendHeartbeat = !this.worker
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
return result
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
/** @internal */
|
|
730
|
-
private async _reconnectAuth() {
|
|
731
|
-
await this._waitForAuthIfNeeded()
|
|
732
|
-
if (!this.isConnected()) {
|
|
733
|
-
this.connect()
|
|
734
1021
|
}
|
|
735
1022
|
}
|
|
736
1023
|
}
|