| "use strict"; |
| var __defProp = Object.defineProperty; |
| var __getOwnPropDesc = Object.getOwnPropertyDescriptor; |
| var __getOwnPropNames = Object.getOwnPropertyNames; |
| var __hasOwnProp = Object.prototype.hasOwnProperty; |
| var __export = (target, all) => { |
| for (var name in all) |
| __defProp(target, name, { get: all[name], enumerable: true }); |
| }; |
| var __copyProps = (to, from, except, desc) => { |
| if (from && typeof from === "object" || typeof from === "function") { |
| for (let key of __getOwnPropNames(from)) |
| if (!__hasOwnProp.call(to, key) && key !== except) |
| __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); |
| } |
| return to; |
| }; |
| var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); |
|
|
| |
| var phoenix_exports = {}; |
| __export(phoenix_exports, { |
| Channel: () => Channel, |
| LongPoll: () => LongPoll, |
| Presence: () => Presence, |
| Push: () => Push, |
| Serializer: () => serializer_default, |
| Socket: () => Socket, |
| Timer: () => Timer |
| }); |
| module.exports = __toCommonJS(phoenix_exports); |
|
|
| |
| var closure = (value) => { |
| if (typeof value === "function") { |
| return ( |
| |
| value |
| ); |
| } else { |
| let closure2 = function() { |
| return value; |
| }; |
| return closure2; |
| } |
| }; |
|
|
| |
| var globalSelf = typeof self !== "undefined" ? self : null; |
| var phxWindow = typeof window !== "undefined" ? window : null; |
| var global = globalSelf || phxWindow || globalThis; |
| var DEFAULT_VSN = "2.0.0"; |
| var DEFAULT_TIMEOUT = 1e4; |
| var WS_CLOSE_NORMAL = 1e3; |
| var MAX_LONGPOLL_BATCH_SIZE = 100; |
| var SOCKET_STATES = ( |
| |
| { connecting: 0, open: 1, closing: 2, closed: 3 } |
| ); |
| var CHANNEL_STATES = ( |
| |
| { |
| closed: "closed", |
| errored: "errored", |
| joined: "joined", |
| joining: "joining", |
| leaving: "leaving" |
| } |
| ); |
| var CHANNEL_EVENTS = ( |
| |
| { |
| close: "phx_close", |
| error: "phx_error", |
| join: "phx_join", |
| reply: "phx_reply", |
| leave: "phx_leave" |
| } |
| ); |
| var TRANSPORTS = ( |
| |
| { |
| longpoll: "longpoll", |
| websocket: "websocket" |
| } |
| ); |
| var XHR_STATES = ( |
| |
| { |
| complete: 4 |
| } |
| ); |
| var AUTH_TOKEN_PREFIX = "base64url.bearer.phx."; |
|
|
| |
| var Push = class { |
| |
| |
| |
| |
| |
| |
| |
| constructor(channel, event, payload, timeout) { |
| this.channel = channel; |
| this.event = event; |
| this.payload = payload || function() { |
| return {}; |
| }; |
| this.receivedResp = null; |
| this.timeout = timeout; |
| this.timeoutTimer = null; |
| this.recHooks = []; |
| this.sent = false; |
| this.ref = void 0; |
| } |
| |
| |
| |
| |
| resend(timeout) { |
| this.timeout = timeout; |
| this.reset(); |
| this.send(); |
| } |
| |
| |
| |
| send() { |
| if (this.hasReceived("timeout")) { |
| return; |
| } |
| this.startTimeout(); |
| this.sent = true; |
| this.channel.socket.push({ |
| topic: this.channel.topic, |
| event: this.event, |
| payload: this.payload(), |
| ref: this.ref, |
| join_ref: this.channel.joinRef() |
| }); |
| } |
| |
| |
| |
| |
| |
| receive(status, callback) { |
| if (this.hasReceived(status)) { |
| callback(this.receivedResp.response); |
| } |
| this.recHooks.push({ status, callback }); |
| return this; |
| } |
| reset() { |
| this.cancelRefEvent(); |
| this.ref = null; |
| this.refEvent = null; |
| this.receivedResp = null; |
| this.sent = false; |
| } |
| destroy() { |
| this.cancelRefEvent(); |
| this.cancelTimeout(); |
| } |
| |
| |
| |
| matchReceive({ status, response, _ref }) { |
| this.recHooks.filter((h) => h.status === status).forEach((h) => h.callback(response)); |
| } |
| |
| |
| |
| cancelRefEvent() { |
| if (!this.refEvent) { |
| return; |
| } |
| this.channel.off(this.refEvent); |
| } |
| cancelTimeout() { |
| clearTimeout(this.timeoutTimer); |
| this.timeoutTimer = null; |
| } |
| startTimeout() { |
| if (this.timeoutTimer) { |
| this.cancelTimeout(); |
| } |
| this.ref = this.channel.socket.makeRef(); |
| this.refEvent = this.channel.replyEventName(this.ref); |
| this.channel.on(this.refEvent, (payload) => { |
| this.cancelRefEvent(); |
| this.cancelTimeout(); |
| this.receivedResp = payload; |
| this.matchReceive(payload); |
| }); |
| this.timeoutTimer = setTimeout(() => { |
| this.trigger("timeout", {}); |
| }, this.timeout); |
| } |
| |
| |
| |
| hasReceived(status) { |
| return this.receivedResp && this.receivedResp.status === status; |
| } |
| trigger(status, response) { |
| this.channel.trigger(this.refEvent, { status, response }); |
| } |
| }; |
|
|
| |
| var Timer = class { |
| |
| |
| |
| |
| constructor(callback, timerCalc) { |
| this.callback = callback; |
| this.timerCalc = timerCalc; |
| this.timer = void 0; |
| this.tries = 0; |
| } |
| reset() { |
| this.tries = 0; |
| clearTimeout(this.timer); |
| } |
| |
| |
| |
| scheduleTimeout() { |
| clearTimeout(this.timer); |
| this.timer = setTimeout(() => { |
| this.tries = this.tries + 1; |
| this.callback(); |
| }, this.timerCalc(this.tries + 1)); |
| } |
| }; |
|
|
| |
| var Channel = class { |
| |
| |
| |
| |
| |
| constructor(topic, params, socket) { |
| this.state = CHANNEL_STATES.closed; |
| this.topic = topic; |
| this.params = closure(params || {}); |
| this.socket = socket; |
| this.bindings = []; |
| this.bindingRef = 0; |
| this.timeout = this.socket.timeout; |
| this.joinedOnce = false; |
| this.joinPush = new Push(this, CHANNEL_EVENTS.join, this.params, this.timeout); |
| this.pushBuffer = []; |
| this.stateChangeRefs = []; |
| this.rejoinTimer = new Timer(() => { |
| if (this.socket.isConnected()) { |
| this.rejoin(); |
| } |
| }, this.socket.rejoinAfterMs); |
| this.stateChangeRefs.push(this.socket.onError(() => this.rejoinTimer.reset())); |
| this.stateChangeRefs.push( |
| this.socket.onOpen(() => { |
| this.rejoinTimer.reset(); |
| if (this.isErrored()) { |
| this.rejoin(); |
| } |
| }) |
| ); |
| this.joinPush.receive("ok", () => { |
| this.state = CHANNEL_STATES.joined; |
| this.rejoinTimer.reset(); |
| this.pushBuffer.forEach((pushEvent) => pushEvent.send()); |
| this.pushBuffer = []; |
| }); |
| this.joinPush.receive("error", (reason) => { |
| this.state = CHANNEL_STATES.errored; |
| if (this.socket.hasLogger()) this.socket.log("channel", `error ${this.topic}`, reason); |
| if (this.socket.isConnected()) { |
| this.rejoinTimer.scheduleTimeout(); |
| } |
| }); |
| this.onClose(() => { |
| this.rejoinTimer.reset(); |
| if (this.socket.hasLogger()) this.socket.log("channel", `close ${this.topic}`); |
| this.state = CHANNEL_STATES.closed; |
| this.socket.remove(this); |
| }); |
| this.onError((reason) => { |
| if (this.socket.hasLogger()) this.socket.log("channel", `error ${this.topic}`, reason); |
| if (this.isJoining()) { |
| this.joinPush.reset(); |
| } |
| this.state = CHANNEL_STATES.errored; |
| if (this.socket.isConnected()) { |
| this.rejoinTimer.scheduleTimeout(); |
| } |
| }); |
| this.joinPush.receive("timeout", () => { |
| if (this.socket.hasLogger()) this.socket.log("channel", `timeout ${this.topic}`, this.joinPush.timeout); |
| let leavePush = new Push(this, CHANNEL_EVENTS.leave, closure({}), this.timeout); |
| leavePush.send(); |
| this.state = CHANNEL_STATES.errored; |
| this.joinPush.reset(); |
| if (this.socket.isConnected()) { |
| this.rejoinTimer.scheduleTimeout(); |
| } |
| }); |
| this.on(CHANNEL_EVENTS.reply, (payload, ref) => { |
| this.trigger(this.replyEventName(ref), payload); |
| }); |
| } |
| |
| |
| |
| |
| |
| join(timeout = this.timeout) { |
| if (this.joinedOnce) { |
| throw new Error("tried to join multiple times. 'join' can only be called a single time per channel instance"); |
| } else { |
| this.timeout = timeout; |
| this.joinedOnce = true; |
| this.rejoin(); |
| return this.joinPush; |
| } |
| } |
| |
| |
| |
| |
| |
| teardown() { |
| this.pushBuffer.forEach((push) => push.destroy()); |
| this.pushBuffer = []; |
| this.rejoinTimer.reset(); |
| this.joinPush.destroy(); |
| this.state = CHANNEL_STATES.closed; |
| this.bindings = []; |
| } |
| |
| |
| |
| |
| onClose(callback) { |
| this.on(CHANNEL_EVENTS.close, callback); |
| } |
| |
| |
| |
| |
| |
| onError(callback) { |
| return this.on(CHANNEL_EVENTS.error, (reason) => callback(reason)); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| on(event, callback) { |
| let ref = this.bindingRef++; |
| this.bindings.push({ event, ref, callback }); |
| return ref; |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| off(event, ref) { |
| this.bindings = this.bindings.filter((bind) => { |
| return !(bind.event === event && (typeof ref === "undefined" || ref === bind.ref)); |
| }); |
| } |
| |
| |
| |
| canPush() { |
| return this.socket.isConnected() && this.isJoined(); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| push(event, payload, timeout = this.timeout) { |
| payload = payload || {}; |
| if (!this.joinedOnce) { |
| throw new Error(`tried to push '${event}' to '${this.topic}' before joining. Use channel.join() before pushing events`); |
| } |
| let pushEvent = new Push(this, event, function() { |
| return payload; |
| }, timeout); |
| if (this.canPush()) { |
| pushEvent.send(); |
| } else { |
| pushEvent.startTimeout(); |
| this.pushBuffer.push(pushEvent); |
| } |
| return pushEvent; |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| leave(timeout = this.timeout) { |
| this.rejoinTimer.reset(); |
| this.joinPush.cancelTimeout(); |
| this.state = CHANNEL_STATES.leaving; |
| let onClose = () => { |
| if (this.socket.hasLogger()) this.socket.log("channel", `leave ${this.topic}`); |
| this.trigger(CHANNEL_EVENTS.close, "leave"); |
| }; |
| let leavePush = new Push(this, CHANNEL_EVENTS.leave, closure({}), timeout); |
| leavePush.receive("ok", () => onClose()).receive("timeout", () => onClose()); |
| leavePush.send(); |
| if (!this.canPush()) { |
| leavePush.trigger("ok", {}); |
| } |
| return leavePush; |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| onMessage(_event, payload, _ref) { |
| return payload; |
| } |
| |
| |
| |
| |
| |
| |
| |
| filterBindings(_binding, _payload, _ref) { |
| return true; |
| } |
| isMember(topic, event, payload, joinRef) { |
| if (this.topic !== topic) { |
| return false; |
| } |
| if (joinRef && joinRef !== this.joinRef()) { |
| if (this.socket.hasLogger()) this.socket.log("channel", "dropping outdated message", { topic, event, payload, joinRef }); |
| return false; |
| } else { |
| return true; |
| } |
| } |
| joinRef() { |
| return this.joinPush.ref; |
| } |
| |
| |
| |
| rejoin(timeout = this.timeout) { |
| if (this.isLeaving()) { |
| return; |
| } |
| this.socket.leaveOpenTopic(this.topic); |
| this.state = CHANNEL_STATES.joining; |
| this.joinPush.resend(timeout); |
| } |
| |
| |
| |
| |
| |
| |
| trigger(event, payload, ref, joinRef) { |
| let handledPayload = this.onMessage(event, payload, ref, joinRef); |
| if (payload && !handledPayload) { |
| throw new Error("channel onMessage callbacks must return the payload, modified or unmodified"); |
| } |
| let eventBindings = this.bindings.filter((bind) => bind.event === event && this.filterBindings(bind, payload, ref)); |
| for (let i = 0; i < eventBindings.length; i++) { |
| let bind = eventBindings[i]; |
| bind.callback(handledPayload, ref, joinRef || this.joinRef()); |
| } |
| } |
| |
| |
| |
| replyEventName(ref) { |
| return `chan_reply_${ref}`; |
| } |
| isClosed() { |
| return this.state === CHANNEL_STATES.closed; |
| } |
| isErrored() { |
| return this.state === CHANNEL_STATES.errored; |
| } |
| isJoined() { |
| return this.state === CHANNEL_STATES.joined; |
| } |
| isJoining() { |
| return this.state === CHANNEL_STATES.joining; |
| } |
| isLeaving() { |
| return this.state === CHANNEL_STATES.leaving; |
| } |
| }; |
|
|
| |
| var Ajax = class { |
| static request(method, endPoint, headers, body, timeout, ontimeout, callback) { |
| if (global.XDomainRequest) { |
| let req = new global.XDomainRequest(); |
| return this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback); |
| } else if (global.XMLHttpRequest) { |
| let req = new global.XMLHttpRequest(); |
| return this.xhrRequest(req, method, endPoint, headers, body, timeout, ontimeout, callback); |
| } else if (global.fetch && global.AbortController) { |
| return this.fetchRequest(method, endPoint, headers, body, timeout, ontimeout, callback); |
| } else { |
| throw new Error("No suitable XMLHttpRequest implementation found"); |
| } |
| } |
| static fetchRequest(method, endPoint, headers, body, timeout, ontimeout, callback) { |
| let options = { |
| method, |
| headers, |
| body |
| }; |
| let controller = null; |
| if (timeout) { |
| controller = new AbortController(); |
| const _timeoutId = setTimeout(() => controller.abort(), timeout); |
| options.signal = controller.signal; |
| } |
| global.fetch(endPoint, options).then((response) => response.text()).then((data) => this.parseJSON(data)).then((data) => callback && callback(data)).catch((err) => { |
| if (err.name === "AbortError" && ontimeout) { |
| ontimeout(); |
| } else { |
| callback && callback(null); |
| } |
| }); |
| return controller; |
| } |
| static xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback) { |
| req.timeout = timeout; |
| req.open(method, endPoint); |
| req.onload = () => { |
| let response = this.parseJSON(req.responseText); |
| callback && callback(response); |
| }; |
| if (ontimeout) { |
| req.ontimeout = ontimeout; |
| } |
| req.onprogress = () => { |
| }; |
| req.send(body); |
| return req; |
| } |
| static xhrRequest(req, method, endPoint, headers, body, timeout, ontimeout, callback) { |
| req.open(method, endPoint, true); |
| req.timeout = timeout; |
| for (let [key, value] of Object.entries(headers)) { |
| req.setRequestHeader(key, value); |
| } |
| req.onerror = () => callback && callback(null); |
| req.onreadystatechange = () => { |
| if (req.readyState === XHR_STATES.complete && callback) { |
| let response = this.parseJSON(req.responseText); |
| callback(response); |
| } |
| }; |
| if (ontimeout) { |
| req.ontimeout = ontimeout; |
| } |
| req.send(body); |
| return req; |
| } |
| static parseJSON(resp) { |
| if (!resp || resp === "") { |
| return null; |
| } |
| try { |
| return JSON.parse(resp); |
| } catch { |
| console && console.log("failed to parse JSON response", resp); |
| return null; |
| } |
| } |
| static serialize(obj, parentKey) { |
| let queryStr = []; |
| for (var key in obj) { |
| if (!Object.prototype.hasOwnProperty.call(obj, key)) { |
| continue; |
| } |
| let paramKey = parentKey ? `${parentKey}[${key}]` : key; |
| let paramVal = obj[key]; |
| if (typeof paramVal === "object") { |
| queryStr.push(this.serialize(paramVal, paramKey)); |
| } else { |
| queryStr.push(encodeURIComponent(paramKey) + "=" + encodeURIComponent(paramVal)); |
| } |
| } |
| return queryStr.join("&"); |
| } |
| static appendParams(url, params) { |
| if (Object.keys(params).length === 0) { |
| return url; |
| } |
| let prefix = url.match(/\?/) ? "&" : "?"; |
| return `${url}${prefix}${this.serialize(params)}`; |
| } |
| }; |
|
|
| |
| var arrayBufferToBase64 = (buffer) => { |
| let binary = ""; |
| let bytes = new Uint8Array(buffer); |
| let len = bytes.byteLength; |
| for (let i = 0; i < len; i++) { |
| binary += String.fromCharCode(bytes[i]); |
| } |
| return btoa(binary); |
| }; |
| var LongPoll = class { |
| constructor(endPoint, protocols) { |
| if (protocols && protocols.length === 2 && protocols[1].startsWith(AUTH_TOKEN_PREFIX)) { |
| this.authToken = atob(protocols[1].slice(AUTH_TOKEN_PREFIX.length)); |
| } |
| this.endPoint = null; |
| this.token = null; |
| this.skipHeartbeat = true; |
| this.reqs = new Set(); |
| this.awaitingBatchAck = false; |
| this.currentBatch = null; |
| this.currentBatchTimer = null; |
| this.batchBuffer = []; |
| this.onopen = function() { |
| }; |
| this.onerror = function() { |
| }; |
| this.onmessage = function() { |
| }; |
| this.onclose = function() { |
| }; |
| this.pollEndpoint = this.normalizeEndpoint(endPoint); |
| this.readyState = SOCKET_STATES.connecting; |
| setTimeout(() => this.poll(), 0); |
| } |
| normalizeEndpoint(endPoint) { |
| return endPoint.replace("ws://", "http://").replace("wss://", "https://").replace(new RegExp("(.*)/" + TRANSPORTS.websocket), "$1/" + TRANSPORTS.longpoll); |
| } |
| endpointURL() { |
| return Ajax.appendParams(this.pollEndpoint, { token: this.token }); |
| } |
| closeAndRetry(code, reason, wasClean) { |
| this.close(code, reason, wasClean); |
| this.readyState = SOCKET_STATES.connecting; |
| } |
| ontimeout() { |
| this.onerror("timeout"); |
| this.closeAndRetry(1005, "timeout", false); |
| } |
| isActive() { |
| return this.readyState === SOCKET_STATES.open || this.readyState === SOCKET_STATES.connecting; |
| } |
| poll() { |
| const headers = { "Accept": "application/json" }; |
| if (this.authToken) { |
| headers["X-Phoenix-AuthToken"] = this.authToken; |
| } |
| this.ajax("GET", headers, null, () => this.ontimeout(), (resp) => { |
| if (resp) { |
| var { status, token, messages } = resp; |
| if (status === 410 && this.token !== null) { |
| this.onerror(410); |
| this.closeAndRetry(3410, "session_gone", false); |
| return; |
| } |
| this.token = token; |
| } else { |
| status = 0; |
| } |
| switch (status) { |
| case 200: |
| messages.forEach((msg) => { |
| setTimeout(() => this.onmessage({ data: msg }), 0); |
| }); |
| this.poll(); |
| break; |
| case 204: |
| this.poll(); |
| break; |
| case 410: |
| this.readyState = SOCKET_STATES.open; |
| this.onopen({}); |
| this.poll(); |
| break; |
| case 403: |
| this.onerror(403); |
| this.close(1008, "forbidden", false); |
| break; |
| case 0: |
| case 500: |
| this.onerror(500); |
| this.closeAndRetry(1011, "internal server error", 500); |
| break; |
| default: |
| throw new Error(`unhandled poll status ${status}`); |
| } |
| }); |
| } |
| |
| |
| |
| send(body) { |
| if (typeof body !== "string") { |
| body = arrayBufferToBase64(body); |
| } |
| if (this.currentBatch) { |
| this.currentBatch.push(body); |
| } else if (this.awaitingBatchAck) { |
| this.batchBuffer.push(body); |
| } else { |
| this.currentBatch = [body]; |
| this.currentBatchTimer = setTimeout(() => { |
| this.batchSend(this.currentBatch); |
| this.currentBatch = null; |
| }, 0); |
| } |
| } |
| batchSend(messages, offset = 0) { |
| this.awaitingBatchAck = true; |
| const next = offset + MAX_LONGPOLL_BATCH_SIZE; |
| const batch = messages.slice(offset, next); |
| this.ajax("POST", { "Content-Type": "application/x-ndjson" }, batch.join("\n"), () => this.onerror("timeout"), (resp) => { |
| if (!resp || resp.status !== 200) { |
| this.awaitingBatchAck = false; |
| this.onerror(resp && resp.status); |
| this.closeAndRetry(1011, "internal server error", false); |
| } else if (next < messages.length) { |
| this.batchSend(messages, next); |
| } else if (this.batchBuffer.length > 0) { |
| this.batchSend(this.batchBuffer); |
| this.batchBuffer = []; |
| } else { |
| this.awaitingBatchAck = false; |
| } |
| }); |
| } |
| close(code, reason, wasClean) { |
| for (let req of this.reqs) { |
| req.abort(); |
| } |
| this.readyState = SOCKET_STATES.closed; |
| let opts = Object.assign({ code: 1e3, reason: void 0, wasClean: true }, { code, reason, wasClean }); |
| this.batchBuffer = []; |
| clearTimeout(this.currentBatchTimer); |
| this.currentBatchTimer = null; |
| if (typeof CloseEvent !== "undefined") { |
| this.onclose(new CloseEvent("close", opts)); |
| } else { |
| this.onclose(opts); |
| } |
| } |
| ajax(method, headers, body, onCallerTimeout, callback) { |
| let req; |
| let ontimeout = () => { |
| this.reqs.delete(req); |
| onCallerTimeout(); |
| }; |
| req = Ajax.request(method, this.endpointURL(), headers, body, this.timeout, ontimeout, (resp) => { |
| this.reqs.delete(req); |
| if (this.isActive()) { |
| callback(resp); |
| } |
| }); |
| this.reqs.add(req); |
| } |
| }; |
|
|
| |
| var Presence = class _Presence { |
| |
| |
| |
| |
| |
| constructor(channel, opts = {}) { |
| let events = opts.events || |
| { state: "presence_state", diff: "presence_diff" }; |
| this.state = Object.create(null); |
| this.pendingDiffs = []; |
| this.channel = channel; |
| this.joinRef = null; |
| this.caller = { |
| onJoin: function() { |
| }, |
| onLeave: function() { |
| }, |
| onSync: function() { |
| } |
| }; |
| this.channel.on(events.state, (newState) => { |
| let { onJoin, onLeave, onSync } = this.caller; |
| this.joinRef = this.channel.joinRef(); |
| this.state = _Presence.syncState(this.state, newState, onJoin, onLeave); |
| this.pendingDiffs.forEach((diff) => { |
| this.state = _Presence.syncDiff(this.state, diff, onJoin, onLeave); |
| }); |
| this.pendingDiffs = []; |
| onSync(); |
| }); |
| this.channel.on(events.diff, (diff) => { |
| let { onJoin, onLeave, onSync } = this.caller; |
| if (this.inPendingSyncState()) { |
| this.pendingDiffs.push(diff); |
| } else { |
| this.state = _Presence.syncDiff(this.state, diff, onJoin, onLeave); |
| onSync(); |
| } |
| }); |
| } |
| |
| |
| |
| onJoin(callback) { |
| this.caller.onJoin = callback; |
| } |
| |
| |
| |
| onLeave(callback) { |
| this.caller.onLeave = callback; |
| } |
| |
| |
| |
| onSync(callback) { |
| this.caller.onSync = callback; |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| list(by) { |
| return _Presence.list(this.state, by); |
| } |
| inPendingSyncState() { |
| return !this.joinRef || this.joinRef !== this.channel.joinRef(); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| static syncState(currentState, newState, onJoin, onLeave) { |
| let state = this.toNullProtoObj(this.clone(currentState)); |
| newState = this.toNullProtoObj(newState); |
| let joins = Object.create(null); |
| let leaves = Object.create(null); |
| this.map(state, (key, presence) => { |
| if (!newState[key]) { |
| leaves[key] = presence; |
| } |
| }); |
| this.map(newState, (key, newPresence) => { |
| let currentPresence = state[key]; |
| if (currentPresence) { |
| let newRefs = newPresence.metas.map((m) => m.phx_ref); |
| let curRefs = currentPresence.metas.map((m) => m.phx_ref); |
| let joinedMetas = newPresence.metas.filter((m) => curRefs.indexOf(m.phx_ref) < 0); |
| let leftMetas = currentPresence.metas.filter((m) => newRefs.indexOf(m.phx_ref) < 0); |
| if (joinedMetas.length > 0) { |
| joins[key] = newPresence; |
| joins[key].metas = joinedMetas; |
| } |
| if (leftMetas.length > 0) { |
| leaves[key] = this.clone(currentPresence); |
| leaves[key].metas = leftMetas; |
| } |
| } else { |
| joins[key] = newPresence; |
| } |
| }); |
| return this.syncDiff(state, { joins, leaves }, onJoin, onLeave); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| static syncDiff(state, diff, onJoin, onLeave) { |
| state = this.toNullProtoObj(state); |
| let { joins, leaves } = this.clone(diff); |
| if (!onJoin) { |
| onJoin = function() { |
| }; |
| } |
| if (!onLeave) { |
| onLeave = function() { |
| }; |
| } |
| this.map(joins, (key, newPresence) => { |
| let currentPresence = state[key]; |
| state[key] = this.clone(newPresence); |
| if (currentPresence) { |
| let joinedRefs = state[key].metas.map((m) => m.phx_ref); |
| let curMetas = currentPresence.metas.filter((m) => joinedRefs.indexOf(m.phx_ref) < 0); |
| state[key].metas.unshift(...curMetas); |
| } |
| onJoin(key, currentPresence, newPresence); |
| }); |
| this.map(leaves, (key, leftPresence) => { |
| let currentPresence = state[key]; |
| if (!currentPresence) { |
| return; |
| } |
| let refsToRemove = leftPresence.metas.map((m) => m.phx_ref); |
| currentPresence.metas = currentPresence.metas.filter((p) => { |
| return refsToRemove.indexOf(p.phx_ref) < 0; |
| }); |
| onLeave(key, currentPresence, leftPresence); |
| if (currentPresence.metas.length === 0) { |
| delete state[key]; |
| } |
| }); |
| return state; |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| static list(presences, chooser) { |
| if (!chooser) { |
| chooser = function(key, pres) { |
| return pres; |
| }; |
| } |
| return this.map(presences, (key, presence) => { |
| return chooser(key, presence); |
| }); |
| } |
| |
| |
| |
| |
| |
| |
| static map(obj, func) { |
| return Object.getOwnPropertyNames(obj).map((key) => func(key, obj[key])); |
| } |
| |
| |
| |
| |
| |
| |
| static toNullProtoObj(obj) { |
| if (Object.getPrototypeOf(obj) === null) { |
| return obj; |
| } |
| let cleaned = Object.create(null); |
| Object.getOwnPropertyNames(obj).forEach((key) => { |
| cleaned[key] = obj[key]; |
| }); |
| return cleaned; |
| } |
| |
| |
| |
| |
| |
| static clone(obj) { |
| return JSON.parse(JSON.stringify(obj)); |
| } |
| }; |
|
|
| |
| var serializer_default = { |
| HEADER_LENGTH: 1, |
| META_LENGTH: 4, |
| KINDS: { push: 0, reply: 1, broadcast: 2 }, |
| |
| |
| |
| |
| |
| |
| encode(msg, callback) { |
| if (msg.payload.constructor === ArrayBuffer) { |
| return callback(this.binaryEncode(msg)); |
| } else { |
| let payload = [msg.join_ref, msg.ref, msg.topic, msg.event, msg.payload]; |
| return callback(JSON.stringify(payload)); |
| } |
| }, |
| |
| |
| |
| |
| |
| |
| decode(rawPayload, callback) { |
| if (rawPayload.constructor === ArrayBuffer) { |
| return callback(this.binaryDecode(rawPayload)); |
| } else { |
| let [join_ref, ref, topic, event, payload] = JSON.parse(rawPayload); |
| return callback({ join_ref, ref, topic, event, payload }); |
| } |
| }, |
| |
| binaryEncode(message) { |
| let { join_ref, ref, event, topic, payload } = message; |
| let encoder = new TextEncoder(); |
| let joinRefBytes = encoder.encode(join_ref); |
| let refBytes = encoder.encode(ref); |
| let topicBytes = encoder.encode(topic); |
| let eventBytes = encoder.encode(event); |
| this.assertFieldSize(joinRefBytes.byteLength, "join_ref"); |
| this.assertFieldSize(refBytes.byteLength, "ref"); |
| this.assertFieldSize(topicBytes.byteLength, "topic"); |
| this.assertFieldSize(eventBytes.byteLength, "event"); |
| let metaLength = this.META_LENGTH + joinRefBytes.byteLength + refBytes.byteLength + topicBytes.byteLength + eventBytes.byteLength; |
| let header = new ArrayBuffer(this.HEADER_LENGTH + metaLength); |
| let headerBytes = new Uint8Array(header); |
| let view = new DataView(header); |
| let offset = 0; |
| view.setUint8(offset++, this.KINDS.push); |
| view.setUint8(offset++, joinRefBytes.byteLength); |
| view.setUint8(offset++, refBytes.byteLength); |
| view.setUint8(offset++, topicBytes.byteLength); |
| view.setUint8(offset++, eventBytes.byteLength); |
| headerBytes.set(joinRefBytes, offset); |
| offset += joinRefBytes.byteLength; |
| headerBytes.set(refBytes, offset); |
| offset += refBytes.byteLength; |
| headerBytes.set(topicBytes, offset); |
| offset += topicBytes.byteLength; |
| headerBytes.set(eventBytes, offset); |
| offset += eventBytes.byteLength; |
| var combined = new Uint8Array(header.byteLength + payload.byteLength); |
| combined.set(headerBytes, 0); |
| combined.set(new Uint8Array(payload), header.byteLength); |
| return combined.buffer; |
| }, |
| assertFieldSize(size, name) { |
| if (size > 255) { |
| throw new Error(`unable to convert ${name} to binary: must be less than or equal to 255 bytes, but is ${size} bytes`); |
| } |
| }, |
| |
| |
| |
| binaryDecode(buffer) { |
| let view = new DataView(buffer); |
| let kind = view.getUint8(0); |
| let decoder = new TextDecoder(); |
| switch (kind) { |
| case this.KINDS.push: |
| return this.decodePush(buffer, view, decoder); |
| case this.KINDS.reply: |
| return this.decodeReply(buffer, view, decoder); |
| case this.KINDS.broadcast: |
| return this.decodeBroadcast(buffer, view, decoder); |
| } |
| }, |
| |
| decodePush(buffer, view, decoder) { |
| let joinRefSize = view.getUint8(1); |
| let topicSize = view.getUint8(2); |
| let eventSize = view.getUint8(3); |
| let offset = this.HEADER_LENGTH + this.META_LENGTH - 1; |
| let joinRef = decoder.decode(buffer.slice(offset, offset + joinRefSize)); |
| offset = offset + joinRefSize; |
| let topic = decoder.decode(buffer.slice(offset, offset + topicSize)); |
| offset = offset + topicSize; |
| let event = decoder.decode(buffer.slice(offset, offset + eventSize)); |
| offset = offset + eventSize; |
| let data = buffer.slice(offset, buffer.byteLength); |
| return { join_ref: joinRef, ref: null, topic, event, payload: data }; |
| }, |
| |
| decodeReply(buffer, view, decoder) { |
| let joinRefSize = view.getUint8(1); |
| let refSize = view.getUint8(2); |
| let topicSize = view.getUint8(3); |
| let eventSize = view.getUint8(4); |
| let offset = this.HEADER_LENGTH + this.META_LENGTH; |
| let joinRef = decoder.decode(buffer.slice(offset, offset + joinRefSize)); |
| offset = offset + joinRefSize; |
| let ref = decoder.decode(buffer.slice(offset, offset + refSize)); |
| offset = offset + refSize; |
| let topic = decoder.decode(buffer.slice(offset, offset + topicSize)); |
| offset = offset + topicSize; |
| let event = decoder.decode(buffer.slice(offset, offset + eventSize)); |
| offset = offset + eventSize; |
| let data = buffer.slice(offset, buffer.byteLength); |
| let payload = { status: event, response: data }; |
| return { join_ref: joinRef, ref, topic, event: CHANNEL_EVENTS.reply, payload }; |
| }, |
| |
| decodeBroadcast(buffer, view, decoder) { |
| let topicSize = view.getUint8(1); |
| let eventSize = view.getUint8(2); |
| let offset = this.HEADER_LENGTH + 2; |
| let topic = decoder.decode(buffer.slice(offset, offset + topicSize)); |
| offset = offset + topicSize; |
| let event = decoder.decode(buffer.slice(offset, offset + eventSize)); |
| offset = offset + eventSize; |
| let data = buffer.slice(offset, buffer.byteLength); |
| return { join_ref: null, ref: null, topic, event, payload: data }; |
| } |
| }; |
|
|
| |
| var Socket = class { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| constructor(endPoint, opts = {}) { |
| this.stateChangeCallbacks = { open: [], close: [], error: [], message: [] }; |
| this.channels = []; |
| this.sendBuffer = []; |
| this.ref = 0; |
| this.fallbackRef = null; |
| this.timeout = opts.timeout || DEFAULT_TIMEOUT; |
| this.transport = opts.transport || global.WebSocket || LongPoll; |
| this.conn = void 0; |
| this.primaryPassedHealthCheck = false; |
| this.longPollFallbackMs = opts.longPollFallbackMs; |
| this.fallbackTimer = null; |
| let envSessionStorage = null; |
| try { |
| envSessionStorage = global && global.sessionStorage; |
| } catch { |
| } |
| this.sessionStore = opts.sessionStorage || envSessionStorage; |
| this.establishedConnections = 0; |
| this.defaultEncoder = serializer_default.encode.bind(serializer_default); |
| this.defaultDecoder = serializer_default.decode.bind(serializer_default); |
| this.closeWasClean = true; |
| this.disconnecting = false; |
| this.binaryType = opts.binaryType || "arraybuffer"; |
| this.connectClock = 1; |
| this.pageHidden = false; |
| this.encode = void 0; |
| this.decode = void 0; |
| if (this.transport !== LongPoll) { |
| this.encode = opts.encode || this.defaultEncoder; |
| this.decode = opts.decode || this.defaultDecoder; |
| } else { |
| this.encode = this.defaultEncoder; |
| this.decode = this.defaultDecoder; |
| } |
| let awaitingConnectionOnPageShow = null; |
| if (phxWindow && phxWindow.addEventListener) { |
| phxWindow.addEventListener("pagehide", (_e) => { |
| if (this.conn) { |
| this.disconnect(); |
| awaitingConnectionOnPageShow = this.connectClock; |
| } |
| }); |
| phxWindow.addEventListener("pageshow", (_e) => { |
| if (awaitingConnectionOnPageShow === this.connectClock) { |
| awaitingConnectionOnPageShow = null; |
| this.connect(); |
| } |
| }); |
| phxWindow.addEventListener("visibilitychange", () => { |
| if (document.visibilityState === "hidden") { |
| this.pageHidden = true; |
| } else { |
| this.pageHidden = false; |
| if (!this.isConnected() && !this.closeWasClean) { |
| this.teardown(() => this.connect()); |
| } |
| } |
| }); |
| } |
| this.heartbeatIntervalMs = opts.heartbeatIntervalMs || 3e4; |
| this.autoSendHeartbeat = opts.autoSendHeartbeat ?? true; |
| this.heartbeatCallback = opts.heartbeatCallback ?? (() => { |
| }); |
| this.rejoinAfterMs = (tries) => { |
| if (opts.rejoinAfterMs) { |
| return opts.rejoinAfterMs(tries); |
| } else { |
| return [1e3, 2e3, 5e3][tries - 1] || 1e4; |
| } |
| }; |
| this.reconnectAfterMs = (tries) => { |
| if (opts.reconnectAfterMs) { |
| return opts.reconnectAfterMs(tries); |
| } else { |
| return [10, 50, 100, 150, 200, 250, 500, 1e3, 2e3][tries - 1] || 5e3; |
| } |
| }; |
| this.logger = opts.logger || null; |
| if (!this.logger && opts.debug) { |
| this.logger = (kind, msg, data) => { |
| console.log(`${kind}: ${msg}`, data); |
| }; |
| } |
| this.longpollerTimeout = opts.longpollerTimeout || 2e4; |
| this.params = closure(opts.params || {}); |
| this.endPoint = `${endPoint}/${TRANSPORTS.websocket}`; |
| this.vsn = opts.vsn || DEFAULT_VSN; |
| this.heartbeatTimeoutTimer = null; |
| this.heartbeatTimer = null; |
| this.heartbeatSentAt = null; |
| this.pendingHeartbeatRef = null; |
| this.reconnectTimer = new Timer(() => { |
| if (this.pageHidden) { |
| this.log("Not reconnecting as page is hidden!"); |
| this.teardown(); |
| return; |
| } |
| this.teardown(async () => { |
| if (opts.beforeReconnect) await opts.beforeReconnect(); |
| this.connect(); |
| }); |
| }, this.reconnectAfterMs); |
| this.authToken = opts.authToken && closure(opts.authToken); |
| } |
| |
| |
| |
| getLongPollTransport() { |
| return LongPoll; |
| } |
| |
| |
| |
| |
| |
| |
| replaceTransport(newTransport) { |
| this.connectClock++; |
| this.closeWasClean = true; |
| clearTimeout(this.fallbackTimer); |
| this.reconnectTimer.reset(); |
| if (this.conn) { |
| this.conn.close(); |
| this.conn = null; |
| } |
| this.transport = newTransport; |
| } |
| |
| |
| |
| |
| |
| protocol() { |
| return location.protocol.match(/^https/) ? "wss" : "ws"; |
| } |
| |
| |
| |
| |
| |
| endPointURL() { |
| let uri = Ajax.appendParams( |
| Ajax.appendParams(this.endPoint, this.params()), |
| { vsn: this.vsn } |
| ); |
| if (uri.charAt(0) !== "/") { |
| return uri; |
| } |
| if (uri.charAt(1) === "/") { |
| return `${this.protocol()}:${uri}`; |
| } |
| return `${this.protocol()}://${location.host}${uri}`; |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| disconnect(callback, code, reason) { |
| this.connectClock++; |
| this.disconnecting = true; |
| this.closeWasClean = true; |
| clearTimeout(this.fallbackTimer); |
| this.reconnectTimer.reset(); |
| this.teardown(() => { |
| this.disconnecting = false; |
| callback && callback(); |
| }, code, reason); |
| } |
| |
| |
| |
| |
| |
| |
| connect(params) { |
| if (params) { |
| console && console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor"); |
| this.params = closure(params); |
| } |
| if (this.conn && !this.disconnecting) { |
| return; |
| } |
| if (this.longPollFallbackMs && this.transport !== LongPoll) { |
| this.connectWithFallback(LongPoll, this.longPollFallbackMs); |
| } else { |
| this.transportConnect(); |
| } |
| } |
| |
| |
| |
| |
| |
| |
| log(kind, msg, data) { |
| this.logger && this.logger(kind, msg, data); |
| } |
| |
| |
| |
| hasLogger() { |
| return this.logger !== null; |
| } |
| |
| |
| |
| |
| |
| |
| |
| onOpen(callback) { |
| let ref = this.makeRef(); |
| this.stateChangeCallbacks.open.push([ref, callback]); |
| return ref; |
| } |
| |
| |
| |
| |
| |
| onClose(callback) { |
| let ref = this.makeRef(); |
| this.stateChangeCallbacks.close.push([ref, callback]); |
| return ref; |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| onError(callback) { |
| let ref = this.makeRef(); |
| this.stateChangeCallbacks.error.push([ref, callback]); |
| return ref; |
| } |
| |
| |
| |
| |
| |
| onMessage(callback) { |
| let ref = this.makeRef(); |
| this.stateChangeCallbacks.message.push([ref, callback]); |
| return ref; |
| } |
| |
| |
| |
| |
| |
| onHeartbeat(callback) { |
| this.heartbeatCallback = callback; |
| } |
| |
| |
| |
| |
| |
| |
| ping(callback) { |
| if (!this.isConnected()) { |
| return false; |
| } |
| let ref = this.makeRef(); |
| let startTime = Date.now(); |
| this.push({ topic: "phoenix", event: "heartbeat", payload: {}, ref }); |
| let onMsgRef = this.onMessage((msg) => { |
| if (msg.ref === ref) { |
| this.off([onMsgRef]); |
| callback(Date.now() - startTime); |
| } |
| }); |
| return true; |
| } |
| |
| |
| |
| |
| |
| transportName(transport) { |
| switch (transport) { |
| case LongPoll: |
| return "LongPoll"; |
| default: |
| return transport.name; |
| } |
| } |
| |
| |
| |
| transportConnect() { |
| this.connectClock++; |
| this.closeWasClean = false; |
| let protocols = void 0; |
| if (this.authToken) { |
| protocols = ["phoenix", `${AUTH_TOKEN_PREFIX}${btoa(this.authToken()).replace(/=/g, "")}`]; |
| } |
| this.conn = new this.transport(this.endPointURL(), protocols); |
| this.conn.binaryType = this.binaryType; |
| this.conn.timeout = this.longpollerTimeout; |
| this.conn.onopen = () => this.onConnOpen(); |
| this.conn.onerror = (error) => this.onConnError(error); |
| this.conn.onmessage = (event) => this.onConnMessage(event); |
| this.conn.onclose = (event) => this.onConnClose(event); |
| } |
| getSession(key) { |
| return this.sessionStore && this.sessionStore.getItem(key); |
| } |
| storeSession(key, val) { |
| this.sessionStore && this.sessionStore.setItem(key, val); |
| } |
| connectWithFallback(fallbackTransport, fallbackThreshold = 2500) { |
| clearTimeout(this.fallbackTimer); |
| let established = false; |
| let primaryTransport = true; |
| let openRef, errorRef; |
| let fallbackTransportName = this.transportName(fallbackTransport); |
| let fallback = (reason) => { |
| this.log("transport", `falling back to ${fallbackTransportName}...`, reason); |
| this.off([openRef, errorRef]); |
| primaryTransport = false; |
| this.replaceTransport(fallbackTransport); |
| this.transportConnect(); |
| }; |
| if (this.getSession(`phx:fallback:${fallbackTransportName}`)) { |
| return fallback("memorized"); |
| } |
| this.fallbackTimer = setTimeout(fallback, fallbackThreshold); |
| errorRef = this.onError((reason) => { |
| this.log("transport", "error", reason); |
| if (primaryTransport && !established) { |
| clearTimeout(this.fallbackTimer); |
| fallback(reason); |
| } |
| }); |
| if (this.fallbackRef) { |
| this.off([this.fallbackRef]); |
| } |
| this.fallbackRef = this.onOpen(() => { |
| established = true; |
| if (!primaryTransport) { |
| let fallbackTransportName2 = this.transportName(fallbackTransport); |
| if (!this.primaryPassedHealthCheck) { |
| this.storeSession(`phx:fallback:${fallbackTransportName2}`, "true"); |
| } |
| return this.log("transport", `established ${fallbackTransportName2} fallback`); |
| } |
| clearTimeout(this.fallbackTimer); |
| this.fallbackTimer = setTimeout(fallback, fallbackThreshold); |
| this.ping((rtt) => { |
| this.log("transport", "connected to primary after", rtt); |
| this.primaryPassedHealthCheck = true; |
| clearTimeout(this.fallbackTimer); |
| }); |
| }); |
| this.transportConnect(); |
| } |
| clearHeartbeats() { |
| clearTimeout(this.heartbeatTimer); |
| clearTimeout(this.heartbeatTimeoutTimer); |
| } |
| onConnOpen() { |
| if (this.hasLogger()) this.log("transport", `connected to ${this.endPointURL()}`); |
| this.closeWasClean = false; |
| this.disconnecting = false; |
| this.establishedConnections++; |
| this.flushSendBuffer(); |
| this.reconnectTimer.reset(); |
| if (this.autoSendHeartbeat) { |
| this.resetHeartbeat(); |
| } |
| this.triggerStateCallbacks("open"); |
| } |
| |
| |
| |
| heartbeatTimeout() { |
| if (this.pendingHeartbeatRef) { |
| this.pendingHeartbeatRef = null; |
| this.heartbeatSentAt = null; |
| if (this.hasLogger()) { |
| this.log("transport", "heartbeat timeout. Attempting to re-establish connection"); |
| } |
| try { |
| this.heartbeatCallback("timeout"); |
| } catch (e) { |
| this.log("error", "error in heartbeat callback", e); |
| } |
| this.triggerChanError(new Error("heartbeat timeout")); |
| this.closeWasClean = false; |
| this.teardown(() => this.reconnectTimer.scheduleTimeout(), WS_CLOSE_NORMAL, "heartbeat timeout"); |
| } |
| } |
| resetHeartbeat() { |
| if (this.conn && this.conn.skipHeartbeat) { |
| return; |
| } |
| this.pendingHeartbeatRef = null; |
| this.clearHeartbeats(); |
| this.heartbeatTimer = setTimeout(() => this.sendHeartbeat(), this.heartbeatIntervalMs); |
| } |
| teardown(callback, code, reason) { |
| if (!this.conn) { |
| return callback && callback(); |
| } |
| const connToClose = this.conn; |
| this.waitForBufferDone(connToClose, () => { |
| if (code) { |
| connToClose.close(code, reason || ""); |
| } else { |
| connToClose.close(); |
| } |
| this.waitForSocketClosed(connToClose, () => { |
| if (this.conn === connToClose) { |
| this.conn.onopen = function() { |
| }; |
| this.conn.onerror = function() { |
| }; |
| this.conn.onmessage = function() { |
| }; |
| this.conn.onclose = function() { |
| }; |
| this.conn = null; |
| } |
| callback && callback(); |
| }); |
| }); |
| } |
| waitForBufferDone(conn, callback, tries = 1) { |
| if (tries === 5 || !conn.bufferedAmount) { |
| callback(); |
| return; |
| } |
| setTimeout(() => { |
| this.waitForBufferDone(conn, callback, tries + 1); |
| }, 150 * tries); |
| } |
| waitForSocketClosed(conn, callback, tries = 1) { |
| if (tries === 5 || conn.readyState === SOCKET_STATES.closed) { |
| callback(); |
| return; |
| } |
| setTimeout(() => { |
| this.waitForSocketClosed(conn, callback, tries + 1); |
| }, 150 * tries); |
| } |
| |
| |
| |
| onConnClose(event) { |
| if (this.conn) this.conn.onclose = () => { |
| }; |
| if (this.hasLogger()) this.log("transport", "close", event); |
| this.triggerChanError(event); |
| this.clearHeartbeats(); |
| if (!this.closeWasClean) { |
| this.reconnectTimer.scheduleTimeout(); |
| } |
| this.triggerStateCallbacks("close", event); |
| } |
| |
| |
| |
| |
| onConnError(error) { |
| if (this.hasLogger()) this.log("transport", "error", error); |
| let transportBefore = this.transport; |
| let establishedBefore = this.establishedConnections; |
| this.triggerStateCallbacks("error", error, transportBefore, establishedBefore); |
| if (transportBefore === this.transport || establishedBefore > 0) { |
| this.triggerChanError(error); |
| } |
| } |
| |
| |
| |
| |
| triggerChanError(reason) { |
| this.channels.forEach((channel) => { |
| if (!(channel.isErrored() || channel.isLeaving() || channel.isClosed())) { |
| channel.trigger(CHANNEL_EVENTS.error, reason); |
| } |
| }); |
| } |
| |
| |
| |
| connectionState() { |
| switch (this.conn && this.conn.readyState) { |
| case SOCKET_STATES.connecting: |
| return "connecting"; |
| case SOCKET_STATES.open: |
| return "open"; |
| case SOCKET_STATES.closing: |
| return "closing"; |
| default: |
| return "closed"; |
| } |
| } |
| |
| |
| |
| isConnected() { |
| return this.connectionState() === "open"; |
| } |
| |
| |
| |
| |
| remove(channel) { |
| this.off(channel.stateChangeRefs); |
| this.channels = this.channels.filter((c) => c !== channel); |
| } |
| |
| |
| |
| |
| |
| |
| off(refs) { |
| for (let key in this.stateChangeCallbacks) { |
| this.stateChangeCallbacks[key] = this.stateChangeCallbacks[key].filter(([ref]) => { |
| return refs.indexOf(ref) === -1; |
| }); |
| } |
| } |
| |
| |
| |
| |
| |
| |
| |
| channel(topic, chanParams = {}) { |
| let chan = new Channel(topic, chanParams, this); |
| this.channels.push(chan); |
| return chan; |
| } |
| |
| |
| |
| push(data) { |
| if (this.hasLogger()) { |
| let { topic, event, payload, ref, join_ref } = data; |
| this.log("push", `${topic} ${event} (${join_ref}, ${ref})`, payload); |
| } |
| if (this.isConnected()) { |
| this.encode(data, (result) => this.conn.send(result)); |
| } else { |
| this.sendBuffer.push(() => this.encode(data, (result) => this.conn.send(result))); |
| } |
| } |
| |
| |
| |
| |
| makeRef() { |
| let newRef = this.ref + 1; |
| if (newRef === this.ref) { |
| this.ref = 0; |
| } else { |
| this.ref = newRef; |
| } |
| return this.ref.toString(); |
| } |
| sendHeartbeat() { |
| if (!this.isConnected()) { |
| try { |
| this.heartbeatCallback("disconnected"); |
| } catch (e) { |
| this.log("error", "error in heartbeat callback", e); |
| } |
| return; |
| } |
| if (this.pendingHeartbeatRef) { |
| this.heartbeatTimeout(); |
| return; |
| } |
| this.pendingHeartbeatRef = this.makeRef(); |
| this.heartbeatSentAt = Date.now(); |
| this.push({ topic: "phoenix", event: "heartbeat", payload: {}, ref: this.pendingHeartbeatRef }); |
| try { |
| this.heartbeatCallback("sent"); |
| } catch (e) { |
| this.log("error", "error in heartbeat callback", e); |
| } |
| this.heartbeatTimeoutTimer = setTimeout(() => this.heartbeatTimeout(), this.heartbeatIntervalMs); |
| } |
| flushSendBuffer() { |
| if (this.isConnected() && this.sendBuffer.length > 0) { |
| this.sendBuffer.forEach((callback) => callback()); |
| this.sendBuffer = []; |
| } |
| } |
| |
| |
| |
| onConnMessage(rawMessage) { |
| this.decode(rawMessage.data, (msg) => { |
| let { topic, event, payload, ref, join_ref } = msg; |
| if (ref && ref === this.pendingHeartbeatRef) { |
| const latency = this.heartbeatSentAt ? Date.now() - this.heartbeatSentAt : void 0; |
| this.clearHeartbeats(); |
| try { |
| this.heartbeatCallback(payload.status === "ok" ? "ok" : "error", latency); |
| } catch (e) { |
| this.log("error", "error in heartbeat callback", e); |
| } |
| this.pendingHeartbeatRef = null; |
| this.heartbeatSentAt = null; |
| if (this.autoSendHeartbeat) { |
| this.heartbeatTimer = setTimeout(() => this.sendHeartbeat(), this.heartbeatIntervalMs); |
| } |
| } |
| if (this.hasLogger()) this.log("receive", `${payload.status || ""} ${topic} ${event} ${ref && "(" + ref + ")" || ""}`.trim(), payload); |
| for (let i = 0; i < this.channels.length; i++) { |
| const channel = this.channels[i]; |
| if (!channel.isMember(topic, event, payload, join_ref)) { |
| continue; |
| } |
| channel.trigger(event, payload, ref, join_ref); |
| } |
| this.triggerStateCallbacks("message", msg); |
| }); |
| } |
| |
| |
| |
| |
| |
| |
| |
| triggerStateCallbacks(event, ...args) { |
| try { |
| this.stateChangeCallbacks[event].forEach(([_, callback]) => { |
| try { |
| callback(...args); |
| } catch (e) { |
| this.log("error", `error in ${event} callback`, e); |
| } |
| }); |
| } catch (e) { |
| this.log("error", `error triggering ${event} callbacks`, e); |
| } |
| } |
| leaveOpenTopic(topic) { |
| let dupChannel = this.channels.find((c) => c.topic === topic && (c.isJoined() || c.isJoining())); |
| if (dupChannel) { |
| if (this.hasLogger()) this.log("transport", `leaving duplicate topic "${topic}"`); |
| dupChannel.leave(); |
| } |
| } |
| }; |
| |
|
|