CellB
@statewalker/cellb 0.1 · on npm

Getting started

CellB (@statewalker/cellb) is a Durable Objects runtime written in TypeScript. It runs the same Workers code in a browser and in Node. A Worker module and its Durable Object classes get synchronous SQLite storage, input and output gates, stubs withfetch and RPC, alarms, WebSockets, Worker Loaders and facets, all without modification.

Install

npm install @statewalker/cellb
npm install esbuild   # for loadNodeProject and bundleWorkerForBrowser

The published package is JavaScript with type declarations. Node 22.13 or later is needed fornode:sqlite. esbuild is an optional peer dependency: install it to load wrangler projects or bundle them for browsers. In a browser app, the bundler must handlenew Worker(new URL("…", import.meta.url)), which Vite does. The sources live in the cellb.dev monorepo.

Entry points

Callers differ by platform, so the package is split by platform, plus two entry points for bundling:

ImportWhat it gives
@statewalker/cellbThe isomorphic runtime: createCellRuntime, createProjectRuntime, DurableObject, WorkerEntrypoint, celldBucket, the object stores, links over ports, CellRouter
@statewalker/cellb/nodenodeSqliteOpener, sqliteObjectStore, loadNodeProject, D1 migrations, and celld's peer protocol
@statewalker/cellb/browserbrowserProjectRuntime; startProjectShards, joinProjectShards and serveProjectShard (shards in workers and tabs); installProjectRouter (the ServiceWorker side); sahpoolOpener, idbObjectStore, createProjectSite, serviceWorkerConnector
@statewalker/cellb/host-modulesFor bundlers other than CellB's own: the sources of cloudflare:workers, node:* and the Workers globals as CellB provides them
@statewalker/cellb/isolate-workerThe Dedicated Worker that runs a Worker Loader's Worker in a realm of its own

A Durable Object in Node

counter.ts
import { createCellRuntime, DurableObject } from "@statewalker/cellb";
import { nodeSqliteOpener } from "@statewalker/cellb/node";

class Counter extends DurableObject {
  increment(): number {
    const sql = this.ctx.storage.sql;
    sql.exec("CREATE TABLE IF NOT EXISTS c (id INTEGER PRIMARY KEY, n INTEGER)");
    sql.exec("INSERT INTO c (id, n) VALUES (1, 1) ON CONFLICT(id) DO UPDATE SET n = n + 1");
    return sql.exec<{ n: number }>("SELECT n FROM c WHERE id = 1").one().n;
  }
}

const runtime = createCellRuntime({
  worker: {
    Counter,
    default: {
      fetch: async (req: Request, env: any) =>
        Response.json({ n: await env.COUNTER.getByName(new URL(req.url).pathname).increment() }),
    },
  },
  durableObjects: { COUNTER: { className: "Counter" } },
  openSql: nodeSqliteOpener("./data"), // a file per cell; () keeps them in memory
});

const res = await runtime.fetch(new Request("http://local/counter/a")); // {"n":1}
await runtime.close();

A runtime is a plain (Request) ⇒ Response handler. Mountruntime.fetch on node:http, a ServiceWorker or a test, and callruntime.close() when you are done. Closing takes final backups and closes every database.

The same project in a browser

In a browser, cells live in SQLite WASM inside a Dedicated Worker, and requests arrive through a ServiceWorker.

worker.ts
import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
import { browserProjectRuntime } from "@statewalker/cellb/browser";

const host = await browserProjectRuntime({
  config,            // the parsed wrangler.jsonc
  code: { bundle },  // main, bundled for browsers (or { site } for the in-browser HTTP server)
  sqlite3: await sqlite3InitModule(),
  pool: "my-app",    // the OPFS SQLite pool; one per worker
  filesDir: "my-app-files",
});
const res = await host.runtime.fetch(new Request("http://browser/"));

Where the cells' data lives is a choice of the bucket option:

StorageWhat it does
an OPFS pool (default)One SQLite file per cell in the worker's opfs-sahpool pool. Needs no COOP/COEP headers; a pool belongs to one worker
{ ltx, node }Each cell as celld keeps it: a snapshot and one LTX file per commit in OPFS, read page by page, so a cell's memory is its page cache. Compacts itself; needs cross-origin isolation
{ idb, node }SQLite in memory, persisted through page-level logs in IndexedDB, whose conditional writes are atomic across tabs and workers
{ s3, node }A celld fleet bucket on S3, shared with celld and Node nodes

Join a celld fleet

Give the runtime a celldBucket and it becomes a node of a celld fleet. It uses the same bucket layout, ownership records and LTX log, so a cell can move between celld and CellB with no conversion.

const { runtime, unsupported } = await loadNodeProject("./counter", {
  openSql: nodeSqliteOpener(),
  files: new MemFilesApi(),
  storage: celldBucket(sqliteObjectStore("./counter/.celld/dev/objects.sqlite3"), { node: "cellb-1" }),
});
console.log(unsupported); // configuration CellB does not support, e.g. []

Next steps