solid-objects 0.12.1 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -1
- package/README.md +284 -950
- package/dist/actor.d.ts +12 -0
- package/dist/actor.d.ts.map +1 -1
- package/dist/actor.js +25 -2
- package/dist/actor.js.map +1 -1
- package/dist/broadcast-worker.d.ts +2 -0
- package/dist/broadcast-worker.d.ts.map +1 -1
- package/dist/broadcast-worker.js +25 -5
- package/dist/broadcast-worker.js.map +1 -1
- package/dist/browser/components.d.ts.map +1 -1
- package/dist/browser/components.js +4 -1
- package/dist/browser/components.js.map +1 -1
- package/dist/browser/index.d.ts +1 -0
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +8 -0
- package/dist/browser/index.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +13 -1
- package/dist/cli.js.map +1 -1
- package/dist/configuration.d.ts +3 -0
- package/dist/configuration.d.ts.map +1 -1
- package/dist/configuration.js +3 -0
- package/dist/configuration.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +12 -3
- package/dist/doctor.js.map +1 -1
- package/dist/effect-worker.d.ts +2 -0
- package/dist/effect-worker.d.ts.map +1 -1
- package/dist/effect-worker.js +25 -5
- package/dist/effect-worker.js.map +1 -1
- package/dist/examples/sqlite-quickstart.js +83 -0
- package/dist/examples/sqlite-quickstart.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/polling-backoff.d.ts +23 -0
- package/dist/polling-backoff.d.ts.map +1 -0
- package/dist/polling-backoff.js +33 -0
- package/dist/polling-backoff.js.map +1 -0
- package/dist/records.d.ts +1 -0
- package/dist/records.d.ts.map +1 -1
- package/dist/reminder-scheduler.d.ts +2 -0
- package/dist/reminder-scheduler.d.ts.map +1 -1
- package/dist/reminder-scheduler.js +25 -5
- package/dist/reminder-scheduler.js.map +1 -1
- package/dist/repository.d.ts +5 -1
- package/dist/repository.d.ts.map +1 -1
- package/dist/repository.js +15 -4
- package/dist/repository.js.map +1 -1
- package/dist/runtime.d.ts +4 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +71 -6
- package/dist/runtime.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +15 -4
- package/dist/schema.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/wake-up/postgresql.d.ts.map +1 -1
- package/dist/wake-up/postgresql.js +12 -11
- package/dist/wake-up/postgresql.js.map +1 -1
- package/dist/wake-up.d.ts +1 -1
- package/dist/wake-up.d.ts.map +1 -1
- package/dist/wake-up.js +11 -10
- package/dist/wake-up.js.map +1 -1
- package/dist/web/assets.d.ts +4 -0
- package/dist/web/assets.d.ts.map +1 -0
- package/dist/web/assets.js +11 -0
- package/dist/web/assets.js.map +1 -0
- package/dist/web/index.d.ts +28 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +642 -0
- package/dist/web/index.js.map +1 -0
- package/dist/web/node.d.ts +3 -0
- package/dist/web/node.d.ts.map +1 -0
- package/dist/web/node.js +109 -0
- package/dist/web/node.js.map +1 -0
- package/dist/web/render.d.ts +64 -0
- package/dist/web/render.d.ts.map +1 -0
- package/dist/web/render.js +331 -0
- package/dist/web/render.js.map +1 -0
- package/dist/web/store.d.ts +65 -0
- package/dist/web/store.d.ts.map +1 -0
- package/dist/web/store.js +303 -0
- package/dist/web/store.js.map +1 -0
- package/dist/web/types.d.ts +82 -0
- package/dist/web/types.d.ts.map +1 -0
- package/dist/web/types.js +2 -0
- package/dist/web/types.js.map +1 -0
- package/dist/worker.d.ts +2 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +25 -5
- package/dist/worker.js.map +1 -1
- package/docs/api.md +70 -4
- package/docs/architecture.md +20 -11
- package/docs/authorization.md +12 -3
- package/docs/benchmarks.md +163 -0
- package/docs/browser-protocol.md +15 -8
- package/docs/comparisons.md +36 -0
- package/docs/configuration.md +14 -3
- package/docs/correctness.md +23 -0
- package/docs/dashboard.md +196 -0
- package/docs/fit.md +58 -0
- package/docs/operations.md +25 -7
- package/docs/parity.md +37 -34
- package/docs/releasing.md +6 -4
- package/docs/state-and-lifecycle.md +10 -0
- package/docs/support.md +39 -0
- package/examples/failure-recovery/actor.ts +51 -0
- package/examples/failure-recovery/demo.ts +233 -0
- package/examples/failure-recovery/worker.ts +46 -0
- package/examples/sqlite-quickstart.ts +109 -0
- package/package.json +26 -4
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Operator dashboard
|
|
2
|
+
|
|
3
|
+
`solid-objects/web` is an optional operator interface over the same relational
|
|
4
|
+
tables used by the runtime. It exposes instances and committed state, ready and
|
|
5
|
+
claimed messages, reminders, effects, broadcasts, dead letters, and processes.
|
|
6
|
+
It needs no separate store or agent.
|
|
7
|
+
|
|
8
|
+
The root `solid-objects` entry point does not import the dashboard. A worker
|
|
9
|
+
process that never mounts it does not load its HTTP adapter, renderer, or
|
|
10
|
+
assets.
|
|
11
|
+
|
|
12
|
+
## Fetch mounting
|
|
13
|
+
|
|
14
|
+
`createDashboard()` returns an immutable Fetch handler. Give every request its
|
|
15
|
+
fresh server-side authentication context and the host session that should own
|
|
16
|
+
the CSRF token:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { createDashboard } from "solid-objects/web"
|
|
20
|
+
|
|
21
|
+
const dashboard = createDashboard({
|
|
22
|
+
runtime,
|
|
23
|
+
mountPath: "/solid-objects/dashboard",
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const response = await dashboard.fetch(request, {
|
|
27
|
+
authorizationContext: currentOperator,
|
|
28
|
+
session: {
|
|
29
|
+
read: (key) => session.get(key),
|
|
30
|
+
write: (key, value) => session.set(key, value),
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`mountPath` defaults to `/solid-objects/dashboard`. Pass `/` only when the
|
|
36
|
+
dashboard owns the whole origin.
|
|
37
|
+
|
|
38
|
+
## Access modes
|
|
39
|
+
|
|
40
|
+
`access` defaults to `authorized`. Every route uses the runtime administration
|
|
41
|
+
policy, pages include mutation controls, and the request context must supply a
|
|
42
|
+
session for CSRF state.
|
|
43
|
+
|
|
44
|
+
Use `authorized-read-only` to retain the policy while removing mutation forms
|
|
45
|
+
and rejecting every dashboard POST with 405. Use `public-read-only` for an
|
|
46
|
+
explicitly public demo mount:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
const demo = createDashboard({
|
|
50
|
+
runtime,
|
|
51
|
+
mountPath: "/solid-objects/demo",
|
|
52
|
+
access: "public-read-only",
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
await demo.fetch(request, {})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Public read-only mode skips `authorizeAdministration` and requires neither an
|
|
59
|
+
authorization context nor a session. It still exposes committed state,
|
|
60
|
+
arguments, results, errors, actor identifiers, and operational metadata. Only
|
|
61
|
+
use it with synthetic or otherwise public demo data. All extension GET routes
|
|
62
|
+
are public in this mode, while extension POST routes are also rejected.
|
|
63
|
+
|
|
64
|
+
## Node and Connect mounting
|
|
65
|
+
|
|
66
|
+
`createNodeDashboardHandler()` converts `IncomingMessage` and `ServerResponse`
|
|
67
|
+
to the Fetch contract. Register it after the application's authentication and
|
|
68
|
+
session middleware. A Connect-compatible host may pass `next`; paths outside
|
|
69
|
+
the mount and unknown dashboard paths cascade. Requests outside the mount
|
|
70
|
+
cascade before context resolution or body consumption, so downstream POST
|
|
71
|
+
handlers receive the original stream.
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { createDashboard, createNodeDashboardHandler } from "solid-objects/web"
|
|
75
|
+
|
|
76
|
+
const dashboard = createDashboard({ runtime })
|
|
77
|
+
const handler = createNodeDashboardHandler({
|
|
78
|
+
dashboard,
|
|
79
|
+
resolveContext: async (request) => ({
|
|
80
|
+
authorizationContext: await currentOperator(request),
|
|
81
|
+
session: sessionFor(request),
|
|
82
|
+
}),
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
app.use(handler)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The adapter caps request bodies at 64 KiB by default. Set
|
|
89
|
+
`maximumBodyBytes` to another positive bound when the host requires one.
|
|
90
|
+
|
|
91
|
+
## Authorization
|
|
92
|
+
|
|
93
|
+
In the default and authorized read-only modes, every data route declares an
|
|
94
|
+
`authorizeAdministration` action and resource. Authorization runs before the
|
|
95
|
+
route reads a record, so a denied caller cannot probe identifiers.
|
|
96
|
+
|
|
97
|
+
| Page or action | Action | Resource |
|
|
98
|
+
| ------------------------------------------ | -------------------------- | ----------------------------- |
|
|
99
|
+
| Dashboard, statistics, health | `index` | `dashboard` |
|
|
100
|
+
| Instance list/detail | `index` / `show` | `instances` |
|
|
101
|
+
| Instance pause/resume | `pause` / `resume` | `instances` |
|
|
102
|
+
| Mailbox/message detail | `index` / `show` | `messages` |
|
|
103
|
+
| Reminder, effect, broadcast, process lists | `index` | corresponding plural resource |
|
|
104
|
+
| Dead-letter list/detail/retry | `index` / `show` / `retry` | `dead_letters` |
|
|
105
|
+
|
|
106
|
+
The policy receives the route ID as `resourceId` when one exists and receives
|
|
107
|
+
the exact `authorizationContext` supplied by the host. The default policy
|
|
108
|
+
denies every route. Dashboard authorization does not authenticate requests;
|
|
109
|
+
resolve the operator before calling the dashboard.
|
|
110
|
+
|
|
111
|
+
## Security
|
|
112
|
+
|
|
113
|
+
The host session persists one random CSRF secret. Every rendered page masks it
|
|
114
|
+
with fresh random bytes, so tokens differ between requests while every form
|
|
115
|
+
already open in the same session remains valid. POST requests without a valid
|
|
116
|
+
token receive 403 and do not perform the action.
|
|
117
|
+
|
|
118
|
+
Every stored or request-derived string is escaped before entering HTML,
|
|
119
|
+
including JSON placed in chart attributes. HTML and statistics responses are
|
|
120
|
+
private and not cached. The dashboard sends a nonce-backed content security
|
|
121
|
+
policy, denies framing, disables MIME sniffing, and limits referrers to the
|
|
122
|
+
same origin.
|
|
123
|
+
|
|
124
|
+
Page size defaults to 25 and is clamped to 200. Actor ID substring filtering is
|
|
125
|
+
parameterized with an adapter-specific SQL expression rather than interpolated
|
|
126
|
+
into a query.
|
|
127
|
+
|
|
128
|
+
## Pages and actions
|
|
129
|
+
|
|
130
|
+
The dashboard page shows totals, instances per actor type, mailbox depth,
|
|
131
|
+
outbox/reminder status, recent processes, and recent dead letters. The summary
|
|
132
|
+
bar can poll `/stats` every five seconds when the operator enables Live. Lists
|
|
133
|
+
do not reload underneath an operator reading them.
|
|
134
|
+
|
|
135
|
+
Instance detail shows committed state and recent related messages, reminders,
|
|
136
|
+
effects, broadcasts, and dead letters. Pause prevents workers from claiming
|
|
137
|
+
new turns for that identity; a turn already executing may still commit. Resume
|
|
138
|
+
clears the brake and normal polling resumes delivery.
|
|
139
|
+
|
|
140
|
+
Dead-letter retry calls `runtime.deadLetters.retry()`, retaining its durable
|
|
141
|
+
idempotency and actor-operation validation. A retry refused by the runtime is
|
|
142
|
+
shown on the detail page with status 422.
|
|
143
|
+
|
|
144
|
+
`HEAD /` performs only a schema reachability query and creates no CSRF session
|
|
145
|
+
state. Use it for liveness checks instead of polling the full dashboard.
|
|
146
|
+
|
|
147
|
+
## Charts
|
|
148
|
+
|
|
149
|
+
The dashboard defaults to Chart.js 4.5.0 from jsDelivr with subresource
|
|
150
|
+
integrity. Only that exact origin is added to `script-src`.
|
|
151
|
+
|
|
152
|
+
Use a self-hosted script without widening the policy:
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
createDashboard({
|
|
156
|
+
runtime,
|
|
157
|
+
chartLibrary: { url: "/assets/chart.umd.min.js", integrity: null },
|
|
158
|
+
})
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Set `chartLibrary: { url: null }` to render without charts. The dedicated chart
|
|
162
|
+
containers have fixed height so responsive redraws cannot grow the page.
|
|
163
|
+
|
|
164
|
+
## Extensions
|
|
165
|
+
|
|
166
|
+
Extensions are supplied when the dashboard is created. Configuration is copied
|
|
167
|
+
and frozen; there is no global registry and no first-request mutation boundary.
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
const dashboard = createDashboard({
|
|
171
|
+
runtime,
|
|
172
|
+
extensions: [
|
|
173
|
+
{
|
|
174
|
+
tab: { label: "Tenants", path: "/tenants" },
|
|
175
|
+
routes: [
|
|
176
|
+
{
|
|
177
|
+
method: "GET",
|
|
178
|
+
path: "/tenants",
|
|
179
|
+
policy: { action: "index", resource: "tenants" },
|
|
180
|
+
handle: ({ render }) =>
|
|
181
|
+
render({
|
|
182
|
+
title: "Tenants",
|
|
183
|
+
content: renderTenants(),
|
|
184
|
+
}),
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
})
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Every extension route must carry a nonempty policy. Missing policies and
|
|
193
|
+
method/path collisions fail when the dashboard is created. Renderer callbacks
|
|
194
|
+
may replace named built-in views, and Fetch middleware may wrap the whole
|
|
195
|
+
dashboard. Extension HTML is trusted application code; escape dynamic values
|
|
196
|
+
with the route context's `escape()` helper.
|
package/docs/fit.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Choosing Solid Objects
|
|
2
|
+
|
|
3
|
+
Solid Objects is useful when an application has many independently addressed
|
|
4
|
+
entities and every entity needs ordered state changes, durable work, recovery,
|
|
5
|
+
or realtime projections.
|
|
6
|
+
|
|
7
|
+
## Use it when
|
|
8
|
+
|
|
9
|
+
- Concurrent requests can update the same room, cart, account, device,
|
|
10
|
+
document, or session.
|
|
11
|
+
- Each identity needs its own ordering boundary and durable mailbox.
|
|
12
|
+
- Operations must recover after a Node process exits.
|
|
13
|
+
- State changes stage reminders, effects, actor-to-actor messages, or realtime
|
|
14
|
+
invalidations atomically.
|
|
15
|
+
- The application already operates SQLite, PostgreSQL, or MySQL and should keep
|
|
16
|
+
durable coordination there.
|
|
17
|
+
|
|
18
|
+
## Prefer a row transaction when
|
|
19
|
+
|
|
20
|
+
One short transaction with an update, constraint, or row lock completely
|
|
21
|
+
enforces the invariant. A direct transaction has less machinery, less stored
|
|
22
|
+
history, and no actor-state migration contract.
|
|
23
|
+
|
|
24
|
+
## Prefer another design when
|
|
25
|
+
|
|
26
|
+
- Work is a bulk or data-parallel pipeline rather than per-identity state.
|
|
27
|
+
- One global identity must sustain more writes than one sequential mailbox can
|
|
28
|
+
commit.
|
|
29
|
+
- State is a large document or relational dataset that should be queried and
|
|
30
|
+
updated in smaller normalized pieces.
|
|
31
|
+
- The application needs a transaction spanning several independent object
|
|
32
|
+
identities.
|
|
33
|
+
- Compute and state must be automatically placed close to clients at the edge.
|
|
34
|
+
- The team wants a managed control plane to place, scale, and recover workers.
|
|
35
|
+
- Durable workflow replay across named steps is more important than a mutable
|
|
36
|
+
object with ordered operations.
|
|
37
|
+
|
|
38
|
+
## Model identities deliberately
|
|
39
|
+
|
|
40
|
+
One hot identity is intentionally serialized. An identity should correspond to
|
|
41
|
+
the smallest domain boundary that requires one total order. Splitting a room by
|
|
42
|
+
player or a cart by item may improve parallelism, but it also gives up atomic
|
|
43
|
+
ordering across the split.
|
|
44
|
+
|
|
45
|
+
Different identities can run concurrently when worker capacity and the
|
|
46
|
+
database allow it. The [benchmark harness](benchmarks.md) measures both the hot
|
|
47
|
+
and independent-identity cases.
|
|
48
|
+
|
|
49
|
+
## Operational cost
|
|
50
|
+
|
|
51
|
+
The relational database stores actor instances, ready and claimed mailbox
|
|
52
|
+
membership, message history, leases, effects, reminders, broadcasts, dead
|
|
53
|
+
letters, and process records. Retention policies and the operator dashboard
|
|
54
|
+
make that state inspectable, but they do not remove the need to monitor and
|
|
55
|
+
back up the database.
|
|
56
|
+
|
|
57
|
+
Redis is optional. It is a transient notification path rather than durable
|
|
58
|
+
state, so losing Redis increases polling latency without losing committed work.
|
package/docs/operations.md
CHANGED
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
# Operations
|
|
2
2
|
|
|
3
|
-
Runtime roles use durable polling as the correctness fallback.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
Runtime roles use durable polling as the correctness fallback. Consecutive
|
|
4
|
+
empty passes double each role's wait from `pollingIntervalMilliseconds` to
|
|
5
|
+
`idlePollingIntervalMilliseconds`, which defaults to one second. Processed
|
|
6
|
+
work and wake-up notifications reset the role to the fast interval. Actor
|
|
7
|
+
workers clamp the ceiling to `leaseRenewalIntervalMilliseconds` while they may
|
|
8
|
+
hold cached activations.
|
|
9
|
+
|
|
10
|
+
The default generation-based wake-up adapter interrupts waits for new actor
|
|
11
|
+
messages, effects, reminders, and broadcasts in the same Node process. It does
|
|
12
|
+
not cross a process boundary. When live processes share the database without a
|
|
13
|
+
configured adapter, the runtime logs
|
|
14
|
+
`solid_objects.polling_only_cross_process_wake_up` once. Use PostgreSQL
|
|
15
|
+
notifications or optional Redis Pub/Sub when separate processes need prompt
|
|
16
|
+
delivery; without one, newly committed work can wait up to the current idle
|
|
17
|
+
polling interval. Notification errors are isolated and logged by role and error
|
|
18
|
+
class without failing the committed work.
|
|
19
|
+
|
|
20
|
+
Each role exposes `currentPollingIntervalMilliseconds`.
|
|
21
|
+
`solid_objects.polling.interval_changed` reports the role, reason, previous
|
|
22
|
+
interval, and current interval. The polling-only warning is also emitted as
|
|
23
|
+
`solid_objects.polling.only_cross_process_wake_up` instrumentation.
|
|
24
|
+
|
|
25
|
+
Graceful shutdown stops new claims and allows active turns to finish within
|
|
26
|
+
`shutdownTimeoutMilliseconds`, which defaults to 15 seconds. A component still
|
|
27
|
+
running or stopping at the deadline emits
|
|
10
28
|
`solid_objects.supervisor.component_shutdown_timeout`; the runtime then returns
|
|
11
29
|
without pretending JavaScript code was forcibly terminated. Operators should
|
|
12
30
|
monitor oldest ready work, claimed work, dead letters, effect failures,
|
package/docs/parity.md
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Design parity ledger
|
|
2
2
|
|
|
3
|
-
This ledger tracks
|
|
3
|
+
This ledger tracks capability parity with the Ruby `solid_objects` gem.
|
|
4
4
|
Parity means preserving a capability and its correctness or security boundary,
|
|
5
5
|
not copying a Rails API into Node.
|
|
6
6
|
|
|
7
|
-
Reference: Ruby `solid_objects` 0.
|
|
7
|
+
Reference: Ruby `solid_objects` 0.13.1. The JavaScript package began at the
|
|
8
|
+
Ruby design's `0.12` capability generation; that version number did not imply
|
|
9
|
+
earlier JavaScript releases.
|
|
8
10
|
|
|
9
|
-
The Node `0.
|
|
11
|
+
The Node `0.13.1` implementation has capability parity with that reference. Its
|
|
10
12
|
relational runtime, correctness boundaries, administration, diagnostics,
|
|
11
|
-
realtime projections, browser behavior, and supported
|
|
12
|
-
equivalents. Rails
|
|
13
|
-
transport- and framework-neutral JavaScript APIs. The partial guard
|
|
14
|
-
shared planned result-lookup row below are explicit scope
|
|
15
|
-
missing Ruby capabilities.
|
|
13
|
+
operator dashboard, realtime projections, browser behavior, and supported
|
|
14
|
+
adapters have native equivalents. Rails-specific rendering surfaces are
|
|
15
|
+
replaced by transport- and framework-neutral JavaScript APIs. The partial guard
|
|
16
|
+
row and the shared planned result-lookup row below are explicit scope
|
|
17
|
+
boundaries, not missing Ruby capabilities.
|
|
16
18
|
|
|
17
19
|
## Status vocabulary
|
|
18
20
|
|
|
@@ -49,18 +51,19 @@ missing Ruby capabilities.
|
|
|
49
51
|
|
|
50
52
|
## Operations
|
|
51
53
|
|
|
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
|
-
|
|
|
63
|
-
|
|
|
54
|
+
| Capability | Status | TypeScript shape or remaining work |
|
|
55
|
+
| ----------------------------------------------------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
56
|
+
| 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. |
|
|
57
|
+
| Failed-role replacement | Native | Built-in and registered roles are rebuilt through their factories with capped backoff; shutdown is the terminal replacement boundary. |
|
|
58
|
+
| Additional supervised components | Native | `registerComponent()` builds, validates, runs, and stops application components with the runtime. |
|
|
59
|
+
| Dead-letter inspection and retry | Native | `runtime.deadLetters` provides deny-by-default immutable inspection and idempotent durable retry linkage. |
|
|
60
|
+
| Reconciliation reads | Native | Authorized cursor pages cover active, quiet, and orphaned instances; bounded state batches are migrated and deeply frozen. |
|
|
61
|
+
| 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. |
|
|
62
|
+
| 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. |
|
|
63
|
+
| CLI | Native | The packaged executable loads an application runtime and exposes start, diagnostics, processes, dead letters, reminders, and explicit retention pruning as JSON. |
|
|
64
|
+
| Operator dashboard | Native | The opt-in `solid-objects/web` export provides Fetch and Node/Connect mounting, authorized runtime views and actions, session-backed CSRF, filtering, paging, charts, and immutable extension hooks. |
|
|
65
|
+
| Structured instrumentation | Native | An isolated transport-neutral sink emits immutable lifecycle metadata and structurally excludes application payloads. |
|
|
66
|
+
| Public test helper | Native | `runtime.testing` provides role-selective deterministic draining, explicit-time due-reminder execution, and dependency-ordered reset without relying on cascades. |
|
|
64
67
|
|
|
65
68
|
## Databases and wake-up
|
|
66
69
|
|
|
@@ -76,19 +79,19 @@ missing Ruby capabilities.
|
|
|
76
79
|
|
|
77
80
|
## Realtime and browser behavior
|
|
78
81
|
|
|
79
|
-
| Capability | Status | TypeScript shape or remaining work
|
|
80
|
-
| ------------------------------------------------------------ | -------------- |
|
|
81
|
-
| Explicit observable projection and durable invalidations | Native | `observables()` is opt-in
|
|
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.
|
|
82
|
+
| Capability | Status | TypeScript shape or remaining work |
|
|
83
|
+
| ------------------------------------------------------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
84
|
+
| Explicit observable projection and durable invalidations | Native | `observables()` is opt-in and invalidation-only by default. `broadcastValue()` sends changed values; `broadcastInvalidation()` explicitly sends only changed names while comparing the real value. Private or subscriber-specific values belong behind invalidation-only component endpoints or in typed payloads. |
|
|
85
|
+
| 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. |
|
|
86
|
+
| 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. |
|
|
87
|
+
| Turbo scalar replacement | Not applicable | The browser client exposes invalidations to application rendering code. Framework adapters can be separate packages. |
|
|
88
|
+
| 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. |
|
|
89
|
+
| 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. |
|
|
90
|
+
| Real-browser compatibility suite | Native | Playwright exercises subscription replay over native WebSocket, incarnation/revision fences, payload delivery, component batching, and cancellation in Chromium. |
|
|
88
91
|
|
|
89
92
|
## Rails-specific surfaces
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
Rails generators, Active Record models/controllers, Turbo rendering, and
|
|
95
|
+
Action Cable are not copied into this package. The Rack dashboard is represented
|
|
96
|
+
by the framework-neutral Fetch and Node adapter, renderer callbacks, and the
|
|
97
|
+
same authorization and CSRF boundaries.
|
package/docs/releasing.md
CHANGED
|
@@ -30,14 +30,16 @@ npm trust github solid-objects \
|
|
|
30
30
|
lockfile when needed, and move the release notes out of the Unreleased
|
|
31
31
|
section in `CHANGELOG.md`.
|
|
32
32
|
2. Run `pnpm run format:check`, `pnpm run check`, `pnpm run test:coverage`,
|
|
33
|
-
`pnpm run build`, `pnpm run pack:check`,
|
|
34
|
-
`pnpm
|
|
33
|
+
`pnpm run build`, `pnpm run pack:check`, `pnpm run test:package`,
|
|
34
|
+
`pnpm run test:recovery`, `pnpm run test:browser`, and
|
|
35
|
+
`pnpm audit --audit-level=high`. Run the PostgreSQL, MySQL, and Redis jobs
|
|
36
|
+
against the versions in [the support matrix](support.md).
|
|
35
37
|
3. Commit and push `main`.
|
|
36
38
|
4. Create and push an annotated tag matching the package version:
|
|
37
39
|
|
|
38
40
|
```shell
|
|
39
|
-
git tag -a v0.
|
|
40
|
-
git push origin v0.
|
|
41
|
+
git tag -a v0.13.1 -m "Version 0.13.1"
|
|
42
|
+
git push origin v0.13.1
|
|
41
43
|
```
|
|
42
44
|
|
|
43
45
|
The tag runs the complete CI matrix. The publish job starts only after every
|
|
@@ -20,6 +20,16 @@ The constructor must establish every persisted field and must not depend on
|
|
|
20
20
|
external state. Solid Objects invokes it while validating the class, creating
|
|
21
21
|
defaults, hydrating state, and projecting a snapshot.
|
|
22
22
|
|
|
23
|
+
## Observable broadcast modes
|
|
24
|
+
|
|
25
|
+
`observables()` defines the named values used for realtime change detection.
|
|
26
|
+
Unwrapped values are invalidation-only: the runtime compares their real values
|
|
27
|
+
but includes only each changed name in committed envelopes. Wrapping a value in
|
|
28
|
+
`broadcastInvalidation()` makes that contract explicit. Wrap it in
|
|
29
|
+
`broadcastValue()` only when every authorized actor subscriber may receive the
|
|
30
|
+
value itself. Every mode evaluates and JSON-validates its real value after a
|
|
31
|
+
successful turn.
|
|
32
|
+
|
|
23
33
|
## State migrations
|
|
24
34
|
|
|
25
35
|
Every actor starts at state version 1. Increase `stateVersion` when existing
|
package/docs/support.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Supported versions and test matrix
|
|
2
|
+
|
|
3
|
+
## Runtime support
|
|
4
|
+
|
|
5
|
+
| Component | Supported or tested range |
|
|
6
|
+
| -------------- | ----------------------------------------------------------- |
|
|
7
|
+
| Node.js | 24.15 or newer; CI uses 24.15 |
|
|
8
|
+
| TypeScript | 5.9 or newer for TypeScript applications |
|
|
9
|
+
| SQLite | Node's built-in `node:sqlite` on the supported Node runtime |
|
|
10
|
+
| PostgreSQL | 14 or newer; CI runs 14 and 18 |
|
|
11
|
+
| MySQL | 8.0 or newer with InnoDB; CI runs 8.0 and 8.4 |
|
|
12
|
+
| Redis wake-up | Optional; CI runs Redis 7 |
|
|
13
|
+
| Browser client | Chromium through Playwright |
|
|
14
|
+
|
|
15
|
+
The package is ESM-only. PostgreSQL, MySQL, and Redis require their optional
|
|
16
|
+
peer dependency. SQLite has no driver dependency beyond Node.js.
|
|
17
|
+
|
|
18
|
+
## What the matrix covers
|
|
19
|
+
|
|
20
|
+
The default suite exercises actor definitions, mailbox ordering, state
|
|
21
|
+
migrations, leases, fencing, retries, dead letters, effects, reminders,
|
|
22
|
+
realtime outboxes, administration, authorization, retention, lifecycle,
|
|
23
|
+
timeouts, and SQLite behavior.
|
|
24
|
+
|
|
25
|
+
Database jobs run the real adapter suites against PostgreSQL and MySQL servers.
|
|
26
|
+
The Redis job runs wake-up behavior against a real Redis server. The browser
|
|
27
|
+
job uses native WebSocket connections and Chromium for replay, payload,
|
|
28
|
+
component, dashboard, and revision-fence behavior.
|
|
29
|
+
|
|
30
|
+
The quality job also builds the ESM package, inspects `npm pack`, installs the
|
|
31
|
+
generated tarball in a clean temporary project, runs its packaged SQLite
|
|
32
|
+
quickstart, and executes the multi-process recovery demonstration.
|
|
33
|
+
|
|
34
|
+
## Boundaries
|
|
35
|
+
|
|
36
|
+
CI currently runs on Ubuntu. Local validation also occurs on macOS, but the
|
|
37
|
+
project does not claim a complete operating-system compatibility matrix. A
|
|
38
|
+
database version being accepted by configuration is not a substitute for its
|
|
39
|
+
listed integration job.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { appendFile, access, writeFile } from "node:fs/promises"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { Actor } from "solid-objects"
|
|
4
|
+
|
|
5
|
+
export class RecoveryCounter extends Actor {
|
|
6
|
+
static override readonly actorType = "RecoveryCounter"
|
|
7
|
+
|
|
8
|
+
count = 0
|
|
9
|
+
|
|
10
|
+
async recover({ controlDirectory }: { controlDirectory: string }): Promise<number> {
|
|
11
|
+
const message = this.currentMessage
|
|
12
|
+
if (!message) throw new Error("recover requires a durable message")
|
|
13
|
+
const attempt = message.attempt
|
|
14
|
+
await appendFile(
|
|
15
|
+
join(controlDirectory, "external-effects.jsonl"),
|
|
16
|
+
`${JSON.stringify({ messageId: message.id, attempt, processId: process.pid })}\n`,
|
|
17
|
+
)
|
|
18
|
+
await writeFile(join(controlDirectory, `started-${attempt}-${process.pid}`), "")
|
|
19
|
+
process.send?.({ event: "operation.started", attempt, processId: process.pid })
|
|
20
|
+
if (attempt === 1) await waitForFile(join(controlDirectory, "release-first-attempt"))
|
|
21
|
+
this.count += 1
|
|
22
|
+
return this.count
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async serialize({ controlDirectory }: { controlDirectory: string }): Promise<number> {
|
|
26
|
+
const message = this.currentMessage
|
|
27
|
+
if (!message) throw new Error("serialize requires a durable message")
|
|
28
|
+
await appendFile(
|
|
29
|
+
join(controlDirectory, "serialization.jsonl"),
|
|
30
|
+
`${JSON.stringify({ event: "start", messageId: message.id, at: Date.now() })}\n`,
|
|
31
|
+
)
|
|
32
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
33
|
+
this.count += 1
|
|
34
|
+
await appendFile(
|
|
35
|
+
join(controlDirectory, "serialization.jsonl"),
|
|
36
|
+
`${JSON.stringify({ event: "finish", messageId: message.id, at: Date.now() })}\n`,
|
|
37
|
+
)
|
|
38
|
+
return this.count
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function waitForFile(path: string): Promise<void> {
|
|
43
|
+
for (;;) {
|
|
44
|
+
try {
|
|
45
|
+
await access(path)
|
|
46
|
+
return
|
|
47
|
+
} catch {
|
|
48
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|