Download node_modules/@supabase/phoenix/priv/static/phoenix.mjs from metarm/QMetaRam: direct link, hf CLI and curl.
- Browser
- Download file 56.2 kB
-
https://huggingface.co/metarm/QMetaRam/resolve/main/node_modules/%40supabase/phoenix/priv/static/phoenix.mjs
- Command line
-
hf download hf://metarm/QMetaRam/node_modules/@supabase/phoenix/priv/static/phoenix.mjs
-
curl -L -o phoenix.mjs https://huggingface.co/metarm/QMetaRam/resolve/main/node_modules/%40supabase/phoenix/priv/static/phoenix.mjs
56.2 kB
| // js/phoenix/utils.js | |
| var closure = (value) => { | |
| if (typeof value === "function") { | |
| return ( | |
| /** @type {() => T} */ | |
| 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 || 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 = ( | |
| /** @type {const} */ | |
| { connecting: 0, open: 1, closing: 2, closed: 3 } | |
| ); | |
| var CHANNEL_STATES = ( | |
| /** @type {const} */ | |
| { | |
| closed: "closed", | |
| errored: "errored", | |
| joined: "joined", | |
| joining: "joining", | |
| leaving: "leaving" | |
| } | |
| ); | |
| var CHANNEL_EVENTS = ( | |
| /** @type {const} */ | |
| { | |
| close: "phx_close", | |
| error: "phx_error", | |
| join: "phx_join", | |
| reply: "phx_reply", | |
| leave: "phx_leave" | |
| } | |
| ); | |
| var TRANSPORTS = ( | |
| /** @type {const} */ | |
| { | |
| longpoll: "longpoll", | |
| websocket: "websocket" | |
| } | |
| ); | |
| var XHR_STATES = ( | |
| /** @type {const} */ | |
| { | |
| complete: 4 | |
| } | |
| ); | |
| var AUTH_TOKEN_PREFIX = "base64url.bearer.phx."; | |
| // js/phoenix/push.js | |
| var Push = class { | |
| /** | |
| * Initializes the Push | |
| * @param {Channel} channel - The Channel | |
| * @param {ChannelEvent} event - The event, for example `"phx_join"` | |
| * @param {() => Record<string, unknown>} payload - The payload, for example `{user_id: 123}` | |
| * @param {number} timeout - The push timeout in milliseconds | |
| */ | |
| 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; | |
| } | |
| /** | |
| * | |
| * @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 {string} status | |
| * @param {(response: any) => void} callback | |
| */ | |
| 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(); | |
| } | |
| /** | |
| * @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); | |
| } | |
| 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); | |
| } | |
| /** | |
| * @private | |
| */ | |
| hasReceived(status) { | |
| return this.receivedResp && this.receivedResp.status === status; | |
| } | |
| trigger(status, response) { | |
| this.channel.trigger(this.refEvent, { status, response }); | |
| } | |
| }; | |
| // js/phoenix/timer.js | |
| var Timer = class { | |
| /** | |
| * @param {() => void} callback | |
| * @param {(tries: number) => number} timerCalc | |
| */ | |
| constructor(callback, timerCalc) { | |
| this.callback = callback; | |
| this.timerCalc = timerCalc; | |
| this.timer = void 0; | |
| 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 { | |
| /** | |
| * @param {string} topic | |
| * @param {Params | (() => Params)} params | |
| * @param {Socket} socket | |
| */ | |
| 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 the channel | |
| * @param {number} 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; | |
| } | |
| } | |
| /** | |
| * Teardown the channel. | |
| * | |
| * Destroys and stops related timers. | |
| */ | |
| teardown() { | |
| this.pushBuffer.forEach((push) => push.destroy()); | |
| this.pushBuffer = []; | |
| this.rejoinTimer.reset(); | |
| this.joinPush.destroy(); | |
| this.state = CHANNEL_STATES.closed; | |
| this.bindings = []; | |
| } | |
| /** | |
| * Hook into channel close | |
| * @param {ChannelBindingCallback} callback | |
| */ | |
| onClose(callback) { | |
| this.on(CHANNEL_EVENTS.close, callback); | |
| } | |
| /** | |
| * Hook into channel errors | |
| * @param {ChannelOnErrorCallback} callback | |
| * @return {number} | |
| */ | |
| 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 {ChannelBindingCallback} callback | |
| * @returns {number} 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 {number} [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 {number} 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 | |
| * @type{ChannelOnMessage} | |
| */ | |
| onMessage(_event, payload, _ref) { | |
| return payload; | |
| } | |
| /** | |
| * Overridable filter hook | |
| * | |
| * If this function returns `true`, `binding`'s callback will be called. | |
| * | |
| * @type{ChannelFilterBindings} | |
| */ | |
| 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; | |
| } | |
| /** | |
| * @private | |
| */ | |
| rejoin(timeout = this.timeout) { | |
| if (this.isLeaving()) { | |
| return; | |
| } | |
| this.socket.leaveOpenTopic(this.topic); | |
| this.state = CHANNEL_STATES.joining; | |
| this.joinPush.resend(timeout); | |
| } | |
| /** | |
| * @param {string} event | |
| * @param {unknown} [payload] | |
| * @param {?string} [ref] | |
| * @param {?string} [joinRef] | |
| */ | |
| 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()); | |
| } | |
| } | |
| /** | |
| * @param {string} ref | |
| */ | |
| 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; | |
| } | |
| }; | |
| // js/phoenix/ajax.js | |
| 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)}`; | |
| } | |
| }; | |
| // 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, 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 = /* @__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() { | |
| 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}`); | |
| } | |
| }); | |
| } | |
| // 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, 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); | |
| } | |
| }; | |
| // js/phoenix/presence.js | |
| var Presence = class _Presence { | |
| /** | |
| * Initializes the Presence | |
| * @param {Channel} channel - The Channel | |
| * @param {PresenceOptions} [opts] - The options, for example `{events: {state: "state", diff: "diff"}}` | |
| */ | |
| constructor(channel, opts = {}) { | |
| let events = opts.events || /** @type {PresenceEvents} */ | |
| { state: "presence_state", diff: "presence_diff" }; | |
| this.state = /* @__PURE__ */ 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(); | |
| } | |
| }); | |
| } | |
| /** | |
| * @param {PresenceOnJoin} callback | |
| */ | |
| onJoin(callback) { | |
| this.caller.onJoin = callback; | |
| } | |
| /** | |
| * @param {PresenceOnLeave} callback | |
| */ | |
| onLeave(callback) { | |
| this.caller.onLeave = callback; | |
| } | |
| /** | |
| * @param {PresenceOnSync} callback | |
| */ | |
| onSync(callback) { | |
| this.caller.onSync = callback; | |
| } | |
| /** | |
| * Returns the array of presences, with selected metadata. | |
| * | |
| * @template [T=PresenceState] | |
| * @param {((key: string, obj: PresenceState) => T)} [by] | |
| * | |
| * @returns {T[]} | |
| */ | |
| 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. | |
| * | |
| * @param {Record<string, PresenceState>} currentState | |
| * @param {Record<string, PresenceState>} newState | |
| * @param {PresenceOnJoin} onJoin | |
| * @param {PresenceOnLeave} onLeave | |
| * | |
| * @returns {Record<string, PresenceState>} | |
| */ | |
| static syncState(currentState, newState, onJoin, onLeave) { | |
| let state = this.toNullProtoObj(this.clone(currentState)); | |
| newState = this.toNullProtoObj(newState); | |
| let joins = /* @__PURE__ */ Object.create(null); | |
| let leaves = /* @__PURE__ */ 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); | |
| } | |
| /** | |
| * | |
| * 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. | |
| * | |
| * @param {Record<string, PresenceState>} state | |
| * @param {PresenceDiff} diff | |
| * @param {PresenceOnJoin} onJoin | |
| * @param {PresenceOnLeave} onLeave | |
| * | |
| * @returns {Record<string, PresenceState>} | |
| */ | |
| 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; | |
| } | |
| /** | |
| * Returns the array of presences, with selected metadata. | |
| * | |
| * @template [T=PresenceState] | |
| * @param {Record<string, PresenceState>} presences | |
| * @param {((key: string, obj: PresenceState) => T)} [chooser] | |
| * | |
| * @returns {T[]} | |
| */ | |
| static list(presences, chooser) { | |
| if (!chooser) { | |
| chooser = function(key, pres) { | |
| return pres; | |
| }; | |
| } | |
| return this.map(presences, (key, presence) => { | |
| return chooser(key, presence); | |
| }); | |
| } | |
| // private | |
| /** | |
| * @template T | |
| * @param {Record<string, PresenceState>} obj | |
| * @param {(key: string, obj: PresenceState) => T} func | |
| */ | |
| static map(obj, func) { | |
| return Object.getOwnPropertyNames(obj).map((key) => func(key, obj[key])); | |
| } | |
| // Presence keys are chosen on the server and may collide with | |
| // Object.prototype properties ("__proto__", "constructor", ...), so any | |
| // object indexed by presence key must not have a prototype chain | |
| // | |
| // TODO: replace the null-prototype objects with Maps in Phoenix 2.0 | |
| // (breaking change for the lower-level static API) | |
| static toNullProtoObj(obj) { | |
| if (Object.getPrototypeOf(obj) === null) { | |
| return obj; | |
| } | |
| let cleaned = /* @__PURE__ */ Object.create(null); | |
| Object.getOwnPropertyNames(obj).forEach((key) => { | |
| cleaned[key] = obj[key]; | |
| }); | |
| return cleaned; | |
| } | |
| /** | |
| * @template T | |
| * @param {T} obj | |
| * @returns {T} | |
| */ | |
| 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 }, | |
| /** | |
| * @template T | |
| * @param {Message<Record<string, any>>} msg | |
| * @param {(msg: ArrayBuffer | string) => T} callback | |
| * @returns {T} | |
| */ | |
| 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)); | |
| } | |
| }, | |
| /** | |
| * @template T | |
| * @param {ArrayBuffer | string} rawPayload | |
| * @param {(msg: Message<unknown>) => T} callback | |
| * @returns {T} | |
| */ | |
| 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 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`); | |
| } | |
| }, | |
| /** | |
| * @private | |
| */ | |
| 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); | |
| } | |
| }, | |
| /** @private */ | |
| 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 }; | |
| }, | |
| /** @private */ | |
| 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 }; | |
| }, | |
| /** @private */ | |
| 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 { | |
| /** Initializes the Socket * | |
| * | |
| * For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim) | |
| * | |
| * @constructor | |
| * @param {string} endPoint - The string WebSocket endpoint, ie, `"ws://example.com/socket"`, | |
| * `"wss://example.com"` | |
| * `"/socket"` (inherited host & protocol) | |
| * @param {SocketOptions} [opts] - Optional configuration | |
| */ | |
| 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); | |
| } | |
| /** | |
| * Returns the LongPoll transport reference | |
| */ | |
| getLongPollTransport() { | |
| return LongPoll; | |
| } | |
| /** | |
| * Disconnects and replaces the active transport | |
| * | |
| * @param {SocketTransport} 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 {"wss" | "ws"} | |
| */ | |
| 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 {() => void} [callback] - Optional callback which is called after socket is disconnected. | |
| * @param {number} [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.disconnecting = true; | |
| this.closeWasClean = true; | |
| clearTimeout(this.fallbackTimer); | |
| this.reconnectTimer.reset(); | |
| this.teardown(() => { | |
| this.disconnecting = false; | |
| callback && callback(); | |
| }, code, reason); | |
| } | |
| /** | |
| * @param {Params} [params] - [DEPRECATED] 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 && !this.disconnecting) { | |
| 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 {SocketOnOpen} callback | |
| */ | |
| onOpen(callback) { | |
| let ref = this.makeRef(); | |
| this.stateChangeCallbacks.open.push([ref, callback]); | |
| return ref; | |
| } | |
| /** | |
| * Registers callbacks for connection close events | |
| * @param {SocketOnClose} callback | |
| * @returns {string} | |
| */ | |
| 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 {SocketOnError} callback | |
| * @returns {string} | |
| */ | |
| onError(callback) { | |
| let ref = this.makeRef(); | |
| this.stateChangeCallbacks.error.push([ref, callback]); | |
| return ref; | |
| } | |
| /** | |
| * Registers callbacks for connection message events | |
| * @param {SocketOnMessage} callback | |
| * @returns {string} | |
| */ | |
| onMessage(callback) { | |
| let ref = this.makeRef(); | |
| this.stateChangeCallbacks.message.push([ref, callback]); | |
| return ref; | |
| } | |
| /** | |
| * Sets a callback that receives lifecycle events for internal heartbeat messages. | |
| * Useful for instrumenting connection health (e.g. sent/ok/timeout/disconnected). | |
| * @param {HeartbeatCallback} callback | |
| */ | |
| onHeartbeat(callback) { | |
| this.heartbeatCallback = callback; | |
| } | |
| /** | |
| * Pings the server and invokes the callback with the RTT in milliseconds | |
| * @param {(timeDelta: number) => void} 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 | |
| * | |
| * @param {Function} | |
| */ | |
| transportName(transport) { | |
| switch (transport) { | |
| case LongPoll: | |
| return "LongPoll"; | |
| default: | |
| return transport.name; | |
| } | |
| } | |
| /** | |
| * @private | |
| */ | |
| 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"); | |
| } | |
| /** | |
| * @private | |
| */ | |
| 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); | |
| } | |
| /** | |
| * @param {CloseEvent} event | |
| */ | |
| 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); | |
| } | |
| /** | |
| * @private | |
| * @param {Event} error | |
| */ | |
| 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); | |
| } | |
| } | |
| /** | |
| * @private | |
| * @param {unknown} [reason] underlying close/error event forwarded to channel error listeners | |
| */ | |
| triggerChanError(reason) { | |
| this.channels.forEach((channel) => { | |
| if (!(channel.isErrored() || channel.isLeaving() || channel.isClosed())) { | |
| channel.trigger(CHANNEL_EVENTS.error, reason); | |
| } | |
| }); | |
| } | |
| /** | |
| * @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"; | |
| } | |
| /** | |
| * | |
| * @param {Channel} channel | |
| */ | |
| remove(channel) { | |
| this.off(channel.stateChangeRefs); | |
| this.channels = this.channels.filter((c) => c !== channel); | |
| } | |
| /** | |
| * Removes `onOpen`, `onClose`, `onError,` and `onMessage` registrations. | |
| * | |
| * @param {string[]} 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 {Params | (() => Params)} [chanParams]- Parameters for the channel | |
| * @returns {Channel} | |
| */ | |
| channel(topic, chanParams = {}) { | |
| let chan = new Channel(topic, chanParams, this); | |
| this.channels.push(chan); | |
| return chan; | |
| } | |
| /** | |
| * @param {Message<Record<string, any>>} 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.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 = []; | |
| } | |
| } | |
| /** | |
| * @param {MessageEvent<any>} rawMessage | |
| */ | |
| 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); | |
| }); | |
| } | |
| /** | |
| * @private | |
| * @template {keyof SocketStateChangeCallbacks} K | |
| * @param {K} event | |
| * @param {...Parameters<SocketStateChangeCallbacks[K][number][1]>} args | |
| * @returns {void} | |
| */ | |
| 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(); | |
| } | |
| } | |
| }; | |
| export { | |
| Channel, | |
| LongPoll, | |
| Presence, | |
| Push, | |
| serializer_default as Serializer, | |
| Socket, | |
| Timer | |
| }; | |
| //# sourceMappingURL=phoenix.mjs.map | |