var Phoenix = (() => { 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); // js/phoenix/index.js var phoenix_exports = {}; __export(phoenix_exports, { Channel: () => Channel, LongPoll: () => LongPoll, Presence: () => Presence, Serializer: () => serializer_default, Socket: () => Socket }); // js/phoenix/utils.js var closure = (value) => { if (typeof value === "function") { return value; } else { let closure2 = function() { return value; }; return closure2; } }; // js/phoenix/constants.js var globalSelf = typeof self !== "undefined" ? self : null; var phxWindow = typeof window !== "undefined" ? window : null; var global = globalSelf || phxWindow || global; var DEFAULT_VSN = "2.0.0"; var SOCKET_STATES = { connecting: 0, open: 1, closing: 2, closed: 3 }; var DEFAULT_TIMEOUT = 1e4; var WS_CLOSE_NORMAL = 1e3; 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 }; // js/phoenix/push.js 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; } /** * * @param {number} timeout */ 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() }); } /** * * @param {*} status * @param {*} callback */ receive(status, callback) { if (this.hasReceived(status)) { callback(this.receivedResp.response); } this.recHooks.push({ status, callback }); return this; } /** * @private */ reset() { this.cancelRefEvent(); this.ref = null; this.refEvent = null; this.receivedResp = null; this.sent = false; } /** * @private */ matchReceive({ status, response, _ref }) { this.recHooks.filter((h) => h.status === status).forEach((h) => h.callback(response)); } /** * @private */ cancelRefEvent() { if (!this.refEvent) { return; } this.channel.off(this.refEvent); } /** * @private */ cancelTimeout() { clearTimeout(this.timeoutTimer); this.timeoutTimer = null; } /** * @private */ 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); } /** * @private */ hasReceived(status) { return this.receivedResp && this.receivedResp.status === status; } /** * @private */ trigger(status, response) { this.channel.trigger(this.refEvent, { status, response }); } }; // js/phoenix/timer.js var Timer = class { constructor(callback, timerCalc) { this.callback = callback; this.timerCalc = timerCalc; this.timer = null; this.tries = 0; } reset() { this.tries = 0; clearTimeout(this.timer); } /** * Cancels any previous scheduleTimeout and schedules callback */ scheduleTimeout() { clearTimeout(this.timer); this.timer = setTimeout(() => { this.tries = this.tries + 1; this.callback(); }, this.timerCalc(this.tries + 1)); } }; // js/phoenix/channel.js 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", () => { this.state = CHANNEL_STATES.errored; if (this.socket.isConnected()) { this.rejoinTimer.scheduleTimeout(); } }); this.onClose(() => { this.rejoinTimer.reset(); if (this.socket.hasLogger()) this.socket.log("channel", `close ${this.topic} ${this.joinRef()}`); 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.joinRef()})`, 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 the channel * @param {integer} timeout * @returns {Push} */ 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; } } /** * Hook into channel close * @param {Function} callback */ onClose(callback) { this.on(CHANNEL_EVENTS.close, callback); } /** * Hook into channel errors * @param {Function} callback */ onError(callback) { return this.on(CHANNEL_EVENTS.error, (reason) => callback(reason)); } /** * Subscribes on channel events * * Subscription returns a ref counter, which can be used later to * unsubscribe the exact event listener * * @example * const ref1 = channel.on("event", do_stuff) * const ref2 = channel.on("event", do_other_stuff) * channel.off("event", ref1) * // Since unsubscription, do_stuff won't fire, * // while do_other_stuff will keep firing on the "event" * * @param {string} event * @param {Function} callback * @returns {integer} ref */ on(event, callback) { let ref = this.bindingRef++; this.bindings.push({ event, ref, callback }); return ref; } /** * Unsubscribes off of channel events * * Use the ref returned from a channel.on() to unsubscribe one * handler, or pass nothing for the ref to unsubscribe all * handlers for the given event. * * @example * // Unsubscribe the do_stuff handler * const ref1 = channel.on("event", do_stuff) * channel.off("event", ref1) * * // Unsubscribe all handlers from event * channel.off("event") * * @param {string} event * @param {integer} ref */ off(event, ref) { this.bindings = this.bindings.filter((bind) => { return !(bind.event === event && (typeof ref === "undefined" || ref === bind.ref)); }); } /** * @private */ canPush() { return this.socket.isConnected() && this.isJoined(); } /** * Sends a message `event` to phoenix with the payload `payload`. * Phoenix receives this in the `handle_in(event, payload, socket)` * function. if phoenix replies or it times out (default 10000ms), * then optionally the reply can be received. * * @example * channel.push("event") * .receive("ok", payload => console.log("phoenix replied:", payload)) * .receive("error", err => console.log("phoenix errored", err)) * .receive("timeout", () => console.log("timed out pushing")) * @param {string} event * @param {Object} payload * @param {number} [timeout] * @returns {Push} */ 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; } /** Leaves the channel * * Unsubscribes from server events, and * instructs channel to terminate on server * * Triggers onClose() hooks * * To receive leave acknowledgements, use the `receive` * hook to bind to the server ack, ie: * * @example * channel.leave().receive("ok", () => alert("left!") ) * * @param {integer} timeout * @returns {Push} */ 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; } /** * Overridable message hook * * Receives all events for specialized message handling * before dispatching to the channel callbacks. * * Must return the payload, modified or unmodified * @param {string} event * @param {Object} payload * @param {integer} ref * @returns {Object} */ onMessage(_event, payload, _ref) { return payload; } /** * @private */ 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; } } /** * @private */ joinRef() { return this.joinPush.ref; } /** * @private */ rejoin(timeout = this.timeout) { if (this.isLeaving()) { return; } this.socket.leaveOpenTopic(this.topic); this.state = CHANNEL_STATES.joining; this.joinPush.resend(timeout); } /** * @private */ 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); for (let i = 0; i < eventBindings.length; i++) { let bind = eventBindings[i]; bind.callback(handledPayload, ref, joinRef || this.joinRef()); } } /** * @private */ replyEventName(ref) { return `chan_reply_${ref}`; } /** * @private */ isClosed() { return this.state === CHANNEL_STATES.closed; } /** * @private */ isErrored() { return this.state === CHANNEL_STATES.errored; } /** * @private */ isJoined() { return this.state === CHANNEL_STATES.joined; } /** * @private */ isJoining() { return this.state === CHANNEL_STATES.joining; } /** * @private */ isLeaving() { return this.state === CHANNEL_STATES.leaving; } }; // js/phoenix/ajax.js var Ajax = class { static request(method, endPoint, accept, body, timeout, ontimeout, callback) { if (global.XDomainRequest) { let req = new global.XDomainRequest(); return this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback); } else { let req = new global.XMLHttpRequest(); return this.xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback); } } 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, accept, body, timeout, ontimeout, callback) { req.open(method, endPoint, true); req.timeout = timeout; req.setRequestHeader("Content-Type", accept); 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 (e) { 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)}`; } }; // js/phoenix/longpoll.js 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) { this.endPoint = null; this.token = null; this.skipHeartbeat = true; this.reqs = /* @__PURE__ */ 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() { this.ajax("GET", "application/json", null, () => this.ontimeout(), (resp) => { if (resp) { var { status, token, messages } = resp; 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}`); } }); } // we collect all pushes within the current event loop by // setTimeout 0, which optimizes back-to-back procedural // pushes against an empty buffer 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) { this.awaitingBatchAck = true; this.ajax("POST", "application/x-ndjson", messages.join("\n"), () => this.onerror("timeout"), (resp) => { this.awaitingBatchAck = false; if (!resp || resp.status !== 200) { this.onerror(resp && resp.status); this.closeAndRetry(1011, "internal server error", false); } else if (this.batchBuffer.length > 0) { this.batchSend(this.batchBuffer); this.batchBuffer = []; } }); } 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, contentType, body, onCallerTimeout, callback) { let req; let ontimeout = () => { this.reqs.delete(req); onCallerTimeout(); }; req = Ajax.request(method, this.endpointURL(), contentType, body, this.timeout, ontimeout, (resp) => { this.reqs.delete(req); if (this.isActive()) { callback(resp); } }); this.reqs.add(req); } }; // js/phoenix/presence.js var Presence = class { constructor(channel, opts = {}) { let events = opts.events || { state: "presence_state", diff: "presence_diff" }; this.state = {}; 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(); } // lower-level public static API /** * Used to sync the list of presences on the server * with the client's state. An optional `onJoin` and `onLeave` callback can * be provided to react to changes in the client's local presences across * disconnects and reconnects with the server. * * @returns {Presence} */ static syncState(currentState, newState, onJoin, onLeave) { let state = this.clone(currentState); let joins = {}; let leaves = {}; 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); } /** * * Used to sync a diff of presence join and leave * events from the server, as they happen. Like `syncState`, `syncDiff` * accepts optional `onJoin` and `onLeave` callbacks to react to a user * joining or leaving from a device. * * @returns {Presence} */ static syncDiff(state, diff, onJoin, onLeave) { 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; } /** * Returns the array of presences, with selected metadata. * * @param {Object} presences * @param {Function} chooser * * @returns {Presence} */ static list(presences, chooser) { if (!chooser) { chooser = function(key, pres) { return pres; }; } return this.map(presences, (key, presence) => { return chooser(key, presence); }); } // private static map(obj, func) { return Object.getOwnPropertyNames(obj).map((key) => func(key, obj[key])); } static clone(obj) { return JSON.parse(JSON.stringify(obj)); } }; // js/phoenix/serializer.js 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 }); } }, // private binaryEncode(message) { let { join_ref, ref, event, topic, payload } = message; let metaLength = this.META_LENGTH + join_ref.length + ref.length + topic.length + event.length; let header = new ArrayBuffer(this.HEADER_LENGTH + metaLength); let view = new DataView(header); let offset = 0; view.setUint8(offset++, this.KINDS.push); view.setUint8(offset++, join_ref.length); view.setUint8(offset++, ref.length); view.setUint8(offset++, topic.length); view.setUint8(offset++, event.length); Array.from(join_ref, (char) => view.setUint8(offset++, char.charCodeAt(0))); Array.from(ref, (char) => view.setUint8(offset++, char.charCodeAt(0))); Array.from(topic, (char) => view.setUint8(offset++, char.charCodeAt(0))); Array.from(event, (char) => view.setUint8(offset++, char.charCodeAt(0))); var combined = new Uint8Array(header.byteLength + payload.byteLength); combined.set(new Uint8Array(header), 0); combined.set(new Uint8Array(payload), header.byteLength); return combined.buffer; }, 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 }; } }; // js/phoenix/socket.js var Socket = class { constructor(endPoint, opts = {}) { this.stateChangeCallbacks = { open: [], close: [], error: [], message: [] }; this.channels = []; this.sendBuffer = []; this.ref = 0; this.timeout = opts.timeout || DEFAULT_TIMEOUT; this.transport = opts.transport || global.WebSocket || LongPoll; this.primaryPassedHealthCheck = false; this.longPollFallbackMs = opts.longPollFallbackMs; this.fallbackTimer = null; this.sessionStore = opts.sessionStorage || global && global.sessionStorage; this.establishedConnections = 0; this.defaultEncoder = serializer_default.encode.bind(serializer_default); this.defaultDecoder = serializer_default.decode.bind(serializer_default); this.closeWasClean = false; this.binaryType = opts.binaryType || "arraybuffer"; this.connectClock = 1; 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(); } }); } this.heartbeatIntervalMs = opts.heartbeatIntervalMs || 3e4; 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.pendingHeartbeatRef = null; this.reconnectTimer = new Timer(() => { this.teardown(() => this.connect()); }, this.reconnectAfterMs); } /** * Returns the LongPoll transport reference */ getLongPollTransport() { return LongPoll; } /** * Disconnects and replaces the active transport * * @param {Function} newTransport - The new transport class to instantiate * */ 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; } /** * Returns the socket protocol * * @returns {string} */ protocol() { return location.protocol.match(/^https/) ? "wss" : "ws"; } /** * The fully qualified socket url * * @returns {string} */ 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}`; } /** * Disconnects the socket * * See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes for valid status codes. * * @param {Function} callback - Optional callback which is called after socket is disconnected. * @param {integer} code - A status code for disconnection (Optional). * @param {string} reason - A textual description of the reason to disconnect. (Optional) */ disconnect(callback, code, reason) { this.connectClock++; this.closeWasClean = true; clearTimeout(this.fallbackTimer); this.reconnectTimer.reset(); this.teardown(callback, code, reason); } /** * * @param {Object} params - The params to send when connecting, for example `{user_id: userToken}` * * Passing params to connect is deprecated; pass them in the Socket constructor instead: * `new Socket("/socket", {params: {user_id: userToken}})`. */ 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) { return; } if (this.longPollFallbackMs && this.transport !== LongPoll) { this.connectWithFallback(LongPoll, this.longPollFallbackMs); } else { this.transportConnect(); } } /** * Logs the message. Override `this.logger` for specialized logging. noops by default * @param {string} kind * @param {string} msg * @param {Object} data */ log(kind, msg, data) { this.logger && this.logger(kind, msg, data); } /** * Returns true if a logger has been set on this socket. */ hasLogger() { return this.logger !== null; } /** * Registers callbacks for connection open events * * @example socket.onOpen(function(){ console.info("the socket was opened") }) * * @param {Function} callback */ onOpen(callback) { let ref = this.makeRef(); this.stateChangeCallbacks.open.push([ref, callback]); return ref; } /** * Registers callbacks for connection close events * @param {Function} callback */ onClose(callback) { let ref = this.makeRef(); this.stateChangeCallbacks.close.push([ref, callback]); return ref; } /** * Registers callbacks for connection error events * * @example socket.onError(function(error){ alert("An error occurred") }) * * @param {Function} callback */ onError(callback) { let ref = this.makeRef(); this.stateChangeCallbacks.error.push([ref, callback]); return ref; } /** * Registers callbacks for connection message events * @param {Function} callback */ onMessage(callback) { let ref = this.makeRef(); this.stateChangeCallbacks.message.push([ref, callback]); return ref; } /** * Pings the server and invokes the callback with the RTT in milliseconds * @param {Function} callback * * Returns true if the ping was pushed or false if unable to be pushed. */ 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; } /** * @private */ transportConnect() { this.connectClock++; this.closeWasClean = false; this.conn = new this.transport(this.endPointURL()); 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 fallback = (reason) => { this.log("transport", `falling back to ${fallbackTransport.name}...`, reason); this.off([openRef, errorRef]); primaryTransport = false; this.replaceTransport(fallbackTransport); this.transportConnect(); }; if (this.getSession(`phx:fallback:${fallbackTransport.name}`)) { 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); } }); this.onOpen(() => { established = true; if (!primaryTransport) { if (!this.primaryPassedHealthCheck) { this.storeSession(`phx:fallback:${fallbackTransport.name}`, "true"); } return this.log("transport", `established ${fallbackTransport.name} 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", `${this.transport.name} connected to ${this.endPointURL()}`); this.closeWasClean = false; this.establishedConnections++; this.flushSendBuffer(); this.reconnectTimer.reset(); this.resetHeartbeat(); this.stateChangeCallbacks.open.forEach(([, callback]) => callback()); } /** * @private */ heartbeatTimeout() { if (this.pendingHeartbeatRef) { this.pendingHeartbeatRef = null; if (this.hasLogger()) { this.log("transport", "heartbeat timeout. Attempting to re-establish connection"); } this.triggerChanError(); 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(); } this.waitForBufferDone(() => { if (this.conn) { if (code) { this.conn.close(code, reason || ""); } else { this.conn.close(); } } this.waitForSocketClosed(() => { if (this.conn) { this.conn.onopen = function() { }; this.conn.onerror = function() { }; this.conn.onmessage = function() { }; this.conn.onclose = function() { }; this.conn = null; } callback && callback(); }); }); } waitForBufferDone(callback, tries = 1) { if (tries === 5 || !this.conn || !this.conn.bufferedAmount) { callback(); return; } setTimeout(() => { this.waitForBufferDone(callback, tries + 1); }, 150 * tries); } waitForSocketClosed(callback, tries = 1) { if (tries === 5 || !this.conn || this.conn.readyState === SOCKET_STATES.closed) { callback(); return; } setTimeout(() => { this.waitForSocketClosed(callback, tries + 1); }, 150 * tries); } onConnClose(event) { let closeCode = event && event.code; if (this.hasLogger()) this.log("transport", "close", event); this.triggerChanError(); this.clearHeartbeats(); if (!this.closeWasClean && closeCode !== 1e3) { this.reconnectTimer.scheduleTimeout(); } this.stateChangeCallbacks.close.forEach(([, callback]) => callback(event)); } /** * @private */ onConnError(error) { if (this.hasLogger()) this.log("transport", error); let transportBefore = this.transport; let establishedBefore = this.establishedConnections; this.stateChangeCallbacks.error.forEach(([, callback]) => { callback(error, transportBefore, establishedBefore); }); if (transportBefore === this.transport || establishedBefore > 0) { this.triggerChanError(); } } /** * @private */ triggerChanError() { this.channels.forEach((channel) => { if (!(channel.isErrored() || channel.isLeaving() || channel.isClosed())) { channel.trigger(CHANNEL_EVENTS.error); } }); } /** * @returns {string} */ 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"; } } /** * @returns {boolean} */ isConnected() { return this.connectionState() === "open"; } /** * @private * * @param {Channel} */ remove(channel) { this.off(channel.stateChangeRefs); this.channels = this.channels.filter((c) => c !== channel); } /** * Removes `onOpen`, `onClose`, `onError,` and `onMessage` registrations. * * @param {refs} - list of refs returned by calls to * `onOpen`, `onClose`, `onError,` and `onMessage` */ off(refs) { for (let key in this.stateChangeCallbacks) { this.stateChangeCallbacks[key] = this.stateChangeCallbacks[key].filter(([ref]) => { return refs.indexOf(ref) === -1; }); } } /** * Initiates a new channel for the given topic * * @param {string} topic * @param {Object} chanParams - Parameters for the channel * @returns {Channel} */ channel(topic, chanParams = {}) { let chan = new Channel(topic, chanParams, this); this.channels.push(chan); return chan; } /** * @param {Object} data */ 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))); } } /** * Return the next message ref, accounting for overflows * @returns {string} */ makeRef() { let newRef = this.ref + 1; if (newRef === this.ref) { this.ref = 0; } else { this.ref = newRef; } return this.ref.toString(); } sendHeartbeat() { if (this.pendingHeartbeatRef && !this.isConnected()) { return; } this.pendingHeartbeatRef = this.makeRef(); this.push({ topic: "phoenix", event: "heartbeat", payload: {}, ref: this.pendingHeartbeatRef }); 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) { this.clearHeartbeats(); this.pendingHeartbeatRef = null; this.heartbeatTimer = setTimeout(() => this.sendHeartbeat(), this.heartbeatIntervalMs); } if (this.hasLogger()) this.log("receive", `${payload.status || ""} ${topic} ${event} ${ref && "(" + ref + ")" || ""}`, 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); } for (let i = 0; i < this.stateChangeCallbacks.message.length; i++) { let [, callback] = this.stateChangeCallbacks.message[i]; callback(msg); } }); } 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(); } } }; return __toCommonJS(phoenix_exports); })();