solid-objects 0.12.1 → 0.13.1
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 +57 -1
- package/README.md +284 -950
- 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/broadcast-worker.d.ts +2 -0
- package/dist/broadcast-worker.d.ts.map +1 -1
- package/dist/broadcast-worker.js +25 -5
- package/dist/broadcast-worker.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 +3 -0
- package/dist/configuration.d.ts.map +1 -1
- package/dist/configuration.js +3 -0
- 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/effect-worker.d.ts +2 -0
- package/dist/effect-worker.d.ts.map +1 -1
- package/dist/effect-worker.js +25 -5
- package/dist/effect-worker.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/polling-backoff.d.ts +23 -0
- package/dist/polling-backoff.d.ts.map +1 -0
- package/dist/polling-backoff.js +33 -0
- package/dist/polling-backoff.js.map +1 -0
- package/dist/records.d.ts +1 -0
- package/dist/records.d.ts.map +1 -1
- package/dist/reminder-scheduler.d.ts +2 -0
- package/dist/reminder-scheduler.d.ts.map +1 -1
- package/dist/reminder-scheduler.js +25 -5
- package/dist/reminder-scheduler.js.map +1 -1
- package/dist/repository.d.ts +5 -1
- package/dist/repository.d.ts.map +1 -1
- package/dist/repository.js +15 -4
- package/dist/repository.js.map +1 -1
- package/dist/runtime.d.ts +4 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +71 -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/wake-up/postgresql.d.ts.map +1 -1
- package/dist/wake-up/postgresql.js +12 -11
- package/dist/wake-up/postgresql.js.map +1 -1
- package/dist/wake-up.d.ts +1 -1
- package/dist/wake-up.d.ts.map +1 -1
- package/dist/wake-up.js +11 -10
- package/dist/wake-up.js.map +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/dist/worker.d.ts +2 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +25 -5
- package/dist/worker.js.map +1 -1
- package/docs/api.md +70 -4
- package/docs/architecture.md +20 -11
- package/docs/authorization.md +12 -3
- package/docs/benchmarks.md +163 -0
- package/docs/browser-protocol.md +15 -8
- package/docs/comparisons.md +36 -0
- package/docs/configuration.md +14 -3
- package/docs/correctness.md +23 -0
- package/docs/dashboard.md +196 -0
- package/docs/fit.md +58 -0
- package/docs/operations.md +25 -7
- 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 +26 -4
package/docs/api.md
CHANGED
|
@@ -16,6 +16,11 @@ generic signatures; this index explains the supported role of every export.
|
|
|
16
16
|
- `Actor`: base class providing `ref()`, `actorId`, `currentMessage`,
|
|
17
17
|
`observables()`, `reject()`, `emit()`, `commitAction()`, `schedule()`,
|
|
18
18
|
`sendTo()`, and protected lifecycle hooks.
|
|
19
|
+
- `broadcastValue(value)`: mark an observable so its changed value enters the
|
|
20
|
+
durable invalidation envelope.
|
|
21
|
+
- `broadcastInvalidation(value)`: compare the real observable value but put
|
|
22
|
+
only its name in the durable envelope when it changes.
|
|
23
|
+
- `ObservableBroadcast`: the immutable marker type returned by either helper.
|
|
19
24
|
- `VERSION`: running package version.
|
|
20
25
|
- `ActorClass`, `ActorReference`, `ActorMessageSender`, `ActorSnapshot`,
|
|
21
26
|
`ActorOperationNames`, `ActorQueryNames`, `StagedOperations`, and
|
|
@@ -31,6 +36,25 @@ generic signatures; this index explains the supported role of every export.
|
|
|
31
36
|
`PayloadBroadcasts`, and `PayloadBroadcastValue` describe actor-declared
|
|
32
37
|
transactional work and typed personalized projections.
|
|
33
38
|
|
|
39
|
+
`observables()` returns a flat object. Unwrapped values are invalidation-only:
|
|
40
|
+
their real values participate in change detection, but only their names enter
|
|
41
|
+
the durable envelope. Use an explicit marker when wire behavior matters:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
override observables(): Record<string, unknown> {
|
|
45
|
+
return {
|
|
46
|
+
version: broadcastValue(this.document.version),
|
|
47
|
+
sidebar: broadcastInvalidation(this.sidebarForCurrentState()),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Both values must be JSON-compatible and are evaluated after each successful
|
|
53
|
+
turn. An invalidation-only value participates in change detection but is never
|
|
54
|
+
written to the broadcast outbox or invalidation envelope. The envelope carries
|
|
55
|
+
its name in `invalidations`, allowing component registries to refresh a
|
|
56
|
+
reauthorized endpoint without exposing the value.
|
|
57
|
+
|
|
34
58
|
`MessageReference` does not retain an invocation's authorization context.
|
|
35
59
|
Supply `authorizationContext` to each `status()`, `result()`, and `wait()` call;
|
|
36
60
|
the runtime reauthorizes the persisted operation every time. Durable results
|
|
@@ -90,6 +114,10 @@ The manager types are `DeadLetter`; `ReminderPage`, `ReminderPageOptions`,
|
|
|
90
114
|
`AdministrationOptions` carries the application-owned authorization context
|
|
91
115
|
for administration calls.
|
|
92
116
|
|
|
117
|
+
The packaged `solid-objects quickstart` command is config-free and runs the
|
|
118
|
+
SQLite example shipped in the npm artifact. Every other CLI command loads the
|
|
119
|
+
application runtime configured through `--config`.
|
|
120
|
+
|
|
93
121
|
`ProcessRecord.shutdownState` is `"running"`, `"draining"`, or `"stopped"`;
|
|
94
122
|
there is no separate `running` field. `RetentionResult.count` means eligible
|
|
95
123
|
rows for `preview()` and rows actually deleted for `prune()`.
|
|
@@ -124,6 +152,11 @@ rows for `preview()` and rows actually deleted for `prune()`.
|
|
|
124
152
|
`DatabaseConnection`, `DatabaseFamily`, and `RunResult` support custom database
|
|
125
153
|
and commit-action integration.
|
|
126
154
|
|
|
155
|
+
`BroadcastEvent.observables` contains changed value-broadcast projections.
|
|
156
|
+
`BroadcastEvent.invalidations` contains changed invalidation-only names. The
|
|
157
|
+
runtime always supplies the array; consumers should treat its absence from an
|
|
158
|
+
older or application-produced event as an empty array.
|
|
159
|
+
|
|
127
160
|
### Runtime extensions and manual workers
|
|
128
161
|
|
|
129
162
|
`runtime.registerComponent(factory, { count = 1 })` adds application-owned
|
|
@@ -146,14 +179,17 @@ Factories should create fresh mutable state and `stop()` should be idempotent.
|
|
|
146
179
|
exported for test runners and hosts that intentionally operate roles outside
|
|
147
180
|
`runtime.run()`. Runtime factory methods create the same classes. Each provides
|
|
148
181
|
`runOnce()`, bounded `runUntilIdle()`, `run(signal)`, `requestShutdown()`,
|
|
149
|
-
`stopped()`,
|
|
150
|
-
|
|
151
|
-
|
|
182
|
+
`stopped()`, `stop()`, and the inspectable
|
|
183
|
+
`currentPollingIntervalMilliseconds`. Manual roles still register process
|
|
184
|
+
ownership and must be stopped. Prefer `runtime.run()` in production and
|
|
185
|
+
`runtime.testing` in tests.
|
|
152
186
|
|
|
153
187
|
`InProcessWakeUpAdapter`, `WakeUpAdapter`, `WakeUpRole`, `WakeUpWatch`, and
|
|
154
188
|
`WakeUpWaitOptions` define the notification extension. A watch must be obtained
|
|
155
189
|
before checking durable state so a notification cannot fall between claim and
|
|
156
|
-
wait.
|
|
190
|
+
wait. `WakeUpWatch.wait()` returns `true` for a notification and `false` for a
|
|
191
|
+
timeout or cancellation. A legacy `void` result remains accepted and preserves
|
|
192
|
+
the fast polling cadence.
|
|
157
193
|
|
|
158
194
|
### Errors
|
|
159
195
|
|
|
@@ -225,5 +261,35 @@ and `SyncTimeoutWaitingOn` type timeout diagnostics. See
|
|
|
225
261
|
`ComponentRefreshFailure`, and `ComponentRegistryOptions`: framework-neutral
|
|
226
262
|
refresh contract types.
|
|
227
263
|
|
|
264
|
+
`InvalidationEnvelope.observables` contains values and
|
|
265
|
+
`InvalidationEnvelope.invalidations` contains names without values. The
|
|
266
|
+
component registry reacts to names in either location.
|
|
267
|
+
|
|
228
268
|
The wire format, trust boundary, revision rules, and component semantics are in
|
|
229
269
|
[Browser protocol](browser-protocol.md).
|
|
270
|
+
|
|
271
|
+
## `solid-objects/web`
|
|
272
|
+
|
|
273
|
+
- `createDashboard(options)` creates an immutable `SolidObjectsDashboard` with
|
|
274
|
+
a standard `fetch(request, context)` entry point.
|
|
275
|
+
- `createNodeDashboardHandler(options)` adapts the Fetch entry point to
|
|
276
|
+
`node:http` and Connect-compatible middleware.
|
|
277
|
+
- `DashboardOptions` selects the runtime, mount path, `DashboardAccess`, chart library,
|
|
278
|
+
`DashboardExtension` objects, and `DashboardMiddleware` functions.
|
|
279
|
+
- `DashboardRequestContext` supplies the existing administration authorization
|
|
280
|
+
context and an optional `DashboardSession`. Read/write access requires the
|
|
281
|
+
session so its `read()` and `write()` methods can hold the masked CSRF token
|
|
282
|
+
across requests; read-only modes do not create CSRF state.
|
|
283
|
+
- `DashboardRoute`, `DashboardRouteContext`, `DashboardPolicy`, `DashboardPage`,
|
|
284
|
+
and `DashboardTab` define extension pages. Every route requires a policy.
|
|
285
|
+
- `DashboardRenderer`, `DashboardRenderInput`, and `DashboardMiddlewareInput`
|
|
286
|
+
define immutable view overrides and middleware inputs.
|
|
287
|
+
- `DashboardChartLibrary` selects the CDN, a self-hosted script, or disabled
|
|
288
|
+
charts.
|
|
289
|
+
- `NodeDashboardHandler`, `NodeDashboardHandlerOptions`, and
|
|
290
|
+
`NodeDashboardRequestContextResolver` describe the Node adapter.
|
|
291
|
+
- `SolidObjectsDashboardContract` is the minimal Fetch contract accepted by the
|
|
292
|
+
Node adapter.
|
|
293
|
+
|
|
294
|
+
Mounting, authorization actions, CSRF behavior, pages, and extensions are in
|
|
295
|
+
[Operator dashboard](dashboard.md).
|
package/docs/architecture.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Architecture
|
|
2
2
|
|
|
3
|
-
Solid Objects
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Solid Objects coordinates stateful TypeScript objects through database-backed
|
|
4
|
+
mailboxes. Each object, called an actor in the API, is addressed by
|
|
5
|
+
`(actor type, actor id)`, processes one durable turn at a time, and persists
|
|
6
|
+
JSON state between activations.
|
|
6
7
|
|
|
7
8
|
A durable message envelope selects an actor `operation` and records its
|
|
8
9
|
`delivery_mode` as `async`, `sync`, or `internal`. An operation is actor code;
|
|
@@ -24,7 +25,9 @@ Each role takes a generation watch before checking for work. A post-commit
|
|
|
24
25
|
wake-up therefore cannot fall into the gap between an empty claim and the
|
|
25
26
|
worker's wait. The default adapter broadcasts within one process; polling
|
|
26
27
|
remains active as the durable fallback and custom adapters can bridge process
|
|
27
|
-
boundaries.
|
|
28
|
+
boundaries. Empty passes double the role's wait up to the configured idle
|
|
29
|
+
ceiling. Work or a notification resets it to the fast interval, and an actor
|
|
30
|
+
worker's ceiling never exceeds its lease-renewal interval.
|
|
28
31
|
|
|
29
32
|
Each runtime role occupies a supervised factory slot. An unexpected promise
|
|
30
33
|
resolution or rejection cleans up that instance, waits with capped exponential
|
|
@@ -59,10 +62,11 @@ than waiting for a connection or serialized SQLite slot it cannot release.
|
|
|
59
62
|
|
|
60
63
|
PostgreSQL notifications are an opt-in latency layer. One event-driven client
|
|
61
64
|
per runtime listens on role-specific channels before the worker checks durable
|
|
62
|
-
state, which closes the listener-startup race without
|
|
63
|
-
|
|
64
|
-
wakes every matching waiter. Reconnection and notification loss fall back
|
|
65
|
-
|
|
65
|
+
state, which closes the listener-startup race without holding a polling
|
|
66
|
+
connection per worker. A notification advances a process-local role generation
|
|
67
|
+
and wakes every matching waiter. Reconnection and notification loss fall back
|
|
68
|
+
to adaptive polling, whose current wait can be as long as the configured idle
|
|
69
|
+
ceiling.
|
|
66
70
|
|
|
67
71
|
The optional Redis adapter provides the same role generations through Pub/Sub
|
|
68
72
|
for deployments that already operate Redis. It keeps commands and subscriptions
|
|
@@ -94,6 +98,12 @@ actor subscriptions, replays a committed observable projection, and follows
|
|
|
94
98
|
the durable broadcast outbox in revision order. The browser client applies the
|
|
95
99
|
same incarnation and revision fence without importing Node APIs.
|
|
96
100
|
|
|
101
|
+
Each observable is compared using its computed JSON value. Value-broadcast
|
|
102
|
+
observables persist that changed value in the outbox; invalidation-only
|
|
103
|
+
observables persist only the changed name. This lets a component registry
|
|
104
|
+
refresh a reauthorized view at the same dependency granularity without storing
|
|
105
|
+
or sending private projection values.
|
|
106
|
+
|
|
97
107
|
Actors may also declare static personalized payload projections. Each requested
|
|
98
108
|
name is reauthorized as a query and evaluated against a fresh actor hydrated
|
|
99
109
|
from committed state under that subscriber's session context. Payload names use
|
|
@@ -108,6 +118,5 @@ broadcast events through an application-owned shared transport and calls
|
|
|
108
118
|
another broker optional for a single Node process while making the
|
|
109
119
|
cross-process boundary explicit.
|
|
110
120
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
Cloudflare's placement or edge-runtime guarantees.
|
|
121
|
+
Solid Objects provides database-backed state coordination for Node.js. It does
|
|
122
|
+
not reproduce Cloudflare's placement or edge-runtime guarantees.
|
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,163 @@
|
|
|
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
|
+
## Idle polling
|
|
7
|
+
|
|
8
|
+
The idle harness measures process CPU and empty database passes for the four
|
|
9
|
+
runtime roles:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm run benchmark:idle
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
It warms each interval for three seconds, measures for ten seconds, and reports
|
|
16
|
+
process user plus system CPU time divided by wall time.
|
|
17
|
+
|
|
18
|
+
Measured on August 16, 2026 on an Apple M5 with Node.js 26.7.0 and in-memory
|
|
19
|
+
SQLite. The before run used 0.13.0; the after run used the prepared 0.13.1 tree.
|
|
20
|
+
Each run started one actor, effect, reminder, and broadcast role.
|
|
21
|
+
|
|
22
|
+
| Fast interval | Before polls/s | Before CPU | After polls/s | After CPU |
|
|
23
|
+
| ------------: | -------------: | ---------: | ------------: | --------: |
|
|
24
|
+
| 20 ms | 188.78 | 3.254% | 4.000 | 0.129% |
|
|
25
|
+
| 100 ms | 39.596 | 0.906% | 3.999 | 0.121% |
|
|
26
|
+
| 500 ms | 7.999 | 0.251% | 3.999 | 0.104% |
|
|
27
|
+
|
|
28
|
+
The after run reached the one-second ceiling for all four roles. These are
|
|
29
|
+
developer-laptop measurements, not a CPU guarantee; timer scheduling, JIT,
|
|
30
|
+
database path, and unrelated host activity affect short samples.
|
|
31
|
+
|
|
32
|
+
Five SQLite samples measured durable enqueue through committed completion after
|
|
33
|
+
2.5 seconds of idleness. The polling-only multi-process harness submits just
|
|
34
|
+
after an empty pass, so it measures approximately the full polling wait rather
|
|
35
|
+
than average arrival latency.
|
|
36
|
+
|
|
37
|
+
| Topology | 0.13.0 p50 | Prepared 0.13.1 p50 |
|
|
38
|
+
| ------------------------------- | ---------: | ------------------: |
|
|
39
|
+
| One process, in-process wake-up | 2.589 ms | 2.662 ms |
|
|
40
|
+
| Two processes, polling only | 107.945 ms | 1,006.232 ms |
|
|
41
|
+
|
|
42
|
+
The local wake-up keeps the one-process path prompt after backoff. The
|
|
43
|
+
polling-only row is the explicit tradeoff: use PostgreSQL notifications or
|
|
44
|
+
optional Redis Pub/Sub when separate processes need low-latency delivery.
|
|
45
|
+
|
|
46
|
+
## Scenarios
|
|
47
|
+
|
|
48
|
+
- `warm-hot`: all operations target one previously created identity.
|
|
49
|
+
- `warm-many`: operations rotate across 100 previously created identities.
|
|
50
|
+
- `cold-many`: every measured operation creates a new identity.
|
|
51
|
+
- `synchronous`: the actor method mutates state without yielding.
|
|
52
|
+
- `asynchronous`: the actor method yields through `setImmediate()` before the
|
|
53
|
+
same mutation.
|
|
54
|
+
- `one-process`: four actor workers run in the caller's Node process.
|
|
55
|
+
- `four-processes`: four Node worker processes share the database.
|
|
56
|
+
|
|
57
|
+
Latency begins before durable enqueue and ends when the committed result is
|
|
58
|
+
available. Throughput uses the wall time for the measured batch. Percentiles
|
|
59
|
+
use nearest rank. Defaults are 25 warmup operations, 250 measured operations,
|
|
60
|
+
and client concurrency 16.
|
|
61
|
+
|
|
62
|
+
## Run the harness
|
|
63
|
+
|
|
64
|
+
SQLite needs no service:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pnpm run benchmark -- --database sqlite
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
PostgreSQL and MySQL runs require a disposable database. The harness deletes
|
|
71
|
+
benchmark rows but leaves its uniquely prefixed empty tables for inspection.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
SOLID_OBJECTS_POSTGRESQL_BENCHMARK_URL=postgresql://... \
|
|
75
|
+
pnpm run benchmark -- --database postgresql
|
|
76
|
+
|
|
77
|
+
SOLID_OBJECTS_MYSQL_BENCHMARK_URL=mysql://... \
|
|
78
|
+
pnpm run benchmark -- --database mysql
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Use `--operations`, `--warmup`, and `--concurrency` to change the recorded
|
|
82
|
+
dataset. Redirect stdout to retain the JSON result.
|
|
83
|
+
|
|
84
|
+
## Observed results
|
|
85
|
+
|
|
86
|
+
Measured on August 15, 2026 with the prepared `0.13.0` source tree:
|
|
87
|
+
|
|
88
|
+
- Apple M5, 10 logical CPUs, 24 GiB memory
|
|
89
|
+
- macOS 26.6 (`darwin 25.6.0`)
|
|
90
|
+
- Node.js 26.7.0
|
|
91
|
+
- SQLite 3.53.4 on the internal SSD, PostgreSQL 18.4 and MySQL 8.4.11 in
|
|
92
|
+
Docker Desktop
|
|
93
|
+
- 25 warmup operations, 250 measured operations, concurrency 16
|
|
94
|
+
|
|
95
|
+
### SQLite 3.53.4
|
|
96
|
+
|
|
97
|
+
| Topology | Shape | Handler | ops/s | p50 ms | p95 ms | p99 ms |
|
|
98
|
+
| -------------- | --------- | ------------ | -----: | -----: | ------: | ------: |
|
|
99
|
+
| one process | warm hot | synchronous | 95.51 | 35.34 | 1159.07 | 1711.76 |
|
|
100
|
+
| one process | warm hot | asynchronous | 448.63 | 35.96 | 38.95 | 41.47 |
|
|
101
|
+
| one process | warm many | synchronous | 44.35 | 141.55 | 1886.35 | 2543.15 |
|
|
102
|
+
| one process | warm many | asynchronous | 240.64 | 48.62 | 75.10 | 671.64 |
|
|
103
|
+
| one process | cold many | synchronous | 30.91 | 436.98 | 1502.26 | 1729.10 |
|
|
104
|
+
| one process | cold many | asynchronous | 57.15 | 167.36 | 967.74 | 1066.49 |
|
|
105
|
+
| four processes | warm hot | synchronous | 453.10 | 27.24 | 72.09 | 77.88 |
|
|
106
|
+
| four processes | warm hot | asynchronous | 487.39 | 29.27 | 60.66 | 68.28 |
|
|
107
|
+
| four processes | warm many | synchronous | 78.88 | 86.33 | 890.29 | 1381.65 |
|
|
108
|
+
| four processes | warm many | asynchronous | 47.56 | 220.37 | 1130.53 | 1341.86 |
|
|
109
|
+
| four processes | cold many | synchronous | 70.45 | 174.67 | 643.53 | 668.46 |
|
|
110
|
+
| four processes | cold many | asynchronous | 39.92 | 221.01 | 1309.13 | 1442.30 |
|
|
111
|
+
|
|
112
|
+
### PostgreSQL 18.4
|
|
113
|
+
|
|
114
|
+
| Topology | Shape | Handler | ops/s | p50 ms | p95 ms | p99 ms |
|
|
115
|
+
| -------------- | --------- | ------------ | ----: | ------: | ------: | ------: |
|
|
116
|
+
| one process | warm hot | synchronous | 66.99 | 232.89 | 317.94 | 375.68 |
|
|
117
|
+
| one process | warm hot | asynchronous | 72.27 | 211.57 | 280.89 | 316.61 |
|
|
118
|
+
| one process | warm many | synchronous | 76.69 | 129.88 | 552.01 | 1963.27 |
|
|
119
|
+
| one process | warm many | asynchronous | 25.78 | 340.96 | 2326.38 | 6917.26 |
|
|
120
|
+
| one process | cold many | synchronous | 12.70 | 1262.08 | 1734.77 | 2199.20 |
|
|
121
|
+
| one process | cold many | asynchronous | 11.23 | 1254.46 | 2796.81 | 2905.99 |
|
|
122
|
+
| four processes | warm hot | synchronous | 83.71 | 191.83 | 220.87 | 231.84 |
|
|
123
|
+
| four processes | warm hot | asynchronous | 86.42 | 184.56 | 210.11 | 215.48 |
|
|
124
|
+
| four processes | warm many | synchronous | 95.47 | 100.31 | 330.27 | 1663.84 |
|
|
125
|
+
| four processes | warm many | asynchronous | 37.04 | 232.71 | 1601.29 | 4447.85 |
|
|
126
|
+
| four processes | cold many | synchronous | 14.79 | 1114.18 | 1263.33 | 1313.70 |
|
|
127
|
+
| four processes | cold many | asynchronous | 11.68 | 1231.94 | 2555.34 | 2848.80 |
|
|
128
|
+
|
|
129
|
+
### MySQL 8.4.11
|
|
130
|
+
|
|
131
|
+
| Topology | Shape | Handler | ops/s | p50 ms | p95 ms | p99 ms |
|
|
132
|
+
| -------------- | --------- | ------------ | ----: | ------: | ------: | ------: |
|
|
133
|
+
| one process | warm hot | synchronous | 28.11 | 504.36 | 1648.10 | 2088.05 |
|
|
134
|
+
| one process | warm hot | asynchronous | 25.29 | 508.26 | 1750.17 | 2222.72 |
|
|
135
|
+
| one process | warm many | synchronous | 61.79 | 165.62 | 463.59 | 2036.31 |
|
|
136
|
+
| one process | warm many | asynchronous | 22.45 | 446.17 | 2420.92 | 7818.65 |
|
|
137
|
+
| one process | cold many | synchronous | 10.23 | 1353.37 | 3038.76 | 3185.15 |
|
|
138
|
+
| one process | cold many | asynchronous | 10.04 | 1338.31 | 3344.18 | 3477.06 |
|
|
139
|
+
| four processes | warm hot | synchronous | 29.89 | 427.18 | 1483.88 | 1997.56 |
|
|
140
|
+
| four processes | warm hot | asynchronous | 27.59 | 448.16 | 1518.00 | 1589.57 |
|
|
141
|
+
| four processes | warm many | synchronous | 69.77 | 157.79 | 410.83 | 2263.13 |
|
|
142
|
+
| four processes | warm many | asynchronous | 36.21 | 265.24 | 1579.43 | 4594.33 |
|
|
143
|
+
| four processes | cold many | synchronous | 13.68 | 1088.93 | 2186.30 | 2519.92 |
|
|
144
|
+
| four processes | cold many | asynchronous | 11.38 | 1209.84 | 2532.38 | 2730.74 |
|
|
145
|
+
|
|
146
|
+
The poor throughput and tail latency in cold and asynchronous cases are
|
|
147
|
+
observed limitations, not capacity recommendations. The small asynchronous
|
|
148
|
+
yield changed scheduling enough to improve some cases and worsen others;
|
|
149
|
+
repeat runs on application-shaped payloads are required before drawing a
|
|
150
|
+
general conclusion. PostgreSQL 14, MySQL 8.0, and other database versions are
|
|
151
|
+
covered by integration tests but were not benchmarked.
|
|
152
|
+
|
|
153
|
+
## Sources of bias
|
|
154
|
+
|
|
155
|
+
- A developer laptop shares CPU, memory, and storage with unrelated processes.
|
|
156
|
+
- Loopback database connections exclude production network latency.
|
|
157
|
+
- Filesystem cache, SQLite WAL state, Node JIT warmup, and garbage collection
|
|
158
|
+
affect short runs.
|
|
159
|
+
- Docker Desktop adds virtualization overhead to containerized databases.
|
|
160
|
+
- The payload is a small counter, not a representative application state size.
|
|
161
|
+
- The harness measures default durability settings and one client concurrency.
|
|
162
|
+
- Hot-identity results deliberately include serialization and cannot be scaled
|
|
163
|
+
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
|
@@ -11,6 +11,7 @@ through `runtime.ref(ActorClass, actorId)`. Both validate options immediately.
|
|
|
11
11
|
| `database` | required | A `Database` adapter. |
|
|
12
12
|
| `tableNamePrefix` | `"solid_objects_"` | Lowercase letters, digits, and underscores; must start with a letter. |
|
|
13
13
|
| `pollingIntervalMilliseconds` | `100` | Positive durable-work polling interval. |
|
|
14
|
+
| `idlePollingIntervalMilliseconds` | `1_000` | Positive ceiling after consecutive empty polling passes. |
|
|
14
15
|
| `syncPollingIntervalMilliseconds` | `50` | Positive result-wait polling interval. |
|
|
15
16
|
| `leaseDurationMilliseconds` | `30_000` | Positive activation lease; must exceed renewal interval. |
|
|
16
17
|
| `leaseRenewalIntervalMilliseconds` | `10_000` | Positive activation renewal cadence. |
|
|
@@ -28,7 +29,9 @@ through `runtime.ref(ActorClass, actorId)`. Both validate options immediately.
|
|
|
28
29
|
The limits measure normalized JSON encoded as UTF-8. `maxResultBytes` also
|
|
29
30
|
limits each computed snapshot getter and effect-handler result. Explicit
|
|
30
31
|
`observables()` values are validated as JSON but do not currently have a
|
|
31
|
-
separate aggregate byte limit.
|
|
32
|
+
separate aggregate byte limit. This includes values wrapped in
|
|
33
|
+
`broadcastInvalidation()`: they are compared in memory even though only their
|
|
34
|
+
names enter the broadcast outbox. `retryDelayMilliseconds` should return a
|
|
32
35
|
non-negative finite number; an invalid application callback will fail the
|
|
33
36
|
affected failure path rather than schedule an invalid timestamp.
|
|
34
37
|
|
|
@@ -50,8 +53,16 @@ affected failure path rather than schedule an invalid timestamp.
|
|
|
50
53
|
|
|
51
54
|
Counts may be zero, but the complete configuration must leave at least one
|
|
52
55
|
runtime role enabled. Broadcast workers are started only when `broadcast` or
|
|
53
|
-
`authorizeSubscription` is configured.
|
|
54
|
-
|
|
56
|
+
`authorizeSubscription` is configured.
|
|
57
|
+
|
|
58
|
+
`pollingIntervalMilliseconds` is the fast interval after work or a wake-up.
|
|
59
|
+
Consecutive empty passes double it up to
|
|
60
|
+
`idlePollingIntervalMilliseconds`. Actor workers never wait longer than
|
|
61
|
+
`leaseRenewalIntervalMilliseconds`. Set the fast and idle values equal for a
|
|
62
|
+
fixed cadence. A custom wake-up adapter should return `true` for a notification
|
|
63
|
+
and `false` for a timeout; an older adapter that returns `void` remains
|
|
64
|
+
compatible and keeps the fast cadence. Wake-ups reduce latency, while database
|
|
65
|
+
polling remains the correctness path.
|
|
55
66
|
|
|
56
67
|
## Retention and cleanup
|
|
57
68
|
|
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.
|