OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-room commits / 81b3673

serve-membrane: gate sync() on new commits — stop the repo-core storage leak
projectRepo re-put ALL of the device's records into the append-only membrane repo Hyperbee on EVERY sync(). The hpp sign loop calls sync() every signIntervalMs (~2s) UNCONDITIONALLY, so with zero new commits the repo core still grew by one full record-set per tick — ~21 blocks/s → 1.3 MILLION blocks / ~1.2 GB in hours. A keyless membrane consumer (favela-web) then re-replicated that bloated core and re-wrote it continuously (~100 MB/s, 200% CPU, 159 GB written in 30 min). projectFirehose was already incremental (from seq.length); projectRepo was not, and the "idempotent, cheap" comment on the sign loop was simply false. Fix: sync() now reads the __commit__ log once and returns early when it has not advanced past the membrane firehose (log.length <= seq.length) — projecting only when there is a genuinely new commit. Regression test (hyper-proto-peer): 25 no-op syncs do not grow the repo core; a real new record still projects. NOTE: this stops further growth; the already-ballooned membrane repo core must be rebuilt separately (it is a rebuildable projection, not the signed statebase). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
81b3673ade8599ac42aadbe6756f2185339ae89d
by plankatron <fleeky@gmail.com> · 2026-07-17T20:14:27.000Z

Changed files (2) +17 -5

lib/serve-membrane.js +15 -3
……@@ -47,8 +47,8 @@ async function projectRepo (device, repo) {
4747
4848// Append any NOT-yet-projected __commit__ frames into the FirehoseSequencer, in seq order. Idempotent
4949// (only entries with seq > sequencer.length). Deterministic across devices (same __commit__ order).
50async function projectFirehose (device, seq) {
51 const log = await device.firehoseLog() // seq-ordered
50async function projectFirehose (device, seq, log = null) {
51 if (!log) log = await device.firehoseLog() // seq-ordered
5252 for (let i = seq.length; i < log.length; i++) {
5353 const write = deserializeEntry(log[i])
5454 // time is derived deterministically from the rev so every device emits an identical frame
……@@ -75,7 +75,19 @@ export async function serveMembrane (device, { corestore, swarm, did, handle, se
7575 if (!corestore || !swarm || !did) throw new Error('serveMembrane: corestore, swarm, did required')
7676 const repo = await new RepoStore(corestore, { name: 'membrane-repo:' + did }).ready()
7777 const seq = await new FirehoseSequencer(corestore.get({ name: 'membrane-firehose:' + did })).ready()
78 const sync = async () => { await projectRepo(device, repo); await projectFirehose(device, seq) }
78 // Project ONLY when the device's __commit__ log has advanced past what the membrane firehose already
79 // holds. WITHOUT this gate, the caller's sign loop (every signIntervalMs, ~2s) called sync()
80 // UNCONDITIONALLY, and projectRepo re-put ALL records into the APPEND-ONLY Hyperbee repo core on EVERY
81 // tick — even with zero new commits — growing it ~one record-set / 2s → 1.3M blocks / GB in hours, a
82 // runaway storage+CPU leak (favela then re-replicates the bloated core at ~100 MB/s). projectFirehose
83 // was already incremental; projectRepo was not. After a full projection seq.length === log.length, so
84 // `log.length <= seq.length` means there is nothing new to project — skip both (read-only, no writes).
85 const sync = async () => {
86 const log = await device.firehoseLog()
87 if (log.length <= seq.length) return
88 await projectRepo(device, repo)
89 await projectFirehose(device, seq, log)
90 }
7991 await sync()
8092
8193 const serverKey = b4a.toString(swarm.keyPair.publicKey, 'hex')
package.json +2 -2
……@@ -1,7 +1,7 @@
11{
22 "name": "atproto-room",
3 "version": "0.2.3",
4 "description": "Attestation-gated, encrypted multi-writer room engine \u2014 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.",
3 "version": "0.2.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.",
55 "type": "module",
66 "main": "index.js",
77 "scripts": {