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
18 changes: 16 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
# Changelog

## Unreleased

## 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,
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ subscriptions. Start with the [Cloudflare example](examples/cloudflare/README.md
## Installation

Solid Objects is ESM-only and requires Node.js 24.4 or newer. TypeScript users
need TypeScript 5.9 or newer.
need TypeScript 5.9 or newer. Coding agents should follow the
[agent guide](docs/agents.md), which gives each step in order.

```bash
npm install solid-objects
Expand Down
37 changes: 34 additions & 3 deletions docs/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ The quickstart runs 25 concurrent calls against one identity on a temporary
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>.

Create the runtime and its tables at startup:

```typescript
Expand All @@ -81,7 +85,9 @@ const runtime = configure({
await runtime.install()
```

[Configuration](configuration.md) lists each option and database adapter.
Every authorization callback denies by default. Do step 5 before you call an
actor. [Configuration](configuration.md) lists each option and database
adapter.

## 5. Authorize

Expand Down Expand Up @@ -184,13 +190,26 @@ Obey these rules in actor code:
object argument.
- Use `this.schedule({ at, key })` for delayed work. A new `schedule` with the
same key moves the alarm.
- Use `this.reject(code, message)` for a business rule failure that must not
retry.
- Use `this.reject(code, { message })` for a business rule failure that must
not retry. The second argument is an object, for example
`this.reject("room_full", { message: "The room is full" })`.
- Do not write application tables directly from an operation. Use
`this.commitAction()` for a short write in the same database.
- Do not call an external API in an operation. Use `this.emit()` and an effect
handler.
- Write each operation so that it can run again. Delivery is at least once.
- A reminder changes state only when it runs, and it runs only while a process
calls `runtime.run(signal)`. Do not compute expiry from the clock in a query;
read the state that the reminder committed.
Comment on lines +201 to +203

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Manual runners appear unsupported

The statement that reminders run only under runtime.run(signal) excludes supported manual runners. Hosts and tests can use ReminderScheduler.runOnce(), runUntilIdle(), or run(signal) without calling runtime.run(), as docs/api.md explains. Agents following this guide may wrongly reject those valid options.

Say that a reminder changes state only when a reminder runner executes it, normally through runtime.run(signal). Update the matching wording in CHANGELOG.md too.

Prompt To Fix With AI
This is a comment left during a code review.
Path: docs/agents.md
Line: 201-203

Comment:
**Manual runners appear unsupported**

The statement that reminders run only under `runtime.run(signal)` excludes supported manual runners. Hosts and tests can use `ReminderScheduler.runOnce()`, `runUntilIdle()`, or `run(signal)` without calling `runtime.run()`, as `docs/api.md` explains. Agents following this guide may wrongly reject those valid options.

Say that a reminder changes state only when a reminder runner executes it, normally through `runtime.run(signal)`. Update the matching wording in `CHANGELOG.md` too.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


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 |

[State and lifecycle](state-and-lifecycle.md) and the
[public API](api.md) give the full rules.
Expand All @@ -206,13 +225,25 @@ runs in the caller. These features need a process that calls
- Messages to other actors.
- Realtime broadcasts.

The worker process runs only the actor classes that it knows. For a message
whose class is not registered, actor setup fails with `UnknownActorType`. The
worker reports `solid_objects.activation.failed` through instrumentation,
returns the message to the queue without counting an attempt, and tries again.
Nothing prints unless the application sets an `instrumentation` callback, so
the message seems to wait with no error. Register each class before `run()`:

```typescript
runtime.register(TicketSale)

const controller = new AbortController()
process.once("SIGTERM", () => controller.abort())
await runtime.run(controller.signal)
await runtime.close()
```

`TicketSale.ref(...)` also registers the class, so a script that calls `ref()`
before `run()`, like the direct call above, already works.

The packaged `solid-objects start` command does the same for a runtime that
`solid-objects.config.js` exports. When no process runs, committed work waits
in SQL and runs later. [Operations](operations.md) covers roles and shutdown.
Expand Down
2 changes: 1 addition & 1 deletion 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.1. The JavaScript package began at the Ruby
Reference: Ruby `solid_objects` 0.17.2. The JavaScript package began at the Ruby
design's `0.12` capability generation; that version number did not imply earlier
JavaScript releases.

Expand Down
4 changes: 4 additions & 0 deletions docs/virtual-actors.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ buyer gets the ticket. The hold and its reminder commit in one transaction. If
the worker stops, the reminder stays in `tickets.sqlite3`. It runs when the
worker starts again.

The `work` branch can run the reminder because `TicketSale.ref(...)` registers
the class first. A separate worker process must call
`runtime.register(TicketSale)` before `runtime.run(signal)`.

The authorization callbacks above allow every caller. Use them only for a
local example. The package release check runs this file against the packed
npm tarball. The source is [`examples/ticket-sale.ts`](../examples/ticket-sale.ts).
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "solid-objects",
"version": "0.17.1",
"version": "0.17.2",
"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
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.1"
export const VERSION = "0.17.2"
Loading