-
Notifications
You must be signed in to change notification settings - Fork 21.9k
Avoid ReferenceError exceptions if ActionCable is used in a web worker #34941
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Avoid ReferenceError exceptions if ActionCable is used in a web worker #34941
Conversation
Before this change, attempting to use ActionCable inside a web worker would result in an exception being thrown: ``` ReferenceError: window is not defined ``` By replacing the `window` reference with `self`, which is available in both a window context and a worker context, we can avoid this error. Ref: https://developer.mozilla.org/en-US/docs/Web/API/Window/self
Yeah, it's possible in the future that the ActionCable library itself could offer built-in support for easily running inside a worker. It might wise to wait a bit for that, though. If we start small by proceeding with these changes, we'll enable users to explore moving cables into a worker themselves, and then we'd be in a better position to see the best patterns emerge and bring them into ActionCable. |
IMO, it’s too opinionated to use Shared Worker by default (btw, AFAICS, it’s not supported by all major browsers yet). Maybe, optional support for |
That condition is pretty unclear out of context. Can it be extracted to an intention revealing helper? Can the test suite be updated to simulate running in a worker scope so we don't accidentally reference rails/actioncable/app/javascript/action_cable/index.js Lines 25 to 35 in a08827a
|
Agreed, I wouldn't suggest we make that the default, just an option. And yeah, |
Just curious: Would ActionCable run as-is in a web worker if you stubbed out self.window = self.document = self;
require("actioncable"); |
Those references don't prevent the usage in a worker because you can pass |
Right, but that's far from explicit, and as a reader of the code it's not clear why we can reference |
Are you thinking along the lines of: const documentInterfaceAvailable = (typeof document !== "undefined") or something else?
Yeah, this is something I'd like to do. I'm not sure the best way to go about it, but my first idea was to try to fork https://github.com/Joris-van-der-Wel/karma-mocha-webworker and have it use QUnit instead of mocha, since that's what the existing test suite uses. Didn't want that to hold up this PR, though |
Yeah. Initially I started making those two functions configurable like: -export function createConsumer(url) {
+const defaultOptions = { getConfig, createWebSocketURL }
+
+export function createConsumer(url, options = defaultOptions) {
if (url == null) {
- const urlConfig = getConfig("url")
+ const urlConfig = (options.getConfig || defaultOptions.getConfig)("url")
url = (urlConfig ? urlConfig : INTERNAL.default_mount_path)
}
- return new Consumer(createWebSocketURL(url))
+ return new Consumer((options.createWebSocketURL || defaultOptions.createWebSocketURL)(url))
} so that if you were using it in a web worker you could pass your own What if the ActionCable guide called it out with something like:
We could also add a comment above those methods warning that they don't work in a web worker. |
Can you give #34941 (comment) a try, please? I realize this is a small change, but it has long term maintenance implications, and I'd prefer to make no changes at all if there's a simple workaround. |
Something else that reveals the intention. I'd prefer to read
|
This allows ActionCable to be used in a web worker, where the `document` global is undefined. Previously, attempting to use ActionCable inside a web worker would result in this exception after you try to open a connection: ``` ReferenceError: document is not defined ``` The visibilitychange event won't ever get triggered in a worker, so adding the listener is effectively a no-op there. But the listener is mainly a convenience, rather than a critical piece of the javascript interface, so using ActionCable in a worker will still work. (And you could listen for visibilitychange yourself in a window script, then tell the worker to reconnect if you still want that behavior.)
af3278c
to
3949318
Compare
Sorry, I missed that comment before.
This works in isolation, but I would strongly recommend against it in a real application. Modifying global state like that to fake that the worker script is running in a window context will leak into all the other code in the worker. This is dangerous because it can break other library code used by the worker by tricking it into incorrectly assuming the worker is running in a window environment and using that assumption to access interfaces that aren't actually available.
I don't want to impose any long term maintenance commitment, so let's try to avoid that. How about we maintain that Rails doesn't officially support running ActionCable in a worker and that it could break at any time, while still avoiding the current
|
Great! That's a perfectly reasonable compromise. For some reason I thought the |
Mind updating the PR description to reflect the final change? It still describes your original approach using |
Updated the PR description! |
Thanks @javan! By the way, are you planning to attend RailsConf this year? Would love to buy you a drink for all your help 🍻 |
Not 100% sure yet, but I hope to be there! 🍻 |
Summary
Before this change, it was not possible to use ActionCable in a web worker because it would throw an error when trying to access
window
ordocument
, which are not available in theWorkerGlobalScope
.It may be desirable to offload WebSocket interaction to a worker to free up the main thread for other activity. But even in applications where the main thread can handle WebSocket interaction and other tasks just fine, there's still a benefit to running ActionCable in a worker: If it is common for your users to have multiple tabs of your application open at once, you can take advantage of the
SharedWorker
API to reduce load on your servers. With aSharedWorker
, multiple tabs can share a single WebSocket connection, meaning you can drastically reduce the number of active connections (if users commonly have 2 tabs open, this can cut the number of connections in half - if they have more than 2, it's even more significant).Fortunately, it's not too difficult to address the issues that prevent the current version of ActionCable from running in a worker. To support the worker context in addition to the window context, it's recommended to use
self
instead ofwindow
. So that's what the first change in this PR does. To avoid thedocument
ReferenceError
, I've removed the explicitdocument
receiver from theaddEventListener
andremoveEventListener
calls. Thevisibilitychange
event won't ever get triggered in a worker, so adding the listener is effectively a no-op there. But this listener is not critical for ActionCable to function properly; it's more of a "nice to have", so ActionCable is still fully functional even without that event handler getting invoked. With those two changes, ActionCable can run in a worker.Other Information
To provide a working example, I've created the
cable-in-web-worker
branch in my fork of @palkan's anycable_demo repository. In that branch I extracted the ActionCable interaction to a shared worker using the changes from this PR.