Copy This To Markdown So My Agent Can Read This Because I Don't Have The Time to Read It Myself ATM

5-minute guide · Node

Increment one number. Eventually.

Incrementing a number is easy, so naturally we will involve SQLite, an actor mailbox, a background supervisor, and a durable ten-minute alarm. Ten minutes from now———after any number of tasteful process deaths———the system will perform available += 1 with the gravity it deserves.

01

Install an industrial counter

JavaScript already knows how to add one. Install this for the ten minutes, the crash, and the two callers who both saw the last ticket.

Terminal
mkdir ten-minute-counter && cd ten-minute-counter
npm init -y
npm pkg set type=module
npm install solid-objects

FIXIT: It's 2026 and my syntax highlighter still isn't working?!?? Need to vibe code my own syntax highlighter

02

Escalate arithmetic into infrastructure

One actor owns one event's availability. A successful hold decrements the counter and commits a keyed reminder in the same turn. The reminder increments it later.

Architecture review

We have turned available += 1 into a supervised database-backed event. This would be embarrassing if process restarts were imaginary.

ticket-counter.mjs
import { Actor, configure } from "solid-objects"
import { sqlite } from "solid-objects/database/sqlite"

const HOLD_MILLISECONDS = 10 * 60 * 1000

class TicketCounter extends Actor {
  static actorType = "TicketCounter"
  available = 1
  holds = {}

  hold({ buyer }) {
    if (this.available === 0 || buyer in this.holds) {
      return { held: false, available: this.available }
    }

    this.available -= 1
    this.holds = { ...this.holds, [buyer]: Date.now() }
    this.schedule({
      at: new Date(Date.now() + HOLD_MILLISECONDS),
      key: buyer,
    }).expire({ buyer })

    return { held: true, available: this.available }
  }

  expire({ buyer }) {
    if (!(buyer in this.holds)) return this.available

    const remainingHolds = { ...this.holds }
    delete remainingHolds[buyer]
    this.holds = remainingHolds
    this.available += 1
    return this.available
  }
}

const runtime = configure({
  database: sqlite({ path: "tickets.sqlite3" }),
  authorizeMessage: () => true,
  authorizeQuery: () => true,
})

await runtime.install()

async function runCommand() {
  const counter = TicketCounter.ref("show-42")
  const command = process.argv[2] ?? "work"
  const buyer = process.argv[3] ?? "ada"

  if (command === "hold") {
    console.log(await counter.hold({ buyer }))
    return
  }

  if (command === "status") {
    console.log(await counter.available)
    return
  }

  if (command !== "work") throw new Error(`Unknown command: ${command}`)

  const controller = new AbortController()
  process.on("SIGINT", () => controller.abort())
  process.on("SIGTERM", () => controller.abort())
  await runtime.run(controller.signal)
}

try {
  await runCommand()
} finally {
  await runtime.close()
}

For this local example only: the authorization policies allow every call. Production code must bind them to the authenticated user or tenant.

03

Place the needlessly consequential hold

Keep the runtime in one terminal. A second process can place and inspect the hold. Both address the same TicketCounter / show-42 identity.

Terminal 1 · runtime
node ticket-counter.mjs work
Terminal 2 · the momentous operation
node ticket-counter.mjs hold ada
node ticket-counter.mjs status
held → { held: true, available: 0 } status → 0
04

Kill it during the dramatic pause

Stop Terminal 1 before the ten minutes are up. Leave it dead past the deadline, then restart it. The stored reminder becomes an ordered actor call and performs the sacred + 1.

Terminal 1 · after the outage
node ticket-counter.mjs work
Terminal 2 · after the runtime catches up
node ticket-counter.mjs status
available → 1

The expiry handler checks that the hold still exists, so retrying it is harmless. The reminder, state change, and message history live in SQLite———not in a heroic setTimeout.

The arithmetic survived

The counter was never the hard part.

The useful part is one event identity owning the guard, the hold, and the delayed release. If all you need is UPDATE counters SET value = value + 1, use the transaction and enjoy your afternoon.

Seriously, for a moment: This same shape appears in ticket holds, carts, rate-limit resets, session expiry, job leases, game rooms, and device workflows. Keeping state, delayed work, retries, and per-identity ordering correct through crashes is genuinely non-trivial. That is the problem Solid Objects is meant to solve.

Read the complete Node guide →