solid-objects 0.14.2 → 0.14.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,60 +1,97 @@
1
- # Solid Objects JS
1
+ # Solid Objects for Node and Your Browser
2
2
 
3
3
  [![CI](https://github.com/cardmagic/solid-objects-js/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/cardmagic/solid-objects-js/actions/workflows/ci.yml)
4
4
  [![npm](https://img.shields.io/npm/v/solid-objects)](https://www.npmjs.com/package/solid-objects)
5
5
 
6
- Open Source Durable Objects for JavaScript, in the SQL database you already run.
7
- No daemon, no broker, and no new datastore.
6
+ **Open Source Durable Objects in your Node app.**
8
7
 
9
- Build addressable TypeScript objects with serialized calls and durable state on
10
- SQLite, PostgreSQL, or MySQL. You don't need Cloudflare for this.
8
+ In a shopping cart, paying twice at the same time is a big problem. The payment provider might time out, and your Node site could be restarting before recovery finishes.
11
9
 
12
- Concurrent calls for one identity cannot overwrite each other. Calls for
13
- different identities can run at the same time.
10
+ To deal with this safely, you often need logic scattered between 7-10 files like database row locks, Redis locks, delayed jobs, retries, and cleanup code to keep that process straight. They are not all large, but they must agree about the same payment state and failure rules. That coordination is the difficult part.
14
11
 
15
- Define ordinary TypeScript classes and run them in ordinary Node.js processes.
16
- Solid Objects keeps the state, the queued operations, the retries, the
17
- reminders, the effects, and the realtime invalidations in the database the
18
- application already operates.
12
+ With Solid Objects, one actor in one file owns each shopping cart's full state and recovery work. Method calls on that object run one at a time, state lives in your existing SQL database, and scheduled recovery resume after restarts.
19
13
 
20
- The same runtime also runs inside a browser worker on SQLite WASM, with
21
- durable actor state in the origin's private file system, and its offline
22
- writes can replay onto a Node **or Rails** backend over one shared wire
23
- contract. See [Solid Objects in the browser](#solid-objects-in-the-browser).
14
+ Solid Object JavaScript Actors elegantly fit anything where one identifiable thing must remember state, handle competing requests in order, or wake up later:
24
15
 
25
- > **Early release:** the correctness core has automated coverage. That coverage
26
- > includes the supported databases, the Chromium browser client, the browser
27
- > runtime, process recovery, and the packaged artifacts. The TypeScript
28
- > implementation is still new. There is one deployed first-party reference
29
- > application. There is no measured scale and no third-party production use
30
- > yet. Read the
31
- > [delivery boundaries](#delivery-boundaries) before you use it for important
32
- > data.
16
+ - Ticket holds and reservations
17
+ - Multiplayer games and shared rooms
18
+ - Shopping carts and checkout recovery
19
+ - Rate limits and account quotas
20
+ - Session expiration
21
+ - Job leases and workflows
22
+ - Connected devices
23
+ - Collaborative documents
33
24
 
34
- > **Not a replacement for SQL transactions:** when one row update inside one
35
- > transaction solves the problem, use that. Solid Objects earns its cost when an
36
- > entity needs ordered calls across requests, retries, reminders, effects, and
37
- > realtime state. See [Good and poor fits](#good-and-poor-fits).
25
+ And so much more.
38
26
 
39
- ## The programming model
27
+ ## Contents
40
28
 
41
- ```typescript
29
+ - [Installation](#installation)
30
+ - [An expiring ticket hold](#an-expiring-ticket-hold)
31
+ - [Why this exists](#why-this-exists)
32
+ - [Good uses](#good-uses)
33
+ - [Solid Objects in the browser](#solid-objects-in-the-browser)
34
+ - [When a transaction is better](#when-a-transaction-is-better)
35
+ - [Guarantees and boundaries](#guarantees-and-boundaries)
36
+ - [Read more](#read-more)
37
+ - [Status and license](#status-and-license)
38
+
39
+ ## Installation
40
+
41
+ Solid Objects is ESM-only and requires Node.js 24.4 or newer. TypeScript users
42
+ need TypeScript 5.9 or newer.
43
+
44
+ ```bash
45
+ npm install solid-objects
46
+ npx solid-objects quickstart --yes
47
+ ```
48
+
49
+ The quickstart uses Node's built-in SQLite driver and proves that concurrent
50
+ calls to one identity do not overwrite each other. It is an unusually formal
51
+ introduction to addition.
52
+
53
+ ## An expiring ticket hold
54
+
55
+ Save this as `ticket-sale.mjs`:
56
+
57
+ ```javascript
42
58
  import { Actor, configure } from "solid-objects"
43
59
  import { sqlite } from "solid-objects/database/sqlite"
44
60
 
45
- class Cart extends Actor {
46
- static override readonly actorType = "Cart"
61
+ const HOLD_MILLISECONDS = 10 * 60 * 1000
47
62
 
48
- items: string[] = []
63
+ class TicketSale extends Actor {
64
+ static actorType = "TicketSale"
65
+ available = 1
66
+ holds = {}
49
67
 
50
- add({ sku }: { sku: string }): number {
51
- this.items.push(sku)
52
- return this.items.length
68
+ hold({ buyer }) {
69
+ if (this.available === 0 || buyer in this.holds) {
70
+ return { held: false, available: this.available }
71
+ }
72
+
73
+ this.available -= 1
74
+ this.holds = { ...this.holds, [buyer]: Date.now() }
75
+ this.schedule({
76
+ at: new Date(Date.now() + HOLD_MILLISECONDS),
77
+ key: buyer,
78
+ }).expire({ buyer })
79
+ return { held: true, available: this.available }
80
+ }
81
+
82
+ expire({ buyer }) {
83
+ if (!(buyer in this.holds)) return this.available
84
+
85
+ const remainingHolds = { ...this.holds }
86
+ delete remainingHolds[buyer]
87
+ this.holds = remainingHolds
88
+ this.available += 1
89
+ return this.available
53
90
  }
54
91
  }
55
92
 
56
93
  const runtime = configure({
57
- database: sqlite({ path: "cart.sqlite3" }),
94
+ database: sqlite({ path: "tickets.sqlite3" }),
58
95
  authorizeMessage: () => true,
59
96
  authorizeQuery: () => true,
60
97
  })
@@ -62,245 +99,58 @@ const runtime = configure({
62
99
  await runtime.install()
63
100
 
64
101
  try {
65
- const cart = Cart.ref("cart-123")
66
- await Promise.all([cart.add({ sku: "blue-shirt" }), cart.add({ sku: "green-hat" })])
102
+ const sale = TicketSale.ref("event-42")
103
+
104
+ if ((process.argv[2] ?? "hold") === "work") {
105
+ const controller = new AbortController()
106
+ process.once("SIGINT", () => controller.abort())
107
+ process.once("SIGTERM", () => controller.abort())
108
+ await runtime.run(controller.signal)
109
+ } else {
110
+ console.log(await Promise.all(["ada", "grace"].map((buyer) => sale.hold({ buyer }))))
111
+ }
67
112
  } finally {
68
113
  await runtime.close()
69
114
  }
70
115
  ```
71
116
 
72
- Both calls enter the durable mailbox for `cart-123`. They execute in order and
73
- commit one state transition at a time, even when different requests or Node.js
74
- processes submit them concurrently.
75
-
76
- `install()` prepares the database and starts nothing. The example above finishes
77
- because the caller's own path executes each call. A process serves background
78
- work only after `runtime.run(signal)` starts its roles, so a process that
79
- installs and then waits never claims a ready message. Nothing is lost while no
80
- process runs. The message stays ready until one does.
117
+ Run the background roles in one terminal and place two concurrent holds in
118
+ another:
81
119
 
82
- ```typescript
83
- const controller = new AbortController()
84
- process.on("SIGTERM", () => controller.abort())
85
- await runtime.run(controller.signal)
120
+ ```bash
121
+ node ticket-sale.mjs work
122
+ node ticket-sale.mjs hold
86
123
  ```
87
124
 
88
- ## Run it now with SQLite
125
+ Only one buyer gets the ticket. The successful call stores the hold and its
126
+ ten-minute expiry together. Stop the worker process before the deadline and
127
+ restart it afterwards; the reminder is still in `tickets.sqlite3` and runs when
128
+ the process returns. We have now given `available += 1` a recovery plan.
89
129
 
90
- Node.js 24.4.0 or newer is required. Node.js 24.15 or newer is preferred,
91
- because `node:sqlite` prints an experimental warning before it. The published
92
- package includes a quickstart:
130
+ The authorization callbacks above are for this local example only. Production
131
+ policies must bind actor IDs and operations to the authenticated user or tenant.
93
132
 
94
- ```bash
95
- npm exec --yes --package=solid-objects@latest -- solid-objects quickstart
96
- ```
133
+ ## Why this exists
97
134
 
98
- The command needs no repository checkout, database server, Redis, container, or
99
- application configuration. It uses Node's built-in SQLite module and removes
100
- its scoped temporary database before exiting.
101
-
102
- It states its plan first, prints the `Counter` class it runs, and asks for
103
- permission. It executes the work only after you answer, and then it explains
104
- what each result proves. It asks nothing when stdin is not a terminal, so CI
105
- never waits. Add `--yes` to skip the question in a terminal, or `--json` for a
106
- machine-readable summary.
107
-
108
- The executable asserts rather than merely printing a plausible result. It exits
109
- with a non-zero code when one of those checks fails.
110
-
111
- ## What Solid Objects is for
112
-
113
- Use Solid Objects when more than one request, job, or process can act on the
114
- same logical thing. The next action must then use the latest committed state of
115
- that thing.
116
- These are the stateful coordination patterns for which people often reach for
117
- Durable Objects:
118
-
119
- | Pattern | One identity per | What the object coordinates |
120
- | --------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------- |
121
- | Multiplayer, presence, or collaboration | Room, session, or document | Joins, moves, and edits commit in order; subscribers refresh from committed state |
122
- | Reservations and expiring holds | Show, resource, or stock item | Availability checks and holds cannot interleave; a durable reminder can release an old hold |
123
- | Checkout and account workflows | Cart, order, account, device | The current step, retries, and effect results return to the same ordered mailbox |
124
- | Per-key rate limits | API key, account, or device | Token checks and decrements are serialized; a reminder can refill the bucket |
125
- | Stateful agent sessions | Agent session | Messages and tool results apply in order and pending work survives a worker exit |
126
-
127
- The common shape is one durable coordination boundary with an application
128
- defined identity. Work for that identity is serialized, while unrelated rooms,
129
- carts, accounts, or sessions can progress concurrently. A single global rate
130
- limiter or another very hot identity is a poor fit because it becomes an
131
- intentional bottleneck. If one ordinary row transaction solves the problem,
132
- prefer that. See [Choosing Solid Objects](docs/fit.md) for the longer guide.
133
-
134
- ## Measured behavior
135
-
136
- One developer machine, not a capacity promise. Apple M5, Node.js 24.18.0, 250
137
- measured operations at client concurrency 16, on August 22, 2026. PostgreSQL
138
- 17.11 and MySQL 9.7.1 run natively, not in a container.
139
-
140
- | Measurement | Result |
141
- | ------------------------------------------------------------- | ---------------: |
142
- | Committed operations per second, one hot identity, SQLite | 286 to 323 ops/s |
143
- | The same identity across four processes, SQLite | 507 to 519 ops/s |
144
- | The same identity across four processes, PostgreSQL | 266 to 331 ops/s |
145
- | The same identity across four processes, MySQL | 214 to 228 ops/s |
146
- | Idle wake-up to committed result, one process | 2.66 ms p50 |
147
- | Idle wake-up to committed result, two processes, polling only | 1,006 ms p50 |
148
- | Idle CPU per process, 100 ms fast interval | 0.121% |
149
- | Idle database passes per second, after backoff | 4.0 |
150
-
151
- The four idle rows come from a separate harness on August 16, 2026.
152
-
153
- Each range spans the synchronous and the asynchronous handler shape. Calls to
154
- one identity are serialized on purpose, so the per-call latency in these runs
155
- includes the wait behind the other fifteen concurrent callers. Throughput is
156
- the honest number for that case.
157
-
158
- The same PostgreSQL and MySQL versions in Docker Desktop reached 1.8x to 4.9x
159
- less throughput on those rows. Measure your own deployment shape before you
160
- plan capacity.
161
-
162
- The polling-only row is the tradeoff to know before you deploy: use PostgreSQL
163
- notifications or the optional Redis Pub/Sub when separate processes need
164
- low-latency delivery.
165
-
166
- Conditions, sources of bias, and the complete matrix for all three databases
167
- are in [Benchmarks](docs/benchmarks.md).
168
-
169
- ## Running in a deployed application
170
-
171
- [Shuffle Up and Play](https://shuffleupandplay.com/) is a deployed reference
172
- application. Two players create a table, load decks, and move cards. Realtime
173
- updates reach both browsers. Its
174
- [source](https://github.com/cardmagic/shuffleupandplay) uses Node 24,
175
- TypeScript, SQLite, `node:http`, and `ws`. Each table code addresses one
176
- `GameRoom` actor that owns both seats, so mutations share one durable mailbox
177
- while each player receives a separately authorized projection.
178
-
179
- The application and its tests exercise more than a counter-shaped happy path:
180
-
181
- | Production concern | Verifiable application evidence |
182
- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
183
- | Concurrent mutations | One [`GameRoom`](https://github.com/cardmagic/shuffleupandplay/blob/519a343e8db0bb6eed961a2ffd374dba80d67cd6/src/actors/game-room.ts#L45-L125) owns a table. [Mailbox tests](https://github.com/cardmagic/shuffleupandplay/blob/519a343e8db0bb6eed961a2ffd374dba80d67cd6/test/durability.test.ts#L39-L94) submit concurrent life, draw, and shuffle operations and assert the final committed state. |
184
- | Controlled restarts | [Restart tests](https://github.com/cardmagic/shuffleupandplay/blob/519a343e8db0bb6eed961a2ffd374dba80d67cd6/test/restart.test.ts#L38-L136) close and reopen the runtime against the same SQLite file, then assert recovery of committed state, an accepted asynchronous operation, an unfinished effect, and a scheduled reminder. |
185
- | Persistent deployment | The [runtime uses SQLite](https://github.com/cardmagic/shuffleupandplay/blob/519a343e8db0bb6eed961a2ffd374dba80d67cd6/src/runtime.ts#L45-L63); the [container runs as an unprivileged user](https://github.com/cardmagic/shuffleupandplay/blob/519a343e8db0bb6eed961a2ffd374dba80d67cd6/Dockerfile#L20-L37), and [Kamal mounts a persistent volume](https://github.com/cardmagic/shuffleupandplay/blob/519a343e8db0bb6eed961a2ffd374dba80d67cd6/config/deploy.yml#L28-L43). |
186
- | Private realtime state | [Subscription policy](https://github.com/cardmagic/shuffleupandplay/blob/519a343e8db0bb6eed961a2ffd374dba80d67cd6/src/runtime.ts#L65-L82) and [per-seat projection](https://github.com/cardmagic/shuffleupandplay/blob/519a343e8db0bb6eed961a2ffd374dba80d67cd6/src/game/room-snapshot.ts#L92-L130) run on the server. [HTTP](https://github.com/cardmagic/shuffleupandplay/blob/519a343e8db0bb6eed961a2ffd374dba80d67cd6/test/server.test.ts#L526-L567) and [WebSocket tests](https://github.com/cardmagic/shuffleupandplay/blob/519a343e8db0bb6eed961a2ffd374dba80d67cd6/test/realtime.test.ts#L184-L235) assert that opponent card identities are absent from player payloads and shared invalidation envelopes. |
187
- | External work | Deck imports run as [durable effects with success and failure callbacks](https://github.com/cardmagic/shuffleupandplay/blob/519a343e8db0bb6eed961a2ffd374dba80d67cd6/src/actors/game-room.ts#L192-L253). [Tests](https://github.com/cardmagic/shuffleupandplay/blob/519a343e8db0bb6eed961a2ffd374dba80d67cd6/test/game-room.test.ts#L209-L331) cover both outcomes and prevent a superseded callback from replacing a newer deck result. |
188
- | Transactional staged work | A room operation stages an [actor-to-actor log message](https://github.com/cardmagic/shuffleupandplay/blob/519a343e8db0bb6eed961a2ffd374dba80d67cd6/src/actors/game-room.ts#L339-L349) and a [database commit action](https://github.com/cardmagic/shuffleupandplay/blob/519a343e8db0bb6eed961a2ffd374dba80d67cd6/src/runtime.ts#L96-L113). Tests cover [rollback of staged messages](https://github.com/cardmagic/shuffleupandplay/blob/519a343e8db0bb6eed961a2ffd374dba80d67cd6/test/durability.test.ts#L160-L192) and the [metrics write](https://github.com/cardmagic/shuffleupandplay/blob/519a343e8db0bb6eed961a2ffd374dba80d67cd6/test/server.test.ts#L467-L499). |
189
- | Time and schema changes | The actor defines [versioned state migrations](https://github.com/cardmagic/shuffleupandplay/blob/519a343e8db0bb6eed961a2ffd374dba80d67cd6/src/actors/game-room.ts#L47-L91) and a [durable reminder](https://github.com/cardmagic/shuffleupandplay/blob/519a343e8db0bb6eed961a2ffd374dba80d67cd6/src/actors/game-room.ts#L276-L310). Tests load [stored version-one state](https://github.com/cardmagic/shuffleupandplay/blob/519a343e8db0bb6eed961a2ffd374dba80d67cd6/test/operations.test.ts#L135-L201) and [run the reminder scheduler](https://github.com/cardmagic/shuffleupandplay/blob/519a343e8db0bb6eed961a2ffd374dba80d67cd6/test/durability.test.ts#L236-L257). |
190
- | Operations and CI | The [operations tests](https://github.com/cardmagic/shuffleupandplay/blob/519a343e8db0bb6eed961a2ffd374dba80d67cd6/test/operations.test.ts#L39-L202) exercise doctor, process, retention, and reconciliation APIs; server suites cover the [dashboard](https://github.com/cardmagic/shuffleupandplay/blob/519a343e8db0bb6eed961a2ffd374dba80d67cd6/test/server.test.ts#L297-L326), [rate limits](https://github.com/cardmagic/shuffleupandplay/blob/519a343e8db0bb6eed961a2ffd374dba80d67cd6/test/rate-limit.test.ts), and [shutdown](https://github.com/cardmagic/shuffleupandplay/blob/519a343e8db0bb6eed961a2ffd374dba80d67cd6/test/shutdown.test.ts). The [current main CI run](https://github.com/cardmagic/shuffleupandplay/actions/runs/31963000789) passed typechecking, 171 tests, the build, the doctor, and a Docker image build. |
191
-
192
- **Scope:** the checked-in deployment configuration runs one Node process with
193
- SQLite on one Docker host. It shows a real deployed workload. It does not show a
194
- measured traffic level or every supported topology. Its deck-import effect reads
195
- an external API. An effect that writes to an external system still needs a
196
- stable idempotency key, because delivery is at least once. The restart tests
197
- close the runtime cleanly. The library verifies abrupt termination, PostgreSQL,
198
- MySQL, and multi-process lease fencing separately in its
199
- [test matrix](docs/support.md),
200
- [failure-recovery demonstration](examples/failure-recovery/demo.ts), and
201
- [correctness contract](docs/correctness.md). Compare those guarantees and limits
202
- with your own workload.
203
-
204
- ## How it works
205
-
206
- Solid Objects addresses an object by its TypeScript class and its
207
- application-defined ID. Public fields are JSON state, public methods are durable
208
- operations, and public getters are ordered queries.
209
-
210
- For each identity, Solid Objects:
211
-
212
- 1. commits calls to a durable per-ID mailbox;
213
- 2. claims one activation with a renewable lease;
214
- 3. executes one operation at a time outside the database transaction;
215
- 4. commits state, completion, and staged work in a short fenced transaction;
216
- 5. retries recoverable failures and exposes terminal failures as dead letters;
217
- 6. publishes committed realtime invalidations in revision order.
218
-
219
- The fence includes the activation owner, token, generation, expiration, and
220
- claimed message. A worker that finishes JavaScript after losing its lease
221
- cannot commit. See the executable [failure-recovery demonstration](examples/failure-recovery/demo.ts)
222
- and the full [architecture](docs/architecture.md).
223
-
224
- Redis is optional wake-up infrastructure. It can reduce notification latency in
225
- a multi-process MySQL deployment. The relational database stays the durable
226
- source of truth, and polling stays the recovery path.
227
-
228
- Idle roles back off from the configured 100 ms fast polling interval to one
229
- second. Processed work and wake-up notifications reset that interval
230
- immediately. The default wake-up reaches only the current Node process; use the
231
- PostgreSQL or optional Redis adapter when separate processes need low-latency
232
- delivery. The runtime warns once when it sees that topology without an adapter.
233
-
234
- ## Good and poor fits
235
-
236
- | Good fit | Poor fit |
237
- | --------------------------------------------------------- | --------------------------------------------------------- |
238
- | Multiplayer rooms and collaborative sessions | A single-row update already solved by one SQL transaction |
239
- | Shopping carts, accounts, devices, and per-user workflows | Bulk ingestion and data-parallel pipelines |
240
- | Stateful agent sessions with ordered tool results | Very high-throughput global counters |
241
- | Per-document or per-device reminders | Large JSON documents that should remain normalized rows |
242
- | Realtime projections of committed state | Globally placed edge state or managed elastic placement |
243
-
244
- One hot identity is intentionally serialized. Split an identity only when the
245
- domain can tolerate independent ordering and transactions. Solid Objects does
246
- not provide a transaction across object identities.
247
-
248
- The longer decision guide is in [Choosing Solid Objects](docs/fit.md).
249
-
250
- ## Delivery boundaries
251
-
252
- - Operations are ordered per identity and execute **at least once**.
253
- - A crash after arbitrary external I/O but before the database commit can cause
254
- that I/O to repeat. Use the stable effect ID or another durable idempotency
255
- key at the external system.
256
- - Fencing protects the Solid Objects database commit. It cannot undo an HTTP
257
- request, email, payment, file write, or other external side effect.
258
- - Different identities can execute concurrently; one hot identity cannot.
259
- - State, result, actor-to-actor delivery, reminders, effects, commit actions,
260
- and realtime invalidations commit together for one operation.
261
- - Cross-object transactions are not provided.
262
- - Application processes with incompatible `stateVersion` values must not run
263
- together. Older code rejects state written by a newer version.
264
- - Direct application-database writes are guarded only when the application
265
- uses the supplied database facade. Unwrapped clients cannot be intercepted.
266
- - Realtime sessions are process-local. A multi-process application must bridge
267
- committed broadcast events to the processes holding live connections.
268
-
269
- See [Correctness and delivery semantics](docs/correctness.md) and
270
- [Errors and recovery](docs/errors-and-recovery.md) for the complete contract.
271
-
272
- ## Realtime committed state
273
-
274
- Actors opt into browser-visible dependencies. In `0.13`, an unwrapped
275
- observable triggers invalidation without storing or sending its value. Use
276
- `broadcastValue()` only for a scalar that every authorized subscriber may see:
277
-
278
- ```typescript
279
- import { Actor, broadcastValue } from "solid-objects"
280
-
281
- class Room extends Actor {
282
- static override readonly actorType = "Room"
283
-
284
- version = 0
285
- privateHands: Record<string, string[]> = {}
286
-
287
- override observables(): Record<string, unknown> {
288
- return {
289
- version: broadcastValue(this.version),
290
- hands: this.privateHands,
291
- }
292
- }
293
- }
294
- ```
135
+ The handwritten version usually starts with a row lock. Then it gains an
136
+ `expiresAt` column, a sweeper, retries, per-room ordering, and a broadcast path
137
+ that must agree with the write. The original transaction has developed a robust
138
+ interplay with four other subsystems.
295
139
 
296
- `version` crosses the shared invalidation channel. `hands` contributes only its
297
- name when its real value changes. A reauthorized component endpoint can then
298
- render subscriber-specific state without a manual revision counter.
140
+ Solid Objects makes the application-defined identity the coordination boundary.
141
+ Its state, mailbox, retries, reminders, and staged consequences live in SQLite,
142
+ PostgreSQL, or MySQL. No daemon, broker, Cloudflare account, or new datastore is
143
+ required. Redis is optional wake-up plumbing, not durable state.
299
144
 
300
- The browser package handles replay, reconnection, incarnation/revision fences,
301
- personalized payloads, and framework-neutral component refresh. Applications
302
- provide authentication, WebSocket transport, and rendering. See the
303
- [browser protocol](docs/browser-protocol.md) and [authorization guide](docs/authorization.md).
145
+ ## Good uses
146
+
147
+ - Multiplayer rooms, collaborative sessions, and documents with ordered edits.
148
+ - Carts, reservations, and inventory holds with durable expiry.
149
+ - Accounts, devices, and long-lived jobs whose next action depends on committed state.
150
+ - Realtime multi-user state where publications must follow committed revisions.
151
+
152
+ Different identities can run concurrently. One global identity is merely a
153
+ queue wearing an ambitious name.
304
154
 
305
155
  ## Solid Objects in the browser
306
156
 
@@ -333,33 +183,28 @@ await runtime.install()
333
183
  await Counter.ref("page-hits").increment()
334
184
  ```
335
185
 
336
- That code runs identically in every tab. `sharedSqliteWasm` elects one
337
- database holder per origin through the Web Locks API, carries the other
338
- tabs' SQL to it over a `BroadcastChannel`, and fails over onto the same
339
- durable state when the holder's tab dies. Use `sqliteWasm` directly for a
340
- single dedicated worker.
341
-
342
- Two companions complete the local-first story:
186
+ That code runs identically in every tab. `sharedSqliteWasm` elects one database
187
+ holder per origin through the Web Locks API, carries the other tabs' SQL to it
188
+ over a `BroadcastChannel`, and fails over onto the same durable state when the
189
+ holder's tab dies. Use `sqliteWasm` for a single dedicated worker.
343
190
 
344
- - `solid-objects/browser/tab-host` runs one runtime for all tabs when the
345
- application prefers request-level routing: the leader's worker executes
346
- every operation, and other tabs invoke through a `BroadcastChannel` client
347
- by name.
348
- - `solid-objects/transmit` drains the transactional effects outbox to a
349
- server with at-least-once delivery, per-actor order, and an idempotent
350
- server ingest, so offline writes reconcile when the network returns.
191
+ Two more modules support local-first applications.
192
+ `solid-objects/browser/tab-host` runs one runtime for all tabs when the
193
+ application prefers request-level routing. `solid-objects/transmit` drains the
194
+ transactional effects outbox to a server with at-least-once delivery, per-actor
195
+ order, and an idempotent server ingest, so offline writes reconcile when the
196
+ network returns.
351
197
 
352
198
  ### The backend can be Rails, not only Node
353
199
 
354
- The browser runtime does not require a Node server behind it. The transmit
355
- wire contract is shared with the Ruby gem
200
+ The browser runtime does not require a Node server behind it. The transmit wire
201
+ contract is shared with the Ruby gem
356
202
  ([solid-objects-ruby](https://github.com/cardmagic/solid-objects-ruby)):
357
203
  `SolidObjects::Transmission.receive` accepts the same envelopes as the Node
358
- ingest `receiveTransmitEnvelope`, dedups on the same `transmit:<effectId>`
359
- key, and both repositories pin the contract with one shared fixture file.
360
- A browser front end on `solid-objects/browser/host` inside a Rails
361
- application therefore replays its offline writes directly onto Ruby server
362
- actors — no Node service in between:
204
+ ingest `receiveTransmitEnvelope`, dedups on the same `transmit:<effectId>` key,
205
+ and both repositories pin the contract with one shared fixture file. A browser
206
+ front end therefore replays its offline writes directly onto Ruby server
207
+ actors, with no Node service in between:
363
208
 
364
209
  ```ruby
365
210
  class TransmitController < ApplicationController
@@ -381,113 +226,48 @@ The wire shapes are documented in the
381
226
  [public API reference](docs/api.md), and the platform boundaries in
382
227
  [supported versions](docs/support.md).
383
228
 
384
- ## Comparison
385
-
386
- These systems solve different coordination problems. The table describes their
387
- default unit and deployment model, not a quality ranking.
388
-
389
- | Approach | Serialization and state unit | Durable substrate | Additional runtime | Recovery model | Placement |
390
- | --------------------------- | ----------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------- |
391
- | SQL transaction or row lock | Selected rows in one transaction | Application database | None | Application retries the transaction | Application deployment |
392
- | Traditional job queue | Job or queue; ordering depends on queue configuration | Broker or queue database | Queue workers and usually a broker | Retry the job | Application deployment |
393
- | Solid Objects | TypeScript class plus object ID | Existing SQLite, PostgreSQL, or MySQL | Library in application processes | Retry the per-ID operation from durable state | Application deployment |
394
- | Cloudflare Durable Objects | Object class plus globally unique ID | Per-object managed storage | Cloudflare Workers platform | Managed object activation | Cloudflare-selected location |
395
- | celld | Object class plus object name | Per-object SQLite replicated to a bucket you own | celld daemon that embeds V8 and runs Wrangler bundles | A new owner restores the object database from the bucket | Any node in your fleet, chosen by bucket compare-and-swap |
396
- | Rivet Actors | Addressable actor | Actor state, KV, or per-actor SQLite | Rivet Engine or managed compute | Actor sleep, wake, and persistence | Configured Rivet deployment |
397
- | DBOS | Workflow ID and checkpointed steps | PostgreSQL system database | Library; Conductor recommended for distributed recovery | Deterministic workflow replay from checkpoints | Application deployment |
398
- | Restate | Service handler or keyed virtual object | Restate log and state store | Restate server or cloud service | Durable handler execution and journal replay | Restate deployment |
399
-
400
- celld and Solid Objects both self-host the Durable Objects model. The difference
401
- is where the state lives and what you run. celld runs a daemon that embeds V8
402
- and executes Wrangler bundles. It gives each object its own SQLite database,
403
- and it replicates that database to an object-storage bucket you own. Object
404
- ownership moves between nodes through compare-and-swap on that bucket. Solid
405
- Objects runs plain TypeScript classes inside your Node processes, adds no
406
- daemon, and keeps object state in the SQL database the application already
407
- operates. Choose celld to run Workers-format code across a fleet with
408
- bucket-based placement. Choose Solid Objects to keep one database, no extra
409
- process, and an ordinary Node deployment.
410
-
411
- [docs/comparisons.md](docs/comparisons.md) holds the sourced comparison for each
412
- dimension: realtime projections, edge placement, cross-identity transactions,
413
- and operational data access.
414
-
415
- ## Requirements and supported systems
416
-
417
- - Node.js 24.4.0 or newer; 24.15 or newer to avoid the `node:sqlite`
418
- experimental warning
419
- - TypeScript 5.9 or newer for TypeScript applications
420
- - SQLite through `node:sqlite`, PostgreSQL 14 or newer, or MySQL 8.0 or newer
421
- with InnoDB
422
- - optional `pg`, `mysql2`, `redis`, or `@sqlite.org/sqlite-wasm` peer
423
- dependency only for the selected adapter
424
- - for the browser runtime: a browser with OPFS for persistent storage and the
425
- Web Locks API for the multi-tab host
426
-
427
- [Supported versions](docs/support.md) records the exact CI matrix and the
428
- boundaries.
429
-
430
- ## Operations
431
-
432
- `runtime.run(signal)` supervises actor, effect, reminder, broadcast, retention,
433
- and stale-process recovery roles. The database-backed operator dashboard is an
434
- optional `solid-objects/web` export with deny-by-default administration policy,
435
- session-backed CSRF protection, and Fetch or Node/Connect mounting.
436
-
437
- The dashboard defaults to authorized read/write access. An authorized read-only
438
- mode removes the mutations. Use the explicitly public read-only mode only for
439
- synthetic demo data, because it exposes stored arguments, results, errors,
440
- identifiers, and operational metadata.
441
-
442
- Administration remains available through the JSON CLI and typed runtime
443
- managers. See [Operations](docs/operations.md), the [dashboard guide](docs/dashboard.md),
444
- and [Configuration](docs/configuration.md).
445
-
446
- ## Design provenance
447
-
448
- Solid Objects JS is a Node.js and TypeScript implementation. The Ruby
449
- [`solid_objects`](https://github.com/cardmagic/solid-objects-ruby) design informed
450
- it. It began at the `0.12` capability generation, because the first
451
- implementation targeted the Ruby `0.12` contract. That number does not represent
452
- twelve earlier JavaScript release generations.
453
-
454
- The TypeScript implementation is not a source translation. It redesigned the
455
- API around inferred TypeScript references, Node runtime supervision,
456
- `node:sqlite`/`pg`/`mysql2` adapters, transport-neutral realtime sessions,
457
- Web Components, and browser-safe package exports. The
458
- [parity ledger](docs/parity.md) records capability relationships and deliberate
459
- runtime differences.
460
-
461
- The Ruby project first appeared publicly on August 6, 2026, and this TypeScript
462
- repository on August 13, 2026. Both remain early releases. The
463
- [`mtg-playmat`](https://github.com/cardmagic/mtg-playmat) application uses the
464
- Ruby actor and realtime design.
465
- [Shuffle Up and Play](https://github.com/cardmagic/shuffleupandplay) uses the
466
- TypeScript package in the deployed Node and SQLite topology above.
467
-
468
- ## Documentation
469
-
470
- - [Getting the architecture right](docs/architecture.md)
471
- - [Correctness and delivery semantics](docs/correctness.md)
229
+ ## When a transaction is better
230
+
231
+ Often. If all the work happens in one request, use a transaction, constraint, or
232
+ `SELECT ... FOR UPDATE`. It is smaller, faster, and does not need a manifesto.
233
+
234
+ Use Solid Objects when the critical section outlives that transaction: work
235
+ must happen later, survive a restart, or remain ordered across several requests
236
+ or jobs. A plain counter is not a reason to install this package.
237
+
238
+ ## Guarantees and boundaries
239
+
240
+ - Calls are durably ordered per identity. Different identities may run concurrently.
241
+ - Delivery is **at least once**, not exactly once. A handler can begin again after a crash.
242
+ - One successful turn commits state and staged reminders, messages, effects, and realtime publications together.
243
+ - Fencing prevents a stale worker from committing, though its JavaScript may keep running. It cannot undo an HTTP request, email, payment, or file write.
244
+ - External effects can repeat and must use the stable effect ID or another durable idempotency key.
245
+ - One hot identity is intentionally sequential. There are no transactions across identities.
246
+ - Background work needs `runtime.run(signal)`. If no process is running, committed work waits in SQL rather than disappearing.
247
+ - Your application still owns authorization, database backups, failover, WebSocket transport, and capacity planning.
248
+
249
+ Exactly once remains absent, despite its excellent branding. Read the
250
+ [correctness contract](docs/correctness.md) before using important data.
251
+
252
+ ## Read more
253
+
254
+ - [Five-minute Node guide](https://solidobjects.dev/5min/node)
472
255
  - [Choosing Solid Objects](docs/fit.md)
473
- - [Benchmarks and methodology](docs/benchmarks.md)
474
- - [Supported versions and test matrix](docs/support.md)
475
- - [Test suite](https://github.com/cardmagic/solid-objects-js/tree/main/test)
476
- - [CI workflow](https://github.com/cardmagic/solid-objects-js/actions/workflows/ci.yml)
477
256
  - [Public API](docs/api.md)
478
- - [State and lifecycle](docs/state-and-lifecycle.md)
479
- - [Operations, retention, and reconciliation](docs/operations.md)
480
- - [Configuration](docs/configuration.md)
481
- - [Authorization](docs/authorization.md)
482
- - [Browser protocol](docs/browser-protocol.md)
483
- - [Operator dashboard](docs/dashboard.md)
484
- - [Errors and recovery](docs/errors-and-recovery.md)
485
- - [Design parity](docs/parity.md)
486
- - [Changelog](CHANGELOG.md)
487
- - [Releases](https://github.com/cardmagic/solid-objects-js/releases)
488
- - [Contributing](CONTRIBUTING.md)
489
- - [Security policy](SECURITY.md)
490
-
491
- ## License
492
-
493
- Solid Objects is released under the [MIT License](MIT-LICENSE).
257
+ - [Operations and recovery](docs/operations.md)
258
+ - [Detailed architecture](docs/architecture.md)
259
+ - [Detailed documentation](docs/)
260
+
261
+ The benchmarks, parity ledger, dashboard, browser setup, and exhaustive API
262
+ notes remain in `docs/`, where long documentation can be long on purpose.
263
+
264
+ ## Status and license
265
+
266
+ Solid Objects JS is a pre-1.0 early release. The correctness core has
267
+ automated coverage across the supported databases, browser runtime, recovery
268
+ paths, and packaged artifact. There is one deployed first-party reference
269
+ application, no measured scale, and no known third-party production use yet.
270
+ Pre-1.0 is doing actual work in that sentence.
271
+
272
+ Solid Objects is released under the [MIT License](MIT-LICENSE). It is an
273
+ independent project and is not affiliated with or endorsed by Cloudflare.