90ceb4f6fe
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
1432 lines
43 KiB
JavaScript
1432 lines
43 KiB
JavaScript
(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;
|
|
}();
|
|
|
|
})));
|