summaryrefslogtreecommitdiff
path: root/priv/static/js
diff options
context:
space:
mode:
authorhref <href@random.sh>2018-01-31 20:36:42 +0100
committerhref <href@random.sh>2018-01-31 20:36:42 +0100
commitfcb2a082346054108f3f16cffe968450cf961976 (patch)
tree8e7d2eb9eeebf854e62e08c29b147f34dfeb1881 /priv/static/js
import
Diffstat (limited to 'priv/static/js')
-rw-r--r--priv/static/js/app.js3
-rw-r--r--priv/static/js/phoenix.js1431
-rw-r--r--priv/static/js/widget.js0
3 files changed, 1434 insertions, 0 deletions
diff --git a/priv/static/js/app.js b/priv/static/js/app.js
new file mode 100644
index 0000000..57999a4
--- /dev/null
+++ b/priv/static/js/app.js
@@ -0,0 +1,3 @@
+// for phoenix_html support, including form and button helpers
+// copy the following scripts into your javascript bundle:
+// * https://raw.githubusercontent.com/phoenixframework/phoenix_html/v2.10.0/priv/static/phoenix_html.js \ No newline at end of file
diff --git a/priv/static/js/phoenix.js b/priv/static/js/phoenix.js
new file mode 100644
index 0000000..c56e0dd
--- /dev/null
+++ b/priv/static/js/phoenix.js
@@ -0,0 +1,1431 @@
+(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;
+}();
+
+})));
diff --git a/priv/static/js/widget.js b/priv/static/js/widget.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/priv/static/js/widget.js