| … | … | @@ -23,11 +23,14 @@ export const PRESENCE_PROTOCOL = 'atproto-room/signer-presence/v1' |
| 23 | 23 | // can tell whether WE are caught up (currency). Returns { send(headRev?), close() }: `send` RE-announces |
| 24 | 24 | // our head when it changes (the caller wires this to head advances) so a stale head can't mis-elect a |
| 25 | 25 | // signer; `close` tears the channel down. (A non-object return would break callers that register `send`.) |
| 26 | export function attachPresence (conn, { selfKey, presence, id = null, getHead = null } = {}) { | |
| 26 | // onReceive: optional (peerKey) => void, called on every received announce (a diagnostic hook). | |
| 27 | export function attachPresence (conn, { selfKey, presence, id = null, getHead = null, onReceive = null } = {}) { | |
| 27 | 28 | // Returns a DETACH function (call it to tear down — the original contract) with a `.send(headRev)` |
| 28 | // property for re-announcing our head when it changes. Hybrid so existing callers that use the return | |
| 29 | // as a cleanup fn keep working, while the currency wiring calls `.send`. | |
| 30 | const noop = () => {}; noop.send = () => {} | |
| 29 | // property for re-announcing our head when it changes, and `.attached`: whether this call actually got the | |
| 30 | // channel. ⛔ A duplicate (protocol, id) on a mux is a deliberate no-op — and was SILENT: 2026-10-14 loser-club's | |
| 31 | // co-owners each saw only themselves while wj62's presence over the same connections worked, and nothing | |
| 32 | // could say whether an attach had been a no-op. `.attached === false` says so. | |
| 33 | const noop = () => {}; noop.send = () => {}; noop.attached = false | |
| 31 | 34 | if (!conn || !selfKey || !presence) return noop |
| 32 | 35 | const self = String(selfKey) |
| 33 | 36 | let peerKey = null |
| … | … | @@ -41,6 +44,7 @@ export function attachPresence (conn, { selfKey, presence, id = null, getHead = |
| 41 | 44 | onmessage: (data) => { |
| 42 | 45 | if (!data || !data.key) return |
| 43 | 46 | peerKey = String(data.key) |
| 47 | if (onReceive) { try { onReceive(peerKey) } catch {} } | |
| 44 | 48 | presence.markPresent(peerKey) // presence (fires onChange on connect) |
| 45 | 49 | if (data.headRev !== undefined) presence.noteHead(peerKey, data.headRev || null) // currency (fires onChange on a head change) |
| 46 | 50 | } |
| … | … | @@ -52,6 +56,7 @@ export function attachPresence (conn, { selfKey, presence, id = null, getHead = |
| 52 | 56 | conn.on('close', onclose) |
| 53 | 57 | const detach = () => { if (done) return; done = true; try { conn.off('close', onclose) } catch {}; clear() } |
| 54 | 58 | detach.send = (headRev) => announce(headRev) // re-announce our head (currency) when it changes |
| 59 | detach.attached = true | |
| 55 | 60 | return detach |
| 56 | 61 | } catch { return noop } |
| 57 | 62 | } |
| … | … | @@ -520,7 +520,18 @@ export class StateBaseV3 { |
| 520 | 520 | // first opens). Single-DID and multi-DID both derive the same roomKey per DID, so peers of a DID match. |
| 521 | 521 | attachSignerPresence (conn) { |
| 522 | 522 | if (!this._presence) return () => {} |
| 523 | const handle = attachPresence(conn, { selfKey: this._selfKey.toString('hex'), presence: this._presence, id: this._roomKey, getHead: () => this._selfHead }) | |
| 523 | // Diagnostic counts (presenceDiag): attaches that got the channel, attaches that were a silent duplicate | |
| 524 | // no-op, and announces received per peer — "channel taken" vs "peer never sends", told apart by a read. | |
| 525 | const d = this._presenceDiagState || (this._presenceDiagState = { attached: 0, duplicate: 0, received: 0, peers: {} }) | |
| 526 | const handle = attachPresence(conn, { | |
| 527 | selfKey: this._selfKey.toString('hex'), | |
| 528 | presence: this._presence, | |
| 529 | id: this._roomKey, | |
| 530 | getHead: () => this._selfHead, | |
| 531 | onReceive: (k) => { d.received++; d.peers[String(k).slice(0, 16)] = Date.now() } | |
| 532 | }) | |
| 533 | if (handle.attached) d.attached++ | |
| 534 | else d.duplicate++ | |
| 524 | 535 | this._presenceSenders.add(handle.send) |
| 525 | 536 | return () => { this._presenceSenders.delete(handle.send); try { handle() } catch {} } // handle() detaches |
| 526 | 537 | } |
| … | … | @@ -542,6 +553,12 @@ export class StateBaseV3 { |
| 542 | 553 | holdsSigner (now, netHead = null) { return this._presence ? this._presence.holds(now, netHead) : true } |
| 543 | 554 | currentSigner (now, netHead = null) { return this._presence ? this._presence.currentSigner(now, netHead) : (this._selfKey ? this._selfKey.toString('hex') : null) } |
| 544 | 555 | signerState (now, netHead = null) { return this._presence ? this._presence.state(now, netHead) : { self: null, signer: null, effective: [], holds: true } } |
| 556 | // Presence wiring diagnostics (see attachSignerPresence). Read-only; null without a presence election. | |
| 557 | presenceDiag () { | |
| 558 | if (!this._presence) return null | |
| 559 | const d = this._presenceDiagState || { attached: 0, duplicate: 0, received: 0, peers: {} } | |
| 560 | return { attached: d.attached, duplicate: d.duplicate, received: d.received, peers: { ...d.peers } } | |
| 561 | } | |
| 545 | 562 | |
| 546 | 563 | // keet's _autobaseApply commit shape: a per-batch transaction; handlers write |
| 547 | 564 | // into it; flush on success / close on error (never insert on the db directly). |
| … | … | @@ -1,6 +1,6 @@ |
| 1 | 1 | { |
| 2 | 2 | "name": "atproto-room", |
| 3 | "version": "0.2.31", | |
| 3 | "version": "0.2.32", | |
| 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", |
| … | … | @@ -40,3 +40,26 @@ test('presence handshake over a real connection: elect lowest-key signer, fail o |
| 40 | 40 | t.ok(spB.holds(), 'after grace, B fails over to signer') |
| 41 | 41 | t.is(spB.currentSigner(), B, 'B is now the sole signer') |
| 42 | 42 | }) |
| 43 | ||
| 44 | // ⛔ A SILENT NO-OP MUST BE VISIBLE. 2026-10-14: loser-club's two co-owners each saw ONLY themselves present | |
| 45 | // (each elected itself → two signers) while wj62's presence over the same connections worked. A second attach of | |
| 46 | // the same (protocol, id) on a mux is a no-op by design — if a stale StateBase holds the channel, the live one's | |
| 47 | // attach does nothing and nothing says so. The handle now reports whether it attached, and every received | |
| 48 | // announce is reported, so "channel taken" and "peer never sends" are told apart by a read. | |
| 49 | test('attach reports whether it got the channel; a duplicate (protocol, id) reports false; receipts are reported', async (t) => { | |
| 50 | const spA = new SignerPresence({ self: A }) | |
| 51 | const spB = new SignerPresence({ self: B }) | |
| 52 | const a = new SecretStream(true) | |
| 53 | const b = new SecretStream(false) | |
| 54 | a.rawStream.pipe(b.rawStream).pipe(a.rawStream) | |
| 55 | const id = Buffer.alloc(32, 7) | |
| 56 | const gotA = [] | |
| 57 | const first = attachPresence(a, { selfKey: A, presence: spA, id, onReceive: (k) => gotA.push(k) }) | |
| 58 | const dup = attachPresence(a, { selfKey: A, presence: spA, id }) | |
| 59 | const other = attachPresence(b, { selfKey: B, presence: spB, id }) | |
| 60 | t.teardown(() => { first(); dup(); other(); try { a.destroy() } catch {} try { b.destroy() } catch {} }) | |
| 61 | t.is(first.attached, true, 'the first attach got the channel') | |
| 62 | t.is(dup.attached, false, 'a second attach of the same (protocol, id) on this mux is a no-op — and says so') | |
| 63 | t.is(other.attached, true) | |
| 64 | t.ok(await waitFor(() => gotA.includes(B)), 'the peer\'s announce is reported to onReceive') | |
| 65 | }) |