Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
# Changelog

## Unreleased

## 0.17.7 - 2026-10-12

- Renew retained activations between short turns and across worker passes,
even when synchronous work prevents renewal timers from running.
- Keep an activation with an outstanding message claim discoverable after
lease loss. The next worker pass recovers the message in mailbox order
without a process restart, including with `maxAttempts: 1`. Reclaiming
increments `attempt_count`; lease loss alone does not create a dead letter.
- Run activation lease regressions in PostgreSQL and MySQL CI, reusing and
resetting their test tables so repeated runs leave no messages or extra tables.
Control the server-database test clocks to avoid runner scheduling delays;
SQLite keeps real synchronous waits with a 200 ms lease.
Allow 15 seconds for the 34-turn test's database round trips in CI.
- Add `scripts/check-compatibility-fixtures.mjs` and a workflow with the name
`Compatibility fixtures`. The script compares the `*.json` files in two
directories by name and by bytes. It fails with one line for each file that
Expand All @@ -10,9 +21,8 @@
of solid-objects-ruby at `main`. It runs on each push to `main`, one time
each day, and on demand. Before this change, no job compared the shared
fixtures.
- `docs/parity.md` now cites Ruby `solid_objects` 0.17.3. That Ruby release
adds guides, tests, and one development dependency, and does not change the
runtime, so the ledger has no new row.
- Track Ruby `solid_objects` 0.17.4 in the parity ledger, including renewal
between short turns and preservation of activation fences for claimed messages.

## 0.17.6 - 2026-10-09

Expand Down
4 changes: 2 additions & 2 deletions docs/parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This ledger tracks capability parity with the Ruby `solid_objects` gem.
Parity preserves a capability and its correctness or security boundary. It does
not copy a Rails API into Node.

Reference: Ruby `solid_objects` 0.17.3. The JavaScript package began at the Ruby
Reference: Ruby `solid_objects` 0.17.4. The JavaScript package began at the Ruby
design's `0.12` capability generation; that version number did not imply earlier
JavaScript releases.

Expand Down Expand Up @@ -54,7 +54,7 @@ is needed for the JavaScript stale-result race fix.
| 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. |
| 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`. |
| Fluent committed calls and background delivery | Native | `await reference.operation()` and `reference.send.operation()`. |
| 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. |
| 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. |
| 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. |
| 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. |
| Bounded claim candidate scan | Native | A configurable ordered scan continues to another ready actor when a worker loses the first candidate's lease race. |
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "solid-objects",
"version": "0.17.6",
"version": "0.17.7",
"description": "SQL-backed virtual actor library for TypeScript and Node.js, with durable state, ordered operations, and automatic activation on SQLite, PostgreSQL, or MySQL",
"type": "module",
"license": "MIT",
Expand Down Expand Up @@ -118,8 +118,8 @@
"test": "vitest run",
"test:browser": "pnpm run build && playwright test",
"test:coverage": "vitest run --coverage",
"test:postgresql": "vitest run test/postgresql.test.ts test/effect-recovery.test.ts test/instance-retention.test.ts test/transmit-ordering.test.ts",
"test:mysql": "vitest run test/mysql.test.ts test/effect-recovery.test.ts test/instance-retention.test.ts test/transmit-ordering.test.ts",
"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",
"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",
"test:package": "node scripts/release-artifact-smoke.mjs",
"test:recovery": "pnpm run build && node examples/failure-recovery/demo.ts",
"test:at-least-once": "pnpm run build && node examples/at-least-once/demo.ts",
Expand Down
7 changes: 6 additions & 1 deletion src/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,13 +651,18 @@ export class Repository {
SET activation_owner_id = NULL, activation_token = NULL,
activation_expires_at_ms = NULL, updated_at_ms = ?
WHERE id = ? AND activation_owner_id = ? AND activation_token = ?
AND activation_generation = ?`,
AND activation_generation = ?
AND NOT EXISTS (
SELECT 1 FROM ${this.table("claimed_messages")} claimed
WHERE claimed.instance_id = ?
)`,
[
now,
activation.instanceId,
activation.processId,
activation.activationToken,
activation.activationGeneration,
activation.instanceId,
],
)
})
Expand Down
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const VERSION = "0.17.6"
export const VERSION = "0.17.7"
5 changes: 4 additions & 1 deletion src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,17 @@ export class Worker {
actor: execution.actor,
turn,
lastUsedAt: now,
renewAt: now + this.runtime.settings.leaseRenewalIntervalMilliseconds,
renewAt:
cached?.renewAt ??
passStartedAt + this.runtime.settings.leaseRenewalIntervalMilliseconds,
}
this.activations.set(turn.instance.id, cached)
passExhausted =
processed >= this.runtime.settings.maxMessagesPerActivationPass ||
performance.now() - passStartedAt >=
this.runtime.settings.maxActivationDurationMilliseconds
if (passExhausted) break
await this.maintainCachedActivations()
turn = await this.runtime.repository.claim(this.processId, {
activation: activationLease(cached.turn),
})
Expand Down
Loading
Loading