npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@remelondb/driver-web

v0.3.3

Published

remelonDB SqliteDriver for the browser (SQLite-WASM + OPFS in a Worker)

Readme

@remelondb/driver-web

The browser SqliteDriver: SQLite-WASM running in a dedicated Worker, persistent via the OPFS SyncAccessHandle pool VFS, reached over a postMessage RPC. This is the architecture the whole seam was designed around: OPFS sync-access handles exist only in workers, the main thread can only reach a worker asynchronously, hence the Promise-shaped driver contract everywhere.

Status: verified on OPFS in Chromium, Firefox, WebKit, and Safari

The worker-side server is transport-abstracted (Endpoint), so the test suite runs the exact same server code against real SQLite-WASM in-process under Node: driver → RPC → wasm SQLite, end to end, including the full stack (Database, models, observation, sync) on top. The storage: 'opfs' path additionally runs as a real-browser conformance suite on all three engines, plus real Safari via safaridriver (checklist below).

Usage

The recommended app bootstrap is the database manager — one shared open, retryable failure, takeover handled — with the React hook for lifecycle state:

import { createDatabaseManager, Database } from '@remelondb/core';
import { useDatabaseState } from '@remelondb/core/react';
import { WebSqliteDriver } from '@remelondb/driver-web';

const manager = createDatabaseManager({
  open: (onTakenOver) =>
    Database.open({
      driver: new WebSqliteDriver({
        shared: true,
        takeover: true,
        onTakenOver,
      }),
      schema,
      migrations,
      modelClasses: [Task],
      name: 'app.db',
    }),
});

// in a component: const { status, error } = useDatabaseState(manager)
// when status is 'ready': manager.database

Database.open is the primitive underneath — fine to call directly in tests and scripts; apps should let the manager own the lifecycle. The todo-sync example's frontend/src/db.ts is the working reference.

  • Persistence is never silently downgraded. The default storage: 'opfs' fails loudly if OPFS is unavailable. open() rejects with a typed OpfsUnavailableError (code: 'OPFS_UNAVAILABLE'), refused up front — before any worker/broker spawns — when OPFS is blocked: an old browser, a sandboxed iframe, Node, or Firefox private browsing / "never remember history" / blocked site data, where navigator.storage.getDirectory() throws SecurityError even over https. Catch it to degrade gracefully (show a message, or reopen with storage: 'memory'). Pass storage: 'memory' directly when non-persistence is intended (previews, tests).
  • A transient OPFS pool lock is retried with bounded backoff. If the browser still has not released it, open() rejects with a typed OpfsPoolHeldError (code: 'OPFS_POOL_HELD') whose diagnostic preserves the browser's final NoModificationAllowedError. Firefox may require a full browser restart after a page worker dies holding the pool.
  • The OPFS SAH-pool VFS needs no COOP/COEP headers (unlike the SharedArrayBuffer-based VFS).
  • The worker is spawned via new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' }) — Vite and comparable bundlers resolve this pattern and bundle the worker (including the wasm asset from @sqlite.org/sqlite-wasm).

Multi-tab usage

The OPFS SAH-pool VFS acquires exclusive sync access handles: one connection per database, full stop. A second tab that tries to open the same database fails loudly (consistent with the no-silent-downgrade rule) — it does not corrupt anything, does not silently fall back to memory, and the error says what is going on: the database is open in another tab.

Single-owner with takeover is built into the driver. Tabs coordinate through a Web Lock (remelondb:<name>) held for the connection's lifetime:

const driver = new WebSqliteDriver({
  takeover: true, // take the database from another tab
  onTakenOver: () => {
    // ...and learn when one takes it from us
    showBanner('This app was opened in another tab.');
  },
});
  • Default (takeover unset): opening a database another tab holds rejects with '<name>' is open in another tab or window.
  • takeover: true: open() steals the lock. The losing driver terminates its worker — which is what releases the pool's handles; in-flight statements there are abandoned, committed data is safe on disk — fires its onTakenOver callback, and every later call on it rejects with a clear error. The winning open() retries the handle acquisition for a bounded period, then reports OpfsPoolHeldError if the browser has not released it.
  • Environments without the Web Locks API (Node tests, non-secure contexts) skip coordination entirely and behave as before.
  • With shared mode active, takeover and onTakenOver are unused: every tab joins one owner, so there is nothing to take over. They apply where the driver falls back to single-tab mode.

Only one tab is active at a time, but the handoff is explicit and nothing can race.

The same rule rules out background sync on the web. A service worker woken by Web Push cannot open the database to sync: it either fails because a tab holds it, or takes it over and terminates that tab's worker. So Web Push can tell the user something changed; the data lands when the tab is next opened or focused. The Background Sync API is blocked for the same reason. Mobile has no such restriction — see when to sync.

For every tab live simultaneously, opt in to shared mode:

const driver = new WebSqliteDriver({ shared: true });

All tabs then route through one SharedWorker broker to a single connection: same-name opens share it instead of contending (no lock, no takeover), commits in any tab reach every other tab's record cache and observers, and db.write blocks are serialized across tabs by the broker so racing read-modify-write converges (see "The RPC protocol" below and docs/multi-tab.md for the design, including the ordering invariant; docs/multi-tab.qnt model-checks it). Where SharedWorker is unavailable (Chrome for Android), the option gracefully falls back to the single-owner behavior above — same API, same errors, the takeover UI simply becomes reachable again.

The broker releases its own OPFS pool before replacing a silent compute worker. If OpfsPoolHeldError still reaches the app, offer Retry; the holder may be another live context, and Firefox may require a full browser restart after an orphaned worker handle. Retry cannot safely steal from a live holder.

