mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Convert ActionCable javascript to ES2015 modules with modern build environment
We've replaced the sprockets `//= require` directives with ES2015 imports. As a result, the ActionCable javascript can now be compiled with rollup (like ActiveStorage already is). - Rename action_cable/index.js.erb -> action_cable/index.js - Add rake task to generate a javascript module of the ActionCable::INTERNAL ruby hash This will allow us to get rid of ERB from the actioncable javascript, since it is only used to interpolate ActionCable::INTERNAL.to_json. - Import INTERNAL directly in ActionCable Connection module This is necessary to remove a load-order dependency conflict in the rollup-compiled build. Using ActionCable.INTERNAL would result in a runtime error: ``` TypeError: Cannot read property 'INTERNAL' of undefined ``` because ActionCable.INTERNAL is not set before the Connection module is executed. All other ActionCable.* references are executed inside of the body of a function, so there is no load-order dependency there. - Add eslint and eslint-plugin-import devDependencies to actioncable These will be used to add a linting setup to actioncable like the one in activestorage. - Add .eslintrc to actioncable This lint configuration was copied from activestorage - Add lint script to actioncable This is the same as the lint script in activestorage - Add babel-core, babel-plugin-external-helpers, and babel-preset-env devDependencies to actioncable These will be used to add ES2015 transpilation support to actioncable like we have in activestorage. - Add .babelrc to actioncable This configuration was copied from activestorage - Enable loose mode in ActionCable's babel config This generates a smaller bundle when compiled - Add rollup devDependencies to actioncable These will be used to add a modern build pipeline to actioncable like the one in activestorage. - Add rollup config to actioncable This is essentially the same as the rollup config from activestorage - Add prebuild and build scripts to actioncable package These scripts were copied from activestorage - Invoke code generation task as part of actioncable's prebuild script This will guarantee that the action_cable/internal.js module is available at build time (which is important, because two other modules now depend on it). - Update actioncable package to reference the rollup-compiled files Now that we have a fully functional rollup pipeline in actioncable, we can use the compiled output in our npm package. - Remove build section from ActionCable blade config Now that rollup is responsible for building ActionCable, we can remove that responsibility from Blade. - Remove assets:compile and assets:verify tasks from ActionCable Now that we've added a compiled ActionCable bundle to version control, we don't need to compile and verify it at publish-time. (We're following the pattern set in ActiveStorage.) - Include compiled ActionCable javascript bundle in published gem This is necessary to maintain support for depending on the ActionCable javascript through the Sprockets asset pipeline. - Add compiled ActionCable bundle to version control This mirrors what we do in ActiveStorage, and allows ActionCable to continue to be consumed via the sprockets-based asset pipeline when using a git source instead of a published version of the gem.
This commit is contained in:
parent
0eb6b86e96
commit
c96139af71
16 changed files with 2780 additions and 334 deletions
8
actioncable/.babelrc
Normal file
8
actioncable/.babelrc
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"presets": [
|
||||
["env", { "modules": false, "loose": true } ]
|
||||
],
|
||||
"plugins": [
|
||||
"external-helpers"
|
||||
]
|
||||
}
|
19
actioncable/.eslintrc
Normal file
19
actioncable/.eslintrc
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "eslint:recommended",
|
||||
"rules": {
|
||||
"semi": ["error", "never"],
|
||||
"quotes": ["error", "double"],
|
||||
"no-unused-vars": ["error", { "vars": "all", "args": "none" }]
|
||||
},
|
||||
"plugins": [
|
||||
"import"
|
||||
],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
1
actioncable/.gitignore
vendored
1
actioncable/.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
/app/javascript/action_cable/internal.js
|
||||
/lib/assets/compiled/
|
||||
/tmp/
|
||||
|
|
|
@ -7,7 +7,7 @@ require "action_cable"
|
|||
|
||||
task default: :test
|
||||
|
||||
task package: %w( assets:compile assets:verify )
|
||||
task :package
|
||||
|
||||
Rake::TestTask.new do |t|
|
||||
t.libs << "test"
|
||||
|
@ -35,42 +35,13 @@ namespace :test do
|
|||
end
|
||||
|
||||
namespace :assets do
|
||||
desc "Compile Action Cable assets"
|
||||
task :compile do
|
||||
require "blade"
|
||||
require "sprockets"
|
||||
require "sprockets/export"
|
||||
Blade.build
|
||||
end
|
||||
desc "Generate ActionCable::INTERNAL JS module"
|
||||
task :codegen do
|
||||
require "json"
|
||||
require "action_cable"
|
||||
|
||||
desc "Verify compiled Action Cable assets"
|
||||
task :verify do
|
||||
file = "lib/assets/compiled/action_cable.js"
|
||||
pathname = Pathname.new("#{__dir__}/#{file}")
|
||||
|
||||
print "[verify] #{file} exists "
|
||||
if pathname.exist?
|
||||
puts "[OK]"
|
||||
else
|
||||
$stderr.puts "[FAIL]"
|
||||
fail
|
||||
end
|
||||
|
||||
print "[verify] #{file} is a UMD module "
|
||||
if /module\.exports.*define\.amd/m.match?(pathname.read)
|
||||
puts "[OK]"
|
||||
else
|
||||
$stderr.puts "[FAIL]"
|
||||
fail
|
||||
end
|
||||
|
||||
print "[verify] #{__dir__} can be required as a module "
|
||||
_, stderr, status = Open3.capture3("node", "--print", "window = {}; require('#{__dir__}');")
|
||||
if status.success?
|
||||
puts "[OK]"
|
||||
else
|
||||
$stderr.puts "[FAIL]\n#{stderr}"
|
||||
fail
|
||||
File.open(File.join(__dir__, "app/javascript/action_cable/internal.js").to_s, "w+") do |file|
|
||||
file.write("export default #{JSON.generate(ActionCable::INTERNAL)}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,7 +17,7 @@ Gem::Specification.new do |s|
|
|||
s.email = ["pratiknaik@gmail.com", "david@loudthinking.com"]
|
||||
s.homepage = "http://rubyonrails.org"
|
||||
|
||||
s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*"]
|
||||
s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*", "app/assets/javascripts/action_cable.js"]
|
||||
s.require_path = "lib"
|
||||
|
||||
s.metadata = {
|
||||
|
|
475
actioncable/app/assets/javascripts/action_cable.js
Normal file
475
actioncable/app/assets/javascripts/action_cable.js
Normal file
|
@ -0,0 +1,475 @@
|
|||
(function(global, factory) {
|
||||
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : global.ActionCable = factory();
|
||||
})(this, function() {
|
||||
"use strict";
|
||||
var INTERNAL = {
|
||||
message_types: {
|
||||
welcome: "welcome",
|
||||
ping: "ping",
|
||||
confirmation: "confirm_subscription",
|
||||
rejection: "reject_subscription"
|
||||
},
|
||||
default_mount_path: "/cable",
|
||||
protocols: [ "actioncable-v1-json", "actioncable-unsupported" ]
|
||||
};
|
||||
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 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 ActionCable.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()) {
|
||||
ActionCable.log("Attempted to open WebSocket, but existing socket is " + this.getState());
|
||||
return false;
|
||||
} else {
|
||||
ActionCable.log("Opening WebSocket, current state is " + this.getState() + ", subprotocols: " + protocols);
|
||||
if (this.webSocket) {
|
||||
this.uninstallEventHandlers();
|
||||
}
|
||||
this.webSocket = new ActionCable.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 ? this.webSocket.close() : undefined;
|
||||
}
|
||||
};
|
||||
Connection.prototype.reopen = function reopen() {
|
||||
ActionCable.log("Reopening WebSocket, current state is " + this.getState());
|
||||
if (this.isActive()) {
|
||||
try {
|
||||
return this.close();
|
||||
} catch (error) {
|
||||
ActionCable.log("Failed to reopen WebSocket", error);
|
||||
} finally {
|
||||
ActionCable.log("Reopening WebSocket in " + this.constructor.reopenDelay + "ms");
|
||||
setTimeout(this.open, this.constructor.reopenDelay);
|
||||
}
|
||||
} else {
|
||||
return this.open();
|
||||
}
|
||||
};
|
||||
Connection.prototype.getProtocol = function getProtocol() {
|
||||
return this.webSocket ? this.webSocket.protocol : undefined;
|
||||
};
|
||||
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 WebSocket) {
|
||||
if (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, type = _JSON$parse.type;
|
||||
switch (type) {
|
||||
case message_types.welcome:
|
||||
this.monitor.recordConnect();
|
||||
return this.subscriptions.reload();
|
||||
|
||||
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() {
|
||||
ActionCable.log("WebSocket onopen event, using '" + this.getProtocol() + "' subprotocol");
|
||||
this.disconnected = false;
|
||||
if (!this.isProtocolSupported()) {
|
||||
ActionCable.log("Protocol is unsupported. Stopping monitor and disconnecting.");
|
||||
return this.close({
|
||||
allowReconnect: false
|
||||
});
|
||||
}
|
||||
},
|
||||
close: function close(event) {
|
||||
ActionCable.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() {
|
||||
ActionCable.log("WebSocket onerror event");
|
||||
}
|
||||
};
|
||||
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();
|
||||
document.addEventListener("visibilitychange", this.visibilityDidChange);
|
||||
ActionCable.log("ConnectionMonitor started. pollInterval = " + this.getPollInterval() + " ms");
|
||||
}
|
||||
};
|
||||
ConnectionMonitor.prototype.stop = function stop() {
|
||||
if (this.isRunning()) {
|
||||
this.stoppedAt = now();
|
||||
this.stopPolling();
|
||||
document.removeEventListener("visibilitychange", this.visibilityDidChange);
|
||||
ActionCable.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;
|
||||
ActionCable.log("ConnectionMonitor recorded connect");
|
||||
};
|
||||
ConnectionMonitor.prototype.recordDisconnect = function recordDisconnect() {
|
||||
this.disconnectedAt = now();
|
||||
ActionCable.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;
|
||||
var interval = 5 * Math.log(this.reconnectAttempts + 1);
|
||||
return Math.round(clamp(interval, min, max) * 1e3);
|
||||
};
|
||||
ConnectionMonitor.prototype.reconnectIfStale = function reconnectIfStale() {
|
||||
if (this.connectionIsStale()) {
|
||||
ActionCable.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()) {
|
||||
ActionCable.log("ConnectionMonitor skipping reopening recent disconnect");
|
||||
} else {
|
||||
ActionCable.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()) {
|
||||
ActionCable.log("ConnectionMonitor reopening stale connection on visibilitychange. visbilityState = " + document.visibilityState);
|
||||
_this2.connection.reopen();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
};
|
||||
return ConnectionMonitor;
|
||||
}();
|
||||
ConnectionMonitor.pollInterval = {
|
||||
min: 3,
|
||||
max: 30
|
||||
};
|
||||
ConnectionMonitor.staleThreshold = 6;
|
||||
var Consumer = function() {
|
||||
function Consumer(url) {
|
||||
classCallCheck(this, Consumer);
|
||||
this.url = url;
|
||||
this.subscriptions = new ActionCable.Subscriptions(this);
|
||||
this.connection = new ActionCable.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;
|
||||
}();
|
||||
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 ActionCable.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 ActionCable = {
|
||||
Connection: Connection,
|
||||
ConnectionMonitor: ConnectionMonitor,
|
||||
Consumer: Consumer,
|
||||
INTERNAL: INTERNAL,
|
||||
Subscription: Subscription,
|
||||
Subscriptions: Subscriptions,
|
||||
WebSocket: window.WebSocket,
|
||||
logger: window.console,
|
||||
createConsumer: function createConsumer(url) {
|
||||
if (url == null) {
|
||||
var urlConfig = this.getConfig("url");
|
||||
url = urlConfig ? urlConfig : this.INTERNAL.default_mount_path;
|
||||
}
|
||||
return new Consumer(this.createWebSocketURL(url));
|
||||
},
|
||||
getConfig: function getConfig(name) {
|
||||
var element = document.head.querySelector("meta[name='action-cable-" + name + "']");
|
||||
return element ? element.getAttribute("content") : undefined;
|
||||
},
|
||||
createWebSocketURL: function createWebSocketURL(url) {
|
||||
if (url && !/^wss?:/i.test(url)) {
|
||||
var a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.href = a.href;
|
||||
a.protocol = a.protocol.replace("http", "ws");
|
||||
return a.href;
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
},
|
||||
startDebugging: function startDebugging() {
|
||||
this.debugging = true;
|
||||
},
|
||||
stopDebugging: function stopDebugging() {
|
||||
this.debugging = null;
|
||||
},
|
||||
log: function log() {
|
||||
if (this.debugging) {
|
||||
var _logger;
|
||||
for (var _len = arguments.length, messages = Array(_len), _key = 0; _key < _len; _key++) {
|
||||
messages[_key] = arguments[_key];
|
||||
}
|
||||
messages.push(Date.now());
|
||||
(_logger = this.logger).log.apply(_logger, [ "[ActionCable]" ].concat(messages));
|
||||
}
|
||||
}
|
||||
};
|
||||
return ActionCable;
|
||||
});
|
|
@ -1,158 +1,156 @@
|
|||
//= require ./connection_monitor
|
||||
import ActionCable from "./index"
|
||||
import INTERNAL from "./internal"
|
||||
|
||||
// Encapsulate the cable connection held by the consumer. This is an internal class not intended for direct user manipulation.
|
||||
|
||||
const {message_types, protocols} = ActionCable.INTERNAL
|
||||
const {message_types, protocols} = INTERNAL
|
||||
const supportedProtocols = protocols.slice(0, protocols.length - 1)
|
||||
|
||||
ActionCable.Connection = (function() {
|
||||
const indexOf = [].indexOf
|
||||
|
||||
class Connection {
|
||||
constructor(consumer) {
|
||||
this.open = this.open.bind(this)
|
||||
this.consumer = consumer
|
||||
this.subscriptions = this.consumer.subscriptions
|
||||
this.monitor = new ActionCable.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()) {
|
||||
ActionCable.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`)
|
||||
return false
|
||||
} else {
|
||||
ActionCable.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`)
|
||||
if (this.webSocket) { this.uninstallEventHandlers() }
|
||||
this.webSocket = new ActionCable.WebSocket(this.consumer.url, protocols)
|
||||
this.installEventHandlers()
|
||||
this.monitor.start()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
close({allowReconnect} = {allowReconnect: true}) {
|
||||
if (!allowReconnect) { this.monitor.stop() }
|
||||
if (this.isActive()) { return (this.webSocket ? this.webSocket.close() : undefined) }
|
||||
}
|
||||
|
||||
reopen() {
|
||||
ActionCable.log(`Reopening WebSocket, current state is ${this.getState()}`)
|
||||
if (this.isActive()) {
|
||||
try {
|
||||
return this.close()
|
||||
} catch (error) {
|
||||
ActionCable.log("Failed to reopen WebSocket", error)
|
||||
}
|
||||
finally {
|
||||
ActionCable.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`)
|
||||
setTimeout(this.open, this.constructor.reopenDelay)
|
||||
}
|
||||
} else {
|
||||
return this.open()
|
||||
}
|
||||
}
|
||||
|
||||
getProtocol() {
|
||||
return (this.webSocket ? this.webSocket.protocol : undefined)
|
||||
}
|
||||
|
||||
isOpen() {
|
||||
return this.isState("open")
|
||||
}
|
||||
|
||||
isActive() {
|
||||
return this.isState("open", "connecting")
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
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 WebSocket) {
|
||||
if (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() {}
|
||||
}
|
||||
}
|
||||
const indexOf = [].indexOf
|
||||
|
||||
class Connection {
|
||||
constructor(consumer) {
|
||||
this.open = this.open.bind(this)
|
||||
this.consumer = consumer
|
||||
this.subscriptions = this.consumer.subscriptions
|
||||
this.monitor = new ActionCable.ConnectionMonitor(this)
|
||||
this.disconnected = true
|
||||
}
|
||||
|
||||
Connection.reopenDelay = 500
|
||||
|
||||
Connection.prototype.events = {
|
||||
message(event) {
|
||||
if (!this.isProtocolSupported()) { return }
|
||||
const {identifier, message, type} = JSON.parse(event.data)
|
||||
switch (type) {
|
||||
case message_types.welcome:
|
||||
this.monitor.recordConnect()
|
||||
return this.subscriptions.reload()
|
||||
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() {
|
||||
ActionCable.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`)
|
||||
this.disconnected = false
|
||||
if (!this.isProtocolSupported()) {
|
||||
ActionCable.log("Protocol is unsupported. Stopping monitor and disconnecting.")
|
||||
return this.close({allowReconnect: false})
|
||||
}
|
||||
},
|
||||
|
||||
close(event) {
|
||||
ActionCable.log("WebSocket onclose event")
|
||||
if (this.disconnected) { return }
|
||||
this.disconnected = true
|
||||
this.monitor.recordDisconnect()
|
||||
return this.subscriptions.notifyAll("disconnected", {willAttemptReconnect: this.monitor.isRunning()})
|
||||
},
|
||||
|
||||
error() {
|
||||
ActionCable.log("WebSocket onerror event")
|
||||
send(data) {
|
||||
if (this.isOpen()) {
|
||||
this.webSocket.send(JSON.stringify(data))
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return Connection
|
||||
open() {
|
||||
if (this.isActive()) {
|
||||
ActionCable.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`)
|
||||
return false
|
||||
} else {
|
||||
ActionCable.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`)
|
||||
if (this.webSocket) { this.uninstallEventHandlers() }
|
||||
this.webSocket = new ActionCable.WebSocket(this.consumer.url, protocols)
|
||||
this.installEventHandlers()
|
||||
this.monitor.start()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
})()
|
||||
close({allowReconnect} = {allowReconnect: true}) {
|
||||
if (!allowReconnect) { this.monitor.stop() }
|
||||
if (this.isActive()) { return (this.webSocket ? this.webSocket.close() : undefined) }
|
||||
}
|
||||
|
||||
reopen() {
|
||||
ActionCable.log(`Reopening WebSocket, current state is ${this.getState()}`)
|
||||
if (this.isActive()) {
|
||||
try {
|
||||
return this.close()
|
||||
} catch (error) {
|
||||
ActionCable.log("Failed to reopen WebSocket", error)
|
||||
}
|
||||
finally {
|
||||
ActionCable.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`)
|
||||
setTimeout(this.open, this.constructor.reopenDelay)
|
||||
}
|
||||
} else {
|
||||
return this.open()
|
||||
}
|
||||
}
|
||||
|
||||
getProtocol() {
|
||||
return (this.webSocket ? this.webSocket.protocol : undefined)
|
||||
}
|
||||
|
||||
isOpen() {
|
||||
return this.isState("open")
|
||||
}
|
||||
|
||||
isActive() {
|
||||
return this.isState("open", "connecting")
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
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 WebSocket) {
|
||||
if (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, message, type} = JSON.parse(event.data)
|
||||
switch (type) {
|
||||
case message_types.welcome:
|
||||
this.monitor.recordConnect()
|
||||
return this.subscriptions.reload()
|
||||
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() {
|
||||
ActionCable.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`)
|
||||
this.disconnected = false
|
||||
if (!this.isProtocolSupported()) {
|
||||
ActionCable.log("Protocol is unsupported. Stopping monitor and disconnecting.")
|
||||
return this.close({allowReconnect: false})
|
||||
}
|
||||
},
|
||||
|
||||
close(event) {
|
||||
ActionCable.log("WebSocket onclose event")
|
||||
if (this.disconnected) { return }
|
||||
this.disconnected = true
|
||||
this.monitor.recordDisconnect()
|
||||
return this.subscriptions.notifyAll("disconnected", {willAttemptReconnect: this.monitor.isRunning()})
|
||||
},
|
||||
|
||||
error() {
|
||||
ActionCable.log("WebSocket onerror event")
|
||||
}
|
||||
}
|
||||
|
||||
export default Connection
|
||||
|
|
|
@ -1,125 +1,125 @@
|
|||
import ActionCable from "./index"
|
||||
|
||||
// Responsible for ensuring the cable connection is in good health by validating the heartbeat pings sent from the server, and attempting
|
||||
// revival reconnections if things go astray. Internal class, not intended for direct user manipulation.
|
||||
ActionCable.ConnectionMonitor = (function() {
|
||||
const now = () => new Date().getTime()
|
||||
|
||||
const secondsSince = time => (now() - time) / 1000
|
||||
const now = () => new Date().getTime()
|
||||
|
||||
const clamp = (number, min, max) => Math.max(min, Math.min(max, number))
|
||||
const secondsSince = time => (now() - time) / 1000
|
||||
|
||||
class ConnectionMonitor {
|
||||
constructor(connection) {
|
||||
this.visibilityDidChange = this.visibilityDidChange.bind(this)
|
||||
this.connection = connection
|
||||
this.reconnectAttempts = 0
|
||||
const clamp = (number, min, max) => Math.max(min, Math.min(max, number))
|
||||
|
||||
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()
|
||||
document.addEventListener("visibilitychange", this.visibilityDidChange)
|
||||
ActionCable.log(`ConnectionMonitor started. pollInterval = ${this.getPollInterval()} ms`)
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
if (!this.isRunning()) {
|
||||
this.startedAt = now()
|
||||
delete this.stoppedAt
|
||||
this.startPolling()
|
||||
document.addEventListener("visibilitychange", this.visibilityDidChange)
|
||||
ActionCable.log(`ConnectionMonitor started. pollInterval = ${this.getPollInterval()} ms`)
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.isRunning()) {
|
||||
this.stoppedAt = now()
|
||||
this.stopPolling()
|
||||
document.removeEventListener("visibilitychange", this.visibilityDidChange)
|
||||
ActionCable.log("ConnectionMonitor stopped")
|
||||
}
|
||||
}
|
||||
|
||||
isRunning() {
|
||||
return this.startedAt && !this.stoppedAt
|
||||
}
|
||||
|
||||
recordPing() {
|
||||
this.pingedAt = now()
|
||||
}
|
||||
|
||||
recordConnect() {
|
||||
this.reconnectAttempts = 0
|
||||
this.recordPing()
|
||||
delete this.disconnectedAt
|
||||
ActionCable.log("ConnectionMonitor recorded connect")
|
||||
}
|
||||
|
||||
recordDisconnect() {
|
||||
this.disconnectedAt = now()
|
||||
ActionCable.log("ConnectionMonitor recorded disconnect")
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
startPolling() {
|
||||
stop() {
|
||||
if (this.isRunning()) {
|
||||
this.stoppedAt = now()
|
||||
this.stopPolling()
|
||||
document.removeEventListener("visibilitychange", this.visibilityDidChange)
|
||||
ActionCable.log("ConnectionMonitor stopped")
|
||||
}
|
||||
}
|
||||
|
||||
isRunning() {
|
||||
return this.startedAt && !this.stoppedAt
|
||||
}
|
||||
|
||||
recordPing() {
|
||||
this.pingedAt = now()
|
||||
}
|
||||
|
||||
recordConnect() {
|
||||
this.reconnectAttempts = 0
|
||||
this.recordPing()
|
||||
delete this.disconnectedAt
|
||||
ActionCable.log("ConnectionMonitor recorded connect")
|
||||
}
|
||||
|
||||
recordDisconnect() {
|
||||
this.disconnectedAt = now()
|
||||
ActionCable.log("ConnectionMonitor recorded disconnect")
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
startPolling() {
|
||||
this.stopPolling()
|
||||
this.poll()
|
||||
}
|
||||
|
||||
stopPolling() {
|
||||
clearTimeout(this.pollTimeout)
|
||||
}
|
||||
|
||||
poll() {
|
||||
this.pollTimeout = setTimeout(() => {
|
||||
this.reconnectIfStale()
|
||||
this.poll()
|
||||
}
|
||||
, this.getPollInterval())
|
||||
}
|
||||
|
||||
stopPolling() {
|
||||
clearTimeout(this.pollTimeout)
|
||||
}
|
||||
getPollInterval() {
|
||||
const {min, max} = this.constructor.pollInterval
|
||||
const interval = 5 * Math.log(this.reconnectAttempts + 1)
|
||||
return Math.round(clamp(interval, min, max) * 1000)
|
||||
}
|
||||
|
||||
poll() {
|
||||
this.pollTimeout = setTimeout(() => {
|
||||
this.reconnectIfStale()
|
||||
this.poll()
|
||||
reconnectIfStale() {
|
||||
if (this.connectionIsStale()) {
|
||||
ActionCable.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()) {
|
||||
ActionCable.log("ConnectionMonitor skipping reopening recent disconnect")
|
||||
} else {
|
||||
ActionCable.log("ConnectionMonitor reopening")
|
||||
this.connection.reopen()
|
||||
}
|
||||
, this.getPollInterval())
|
||||
}
|
||||
}
|
||||
|
||||
getPollInterval() {
|
||||
const {min, max} = this.constructor.pollInterval
|
||||
const interval = 5 * Math.log(this.reconnectAttempts + 1)
|
||||
return Math.round(clamp(interval, min, max) * 1000)
|
||||
}
|
||||
connectionIsStale() {
|
||||
return secondsSince(this.pingedAt ? this.pingedAt : this.startedAt) > this.constructor.staleThreshold
|
||||
}
|
||||
|
||||
reconnectIfStale() {
|
||||
if (this.connectionIsStale()) {
|
||||
ActionCable.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()) {
|
||||
ActionCable.log("ConnectionMonitor skipping reopening recent disconnect")
|
||||
} else {
|
||||
ActionCable.log("ConnectionMonitor reopening")
|
||||
disconnectedRecently() {
|
||||
return this.disconnectedAt && (secondsSince(this.disconnectedAt) < this.constructor.staleThreshold)
|
||||
}
|
||||
|
||||
visibilityDidChange() {
|
||||
if (document.visibilityState === "visible") {
|
||||
setTimeout(() => {
|
||||
if (this.connectionIsStale() || !this.connection.isOpen()) {
|
||||
ActionCable.log(`ConnectionMonitor reopening stale connection on visibilitychange. visbilityState = ${document.visibilityState}`)
|
||||
this.connection.reopen()
|
||||
}
|
||||
}
|
||||
, 200)
|
||||
}
|
||||
|
||||
connectionIsStale() {
|
||||
return secondsSince(this.pingedAt ? this.pingedAt : this.startedAt) > 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()) {
|
||||
ActionCable.log(`ConnectionMonitor reopening stale connection on visibilitychange. visbilityState = ${document.visibilityState}`)
|
||||
this.connection.reopen()
|
||||
}
|
||||
}
|
||||
, 200)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ConnectionMonitor.pollInterval = {
|
||||
min: 3,
|
||||
max: 30
|
||||
}
|
||||
}
|
||||
|
||||
ConnectionMonitor.staleThreshold = 6 // Server::Connections::BEAT_INTERVAL * 2 (missed two pings)
|
||||
ConnectionMonitor.pollInterval = {
|
||||
min: 3,
|
||||
max: 30
|
||||
}
|
||||
|
||||
return ConnectionMonitor
|
||||
ConnectionMonitor.staleThreshold = 6 // Server::Connections::BEAT_INTERVAL * 2 (missed two pings)
|
||||
|
||||
})()
|
||||
export default ConnectionMonitor
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
//= require ./connection
|
||||
//= require ./subscriptions
|
||||
//= require ./subscription
|
||||
import ActionCable from "./index"
|
||||
|
||||
// The ActionCable.Consumer establishes the connection to a server-side Ruby Connection object. Once established,
|
||||
// the ActionCable.ConnectionMonitor will ensure that its properly maintained through heartbeats and checking for stale updates.
|
||||
|
@ -27,7 +25,8 @@
|
|||
//
|
||||
// Any channel subscriptions which existed prior to disconnecting will
|
||||
// automatically resubscribe.
|
||||
ActionCable.Consumer = class Consumer {
|
||||
|
||||
export default class Consumer {
|
||||
constructor(url) {
|
||||
this.url = url
|
||||
this.subscriptions = new ActionCable.Subscriptions(this)
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
//= export ActionCable
|
||||
//= require_self
|
||||
//= require ./action_cable/consumer
|
||||
import Connection from "./connection"
|
||||
import ConnectionMonitor from "./connection_monitor"
|
||||
import Consumer from "./consumer"
|
||||
import INTERNAL from "./internal"
|
||||
import Subscription from "./subscription"
|
||||
import Subscriptions from "./subscriptions"
|
||||
|
||||
this.ActionCable = {
|
||||
INTERNAL: <%= ActionCable::INTERNAL.to_json %>,
|
||||
export default {
|
||||
Connection,
|
||||
ConnectionMonitor,
|
||||
Consumer,
|
||||
INTERNAL,
|
||||
Subscription,
|
||||
Subscriptions,
|
||||
WebSocket: window.WebSocket,
|
||||
logger: window.console,
|
||||
|
||||
|
@ -12,7 +20,7 @@ this.ActionCable = {
|
|||
const urlConfig = this.getConfig("url")
|
||||
url = (urlConfig ? urlConfig : this.INTERNAL.default_mount_path)
|
||||
}
|
||||
return new ActionCable.Consumer(this.createWebSocketURL(url))
|
||||
return new Consumer(this.createWebSocketURL(url))
|
||||
},
|
||||
|
||||
getConfig(name) {
|
|
@ -55,38 +55,35 @@
|
|||
//
|
||||
// The "AppearanceChannel" name is automatically mapped between the client-side subscription creation and the server-side Ruby class name.
|
||||
// The AppearanceChannel#appear/away public methods are exposed automatically to client-side invocation through the perform method.
|
||||
ActionCable.Subscription = (function() {
|
||||
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 a channel action with the optional data passed as an attribute
|
||||
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)
|
||||
const extend = function(object, properties) {
|
||||
if (properties != null) {
|
||||
for (let key in properties) {
|
||||
const value = properties[key]
|
||||
object[key] = value
|
||||
}
|
||||
}
|
||||
return object
|
||||
}
|
||||
|
||||
return Subscription
|
||||
})()
|
||||
export default class Subscription {
|
||||
constructor(consumer, params = {}, mixin) {
|
||||
this.consumer = consumer
|
||||
this.identifier = JSON.stringify(params)
|
||||
extend(this, mixin)
|
||||
}
|
||||
|
||||
// Perform a channel action with the optional data passed as an attribute
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import ActionCable from "./index"
|
||||
|
||||
// Collection class for creating (and internally managing) channel subscriptions. The only method intended to be triggered by the user
|
||||
// us ActionCable.Subscriptions#create, and it should be called through the consumer like so:
|
||||
//
|
||||
|
@ -6,7 +8,8 @@
|
|||
// App.appearance = App.cable.subscriptions.create("AppearanceChannel")
|
||||
//
|
||||
// For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription.
|
||||
ActionCable.Subscriptions = class Subscriptions {
|
||||
|
||||
export default class Subscriptions {
|
||||
constructor(consumer) {
|
||||
this.consumer = consumer
|
||||
this.subscriptions = []
|
||||
|
|
|
@ -6,12 +6,6 @@ load_paths:
|
|||
logical_paths:
|
||||
- test.js
|
||||
|
||||
build:
|
||||
logical_paths:
|
||||
- action_cable.js
|
||||
path: lib/assets/compiled
|
||||
clean: true
|
||||
|
||||
plugins:
|
||||
sauce_labs:
|
||||
browsers:
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
"name": "actioncable",
|
||||
"version": "6.0.0-alpha",
|
||||
"description": "WebSocket framework for Ruby on Rails.",
|
||||
"main": "lib/assets/compiled/action_cable.js",
|
||||
"main": "app/assets/javascripts/action_cable.js",
|
||||
"files": [
|
||||
"lib/assets/compiled/*.js"
|
||||
"app/assets/javascripts/*.js"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -20,5 +20,22 @@
|
|||
"bugs": {
|
||||
"url": "https://github.com/rails/rails/issues"
|
||||
},
|
||||
"homepage": "http://rubyonrails.org/"
|
||||
"homepage": "http://rubyonrails.org/",
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.25.0",
|
||||
"babel-plugin-external-helpers": "^6.22.0",
|
||||
"babel-preset-env": "^1.6.0",
|
||||
"eslint": "^4.3.0",
|
||||
"eslint-plugin-import": "^2.7.0",
|
||||
"rollup": "^0.58.2",
|
||||
"rollup-plugin-babel": "^3.0.4",
|
||||
"rollup-plugin-commonjs": "^9.1.0",
|
||||
"rollup-plugin-node-resolve": "^3.3.0",
|
||||
"rollup-plugin-uglify": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prebuild": "yarn lint && bundle exec rake assets:codegen",
|
||||
"build": "rollup --config rollup.config.js",
|
||||
"lint": "eslint app/javascript"
|
||||
}
|
||||
}
|
||||
|
|
28
actioncable/rollup.config.js
Normal file
28
actioncable/rollup.config.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import resolve from "rollup-plugin-node-resolve"
|
||||
import commonjs from "rollup-plugin-commonjs"
|
||||
import babel from "rollup-plugin-babel"
|
||||
import uglify from "rollup-plugin-uglify"
|
||||
|
||||
const uglifyOptions = {
|
||||
mangle: false,
|
||||
compress: false,
|
||||
output: {
|
||||
beautify: true,
|
||||
indent_level: 2
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
input: "app/javascript/action_cable/index.js",
|
||||
output: {
|
||||
file: "app/assets/javascripts/action_cable.js",
|
||||
format: "umd",
|
||||
name: "ActionCable"
|
||||
},
|
||||
plugins: [
|
||||
resolve(),
|
||||
commonjs(),
|
||||
babel(),
|
||||
uglify(uglifyOptions)
|
||||
]
|
||||
}
|
1928
actioncable/yarn.lock
Normal file
1928
actioncable/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue