OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-room commits / b9537c7

state-base: track signer presence + expose holdsSigner() (S1 wiring)
StateBaseV3 now owns a SignerPresence (self = its member key), attaches the presence handshake on every swarm connection (attachSignerPresence — protomux-muxed alongside base.replicate, the keet one-mux-per-conn model), and exposes holdsSigner() / currentSigner() / signerState() for the sign loop. ADDITIVE + PASSIVE: nothing reads holdsSigner() yet, so behavior is unchanged — this only starts tracking who is connected. Regression: multi-device.test.js 2/2 (attested two-device convergence + replication unaffected). attachSignerPresence is public so the multi-DID daemon can attach presence on the shared swarm too. Remaining for S1: swap the sign loop (swarm-device.js sign() gate + device-node sign loop) lease.holds()->holdsSigner(), drop __lease__ writes + the federation heartbeat, with a two-StateBase failover test as the gate. Then version-bump + deploy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
b9537c74bc20a4b5f0204e3aaa9276ad1b9eda97
by plankatron <fleeky@gmail.com> · 2026-07-18T22:37:44.000Z

Changed files (1) +21 -0

lib/state-base.js +21 -0
……@@ -27,6 +27,8 @@ import process from 'bare-process'
2727
2828import hyperdbDefinition from '../spec/state-base/contract/hyperdb/index.js'
2929import dispatch from '../spec/state-base/contract/hyperdispatch/index.js'
30import { SignerPresence } from './signer-presence.js'
31import { attachPresence } from './signer-presence-channel.js'
3032
3133const { Router, encode } = dispatch
3234
……@@ -136,6 +138,11 @@ export class StateBaseV3 {
136138 this._selfKey = opts.selfKey
137139 ? toBuf(opts.selfKey)
138140 : (this._selfKeyPair ? toBuf(this._selfKeyPair.publicKey) : null)
141 // Leak-free single-signer election from live CONNECTION PRESENCE (replaces the __lease__ ts-heartbeat,
142 // which appended to the log every ~8s forever). self = our member key; graceMs = failover blip-tolerance
143 // (default matches the old lease TTL). Fed by attachSignerPresence() on each swarm connection; the sign
144 // loop reads holdsSigner(). Null when we have no member identity (read-only opens).
145 this._presence = this._selfKey ? new SignerPresence({ self: this._selfKey.toString('hex'), graceMs: Number(opts.signerGraceMs) || 15000 }) : null
139146 this._getMembers = typeof opts.getMembers === 'function' ? opts.getMembers : null
140147 // Injectable crypto for membership-gated admission (keet attestation primitives).
141148 // attest: sign localKey‖base.key as our keet identity; verifyAttest: IdentityKey.verify.
……@@ -370,6 +377,7 @@ export class StateBaseV3 {
370377 // (the testy1/AI-Testing-Town "can't write" deadlock). cf. pear-git-v3
371378 // pear-git.js:447 ("corestore.replicate unreliable for writer cores").
372379 try { this.base.replicate(socket) } catch (err) { console.error('[state-base-v3] replicate threw:', err.message) }
380 try { this.attachSignerPresence(socket) } catch {} // ephemeral signer-presence handshake — feeds the election, appends nothing
373381 })
374382 }
375383 this.swarm.join(this.base.discoveryKey)
……@@ -378,6 +386,19 @@ export class StateBaseV3 {
378386 this.base.key.toString('hex').slice(0, 12), this.base.writable)
379387 }
380388
389 // Attach the ephemeral signer-presence handshake to a connection (announce our member key, learn the
390 // peer's → feed the election). Public so the multi-DID daemon can also attach it on the shared swarm;
391 // the single-DID path calls it from the internal connection handler. Returns a detach fn; no-op without a
392 // member identity. Appends NOTHING — presence lives and dies with the connection.
393 attachSignerPresence (conn) { return this._presence ? attachPresence(conn, { selfKey: this._selfKey.toString('hex'), presence: this._presence }) : (() => {}) }
394
395 // Do WE hold the active-signer role right now? Drop-in for the old lease.holds() in the sign loop:
396 // election is a deterministic function of who is CONNECTED (lowest-key effective co-owner), computed in
397 // RAM. A device with no co-owners connected is the sole signer (holds=true). No writes, ever.
398 holdsSigner (now) { return this._presence ? this._presence.holds(now) : true }
399 currentSigner (now) { return this._presence ? this._presence.currentSigner(now) : (this._selfKey ? this._selfKey.toString('hex') : null) }
400 signerState (now) { return this._presence ? this._presence.state(now) : { self: null, signer: null, effective: [], holds: true } }
401
381402 // keet's _autobaseApply commit shape: a per-batch transaction; handlers write
382403 // into it; flush on success / close on error (never insert on the db directly).
383404 async _apply (nodes, view, base) {