Skip to content

Latest commit

 

History

History
970 lines (887 loc) · 58 KB

CHANGELOG.md

File metadata and controls

970 lines (887 loc) · 58 KB

Changelog

1.0.2 - 2026-10-15

  • Add four comparison pages in docs/compare/: Restate virtual objects, DBOS Transact, RivetKit actors, and Graphile Worker named queues. Each page compares one real task, says when the other system is the better choice, and links the primary sources that were checked on October 15, 2026. The Solid Objects code on each page is in examples/compare/ and runs in the test suite. docs/comparisons.md and the README link the pages. An evaluation on October 15, 2026 found that Claude Code, with web search, searched only for these names and never found Solid Objects.
  • pnpm run check:documentation now checks the comparison pages. Each page must embed its current example files, link only to files and headings that exist, and use no em dash or en dash. docs/comparisons.md must link each page.

1.0.1 - 2026-10-14

  • The agent guide now lists the import path, the call, and the driver of each SQL database adapter, and it shows how to cancel a reminder with unschedule(). Agents in the Track B evaluation invented the names solid-objects/database/postgres and this.cancelReminder().

1.0.0 - 2026-10-13

  • Version 1.0 freezes the public API under semantic versioning. docs/versioning.md gives the promise and its limits. The Cloudflare backend stays experimental and is outside the promise. Version 1.0 is not a claim of measured scale: the benchmarks are development measurements, and longer soak tests are open work.
  • Breaking: a turn that calls schedule() and then calls no operation on the result now fails with ScheduleWithoutOperation. The runtime does not retry that turn, and the message becomes a dead letter. Before this release, such a call stored no reminder and gave no error. Correct code, such as this.schedule({ at }).expire(), does not change. The turn can keep the result and call the operation later. A schedule() call with no operation in observables() fails the turn with QueryMutatedState. Ruby has the same behavior.
  • The package description now starts with the problems that the package solves, and the keywords add race-conditions, reservations, and transactional-outbox. The README has a status statement and names the projects that Solid Objects is not.
  • The README and the category guide no longer say "no daemon". They say that a process of your application calls runtime.run(signal) to run reminders, effects, and queued messages.

0.17.8 - 2026-10-12

  • Back off repeated interrupted-message recovery using retryDelayMilliseconds. The first claimed attempt recovers immediately; after attempts 2, 3, 4, and onward, the delay callback receives 1, 2, 3, and onward (defaults: 1, 2, 4 seconds, capped at 60 seconds). Total persisted claim attempts, including earlier handler failures, determine the delay. The ready timestamp survives worker replacement and preserves mailbox order while other actors can proceed. Lease loss alone still does not dead-letter a message; handler failure attempt limits, setting names, defaults, and schema stay unchanged.

  • Yield to timers and input between busy passes in actor, effect, reminder, and broadcast workers and drain helpers, including repeated lease loss. Timer-driven aborts can stop the runtime between attempts; an instrumentation abort stops before another pass. Synchronous handlers remain non-preemptible.

  • Report activation.lost when a handler failure or rejection cannot commit because its lease expired. These attempts recover with the same fencing and backoff as lost completions, and instrumentation can stop the worker.

  • Give overlapping synchronous caller passes separate, reusable workers so cleanup cannot discard another turn's staged effects or other intents. Calls to different actors still execute concurrently; workers are reused once their pass and activation cleanup finish. A caller interrupted by test reset cannot return its stopped worker to the pool and stall subsequent calls.

  • Re-establish actor, effect, reminder, and broadcast process registrations before each pass, including after stale-process cleanup and retention. Explicitly stopped workers stay stopped. No schema or setting changes.

  • Cover caller concurrency and process retention on SQLite, PostgreSQL, and MySQL, with maxAttempts: 1 and both default and single-message pass limits. Keep adapter and worker-role selection explicit in the regression fixtures.

  • Drain recovered turns in the failure-recovery demo before checking committed state. Verify completion and allow additional fenced retries when a worker loses its lease on a delayed runner.

  • Advance the database test clock after acquiring commit locks in PostgreSQL and MySQL fencing tests, so runner delays cannot expire the lease before the commit barrier is reached.

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 is missing on one side or different. The workflow runs the script on this repository's compatibility/ directory and on the compatibility/ directory 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.
  • 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

  • Add three problem guides in docs/guides/. Each one reproduces a failure, shows the simplest fix without an actor, and then shows a tested Solid Objects version with its limits:
    • race-conditions.md: a lost update across an await, why a mutex in one process does not help, the conditional UPDATE fix, and an EventSeats actor whose holds, payment confirmations, and expiry reminders race.
    • persistent-rooms.md: a turn-based room whose state and turn timer survive a restart, the SQL version-column fix, and reconnect that returns the current authorized room state.
    • offline-first.md: browser state in SQLite WASM shared across tabs, and writes queued with solid-objects/transmit that a server applies once, in order, after the network returns.
  • The examples live in examples/guides/. Vitest and Chromium tests prove each claim, including two real processes that hold seats at the same time. pnpm run check:documentation requires each guide to embed its current example files and rejects an em dash in a guide.
  • Tests resolve solid-objects to src/ through the Vitest and TypeScript configuration, so an example that imports the package name runs under test before a build.

0.17.5 - 2026-10-09

  • Correct docs/agents.md and docs/fit.md: Solid Objects Pro ships for Node.js as @solidobjects/pro from the private registry at https://solidobjects.pro, beside the Rails gem. The docs said that its Node build was in development.

0.17.4 - 2026-10-08

  • Step 12 of docs/agents.md now says plainly that every authorization callback denies by default in the browser too. A worker with no authorizeMessage and no authorizeQuery answers no call. The install step points to the browser policy and to runtime.run(signal), and the checks start with the policy. In an evaluation, two agent answers gave the browser install with no policy after they read that a browser policy is not a security boundary.
  • A new Chromium test proves that a browser worker with no authorization callbacks rejects actor calls with Unauthorized.

