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 +26 -0
- package/README.md +236 -184
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
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
|
|
36
|
-
>
|
|
37
|
-
>
|
|
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
|
|
46
|
-
static override readonly actorType = "
|
|
69
|
+
class TicketSale extends Actor {
|
|
70
|
+
static override readonly actorType = "TicketSale"
|
|
71
|
+
|
|
72
|
+
remaining = 100
|
|
73
|
+
holds: Record<string, number> = {}
|
|
47
74
|
|
|
48
|
-
|
|
75
|
+
override observables(): Record<string, unknown> {
|
|
76
|
+
return { remaining: broadcastValue(this.remaining) }
|
|
77
|
+
}
|
|
49
78
|
|
|
50
|
-
|
|
51
|
-
this.
|
|
52
|
-
|
|
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: "
|
|
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
|
|
66
|
-
|
|
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
|
-
|
|
73
|
-
commit one state transition at a time, even when different requests or
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
137
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
221
|
-
cannot commit.
|
|
222
|
-
and the
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
-
|
|
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
|
-
-
|
|
263
|
-
|
|
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
|
-
|
|
270
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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`
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
|
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,
|
|
433
|
-
and stale-process recovery roles.
|
|
434
|
-
|
|
435
|
-
session-backed CSRF protection
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
and [
|
|
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
|
-
|
|
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.
|
|
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.
|
|
1
|
+
export const VERSION = "0.14.3";
|
|
2
2
|
//# sourceMappingURL=version.js.map
|