examples/live-notes Public
id: snapshots
type: concept
owner: justin
component: snapshots
describes:
- src/snapshots.ts
reviewed: 2026-10-16
Snapshots in R2
A room keeps only the latest text and its last 200 ops ([[note-room]]). Every 100 versions it
also writes the note's text to the SNAPSHOTS bucket, so what a note said last week can still
be read after the op log has moved on. [[0003-snapshots-in-r2]] has the reasoning.
These are the same thing as the snapshot message on the socket ([[wire-protocol]]), the text
at a version. The difference is that these are kept.
When the room saves one
Room.receive() (src/room.ts) marks an outcome with saveSnapshot when the new version is at
least SNAPSHOT_EVERY (100) past the last saved one. NoteRoom sends the ack and the broadcast
first, then puts the text in R2, then records the version in its SQLite snapshot table. The
editors never wait for R2.
If the put fails, nothing is recorded, so the next edit is due again and tries once more. Only
one save runs at a time per room; edits that arrive while one is in flight do not start another.
A failed save is logged as saving a snapshot failed with the version.
Edits that change nothing, and edits the room refuses, never trigger a save.
Keys
snapshots/<object id>/<version, 12 digits>.txt
snapshots/6f1c...e2/000000000300.txt
The object id is idFromName(noteId).toString(). The room knows it as ctx.id, and the Worker
can compute it from the URL, so both sides agree on the prefix without the room ever being told
its name. The zero padding makes R2 list keys in version order.
An object is never overwritten: a version is only saved once, and a note's text at a version never changes.
Reading them
The Worker serves them from R2 without touching the room:
GET /notes/groceries/snapshots
{ "id": "groceries", "snapshots": [{ "version": 100, "saved_at": "2026-10-15T09:00:00.000Z", "size": 412 }], "next": null }
GET /notes/groceries/snapshots?after=10000
GET /notes/groceries/snapshots/100
The list is oldest first, 100 at a time; pass next back as after for the following page.
saved_at is when R2 stored the object. One snapshot comes back as text/plain with a year of
cache-control, because it can never change. A version that was never saved is a 404.
Tests
test/snapshots.test.ts covers when the room asks for a save, retries after a failed one, the
recorded version surviving a wake from hibernation, and the key format, saving, reading and
paging against an in-memory bucket (test/helpers/memory-bucket.ts).