Paint a room before the space finishes syncing
This commit is contained in:
parent
2edd75e275
commit
57e0c4985d
4 changed files with 69 additions and 4 deletions
|
|
@ -21,6 +21,7 @@ import {
|
||||||
formatTranscript,
|
formatTranscript,
|
||||||
installWebSocketRoutes,
|
installWebSocketRoutes,
|
||||||
silenceRelay,
|
silenceRelay,
|
||||||
|
withholdEose,
|
||||||
} from "./net/websocket"
|
} from "./net/websocket"
|
||||||
import {watchFaults} from "./faults"
|
import {watchFaults} from "./faults"
|
||||||
import {boot} from "./app/boot"
|
import {boot} from "./app/boot"
|
||||||
|
|
@ -47,6 +48,7 @@ export {
|
||||||
getPublishedEvents,
|
getPublishedEvents,
|
||||||
getTranscript,
|
getTranscript,
|
||||||
silenceRelay,
|
silenceRelay,
|
||||||
|
withholdEose,
|
||||||
} from "./net/websocket"
|
} from "./net/websocket"
|
||||||
export {readCachedEvents} from "./app/cache"
|
export {readCachedEvents} from "./app/cache"
|
||||||
export {
|
export {
|
||||||
|
|
@ -137,6 +139,8 @@ export type PageOptions = {
|
||||||
hosting?: HostingFixtures
|
hosting?: HostingFixtures
|
||||||
// Relay urls that take the socket and answer nothing; silenceRelay does the same mid-test.
|
// Relay urls that take the socket and answer nothing; silenceRelay does the same mid-test.
|
||||||
silent?: string[]
|
silent?: string[]
|
||||||
|
// Relay urls that serve events and never eose, so every request runs out its own deadline.
|
||||||
|
eoseless?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Harness = {
|
export type Harness = {
|
||||||
|
|
@ -232,6 +236,10 @@ export const test = base.extend<HarnessFixtures, HarnessWorkerFixtures>({
|
||||||
silenceRelay(context, url)
|
silenceRelay(context, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const url of options.eoseless ?? []) {
|
||||||
|
withholdEose(context, url)
|
||||||
|
}
|
||||||
|
|
||||||
await mockRelayInfo(context, options.relayInfo ?? {})
|
await mockRelayInfo(context, options.relayInfo ?? {})
|
||||||
await mockAnalytics(context)
|
await mockAnalytics(context)
|
||||||
await mockDufflepud(context)
|
await mockDufflepud(context)
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ type Traffic = {
|
||||||
leaks: Set<string>
|
leaks: Set<string>
|
||||||
forgotten: Set<string>
|
forgotten: Set<string>
|
||||||
silenced: Set<string>
|
silenced: Set<string>
|
||||||
|
eoseless: Set<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
const trafficStore = makeContextStore<Traffic>("installWebSocketRoutes")
|
const trafficStore = makeContextStore<Traffic>("installWebSocketRoutes")
|
||||||
|
|
@ -51,6 +52,19 @@ const openEmptyRelay = (): RelayConnection => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A relay that serves what it has and never says it is done, so a request runs out its own deadline.
|
||||||
|
const openEoselessRelay = (connection: RelayConnection): RelayConnection => ({
|
||||||
|
onMessage(listener) {
|
||||||
|
connection.onMessage(message => {
|
||||||
|
if (message[0] !== RelayMessageType.Eose) {
|
||||||
|
listener(message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
send: message => connection.send(message),
|
||||||
|
close: () => connection.close(),
|
||||||
|
})
|
||||||
|
|
||||||
// A relay that takes the socket and says nothing, so the only way out is the caller's own deadline.
|
// A relay that takes the socket and says nothing, so the only way out is the caller's own deadline.
|
||||||
const openSilentRelay = (): RelayConnection => ({
|
const openSilentRelay = (): RelayConnection => ({
|
||||||
onMessage() {},
|
onMessage() {},
|
||||||
|
|
@ -72,7 +86,9 @@ const serve = (traffic: Traffic, zooid: Zooid, route: WebSocketRoute) => {
|
||||||
const relay = zooid.relays.get(url)
|
const relay = zooid.relays.get(url)
|
||||||
|
|
||||||
if (relay) {
|
if (relay) {
|
||||||
return relay.connect()
|
const connection = relay.connect()
|
||||||
|
|
||||||
|
return traffic.eoseless.has(url) ? openEoselessRelay(connection) : connection
|
||||||
}
|
}
|
||||||
|
|
||||||
traffic.leaks.add(url)
|
traffic.leaks.add(url)
|
||||||
|
|
@ -104,6 +120,7 @@ export const installWebSocketRoutes = (context: BrowserContext, zooid: Zooid) =>
|
||||||
leaks: new Set(),
|
leaks: new Set(),
|
||||||
forgotten: new Set(),
|
forgotten: new Set(),
|
||||||
silenced: new Set(),
|
silenced: new Set(),
|
||||||
|
eoseless: new Set(),
|
||||||
})
|
})
|
||||||
|
|
||||||
return context.routeWebSocket(
|
return context.routeWebSocket(
|
||||||
|
|
@ -136,6 +153,10 @@ export const forgetRelay = (context: BrowserContext, url: string) =>
|
||||||
export const silenceRelay = (context: BrowserContext, url: string) =>
|
export const silenceRelay = (context: BrowserContext, url: string) =>
|
||||||
trafficStore.get(context).silenced.add(normalizeRelayUrl(url))
|
trafficStore.get(context).silenced.add(normalizeRelayUrl(url))
|
||||||
|
|
||||||
|
// Resolved at open too, so a page that boots into it passes `eoseless` to `as`.
|
||||||
|
export const withholdEose = (context: BrowserContext, url: string) =>
|
||||||
|
trafficStore.get(context).eoseless.add(normalizeRelayUrl(url))
|
||||||
|
|
||||||
// Every frame in both directions, oldest first. Attach it to a failing test.
|
// Every frame in both directions, oldest first. Attach it to a failing test.
|
||||||
export const formatTranscript = (context: BrowserContext) =>
|
export const formatTranscript = (context: BrowserContext) =>
|
||||||
getTranscript(context)
|
getTranscript(context)
|
||||||
|
|
|
||||||
|
|
@ -1040,3 +1040,20 @@ test("US-115 connect a wallet without losing the zap you were composing", async
|
||||||
await expect(zap.getByRole("button", {name: "Send Zap"})).toBeVisible()
|
await expect(zap.getByRole("button", {name: "Send Zap"})).toBeVisible()
|
||||||
await expect(amount).toHaveValue("210")
|
await expect(amount).toHaveValue("210")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("fills a room from a relay that never says it is done", async ({seed, as}) => {
|
||||||
|
const scenario = await seed(({relay, user, at}) => {
|
||||||
|
const space = relay("space")
|
||||||
|
|
||||||
|
space.room("general", {name: "General"})
|
||||||
|
space.join(user.alice, "general")
|
||||||
|
space.join(user.bob, "general")
|
||||||
|
space.message(user.bob, "general", "the buoy is back on station", at(3, HOUR))
|
||||||
|
})
|
||||||
|
|
||||||
|
const {url} = scenario.space("space")
|
||||||
|
const page = await as(users.alice, roomPath(url, "general"), {eoseless: [url]})
|
||||||
|
|
||||||
|
// Nothing will tell the feed its page is finished, so a room that waits for that stays empty.
|
||||||
|
await expect(message(page, "the buoy is back on station")).toBeVisible()
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -547,7 +547,7 @@ export const makeFeed = ({
|
||||||
const unsubscribers = syncFeed({relays, filters, addEvents, removeEvents})
|
const unsubscribers = syncFeed({relays, filters, addEvents, removeEvents})
|
||||||
|
|
||||||
// Each relay answers a limit for itself, so a page runs out where the relay that gave least ran out.
|
// Each relay answers a limit for itself, so a page runs out where the relay that gave least ran out.
|
||||||
const loadSpan = async (extension: Filter) => {
|
const loadSpan = async (extension: Filter, onAnswer?: (event: TrustedEvent) => void) => {
|
||||||
let complete = false
|
let complete = false
|
||||||
|
|
||||||
const pages = new Map<string, {count: number; lowest: number}>()
|
const pages = new Map<string, {count: number; lowest: number}>()
|
||||||
|
|
@ -561,11 +561,15 @@ export const makeFeed = ({
|
||||||
} else {
|
} else {
|
||||||
pages.set(url, {count: 1, lowest: event.created_at})
|
pages.set(url, {count: 1, lowest: event.created_at})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onAnswer?.(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
const found = await network.get().request({
|
const found = await network.get().request({
|
||||||
relays,
|
relays,
|
||||||
autoClose: true,
|
autoClose: true,
|
||||||
|
// The socket sends five per 100ms first come first served, so the feed on screen goes ahead of the space's background pulls.
|
||||||
|
priority: 1,
|
||||||
signal: spanSignal(controller.signal),
|
signal: spanSignal(controller.signal),
|
||||||
threshold: SPAN_THRESHOLD,
|
threshold: SPAN_THRESHOLD,
|
||||||
filters: filters.map(filter => ({...filter, ...extension})),
|
filters: filters.map(filter => ({...filter, ...extension})),
|
||||||
|
|
@ -587,7 +591,11 @@ export const makeFeed = ({
|
||||||
|
|
||||||
const until = oldest
|
const until = oldest
|
||||||
const since = until - olderInterval
|
const since = until - olderInterval
|
||||||
const {found, complete, pages} = await loadSpan({since, until, limit: PAGE_SIZE})
|
|
||||||
|
// A page comes back newest first, so each event proves the stretch above it has been answered.
|
||||||
|
const {found, complete, pages} = await loadSpan({since, until, limit: PAGE_SIZE}, event =>
|
||||||
|
reach(event.created_at),
|
||||||
|
)
|
||||||
|
|
||||||
// A relay that answered with less than its limit has covered its whole span.
|
// A relay that answered with less than its limit has covered its whole span.
|
||||||
let edge: Maybe<number>
|
let edge: Maybe<number>
|
||||||
|
|
@ -632,7 +640,17 @@ export const makeFeed = ({
|
||||||
return {found: found.length, complete, exhausted: false}
|
return {found: found.length, complete, exhausted: false}
|
||||||
}
|
}
|
||||||
|
|
||||||
addEvents(relays.flatMap(url => Array.from(getEventsForUrl(url, filters))))
|
// What the repository already holds is an answer too, so a page of it renders before any relay replies.
|
||||||
|
const stored = relays
|
||||||
|
.flatMap(url => Array.from(getEventsForUrl(url, filters)))
|
||||||
|
.sort(compareEventsAsc)
|
||||||
|
.slice(-PAGE_SIZE)
|
||||||
|
|
||||||
|
if (stored.length > 0) {
|
||||||
|
reach(stored[0].created_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
addEvents(stored)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
events,
|
events,
|
||||||
|
|
@ -725,6 +743,7 @@ export const makeCalendarFeed = ({
|
||||||
const found = await network.get().request({
|
const found = await network.get().request({
|
||||||
relays,
|
relays,
|
||||||
autoClose: true,
|
autoClose: true,
|
||||||
|
priority: 1,
|
||||||
signal: spanSignal(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)}],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue