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.
Files changed (187) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/MIT-LICENSE +19 -0
  3. package/README.md +958 -0
  4. package/dist/actor.d.ts +81 -0
  5. package/dist/actor.d.ts.map +1 -0
  6. package/dist/actor.js +141 -0
  7. package/dist/actor.js.map +1 -0
  8. package/dist/application-database.d.ts +3 -0
  9. package/dist/application-database.d.ts.map +1 -0
  10. package/dist/application-database.js +59 -0
  11. package/dist/application-database.js.map +1 -0
  12. package/dist/broadcast-worker.d.ts +18 -0
  13. package/dist/broadcast-worker.d.ts.map +1 -0
  14. package/dist/broadcast-worker.js +74 -0
  15. package/dist/broadcast-worker.js.map +1 -0
  16. package/dist/browser/components.d.ts +64 -0
  17. package/dist/browser/components.d.ts.map +1 -0
  18. package/dist/browser/components.js +294 -0
  19. package/dist/browser/components.js.map +1 -0
  20. package/dist/browser/index.d.ts +46 -0
  21. package/dist/browser/index.d.ts.map +1 -0
  22. package/dist/browser/index.js +207 -0
  23. package/dist/browser/index.js.map +1 -0
  24. package/dist/cli.d.ts +9 -0
  25. package/dist/cli.d.ts.map +1 -0
  26. package/dist/cli.js +216 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/configuration.d.ts +96 -0
  29. package/dist/configuration.d.ts.map +1 -0
  30. package/dist/configuration.js +169 -0
  31. package/dist/configuration.js.map +1 -0
  32. package/dist/context.d.ts +20 -0
  33. package/dist/context.d.ts.map +1 -0
  34. package/dist/context.js +24 -0
  35. package/dist/context.js.map +1 -0
  36. package/dist/database/deadline.d.ts +9 -0
  37. package/dist/database/deadline.d.ts.map +1 -0
  38. package/dist/database/deadline.js +49 -0
  39. package/dist/database/deadline.js.map +1 -0
  40. package/dist/database/mysql.d.ts +21 -0
  41. package/dist/database/mysql.d.ts.map +1 -0
  42. package/dist/database/mysql.js +192 -0
  43. package/dist/database/mysql.js.map +1 -0
  44. package/dist/database/postgresql-sql.d.ts +2 -0
  45. package/dist/database/postgresql-sql.d.ts.map +1 -0
  46. package/dist/database/postgresql-sql.js +102 -0
  47. package/dist/database/postgresql-sql.js.map +1 -0
  48. package/dist/database/postgresql.d.ts +31 -0
  49. package/dist/database/postgresql.d.ts.map +1 -0
  50. package/dist/database/postgresql.js +172 -0
  51. package/dist/database/postgresql.js.map +1 -0
  52. package/dist/database/sqlite.d.ts +27 -0
  53. package/dist/database/sqlite.d.ts.map +1 -0
  54. package/dist/database/sqlite.js +190 -0
  55. package/dist/database/sqlite.js.map +1 -0
  56. package/dist/database/transaction-context.d.ts +3 -0
  57. package/dist/database/transaction-context.d.ts.map +1 -0
  58. package/dist/database/transaction-context.js +19 -0
  59. package/dist/database/transaction-context.js.map +1 -0
  60. package/dist/database/types.d.ts +20 -0
  61. package/dist/database/types.d.ts.map +1 -0
  62. package/dist/database/types.js +2 -0
  63. package/dist/database/types.js.map +1 -0
  64. package/dist/dead-letters.d.ts +23 -0
  65. package/dist/dead-letters.d.ts.map +1 -0
  66. package/dist/dead-letters.js +13 -0
  67. package/dist/dead-letters.js.map +1 -0
  68. package/dist/default-runtime.d.ts +5 -0
  69. package/dist/default-runtime.d.ts.map +1 -0
  70. package/dist/default-runtime.js +14 -0
  71. package/dist/default-runtime.js.map +1 -0
  72. package/dist/definition.d.ts +32 -0
  73. package/dist/definition.d.ts.map +1 -0
  74. package/dist/definition.js +173 -0
  75. package/dist/definition.js.map +1 -0
  76. package/dist/doctor.d.ts +32 -0
  77. package/dist/doctor.d.ts.map +1 -0
  78. package/dist/doctor.js +500 -0
  79. package/dist/doctor.js.map +1 -0
  80. package/dist/effect-worker.d.ts +18 -0
  81. package/dist/effect-worker.d.ts.map +1 -0
  82. package/dist/effect-worker.js +74 -0
  83. package/dist/effect-worker.js.map +1 -0
  84. package/dist/errors.d.ts +145 -0
  85. package/dist/errors.d.ts.map +1 -0
  86. package/dist/errors.js +120 -0
  87. package/dist/errors.js.map +1 -0
  88. package/dist/executable.d.ts +3 -0
  89. package/dist/executable.d.ts.map +1 -0
  90. package/dist/executable.js +12 -0
  91. package/dist/executable.js.map +1 -0
  92. package/dist/index.d.ts +24 -0
  93. package/dist/index.d.ts.map +1 -0
  94. package/dist/index.js +21 -0
  95. package/dist/index.js.map +1 -0
  96. package/dist/maintenance-scheduler.d.ts +17 -0
  97. package/dist/maintenance-scheduler.d.ts.map +1 -0
  98. package/dist/maintenance-scheduler.js +41 -0
  99. package/dist/maintenance-scheduler.js.map +1 -0
  100. package/dist/process-administration.d.ts +30 -0
  101. package/dist/process-administration.d.ts.map +1 -0
  102. package/dist/process-administration.js +13 -0
  103. package/dist/process-administration.js.map +1 -0
  104. package/dist/realtime.d.ts +33 -0
  105. package/dist/realtime.d.ts.map +1 -0
  106. package/dist/realtime.js +238 -0
  107. package/dist/realtime.js.map +1 -0
  108. package/dist/reconciliation.d.ts +41 -0
  109. package/dist/reconciliation.d.ts.map +1 -0
  110. package/dist/reconciliation.js +19 -0
  111. package/dist/reconciliation.js.map +1 -0
  112. package/dist/records.d.ts +137 -0
  113. package/dist/records.d.ts.map +1 -0
  114. package/dist/records.js +2 -0
  115. package/dist/records.js.map +1 -0
  116. package/dist/reference.d.ts +97 -0
  117. package/dist/reference.d.ts.map +1 -0
  118. package/dist/reference.js +157 -0
  119. package/dist/reference.js.map +1 -0
  120. package/dist/reminder-administration.d.ts +35 -0
  121. package/dist/reminder-administration.d.ts.map +1 -0
  122. package/dist/reminder-administration.js +13 -0
  123. package/dist/reminder-administration.js.map +1 -0
  124. package/dist/reminder-scheduler.d.ts +18 -0
  125. package/dist/reminder-scheduler.d.ts.map +1 -0
  126. package/dist/reminder-scheduler.js +88 -0
  127. package/dist/reminder-scheduler.js.map +1 -0
  128. package/dist/repository.d.ts +142 -0
  129. package/dist/repository.d.ts.map +1 -0
  130. package/dist/repository.js +1237 -0
  131. package/dist/repository.js.map +1 -0
  132. package/dist/retention.d.ts +17 -0
  133. package/dist/retention.d.ts.map +1 -0
  134. package/dist/retention.js +13 -0
  135. package/dist/retention.js.map +1 -0
  136. package/dist/runtime.d.ts +140 -0
  137. package/dist/runtime.d.ts.map +1 -0
  138. package/dist/runtime.js +1737 -0
  139. package/dist/runtime.js.map +1 -0
  140. package/dist/schema.d.ts +8 -0
  141. package/dist/schema.d.ts.map +1 -0
  142. package/dist/schema.js +260 -0
  143. package/dist/schema.js.map +1 -0
  144. package/dist/serialization.d.ts +11 -0
  145. package/dist/serialization.d.ts.map +1 -0
  146. package/dist/serialization.js +82 -0
  147. package/dist/serialization.js.map +1 -0
  148. package/dist/test-helper.d.ts +15 -0
  149. package/dist/test-helper.d.ts.map +1 -0
  150. package/dist/test-helper.js +69 -0
  151. package/dist/test-helper.js.map +1 -0
  152. package/dist/types.d.ts +71 -0
  153. package/dist/types.d.ts.map +1 -0
  154. package/dist/types.js +2 -0
  155. package/dist/types.js.map +1 -0
  156. package/dist/version.d.ts +2 -0
  157. package/dist/version.d.ts.map +1 -0
  158. package/dist/version.js +2 -0
  159. package/dist/version.js.map +1 -0
  160. package/dist/wake-up/postgresql.d.ts +42 -0
  161. package/dist/wake-up/postgresql.d.ts.map +1 -0
  162. package/dist/wake-up/postgresql.js +234 -0
  163. package/dist/wake-up/postgresql.js.map +1 -0
  164. package/dist/wake-up/redis.d.ts +35 -0
  165. package/dist/wake-up/redis.d.ts.map +1 -0
  166. package/dist/wake-up/redis.js +151 -0
  167. package/dist/wake-up/redis.js.map +1 -0
  168. package/dist/wake-up.d.ts +24 -0
  169. package/dist/wake-up.d.ts.map +1 -0
  170. package/dist/wake-up.js +66 -0
  171. package/dist/wake-up.js.map +1 -0
  172. package/dist/worker.d.ts +36 -0
  173. package/dist/worker.d.ts.map +1 -0
  174. package/dist/worker.js +281 -0
  175. package/dist/worker.js.map +1 -0
  176. package/docs/api.md +197 -0
  177. package/docs/architecture.md +113 -0
  178. package/docs/authorization.md +31 -0
  179. package/docs/browser-protocol.md +93 -0
  180. package/docs/configuration.md +168 -0
  181. package/docs/correctness.md +53 -0
  182. package/docs/errors-and-recovery.md +89 -0
  183. package/docs/operations.md +170 -0
  184. package/docs/parity.md +94 -0
  185. package/docs/releasing.md +45 -0
  186. package/docs/state-and-lifecycle.md +162 -0
  187. 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`.