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.
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
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.
We have turned available += 1 into a supervised database-backed event.
This would be embarrassing if process restarts were imaginary.
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.
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.
node ticket-counter.mjs work
node ticket-counter.mjs hold ada
node ticket-counter.mjs status
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.
node ticket-counter.mjs work
node ticket-counter.mjs status
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 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 →