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