@supabase/realtime-js 2.99.2 → 2.100.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main/RealtimeChannel.d.ts +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 +38 -57
- package/dist/main/RealtimeClient.d.ts.map +1 -1
- package/dist/main/RealtimeClient.js +232 -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 +38 -57
- package/dist/module/RealtimeClient.d.ts.map +1 -1
- package/dist/module/RealtimeClient.js +233 -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 +296 -583
- 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,27 @@ 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 {
|
|
19
|
+
Message,
|
|
20
|
+
SocketOptions,
|
|
21
|
+
HeartbeatCallback,
|
|
22
|
+
Encode,
|
|
23
|
+
Decode,
|
|
24
|
+
Vsn,
|
|
25
|
+
} from './phoenix/types'
|
|
22
26
|
|
|
23
27
|
type Fetch = typeof fetch
|
|
24
28
|
|
|
25
|
-
export type Channel = {
|
|
26
|
-
name: string
|
|
27
|
-
inserted_at: string
|
|
28
|
-
updated_at: string
|
|
29
|
-
id: number
|
|
30
|
-
}
|
|
31
29
|
export type LogLevel = 'info' | 'warn' | 'error'
|
|
32
30
|
|
|
33
31
|
export type RealtimeMessage = {
|
|
@@ -40,10 +38,7 @@ export type RealtimeMessage = {
|
|
|
40
38
|
|
|
41
39
|
export type RealtimeRemoveChannelResponse = 'ok' | 'timed out' | 'error'
|
|
42
40
|
export type HeartbeatStatus = 'sent' | 'ok' | 'error' | 'timeout' | 'disconnected'
|
|
43
|
-
|
|
44
|
-
const noop = () => {}
|
|
45
|
-
|
|
46
|
-
type RealtimeClientState = 'connecting' | 'connected' | 'disconnecting' | 'disconnected'
|
|
41
|
+
export type HeartbeatTimer = ReturnType<typeof setTimeout> | undefined
|
|
47
42
|
|
|
48
43
|
// Connection-related constants
|
|
49
44
|
const CONNECTION_TIMEOUTS = {
|
|
@@ -65,22 +60,16 @@ export interface WebSocketLikeConstructor {
|
|
|
65
60
|
[key: string]: any
|
|
66
61
|
}
|
|
67
62
|
|
|
68
|
-
export interface WebSocketLikeError {
|
|
69
|
-
error: any
|
|
70
|
-
message: string
|
|
71
|
-
type: string
|
|
72
|
-
}
|
|
73
|
-
|
|
74
63
|
export type RealtimeClientOptions = {
|
|
75
64
|
transport?: WebSocketLikeConstructor
|
|
76
65
|
timeout?: number
|
|
77
66
|
heartbeatIntervalMs?: number
|
|
78
67
|
heartbeatCallback?: (status: HeartbeatStatus, latency?: number) => void
|
|
79
|
-
vsn?:
|
|
80
|
-
logger?:
|
|
81
|
-
encode?:
|
|
82
|
-
decode?:
|
|
83
|
-
reconnectAfterMs?:
|
|
68
|
+
vsn?: Vsn
|
|
69
|
+
logger?: (kind: string, msg: string, data?: any) => void
|
|
70
|
+
encode?: Encode<void>
|
|
71
|
+
decode?: Decode<void>
|
|
72
|
+
reconnectAfterMs?: (tries: number) => number
|
|
84
73
|
headers?: { [key: string]: string }
|
|
85
74
|
params?: { [key: string]: any }
|
|
86
75
|
//Deprecated: Use it in favour of correct casing `logLevel`
|
|
@@ -100,52 +89,101 @@ const WORKER_SCRIPT = `
|
|
|
100
89
|
});`
|
|
101
90
|
|
|
102
91
|
export default class RealtimeClient {
|
|
92
|
+
/** @internal */
|
|
93
|
+
socketAdapter: SocketAdapter
|
|
94
|
+
channels: RealtimeChannel[] = new Array()
|
|
95
|
+
|
|
103
96
|
accessTokenValue: string | null = null
|
|
97
|
+
accessToken: (() => Promise<string | null>) | null = null
|
|
104
98
|
apiKey: string | null = null
|
|
105
|
-
|
|
106
|
-
channels: RealtimeChannel[] = new Array()
|
|
107
|
-
endPoint: string = ''
|
|
99
|
+
|
|
108
100
|
httpEndpoint: string = ''
|
|
109
101
|
/** @deprecated headers cannot be set on websocket connections */
|
|
110
102
|
headers?: { [key: string]: string } = {}
|
|
111
103
|
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
|
|
104
|
+
|
|
118
105
|
ref: number = 0
|
|
119
|
-
|
|
120
|
-
vsn: string = DEFAULT_VSN
|
|
121
|
-
logger: Function = noop
|
|
106
|
+
|
|
122
107
|
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
|
-
}
|
|
108
|
+
|
|
140
109
|
fetch: Fetch
|
|
141
|
-
accessToken: (() => Promise<string | null>) | null = null
|
|
142
110
|
worker?: boolean
|
|
143
111
|
workerUrl?: string
|
|
144
112
|
workerRef?: Worker
|
|
145
|
-
|
|
146
|
-
|
|
113
|
+
|
|
114
|
+
serializer: Serializer = new Serializer()
|
|
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
|
|
147
184
|
private _authPromise: Promise<void> | null = null
|
|
148
|
-
private
|
|
185
|
+
private _workerHeartbeatTimer: HeartbeatTimer = undefined
|
|
186
|
+
private _pendingWorkerHeartbeatRef: string | null = null
|
|
149
187
|
|
|
150
188
|
/**
|
|
151
189
|
* Initializes the Socket.
|
|
@@ -183,12 +221,11 @@ export default class RealtimeClient {
|
|
|
183
221
|
}
|
|
184
222
|
this.apiKey = options.params.apikey
|
|
185
223
|
|
|
186
|
-
|
|
187
|
-
|
|
224
|
+
const socketAdapterOptions = this._initializeOptions(options)
|
|
225
|
+
|
|
226
|
+
this.socketAdapter = new SocketAdapter(endPoint, socketAdapterOptions)
|
|
188
227
|
this.httpEndpoint = httpEndpointURL(endPoint)
|
|
189
228
|
|
|
190
|
-
this._initializeOptions(options)
|
|
191
|
-
this._setupReconnectionTimer()
|
|
192
229
|
this.fetch = this._resolveFetch(options?.fetch)
|
|
193
230
|
}
|
|
194
231
|
|
|
@@ -197,16 +234,10 @@ export default class RealtimeClient {
|
|
|
197
234
|
*/
|
|
198
235
|
connect(): void {
|
|
199
236
|
// Skip if already connecting, disconnecting, or connected
|
|
200
|
-
if (
|
|
201
|
-
this.isConnecting() ||
|
|
202
|
-
this.isDisconnecting() ||
|
|
203
|
-
(this.conn !== null && this.isConnected())
|
|
204
|
-
) {
|
|
237
|
+
if (this.isConnecting() || this.isDisconnecting() || this.isConnected()) {
|
|
205
238
|
return
|
|
206
239
|
}
|
|
207
240
|
|
|
208
|
-
this._setConnectionState('connecting')
|
|
209
|
-
|
|
210
241
|
// Trigger auth if needed and not already in progress
|
|
211
242
|
// This ensures auth is called for standalone RealtimeClient usage
|
|
212
243
|
// while avoiding race conditions with SupabaseClient's immediate setAuth call
|
|
@@ -214,37 +245,32 @@ export default class RealtimeClient {
|
|
|
214
245
|
this._setAuthSafely('connect')
|
|
215
246
|
}
|
|
216
247
|
|
|
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}`)
|
|
248
|
+
this._setupConnectionHandlers()
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
this.socketAdapter.connect()
|
|
252
|
+
} catch (error) {
|
|
253
|
+
const errorMessage = (error as Error).message
|
|
254
|
+
|
|
255
|
+
// Provide helpful error message based on environment
|
|
256
|
+
if (errorMessage.includes('Node.js')) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`${errorMessage}\n\n` +
|
|
259
|
+
'To use Realtime in Node.js, you need to provide a WebSocket implementation:\n\n' +
|
|
260
|
+
'Option 1: Use Node.js 22+ which has native WebSocket support\n' +
|
|
261
|
+
'Option 2: Install and provide the "ws" package:\n\n' +
|
|
262
|
+
' npm install ws\n\n' +
|
|
263
|
+
' import ws from "ws"\n' +
|
|
264
|
+
' const client = new RealtimeClient(url, {\n' +
|
|
265
|
+
' ...options,\n' +
|
|
266
|
+
' transport: ws\n' +
|
|
267
|
+
' })'
|
|
268
|
+
)
|
|
245
269
|
}
|
|
270
|
+
throw new Error(`WebSocket not available: ${errorMessage}`)
|
|
246
271
|
}
|
|
247
|
-
|
|
272
|
+
|
|
273
|
+
this._handleNodeJsRaceCondition()
|
|
248
274
|
}
|
|
249
275
|
|
|
250
276
|
/**
|
|
@@ -252,7 +278,7 @@ export default class RealtimeClient {
|
|
|
252
278
|
* @returns string The URL of the websocket.
|
|
253
279
|
*/
|
|
254
280
|
endpointURL(): string {
|
|
255
|
-
return this.
|
|
281
|
+
return this.socketAdapter.endPointURL()
|
|
256
282
|
}
|
|
257
283
|
|
|
258
284
|
/**
|
|
@@ -261,37 +287,18 @@ export default class RealtimeClient {
|
|
|
261
287
|
* @param code A numeric status code to send on disconnect.
|
|
262
288
|
* @param reason A custom reason for the disconnect.
|
|
263
289
|
*/
|
|
264
|
-
disconnect(code?: number, reason?: string)
|
|
290
|
+
async disconnect(code?: number, reason?: string) {
|
|
265
291
|
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
|
-
}
|
|
292
|
+
return 'ok'
|
|
293
|
+
}
|
|
294
|
+
return await this.socketAdapter.disconnect(
|
|
295
|
+
() => {
|
|
296
|
+
clearInterval(this._workerHeartbeatTimer)
|
|
297
|
+
this._terminateWorker()
|
|
298
|
+
},
|
|
299
|
+
code,
|
|
300
|
+
reason
|
|
301
|
+
)
|
|
295
302
|
}
|
|
296
303
|
|
|
297
304
|
/**
|
|
@@ -302,12 +309,16 @@ export default class RealtimeClient {
|
|
|
302
309
|
}
|
|
303
310
|
|
|
304
311
|
/**
|
|
305
|
-
* Unsubscribes and
|
|
312
|
+
* Unsubscribes, removes and tears down a single channel
|
|
306
313
|
* @param channel A RealtimeChannel instance
|
|
307
314
|
*/
|
|
308
315
|
async removeChannel(channel: RealtimeChannel): Promise<RealtimeRemoveChannelResponse> {
|
|
309
316
|
const status = await channel.unsubscribe()
|
|
310
317
|
|
|
318
|
+
if (status === 'ok') {
|
|
319
|
+
channel.teardown()
|
|
320
|
+
}
|
|
321
|
+
|
|
311
322
|
if (this.channels.length === 0) {
|
|
312
323
|
this.disconnect()
|
|
313
324
|
}
|
|
@@ -316,59 +327,55 @@ export default class RealtimeClient {
|
|
|
316
327
|
}
|
|
317
328
|
|
|
318
329
|
/**
|
|
319
|
-
* Unsubscribes and
|
|
330
|
+
* Unsubscribes, removes and tears down all channels
|
|
320
331
|
*/
|
|
321
332
|
async removeAllChannels(): Promise<RealtimeRemoveChannelResponse[]> {
|
|
322
|
-
const
|
|
323
|
-
|
|
333
|
+
const promises = this.channels.map(async (channel) => {
|
|
334
|
+
const result = await channel.unsubscribe()
|
|
335
|
+
channel.teardown()
|
|
336
|
+
return result
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
const result = await Promise.all(promises)
|
|
324
340
|
this.disconnect()
|
|
325
|
-
return
|
|
341
|
+
return result
|
|
326
342
|
}
|
|
327
343
|
|
|
328
344
|
/**
|
|
329
345
|
* Logs the message.
|
|
330
346
|
*
|
|
331
|
-
* For customized logging, `this.logger` can be overridden.
|
|
347
|
+
* For customized logging, `this.logger` can be overridden in Client constructor.
|
|
332
348
|
*/
|
|
333
349
|
log(kind: string, msg: string, data?: any) {
|
|
334
|
-
this.
|
|
350
|
+
this.socketAdapter.log(kind, msg, data)
|
|
335
351
|
}
|
|
336
352
|
|
|
337
353
|
/**
|
|
338
354
|
* Returns the current state of the socket.
|
|
339
355
|
*/
|
|
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
|
-
}
|
|
356
|
+
connectionState() {
|
|
357
|
+
return this.socketAdapter.connectionState() || CONNECTION_STATE.closed
|
|
351
358
|
}
|
|
352
359
|
|
|
353
360
|
/**
|
|
354
361
|
* Returns `true` is the connection is open.
|
|
355
362
|
*/
|
|
356
363
|
isConnected(): boolean {
|
|
357
|
-
return this.
|
|
364
|
+
return this.socketAdapter.isConnected()
|
|
358
365
|
}
|
|
359
366
|
|
|
360
367
|
/**
|
|
361
368
|
* Returns `true` if the connection is currently connecting.
|
|
362
369
|
*/
|
|
363
370
|
isConnecting(): boolean {
|
|
364
|
-
return this.
|
|
371
|
+
return this.socketAdapter.isConnecting()
|
|
365
372
|
}
|
|
366
373
|
|
|
367
374
|
/**
|
|
368
375
|
* Returns `true` if the connection is currently disconnecting.
|
|
369
376
|
*/
|
|
370
377
|
isDisconnecting(): boolean {
|
|
371
|
-
return this.
|
|
378
|
+
return this.socketAdapter.isDisconnecting()
|
|
372
379
|
}
|
|
373
380
|
|
|
374
381
|
/**
|
|
@@ -398,18 +405,7 @@ export default class RealtimeClient {
|
|
|
398
405
|
* If the socket is not connected, the message gets enqueued within a local buffer, and sent out when a connection is next established.
|
|
399
406
|
*/
|
|
400
407
|
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
|
-
}
|
|
408
|
+
this.socketAdapter.push(data)
|
|
413
409
|
}
|
|
414
410
|
|
|
415
411
|
/**
|
|
@@ -454,71 +450,15 @@ export default class RealtimeClient {
|
|
|
454
450
|
* Sends a heartbeat message if the socket is connected.
|
|
455
451
|
*/
|
|
456
452
|
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')
|
|
453
|
+
this.socketAdapter.sendHeartbeat()
|
|
505
454
|
}
|
|
506
455
|
|
|
507
456
|
/**
|
|
508
457
|
* Sets a callback that receives lifecycle events for internal heartbeat messages.
|
|
509
458
|
* Useful for instrumenting connection health (e.g. sent/ok/timeout/disconnected).
|
|
510
459
|
*/
|
|
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
|
-
}
|
|
460
|
+
onHeartbeat(callback: HeartbeatCallback) {
|
|
461
|
+
this.socketAdapter.heartbeatCallback = this._wrapHeartbeatCallback(callback)
|
|
522
462
|
}
|
|
523
463
|
|
|
524
464
|
/**
|
|
@@ -539,33 +479,11 @@ export default class RealtimeClient {
|
|
|
539
479
|
* @internal
|
|
540
480
|
*/
|
|
541
481
|
_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()
|
|
482
|
+
return this.socketAdapter.makeRef()
|
|
550
483
|
}
|
|
551
484
|
|
|
552
485
|
/**
|
|
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.
|
|
486
|
+
* Removes a channel from RealtimeClient
|
|
569
487
|
*
|
|
570
488
|
* @param channel An open subscription.
|
|
571
489
|
*
|
|
@@ -575,283 +493,6 @@ export default class RealtimeClient {
|
|
|
575
493
|
this.channels = this.channels.filter((c) => c.topic !== channel.topic)
|
|
576
494
|
}
|
|
577
495
|
|
|
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
496
|
/**
|
|
856
497
|
* Perform the actual auth operation
|
|
857
498
|
* @internal
|
|
@@ -895,8 +536,8 @@ export default class RealtimeClient {
|
|
|
895
536
|
|
|
896
537
|
tokenToSend && channel.updateJoinPayload(payload)
|
|
897
538
|
|
|
898
|
-
if (channel.joinedOnce && channel.
|
|
899
|
-
channel.
|
|
539
|
+
if (channel.joinedOnce && channel.channelAdapter.isJoined()) {
|
|
540
|
+
channel.channelAdapter.push(CHANNEL_EVENTS.access_token, {
|
|
900
541
|
access_token: tokenToSend,
|
|
901
542
|
})
|
|
902
543
|
}
|
|
@@ -927,89 +568,150 @@ export default class RealtimeClient {
|
|
|
927
568
|
}
|
|
928
569
|
}
|
|
929
570
|
|
|
930
|
-
/**
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
} catch (e) {
|
|
940
|
-
this.log('error', `error in ${event} callback`, e)
|
|
941
|
-
}
|
|
571
|
+
/** @internal */
|
|
572
|
+
private _setupConnectionHandlers(): void {
|
|
573
|
+
this.socketAdapter.onOpen(() => {
|
|
574
|
+
const authPromise =
|
|
575
|
+
this._authPromise ||
|
|
576
|
+
(this.accessToken && !this.accessTokenValue ? this.setAuth() : Promise.resolve())
|
|
577
|
+
|
|
578
|
+
authPromise.catch((e) => {
|
|
579
|
+
this.log('error', 'error waiting for auth on connect', e)
|
|
942
580
|
})
|
|
943
|
-
|
|
944
|
-
this.
|
|
581
|
+
|
|
582
|
+
if (this.worker && !this.workerRef) {
|
|
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)
|
|
945
611
|
}
|
|
946
612
|
}
|
|
947
613
|
|
|
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
|
+
|
|
948
639
|
/**
|
|
949
|
-
*
|
|
640
|
+
* Terminate the Web Worker and clear the reference
|
|
950
641
|
* @internal
|
|
951
642
|
*/
|
|
952
|
-
private
|
|
953
|
-
this.
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
643
|
+
private _terminateWorker(): void {
|
|
644
|
+
if (this.workerRef) {
|
|
645
|
+
this.log('worker', 'terminating worker')
|
|
646
|
+
this.workerRef.terminate()
|
|
647
|
+
this.workerRef = undefined
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** @internal */
|
|
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
|
|
961
661
|
}
|
|
962
662
|
|
|
963
663
|
/**
|
|
964
|
-
* Initialize
|
|
664
|
+
* Initialize socket options with defaults
|
|
965
665
|
* @internal
|
|
966
666
|
*/
|
|
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
|
|
667
|
+
private _initializeOptions(options?: RealtimeClientOptions): SocketOptions {
|
|
973
668
|
this.worker = options?.worker ?? false
|
|
974
669
|
this.accessToken = options?.accessToken ?? null
|
|
975
|
-
this.heartbeatCallback = options?.heartbeatCallback ?? noop
|
|
976
|
-
this.vsn = options?.vsn ?? DEFAULT_VSN
|
|
977
670
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
671
|
+
const result: SocketOptions = {}
|
|
672
|
+
result.timeout = options?.timeout ?? DEFAULT_TIMEOUT
|
|
673
|
+
result.heartbeatIntervalMs =
|
|
674
|
+
options?.heartbeatIntervalMs ?? CONNECTION_TIMEOUTS.HEARTBEAT_INTERVAL
|
|
675
|
+
result.vsn = options?.vsn ?? DEFAULT_VSN
|
|
676
|
+
// @ts-ignore - mismatch between phoenix and supabase
|
|
677
|
+
result.transport = options?.transport ?? WebSocketFactory.getWebSocketConstructor()
|
|
678
|
+
result.params = options?.params
|
|
679
|
+
result.logger = options?.logger
|
|
680
|
+
result.heartbeatCallback = this._wrapHeartbeatCallback(options?.heartbeatCallback)
|
|
681
|
+
result.reconnectAfterMs =
|
|
988
682
|
options?.reconnectAfterMs ??
|
|
989
683
|
((tries: number) => {
|
|
990
684
|
return RECONNECT_INTERVALS[tries - 1] || DEFAULT_RECONNECT_FALLBACK
|
|
991
685
|
})
|
|
992
686
|
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
this.encode =
|
|
996
|
-
options?.encode ??
|
|
997
|
-
((payload: JSON, callback: Function) => {
|
|
998
|
-
return callback(JSON.stringify(payload))
|
|
999
|
-
})
|
|
687
|
+
let defaultEncode: Encode<void>
|
|
688
|
+
let defaultDecode: Decode<void>
|
|
1000
689
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
690
|
+
switch (result.vsn) {
|
|
691
|
+
case VSN_1_0_0:
|
|
692
|
+
defaultEncode = (payload, callback) => {
|
|
693
|
+
return callback(JSON.stringify(payload))
|
|
694
|
+
}
|
|
695
|
+
defaultDecode = (payload, callback) => {
|
|
696
|
+
return callback(JSON.parse(payload as string))
|
|
697
|
+
}
|
|
1006
698
|
break
|
|
1007
699
|
case VSN_2_0_0:
|
|
1008
|
-
|
|
1009
|
-
|
|
700
|
+
defaultEncode = this.serializer.encode.bind(this.serializer)
|
|
701
|
+
defaultDecode = this.serializer.decode.bind(this.serializer)
|
|
1010
702
|
break
|
|
1011
703
|
default:
|
|
1012
|
-
throw new Error(`Unsupported serializer version: ${
|
|
704
|
+
throw new Error(`Unsupported serializer version: ${result.vsn}`)
|
|
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 }
|
|
1013
715
|
}
|
|
1014
716
|
|
|
1015
717
|
// Handle worker setup
|
|
@@ -1018,6 +720,17 @@ export default class RealtimeClient {
|
|
|
1018
720
|
throw new Error('Web Worker is not supported')
|
|
1019
721
|
}
|
|
1020
722
|
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()
|
|
1021
734
|
}
|
|
1022
735
|
}
|
|
1023
736
|
}
|