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,170 @@
1
+ # Operations
2
+
3
+ Runtime roles use durable polling as the correctness fallback. The default
4
+ generation-based wake-up adapter interrupts waits for new actor messages,
5
+ effects, reminders, and broadcasts in the same Node process. Notification
6
+ errors are isolated and logged by role and error class without failing the
7
+ committed work. Graceful shutdown stops new claims and allows active turns to
8
+ finish within `shutdownTimeoutMilliseconds`, which defaults to 15 seconds. A
9
+ component still running or stopping at the deadline emits
10
+ `solid_objects.supervisor.component_shutdown_timeout`; the runtime then returns
11
+ without pretending JavaScript code was forcibly terminated. Operators should
12
+ monitor oldest ready work, claimed work, dead letters, effect failures,
13
+ reminder lag, lease loss, process heartbeats, and database contention.
14
+
15
+ `runtime.run()` replaces built-in roles and registered application components
16
+ that return or reject before shutdown. `supervisorRestartDelayMilliseconds`
17
+ defaults to 100 ms and doubles after consecutive run or construction failures,
18
+ capped by `supervisorMaximumRestartDelayMilliseconds` at 10 seconds. Replacement
19
+ and replacement-failure events contain the role class, failure count, and error
20
+ class only. Repeated failure messages are excluded from instrumentation.
21
+
22
+ `maxMessagesPerActivationPass` defaults to 50 and
23
+ `maxActivationDurationMilliseconds` defaults to 5 seconds. A pass yields when
24
+ either budget is exhausted. Lower values improve fairness when a few actor
25
+ identities stay continuously busy; higher values reduce claim overhead for
26
+ isolated backlogs. `solid_objects.activation.yielded` reports the actor
27
+ identity, turns processed, and remaining due membership count.
28
+
29
+ `claimScanLimit` defaults to 100. Global claims inspect a bounded ordered set of
30
+ actor identities and continue after a lost lease race, preserving worker
31
+ parallelism without an unbounded scan.
32
+
33
+ Workers retain a hydrated actor and its fenced lease for
34
+ `idleDeactivationTimeoutMilliseconds`, which defaults to 30 seconds. Idle
35
+ leases renew at `leaseRenewalIntervalMilliseconds`; the worker polling cadence
36
+ is capped at that interval while any activation may be cached. Fairness yield,
37
+ lease loss, timeout, and shutdown release the lease. `runUntilIdle()` and the
38
+ runtime's synchronous caller release before returning because they are no
39
+ longer polling.
40
+
41
+ Actors can override protected `onActivate()` and `onDeactivate()` methods for
42
+ nondurable, process-local resources. Either hook may be asynchronous. Hook code
43
+ runs under the application-write guard, and `onDeactivate()` is best effort:
44
+ it may not run after a crash, cannot establish a correctness guarantee, and a
45
+ failure is logged without preventing lease release.
46
+
47
+ `runtime.processes.all()` returns administration-authorized immutable process
48
+ metadata with hostname, host process ID, Node and Solid Objects versions, and a
49
+ current `stale` flag. Graceful shutdown first persists `draining` with a
50
+ `shutdownRequestedAt` timestamp, then deactivates owned actors and atomically
51
+ releases every role claim before persisting `stopped`. `cleanup()` reauthorizes
52
+ separately and performs the same release for stale running or draining
53
+ processes. The cleanup count is instrumented; application payloads are not.
54
+
55
+ Committed calls and `message.wait()` apply `timeoutMilliseconds` to the entire
56
+ durable wait, beginning before enqueue or message lookup. Adapter deadlines
57
+ bound serialized SQLite access and lock waits, PostgreSQL pool acquisition,
58
+ statements, and locks, and MySQL pool acquisition, queries, and transaction
59
+ lock waits. A `SyncEnqueueTimeout` means the enqueue transaction did not commit
60
+ and there is no durable message to recover. Once enqueue commits, a
61
+ `SyncTimeout` includes `waitingOn`, activation and process metadata, an earlier
62
+ blocking message when present, and the original `messageReference`. Database
63
+ contention that prevents inspection is reported as `databaseContention`. The
64
+ durable message continues after an ordinary wait timeout, so callers can store
65
+ that reference or await `error.messageReference.wait()` later. Timeout
66
+ instrumentation excludes actor arguments, results, and error messages.
67
+
68
+ JavaScript promises already running in the process are cooperative and are not
69
+ forcefully terminated. An actor operation that has started may therefore
70
+ finish after the caller's timeout; durable leases and fenced commits remain the
71
+ correctness boundary.
72
+
73
+ Committed calls and `message.wait()` fail with `SyncInsideTransaction` when
74
+ invoked inside `database.transaction(...)` on the configured Solid Objects
75
+ adapter. The check happens before enqueue for direct calls. This prevents the
76
+ caller from waiting on a pool connection or SQLite access slot that its own
77
+ ambient transaction still holds.
78
+
79
+ Authorized operators can inspect terminal actor failures with
80
+ `runtime.deadLetters.all()` and retry one with `runtime.deadLetters.retry()`.
81
+ Retry is idempotent per dead letter: the record retains the replacement message
82
+ ID and later calls return a reference to that same message.
83
+
84
+ Self-scheduling actors need a low-frequency reconciler because application
85
+ alarms can still be lost. `runtime.reconciliation` provides administration-
86
+ authorized, read-only views for active instances, quiet instances without
87
+ ready work, claimed work, or scheduled reminders, migrated state batches, and
88
+ orphaned actor IDs. Collection reads use a maximum page size of 1,000 and a
89
+ stable cursor.
90
+
91
+ The host application supplies its current owner IDs to `orphaned()` because
92
+ Node applications do not share an Active Record relation abstraction. Send
93
+ every repair through the actor's typed `send` dispatcher, optionally with a
94
+ future `availableAt` to spread large repairs. Never update persisted actor
95
+ state from reconciliation code.
96
+
97
+ The runtime automatically prunes expired message and stopped-process history
98
+ once at startup and every `retentionIntervalMilliseconds`, which defaults to
99
+ one hour. Stale process ownership is recovered independently every
100
+ `deadProcessCleanupIntervalMilliseconds`, which defaults to one minute. Set an
101
+ interval to zero to disable its scheduler. Failed passes emit metadata-only
102
+ events and retry with bounded exponential backoff without stopping other
103
+ runtime roles.
104
+
105
+ Operators can also use `runtime.retention.preview()` and
106
+ `runtime.retention.prune()`. Both calls require administration authorization;
107
+ use preview first and alert on an unexpected count before executing deletion.
108
+ Message history defaults to 30 days with optional per-actor overrides. Stopped
109
+ process history defaults to 7 days. Instance expiration is disabled unless an
110
+ actor type appears in `instanceRetentionByActorType`, and remains an explicit
111
+ operator action because it deletes the entire actor incarnation.
112
+
113
+ Pruning selects and rechecks at most `pruneBatchSize` rows per transaction. It
114
+ preserves ready and claimed messages, dead-letter originals and replacements,
115
+ unfinished effects and broadcasts, scheduled reminders, leased or paused
116
+ instances, and processes that still own a claim or activation. Instance
117
+ expiration removes the entire actor incarnation and all of its retained
118
+ history, so use it only for actor types whose state is safely disposable.
119
+
120
+ `runtime.doctor.run()` returns structured configuration, schema, database,
121
+ authorization-posture, live-role, and durable-round-trip checks. A warning does
122
+ not make the report unhealthy; a failed check does. The round trip targets only
123
+ its probe actor, uses the ordinary lease and fenced commit path, and removes its
124
+ instance and process records afterward. Use `{ roundTrip: "skip" }` when the
125
+ diagnostic must not write.
126
+
127
+ The doctor reports whether each authorization callback was explicitly
128
+ configured and probes all configured policies with a neutral context. It warns
129
+ when every policy denies the probe, a policy needs unavailable application
130
+ context, or destruction, subscription, or administration allows it. The probe
131
+ does not replace application-specific authorization tests. Do not expose the
132
+ report through an HTTP or CLI surface without applying the host application's
133
+ own administration policy.
134
+
135
+ The packaged `solid-objects` executable loads a configured runtime exported as
136
+ default or `runtime` from `solid-objects.config.js`, or from `--config PATH`.
137
+ It provides `start`, `doctor`, `status`, `cleanup`, `dead-letters`,
138
+ `retry-dead-letter`, `reminders`, `resume-reminder`, and `prune`. Output is JSON,
139
+ and destructive retention requires `prune TARGET --execute`; the unqualified
140
+ command is a preview. Administrative commands consistently pass
141
+ `{ source: "cli" }` through the ordinary policy boundary.
142
+
143
+ Tests can use `runtime.testing.drain()` to process reminders, actor turns,
144
+ effects, actor callbacks, and broadcasts in deterministic passes. The helper
145
+ stops every temporary runner even when a handler raises. `reset()` must only run
146
+ while the supervised runtime is stopped; it explicitly clears child tables,
147
+ instances, and process records and replaces the cached synchronous caller.
148
+
149
+ The optional `instrumentation` callback receives immutable events with the
150
+ `solid_objects.` prefix. Core events cover runtime lifecycle, message
151
+ enqueue/start/completion/rejection/failure, activation loss, commit actions,
152
+ effects, reminders, broadcasts, dead letters, worker setup failures, actor
153
+ destruction, and retention pruning. Failed realtime delivery emits
154
+ `subscription.delivery_failed` and
155
+ removes that session's actor registration without retrying application socket
156
+ code. Event attributes are restricted to identities, operation or handler
157
+ names, delivery mode, sequence, attempt, outcome, counts, durations, and error
158
+ class names. The runtime catches sink exceptions and reports only the failed
159
+ event name and sink error class through the configured logger.
160
+
161
+ `runtime.reminders.all()` provides administration-authorized, cursor-paginated
162
+ alarm metadata filtered by actor type or status. Arguments and error messages
163
+ remain private. `resume()` moves a paused reminder back to `scheduled`, clears
164
+ its claim and stored error, and optionally assigns a new run time. Repeating
165
+ resume on an already scheduled reminder is a no-op; completed reminders cannot
166
+ be resumed.
167
+
168
+ Scheduling an existing actor operation for a different time emits
169
+ `solid_objects.reminder.replaced` after the fenced actor commit. Scheduling it
170
+ for the same time emits nothing.
package/docs/parity.md ADDED
@@ -0,0 +1,94 @@
1
+ # Ruby parity ledger
2
+
3
+ This ledger tracks spiritual feature parity with the Ruby `solid_objects` gem.
4
+ Parity means preserving a capability and its correctness or security boundary,
5
+ not copying a Rails API into Node.
6
+
7
+ Reference: Ruby `solid_objects` 0.12.0 at commit `a01b6f5`.
8
+
9
+ The Node `0.12.0` implementation has spiritual parity with that reference. Its
10
+ relational runtime, correctness boundaries, administration, diagnostics,
11
+ realtime projections, browser behavior, and supported adapters have native
12
+ equivalents. Rails engine and rendering surfaces are intentionally replaced by
13
+ transport- and framework-neutral JavaScript APIs. The partial guard row and the
14
+ shared planned result-lookup row below are explicit scope boundaries, not
15
+ missing Ruby capabilities.
16
+
17
+ ## Status vocabulary
18
+
19
+ - **Native**: the TypeScript runtime provides the capability in a Node-native
20
+ shape.
21
+ - **Partial**: the core exists, but an important Ruby guarantee or operational
22
+ surface is missing.
23
+ - **Planned**: no defensible equivalent exists yet.
24
+ - **Not applicable**: the Ruby feature belongs specifically to Rails, Active
25
+ Record, Action Cable, Turbo, or Ruby language constraints. The equivalent
26
+ Node capability is tracked separately when one is useful.
27
+
28
+ ## Runtime and correctness
29
+
30
+ | Capability | Status | TypeScript shape or remaining work |
31
+ | ------------------------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
32
+ | Actor registry, durable identity, JSON state, and adjacent state migrations | Native | Ordinary classes, static actor types, inferred state, explicit migrations, and isolated runtime context across every actor-instance callback. |
33
+ | Fluent committed calls and background delivery | Native | `await reference.operation()` and `reference.send.operation()`. |
34
+ | Ordered mailbox, sequence allocation, idempotency, retries, dead letters, leases, renewal, and fenced commits | Native | Relational ready/claimed membership tables, distinct generated request IDs and caller idempotency keys, durable history, and adapter-appropriate sequence locking. |
35
+ | Domain rejection and strict poison ordering | Native | Rejections roll back without retry; retryable failures block later operations until completion or dead-lettering. |
36
+ | Bounded activation passes and hot-actor fairness | Native | Configurable turn-count and elapsed-time budgets bound each pass, then move only that actor's already-due memberships behind actors already waiting. |
37
+ | Bounded claim candidate scan | Native | A configurable ordered scan continues to another ready actor when a worker loses the first candidate's lease race. |
38
+ | Idle activation cache | Native | Long-running workers retain hydrated actors under renewable fenced leases, restore public state after failed turns, and release on timeout, fairness yield, lease loss, or shutdown. |
39
+ | Transactional effects and outcome operations | Native | At-least-once handlers receive immutable stable effect, attempt, source-message, and actor identity; success and failure return through actor operations. |
40
+ | Actor-to-actor delivery | Native | `sendTo(reference).operation()` stages delivery in the source actor commit. |
41
+ | One-shot and recurring reminders | Native | Scheduling, replacement events, catch-up policy, stale-claim recovery, pausing, authorized inspection, and idempotent resume are implemented. |
42
+ | Same-database commit actions | Native | Registered actions receive source-message identity, mailbox sequence, activation generation, and the fenced transaction connection. |
43
+ | Ambient transaction rejection | Native | Committed calls and message waits fail before blocking when the current async context already owns a transaction on the Solid Objects adapter. |
44
+ | Direct application-write isolation during actor code | Partial | `guardApplicationDatabase()` fails closed for operations, projections, migrations, and commit actions; only the supplied fenced commit-action connection may write. Unwrapped clients cannot be intercepted. |
45
+ | Committed snapshots | Native | `snapshot()` returns authorized persisted fields and inferred getters from one read-only committed state image; realtime replay reads explicit observables without mailbox history. |
46
+ | Actor destruction and incarnation fencing | Native | Authorized cascading deletion creates a fresh instance ID on recreation; an authorized waiter receives `ActorDestroyed` when that incarnation disappears. |
47
+ | Result recovery and sync timeout diagnostics | Native | Status, result, and wait reauthorize the stored operation; terminal failure raises structured `MessageFailed`; whole-call adapter deadlines distinguish enqueue, wait, database, activation, and mailbox blockers. |
48
+ | Result lookup by request ID | Planned | This is also an open Ruby roadmap item and will be implemented in both runtimes when its authorization shape is settled. |
49
+
50
+ ## Operations
51
+
52
+ | Capability | Status | TypeScript shape or remaining work |
53
+ | ----------------------------------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
54
+ | Process registration, heartbeats, stale claim recovery, and graceful shutdown | Native | Runtime roles persist host, PID, runtime versions, draining and stopped transitions, cooperative cancellation, and a bounded shutdown deadline; cleanup recovers stale claims. |
55
+ | Failed-role replacement | Native | Built-in and registered roles are rebuilt through their factories with capped backoff; shutdown is the terminal replacement boundary. |
56
+ | Additional supervised components | Native | `registerComponent()` builds, validates, runs, and stops application components with the runtime. |
57
+ | Dead-letter inspection and retry | Native | `runtime.deadLetters` provides deny-by-default immutable inspection and idempotent durable retry linkage. |
58
+ | Reconciliation reads | Native | Authorized cursor pages cover active, quiet, and orphaned instances; bounded state batches are migrated and deeply frozen. |
59
+ | Message, process, and opt-in instance retention | Native | Supervised scheduling bounds message and process growth; authorized manual APIs add preview and keep destructive instance expiration explicit. |
60
+ | Doctor and schema verification | Native | Structured checks cover configuration, schema/version shape, adapter server versions, neutral-context policy probes, live roles, and a targeted round trip. |
61
+ | CLI | Native | The packaged executable loads an application runtime and exposes start, diagnostics, processes, dead letters, reminders, and explicit retention pruning as JSON. |
62
+ | Structured instrumentation | Native | An isolated transport-neutral sink emits immutable lifecycle metadata and structurally excludes application payloads. |
63
+ | Public test helper | Native | `runtime.testing` provides role-selective deterministic draining and dependency-ordered reset without relying on cascades. |
64
+
65
+ ## Databases and wake-up
66
+
67
+ | Capability | Status | TypeScript shape or remaining work |
68
+ | ------------------------ | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
69
+ | SQLite | Native | Uses built-in `node:sqlite`, serialized process-local access, bounded transient writer retries, foreign keys, strict tables, database time, and deadline-bounded access and lock waits. |
70
+ | PostgreSQL | Native | Optional `pg` 8.23 peer, bounded pooling, 64-bit schema, row-locked sequences, server checks, and deadline-bounded pool, statement, and lock waits. |
71
+ | MySQL | Native | Optional `mysql2` 3.23 peer, bounded pooling, InnoDB schema, row-locked sequences, scoped deadlock retry, and deadline-bounded pool, query, and lock waits. |
72
+ | Durable polling fallback | Native | Every role progresses without a notification service. |
73
+ | In-process wake-up | Native | A generation-based default adapter prevents claim-to-wait signal loss; commits wake role-specific waiters and polling remains the fallback. |
74
+ | PostgreSQL wake-up | Native | `database.wakeUp()` uses one dedicated event-driven client, role-specific `LISTEN/NOTIFY`, generation fencing, reconnectable listeners, and durable polling fallback. |
75
+ | Redis wake-up | Native | An optional `redis` peer provides role-specific Pub/Sub over separate lazy publisher/subscriber connections, with bounded failures and durable polling fallback. |
76
+
77
+ ## Realtime and browser behavior
78
+
79
+ | Capability | Status | TypeScript shape or remaining work |
80
+ | ------------------------------------------------------------ | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
81
+ | Explicit observable projection and durable invalidations | Native | `observables()` is opt-in; only changed projected values enter the broadcast outbox. |
82
+ | Action Cable channels and signed stream names | Not applicable | `runtime.realtime` provides authenticated transport-neutral sessions; the host owns its HTTP/WebSocket server and authentication. |
83
+ | Authorized subscriptions | Native | Each request is denied by default and authorized before actor lookup; sessions replay committed observables and fence ordered durable revisions. Multi-process hosts explicitly bridge their shared transport. |
84
+ | Turbo scalar replacement | Not applicable | The browser client exposes invalidations to application rendering code. Framework adapters can be separate packages. |
85
+ | Keyed component refresh, morph/replace, and batch coalescing | Native | A typed framework-neutral registry selects explicit dependencies, coalesces batch requests, aborts superseded work, fences each target, and delegates synchronous application strategy to the host. |
86
+ | Personalized payload broadcasts | Native | Static typed projections run against committed state under each fresh subscriber context, reauthorize as queries, isolate failures, and carry independent revision fences. |
87
+ | Real-browser compatibility suite | Native | Playwright exercises subscription replay over native WebSocket, incarnation/revision fences, payload delivery, component batching, and cancellation in Chromium. |
88
+
89
+ ## Rails-specific surfaces
90
+
91
+ The Rails engine, generators, Active Record models/controllers, ERB helpers,
92
+ Turbo renderer, and Action Cable channel are not copied into this package.
93
+ Their underlying runtime, administration, authorization, and realtime
94
+ capabilities are represented above by Node-native APIs and protocols.
@@ -0,0 +1,45 @@
1
+ # Releasing
2
+
3
+ Publishing is driven by version tags and npm Trusted Publishing. GitHub Actions
4
+ uses a short-lived OpenID Connect identity, so the repository does not need an
5
+ npm token.
6
+
7
+ ## npm trusted publisher
8
+
9
+ The `solid-objects` package must authorize this GitHub Actions identity:
10
+
11
+ - Organization or user: `cardmagic`
12
+ - Repository: `solid-objects-js`
13
+ - Workflow filename: `ci.yml`
14
+ - Environment: none
15
+ - Allowed action: `npm publish`
16
+
17
+ The relationship can be created while authenticated as a package owner:
18
+
19
+ ```shell
20
+ npm trust github solid-objects \
21
+ --repo cardmagic/solid-objects-js \
22
+ --file ci.yml \
23
+ --allow-publish \
24
+ --yes
25
+ ```
26
+
27
+ ## Release procedure
28
+
29
+ 1. Update the version in `package.json` and `pnpm-lock.yaml` and move the
30
+ release notes out of the Unreleased section in `CHANGELOG.md`.
31
+ 2. Run `pnpm run format:check`, `pnpm run check`, `pnpm run test:coverage`,
32
+ `pnpm run build`, `pnpm run pack:check`, and
33
+ `pnpm audit --audit-level=high`.
34
+ 3. Commit and push `main`.
35
+ 4. Create and push an annotated tag matching the package version:
36
+
37
+ ```shell
38
+ git tag -a v0.13.0 -m "Version 0.13.0"
39
+ git push origin v0.13.0
40
+ ```
41
+
42
+ The tag runs the complete CI matrix. The publish job starts only after every
43
+ quality, database, Redis, and browser job succeeds. It rejects tags that do not
44
+ match `package.json`, safely skips versions already present in npm, and
45
+ publishes new versions with npm provenance.
@@ -0,0 +1,162 @@
1
+ # State and lifecycle
2
+
3
+ An actor class is both the persisted state definition and the typed operation
4
+ surface. Registration constructs the class to discover its enumerable public
5
+ fields, then walks its prototype chain to discover methods and getters.
6
+
7
+ ## Persisted state and operations
8
+
9
+ - Enumerable public instance fields are persisted JSON state.
10
+ - Public methods are ordered operations and accept zero arguments or one
11
+ object argument.
12
+ - Public fields and getters are ordered, read-only queries on a reference.
13
+ - `#private` fields are process-local and survive only while an activation is
14
+ cached.
15
+ - Actor arguments, state, results, observables, and payloads must be
16
+ JSON-compatible and fit their configured byte limits.
17
+ - Operation, field, and getter names must not collide with the reference API.
18
+
19
+ The constructor must establish every persisted field and must not depend on
20
+ external state. Solid Objects invokes it while validating the class, creating
21
+ defaults, hydrating state, and projecting a snapshot.
22
+
23
+ ## State migrations
24
+
25
+ Every actor starts at state version 1. Increase `stateVersion` when existing
26
+ rows need a structural transformation and retain an adjacent migration for
27
+ every version:
28
+
29
+ ```typescript
30
+ import { Actor, type JsonObject } from "solid-objects"
31
+
32
+ class Document extends Actor {
33
+ static override readonly actorType = "Document"
34
+ static override readonly stateVersion = 3
35
+ static override readonly migrations = [
36
+ {
37
+ from: 1,
38
+ to: 2,
39
+ migrate: (state: JsonObject): JsonObject => ({
40
+ title: state.name ?? "Untitled",
41
+ }),
42
+ },
43
+ {
44
+ from: 2,
45
+ to: 3,
46
+ migrate: (state: JsonObject): JsonObject => ({
47
+ ...state,
48
+ archived: false,
49
+ }),
50
+ },
51
+ ]
52
+
53
+ title = "Untitled"
54
+ archived = false
55
+ }
56
+ ```
57
+
58
+ Migrations run synchronously in ascending order when an actor is hydrated for
59
+ an operation, snapshot, reconciliation state read, observable replay, or
60
+ personalized payload. Each receives a detached JSON object and must return a
61
+ JSON object. After the chain completes, any new persisted field missing from
62
+ the result receives its constructor default. Migrated state is persisted only
63
+ by the next successful fenced operation commit; read-only projections do not
64
+ rewrite the row.
65
+
66
+ A migration must be deterministic and side-effect free. It cannot perform
67
+ asynchronous work or write through a database wrapped by
68
+ `guardApplicationDatabase()`. Missing, duplicate, or non-adjacent migrations,
69
+ a failing migration, or state newer than the running code raises
70
+ `StateMigrationError`.
71
+
72
+ For a destructive shape change, use expand/contract deployment: first deploy
73
+ readers that understand both shapes, then deploy the migration, wait for
74
+ operational evidence that actors have advanced, and only then remove old-shape
75
+ support. If old code cannot understand the new shape, drain it before new code
76
+ can persist the migration. Keep old migration steps so actors idle for several
77
+ releases can still advance one version at a time.
78
+
79
+ Actor state migration is separate from `runtime.install()`. The latter applies
80
+ the package's relational schema migrations; it does not eagerly rewrite actor
81
+ state.
82
+
83
+ ## Activation lifecycle
84
+
85
+ Override `onActivate()` and `onDeactivate()` for process-local resources:
86
+
87
+ ```typescript
88
+ class Device extends Actor {
89
+ static override readonly actorType = "Device"
90
+
91
+ #connection: DeviceConnection | undefined
92
+
93
+ protected override async onActivate(): Promise<void> {
94
+ this.#connection = await connectToDevice(this.actorId)
95
+ }
96
+
97
+ protected override async onDeactivate(): Promise<void> {
98
+ await this.#connection?.close()
99
+ this.#connection = undefined
100
+ }
101
+ }
102
+ ```
103
+
104
+ Activation happens after state migration and before an operation attempt
105
+ starts. A setup failure restores the claimed message without consuming an
106
+ attempt. A long-running worker may reuse the hydrated actor and its private
107
+ fields until `idleDeactivationTimeoutMilliseconds`, fairness yield, lease loss,
108
+ or shutdown.
109
+
110
+ Lifecycle hooks have no `currentMessage` and run under the application-write
111
+ guard. Intents staged by `onActivate()` join the first turn's commit; use that
112
+ only when reactivation should repeat the intent safely. `onDeactivate()` is
113
+ best effort, its staged intents are discarded, and it may not run after a
114
+ crash, so it cannot establish correctness.
115
+
116
+ ## Domain rejection and failure
117
+
118
+ Call `this.reject(code, { message, details })` when an operation is valid but
119
+ the domain refuses it. Codes contain lowercase letters, digits, and
120
+ underscores. The turn rolls back, staged work is discarded, the message becomes
121
+ `rejected`, and later mailbox work remains eligible. The caller receives
122
+ `Rejected` with the code, frozen JSON details, and durable message ID.
123
+
124
+ Throwing another error rolls the turn back and schedules a retry according to
125
+ `maxAttempts` and `retryDelayMilliseconds`. Throw a custom subclass of
126
+ `NonRetryableError` only when retry cannot help; the message goes directly to a
127
+ terminal dead letter. See [Errors and recovery](errors-and-recovery.md).
128
+
129
+ ## Queries and snapshots
130
+
131
+ Reading a public field or getter through an actor reference is an ordered
132
+ mailbox query:
133
+
134
+ ```typescript
135
+ const count = await Counter.ref("primary").count
136
+ ```
137
+
138
+ Use `snapshot()` when the read need not wait behind mailbox work:
139
+
140
+ ```typescript
141
+ const snapshot = await Counter.ref("primary").snapshot({
142
+ authorizationContext: currentUser,
143
+ })
144
+ ```
145
+
146
+ Both paths call `authorizeQuery`. A snapshot hydrates one committed state
147
+ image, returns all fields and getters as a deeply frozen value, and never adds
148
+ a message to history. A getter that mutates state or stages work raises
149
+ `QueryMutatedState` for the whole snapshot.
150
+
151
+ ## Destruction and reincarnation
152
+
153
+ `reference.destroy()` calls the separate `authorizeDestroy` policy, deletes
154
+ the current actor incarnation and every owned row, and returns `true` when an
155
+ incarnation existed. Repeating it returns `false`.
156
+
157
+ An actor cannot destroy another actor from inside a turn. A later message to
158
+ the same actor type and ID creates a new instance ID and revision sequence.
159
+ Old activation leases cannot commit to the new incarnation, and an authorized
160
+ caller waiting on the deleted incarnation receives `ActorDestroyed`. An
161
+ external effect or broadcast already executing cannot be recalled, but its
162
+ stale completion cannot recreate or enqueue a callback to the deleted source.
package/package.json ADDED
@@ -0,0 +1,100 @@
1
+ {
2
+ "name": "solid-objects",
3
+ "version": "0.12.0",
4
+ "description": "Durable, addressable objects for Node.js",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Lucas Carlson",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/cardmagic/solid-objects-js.git"
11
+ },
12
+ "engines": {
13
+ "node": ">=24.15.0"
14
+ },
15
+ "packageManager": "pnpm@11.11.0",
16
+ "bin": {
17
+ "solid-objects": "dist/executable.js"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "docs",
22
+ "README.md",
23
+ "CHANGELOG.md",
24
+ "MIT-LICENSE"
25
+ ],
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js"
30
+ },
31
+ "./database/sqlite": {
32
+ "types": "./dist/database/sqlite.d.ts",
33
+ "import": "./dist/database/sqlite.js"
34
+ },
35
+ "./database/postgresql": {
36
+ "types": "./dist/database/postgresql.d.ts",
37
+ "import": "./dist/database/postgresql.js"
38
+ },
39
+ "./database/mysql": {
40
+ "types": "./dist/database/mysql.d.ts",
41
+ "import": "./dist/database/mysql.js"
42
+ },
43
+ "./wake-up/redis": {
44
+ "types": "./dist/wake-up/redis.d.ts",
45
+ "import": "./dist/wake-up/redis.js"
46
+ },
47
+ "./browser": {
48
+ "types": "./dist/browser/index.d.ts",
49
+ "import": "./dist/browser/index.js"
50
+ }
51
+ },
52
+ "scripts": {
53
+ "build": "pnpm run clean && tsc -p tsconfig.build.json && node scripts/prepare-executable.mjs",
54
+ "check": "pnpm run check:parameters && pnpm run check:documentation && tsc -p tsconfig.json --noEmit",
55
+ "check:documentation": "node scripts/check-documentation.mjs",
56
+ "check:parameters": "node scripts/check-parameter-style.mjs",
57
+ "clean": "node scripts/clean.mjs",
58
+ "format": "prettier --write .",
59
+ "format:check": "prettier --check .",
60
+ "test": "vitest run",
61
+ "test:browser": "pnpm run build && playwright test",
62
+ "test:coverage": "vitest run --coverage",
63
+ "test:postgresql": "vitest run test/postgresql.test.ts",
64
+ "test:mysql": "vitest run test/mysql.test.ts",
65
+ "test:redis": "vitest run test/redis-wake-up.test.ts",
66
+ "test:watch": "vitest",
67
+ "pack:check": "pnpm pack --dry-run && node scripts/check-package.mjs",
68
+ "prepack": "pnpm run build"
69
+ },
70
+ "devDependencies": {
71
+ "@playwright/test": "^1.62.1",
72
+ "@types/node": "^24.0.0",
73
+ "@types/pg": "^8.21.0",
74
+ "@types/ws": "^8.18.1",
75
+ "@vitest/coverage-v8": "^4.1.10",
76
+ "mysql2": "^3.23.3",
77
+ "pg": "^8.23.0",
78
+ "prettier": "^3.9.6",
79
+ "redis": "^6.2.1",
80
+ "typescript": "^5.9.0",
81
+ "vitest": "^4.1.10",
82
+ "ws": "^8.21.3"
83
+ },
84
+ "peerDependencies": {
85
+ "mysql2": "^3.23.3",
86
+ "pg": "^8.23.0",
87
+ "redis": "^6.2.1"
88
+ },
89
+ "peerDependenciesMeta": {
90
+ "mysql2": {
91
+ "optional": true
92
+ },
93
+ "pg": {
94
+ "optional": true
95
+ },
96
+ "redis": {
97
+ "optional": true
98
+ }
99
+ }
100
+ }