1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432 |
- (function (global, factory) {
- typeof exports === 'object' ? factory(exports) :
- typeof define === 'function' && define.amd ? define(['exports'], factory) :
- factory(global.Phoenix = global.Phoenix || {});
- }(this, (function (exports) {
- "use strict";
- Object.defineProperty(exports, "__esModule", {
- value: true
- });
- var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
- var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
- var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
- function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
- function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
- /**
- * Phoenix Channels JavaScript client
- *
- * ## Socket Connection
- *
- * A single connection is established to the server and
- * channels are multiplexed over the connection.
- * Connect to the server using the `Socket` class:
- *
- * ```javascript
- * let socket = new Socket("/socket", {params: {userToken: "123"}})
- * socket.connect()
- * ```
- *
- * The `Socket` constructor takes the mount point of the socket,
- * the authentication params, as well as options that can be found in
- * the Socket docs, such as configuring the `LongPoll` transport, and
- * heartbeat.
- *
- * ## Channels
- *
- * Channels are isolated, concurrent processes on the server that
- * subscribe to topics and broker events between the client and server.
- * To join a channel, you must provide the topic, and channel params for
- * authorization. Here's an example chat room example where `"new_msg"`
- * events are listened for, messages are pushed to the server, and
- * the channel is joined with ok/error/timeout matches:
- *
- * ```javascript
- * let channel = socket.channel("room:123", {token: roomToken})
- * channel.on("new_msg", msg => console.log("Got message", msg) )
- * $input.onEnter( e => {
- * channel.push("new_msg", {body: e.target.val}, 10000)
- * .receive("ok", (msg) => console.log("created message", msg) )
- * .receive("error", (reasons) => console.log("create failed", reasons) )
- * .receive("timeout", () => console.log("Networking issue...") )
- * })
- * channel.join()
- * .receive("ok", ({messages}) => console.log("catching up", messages) )
- * .receive("error", ({reason}) => console.log("failed join", reason) )
- * .receive("timeout", () => console.log("Networking issue. Still waiting...") )
- *```
- *
- * ## Joining
- *
- * Creating a channel with `socket.channel(topic, params)`, binds the params to
- * `channel.params`, which are sent up on `channel.join()`.
- * Subsequent rejoins will send up the modified params for
- * updating authorization params, or passing up last_message_id information.
- * Successful joins receive an "ok" status, while unsuccessful joins
- * receive "error".
- *
- * ## Duplicate Join Subscriptions
- *
- * While the client may join any number of topics on any number of channels,
- * the client may only hold a single subscription for each unique topic at any
- * given time. When attempting to create a duplicate subscription,
- * the server will close the existing channel, log a warning, and
- * spawn a new channel for the topic. The client will have their
- * `channel.onClose` callbacks fired for the existing channel, and the new
- * channel join will have its receive hooks processed as normal.
- *
- * ## Pushing Messages
- *
- * From the previous example, we can see that pushing messages to the server
- * can be done with `channel.push(eventName, payload)` and we can optionally
- * receive responses from the push. Additionally, we can use
- * `receive("timeout", callback)` to abort waiting for our other `receive` hooks
- * and take action after some period of waiting. The default timeout is 5000ms.
- *
- *
- * ## Socket Hooks
- *
- * Lifecycle events of the multiplexed connection can be hooked into via
- * `socket.onError()` and `socket.onClose()` events, ie:
- *
- * ```javascript
- * socket.onError( () => console.log("there was an error with the connection!") )
- * socket.onClose( () => console.log("the connection dropped") )
- * ```
- *
- *
- * ## Channel Hooks
- *
- * For each joined channel, you can bind to `onError` and `onClose` events
- * to monitor the channel lifecycle, ie:
- *
- * ```javascript
- * channel.onError( () => console.log("there was an error!") )
- * channel.onClose( () => console.log("the channel has gone away gracefully") )
- * ```
- *
- * ### onError hooks
- *
- * `onError` hooks are invoked if the socket connection drops, or the channel
- * crashes on the server. In either case, a channel rejoin is attempted
- * automatically in an exponential backoff manner.
- *
- * ### onClose hooks
- *
- * `onClose` hooks are invoked only in two cases. 1) the channel explicitly
- * closed on the server, or 2). The client explicitly closed, by calling
- * `channel.leave()`
- *
- *
- * ## Presence
- *
- * The `Presence` object provides features for syncing presence information
- * from the server with the client and handling presences joining and leaving.
- *
- * ### Syncing initial state from the server
- *
- * `Presence.syncState` is 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.
- *
- * `Presence.syncDiff` is 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.
- *
- * ### Listing Presences
- *
- * `Presence.list` is used to return a list of presence information
- * based on the local state of metadata. By default, all presence
- * metadata is returned, but a `listBy` function can be supplied to
- * allow the client to select which metadata to use for a given presence.
- * For example, you may have a user online from different devices with
- * a metadata status of "online", but they have set themselves to "away"
- * on another device. In this case, the app may choose to use the "away"
- * status for what appears on the UI. The example below defines a `listBy`
- * function which prioritizes the first metadata which was registered for
- * each user. This could be the first tab they opened, or the first device
- * they came online from:
- *
- * ```javascript
- * let state = {}
- * state = Presence.syncState(state, stateFromServer)
- * let listBy = (id, {metas: [first, ...rest]}) => {
- * first.count = rest.length + 1 // count of this user's presences
- * first.id = id
- * return first
- * }
- * let onlineUsers = Presence.list(state, listBy)
- * ```
- *
- *
- * ### Example Usage
- *```javascript
- * // detect if user has joined for the 1st time or from another tab/device
- * let onJoin = (id, current, newPres) => {
- * if(!current){
- * console.log("user has entered for the first time", newPres)
- * } else {
- * console.log("user additional presence", newPres)
- * }
- * }
- * // detect if user has left from all tabs/devices, or is still present
- * let onLeave = (id, current, leftPres) => {
- * if(current.metas.length === 0){
- * console.log("user has left from all devices", leftPres)
- * } else {
- * console.log("user left from a device", leftPres)
- * }
- * }
- * let presences = {} // client's initial empty presence state
- * // receive initial presence data from server, sent after join
- * myChannel.on("presence_state", state => {
- * presences = Presence.syncState(presences, state, onJoin, onLeave)
- * displayUsers(Presence.list(presences))
- * })
- * // receive "presence_diff" from server, containing join/leave events
- * myChannel.on("presence_diff", diff => {
- * presences = Presence.syncDiff(presences, diff, onJoin, onLeave)
- * this.setState({users: Presence.list(room.presences, listBy)})
- * })
- * ```
- * @module phoenix
- */
- var VSN = "2.0.0";
- var SOCKET_STATES = { connecting: 0, open: 1, closing: 2, closed: 3 };
- var DEFAULT_TIMEOUT = 10000;
- var WS_CLOSE_NORMAL = 1000;
- 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 CHANNEL_LIFECYCLE_EVENTS = [CHANNEL_EVENTS.close, CHANNEL_EVENTS.error, CHANNEL_EVENTS.join, CHANNEL_EVENTS.reply, CHANNEL_EVENTS.leave];
- var TRANSPORTS = {
- longpoll: "longpoll",
- websocket: "websocket"
- };
- /**
- * Initializes the Push
- * @param {Channel} channel - The Channel
- * @param {string} event - The event, for example `"phx_join"`
- * @param {Object} payload - The payload, for example `{user_id: 123}`
- * @param {number} timeout - The push timeout in milliseconds
- */
- var Push = function () {
- function Push(channel, event, payload, timeout) {
- _classCallCheck(this, Push);
- this.channel = channel;
- this.event = event;
- this.payload = payload || {};
- this.receivedResp = null;
- this.timeout = timeout;
- this.timeoutTimer = null;
- this.recHooks = [];
- this.sent = false;
- }
- /**
- *
- * @param {number} timeout
- */
- _createClass(Push, [{
- key: "resend",
- value: function resend(timeout) {
- this.timeout = timeout;
- this.reset();
- this.send();
- }
- /**
- *
- */
- }, {
- key: "send",
- value: function 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
- */
- }, {
- key: "receive",
- value: function receive(status, callback) {
- if (this.hasReceived(status)) {
- callback(this.receivedResp.response);
- }
- this.recHooks.push({ status: status, callback: callback });
- return this;
- }
- // private
- }, {
- key: "reset",
- value: function reset() {
- this.cancelRefEvent();
- this.ref = null;
- this.refEvent = null;
- this.receivedResp = null;
- this.sent = false;
- }
- }, {
- key: "matchReceive",
- value: function matchReceive(_ref) {
- var status = _ref.status,
- response = _ref.response,
- ref = _ref.ref;
- this.recHooks.filter(function (h) {
- return h.status === status;
- }).forEach(function (h) {
- return h.callback(response);
- });
- }
- }, {
- key: "cancelRefEvent",
- value: function cancelRefEvent() {
- if (!this.refEvent) {
- return;
- }
- this.channel.off(this.refEvent);
- }
- }, {
- key: "cancelTimeout",
- value: function cancelTimeout() {
- clearTimeout(this.timeoutTimer);
- this.timeoutTimer = null;
- }
- }, {
- key: "startTimeout",
- value: function startTimeout() {
- var _this = this;
- if (this.timeoutTimer) {
- this.cancelTimeout();
- }
- this.ref = this.channel.socket.makeRef();
- this.refEvent = this.channel.replyEventName(this.ref);
- this.channel.on(this.refEvent, function (payload) {
- _this.cancelRefEvent();
- _this.cancelTimeout();
- _this.receivedResp = payload;
- _this.matchReceive(payload);
- });
- this.timeoutTimer = setTimeout(function () {
- _this.trigger("timeout", {});
- }, this.timeout);
- }
- }, {
- key: "hasReceived",
- value: function hasReceived(status) {
- return this.receivedResp && this.receivedResp.status === status;
- }
- }, {
- key: "trigger",
- value: function trigger(status, response) {
- this.channel.trigger(this.refEvent, { status: status, response: response });
- }
- }]);
- return Push;
- }();
- /**
- *
- * @param {string} topic
- * @param {Object} params
- * @param {Socket} socket
- */
- var Channel = exports.Channel = function () {
- function Channel(topic, params, socket) {
- var _this2 = this;
- _classCallCheck(this, Channel);
- this.state = CHANNEL_STATES.closed;
- this.topic = topic;
- this.params = params || {};
- this.socket = socket;
- this.bindings = [];
- this.timeout = this.socket.timeout;
- this.joinedOnce = false;
- this.joinPush = new Push(this, CHANNEL_EVENTS.join, this.params, this.timeout);
- this.pushBuffer = [];
- this.rejoinTimer = new Timer(function () {
- return _this2.rejoinUntilConnected();
- }, this.socket.reconnectAfterMs);
- this.joinPush.receive("ok", function () {
- _this2.state = CHANNEL_STATES.joined;
- _this2.rejoinTimer.reset();
- _this2.pushBuffer.forEach(function (pushEvent) {
- return pushEvent.send();
- });
- _this2.pushBuffer = [];
- });
- this.onClose(function () {
- _this2.rejoinTimer.reset();
- _this2.socket.log("channel", "close " + _this2.topic + " " + _this2.joinRef());
- _this2.state = CHANNEL_STATES.closed;
- _this2.socket.remove(_this2);
- });
- this.onError(function (reason) {
- if (_this2.isLeaving() || _this2.isClosed()) {
- return;
- }
- _this2.socket.log("channel", "error " + _this2.topic, reason);
- _this2.state = CHANNEL_STATES.errored;
- _this2.rejoinTimer.scheduleTimeout();
- });
- this.joinPush.receive("timeout", function () {
- if (!_this2.isJoining()) {
- return;
- }
- _this2.socket.log("channel", "timeout " + _this2.topic + " (" + _this2.joinRef() + ")", _this2.joinPush.timeout);
- var leavePush = new Push(_this2, CHANNEL_EVENTS.leave, {}, _this2.timeout);
- leavePush.send();
- _this2.state = CHANNEL_STATES.errored;
- _this2.joinPush.reset();
- _this2.rejoinTimer.scheduleTimeout();
- });
- this.on(CHANNEL_EVENTS.reply, function (payload, ref) {
- _this2.trigger(_this2.replyEventName(ref), payload);
- });
- }
- _createClass(Channel, [{
- key: "rejoinUntilConnected",
- value: function rejoinUntilConnected() {
- this.rejoinTimer.scheduleTimeout();
- if (this.socket.isConnected()) {
- this.rejoin();
- }
- }
- }, {
- key: "join",
- value: function join() {
- var timeout = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.timeout;
- if (this.joinedOnce) {
- throw "tried to join multiple times. 'join' can only be called a single time per channel instance";
- } else {
- this.joinedOnce = true;
- this.rejoin(timeout);
- return this.joinPush;
- }
- }
- }, {
- key: "onClose",
- value: function onClose(callback) {
- this.on(CHANNEL_EVENTS.close, callback);
- }
- }, {
- key: "onError",
- value: function onError(callback) {
- this.on(CHANNEL_EVENTS.error, function (reason) {
- return callback(reason);
- });
- }
- }, {
- key: "on",
- value: function on(event, callback) {
- this.bindings.push({ event: event, callback: callback });
- }
- }, {
- key: "off",
- value: function off(event) {
- this.bindings = this.bindings.filter(function (bind) {
- return bind.event !== event;
- });
- }
- }, {
- key: "canPush",
- value: function canPush() {
- return this.socket.isConnected() && this.isJoined();
- }
- }, {
- key: "push",
- value: function push(event, payload) {
- var timeout = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.timeout;
- if (!this.joinedOnce) {
- throw "tried to push '" + event + "' to '" + this.topic + "' before joining. Use channel.join() before pushing events";
- }
- var pushEvent = new Push(this, event, 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 a `receive`
- * hook to bind to the server ack, ie:
- *
- * ```javascript
- * channel.leave().receive("ok", () => alert("left!") )
- * ```
- */
- }, {
- key: "leave",
- value: function leave() {
- var _this3 = this;
- var timeout = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.timeout;
- this.state = CHANNEL_STATES.leaving;
- var onClose = function onClose() {
- _this3.socket.log("channel", "leave " + _this3.topic);
- _this3.trigger(CHANNEL_EVENTS.close, "leave");
- };
- var leavePush = new Push(this, CHANNEL_EVENTS.leave, {}, timeout);
- leavePush.receive("ok", function () {
- return onClose();
- }).receive("timeout", function () {
- return 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
- */
- }, {
- key: "onMessage",
- value: function onMessage(event, payload, ref) {
- return payload;
- }
- // private
- }, {
- key: "isMember",
- value: function isMember(topic, event, payload, joinRef) {
- if (this.topic !== topic) {
- return false;
- }
- var isLifecycleEvent = CHANNEL_LIFECYCLE_EVENTS.indexOf(event) >= 0;
- if (joinRef && isLifecycleEvent && joinRef !== this.joinRef()) {
- this.socket.log("channel", "dropping outdated message", { topic: topic, event: event, payload: payload, joinRef: joinRef });
- return false;
- } else {
- return true;
- }
- }
- }, {
- key: "joinRef",
- value: function joinRef() {
- return this.joinPush.ref;
- }
- }, {
- key: "sendJoin",
- value: function sendJoin(timeout) {
- this.state = CHANNEL_STATES.joining;
- this.joinPush.resend(timeout);
- }
- }, {
- key: "rejoin",
- value: function rejoin() {
- var timeout = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.timeout;
- if (this.isLeaving()) {
- return;
- }
- this.sendJoin(timeout);
- }
- }, {
- key: "trigger",
- value: function trigger(event, payload, ref, joinRef) {
- var _this4 = this;
- var handledPayload = this.onMessage(event, payload, ref, joinRef);
- if (payload && !handledPayload) {
- throw "channel onMessage callbacks must return the payload, modified or unmodified";
- }
- this.bindings.filter(function (bind) {
- return bind.event === event;
- }).map(function (bind) {
- return bind.callback(handledPayload, ref, joinRef || _this4.joinRef());
- });
- }
- }, {
- key: "replyEventName",
- value: function replyEventName(ref) {
- return "chan_reply_" + ref;
- }
- }, {
- key: "isClosed",
- value: function isClosed() {
- return this.state === CHANNEL_STATES.closed;
- }
- }, {
- key: "isErrored",
- value: function isErrored() {
- return this.state === CHANNEL_STATES.errored;
- }
- }, {
- key: "isJoined",
- value: function isJoined() {
- return this.state === CHANNEL_STATES.joined;
- }
- }, {
- key: "isJoining",
- value: function isJoining() {
- return this.state === CHANNEL_STATES.joining;
- }
- }, {
- key: "isLeaving",
- value: function isLeaving() {
- return this.state === CHANNEL_STATES.leaving;
- }
- }]);
- return Channel;
- }();
- var Serializer = {
- encode: function encode(msg, callback) {
- var payload = [msg.join_ref, msg.ref, msg.topic, msg.event, msg.payload];
- return callback(JSON.stringify(payload));
- },
- decode: function decode(rawPayload, callback) {
- var _JSON$parse = JSON.parse(rawPayload),
- _JSON$parse2 = _slicedToArray(_JSON$parse, 5),
- join_ref = _JSON$parse2[0],
- ref = _JSON$parse2[1],
- topic = _JSON$parse2[2],
- event = _JSON$parse2[3],
- payload = _JSON$parse2[4];
- return callback({ join_ref: join_ref, ref: ref, topic: topic, event: event, payload: payload });
- }
- };
- /** Initializes the Socket
- *
- *
- * For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim)
- *
- * @param {string} endPoint - The string WebSocket endpoint, ie, `"ws://example.com/socket"`,
- * `"wss://example.com"`
- * `"/socket"` (inherited host & protocol)
- * @param {Object} opts - Optional configuration
- * @param {string} opts.transport - The Websocket Transport, for example WebSocket or Phoenix.LongPoll.
- *
- * Defaults to WebSocket with automatic LongPoll fallback.
- * @param {Function} opts.encode - The function to encode outgoing messages.
- *
- * Defaults to JSON:
- *
- * ```javascript
- * (payload, callback) => callback(JSON.stringify(payload))
- * ```
- *
- * @param {Function} opts.decode - The function to decode incoming messages.
- *
- * Defaults to JSON:
- *
- * ```javascript
- * (payload, callback) => callback(JSON.parse(payload))
- * ```
- *
- * @param {number} opts.timeout - The default timeout in milliseconds to trigger push timeouts.
- *
- * Defaults `DEFAULT_TIMEOUT`
- * @param {number} opts.heartbeatIntervalMs - The millisec interval to send a heartbeat message
- * @param {number} opts.reconnectAfterMs - The optional function that returns the millsec reconnect interval.
- *
- * Defaults to stepped backoff of:
- *
- * ```javascript
- * function(tries){
- * return [1000, 5000, 10000][tries - 1] || 10000
- * }
- * ```
- * @param {Function} opts.logger - The optional function for specialized logging, ie:
- * ```javascript
- * logger: (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) }
- * ```
- *
- * @param {number} opts.longpollerTimeout - The maximum timeout of a long poll AJAX request.
- *
- * Defaults to 20s (double the server long poll timer).
- *
- * @param {Object} opts.params - The optional params to pass when connecting
- *
- *
- */
- var Socket = exports.Socket = function () {
- function Socket(endPoint) {
- var _this5 = this;
- var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
- _classCallCheck(this, Socket);
- this.stateChangeCallbacks = { open: [], close: [], error: [], message: [] };
- this.channels = [];
- this.sendBuffer = [];
- this.ref = 0;
- this.timeout = opts.timeout || DEFAULT_TIMEOUT;
- this.transport = opts.transport || window.WebSocket || LongPoll;
- this.defaultEncoder = Serializer.encode;
- this.defaultDecoder = Serializer.decode;
- 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;
- }
- this.heartbeatIntervalMs = opts.heartbeatIntervalMs || 30000;
- this.reconnectAfterMs = opts.reconnectAfterMs || function (tries) {
- return [1000, 2000, 5000, 10000][tries - 1] || 10000;
- };
- this.logger = opts.logger || function () {}; // noop
- this.longpollerTimeout = opts.longpollerTimeout || 20000;
- this.params = opts.params || {};
- this.endPoint = endPoint + "/" + TRANSPORTS.websocket;
- this.heartbeatTimer = null;
- this.pendingHeartbeatRef = null;
- this.reconnectTimer = new Timer(function () {
- _this5.disconnect(function () {
- return _this5.connect();
- });
- }, this.reconnectAfterMs);
- }
- _createClass(Socket, [{
- key: "protocol",
- value: function protocol() {
- return location.protocol.match(/^https/) ? "wss" : "ws";
- }
- }, {
- key: "endPointURL",
- value: function endPointURL() {
- var uri = Ajax.appendParams(Ajax.appendParams(this.endPoint, this.params), { vsn: VSN });
- if (uri.charAt(0) !== "/") {
- return uri;
- }
- if (uri.charAt(1) === "/") {
- return this.protocol() + ":" + uri;
- }
- return this.protocol() + "://" + location.host + uri;
- }
- }, {
- key: "disconnect",
- value: function disconnect(callback, code, reason) {
- if (this.conn) {
- this.conn.onclose = function () {}; // noop
- if (code) {
- this.conn.close(code, reason || "");
- } else {
- this.conn.close();
- }
- this.conn = null;
- }
- callback && callback();
- }
- /**
- *
- * @param {Object} params - The params to send when connecting, for example `{user_id: userToken}`
- */
- }, {
- key: "connect",
- value: function connect(params) {
- var _this6 = this;
- if (params) {
- console && console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor");
- this.params = params;
- }
- if (this.conn) {
- return;
- }
- this.conn = new this.transport(this.endPointURL());
- this.conn.timeout = this.longpollerTimeout;
- this.conn.onopen = function () {
- return _this6.onConnOpen();
- };
- this.conn.onerror = function (error) {
- return _this6.onConnError(error);
- };
- this.conn.onmessage = function (event) {
- return _this6.onConnMessage(event);
- };
- this.conn.onclose = function (event) {
- return _this6.onConnClose(event);
- };
- }
- /**
- * Logs the message. Override `this.logger` for specialized logging. noops by default
- * @param {string} kind
- * @param {string} msg
- * @param {Object} data
- */
- }, {
- key: "log",
- value: function log(kind, msg, data) {
- this.logger(kind, msg, data);
- }
- // Registers callbacks for connection state change events
- //
- // Examples
- //
- // socket.onError(function(error){ alert("An error occurred") })
- //
- }, {
- key: "onOpen",
- value: function onOpen(callback) {
- this.stateChangeCallbacks.open.push(callback);
- }
- }, {
- key: "onClose",
- value: function onClose(callback) {
- this.stateChangeCallbacks.close.push(callback);
- }
- }, {
- key: "onError",
- value: function onError(callback) {
- this.stateChangeCallbacks.error.push(callback);
- }
- }, {
- key: "onMessage",
- value: function onMessage(callback) {
- this.stateChangeCallbacks.message.push(callback);
- }
- }, {
- key: "onConnOpen",
- value: function onConnOpen() {
- var _this7 = this;
- this.log("transport", "connected to " + this.endPointURL());
- this.flushSendBuffer();
- this.reconnectTimer.reset();
- if (!this.conn.skipHeartbeat) {
- clearInterval(this.heartbeatTimer);
- this.heartbeatTimer = setInterval(function () {
- return _this7.sendHeartbeat();
- }, this.heartbeatIntervalMs);
- }
- this.stateChangeCallbacks.open.forEach(function (callback) {
- return callback();
- });
- }
- }, {
- key: "onConnClose",
- value: function onConnClose(event) {
- this.log("transport", "close", event);
- this.triggerChanError();
- clearInterval(this.heartbeatTimer);
- this.reconnectTimer.scheduleTimeout();
- this.stateChangeCallbacks.close.forEach(function (callback) {
- return callback(event);
- });
- }
- }, {
- key: "onConnError",
- value: function onConnError(error) {
- this.log("transport", error);
- this.triggerChanError();
- this.stateChangeCallbacks.error.forEach(function (callback) {
- return callback(error);
- });
- }
- }, {
- key: "triggerChanError",
- value: function triggerChanError() {
- this.channels.forEach(function (channel) {
- return channel.trigger(CHANNEL_EVENTS.error);
- });
- }
- }, {
- key: "connectionState",
- value: function 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";
- }
- }
- }, {
- key: "isConnected",
- value: function isConnected() {
- return this.connectionState() === "open";
- }
- }, {
- key: "remove",
- value: function remove(channel) {
- this.channels = this.channels.filter(function (c) {
- return c.joinRef() !== channel.joinRef();
- });
- }
- }, {
- key: "channel",
- value: function channel(topic) {
- var chanParams = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
- var chan = new Channel(topic, chanParams, this);
- this.channels.push(chan);
- return chan;
- }
- }, {
- key: "push",
- value: function push(data) {
- var _this8 = this;
- var topic = data.topic,
- event = data.event,
- payload = data.payload,
- ref = data.ref,
- join_ref = data.join_ref;
- var callback = function callback() {
- _this8.encode(data, function (result) {
- _this8.conn.send(result);
- });
- };
- this.log("push", topic + " " + event + " (" + join_ref + ", " + ref + ")", payload);
- if (this.isConnected()) {
- callback();
- } else {
- this.sendBuffer.push(callback);
- }
- }
- /**
- * Return the next message ref, accounting for overflows
- */
- }, {
- key: "makeRef",
- value: function makeRef() {
- var newRef = this.ref + 1;
- if (newRef === this.ref) {
- this.ref = 0;
- } else {
- this.ref = newRef;
- }
- return this.ref.toString();
- }
- }, {
- key: "sendHeartbeat",
- value: function sendHeartbeat() {
- if (!this.isConnected()) {
- return;
- }
- if (this.pendingHeartbeatRef) {
- this.pendingHeartbeatRef = null;
- this.log("transport", "heartbeat timeout. Attempting to re-establish connection");
- this.conn.close(WS_CLOSE_NORMAL, "hearbeat timeout");
- return;
- }
- this.pendingHeartbeatRef = this.makeRef();
- this.push({ topic: "phoenix", event: "heartbeat", payload: {}, ref: this.pendingHeartbeatRef });
- }
- }, {
- key: "flushSendBuffer",
- value: function flushSendBuffer() {
- if (this.isConnected() && this.sendBuffer.length > 0) {
- this.sendBuffer.forEach(function (callback) {
- return callback();
- });
- this.sendBuffer = [];
- }
- }
- }, {
- key: "onConnMessage",
- value: function onConnMessage(rawMessage) {
- var _this9 = this;
- this.decode(rawMessage.data, function (msg) {
- var topic = msg.topic,
- event = msg.event,
- payload = msg.payload,
- ref = msg.ref,
- join_ref = msg.join_ref;
- if (ref && ref === _this9.pendingHeartbeatRef) {
- _this9.pendingHeartbeatRef = null;
- }
- _this9.log("receive", (payload.status || "") + " " + topic + " " + event + " " + (ref && "(" + ref + ")" || ""), payload);
- _this9.channels.filter(function (channel) {
- return channel.isMember(topic, event, payload, join_ref);
- }).forEach(function (channel) {
- return channel.trigger(event, payload, ref, join_ref);
- });
- _this9.stateChangeCallbacks.message.forEach(function (callback) {
- return callback(msg);
- });
- });
- }
- }]);
- return Socket;
- }();
- var LongPoll = exports.LongPoll = function () {
- function LongPoll(endPoint) {
- _classCallCheck(this, LongPoll);
- this.endPoint = null;
- this.token = null;
- this.skipHeartbeat = true;
- this.onopen = function () {}; // noop
- this.onerror = function () {}; // noop
- this.onmessage = function () {}; // noop
- this.onclose = function () {}; // noop
- this.pollEndpoint = this.normalizeEndpoint(endPoint);
- this.readyState = SOCKET_STATES.connecting;
- this.poll();
- }
- _createClass(LongPoll, [{
- key: "normalizeEndpoint",
- value: function normalizeEndpoint(endPoint) {
- return endPoint.replace("ws://", "http://").replace("wss://", "https://").replace(new RegExp("(.*)\/" + TRANSPORTS.websocket), "$1/" + TRANSPORTS.longpoll);
- }
- }, {
- key: "endpointURL",
- value: function endpointURL() {
- return Ajax.appendParams(this.pollEndpoint, { token: this.token });
- }
- }, {
- key: "closeAndRetry",
- value: function closeAndRetry() {
- this.close();
- this.readyState = SOCKET_STATES.connecting;
- }
- }, {
- key: "ontimeout",
- value: function ontimeout() {
- this.onerror("timeout");
- this.closeAndRetry();
- }
- }, {
- key: "poll",
- value: function poll() {
- var _this10 = this;
- if (!(this.readyState === SOCKET_STATES.open || this.readyState === SOCKET_STATES.connecting)) {
- return;
- }
- Ajax.request("GET", this.endpointURL(), "application/json", null, this.timeout, this.ontimeout.bind(this), function (resp) {
- if (resp) {
- var status = resp.status,
- token = resp.token,
- messages = resp.messages;
- _this10.token = token;
- } else {
- var status = 0;
- }
- switch (status) {
- case 200:
- messages.forEach(function (msg) {
- return _this10.onmessage({ data: msg });
- });
- _this10.poll();
- break;
- case 204:
- _this10.poll();
- break;
- case 410:
- _this10.readyState = SOCKET_STATES.open;
- _this10.onopen();
- _this10.poll();
- break;
- case 0:
- case 500:
- _this10.onerror();
- _this10.closeAndRetry();
- break;
- default:
- throw "unhandled poll status " + status;
- }
- });
- }
- }, {
- key: "send",
- value: function send(body) {
- var _this11 = this;
- Ajax.request("POST", this.endpointURL(), "application/json", body, this.timeout, this.onerror.bind(this, "timeout"), function (resp) {
- if (!resp || resp.status !== 200) {
- _this11.onerror(resp && resp.status);
- _this11.closeAndRetry();
- }
- });
- }
- }, {
- key: "close",
- value: function close(code, reason) {
- this.readyState = SOCKET_STATES.closed;
- this.onclose();
- }
- }]);
- return LongPoll;
- }();
- var Ajax = exports.Ajax = function () {
- function Ajax() {
- _classCallCheck(this, Ajax);
- }
- _createClass(Ajax, null, [{
- key: "request",
- value: function request(method, endPoint, accept, body, timeout, ontimeout, callback) {
- if (window.XDomainRequest) {
- var req = new XDomainRequest(); // IE8, IE9
- this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback);
- } else {
- var _req = window.XMLHttpRequest ? new window.XMLHttpRequest() : // IE7+, Firefox, Chrome, Opera, Safari
- new ActiveXObject("Microsoft.XMLHTTP"); // IE6, IE5
- this.xhrRequest(_req, method, endPoint, accept, body, timeout, ontimeout, callback);
- }
- }
- }, {
- key: "xdomainRequest",
- value: function xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback) {
- var _this12 = this;
- req.timeout = timeout;
- req.open(method, endPoint);
- req.onload = function () {
- var response = _this12.parseJSON(req.responseText);
- callback && callback(response);
- };
- if (ontimeout) {
- req.ontimeout = ontimeout;
- }
- // Work around bug in IE9 that requires an attached onprogress handler
- req.onprogress = function () {};
- req.send(body);
- }
- }, {
- key: "xhrRequest",
- value: function xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback) {
- var _this13 = this;
- req.open(method, endPoint, true);
- req.timeout = timeout;
- req.setRequestHeader("Content-Type", accept);
- req.onerror = function () {
- callback && callback(null);
- };
- req.onreadystatechange = function () {
- if (req.readyState === _this13.states.complete && callback) {
- var response = _this13.parseJSON(req.responseText);
- callback(response);
- }
- };
- if (ontimeout) {
- req.ontimeout = ontimeout;
- }
- req.send(body);
- }
- }, {
- key: "parseJSON",
- value: function 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;
- }
- }
- }, {
- key: "serialize",
- value: function serialize(obj, parentKey) {
- var queryStr = [];
- for (var key in obj) {
- if (!obj.hasOwnProperty(key)) {
- continue;
- }
- var paramKey = parentKey ? parentKey + "[" + key + "]" : key;
- var paramVal = obj[key];
- if ((typeof paramVal === "undefined" ? "undefined" : _typeof(paramVal)) === "object") {
- queryStr.push(this.serialize(paramVal, paramKey));
- } else {
- queryStr.push(encodeURIComponent(paramKey) + "=" + encodeURIComponent(paramVal));
- }
- }
- return queryStr.join("&");
- }
- }, {
- key: "appendParams",
- value: function appendParams(url, params) {
- if (Object.keys(params).length === 0) {
- return url;
- }
- var prefix = url.match(/\?/) ? "&" : "?";
- return "" + url + prefix + this.serialize(params);
- }
- }]);
- return Ajax;
- }();
- Ajax.states = { complete: 4 };
- var Presence = exports.Presence = {
- syncState: function syncState(currentState, newState, onJoin, onLeave) {
- var _this14 = this;
- var state = this.clone(currentState);
- var joins = {};
- var leaves = {};
- this.map(state, function (key, presence) {
- if (!newState[key]) {
- leaves[key] = presence;
- }
- });
- this.map(newState, function (key, newPresence) {
- var currentPresence = state[key];
- if (currentPresence) {
- var newRefs = newPresence.metas.map(function (m) {
- return m.phx_ref;
- });
- var curRefs = currentPresence.metas.map(function (m) {
- return m.phx_ref;
- });
- var joinedMetas = newPresence.metas.filter(function (m) {
- return curRefs.indexOf(m.phx_ref) < 0;
- });
- var leftMetas = currentPresence.metas.filter(function (m) {
- return newRefs.indexOf(m.phx_ref) < 0;
- });
- if (joinedMetas.length > 0) {
- joins[key] = newPresence;
- joins[key].metas = joinedMetas;
- }
- if (leftMetas.length > 0) {
- leaves[key] = _this14.clone(currentPresence);
- leaves[key].metas = leftMetas;
- }
- } else {
- joins[key] = newPresence;
- }
- });
- return this.syncDiff(state, { joins: joins, leaves: leaves }, onJoin, onLeave);
- },
- syncDiff: function syncDiff(currentState, _ref2, onJoin, onLeave) {
- var joins = _ref2.joins,
- leaves = _ref2.leaves;
- var state = this.clone(currentState);
- if (!onJoin) {
- onJoin = function onJoin() {};
- }
- if (!onLeave) {
- onLeave = function onLeave() {};
- }
- this.map(joins, function (key, newPresence) {
- var currentPresence = state[key];
- state[key] = newPresence;
- if (currentPresence) {
- var _state$key$metas;
- (_state$key$metas = state[key].metas).unshift.apply(_state$key$metas, _toConsumableArray(currentPresence.metas));
- }
- onJoin(key, currentPresence, newPresence);
- });
- this.map(leaves, function (key, leftPresence) {
- var currentPresence = state[key];
- if (!currentPresence) {
- return;
- }
- var refsToRemove = leftPresence.metas.map(function (m) {
- return m.phx_ref;
- });
- currentPresence.metas = currentPresence.metas.filter(function (p) {
- return refsToRemove.indexOf(p.phx_ref) < 0;
- });
- onLeave(key, currentPresence, leftPresence);
- if (currentPresence.metas.length === 0) {
- delete state[key];
- }
- });
- return state;
- },
- list: function list(presences, chooser) {
- if (!chooser) {
- chooser = function chooser(key, pres) {
- return pres;
- };
- }
- return this.map(presences, function (key, presence) {
- return chooser(key, presence);
- });
- },
- // private
- map: function map(obj, func) {
- return Object.getOwnPropertyNames(obj).map(function (key) {
- return func(key, obj[key]);
- });
- },
- clone: function clone(obj) {
- return JSON.parse(JSON.stringify(obj));
- }
- };
- /**
- *
- * Creates a timer that accepts a `timerCalc` function to perform
- * calculated timeout retries, such as exponential backoff.
- *
- * ## Examples
- *
- * ```javascript
- * let reconnectTimer = new Timer(() => this.connect(), function(tries){
- * return [1000, 5000, 10000][tries - 1] || 10000
- * })
- * reconnectTimer.scheduleTimeout() // fires after 1000
- * reconnectTimer.scheduleTimeout() // fires after 5000
- * reconnectTimer.reset()
- * reconnectTimer.scheduleTimeout() // fires after 1000
- * ```
- * @param {Function} callback
- * @param {Function} timerCalc
- */
- var Timer = function () {
- function Timer(callback, timerCalc) {
- _classCallCheck(this, Timer);
- this.callback = callback;
- this.timerCalc = timerCalc;
- this.timer = null;
- this.tries = 0;
- }
- _createClass(Timer, [{
- key: "reset",
- value: function reset() {
- this.tries = 0;
- clearTimeout(this.timer);
- }
- /**
- * Cancels any previous scheduleTimeout and schedules callback
- */
- }, {
- key: "scheduleTimeout",
- value: function scheduleTimeout() {
- var _this15 = this;
- clearTimeout(this.timer);
- this.timer = setTimeout(function () {
- _this15.tries = _this15.tries + 1;
- _this15.callback();
- }, this.timerCalc(this.tries + 1));
- }
- }]);
- return Timer;
- }();
- })));
|