Give one integer an origin strategy
The module worker owns the actor runtime. sharedSqliteWasm gives every
tab one OPFS database and elects a new holder when the current tab makes its escape.
Yes, this is an SQLite database, a Web Lock, a BroadcastChannel, and a durable alarm so that one integer can become one larger. We are doing excellent work.
import {
Actor,
configure,
sharedSqliteWasm,
} from "https://esm.sh/solid-objects@0.14.3/browser/host"
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: sharedSqliteWasm({ path: "tickets.db" }),
authorizeMessage: () => true,
authorizeQuery: () => true,
})
await runtime.install()
const controller = new AbortController()
void runtime.run(controller.signal)
const counter = TicketCounter.ref("show-42")
self.addEventListener("message", async (event) => {
if (event.data.type === "hold") {
self.postMessage(await counter.hold({ buyer: event.data.buyer }))
return
}
if (event.data.type === "status") {
self.postMessage({ available: await counter.available })
}
})
For this local example only: the authorization policies allow every call. Apply real authorization before accepting untrusted input.
Add two buttons for the one number
One button starts the hold. The other asks whether our daring integer has completed its ten-minute character arc.
<button data-action="hold">Hold the last ticket</button>
<button data-action="status">Check the sacred integer</button>
<output id="status">Available: 1</output>
<script type="module">
const worker = new Worker("./worker.mjs", { type: "module" })
const status = document.querySelector("#status")
worker.addEventListener("message", (event) => {
status.textContent = `Available: ${event.data.available}`
})
document.querySelector('[data-action="hold"]').addEventListener("click", () => {
worker.postMessage({ type: "hold", buyer: "ada" })
})
document.querySelector('[data-action="status"]').addEventListener("click", () => {
worker.postMessage({ type: "status" })
})
</script>
Serve the two files
OPFS needs an origin, so open the page through localhost instead of a file URL.
npx --yes serve . --listen 3000
Open http://localhost:3000 and hold the ticket. Availability becomes 0. A second tab cannot also claim the last one, despite its confidence.
Close everything at the exciting part
Close every tab before the deadline. Reopen the page after ten minutes, wait for the runtime to claim the due reminder, then check the counter. OPFS remembers what the vanished workers promised.
In an application with a Content Security Policy, allow https://esm.sh and 'wasm-unsafe-eval'. Pin the package version as shown above.
The browser was never short on arithmetic.
The useful part is one event identity across tabs, plus a delayed release that survives losing every JavaScript heap. Without delayed work, a Web Lock and a transaction are delightfully less theatrical.
Seriously, for a moment: This same shape appears in chat rooms, collaborative documents, offline carts, shared sessions, timed reservations, games, and device control. Keeping state, delayed work, retries, and cross-tab ownership correct through reloads and closed browsers is genuinely non-trivial. That is the problem Solid Objects is meant to solve.
Read the complete Browser guide →