(function(global, factory) { typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : factory(global.ActionCable = {}); })(this, function(exports) { "use strict"; var adapters = { logger: self.console, WebSocket: self.WebSocket }; var logger = { log: function log() { if (this.enabled) { var _adapters$logger; for (var _len = arguments.length, messages = Array(_len), _key = 0; _key < _len; _key++) { messages[_key] = arguments[_key]; } messages.push(Date.now()); (_adapters$logger = adapters.logger).log.apply(_adapters$logger, [ "[ActionCable]" ].concat(messages)); } } }; 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 classCallCheck = function(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; var now = function now() { return new Date().getTime(); }; var secondsSince = function secondsSince(time) { return (now() - time) / 1e3; }; var clamp = function clamp(number, min, max) { return Math.max(min, Math.min(max, number)); }; var ConnectionMonitor = function() { function ConnectionMonitor(connection) { classCallCheck(this, ConnectionMonitor); this.visibilityDidChange = this.visibilityDidChange.bind(this); this.connection = connection; this.reconnectAttempts = 0; } ConnectionMonitor.prototype.start = function start() { if (!this.isRunning()) { this.startedAt = now(); delete this.stoppedAt; this.startPolling(); addEventListener("visibilitychange", this.visibilityDidChange); logger.log("ConnectionMonitor started. pollInterval = " + this.getPollInterval() + " ms"); } }; ConnectionMonitor.prototype.stop = function stop() { if (this.isRunning()) { this.stoppedAt = now(); this.stopPolling(); removeEventListener("visibilitychange", this.visibilityDidChange); logger.log("ConnectionMonitor stopped"); } }; ConnectionMonitor.prototype.isRunning = function isRunning() { return this.startedAt && !this.stoppedAt; }; ConnectionMonitor.prototype.recordPing = function recordPing() { this.pingedAt = now(); }; ConnectionMonitor.prototype.recordConnect = function recordConnect() { this.reconnectAttempts = 0; this.recordPing(); delete this.disconnectedAt; logger.log("ConnectionMonitor recorded connect"); }; ConnectionMonitor.prototype.recordDisconnect = function recordDisconnect() { this.disconnectedAt = now(); logger.log("ConnectionMonitor recorded disconnect"); }; ConnectionMonitor.prototype.startPolling = function startPolling() { this.stopPolling(); this.poll(); }; ConnectionMonitor.prototype.stopPolling = function stopPolling() { clearTimeout(this.pollTimeout); }; ConnectionMonitor.prototype.poll = function poll() { var _this = this; this.pollTimeout = setTimeout(function() { _this.reconnectIfStale(); _this.poll(); }, this.getPollInterval()); }; ConnectionMonitor.prototype.getPollInterval = function getPollInterval() { var _constructor$pollInte = this.constructor.pollInterval, min = _constructor$pollInte.min, max = _constructor$pollInte.max, multiplier = _constructor$pollInte.multiplier; var interval = multiplier * Math.log(this.reconnectAttempts + 1); return Math.round(clamp(interval, min, max) * 1e3); }; ConnectionMonitor.prototype.reconnectIfStale = function reconnectIfStale() { if (this.connectionIsStale()) { logger.log("ConnectionMonitor detected stale connection. reconnectAttempts = " + this.reconnectAttempts + ", pollInterval = " + this.getPollInterval() + " ms, time disconnected = " + secondsSince(this.disconnectedAt) + " s, stale threshold = " + this.constructor.staleThreshold + " s"); this.reconnectAttempts++; if (this.disconnectedRecently()) { logger.log("ConnectionMonitor skipping reopening recent disconnect"); } else { logger.log("ConnectionMonitor reopening"); this.connection.reopen(); } } }; ConnectionMonitor.prototype.connectionIsStale = function connectionIsStale() { return secondsSince(this.pingedAt ? this.pingedAt : this.startedAt) > this.constructor.staleThreshold; }; ConnectionMonitor.prototype.disconnectedRecently = function disconnectedRecently() { return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold; }; ConnectionMonitor.prototype.visibilityDidChange = function visibilityDidChange() { var _this2 = this; if (document.visibilityState === "visible") { setTimeout(function() { if (_this2.connectionIsStale() || !_this2.connection.isOpen()) { logger.log("ConnectionMonitor reopening stale connection on visibilitychange. visbilityState = " + document.visibilityState); _this2.connection.reopen(); } }, 200); } }; return ConnectionMonitor; }(); ConnectionMonitor.pollInterval = { min: 3, max: 30, multiplier: 5 }; ConnectionMonitor.staleThreshold = 6; var INTERNAL = { message_types: { welcome: "welcome", disconnect: "disconnect", ping: "ping", confirmation: "confirm_subscription", rejection: "reject_subscription" }, disconnect_reasons: { unauthorized: "unauthorized", invalid_request: "invalid_request", server_restart: "server_restart" }, default_mount_path: "/cable", protocols: [ "actioncable-v1-json", "actioncable-unsupported" ] }; var message_types = INTERNAL.message_types, protocols = INTERNAL.protocols; var supportedProtocols = protocols.slice(0, protocols.length - 1); var indexOf = [].indexOf; var Connection = function() { function Connection(consumer) { classCallCheck(this, Connection); this.open = this.open.bind(this); this.consumer = consumer; this.subscriptions = this.consumer.subscriptions; this.monitor = new ConnectionMonitor(this); this.disconnected = true; } Connection.prototype.send = function send(data) { if (this.isOpen()) { this.webSocket.send(JSON.stringify(data)); return true; } else { return false; } }; Connection.prototype.open = function open() { if (this.isActive()) { logger.log("Attempted to open WebSocket, but existing socket is " + this.getState()); return false; } else { logger.log("Opening WebSocket, current state is " + this.getState() + ", subprotocols: " + protocols); if (this.webSocket) { this.uninstallEventHandlers(); } this.webSocket = new adapters.WebSocket(this.consumer.url, protocols); this.installEventHandlers(); this.monitor.start(); return true; } }; Connection.prototype.close = function close() { var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { allowReconnect: true }, allowReconnect = _ref.allowReconnect; if (!allowReconnect) { this.monitor.stop(); } if (this.isActive()) { return this.webSocket.close(); } }; Connection.prototype.reopen = function reopen() { logger.log("Reopening WebSocket, current state is " + this.getState()); if (this.isActive()) { try { return this.close(); } catch (error) { logger.log("Failed to reopen WebSocket", error); } finally { logger.log("Reopening WebSocket in " + this.constructor.reopenDelay + "ms"); setTimeout(this.open, this.constructor.reopenDelay); } } else { return this.open(); } }; Connection.prototype.getProtocol = function getProtocol() { if (this.webSocket) { return this.webSocket.protocol; } }; Connection.prototype.isOpen = function isOpen() { return this.isState("open"); }; Connection.prototype.isActive = function isActive() { return this.isState("open", "connecting"); }; Connection.prototype.isProtocolSupported = function isProtocolSupported() { return indexOf.call(supportedProtocols, this.getProtocol()) >= 0; }; Connection.prototype.isState = function isState() { for (var _len = arguments.length, states = Array(_len), _key = 0; _key < _len; _key++) { states[_key] = arguments[_key]; } return indexOf.call(states, this.getState()) >= 0; }; Connection.prototype.getState = function getState() { if (this.webSocket) { for (var state in adapters.WebSocket) { if (adapters.WebSocket[state] === this.webSocket.readyState) { return state.toLowerCase(); } } } return null; }; Connection.prototype.installEventHandlers = function installEventHandlers() { for (var eventName in this.events) { var handler = this.events[eventName].bind(this); this.webSocket["on" + eventName] = handler; } }; Connection.prototype.uninstallEventHandlers = function uninstallEventHandlers() { for (var eventName in this.events) { this.webSocket["on" + eventName] = function() {}; } }; return Connection; }(); Connection.reopenDelay = 500; Connection.prototype.events = { message: function message(event) { if (!this.isProtocolSupported()) { return; } var _JSON$parse = JSON.parse(event.data), identifier = _JSON$parse.identifier, message = _JSON$parse.message, reason = _JSON$parse.reason, reconnect = _JSON$parse.reconnect, type = _JSON$parse.type; switch (type) { case message_types.welcome: this.monitor.recordConnect(); return this.subscriptions.reload(); case message_types.disconnect: logger.log("Disconnecting. Reason: " + reason); return this.close({ allowReconnect: reconnect }); case message_types.ping: return this.monitor.recordPing(); case message_types.confirmation: return this.subscriptions.notify(identifier, "connected"); case message_types.rejection: return this.subscriptions.reject(identifier); default: return this.subscriptions.notify(identifier, "received", message); } }, open: function open() { logger.log("WebSocket onopen event, using '" + this.getProtocol() + "' subprotocol"); this.disconnected = false; if (!this.isProtocolSupported()) { logger.log("Protocol is unsupported. Stopping monitor and disconnecting."); return this.close({ allowReconnect: false }); } }, close: function close(event) { logger.log("WebSocket onclose event"); if (this.disconnected) { return; } this.disconnected = true; this.monitor.recordDisconnect(); return this.subscriptions.notifyAll("disconnected", { willAttemptReconnect: this.monitor.isRunning() }); }, error: function error() { logger.log("WebSocket onerror event"); } }; var extend = function extend(object, properties) { if (properties != null) { for (var key in properties) { var value = properties[key]; object[key] = value; } } return object; }; var Subscription = function() { function Subscription(consumer) { var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var mixin = arguments[2]; classCallCheck(this, Subscription); this.consumer = consumer; this.identifier = JSON.stringify(params); extend(this, mixin); } Subscription.prototype.perform = function perform(action) { var data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; data.action = action; return this.send(data); }; Subscription.prototype.send = function send(data) { return this.consumer.send({ command: "message", identifier: this.identifier, data: JSON.stringify(data) }); }; Subscription.prototype.unsubscribe = function unsubscribe() { return this.consumer.subscriptions.remove(this); }; return Subscription; }(); var Subscriptions = function() { function Subscriptions(consumer) { classCallCheck(this, Subscriptions); this.consumer = consumer; this.subscriptions = []; } Subscriptions.prototype.create = function create(channelName, mixin) { var channel = channelName; var params = (typeof channel === "undefined" ? "undefined" : _typeof(channel)) === "object" ? channel : { channel: channel }; var subscription = new Subscription(this.consumer, params, mixin); return this.add(subscription); }; Subscriptions.prototype.add = function add(subscription) { this.subscriptions.push(subscription); this.consumer.ensureActiveConnection(); this.notify(subscription, "initialized"); this.sendCommand(subscription, "subscribe"); return subscription; }; Subscriptions.prototype.remove = function remove(subscription) { this.forget(subscription); if (!this.findAll(subscription.identifier).length) { this.sendCommand(subscription, "unsubscribe"); } return subscription; }; Subscriptions.prototype.reject = function reject(identifier) { var _this = this; return this.findAll(identifier).map(function(subscription) { _this.forget(subscription); _this.notify(subscription, "rejected"); return subscription; }); }; Subscriptions.prototype.forget = function forget(subscription) { this.subscriptions = this.subscriptions.filter(function(s) { return s !== subscription; }); return subscription; }; Subscriptions.prototype.findAll = function findAll(identifier) { return this.subscriptions.filter(function(s) { return s.identifier === identifier; }); }; Subscriptions.prototype.reload = function reload() { var _this2 = this; return this.subscriptions.map(function(subscription) { return _this2.sendCommand(subscription, "subscribe"); }); }; Subscriptions.prototype.notifyAll = function notifyAll(callbackName) { var _this3 = this; for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { args[_key - 1] = arguments[_key]; } return this.subscriptions.map(function(subscription) { return _this3.notify.apply(_this3, [ subscription, callbackName ].concat(args)); }); }; Subscriptions.prototype.notify = function notify(subscription, callbackName) { for (var _len2 = arguments.length, args = Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) { args[_key2 - 2] = arguments[_key2]; } var subscriptions = void 0; if (typeof subscription === "string") { subscriptions = this.findAll(subscription); } else { subscriptions = [ subscription ]; } return subscriptions.map(function(subscription) { return typeof subscription[callbackName] === "function" ? subscription[callbackName].apply(subscription, args) : undefined; }); }; Subscriptions.prototype.sendCommand = function sendCommand(subscription, command) { var identifier = subscription.identifier; return this.consumer.send({ command: command, identifier: identifier }); }; return Subscriptions; }(); var Consumer = function() { function Consumer(url) { classCallCheck(this, Consumer); this.url = url; this.subscriptions = new Subscriptions(this); this.connection = new Connection(this); } Consumer.prototype.send = function send(data) { return this.connection.send(data); }; Consumer.prototype.connect = function connect() { return this.connection.open(); }; Consumer.prototype.disconnect = function disconnect() { return this.connection.close({ allowReconnect: false }); }; Consumer.prototype.ensureActiveConnection = function ensureActiveConnection() { if (!this.connection.isActive()) { return this.connection.open(); } }; return Consumer; }(); function createConsumer() { var url = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getConfig("url") || INTERNAL.default_mount_path; return new Consumer(createWebSocketURL(url)); } function getConfig(name) { var element = document.head.querySelector("meta[name='action-cable-" + name + "']"); if (element) { return element.getAttribute("content"); } } function createWebSocketURL(url) { var webSocketURL = typeof url === "function" ? url() : url; if (webSocketURL && !/^wss?:/i.test(webSocketURL)) { var a = document.createElement("a"); a.href = webSocketURL; a.href = a.href; a.protocol = a.protocol.replace("http", "ws"); return a.href; } else { return webSocketURL; } } exports.Connection = Connection; exports.ConnectionMonitor = ConnectionMonitor; exports.Consumer = Consumer; exports.INTERNAL = INTERNAL; exports.Subscription = Subscription; exports.Subscriptions = Subscriptions; exports.adapters = adapters; exports.logger = logger; exports.createConsumer = createConsumer; exports.getConfig = getConfig; exports.createWebSocketURL = createWebSocketURL; Object.defineProperty(exports, "__esModule", { value: true }); });