Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
84eeefc
feat: seam platform APIs for the browser runtime
cardmagic Aug 22, 2026
384d347
feat: add the SQLite WASM database adapter
cardmagic Aug 22, 2026
cf78e48
feat: host the full runtime in a browser worker
cardmagic Aug 22, 2026
6c5a0be
feat: finish the browser runtime plan (M3, M4)
cardmagic Aug 22, 2026
fd73e00
fix: address the first Greptile review round
cardmagic Aug 23, 2026
31f7801
test: skip tab host suite where Web Locks is absent
cardmagic Aug 23, 2026
fc69964
fix: enforce transaction deadlines without ambient context
cardmagic Aug 23, 2026
2a43922
docs: cover the browser runtime across the doc set
cardmagic Aug 23, 2026
064174b
Merge remote-tracking branch 'origin/main' into worktree-browser-runt…
cardmagic Aug 23, 2026
4a71f75
release: date 0.14.0 and fold the browser runtime notes
cardmagic Aug 23, 2026
31010e2
feat: make multi-tab hosting transparent at the database seam
cardmagic Aug 23, 2026
bf5bc38
test: skip the shared adapter suite where Web Locks is absent
cardmagic Aug 23, 2026
7f5cd7b
feat: stage sync intents with a fluent mirror() proxy
cardmagic Aug 23, 2026
cf00c74
refactor: rename the sync bridge family to mirror
cardmagic Aug 23, 2026
b523e0b
docs: show the mirror ingest inside its HTTP route
cardmagic Aug 23, 2026
5fdf2b7
refactor: rename the mirror family to transmit
cardmagic Aug 23, 2026
64a89be
test: prove the transmit family on PostgreSQL and MySQL
cardmagic Aug 23, 2026
acdcba5
fix: accept transmit envelopes without arguments
cardmagic Aug 23, 2026
db348cd
test: consume the shared transmit fixtures
cardmagic Aug 23, 2026
78f49ea
style: format the fixture file
cardmagic Aug 23, 2026
e3ac8cd
docs: track the transmit family as a shared capability
cardmagic Aug 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 77 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,82 @@
# Changelog

## 0.14.0 - 2026-08-18
## 0.14.0 - 2026-08-22

