solid-objects 0.14.2 → 0.14.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.3 - 2026-08-25
4
+
5
+ - Rewrite the first screen around the objection a reader actually has. The
6
+ README led with a shopping cart that appended to an array, which invites the
7
+ reply that one SQL statement already does it. It now leads with the ticket
8
+ sale from the homepage: 100 seats, a hold, a ten-minute expiry that frees the
9
+ seat, and a published count. That is the smallest example needing three
10
+ things from one number, and the three things are the argument.
11
+ - Answer "why not just use transactions?" in the first screen instead of
12
+ burying the fit sections. The section concedes the transaction and the row
13
+ lock first, including `navigator.locks` in the browser, then argues scope
14
+ rather than discipline: any `expiresAt` or `scheduledAt` column is evidence
15
+ the critical section already outlived the lock, and what follows it is a
16
+ sweeper and a race.
17
+ - Add "Is it worth installing here?", which names who should not install this,
18
+ and point readers with high-QPS reads or hot identities at
19
+ [Solid Objects Pro](https://solidobjects.pro/).
20
+ - Correct two claims. The realtime section said a value is published once per
21
+ change from the saving turn; the turn records the publication atomically,
22
+ while delivery is a separate worker and is at least once. Design provenance
23
+ said the API was redesigned around Web Components; there is no
24
+ `customElements` or `HTMLElement` in the package.
25
+ - Cut the README from 590 to about 540 lines by removing what `docs/` already
26
+ documented and what the page said three times, and add the table of contents
27
+ the standard-readme specification asks for above 100 lines.
28
+
3
29
  ## 0.14.2 - 2026-08-24
4
30
 
5
31
  - State that background pickup needs `runtime.run(signal)`
package/README.md CHANGED
@@ -32,29 +32,69 @@ contract. See [Solid Objects in the browser](#solid-objects-in-the-browser).
32
32
  > data.
33
33
 
34
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).
35
+ > transaction solves the problem, use that and install nothing. Solid Objects
36
+ > earns its cost when the critical section outlives the transaction: a hold that
37
+ > expires in ten minutes, work that must survive a restart, or a fan-in that
38
+ > spans many jobs. See
39
+ > [Why not just use transactions?](#why-not-just-use-transactions).
40
+
41
+ ## Contents
42
+
43
+ - [The programming model](#the-programming-model)
44
+ - [Why not just use transactions?](#why-not-just-use-transactions)
45
+ - [Is it worth installing here?](#is-it-worth-installing-here)
46
+ - [Run it now with SQLite](#run-it-now-with-sqlite)
47
+ - [What Solid Objects is for](#what-solid-objects-is-for)
48
+ - [Measured behavior](#measured-behavior)
49
+ - [Running in a deployed application](#running-in-a-deployed-application)
50
+ - [How it works](#how-it-works)
51
+ - [Delivery boundaries](#delivery-boundaries)
52
+ - [Realtime committed state](#realtime-committed-state)
53
+ - [Solid Objects in the browser](#solid-objects-in-the-browser)
54
+ - [Comparison](#comparison)
55
+ - [Requirements and supported systems](#requirements-and-supported-systems)
56
+ - [Operations](#operations)
57
+ - [Design provenance](#design-provenance)
58
+ - [Documentation](#documentation)
59
+ - [License](#license)
38
60
 
39
61
  ## The programming model
40
62
 
63
+ A ticket sale for one event, with 100 seats and a hold that expires:
64
+
41
65
  ```typescript
42
- import { Actor, configure } from "solid-objects"
66
+ import { Actor, broadcastValue, configure } from "solid-objects"
43
67
  import { sqlite } from "solid-objects/database/sqlite"
44
68
 
45
- class Cart extends Actor {
46
- static override readonly actorType = "Cart"
69
+ class TicketSale extends Actor {
70
+ static override readonly actorType = "TicketSale"
71
+
72
+ remaining = 100
73
+ holds: Record<string, number> = {}
47
74
 
48
- items: string[] = []
75
+ override observables(): Record<string, unknown> {
76
+ return { remaining: broadcastValue(this.remaining) }
77
+ }
49
78
 
50
- add({ sku }: { sku: string }): number {
51
- this.items.push(sku)
52
- return this.items.length
79
+ reserve({ buyer }: { buyer: string }): boolean {
80
+ if (this.remaining === 0 || buyer in this.holds) return false
81
+ this.remaining -= 1
82
+ this.holds = { ...this.holds, [buyer]: Date.now() }
83
+ this.schedule({ at: new Date(Date.now() + 600_000), key: buyer }).expire!({ buyer })
84
+ return true
85
+ }
86
+
87
+ expire({ buyer }: { buyer: string }): void {
88
+ if (!(buyer in this.holds)) return
89
+ const rest = { ...this.holds }
90
+ delete rest[buyer]
91
+ this.holds = rest
92
+ this.remaining += 1
53
93
  }
54
94
  }
55
95
 
56
96
  const runtime = configure({
57
- database: sqlite({ path: "cart.sqlite3" }),
97
+ database: sqlite({ path: "tickets.sqlite3" }),
58
98
  authorizeMessage: () => true,
59
99
  authorizeQuery: () => true,
60
100
  })
@@ -62,16 +102,27 @@ const runtime = configure({
62
102
  await runtime.install()
63
103
 
64
104
  try {
65
- const cart = Cart.ref("cart-123")
66
- await Promise.all([cart.add({ sku: "blue-shirt" }), cart.add({ sku: "green-hat" })])
105
+ const sale = TicketSale.ref("event-42")
106
+ const buyers = ["ada", "grace", "alan"]
107
+ await Promise.all(buyers.map((buyer) => sale.reserve({ buyer })))
67
108
  } finally {
68
109
  await runtime.close()
69
110
  }
70
111
  ```
71
112
 
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.
113
+ Every reserve enters the durable mailbox for `event-42`. They execute in order
114
+ and commit one state transition at a time, even when different requests or
115
+ Node.js processes submit them concurrently, so the guard on `remaining` cannot
116
+ oversell.
117
+
118
+ That example wants three things from the same number. It must never go below
119
+ zero. It must give the seat back if the buyer does not pay within ten minutes.
120
+ It must show the current count to everyone watching the page.
121
+
122
+ The first is one UPDATE statement. The second is an `expiresAt` column plus a
123
+ sweeper. The third is a push on every code path that changes the number. The
124
+ combination is what costs, not any one of them. Here the guard, the ten-minute
125
+ alarm, and the published count are one class, and they commit together.
75
126
 
76
127
  `install()` prepares the database and starts nothing. The example above finishes
77
128
  because the caller's own path executes each call. A process serves background
@@ -85,28 +136,77 @@ process.on("SIGTERM", () => controller.abort())
85
136
  await runtime.run(controller.signal)
86
137
  ```
87
138
 
139
+ ## Why not just use transactions?
140
+
141
+ Often you should. If the whole job is read a row, decide, write it back, and
142
+ answer the request, then a transaction with `SELECT ... FOR UPDATE` does that
143
+ and you need nothing else installed. In the browser, `navigator.locks` is the
144
+ same answer. Reach for those first.
145
+
146
+ The argument for an actor is scope, not discipline. A lock is scoped to one
147
+ transaction, on one connection, in one process. The ticket sale above leaves
148
+ that scope on one line: the hold expires in ten minutes, and no transaction
149
+ stays open for ten minutes. A `setTimeout` does not cover it either, because it
150
+ dies with the process.
151
+
152
+ Any column named `expiresAt`, `scheduledAt`, or `nextRunAt` is evidence that
153
+ the critical section already outlived the lock that was supposed to cover it.
154
+ What follows such a column is a sweeper that looks for due rows, and then a
155
+ race between that sweeper and the next writer of the same row. The column, the
156
+ sweeper, and the race are what a Solid Objects actor replaces.
157
+
158
+ Three cases a lock cannot reach:
159
+
160
+ - work that fires at a future moment, when no transaction of yours is open;
161
+ - work that must survive a process restart, which rules out an in-process
162
+ timer; and
163
+ - a fan-in whose critical section spans many jobs over minutes, such as an
164
+ import that counts its own chunks as each one finishes.
165
+
166
+ If it all happens inside one request, use a lock. If something has to happen
167
+ later, or has to survive a restart, that is when this is worth installing.
168
+
169
+ ## Is it worth installing here?
170
+
171
+ Worth it when several requests, jobs, or processes act on the same cart, room,
172
+ device, event, or session, and each next action needs the last committed state.
173
+ Worth it when that same thing also owns work that fires later, or a number a
174
+ live page must show: per-document reminders and realtime projections of
175
+ committed state are the same argument.
176
+
177
+ Not worth it for a plain counter, a single-row update inside one transaction, a
178
+ stateless job, bulk ingestion or a data-parallel pipeline, CPU-heavy work, a
179
+ large JSON document that belongs in normalized rows, globally placed edge
180
+ state, or a global rate-limit counter that every request touches. One hot
181
+ identity is serialized on purpose, so making everything one identity makes a
182
+ queue. Split an identity only when the domain can tolerate independent
183
+ ordering and transactions.
184
+
185
+ High-QPS reads and hot identities are where this runtime stops being the right
186
+ tool on its own. [Solid Objects Pro](https://solidobjects.pro/) is a commercial
187
+ performance layer for the family that adds grouped commits, which coalesce
188
+ concurrent writes into fewer database commits; optional ephemeral operations,
189
+ which take loss-tolerant calls out of the durable journal; and materialized
190
+ projections, which build read models after commit so reads stop competing with
191
+ mailbox work. It ships for the Rails gem today, and the Node build is in
192
+ development.
193
+
194
+ [What Solid Objects is for](#what-solid-objects-is-for) has the pattern table,
195
+ and [Choosing Solid Objects](docs/fit.md) is the longer guide.
196
+
88
197
  ## Run it now with SQLite
89
198
 
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:
199
+ The published package includes a quickstart that needs no checkout, database
200
+ server, container, or configuration:
93
201
 
94
202
  ```bash
95
203
  npm exec --yes --package=solid-objects@latest -- solid-objects quickstart
96
204
  ```
97
205
 
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.
206
+ It states its plan, prints the `Counter` class it runs, and asks before doing
207
+ anything. It asserts rather than printing a plausible result, and exits
208
+ non-zero when a check fails. Add `--yes` to skip the question, or `--json` for
209
+ a machine-readable summary.
110
210
 
111
211
  ## What Solid Objects is for
112
212
 
@@ -133,38 +233,25 @@ prefer that. See [Choosing Solid Objects](docs/fit.md) for the longer guide.
133
233
 
134
234
  ## Measured behavior
135
235
 
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.
236
+ One developer machine, not a capacity promise: Apple M5, Node.js 24.18.0, 250
237
+ operations at client concurrency 16, on August 22, 2026.
139
238
 
140
239
  | Measurement | Result |
141
240
  | ------------------------------------------------------------- | ---------------: |
142
241
  | Committed operations per second, one hot identity, SQLite | 286 to 323 ops/s |
143
242
  | 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
243
  | Idle wake-up to committed result, one process | 2.66 ms p50 |
147
244
  | 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
245
 
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.
246
+ Calls to one identity are serialized on purpose, so per-call latency includes
247
+ the wait behind the other fifteen callers. That last row is the tradeoff to
248
+ know before deploying: use PostgreSQL notifications or the optional Redis
249
+ Pub/Sub when separate processes need low-latency delivery. The same databases
250
+ in Docker Desktop reached 1.8x to 4.9x less throughput.
161
251
 
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).
252
+ PostgreSQL and MySQL numbers, idle CPU, sources of bias, and the full matrix
253
+ are in [Benchmarks](docs/benchmarks.md). Measure your own deployment shape
254
+ before planning capacity.
168
255
 
169
256
  ## Running in a deployed application
170
257
 
@@ -203,74 +290,59 @@ with your own workload.
203
290
 
204
291
  ## How it works
205
292
 
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.
293
+ An actor is addressed by its class and an application-chosen ID. Fields are
294
+ durable state, methods are operations, and getters are queries. One operation
295
+ is one turn: the runtime claims the actor under a fenced lease, runs your
296
+ JavaScript outside the transaction, then commits state, results, staged
297
+ effects, reminders, and realtime invalidations together. A failure retries with
298
+ backoff and dead-letters at the limit.
218
299
 
219
300
  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 |
301
+ claimed message, so a worker that finishes its JavaScript after losing the
302
+ lease cannot commit. The database is the source of truth. Redis is optional
303
+ acceleration, and polling remains the recovery path.
243
304
 
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).
305
+ [`examples/failure-recovery/demo.ts`](examples/failure-recovery/demo.ts) kills a
306
+ worker mid-turn and shows the survivor finish it.
307
+ [Architecture](docs/architecture.md) and
308
+ [Operations](docs/operations.md) cover the turn lifecycle, polling backoff, and
309
+ wake-up adapters.
249
310
 
250
311
  ## Delivery boundaries
251
312
 
252
313
  - 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.
314
+ - Fencing protects the database commit. It cannot undo an HTTP request, email,
315
+ payment, or file write, so external systems need the stable effect ID or
316
+ another durable idempotency key.
317
+ - Different identities run concurrently; one hot identity cannot.
261
318
  - 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.
319
+ - Realtime sessions are process-local, so a multi-process application must
320
+ bridge committed broadcast events to the processes holding live connections.
268
321
 
269
- See [Correctness and delivery semantics](docs/correctness.md) and
270
- [Errors and recovery](docs/errors-and-recovery.md) for the complete contract.
322
+ [Correctness and delivery semantics](docs/correctness.md) has the complete
323
+ contract, including `stateVersion` compatibility and the database write guard,
324
+ and [Errors and recovery](docs/errors-and-recovery.md) covers failure handling.
271
325
 
272
326
  ## Realtime committed state
273
327
 
328
+ For a like count or a dashboard number, write the row and then send on your own
329
+ socket. That is less code than this library and it works.
330
+
331
+ It gets harder when several people write to the same record at once. Each
332
+ request builds its payload in its own process and sends it. The lock decided
333
+ who wrote first, but it has no say over which of the two sends arrives last, so
334
+ a viewer can be left looking at the older number. The second gap is that the
335
+ send is not part of the write: if the process dies after the database commits
336
+ and before the send goes out, the tab keeps a wrong number and nothing corrects
337
+ it.
338
+
339
+ An observable is the alternative. The change and its publication commit
340
+ together, so no crash can leave one without the other. A worker delivers the
341
+ publication afterwards, claiming rows in actor revision order, and subscribers
342
+ reject a duplicate or stale revision. Delivery is still at least once, so the
343
+ guarantee is that a subscriber cannot end up on an older value, not that a
344
+ value is sent exactly once.
345
+
274
346
  Actors opt into browser-visible dependencies. In `0.13`, an unwrapped
275
347
  observable triggers invalidation without storing or sending its value. Use
276
348
  `broadcastValue()` only for a scalar that every authorized subscriber may see:
@@ -333,33 +405,27 @@ await runtime.install()
333
405
  await Counter.ref("page-hits").increment()
334
406
  ```
335
407
 
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:
408
+ That code runs identically in every tab. `sharedSqliteWasm` elects one database
409
+ holder per origin through the Web Locks API, carries the other tabs' SQL to it
410
+ over a `BroadcastChannel`, and fails over onto the same durable state when the
411
+ holder's tab dies. Use `sqliteWasm` for a single dedicated worker.
343
412
 
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.
413
+ Two companions complete the local-first story: `solid-objects/browser/tab-host`
414
+ runs one runtime for all tabs when the application prefers request-level
415
+ routing, and `solid-objects/transmit` drains the transactional effects outbox
416
+ to a server with at-least-once delivery, per-actor order, and an idempotent
417
+ server ingest, so offline writes reconcile when the network returns.
351
418
 
352
419
  ### The backend can be Rails, not only Node
353
420
 
354
- The browser runtime does not require a Node server behind it. The transmit
355
- wire contract is shared with the Ruby gem
421
+ The browser runtime does not require a Node server behind it. The transmit wire
422
+ contract is shared with the Ruby gem
356
423
  ([solid-objects-ruby](https://github.com/cardmagic/solid-objects-ruby)):
357
424
  `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:
425
+ ingest `receiveTransmitEnvelope`, dedups on the same `transmit:<effectId>` key,
426
+ and both repositories pin the contract with one shared fixture file. A browser
427
+ front end therefore replays its offline writes directly onto Ruby server
428
+ actors, with no Node service in between:
363
429
 
364
430
  ```ruby
365
431
  class TransmitController < ApplicationController
@@ -383,65 +449,51 @@ The wire shapes are documented in the
383
449
 
384
450
  ## Comparison
385
451
 
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.
452
+ A SQL transaction or row lock serializes selected rows for one transaction and
453
+ needs nothing installed. A job queue retries a job but leaves ordering to queue
454
+ configuration. Solid Objects serializes by object ID, keeps state in the
455
+ database you already run, and adds a library rather than a service. Cloudflare
456
+ Durable Objects, Rivet, DBOS, and Restate each add a managed runtime or server.
457
+
458
+ celld and Solid Objects both self-host the Durable Objects model, and the
459
+ difference is what you run. celld runs a daemon that embeds V8 and executes
460
+ Wrangler bundles, gives each object its own SQLite database, replicates that
461
+ database to an object-storage bucket you own, and moves ownership between nodes
462
+ through compare-and-swap on that bucket. Solid Objects runs plain TypeScript
463
+ classes inside your Node processes, adds no daemon, and keeps object state in
464
+ the SQL database the application already operates. Choose celld to run
465
+ Workers-format code across a fleet with bucket-based placement. Choose Solid
466
+ Objects to keep one database, no extra process, and an ordinary Node
467
+ deployment.
468
+
469
+ [docs/comparisons.md](docs/comparisons.md) has the full table across eight
470
+ systems, with primary sources for every dimension: realtime projections, edge
471
+ placement, cross-identity transactions, and operational data access.
414
472
 
415
473
  ## Requirements and supported systems
416
474
 
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
475
+ Node.js 24.4.0 or newer, TypeScript 5.9 or newer, and SQLite through
476
+ `node:sqlite`, PostgreSQL 14 or newer, or MySQL 8.0 or newer on InnoDB.
477
+ `pg`, `mysql2`, `ioredis`, and `@sqlite.org/sqlite-wasm` are optional peer
478
+ dependencies, installed only for the adapters you use.
426
479
 
427
- [Supported versions](docs/support.md) records the exact CI matrix and the
428
- boundaries.
480
+ [Supported versions](docs/support.md) records the exact CI matrix, the browser
481
+ requirements for OPFS and the Web Locks API, and the boundaries.
429
482
 
430
483
  ## Operations
431
484
 
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.
485
+ `runtime.run(signal)` supervises the actor, effect, reminder, broadcast,
486
+ retention, and stale-process recovery roles. An optional `solid-objects/web`
487
+ export mounts a database-backed operator dashboard behind a deny-by-default
488
+ administration policy, with session-backed CSRF protection and Fetch or
489
+ Node/Connect mounting. Administration is also available through the JSON CLI
490
+ and typed runtime managers.
441
491
 
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).
492
+ The dashboard has an explicitly public read-only mode. Use it only for
493
+ synthetic demo data: it exposes stored arguments, results, errors, identifiers,
494
+ and operational metadata. See [Operations](docs/operations.md), the
495
+ [dashboard guide](docs/dashboard.md), and
496
+ [Configuration](docs/configuration.md).
445
497
 
446
498
  ## Design provenance
447
499
 
@@ -453,8 +505,8 @@ twelve earlier JavaScript release generations.
453
505
 
454
506
  The TypeScript implementation is not a source translation. It redesigned the
455
507
  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
508
+ `node:sqlite`/`pg`/`mysql2` adapters, transport-neutral realtime sessions, a
509
+ framework-neutral component registry, and browser-safe package exports. The
458
510
  [parity ledger](docs/parity.md) records capability relationships and deliberate
459
511
  runtime differences.
460
512
 
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.14.2";
1
+ export declare const VERSION = "0.14.3";
2
2
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
- export const VERSION = "0.14.2";
1
+ export const VERSION = "0.14.3";
2
2
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solid-objects",
3
- "version": "0.14.2",
3
+ "version": "0.14.3",
4
4
  "description": "Race-free realtime state per application identity, backed by your SQL database",
5
5
  "type": "module",
6
6
  "license": "MIT",