Render a feed as one unbroken stretch back from its anchor

This commit is contained in:
Coracle-Bot 2026-09-24 03:13:06 +00:00
parent e47957b4c2
commit d905c9dc1b
3 changed files with 69 additions and 13 deletions

View file

@ -1715,6 +1715,9 @@ Acceptance:
drawn underneath it, and never appears as an item of its own. drawn underneath it, and never appears as an item of its own.
- Every note carries its reply count, including the ones with no replies. - Every note carries its reply count, including the ones with no replies.
- Scrolling to the end of the feed loads more rather than asking her to. - Scrolling to the end of the feed loads more rather than asking her to.
- The feed is one unbroken stretch back from now. A relay that answers a page
from further back than the rest have been asked about waits for the feed to
reach it, so nothing lands below the end of the list.
- The section fills from the relays that answer. One that takes the connection - The section fills from the relays that answer. One that takes the connection
and then says nothing does not hold it empty. and then says nothing does not hold it empty.

View file

@ -507,6 +507,61 @@ test("US-117 read a follow who is in none of your spaces", async ({seed, as}) =>
expect(fromSpace).toEqual([]) expect(fromSpace).toEqual([])
}) })
test("US-117 read a network feed whose relays answer from different depths", async ({seed, as}) => {
// What each relay is asked for one page at a time. A relay that fills its page has only covered
// as far back as the page reaches, so a busy one is what decides how far the feed has got.
const pageSize = 100
const recent = "the harbourmaster moved the moorings again"
const deep = "the old crane was scrapped in the spring"
await seed(({relay, open, user, at}) => {
const space = relay("space")
const indexer = open("indexer")
const outbox = open("outbox")
space.room("general", {name: "General"})
space.join(user.alice, "general")
indexer.relayList(user.alice, {
read: [space.url, indexer.url],
write: [space.url, indexer.url, outbox.url],
})
indexer.follows(user.alice, [user.bob])
// Bob writes to both, so the feed asks both. His busy relay holds more than one page of the
// last couple of hours; his quiet one holds a single note from a week ago.
indexer.relayList(user.bob, {read: [outbox.url], write: [outbox.url, indexer.url]})
outbox.profile(user.bob, {name: "Bob Barker"})
outbox.note(user.bob, recent, at(1, MINUTE))
// More than one page, so the page comes back full however the relay rounds it
for (let minute = 2; minute <= pageSize + 10; minute++) {
outbox.note(user.bob, `mooring report ${minute}`, at(minute, MINUTE))
}
indexer.note(user.bob, deep, at(7, DAY))
})
const page = await as(users.alice, "/home")
const network = page
.locator("section")
.filter({has: page.getByRole("heading", {name: "Network"})})
await expect(network.getByText(recent)).toBeVisible()
// The quiet relay answered from a week back, which is days below where the busy one has been
// asked about. Drawing it would put a note under the end of the list with the whole week
// between still missing.
await expect(network.getByText(deep)).toHaveCount(0)
// It is held rather than dropped, so the feed hands it over once it has paged back that far.
await network.locator(".card").last().scrollIntoViewIfNeeded()
await expect(network.getByText(deep)).toBeVisible()
})
test("US-117 read the network feed when one relay never answers", async ({seed, as}) => { test("US-117 read the network feed when one relay never answers", async ({seed, as}) => {
// Nothing serves this url, and nothing needs to: the fault is a relay that takes the socket and // Nothing serves this url, and nothing needs to: the fault is a relay that takes the socket and
// then says nothing, which is all the spec asks of it. // then says nothing, which is all the spec asks of it.

View file

@ -530,8 +530,8 @@ export const makeFeed = ({
} }
} }
// What arrives from elsewhere in the app, which is only rendered as far back as the feed has // The one door into the feed, so that what it renders is one unbroken stretch back from the
// got to on its own // anchor: anything older than the feed has reached waits there until the window gets to it.
const addEvents = (newEvents: TrustedEvent[]) => { const addEvents = (newEvents: TrustedEvent[]) => {
const ready: TrustedEvent[] = [] const ready: TrustedEvent[] = []
@ -644,7 +644,11 @@ export const makeFeed = ({
oldest = edge === undefined ? since : Math.min(edge, until - 1) oldest = edge === undefined ? since : Math.min(edge, until - 1)
} }
insertEvents(found) // A span reaches further back than it covers. A relay holding few of these authors answers
// a page from days deeper than a busy one does, and rendering that stretch leaves a list
// whose bottom is a scatter the window then fills in above — so it waits for the window
// like anything else arriving early.
addEvents(found)
reach(oldest) reach(oldest)
return {found: found.length, complete, exhausted: false} return {found: found.length, complete, exhausted: false}
@ -666,20 +670,14 @@ export const makeFeed = ({
newest = until newest = until
} }
insertEvents(found) addEvents(found)
return {found: found.length, complete, exhausted: false} return {found: found.length, complete, exhausted: false}
} }
// What the repository already holds for these relays is in hand and goes in as one insert, // What the repository already holds is a scatter rather than a stretch — an earlier visit's
// which takes the window back with it rather than leaving the rest of that stretch behind // pages, a reply pulled in as context from weeks back — so it waits for the window too.
const cached = relays.flatMap(url => Array.from(getEventsForUrl(url, filters))) addEvents(relays.flatMap(url => Array.from(getEventsForUrl(url, filters))))
for (const event of cached) {
reached = Math.min(reached, event.created_at)
}
insertEvents(cached)
return { return {
events, events,