OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-store commits / 4fcd186

pointer-substrate: store a delete tombstone as the pointer, drop the bytes
RepoBase.deleteRecord routes deletions through set(), so the pointer design needs to recognise a tombstone value rather than writing it to the overlay as if it were a record: keep { $deleted:true } as the pointer itself and remove the record's bytes. get()/_resolve() treat a tombstone as absent and never try to fetch bytes for one. The predicate is duplicated here rather than imported because the dependency direction is atproto-room -> atproto-store and must never invert. del() stays for callers that genuinely want a key gone; record deletion no longer uses it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4fcd186a2a450c91ee6ddf4a7719b7904770b099
by plankatron <fleeky@gmail.com> · 2026-07-24T22:59:45.000Z

Changed files (1) +19 -2

lib/pointer-substrate.js +19 -2
……@@ -16,6 +16,12 @@
1616
1717const SEP = '/'
1818
19// The record-layer DELETE TOMBSTONE convention (owned by atproto-room's RepoBase; duplicated as a
20// predicate here because the dependency direction is atproto-room -> atproto-store, never back).
21// A deleted record keeps a pointer-shaped { $deleted:true } entry instead of vanishing, so a peer
22// can tell "deleted" from "not replicated to me yet" — see RepoBase.deleteRecord.
23const isTombstone = (v) => !!(v && typeof v === 'object' && v.$deleted === true)
24
1925function split (key) {
2026 const k = String(key)
2127 const i = k.indexOf(SEP)
……@@ -46,22 +52,32 @@ export class PointerSubstrate {
4652 }
4753
4854 // Write the bytes to the overlay (dedup + servable by oid), then the tiny pointer to the autobase.
55 // A TOMBSTONE value is not a record: it carries no bytes, so it is stored as the pointer itself and
56 // the record's bytes are dropped. (RepoBase.deleteRecord routes deletes through set(), so this is
57 // the delete path for the pointer design.)
4958 async set (key, record) {
5059 const { collection, rkey } = split(key)
60 if (isTombstone(record)) {
61 await this.statebase.set(key, { $deleted: true }) // converges symmetrically, like any pointer
62 try { await this.store.del(collection, rkey) } catch {}
63 return
64 }
5165 const { oid, size } = await this.store.put(collection, rkey, record)
5266 await this.statebase.set(key, { $oid: oid, size })
5367 }
5468
69 // Legacy hard-delete. Retained for callers that genuinely want the key GONE; record deletion now
70 // goes through set(TOMBSTONE) so absence stays unambiguous.
5571 async del (key) {
5672 const { collection, rkey } = split(key)
57 await this.statebase.del(key) // pointer tombstone converges symmetrically
73 await this.statebase.del(key)
5874 try { await this.store.del(collection, rkey) } catch {}
5975 }
6076
6177 // Resolve the pointer to the record bytes (local file, or fetch-by-oid from a peer).
6278 async get (key) {
6379 const ptr = await this.statebase.get(key)
64 if (!ptr) return null
80 if (!ptr || isTombstone(ptr)) return null // deleted reads as absent
6581 const { collection, rkey } = split(key)
6682 return this.store.ensureLocal(collection, rkey, ptr && ptr.$oid)
6783 }
……@@ -79,6 +95,7 @@ export class PointerSubstrate {
7995 const s = recSplit(key)
8096 if (!s) continue // reserved/control key — not a record
8197 const ptr = ptrMap[key]
98 if (isTombstone(ptr)) continue // deleted — never resolve bytes for a tombstone
8299 const rec = await this.store.ensureLocal(s.collection, s.rkey, ptr && ptr.$oid)
83100 if (rec != null) out[key] = rec
84101 }