OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-did-peer commits / abfdbbf

0.2.2: DidPeer noRepo mode — follow the firehose only, drop the repo-core replica
noRepo: true opens ONLY the firehose core (verified head + tail ops) and never opens the repo core, so corestore.replicate never syncs it — no duplicate repo-core replica. Records come from a shared content-addressed OverlayRepoStore elsewhere (the "one copy" consumer: favela-web co-located with hpp reads hpp's authoritative overlay files instead of replicating a second copy). getRepo/getRecord/listRecords already guard on !this.repo → null/[]/throw. DidPeerRouter threads noRepo per-DID. test/no-repo.test.mjs proves head verifies + this.repo stays null + firehose replicates. NOTE: test/co-owned.test.mjs is FLAKY in the `npm test` SEQUENCE (testnet/port contention right after router.test) — it passes 3/3 in isolation. (This also means the earlier claim that _staleStrikes broke co-owned was a misdiagnosis of this same flakiness.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
abfdbbf77a7e32927c8f3d89af5ba1b96aa79f5a
by plankatron <fleeky@gmail.com> · 2026-07-24T01:56:36.000Z

Changed files (3) +72 -9

lib/did-peer.js +14 -7
……@@ -45,7 +45,7 @@ export class DidPeer {
4545 // corestore: a Corestore (cores are global by key). did: the DID. verifyKey: its signing pubkey
4646 // (Uint8Array) — REQUIRED to accept a served head. onHead: (head) => void on every NEW verified
4747 // head. onError: optional (err) => void.
48 constructor (corestore, { did, verifyKey, handle = null, onHead = null, onError = null } = {}) {
48 constructor (corestore, { did, verifyKey, handle = null, onHead = null, onError = null, noRepo = false } = {}) {
4949 if (!corestore) throw new Error('DidPeer: corestore required')
5050 if (!did) throw new Error('DidPeer: did required')
5151 if (!verifyKey) throw new Error('DidPeer: verifyKey (DID signing pubkey) required — auth is mandatory')
……@@ -55,6 +55,11 @@ export class DidPeer {
5555 this.handle = handle
5656 this._onHead = onHead
5757 this._onError = onError
58 // noRepo: follow the firehose ONLY (verified head + tail ops) and DO NOT open/replicate the repo core.
59 // Records come from elsewhere (the shared content-addressed OverlayRepoStore, co-located with the
60 // producer). This is the "one copy" consumer: no duplicate repo-core replica. getRepo/getRecord/
61 // listRecords return null/[]/throw (this.repo stays null) — the caller reads records from the store.
62 this._noRepo = !!noRepo
5863 this.repo = null
5964 this.seq = null
6065 // Optional write/session/blob RPC to the peer (peer-signs-over-RPC). Set by DidPeerRouter from the
……@@ -73,12 +78,13 @@ export class DidPeer {
7378 async provide ({ repoKey, firehoseKey } = {}) { return this._openCores(repoKey, firehoseKey) }
7479
7580 async _openCores (repoKey, firehoseKey) {
76 if (this.repo) return
81 if (this.seq) return // firehose is opened in BOTH modes; the idempotency guard (repo may stay null under noRepo)
7782 if (this._opening) return this._opening
7883 this._opening = (async () => {
7984 this._repoKey = toKey(repoKey)
8085 this._firehoseKey = toKey(firehoseKey)
81 this.repo = await new RepoStore(this.corestore, { key: this._repoKey }).ready()
86 // noRepo: skip the repo core entirely — never opened, so corestore.replicate never syncs it (no replica).
87 if (!this._noRepo) this.repo = await new RepoStore(this.corestore, { key: this._repoKey }).ready()
8288 this.seq = await new FirehoseSequencer(this.corestore.get({ key: this._firehoseKey })).ready()
8389 // FULL seed so this node can serve the cores to others (download all + follow appends) — this is
8490 // the "each peer seeds" torrent behaviour; a read-only consumer that does NOT want to seed can
……@@ -89,7 +95,8 @@ export class DidPeer {
8995 // added another append listener, an UNBOUNDED range/listener leak that a re-authoring or ballooning
9096 // producer turned into runaway replication churn (the ~100MB/s "never settles" amplifier).
9197 const seed = (core) => { try { core.download({ start: 0, end: -1 }) } catch {} }
92 seed(this.repo.core); seed(this.seq.core)
98 if (this.repo) seed(this.repo.core)
99 seed(this.seq.core)
93100 // EVENT-DRIVEN: re-derive the served head on every firehose append (local or replicated), and
94101 // re-announce the new length to connected peers. No poll loop.
95102 this.seq.core.on('append', () => { this._reconcile(); this._announceAll() })
……@@ -401,7 +408,7 @@ export class DidPeerRouter {
401408 // verifyKey resolved lazily at front time). Idempotent.
402409 add (did, opts = {}) {
403410 this._fronted.add(did)
404 if (opts.verifyKey || opts.onHead || opts.onError || opts.handle) {
411 if (opts.verifyKey || opts.onHead || opts.onError || opts.handle || opts.noRepo) {
405412 this._opts.set(did, { ...(this._opts.get(did) || {}), ...opts })
406413 }
407414 this._joinDid(did)
……@@ -409,7 +416,7 @@ export class DidPeerRouter {
409416 if (existing) return existing
410417 const o = this._opts.get(did) || {}
411418 if (!o.verifyKey) return this // bridge allow-set add (verifyKey lazy); return value unused/chainable
412 const p = new DidPeer(this.corestore, { did, verifyKey: o.verifyKey, handle: o.handle || null, onHead: o.onHead || null, onError: o.onError || null })
419 const p = new DidPeer(this.corestore, { did, verifyKey: o.verifyKey, handle: o.handle || null, onHead: o.onHead || null, onError: o.onError || null, noRepo: o.noRepo })
413420 this._peers.set(did, p)
414421 for (const chan of this._channels) p.announceVia(chan._announce) // (re-add) announce if already held
415422 return p
……@@ -480,7 +487,7 @@ export class DidPeerRouter {
480487 const boundFh = peer._firehoseKey ? b4a.toString(peer._firehoseKey, 'hex') : null
481488 if (boundFh !== fhKeyHex) peer = null // primary already bound to a different co-owner -> new front
482489 }
483 if (!peer) peer = new DidPeer(this.corestore, { did, verifyKey, handle: entry.coords.handle || o.handle || null, onHead: o.onHead || null, onError: o.onError || null })
490 if (!peer) peer = new DidPeer(this.corestore, { did, verifyKey, handle: entry.coords.handle || o.handle || null, onHead: o.onHead || null, onError: o.onError || null, noRepo: o.noRepo })
484491 if (entry.coords.handle) peer.handle = entry.coords.handle
485492 if (this._useRpc && entry.rpc) peer.rpc = entry.rpc
486493 await peer.provide({ repoKey: entry.coords.repoKey, firehoseKey: entry.coords.firehoseKey })
package.json +2 -2
……@@ -1,6 +1,6 @@
11{
22 "name": "atproto-did-peer",
3 "version": "0.2.1",
3 "version": "0.2.2",
44 "description": "Keyless, direction-free P2P reader/seeder for an atproto DID's repo+firehose Hypercores: announce-push discovery + event-driven head reconciliation + a keyless read surface. The shared primitive beneath atproto-bridge, atproto-relay and favela (extracted from atproto-bridge did-peer.js).",
55 "type": "module",
66 "main": "index.js",
……@@ -8,7 +8,7 @@
88 ".": "./index.js"
99 },
1010 "scripts": {
11 "test": "node test/commitops.test.mjs && node test/router.test.mjs && node test/co-owned.test.mjs",
11 "test": "node test/commitops.test.mjs && node test/router.test.mjs && node test/co-owned.test.mjs && node test/no-repo.test.mjs",
1212 "test:live": "node test/read-live.mjs"
1313 },
1414 "dependencies": {
test/no-repo.test.mjs +56 -0
……@@ -0,0 +1,56 @@
1// noRepo mode: a DidPeer follows the firehose ONLY (verified head + tail) and does NOT open/replicate the
2// repo core — the "one copy" consumer (records come from a shared OverlayRepoStore elsewhere). Proves the
3// head still verifies via the firehose, this.repo stays null, and the repo core is never opened.
4// Run: node test/no-repo.test.mjs
5import { mkdtempSync, rmSync } from 'node:fs'
6import { join } from 'node:path'
7import { tmpdir } from 'node:os'
8import createTestnet from '@hyperswarm/testnet'
9import Hyperswarm from 'hyperswarm'
10import { RepoStore } from 'atproto-store/lib/repo-store.js'
11import { RepoManager } from 'atproto-store/lib/repo-manager.js'
12import { FirehoseSequencer } from 'atproto-store/lib/sequencer.js'
13import { openCorestore } from 'atproto-store/lib/corestore.js'
14import { createAtprotoIdentityFromKey } from 'atproto-identity/lib/atproto-identity.js'
15import { discoveryTopic } from 'atproto-identity/lib/discovery.js'
16import { DidPeer } from '../index.js'
17
18const assert = (c, m) => { if (!c) { console.error('FAIL:', m); process.exit(1) } console.log(' ok -', m) }
19const iso = '2026-07-01T00:00:00.000Z'
20const id = createAtprotoIdentityFromKey(new Uint8Array(32).fill(5), { handle: 'norepo.test', pdsUrl: 'https://pds.local' })
21const testnet = await createTestnet(3)
22
23// A holder serves a signed repo + firehose.
24const hDir = mkdtempSync(join(tmpdir(), 'norepo-holder-'))
25const hStore = openCorestore(hDir)
26const repo = await new RepoStore(hStore, { name: `atproto-repo:${id.did}` }).ready()
27let micros = 1_700_000_000_000_000n
28const mgr = await new RepoManager(repo, { did: id.did, privateKey: id.privateKey, clock: () => (micros += 1_000_000n) }).init()
29const seq = await new FirehoseSequencer(hStore.get({ name: 'firehose' })).ready()
30const w = await mgr.applyWrite({ action: 'create', collection: 'app.bsky.feed.post', rkey: '1', record: { $type: 'app.bsky.feed.post', text: 'hi', createdAt: iso } })
31await seq.sequenceCommit(w, { time: iso })
32const holder = new DidPeer(hStore, { did: id.did, verifyKey: id.publicKey })
33await holder.provide({ repoKey: repo.core.key, firehoseKey: seq.core.key })
34const hSwarm = new Hyperswarm({ bootstrap: testnet.bootstrap })
35hSwarm.on('connection', (c) => holder.attach(c))
36await hSwarm.join(discoveryTopic(id.did), { server: true, client: false }).flushed()
37
38// A noRepo reader: firehose-only, no repo core.
39const rStore = openCorestore(mkdtempSync(join(tmpdir(), 'norepo-reader-')))
40const rSwarm = new Hyperswarm({ bootstrap: testnet.bootstrap })
41const reader = new DidPeer(rStore, { did: id.did, verifyKey: id.publicKey, noRepo: true })
42rSwarm.on('connection', (c) => reader.attach(c))
43await rSwarm.join(discoveryTopic(id.did), { server: false, client: true }).flushed()
44
45const head = await reader.ready({ timeout: 20000 })
46assert(!!head, 'noRepo reader reached a VERIFIED head via the firehose (no repo core)')
47assert(reader.repo === null, 'repo core was NEVER opened (this.repo === null) — no repo-core replica')
48assert(reader.seq && reader.seq.core.length >= 1, 'firehose core IS opened + replicated')
49assert((await reader.listRecords('app.bsky.feed.post')).length === 0, 'listRecords returns [] (records come from the shared store, not the repo core)')
50assert((await reader.getRecord('app.bsky.feed.post', '1')) === null, 'getRecord returns null in noRepo mode')
51
52console.log('\nNO-REPO OK: firehose-only consumer verifies the head + follows the tail with NO repo-core replica')
53await rSwarm.destroy(); await reader.close(); await rStore.close()
54await hSwarm.destroy(); await holder.close(); await seq.close(); await repo.close(); await hStore.close()
55await testnet.destroy()
56rmSync(hDir, { recursive: true, force: true })