| … | … | @@ -143,9 +143,23 @@ export class PointerSubstrate { |
| 143 | 143 | // Nothing is deleted (statebase.set appends; the old value stays in oplog history) and files are |
| 144 | 144 | // never modified beyond a byte-identical rewrite. WRITABLE-ONLY: the caller gates on base.writable. |
| 145 | 145 | // Returns { converted, alreadyPointer, tombstones, fromInline, mismatched, failed, total }. |
| 146 | // TWO PASSES, and the order is a SAFETY property, not tidiness. |
| 147 | // |
| 148 | // PASS 1 MATERIALIZES every inline record to a file. PASS 2 publishes the pointers. Interleaving them |
| 149 | // (put/set per record, as this did originally) leaves the set only partly protected while it runs. |
| 150 | // |
| 151 | // Why that matters: a legacy repo being flipped to this substrate reads as an EMPTY record set (_resolve |
| 152 | // has no $oid and no file to fall back to), and sign()'s anti-wipe guard aborts only when record FILES |
| 153 | // exist. With no files it does not fire, and the first sign publishes a delete-every-record commit. |
| 154 | // Materializing the whole set FIRST arms that guard before a single autobase entry is touched — so if |
| 155 | // pass 2 never runs (crash, lost writability, autobase error), the repo is protected rather than wiped. |
| 156 | // |
| 157 | // Pass 1 is also purely local: content-addressed files, nothing published, no writability needed, and |
| 158 | // nothing to roll back. Pass 2 is the only part that appends. |
| 146 | 159 | async migrateInline () { |
| 147 | 160 | const raw = await this.statebase.list('') |
| 148 | 161 | let converted = 0; let alreadyPointer = 0; let tombstones = 0; let fromInline = 0; let mismatched = 0; let failed = 0; let total = 0 |
| 162 | const staged = [] |
| 149 | 163 | for (const key of Object.keys(raw)) { |
| 150 | 164 | const s = recSplit(key) |
| 151 | 165 | if (!s) continue // reserved/control key — never part of the record pointer set |
| … | … | @@ -162,11 +176,14 @@ export class PointerSubstrate { |
| 162 | 176 | const { oid, size } = await this.store.put(s.collection, s.rkey, record) |
| 163 | 177 | if (!oid) { failed++; continue } // no overlay → no oid to publish; leave the entry alone |
| 164 | 178 | if (before && oid !== before.oid) { mismatched++; continue } // would change published content — refuse |
| 165 | | await this.statebase.set(key, { $oid: oid, size }) |
| 166 | | converted++ |
| 179 | staged.push({ key, oid, size }) |
| 167 | 180 | } catch { failed++ } |
| 168 | 181 | } |
| 169 | | return { converted, alreadyPointer, tombstones, fromInline, mismatched, failed, total } |
| 182 | const materialized = staged.length |
| 183 | for (const { key, oid, size } of staged) { |
| 184 | try { await this.statebase.set(key, { $oid: oid, size }); converted++ } catch { failed++ } |
| 185 | } |
| 186 | return { converted, materialized, alreadyPointer, tombstones, fromInline, mismatched, failed, total } |
| 170 | 187 | } |
| 171 | 188 | |
| 172 | 189 | async backfill () { |