Wait for most of a span's relays, with a three second ceiling

This commit is contained in:
Coracle-Bot 2026-09-24 15:31:51 +00:00
parent d905c9dc1b
commit eb9323a88b
3 changed files with 18 additions and 26 deletions

View file

@ -1715,9 +1715,8 @@ 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 - The feed is one unbroken stretch back from now, so nothing appears below the
from further back than the rest have been asked about waits for the feed to end of the list.
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

@ -508,8 +508,7 @@ test("US-117 read a follow who is in none of your spaces", async ({seed, as}) =>
}) })
test("US-117 read a network feed whose relays answer from different depths", async ({seed, as}) => { 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 // A relay that fills its page has covered only as far back as the page reaches.
// as far back as the page reaches, so a busy one is what decides how far the feed has got.
const pageSize = 100 const pageSize = 100
const recent = "the harbourmaster moved the moorings again" const recent = "the harbourmaster moved the moorings again"
const deep = "the old crane was scrapped in the spring" const deep = "the old crane was scrapped in the spring"
@ -528,8 +527,7 @@ test("US-117 read a network feed whose relays answer from different depths", asy
}) })
indexer.follows(user.alice, [user.bob]) 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 // Bob's busy relay holds a page of the last two hours, his quiet one a week-old note.
// 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]}) indexer.relayList(user.bob, {read: [outbox.url], write: [outbox.url, indexer.url]})
outbox.profile(user.bob, {name: "Bob Barker"}) outbox.profile(user.bob, {name: "Bob Barker"})
@ -551,12 +549,9 @@ test("US-117 read a network feed whose relays answer from different depths", asy
await expect(network.getByText(recent)).toBeVisible() await expect(network.getByText(recent)).toBeVisible()
// The quiet relay answered from a week back, which is days below where the busy one has been // A week back is days below where the busy relay has been asked about, so it is held.
// 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) 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 network.locator(".card").last().scrollIntoViewIfNeeded()
await expect(network.getByText(deep)).toBeVisible() await expect(network.getByText(deep)).toBeVisible()

View file

@ -375,11 +375,15 @@ export type FeedLoadState =
// nothing in it, and the two have to move the window differently. // nothing in it, and the two have to move the window differently.
export type FeedSpan = {found: number; complete: boolean; exhausted: boolean} export type FeedSpan = {found: number; complete: boolean; exhausted: boolean}
// The share of the relays a span asked that have to answer before it stops waiting on the rest. A // How far a span covered is set by its least generous relay, and quiet relays answer first.
// request otherwise waits on every one of them, and a relay that accepts a socket and then says const SPAN_THRESHOLD = 0.8
// nothing neither answers nor drops. A span is also what releases the events it found, so one
// silent relay leaves the feed empty rather than slow. // A relay that accepts a socket and then says nothing neither answers nor drops.
const SPAN_THRESHOLD = 0.5 const SPAN_TIMEOUT = 3000
// Aborting a request resolves it with what arrived, the same way closing it does.
const spanSignal = (signal: AbortSignal) =>
AbortSignal.any([signal, AbortSignal.timeout(SPAN_TIMEOUT)])
// Empty spans to walk per trigger. Enough to cross a gap; not enough to reach the end of the // Empty spans to walk per trigger. Enough to cross a gap; not enough to reach the end of the
// history on a single request. // history on a single request.
@ -530,8 +534,7 @@ export const makeFeed = ({
} }
} }
// The one door into the feed, so that what it renders is one unbroken stretch back from the // The one door into the feed, and anything older than it has reached waits for the window.
// 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[] = []
@ -599,7 +602,7 @@ export const makeFeed = ({
const found = await network.get().request({ const found = await network.get().request({
relays, relays,
autoClose: true, autoClose: true,
signal: controller.signal, signal: spanSignal(controller.signal),
threshold: SPAN_THRESHOLD, threshold: SPAN_THRESHOLD,
filters: filters.map(filter => ({...filter, ...extension})), filters: filters.map(filter => ({...filter, ...extension})),
onEvent: countEvent, onEvent: countEvent,
@ -644,10 +647,7 @@ export const makeFeed = ({
oldest = edge === undefined ? since : Math.min(edge, until - 1) oldest = edge === undefined ? since : Math.min(edge, until - 1)
} }
// A span reaches further back than it covers. A relay holding few of these authors answers // A span reaches further back than it covers, so its deepest events wait for the window.
// 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) addEvents(found)
reach(oldest) reach(oldest)
@ -675,8 +675,6 @@ export const makeFeed = ({
return {found: found.length, complete, exhausted: false} return {found: found.length, complete, exhausted: false}
} }
// What the repository already holds is a scatter rather than a stretch — an earlier visit's
// pages, a reply pulled in as context from weeks back — so it waits for the window too.
addEvents(relays.flatMap(url => Array.from(getEventsForUrl(url, filters)))) addEvents(relays.flatMap(url => Array.from(getEventsForUrl(url, filters))))
return { return {
@ -773,7 +771,7 @@ export const makeCalendarFeed = ({
const found = await network.get().request({ const found = await network.get().request({
relays, relays,
autoClose: true, autoClose: true,
signal: controller.signal, signal: spanSignal(controller.signal),
threshold: SPAN_THRESHOLD, threshold: SPAN_THRESHOLD,
filters: [{kinds: [EVENT_TIME], "#D": daysBetween(since, until).map(String)}], filters: [{kinds: [EVENT_TIME], "#D": daysBetween(since, until).map(String)}],
onEose: () => { onEose: () => {