0.17.3 - 2026-10-08

  • docs/agents.md has a new step 12 for the browser runtime. It covers when the browser runtime fits, the install with @sqlite.org/sqlite-wasm, the entry points, a module worker example, authorization in the browser, the platform limits, transmit to a Node.js or Rails server, and the checks.
  • examples/browser/draft-worker.js and examples/browser/page.js are the guide example. The Chromium suite runs them: state survives a reload, two tabs share one draft, and the second tab continues after the holder tab closes. pnpm run check:documentation fails when the guide does not embed the current files.
  • The example keeps short lease settings. With the default settings, a call from the remaining tab timed out while it waited for the lease of the closed tab.
  • The documentation link check skips fenced code blocks, so code such as ref[operation](value) is not read as a link.
  • Correct the transmit docs: registerTransmit never reads the HTTP response, so a server 422 stops retries only when the deliver callback throws NonRetryableError. The agent guide shows that callback, docs/api.md says so, and a new test proves that NonRetryableError from deliver dead-letters the effect after one attempt, where a plain Error retries.
  • The page example rejects every waiting and later call when the worker fails to load. Before, a call waited with no end.

0.17.2 - 2026-10-08

  • The README names the agent guide at the start of Installation, and the agent guide says that a reminder changes state only when it runs under runtime.run(signal), so a query must not compute expiry from the clock.
  • Correct docs/agents.md: reject takes a code and an options object, this.reject(code, { message }), not this.reject(code, message).
  • docs/agents.md now says that a worker process runs only the actor classes that it knows. For an unregistered class, actor setup fails with UnknownActorType, the worker reports solid_objects.activation.failed, and the message returns to the queue without a counted attempt. Nothing prints without an instrumentation callback. Register each class before runtime.run(signal). It also tells agents to install the current release, puts the deny-by-default reminder beside the install step, and lists the API mistakes found in agent-written code.
  • Claim the Context7 library: context7.json now carries the library url and the maintainer public_key.
  • Add the Context7 refresh workflow. A push to main that changes the README, context7.json, docs/, or examples/ asks Context7 to refresh the index. It uses the CONTEXT7_API_KEY repository secret.