- Add `solid-objects/database/shared-sqlite-wasm`, the transparent
multi-tab database. Every tab runs an ordinary
`configure -> install -> ref` flow against the same shared database; the
adapter elects one holder per origin with the Web Locks API, sends every
other tab's SQL over a `BroadcastChannel` session to the holder, and
fails over onto the same OPFS state when the holder's tab dies. The
runtime's leases and fencing arbitrate the tabs' workers exactly as they
arbitrate Node processes. A Playwright test proves plain actor references
incrementing one durable counter from two tabs with failover.
- Add `solid-objects/browser/tab-host`, the multi-tab host that completes
milestone M3 of the in-browser runtime plan
([#17](https://github.com/cardmagic/solid-objects-js/issues/17)). Every
tab starts a candidate host; the Web Locks API elects one leader per
origin, and only the leader opens the database and runs the runtime.
Tabs invoke actors through a `BroadcastChannel` client that retries with
idempotent request ids. When the leader's tab dies, the lock releases,
the next host promotes, and the runtime continues from the same OPFS
state. A Playwright test proves shared state across two tabs and
failover after the leader closes.
- Share the transmit wire contract with the Ruby gem. The golden fixture
file `compatibility/transmit-envelopes.json` is committed to both
repositories with a consuming test on each side; `receiveTransmitEnvelope`
now defaults a missing `arguments` to an empty object, matching the Ruby
ingest and the staging side.
- Add `solid-objects/transmit`, milestone M4 of the plan. An actor
stages a transmit intent with `this.transmit().operation(arguments)` (or with
`emit(TRANSMIT_EFFECT, ...)` for a different target) in the same
transaction as its state change. `registerTransmit` drains the outbox
with at-least-once delivery and per-actor order (an ordered drain up to
the claimed effect's mailbox sequence), and `receiveTransmitEnvelope` gives
the server an idempotent ingest keyed on the effect id. Vitest covers
order under transmit failures, replay deduplication, and recovery after
an offline period; a Playwright test drains a browser outbox into the
Node server runtime.
- Change `TurnContextStore` to synchronous scoping. The store no longer
stays set across `await` boundaries, so an open transaction scope cannot
leak into interleaved tasks and trip the inside-transaction guards. In
the browser those guards are best-effort; Node keeps full
`AsyncLocalStorage` semantics.
- Retry OPFS SAH pool acquisition. The upstream module caches a failed
initialization; the adapter now passes `forceReinitIfPreviouslyFailed`,
so a new leader can claim the pool after the old tab dies.

- Add a platform seam for async context propagation
(`src/platform/context-store.ts`). Shared modules no longer import
`node:async_hooks` directly. Node entry points register an
`AsyncLocalStorage` factory. A `TurnContextStore` gives a browser host a
turn-scoped store for serialized actor turns. This is milestone M1 of the
in-browser runtime plan
([#17](https://github.com/cardmagic/solid-objects-js/issues/17)).
- Route UUID generation through `src/platform/uuid.ts`, which uses the
standard `crypto.randomUUID()`. Shared modules no longer import
`node:crypto`.
- Add `check:browser-imports` to `pnpm run check`. The script walks the
import graph of the browser-safe modules and fails when a `node:` module
or a server-only driver reaches that graph.
- Add `solid-objects/browser/host`, the entry point for a runtime host
inside a browser worker. An import registers the browser platform: a
turn-scoped context store and a browser host identity. The module
re-exports the core runtime API and the WASM adapter. A Playwright test
runs the full runtime in a Chromium module worker and proves durable
actor state across a page reload. This is the first stage of milestone M3
([#17](https://github.com/cardmagic/solid-objects-js/issues/17)).
- Replace the `Buffer.byteLength` payload size check with `TextEncoder`, so
serialization works without the Node `Buffer` global.
- Route the process identity (hostname, process id, runtime version)
through `src/platform/host-identity.ts`. The repository no longer imports
`node:os` or reads `process.pid` directly.
- Add `solid-objects/database/sqlite-wasm`, a browser-safe `Database`
adapter on `@sqlite.org/sqlite-wasm` (an optional peer dependency). The
full runtime passes its round-trip test against this adapter in Node, and
a Playwright test proves transactions, rollback, and OPFS persistence
across a page reload in Chromium. This is milestone M2 of the in-browser
runtime plan
([#17](https://github.com/cardmagic/solid-objects-js/issues/17)).

- Add `runtime.enqueueInternalMessage()` and
`runtime.enqueueInternalMessageInTransaction(connection, options)`, public
Expand Down
72 changes: 66 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,16 @@ Solid Objects keeps the state, the queued operations, the retries, the
reminders, the effects, and the realtime invalidations in the database the
application already operates.

The same runtime also runs inside a browser worker on SQLite WASM, with
durable actor state in the origin's private file system. See
[Solid Objects in the browser](#solid-objects-in-the-browser).

> **Early release:** the correctness core has automated coverage. That coverage
> includes the supported databases, the Chromium browser client, process
> recovery, and the packaged artifacts. The TypeScript implementation is still
> new. There is one deployed first-party reference application. There is no
> measured scale and no third-party production use yet. Read the
> includes the supported databases, the Chromium browser client, the browser
> runtime, process recovery, and the packaged artifacts. The TypeScript
> implementation is still new. There is one deployed first-party reference
> application. There is no measured scale and no third-party production use
> yet. Read the
> [delivery boundaries](#delivery-boundaries) before you use it for important
> data.

Expand Down Expand Up @@ -284,6 +289,59 @@ personalized payloads, and framework-neutral component refresh. Applications
provide authentication, WebSocket transport, and rendering. See the
[browser protocol](docs/browser-protocol.md) and [authorization guide](docs/authorization.md).

## Solid Objects in the browser

The full runtime runs inside a browser module worker. Actors look exactly
like they do in Node; the database is SQLite WASM, and persistent storage
lives in the origin's private file system (OPFS), so actor state survives
page reloads.

```javascript
import { Actor, configure, sharedSqliteWasm } from "solid-objects/browser/host"

class Counter extends Actor {
static actorType = "Counter"

count = 0

increment({ amount = 1 } = {}) {
this.count += amount
return this.count
}
}

const runtime = configure({
database: sharedSqliteWasm({ path: "app.db" }),
authorizeMessage: () => true,
authorizeQuery: () => true,
})
await runtime.install()

await Counter.ref("page-hits").increment()
```

That code runs identically in every tab. `sharedSqliteWasm` elects one
database holder per origin through the Web Locks API, carries the other
tabs' SQL to it over a `BroadcastChannel`, and fails over onto the same
durable state when the holder's tab dies. Use `sqliteWasm` directly for a
single dedicated worker.

Two companions complete the local-first story:

- `solid-objects/browser/tab-host` runs one runtime for all tabs when the
application prefers request-level routing: the leader's worker executes
every operation, and other tabs invoke through a `BroadcastChannel` client
by name.
- `solid-objects/transmit` drains the transactional effects outbox to a
server runtime with at-least-once delivery, per-actor order, and an
idempotent server ingest, so offline writes reconcile when the network
returns.

The wire shapes are documented in the
[browser protocol](docs/browser-protocol.md), the API in the
[public API reference](docs/api.md), and the platform boundaries in
[supported versions](docs/support.md).

## Comparison

These systems solve different coordination problems. The table describes their
Expand Down Expand Up @@ -322,8 +380,10 @@ and operational data access.
- TypeScript 5.9 or newer for TypeScript applications
- SQLite through `node:sqlite`, PostgreSQL 14 or newer, or MySQL 8.0 or newer
with InnoDB
- optional `pg`, `mysql2`, or `redis` peer dependency only for the selected
adapter
- optional `pg`, `mysql2`, `redis`, or `@sqlite.org/sqlite-wasm` peer
dependency only for the selected adapter
- for the browser runtime: a browser with OPFS for persistent storage and the
Web Locks API for the multi-tab host

[Supported versions](docs/support.md) records the exact CI matrix and the
boundaries.
Expand Down
89 changes: 89 additions & 0 deletions compatibility/transmit-envelopes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{
"description": "Golden transmit envelopes shared by solid_objects and solid-objects-js. The JS bridge must produce and accept these; SolidObjects::Transmission.receive must accept the valid ones, apply the duplicate pair once, and reject the malformed ones.",
"valid": [
{
"name": "increment with arguments",
"envelope": {
"effectId": "fixture-effect-0001",
"actorType": "transmit-counters",
"actorId": "fixture-counter",
"operation": "increment",
"arguments": { "amount": 2 }
},
"idempotencyKey": "transmit:fixture-effect-0001"
},
{
"name": "increment without arguments",
"envelope": {
"effectId": "fixture-effect-0002",
"actorType": "transmit-counters",
"actorId": "fixture-counter",
"operation": "increment"
},
"idempotencyKey": "transmit:fixture-effect-0002"
}
],
"duplicatePair": [
{
"effectId": "fixture-effect-0003",
"actorType": "transmit-counters",
"actorId": "fixture-counter",
"operation": "increment",
"arguments": { "amount": 1 }
},
{
"effectId": "fixture-effect-0003",
"actorType": "transmit-counters",
"actorId": "fixture-counter",
"operation": "increment",
"arguments": { "amount": 1 }
}
],
"malformed": [
{
"name": "missing effectId",
"envelope": {
"actorType": "transmit-counters",
"actorId": "fixture-counter",
"operation": "increment"
}
},
{
"name": "empty actorId",
"envelope": {
"effectId": "fixture-effect-0004",
"actorType": "transmit-counters",
"actorId": "",
"operation": "increment"
}
},
{
"name": "snake_case keys",
"envelope": {
"effect_id": "fixture-effect-0005",
"actor_type": "transmit-counters",
"actor_id": "fixture-counter",
"operation": "increment"
}
},
{
"name": "non-string operation",
"envelope": {
"effectId": "fixture-effect-0006",
"actorType": "transmit-counters",
"actorId": "fixture-counter",
"operation": 7
}
},
{
"name": "arguments not an object",
"envelope": {
"effectId": "fixture-effect-0007",
"actorType": "transmit-counters",
"actorId": "fixture-counter",
"operation": "increment",
"arguments": [1]
}
}
]
}
Loading