DEV Community

Cover image for Some notes on SharedWorkers
Chris How
Chris How

Posted on

Some notes on SharedWorkers

I recently needed to implement a shared worker in a project. Though they are super useful, there wasn't much info to be found in the usual places, so here are some pointers that might help searchers from the mysterious future.

Background

SharedWorkers are a special class of WebWorker that can be shared across multiple tabs, windows or other (regular) web workers.

In my application, I needed a process which would poll for new application events (for example, a customer completing a purchase), and show a notification (using the Notifications API) to logged in administrators (or more specifically, those logged in administrators who had chosen to receive notifications).

An administrator could have the application open in several tabs or windows, so it would be wasteful to have each tab polling for new events. I only wanted one notification per event, regardless of the number of open tabs or windows.

SharedWorker to the rescue! Each of the open tabs or windows shares a single worker, which polls in the background, and shows just one notification per new event.

Creating a shared worker with Vite

The first challenge was loading the shared worker in my Vite-based setup.

If you're running Vite in dev mode, Vite serves the script from a different domain and port (eg http://[::1]:5173/), which won't work, because shared workers must obey the same-origin policy.

I tried various Vite workarounds for web workers:

  • The official Vite web worker method doesn’t work for shared workers due to the same-origin policy requirement.
  • Blob URLs aren't supported for shared workers.
  • Inlining the worker as a base64 string doesn’t work because the browser treats them as different workers: fine for web workers, but not for shared workers.

In the end, I created a new route to serve the script either from the resources directory in dev, or the build directory in staging and live environments.

Route::addRoute('GET', '/notifications-shared-worker.js', function () {
    // If in dev environment, send the file from the resources folder
    if (app()->environment('local')) {
        return response()->file(resource_path('js/notificationWatcherWorker.js'), ['Content-Type' => 'text/javascript']);
    } else {
        // Otherwise, send the file from the public folder
        return response()->file(public_path('build/assets/notificationWatcherWorker.js'), ['Content-Type' => 'text/javascript']);
    }
});

Enter fullscreen mode Exit fullscreen mode

I then create the shared worker with that route as the URL:

const worker = new SharedWorker('/notifications-shared-worker.js');
Enter fullscreen mode Exit fullscreen mode

Debugging the Shared Worker

You'll quickly find that any syntax or runtime errors in your shared worker don't appear in your devtools. Nor do any console log/warn/info calls.

This one is easy, paste chrome://inspect/#workers into your URL bar, find the shared worker and click on 'inspect'. Now you have a devtools window just for the shared worker.

Communicating back to the main tab or window

To communicate back to the ‘parent’ tab, use the port.postMessage method, as described in the MDN SharedWorker documentation.

However, the example code only allows communication with the most recent ‘parent’ tab/window because it overwrites the communication port reference each time a parent connects.

Instead, store an array of ports, and add each new port to the array when a new ‘parent’ connects.


const ports = [];

onconnect = (e) => {
    // Get the port for this new connection
    const port = e.ports[0];
    // Add it to our array of ports
    ports.push(port);
...
}
Enter fullscreen mode Exit fullscreen mode

Then, send a message to all the parent pages like this:

ports.forEach(port => port.postMessage({ 
    message: "Hello, world!", 
    flavor: "vanilla" 
}));
Enter fullscreen mode Exit fullscreen mode

Top comments (0)