0.17.1 - 2026-10-08

  • Check holds with Object.hasOwn in the README and guide ticket sale examples. The in operator also matched inherited names, so a buyer named constructor could not hold a free ticket, and expire could add a ticket that no hold had taken. pnpm run test:package now holds a ticket for that buyer.
  • Override sharp with 0.35.5 and source-map-js with 1.2.2. Two high advisories reach the repository only through the development tooling: sharp through Miniflare and source-map-js through Vite and PostCSS. The published package does not depend on either.
  • Name the category in the package metadata and the README: Solid Objects is a SQL-backed virtual actor library for TypeScript and Node.js. The package now declares https://solidobjects.dev/js as its homepage and adds the virtual-actors and actor-model keywords.
  • Add docs/virtual-actors.md, a category guide with the definition, a small example, fit and poor-fit criteria, comparisons, an Orleans concept map, and separate compatibility statements for the Node runtime, the browser client, the browser runtime, and the Cloudflare backend.
  • Add docs/agents.md, a consumer guide for coding agents with setup, authorization, effect idempotency, verification, and troubleshooting steps.
  • Add Dapr actors, Temporal, and a comparison vocabulary to docs/comparisons.md.
  • Add examples/ticket-sale.ts. pnpm run test:package runs it against the packed tarball, and the documentation check fails when the guide no longer embeds it.
  • The documentation check now reads every docs/*.md file instead of a fixed list.
  • Add context7.json so that Context7 indexes the consumer documentation and skips maintainer files.
  • Record the clean-install artifact proof in docs/parity.md. Ruby now has a matching rake quickstart check.

0.17.0 - 2026-10-03

  • Breaking: solid_objects.activation.started now fires before the actor's activate() hook. Before, it fired after a successful hook. The new solid_objects.activation.completed event takes that meaning, and solid_objects.activation.failed reports a failed hook. Ruby changes the same events. Move a subscriber that reads activation.started as a finished activation to activation.completed.

  • Reference observe() and on() reject a missing onEvent callback with TypeError before authorization, as Ruby raises ArgumentError without a block. Tests pin the 1,000-observer limit and observer removal on close(). The roadmap no longer lists portable observability as future work.

  • Add the shared telemetry contract compatibility/telemetry-events.json. Tests compare the attribute allowlist and the attribute keys of each core SQL event with Ruby. Activation events carry ownerId, commit action events carry activationGeneration, reminder.enqueued carries messageId and attempt, and a truncated mailbox.depth sample reports depth: null.

  • Port Ruby's query and observable purity tests for each kind of staged work. Name runtime.deadLetters.effects.retry(id) for a dead transmit, document the actor_diagnostics authorization, and remove the unused compatibility/ruby.yml.

  • Pin personalized payload isolation, staged-work rejection, and configured UTF-8 byte limits with regressions matching Ruby. Share timeout telemetry fixtures for wait reasons and activation ownership; document Ruby's fixes and its yielding SQLite wait handler in the parity ledger.

  • Detect observable replacement of staged work even when the intent count is unchanged. Compare complete intent snapshots around projection evaluation.

  • Preserve reserved JSON keys such as __proto__ as own data properties without changing object prototypes. Include them in size limits, state, arguments, and retained results; share compatibility fixtures with Ruby.

  • Record Ruby's matching query/projection purity guards, retained background results, and the intentional 191-character Ruby / 255-character JS reminder name limits in the parity ledger.

  • Preserve transmit staging order within a source message through a persisted effect position, including retries. Schema migration 14 adds effects.position; run runtime.install() before starting upgraded workers. Legacy rows retain their existing ID tie-break because their original order cannot be recovered.

  • Breaking: reject explicit null transmit arguments, matching Ruby; omission still defaults to an empty object. Expand the shared wire fixtures and adapter ordering tests.

  • Refresh the parity ledger for authorized message reads and retained background results, bulk redrive/audit records, reminder cancellation, and automatic wake-up.

  • Patch the Cloudflare development tooling's Undici dependency to 7.29.1 to resolve the high-severity WebSocket and TLS advisories reported by CI.

  • Add portable observability envelopes, metric definitions, actor observers, and bounded authorization-aware diagnostics across SQL runtimes and the Durable Objects host. Isolate failing instrumentation and error loggers from actor work.

0.16.1 - 2026-10-03

  • Find SQLite effect recovery candidates through the processing effects. sqlite_stat1 records only the average row count for each effect status. When most effects are complete, SQLite estimated that status = 'processing' matched most of the effects table. It then read every recovery row in index order, on each effect poll. On SQLite the status test now carries likelihood(..., 0.000001), so the plan starts from the effects_poll index. The PostgreSQL and MySQL queries do not change. Ruby 0.16.1 has the same fix.

  • Ruby 0.16.1 also fixes the SQLite join order of its claimed-message scan. The JavaScript claim path has no such scan, and SQLite already starts recoverExpiredClaims from the claimed messages, so that fix needs no JavaScript change.

  • Build before npm publish reads the manifest. npm validates bin against the working tree before prepack produces dist, so every release logged No bin file found at dist/executable.js twice. The published package was always correct, and npx solid-objects has always worked, but a warning that says the binary is missing is a poor thing to print while publishing one. The publish job builds first and publishes with --ignore-scripts, so the build runs once rather than twice.

  • Check the bin entry the manifest declares rather than a hard-coded path. check-package.mjs asserted dist/executable.js directly, so renaming the build output would have shipped a bin that resolves to nothing while the check still passed.

0.16.0 - 2026-09-23

  • Find a message whose reference a caller lost. runtime.findBy({ requestId }) answers a request id, which is unique across the table, and reference.findBy({ idempotencyKey }) answers a key, which is unique per actor, so the receiver supplies the scope the key needs. Naming neither key, naming both, or naming an idempotency key without a reference throws.

  • Authorize every lookup with the hook the original call ran, against the stored operation and arguments, because a request id is not a capability. An absent row, an actor this process no longer registers, and a caller the policy refuses all return undefined, so a lookup cannot be used to ask whether a request id exists.

  • Add messageReference.outcome(), which reports the status, the result, the persisted error, the rejection, and the attempt count, so a terminal failure answers as well as a success. Every value is frozen, so a caller cannot mutate a durable result it read. The error carries name and message; this runtime has never persisted a stack, which parity now records against Ruby's backtrace.

  • Add schema version 12: a unique index on messages.request_id. The table had only UNIQUE (actor_type, actor_id, request_id), which cannot serve a lookup that names the request id alone.

  • Tell a pruned message from one that never existed. An actor remembers the idempotency keys of its own finished turns, the way an Orleans grain keeps its deduplication history in grain state, so the memory needs no second store and no second write. reference.findBy({ idempotencyKey }) throws MessagePruned for a key the actor remembers and whose message retention removed, and still returns undefined for a key no caller ever sent. The An actor remembers the operation beside each key, so the pruned answer runs the same hook against the same operation a lookup of the surviving row would, and a caller the policy refuses reads undefined for both. Gating it on snapshot would have told a caller who may read state, but not the operation, that the operation had run. The Durable Objects lookup no longer demands a synthetic __lookupMessage__ query in addition, which a policy that allows only declared queries refused. retainedIdempotencyKeys bounds the memory and defaults to 64 keys for each actor. A lookup by request id cannot make the distinction, because the runtime generates a request id and no actor remembers one. retainedIdempotencyKeysBytes bounds the serialized memory as well, because an idempotency key has no length limit and the memory outlives the message row. An actor drops its oldest keys until the list fits, so a key long enough to fill the limit by itself is never remembered.

  • Add schema version 13: instances.completed_idempotency_keys. The doctor now reports the column as missing when it is not installed.

  • Implement findBy and messageOutcome on the Durable Objects runtime, which ActorRuntime required and the backend did not supply, so pnpm run check and pnpm run build both failed. A Durable Object indexes only its own messages, so runtime.findBy({ requestId }) without a reference raises UnsupportedCapability; every other form works. A lookup that the policy refuses answers undefined there too. The Durable Objects lookup gates on authorizeQuery before it reads, which would otherwise have thrown Unauthorized where the SQL runtime answers absent, and a lookup that threw where it was refused is a way to ask whether a key exists.

  • Read the expected schema migration list from SCHEMA_VERSIONS in the doctor and in the tests that assert it, rather than from four hand-copied lists.

  • Retry a dead effect or broadcast. runtime.deadLetters keeps its message meaning and answers effects and broadcasts, so the kind rides on the receiver. retry returns a dead row to pending with a zero attempt count and no claim, keeps its id so a deduplicating handler sees the same key, and acts only on a dead row, so a second press cannot double-enqueue.

  • Redrive a whole scope. redrive opens a durable task, returns at once, and is idempotent over its scope and filters, which a dashboard button needs. A unique index on the active scope enforces that in the database. runtime.run() advances one bounded batch per pass, so a redrive never holds a transaction longer than one batch. runtime.redrives reads tasks back, and task.cancel() stops one and leaves the rows it already moved. A redrive moves what was dead when it started, so a still-broken handler cannot make it run forever.

  • Record who pressed what. Every retry and every redrive transition writes one row to solid_objects_administration_events. The identity comes from the authorization context through a new administrationIdentity option.

  • Add redriveBatchSize, which defaults to 100, and redriveBatchPauseMilliseconds, which defaults to 50.

  • Add schema version 11: two tables, solid_objects_administration_events and solid_objects_redrives, and a failed_at_ms stamp on effects and broadcasts, which a redrive filters on. Existing dead rows take their availability stamp.

  • Select a wake-up adapter automatically. wakeUp now takes a name or an adapter, as config.cache_store does in Rails, and defaults to "automatic". Selection prefers SOLID_OBJECTS_REDIS_URL, then PostgreSQL notifications, then polling. "in_process" opts out, and an unknown name throws rather than polls quietly.

  • PostgreSQL applications that configure nothing now use notifications. They gain cross-process wake-up, one dedicated listening client, and one NOTIFY per commit. Set wakeUp: "in_process" to keep polling.

  • Prove the PostgreSQL notification path before selecting it, because LISTEN does not survive a transaction pooler such as PgBouncer. Selection listens on a probe channel, sends one NOTIFY from a second connection, and waits up to two seconds for it to arrive. A probe that does not deliver falls back to polling and warns once.

  • Report the resolved choice. runtime.wakeUpCapability() names the adapter, whether it crosses processes, its measured floor, and why it was chosen. The doctor reports it as a wakeUp check, and the polling-only warning now fires on what was installed rather than on whether a setting was set.

  • Keep the capability a configured adapter reports about itself. A configured InProcessWakeUpAdapter now reports in_process and warns, rather than claim that it crosses processes.

  • Poll rather than pretend when a requested adapter cannot be built. wakeUp: "postgresql" on a database with no notification channel, wakeUp: "redis" without SOLID_OBJECTS_REDIS_URL, and a Redis URL without the redis package each log solid_objects.wake_up.unavailable once and report the reason in the capability, so the doctor warns rather than claim a cross-process wake-up that cannot happen.

  • Select once per runtime. runtime.wakeUpAdapter() memoises the selection, so callers that race for the first use share one probe rather than run one each.

  • Add reminder reading. reminder() returns one armed alarm as a ScheduledReminder, and reminders() lists every key of one operation. Both apply the intents staged so far in the turn, so a read agrees with what the commit will write. Reading works on the SQL backends and on Durable Objects.

  • Leave a one-shot reminder that already fired out of reminder() and reminders(). Its row stays as completed, so a next-run lookup reported an old time rather than nothing, and an existence check refused to re-arm an alarm that could never fire again.

  • Refuse an unknown operation in unschedule() and unscheduleAll(). schedule() already threw UnknownOperation for one, so a typo cancelled nothing quietly and left a recurring reminder running.

  • Add reminder cancellation. unschedule() removes one alarm by operation, by operation and key, or by the handle schedule() now returns. unscheduleAll() removes every key of one operation. Both stage an intent beside the schedules, so they apply in the order the turn called them, commit with the state change that decided them, and cancel nothing when a turn throws. Cancellation works on the SQL backends and on Durable Objects. A cancellation that lands on an occurrence the scheduler claimed but has not yet enqueued pre-empts it, and the scheduler continues rather than failing.

  • schedule() now returns a ReminderHandle ({ name: string }) instead of void. A handle is a plain object, so it survives in actor state and still cancels after a deactivation. Code that assigned the result to void needs updating, as emit required in 0.15.0.

0.15.2 - 2026-09-21

  • Stop the deadlock between concurrent callers that create the same actor from a transaction of their own. MySQL turns a portable conflict clause into INSERT IGNORE, which keeps a shared lock on the identity index, and the mailbox then asked to upgrade that lock. It now reads the winning row in shared mode, selects only its id so the read stays inside the index, and locks the row by its primary key.
  • Read the instance without a lock before the insert, on every database, and take the row lock by primary key rather than by actor type and actor id.

0.15.1 - 2026-09-16

  • Index opt-in instance expiration by actor type and update time so pruning can find expired instances without scanning retained history. Install schema migration 10 before running this version. Retention policies, timestamps, pending-work protections, and deletion order are unchanged.

0.15.0 - 2026-09-15

  • Report transient process-heartbeat errors and keep retrying while effects run, so one failed update cannot permanently disable owner freshness maintenance.
  • Return a stable EffectHandle from every emit. Overrides/wrappers must return the handle; code expecting void/undefined needs migration.
  • Add SQL effect recovery coordination with onRecovery, optional onStatus, staged requestEffectRecovery, and an extending recoveryTimeoutMilliseconds. Retirement and durable callbacks share the claim-locking transaction. Export typed outcome constants and callback envelopes. Install schema migration 9 before running this version. Cloudflare returns emit handles and rejects the unsupported process-heartbeat recovery options before commit.

0.14.9 - 2026-09-14

  • Export effect failure/success payload types and SerializedError from the root and core entry points. Check SQL and Cloudflare callback construction against the same contracts without changing the delivered messages.
  • Infer operation names and arguments for schedule/transmit calls inside actor methods and check literal effect callback names. Export ScheduledOperationsFor and EffectOptions; retain explicit dynamic-name paths and runtime validation. Subclasses with explicit legacy ScheduledOperations return annotations on schedule or transmit must update their override signatures to match the generic Actor methods.

0.14.8 - 2026-09-09

  • Hold the actor instance and message claim locks through every fenced commit on PostgreSQL and MySQL. Lease recovery and stale-process cleanup revalidate those rows after waiting, so they cannot requeue a message that completed in the meantime.

0.14.7 - 2026-09-05

  • Read SQL message results and completion status in one statement so concurrent completion cannot return a stale null result.
  • Add solid-objects/core and the Cloudflare Durable Objects runtime backend. Each actor identity owns SQLite state, a durable mailbox, retry state, reminders, and effect/message outboxes. Request IDs recover ambiguous RPC acceptance; incarnation and execution generations fence stale commits.
  • Add hibernating session Durable Objects for existing browser subscriptions, including multi-actor connections, fresh authorization, personalized payloads, revision fencing, and durable subscription cleanup.
  • Share actor turn evaluation across SQL and Cloudflare. Keep Workers types and imports separate from the Node target. Add Workers integration tests, a runnable example, bundle checks, and an explicit backend capability matrix. The Cloudflare backend is experimental pending deployed failover/soak testing.

0.14.6 - 2026-09-03

  • Poll effects, reminders, and broadcasts through ordered indexes installed by schema migration 8. PostgreSQL and MySQL claim one row with FOR UPDATE SKIP LOCKED; candidate probes avoid joins and load actor identity by primary key after the claim. SQLite keeps its serialized transaction path. The broadcast revision guard has its own (instance_id, state_revision, status) index instead of rescanning the outbox for every candidate. Claim transactions use read committed isolation on PostgreSQL and MySQL so preceding recovery work cannot turn row skipping into InnoDB gap-lock contention.
  • Replace bulk broadcast and reminder recovery updates with separate available and stale probes. Compare category heads without locking them, lock only the oldest candidate by primary key, and retry past work already locked by another claimant. This preserves global delivery order, concurrent progress, per-instance broadcast revision order, stale-process recovery, and at-least-once delivery.
  • On 50,000 production-shaped rows, SQLite, PostgreSQL 18, and MySQL 8.4 all move from scans and explicit sorts to ordered index probes. The worst SQLite broadcast probe fell from 944 ms to 0.018 ms; PostgreSQL's probes finish in 0.02-0.09 ms and MySQL's in 0.10-1.2 ms.

0.14.5 - 2026-08-29

  • Record the Ruby state warning in the parity ledger. The row said the Ruby gem has no soft threshold. It carries the same solid_objects.state.large 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 the measured Ruby curve falls sooner: the gem keeps 55% of its empty-state throughput at 13 KB of state, where this package keeps 98% at 16 KB. Documentation only; no runtime change.

0.14.4 - 2026-08-29

  • Cut the per-turn state traversals from eight to four. The runtime built the whole state image eight times for each committed operation, and nine times for a query, where two are necessary. The commit path now passes the image it already holds to the observables guard, and the query check reads the committed image instead of taking its own. The guard is unchanged: it still reads the state after observables() returns, because only that read sees a mutation.

  • Compute the default state once for each registered actor class. Every send and every hydration constructed a throwaway actor and serialized its full default state. The constructor must not depend on external state, so one cached image per validated definition is correct. Each caller receives a detached copy.

  • Stop building a string that normalizeJson discards. It called JSON.stringify on every value, then used the result only when a byte limit was given. actorState, deepCopy, and stableJson all pass no limit.

  • Measured on an Apple M5 with SQLite: 1.2x throughput at 0 KB of state, 1.3x at 16 KB, 1.6x at 128 KB, and 2.1x at 1 MB. See Large state.

  • Add warnStateBytes, a soft threshold that defaults to 128 KB. A commit above it reports one solid_objects.state.large instrumentation event with the actor type, the actor ID, the byte count, and the threshold. The runtime reports it only after the commit succeeds, so a turn that rolls back stays silent. The event holds no application state, and the runtime measures the size only when an instrumentation callback is configured. maxStateBytes keeps its 5 MB hard default, which fails the turn. Throughput at that size is about one operation per second, so the warning names the constraint before an application meets it.

  • Add a large-state benchmark scenario, pnpm run benchmark:large-state, that reports operations per second at 0 KB, 16 KB, 128 KB, and 1 MB, and document the measured curve in docs/state-and-lifecycle.md.

  • Align the use-case claims with the Ruby gem. The README table sold per-key rate limits, while the Ruby fit guide called a rate limiter an anti-pattern. Both projects now draw one line: a low-rate quota that a reminder refills fits, because each check is one durable ordered message, and a limiter that every request touches does not.

  • Point the high-QPS reader at Solid Objects Pro from the rate-limit sentence in the README and docs/fit.md, and name what it adds for that shape: grouped commits and ephemeral operations. The README states that it ships for the Rails gem today and that the Node build is in development.

  • Move the early-release caveat off the first screen. It is now a Status section at the end of the README, with a table-of-contents entry, which matches where the Ruby gem keeps the same statement. The first screen keeps the transaction caveat, because that one changes whether a reader should install anything.

  • State the workflow limit next to the workflow row instead of only in docs/fit.md. A workflow fits when one entity owns the mutable state and its mailbox holds the step order. A durable execution engine that replays named steps from a step log is a different tool.

0.14.3 - 2026-08-25

  • Rewrite the first screen around the objection a reader actually has. The README led with a shopping cart that appended to an array, which invites the reply that one SQL statement already does it. It now leads with the ticket sale from the homepage: 100 seats, a hold, a ten-minute expiry that frees the seat, and a published count. That is the smallest example needing three things from one number, and the three things are the argument.
  • Answer "why not just use transactions?" in the first screen instead of burying the fit sections. The section concedes the transaction and the row lock first, including navigator.locks in the browser, then argues scope rather than discipline: any expiresAt or scheduledAt column is evidence the critical section already outlived the lock, and what follows it is a sweeper and a race.
  • Add "Is it worth installing here?", which names who should not install this, and point readers with high-QPS reads or hot identities at Solid Objects Pro.
  • Correct two claims. The realtime section said a value is published once per change from the saving turn; the turn records the publication atomically, while delivery is a separate worker and is at least once. Design provenance said the API was redesigned around Web Components; there is no customElements or HTMLElement in the package.
  • Cut the README from 590 to about 540 lines by removing what docs/ already documented and what the page said three times, and add the table of contents the standard-readme specification asks for above 100 lines.

0.14.2 - 2026-08-24

  • State that background pickup needs runtime.run(signal) (#22). The README's programming-model example works without it because the caller's own path executes the call, and nothing on that page said that a process which installs and then waits claims nothing. An external prober built a two-process harness from the README and read the unclaimed messages as stranded. The README and docs/operations.md now state it, and test/background-pickup.test.ts pins it: a sent message reads ready after install(), and completed once run(signal) starts the roles.
  • Build the same example with configure() and address the actor as Cart.ref("cart-123"), matching every other reference example in the documentation. createRuntime() deliberately leaves the process default unset, so the static form needs configure().
  • Add examples/at-least-once and pnpm run test:at-least-once (#23): an executable proof that the at-least-once clause fires and that the documented remedy absorbs it. An effect worker crashes between the external sink write and the acknowledgement; after restart the sink reads 2 with deduplication off, and 1 when a guard on the stable effect id is in place. The state commit happens exactly once in both runs, and both deliveries carry the same effect id. CI runs the demo alongside the recovery demo.

0.14.1 - 2026-08-23

  • Add solid-objects/signals, live signals on actor references (#24). One side-effect import enables reference.live: read-only signals on the proposed standard JavaScript signals API (signal-polyfill, a new optional peer dependency) that subscribe through an in-process runtime.realtime session when first watched and unsubscribe after a linger when the last watcher leaves. Value-broadcast observables feed named signals; live.snapshot re-fetches the authorized snapshot on each accepted envelope; live.payloads.<name> carries personalized payload projections under their independent revision fences; stale revisions are fenced. Works in Node and in the browser runtime.

0.14.0 - 2026-08-23

  • Add solid-objects/database/shared-sqlite-wasm, the transparent multi-tab database. Every tab runs an ordinary configure -> install -> ref flow against the same shared database; the adapter elects one holder per origin with the Web Locks API, sends every other tab's SQL over a BroadcastChannel session to the holder, and fails over onto the same OPFS state when the holder's tab dies. The runtime's leases and fencing arbitrate the tabs' workers exactly as they arbitrate Node processes. A Playwright test proves plain actor references incrementing one durable counter from two tabs with failover.

  • Add solid-objects/browser/tab-host, the multi-tab host that completes milestone M3 of the in-browser runtime plan (#17). Every tab starts a candidate host; the Web Locks API elects one leader per origin, and only the leader opens the database and runs the runtime. Tabs invoke actors through a BroadcastChannel client that retries with idempotent request ids. When the leader's tab dies, the lock releases, the next host promotes, and the runtime continues from the same OPFS state. A Playwright test proves shared state across two tabs and failover after the leader closes.

  • Share the transmit wire contract with the Ruby gem. The golden fixture file compatibility/transmit-envelopes.json is committed to both repositories with a consuming test on each side; receiveTransmitEnvelope now defaults a missing arguments to an empty object, matching the Ruby ingest and the staging side.

  • Add solid-objects/transmit, milestone M4 of the plan. An actor stages a transmit intent with this.transmit().operation(arguments) (or with emit(TRANSMIT_EFFECT, ...) for a different target) in the same transaction as its state change. registerTransmit drains the outbox with at-least-once delivery and per-actor order (an ordered drain up to the claimed effect's mailbox sequence), and receiveTransmitEnvelope gives the server an idempotent ingest keyed on the effect id. Vitest covers order under transmit failures, replay deduplication, and recovery after an offline period; a Playwright test drains a browser outbox into the Node server runtime.

  • Change TurnContextStore to synchronous scoping. The store no longer stays set across await boundaries, so an open transaction scope cannot leak into interleaved tasks and trip the inside-transaction guards. In the browser those guards are best-effort; Node keeps full AsyncLocalStorage semantics.

  • Retry OPFS SAH pool acquisition. The upstream module caches a failed initialization; the adapter now passes forceReinitIfPreviouslyFailed, so a new leader can claim the pool after the old tab dies.

  • Add a platform seam for async context propagation (src/platform/context-store.ts). Shared modules no longer import node:async_hooks directly. Node entry points register an AsyncLocalStorage factory. A TurnContextStore gives a browser host a turn-scoped store for serialized actor turns. This is milestone M1 of the in-browser runtime plan (#17).

  • Route UUID generation through src/platform/uuid.ts, which uses the standard crypto.randomUUID(). Shared modules no longer import node:crypto.

  • Add check:browser-imports to pnpm run check. The script walks the import graph of the browser-safe modules and fails when a node: module or a server-only driver reaches that graph.

  • Add solid-objects/browser/host, the entry point for a runtime host inside a browser worker. An import registers the browser platform: a turn-scoped context store and a browser host identity. The module re-exports the core runtime API and the WASM adapter. A Playwright test runs the full runtime in a Chromium module worker and proves durable actor state across a page reload. This is the first stage of milestone M3 (#17).

  • Replace the Buffer.byteLength payload size check with TextEncoder, so serialization works without the Node Buffer global.

  • Route the process identity (hostname, process id, runtime version) through src/platform/host-identity.ts. The repository no longer imports node:os or reads process.pid directly.

  • Add solid-objects/database/sqlite-wasm, a browser-safe Database adapter on @sqlite.org/sqlite-wasm (an optional peer dependency). The full runtime passes its round-trip test against this adapter in Node, and a Playwright test proves transactions, rollback, and OPFS persistence across a page reload in Chromium. This is milestone M2 of the in-browser runtime plan (#17).

  • Add runtime.enqueueInternalMessage() and runtime.enqueueInternalMessageInTransaction(connection, options), public entry points for a host package to enqueue an internal-delivery-mode actor message without going through user authorization. The transaction-scoped variant accepts a caller-supplied DatabaseConnection so the enqueue can commit atomically alongside other writes in the same transaction; call the new runtime.announceInternalMessage(message) after that transaction commits to wake worker roles the same way a normal enqueue does.

  • Add runtime.snapshotWithIncarnation(reference), returning the same authorized fields as snapshot() alongside the read instance's instanceId, revision, and createdAtMs, computed from one shared read. instanceId is a random UUID, not a monotonically increasing value, so a caller that needs to detect actor recreation (a new incarnation superseding an old one, regardless of revision) should fence on createdAtMs rather than comparing instanceId values directly. createdAtMs orders incarnations at millisecond granularity; destroying and recreating the same actor identity within the same millisecond produces two incarnations a caller cannot order by createdAtMs alone. See docs/correctness.md.

0.13.3 - 2026-08-18

  • Lower the supported Node.js floor from 24.15.0 to 24.4.0. Node.js 24.4.0 is the first release that accepts readBigInts on the DatabaseSync constructor, which the SQLite adapter needs to read 64-bit integers without losing precision. Node.js 24.0.0 through 24.3.x ignore the option, and the effect recovery and transaction retry tests fail there. A new CI job runs the default suite, the build, the packaged artifact smoke test, and the recovery demo on the floor.
  • Record that node:sqlite stays experimental until Node.js 24.15.0 and prints a warning on stderr before it.
  • Make the failure-recovery demo's serialization proof count messages rather than executions. A worker that loses its lease mid-operation leaves the replacement to execute the same message again, which is the at-least-once contract, so the proof failed on slower machines for behaviour it documents elsewhere. Each serialization event now carries its attempt and process, so a start pairs with its own finish instead of with whichever finish came next. The proof asserts that every start has its own finish, that exactly the two sent messages ran, and that the surviving attempt of each message never overlaps another message's surviving attempt; a superseded attempt may overlap anything, because it keeps running until it notices the lost lease and its write is fenced out. The committed state check is unchanged, and the demo reports the executions it saw. assertSerializedExecution moved into its own module with unit coverage for the clean, retried, superseded-overlap, still-running-replacement, unexplained-overlap, boundary, unfinished, unmatched-finish, double-start, restart-after-finish, and lost-message cases.

0.13.2 - 2026-08-17

  • Accept a key on schedule, naming a reminder for the item it is waiting on rather than for its operation, so one actor can hold an alarm per queued item. Scheduling the same key again moves that item's alarm and leaves the others alone. Without a key the name is still the operation, so existing reminders keep their names and their coalescing behaviour. Adds a nullable message_operation column to the reminders table, left null on existing rows. The composed name is bounded by the 255 characters MySQL holds it in, checked on the name rather than the key alone.
  • Add an authorized runtime.administration.processes() query for inspecting live and stale process rows through the runtime's database adapter.
  • Document rolling-deployment overlap as a reason for the polling-only warning.

0.13.1 - 2026-08-16

  • Back idle actor, effect, reminder, and broadcast polling off exponentially from the configured fast interval to a new one-second idle ceiling. Any processed work or wake-up resets the role immediately, and actor polling remains capped by the lease-renewal interval.
  • Expose each role's current polling interval and emit solid_objects.polling.interval_changed instrumentation for every idle, work, and wake-up transition.
  • Warn once when live processes share the database without a configured cross-process wake-up adapter.
  • Preserve older custom wake-up adapters that return void; return true for notifications and false for timeouts from the built-in PostgreSQL, Redis, and in-process adapters so adaptive polling can distinguish them.
  • Add a reproducible four-role SQLite idle benchmark.
  • Behavior change: pollingIntervalMilliseconds is now the fast interval after activity, not a constant idle cadence. Existing explicit values back off to idlePollingIntervalMilliseconds, which defaults to 1_000. Set both options to the same value to preserve a fixed cadence.

0.13.0 - 2026-08-16

  • Replace the exhaustive README with an outcome-first introduction, explicit fit and correctness boundaries, sourced comparisons, and factual design provenance.

  • Add a packaged SQLite quickstart, a clean-install tarball smoke test, and a deterministic multi-process crash and fencing demonstration.

  • Add a reproducible benchmark harness, record locally observed SQLite, PostgreSQL 18, and MySQL 8.4 measurements, and label other configurations as unmeasured.

  • Add support, contribution, and security policies plus stronger local Markdown link validation.

  • Breaking: make unwrapped observables invalidation-only by default. They continue to detect changes and refresh dependent components without storing or sending their values. Wrap a projection in broadcastValue() to share its scalar value with every authorized actor subscriber.

  • Add broadcastInvalidation() so actors can drive granular component refresh without persisting or sending the observable value.

  • Add broadcastValue() for the explicit value-bearing observable contract.

  • Extend durable and browser invalidation envelopes with invalidation-only observable names.

  • Add the framework-neutral solid-objects/web operator dashboard with Fetch and Node/Connect mounting, runtime statistics, instance and mailbox detail, reminder/effect/broadcast/dead-letter/process views, filtering, paging, instance pause/resume, and idempotent dead-letter retry.

  • Deny dashboard data by default through route-specific administration policy, require session-backed masked CSRF tokens for actions, escape stored values, and serve a strict content security policy.

  • Add immutable dashboard extension routes, tabs, renderer overrides, and middleware, plus configurable Chart.js dashboards and live statistics.

  • Preserve downstream request bodies when the Node/Connect dashboard adapter cascades a request outside its mount path.

  • Add authorized and public read-only dashboard access modes that omit mutation controls, reject POST actions, and require no CSRF session for public demos.

  • Refocus the README and package metadata on native Node.js concurrency, realtime state synchronization, and database-backed infrastructure.

0.12.1 - 2026-08-13

  • Accept camelCase rejection codes and fail malformed codes immediately with non-retryable InvalidRejectionCode diagnostics.
  • Breaking: include the originally staged arguments in effect success and failure callback payloads so actors can correlate concurrent effects.
  • Add runtime.testing.runDueReminders({ now }) for deterministic reminder tests without database writes or sleeps.
  • Document adapter value mapping, readonly snapshot helpers, payload type-alias requirements, realtime observable exposure, message-reference authorization, durable void results, retry backoff in tests, and administration result fields.

0.12.0 - 2026-08-13

  • Document the complete public API, runtime and adapter configuration, state-evolution lifecycle, and error-recovery contracts.

  • Complete the Ruby 0.12.0 spiritual-parity pass with Node-native runtime, administration, realtime, browser, and multi-adapter equivalents.

  • Return claimed messages to ready membership without consuming an attempt when actor hydration, state migration, or activation fails before execution begins.

  • Keep guarded application databases read-only during commit actions so the supplied fenced transaction connection is the only atomic write path.

  • Persist the start of graceful runtime-role shutdown and expose its timestamp so operators can distinguish actively draining processes from dead owners.

  • Release actor, message, effect, reminder, and broadcast ownership atomically during graceful shutdown and recover stale draining processes as well.

  • Persist hostname, host PID, Node version, and Solid Objects version for every runtime role and expose them through immutable process administration reads.

  • Make typed snapshots return persisted fields and getter values from one committed state image, rejecting getters that mutate state or stage work.

  • Probe configured authorization policies with a neutral context in the doctor and warn when sensitive access is open or a policy cannot evaluate safely.

  • Align effect and commit-action context with their durable source message and activation fence, using immutable TypeScript contracts.

  • Bound runtime shutdown with a configurable deadline and report components that do not cooperate with cancellation or stop before it expires.

  • Separate generated request identity from caller-supplied idempotency keys and expose both with enqueue time through immutable actor message context.

  • Preserve isolated runtime ownership across operation, lifecycle, observable, and payload callbacks while keeping lifecycle callbacks message-free.

  • Retry transient SQLite writer acquisition with bounded capped backoff without replaying transaction callbacks or extending synchronous deadlines.

  • Expose concise configure() and isolated createRuntime() entry points; isolated actor turns resolve references from their executing runtime.

  • Raise structured MessageFailed errors for terminal operations and ActorDestroyed when an authorized incarnation disappears during a wait.

  • Add deny-by-default dead-letter inspection and idempotent durable retry.

  • Add sequential schema migrations and upgrade version-one SQLite databases.

  • Add bounded, authorization-gated reconciliation reads for actor lifecycle repair.

  • Add previewable bounded retention for messages, stopped processes, and opt-in actor instances.

  • Add structured installation diagnostics and a targeted durable round trip.

  • Add deterministic runtime draining and explicit database reset helpers for tests.

  • Add isolated structured lifecycle instrumentation with metadata-only events.

  • Add reminder replacement events and authorized inspection and resume APIs.

  • Add deny-by-default transport-neutral realtime subscriptions with committed replay and revision fencing.

  • Add injectable generation-based wake-up signals with a process-local default.

  • Add supervised role replacement through original factories with capped restart backoff.

  • Add authorized process inspection and atomic stale-owner cleanup.

  • Add a JSON CLI for runtime startup, diagnostics, administration, and explicit retention pruning.

  • Add bounded multi-turn activation passes with database-ordered hot-actor fairness.

  • Add an opt-in application-database facade that rejects direct writes during actor execution while keeping registered commit actions writable.

  • Add typed subscriber-specific realtime payloads with query authorization, committed-state projection, failure isolation, and independent revision fencing.

  • Add PostgreSQL 14+ through the optional pg driver with pooled transactions, row-locked sequence allocation, portable schema and set queries, diagnostics, and real-server integration coverage.

  • Add opt-in role-specific PostgreSQL wake-ups with one event-driven listener per runtime and polling as the correctness fallback.

  • Add a typed browser component registry with keyed dependencies, replace/morph strategies, batch coalescing, cancellation, and per-target revision fencing.

  • Add Chromium QA for native WebSocket subscription replay, realtime fences, personalized payloads, component batching, and request cancellation.

  • Add optional Redis Pub/Sub wake-ups with role-specific channels, separate lazy connections, bounded failure handling, and real-server integration QA.

  • Add MySQL 8.0+ through the optional mysql2 driver with pooled transactions, an InnoDB schema, scoped enqueue deadlock recovery, diagnostics, and real-server coverage across MySQL 8.0 and 8.4.

  • Add hydrated idle activation reuse with renewable fenced leases, protected async lifecycle hooks, state restoration after failed turns, and release on timeout, fairness yield, lease loss, and shutdown.

  • Bound activation passes by configurable elapsed time as well as message count so slow hot actors yield workers fairly.

  • Add independent supervised schedulers for expired message and process retention and stale process recovery, with disable switches and bounded failure backoff.

  • Bound synchronous database pool, query, and lock waits across SQLite, PostgreSQL, and MySQL; distinguish an uncommitted enqueue timeout from a recoverable durable wait timeout; and report structured database, activation, and mailbox blockers.

  • Reject committed calls and message waits inside an ambient Solid Objects database transaction before they can self-deadlock.

  • Scan a bounded set of ready actor candidates so a lost lease race does not make a worker report idle while other actors are ready.

  • Add ordinary TypeScript actor classes with inferred, typed references.

  • Add committed calls, asynchronous sends, actor-to-actor delivery, reminders, effects, commit actions, snapshots, destruction, and explicit observables.

  • Add a SQLite durable mailbox with ordered turns, idempotency, retries, leases, fencing, dead letters, process heartbeats, and stale-claim recovery.

  • Add supervised workers for actor turns, effects, reminders, and broadcasts.

  • Add a Node-free browser client for versioned observable invalidations.

  • Deny public actor operations and destruction by default, and reauthorize durable message status, result, and wait reads.

  • Use options objects for library APIs with more than two inputs and enforce the convention during type checking.

  • Name invoked actor methods operation throughout the API and persistence, and name message submission semantics delivery_mode.

  • Support SQLite, PostgreSQL, and MySQL database adapters in the initial release.