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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
# Changelog

## 1.0.0 - 2026-10-13

- Version 1.0 freezes the public API under semantic versioning.
[docs/versioning.md](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`.
Expand Down
30 changes: 23 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ has a stable identity, and its state lives in the SQL database that your app
already uses. [Virtual actors in TypeScript and Node.js](docs/virtual-actors.md)
explains the model and when to use it.

**Status.** Version 1.0: the public API follows semantic versioning and does
not break before 2.0. [Versioning](docs/versioning.md) gives the limits of the
promise. First public release: August 13, 2026. One maintainer.

**Production use.** One first-party application with more than 100,000 users
runs it in production. That is a count of users, not a measure of load. No
production use by other people is known. The [benchmarks](docs/benchmarks.md)
are development measurements, and longer soak tests are open work.

**It is not** Solid Queue, SolidJS, or the `service_actor` gem, and it is not
affiliated with Rails or Cloudflare.

In a shopping cart, paying twice at the same time is a big problem. The payment provider might time out, and your Node site could be restarting before recovery finishes.

To deal with this safely, you often need logic scattered between 7-10 files like database row locks, Redis locks, delayed jobs, retries, and cleanup code to keep that process straight. They are not all large, but they must agree about the same payment state and failure rules. That coordination is the difficult part.
Expand Down Expand Up @@ -150,8 +162,10 @@ that must agree with the write. The original transaction now depends on four oth

Solid Objects makes the application-defined identity the coordination boundary.
Its state, mailbox, retries, reminders, and staged consequences live in SQLite,
PostgreSQL, or MySQL. No daemon, broker, Cloudflare account, or new datastore is
required. Redis is optional wake-up plumbing, not durable state.
PostgreSQL, or MySQL. No separate actor service, broker, Cloudflare account, or
new datastore is required. A process of your application calls
`runtime.run(signal)` to run reminders, effects, and queued messages. Redis is
optional wake-up plumbing, not durable state.

## Good uses

Expand Down Expand Up @@ -275,6 +289,7 @@ then shows a tested Solid Objects version with its limits:
- [Guide for coding agents](docs/agents.md)
- [Choosing Solid Objects](docs/fit.md)
- [Public API](docs/api.md)
- [Versioning](docs/versioning.md)
- [Operations and recovery](docs/operations.md)
- [Observability and diagnostics](docs/observability.md)
- [Detailed architecture](docs/architecture.md)
Expand All @@ -285,11 +300,12 @@ notes remain in `docs/`, where long documentation can be long on purpose.

## Status and license

Solid Objects JS is a pre-1.0 early release. The correctness core has
automated coverage across the supported databases, browser runtime, recovery
paths, and packaged artifact. There is one deployed first-party reference
application, no measured scale, and no known third-party production use yet.
Pre-1.0 is doing actual work in that sentence.
Version 1.0 promises a stable public API. The correctness core has automated
coverage across the supported databases, browser runtime, recovery paths, and
packaged artifact. It makes no claim of measured scale: the
[benchmarks](docs/benchmarks.md) are development measurements, and longer soak
tests are open work. [Versioning](docs/versioning.md) says what a release can
change. The Cloudflare backend is experimental and is outside that promise.

Solid Objects is released under the [MIT License](MIT-LICENSE). It is an
independent project and is not affiliated with or endorsed by Cloudflare.
24 changes: 13 additions & 11 deletions docs/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,10 @@ SQLite database. It proves that no update is lost. Add `pg` or `mysql2` when
you use PostgreSQL or MySQL.

Install the current release. `npm install solid-objects` selects it. Do not
pin a version that you remember from earlier work; the API changed between
releases. The current version is on <https://www.npmjs.com/package/solid-objects>.
use an API that you remember from a 0.x release; the API changed between 0.x
releases. From version 1.0, the public API follows
[semantic versioning](versioning.md). The current version is on
<https://www.npmjs.com/package/solid-objects>.

Create the runtime and its tables at startup:

Expand Down Expand Up @@ -215,12 +217,12 @@ Obey these rules in actor code:

Avoid these mistakes:

| Mistake | Correct form |
| ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `this.schedule({ at, key })` with no operation after it | `this.schedule({ at, key }).expire({ buyer })`. `schedule()` stages a reminder only when you call an operation on its result |
| `this.reject("room full")` or `this.reject(code, message)` | `this.reject("room_full", { message: "The room is full" })` |
| `registerEffect(name, (context, argumentsValue) => ...)` | `registerEffect(name, (argumentsValue, context) => ...)`. The staged arguments come first |
| A worker process that never names the actor class | `runtime.register(TicketSale)` before `runtime.run(signal)`. See step 7 |
| Mistake | Correct form |
| ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `this.schedule({ at, key })` with no operation after it | `this.schedule({ at, key }).expire({ buyer })`. A `schedule()` call with no operation after it fails the turn with `ScheduleWithoutOperation` |
| `this.reject("room full")` or `this.reject(code, message)` | `this.reject("room_full", { message: "The room is full" })` |
| `registerEffect(name, (context, argumentsValue) => ...)` | `registerEffect(name, (argumentsValue, context) => ...)`. The staged arguments come first |
| A worker process that never names the actor class | `runtime.register(TicketSale)` before `runtime.run(signal)`. See step 7 |

[State and lifecycle](state-and-lifecycle.md) and the
[public API](api.md) give the full rules.
Expand Down Expand Up @@ -334,11 +336,11 @@ When you explain Solid Objects to a user, state these limits:
- There are no transactions across actor identities.
- Fencing stops a stale activation from a commit, but its code can continue to
run.
- The package is pre-1.0. It has no measured scale and no known third-party
production use.
- Version 1.0 promises a stable public API. It makes no claim of measured scale,
and no production use by other people is known.
- `solid-objects/browser` is a WebSocket client, not the SQL runtime. The
browser runtime in step 12 is tested in Chromium only. The Cloudflare backend
is experimental.
is experimental and is outside the version 1.0 promise.

The [correctness contract](correctness.md) is the source for each guarantee.

Expand Down
26 changes: 23 additions & 3 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,30 @@ row per item. It also cannot strand an entry when the runtime coalesces an
occurrence. Prefer it for a large queue of interchangeable items. Prefer `key`
when one item needs an alarm that you can move on its own.

#### A schedule call needs an operation

`schedule()` returns an object, and the reminder exists only after you call an
operation on that object. A turn that calls `schedule()` and then calls no
operation on the result fails with `ScheduleWithoutOperation`. The runtime
does not retry that turn, and the message becomes a dead letter.

```typescript
// Wrong. The turn fails.
this.schedule({ at: holdEnd, key: buyer })

// Correct.
this.schedule({ at: holdEnd, key: buyer }).expire({ buyer })
```

You can keep the result and call the operation later in the same turn.

`observables()` cannot call `schedule()`, with or without an operation. The
turn that reads the observables fails with `QueryMutatedState`.

#### Cancelling a reminder

`schedule()` returns a `ReminderHandle` (`{ name: string }`) naming the alarm it
armed. `unschedule()` cancels one alarm, by operation, by operation and key, or
The operation that you call on the result of `schedule()` returns a
`ReminderHandle` (`{ name: string }`) naming the alarm it armed. `unschedule()` cancels one alarm, by operation, by operation and key, or
by that handle. `unscheduleAll()` cancels every key of one operation.

```typescript
Expand Down Expand Up @@ -742,7 +762,7 @@ The root exports `SolidObjectsError` and its supported subclasses:
- definition and execution failures: `InvalidActor`, `InvalidRejectionCode`,
`UnknownActorType`,
`UnknownOperation`, `ActorCallCycle`, `QueryMutatedState`,
`StateMigrationError`, `ApplicationWriteForbidden`, `UnknownEffect`, and
`ScheduleWithoutOperation`, `StateMigrationError`, `ApplicationWriteForbidden`, `UnknownEffect`, and
`UnknownCommitAction`;
- operational failures: `LostActivation`, `DatabaseDeadlineExceeded`,
`UnknownDeadLetter`, `UnknownReminder`, `ReminderNotPaused`, and
Expand Down
2 changes: 2 additions & 0 deletions docs/errors-and-recovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ These errors normally require a code or deployment correction:
reference's `send`, or destroy an actor. Stage delivery with `this.sendTo()`.
- `QueryMutatedState`: a field/getter query, snapshot, observable, or payload
projection changed state or staged durable work.
- `ScheduleWithoutOperation`: a turn called `schedule()` and then called no
operation on the result, so the call stored no reminder.
- `ApplicationWriteForbidden`: actor-owned code attempted a write through
`guardApplicationDatabase()` outside the fenced commit-action connection.
- `InvalidPayload`, `InvalidPayloadBroadcast`, and
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/persistent-rooms.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ The seventh test does not inject a late or repeated timer. The turn number check

Delivery is at least once. Write each operation so that it can run again. One room runs one write at a time. This fits turn-based play, not a high-frequency game loop or fast real-time physics.

The application owns the WebSocket server, authentication, and the display of the room. There are no transactions across two rooms. The package requires Node.js 24.4 or newer and ESM only. The package is pre-1.0.
The application owns the WebSocket server, authentication, and the display of the room. There are no transactions across two rooms. The package requires Node.js 24.4 or newer and ESM only. Version 1.0 promises a stable public API. It makes no claim of measured scale.

See [Correctness and delivery semantics](../correctness.md) and [Virtual actors in TypeScript and Node.js](../virtual-actors.md).

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/race-conditions.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ There are no transactions across two actor identities. One identity runs one wri

Each operation adds a message row. The runtime keeps completed messages for 30 days by default through `messageRetentionMilliseconds`. See [Configuration](../configuration.md#retention-and-cleanup) for retention and cleanup controls.

The package requires Node.js 24.4 or newer and supports ESM only. The package is pre-1.0. See [Correctness and delivery semantics](../correctness.md) and [Virtual actors in TypeScript and Node.js](../virtual-actors.md).
The package requires Node.js 24.4 or newer and supports ESM only. Version 1.0 promises a stable public API. It makes no claim of measured scale. See [Correctness and delivery semantics](../correctness.md) and [Virtual actors in TypeScript and Node.js](../virtual-actors.md).

## Sources

Expand Down
Loading
Loading