Using a shared Worker (instead of SharedWorker) #81
Replies: 6 comments 26 replies
-
Wow, this is great work.
I think the biggest drawback here is related to transaction management. You'd need to do some locking to exclude one tab (or worker) from starting a transaction while another tab has an open transaction. If one tab has a long running transaction then that could starve other tabs or workers. Each worker/tab having their own connection allows long-ish read transactions to at least time-slice their reads so each reader can jointly make progress. A transaction locking mechanism over a single connection would preclude this. Maybe this drawback isn't that big of a deal though. Were you thinking of other downsides to only having a single DB connection? I've been assuming that many connections doesn't really buy much in the browser since:
|
Beta Was this translation helpful? Give feedback.
-
One of the advantages of multiplexing requests from different contexts onto a single database connection is that you can set
This means that sharing one connection could be faster even for a VFS that would allow multiple connections, depending on your workload. For example, I measured how fast IDBBatchAtomicVFS is on repeating a small write transaction (one update and one insert) as fast as possible on my aging Mac mini:
This is a decent increase in performance. At least part of the reason is that SQLite doesn't need to make a round trip to the file system to make sure that nothing has changed in its cache. IDBBatchAtomicVFS has a "relaxed" durability mode that lets you trade durability for performance. As a refresher, durability guarantees that once a write transaction completes it is stored on persistent media. Relaxing durability means that in some cases when service is interrupted (e.g. crash, power failure, etc.) one or more of the most recent transactions may be missing on reconnection. Durability is critically important for some applications and not very important for others. How does performance compare if we relax durability on IDBBatchAtomicVFS?
Wow! What's happening here with exclusive mode? What's happening is we're not waiting for IndexedDB at all. We don't wait for writes because we're allowing relaxed durability, and we don't wait for reads because the data is in the cache. No round trips to storage. Effectively we're streaming data to IndexedDB. This is a pretty interesting option if your application can tolerate reduced durability. Also note that IDBBatchAtomicVFS should work in a SharedWorker so it wouldn't need to be a migrating service (but it could be in case it doesn't work in SharedWorker for some reason). |
Beta Was this translation helpful? Give feedback.
-
Was this just a case in the past?
Returns an access handle in a shared worker context in both Firefox and Chrome. Can't seem to find the SharedWorker logs in Safari. |
Beta Was this translation helpful? Give feedback.
-
I have this successfully working for the demo worker but would like to use the same system to set up a second, distinct worker ( // Connect Worker and SharedService.
const worker = new Worker('./ahp-worker.js', { type: 'module' });
const sharedService = new SharedService(SHARED_SERVICE_NAME, async () => {
const providerPort = await new Promise(resolve => {
worker.addEventListener('message', event => {
resolve(event.ports[0]);
}, { once: true });
worker.postMessage(null);
});
return providerPort;
});
sharedService.activate();
const two = new Worker('./separate-non-sqlite-worker2.js', { type: 'module' });
const sharedService2 = new SharedService(SHARED_SERVICE_NAME + "2", async () => {
const providerPort = await new Promise(resolve => {
two.addEventListener('message', event => {
resolve(event.ports[0]);
}, { once: true });
two.postMessage(null);
});
return providerPort;
});
sharedService2.activate(); That I would be able to use sharedService2 Is this probably something to do with the |
Beta Was this translation helpful? Give feedback.
-
I've built a basic library that does this (for the purposes of using sqlite wasm or wa-sqlite), basically a backend agnostic comlink. Generifying it now and I'll probably just throw it on github open source. One layer does the processing, the other layer negotiates communication channels, so you can plug in a simple tab->shared/dedicated worker messaging channel, a dumb non-optimized broadcast channel or a more advanced channel that manages a migrating worker and creates/closes new channels as needed. Also supports observables so you can subscribe to database updates. I'm doing this with sqlite and it works great, broadcasts out new query results when something happens to the db, and since the comparison logic is in the worker it doesn't bog down the main thread. |
Beta Was this translation helpful? Give feedback.
-
Hi rhashimoto, thanks for this demo, it's excellent, but I have a quick question. Could this be done with just broadcast channels? If communication between the main and worker threads was done via broadcast channels, wouldn't that be simpler than setting up the shared worker and passing message ports around? We'd use web locks to establish the Providing tab and only that one would respond to any message requests. I haven't actually implemented my suggestion - I have successfully implemented your solution with some modest modifications and it works great, thanks! 😊 - but I'm just wondering if there is a simpler way to do it. It would also be good as shared workers aren't universally supported. |
Beta Was this translation helpful? Give feedback.
-
TL;DR It's possible to share an OPFS database connection across multiple browser tabs. There's a toy demo page to show how this could be done, working around the lack of OPFS in SharedWorker. I think this will turn out to be the best way to use OPFS.
Earlier I outlined an access handle pooling approach to take advantage of the new more synchronous access handles of the Origin Private File System API (OPFS). The result is a completely synchronous VFS that doesn't need Asyncify or SharedArrayBuffer to bridge to asynchronous calls. A proof-of-concept implementation works quite well - I think this is as fast as OPFS can go, which is also faster than IndexedDB except for write transaction overhead and perhaps on concurrent reads. The main drawback of this VFS is it only supports one database connection, and while multiple browser tabs could detach and reattach their VFS cooperatively, that would be expensive, probably prohibitively expensive. What can we do if we want to support access from multiple contexts?
An alternative way to support multiple tabs accessing the same database without detach/attach on every transaction is to multiplex requests from tabs onto a single connection, i.e. handling arbitration at the application level instead of inside SQLite. The question is where would the database live? Ideally the database could go into a SharedWorker but unfortunately that won't work because OPFS FileSystemSyncAccessHandle isn't available in SharedWorker. In order to share access to an OPFS-based SQLite database, the database has to live in a regular Worker belonging to one of the tabs.
Sharing a Worker on one tab with other same-origin tabs seems like a real headache. How do you decide which tab is the one that exposes its Worker? What happens if the user closes that tab? Fortunately, however, getting this to work turns out to be easier than expected if you take advantage of some of the newer browser APIs.
There's a proof of concept for sharing a simple arithmetic service across multiple tabs without putting the service in SharedWorker at this link (source). Try it by opening the page on a few browser tabs. The first tab you open will initially be designated as the service provider. If you close or reload that tab, another one will automatically become the provider. Clicking the buttons will make calls to the service from the clicked tab to the provider tab (including when both are the same tab). Use the slow operation button to verify that overlapping calls works, and that an exception is thrown if the service moves during a call (a real app would retry on this exception). Going from this toy page to a shared database connection shouldn't be difficult (
proof left as an exercise for the readerdemos).How does this work? It does actually use a small SharedWorker to pass MessagePorts between tabs, but it is not involved with calls to the service (an alternative implementation replacing SharedWorker with a service worker is linked later in this thread). Web Locks turn out to be a great way to watch the lifetime of a tab and initiate the migration of the service to a new tab. And BroadcastChannel tells all the tabs to execute that migration. Those mechanics are only a couple hundred lines of code, and that includes providing a (simplified) Comlink-style proxy so the application doesn't have to deal with raw messages.
The OPFS access handle pooling approach now works on all the major browsers. With this option to support multiple browser tabs, I think this is
going to bethe best way to implement an OPFS-backed database. IndexedDB will likely remain superior for applications prioritizing lower write transaction overhead, the option to trade durability for performance, concurrent reads, and browser support. Everything else can enjoy OPFS faster I/O, no Asyncify, no COOP/COEP headers, and never invalidating the read cache.Beta Was this translation helpful? Give feedback.
All reactions