Sync also coordinates itself: synchronize runs only in the tab holding the broker's sync lease (renewed on each tick, inherited when the holder closes), so a naive per-tab sync interval is correct. The remaining sharp edges before shared mode becomes the default are the open questions in docs/multi-tab.md.

Bundlers (Vite)

When consuming this package from npm with Vite, exclude it from dependency prebundling, or the worker URLs resolve into .vite/deps where the worker files do not exist:

// vite.config.ts
export default defineConfig({
  optimizeDeps: {
    // prebundling rewrites the driver's worker URLs into .vite/deps,
    // where the worker files do not exist
    exclude: [
      '@remelondb/driver-web',
      '@remelondb/core',
      '@sqlite.org/sqlite-wasm',
    ],
  },
});

Shared mode needs @remelondb/driver-web 0.1.2 or newer.

A complete minimal consumer (registry tarballs, Vite, single-tab and shared mode) is scaffolded by scripts/vite-smoke.mjs — it runs in CI against every change and doubles as the reference setup.

The RPC protocol

Every request carries an id; the answering side replies with {id, ok, result} or {id, ok: false, error}. Requests and responses are structured-clonable plain data (src/protocol.ts is the source of truth). Who answers depends on the op:

| Op | Answered by | Purpose | | ----------------------------- | ----------- | ---------------------------------------------------------------- | | open | worker | open by name, report user_version (async: awaits pool install) | | close | worker | close the named connection | | query / execute | worker | one SELECT / one non-SELECT statement | | executeBatch | worker | atomic multi-statement transaction | | setUserVersion | worker | PRAGMA user_version after setup/migration | | destroy | worker | delete the database and sidecar files | | ping | worker | liveness probe (the broker checks its compute channel) | | releasePool | worker | broker retirement: close databases and pause the OPFS VFS | | acquireSlot / releaseSlot | broker | cross-tab write-block arbitration (never reach SQLite) | | publishChanges | broker | relay a commit's change set to the other tabs | | syncTurn | broker | sync-lease request: grant/renew for the holder, deny others |

In shared mode the broker additionally pushes three unsolicited control messages to tabs: {control: 'spawnWorker'} asks a tab to host the compute worker, {control: 'discardWorker'} terminates a late worker after another tab won the spawn race, and {control: 'externalChanges', name, changes} carries another tab's commit. A hosting tab transfers the worker channel back with {control: 'adoptWorkerPort'}. In dedicated mode only the worker-answered ops occur, over a direct Worker transport.

Layout

| Piece | Role | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | | src/protocol.ts | RPC message types + the Endpoint transport abstraction | | src/server.ts | worker-side server: connections by name, statement cache, boolean→0/1 binding, atomic batches with rollback, OPFS/memory storage resolution | | src/worker.ts | browser Worker entry (sqlite-wasm init + server) | | src/WebSqliteDriver.ts | main-thread driver: request/response correlation, seam contract |

Browser-verification checklist

  • [x] Full driver contract against real sqlite-wasm (Node, in-process endpoint): round-trip, batch rollback, boolean binding, user_version, error surfaces
  • [x] Full stack on the driver: Database + models + observation + sync
  • [x] Loud failure when OPFS is unavailable (no silent downgrade)
  • [x] storage: 'opfs' in real Chromium, Firefox, and WebKit (vitest browser mode + Playwright): the FULL conformance suite on OPFS, persistence, destroy() unlinking pool + journal files, and durability across worker termination (page-reload equivalent). Run: pnpm --filter @remelondb/driver-web test:browser (BROWSER=firefox / BROWSER=webkit for the others; WebKit needs the persistent context wired up in vitest.webkit-provider.ts — ephemeral WebKit contexts have no OPFS backing store. WebKit OPFS is macOS-only: the Linux GTK port doesn't ship the sync-access-handle APIs, so CI runs WebKit on a macOS runner)
  • [x] Real Safari via BROWSER=safari (webdriverio + safaridriver, macOS only): the full conformance suite, 51/51. One-time setup: enable "Allow remote automation" in Safari's Developer settings (sudo safaridriver --enable). Not headless — Safari can't. Only one automation session may exist; kill stray safaridriver processes if the session refuses to start.
  • [x] Vite production smoke: pnpm --filter @remelondb/driver-web smoke:vite. It covers worker and wasm loading in browser mode, packs the tarballs, scaffolds the reference Vite app, runs vite build and vite preview, and drives headless Chromium against the output. OPFS opens from the production bundle and data persists across a page reload.
  • [x] Multi-tab behavior: single-connection by design, documented below (see "Multi-tab usage")

Why browser tests run serially (fileParallelism: false)

The OPFS SAH pool is a single exclusive resource per origin — one worker owns it at a time (see "Multi-tab usage"). Vitest runs test files in parallel by default, so several *.browser.test.ts files then contend for that one pool. Two failures come out of that contention, both most visible on WebKit:

  • a recycled pool slot returns stale state, so a just-created table reads back missing (SQLITE_ERROR: no such table) or a fresh database reads as corrupt (SQLITE_NOTADB);
  • the single-owner takeover in sharedFallback.browser.test.ts never gets the pool and times out.

Running the files serially (each owns the pool uncontended) fixes both: the full suite is green across repeated WebKit runs with no retries. It is not a product bug — an application opens one long-lived database and never has two contexts race a cold pool this way. Ruled out as causes along the way: the request dispatcher (serialized, covered by server.concurrency.test.ts), pool capacity (fails at 512 too), unlink accumulation (pool stays at one file), and stale sqlite-wasm (on the latest, 3.53.0-build1). To see the raw flake, set fileParallelism back to true.