| … | … | @@ -0,0 +1,87 @@ |
| 1 | // PointerSubstrate — the RepoBase KV that restores SYMMETRIC multi-writer convergence to the record |
| 2 | // set while keeping the bytes off the append-only log. A tiny { $oid, size } POINTER per record lives |
| 3 | // in the shared StateBase autobase (so every attested device converges on WHICH records exist, exactly |
| 4 | // like the default statebase path), and the record BYTES live as deduped files in the overlay, |
| 5 | // content-addressed + served by `oid` (blake2b). This is the "pointers in the autobase + bytes in the |
| 6 | // overlay" design: the autobase carries ~50 bytes/record instead of the full record (+ the firehose's |
| 7 | // inline copy), and cross-device convergence is the autobase — no bespoke overlay-to-overlay sync |
| 8 | // (RepoMirror is retired). |
| 9 | // |
| 10 | // RepoBase sees the SAME interface + shape as any substrate: set(key,record) / del(key) / |
| 11 | // get(key)->record|null / list(prefix)->{key:record} / listNow. The pointer indirection is internal: |
| 12 | // list/get RESOLVE each pointer to its record bytes via the overlay store (a local file hit, or a |
| 13 | // fetch-by-oid from a connected peer when this node — e.g. a just-promoted signer — holds only the |
| 14 | // converged pointer). Reserved "__" keys (firehose/admission) never reach here (swarm-device writes |
| 15 | // those to the StateBase directly); we defensively skip any that appear in a raw list. |
| 16 | |
| 17 | const SEP = '/' |
| 18 | |
| 19 | function split (key) { |
| 20 | const k = String(key) |
| 21 | const i = k.indexOf(SEP) |
| 22 | if (i < 0) throw new Error('PointerSubstrate: expected a "collection/rkey" record key, got ' + JSON.stringify(key)) |
| 23 | return { collection: k.slice(0, i), rkey: k.slice(i + 1) } |
| 24 | } |
| 25 | |
| 26 | // Like RepoBase.splitKey: null for a reserved/non-record key (single-segment, or a "__"-prefixed |
| 27 | // collection such as __commit__/<rev>), so a raw statebase.list('') never resolves control data. |
| 28 | function recSplit (key) { |
| 29 | const i = key.indexOf(SEP) |
| 30 | if (i < 0) return null |
| 31 | const collection = key.slice(0, i) |
| 32 | if (collection.startsWith('__')) return null |
| 33 | return { collection, rkey: key.slice(i + 1) } |
| 34 | } |
| 35 | |
| 36 | export class PointerSubstrate { |
| 37 | constructor (statebase, store) { |
| 38 | if (!statebase || typeof statebase.set !== 'function' || typeof statebase.list !== 'function') { |
| 39 | throw new Error('PointerSubstrate: a StateBase (set/get/del/list) is required for the pointer set') |
| 40 | } |
| 41 | if (!store || typeof store.put !== 'function' || typeof store.ensureLocal !== 'function') { |
| 42 | throw new Error('PointerSubstrate: an OverlayRepoStore (put/ensureLocal) is required for the bytes') |
| 43 | } |
| 44 | this.statebase = statebase |
| 45 | this.store = store |
| 46 | } |
| 47 | |
| 48 | // Write the bytes to the overlay (dedup + servable by oid), then the tiny pointer to the autobase. |
| 49 | async set (key, record) { |
| 50 | const { collection, rkey } = split(key) |
| 51 | const { oid, size } = await this.store.put(collection, rkey, record) |
| 52 | await this.statebase.set(key, { $oid: oid, size }) |
| 53 | } |
| 54 | |
| 55 | async del (key) { |
| 56 | const { collection, rkey } = split(key) |
| 57 | await this.statebase.del(key) // pointer tombstone converges symmetrically |
| 58 | try { await this.store.del(collection, rkey) } catch {} |
| 59 | } |
| 60 | |
| 61 | // Resolve the pointer to the record bytes (local file, or fetch-by-oid from a peer). |
| 62 | async get (key) { |
| 63 | const ptr = await this.statebase.get(key) |
| 64 | if (!ptr) return null |
| 65 | const { collection, rkey } = split(key) |
| 66 | return this.store.ensureLocal(collection, rkey, ptr && ptr.$oid) |
| 67 | } |
| 68 | |
| 69 | // The converged pointer set (authoritative) resolved to { key: record }. Skips reserved keys. |
| 70 | async list (prefix = '') { return this._resolve(await this.statebase.list(prefix)) } |
| 71 | async listNow (prefix = '') { |
| 72 | const raw = (typeof this.statebase.listNow === 'function') ? await this.statebase.listNow(prefix) : await this.statebase.list(prefix) |
| 73 | return this._resolve(raw) |
| 74 | } |
| 75 | |
| 76 | async _resolve (ptrMap) { |
| 77 | const out = {} |
| 78 | for (const key of Object.keys(ptrMap)) { |
| 79 | const s = recSplit(key) |
| 80 | if (!s) continue // reserved/control key — not a record |
| 81 | const ptr = ptrMap[key] |
| 82 | const rec = await this.store.ensureLocal(s.collection, s.rkey, ptr && ptr.$oid) |
| 83 | if (rec != null) out[key] = rec |
| 84 | } |
| 85 | return out |
| 86 | } |
| 87 | } |