aboutsummaryrefslogtreecommitdiffstats
path: root/actioncable/app/javascript/action_cable/connection.js
diff options
context:
space:
mode:
authorRichard Macklin <richard.github@nrm.com>2018-01-20 23:33:32 -0800
committerRichard Macklin <richard.github@nrm.com>2018-11-02 08:41:05 -0700
commitc96139af71e6f7c36e25bccea6b05ccd9523531a (patch)
tree119448211713b6a172b301ab164dd54dd5718654 /actioncable/app/javascript/action_cable/connection.js
parent0eb6b86e9606cace49afba0b35ec18916c73646e (diff)
downloadrails-c96139af71e6f7c36e25bccea6b05ccd9523531a.tar.gz
rails-c96139af71e6f7c36e25bccea6b05ccd9523531a.tar.bz2
rails-c96139af71e6f7c36e25bccea6b05ccd9523531a.zip
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.
Diffstat (limited to 'actioncable/app/javascript/action_cable/connection.js')
-rw-r--r--actioncable/app/javascript/action_cable/connection.js248
1 files changed, 123 insertions, 125 deletions
diff --git a/actioncable/app/javascript/action_cable/connection.js b/actioncable/app/javascript/action_cable/connection.js
index 540f597303..4ad436f2c0 100644
--- a/actioncable/app/javascript/action_cable/connection.js
+++ b/actioncable/app/javascript/action_cable/connection.js
@@ -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
+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
- }
+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
- }
+ 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
- }
+ 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) }
- }
+ 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()
+ 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)
- }
+ getProtocol() {
+ return (this.webSocket ? this.webSocket.protocol : undefined)
+ }
- isOpen() {
- return this.isState("open")
- }
+ isOpen() {
+ return this.isState("open")
+ }
- isActive() {
- return this.isState("open", "connecting")
- }
+ isActive() {
+ return this.isState("open", "connecting")
+ }
- // Private
+ // Private
- isProtocolSupported() {
- return indexOf.call(supportedProtocols, this.getProtocol()) >= 0
- }
+ isProtocolSupported() {
+ return indexOf.call(supportedProtocols, this.getProtocol()) >= 0
+ }
- isState(...states) {
- return indexOf.call(states, this.getState()) >= 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()
- }
+ getState() {
+ if (this.webSocket) {
+ for (let state in WebSocket) {
+ if (WebSocket[state] === this.webSocket.readyState) {
+ return state.toLowerCase()
}
}
- return null
}
+ return null
+ }
- installEventHandlers() {
- for (let eventName in this.events) {
- const handler = this.events[eventName].bind(this)
- this.webSocket[`on${eventName}`] = handler
- }
+ 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() {}
- }
+ 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")
+}
+
+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")
}
+}
- return Connection
-
-})()
+export default Connection