solid-objects 0.12.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 +110 -0
- package/MIT-LICENSE +19 -0
- package/README.md +958 -0
- package/dist/actor.d.ts +81 -0
- package/dist/actor.d.ts.map +1 -0
- package/dist/actor.js +141 -0
- package/dist/actor.js.map +1 -0
- package/dist/application-database.d.ts +3 -0
- package/dist/application-database.d.ts.map +1 -0
- package/dist/application-database.js +59 -0
- package/dist/application-database.js.map +1 -0
- package/dist/broadcast-worker.d.ts +18 -0
- package/dist/broadcast-worker.d.ts.map +1 -0
- package/dist/broadcast-worker.js +74 -0
- package/dist/broadcast-worker.js.map +1 -0
- package/dist/browser/components.d.ts +64 -0
- package/dist/browser/components.d.ts.map +1 -0
- package/dist/browser/components.js +294 -0
- package/dist/browser/components.js.map +1 -0
- package/dist/browser/index.d.ts +46 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +207 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +216 -0
- package/dist/cli.js.map +1 -0
- package/dist/configuration.d.ts +96 -0
- package/dist/configuration.d.ts.map +1 -0
- package/dist/configuration.js +169 -0
- package/dist/configuration.js.map +1 -0
- package/dist/context.d.ts +20 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +24 -0
- package/dist/context.js.map +1 -0
- package/dist/database/deadline.d.ts +9 -0
- package/dist/database/deadline.d.ts.map +1 -0
- package/dist/database/deadline.js +49 -0
- package/dist/database/deadline.js.map +1 -0
- package/dist/database/mysql.d.ts +21 -0
- package/dist/database/mysql.d.ts.map +1 -0
- package/dist/database/mysql.js +192 -0
- package/dist/database/mysql.js.map +1 -0
- package/dist/database/postgresql-sql.d.ts +2 -0
- package/dist/database/postgresql-sql.d.ts.map +1 -0
- package/dist/database/postgresql-sql.js +102 -0
- package/dist/database/postgresql-sql.js.map +1 -0
- package/dist/database/postgresql.d.ts +31 -0
- package/dist/database/postgresql.d.ts.map +1 -0
- package/dist/database/postgresql.js +172 -0
- package/dist/database/postgresql.js.map +1 -0
- package/dist/database/sqlite.d.ts +27 -0
- package/dist/database/sqlite.d.ts.map +1 -0
- package/dist/database/sqlite.js +190 -0
- package/dist/database/sqlite.js.map +1 -0
- package/dist/database/transaction-context.d.ts +3 -0
- package/dist/database/transaction-context.d.ts.map +1 -0
- package/dist/database/transaction-context.js +19 -0
- package/dist/database/transaction-context.js.map +1 -0
- package/dist/database/types.d.ts +20 -0
- package/dist/database/types.d.ts.map +1 -0
- package/dist/database/types.js +2 -0
- package/dist/database/types.js.map +1 -0
- package/dist/dead-letters.d.ts +23 -0
- package/dist/dead-letters.d.ts.map +1 -0
- package/dist/dead-letters.js +13 -0
- package/dist/dead-letters.js.map +1 -0
- package/dist/default-runtime.d.ts +5 -0
- package/dist/default-runtime.d.ts.map +1 -0
- package/dist/default-runtime.js +14 -0
- package/dist/default-runtime.js.map +1 -0
- package/dist/definition.d.ts +32 -0
- package/dist/definition.d.ts.map +1 -0
- package/dist/definition.js +173 -0
- package/dist/definition.js.map +1 -0
- package/dist/doctor.d.ts +32 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +500 -0
- package/dist/doctor.js.map +1 -0
- package/dist/effect-worker.d.ts +18 -0
- package/dist/effect-worker.d.ts.map +1 -0
- package/dist/effect-worker.js +74 -0
- package/dist/effect-worker.js.map +1 -0
- package/dist/errors.d.ts +145 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +120 -0
- package/dist/errors.js.map +1 -0
- package/dist/executable.d.ts +3 -0
- package/dist/executable.d.ts.map +1 -0
- package/dist/executable.js +12 -0
- package/dist/executable.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/maintenance-scheduler.d.ts +17 -0
- package/dist/maintenance-scheduler.d.ts.map +1 -0
- package/dist/maintenance-scheduler.js +41 -0
- package/dist/maintenance-scheduler.js.map +1 -0
- package/dist/process-administration.d.ts +30 -0
- package/dist/process-administration.d.ts.map +1 -0
- package/dist/process-administration.js +13 -0
- package/dist/process-administration.js.map +1 -0
- package/dist/realtime.d.ts +33 -0
- package/dist/realtime.d.ts.map +1 -0
- package/dist/realtime.js +238 -0
- package/dist/realtime.js.map +1 -0
- package/dist/reconciliation.d.ts +41 -0
- package/dist/reconciliation.d.ts.map +1 -0
- package/dist/reconciliation.js +19 -0
- package/dist/reconciliation.js.map +1 -0
- package/dist/records.d.ts +137 -0
- package/dist/records.d.ts.map +1 -0
- package/dist/records.js +2 -0
- package/dist/records.js.map +1 -0
- package/dist/reference.d.ts +97 -0
- package/dist/reference.d.ts.map +1 -0
- package/dist/reference.js +157 -0
- package/dist/reference.js.map +1 -0
- package/dist/reminder-administration.d.ts +35 -0
- package/dist/reminder-administration.d.ts.map +1 -0
- package/dist/reminder-administration.js +13 -0
- package/dist/reminder-administration.js.map +1 -0
- package/dist/reminder-scheduler.d.ts +18 -0
- package/dist/reminder-scheduler.d.ts.map +1 -0
- package/dist/reminder-scheduler.js +88 -0
- package/dist/reminder-scheduler.js.map +1 -0
- package/dist/repository.d.ts +142 -0
- package/dist/repository.d.ts.map +1 -0
- package/dist/repository.js +1237 -0
- package/dist/repository.js.map +1 -0
- package/dist/retention.d.ts +17 -0
- package/dist/retention.d.ts.map +1 -0
- package/dist/retention.js +13 -0
- package/dist/retention.js.map +1 -0
- package/dist/runtime.d.ts +140 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +1737 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schema.d.ts +8 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +260 -0
- package/dist/schema.js.map +1 -0
- package/dist/serialization.d.ts +11 -0
- package/dist/serialization.d.ts.map +1 -0
- package/dist/serialization.js +82 -0
- package/dist/serialization.js.map +1 -0
- package/dist/test-helper.d.ts +15 -0
- package/dist/test-helper.d.ts.map +1 -0
- package/dist/test-helper.js +69 -0
- package/dist/test-helper.js.map +1 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/dist/wake-up/postgresql.d.ts +42 -0
- package/dist/wake-up/postgresql.d.ts.map +1 -0
- package/dist/wake-up/postgresql.js +234 -0
- package/dist/wake-up/postgresql.js.map +1 -0
- package/dist/wake-up/redis.d.ts +35 -0
- package/dist/wake-up/redis.d.ts.map +1 -0
- package/dist/wake-up/redis.js +151 -0
- package/dist/wake-up/redis.js.map +1 -0
- package/dist/wake-up.d.ts +24 -0
- package/dist/wake-up.d.ts.map +1 -0
- package/dist/wake-up.js +66 -0
- package/dist/wake-up.js.map +1 -0
- package/dist/worker.d.ts +36 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +281 -0
- package/dist/worker.js.map +1 -0
- package/docs/api.md +197 -0
- package/docs/architecture.md +113 -0
- package/docs/authorization.md +31 -0
- package/docs/browser-protocol.md +93 -0
- package/docs/configuration.md +168 -0
- package/docs/correctness.md +53 -0
- package/docs/errors-and-recovery.md +89 -0
- package/docs/operations.md +170 -0
- package/docs/parity.md +94 -0
- package/docs/releasing.md +45 -0
- package/docs/state-and-lifecycle.md +162 -0
- package/package.json +100 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
Solid Objects is a database-backed virtual actor runtime. An actor is addressed
|
|
4
|
+
by `(actor type, actor id)`, processes one durable mailbox turn at a time, and
|
|
5
|
+
persists JSON state between activations.
|
|
6
|
+
|
|
7
|
+
A durable message envelope selects an actor `operation` and records its
|
|
8
|
+
`delivery_mode` as `async`, `sync`, or `internal`. An operation is actor code;
|
|
9
|
+
a message is the durable delivery record that invokes it. Each message has a
|
|
10
|
+
generated request ID independent of its optional caller-supplied idempotency
|
|
11
|
+
key. Matching idempotency keys are scoped to one actor and must identify the
|
|
12
|
+
same operation, delivery mode, and arguments.
|
|
13
|
+
|
|
14
|
+
The correctness contract is:
|
|
15
|
+
|
|
16
|
+
> Messages for one actor are durably enqueued and processed sequentially, at
|
|
17
|
+
> least once, by at most one valid activation lease holder at a time.
|
|
18
|
+
|
|
19
|
+
Actor code runs outside the database transaction. A short transaction guarded
|
|
20
|
+
by the activation owner, token, generation, expiration, and claimed-message
|
|
21
|
+
membership commits state, completion, and staged outboxes together.
|
|
22
|
+
|
|
23
|
+
Each role takes a generation watch before checking for work. A post-commit
|
|
24
|
+
wake-up therefore cannot fall into the gap between an empty claim and the
|
|
25
|
+
worker's wait. The default adapter broadcasts within one process; polling
|
|
26
|
+
remains active as the durable fallback and custom adapters can bridge process
|
|
27
|
+
boundaries.
|
|
28
|
+
|
|
29
|
+
Each runtime role occupies a supervised factory slot. An unexpected promise
|
|
30
|
+
resolution or rejection cleans up that instance, waits with capped exponential
|
|
31
|
+
backoff, and builds the replacement through the same factory. Supervision stops
|
|
32
|
+
at the shutdown boundary; database leases and fencing remain the correctness
|
|
33
|
+
mechanism if a failed role was still executing actor code.
|
|
34
|
+
|
|
35
|
+
SQLite serializes access through one process-local connection and begins write
|
|
36
|
+
transactions immediately. PostgreSQL and MySQL use bounded pools, keep each
|
|
37
|
+
transaction on one checked-out client, store timestamps and sequences as
|
|
38
|
+
64-bit integers, and lock an actor's instance row while allocating mailbox
|
|
39
|
+
sequences. MySQL creates InnoDB tables and retries only the side-effect-free
|
|
40
|
+
enqueue transaction when InnoDB chooses it as a deadlock victim. Every adapter
|
|
41
|
+
uses database time and the same fencing predicates.
|
|
42
|
+
|
|
43
|
+
Synchronous invocation carries a monotonic deadline into adapter operations.
|
|
44
|
+
SQLite bounds its process-local access queue and busy timeout. Outside a caller
|
|
45
|
+
deadline, it retries only acquisition of a failed `BEGIN IMMEDIATE`; it never
|
|
46
|
+
replays a transaction callback. PostgreSQL bounds pool checkout and installs
|
|
47
|
+
statement and lock timeouts. MySQL bounds pool checkout and client queries and
|
|
48
|
+
installs transaction execution and lock-wait limits. A deadline before enqueue
|
|
49
|
+
commit produces no durable message. After commit, timeout diagnostics retain
|
|
50
|
+
the message reference for later recovery.
|
|
51
|
+
Already-running JavaScript actor code is cooperative rather than forcefully
|
|
52
|
+
preempted; leases and fenced commits remain authoritative if it outlives the
|
|
53
|
+
caller's wait.
|
|
54
|
+
|
|
55
|
+
Each database adapter also tracks its active transaction through Node's async
|
|
56
|
+
context. A committed call or message wait fails before enqueue or polling when
|
|
57
|
+
the same logical call stack already owns a Solid Objects transaction, rather
|
|
58
|
+
than waiting for a connection or serialized SQLite slot it cannot release.
|
|
59
|
+
|
|
60
|
+
PostgreSQL notifications are an opt-in latency layer. One event-driven client
|
|
61
|
+
per runtime listens on role-specific channels before the worker checks durable
|
|
62
|
+
state, which closes the listener-startup race without Ruby's connection per
|
|
63
|
+
blocking thread. A notification advances a process-local role generation and
|
|
64
|
+
wakes every matching waiter. Reconnection and notification loss fall back to
|
|
65
|
+
the ordinary polling interval.
|
|
66
|
+
|
|
67
|
+
The optional Redis adapter provides the same role generations through Pub/Sub
|
|
68
|
+
for deployments that already operate Redis. It keeps commands and subscriptions
|
|
69
|
+
on separate lazy connections. Connect, subscribe, and publish failures are
|
|
70
|
+
bounded and isolated; durable polling continues independently.
|
|
71
|
+
|
|
72
|
+
A worker claims one actor globally, then preferentially drains up to
|
|
73
|
+
`maxMessagesPerActivationPass` ready turns for that instance. Reaching the cap
|
|
74
|
+
moves only that actor's already-due ready memberships to current database time.
|
|
75
|
+
Actors with older ready work therefore win the next global claim; delayed work
|
|
76
|
+
keeps its original future availability.
|
|
77
|
+
|
|
78
|
+
The global claim reads at most `claimScanLimit` ordered candidates. If another
|
|
79
|
+
worker acquires the first candidate's lease, the transaction continues through
|
|
80
|
+
that bounded set instead of returning idle and sacrificing parallelism across
|
|
81
|
+
independent actor identities.
|
|
82
|
+
|
|
83
|
+
When a pass becomes idle, a long-running worker keeps the hydrated actor and
|
|
84
|
+
continues renewing the same fenced lease until its idle timeout. A later turn
|
|
85
|
+
on that actor reuses both its persisted public fields and process-local private
|
|
86
|
+
fields. Failed and rejected turns restore public fields to their pre-turn
|
|
87
|
+
values before reuse. Fairness yield, timeout, lease loss, and shutdown run the
|
|
88
|
+
best-effort deactivation hook and conditionally release the matching lease.
|
|
89
|
+
One-shot drain helpers release immediately because they will not remain alive
|
|
90
|
+
to renew.
|
|
91
|
+
|
|
92
|
+
Realtime delivery is transport-neutral. A host-authenticated session authorizes
|
|
93
|
+
actor subscriptions, replays a committed observable projection, and follows
|
|
94
|
+
the durable broadcast outbox in revision order. The browser client applies the
|
|
95
|
+
same incarnation and revision fence without importing Node APIs.
|
|
96
|
+
|
|
97
|
+
Actors may also declare static personalized payload projections. Each requested
|
|
98
|
+
name is reauthorized as a query and evaluated against a fresh actor hydrated
|
|
99
|
+
from committed state under that subscriber's session context. Payload names use
|
|
100
|
+
independent revision fences, so a failed projection is omitted and retried at a
|
|
101
|
+
later committed revision without blocking invalidations or sibling payloads.
|
|
102
|
+
Actors with payload declarations emit an empty-observable revision event for a
|
|
103
|
+
state change when no scalar observable changed.
|
|
104
|
+
|
|
105
|
+
Sessions are process-local. A multi-process deployment bridges committed
|
|
106
|
+
broadcast events through an application-owned shared transport and calls
|
|
107
|
+
`runtime.realtime.publish()` in each process that owns connections. This keeps
|
|
108
|
+
another broker optional for a single Node process while making the
|
|
109
|
+
cross-process boundary explicit.
|
|
110
|
+
|
|
111
|
+
This package ports the Solid Objects programming model. It does not share a
|
|
112
|
+
database schema or runtime protocol with the Ruby gem and does not reproduce
|
|
113
|
+
Cloudflare's placement or edge-runtime guarantees.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Authorization
|
|
2
|
+
|
|
3
|
+
Actor identifiers are identifiers, not capabilities. Message, query,
|
|
4
|
+
destruction, subscription, and administration policies deny by default.
|
|
5
|
+
Applications must authorize the current server-side subject at every entry
|
|
6
|
+
point.
|
|
7
|
+
|
|
8
|
+
Dead-letter inspection and retry call `authorizeAdministration` with an action,
|
|
9
|
+
resource, optional resource ID, and the caller's authorization context. Retry
|
|
10
|
+
authorization happens before lookup so a denied caller cannot use record IDs as
|
|
11
|
+
an existence oracle.
|
|
12
|
+
|
|
13
|
+
`runtime.realtime` is transport-neutral. The host application authenticates its
|
|
14
|
+
WebSocket or stream connection, passes that fresh server-side subject as the
|
|
15
|
+
session's `authorizationContext`, and forwards incoming protocol messages to
|
|
16
|
+
`session.receive()`. Every subscribe request calls `authorizeSubscription`
|
|
17
|
+
before actor type lookup, so denied callers cannot probe the registry. A new
|
|
18
|
+
connection must use a newly resolved authorization context; do not copy a user
|
|
19
|
+
object from an earlier request or trust an actor ID supplied by the browser.
|
|
20
|
+
|
|
21
|
+
Successful subscription authorization allows the explicit `observables()`
|
|
22
|
+
projection for that actor, including the immediate committed replay. It does
|
|
23
|
+
not authorize actor state, operations, queries, destruction, or administration.
|
|
24
|
+
The runtime removes all registrations when the host calls `session.close()`.
|
|
25
|
+
|
|
26
|
+
Requested personalized payloads cross both boundaries. The runtime first calls
|
|
27
|
+
`authorizeSubscription`, then calls `authorizeQuery` with the payload name and
|
|
28
|
+
the same fresh session authorization context before every projection. A false
|
|
29
|
+
query decision omits that payload. Projection and authorization failures are
|
|
30
|
+
confined to the payload name and emit metadata-only instrumentation; they do
|
|
31
|
+
not reject observable delivery or expose exception messages.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Browser protocol
|
|
2
|
+
|
|
3
|
+
The browser entry point contains no Node imports. It sends versioned
|
|
4
|
+
subscription requests and receives JSON invalidation envelopes over WebSocket.
|
|
5
|
+
The application supplies the rendering callback and authenticated WebSocket
|
|
6
|
+
server; `runtime.realtime` supplies the server-side session protocol.
|
|
7
|
+
|
|
8
|
+
The 0.1 subscription request is:
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"version": 1,
|
|
13
|
+
"action": "subscribe",
|
|
14
|
+
"actorType": "Counter",
|
|
15
|
+
"actorId": "primary",
|
|
16
|
+
"payloads": ["summary"]
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`unsubscribe` uses the same shape with `"action": "unsubscribe"`. The server
|
|
21
|
+
authorizes every subscribe request against the session's fresh connection
|
|
22
|
+
context before looking up the actor. A successful subscription immediately
|
|
23
|
+
sends the latest committed projection and then follows its durable outbox.
|
|
24
|
+
|
|
25
|
+
The 0.1 envelope is:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"version": 1,
|
|
30
|
+
"kind": "invalidation",
|
|
31
|
+
"actorType": "Counter",
|
|
32
|
+
"actorId": "primary",
|
|
33
|
+
"instanceId": "019...",
|
|
34
|
+
"revision": "42",
|
|
35
|
+
"observables": { "count": 3 }
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Requested personalized projections use a separate envelope:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"version": 1,
|
|
44
|
+
"kind": "payload",
|
|
45
|
+
"actorType": "Counter",
|
|
46
|
+
"actorId": "primary",
|
|
47
|
+
"instanceId": "019...",
|
|
48
|
+
"revision": "42",
|
|
49
|
+
"name": "summary",
|
|
50
|
+
"payload": { "label": "Your counter", "count": 3 }
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Invalidations and each payload name have independent revision fences. This
|
|
55
|
+
lets the server project the latest committed subscriber view even while an
|
|
56
|
+
older durable invalidation is being delivered. Payload bodies must be JSON
|
|
57
|
+
objects or arrays and are deeply frozen after parsing.
|
|
58
|
+
|
|
59
|
+
Revisions are non-negative integer strings so values larger than JavaScript's
|
|
60
|
+
safe integer range remain exact. A new instance ID establishes a new actor
|
|
61
|
+
incarnation and resets revision comparison. The runtime preserves revision
|
|
62
|
+
order per actor; server sessions and browser clients also reject duplicates and
|
|
63
|
+
stale revisions within an incarnation.
|
|
64
|
+
|
|
65
|
+
## Component refresh registry
|
|
66
|
+
|
|
67
|
+
`SolidObjectsComponentRegistry` maps changed observable names to keyed UI
|
|
68
|
+
registrations. The browser supplies an asynchronous `refresh` function and a
|
|
69
|
+
synchronous `apply` function, so HTML, virtual DOM, and framework-native render
|
|
70
|
+
results use the same coordination contract without importing Turbo.
|
|
71
|
+
|
|
72
|
+
Components may share a batch name. A microtask unions affected components in
|
|
73
|
+
the same actor, batch, incarnation, and revision into one refresh request.
|
|
74
|
+
Unbatched components refresh independently. A strictly newer request aborts an
|
|
75
|
+
older request for the same group; same-revision requests do not cancel each
|
|
76
|
+
other. Results are accepted only for requested, still-registered targets, and
|
|
77
|
+
each target has its own incarnation/revision fence.
|
|
78
|
+
|
|
79
|
+
The registry never treats actor identity, component name, key, dependency, or
|
|
80
|
+
target as authorization. The application-owned refresh endpoint authenticates
|
|
81
|
+
the browser and reauthorizes every requested component before returning render
|
|
82
|
+
results.
|
|
83
|
+
|
|
84
|
+
`pnpm run test:browser` builds the browser entry and runs its Playwright suite
|
|
85
|
+
in Chromium. It covers native WebSocket subscription replay, stale revision and
|
|
86
|
+
incarnation handling, personalized payloads, component batching, and
|
|
87
|
+
superseded-request cancellation.
|
|
88
|
+
|
|
89
|
+
`runtime.realtime` delivers directly to sessions in its own Node process. For
|
|
90
|
+
several WebSocket processes, the configured `broadcast` callback publishes the
|
|
91
|
+
committed envelope through a shared transport and each process passes received
|
|
92
|
+
envelopes to `runtime.realtime.publish()`. The session fence safely drops the
|
|
93
|
+
duplicate seen by a process that both claimed and received the same event.
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Configuration reference
|
|
2
|
+
|
|
3
|
+
`configure(options)` creates a runtime and installs it as the default used by
|
|
4
|
+
`Actor.ref()`. `createRuntime(options)` creates an isolated runtime addressed
|
|
5
|
+
through `runtime.ref(ActorClass, actorId)`. Both validate options immediately.
|
|
6
|
+
|
|
7
|
+
## Core runtime options
|
|
8
|
+
|
|
9
|
+
| Option | Default | Contract |
|
|
10
|
+
| ------------------------------------- | -----------------------: | ----------------------------------------------------------------------------- |
|
|
11
|
+
| `database` | required | A `Database` adapter. |
|
|
12
|
+
| `tableNamePrefix` | `"solid_objects_"` | Lowercase letters, digits, and underscores; must start with a letter. |
|
|
13
|
+
| `pollingIntervalMilliseconds` | `100` | Positive durable-work polling interval. |
|
|
14
|
+
| `syncPollingIntervalMilliseconds` | `50` | Positive result-wait polling interval. |
|
|
15
|
+
| `leaseDurationMilliseconds` | `30_000` | Positive activation lease; must exceed renewal interval. |
|
|
16
|
+
| `leaseRenewalIntervalMilliseconds` | `10_000` | Positive activation renewal cadence. |
|
|
17
|
+
| `idleDeactivationTimeoutMilliseconds` | `30_000` | Non-negative hydrated activation cache lifetime; `0` disables idle retention. |
|
|
18
|
+
| `maxMailboxLength` | `10_000` | Positive maximum ready and claimed messages for one actor. |
|
|
19
|
+
| `maxPayloadBytes` | `1_048_576` | Positive byte limit for operation arguments and personalized payloads. |
|
|
20
|
+
| `maxStateBytes` | `5_242_880` | Positive persisted actor-state byte limit. |
|
|
21
|
+
| `maxResultBytes` | `1_048_576` | Positive operation-result byte limit. |
|
|
22
|
+
| `maxAttempts` | `5` | Positive maximum operation, effect, and broadcast attempts. |
|
|
23
|
+
| `maxMessagesPerActivationPass` | `50` | Positive integer turn budget before fairness yield. |
|
|
24
|
+
| `maxActivationDurationMilliseconds` | `5_000` | Positive elapsed-time budget before fairness yield. |
|
|
25
|
+
| `claimScanLimit` | `100` | Positive integer ready-actor candidates considered per global claim. |
|
|
26
|
+
| `retryDelayMilliseconds` | exponential 1-60 seconds | Receives the next attempt number and returns its delay. |
|
|
27
|
+
|
|
28
|
+
The limits measure normalized JSON encoded as UTF-8. `maxResultBytes` also
|
|
29
|
+
limits each computed snapshot getter and effect-handler result. Explicit
|
|
30
|
+
`observables()` values are validated as JSON but do not currently have a
|
|
31
|
+
separate aggregate byte limit. `retryDelayMilliseconds` should return a
|
|
32
|
+
non-negative finite number; an invalid application callback will fail the
|
|
33
|
+
affected failure path rather than schedule an invalid timestamp.
|
|
34
|
+
|
|
35
|
+
## Runtime roles and supervision
|
|
36
|
+
|
|
37
|
+
| Option | Default | Contract |
|
|
38
|
+
| ------------------------------------------- | -----------------------: | -------------------------------------------------------- |
|
|
39
|
+
| `workerCount` | `1` | Non-negative actor workers. |
|
|
40
|
+
| `effectWorkerCount` | `1` | Non-negative effect workers. |
|
|
41
|
+
| `broadcastWorkerCount` | `1` | Non-negative broadcast workers when realtime is enabled. |
|
|
42
|
+
| `reminderSchedulerCount` | `1` | Non-negative reminder schedulers. |
|
|
43
|
+
| `processHeartbeatIntervalMilliseconds` | `15_000` | Positive persisted heartbeat cadence. |
|
|
44
|
+
| `processAliveThresholdMilliseconds` | `60_000` | Positive age after which an owner is stale. |
|
|
45
|
+
| `shutdownTimeoutMilliseconds` | `15_000` | Positive shared graceful-shutdown budget. |
|
|
46
|
+
| `supervisorRestartDelayMilliseconds` | `100` | Positive initial failed-role replacement delay. |
|
|
47
|
+
| `supervisorMaximumRestartDelayMilliseconds` | `10_000` | Positive cap no smaller than the initial delay. |
|
|
48
|
+
| `wakeUp` | `InProcessWakeUpAdapter` | Adapter implementing `watch`, `notify`, and `close`. |
|
|
49
|
+
| `logger` | console methods | Structured `debug`, `info`, `warn`, and `error` sink. |
|
|
50
|
+
|
|
51
|
+
Counts may be zero, but the complete configuration must leave at least one
|
|
52
|
+
runtime role enabled. Broadcast workers are started only when `broadcast` or
|
|
53
|
+
`authorizeSubscription` is configured. Wake-ups reduce latency; durable polling
|
|
54
|
+
remains the correctness path.
|
|
55
|
+
|
|
56
|
+
## Retention and cleanup
|
|
57
|
+
|
|
58
|
+
| Option | Default | Contract |
|
|
59
|
+
| ---------------------------------------- | ----------: | ---------------------------------------------------------------- |
|
|
60
|
+
| `retentionIntervalMilliseconds` | `3_600_000` | Non-negative automatic history-pruning cadence; `0` disables it. |
|
|
61
|
+
| `deadProcessCleanupIntervalMilliseconds` | `60_000` | Non-negative stale-owner recovery cadence; `0` disables it. |
|
|
62
|
+
| `messageRetentionMilliseconds` | 30 days | Positive default completed-message retention. |
|
|
63
|
+
| `messageRetentionByActorType` | `{}` | Positive per-type message-retention overrides. |
|
|
64
|
+
| `instanceRetentionByActorType` | `{}` | Positive per-type instance-expiration opt-ins. |
|
|
65
|
+
| `processRetentionMilliseconds` | 7 days | Positive stopped-process retention. |
|
|
66
|
+
| `pruneBatchSize` | `1_000` | Positive integer maximum rows rechecked per pruning transaction. |
|
|
67
|
+
|
|
68
|
+
Automatic retention prunes messages and stopped process records. Actor
|
|
69
|
+
instance expiration remains an explicit administration action even when a type
|
|
70
|
+
opts in.
|
|
71
|
+
|
|
72
|
+
## Authorization, instrumentation, and broadcast
|
|
73
|
+
|
|
74
|
+
All authorization callbacks default to `false`:
|
|
75
|
+
|
|
76
|
+
- `authorizeMessage({ actorType, actorId, operation, arguments,
|
|
77
|
+
authorizationContext })`
|
|
78
|
+
- `authorizeQuery({ actorType, actorId, operation, arguments,
|
|
79
|
+
authorizationContext })`
|
|
80
|
+
- `authorizeDestroy({ actorType, actorId, authorizationContext })`
|
|
81
|
+
- `authorizeAdministration({ action, resource, resourceId,
|
|
82
|
+
authorizationContext })`
|
|
83
|
+
- `authorizeSubscription({ actorType, actorId, authorizationContext })`
|
|
84
|
+
|
|
85
|
+
Callbacks may return a boolean or promise. Authorization contexts are
|
|
86
|
+
application-owned server values and are never persisted. See
|
|
87
|
+
[Authorization](authorization.md) for the entry-point matrix.
|
|
88
|
+
|
|
89
|
+
`instrumentation(event)` is synchronous. The runtime freezes metadata-only
|
|
90
|
+
events, catches sink failures, and reports the sink error through `logger`.
|
|
91
|
+
|
|
92
|
+
`broadcast(event)` forwards committed realtime invalidations through an
|
|
93
|
+
application-owned shared transport. A receiving process calls
|
|
94
|
+
`runtime.realtime.publish(event)`. It is unnecessary for a single-process
|
|
95
|
+
server.
|
|
96
|
+
|
|
97
|
+
## Database adapters
|
|
98
|
+
|
|
99
|
+
### SQLite
|
|
100
|
+
|
|
101
|
+
`sqlite({ path, timeoutMilliseconds = 5_000, lockRetryAttempts = 10 })` uses
|
|
102
|
+
Node's built-in `node:sqlite`. `lockRetryAttempts` is a positive integer. The
|
|
103
|
+
timeout controls SQLite's busy timeout; transaction acquisition retries use
|
|
104
|
+
short capped backoff only when no synchronous deadline is active.
|
|
105
|
+
|
|
106
|
+
### PostgreSQL
|
|
107
|
+
|
|
108
|
+
`postgresql(options)` accepts:
|
|
109
|
+
|
|
110
|
+
| Option | Default |
|
|
111
|
+
| ------------------------------- | -------------------------: |
|
|
112
|
+
| `connectionString` | required |
|
|
113
|
+
| `maximumConnections` | `pg` default |
|
|
114
|
+
| `idleTimeoutMilliseconds` | `pg` default |
|
|
115
|
+
| `connectionTimeoutMilliseconds` | `pg` default |
|
|
116
|
+
| `applicationName` | `"solid-objects"` |
|
|
117
|
+
| `onPoolError` | structured `console.error` |
|
|
118
|
+
|
|
119
|
+
`database.wakeUp(options)` creates a dedicated notification adapter using the
|
|
120
|
+
same connection string. Its options are `channelPrefix = "solid_objects"`,
|
|
121
|
+
`applicationName = "solid-objects-wake-up"`, and `onListenerError`. Use a
|
|
122
|
+
direct or session-pooled connection because PostgreSQL `LISTEN` is
|
|
123
|
+
session-scoped.
|
|
124
|
+
|
|
125
|
+
`postgresqlWakeUp({ connectionString, ...options })` constructs that adapter
|
|
126
|
+
without a database instance. The adapter translates portable `?` parameters;
|
|
127
|
+
write `??` in SQL passed through `DatabaseConnection` when PostgreSQL needs the
|
|
128
|
+
literal JSON existence operator.
|
|
129
|
+
|
|
130
|
+
### MySQL
|
|
131
|
+
|
|
132
|
+
`mysql(options)` accepts:
|
|
133
|
+
|
|
134
|
+
| Option | Default |
|
|
135
|
+
| ------------------------- | ---------------------------: |
|
|
136
|
+
| `connectionString` | required |
|
|
137
|
+
| `maximumConnections` | `10` |
|
|
138
|
+
| `idleTimeoutMilliseconds` | `60_000` |
|
|
139
|
+
| `queueLimit` | `0` (unbounded driver queue) |
|
|
140
|
+
|
|
141
|
+
Counts are non-negative except `maximumConnections`, which is positive. Tables
|
|
142
|
+
use InnoDB. `mysqlSql(sql)` translates the limited portable conflict syntax
|
|
143
|
+
used by database integrations into MySQL syntax.
|
|
144
|
+
|
|
145
|
+
### Redis wake-up
|
|
146
|
+
|
|
147
|
+
`redisWakeUp({ url, channelPrefix = "solid_objects",
|
|
148
|
+
connectionTimeoutMilliseconds = 1_000, onError })` uses separate lazy
|
|
149
|
+
publisher and subscriber clients. Connection, subscription, and publication
|
|
150
|
+
failures are reported and bounded; database polling continues.
|
|
151
|
+
|
|
152
|
+
## Lifecycle order
|
|
153
|
+
|
|
154
|
+
Call `runtime.install()` before serving traffic or starting roles. Run the
|
|
155
|
+
supervisor with an application-owned abort signal. To shut down, abort, await
|
|
156
|
+
`runtime.run()`, then close adapters through `runtime.close()`:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
const controller = new AbortController()
|
|
160
|
+
const running = runtime.run(controller.signal)
|
|
161
|
+
|
|
162
|
+
controller.abort()
|
|
163
|
+
await running
|
|
164
|
+
await runtime.close()
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Calling `close()` while `run()` is active raises an error so database and
|
|
168
|
+
wake-up connections cannot disappear beneath live roles.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Correctness and delivery semantics
|
|
2
|
+
|
|
3
|
+
- Delivery is ordered per actor identity and at least once.
|
|
4
|
+
- Different identities may execute concurrently.
|
|
5
|
+
- Sequence allocation and durable enqueue are one transaction.
|
|
6
|
+
- A retryable failure rolls state and staged intents back and blocks later work.
|
|
7
|
+
- A stale activation may finish JavaScript but cannot commit.
|
|
8
|
+
- Effects can execute more than once and must deduplicate by their stable id.
|
|
9
|
+
- Destruction creates an incarnation boundary; old leases cannot address a
|
|
10
|
+
recreated actor. A caller authorized before destruction receives
|
|
11
|
+
`ActorDestroyed`; an unknown or forged reference remains unauthorized.
|
|
12
|
+
- Graceful process shutdown and stale-process cleanup use the same atomic
|
|
13
|
+
ownership release: claimed messages return to ready membership, activations
|
|
14
|
+
are unfenced, and processing effect, reminder, and broadcast claims become
|
|
15
|
+
available again. A stale draining process is recoverable like a stale running
|
|
16
|
+
process.
|
|
17
|
+
- Permanent operation failure raises `MessageFailed` with the durable message
|
|
18
|
+
ID and persisted error details instead of treating actor code text as the
|
|
19
|
+
public exception contract.
|
|
20
|
+
- Operation, lifecycle, observable, and payload callbacks retain their owning
|
|
21
|
+
runtime through async context. Isolated runtimes therefore never fall back to
|
|
22
|
+
a global default while actor-owned code resolves another actor reference.
|
|
23
|
+
- Observable outbox rows are claimed in actor revision order. Subscription
|
|
24
|
+
sessions and browser clients reject duplicate or stale revisions within an
|
|
25
|
+
incarnation, while a recreated actor starts a new revision sequence.
|
|
26
|
+
- Wake-up notifications happen only after commit and never replace polling.
|
|
27
|
+
Workers watch before claiming, so an in-process notification between an empty
|
|
28
|
+
claim and the following wait is retained.
|
|
29
|
+
- Activation passes are bounded. Yielding changes ready-membership polling
|
|
30
|
+
order only; it neither changes durable message sequence nor makes future work
|
|
31
|
+
due early.
|
|
32
|
+
- Idle hydrated actors remain fenced by the same renewable lease. Cache reuse
|
|
33
|
+
never bypasses claim membership or the commit fence, failed turns restore
|
|
34
|
+
their public fields before reuse, and conditional release cannot clear a
|
|
35
|
+
newer owner or generation.
|
|
36
|
+
- Actor setup completes before an attempt begins. A hydration, migration, or
|
|
37
|
+
activation failure atomically restores ready membership, releases its
|
|
38
|
+
activation fence, and restores the attempt count; an awaiting caller receives
|
|
39
|
+
the setup error.
|
|
40
|
+
- `guardApplicationDatabase()` rejects direct application writes during actor
|
|
41
|
+
operations, observable and payload projections, and state migrations. It
|
|
42
|
+
permits only `SELECT` through row-returning methods. Commit actions remain in
|
|
43
|
+
that read-only context and may write only through their supplied fenced
|
|
44
|
+
connection. The guarantee applies only to clients passed through the facade.
|
|
45
|
+
- Snapshots hydrate one committed state image and evaluate every inferred getter
|
|
46
|
+
against it. Getter mutation or staged durable work rejects the whole snapshot;
|
|
47
|
+
successful snapshots and their nested JSON values are frozen copies.
|
|
48
|
+
- Personalized payloads hydrate committed state separately for every payload
|
|
49
|
+
name and subscriber. Each projection is read-only, size bounded, and fenced
|
|
50
|
+
independently by actor incarnation and revision. One denied, mutating, or
|
|
51
|
+
failing projection cannot stop its siblings or observable delivery. A state
|
|
52
|
+
change on an actor declaring payloads creates a revision broadcast even when
|
|
53
|
+
the actor declares no scalar observables.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Errors and recovery
|
|
2
|
+
|
|
3
|
+
All library errors extend `SolidObjectsError`. Ordinary validation failures may
|
|
4
|
+
also use `TypeError`. Catch the narrow class whose recovery you can perform;
|
|
5
|
+
do not parse error messages.
|
|
6
|
+
|
|
7
|
+
## Caller-facing control flow
|
|
8
|
+
|
|
9
|
+
| Error | Meaning | Recovery |
|
|
10
|
+
| ----------------------- | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| `Rejected` | Actor domain refusal. The turn rolled back and will not retry. | Inspect `code`, frozen `details`, and `messageId`; correct the request or present the refusal. |
|
|
12
|
+
| `Unauthorized` | The relevant deny-by-default policy refused the operation. | Reauthenticate or change the application policy; actor IDs are not credentials. |
|
|
13
|
+
| `SyncEnqueueTimeout` | The enqueue transaction did not commit before the call deadline. | It is safe to retry with an idempotency key; no durable message exists from this attempt. |
|
|
14
|
+
| `SyncTimeout` | Enqueue committed but result waiting expired. | Retain `messageReference`, inspect `details.waitingOn`, and call `wait()` again later. |
|
|
15
|
+
| `MessageFailed` | The durable operation failed permanently. | Correlate `messageId`, inspect persisted `details`, then inspect or retry its dead letter under administration authorization. |
|
|
16
|
+
| `ActorDestroyed` | An authorized waiter lost the actor incarnation it was waiting on. | Decide whether the new incarnation should receive a new operation. |
|
|
17
|
+
| `IdempotencyConflict` | One actor reused a key for different arguments, operation, or delivery mode. | Reuse a key only for the identical request. |
|
|
18
|
+
| `MailboxFull` | One actor reached `maxMailboxLength`. | Apply admission control or wait for existing work; do not blindly increase the cap. |
|
|
19
|
+
| `PayloadTooLarge` | Arguments, state, result, snapshot getter, effect result, or personalized payload exceeded its configured limit. | Reduce the JSON value or deliberately raise the corresponding limit. |
|
|
20
|
+
| `SyncInsideTransaction` | A committed call or message wait would self-deadlock inside this adapter's transaction. | Finish the transaction first or stage actor-owned work through a commit action. |
|
|
21
|
+
|
|
22
|
+
`MessageReference.status()`, `result()`, and `wait()` reauthorize the stored
|
|
23
|
+
operation. `result()` returns `undefined` while work is nonterminal, returns the
|
|
24
|
+
committed result when complete, and raises `Rejected` or `MessageFailed` for a
|
|
25
|
+
terminal refusal or failure. `wait()` blocks until the same terminal outcomes
|
|
26
|
+
or its deadline.
|
|
27
|
+
|
|
28
|
+
## Definition and programming errors
|
|
29
|
+
|
|
30
|
+
These errors normally require a code or deployment correction:
|
|
31
|
+
|
|
32
|
+
- `InvalidActor`: invalid actor type, state version, migration declaration,
|
|
33
|
+
member name, payload declaration, or duplicate registration.
|
|
34
|
+
- `UnknownActorType`, `UnknownOperation`, `UnknownEffect`, and
|
|
35
|
+
`UnknownCommitAction`: deployed registration does not match durable work or
|
|
36
|
+
code attempted an undeclared operation.
|
|
37
|
+
- `StateMigrationError`: migration path is missing or failed, or stored state
|
|
38
|
+
is newer than the process code.
|
|
39
|
+
- `ActorCallCycle`: actor code tried to make a synchronous actor call, use a
|
|
40
|
+
reference's `send`, or destroy an actor. Stage delivery with `this.sendTo()`.
|
|
41
|
+
- `QueryMutatedState`: a field/getter query, snapshot, observable, or payload
|
|
42
|
+
projection changed state or staged durable work.
|
|
43
|
+
- `ApplicationWriteForbidden`: actor-owned code attempted a write through
|
|
44
|
+
`guardApplicationDatabase()` outside the fenced commit-action connection.
|
|
45
|
+
- `InvalidPayload`, `InvalidPayloadBroadcast`, and
|
|
46
|
+
`UnknownPayloadBroadcast`: a value or requested personalized projection did
|
|
47
|
+
not satisfy the JSON and declaration contract.
|
|
48
|
+
- `UnsupportedDatabase`: an adapter reports a database family the runtime does
|
|
49
|
+
not support.
|
|
50
|
+
|
|
51
|
+
`NonRetryableError` is also the application extension point for an unexpected
|
|
52
|
+
failure that retry cannot resolve:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { NonRetryableError } from "solid-objects"
|
|
56
|
+
|
|
57
|
+
class InvalidWorkflowDefinition extends NonRetryableError {}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Throwing it from an operation rolls back the turn and dead-letters immediately.
|
|
61
|
+
Prefer `this.reject()` for expected domain decisions because it preserves a
|
|
62
|
+
stable public code and does not classify the operation as failed.
|
|
63
|
+
|
|
64
|
+
## Lease, database, and administration errors
|
|
65
|
+
|
|
66
|
+
- `LostActivation` means the worker no longer owns the commit fence. The
|
|
67
|
+
runtime discards the stale result; application code should not suppress it.
|
|
68
|
+
- `DatabaseDeadlineExceeded` is the adapter-level deadline used to construct
|
|
69
|
+
synchronous timeout behavior.
|
|
70
|
+
- `UnknownDeadLetter` and `UnknownReminder` mean an authorized administration
|
|
71
|
+
lookup found no record.
|
|
72
|
+
- `ReminderNotPaused` means only a completed reminder remains; schedule it
|
|
73
|
+
again through its actor instead of resuming it.
|
|
74
|
+
|
|
75
|
+
Realtime protocol validation uses `TypeError` for malformed subscription or
|
|
76
|
+
browser envelopes. Server delivery failures are isolated to the session and
|
|
77
|
+
reported through instrumentation; one connection cannot fail durable
|
|
78
|
+
broadcast processing for the others.
|
|
79
|
+
|
|
80
|
+
## Retry ownership
|
|
81
|
+
|
|
82
|
+
An ordinary thrown error is retryable until `maxAttempts`. Each failure rolls
|
|
83
|
+
back state and staged work and preserves strict ordering, so later messages do
|
|
84
|
+
not pass the poison message. After terminal failure, the dead letter records
|
|
85
|
+
the arguments, attempts, and error. `runtime.deadLetters.retry(id)` creates one
|
|
86
|
+
linked replacement message; repeating the call returns the same replacement.
|
|
87
|
+
|
|
88
|
+
Effects are different: they execute outside the actor transaction and are at
|
|
89
|
+
least once. Deduplicate external work with the stable `EffectContext.id`.
|