Skip to content

Instantly share code, notes, and snippets.

@csexton
Created October 19, 2017 19:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save csexton/17dc1b1035e8faf83e837d758605c234 to your computer and use it in GitHub Desktop.
Save csexton/17dc1b1035e8faf83e837d758605c234 to your computer and use it in GitHub Desktop.
(function() {
(function() {
(function() {
var slice = [].slice;
this.ActionCable = {
INTERNAL: {
"message_types": {
"welcome": "welcome",
"ping": "ping",
"confirmation": "confirm_subscription",
"rejection": "reject_subscription"
},
"default_mount_path": "/cable",
"protocols": ["actioncable-v1-json", "actioncable-unsupported"]
},
WebSocket: window.WebSocket,
logger: window.console,
createConsumer: function(url) {
var ref;
if (url == null) {
url = (ref = this.getConfig("url")) != null ? ref : this.INTERNAL.default_mount_path;
}
return new ActionCable.Consumer(this.createWebSocketURL(url));
},
getConfig: function(name) {
var element;
element = document.head.querySelector("meta[name='action-cable-" + name + "']");
return element != null ? element.getAttribute("content") : void 0;
},
createWebSocketURL: function(url) {
var a;
if (url && !/^wss?:/i.test(url)) {
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() {
return this.debugging = true;
},
stopDebugging: function() {
return this.debugging = null;
},
log: function() {
var messages, ref;
messages = 1 <= arguments.length ? slice.call(arguments, 0) : [];
if (this.debugging) {
messages.push(Date.now());
return (ref = this.logger).log.apply(ref, ["[ActionCable]"].concat(slice.call(messages)));
}
}
};
}).call(this);
}).call(this);
var ActionCable = this.ActionCable;
(function() {
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
ActionCable.ConnectionMonitor = (function() {
var clamp, now, secondsSince;
ConnectionMonitor.pollInterval = {
min: 3,
max: 30
};
ConnectionMonitor.staleThreshold = 6;
function ConnectionMonitor(connection) {
this.connection = connection;
this.visibilityDidChange = bind(this.visibilityDidChange, this);
this.reconnectAttempts = 0;
}
ConnectionMonitor.prototype.start = function() {
if (!this.isRunning()) {
this.startedAt = now();
delete this.stoppedAt;
this.startPolling();
document.addEventListener("visibilitychange", this.visibilityDidChange);
return ActionCable.log("ConnectionMonitor started. pollInterval = " + (this.getPollInterval()) + " ms");
}
};
ConnectionMonitor.prototype.stop = function() {
if (this.isRunning()) {
this.stoppedAt = now();
this.stopPolling();
document.removeEventListener("visibilitychange", this.visibilityDidChange);
return ActionCable.log("ConnectionMonitor stopped");
}
};
ConnectionMonitor.prototype.isRunning = function() {
return (this.startedAt != null) && (this.stoppedAt == null);
};
ConnectionMonitor.prototype.recordPing = function() {
return this.pingedAt = now();
};
ConnectionMonitor.prototype.recordConnect = function() {
this.reconnectAttempts = 0;
this.recordPing();
delete this.disconnectedAt;
return ActionCable.log("ConnectionMonitor recorded connect");
};
ConnectionMonitor.prototype.recordDisconnect = function() {
this.disconnectedAt = now();
return ActionCable.log("ConnectionMonitor recorded disconnect");
};
ConnectionMonitor.prototype.startPolling = function() {
this.stopPolling();
return this.poll();
};
ConnectionMonitor.prototype.stopPolling = function() {
return clearTimeout(this.pollTimeout);
};
ConnectionMonitor.prototype.poll = function() {
return this.pollTimeout = setTimeout((function(_this) {
return function() {
_this.reconnectIfStale();
return _this.poll();
};
})(this), this.getPollInterval());
};
ConnectionMonitor.prototype.getPollInterval = function() {
var interval, max, min, ref;
ref = this.constructor.pollInterval, min = ref.min, max = ref.max;
interval = 5 * Math.log(this.reconnectAttempts + 1);
return Math.round(clamp(interval, min, max) * 1000);
};
ConnectionMonitor.prototype.reconnectIfStale = function() {
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()) {
return ActionCable.log("ConnectionMonitor skipping reopening recent disconnect");
} else {
ActionCable.log("ConnectionMonitor reopening");
return this.connection.reopen();
}
}
};
ConnectionMonitor.prototype.connectionIsStale = function() {
var ref;
return secondsSince((ref = this.pingedAt) != null ? ref : this.startedAt) > this.constructor.staleThreshold;
};
ConnectionMonitor.prototype.disconnectedRecently = function() {
return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold;
};
ConnectionMonitor.prototype.visibilityDidChange = function() {
if (document.visibilityState === "visible") {
return setTimeout((function(_this) {
return function() {
if (_this.connectionIsStale() || !_this.connection.isOpen()) {
ActionCable.log("ConnectionMonitor reopening stale connection on visibilitychange. visbilityState = " + document.visibilityState);
return _this.connection.reopen();
}
};
})(this), 200);
}
};
now = function() {
return new Date().getTime();
};
secondsSince = function(time) {
return (now() - time) / 1000;
};
clamp = function(number, min, max) {
return Math.max(min, Math.min(max, number));
};
return ConnectionMonitor;
})();
}).call(this);
(function() {
var i, message_types, protocols, ref, supportedProtocols, unsupportedProtocol,
slice = [].slice,
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
ref = ActionCable.INTERNAL, message_types = ref.message_types, protocols = ref.protocols;
supportedProtocols = 2 <= protocols.length ? slice.call(protocols, 0, i = protocols.length - 1) : (i = 0, []), unsupportedProtocol = protocols[i++];
ActionCable.Connection = (function() {
Connection.reopenDelay = 500;
function Connection(consumer) {
this.consumer = consumer;
this.open = bind(this.open, this);
this.subscriptions = this.consumer.subscriptions;
this.monitor = new ActionCable.ConnectionMonitor(this);
this.disconnected = true;
}
Connection.prototype.send = function(data) {
if (this.isOpen()) {
this.webSocket.send(JSON.stringify(data));
return true;
} else {
return false;
}
};
Connection.prototype.open = function() {
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 != null) {
this.uninstallEventHandlers();
}
this.webSocket = new ActionCable.WebSocket(this.consumer.url, protocols);
this.installEventHandlers();
this.monitor.start();
return true;
}
};
Connection.prototype.close = function(arg) {
var allowReconnect, ref1;
allowReconnect = (arg != null ? arg : {
allowReconnect: true
}).allowReconnect;
if (!allowReconnect) {
this.monitor.stop();
}
if (this.isActive()) {
return (ref1 = this.webSocket) != null ? ref1.close() : void 0;
}
};
Connection.prototype.reopen = function() {
var error;
ActionCable.log("Reopening WebSocket, current state is " + (this.getState()));
if (this.isActive()) {
try {
return this.close();
} catch (error1) {
error = error1;
return 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() {
var ref1;
return (ref1 = this.webSocket) != null ? ref1.protocol : void 0;
};
Connection.prototype.isOpen = function() {
return this.isState("open");
};
Connection.prototype.isActive = function() {
return this.isState("open", "connecting");
};
Connection.prototype.isProtocolSupported = function() {
var ref1;
return ref1 = this.getProtocol(), indexOf.call(supportedProtocols, ref1) >= 0;
};
Connection.prototype.isState = function() {
var ref1, states;
states = 1 <= arguments.length ? slice.call(arguments, 0) : [];
return ref1 = this.getState(), indexOf.call(states, ref1) >= 0;
};
Connection.prototype.getState = function() {
var ref1, state, value;
for (state in WebSocket) {
value = WebSocket[state];
if (value === ((ref1 = this.webSocket) != null ? ref1.readyState : void 0)) {
return state.toLowerCase();
}
}
return null;
};
Connection.prototype.installEventHandlers = function() {
var eventName, handler;
for (eventName in this.events) {
handler = this.events[eventName].bind(this);
this.webSocket["on" + eventName] = handler;
}
};
Connection.prototype.uninstallEventHandlers = function() {
var eventName;
for (eventName in this.events) {
this.webSocket["on" + eventName] = function() {};
}
};
Connection.prototype.events = {
message: function(event) {
var identifier, message, ref1, type;
if (!this.isProtocolSupported()) {
return;
}
ref1 = JSON.parse(event.data), identifier = ref1.identifier, message = ref1.message, type = ref1.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() {
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(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() {
return ActionCable.log("WebSocket onerror event");
}
};
return Connection;
})();
}).call(this);
(function() {
var slice = [].slice;
ActionCable.Subscriptions = (function() {
function Subscriptions(consumer) {
this.consumer = consumer;
this.subscriptions = [];
}
Subscriptions.prototype.create = function(channelName, mixin) {
var channel, params, subscription;
channel = channelName;
params = typeof channel === "object" ? channel : {
channel: channel
};
subscription = new ActionCable.Subscription(this.consumer, params, mixin);
return this.add(subscription);
};
Subscriptions.prototype.add = function(subscription) {
this.subscriptions.push(subscription);
this.consumer.ensureActiveConnection();
this.notify(subscription, "initialized");
this.sendCommand(subscription, "subscribe");
return subscription;
};
Subscriptions.prototype.remove = function(subscription) {
this.forget(subscription);
if (!this.findAll(subscription.identifier).length) {
this.sendCommand(subscription, "unsubscribe");
}
return subscription;
};
Subscriptions.prototype.reject = function(identifier) {
var i, len, ref, results, subscription;
ref = this.findAll(identifier);
results = [];
for (i = 0, len = ref.length; i < len; i++) {
subscription = ref[i];
this.forget(subscription);
this.notify(subscription, "rejected");
results.push(subscription);
}
return results;
};
Subscriptions.prototype.forget = function(subscription) {
var s;
this.subscriptions = (function() {
var i, len, ref, results;
ref = this.subscriptions;
results = [];
for (i = 0, len = ref.length; i < len; i++) {
s = ref[i];
if (s !== subscription) {
results.push(s);
}
}
return results;
}).call(this);
return subscription;
};
Subscriptions.prototype.findAll = function(identifier) {
var i, len, ref, results, s;
ref = this.subscriptions;
results = [];
for (i = 0, len = ref.length; i < len; i++) {
s = ref[i];
if (s.identifier === identifier) {
results.push(s);
}
}
return results;
};
Subscriptions.prototype.reload = function() {
var i, len, ref, results, subscription;
ref = this.subscriptions;
results = [];
for (i = 0, len = ref.length; i < len; i++) {
subscription = ref[i];
results.push(this.sendCommand(subscription, "subscribe"));
}
return results;
};
Subscriptions.prototype.notifyAll = function() {
var args, callbackName, i, len, ref, results, subscription;
callbackName = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
ref = this.subscriptions;
results = [];
for (i = 0, len = ref.length; i < len; i++) {
subscription = ref[i];
results.push(this.notify.apply(this, [subscription, callbackName].concat(slice.call(args))));
}
return results;
};
Subscriptions.prototype.notify = function() {
var args, callbackName, i, len, results, subscription, subscriptions;
subscription = arguments[0], callbackName = arguments[1], args = 3 <= arguments.length ? slice.call(arguments, 2) : [];
if (typeof subscription === "string") {
subscriptions = this.findAll(subscription);
} else {
subscriptions = [subscription];
}
results = [];
for (i = 0, len = subscriptions.length; i < len; i++) {
subscription = subscriptions[i];
results.push(typeof subscription[callbackName] === "function" ? subscription[callbackName].apply(subscription, args) : void 0);
}
return results;
};
Subscriptions.prototype.sendCommand = function(subscription, command) {
var identifier;
identifier = subscription.identifier;
return this.consumer.send({
command: command,
identifier: identifier
});
};
return Subscriptions;
})();
}).call(this);
(function() {
ActionCable.Subscription = (function() {
var extend;
function Subscription(consumer, params, mixin) {
this.consumer = consumer;
if (params == null) {
params = {};
}
this.identifier = JSON.stringify(params);
extend(this, mixin);
}
Subscription.prototype.perform = function(action, data) {
if (data == null) {
data = {};
}
data.action = action;
return this.send(data);
};
Subscription.prototype.send = function(data) {
return this.consumer.send({
command: "message",
identifier: this.identifier,
data: JSON.stringify(data)
});
};
Subscription.prototype.unsubscribe = function() {
return this.consumer.subscriptions.remove(this);
};
extend = function(object, properties) {
var key, value;
if (properties != null) {
for (key in properties) {
value = properties[key];
object[key] = value;
}
}
return object;
};
return Subscription;
})();
}).call(this);
(function() {
ActionCable.Consumer = (function() {
function Consumer(url) {
this.url = url;
this.subscriptions = new ActionCable.Subscriptions(this);
this.connection = new ActionCable.Connection(this);
}
Consumer.prototype.send = function(data) {
return this.connection.send(data);
};
Consumer.prototype.connect = function() {
return this.connection.open();
};
Consumer.prototype.disconnect = function() {
return this.connection.close({
allowReconnect: false
});
};
Consumer.prototype.ensureActiveConnection = function() {
if (!this.connection.isActive()) {
return this.connection.open();
}
};
return Consumer;
})();
}).call(this);
}).call(this);
if (typeof module === "object" && module.exports) {
module.exports = ActionCable;
} else if (typeof define === "function" && define.amd) {
define(ActionCable);
}
}).call(this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment