OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-store commits / bdc7957

0.0.11: seed overlay paths at ready() so records stay servable across a restart
ready() now seeds the overlay's path maps from the PERSISTED index (hyper-overlay 0.3.9 seedFilePathsFromIndex). Our overlay paths are disk paths relative to root, so the resolver is a plain join. One scan, no hashing, no file reads — and it avoids the protocol's tier-3 full-index re-scan on every content request. This is what was actually broken on the live fleet: a restarted node served only the records it had written since boot, so a co-owner's converged pointer could never resolve its bytes — which GATES SIGNING (the forward-only gate skips while bytes are unresolved). reindexOverlay() is reclassified as the EXPLICIT REPAIR, deliberately not called from ready(): it re-hashes every record file, which is the cold-start cost that made pgh-sidecar take ~190s to bind its socket. It remains the right tool for a store whose INDEX is missing or stale rather than just its in-memory maps — i.e. nodes whose indexes predate this fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bdc7957fc0da37f19a94d76997ec0e361df4302f
by plankatron <fleeky@gmail.com> · 2026-07-25T13:09:47.000Z

Changed files (2) +60 -1

lib/overlay-repo-store.js +59 -0
……@@ -43,9 +43,68 @@ export class OverlayRepoStore {
4343
4444 async ready () {
4545 fs.mkdirSync(this._recordsDir, { recursive: true })
46 // Restore servability across restart. The overlay's path maps are in-memory and only registerFile
47 // fills them, so without this a restarted node serves ONLY records written since boot — every file
48 // still on disk, yet peers get "no connected peer served this oid" (75+ hours of it on wj62), which
49 // blocks signing. Seeding from the PERSISTED index costs one scan with no hashing and no file
50 // reads, and it avoids the protocol's tier-3 full-index re-scan on every content request.
51 // Our overlay paths are disk paths relative to root, so the resolver is a plain join.
52 if (this._overlay && typeof this._overlay.seedFilePathsFromIndex === 'function') {
53 try {
54 const n = await this._overlay.seedFilePathsFromIndex((overlayPath) => join(this._root, overlayPath))
55 if (n) console.log(JSON.stringify({ msg: 'overlay: seeded record paths from the persisted index (servable-by-oid after restart)', did: this._did, seeded: n }))
56 } catch (err) { console.error(JSON.stringify({ msg: 'overlay path seed FAILED — this node may not serve its records by oid until re-registered', did: this._did, error: err.message })) }
57 }
4658 return this
4759 }
4860
61 // EXPLICIT REPAIR — not a boot step. ready() seeds the path maps from the persisted index, which is
62 // the cheap normal path. This is the heavier fallback for a store whose INDEX itself is missing or
63 // stale (records written before overlay indexing existed, or an index rebuilt/lost): it re-hashes
64 // every record file and re-registers it. Needed once on nodes whose indexes predate the seed fix;
65 // deliberately NOT wired into ready(), because re-hashing every file at every boot is the cold-start
66 // cost that made pgh-sidecar take ~190s to bind its socket.
67 //
68 // Re-register every record file with the overlay so peers can fetch it BY OID.
69 //
70 // hyper-overlay's serve path resolves contentHash -> disk path through an IN-MEMORY map
71 // (overlay.js `_contentHashPaths`, populated only by registerFile and handed to protocol.js by
72 // reference). Nothing rebuilds it at boot, and the persisted FileIndex stores { contentHash, size,
73 // mtime } WITHOUT the disk path — so the overlay cannot repair itself; only the consumer knows the
74 // layout. Net effect before this: after a restart a node could serve ONLY the records it had
75 // written since boot ("registering a file makes it servable", overlay.js:15), even though every
76 // file was still on disk.
77 //
78 // That silently breaks cross-node byte resolution, which GATES SIGNING: the forward-only sign gate
79 // skips while a converged pointer's bytes are unresolved. Measured on the live fleet — wj62's
80 // app.bsky.feed.post/3mr6v6r2ofs22 sat unresolvable for 75+ hours while the peer holding it had the
81 // exact bytes on disk (hash verified equal to the pointer's oid), reporting only "no connected peer
82 // served this oid"; both co-owners held different incomplete subsets for the same reason.
83 //
84 // Idempotent and safe to run every boot: registerFile re-asserts an identical entry. Deliberately
85 // passes NO `rev` (a fresh TID would make a re-registration look like a newer write to rev-wins
86 // conflict resolution) and `prepare: false` (the serve path chunks lazily on first request, so we
87 // avoid a second full read of every file at boot).
88 async reindexOverlay () {
89 if (!this._overlay) return { skipped: 'no-overlay' }
90 let registered = 0
91 let failed = 0
92 const files = await this.records()
93 for (const { key, bytes } of files) {
94 const i = key.indexOf('/')
95 const collection = key.slice(0, i)
96 const rkey = key.slice(i + 1)
97 try {
98 const r = await this._overlay.registerFile(this._overlayPath(collection, rkey), this._pathFor(collection, rkey), { size: bytes.length, prepare: false })
99 if (r) registered++; else failed++
100 } catch { failed++ }
101 }
102 if (files.length) {
103 try { console.log(JSON.stringify({ msg: 'overlay reindex: record files re-registered as servable-by-oid', did: this._did, registered, failed, total: files.length })) } catch {}
104 }
105 return { registered, failed, total: files.length }
106 }
107
49108 _pathFor (collection, rkey) {
50109 if (!isSafeSeg(collection) || !isSafeSeg(rkey)) throw new Error(`OverlayRepoStore: unsafe record key "${collection}/${rkey}"`)
51110 return join(this._recordsDir, collection, rkey + EXT)
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "atproto-store",
3 "version": "0.0.10",
3 "version": "0.0.11",
44 "description": "Holepunch-native atproto repo store — records in a classic Hyperbee (collection/rkey -> DAG-CBOR). The P2P home; the atproto-repo module is the bridge serializer over it.",
55 "type": "module",
66 "main": "lib/repo-store.js",