@supabase/phoenix 0.1.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/LICENSE.md +22 -0
- package/README.md +122 -0
- package/assets/js/phoenix/ajax.js +116 -0
- package/assets/js/phoenix/channel.js +331 -0
- package/assets/js/phoenix/constants.js +35 -0
- package/assets/js/phoenix/index.js +212 -0
- package/assets/js/phoenix/longpoll.js +192 -0
- package/assets/js/phoenix/presence.js +208 -0
- package/assets/js/phoenix/push.js +134 -0
- package/assets/js/phoenix/serializer.js +133 -0
- package/assets/js/phoenix/socket.js +747 -0
- package/assets/js/phoenix/timer.js +48 -0
- package/assets/js/phoenix/types.js +184 -0
- package/assets/js/phoenix/utils.js +16 -0
- package/package.json +58 -0
- package/priv/static/favicon.ico +0 -0
- package/priv/static/phoenix-orange.png +0 -0
- package/priv/static/phoenix.cjs.js +1812 -0
- package/priv/static/phoenix.cjs.js.map +7 -0
- package/priv/static/phoenix.js +1834 -0
- package/priv/static/phoenix.min.js +2 -0
- package/priv/static/phoenix.mjs +1789 -0
- package/priv/static/phoenix.mjs.map +7 -0
- package/priv/static/phoenix.png +0 -0
- package/priv/static/types/ajax.d.ts +10 -0
- package/priv/static/types/ajax.d.ts.map +1 -0
- package/priv/static/types/channel.d.ts +167 -0
- package/priv/static/types/channel.d.ts.map +1 -0
- package/priv/static/types/constants.d.ts +36 -0
- package/priv/static/types/constants.d.ts.map +1 -0
- package/priv/static/types/index.d.ts +10 -0
- package/priv/static/types/index.d.ts.map +1 -0
- package/priv/static/types/longpoll.d.ts +29 -0
- package/priv/static/types/longpoll.d.ts.map +1 -0
- package/priv/static/types/presence.d.ts +107 -0
- package/priv/static/types/presence.d.ts.map +1 -0
- package/priv/static/types/push.d.ts +70 -0
- package/priv/static/types/push.d.ts.map +1 -0
- package/priv/static/types/serializer.d.ts +74 -0
- package/priv/static/types/serializer.d.ts.map +1 -0
- package/priv/static/types/socket.d.ts +284 -0
- package/priv/static/types/socket.d.ts.map +1 -0
- package/priv/static/types/timer.d.ts +36 -0
- package/priv/static/types/timer.d.ts.map +1 -0
- package/priv/static/types/types.d.ts +280 -0
- package/priv/static/types/types.d.ts.map +1 -0
- package/priv/static/types/utils.d.ts +2 -0
- package/priv/static/types/utils.d.ts.map +1 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
import {
|
|
2
|
+
global,
|
|
3
|
+
phxWindow,
|
|
4
|
+
CHANNEL_EVENTS,
|
|
5
|
+
DEFAULT_TIMEOUT,
|
|
6
|
+
DEFAULT_VSN,
|
|
7
|
+
SOCKET_STATES,
|
|
8
|
+
TRANSPORTS,
|
|
9
|
+
WS_CLOSE_NORMAL,
|
|
10
|
+
AUTH_TOKEN_PREFIX
|
|
11
|
+
} from "./constants"
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
closure
|
|
15
|
+
} from "./utils"
|
|
16
|
+
|
|
17
|
+
import Ajax from "./ajax"
|
|
18
|
+
import Channel from "./channel"
|
|
19
|
+
import LongPoll from "./longpoll"
|
|
20
|
+
import Serializer from "./serializer"
|
|
21
|
+
import Timer from "./timer"
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @import { Encode, Decode, Message, Vsn, SocketTransport, Params, SocketOnOpen, SocketOnClose, SocketOnError, SocketOnMessage, SocketOptions, SocketStateChangeCallbacks, HeartbeatCallback } from "./types"
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export default class Socket {
|
|
28
|
+
/** Initializes the Socket *
|
|
29
|
+
*
|
|
30
|
+
* For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim)
|
|
31
|
+
*
|
|
32
|
+
* @constructor
|
|
33
|
+
* @param {string} endPoint - The string WebSocket endpoint, ie, `"ws://example.com/socket"`,
|
|
34
|
+
* `"wss://example.com"`
|
|
35
|
+
* `"/socket"` (inherited host & protocol)
|
|
36
|
+
* @param {SocketOptions} [opts] - Optional configuration
|
|
37
|
+
*/
|
|
38
|
+
constructor(endPoint, opts = {}){
|
|
39
|
+
/** @type{SocketStateChangeCallbacks} */
|
|
40
|
+
this.stateChangeCallbacks = {open: [], close: [], error: [], message: []}
|
|
41
|
+
/** @type{Channel[]} */
|
|
42
|
+
this.channels = []
|
|
43
|
+
/** @type{(() => void)[]} */
|
|
44
|
+
this.sendBuffer = []
|
|
45
|
+
/** @type{number} */
|
|
46
|
+
this.ref = 0
|
|
47
|
+
/** @type{?string} */
|
|
48
|
+
this.fallbackRef = null
|
|
49
|
+
/** @type{number} */
|
|
50
|
+
this.timeout = opts.timeout || DEFAULT_TIMEOUT
|
|
51
|
+
/** @type{SocketTransport} */
|
|
52
|
+
this.transport = opts.transport || global.WebSocket || LongPoll
|
|
53
|
+
/** @type{InstanceType<SocketTransport> | undefined | null} */
|
|
54
|
+
this.conn = undefined
|
|
55
|
+
/** @type{boolean} */
|
|
56
|
+
this.primaryPassedHealthCheck = false
|
|
57
|
+
/** @type{number | undefined} */
|
|
58
|
+
this.longPollFallbackMs = opts.longPollFallbackMs
|
|
59
|
+
/** @type{ReturnType<typeof setTimeout>} */
|
|
60
|
+
this.fallbackTimer = null
|
|
61
|
+
/** @type{Storage} */
|
|
62
|
+
this.sessionStore = opts.sessionStorage || (global && global.sessionStorage)
|
|
63
|
+
/** @type{number} */
|
|
64
|
+
this.establishedConnections = 0
|
|
65
|
+
/** @type{Encode<void>} */
|
|
66
|
+
this.defaultEncoder = Serializer.encode.bind(Serializer)
|
|
67
|
+
/** @type{Decode<void>} */
|
|
68
|
+
this.defaultDecoder = Serializer.decode.bind(Serializer)
|
|
69
|
+
/** @type{boolean} */
|
|
70
|
+
this.closeWasClean = false
|
|
71
|
+
/** @type{boolean} */
|
|
72
|
+
this.disconnecting = false
|
|
73
|
+
/** @type{BinaryType} */
|
|
74
|
+
this.binaryType = opts.binaryType || "arraybuffer"
|
|
75
|
+
/** @type{number} */
|
|
76
|
+
this.connectClock = 1
|
|
77
|
+
/** @type{boolean} */
|
|
78
|
+
this.pageHidden = false
|
|
79
|
+
/** @type{Encode<void>} */
|
|
80
|
+
this.encode = undefined
|
|
81
|
+
/** @type{Decode<void>} */
|
|
82
|
+
this.decode = undefined
|
|
83
|
+
if(this.transport !== LongPoll){
|
|
84
|
+
this.encode = opts.encode || this.defaultEncoder
|
|
85
|
+
this.decode = opts.decode || this.defaultDecoder
|
|
86
|
+
} else {
|
|
87
|
+
this.encode = this.defaultEncoder
|
|
88
|
+
this.decode = this.defaultDecoder
|
|
89
|
+
}
|
|
90
|
+
/** @type{number | null} */
|
|
91
|
+
let awaitingConnectionOnPageShow = null
|
|
92
|
+
if(phxWindow && phxWindow.addEventListener){
|
|
93
|
+
phxWindow.addEventListener("pagehide", _e => {
|
|
94
|
+
if(this.conn){
|
|
95
|
+
this.disconnect()
|
|
96
|
+
awaitingConnectionOnPageShow = this.connectClock
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
phxWindow.addEventListener("pageshow", _e => {
|
|
100
|
+
if(awaitingConnectionOnPageShow === this.connectClock){
|
|
101
|
+
awaitingConnectionOnPageShow = null
|
|
102
|
+
this.connect()
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
phxWindow.addEventListener("visibilitychange", () => {
|
|
106
|
+
if(document.visibilityState === "hidden"){
|
|
107
|
+
this.pageHidden = true
|
|
108
|
+
} else {
|
|
109
|
+
this.pageHidden = false
|
|
110
|
+
// reconnect immediately
|
|
111
|
+
if(!this.isConnected() && !this.closeWasClean){
|
|
112
|
+
this.teardown(() => this.connect())
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
/** @type{number} */
|
|
118
|
+
this.heartbeatIntervalMs = opts.heartbeatIntervalMs || 30000
|
|
119
|
+
/** @type{boolean} */
|
|
120
|
+
this.autoSendHeartbeat = opts.autoSendHeartbeat ?? true
|
|
121
|
+
/** @type{HeartbeatCallback} */
|
|
122
|
+
this.heartbeatCallback = opts.heartbeatCallback ?? (() => {})
|
|
123
|
+
/** @type{(tries: number) => number} */
|
|
124
|
+
this.rejoinAfterMs = (tries) => {
|
|
125
|
+
if(opts.rejoinAfterMs){
|
|
126
|
+
return opts.rejoinAfterMs(tries)
|
|
127
|
+
} else {
|
|
128
|
+
return [1000, 2000, 5000][tries - 1] || 10000
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** @type{(tries: number) => number} */
|
|
132
|
+
this.reconnectAfterMs = (tries) => {
|
|
133
|
+
if(opts.reconnectAfterMs){
|
|
134
|
+
return opts.reconnectAfterMs(tries)
|
|
135
|
+
} else {
|
|
136
|
+
return [10, 50, 100, 150, 200, 250, 500, 1000, 2000][tries - 1] || 5000
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** @type{((kind: string, msg: string, data: any) => void) | null} */
|
|
140
|
+
this.logger = opts.logger || null
|
|
141
|
+
if(!this.logger && opts.debug){
|
|
142
|
+
this.logger = (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) }
|
|
143
|
+
}
|
|
144
|
+
/** @type{number} */
|
|
145
|
+
this.longpollerTimeout = opts.longpollerTimeout || 20000
|
|
146
|
+
/** @type{() => Params} */
|
|
147
|
+
this.params = closure(opts.params || {})
|
|
148
|
+
/** @type{string} */
|
|
149
|
+
this.endPoint = `${endPoint}/${TRANSPORTS.websocket}`
|
|
150
|
+
/** @type{Vsn} */
|
|
151
|
+
this.vsn = opts.vsn || DEFAULT_VSN
|
|
152
|
+
/** @type{ReturnType<typeof setTimeout>} */
|
|
153
|
+
this.heartbeatTimeoutTimer = null
|
|
154
|
+
/** @type{ReturnType<typeof setTimeout>} */
|
|
155
|
+
this.heartbeatTimer = null
|
|
156
|
+
/** @type{number | null} */
|
|
157
|
+
this.heartbeatSentAt = null
|
|
158
|
+
/** @type{?string} */
|
|
159
|
+
this.pendingHeartbeatRef = null
|
|
160
|
+
/** @type{Timer} */
|
|
161
|
+
this.reconnectTimer = new Timer( () => {
|
|
162
|
+
if(this.pageHidden){
|
|
163
|
+
this.log("Not reconnecting as page is hidden!")
|
|
164
|
+
this.teardown()
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
this.teardown(async () => {
|
|
169
|
+
if(opts.beforeReconnect) await opts.beforeReconnect()
|
|
170
|
+
this.connect()
|
|
171
|
+
})
|
|
172
|
+
}, this.reconnectAfterMs)
|
|
173
|
+
/** @type{string | undefined} */
|
|
174
|
+
this.authToken = opts.authToken
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Returns the LongPoll transport reference
|
|
179
|
+
*/
|
|
180
|
+
getLongPollTransport(){ return LongPoll }
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Disconnects and replaces the active transport
|
|
184
|
+
*
|
|
185
|
+
* @param {SocketTransport} newTransport - The new transport class to instantiate
|
|
186
|
+
*
|
|
187
|
+
*/
|
|
188
|
+
replaceTransport(newTransport){
|
|
189
|
+
this.connectClock++
|
|
190
|
+
this.closeWasClean = true
|
|
191
|
+
clearTimeout(this.fallbackTimer)
|
|
192
|
+
this.reconnectTimer.reset()
|
|
193
|
+
if(this.conn){
|
|
194
|
+
this.conn.close()
|
|
195
|
+
this.conn = null
|
|
196
|
+
}
|
|
197
|
+
this.transport = newTransport
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Returns the socket protocol
|
|
202
|
+
*
|
|
203
|
+
* @returns {"wss" | "ws"}
|
|
204
|
+
*/
|
|
205
|
+
protocol(){ return location.protocol.match(/^https/) ? "wss" : "ws" }
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The fully qualified socket url
|
|
209
|
+
*
|
|
210
|
+
* @returns {string}
|
|
211
|
+
*/
|
|
212
|
+
endPointURL(){
|
|
213
|
+
let uri = Ajax.appendParams(
|
|
214
|
+
Ajax.appendParams(this.endPoint, this.params()), {vsn: this.vsn})
|
|
215
|
+
if(uri.charAt(0) !== "/"){ return uri }
|
|
216
|
+
if(uri.charAt(1) === "/"){ return `${this.protocol()}:${uri}` }
|
|
217
|
+
|
|
218
|
+
return `${this.protocol()}://${location.host}${uri}`
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Disconnects the socket
|
|
223
|
+
*
|
|
224
|
+
* See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes for valid status codes.
|
|
225
|
+
*
|
|
226
|
+
* @param {() => void} [callback] - Optional callback which is called after socket is disconnected.
|
|
227
|
+
* @param {number} [code] - A status code for disconnection (Optional).
|
|
228
|
+
* @param {string} [reason] - A textual description of the reason to disconnect. (Optional)
|
|
229
|
+
*/
|
|
230
|
+
disconnect(callback, code, reason){
|
|
231
|
+
this.connectClock++
|
|
232
|
+
this.disconnecting = true
|
|
233
|
+
this.closeWasClean = true
|
|
234
|
+
clearTimeout(this.fallbackTimer)
|
|
235
|
+
this.reconnectTimer.reset()
|
|
236
|
+
this.teardown(() => {
|
|
237
|
+
this.disconnecting = false
|
|
238
|
+
callback && callback()
|
|
239
|
+
}, code, reason)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* @param {Params} [params] - [DEPRECATED] The params to send when connecting, for example `{user_id: userToken}`
|
|
244
|
+
*
|
|
245
|
+
* Passing params to connect is deprecated; pass them in the Socket constructor instead:
|
|
246
|
+
* `new Socket("/socket", {params: {user_id: userToken}})`.
|
|
247
|
+
*/
|
|
248
|
+
connect(params){
|
|
249
|
+
if(params){
|
|
250
|
+
console && console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor")
|
|
251
|
+
this.params = closure(params)
|
|
252
|
+
}
|
|
253
|
+
if(this.conn && !this.disconnecting){ return }
|
|
254
|
+
if(this.longPollFallbackMs && this.transport !== LongPoll){
|
|
255
|
+
this.connectWithFallback(LongPoll, this.longPollFallbackMs)
|
|
256
|
+
} else {
|
|
257
|
+
this.transportConnect()
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Logs the message. Override `this.logger` for specialized logging. noops by default
|
|
263
|
+
* @param {string} kind
|
|
264
|
+
* @param {string} msg
|
|
265
|
+
* @param {Object} data
|
|
266
|
+
*/
|
|
267
|
+
log(kind, msg, data){ this.logger && this.logger(kind, msg, data) }
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Returns true if a logger has been set on this socket.
|
|
271
|
+
*/
|
|
272
|
+
hasLogger(){ return this.logger !== null }
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Registers callbacks for connection open events
|
|
276
|
+
*
|
|
277
|
+
* @example socket.onOpen(function(){ console.info("the socket was opened") })
|
|
278
|
+
*
|
|
279
|
+
* @param {SocketOnOpen} callback
|
|
280
|
+
*/
|
|
281
|
+
onOpen(callback){
|
|
282
|
+
let ref = this.makeRef()
|
|
283
|
+
this.stateChangeCallbacks.open.push([ref, callback])
|
|
284
|
+
return ref
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Registers callbacks for connection close events
|
|
289
|
+
* @param {SocketOnClose} callback
|
|
290
|
+
* @returns {string}
|
|
291
|
+
*/
|
|
292
|
+
onClose(callback){
|
|
293
|
+
let ref = this.makeRef()
|
|
294
|
+
this.stateChangeCallbacks.close.push([ref, callback])
|
|
295
|
+
return ref
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Registers callbacks for connection error events
|
|
300
|
+
*
|
|
301
|
+
* @example socket.onError(function(error){ alert("An error occurred") })
|
|
302
|
+
*
|
|
303
|
+
* @param {SocketOnError} callback
|
|
304
|
+
* @returns {string}
|
|
305
|
+
*/
|
|
306
|
+
onError(callback){
|
|
307
|
+
let ref = this.makeRef()
|
|
308
|
+
this.stateChangeCallbacks.error.push([ref, callback])
|
|
309
|
+
return ref
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Registers callbacks for connection message events
|
|
314
|
+
* @param {SocketOnMessage} callback
|
|
315
|
+
* @returns {string}
|
|
316
|
+
*/
|
|
317
|
+
onMessage(callback){
|
|
318
|
+
let ref = this.makeRef()
|
|
319
|
+
this.stateChangeCallbacks.message.push([ref, callback])
|
|
320
|
+
return ref
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Sets a callback that receives lifecycle events for internal heartbeat messages.
|
|
325
|
+
* Useful for instrumenting connection health (e.g. sent/ok/timeout/disconnected).
|
|
326
|
+
* @param {HeartbeatCallback} callback
|
|
327
|
+
*/
|
|
328
|
+
onHeartbeat(callback){
|
|
329
|
+
this.heartbeatCallback = callback
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Pings the server and invokes the callback with the RTT in milliseconds
|
|
334
|
+
* @param {(timeDelta: number) => void} callback
|
|
335
|
+
*
|
|
336
|
+
* Returns true if the ping was pushed or false if unable to be pushed.
|
|
337
|
+
*/
|
|
338
|
+
ping(callback){
|
|
339
|
+
if(!this.isConnected()){ return false }
|
|
340
|
+
let ref = this.makeRef()
|
|
341
|
+
let startTime = Date.now()
|
|
342
|
+
this.push({topic: "phoenix", event: "heartbeat", payload: {}, ref: ref})
|
|
343
|
+
let onMsgRef = this.onMessage(msg => {
|
|
344
|
+
if(msg.ref === ref){
|
|
345
|
+
this.off([onMsgRef])
|
|
346
|
+
callback(Date.now() - startTime)
|
|
347
|
+
}
|
|
348
|
+
})
|
|
349
|
+
return true
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* @private
|
|
354
|
+
*
|
|
355
|
+
* @param {Function}
|
|
356
|
+
*/
|
|
357
|
+
transportName(transport){
|
|
358
|
+
// JavaScript minification, enabled by default in production in Phoenix
|
|
359
|
+
// projects, renames symbols to reduce code size.
|
|
360
|
+
// See https://esbuild.github.io/api/#keep-names.
|
|
361
|
+
// This helper ensures we return the correct name for the LongPoll transport
|
|
362
|
+
// even after minification. The other common transport is WebSocket, which
|
|
363
|
+
// is native to browsers and does not need special handling.
|
|
364
|
+
switch(transport){
|
|
365
|
+
case LongPoll: return "LongPoll"
|
|
366
|
+
default: return transport.name
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* @private
|
|
372
|
+
*/
|
|
373
|
+
transportConnect(){
|
|
374
|
+
this.connectClock++
|
|
375
|
+
this.closeWasClean = false
|
|
376
|
+
let protocols = undefined
|
|
377
|
+
// Sec-WebSocket-Protocol based token
|
|
378
|
+
// (longpoll uses Authorization header instead)
|
|
379
|
+
if(this.authToken){
|
|
380
|
+
protocols = ["phoenix", `${AUTH_TOKEN_PREFIX}${btoa(this.authToken).replace(/=/g, "")}`]
|
|
381
|
+
}
|
|
382
|
+
this.conn = new this.transport(this.endPointURL(), protocols)
|
|
383
|
+
this.conn.binaryType = this.binaryType
|
|
384
|
+
this.conn.timeout = this.longpollerTimeout
|
|
385
|
+
this.conn.onopen = () => this.onConnOpen()
|
|
386
|
+
this.conn.onerror = error => this.onConnError(error)
|
|
387
|
+
this.conn.onmessage = event => this.onConnMessage(event)
|
|
388
|
+
this.conn.onclose = event => this.onConnClose(event)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
getSession(key){ return this.sessionStore && this.sessionStore.getItem(key) }
|
|
392
|
+
|
|
393
|
+
storeSession(key, val){ this.sessionStore && this.sessionStore.setItem(key, val) }
|
|
394
|
+
|
|
395
|
+
connectWithFallback(fallbackTransport, fallbackThreshold = 2500){
|
|
396
|
+
clearTimeout(this.fallbackTimer)
|
|
397
|
+
let established = false
|
|
398
|
+
let primaryTransport = true
|
|
399
|
+
let openRef, errorRef
|
|
400
|
+
let fallbackTransportName = this.transportName(fallbackTransport)
|
|
401
|
+
let fallback = (reason) => {
|
|
402
|
+
this.log("transport", `falling back to ${fallbackTransportName}...`, reason)
|
|
403
|
+
this.off([openRef, errorRef])
|
|
404
|
+
primaryTransport = false
|
|
405
|
+
this.replaceTransport(fallbackTransport)
|
|
406
|
+
this.transportConnect()
|
|
407
|
+
}
|
|
408
|
+
if(this.getSession(`phx:fallback:${fallbackTransportName}`)){ return fallback("memorized") }
|
|
409
|
+
|
|
410
|
+
this.fallbackTimer = setTimeout(fallback, fallbackThreshold)
|
|
411
|
+
|
|
412
|
+
errorRef = this.onError(reason => {
|
|
413
|
+
this.log("transport", "error", reason)
|
|
414
|
+
if(primaryTransport && !established){
|
|
415
|
+
clearTimeout(this.fallbackTimer)
|
|
416
|
+
fallback(reason)
|
|
417
|
+
}
|
|
418
|
+
})
|
|
419
|
+
if(this.fallbackRef){
|
|
420
|
+
this.off([this.fallbackRef])
|
|
421
|
+
}
|
|
422
|
+
this.fallbackRef = this.onOpen(() => {
|
|
423
|
+
established = true
|
|
424
|
+
if(!primaryTransport){
|
|
425
|
+
let fallbackTransportName = this.transportName(fallbackTransport)
|
|
426
|
+
// only memorize LP if we never connected to primary
|
|
427
|
+
if(!this.primaryPassedHealthCheck){ this.storeSession(`phx:fallback:${fallbackTransportName}`, "true") }
|
|
428
|
+
return this.log("transport", `established ${fallbackTransportName} fallback`)
|
|
429
|
+
}
|
|
430
|
+
// if we've established primary, give the fallback a new period to attempt ping
|
|
431
|
+
clearTimeout(this.fallbackTimer)
|
|
432
|
+
this.fallbackTimer = setTimeout(fallback, fallbackThreshold)
|
|
433
|
+
this.ping(rtt => {
|
|
434
|
+
this.log("transport", "connected to primary after", rtt)
|
|
435
|
+
this.primaryPassedHealthCheck = true
|
|
436
|
+
clearTimeout(this.fallbackTimer)
|
|
437
|
+
})
|
|
438
|
+
})
|
|
439
|
+
this.transportConnect()
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
clearHeartbeats(){
|
|
443
|
+
clearTimeout(this.heartbeatTimer)
|
|
444
|
+
clearTimeout(this.heartbeatTimeoutTimer)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
onConnOpen(){
|
|
448
|
+
if(this.hasLogger()) this.log("transport", `connected to ${this.endPointURL()}`)
|
|
449
|
+
this.closeWasClean = false
|
|
450
|
+
this.disconnecting = false
|
|
451
|
+
this.establishedConnections++
|
|
452
|
+
this.flushSendBuffer()
|
|
453
|
+
this.reconnectTimer.reset()
|
|
454
|
+
if(this.autoSendHeartbeat){
|
|
455
|
+
this.resetHeartbeat()
|
|
456
|
+
}
|
|
457
|
+
this.triggerStateCallbacks("open")
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* @private
|
|
462
|
+
*/
|
|
463
|
+
|
|
464
|
+
heartbeatTimeout(){
|
|
465
|
+
if(this.pendingHeartbeatRef){
|
|
466
|
+
this.pendingHeartbeatRef = null
|
|
467
|
+
this.heartbeatSentAt = null
|
|
468
|
+
if(this.hasLogger()){ this.log("transport", "heartbeat timeout. Attempting to re-establish connection") }
|
|
469
|
+
try {
|
|
470
|
+
this.heartbeatCallback("timeout")
|
|
471
|
+
} catch (e){
|
|
472
|
+
this.log("error", "error in heartbeat callback", e)
|
|
473
|
+
}
|
|
474
|
+
this.triggerChanError()
|
|
475
|
+
this.closeWasClean = false
|
|
476
|
+
this.teardown(() => this.reconnectTimer.scheduleTimeout(), WS_CLOSE_NORMAL, "heartbeat timeout")
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
resetHeartbeat(){
|
|
481
|
+
if(this.conn && this.conn.skipHeartbeat){ return }
|
|
482
|
+
this.pendingHeartbeatRef = null
|
|
483
|
+
this.clearHeartbeats()
|
|
484
|
+
this.heartbeatTimer = setTimeout(() => this.sendHeartbeat(), this.heartbeatIntervalMs)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
teardown(callback, code, reason){
|
|
488
|
+
if(!this.conn){
|
|
489
|
+
return callback && callback()
|
|
490
|
+
}
|
|
491
|
+
let connectClock = this.connectClock
|
|
492
|
+
|
|
493
|
+
this.waitForBufferDone(() => {
|
|
494
|
+
if(connectClock !== this.connectClock){ return }
|
|
495
|
+
if(this.conn){
|
|
496
|
+
if(code){ this.conn.close(code, reason || "") } else { this.conn.close() }
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
this.waitForSocketClosed(() => {
|
|
500
|
+
if(connectClock !== this.connectClock){ return }
|
|
501
|
+
if(this.conn){
|
|
502
|
+
this.conn.onopen = function (){ } // noop
|
|
503
|
+
this.conn.onerror = function (){ } // noop
|
|
504
|
+
this.conn.onmessage = function (){ } // noop
|
|
505
|
+
this.conn.onclose = function (){ } // noop
|
|
506
|
+
this.conn = null
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
callback && callback()
|
|
510
|
+
})
|
|
511
|
+
})
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
waitForBufferDone(callback, tries = 1){
|
|
515
|
+
if(tries === 5 || !this.conn || !this.conn.bufferedAmount){
|
|
516
|
+
callback()
|
|
517
|
+
return
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
setTimeout(() => {
|
|
521
|
+
this.waitForBufferDone(callback, tries + 1)
|
|
522
|
+
}, 150 * tries)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
waitForSocketClosed(callback, tries = 1){
|
|
526
|
+
if(tries === 5 || !this.conn || this.conn.readyState === SOCKET_STATES.closed){
|
|
527
|
+
callback()
|
|
528
|
+
return
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
setTimeout(() => {
|
|
532
|
+
this.waitForSocketClosed(callback, tries + 1)
|
|
533
|
+
}, 150 * tries)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* @param {CloseEvent} event
|
|
538
|
+
*/
|
|
539
|
+
onConnClose(event){
|
|
540
|
+
if(this.conn) this.conn.onclose = () => {} // noop to prevent recursive calls in teardown
|
|
541
|
+
if(this.hasLogger()) this.log("transport", "close", event)
|
|
542
|
+
this.triggerChanError()
|
|
543
|
+
this.clearHeartbeats()
|
|
544
|
+
if(!this.closeWasClean){
|
|
545
|
+
this.reconnectTimer.scheduleTimeout()
|
|
546
|
+
}
|
|
547
|
+
this.triggerStateCallbacks("close", event)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* @private
|
|
552
|
+
* @param {Event} error
|
|
553
|
+
*/
|
|
554
|
+
onConnError(error){
|
|
555
|
+
if(this.hasLogger()) this.log("transport", error)
|
|
556
|
+
let transportBefore = this.transport
|
|
557
|
+
let establishedBefore = this.establishedConnections
|
|
558
|
+
this.triggerStateCallbacks("error", error, transportBefore, establishedBefore)
|
|
559
|
+
if(transportBefore === this.transport || establishedBefore > 0){
|
|
560
|
+
this.triggerChanError()
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* @private
|
|
566
|
+
*/
|
|
567
|
+
triggerChanError(){
|
|
568
|
+
this.channels.forEach(channel => {
|
|
569
|
+
if(!(channel.isErrored() || channel.isLeaving() || channel.isClosed())){
|
|
570
|
+
channel.trigger(CHANNEL_EVENTS.error)
|
|
571
|
+
}
|
|
572
|
+
})
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* @returns {string}
|
|
577
|
+
*/
|
|
578
|
+
connectionState(){
|
|
579
|
+
switch(this.conn && this.conn.readyState){
|
|
580
|
+
case SOCKET_STATES.connecting: return "connecting"
|
|
581
|
+
case SOCKET_STATES.open: return "open"
|
|
582
|
+
case SOCKET_STATES.closing: return "closing"
|
|
583
|
+
default: return "closed"
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* @returns {boolean}
|
|
589
|
+
*/
|
|
590
|
+
isConnected(){ return this.connectionState() === "open" }
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
*
|
|
594
|
+
* @param {Channel} channel
|
|
595
|
+
*/
|
|
596
|
+
remove(channel){
|
|
597
|
+
this.off(channel.stateChangeRefs)
|
|
598
|
+
this.channels = this.channels.filter(c => c !== channel)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Removes `onOpen`, `onClose`, `onError,` and `onMessage` registrations.
|
|
603
|
+
*
|
|
604
|
+
* @param {string[]} refs - list of refs returned by calls to
|
|
605
|
+
* `onOpen`, `onClose`, `onError,` and `onMessage`
|
|
606
|
+
*/
|
|
607
|
+
off(refs){
|
|
608
|
+
for(let key in this.stateChangeCallbacks){
|
|
609
|
+
this.stateChangeCallbacks[key] = this.stateChangeCallbacks[key].filter(([ref]) => {
|
|
610
|
+
return refs.indexOf(ref) === -1
|
|
611
|
+
})
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Initiates a new channel for the given topic
|
|
617
|
+
*
|
|
618
|
+
* @param {string} topic
|
|
619
|
+
* @param {Params | (() => Params)} [chanParams]- Parameters for the channel
|
|
620
|
+
* @returns {Channel}
|
|
621
|
+
*/
|
|
622
|
+
channel(topic, chanParams = {}){
|
|
623
|
+
let chan = new Channel(topic, chanParams, this)
|
|
624
|
+
this.channels.push(chan)
|
|
625
|
+
return chan
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* @param {Message<Record<string, any>>} data
|
|
630
|
+
*/
|
|
631
|
+
push(data){
|
|
632
|
+
if(this.hasLogger()){
|
|
633
|
+
let {topic, event, payload, ref, join_ref} = data
|
|
634
|
+
this.log("push", `${topic} ${event} (${join_ref}, ${ref})`, payload)
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if(this.isConnected()){
|
|
638
|
+
this.encode(data, result => this.conn.send(result))
|
|
639
|
+
} else {
|
|
640
|
+
this.sendBuffer.push(() => this.encode(data, result => this.conn.send(result)))
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Return the next message ref, accounting for overflows
|
|
646
|
+
* @returns {string}
|
|
647
|
+
*/
|
|
648
|
+
makeRef(){
|
|
649
|
+
let newRef = this.ref + 1
|
|
650
|
+
if(newRef === this.ref){ this.ref = 0 } else { this.ref = newRef }
|
|
651
|
+
|
|
652
|
+
return this.ref.toString()
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
sendHeartbeat(){
|
|
656
|
+
if(!this.isConnected()){
|
|
657
|
+
try {
|
|
658
|
+
this.heartbeatCallback("disconnected")
|
|
659
|
+
} catch (e){
|
|
660
|
+
this.log("error", "error in heartbeat callback", e)
|
|
661
|
+
}
|
|
662
|
+
return
|
|
663
|
+
}
|
|
664
|
+
if(this.pendingHeartbeatRef){
|
|
665
|
+
this.heartbeatTimeout()
|
|
666
|
+
return
|
|
667
|
+
}
|
|
668
|
+
this.pendingHeartbeatRef = this.makeRef()
|
|
669
|
+
this.heartbeatSentAt = Date.now()
|
|
670
|
+
this.push({topic: "phoenix", event: "heartbeat", payload: {}, ref: this.pendingHeartbeatRef})
|
|
671
|
+
try {
|
|
672
|
+
this.heartbeatCallback("sent")
|
|
673
|
+
} catch (e){
|
|
674
|
+
this.log("error", "error in heartbeat callback", e)
|
|
675
|
+
}
|
|
676
|
+
this.heartbeatTimeoutTimer = setTimeout(() => this.heartbeatTimeout(), this.heartbeatIntervalMs)
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
flushSendBuffer(){
|
|
680
|
+
if(this.isConnected() && this.sendBuffer.length > 0){
|
|
681
|
+
this.sendBuffer.forEach(callback => callback())
|
|
682
|
+
this.sendBuffer = []
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* @param {MessageEvent<any>} rawMessage
|
|
688
|
+
*/
|
|
689
|
+
onConnMessage(rawMessage){
|
|
690
|
+
this.decode(rawMessage.data, msg => {
|
|
691
|
+
let {topic, event, payload, ref, join_ref} = msg
|
|
692
|
+
if(ref && ref === this.pendingHeartbeatRef){
|
|
693
|
+
const latency = this.heartbeatSentAt ? Date.now() - this.heartbeatSentAt : undefined
|
|
694
|
+
this.clearHeartbeats()
|
|
695
|
+
try {
|
|
696
|
+
this.heartbeatCallback(payload.status === "ok" ? "ok" : "error", latency)
|
|
697
|
+
} catch (e){
|
|
698
|
+
this.log("error", "error in heartbeat callback", e)
|
|
699
|
+
}
|
|
700
|
+
this.pendingHeartbeatRef = null
|
|
701
|
+
this.heartbeatSentAt = null
|
|
702
|
+
if(this.autoSendHeartbeat){
|
|
703
|
+
this.heartbeatTimer = setTimeout(() => this.sendHeartbeat(), this.heartbeatIntervalMs)
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if(this.hasLogger()) this.log("receive", `${payload.status || ""} ${topic} ${event} ${ref && "(" + ref + ")" || ""}`.trim(), payload)
|
|
708
|
+
|
|
709
|
+
for(let i = 0; i < this.channels.length; i++){
|
|
710
|
+
const channel = this.channels[i]
|
|
711
|
+
if(!channel.isMember(topic, event, payload, join_ref)){ continue }
|
|
712
|
+
channel.trigger(event, payload, ref, join_ref)
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
this.triggerStateCallbacks("message", msg)
|
|
716
|
+
})
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* @private
|
|
721
|
+
* @template {keyof SocketStateChangeCallbacks} K
|
|
722
|
+
* @param {K} event
|
|
723
|
+
* @param {...Parameters<SocketStateChangeCallbacks[K][number][1]>} args
|
|
724
|
+
* @returns {void}
|
|
725
|
+
*/
|
|
726
|
+
triggerStateCallbacks(event, ...args){
|
|
727
|
+
try {
|
|
728
|
+
this.stateChangeCallbacks[event].forEach(([_, callback]) => {
|
|
729
|
+
try {
|
|
730
|
+
callback(...args)
|
|
731
|
+
} catch (e){
|
|
732
|
+
this.log("error", `error in ${event} callback`, e)
|
|
733
|
+
}
|
|
734
|
+
})
|
|
735
|
+
} catch (e){
|
|
736
|
+
this.log("error", `error triggering ${event} callbacks`, e)
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
leaveOpenTopic(topic){
|
|
741
|
+
let dupChannel = this.channels.find(c => c.topic === topic && (c.isJoined() || c.isJoining()))
|
|
742
|
+
if(dupChannel){
|
|
743
|
+
if(this.hasLogger()) this.log("transport", `leaving duplicate topic "${topic}"`)
|
|
744
|
+
dupChannel.leave()
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|