solid-objects 0.17.7 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/README.md +23 -7
- package/dist/actor.d.ts.map +1 -1
- package/dist/actor.js +24 -2
- package/dist/actor.js.map +1 -1
- package/dist/broadcast-worker.d.ts.map +1 -1
- package/dist/broadcast-worker.js +5 -3
- package/dist/broadcast-worker.js.map +1 -1
- package/dist/cloudflare/protocol.d.ts.map +1 -1
- package/dist/cloudflare/protocol.js +1 -0
- package/dist/cloudflare/protocol.js.map +1 -1
- package/dist/effect-worker.d.ts.map +1 -1
- package/dist/effect-worker.js +5 -3
- package/dist/effect-worker.js.map +1 -1
- package/dist/errors.d.ts +2 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -0
- package/dist/errors.js.map +1 -1
- 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/platform/node.js +2 -0
- package/dist/platform/node.js.map +1 -1
- package/dist/reminder-scheduler.d.ts.map +1 -1
- package/dist/reminder-scheduler.js +5 -3
- package/dist/reminder-scheduler.js.map +1 -1
- package/dist/repository.d.ts +1 -0
- package/dist/repository.d.ts.map +1 -1
- package/dist/repository.js +22 -2
- package/dist/repository.js.map +1 -1
- package/dist/runtime.d.ts +4 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +58 -37
- package/dist/runtime.js.map +1 -1
- package/dist/test-helper.d.ts.map +1 -1
- package/dist/test-helper.js +8 -2
- package/dist/test-helper.js.map +1 -1
- package/dist/turn.d.ts.map +1 -1
- package/dist/turn.js +1 -0
- package/dist/turn.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +5 -3
- package/dist/worker.js.map +1 -1
- package/dist/yield-to-event-loop.d.ts +3 -0
- package/dist/yield-to-event-loop.d.ts.map +1 -0
- package/dist/yield-to-event-loop.js +10 -0
- package/dist/yield-to-event-loop.js.map +1 -0
- package/docs/agents.md +16 -11
- package/docs/api.md +23 -3
- package/docs/configuration.md +24 -2
- package/docs/errors-and-recovery.md +2 -0
- package/docs/guides/persistent-rooms.md +1 -1
- package/docs/guides/race-conditions.md +1 -1
- package/docs/observability.md +16 -15
- package/docs/operations.md +2 -1
- package/docs/parity.md +41 -39
- package/docs/versioning.md +50 -0
- package/docs/virtual-actors.md +15 -6
- package/examples/failure-recovery/demo.ts +6 -5
- package/examples/failure-recovery/process-messages.ts +10 -0
- package/examples/failure-recovery/worker.ts +2 -5
- package/package.json +8 -5
package/docs/parity.md
CHANGED
|
@@ -4,7 +4,7 @@ This ledger tracks capability parity with the Ruby `solid_objects` gem.
|
|
|
4
4
|
Parity preserves a capability and its correctness or security boundary. It does
|
|
5
5
|
not copy a Rails API into Node.
|
|
6
6
|
|
|
7
|
-
Reference: Ruby `solid_objects` 0.
|
|
7
|
+
Reference: Ruby `solid_objects` 1.0.0. The JavaScript package began at the Ruby
|
|
8
8
|
design's `0.12` capability generation; that version number did not imply earlier
|
|
9
9
|
JavaScript releases.
|
|
10
10
|
|
|
@@ -48,28 +48,29 @@ SQL waiters read results and status from one statement. Ruby already checks
|
|
|
48
48
|
completion and returns the result from the same loaded message; no Ruby change
|
|
49
49
|
is needed for the JavaScript stale-result race fix.
|
|
50
50
|
|
|
51
|
-
| Capability | Status | TypeScript shape or remaining work
|
|
52
|
-
| ------------------------------------------------------------------------------------------------------------- | ------- |
|
|
53
|
-
| 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.
|
|
54
|
-
| Query and projection purity | Native | Both runtimes reject state mutation and staged effects, recovery checks, commit actions, reminders, or outbound messages in queries, observable projections, and personalized payloads. Projection guards compare the complete staged work, including same-count replacements. Query and observable violations fail terminally with `QueryMutatedState`; payload violations fail only that payload. Snapshot projection reads enforce the same rule.
|
|
55
|
-
| JSON property preservation | Native | Reserved property names such as `__proto__`, `constructor`, and `prototype` remain ordinary data, including through size checks and immutable copies. Both suites consume `compatibility/json-values.json`.
|
|
56
|
-
| Fluent committed calls and background delivery | Native | `await reference.operation()` and `reference.send.operation()`.
|
|
57
|
-
| Ordered mailbox, sequence allocation, idempotency, retries, dead letters, leases, renewal, and fenced commits | Native | Both runtimes renew between turns when due and retain the fence while a claim remains, allowing expired-turn recovery in mailbox order even with one allowed attempt. Reclaiming increments the attempt count; lease loss alone does not dead-letter a message. Relational ready/claimed membership tables, distinct generated request IDs and caller idempotency keys, durable history, adapter-appropriate sequence locking, and PostgreSQL/MySQL row locks held from fence validation through commit.
|
|
58
|
-
| Domain rejection and strict poison ordering | Native | Rejections accept JavaScript identifier-style codes and roll back without retry; invalid codes fail terminally, while retryable failures block later operations until completion or dead-lettering.
|
|
59
|
-
| 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.
|
|
60
|
-
| Bounded claim candidate scan | Native | A configurable ordered scan continues to another ready actor when a worker loses the first candidate's lease race.
|
|
61
|
-
| Backpressure and payload caps | Partial | Serialization enforces a shared maximum JSON nesting depth, raising `InvalidPayload`, and an optional caller-supplied `maxBytes` limit, raising `PayloadTooLarge`; keyed reminder names are bounded to 255 characters in JS and 191 in Ruby. These existing database limits remain runtime-specific; use at most 191 ASCII characters for a name shared across runtimes. Distributed per-actor rate limits, global admission control, and cache-capacity eviction are not planned in either runtime. They are hot, request-path, and loss-tolerant, so one durable ordered message per check is the wrong shape. Solid Objects Pro answers them with grouped and ephemeral operations, which [fit](fit.md) describes.
|
|
62
|
-
| 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.
|
|
63
|
-
| Transactional effects and outcome operations | Native | At-least-once handlers receive immutable stable effect, attempt, source-message, and actor identity; success and failure operations also receive the originally staged arguments for correlation. Typed callback envelopes are exported.
|
|
64
|
-
| Actor-to-actor delivery | Native | `sendTo(reference).operation()` stages delivery in the source actor commit.
|
|
65
|
-
| One-shot and recurring reminders | Native | Scheduling, cancellation by name/key/handle, read-your-writes inspection, catch-up policy, stale-claim recovery, pausing, and authorized resume. Cancellation cannot recall an already-enqueued occurrence.
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
|
|
|
71
|
-
|
|
|
72
|
-
| Result
|
|
51
|
+
| Capability | Status | TypeScript shape or remaining work |
|
|
52
|
+
| ------------------------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
53
|
+
| 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. |
|
|
54
|
+
| Query and projection purity | Native | Both runtimes reject state mutation and staged effects, recovery checks, commit actions, reminders, or outbound messages in queries, observable projections, and personalized payloads. Projection guards compare the complete staged work, including same-count replacements. Query and observable violations fail terminally with `QueryMutatedState`; payload violations fail only that payload. Snapshot projection reads enforce the same rule. |
|
|
55
|
+
| JSON property preservation | Native | Reserved property names such as `__proto__`, `constructor`, and `prototype` remain ordinary data, including through size checks and immutable copies. Both suites consume `compatibility/json-values.json`. |
|
|
56
|
+
| Fluent committed calls and background delivery | Native | `await reference.operation()` and `reference.send.operation()`. |
|
|
57
|
+
| Ordered mailbox, sequence allocation, idempotency, retries, dead letters, leases, renewal, and fenced commits | Native | Both runtimes renew between turns when due and retain the fence while a claim remains, allowing expired-turn recovery in mailbox order even with one allowed attempt. Reclaiming increments the attempt count; lease loss alone does not dead-letter a message. Recovery after attempt one is immediate; subsequent interruptions use the existing retry delay with total attempts minus one, stored in ready membership. Mailbox order and other-actor progress are preserved. Both runtimes emit `activation.lost` with the message ID and attempt, including losses while saving a handler failure or rejection. JS busy workers and drains yield to timers and input between passes; Ruby uses threads and waits after lost turns. Relational ready/claimed membership tables, distinct generated request IDs and caller idempotency keys, durable history, adapter-appropriate sequence locking, and PostgreSQL/MySQL row locks held from fence validation through commit. |
|
|
58
|
+
| Domain rejection and strict poison ordering | Native | Rejections accept JavaScript identifier-style codes and roll back without retry; invalid codes fail terminally, while retryable failures block later operations until completion or dead-lettering. |
|
|
59
|
+
| 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. |
|
|
60
|
+
| Bounded claim candidate scan | Native | A configurable ordered scan continues to another ready actor when a worker loses the first candidate's lease race. |
|
|
61
|
+
| Backpressure and payload caps | Partial | Serialization enforces a shared maximum JSON nesting depth, raising `InvalidPayload`, and an optional caller-supplied `maxBytes` limit, raising `PayloadTooLarge`; keyed reminder names are bounded to 255 characters in JS and 191 in Ruby. These existing database limits remain runtime-specific; use at most 191 ASCII characters for a name shared across runtimes. Distributed per-actor rate limits, global admission control, and cache-capacity eviction are not planned in either runtime. They are hot, request-path, and loss-tolerant, so one durable ordered message per check is the wrong shape. Solid Objects Pro answers them with grouped and ephemeral operations, which [fit](fit.md) describes. |
|
|
62
|
+
| 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. Overlapping synchronous caller passes own separate workers, preserving staged intents and concurrent execution across actors; Ruby already isolates each invocation's activation. |
|
|
63
|
+
| Transactional effects and outcome operations | Native | At-least-once handlers receive immutable stable effect, attempt, source-message, and actor identity; success and failure operations also receive the originally staged arguments for correlation. Typed callback envelopes are exported. |
|
|
64
|
+
| Actor-to-actor delivery | Native | `sendTo(reference).operation()` stages delivery in the source actor commit. |
|
|
65
|
+
| One-shot and recurring reminders | Native | Scheduling, cancellation by name/key/handle, read-your-writes inspection, catch-up policy, stale-claim recovery, pausing, and authorized resume. Cancellation cannot recall an already-enqueued occurrence. |
|
|
66
|
+
| Schedule call without an operation | Native | Both runtimes fail a turn that calls `schedule()` and then calls no operation on the result. The error is `ScheduleWithoutOperation`, which is not retried: the message becomes a dead letter, and the turn commits no state and no reminder. The turn can keep the result and call the operation later in the same turn. The projection guard counts such a call in an observable as staged work, so that turn fails with `QueryMutatedState`. The Cloudflare backend applies the same rules. |
|
|
67
|
+
| Same-database commit actions | Native | Registered actions receive source-message identity, mailbox sequence, activation generation, and the fenced transaction connection. |
|
|
68
|
+
| 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. |
|
|
69
|
+
| 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. |
|
|
70
|
+
| 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. |
|
|
71
|
+
| 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. |
|
|
72
|
+
| Result recovery and sync timeout diagnostics | Native | Status, result, outcome, and wait reauthorize the original stored operation and arguments. All delivery modes retain immutable JSON results under the result-size cap. Result reads raise terminal rejection/failure errors; outcome returns them as data. Whole-call deadlines distinguish enqueue, wait, database, activation, and mailbox blockers. |
|
|
73
|
+
| Result lookup by request ID and idempotency key | Native | `runtime.findBy({ requestId })` and `reference.findBy({ idempotencyKey })` rebuild a `MessageReference`, authorized with the hook the original call ran. An actor remembers the keys of its own finished turns, so a key lookup separates a pruned message from one that never existed. |
|
|
73
74
|
|
|
74
75
|
Effect callback envelopes are typed with `EffectFailurePayload`,
|
|
75
76
|
`EffectSuccessPayload`, and `SerializedError` in both SQL and Cloudflare.
|
|
@@ -77,22 +78,23 @@ Ruby publishes RBS contracts with native Ruby field names; this does not change
|
|
|
77
78
|
|
|
78
79
|
## Operations
|
|
79
80
|
|
|
80
|
-
| Capability | Status
|
|
81
|
-
| ----------------------------------------------------------------------------- |
|
|
82
|
-
| Process registration, heartbeats, stale claim recovery, and graceful shutdown | Native
|
|
83
|
-
| Failed-role replacement | Native
|
|
84
|
-
| Additional supervised components | Native
|
|
85
|
-
| Dead-letter inspection and retry | Native
|
|
86
|
-
| Reconciliation reads | Native
|
|
87
|
-
| Message, process, and opt-in instance retention | Native
|
|
88
|
-
| Doctor and schema verification | Native
|
|
89
|
-
|
|
|
90
|
-
|
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
|
|
|
94
|
-
|
|
|
95
|
-
|
|
|
81
|
+
| Capability | Status | TypeScript shape or remaining work |
|
|
82
|
+
| ----------------------------------------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
83
|
+
| 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. |
|
|
84
|
+
| Failed-role replacement | Native | Built-in and registered roles are rebuilt through their factories with capped backoff; shutdown is the terminal replacement boundary. |
|
|
85
|
+
| Additional supervised components | Native | `registerComponent()` builds, validates, runs, and stops application components with the runtime. |
|
|
86
|
+
| Dead-letter inspection and retry | Native | `runtime.deadLetters` provides deny-by-default immutable inspection and idempotent durable retry linkage. |
|
|
87
|
+
| Reconciliation reads | Native | Authorized cursor pages cover active, quiet, and orphaned instances; bounded state batches are migrated and deeply frozen. |
|
|
88
|
+
| 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. Reused JS workers refresh their registration before each pass after cleanup or pruning; Ruby callers already reload and replace stale registrations. SQL instance pruning uses an index on actor type and update time. |
|
|
89
|
+
| 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. |
|
|
90
|
+
| `json` gem compatibility check | Not applicable | The Ruby doctor and the Rails boot decode one JSON document with Active Support, to find a `json` gem release that Active Support cannot use. Node.js has `JSON` built in, so this package has no such dependency. |
|
|
91
|
+
| CLI | Native | The packaged executable loads an application runtime and exposes start, diagnostics, processes, dead letters, reminders, and explicit retention pruning as JSON. |
|
|
92
|
+
| Operator dashboard | Native | Fetch and Node/Connect mounting, authorized views and actions, CSRF, filtering, paging, charts, and extensions match the Rack dashboard. Both dashboards expose individual retries and pause/resume; bulk redrive is available through the administration APIs. Pause does not interrupt an in-flight turn. |
|
|
93
|
+
| Bulk redrive and administration audit | Native | Message, effect, and broadcast scopes support idempotent retry and durable, bounded, cancellable redrive. Retry and task transitions persist administration identity in audit rows. |
|
|
94
|
+
| Structured instrumentation | Native | Versioned events, metric samples, isolated observers, and bounded authorized diagnostics match Ruby. Both test suites check SQL event attributes against `compatibility/telemetry-events.json`; only JS activation events add their turn's message fields. Both runtimes log exporter and observer failures, require observer callbacks, and cap local observers at 1,000. Timeout events share wait reasons, activation owner IDs, and string generations; see [observability](observability.md). Durable Objects uses host instrumentation and remote diagnostics, sends fewer attributes, and does not send `sync.timeout`. |
|
|
95
|
+
| Large committed state warning | Native | `warnStateBytes` reports one `solid_objects.state.large` event, holding the actor type, actor ID, byte count, and threshold, when a committed image passes a 128 KB soft threshold. The event holds no application state, and it reports after the commit. The Ruby gem carries the same event and the same 5 MB hard default from `0.14.3`, as `warn_state_bytes`. Its threshold defaults to 64 KB rather than 128 KB, because its measured curve falls sooner: it keeps 55% of its empty-state throughput at 13 KB, where this package keeps 98% at 16 KB. |
|
|
96
|
+
| Public test helper | Native | `runtime.testing` provides role-selective deterministic draining, explicit-time due-reminder execution, and dependency-ordered reset without relying on cascades. |
|
|
97
|
+
| Clean-install artifact proof | Native | `pnpm run test:package` installs the packed tarball in a new project. It runs the packaged quickstart, which sends 25 concurrent calls to one identity, and the `examples/ticket-sale.ts` guide example. `pnpm run test:recovery` proves recovery after a worker process stops. Ruby's `rake quickstart` builds the gem, installs it in a new Rails application, and proves ordering and reminder recovery after a runtime restart. |
|
|
96
98
|
|
|
97
99
|
## Databases and wake-up
|
|
98
100
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Versioning
|
|
2
|
+
|
|
3
|
+
From version 1.0.0, Solid Objects follows
|
|
4
|
+
[Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## The promise
|
|
7
|
+
|
|
8
|
+
The public API is the set of exports, commands, and configuration options that
|
|
9
|
+
`README.md` and the files in `docs/` describe. [Public API](api.md) lists each
|
|
10
|
+
export.
|
|
11
|
+
|
|
12
|
+
| Release | What it can change |
|
|
13
|
+
| ------------------------ | ------------------------------------------------------------- |
|
|
14
|
+
| Patch, for example 1.0.1 | Corrections of defects. No new behavior |
|
|
15
|
+
| Minor, for example 1.1.0 | New behavior. Code that uses the public API continues to work |
|
|
16
|
+
| Major, for example 2.0.0 | A change that breaks code that uses the public API |
|
|
17
|
+
|
|
18
|
+
## Supported versions
|
|
19
|
+
|
|
20
|
+
A minor release can raise the minimum version of Node.js, TypeScript, SQLite,
|
|
21
|
+
PostgreSQL, MySQL, or Redis. It does this only for a version that its own
|
|
22
|
+
project no longer supports, and [CHANGELOG.md](../CHANGELOG.md) names the
|
|
23
|
+
change. A patch release does not raise a minimum version.
|
|
24
|
+
[Supported versions](support.md) lists the current minimum versions.
|
|
25
|
+
|
|
26
|
+
## Outside the promise
|
|
27
|
+
|
|
28
|
+
- The Cloudflare backend, `solid-objects/cloudflare`. It is experimental, and a
|
|
29
|
+
minor release can change it.
|
|
30
|
+
- Exports that the documentation does not describe.
|
|
31
|
+
- The database schema. A minor release can add tables, columns, and indexes.
|
|
32
|
+
`runtime.install()` applies each change.
|
|
33
|
+
- The text of error messages and log lines. The error classes are part of the
|
|
34
|
+
public API.
|
|
35
|
+
- The timing of background work, where no configuration option sets it.
|
|
36
|
+
- A correction of behavior that is different from the
|
|
37
|
+
[correctness contract](correctness.md). A patch release can make such a
|
|
38
|
+
correction.
|
|
39
|
+
|
|
40
|
+
## What version 1.0 does not claim
|
|
41
|
+
|
|
42
|
+
Version 1.0 is a promise about the API. It is not a claim of measured scale.
|
|
43
|
+
The [benchmarks](benchmarks.md) are development measurements and do not
|
|
44
|
+
guarantee capacity. Longer soak tests are open work. The project has one
|
|
45
|
+
maintainer, and no production use by other people is known.
|
|
46
|
+
|
|
47
|
+
## Releases before 1.0
|
|
48
|
+
|
|
49
|
+
The 0.x releases changed the API between minor versions.
|
|
50
|
+
[CHANGELOG.md](../CHANGELOG.md) lists each change.
|
package/docs/virtual-actors.md
CHANGED
|
@@ -7,11 +7,13 @@ Node.js. The npm package is `solid-objects`. It gives each actor a stable
|
|
|
7
7
|
identity, durable state, ordered operations, and automatic activation.
|
|
8
8
|
|
|
9
9
|
The actor runtime runs in your Node.js processes. State and mailboxes live in
|
|
10
|
-
SQLite, PostgreSQL, or MySQL. It needs no
|
|
11
|
-
account, and no new datastore.
|
|
12
|
-
|
|
10
|
+
SQLite, PostgreSQL, or MySQL. It needs no separate actor service, no broker,
|
|
11
|
+
no Cloudflare account, and no new datastore. A process of your application
|
|
12
|
+
calls `runtime.run(signal)` to run reminders, effects, and queued messages.
|
|
13
|
+
Redis is optional and only shortens wake-up latency.
|
|
13
14
|
|
|
14
|
-
Solid Objects
|
|
15
|
+
Solid Objects 1.0 promises a stable public API. It makes no claim of measured
|
|
16
|
+
scale. Read
|
|
15
17
|
[Compatibility and maturity](#compatibility-and-maturity) before you choose it.
|
|
16
18
|
|
|
17
19
|
## What a virtual actor is
|
|
@@ -213,6 +215,10 @@ run again without harm.
|
|
|
213
215
|
after a crash or a lost lease.
|
|
214
216
|
- Ordered turns do not cancel stale JavaScript. Fencing stops a stale
|
|
215
217
|
activation from a commit, but that code can continue to run.
|
|
218
|
+
- An interrupted first attempt recovers immediately. Repeated interruptions
|
|
219
|
+
use growing, durable retry delays while keeping mailbox order. Lease loss
|
|
220
|
+
alone has no attempt limit; use instrumentation and an application stopping
|
|
221
|
+
policy when a hard bound is required.
|
|
216
222
|
- External effects can run more than once. Use the stable effect ID, or
|
|
217
223
|
another durable key, as the idempotency key at the provider.
|
|
218
224
|
- There are no transactions across actor identities, and there is no replay of
|
|
@@ -240,7 +246,10 @@ These runtimes have different compatibility statements:
|
|
|
240
246
|
| Browser runtime (`solid-objects/browser/host`) | The actor runtime in a browser module worker on SQLite WASM. Tested in Chromium |
|
|
241
247
|
| Cloudflare backend (`solid-objects/cloudflare`) | Experimental. It runs the actor API on Cloudflare Durable Objects, with different capability limits |
|
|
242
248
|
|
|
243
|
-
|
|
244
|
-
|
|
249
|
+
Version 1.0: the public API follows [semantic versioning](versioning.md). The
|
|
250
|
+
Cloudflare backend is outside that promise. One first-party application with
|
|
251
|
+
more than 100,000 users runs the package in production. That is a count of
|
|
252
|
+
users, not a measure of load. The project makes no claim of measured scale,
|
|
253
|
+
and no production use by other people is known.
|
|
245
254
|
[Supported versions](support.md) lists the CI matrix. For Ruby on Rails, use
|
|
246
255
|
the [solid_objects](https://github.com/cardmagic/solid-objects-ruby) gem.
|
|
@@ -72,7 +72,7 @@ async function proveSerialization(): Promise<SerializationProof & { finalState:
|
|
|
72
72
|
])
|
|
73
73
|
const workers = [spawnWorker(), spawnWorker()]
|
|
74
74
|
await Promise.all(workers.map(({ finished }) => finished))
|
|
75
|
-
|
|
75
|
+
for (const message of messages) assert.equal(await message.status(), "completed")
|
|
76
76
|
const events = await readJsonLines(
|
|
77
77
|
join(controlDirectory, "serialization.jsonl"),
|
|
78
78
|
parseSerializationEvent,
|
|
@@ -99,7 +99,7 @@ async function proveCrashRecovery(): Promise<{
|
|
|
99
99
|
await wait(350)
|
|
100
100
|
const recoveryWorker = spawnWorker()
|
|
101
101
|
await recoveryWorker.finished
|
|
102
|
-
await message.
|
|
102
|
+
assert.equal(await message.status(), "completed")
|
|
103
103
|
return recoveryResult({ reference, message, controlDirectory })
|
|
104
104
|
}
|
|
105
105
|
|
|
@@ -118,7 +118,7 @@ async function proveFencing(): Promise<{
|
|
|
118
118
|
await wait(350)
|
|
119
119
|
const recoveryWorker = spawnWorker()
|
|
120
120
|
await recoveryWorker.finished
|
|
121
|
-
await message.
|
|
121
|
+
assert.equal(await message.status(), "completed")
|
|
122
122
|
staleWorker.child.kill("SIGCONT")
|
|
123
123
|
await writeFile(join(controlDirectory, "release-first-attempt"), "")
|
|
124
124
|
await staleWorker.waitFor((entry) => entry.event === "solid_objects.activation.lost")
|
|
@@ -138,9 +138,10 @@ async function recoveryResult(options: {
|
|
|
138
138
|
join(options.controlDirectory, "external-effects.jsonl"),
|
|
139
139
|
parseExternalEffectEvent,
|
|
140
140
|
)
|
|
141
|
-
assert.
|
|
141
|
+
assert.ok(attempts >= 2)
|
|
142
142
|
assert.equal(snapshot.count, 1)
|
|
143
|
-
assert.
|
|
143
|
+
assert.ok(effects.length >= 2)
|
|
144
|
+
assert.ok(effects.length <= attempts)
|
|
144
145
|
return { attempts, finalState: snapshot.count, repeatedEffects: effects.length }
|
|
145
146
|
}
|
|
146
147
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export async function processMessages(worker: {
|
|
2
|
+
runUntilIdle(): Promise<number>
|
|
3
|
+
}): Promise<number> {
|
|
4
|
+
let processed = 0
|
|
5
|
+
for (let attempt = 0; attempt < 200 && processed === 0; attempt += 1) {
|
|
6
|
+
processed = await worker.runUntilIdle()
|
|
7
|
+
if (processed === 0) await new Promise((resolve) => setTimeout(resolve, 10))
|
|
8
|
+
}
|
|
9
|
+
return processed
|
|
10
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createRuntime } from "solid-objects"
|
|
2
2
|
import { sqlite } from "solid-objects/database/sqlite"
|
|
3
3
|
import { RecoverableReport, RecoveryCounter } from "./actor.ts"
|
|
4
|
+
import { processMessages } from "./process-messages.ts"
|
|
4
5
|
|
|
5
6
|
const databasePath = requiredArgument(2)
|
|
6
7
|
const runtime = createRuntime({
|
|
@@ -36,11 +37,7 @@ if (process.argv[3] === "retire-effects") {
|
|
|
36
37
|
const worker = runtime.worker()
|
|
37
38
|
|
|
38
39
|
try {
|
|
39
|
-
|
|
40
|
-
for (let attempt = 0; attempt < 200 && processed === 0; attempt += 1) {
|
|
41
|
-
processed = await worker.runOnce({ activationRetention: "release" })
|
|
42
|
-
if (processed === 0) await new Promise((resolve) => setTimeout(resolve, 10))
|
|
43
|
-
}
|
|
40
|
+
const processed = await processMessages(worker)
|
|
44
41
|
process.send?.({ event: "worker.finished", processed })
|
|
45
42
|
} finally {
|
|
46
43
|
await worker.stop()
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solid-objects",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Fix race conditions across Node.js processes: ordered operations, expiring holds, and state that survives a restart, in your SQL database. A SQL-backed virtual actor library for TypeScript and Node.js.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Lucas Carlson",
|
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
"actors",
|
|
20
20
|
"durable-objects",
|
|
21
21
|
"virtual-actors",
|
|
22
|
-
"actor-model"
|
|
22
|
+
"actor-model",
|
|
23
|
+
"race-conditions",
|
|
24
|
+
"reservations",
|
|
25
|
+
"transactional-outbox"
|
|
23
26
|
],
|
|
24
27
|
"repository": {
|
|
25
28
|
"type": "git",
|
|
@@ -118,8 +121,8 @@
|
|
|
118
121
|
"test": "vitest run",
|
|
119
122
|
"test:browser": "pnpm run build && playwright test",
|
|
120
123
|
"test:coverage": "vitest run --coverage",
|
|
121
|
-
"test:postgresql": "vitest run test/postgresql.test.ts test/activation-lease.test.ts test/effect-recovery.test.ts test/instance-retention.test.ts test/transmit-ordering.test.ts",
|
|
122
|
-
"test:mysql": "vitest run test/mysql.test.ts test/activation-lease.test.ts test/effect-recovery.test.ts test/instance-retention.test.ts test/transmit-ordering.test.ts",
|
|
124
|
+
"test:postgresql": "vitest run test/postgresql.test.ts test/activation-lease.test.ts test/activation-recovery-backoff.test.ts test/caller-lifecycle.test.ts test/effect-recovery.test.ts test/instance-retention.test.ts test/transmit-ordering.test.ts",
|
|
125
|
+
"test:mysql": "vitest run test/mysql.test.ts test/activation-lease.test.ts test/activation-recovery-backoff.test.ts test/caller-lifecycle.test.ts test/effect-recovery.test.ts test/instance-retention.test.ts test/transmit-ordering.test.ts",
|
|
123
126
|
"test:package": "node scripts/release-artifact-smoke.mjs",
|
|
124
127
|
"test:recovery": "pnpm run build && node examples/failure-recovery/demo.ts",
|
|
125
128
|
"test:at-least-once": "pnpm run build && node examples/at-least-once/demo.ts",
|