Rebasely Beta: data may be purged

examples/live-notes Public

Markdown · 2.6 KiB · at 70b1506

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. Room caps 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.