examples/live-notes Public
id: note-room
type: concept
owner: justin
reviewed: 2026-10-16
component: note-room
describes:
- src/note-room.ts
- src/room.ts
- src/index.ts
The note room
Each note is one Durable Object of class NoteRoom, found with idFromName(noteId) through the
NOTES binding. Everyone who opens the same note id reaches the same object, wherever they
are, and that object is the single sequencer described in [[conflict-strategy]].
Two layers
NoteRoom (src/note-room.ts) is the part that knows about the runtime: it accepts sockets,
reads frames, sends replies and broadcasts, and owns the SQLite storage. Everything it decides
is delegated to Room (src/room.ts), which takes a NoteStore and returns, for each
message, a reply for the sender and an optional broadcast for the others. The tests drive
Room with an in-memory store, so they run in plain Node without workerd.
Storage
Three tables in the object's SQLite database:
note: one row with the text and its version.Roomcaps the text at 100,000 UTF-16 code units, so the row and every snapshot frame stay small.ops: the op log, keyed by the version each op produced, trimmed to the last 200.snapshot: one row with the version of the last copy saved to R2.
commit() writes the new text, appends the op and trims the log inside one transactionSync,
so a note and its log never disagree. The room only updates its in-memory copy after the
write.
Snapshots
Every 100 versions Room asks for the note to be copied to the SNAPSHOTS bucket.
webSocketMessage sends the ack and the broadcast first, then writes the copy and records its
version in snapshot. A failed write is logged and tried again on the next edit, and only one
runs at a time. See [[snapshots]].
Hibernation
Sockets are accepted with ctx.acceptWebSocket, the Hibernation API. When nobody has typed for
a while the runtime evicts the object but keeps the sockets open; the next frame wakes it,
runs the constructor, and calls webSocketMessage. The constructor creates the tables if
needed and loads the note, so waking up is the same as starting fresh. Nothing the room needs
lives only in memory.
Broadcasting uses ctx.getWebSockets(), which lists every accepted socket even after a wake,
so there is no connection list to rebuild.
Reads without a socket
GET /notes/:id calls the read() RPC method and returns { id, text, version }. It reads the
room's in-memory copy, which is always the last committed state.
GET /notes/:id/snapshots and GET /notes/:id/snapshots/:version do not reach the room at all:
the Worker reads them from R2 under idFromName(id), the same id the room saves them under.