Output Action Cable JS without transpiling and as ESM (#42856)
* Output Action Cable JS without transpiling and as ESM
* Retain umd version under the old name, generate ESM version + duplicate under new name
* Precompile JavaScripts for direct asset pipeline use
* We've dropped support for IE11
* Include deprecation notice for the old file reference
Thanks @rafaelfranca 👍
* Allow app to opt out of precompiling actioncable js assets
cc @rafaelfranca
* Add changelog entries
This commit is contained in:
parent
58dc8b634b
commit
4a23cb3415
@ -3,7 +3,8 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"semi": ["error", "never"],
|
"semi": ["error", "never"],
|
||||||
"quotes": ["error", "double"],
|
"quotes": ["error", "double"],
|
||||||
"no-unused-vars": ["error", { "vars": "all", "args": "none" }]
|
"no-unused-vars": ["error", { "vars": "all", "args": "none" }],
|
||||||
|
"no-console": "off"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"import"
|
"import"
|
||||||
|
@ -1,3 +1,15 @@
|
|||||||
|
* Compile ESM package that can be used directly in the browser as actioncable.esm.js.
|
||||||
|
|
||||||
|
*DHH*
|
||||||
|
|
||||||
|
* Move action_cable.js to actioncable.js to match naming convention used for other Rails frameworks, and use JS console to communicate the deprecation.
|
||||||
|
|
||||||
|
*DHH*
|
||||||
|
|
||||||
|
* Stop transpiling the UMD package generated as actioncable.js and drop the IE11 testing that relied on that.
|
||||||
|
|
||||||
|
*DHH*
|
||||||
|
|
||||||
* Truncate broadcast logging messages.
|
* Truncate broadcast logging messages.
|
||||||
|
|
||||||
*J Smith*
|
*J Smith*
|
||||||
|
429
actioncable/app/assets/javascripts/action_cable.js
generated
429
actioncable/app/assets/javascripts/action_cable.js
generated
@ -1,153 +1,113 @@
|
|||||||
(function(global, factory) {
|
(function(global, factory) {
|
||||||
typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : factory(global.ActionCable = {});
|
typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
|
||||||
})(this, function(exports) {
|
factory(global.ActionCable = {}));
|
||||||
|
})(this, (function(exports) {
|
||||||
"use strict";
|
"use strict";
|
||||||
var adapters = {
|
var adapters = {
|
||||||
logger: self.console,
|
logger: self.console,
|
||||||
WebSocket: self.WebSocket
|
WebSocket: self.WebSocket
|
||||||
};
|
};
|
||||||
var logger = {
|
var logger = {
|
||||||
log: function log() {
|
log(...messages) {
|
||||||
if (this.enabled) {
|
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());
|
messages.push(Date.now());
|
||||||
(_adapters$logger = adapters.logger).log.apply(_adapters$logger, [ "[ActionCable]" ].concat(messages));
|
adapters.logger.log("[ActionCable]", ...messages);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function(obj) {
|
const now = () => (new Date).getTime();
|
||||||
return typeof obj;
|
const secondsSince = time => (now() - time) / 1e3;
|
||||||
} : function(obj) {
|
class ConnectionMonitor {
|
||||||
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
|
constructor(connection) {
|
||||||
};
|
|
||||||
var classCallCheck = function(instance, Constructor) {
|
|
||||||
if (!(instance instanceof Constructor)) {
|
|
||||||
throw new TypeError("Cannot call a class as a function");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
}();
|
|
||||||
var now = function now() {
|
|
||||||
return new Date().getTime();
|
|
||||||
};
|
|
||||||
var secondsSince = function secondsSince(time) {
|
|
||||||
return (now() - time) / 1e3;
|
|
||||||
};
|
|
||||||
var ConnectionMonitor = function() {
|
|
||||||
function ConnectionMonitor(connection) {
|
|
||||||
classCallCheck(this, ConnectionMonitor);
|
|
||||||
this.visibilityDidChange = this.visibilityDidChange.bind(this);
|
this.visibilityDidChange = this.visibilityDidChange.bind(this);
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
this.reconnectAttempts = 0;
|
this.reconnectAttempts = 0;
|
||||||
}
|
}
|
||||||
ConnectionMonitor.prototype.start = function start() {
|
start() {
|
||||||
if (!this.isRunning()) {
|
if (!this.isRunning()) {
|
||||||
this.startedAt = now();
|
this.startedAt = now();
|
||||||
delete this.stoppedAt;
|
delete this.stoppedAt;
|
||||||
this.startPolling();
|
this.startPolling();
|
||||||
addEventListener("visibilitychange", this.visibilityDidChange);
|
addEventListener("visibilitychange", this.visibilityDidChange);
|
||||||
logger.log("ConnectionMonitor started. stale threshold = " + this.constructor.staleThreshold + " s");
|
logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
ConnectionMonitor.prototype.stop = function stop() {
|
stop() {
|
||||||
if (this.isRunning()) {
|
if (this.isRunning()) {
|
||||||
this.stoppedAt = now();
|
this.stoppedAt = now();
|
||||||
this.stopPolling();
|
this.stopPolling();
|
||||||
removeEventListener("visibilitychange", this.visibilityDidChange);
|
removeEventListener("visibilitychange", this.visibilityDidChange);
|
||||||
logger.log("ConnectionMonitor stopped");
|
logger.log("ConnectionMonitor stopped");
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
ConnectionMonitor.prototype.isRunning = function isRunning() {
|
isRunning() {
|
||||||
return this.startedAt && !this.stoppedAt;
|
return this.startedAt && !this.stoppedAt;
|
||||||
};
|
}
|
||||||
ConnectionMonitor.prototype.recordPing = function recordPing() {
|
recordPing() {
|
||||||
this.pingedAt = now();
|
this.pingedAt = now();
|
||||||
};
|
}
|
||||||
ConnectionMonitor.prototype.recordConnect = function recordConnect() {
|
recordConnect() {
|
||||||
this.reconnectAttempts = 0;
|
this.reconnectAttempts = 0;
|
||||||
this.recordPing();
|
this.recordPing();
|
||||||
delete this.disconnectedAt;
|
delete this.disconnectedAt;
|
||||||
logger.log("ConnectionMonitor recorded connect");
|
logger.log("ConnectionMonitor recorded connect");
|
||||||
};
|
}
|
||||||
ConnectionMonitor.prototype.recordDisconnect = function recordDisconnect() {
|
recordDisconnect() {
|
||||||
this.disconnectedAt = now();
|
this.disconnectedAt = now();
|
||||||
logger.log("ConnectionMonitor recorded disconnect");
|
logger.log("ConnectionMonitor recorded disconnect");
|
||||||
};
|
}
|
||||||
ConnectionMonitor.prototype.startPolling = function startPolling() {
|
startPolling() {
|
||||||
this.stopPolling();
|
this.stopPolling();
|
||||||
this.poll();
|
this.poll();
|
||||||
};
|
}
|
||||||
ConnectionMonitor.prototype.stopPolling = function stopPolling() {
|
stopPolling() {
|
||||||
clearTimeout(this.pollTimeout);
|
clearTimeout(this.pollTimeout);
|
||||||
};
|
}
|
||||||
ConnectionMonitor.prototype.poll = function poll() {
|
poll() {
|
||||||
var _this = this;
|
this.pollTimeout = setTimeout((() => {
|
||||||
this.pollTimeout = setTimeout(function() {
|
this.reconnectIfStale();
|
||||||
_this.reconnectIfStale();
|
this.poll();
|
||||||
_this.poll();
|
}), this.getPollInterval());
|
||||||
}, this.getPollInterval());
|
}
|
||||||
};
|
getPollInterval() {
|
||||||
ConnectionMonitor.prototype.getPollInterval = function getPollInterval() {
|
const {staleThreshold: staleThreshold, reconnectionBackoffRate: reconnectionBackoffRate} = this.constructor;
|
||||||
var _constructor = this.constructor, staleThreshold = _constructor.staleThreshold, reconnectionBackoffRate = _constructor.reconnectionBackoffRate;
|
const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10));
|
||||||
var backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10));
|
const jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate;
|
||||||
var jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate;
|
const jitter = jitterMax * Math.random();
|
||||||
var jitter = jitterMax * Math.random();
|
|
||||||
return staleThreshold * 1e3 * backoff * (1 + jitter);
|
return staleThreshold * 1e3 * backoff * (1 + jitter);
|
||||||
};
|
}
|
||||||
ConnectionMonitor.prototype.reconnectIfStale = function reconnectIfStale() {
|
reconnectIfStale() {
|
||||||
if (this.connectionIsStale()) {
|
if (this.connectionIsStale()) {
|
||||||
logger.log("ConnectionMonitor detected stale connection. reconnectAttempts = " + this.reconnectAttempts + ", time stale = " + secondsSince(this.refreshedAt) + " s, stale threshold = " + this.constructor.staleThreshold + " s");
|
logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`);
|
||||||
this.reconnectAttempts++;
|
this.reconnectAttempts++;
|
||||||
if (this.disconnectedRecently()) {
|
if (this.disconnectedRecently()) {
|
||||||
logger.log("ConnectionMonitor skipping reopening recent disconnect. time disconnected = " + secondsSince(this.disconnectedAt) + " s");
|
logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`);
|
||||||
} else {
|
} else {
|
||||||
logger.log("ConnectionMonitor reopening");
|
logger.log("ConnectionMonitor reopening");
|
||||||
this.connection.reopen();
|
this.connection.reopen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
ConnectionMonitor.prototype.connectionIsStale = function connectionIsStale() {
|
get refreshedAt() {
|
||||||
|
return this.pingedAt ? this.pingedAt : this.startedAt;
|
||||||
|
}
|
||||||
|
connectionIsStale() {
|
||||||
return secondsSince(this.refreshedAt) > this.constructor.staleThreshold;
|
return secondsSince(this.refreshedAt) > this.constructor.staleThreshold;
|
||||||
};
|
}
|
||||||
ConnectionMonitor.prototype.disconnectedRecently = function disconnectedRecently() {
|
disconnectedRecently() {
|
||||||
return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold;
|
return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold;
|
||||||
};
|
}
|
||||||
ConnectionMonitor.prototype.visibilityDidChange = function visibilityDidChange() {
|
visibilityDidChange() {
|
||||||
var _this2 = this;
|
|
||||||
if (document.visibilityState === "visible") {
|
if (document.visibilityState === "visible") {
|
||||||
setTimeout(function() {
|
setTimeout((() => {
|
||||||
if (_this2.connectionIsStale() || !_this2.connection.isOpen()) {
|
if (this.connectionIsStale() || !this.connection.isOpen()) {
|
||||||
logger.log("ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = " + document.visibilityState);
|
logger.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`);
|
||||||
_this2.connection.reopen();
|
this.connection.reopen();
|
||||||
}
|
}
|
||||||
}, 200);
|
}), 200);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
createClass(ConnectionMonitor, [ {
|
}
|
||||||
key: "refreshedAt",
|
|
||||||
get: function get$$1() {
|
|
||||||
return this.pingedAt ? this.pingedAt : this.startedAt;
|
|
||||||
}
|
|
||||||
} ]);
|
|
||||||
return ConnectionMonitor;
|
|
||||||
}();
|
|
||||||
ConnectionMonitor.staleThreshold = 6;
|
ConnectionMonitor.staleThreshold = 6;
|
||||||
ConnectionMonitor.reconnectionBackoffRate = .15;
|
ConnectionMonitor.reconnectionBackoffRate = .15;
|
||||||
var INTERNAL = {
|
var INTERNAL = {
|
||||||
@ -166,32 +126,31 @@
|
|||||||
default_mount_path: "/cable",
|
default_mount_path: "/cable",
|
||||||
protocols: [ "actioncable-v1-json", "actioncable-unsupported" ]
|
protocols: [ "actioncable-v1-json", "actioncable-unsupported" ]
|
||||||
};
|
};
|
||||||
var message_types = INTERNAL.message_types, protocols = INTERNAL.protocols;
|
const {message_types: message_types, protocols: protocols} = INTERNAL;
|
||||||
var supportedProtocols = protocols.slice(0, protocols.length - 1);
|
const supportedProtocols = protocols.slice(0, protocols.length - 1);
|
||||||
var indexOf = [].indexOf;
|
const indexOf = [].indexOf;
|
||||||
var Connection = function() {
|
class Connection {
|
||||||
function Connection(consumer) {
|
constructor(consumer) {
|
||||||
classCallCheck(this, Connection);
|
|
||||||
this.open = this.open.bind(this);
|
this.open = this.open.bind(this);
|
||||||
this.consumer = consumer;
|
this.consumer = consumer;
|
||||||
this.subscriptions = this.consumer.subscriptions;
|
this.subscriptions = this.consumer.subscriptions;
|
||||||
this.monitor = new ConnectionMonitor(this);
|
this.monitor = new ConnectionMonitor(this);
|
||||||
this.disconnected = true;
|
this.disconnected = true;
|
||||||
}
|
}
|
||||||
Connection.prototype.send = function send(data) {
|
send(data) {
|
||||||
if (this.isOpen()) {
|
if (this.isOpen()) {
|
||||||
this.webSocket.send(JSON.stringify(data));
|
this.webSocket.send(JSON.stringify(data));
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
Connection.prototype.open = function open() {
|
open() {
|
||||||
if (this.isActive()) {
|
if (this.isActive()) {
|
||||||
logger.log("Attempted to open WebSocket, but existing socket is " + this.getState());
|
logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`);
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
logger.log("Opening WebSocket, current state is " + this.getState() + ", subprotocols: " + protocols);
|
logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`);
|
||||||
if (this.webSocket) {
|
if (this.webSocket) {
|
||||||
this.uninstallEventHandlers();
|
this.uninstallEventHandlers();
|
||||||
}
|
}
|
||||||
@ -200,90 +159,85 @@
|
|||||||
this.monitor.start();
|
this.monitor.start();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
Connection.prototype.close = function close() {
|
close({allowReconnect: allowReconnect} = {
|
||||||
var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {
|
allowReconnect: true
|
||||||
allowReconnect: true
|
}) {
|
||||||
}, allowReconnect = _ref.allowReconnect;
|
|
||||||
if (!allowReconnect) {
|
if (!allowReconnect) {
|
||||||
this.monitor.stop();
|
this.monitor.stop();
|
||||||
}
|
}
|
||||||
if (this.isActive()) {
|
if (this.isActive()) {
|
||||||
return this.webSocket.close();
|
return this.webSocket.close();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
Connection.prototype.reopen = function reopen() {
|
reopen() {
|
||||||
logger.log("Reopening WebSocket, current state is " + this.getState());
|
logger.log(`Reopening WebSocket, current state is ${this.getState()}`);
|
||||||
if (this.isActive()) {
|
if (this.isActive()) {
|
||||||
try {
|
try {
|
||||||
return this.close();
|
return this.close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Failed to reopen WebSocket", error);
|
logger.log("Failed to reopen WebSocket", error);
|
||||||
} finally {
|
} finally {
|
||||||
logger.log("Reopening WebSocket in " + this.constructor.reopenDelay + "ms");
|
logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`);
|
||||||
setTimeout(this.open, this.constructor.reopenDelay);
|
setTimeout(this.open, this.constructor.reopenDelay);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return this.open();
|
return this.open();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
Connection.prototype.getProtocol = function getProtocol() {
|
getProtocol() {
|
||||||
if (this.webSocket) {
|
if (this.webSocket) {
|
||||||
return this.webSocket.protocol;
|
return this.webSocket.protocol;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
Connection.prototype.isOpen = function isOpen() {
|
isOpen() {
|
||||||
return this.isState("open");
|
return this.isState("open");
|
||||||
};
|
}
|
||||||
Connection.prototype.isActive = function isActive() {
|
isActive() {
|
||||||
return this.isState("open", "connecting");
|
return this.isState("open", "connecting");
|
||||||
};
|
}
|
||||||
Connection.prototype.isProtocolSupported = function isProtocolSupported() {
|
isProtocolSupported() {
|
||||||
return indexOf.call(supportedProtocols, this.getProtocol()) >= 0;
|
return indexOf.call(supportedProtocols, this.getProtocol()) >= 0;
|
||||||
};
|
}
|
||||||
Connection.prototype.isState = function isState() {
|
isState(...states) {
|
||||||
for (var _len = arguments.length, states = Array(_len), _key = 0; _key < _len; _key++) {
|
|
||||||
states[_key] = arguments[_key];
|
|
||||||
}
|
|
||||||
return indexOf.call(states, this.getState()) >= 0;
|
return indexOf.call(states, this.getState()) >= 0;
|
||||||
};
|
}
|
||||||
Connection.prototype.getState = function getState() {
|
getState() {
|
||||||
if (this.webSocket) {
|
if (this.webSocket) {
|
||||||
for (var state in adapters.WebSocket) {
|
for (let state in adapters.WebSocket) {
|
||||||
if (adapters.WebSocket[state] === this.webSocket.readyState) {
|
if (adapters.WebSocket[state] === this.webSocket.readyState) {
|
||||||
return state.toLowerCase();
|
return state.toLowerCase();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
}
|
||||||
Connection.prototype.installEventHandlers = function installEventHandlers() {
|
installEventHandlers() {
|
||||||
for (var eventName in this.events) {
|
for (let eventName in this.events) {
|
||||||
var handler = this.events[eventName].bind(this);
|
const handler = this.events[eventName].bind(this);
|
||||||
this.webSocket["on" + eventName] = handler;
|
this.webSocket[`on${eventName}`] = handler;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
Connection.prototype.uninstallEventHandlers = function uninstallEventHandlers() {
|
uninstallEventHandlers() {
|
||||||
for (var eventName in this.events) {
|
for (let eventName in this.events) {
|
||||||
this.webSocket["on" + eventName] = function() {};
|
this.webSocket[`on${eventName}`] = function() {};
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
return Connection;
|
}
|
||||||
}();
|
|
||||||
Connection.reopenDelay = 500;
|
Connection.reopenDelay = 500;
|
||||||
Connection.prototype.events = {
|
Connection.prototype.events = {
|
||||||
message: function message(event) {
|
message(event) {
|
||||||
if (!this.isProtocolSupported()) {
|
if (!this.isProtocolSupported()) {
|
||||||
return;
|
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;
|
const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case message_types.welcome:
|
case message_types.welcome:
|
||||||
this.monitor.recordConnect();
|
this.monitor.recordConnect();
|
||||||
return this.subscriptions.reload();
|
return this.subscriptions.reload();
|
||||||
|
|
||||||
case message_types.disconnect:
|
case message_types.disconnect:
|
||||||
logger.log("Disconnecting. Reason: " + reason);
|
logger.log(`Disconnecting. Reason: ${reason}`);
|
||||||
return this.close({
|
return this.close({
|
||||||
allowReconnect: reconnect
|
allowReconnect: reconnect
|
||||||
});
|
});
|
||||||
@ -301,8 +255,8 @@
|
|||||||
return this.subscriptions.notify(identifier, "received", message);
|
return this.subscriptions.notify(identifier, "received", message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
open: function open() {
|
open() {
|
||||||
logger.log("WebSocket onopen event, using '" + this.getProtocol() + "' subprotocol");
|
logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`);
|
||||||
this.disconnected = false;
|
this.disconnected = false;
|
||||||
if (!this.isProtocolSupported()) {
|
if (!this.isProtocolSupported()) {
|
||||||
logger.log("Protocol is unsupported. Stopping monitor and disconnecting.");
|
logger.log("Protocol is unsupported. Stopping monitor and disconnecting.");
|
||||||
@ -311,7 +265,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
close: function close(event) {
|
close(event) {
|
||||||
logger.log("WebSocket onclose event");
|
logger.log("WebSocket onclose event");
|
||||||
if (this.disconnected) {
|
if (this.disconnected) {
|
||||||
return;
|
return;
|
||||||
@ -322,167 +276,136 @@
|
|||||||
willAttemptReconnect: this.monitor.isRunning()
|
willAttemptReconnect: this.monitor.isRunning()
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
error: function error() {
|
error() {
|
||||||
logger.log("WebSocket onerror event");
|
logger.log("WebSocket onerror event");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var extend = function extend(object, properties) {
|
const extend = function(object, properties) {
|
||||||
if (properties != null) {
|
if (properties != null) {
|
||||||
for (var key in properties) {
|
for (let key in properties) {
|
||||||
var value = properties[key];
|
const value = properties[key];
|
||||||
object[key] = value;
|
object[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return object;
|
return object;
|
||||||
};
|
};
|
||||||
var Subscription = function() {
|
class Subscription {
|
||||||
function Subscription(consumer) {
|
constructor(consumer, params = {}, mixin) {
|
||||||
var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
||||||
var mixin = arguments[2];
|
|
||||||
classCallCheck(this, Subscription);
|
|
||||||
this.consumer = consumer;
|
this.consumer = consumer;
|
||||||
this.identifier = JSON.stringify(params);
|
this.identifier = JSON.stringify(params);
|
||||||
extend(this, mixin);
|
extend(this, mixin);
|
||||||
}
|
}
|
||||||
Subscription.prototype.perform = function perform(action) {
|
perform(action, data = {}) {
|
||||||
var data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
||||||
data.action = action;
|
data.action = action;
|
||||||
return this.send(data);
|
return this.send(data);
|
||||||
};
|
}
|
||||||
Subscription.prototype.send = function send(data) {
|
send(data) {
|
||||||
return this.consumer.send({
|
return this.consumer.send({
|
||||||
command: "message",
|
command: "message",
|
||||||
identifier: this.identifier,
|
identifier: this.identifier,
|
||||||
data: JSON.stringify(data)
|
data: JSON.stringify(data)
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
Subscription.prototype.unsubscribe = function unsubscribe() {
|
unsubscribe() {
|
||||||
return this.consumer.subscriptions.remove(this);
|
return this.consumer.subscriptions.remove(this);
|
||||||
};
|
}
|
||||||
return Subscription;
|
}
|
||||||
}();
|
class Subscriptions {
|
||||||
var Subscriptions = function() {
|
constructor(consumer) {
|
||||||
function Subscriptions(consumer) {
|
|
||||||
classCallCheck(this, Subscriptions);
|
|
||||||
this.consumer = consumer;
|
this.consumer = consumer;
|
||||||
this.subscriptions = [];
|
this.subscriptions = [];
|
||||||
}
|
}
|
||||||
Subscriptions.prototype.create = function create(channelName, mixin) {
|
create(channelName, mixin) {
|
||||||
var channel = channelName;
|
const channel = channelName;
|
||||||
var params = (typeof channel === "undefined" ? "undefined" : _typeof(channel)) === "object" ? channel : {
|
const params = typeof channel === "object" ? channel : {
|
||||||
channel: channel
|
channel: channel
|
||||||
};
|
};
|
||||||
var subscription = new Subscription(this.consumer, params, mixin);
|
const subscription = new Subscription(this.consumer, params, mixin);
|
||||||
return this.add(subscription);
|
return this.add(subscription);
|
||||||
};
|
}
|
||||||
Subscriptions.prototype.add = function add(subscription) {
|
add(subscription) {
|
||||||
this.subscriptions.push(subscription);
|
this.subscriptions.push(subscription);
|
||||||
this.consumer.ensureActiveConnection();
|
this.consumer.ensureActiveConnection();
|
||||||
this.notify(subscription, "initialized");
|
this.notify(subscription, "initialized");
|
||||||
this.sendCommand(subscription, "subscribe");
|
this.sendCommand(subscription, "subscribe");
|
||||||
return subscription;
|
return subscription;
|
||||||
};
|
}
|
||||||
Subscriptions.prototype.remove = function remove(subscription) {
|
remove(subscription) {
|
||||||
this.forget(subscription);
|
this.forget(subscription);
|
||||||
if (!this.findAll(subscription.identifier).length) {
|
if (!this.findAll(subscription.identifier).length) {
|
||||||
this.sendCommand(subscription, "unsubscribe");
|
this.sendCommand(subscription, "unsubscribe");
|
||||||
}
|
}
|
||||||
return subscription;
|
return subscription;
|
||||||
};
|
}
|
||||||
Subscriptions.prototype.reject = function reject(identifier) {
|
reject(identifier) {
|
||||||
var _this = this;
|
return this.findAll(identifier).map((subscription => {
|
||||||
return this.findAll(identifier).map(function(subscription) {
|
this.forget(subscription);
|
||||||
_this.forget(subscription);
|
this.notify(subscription, "rejected");
|
||||||
_this.notify(subscription, "rejected");
|
|
||||||
return subscription;
|
return subscription;
|
||||||
});
|
}));
|
||||||
};
|
}
|
||||||
Subscriptions.prototype.forget = function forget(subscription) {
|
forget(subscription) {
|
||||||
this.subscriptions = this.subscriptions.filter(function(s) {
|
this.subscriptions = this.subscriptions.filter((s => s !== subscription));
|
||||||
return s !== subscription;
|
|
||||||
});
|
|
||||||
return subscription;
|
return subscription;
|
||||||
};
|
}
|
||||||
Subscriptions.prototype.findAll = function findAll(identifier) {
|
findAll(identifier) {
|
||||||
return this.subscriptions.filter(function(s) {
|
return this.subscriptions.filter((s => s.identifier === identifier));
|
||||||
return s.identifier === identifier;
|
}
|
||||||
});
|
reload() {
|
||||||
};
|
return this.subscriptions.map((subscription => this.sendCommand(subscription, "subscribe")));
|
||||||
Subscriptions.prototype.reload = function reload() {
|
}
|
||||||
var _this2 = this;
|
notifyAll(callbackName, ...args) {
|
||||||
return this.subscriptions.map(function(subscription) {
|
return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args)));
|
||||||
return _this2.sendCommand(subscription, "subscribe");
|
}
|
||||||
});
|
notify(subscription, callbackName, ...args) {
|
||||||
};
|
let subscriptions;
|
||||||
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") {
|
if (typeof subscription === "string") {
|
||||||
subscriptions = this.findAll(subscription);
|
subscriptions = this.findAll(subscription);
|
||||||
} else {
|
} else {
|
||||||
subscriptions = [ subscription ];
|
subscriptions = [ subscription ];
|
||||||
}
|
}
|
||||||
return subscriptions.map(function(subscription) {
|
return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined));
|
||||||
return typeof subscription[callbackName] === "function" ? subscription[callbackName].apply(subscription, args) : undefined;
|
}
|
||||||
});
|
sendCommand(subscription, command) {
|
||||||
};
|
const {identifier: identifier} = subscription;
|
||||||
Subscriptions.prototype.sendCommand = function sendCommand(subscription, command) {
|
|
||||||
var identifier = subscription.identifier;
|
|
||||||
return this.consumer.send({
|
return this.consumer.send({
|
||||||
command: command,
|
command: command,
|
||||||
identifier: identifier
|
identifier: identifier
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
return Subscriptions;
|
}
|
||||||
}();
|
class Consumer {
|
||||||
var Consumer = function() {
|
constructor(url) {
|
||||||
function Consumer(url) {
|
|
||||||
classCallCheck(this, Consumer);
|
|
||||||
this._url = url;
|
this._url = url;
|
||||||
this.subscriptions = new Subscriptions(this);
|
this.subscriptions = new Subscriptions(this);
|
||||||
this.connection = new Connection(this);
|
this.connection = new Connection(this);
|
||||||
}
|
}
|
||||||
Consumer.prototype.send = function send(data) {
|
get url() {
|
||||||
|
return createWebSocketURL(this._url);
|
||||||
|
}
|
||||||
|
send(data) {
|
||||||
return this.connection.send(data);
|
return this.connection.send(data);
|
||||||
};
|
}
|
||||||
Consumer.prototype.connect = function connect() {
|
connect() {
|
||||||
return this.connection.open();
|
return this.connection.open();
|
||||||
};
|
}
|
||||||
Consumer.prototype.disconnect = function disconnect() {
|
disconnect() {
|
||||||
return this.connection.close({
|
return this.connection.close({
|
||||||
allowReconnect: false
|
allowReconnect: false
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
Consumer.prototype.ensureActiveConnection = function ensureActiveConnection() {
|
ensureActiveConnection() {
|
||||||
if (!this.connection.isActive()) {
|
if (!this.connection.isActive()) {
|
||||||
return this.connection.open();
|
return this.connection.open();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
createClass(Consumer, [ {
|
}
|
||||||
key: "url",
|
|
||||||
get: function get$$1() {
|
|
||||||
return createWebSocketURL(this._url);
|
|
||||||
}
|
|
||||||
} ]);
|
|
||||||
return Consumer;
|
|
||||||
}();
|
|
||||||
function createWebSocketURL(url) {
|
function createWebSocketURL(url) {
|
||||||
if (typeof url === "function") {
|
if (typeof url === "function") {
|
||||||
url = url();
|
url = url();
|
||||||
}
|
}
|
||||||
if (url && !/^wss?:/i.test(url)) {
|
if (url && !/^wss?:/i.test(url)) {
|
||||||
var a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.href = a.href;
|
a.href = a.href;
|
||||||
a.protocol = a.protocol.replace("http", "ws");
|
a.protocol = a.protocol.replace("http", "ws");
|
||||||
@ -491,16 +414,16 @@
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function createConsumer() {
|
function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
|
||||||
var url = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getConfig("url") || INTERNAL.default_mount_path;
|
|
||||||
return new Consumer(url);
|
return new Consumer(url);
|
||||||
}
|
}
|
||||||
function getConfig(name) {
|
function getConfig(name) {
|
||||||
var element = document.head.querySelector("meta[name='action-cable-" + name + "']");
|
const element = document.head.querySelector(`meta[name='action-cable-${name}']`);
|
||||||
if (element) {
|
if (element) {
|
||||||
return element.getAttribute("content");
|
return element.getAttribute("content");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.log("DEPRECATION: action_cable.js has been renamed to actioncable.js – please update your reference before Rails 8");
|
||||||
exports.Connection = Connection;
|
exports.Connection = Connection;
|
||||||
exports.ConnectionMonitor = ConnectionMonitor;
|
exports.ConnectionMonitor = ConnectionMonitor;
|
||||||
exports.Consumer = Consumer;
|
exports.Consumer = Consumer;
|
||||||
@ -508,11 +431,11 @@
|
|||||||
exports.Subscription = Subscription;
|
exports.Subscription = Subscription;
|
||||||
exports.Subscriptions = Subscriptions;
|
exports.Subscriptions = Subscriptions;
|
||||||
exports.adapters = adapters;
|
exports.adapters = adapters;
|
||||||
exports.createWebSocketURL = createWebSocketURL;
|
|
||||||
exports.logger = logger;
|
|
||||||
exports.createConsumer = createConsumer;
|
exports.createConsumer = createConsumer;
|
||||||
|
exports.createWebSocketURL = createWebSocketURL;
|
||||||
exports.getConfig = getConfig;
|
exports.getConfig = getConfig;
|
||||||
|
exports.logger = logger;
|
||||||
Object.defineProperty(exports, "__esModule", {
|
Object.defineProperty(exports, "__esModule", {
|
||||||
value: true
|
value: true
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
442
actioncable/app/assets/javascripts/actioncable.esm.js
Normal file
442
actioncable/app/assets/javascripts/actioncable.esm.js
Normal file
@ -0,0 +1,442 @@
|
|||||||
|
var adapters = {
|
||||||
|
logger: self.console,
|
||||||
|
WebSocket: self.WebSocket
|
||||||
|
};
|
||||||
|
|
||||||
|
var logger = {
|
||||||
|
log(...messages) {
|
||||||
|
if (this.enabled) {
|
||||||
|
messages.push(Date.now());
|
||||||
|
adapters.logger.log("[ActionCable]", ...messages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const now = () => (new Date).getTime();
|
||||||
|
|
||||||
|
const secondsSince = time => (now() - time) / 1e3;
|
||||||
|
|
||||||
|
class ConnectionMonitor {
|
||||||
|
constructor(connection) {
|
||||||
|
this.visibilityDidChange = this.visibilityDidChange.bind(this);
|
||||||
|
this.connection = connection;
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
}
|
||||||
|
start() {
|
||||||
|
if (!this.isRunning()) {
|
||||||
|
this.startedAt = now();
|
||||||
|
delete this.stoppedAt;
|
||||||
|
this.startPolling();
|
||||||
|
addEventListener("visibilitychange", this.visibilityDidChange);
|
||||||
|
logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
if (this.isRunning()) {
|
||||||
|
this.stoppedAt = now();
|
||||||
|
this.stopPolling();
|
||||||
|
removeEventListener("visibilitychange", this.visibilityDidChange);
|
||||||
|
logger.log("ConnectionMonitor stopped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isRunning() {
|
||||||
|
return this.startedAt && !this.stoppedAt;
|
||||||
|
}
|
||||||
|
recordPing() {
|
||||||
|
this.pingedAt = now();
|
||||||
|
}
|
||||||
|
recordConnect() {
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
this.recordPing();
|
||||||
|
delete this.disconnectedAt;
|
||||||
|
logger.log("ConnectionMonitor recorded connect");
|
||||||
|
}
|
||||||
|
recordDisconnect() {
|
||||||
|
this.disconnectedAt = now();
|
||||||
|
logger.log("ConnectionMonitor recorded disconnect");
|
||||||
|
}
|
||||||
|
startPolling() {
|
||||||
|
this.stopPolling();
|
||||||
|
this.poll();
|
||||||
|
}
|
||||||
|
stopPolling() {
|
||||||
|
clearTimeout(this.pollTimeout);
|
||||||
|
}
|
||||||
|
poll() {
|
||||||
|
this.pollTimeout = setTimeout((() => {
|
||||||
|
this.reconnectIfStale();
|
||||||
|
this.poll();
|
||||||
|
}), this.getPollInterval());
|
||||||
|
}
|
||||||
|
getPollInterval() {
|
||||||
|
const {staleThreshold: staleThreshold, reconnectionBackoffRate: reconnectionBackoffRate} = this.constructor;
|
||||||
|
const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10));
|
||||||
|
const jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate;
|
||||||
|
const jitter = jitterMax * Math.random();
|
||||||
|
return staleThreshold * 1e3 * backoff * (1 + jitter);
|
||||||
|
}
|
||||||
|
reconnectIfStale() {
|
||||||
|
if (this.connectionIsStale()) {
|
||||||
|
logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`);
|
||||||
|
this.reconnectAttempts++;
|
||||||
|
if (this.disconnectedRecently()) {
|
||||||
|
logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`);
|
||||||
|
} else {
|
||||||
|
logger.log("ConnectionMonitor reopening");
|
||||||
|
this.connection.reopen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get refreshedAt() {
|
||||||
|
return this.pingedAt ? this.pingedAt : this.startedAt;
|
||||||
|
}
|
||||||
|
connectionIsStale() {
|
||||||
|
return secondsSince(this.refreshedAt) > this.constructor.staleThreshold;
|
||||||
|
}
|
||||||
|
disconnectedRecently() {
|
||||||
|
return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold;
|
||||||
|
}
|
||||||
|
visibilityDidChange() {
|
||||||
|
if (document.visibilityState === "visible") {
|
||||||
|
setTimeout((() => {
|
||||||
|
if (this.connectionIsStale() || !this.connection.isOpen()) {
|
||||||
|
logger.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`);
|
||||||
|
this.connection.reopen();
|
||||||
|
}
|
||||||
|
}), 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectionMonitor.staleThreshold = 6;
|
||||||
|
|
||||||
|
ConnectionMonitor.reconnectionBackoffRate = .15;
|
||||||
|
|
||||||
|
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" ]
|
||||||
|
};
|
||||||
|
|
||||||
|
const {message_types: message_types, protocols: protocols} = INTERNAL;
|
||||||
|
|
||||||
|
const supportedProtocols = protocols.slice(0, protocols.length - 1);
|
||||||
|
|
||||||
|
const indexOf = [].indexOf;
|
||||||
|
|
||||||
|
class Connection {
|
||||||
|
constructor(consumer) {
|
||||||
|
this.open = this.open.bind(this);
|
||||||
|
this.consumer = consumer;
|
||||||
|
this.subscriptions = this.consumer.subscriptions;
|
||||||
|
this.monitor = new ConnectionMonitor(this);
|
||||||
|
this.disconnected = true;
|
||||||
|
}
|
||||||
|
send(data) {
|
||||||
|
if (this.isOpen()) {
|
||||||
|
this.webSocket.send(JSON.stringify(data));
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close({allowReconnect: allowReconnect} = {
|
||||||
|
allowReconnect: true
|
||||||
|
}) {
|
||||||
|
if (!allowReconnect) {
|
||||||
|
this.monitor.stop();
|
||||||
|
}
|
||||||
|
if (this.isActive()) {
|
||||||
|
return this.webSocket.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getProtocol() {
|
||||||
|
if (this.webSocket) {
|
||||||
|
return this.webSocket.protocol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isOpen() {
|
||||||
|
return this.isState("open");
|
||||||
|
}
|
||||||
|
isActive() {
|
||||||
|
return this.isState("open", "connecting");
|
||||||
|
}
|
||||||
|
isProtocolSupported() {
|
||||||
|
return indexOf.call(supportedProtocols, this.getProtocol()) >= 0;
|
||||||
|
}
|
||||||
|
isState(...states) {
|
||||||
|
return indexOf.call(states, this.getState()) >= 0;
|
||||||
|
}
|
||||||
|
getState() {
|
||||||
|
if (this.webSocket) {
|
||||||
|
for (let state in adapters.WebSocket) {
|
||||||
|
if (adapters.WebSocket[state] === this.webSocket.readyState) {
|
||||||
|
return state.toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
installEventHandlers() {
|
||||||
|
for (let eventName in this.events) {
|
||||||
|
const handler = this.events[eventName].bind(this);
|
||||||
|
this.webSocket[`on${eventName}`] = handler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uninstallEventHandlers() {
|
||||||
|
for (let eventName in this.events) {
|
||||||
|
this.webSocket[`on${eventName}`] = function() {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connection.reopenDelay = 500;
|
||||||
|
|
||||||
|
Connection.prototype.events = {
|
||||||
|
message(event) {
|
||||||
|
if (!this.isProtocolSupported()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data);
|
||||||
|
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() {
|
||||||
|
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(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() {
|
||||||
|
logger.log("WebSocket onerror event");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const extend = function(object, properties) {
|
||||||
|
if (properties != null) {
|
||||||
|
for (let key in properties) {
|
||||||
|
const value = properties[key];
|
||||||
|
object[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return object;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Subscription {
|
||||||
|
constructor(consumer, params = {}, mixin) {
|
||||||
|
this.consumer = consumer;
|
||||||
|
this.identifier = JSON.stringify(params);
|
||||||
|
extend(this, mixin);
|
||||||
|
}
|
||||||
|
perform(action, data = {}) {
|
||||||
|
data.action = action;
|
||||||
|
return this.send(data);
|
||||||
|
}
|
||||||
|
send(data) {
|
||||||
|
return this.consumer.send({
|
||||||
|
command: "message",
|
||||||
|
identifier: this.identifier,
|
||||||
|
data: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
unsubscribe() {
|
||||||
|
return this.consumer.subscriptions.remove(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Subscriptions {
|
||||||
|
constructor(consumer) {
|
||||||
|
this.consumer = consumer;
|
||||||
|
this.subscriptions = [];
|
||||||
|
}
|
||||||
|
create(channelName, mixin) {
|
||||||
|
const channel = channelName;
|
||||||
|
const params = typeof channel === "object" ? channel : {
|
||||||
|
channel: channel
|
||||||
|
};
|
||||||
|
const subscription = new Subscription(this.consumer, params, mixin);
|
||||||
|
return this.add(subscription);
|
||||||
|
}
|
||||||
|
add(subscription) {
|
||||||
|
this.subscriptions.push(subscription);
|
||||||
|
this.consumer.ensureActiveConnection();
|
||||||
|
this.notify(subscription, "initialized");
|
||||||
|
this.sendCommand(subscription, "subscribe");
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
remove(subscription) {
|
||||||
|
this.forget(subscription);
|
||||||
|
if (!this.findAll(subscription.identifier).length) {
|
||||||
|
this.sendCommand(subscription, "unsubscribe");
|
||||||
|
}
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
reject(identifier) {
|
||||||
|
return this.findAll(identifier).map((subscription => {
|
||||||
|
this.forget(subscription);
|
||||||
|
this.notify(subscription, "rejected");
|
||||||
|
return subscription;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
forget(subscription) {
|
||||||
|
this.subscriptions = this.subscriptions.filter((s => s !== subscription));
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
findAll(identifier) {
|
||||||
|
return this.subscriptions.filter((s => s.identifier === identifier));
|
||||||
|
}
|
||||||
|
reload() {
|
||||||
|
return this.subscriptions.map((subscription => this.sendCommand(subscription, "subscribe")));
|
||||||
|
}
|
||||||
|
notifyAll(callbackName, ...args) {
|
||||||
|
return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args)));
|
||||||
|
}
|
||||||
|
notify(subscription, callbackName, ...args) {
|
||||||
|
let subscriptions;
|
||||||
|
if (typeof subscription === "string") {
|
||||||
|
subscriptions = this.findAll(subscription);
|
||||||
|
} else {
|
||||||
|
subscriptions = [ subscription ];
|
||||||
|
}
|
||||||
|
return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined));
|
||||||
|
}
|
||||||
|
sendCommand(subscription, command) {
|
||||||
|
const {identifier: identifier} = subscription;
|
||||||
|
return this.consumer.send({
|
||||||
|
command: command,
|
||||||
|
identifier: identifier
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Consumer {
|
||||||
|
constructor(url) {
|
||||||
|
this._url = url;
|
||||||
|
this.subscriptions = new Subscriptions(this);
|
||||||
|
this.connection = new Connection(this);
|
||||||
|
}
|
||||||
|
get url() {
|
||||||
|
return createWebSocketURL(this._url);
|
||||||
|
}
|
||||||
|
send(data) {
|
||||||
|
return this.connection.send(data);
|
||||||
|
}
|
||||||
|
connect() {
|
||||||
|
return this.connection.open();
|
||||||
|
}
|
||||||
|
disconnect() {
|
||||||
|
return this.connection.close({
|
||||||
|
allowReconnect: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ensureActiveConnection() {
|
||||||
|
if (!this.connection.isActive()) {
|
||||||
|
return this.connection.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWebSocketURL(url) {
|
||||||
|
if (typeof url === "function") {
|
||||||
|
url = url();
|
||||||
|
}
|
||||||
|
if (url && !/^wss?:/i.test(url)) {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.href = a.href;
|
||||||
|
a.protocol = a.protocol.replace("http", "ws");
|
||||||
|
return a.href;
|
||||||
|
} else {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
|
||||||
|
return new Consumer(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfig(name) {
|
||||||
|
const element = document.head.querySelector(`meta[name='action-cable-${name}']`);
|
||||||
|
if (element) {
|
||||||
|
return element.getAttribute("content");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Connection, ConnectionMonitor, Consumer, INTERNAL, Subscription, Subscriptions, adapters, createConsumer, createWebSocketURL, getConfig, logger };
|
440
actioncable/app/assets/javascripts/actioncable.js
Normal file
440
actioncable/app/assets/javascripts/actioncable.js
Normal file
@ -0,0 +1,440 @@
|
|||||||
|
(function(global, factory) {
|
||||||
|
typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
|
||||||
|
factory(global.ActionCable = {}));
|
||||||
|
})(this, (function(exports) {
|
||||||
|
"use strict";
|
||||||
|
var adapters = {
|
||||||
|
logger: self.console,
|
||||||
|
WebSocket: self.WebSocket
|
||||||
|
};
|
||||||
|
var logger = {
|
||||||
|
log(...messages) {
|
||||||
|
if (this.enabled) {
|
||||||
|
messages.push(Date.now());
|
||||||
|
adapters.logger.log("[ActionCable]", ...messages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const now = () => (new Date).getTime();
|
||||||
|
const secondsSince = time => (now() - time) / 1e3;
|
||||||
|
class ConnectionMonitor {
|
||||||
|
constructor(connection) {
|
||||||
|
this.visibilityDidChange = this.visibilityDidChange.bind(this);
|
||||||
|
this.connection = connection;
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
}
|
||||||
|
start() {
|
||||||
|
if (!this.isRunning()) {
|
||||||
|
this.startedAt = now();
|
||||||
|
delete this.stoppedAt;
|
||||||
|
this.startPolling();
|
||||||
|
addEventListener("visibilitychange", this.visibilityDidChange);
|
||||||
|
logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
if (this.isRunning()) {
|
||||||
|
this.stoppedAt = now();
|
||||||
|
this.stopPolling();
|
||||||
|
removeEventListener("visibilitychange", this.visibilityDidChange);
|
||||||
|
logger.log("ConnectionMonitor stopped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isRunning() {
|
||||||
|
return this.startedAt && !this.stoppedAt;
|
||||||
|
}
|
||||||
|
recordPing() {
|
||||||
|
this.pingedAt = now();
|
||||||
|
}
|
||||||
|
recordConnect() {
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
this.recordPing();
|
||||||
|
delete this.disconnectedAt;
|
||||||
|
logger.log("ConnectionMonitor recorded connect");
|
||||||
|
}
|
||||||
|
recordDisconnect() {
|
||||||
|
this.disconnectedAt = now();
|
||||||
|
logger.log("ConnectionMonitor recorded disconnect");
|
||||||
|
}
|
||||||
|
startPolling() {
|
||||||
|
this.stopPolling();
|
||||||
|
this.poll();
|
||||||
|
}
|
||||||
|
stopPolling() {
|
||||||
|
clearTimeout(this.pollTimeout);
|
||||||
|
}
|
||||||
|
poll() {
|
||||||
|
this.pollTimeout = setTimeout((() => {
|
||||||
|
this.reconnectIfStale();
|
||||||
|
this.poll();
|
||||||
|
}), this.getPollInterval());
|
||||||
|
}
|
||||||
|
getPollInterval() {
|
||||||
|
const {staleThreshold: staleThreshold, reconnectionBackoffRate: reconnectionBackoffRate} = this.constructor;
|
||||||
|
const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10));
|
||||||
|
const jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate;
|
||||||
|
const jitter = jitterMax * Math.random();
|
||||||
|
return staleThreshold * 1e3 * backoff * (1 + jitter);
|
||||||
|
}
|
||||||
|
reconnectIfStale() {
|
||||||
|
if (this.connectionIsStale()) {
|
||||||
|
logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`);
|
||||||
|
this.reconnectAttempts++;
|
||||||
|
if (this.disconnectedRecently()) {
|
||||||
|
logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`);
|
||||||
|
} else {
|
||||||
|
logger.log("ConnectionMonitor reopening");
|
||||||
|
this.connection.reopen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get refreshedAt() {
|
||||||
|
return this.pingedAt ? this.pingedAt : this.startedAt;
|
||||||
|
}
|
||||||
|
connectionIsStale() {
|
||||||
|
return secondsSince(this.refreshedAt) > this.constructor.staleThreshold;
|
||||||
|
}
|
||||||
|
disconnectedRecently() {
|
||||||
|
return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold;
|
||||||
|
}
|
||||||
|
visibilityDidChange() {
|
||||||
|
if (document.visibilityState === "visible") {
|
||||||
|
setTimeout((() => {
|
||||||
|
if (this.connectionIsStale() || !this.connection.isOpen()) {
|
||||||
|
logger.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`);
|
||||||
|
this.connection.reopen();
|
||||||
|
}
|
||||||
|
}), 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ConnectionMonitor.staleThreshold = 6;
|
||||||
|
ConnectionMonitor.reconnectionBackoffRate = .15;
|
||||||
|
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" ]
|
||||||
|
};
|
||||||
|
const {message_types: message_types, protocols: protocols} = INTERNAL;
|
||||||
|
const supportedProtocols = protocols.slice(0, protocols.length - 1);
|
||||||
|
const indexOf = [].indexOf;
|
||||||
|
class Connection {
|
||||||
|
constructor(consumer) {
|
||||||
|
this.open = this.open.bind(this);
|
||||||
|
this.consumer = consumer;
|
||||||
|
this.subscriptions = this.consumer.subscriptions;
|
||||||
|
this.monitor = new ConnectionMonitor(this);
|
||||||
|
this.disconnected = true;
|
||||||
|
}
|
||||||
|
send(data) {
|
||||||
|
if (this.isOpen()) {
|
||||||
|
this.webSocket.send(JSON.stringify(data));
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close({allowReconnect: allowReconnect} = {
|
||||||
|
allowReconnect: true
|
||||||
|
}) {
|
||||||
|
if (!allowReconnect) {
|
||||||
|
this.monitor.stop();
|
||||||
|
}
|
||||||
|
if (this.isActive()) {
|
||||||
|
return this.webSocket.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getProtocol() {
|
||||||
|
if (this.webSocket) {
|
||||||
|
return this.webSocket.protocol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isOpen() {
|
||||||
|
return this.isState("open");
|
||||||
|
}
|
||||||
|
isActive() {
|
||||||
|
return this.isState("open", "connecting");
|
||||||
|
}
|
||||||
|
isProtocolSupported() {
|
||||||
|
return indexOf.call(supportedProtocols, this.getProtocol()) >= 0;
|
||||||
|
}
|
||||||
|
isState(...states) {
|
||||||
|
return indexOf.call(states, this.getState()) >= 0;
|
||||||
|
}
|
||||||
|
getState() {
|
||||||
|
if (this.webSocket) {
|
||||||
|
for (let state in adapters.WebSocket) {
|
||||||
|
if (adapters.WebSocket[state] === this.webSocket.readyState) {
|
||||||
|
return state.toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
installEventHandlers() {
|
||||||
|
for (let eventName in this.events) {
|
||||||
|
const handler = this.events[eventName].bind(this);
|
||||||
|
this.webSocket[`on${eventName}`] = handler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uninstallEventHandlers() {
|
||||||
|
for (let eventName in this.events) {
|
||||||
|
this.webSocket[`on${eventName}`] = function() {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Connection.reopenDelay = 500;
|
||||||
|
Connection.prototype.events = {
|
||||||
|
message(event) {
|
||||||
|
if (!this.isProtocolSupported()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data);
|
||||||
|
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() {
|
||||||
|
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(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() {
|
||||||
|
logger.log("WebSocket onerror event");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const extend = function(object, properties) {
|
||||||
|
if (properties != null) {
|
||||||
|
for (let key in properties) {
|
||||||
|
const value = properties[key];
|
||||||
|
object[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return object;
|
||||||
|
};
|
||||||
|
class Subscription {
|
||||||
|
constructor(consumer, params = {}, mixin) {
|
||||||
|
this.consumer = consumer;
|
||||||
|
this.identifier = JSON.stringify(params);
|
||||||
|
extend(this, mixin);
|
||||||
|
}
|
||||||
|
perform(action, data = {}) {
|
||||||
|
data.action = action;
|
||||||
|
return this.send(data);
|
||||||
|
}
|
||||||
|
send(data) {
|
||||||
|
return this.consumer.send({
|
||||||
|
command: "message",
|
||||||
|
identifier: this.identifier,
|
||||||
|
data: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
unsubscribe() {
|
||||||
|
return this.consumer.subscriptions.remove(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Subscriptions {
|
||||||
|
constructor(consumer) {
|
||||||
|
this.consumer = consumer;
|
||||||
|
this.subscriptions = [];
|
||||||
|
}
|
||||||
|
create(channelName, mixin) {
|
||||||
|
const channel = channelName;
|
||||||
|
const params = typeof channel === "object" ? channel : {
|
||||||
|
channel: channel
|
||||||
|
};
|
||||||
|
const subscription = new Subscription(this.consumer, params, mixin);
|
||||||
|
return this.add(subscription);
|
||||||
|
}
|
||||||
|
add(subscription) {
|
||||||
|
this.subscriptions.push(subscription);
|
||||||
|
this.consumer.ensureActiveConnection();
|
||||||
|
this.notify(subscription, "initialized");
|
||||||
|
this.sendCommand(subscription, "subscribe");
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
remove(subscription) {
|
||||||
|
this.forget(subscription);
|
||||||
|
if (!this.findAll(subscription.identifier).length) {
|
||||||
|
this.sendCommand(subscription, "unsubscribe");
|
||||||
|
}
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
reject(identifier) {
|
||||||
|
return this.findAll(identifier).map((subscription => {
|
||||||
|
this.forget(subscription);
|
||||||
|
this.notify(subscription, "rejected");
|
||||||
|
return subscription;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
forget(subscription) {
|
||||||
|
this.subscriptions = this.subscriptions.filter((s => s !== subscription));
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
findAll(identifier) {
|
||||||
|
return this.subscriptions.filter((s => s.identifier === identifier));
|
||||||
|
}
|
||||||
|
reload() {
|
||||||
|
return this.subscriptions.map((subscription => this.sendCommand(subscription, "subscribe")));
|
||||||
|
}
|
||||||
|
notifyAll(callbackName, ...args) {
|
||||||
|
return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args)));
|
||||||
|
}
|
||||||
|
notify(subscription, callbackName, ...args) {
|
||||||
|
let subscriptions;
|
||||||
|
if (typeof subscription === "string") {
|
||||||
|
subscriptions = this.findAll(subscription);
|
||||||
|
} else {
|
||||||
|
subscriptions = [ subscription ];
|
||||||
|
}
|
||||||
|
return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined));
|
||||||
|
}
|
||||||
|
sendCommand(subscription, command) {
|
||||||
|
const {identifier: identifier} = subscription;
|
||||||
|
return this.consumer.send({
|
||||||
|
command: command,
|
||||||
|
identifier: identifier
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Consumer {
|
||||||
|
constructor(url) {
|
||||||
|
this._url = url;
|
||||||
|
this.subscriptions = new Subscriptions(this);
|
||||||
|
this.connection = new Connection(this);
|
||||||
|
}
|
||||||
|
get url() {
|
||||||
|
return createWebSocketURL(this._url);
|
||||||
|
}
|
||||||
|
send(data) {
|
||||||
|
return this.connection.send(data);
|
||||||
|
}
|
||||||
|
connect() {
|
||||||
|
return this.connection.open();
|
||||||
|
}
|
||||||
|
disconnect() {
|
||||||
|
return this.connection.close({
|
||||||
|
allowReconnect: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ensureActiveConnection() {
|
||||||
|
if (!this.connection.isActive()) {
|
||||||
|
return this.connection.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function createWebSocketURL(url) {
|
||||||
|
if (typeof url === "function") {
|
||||||
|
url = url();
|
||||||
|
}
|
||||||
|
if (url && !/^wss?:/i.test(url)) {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.href = a.href;
|
||||||
|
a.protocol = a.protocol.replace("http", "ws");
|
||||||
|
return a.href;
|
||||||
|
} else {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
|
||||||
|
return new Consumer(url);
|
||||||
|
}
|
||||||
|
function getConfig(name) {
|
||||||
|
const element = document.head.querySelector(`meta[name='action-cable-${name}']`);
|
||||||
|
if (element) {
|
||||||
|
return element.getAttribute("content");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.Connection = Connection;
|
||||||
|
exports.ConnectionMonitor = ConnectionMonitor;
|
||||||
|
exports.Consumer = Consumer;
|
||||||
|
exports.INTERNAL = INTERNAL;
|
||||||
|
exports.Subscription = Subscription;
|
||||||
|
exports.Subscriptions = Subscriptions;
|
||||||
|
exports.adapters = adapters;
|
||||||
|
exports.createConsumer = createConsumer;
|
||||||
|
exports.createWebSocketURL = createWebSocketURL;
|
||||||
|
exports.getConfig = getConfig;
|
||||||
|
exports.logger = logger;
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
}));
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./index"
|
||||||
|
console.log("DEPRECATION: action_cable.js has been renamed to actioncable.js – please update your reference before Rails 8")
|
@ -27,7 +27,6 @@ if (process.env.CI) {
|
|||||||
sl_ff: sauce("firefox", 63),
|
sl_ff: sauce("firefox", 63),
|
||||||
sl_safari: sauce("safari", 12.0, "macOS 10.13"),
|
sl_safari: sauce("safari", 12.0, "macOS 10.13"),
|
||||||
sl_edge: sauce("microsoftedge", 17.17134, "Windows 10"),
|
sl_edge: sauce("microsoftedge", 17.17134, "Windows 10"),
|
||||||
sl_ie_11: sauce("internet explorer", 11, "Windows 8.1"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
config.browsers = Object.keys(config.customLaunchers)
|
config.browsers = Object.keys(config.customLaunchers)
|
||||||
|
@ -9,6 +9,7 @@ module ActionCable
|
|||||||
class Engine < Rails::Engine # :nodoc:
|
class Engine < Rails::Engine # :nodoc:
|
||||||
config.action_cable = ActiveSupport::OrderedOptions.new
|
config.action_cable = ActiveSupport::OrderedOptions.new
|
||||||
config.action_cable.mount_path = ActionCable::INTERNAL[:default_mount_path]
|
config.action_cable.mount_path = ActionCable::INTERNAL[:default_mount_path]
|
||||||
|
config.action_cable.precompile_assets = true
|
||||||
|
|
||||||
config.eager_load_namespaces << ActionCable
|
config.eager_load_namespaces << ActionCable
|
||||||
|
|
||||||
@ -22,6 +23,14 @@ class Engine < Rails::Engine # :nodoc:
|
|||||||
ActiveSupport.on_load(:action_cable) { self.logger ||= ::Rails.logger }
|
ActiveSupport.on_load(:action_cable) { self.logger ||= ::Rails.logger }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
initializer "action_cable.asset" do
|
||||||
|
config.after_initialize do |app|
|
||||||
|
if Rails.application.config.respond_to?(:assets) && app.config.action_cable.precompile_assets
|
||||||
|
Rails.application.config.assets.precompile += %w( actioncable.js actioncable.esm.js )
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
initializer "action_cable.set_configs" do |app|
|
initializer "action_cable.set_configs" do |app|
|
||||||
options = app.config.action_cable
|
options = app.config.action_cable
|
||||||
options.allowed_request_origins ||= /https?:\/\/localhost:\d+/ if ::Rails.env.development?
|
options.allowed_request_origins ||= /https?:\/\/localhost:\d+/ if ::Rails.env.development?
|
||||||
|
@ -9,6 +9,7 @@ class Configuration
|
|||||||
attr_accessor :connection_class, :worker_pool_size
|
attr_accessor :connection_class, :worker_pool_size
|
||||||
attr_accessor :disable_request_forgery_protection, :allowed_request_origins, :allow_same_origin_as_host
|
attr_accessor :disable_request_forgery_protection, :allowed_request_origins, :allow_same_origin_as_host
|
||||||
attr_accessor :cable, :url, :mount_path
|
attr_accessor :cable, :url, :mount_path
|
||||||
|
attr_accessor :precompile_assets
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
@log_tags = []
|
@log_tags = []
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
"name": "@rails/actioncable",
|
"name": "@rails/actioncable",
|
||||||
"version": "7.0.0-alpha",
|
"version": "7.0.0-alpha",
|
||||||
"description": "WebSocket framework for Ruby on Rails.",
|
"description": "WebSocket framework for Ruby on Rails.",
|
||||||
|
"module": "app/javascript/action_cable/index.js",
|
||||||
"main": "app/assets/javascripts/action_cable.js",
|
"main": "app/assets/javascripts/action_cable.js",
|
||||||
"files": [
|
"files": [
|
||||||
"app/assets/javascripts/*.js",
|
"app/assets/javascripts/*.js",
|
||||||
@ -23,9 +24,8 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://rubyonrails.org/",
|
"homepage": "https://rubyonrails.org/",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-core": "^6.25.0",
|
"@rollup/plugin-node-resolve": "^11.0.1",
|
||||||
"babel-plugin-external-helpers": "^6.22.0",
|
"@rollup/plugin-commonjs": "^19.0.1",
|
||||||
"babel-preset-env": "^1.6.0",
|
|
||||||
"eslint": "^4.3.0",
|
"eslint": "^4.3.0",
|
||||||
"eslint-plugin-import": "^2.7.0",
|
"eslint-plugin-import": "^2.7.0",
|
||||||
"karma": "^3.1.1",
|
"karma": "^3.1.1",
|
||||||
@ -34,11 +34,8 @@
|
|||||||
"karma-sauce-launcher": "^1.2.0",
|
"karma-sauce-launcher": "^1.2.0",
|
||||||
"mock-socket": "^2.0.0",
|
"mock-socket": "^2.0.0",
|
||||||
"qunit": "^2.8.0",
|
"qunit": "^2.8.0",
|
||||||
"rollup": "^0.58.2",
|
"rollup": "^2.35.1",
|
||||||
"rollup-plugin-babel": "^3.0.4",
|
"rollup-plugin-terser": "^7.0.2"
|
||||||
"rollup-plugin-commonjs": "^9.1.0",
|
|
||||||
"rollup-plugin-node-resolve": "^3.3.0",
|
|
||||||
"rollup-plugin-uglify": "^3.0.0"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prebuild": "yarn lint && bundle exec rake assets:codegen",
|
"prebuild": "yarn lint && bundle exec rake assets:codegen",
|
||||||
|
@ -1,24 +1,47 @@
|
|||||||
import babel from "rollup-plugin-babel"
|
import resolve from "@rollup/plugin-node-resolve"
|
||||||
import uglify from "rollup-plugin-uglify"
|
import { terser } from "rollup-plugin-terser"
|
||||||
|
|
||||||
const uglifyOptions = {
|
const terserOptions = {
|
||||||
mangle: false,
|
mangle: false,
|
||||||
compress: false,
|
compress: false,
|
||||||
output: {
|
format: {
|
||||||
beautify: true,
|
beautify: true,
|
||||||
indent_level: 2
|
indent_level: 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default [
|
||||||
input: "app/javascript/action_cable/index.js",
|
{
|
||||||
output: {
|
input: "app/javascript/action_cable/index.js",
|
||||||
file: "app/assets/javascripts/action_cable.js",
|
output: [
|
||||||
format: "umd",
|
{
|
||||||
name: "ActionCable"
|
file: "app/assets/javascripts/actioncable.js",
|
||||||
|
format: "umd",
|
||||||
|
name: "ActionCable"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
file: "app/assets/javascripts/actioncable.esm.js",
|
||||||
|
format: "es"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
resolve(),
|
||||||
|
terser(terserOptions)
|
||||||
|
]
|
||||||
},
|
},
|
||||||
plugins: [
|
|
||||||
babel(),
|
{
|
||||||
uglify(uglifyOptions)
|
input: "app/javascript/action_cable/index_with_name_deprecation.js",
|
||||||
]
|
output: {
|
||||||
}
|
file: "app/assets/javascripts/action_cable.js",
|
||||||
|
format: "umd",
|
||||||
|
name: "ActionCable"
|
||||||
|
},
|
||||||
|
breakOnWarning: false,
|
||||||
|
plugins: [
|
||||||
|
resolve(),
|
||||||
|
terser(terserOptions)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
@ -1,6 +1,5 @@
|
|||||||
import babel from "rollup-plugin-babel"
|
import commonjs from "@rollup/plugin-commonjs"
|
||||||
import commonjs from "rollup-plugin-commonjs"
|
import resolve from "@rollup/plugin-node-resolve"
|
||||||
import resolve from "rollup-plugin-node-resolve"
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: "test/javascript/src/test.js",
|
input: "test/javascript/src/test.js",
|
||||||
@ -12,7 +11,6 @@ export default {
|
|||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
resolve(),
|
resolve(),
|
||||||
commonjs(),
|
commonjs()
|
||||||
babel()
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -5201,7 +5201,7 @@ resolve@^1.1.6, resolve@^1.5.0, resolve@^1.6.0, resolve@^1.8.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
path-parse "^1.0.5"
|
path-parse "^1.0.5"
|
||||||
|
|
||||||
resolve@^1.13.1, resolve@^1.17.0, resolve@^1.19.0, resolve@^1.20.0:
|
resolve@^1.17.0, resolve@^1.19.0:
|
||||||
version "1.20.0"
|
version "1.20.0"
|
||||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
|
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
|
||||||
integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
|
integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
|
||||||
@ -5318,9 +5318,9 @@ rollup@^0.58.2:
|
|||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
rollup@^2.35.1:
|
rollup@^2.35.1:
|
||||||
version "2.55.1"
|
version "2.53.3"
|
||||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.55.1.tgz#66a444648e2fb603d8e329e77a61c608a6510fda"
|
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.53.3.tgz#14b0e57f0874d4ad23bdbb13050cf70bcd1eabf7"
|
||||||
integrity sha512-1P9w5fpb6b4qroePh8vHKGIvPNxwoCQhjJpIqfZGHLKpZ0xcU2/XBmFxFbc9697/6bmHpmFTLk5R1dAQhFSo0g==
|
integrity sha512-79QIGP5DXz5ZHYnCPi3tLz+elOQi6gudp9YINdaJdjG0Yddubo6JRFUM//qCZ0Bap/GJrsUoEBVdSOc4AkMlRA==
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.2"
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user