// followers.js — the follower collection.
//
// ⚠ M1 SCOPE: a JSON file. DESIGN.md §3 says this state belongs in HyperDB (the delivery queue
// needs secondary indexes by host/status/next-retry, which a flat file cannot give), and §7 says
// it eventually lives on the virtual-ap-device so it replicates across the user's own devices.
// This exists so M1 can prove the federation loop without also building the storage layer —
// replace it before there is anything worth losing.
//
// Followers are the one piece of inbox state that is NOT reconstructible: if we lose the list we
// stop delivering to people who still believe they follow us, and nothing tells them. Treat it as
// durable even while the backend is this crude.
import fs from 'bare-fs'
import { dirname } from 'bare-path'
export class FollowerStore {
constructor (path) {
this._path = path
this._set = new Set()
this._loaded = false
}
_load () {
if (this._loaded) return
try {
const raw = fs.readFileSync(this._path, 'utf8')
for (const uri of JSON.parse(raw)) this._set.add(uri)
} catch {
// absent or unreadable: start empty. A corrupt file must not stop the daemon booting —
// but it IS logged by the caller, because silently starting with zero followers is the
// failure mode that loses people quietly.
}
this._loaded = true
}
_flush () {
fs.mkdirSync(dirname(this._path), { recursive: true })
// write-then-rename so a crash mid-write can't truncate the list to nothing
const tmp = this._path + '.tmp'
fs.writeFileSync(tmp, JSON.stringify([...this._set], null, 2))
fs.renameSync(tmp, this._path)
}
async add (uri) {
this._load()
if (this._set.has(uri)) return false
this._set.add(uri)
this._flush()
return true
}
async remove (uri) {
this._load()
if (!this._set.delete(uri)) return false
this._flush()
return true
}
async list () { this._load(); return [...this._set] }
async has (uri) { this._load(); return this._set.has(uri) }
async count () { this._load(); return this._set.size }
}
// In-memory variant for tests — same interface, no disk.
export class MemoryFollowerStore {
constructor (initial = []) { this._set = new Set(initial) }
async add (uri) { const had = this._set.has(uri); this._set.add(uri); return !had }
async remove (uri) { return this._set.delete(uri) }
async list () { return [...this._set] }
async has (uri) { return this._set.has(uri) }
async count () { return this._set.size }
}