solid-objects 0.17.6 → 0.17.8
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 +61 -0
- 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/effect-worker.d.ts.map +1 -1
- package/dist/effect-worker.js +5 -3
- package/dist/effect-worker.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 +28 -3
- 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/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +8 -4
- 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 +3 -0
- package/docs/configuration.md +24 -2
- package/docs/observability.md +16 -15
- package/docs/operations.md +2 -1
- package/docs/parity.md +32 -24
- package/docs/virtual-actors.md +4 -0
- 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 +3 -3
package/docs/configuration.md
CHANGED
|
@@ -21,11 +21,11 @@ through `runtime.ref(ActorClass, actorId)`. Both validate options immediately.
|
|
|
21
21
|
| `maxStateBytes` | `5_242_880` | Positive persisted actor-state byte limit. |
|
|
22
22
|
| `warnStateBytes` | `131_072` | Positive persisted actor-state byte threshold that reports one warning. |
|
|
23
23
|
| `maxResultBytes` | `1_048_576` | Positive operation-result byte limit. |
|
|
24
|
-
| `maxAttempts` | `5` | Positive
|
|
24
|
+
| `maxAttempts` | `5` | Positive attempt limit evaluated on handler failure. |
|
|
25
25
|
| `maxMessagesPerActivationPass` | `50` | Positive integer turn budget before fairness yield. |
|
|
26
26
|
| `maxActivationDurationMilliseconds` | `5_000` | Positive elapsed-time budget before fairness yield. |
|
|
27
27
|
| `claimScanLimit` | `100` | Positive integer ready-actor candidates considered per global claim. |
|
|
28
|
-
| `retryDelayMilliseconds` | exponential 1-60 seconds | Receives
|
|
28
|
+
| `retryDelayMilliseconds` | exponential 1-60 seconds | Receives a retry index; returns milliseconds (see below). |
|
|
29
29
|
|
|
30
30
|
The limits measure normalized JSON encoded as UTF-8. `maxResultBytes` also
|
|
31
31
|
limits each computed snapshot getter and effect-handler result. Explicit
|
|
@@ -36,6 +36,22 @@ names enter the broadcast outbox. `retryDelayMilliseconds` should return a
|
|
|
36
36
|
non-negative finite number; an invalid application callback will fail the
|
|
37
37
|
affected failure path rather than schedule an invalid timestamp.
|
|
38
38
|
|
|
39
|
+
Interrupted actor messages use the existing `retryDelayMilliseconds` callback
|
|
40
|
+
on recovery. After the first claimed attempt they are immediately eligible;
|
|
41
|
+
after persisted attempts 2, 3, 4, ... the callback receives 1, 2, 3, ... . Its
|
|
42
|
+
default delays are 1, 2, 4 seconds up to 60 seconds. This is the total claim count,
|
|
43
|
+
including earlier handler failures, not a separate lease-loss counter. The
|
|
44
|
+
availability timestamp is stored transactionally in ready membership, survives
|
|
45
|
+
worker replacement and stale-process cleanup, and keeps the message ahead of
|
|
46
|
+
later messages for that actor. Other actors remain eligible. Custom delays,
|
|
47
|
+
including zero, remain authoritative.
|
|
48
|
+
|
|
49
|
+
Lease loss alone never creates a dead letter, including with `maxAttempts: 1`.
|
|
50
|
+
A handler failure still uses the full attempt count with `maxAttempts` and
|
|
51
|
+
passes that count to `retryDelayMilliseconds`, as before. Applications needing
|
|
52
|
+
a hard retry or elapsed-time bound must observe `activation.lost` and stop or
|
|
53
|
+
intervene. No setting or schema change is required.
|
|
54
|
+
|
|
39
55
|
`maxStateBytes` is a hard limit that fails the turn. `warnStateBytes` is a soft
|
|
40
56
|
threshold that keeps the turn. A commit above the threshold reports one
|
|
41
57
|
`solid_objects.state.large` instrumentation event with the actor type, the actor
|
|
@@ -76,6 +92,12 @@ and `false` for a timeout. An older adapter that returns `void` remains
|
|
|
76
92
|
compatible and keeps the fast cadence. Wake-ups reduce latency, while database
|
|
77
93
|
polling remains the correctness path.
|
|
78
94
|
|
|
95
|
+
Busy SQL workers and testing drains give the event loop a turn between passes.
|
|
96
|
+
In Node this lets timers and input run even with synchronous SQLite calls.
|
|
97
|
+
An instrumentation callback can abort `runtime.run()` before its next pass.
|
|
98
|
+
This does not interrupt a synchronous handler: responsiveness is bounded by
|
|
99
|
+
one pass, or by a single overlong handler that must return first.
|
|
100
|
+
|
|
79
101
|
## Retention and cleanup
|
|
80
102
|
|
|
81
103
|
| Option | Default | Contract |
|
package/docs/observability.md
CHANGED
|
@@ -44,21 +44,22 @@ result. When an exporter or observer raises, both runtimes log
|
|
|
44
44
|
Delivery is best effort and synchronous callbacks should be short;
|
|
45
45
|
JavaScript does not await exporters. Telemetry is not a durable audit trail.
|
|
46
46
|
|
|
47
|
-
| Event | Meaning
|
|
48
|
-
| ------------------------------------------- |
|
|
49
|
-
| `activation.started/completed/failed` | Local actor activation hook lifecycle
|
|
50
|
-
| `message.started/completed/failed/rejected` | One attempt's execution outcome
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
57
|
-
| `
|
|
58
|
-
| `recovery.
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
47
|
+
| Event | Meaning |
|
|
48
|
+
| ------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
49
|
+
| `activation.started/completed/failed` | Local actor activation hook lifecycle |
|
|
50
|
+
| `message.started/completed/failed/rejected` | One attempt's execution outcome |
|
|
51
|
+
| `activation.lost` | A message turn lost its activation fence; includes message ID and total attempt count |
|
|
52
|
+
| `message.retry` | Failed attempt durably queued for another attempt |
|
|
53
|
+
| `dead_letter.created` | Message exhausted retries or failed permanently |
|
|
54
|
+
| `commit_action.started/completed/failed` | One registered commit action inside the commit transaction |
|
|
55
|
+
| `mailbox.depth` | On-demand diagnostic sample; `depth` is null when the sample is truncated |
|
|
56
|
+
| `reminder.enqueued` | Due reminder dispatch; lateness is measured from due time |
|
|
57
|
+
| `outbox.age` | Delivery observation; age is time since the item's current availability time |
|
|
58
|
+
| `recovery.reclaimed` | A previously claimed, interrupted message begins another attempt |
|
|
59
|
+
| `recovery.completed/failed` | Durable effect recovery callback commits or enters the dead-letter queue |
|
|
60
|
+
| `snapshot.read` | Authorized snapshot constructed without exposing its contents |
|
|
61
|
+
| `realtime.connected/disconnected` | Actor subscription added or removed |
|
|
62
|
+
| `payload_broadcast.failed` | One personalized payload failed; `payload` names it |
|
|
62
63
|
|
|
63
64
|
Events describe local observations. Concurrent deletion, crashes, and failed
|
|
64
65
|
exporters can omit events. Never infer exactly-once delivery from event counts.
|
package/docs/operations.md
CHANGED
|
@@ -230,7 +230,8 @@ for the same time emits nothing.
|
|
|
230
230
|
|
|
231
231
|
## Dead letters, retry, and redrive
|
|
232
232
|
|
|
233
|
-
A
|
|
233
|
+
A handler failure that exhausts its attempts becomes a dead letter.
|
|
234
|
+
Lease loss alone remains pending with [recovery backoff](configuration.md). An effect or a
|
|
234
235
|
broadcast that exhausts its attempts stays in its own table with
|
|
235
236
|
`status = 'dead'`. All three are read and retried through one receiver, which
|
|
236
237
|
carries the kind:
|
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.17.
|
|
7
|
+
Reference: Ruby `solid_objects` 0.17.5. 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,28 @@ 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 | 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
|
-
| Same-database commit actions | Native | Registered actions receive source-message identity, mailbox sequence, activation generation, and the fenced transaction connection.
|
|
67
|
-
| 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.
|
|
68
|
-
| 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.
|
|
69
|
-
| 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.
|
|
70
|
-
| 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.
|
|
71
|
-
| 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.
|
|
72
|
-
| 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.
|
|
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
|
+
| Same-database commit actions | Native | Registered actions receive source-message identity, mailbox sequence, activation generation, and the fenced transaction connection. |
|
|
67
|
+
| 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. |
|
|
68
|
+
| 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. |
|
|
69
|
+
| 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. |
|
|
70
|
+
| 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. |
|
|
71
|
+
| 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. |
|
|
72
|
+
| 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
73
|
|
|
74
74
|
Effect callback envelopes are typed with `EffectFailurePayload`,
|
|
75
75
|
`EffectSuccessPayload`, and `SerializedError` in both SQL and Cloudflare.
|
|
@@ -84,7 +84,7 @@ Ruby publishes RBS contracts with native Ruby field names; this does not change
|
|
|
84
84
|
| Additional supervised components | Native | `registerComponent()` builds, validates, runs, and stops application components with the runtime. |
|
|
85
85
|
| Dead-letter inspection and retry | Native | `runtime.deadLetters` provides deny-by-default immutable inspection and idempotent durable retry linkage. |
|
|
86
86
|
| Reconciliation reads | Native | Authorized cursor pages cover active, quiet, and orphaned instances; bounded state batches are migrated and deeply frozen. |
|
|
87
|
-
| 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. SQL instance pruning uses an index on actor type and update time.
|
|
87
|
+
| 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. |
|
|
88
88
|
| 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. |
|
|
89
89
|
| CLI | Native | The packaged executable loads an application runtime and exposes start, diagnostics, processes, dead letters, reminders, and explicit retention pruning as JSON. |
|
|
90
90
|
| 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. |
|
|
@@ -284,6 +284,14 @@ and Rails to Node) ran in solid-objects-ruby#49; the one disagreement it
|
|
|
284
284
|
found (the optional `arguments` default) is fixed and pinned by the shared
|
|
285
285
|
fixture.
|
|
286
286
|
|
|
287
|
+
The `Compatibility fixtures` workflow compares each `compatibility/*.json` file
|
|
288
|
+
with the same file in solid-objects-ruby at `main`, byte for byte. It runs on
|
|
289
|
+
each push to `main`, one time each day, and on demand. It does not run on a
|
|
290
|
+
pull request, because the other repository holds the old fixture until the two
|
|
291
|
+
pull requests of a paired change merge. Run the same check on a local clone of
|
|
292
|
+
the Ruby repository with
|
|
293
|
+
`node scripts/check-compatibility-fixtures.mjs compatibility ../solid-objects-ruby/compatibility`.
|
|
294
|
+
|
|
287
295
|
## JavaScript-only: Cloudflare hosting
|
|
288
296
|
|
|
289
297
|
The experimental `solid-objects/cloudflare` backend hosts each actor identity
|
package/docs/virtual-actors.md
CHANGED
|
@@ -213,6 +213,10 @@ run again without harm.
|
|
|
213
213
|
after a crash or a lost lease.
|
|
214
214
|
- Ordered turns do not cancel stale JavaScript. Fencing stops a stale
|
|
215
215
|
activation from a commit, but that code can continue to run.
|
|
216
|
+
- An interrupted first attempt recovers immediately. Repeated interruptions
|
|
217
|
+
use growing, durable retry delays while keeping mailbox order. Lease loss
|
|
218
|
+
alone has no attempt limit; use instrumentation and an application stopping
|
|
219
|
+
policy when a hard bound is required.
|
|
216
220
|
- External effects can run more than once. Use the stable effect ID, or
|
|
217
221
|
another durable key, as the idempotency key at the provider.
|
|
218
222
|
- There are no transactions across actor identities, and there is no replay of
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solid-objects",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.8",
|
|
4
4
|
"description": "SQL-backed virtual actor library for TypeScript and Node.js, with durable state, ordered operations, and automatic activation on SQLite, PostgreSQL, or MySQL",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -118,8 +118,8 @@
|
|
|
118
118
|
"test": "vitest run",
|
|
119
119
|
"test:browser": "pnpm run build && playwright test",
|
|
120
120
|
"test:coverage": "vitest run --coverage",
|
|
121
|
-
"test:postgresql": "vitest run test/postgresql.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/effect-recovery.test.ts test/instance-retention.test.ts test/transmit-ordering.test.ts",
|
|
121
|
+
"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",
|
|
122
|
+
"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
123
|
"test:package": "node scripts/release-artifact-smoke.mjs",
|
|
124
124
|
"test:recovery": "pnpm run build && node examples/failure-recovery/demo.ts",
|
|
125
125
|
"test:at-least-once": "pnpm run build && node examples/at-least-once/demo.ts",
|