OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-room commits / f943fd2

state-base: fail-fast when the device is banned (removed) instead of stalling the doorbell
A device whose identity is in the durable, replicated __removed__ set can never be re-admitted (the in-apply admission guard rejects its every proof), so ringing the block-until-admitted doorbell for the full timeout is pointless — it made every write a banned device attempts stall ~timeoutMs before throwing (8 post-ban writes x ~20s each in hyper-proto-peer ban.test.js, which had been masked by brittle's 30s default cutting the test off earlier). requestWriter now checks _isSelfRemoved() up front and throws immediately with a clear reason. Best-effort: false on any uncertainty, so a non-banned device is never wrongly rejected — worst case falls back to the normal doorbell. Verified: atproto-room bare suite 26/26; hyper-proto-peer ban.test.js 5/5 in ~14s (was hanging past 180s). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
f943fd29d0d4d0e5533e91803aa01bcffd181d45
by plankatron <plankatron@users.noreply.github.com> · 2026-07-14T21:31:05.000Z

Changed files (1) +24 -0

lib/state-base.js +24 -0
……@@ -421,6 +421,13 @@ export class StateBaseV3 {
421421 const wantIndexer = claimIndexer || startIndexing
422422 const done = () => this.base.writable && (!wantIndexer || this._isLocalIndexer() === true)
423423 if (done()) return { writable: true, already: true, indexer: this._isLocalIndexer() }
424 // FAIL FAST if this device has been BANNED. Its identity is in the durable, replicated
425 // __removed__ set, so the admission guard (_verifyAdmissionInApply) will reject every admit
426 // proof it ever sends — ringing the doorbell for the full timeoutMs is pointless and makes every
427 // write a removed device attempts stall ~timeoutMs before throwing. Throw immediately instead.
428 // Best-effort (see _isSelfRemoved): false on any uncertainty, so a non-banned device is never
429 // wrongly rejected — it just falls back to the normal doorbell.
430 if (await this._isSelfRemoved()) throw new Error('StateBaseV3: this device has been removed (banned) — cannot become a writer')
424431 const admit = await this._buildAdmissionProof()
425432 if (!admit) throw new Error('StateBaseV3 cannot request writer: no membership/identity to build an admission proof')
426433 // key MUST be a string (encoder always writes it; undefined crashes compact-
……@@ -622,6 +629,23 @@ export class StateBaseV3 {
622629 return false
623630 }
624631
632 // Out-of-apply self-ban check: is THIS device's own identity in the durable __removed__ set (a ban
633 // that has replicated to us)? Read against the linearized view exactly like getProjectedRole. Used
634 // by requestWriter to fail fast instead of ringing a doorbell the admission guard will never
635 // answer. Best-effort: returns false on any uncertainty (no selfKey / view not ready / parse
636 // error), so it can never wrongly reject a non-banned device — worst case falls back to the
637 // normal doorbell timeout.
638 async _isSelfRemoved () {
639 try {
640 if (!this._ready || !this.base || !this._selfKey) return false
641 await this.base.update()
642 const rrec = await this.base.view.get(ENTRIES, { key: REMOVED_SET })
643 const removed = rrec && rrec.value ? JSON.parse(rrec.value) : {}
644 const hex = this._selfKey.toString('hex')
645 return !!(removed[hex] || removed[normKeyHex(hex)])
646 } catch { return false }
647 }
648
625649 // ── Membership projection (cross-base sync) ────────────────
626650 // Daemon's out-of-apply watcher copies keet-room membership in so apply can
627651 // gate admission deterministically. Stored under the reserved key __members__