| … | … | @@ -114,7 +114,15 @@ export function commitLine (frames) { |
| 114 | 114 | // frame has a rev at or above it, and every fork frame below it is either one it knew or one an earlier |
| 115 | 115 | // effective heal covered. Forks covered by an effective heal are done; any other fork reopens a window. |
| 116 | 116 | // |
| 117 | // Returns null (nothing to heal) or { from, paths: Set, heals: [commitCid] }. | |
| 117 | // SPLIT HEALS. indigo drops a #commit with more than 200 ops (verify.go MaxCommitOps), so a window wider | |
| 118 | // than that is restated over several commits: partial ones carry `restates` (the window's fork-point | |
| 119 | // rev), the last carries `asserts`, `heals` and `partsFrom` (the first partial's rev). A partial's paths | |
| 120 | // are not restated again — unless a fork frame reaches its rev, because then a consumer may have taken | |
| 121 | // the fork and skipped it. For the same reason a split heal is effective only if no fork frame reaches | |
| 122 | // its FIRST part. | |
| 123 | // | |
| 124 | // Returns null (nothing to heal) or { from, paths: Set (still to restate), window: Set (every path of the | |
| 125 | // window), heals: [commitCid], partsFrom }. | |
| 118 | 126 | export function healPlan ({ line, losers }) { |
| 119 | 127 | if (!losers || losers.length === 0) return null |
| 120 | 128 | const all = line.concat(losers) |
| … | … | @@ -133,10 +141,11 @@ export function healPlan ({ line, losers }) { |
| 133 | 141 | for (const h of line) { |
| 134 | 142 | if (h.asserts === undefined) continue |
| 135 | 143 | const knew = new Set((h.heals || []).map(String)) |
| 144 | const reach = h.partsFrom || h.rev | |
| 136 | 145 | let effective = true |
| 137 | 146 | for (const l of losers) { |
| 138 | 147 | const c = String(l.commitCid) |
| 139 | if (l.rev >= h.rev) { effective = false; break } | |
| 148 | if (l.rev >= reach) { effective = false; break } | |
| 140 | 149 | if (!covered.has(c) && !knew.has(c)) { effective = false; break } |
| 141 | 150 | } |
| 142 | 151 | if (effective) for (const c of knew) covered.add(c) |
| … | … | @@ -148,8 +157,20 @@ export function healPlan ({ line, losers }) { |
| 148 | 157 | for (const l of open) { const r = forkPointRev(l); if (from === null || r < from) from = r } |
| 149 | 158 | const paths = new Set() |
| 150 | 159 | for (const f of all) if (f.rev > from) for (const op of (f.ops || [])) paths.add(op.path) |
| 160 | const window = new Set(paths) | |
| 161 | // Partials of THIS window that no fork reaches: their paths are done. | |
| 162 | let maxLoserRev = '' | |
| 163 | for (const l of losers) if (l.rev > maxLoserRev) maxLoserRev = l.rev | |
| 164 | let partsFrom = null | |
| 165 | for (const f of line) { | |
| 166 | if (f.restates === undefined || f.restates !== from || f.rev <= maxLoserRev) continue | |
| 167 | if (partsFrom === null || f.rev < partsFrom) partsFrom = f.rev | |
| 168 | // Only update/delete restate a path for every consumer; a `create` in a partial is an ordinary diff op | |
| 169 | // the reference AppView ignores over a stale copy, so that path is still owed. | |
| 170 | for (const op of (f.ops || [])) if (op.action !== 'create') paths.delete(op.path) | |
| 171 | } | |
| 151 | 172 | const heals = losers.filter((l) => l.rev > from).map((l) => String(l.commitCid)) |
| 152 | return { from, paths, heals } | |
| 173 | return { from, paths, window, heals, partsFrom } | |
| 153 | 174 | } |
| 154 | 175 | |
| 155 | 176 | // path -> record CID string after replaying `frames` in order onto `start` (a Map, copied). Uses only the |
| … | … | @@ -1,6 +1,6 @@ |
| 1 | 1 | { |
| 2 | 2 | "name": "atproto-room", |
| 3 | "version": "0.2.24", | |
| 3 | "version": "0.2.25", | |
| 4 | 4 | "description": "Attestation-gated, encrypted multi-writer room engine — StateBase (keet-model admission) + Lease + signer + RepoBase + LabelBase/LabelRoom + serve-membrane. Extracted from hyper-proto-peer; consumed by the atproto peer (repo devices) and the co-authored labeler.", |
| 5 | 5 | "type": "module", |
| 6 | 6 | "main": "index.js", |
| … | … | @@ -131,3 +131,33 @@ test('healPlan: a fork the heal never KNEW (arrived late, below it) reopens the |
| 131 | 131 | t.ok(plan && plan.paths.has('p/late'), 'the late fork\'s paths are restated') |
| 132 | 132 | t.alike(plan.heals.sort(), ['cB', 'cLate']) |
| 133 | 133 | }) |
| 134 | ||
| 135 | const upd = (path) => ({ action: 'update', path, cid: 'y' }) | |
| 136 | ||
| 137 | test('healPlan: a partial heal\'s update/delete paths are done; its CREATE paths are still owed', async (t) => { | |
| 138 | const log = [f('3a', 'c1', null), f('3b', 'cA', 'c1', { ops: [op('p/a'), op('p/c')] }), f('3b', 'cB', 'c1', { ops: [op('p/b')] }), | |
| 139 | f('3c', 'part1', 'cA', { ops: [upd('p/a'), op('p/c')], restates: '3a' })] | |
| 140 | const plan = healPlan(commitLine(log)) | |
| 141 | t.alike([...plan.paths].sort(), ['p/b', 'p/c'], 'p/a restated by update; p/c only by a create, which a stale consumer ignores') | |
| 142 | t.is(plan.partsFrom, '3c') | |
| 143 | t.ok(plan.window.has('p/a'), 'the window still names every path') | |
| 144 | }) | |
| 145 | ||
| 146 | test('healPlan: a fork reaching a partial means it may have been skipped — its paths are owed again', async (t) => { | |
| 147 | const log = [f('3a', 'c1', null), f('3b', 'cA', 'c1', { ops: [op('p/a')] }), f('3b', 'cB', 'c1', { ops: [op('p/b')] }), | |
| 148 | f('3c', 'part1', 'cA', { ops: [upd('p/a')], restates: '3a' }), f('3c', 'zSib', 'cA', { ops: [op('p/z')] })] | |
| 149 | const plan = healPlan(commitLine(log)) | |
| 150 | t.ok(plan.paths.has('p/a'), 'restated again') | |
| 151 | t.is(plan.partsFrom, null) | |
| 152 | }) | |
| 153 | ||
| 154 | test('healPlan: a split heal is effective only if no fork reaches its FIRST part', async (t) => { | |
| 155 | const base = [f('3a', 'c1', null), f('3b', 'cA', 'c1', { ops: [op('p/a')] }), f('3b', 'cB', 'c1', { ops: [op('p/b')] }), | |
| 156 | f('3c', 'part1', 'cA', { ops: [upd('p/a')], restates: '3a' }), | |
| 157 | f('3d', 'final', 'part1', { ops: [upd('p/b')], asserts: '3a', heals: ['cB'], partsFrom: '3c' })] | |
| 158 | t.is(healPlan(commitLine(base)), null, 'closed') | |
| 159 | // A fork at the first part's rev, known to the final: a consumer may have skipped part1. | |
| 160 | const forked = base.concat([f('3c', 'zFork', 'cA', { ops: [op('p/z')] })]) | |
| 161 | forked[4] = f('3d', 'final', 'part1', { ops: [upd('p/b')], asserts: '3a', heals: ['cB', 'zFork'], partsFrom: '3c' }) | |
| 162 | t.ok(healPlan(commitLine(forked)), 'reopened') | |
| 163 | }) |