// Follow and the timeline.
//
// Following is not a subscription the network knows about. It is a RECORD IN YOUR OWN FEED naming
// someone else's feed key — which is the same rule every interaction obeys, and the reason a feed's
// writer set never grows beyond its owner's devices.
//
// The timeline is a pure projection over the feeds this node happens to hold. There is no relay and
// no aggregator, so two peers legitimately compute different timelines, and a feed you follow but
// have not replicated contributes NOTHING rather than an error. Both are properties to assert, not
// caveats to apologise for.
//
// Networking is deliberately absent: feeds are handed in directly. There is no transport seam yet.
import test from 'brittle'
import fs from '#node:fs'
import os from '#node:os'
import { join } from '#node:path'
import Corestore from 'corestore'
import { RecordStore } from '../lib/record-store.js'
import { Feed } from '../lib/feed-base.js'
import { Peer, POST, FOLLOW } from '../lib/peer.js'
import { attest } from './helpers.js'
const tmp = () => fs.mkdtempSync(join(os.tmpdir(), 'favela-peer-t-'))
const rec = (body, createdAt = '2026-09-06T00:00:00Z') => ({ frontmatter: { createdAt }, body })
async function mkFeed () {
const f = new Feed(new Corestore(tmp()), null, { store: new RecordStore(tmp()) })
await f.ready()
await f.addDevice(await attest(f))
await f.update()
return f
}
async function mkPeer () {
const feed = await mkFeed()
const p = new Peer(feed)
await p.ready()
return p
}
test('follow writes a record in YOUR OWN feed', async (t) => {
const me = await mkPeer()
const them = await mkFeed()
t.teardown(() => Promise.all([me.close(), them.close()]))
await me.follow(them.key, them)
await me.update()
const r = await me.feed.get(FOLLOW, me.constructor.nameFor(them.key))
t.ok(r, 'the follow is a record, in my feed')
t.alike(await me.following(), [me.constructor.nameFor(them.key)], 'and it lists')
t.alike(await them.devices(), [them.deviceKey], 'their writer set is untouched by my following them')
})
test('unfollow tombstones the record and drops the feed', async (t) => {
const me = await mkPeer()
const them = await mkFeed()
t.teardown(() => Promise.all([me.close(), them.close()]))
await me.follow(them.key, them)
await me.update()
await me.unfollow(them.key)
await me.update()
t.alike(await me.following(), [], 'gone from the list')
t.is((await me.feed.resolve(FOLLOW, me.constructor.nameFor(them.key))).status, 'deleted',
'as a tombstone, not an absence')
})
test('the timeline merges my posts with the feeds I hold, newest first', async (t) => {
const me = await mkPeer()
const them = await mkFeed()
t.teardown(() => Promise.all([me.close(), them.close()]))
await me.feed.put(POST, 'a1', rec('mine, older', '2026-09-01T00:00:00Z'))
await them.put(POST, 'b1', rec('theirs, newer', '2026-09-05T00:00:00Z'))
await me.follow(them.key, them)
await me.update()
await them.update()
const tl = await me.timeline()
t.alike(tl.map((x) => x.record.body), ['theirs, newer\n', 'mine, older\n'], 'newest first')
t.is(tl[0].author, them.key.toString('hex'), 'each entry names the feed it came from')
})
test('⛔ a followed feed we do not hold contributes NOTHING, and is not an error', async (t) => {
const me = await mkPeer()
const them = await mkFeed()
const key = them.key
t.teardown(() => Promise.all([me.close(), them.close()]))
await them.put(POST, 'b1', rec('you cannot see this'))
await them.update()
await me.follow(key) // followed by key only — no feed handed in, nothing replicated
await me.feed.put(POST, 'a1', rec('mine'))
await me.update()
const tl = await me.timeline()
t.alike(tl.map((x) => x.record.body), ['mine\n'], 'their post is absent, silently and correctly')
t.alike(await me.following(), [me.constructor.nameFor(key)], 'though the follow is recorded')
t.alike(await me.missing(), [me.constructor.nameFor(key)], 'and we can SAY which follows we lack')
})
test('the timeline carries only posts, not follows or other collections', async (t) => {
const me = await mkPeer()
const them = await mkFeed()
t.teardown(() => Promise.all([me.close(), them.close()]))
await me.follow(them.key, them)
await me.feed.put(POST, 'a1', rec('a post'))
await me.feed.put('like', 'l1', rec(''))
await me.update()
t.alike((await me.timeline()).map((x) => x.record.body), ['a post\n'])
})
test('★ ordering is deterministic when createdAt ties', async (t) => {
const me = await mkPeer()
const them = await mkFeed()
t.teardown(() => Promise.all([me.close(), them.close()]))
const same = '2026-09-06T12:00:00Z'
await me.feed.put(POST, 'zz', rec('mine', same))
await them.put(POST, 'aa', rec('theirs', same))
await me.follow(them.key, them)
await me.update()
await them.update()
const a = (await me.timeline()).map((x) => x.key)
const b = (await me.timeline()).map((x) => x.key)
t.alike(a, b, 'the same inputs give the same order, every time')
t.is(a.length, 2)
})
test('limit takes the newest N', async (t) => {
const me = await mkPeer()
t.teardown(() => me.close())
for (let i = 1; i <= 4; i++) {
await me.feed.put(POST, 'p' + i, rec('post ' + i, '2026-09-0' + i + 'T00:00:00Z'))
}
await me.update()
t.alike((await me.timeline({ limit: 2 })).map((x) => x.record.body), ['post 4\n', 'post 3\n'])
})
// ── peeking ───────────────────────────────────────────────────────────────────────────────────
//
// Reading someone's page should not require following them. `peer.js` already separates the two —
// "you can hold a feed you never followed" — but nothing distinguished the two kinds of held feed,
// and `timeline()` draws from every readable one. So holding a stranger to read their page would
// have quietly injected them into your timeline, which is following them by another route.
test('★ a peeked feed is READABLE but does not reach the timeline', async (t) => {
const me = await mkPeer()
const them = await mkPeer()
t.teardown(() => Promise.all([me.close(), them.close()]))
await them.feed.put(POST, 'p1', rec('a post by someone I have not followed'))
await them.update()
await me.feed.put(POST, 'mine', rec('my own post'))
await me.update()
me.peek(them.feed.key, them.feed)
await me.update()
t.alike(await me.following(), [], 'peeking writes NO follow record — nothing enters my feed')
// ⛔ The distinction that matters. Their post is readable, so their page renders; it is not in my
// timeline, so peeking is not following by another name.
t.alike((await me.timeline()).map((r) => r.name), ['mine'], 'my timeline is unchanged')
t.is((await me.postsBy(them.feed.key)).length, 1, 'and their page still has something on it')
})
test('following a feed you peeked PROMOTES it into the timeline', async (t) => {
const me = await mkPeer()
const them = await mkPeer()
t.teardown(() => Promise.all([me.close(), them.close()]))
await them.feed.put(POST, 'p1', rec('hello'))
await them.update()
me.peek(them.feed.key, them.feed)
t.alike((await me.timeline()).map((r) => r.name), [])
await me.follow(them.feed.key, them.feed)
await me.update()
// ⚠ A peek that stayed a peek after you followed would leave the timeline silently short.
t.alike((await me.timeline()).map((r) => r.name), ['p1'], 'now it counts')
t.is(me.peeked.size, 0, 'and it is no longer a peek')
})
test('⚠ peeking a feed you already follow does not demote it', async (t) => {
const me = await mkPeer()
const them = await mkPeer()
t.teardown(() => Promise.all([me.close(), them.close()]))
await them.feed.put(POST, 'p1', rec('hello'))
await them.update()
await me.follow(them.feed.key, them.feed)
await me.update()
me.peek(them.feed.key, them.feed)
t.alike((await me.timeline()).map((r) => r.name), ['p1'], 'still followed, still in the timeline')
})
test('postsBy reads one author, followed or merely peeked', async (t) => {
const me = await mkPeer()
const them = await mkPeer()
t.teardown(() => Promise.all([me.close(), them.close()]))
await them.feed.put(POST, 'a', rec('one'))
await them.feed.put(POST, 'b', rec('two'))
await them.update()
me.peek(them.feed.key, them.feed)
// ⛔ Its own method rather than filtering a 200-row timeline, which is what the person page did —
// that could not render a peeked feed at all once the timeline stopped including them.
t.is((await me.postsBy(them.feed.key)).length, 2)
t.alike(await me.postsBy('ab'.repeat(32)), [], 'a feed we hold nothing for is empty, not an error')
})