···1818 "parentRevision": {
1919 "type": "string",
2020 "format": "at-uri",
2121- "description": "AT-URI of the previous revision. Null for the first revision."
2121+ "description": "AT-URI of the previous revision. Omitted for the first revision of a note."
2222 },
2323 "diff": {
2424 "type": "string",
+5
src/firehose/handlers.ts
···340340 case COLLECTIONS.note:
341341 deleteNoteByAtUri(atUri);
342342 break;
343343+ case COLLECTIONS.noteRevision:
344344+ // Revisions form a diff chain -- deleting one would break the chain
345345+ // for all subsequent revisions. We intentionally retain the appview
346346+ // copy even if the PDS record is deleted.
347347+ break;
343348 case COLLECTIONS.membership:
344349 deleteMembershipByUri(atUri);
345350 break;
+21-2
src/server/db/queries/revision.ts
···134134 if (exists) return;
135135136136 const current = db
137137- .query("SELECT content FROM current_note WHERE note_at_uri = ?")
138138- .get(noteAtUri) as { content: string } | null;
137137+ .query(
138138+ "SELECT content, latest_revision_uri FROM current_note WHERE note_at_uri = ?",
139139+ )
140140+ .get(noteAtUri) as {
141141+ content: string;
142142+ latest_revision_uri: string;
143143+ } | null;
144144+145145+ // Out-of-order guard: if this revision declares a parent that doesn't match
146146+ // the current head, the diff would be applied to the wrong base content.
147147+ if (
148148+ parentRevisionUri &&
149149+ current &&
150150+ current.latest_revision_uri !== parentRevisionUri
151151+ ) {
152152+ console.warn(
153153+ `[firehose] skipping out-of-order revision ${revisionAtUri}: ` +
154154+ `parent ${parentRevisionUri} != head ${current.latest_revision_uri}`,
155155+ );
156156+ return;
157157+ }
139158140159 const oldContent = current?.content ?? "";
141160 const newContent = applyDiff(oldContent, diff);