| … | … | @@ -16,6 +16,12 @@ |
| 16 | 16 | |
| 17 | 17 | const SEP = '/' |
| 18 | 18 | |
| 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. |
| 23 | const isTombstone = (v) => !!(v && typeof v === 'object' && v.$deleted === true) |
| 24 | |
| 19 | 25 | function split (key) { |
| 20 | 26 | const k = String(key) |
| 21 | 27 | const i = k.indexOf(SEP) |
| … | … | @@ -46,22 +52,32 @@ export class PointerSubstrate { |
| 46 | 52 | } |
| 47 | 53 | |
| 48 | 54 | // 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.) |
| 49 | 58 | async set (key, record) { |
| 50 | 59 | 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 | } |
| 51 | 65 | const { oid, size } = await this.store.put(collection, rkey, record) |
| 52 | 66 | await this.statebase.set(key, { $oid: oid, size }) |
| 53 | 67 | } |
| 54 | 68 | |
| 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. |
| 55 | 71 | async del (key) { |
| 56 | 72 | const { collection, rkey } = split(key) |
| 57 | | await this.statebase.del(key) // pointer tombstone converges symmetrically |
| 73 | await this.statebase.del(key) |
| 58 | 74 | try { await this.store.del(collection, rkey) } catch {} |
| 59 | 75 | } |
| 60 | 76 | |
| 61 | 77 | // Resolve the pointer to the record bytes (local file, or fetch-by-oid from a peer). |
| 62 | 78 | async get (key) { |
| 63 | 79 | const ptr = await this.statebase.get(key) |
| 64 | | if (!ptr) return null |
| 80 | if (!ptr || isTombstone(ptr)) return null // deleted reads as absent |
| 65 | 81 | const { collection, rkey } = split(key) |
| 66 | 82 | return this.store.ensureLocal(collection, rkey, ptr && ptr.$oid) |
| 67 | 83 | } |
| … | … | @@ -79,6 +95,7 @@ export class PointerSubstrate { |
| 79 | 95 | const s = recSplit(key) |
| 80 | 96 | if (!s) continue // reserved/control key — not a record |
| 81 | 97 | const ptr = ptrMap[key] |
| 98 | if (isTombstone(ptr)) continue // deleted — never resolve bytes for a tombstone |
| 82 | 99 | const rec = await this.store.ensureLocal(s.collection, s.rkey, ptr && ptr.$oid) |
| 83 | 100 | if (rec != null) out[key] = rec |
| 84 | 101 | } |