solid-objects 0.12.1 → 0.13.0
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 +38 -0
- package/README.md +228 -953
- package/dist/actor.d.ts +12 -0
- package/dist/actor.d.ts.map +1 -1
- package/dist/actor.js +25 -2
- package/dist/actor.js.map +1 -1
- package/dist/browser/components.d.ts.map +1 -1
- package/dist/browser/components.js +4 -1
- package/dist/browser/components.js.map +1 -1
- package/dist/browser/index.d.ts +1 -0
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +8 -0
- package/dist/browser/index.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +13 -1
- package/dist/cli.js.map +1 -1
- package/dist/configuration.d.ts +1 -0
- package/dist/configuration.d.ts.map +1 -1
- package/dist/configuration.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +12 -3
- package/dist/doctor.js.map +1 -1
- package/dist/examples/sqlite-quickstart.js +83 -0
- package/dist/examples/sqlite-quickstart.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/records.d.ts +1 -0
- package/dist/records.d.ts.map +1 -1
- package/dist/repository.d.ts +4 -1
- package/dist/repository.d.ts.map +1 -1
- package/dist/repository.js +5 -4
- package/dist/repository.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +37 -6
- package/dist/runtime.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +15 -4
- package/dist/schema.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/web/assets.d.ts +4 -0
- package/dist/web/assets.d.ts.map +1 -0
- package/dist/web/assets.js +11 -0
- package/dist/web/assets.js.map +1 -0
- package/dist/web/index.d.ts +28 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +642 -0
- package/dist/web/index.js.map +1 -0
- package/dist/web/node.d.ts +3 -0
- package/dist/web/node.d.ts.map +1 -0
- package/dist/web/node.js +109 -0
- package/dist/web/node.js.map +1 -0
- package/dist/web/render.d.ts +64 -0
- package/dist/web/render.d.ts.map +1 -0
- package/dist/web/render.js +331 -0
- package/dist/web/render.js.map +1 -0
- package/dist/web/store.d.ts +65 -0
- package/dist/web/store.d.ts.map +1 -0
- package/dist/web/store.js +303 -0
- package/dist/web/store.js.map +1 -0
- package/dist/web/types.d.ts +82 -0
- package/dist/web/types.d.ts.map +1 -0
- package/dist/web/types.js +2 -0
- package/dist/web/types.js.map +1 -0
- package/docs/api.md +63 -0
- package/docs/architecture.md +16 -10
- package/docs/authorization.md +12 -3
- package/docs/benchmarks.md +123 -0
- package/docs/browser-protocol.md +15 -8
- package/docs/comparisons.md +36 -0
- package/docs/configuration.md +3 -1
- package/docs/correctness.md +23 -0
- package/docs/dashboard.md +196 -0
- package/docs/fit.md +58 -0
- package/docs/parity.md +37 -34
- package/docs/releasing.md +6 -4
- package/docs/state-and-lifecycle.md +10 -0
- package/docs/support.md +39 -0
- package/examples/failure-recovery/actor.ts +51 -0
- package/examples/failure-recovery/demo.ts +233 -0
- package/examples/failure-recovery/worker.ts +46 -0
- package/examples/sqlite-quickstart.ts +109 -0
- package/package.json +25 -4
package/docs/authorization.md
CHANGED
|
@@ -18,9 +18,12 @@ before actor type lookup, so denied callers cannot probe the registry. A new
|
|
|
18
18
|
connection must use a newly resolved authorization context; do not copy a user
|
|
19
19
|
object from an earlier request or trust an actor ID supplied by the browser.
|
|
20
20
|
|
|
21
|
-
Successful subscription authorization allows the
|
|
22
|
-
projection for that actor, including the
|
|
23
|
-
|
|
21
|
+
Successful subscription authorization allows the `broadcastValue()` portion
|
|
22
|
+
of the explicit `observables()` projection for that actor, including the
|
|
23
|
+
immediate committed replay. Unwrapped observables and values marked with
|
|
24
|
+
`broadcastInvalidation()` disclose only their names, not their values.
|
|
25
|
+
Subscription authorization does not
|
|
26
|
+
authorize actor state, operations, queries, destruction, or administration.
|
|
24
27
|
The runtime removes all registrations when the host calls `session.close()`.
|
|
25
28
|
|
|
26
29
|
Requested personalized payloads cross both boundaries. The runtime first calls
|
|
@@ -29,3 +32,9 @@ the same fresh session authorization context before every projection. A false
|
|
|
29
32
|
query decision omits that payload. Projection and authorization failures are
|
|
30
33
|
confined to the payload name and emit metadata-only instrumentation; they do
|
|
31
34
|
not reject observable delivery or expose exception messages.
|
|
35
|
+
|
|
36
|
+
The optional operator dashboard applies `authorizeAdministration` before every
|
|
37
|
+
data route. Each route supplies its own action, resource, optional resource ID,
|
|
38
|
+
and the host-provided request authorization context. Dashboard authorization
|
|
39
|
+
does not replace host authentication. State-changing routes additionally
|
|
40
|
+
require a masked token backed by the host's `DashboardSession`.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Benchmarks
|
|
2
|
+
|
|
3
|
+
The benchmark harness measures committed actor operations. It is intended to
|
|
4
|
+
show tradeoffs and catch large regressions, not to predict application capacity.
|
|
5
|
+
|
|
6
|
+
## Scenarios
|
|
7
|
+
|
|
8
|
+
- `warm-hot`: all operations target one previously created identity.
|
|
9
|
+
- `warm-many`: operations rotate across 100 previously created identities.
|
|
10
|
+
- `cold-many`: every measured operation creates a new identity.
|
|
11
|
+
- `synchronous`: the actor method mutates state without yielding.
|
|
12
|
+
- `asynchronous`: the actor method yields through `setImmediate()` before the
|
|
13
|
+
same mutation.
|
|
14
|
+
- `one-process`: four actor workers run in the caller's Node process.
|
|
15
|
+
- `four-processes`: four Node worker processes share the database.
|
|
16
|
+
|
|
17
|
+
Latency begins before durable enqueue and ends when the committed result is
|
|
18
|
+
available. Throughput uses the wall time for the measured batch. Percentiles
|
|
19
|
+
use nearest rank. Defaults are 25 warmup operations, 250 measured operations,
|
|
20
|
+
and client concurrency 16.
|
|
21
|
+
|
|
22
|
+
## Run the harness
|
|
23
|
+
|
|
24
|
+
SQLite needs no service:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pnpm run benchmark -- --database sqlite
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
PostgreSQL and MySQL runs require a disposable database. The harness deletes
|
|
31
|
+
benchmark rows but leaves its uniquely prefixed empty tables for inspection.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
SOLID_OBJECTS_POSTGRESQL_BENCHMARK_URL=postgresql://... \
|
|
35
|
+
pnpm run benchmark -- --database postgresql
|
|
36
|
+
|
|
37
|
+
SOLID_OBJECTS_MYSQL_BENCHMARK_URL=mysql://... \
|
|
38
|
+
pnpm run benchmark -- --database mysql
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Use `--operations`, `--warmup`, and `--concurrency` to change the recorded
|
|
42
|
+
dataset. Redirect stdout to retain the JSON result.
|
|
43
|
+
|
|
44
|
+
## Observed results
|
|
45
|
+
|
|
46
|
+
Measured on August 15, 2026 with the prepared `0.13.0` source tree:
|
|
47
|
+
|
|
48
|
+
- Apple M5, 10 logical CPUs, 24 GiB memory
|
|
49
|
+
- macOS 26.6 (`darwin 25.6.0`)
|
|
50
|
+
- Node.js 26.7.0
|
|
51
|
+
- SQLite 3.53.4 on the internal SSD, PostgreSQL 18.4 and MySQL 8.4.11 in
|
|
52
|
+
Docker Desktop
|
|
53
|
+
- 25 warmup operations, 250 measured operations, concurrency 16
|
|
54
|
+
|
|
55
|
+
### SQLite 3.53.4
|
|
56
|
+
|
|
57
|
+
| Topology | Shape | Handler | ops/s | p50 ms | p95 ms | p99 ms |
|
|
58
|
+
| -------------- | --------- | ------------ | -----: | -----: | ------: | ------: |
|
|
59
|
+
| one process | warm hot | synchronous | 95.51 | 35.34 | 1159.07 | 1711.76 |
|
|
60
|
+
| one process | warm hot | asynchronous | 448.63 | 35.96 | 38.95 | 41.47 |
|
|
61
|
+
| one process | warm many | synchronous | 44.35 | 141.55 | 1886.35 | 2543.15 |
|
|
62
|
+
| one process | warm many | asynchronous | 240.64 | 48.62 | 75.10 | 671.64 |
|
|
63
|
+
| one process | cold many | synchronous | 30.91 | 436.98 | 1502.26 | 1729.10 |
|
|
64
|
+
| one process | cold many | asynchronous | 57.15 | 167.36 | 967.74 | 1066.49 |
|
|
65
|
+
| four processes | warm hot | synchronous | 453.10 | 27.24 | 72.09 | 77.88 |
|
|
66
|
+
| four processes | warm hot | asynchronous | 487.39 | 29.27 | 60.66 | 68.28 |
|
|
67
|
+
| four processes | warm many | synchronous | 78.88 | 86.33 | 890.29 | 1381.65 |
|
|
68
|
+
| four processes | warm many | asynchronous | 47.56 | 220.37 | 1130.53 | 1341.86 |
|
|
69
|
+
| four processes | cold many | synchronous | 70.45 | 174.67 | 643.53 | 668.46 |
|
|
70
|
+
| four processes | cold many | asynchronous | 39.92 | 221.01 | 1309.13 | 1442.30 |
|
|
71
|
+
|
|
72
|
+
### PostgreSQL 18.4
|
|
73
|
+
|
|
74
|
+
| Topology | Shape | Handler | ops/s | p50 ms | p95 ms | p99 ms |
|
|
75
|
+
| -------------- | --------- | ------------ | ----: | ------: | ------: | ------: |
|
|
76
|
+
| one process | warm hot | synchronous | 66.99 | 232.89 | 317.94 | 375.68 |
|
|
77
|
+
| one process | warm hot | asynchronous | 72.27 | 211.57 | 280.89 | 316.61 |
|
|
78
|
+
| one process | warm many | synchronous | 76.69 | 129.88 | 552.01 | 1963.27 |
|
|
79
|
+
| one process | warm many | asynchronous | 25.78 | 340.96 | 2326.38 | 6917.26 |
|
|
80
|
+
| one process | cold many | synchronous | 12.70 | 1262.08 | 1734.77 | 2199.20 |
|
|
81
|
+
| one process | cold many | asynchronous | 11.23 | 1254.46 | 2796.81 | 2905.99 |
|
|
82
|
+
| four processes | warm hot | synchronous | 83.71 | 191.83 | 220.87 | 231.84 |
|
|
83
|
+
| four processes | warm hot | asynchronous | 86.42 | 184.56 | 210.11 | 215.48 |
|
|
84
|
+
| four processes | warm many | synchronous | 95.47 | 100.31 | 330.27 | 1663.84 |
|
|
85
|
+
| four processes | warm many | asynchronous | 37.04 | 232.71 | 1601.29 | 4447.85 |
|
|
86
|
+
| four processes | cold many | synchronous | 14.79 | 1114.18 | 1263.33 | 1313.70 |
|
|
87
|
+
| four processes | cold many | asynchronous | 11.68 | 1231.94 | 2555.34 | 2848.80 |
|
|
88
|
+
|
|
89
|
+
### MySQL 8.4.11
|
|
90
|
+
|
|
91
|
+
| Topology | Shape | Handler | ops/s | p50 ms | p95 ms | p99 ms |
|
|
92
|
+
| -------------- | --------- | ------------ | ----: | ------: | ------: | ------: |
|
|
93
|
+
| one process | warm hot | synchronous | 28.11 | 504.36 | 1648.10 | 2088.05 |
|
|
94
|
+
| one process | warm hot | asynchronous | 25.29 | 508.26 | 1750.17 | 2222.72 |
|
|
95
|
+
| one process | warm many | synchronous | 61.79 | 165.62 | 463.59 | 2036.31 |
|
|
96
|
+
| one process | warm many | asynchronous | 22.45 | 446.17 | 2420.92 | 7818.65 |
|
|
97
|
+
| one process | cold many | synchronous | 10.23 | 1353.37 | 3038.76 | 3185.15 |
|
|
98
|
+
| one process | cold many | asynchronous | 10.04 | 1338.31 | 3344.18 | 3477.06 |
|
|
99
|
+
| four processes | warm hot | synchronous | 29.89 | 427.18 | 1483.88 | 1997.56 |
|
|
100
|
+
| four processes | warm hot | asynchronous | 27.59 | 448.16 | 1518.00 | 1589.57 |
|
|
101
|
+
| four processes | warm many | synchronous | 69.77 | 157.79 | 410.83 | 2263.13 |
|
|
102
|
+
| four processes | warm many | asynchronous | 36.21 | 265.24 | 1579.43 | 4594.33 |
|
|
103
|
+
| four processes | cold many | synchronous | 13.68 | 1088.93 | 2186.30 | 2519.92 |
|
|
104
|
+
| four processes | cold many | asynchronous | 11.38 | 1209.84 | 2532.38 | 2730.74 |
|
|
105
|
+
|
|
106
|
+
The poor throughput and tail latency in cold and asynchronous cases are
|
|
107
|
+
observed limitations, not capacity recommendations. The small asynchronous
|
|
108
|
+
yield changed scheduling enough to improve some cases and worsen others;
|
|
109
|
+
repeat runs on application-shaped payloads are required before drawing a
|
|
110
|
+
general conclusion. PostgreSQL 14, MySQL 8.0, and other database versions are
|
|
111
|
+
covered by integration tests but were not benchmarked.
|
|
112
|
+
|
|
113
|
+
## Sources of bias
|
|
114
|
+
|
|
115
|
+
- A developer laptop shares CPU, memory, and storage with unrelated processes.
|
|
116
|
+
- Loopback database connections exclude production network latency.
|
|
117
|
+
- Filesystem cache, SQLite WAL state, Node JIT warmup, and garbage collection
|
|
118
|
+
affect short runs.
|
|
119
|
+
- Docker Desktop adds virtualization overhead to containerized databases.
|
|
120
|
+
- The payload is a small counter, not a representative application state size.
|
|
121
|
+
- The harness measures default durability settings and one client concurrency.
|
|
122
|
+
- Hot-identity results deliberately include serialization and cannot be scaled
|
|
123
|
+
by adding workers.
|
package/docs/browser-protocol.md
CHANGED
|
@@ -5,12 +5,13 @@ subscription requests and receives JSON invalidation envelopes over WebSocket.
|
|
|
5
5
|
The application supplies the rendering callback and authenticated WebSocket
|
|
6
6
|
server; `runtime.realtime` supplies the server-side session protocol.
|
|
7
7
|
|
|
8
|
-
The `observables` object contains values
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
The `observables` object contains changed values and every subscriber authorized
|
|
9
|
+
for that actor receives the same projection. The `invalidations` array contains
|
|
10
|
+
changed observable names whose values were deliberately withheld with
|
|
11
|
+
`broadcastInvalidation()`. Use those names to refresh an application endpoint
|
|
12
|
+
that reauthorizes the component request. Use personalized payloads when the
|
|
13
|
+
browser needs subscriber-specific data, and keep secrets out of value-broadcast
|
|
14
|
+
observables.
|
|
14
15
|
|
|
15
16
|
The 0.1 subscription request is:
|
|
16
17
|
|
|
@@ -39,10 +40,15 @@ The 0.1 envelope is:
|
|
|
39
40
|
"actorId": "primary",
|
|
40
41
|
"instanceId": "019...",
|
|
41
42
|
"revision": "42",
|
|
42
|
-
"observables": { "
|
|
43
|
+
"observables": { "version": 3 },
|
|
44
|
+
"invalidations": ["playerOne"]
|
|
43
45
|
}
|
|
44
46
|
```
|
|
45
47
|
|
|
48
|
+
`invalidations` was added as an optional version-1 field in 0.13.0. Clients
|
|
49
|
+
must treat a missing field as an empty array. A name appears in either
|
|
50
|
+
`observables` or `invalidations`, never both, in runtime-produced envelopes.
|
|
51
|
+
|
|
46
52
|
Requested personalized projections use a separate envelope:
|
|
47
53
|
|
|
48
54
|
```json
|
|
@@ -74,7 +80,8 @@ stale revisions within an incarnation.
|
|
|
74
80
|
`SolidObjectsComponentRegistry` maps changed observable names to keyed UI
|
|
75
81
|
registrations. The browser supplies an asynchronous `refresh` function and a
|
|
76
82
|
synchronous `apply` function, so HTML, virtual DOM, and framework-native render
|
|
77
|
-
results use the same coordination contract without
|
|
83
|
+
results use the same coordination contract without assuming a rendering
|
|
84
|
+
framework.
|
|
78
85
|
|
|
79
86
|
Components may share a batch name. A microtask unions affected components in
|
|
80
87
|
the same actor, batch, incarnation, and revision into one refresh request.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# System comparisons
|
|
2
|
+
|
|
3
|
+
This guide compares coordination models so an application can choose the
|
|
4
|
+
smallest mechanism that meets its requirements. It does not rank the projects.
|
|
5
|
+
|
|
6
|
+
| Approach | State and serialization unit | Deployment and durable substrate | Separate service | Replay versus state | Realtime and edge placement | Cross-identity transaction | Data access |
|
|
7
|
+
| --------------------------- | ---------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | --------------------------------------------------------------------- | -------------------------------------------------------------- | --------------------------------------------------------------- |
|
|
8
|
+
| SQL transaction or row lock | Rows selected by one transaction | Application process and SQL database | No | The application retries a failed transaction | Application-owned | Yes, for rows in the same database transaction | Ordinary application tables and SQL tools |
|
|
9
|
+
| Traditional job queue | A job, queue, or configured grouping key | Workers plus broker or queue database | Usually | The job is retried; mutable entity state remains application-owned | Application-owned | Not supplied by the queue | Queue administration plus application data stores |
|
|
10
|
+
| Solid Objects | Actor class and application-defined ID | Node processes plus existing SQLite, PostgreSQL, or MySQL | No; Redis wake-up is optional | The operation retries against durable actor state | Committed projections; application-owned transport; no edge placement | No | Relational tables, typed administration, CLI, and dashboard |
|
|
11
|
+
| Cloudflare Durable Objects | Object class and globally unique ID | Cloudflare Workers plus per-object managed storage | Cloudflare platform | Object activation with durable state, not workflow-step replay | WebSockets and Cloudflare-selected object location | Storage transactions are scoped to one object | Object storage APIs and platform tooling |
|
|
12
|
+
| Rivet Actors | Addressable actor | Rivet Engine or managed compute with actor state, KV, or per-actor SQLite | Rivet Engine | Actor persistence and lifecycle; workflows add recorded steps | Actor events and deployment-dependent placement | No general transaction across actors | Actor APIs and selected persistence model |
|
|
13
|
+
| DBOS | Workflow ID and checkpointed steps | Application processes plus PostgreSQL system database | No orchestration server for the library; Conductor is recommended for distributed recovery | Deterministic workflow replay skips checkpointed steps | Workflow events; application placement | PostgreSQL transactions remain separate from workflow identity | PostgreSQL system database, client, CLI, and optional Conductor |
|
|
14
|
+
| Restate | Service handler or keyed virtual object | Application services plus Restate's durable log and state store | Yes | Durable execution journals handler progress and object state | Service protocol and Restate deployment | No shared SQL transaction across object keys | Restate APIs, state tools, snapshots, and backups |
|
|
15
|
+
|
|
16
|
+
## Primary references
|
|
17
|
+
|
|
18
|
+
- PostgreSQL documents row-lock behavior, transaction lifetime, and deadlock
|
|
19
|
+
handling in [Explicit Locking](https://www.postgresql.org/docs/18/explicit-locking.html).
|
|
20
|
+
- BullMQ is one representative traditional queue; its
|
|
21
|
+
[worker concurrency documentation](https://docs.bullmq.io/guide/workers/concurrency)
|
|
22
|
+
distinguishes local concurrency from multiple worker processes.
|
|
23
|
+
- Cloudflare documents global uniqueness, per-object storage, single-threaded
|
|
24
|
+
execution, and placement in [What are Durable Objects?](https://developers.cloudflare.com/durable-objects/concepts/what-are-durable-objects/).
|
|
25
|
+
- Rivet documents addressable actors and persistence in
|
|
26
|
+
[Actors](https://rivet.dev/docs/actors/) and
|
|
27
|
+
[Persistence](https://rivet.dev/docs/actors/persistence).
|
|
28
|
+
- DBOS documents its PostgreSQL checkpoints, recovery, distributed processes,
|
|
29
|
+
and optional control plane in [DBOS Architecture](https://docs.dbos.dev/architecture).
|
|
30
|
+
- Restate documents per-key write serialization in
|
|
31
|
+
[Virtual Objects](https://docs.restate.dev/foundations/services#virtual-object)
|
|
32
|
+
and its storage requirements in the
|
|
33
|
+
[self-hosted server overview](https://docs.restate.dev/server/overview).
|
|
34
|
+
|
|
35
|
+
External systems evolve. Recheck these primary sources before relying on one
|
|
36
|
+
row as a procurement or architecture decision.
|
package/docs/configuration.md
CHANGED
|
@@ -28,7 +28,9 @@ through `runtime.ref(ActorClass, actorId)`. Both validate options immediately.
|
|
|
28
28
|
The limits measure normalized JSON encoded as UTF-8. `maxResultBytes` also
|
|
29
29
|
limits each computed snapshot getter and effect-handler result. Explicit
|
|
30
30
|
`observables()` values are validated as JSON but do not currently have a
|
|
31
|
-
separate aggregate byte limit.
|
|
31
|
+
separate aggregate byte limit. This includes values wrapped in
|
|
32
|
+
`broadcastInvalidation()`: they are compared in memory even though only their
|
|
33
|
+
names enter the broadcast outbox. `retryDelayMilliseconds` should return a
|
|
32
34
|
non-negative finite number; an invalid application callback will fail the
|
|
33
35
|
affected failure path rather than schedule an invalid timestamp.
|
|
34
36
|
|
package/docs/correctness.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Correctness and delivery semantics
|
|
2
2
|
|
|
3
|
+
## Guarantees
|
|
4
|
+
|
|
3
5
|
- Delivery is ordered per actor identity and at least once.
|
|
4
6
|
- Different identities may execute concurrently.
|
|
5
7
|
- Sequence allocation and durable enqueue are one transaction.
|
|
@@ -51,3 +53,24 @@
|
|
|
51
53
|
failing projection cannot stop its siblings or observable delivery. A state
|
|
52
54
|
change on an actor declaring payloads creates a revision broadcast even when
|
|
53
55
|
the actor declares no scalar observables.
|
|
56
|
+
|
|
57
|
+
## Limitations and non-goals
|
|
58
|
+
|
|
59
|
+
- At-least-once execution means actor code may begin more than once. State and
|
|
60
|
+
staged intents from a failed turn roll back, but arbitrary external work does
|
|
61
|
+
not. External systems need stable idempotency keys.
|
|
62
|
+
- The activation fence protects the Solid Objects commit. It cannot revoke or
|
|
63
|
+
undo network calls, files, emails, payments, or other external effects.
|
|
64
|
+
- One identity processes one write operation at a time. This is the ordering
|
|
65
|
+
guarantee and also the hot-identity throughput limit.
|
|
66
|
+
- A commit is scoped to one actor turn. There is no transaction spanning two
|
|
67
|
+
actor identities.
|
|
68
|
+
- Processes with incompatible `stateVersion` definitions cannot safely overlap.
|
|
69
|
+
Once newer code persists a state version, older code rejects that actor.
|
|
70
|
+
- The application owns HTTP, WebSocket authentication, rendering, process
|
|
71
|
+
placement, capacity, database backups, and database failover.
|
|
72
|
+
- Redis and PostgreSQL notifications reduce wake-up latency but do not replace
|
|
73
|
+
durable polling or become a source of truth.
|
|
74
|
+
- Large documents, bulk pipelines, globally placed edge state, and global
|
|
75
|
+
counters are outside the intended workload. Prefer an ordinary row
|
|
76
|
+
transaction when it completely enforces the invariant.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Operator dashboard
|
|
2
|
+
|
|
3
|
+
`solid-objects/web` is an optional operator interface over the same relational
|
|
4
|
+
tables used by the runtime. It exposes instances and committed state, ready and
|
|
5
|
+
claimed messages, reminders, effects, broadcasts, dead letters, and processes.
|
|
6
|
+
It needs no separate store or agent.
|
|
7
|
+
|
|
8
|
+
The root `solid-objects` entry point does not import the dashboard. A worker
|
|
9
|
+
process that never mounts it does not load its HTTP adapter, renderer, or
|
|
10
|
+
assets.
|
|
11
|
+
|
|
12
|
+
## Fetch mounting
|
|
13
|
+
|
|
14
|
+
`createDashboard()` returns an immutable Fetch handler. Give every request its
|
|
15
|
+
fresh server-side authentication context and the host session that should own
|
|
16
|
+
the CSRF token:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { createDashboard } from "solid-objects/web"
|
|
20
|
+
|
|
21
|
+
const dashboard = createDashboard({
|
|
22
|
+
runtime,
|
|
23
|
+
mountPath: "/solid-objects/dashboard",
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const response = await dashboard.fetch(request, {
|
|
27
|
+
authorizationContext: currentOperator,
|
|
28
|
+
session: {
|
|
29
|
+
read: (key) => session.get(key),
|
|
30
|
+
write: (key, value) => session.set(key, value),
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`mountPath` defaults to `/solid-objects/dashboard`. Pass `/` only when the
|
|
36
|
+
dashboard owns the whole origin.
|
|
37
|
+
|
|
38
|
+
## Access modes
|
|
39
|
+
|
|
40
|
+
`access` defaults to `authorized`. Every route uses the runtime administration
|
|
41
|
+
policy, pages include mutation controls, and the request context must supply a
|
|
42
|
+
session for CSRF state.
|
|
43
|
+
|
|
44
|
+
Use `authorized-read-only` to retain the policy while removing mutation forms
|
|
45
|
+
and rejecting every dashboard POST with 405. Use `public-read-only` for an
|
|
46
|
+
explicitly public demo mount:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
const demo = createDashboard({
|
|
50
|
+
runtime,
|
|
51
|
+
mountPath: "/solid-objects/demo",
|
|
52
|
+
access: "public-read-only",
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
await demo.fetch(request, {})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Public read-only mode skips `authorizeAdministration` and requires neither an
|
|
59
|
+
authorization context nor a session. It still exposes committed state,
|
|
60
|
+
arguments, results, errors, actor identifiers, and operational metadata. Only
|
|
61
|
+
use it with synthetic or otherwise public demo data. All extension GET routes
|
|
62
|
+
are public in this mode, while extension POST routes are also rejected.
|
|
63
|
+
|
|
64
|
+
## Node and Connect mounting
|
|
65
|
+
|
|
66
|
+
`createNodeDashboardHandler()` converts `IncomingMessage` and `ServerResponse`
|
|
67
|
+
to the Fetch contract. Register it after the application's authentication and
|
|
68
|
+
session middleware. A Connect-compatible host may pass `next`; paths outside
|
|
69
|
+
the mount and unknown dashboard paths cascade. Requests outside the mount
|
|
70
|
+
cascade before context resolution or body consumption, so downstream POST
|
|
71
|
+
handlers receive the original stream.
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { createDashboard, createNodeDashboardHandler } from "solid-objects/web"
|
|
75
|
+
|
|
76
|
+
const dashboard = createDashboard({ runtime })
|
|
77
|
+
const handler = createNodeDashboardHandler({
|
|
78
|
+
dashboard,
|
|
79
|
+
resolveContext: async (request) => ({
|
|
80
|
+
authorizationContext: await currentOperator(request),
|
|
81
|
+
session: sessionFor(request),
|
|
82
|
+
}),
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
app.use(handler)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The adapter caps request bodies at 64 KiB by default. Set
|
|
89
|
+
`maximumBodyBytes` to another positive bound when the host requires one.
|
|
90
|
+
|
|
91
|
+
## Authorization
|
|
92
|
+
|
|
93
|
+
In the default and authorized read-only modes, every data route declares an
|
|
94
|
+
`authorizeAdministration` action and resource. Authorization runs before the
|
|
95
|
+
route reads a record, so a denied caller cannot probe identifiers.
|
|
96
|
+
|
|
97
|
+
| Page or action | Action | Resource |
|
|
98
|
+
| ------------------------------------------ | -------------------------- | ----------------------------- |
|
|
99
|
+
| Dashboard, statistics, health | `index` | `dashboard` |
|
|
100
|
+
| Instance list/detail | `index` / `show` | `instances` |
|
|
101
|
+
| Instance pause/resume | `pause` / `resume` | `instances` |
|
|
102
|
+
| Mailbox/message detail | `index` / `show` | `messages` |
|
|
103
|
+
| Reminder, effect, broadcast, process lists | `index` | corresponding plural resource |
|
|
104
|
+
| Dead-letter list/detail/retry | `index` / `show` / `retry` | `dead_letters` |
|
|
105
|
+
|
|
106
|
+
The policy receives the route ID as `resourceId` when one exists and receives
|
|
107
|
+
the exact `authorizationContext` supplied by the host. The default policy
|
|
108
|
+
denies every route. Dashboard authorization does not authenticate requests;
|
|
109
|
+
resolve the operator before calling the dashboard.
|
|
110
|
+
|
|
111
|
+
## Security
|
|
112
|
+
|
|
113
|
+
The host session persists one random CSRF secret. Every rendered page masks it
|
|
114
|
+
with fresh random bytes, so tokens differ between requests while every form
|
|
115
|
+
already open in the same session remains valid. POST requests without a valid
|
|
116
|
+
token receive 403 and do not perform the action.
|
|
117
|
+
|
|
118
|
+
Every stored or request-derived string is escaped before entering HTML,
|
|
119
|
+
including JSON placed in chart attributes. HTML and statistics responses are
|
|
120
|
+
private and not cached. The dashboard sends a nonce-backed content security
|
|
121
|
+
policy, denies framing, disables MIME sniffing, and limits referrers to the
|
|
122
|
+
same origin.
|
|
123
|
+
|
|
124
|
+
Page size defaults to 25 and is clamped to 200. Actor ID substring filtering is
|
|
125
|
+
parameterized with an adapter-specific SQL expression rather than interpolated
|
|
126
|
+
into a query.
|
|
127
|
+
|
|
128
|
+
## Pages and actions
|
|
129
|
+
|
|
130
|
+
The dashboard page shows totals, instances per actor type, mailbox depth,
|
|
131
|
+
outbox/reminder status, recent processes, and recent dead letters. The summary
|
|
132
|
+
bar can poll `/stats` every five seconds when the operator enables Live. Lists
|
|
133
|
+
do not reload underneath an operator reading them.
|
|
134
|
+
|
|
135
|
+
Instance detail shows committed state and recent related messages, reminders,
|
|
136
|
+
effects, broadcasts, and dead letters. Pause prevents workers from claiming
|
|
137
|
+
new turns for that identity; a turn already executing may still commit. Resume
|
|
138
|
+
clears the brake and normal polling resumes delivery.
|
|
139
|
+
|
|
140
|
+
Dead-letter retry calls `runtime.deadLetters.retry()`, retaining its durable
|
|
141
|
+
idempotency and actor-operation validation. A retry refused by the runtime is
|
|
142
|
+
shown on the detail page with status 422.
|
|
143
|
+
|
|
144
|
+
`HEAD /` performs only a schema reachability query and creates no CSRF session
|
|
145
|
+
state. Use it for liveness checks instead of polling the full dashboard.
|
|
146
|
+
|
|
147
|
+
## Charts
|
|
148
|
+
|
|
149
|
+
The dashboard defaults to Chart.js 4.5.0 from jsDelivr with subresource
|
|
150
|
+
integrity. Only that exact origin is added to `script-src`.
|
|
151
|
+
|
|
152
|
+
Use a self-hosted script without widening the policy:
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
createDashboard({
|
|
156
|
+
runtime,
|
|
157
|
+
chartLibrary: { url: "/assets/chart.umd.min.js", integrity: null },
|
|
158
|
+
})
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Set `chartLibrary: { url: null }` to render without charts. The dedicated chart
|
|
162
|
+
containers have fixed height so responsive redraws cannot grow the page.
|
|
163
|
+
|
|
164
|
+
## Extensions
|
|
165
|
+
|
|
166
|
+
Extensions are supplied when the dashboard is created. Configuration is copied
|
|
167
|
+
and frozen; there is no global registry and no first-request mutation boundary.
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
const dashboard = createDashboard({
|
|
171
|
+
runtime,
|
|
172
|
+
extensions: [
|
|
173
|
+
{
|
|
174
|
+
tab: { label: "Tenants", path: "/tenants" },
|
|
175
|
+
routes: [
|
|
176
|
+
{
|
|
177
|
+
method: "GET",
|
|
178
|
+
path: "/tenants",
|
|
179
|
+
policy: { action: "index", resource: "tenants" },
|
|
180
|
+
handle: ({ render }) =>
|
|
181
|
+
render({
|
|
182
|
+
title: "Tenants",
|
|
183
|
+
content: renderTenants(),
|
|
184
|
+
}),
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
})
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Every extension route must carry a nonempty policy. Missing policies and
|
|
193
|
+
method/path collisions fail when the dashboard is created. Renderer callbacks
|
|
194
|
+
may replace named built-in views, and Fetch middleware may wrap the whole
|
|
195
|
+
dashboard. Extension HTML is trusted application code; escape dynamic values
|
|
196
|
+
with the route context's `escape()` helper.
|
package/docs/fit.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Choosing Solid Objects
|
|
2
|
+
|
|
3
|
+
Solid Objects is useful when an application has many independently addressed
|
|
4
|
+
entities and every entity needs ordered state changes, durable work, recovery,
|
|
5
|
+
or realtime projections.
|
|
6
|
+
|
|
7
|
+
## Use it when
|
|
8
|
+
|
|
9
|
+
- Concurrent requests can update the same room, cart, account, device,
|
|
10
|
+
document, or session.
|
|
11
|
+
- Each identity needs its own ordering boundary and durable mailbox.
|
|
12
|
+
- Operations must recover after a Node process exits.
|
|
13
|
+
- State changes stage reminders, effects, actor-to-actor messages, or realtime
|
|
14
|
+
invalidations atomically.
|
|
15
|
+
- The application already operates SQLite, PostgreSQL, or MySQL and should keep
|
|
16
|
+
durable coordination there.
|
|
17
|
+
|
|
18
|
+
## Prefer a row transaction when
|
|
19
|
+
|
|
20
|
+
One short transaction with an update, constraint, or row lock completely
|
|
21
|
+
enforces the invariant. A direct transaction has less machinery, less stored
|
|
22
|
+
history, and no actor-state migration contract.
|
|
23
|
+
|
|
24
|
+
## Prefer another design when
|
|
25
|
+
|
|
26
|
+
- Work is a bulk or data-parallel pipeline rather than per-identity state.
|
|
27
|
+
- One global identity must sustain more writes than one sequential mailbox can
|
|
28
|
+
commit.
|
|
29
|
+
- State is a large document or relational dataset that should be queried and
|
|
30
|
+
updated in smaller normalized pieces.
|
|
31
|
+
- The application needs a transaction spanning several independent object
|
|
32
|
+
identities.
|
|
33
|
+
- Compute and state must be automatically placed close to clients at the edge.
|
|
34
|
+
- The team wants a managed control plane to place, scale, and recover workers.
|
|
35
|
+
- Durable workflow replay across named steps is more important than a mutable
|
|
36
|
+
object with ordered operations.
|
|
37
|
+
|
|
38
|
+
## Model identities deliberately
|
|
39
|
+
|
|
40
|
+
One hot identity is intentionally serialized. An identity should correspond to
|
|
41
|
+
the smallest domain boundary that requires one total order. Splitting a room by
|
|
42
|
+
player or a cart by item may improve parallelism, but it also gives up atomic
|
|
43
|
+
ordering across the split.
|
|
44
|
+
|
|
45
|
+
Different identities can run concurrently when worker capacity and the
|
|
46
|
+
database allow it. The [benchmark harness](benchmarks.md) measures both the hot
|
|
47
|
+
and independent-identity cases.
|
|
48
|
+
|
|
49
|
+
## Operational cost
|
|
50
|
+
|
|
51
|
+
The relational database stores actor instances, ready and claimed mailbox
|
|
52
|
+
membership, message history, leases, effects, reminders, broadcasts, dead
|
|
53
|
+
letters, and process records. Retention policies and the operator dashboard
|
|
54
|
+
make that state inspectable, but they do not remove the need to monitor and
|
|
55
|
+
back up the database.
|
|
56
|
+
|
|
57
|
+
Redis is optional. It is a transient notification path rather than durable
|
|
58
|
+
state, so losing Redis increases polling latency without losing committed work.
|