Rebasely Beta: data may be purged

examples/live-notes Public

Markdown · 2.5 KiB · at 70b1506

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).