solid-objects 0.17.1 → 0.17.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 +46 -0
- package/README.md +2 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/agents.md +266 -5
- package/docs/api.md +7 -5
- package/docs/parity.md +1 -1
- package/docs/releasing.md +7 -4
- package/docs/virtual-actors.md +4 -0
- package/examples/browser/draft-worker.js +39 -0
- package/examples/browser/page.js +30 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.17.3 - 2026-10-08
|
|
4
|
+
|
|
5
|
+
- `docs/agents.md` has a new step 12 for the browser runtime. It covers when
|
|
6
|
+
the browser runtime fits, the install with `@sqlite.org/sqlite-wasm`, the
|
|
7
|
+
entry points, a module worker example, authorization in the browser, the
|
|
8
|
+
platform limits, transmit to a Node.js or Rails server, and the checks.
|
|
9
|
+
- `examples/browser/draft-worker.js` and `examples/browser/page.js` are the
|
|
10
|
+
guide example. The Chromium suite runs them: state survives a reload, two
|
|
11
|
+
tabs share one draft, and the second tab continues after the holder tab
|
|
12
|
+
closes. `pnpm run check:documentation` fails when the guide does not embed
|
|
13
|
+
the current files.
|
|
14
|
+
- The example keeps short lease settings. With the default settings, a call
|
|
15
|
+
from the remaining tab timed out while it waited for the lease of the closed
|
|
16
|
+
tab.
|
|
17
|
+
- The documentation link check skips fenced code blocks, so code such as
|
|
18
|
+
`ref[operation](value)` is not read as a link.
|
|
19
|
+
- Correct the transmit docs: `registerTransmit` never reads the HTTP response,
|
|
20
|
+
so a server 422 stops retries only when the `deliver` callback throws
|
|
21
|
+
`NonRetryableError`. The agent guide shows that callback, `docs/api.md` says
|
|
22
|
+
so, and a new test proves that `NonRetryableError` from `deliver`
|
|
23
|
+
dead-letters the effect after one attempt, where a plain `Error` retries.
|
|
24
|
+
- The page example rejects every waiting and later call when the worker fails
|
|
25
|
+
to load. Before, a call waited with no end.
|
|
26
|
+
|
|
27
|
+
## 0.17.2 - 2026-10-08
|
|
28
|
+
|
|
29
|
+
- The README names the agent guide at the start of Installation, and the agent
|
|
30
|
+
guide says that a reminder changes state only when it runs under
|
|
31
|
+
`runtime.run(signal)`, so a query must not compute expiry from the clock.
|
|
32
|
+
- Correct `docs/agents.md`: `reject` takes a code and an options object,
|
|
33
|
+
`this.reject(code, { message })`, not `this.reject(code, message)`.
|
|
34
|
+
- `docs/agents.md` now says that a worker process runs only the actor classes
|
|
35
|
+
that it knows. For an unregistered class, actor setup fails with
|
|
36
|
+
`UnknownActorType`, the worker reports `solid_objects.activation.failed`,
|
|
37
|
+
and the message returns to the queue without a counted attempt. Nothing
|
|
38
|
+
prints without an `instrumentation` callback. Register each class before
|
|
39
|
+
`runtime.run(signal)`.
|
|
40
|
+
It also tells agents to install the current release, puts the
|
|
41
|
+
deny-by-default reminder beside the install step, and lists the API
|
|
42
|
+
mistakes found in agent-written code.
|
|
43
|
+
- Claim the Context7 library: `context7.json` now carries the library `url` and
|
|
44
|
+
the maintainer `public_key`.
|
|
45
|
+
- Add the Context7 refresh workflow. A push to `main` that changes the README,
|
|
46
|
+
`context7.json`, `docs/`, or `examples/` asks Context7 to refresh the index.
|
|
47
|
+
It uses the `CONTEXT7_API_KEY` repository secret.
|
|
48
|
+
|
|
3
49
|
## 0.17.1 - 2026-10-08
|
|
4
50
|
|
|
5
51
|
- Check holds with `Object.hasOwn` in the README and guide ticket sale
|
package/README.md
CHANGED
|
@@ -49,7 +49,8 @@ subscriptions. Start with the [Cloudflare example](examples/cloudflare/README.md
|
|
|
49
49
|
## Installation
|
|
50
50
|
|
|
51
51
|
Solid Objects is ESM-only and requires Node.js 24.4 or newer. TypeScript users
|
|
52
|
-
need TypeScript 5.9 or newer.
|
|
52
|
+
need TypeScript 5.9 or newer. Coding agents should follow the
|
|
53
|
+
[agent guide](docs/agents.md), which gives each step in order.
|
|
53
54
|
|
|
54
55
|
```bash
|
|
55
56
|
npm install solid-objects
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.17.
|
|
1
|
+
export declare const VERSION = "0.17.3";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = "0.17.
|
|
1
|
+
export const VERSION = "0.17.3";
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
package/docs/agents.md
CHANGED
|
@@ -55,6 +55,9 @@ with Cloudflare.
|
|
|
55
55
|
- Redis is optional. It only shortens wake-up latency and holds no durable
|
|
56
56
|
state.
|
|
57
57
|
|
|
58
|
+
The browser runtime has different requirements. See
|
|
59
|
+
[step 12](#12-use-the-browser-runtime).
|
|
60
|
+
|
|
58
61
|
[Supported versions](support.md) lists the CI matrix.
|
|
59
62
|
|
|
60
63
|
## 4. Install
|
|
@@ -68,6 +71,10 @@ The quickstart runs 25 concurrent calls against one identity on a temporary
|
|
|
68
71
|
SQLite database. It proves that no update is lost. Add `pg` or `mysql2` when
|
|
69
72
|
you use PostgreSQL or MySQL.
|
|
70
73
|
|
|
74
|
+
Install the current release. `npm install solid-objects` selects it. Do not
|
|
75
|
+
pin a version that you remember from earlier work; the API changed between
|
|
76
|
+
releases. The current version is on <https://www.npmjs.com/package/solid-objects>.
|
|
77
|
+
|
|
71
78
|
Create the runtime and its tables at startup:
|
|
72
79
|
|
|
73
80
|
```typescript
|
|
@@ -81,7 +88,9 @@ const runtime = configure({
|
|
|
81
88
|
await runtime.install()
|
|
82
89
|
```
|
|
83
90
|
|
|
84
|
-
|
|
91
|
+
Every authorization callback denies by default. Do step 5 before you call an
|
|
92
|
+
actor. [Configuration](configuration.md) lists each option and database
|
|
93
|
+
adapter.
|
|
85
94
|
|
|
86
95
|
## 5. Authorize
|
|
87
96
|
|
|
@@ -184,13 +193,26 @@ Obey these rules in actor code:
|
|
|
184
193
|
object argument.
|
|
185
194
|
- Use `this.schedule({ at, key })` for delayed work. A new `schedule` with the
|
|
186
195
|
same key moves the alarm.
|
|
187
|
-
- Use `this.reject(code, message)` for a business rule failure that must
|
|
188
|
-
retry.
|
|
196
|
+
- Use `this.reject(code, { message })` for a business rule failure that must
|
|
197
|
+
not retry. The second argument is an object, for example
|
|
198
|
+
`this.reject("room_full", { message: "The room is full" })`.
|
|
189
199
|
- Do not write application tables directly from an operation. Use
|
|
190
200
|
`this.commitAction()` for a short write in the same database.
|
|
191
201
|
- Do not call an external API in an operation. Use `this.emit()` and an effect
|
|
192
202
|
handler.
|
|
193
203
|
- Write each operation so that it can run again. Delivery is at least once.
|
|
204
|
+
- A reminder changes state only when it runs, and it runs only while a process
|
|
205
|
+
calls `runtime.run(signal)`. Do not compute expiry from the clock in a query;
|
|
206
|
+
read the state that the reminder committed.
|
|
207
|
+
|
|
208
|
+
Avoid these mistakes:
|
|
209
|
+
|
|
210
|
+
| Mistake | Correct form |
|
|
211
|
+
| ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
212
|
+
| `this.schedule({ at, key })` with no operation after it | `this.schedule({ at, key }).expire({ buyer })`. `schedule()` stages a reminder only when you call an operation on its result |
|
|
213
|
+
| `this.reject("room full")` or `this.reject(code, message)` | `this.reject("room_full", { message: "The room is full" })` |
|
|
214
|
+
| `registerEffect(name, (context, argumentsValue) => ...)` | `registerEffect(name, (argumentsValue, context) => ...)`. The staged arguments come first |
|
|
215
|
+
| A worker process that never names the actor class | `runtime.register(TicketSale)` before `runtime.run(signal)`. See step 7 |
|
|
194
216
|
|
|
195
217
|
[State and lifecycle](state-and-lifecycle.md) and the
|
|
196
218
|
[public API](api.md) give the full rules.
|
|
@@ -206,13 +228,25 @@ runs in the caller. These features need a process that calls
|
|
|
206
228
|
- Messages to other actors.
|
|
207
229
|
- Realtime broadcasts.
|
|
208
230
|
|
|
231
|
+
The worker process runs only the actor classes that it knows. For a message
|
|
232
|
+
whose class is not registered, actor setup fails with `UnknownActorType`. The
|
|
233
|
+
worker reports `solid_objects.activation.failed` through instrumentation,
|
|
234
|
+
returns the message to the queue without counting an attempt, and tries again.
|
|
235
|
+
Nothing prints unless the application sets an `instrumentation` callback, so
|
|
236
|
+
the message seems to wait with no error. Register each class before `run()`:
|
|
237
|
+
|
|
209
238
|
```typescript
|
|
239
|
+
runtime.register(TicketSale)
|
|
240
|
+
|
|
210
241
|
const controller = new AbortController()
|
|
211
242
|
process.once("SIGTERM", () => controller.abort())
|
|
212
243
|
await runtime.run(controller.signal)
|
|
213
244
|
await runtime.close()
|
|
214
245
|
```
|
|
215
246
|
|
|
247
|
+
`TicketSale.ref(...)` also registers the class, so a script that calls `ref()`
|
|
248
|
+
before `run()`, like the direct call above, already works.
|
|
249
|
+
|
|
216
250
|
The packaged `solid-objects start` command does the same for a runtime that
|
|
217
251
|
`solid-objects.config.js` exports. When no process runs, committed work waits
|
|
218
252
|
in SQL and runs later. [Operations](operations.md) covers roles and shutdown.
|
|
@@ -294,7 +328,234 @@ When you explain Solid Objects to a user, state these limits:
|
|
|
294
328
|
run.
|
|
295
329
|
- The package is pre-1.0. It has no measured scale and no known third-party
|
|
296
330
|
production use.
|
|
297
|
-
-
|
|
298
|
-
|
|
331
|
+
- `solid-objects/browser` is a WebSocket client, not the SQL runtime. The
|
|
332
|
+
browser runtime in step 12 is tested in Chromium only. The Cloudflare backend
|
|
333
|
+
is experimental.
|
|
299
334
|
|
|
300
335
|
The [correctness contract](correctness.md) is the source for each guarantee.
|
|
336
|
+
|
|
337
|
+
## 12. Use the browser runtime
|
|
338
|
+
|
|
339
|
+
The same `Actor` classes run in a browser module worker. The database is SQLite
|
|
340
|
+
WASM. Persistent state is in the origin private file system (OPFS), so it
|
|
341
|
+
survives a page reload.
|
|
342
|
+
|
|
343
|
+
Use the browser runtime when one identity in the browser, such as a draft, a
|
|
344
|
+
game, or a form, must keep its state across reloads and tabs. It also fits
|
|
345
|
+
writes that must wait on the device until the network returns. Select a simpler
|
|
346
|
+
tool in these cases:
|
|
347
|
+
|
|
348
|
+
| Requirement | Use instead |
|
|
349
|
+
| ---------------------------------------- | ----------------------------------------------- |
|
|
350
|
+
| A preference or another small value | `localStorage` |
|
|
351
|
+
| A cache of server responses | The Cache API or IndexedDB |
|
|
352
|
+
| State that must be correct for all users | A server runtime. The user controls the browser |
|
|
353
|
+
| Live updates from a server runtime only | `solid-objects/browser`, the WebSocket client |
|
|
354
|
+
|
|
355
|
+
### Install
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
npm install solid-objects @sqlite.org/sqlite-wasm
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
`@sqlite.org/sqlite-wasm` 3.50 or newer is an optional peer dependency. The
|
|
362
|
+
browser runtime needs it.
|
|
363
|
+
|
|
364
|
+
### Choose the entry point
|
|
365
|
+
|
|
366
|
+
| Need | Use |
|
|
367
|
+
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
368
|
+
| The runtime in a worker | `solid-objects/browser/host`. It exports `Actor`, `configure`, `sqliteWasm`, `sharedSqliteWasm`, and the tab host |
|
|
369
|
+
| One database for all tabs, a worker in each tab | `sharedSqliteWasm({ path })`. Storage is persistent by default |
|
|
370
|
+
| One worker in one tab only | `sqliteWasm({ path, storage: "persistent" })`. The default storage is `"temporary"` |
|
|
371
|
+
| One runtime for all tabs, with a leader tab | `startTabHost()` and `connectTabClient()` from `solid-objects/browser/tab-host` |
|
|
372
|
+
| Send local writes to a server | `this.transmit()` and `registerTransmit()`. See [Send writes to a server](#send-writes-to-a-server) |
|
|
373
|
+
|
|
374
|
+
### Example
|
|
375
|
+
|
|
376
|
+
This module worker hosts the runtime. Each tab starts one copy.
|
|
377
|
+
`sharedSqliteWasm` elects one tab to hold the database. The other tabs send
|
|
378
|
+
their SQL to that tab, and a new tab takes over when it closes.
|
|
379
|
+
|
|
380
|
+
```javascript
|
|
381
|
+
import { Actor, configure, sharedSqliteWasm } from "solid-objects/browser/host"
|
|
382
|
+
|
|
383
|
+
class NoteDraft extends Actor {
|
|
384
|
+
static actorType = "NoteDraft"
|
|
385
|
+
|
|
386
|
+
text = ""
|
|
387
|
+
revision = 0
|
|
388
|
+
|
|
389
|
+
edit({ text }) {
|
|
390
|
+
this.text = text
|
|
391
|
+
this.revision += 1
|
|
392
|
+
return this.revision
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const allowNoteDrafts = ({ actorType }) => actorType === "NoteDraft"
|
|
397
|
+
|
|
398
|
+
const runtime = configure({
|
|
399
|
+
database: sharedSqliteWasm({ path: "notes.db" }),
|
|
400
|
+
authorizeMessage: allowNoteDrafts,
|
|
401
|
+
authorizeQuery: allowNoteDrafts,
|
|
402
|
+
processAliveThresholdMilliseconds: 750,
|
|
403
|
+
leaseDurationMilliseconds: 750,
|
|
404
|
+
leaseRenewalIntervalMilliseconds: 250,
|
|
405
|
+
})
|
|
406
|
+
runtime.register(NoteDraft)
|
|
407
|
+
const installed = runtime.install()
|
|
408
|
+
installed.then(() => runtime.run(new AbortController().signal))
|
|
409
|
+
|
|
410
|
+
self.onmessage = async (event) => {
|
|
411
|
+
const { requestId, actorId, operation, argumentsValue } = event.data
|
|
412
|
+
try {
|
|
413
|
+
await installed
|
|
414
|
+
const value = await NoteDraft.ref(actorId)[operation](argumentsValue)
|
|
415
|
+
postMessage({ requestId, ok: true, value })
|
|
416
|
+
} catch (error) {
|
|
417
|
+
postMessage({ requestId, ok: false, message: String(error?.message ?? error) })
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
The page sends messages to the worker. It does not hold actor references:
|
|
423
|
+
|
|
424
|
+
```javascript
|
|
425
|
+
const worker = new Worker(new URL("./draft-worker.js", import.meta.url), { type: "module" })
|
|
426
|
+
const pending = new Map()
|
|
427
|
+
let nextRequestId = 0
|
|
428
|
+
let workerFailure
|
|
429
|
+
|
|
430
|
+
worker.onmessage = (event) => {
|
|
431
|
+
const { requestId, ok, value, message } = event.data
|
|
432
|
+
const request = pending.get(requestId)
|
|
433
|
+
pending.delete(requestId)
|
|
434
|
+
if (ok) {
|
|
435
|
+
request.resolve(value)
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
request.reject(new Error(message))
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
worker.onerror = (event) => {
|
|
442
|
+
workerFailure = new Error(`The actor worker failed: ${event.message || "it did not load"}`)
|
|
443
|
+
for (const request of pending.values()) request.reject(workerFailure)
|
|
444
|
+
pending.clear()
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function callActor({ actorId, operation, argumentsValue }) {
|
|
448
|
+
if (workerFailure) return Promise.reject(workerFailure)
|
|
449
|
+
const requestId = nextRequestId++
|
|
450
|
+
return new Promise((resolve, reject) => {
|
|
451
|
+
pending.set(requestId, { resolve, reject })
|
|
452
|
+
worker.postMessage({ requestId, actorId, operation, argumentsValue })
|
|
453
|
+
})
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
CI runs this example in Chromium. It checks that state survives a reload, that
|
|
458
|
+
two tabs share one draft, and that the second tab continues after the first
|
|
459
|
+
tab closes.
|
|
460
|
+
|
|
461
|
+
A closed tab does not shut down its runtime. Keep the short lease settings in
|
|
462
|
+
the example. With the default settings, the next tab waits for the old lease,
|
|
463
|
+
and calls time out before it expires.
|
|
464
|
+
|
|
465
|
+
### Authorize in the browser
|
|
466
|
+
|
|
467
|
+
The page and the worker run on the user's device, and the user can change
|
|
468
|
+
their code. A browser policy limits what your own page can call. It is not a
|
|
469
|
+
security boundary. Authorize again on the server for each write that leaves the
|
|
470
|
+
device.
|
|
471
|
+
|
|
472
|
+
### Rules for browser actors
|
|
473
|
+
|
|
474
|
+
- One worker hosts one runtime. Do not import `solid-objects/browser/host` in
|
|
475
|
+
a process that also imports the Node.js entry points.
|
|
476
|
+
- After the first `await` in an operation, `currentActor()`,
|
|
477
|
+
`applicationWritesForbidden()`, and the database deadline read as unset. Keep
|
|
478
|
+
guarded writes in synchronous actor code or in commit actions.
|
|
479
|
+
- Reminders and effects run only while a worker that calls
|
|
480
|
+
`runtime.run(signal)` is alive. When the user closes every tab, nothing runs.
|
|
481
|
+
- With `sharedSqliteWasm`, a statement can fail with `SharedDatabaseFailover`
|
|
482
|
+
when the holder tab closes during the statement. Write operations so that
|
|
483
|
+
they can run again, and retry the call.
|
|
484
|
+
- With `startTabHost()`, close the database in a `catch` block when
|
|
485
|
+
`startRuntime` fails. An open database blocks the next tab.
|
|
486
|
+
|
|
487
|
+
### Platform limits
|
|
488
|
+
|
|
489
|
+
- CI tests the browser runtime in Chromium only. Safari 16.4 and Firefox 111
|
|
490
|
+
added the OPFS API that persistent storage needs. Test on each engine that
|
|
491
|
+
you support.
|
|
492
|
+
- Persistent storage needs a secure context (HTTPS or `localhost`) and a
|
|
493
|
+
dedicated worker. `storage: "persistent"` fails fast where OPFS is missing.
|
|
494
|
+
- An embedded WebView, such as Cordova, WKWebView, or Android WebView, can lack
|
|
495
|
+
OPFS when the device browser has it. Test the WebView itself.
|
|
496
|
+
- The browser can clear the storage of an origin. Call
|
|
497
|
+
`navigator.storage.persist()` to ask it to keep the data, and send important
|
|
498
|
+
writes to a server.
|
|
499
|
+
|
|
500
|
+
### Send writes to a server
|
|
501
|
+
|
|
502
|
+
An operation stages a write for the server in the same transaction as its
|
|
503
|
+
state change:
|
|
504
|
+
|
|
505
|
+
```javascript
|
|
506
|
+
this.transmit().edit({ text })
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
`registerTransmit({ runtime, deliver })` sends each staged write to the server.
|
|
510
|
+
The runtime does not read the HTTP response. Your `deliver` callback decides
|
|
511
|
+
what happens:
|
|
512
|
+
|
|
513
|
+
```javascript
|
|
514
|
+
import { NonRetryableError } from "solid-objects/core"
|
|
515
|
+
import { registerTransmit } from "solid-objects/browser/host"
|
|
516
|
+
|
|
517
|
+
registerTransmit({
|
|
518
|
+
runtime,
|
|
519
|
+
deliver: async (envelope) => {
|
|
520
|
+
const response = await fetch("/sync", {
|
|
521
|
+
method: "POST",
|
|
522
|
+
headers: { "content-type": "application/json" },
|
|
523
|
+
body: JSON.stringify(envelope),
|
|
524
|
+
})
|
|
525
|
+
if (response.status === 422) {
|
|
526
|
+
throw new NonRetryableError(`The server rejected effect ${envelope.effectId}`)
|
|
527
|
+
}
|
|
528
|
+
if (!response.ok) throw new Error(`Sync failed with HTTP ${response.status}`)
|
|
529
|
+
},
|
|
530
|
+
})
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
- A normal return marks the write as delivered.
|
|
534
|
+
- An `Error` makes the effect try again later. This is the offline case.
|
|
535
|
+
- A `NonRetryableError` moves the effect to dead letters with no retry.
|
|
536
|
+
|
|
537
|
+
Set a high `maxAttempts` in `configure()`, because the default is 5 and a long
|
|
538
|
+
offline period can use all attempts. Run one effect worker for each local
|
|
539
|
+
runtime to keep the order of writes for each actor.
|
|
540
|
+
|
|
541
|
+
The server receives the write with `receiveTransmitEnvelope({ runtime, envelope })`
|
|
542
|
+
in Node.js, or with `SolidObjects::Transmission.receive(envelope)` in Rails.
|
|
543
|
+
Authenticate the device before this call, because this delivery skips
|
|
544
|
+
`authorizeMessage`. Delivery is at least once. The server applies a repeated
|
|
545
|
+
write once, because it uses the effect ID as the idempotency key. In Node.js,
|
|
546
|
+
return HTTP 422 for `InvalidPayload` and `IdempotencyConflict`. The `deliver`
|
|
547
|
+
callback above turns that status into a `NonRetryableError`, so the device
|
|
548
|
+
stops the retries.
|
|
549
|
+
|
|
550
|
+
The [public API](api.md#solid-objectstransmit) and the
|
|
551
|
+
[browser protocol](browser-protocol.md) give the full contract.
|
|
552
|
+
|
|
553
|
+
### Verify a browser implementation
|
|
554
|
+
|
|
555
|
+
1. Call an operation, reload the page, and confirm that the state is the same.
|
|
556
|
+
2. Send concurrent calls to one identity from two tabs. Assert the final state.
|
|
557
|
+
3. Close the tab that holds the database. Confirm that the other tab continues.
|
|
558
|
+
4. On each target engine, confirm that persistent storage opens, or fails with
|
|
559
|
+
a clear error.
|
|
560
|
+
5. If you send writes to a server, confirm that the server rejects a device
|
|
561
|
+
that it cannot authenticate, and applies a repeated write once.
|
package/docs/api.md
CHANGED
|
@@ -961,11 +961,13 @@ outbox with at-least-once delivery, per-actor order, and retry backoff.
|
|
|
961
961
|
```
|
|
962
962
|
|
|
963
963
|
The example uses a Fetch-style handler; any HTTP framework works. The 422
|
|
964
|
-
matters
|
|
965
|
-
|
|
966
|
-
`
|
|
967
|
-
|
|
968
|
-
|
|
964
|
+
matters only when the sending `deliver` callback reads it:
|
|
965
|
+
`registerTransmit` never sees the HTTP response. Throw `NonRetryableError`
|
|
966
|
+
(from `solid-objects/core` in a browser) for a 422, and the outbox
|
|
967
|
+
dead-letters the effect instead of retrying it; any other error retries.
|
|
968
|
+
`InvalidPayload` marks a malformed envelope, and `IdempotencyConflict`
|
|
969
|
+
marks a replay whose arguments changed; both are permanently unappliable,
|
|
970
|
+
and a 500 would make the outbox retry them until `maxAttempts` runs out.
|
|
969
971
|
|
|
970
972
|
- Per-actor order comes from an ordered drain: a claimed transmit effect
|
|
971
973
|
transmits every undelivered envelope for its actor up to its own mailbox
|
package/docs/parity.md
CHANGED
|
@@ -4,7 +4,7 @@ This ledger tracks capability parity with the Ruby `solid_objects` gem.
|
|
|
4
4
|
Parity preserves a capability and its correctness or security boundary. It does
|
|
5
5
|
not copy a Rails API into Node.
|
|
6
6
|
|
|
7
|
-
Reference: Ruby `solid_objects` 0.17.
|
|
7
|
+
Reference: Ruby `solid_objects` 0.17.2. The JavaScript package began at the Ruby
|
|
8
8
|
design's `0.12` capability generation; that version number did not imply earlier
|
|
9
9
|
JavaScript releases.
|
|
10
10
|
|
package/docs/releasing.md
CHANGED
|
@@ -38,8 +38,10 @@ npm trust github solid-objects \
|
|
|
38
38
|
3. Read `docs/virtual-actors.md` and `docs/agents.md` against the release.
|
|
39
39
|
Correct any requirement, compatibility, or guarantee statement that the
|
|
40
40
|
release changed. `pnpm run check:documentation` fails when the category
|
|
41
|
-
guide no longer embeds `examples/ticket-sale.ts`,
|
|
42
|
-
|
|
41
|
+
guide no longer embeds `examples/ticket-sale.ts`, or when the agent guide
|
|
42
|
+
no longer embeds the files in `examples/browser/`. `pnpm run test:package`
|
|
43
|
+
runs the ticket sale example against the packed tarball, and
|
|
44
|
+
`pnpm run test:browser` runs the browser example in Chromium.
|
|
43
45
|
4. Commit and push `main`.
|
|
44
46
|
5. Create and push an annotated tag matching the package version:
|
|
45
47
|
|
|
@@ -60,5 +62,6 @@ npm already holds, it still creates a missing release.
|
|
|
60
62
|
|
|
61
63
|
After the tag publishes, refresh the solidobjects.dev documentation snapshot
|
|
62
64
|
from the tag and redeploy the site. The site's `check:release` step refuses a
|
|
63
|
-
snapshot that is not the latest published tag.
|
|
64
|
-
|
|
65
|
+
snapshot that is not the latest published tag. The Context7 refresh workflow
|
|
66
|
+
refreshes the index after each push to `main` that changes the documentation.
|
|
67
|
+
Confirm that its run succeeded.
|
package/docs/virtual-actors.md
CHANGED
|
@@ -113,6 +113,10 @@ buyer gets the ticket. The hold and its reminder commit in one transaction. If
|
|
|
113
113
|
the worker stops, the reminder stays in `tickets.sqlite3`. It runs when the
|
|
114
114
|
worker starts again.
|
|
115
115
|
|
|
116
|
+
The `work` branch can run the reminder because `TicketSale.ref(...)` registers
|
|
117
|
+
the class first. A separate worker process must call
|
|
118
|
+
`runtime.register(TicketSale)` before `runtime.run(signal)`.
|
|
119
|
+
|
|
116
120
|
The authorization callbacks above allow every caller. Use them only for a
|
|
117
121
|
local example. The package release check runs this file against the packed
|
|
118
122
|
npm tarball. The source is [`examples/ticket-sale.ts`](../examples/ticket-sale.ts).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Actor, configure, sharedSqliteWasm } from "solid-objects/browser/host"
|
|
2
|
+
|
|
3
|
+
class NoteDraft extends Actor {
|
|
4
|
+
static actorType = "NoteDraft"
|
|
5
|
+
|
|
6
|
+
text = ""
|
|
7
|
+
revision = 0
|
|
8
|
+
|
|
9
|
+
edit({ text }) {
|
|
10
|
+
this.text = text
|
|
11
|
+
this.revision += 1
|
|
12
|
+
return this.revision
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const allowNoteDrafts = ({ actorType }) => actorType === "NoteDraft"
|
|
17
|
+
|
|
18
|
+
const runtime = configure({
|
|
19
|
+
database: sharedSqliteWasm({ path: "notes.db" }),
|
|
20
|
+
authorizeMessage: allowNoteDrafts,
|
|
21
|
+
authorizeQuery: allowNoteDrafts,
|
|
22
|
+
processAliveThresholdMilliseconds: 750,
|
|
23
|
+
leaseDurationMilliseconds: 750,
|
|
24
|
+
leaseRenewalIntervalMilliseconds: 250,
|
|
25
|
+
})
|
|
26
|
+
runtime.register(NoteDraft)
|
|
27
|
+
const installed = runtime.install()
|
|
28
|
+
installed.then(() => runtime.run(new AbortController().signal))
|
|
29
|
+
|
|
30
|
+
self.onmessage = async (event) => {
|
|
31
|
+
const { requestId, actorId, operation, argumentsValue } = event.data
|
|
32
|
+
try {
|
|
33
|
+
await installed
|
|
34
|
+
const value = await NoteDraft.ref(actorId)[operation](argumentsValue)
|
|
35
|
+
postMessage({ requestId, ok: true, value })
|
|
36
|
+
} catch (error) {
|
|
37
|
+
postMessage({ requestId, ok: false, message: String(error?.message ?? error) })
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const worker = new Worker(new URL("./draft-worker.js", import.meta.url), { type: "module" })
|
|
2
|
+
const pending = new Map()
|
|
3
|
+
let nextRequestId = 0
|
|
4
|
+
let workerFailure
|
|
5
|
+
|
|
6
|
+
worker.onmessage = (event) => {
|
|
7
|
+
const { requestId, ok, value, message } = event.data
|
|
8
|
+
const request = pending.get(requestId)
|
|
9
|
+
pending.delete(requestId)
|
|
10
|
+
if (ok) {
|
|
11
|
+
request.resolve(value)
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
request.reject(new Error(message))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
worker.onerror = (event) => {
|
|
18
|
+
workerFailure = new Error(`The actor worker failed: ${event.message || "it did not load"}`)
|
|
19
|
+
for (const request of pending.values()) request.reject(workerFailure)
|
|
20
|
+
pending.clear()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function callActor({ actorId, operation, argumentsValue }) {
|
|
24
|
+
if (workerFailure) return Promise.reject(workerFailure)
|
|
25
|
+
const requestId = nextRequestId++
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
pending.set(requestId, { resolve, reject })
|
|
28
|
+
worker.postMessage({ requestId, actorId, operation, argumentsValue })
|
|
29
|
+
})
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solid-objects",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.3",
|
|
4
4
|
"description": "SQL-backed virtual actor library for TypeScript and Node.js, with durable state, ordered operations, and automatic activation on SQLite, PostgreSQL, or MySQL",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|