Ask a search relay whether it supports search before offering to drop it (#659)

This commit is contained in:
Coracle-Bot 2026-09-25 20:59:18 +00:00 committed by hodlbod
parent 1c934576fe
commit 653664751f
4 changed files with 86 additions and 4 deletions

View file

@ -1724,6 +1724,18 @@ Acceptance:
- 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.
### US-133 — Keep the search relays that work
As alice, I want the search health check to leave alone the relays that do
support search, so that fixing it does not cost me the ones I chose.
Acceptance:
- The check reads each search relay's own metadata, so a relay that advertises
search is never offered for removal, and neither is one whose metadata never
arrives.
- A relay that answers and advertises no search is.
### US-106 — Share text into the app ### US-106 — Share text into the app
As alice, I want to hand text to Flotilla and choose where it lands, so that I As alice, I want to hand text to Flotilla and choose where it lands, so that I

View file

@ -2,7 +2,15 @@ import {neventEncode, npubEncode} from "nostr-tools/nip19"
import {DAY, HOUR, MINUTE} from "@welshman/lib" import {DAY, HOUR, MINUTE} from "@welshman/lib"
import {POLL, displayRelayUrl} from "@welshman/util" import {POLL, displayRelayUrl} from "@welshman/util"
import {RelayMessageType} from "@welshman/net" import {RelayMessageType} from "@welshman/net"
import {Classified, Comment, FollowList, Note, Poll, Thread} from "@welshman/domain" import {
Classified,
Comment,
FollowList,
Note,
Poll,
SearchRelayList,
Thread,
} from "@welshman/domain"
import type {Locator, Page} from "@playwright/test" import type {Locator, Page} from "@playwright/test"
import { import {
composer, composer,
@ -560,6 +568,54 @@ test("US-117 read the network feed when one relay never answers", async ({seed,
await expect(network.getByText("Bob Barker")).toBeVisible() await expect(network.getByText("Bob Barker")).toBeVisible()
}) })
// A url the container does not serve, so its nip-11 document never arrives and nothing knows what it supports.
const unreachableRelayUrl = "wss://unreachable.test/"
test("US-133 keep the search relays that support search", async ({seed, as}) => {
const scenario = await seed(({relay, open, user}) => {
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],
})
// No search relay of hers is a space, so the health check is the only thing asking for their metadata.
indexer.event(user.alice, () =>
indexer
.kind(SearchRelayList)
.writer()
.setUrls([indexer.url, outbox.url, unreachableRelayUrl])
.renderTemplate(),
)
})
const indexer = scenario.open("indexer")
const outbox = scenario.open("outbox")
const page = await as(users.alice, "/home", {
relayInfo: {[indexer.url]: {supported_nips: [1, 50]}},
})
const healthCheck = page.getByRole("group", {name: "Invalid Search Relays"})
await expect(healthCheck).toBeVisible()
await healthCheck.getByRole("button", {name: "Fix"}).click()
const review = dialog(page, "Review changes")
// Outbox answered and advertised no search; the other two are a relay that does and one nobody can ask.
await expect(review.getByText(displayRelayUrl(outbox.url))).toBeVisible()
await expect(review.getByText(displayRelayUrl(indexer.url))).toHaveCount(0)
await expect(review.getByText(displayRelayUrl(unreachableRelayUrl))).toHaveCount(0)
})
test("US-106 share text into the app", async ({seed, as}) => { test("US-106 share text into the app", async ({seed, as}) => {
const shared = "the offsite is moving to the 14th" const shared = "the offsite is moving to the 14th"

View file

@ -109,13 +109,15 @@ export class HealthChecks {
) )
this.pending = projection( this.pending = projection(
derived([this.context.$, forceHealthChecks], ([$context, $forceHealthChecks]) => derived(
this.checks.filter(check => $forceHealthChecks[check.id] ?? check.isPending($context)), [this.context.$, forceHealthChecks, app.use(Relays).index.$],
([$context, $forceHealthChecks]) =>
this.checks.filter(check => $forceHealthChecks[check.id] ?? check.isPending($context)),
), ),
) )
} }
private supportsSearch = (url: string) => this.app.use(Relays).get(url)?.hasNip(50) private supportsSearch = (url: string) => this.app.use(Relays).get(url)?.hasNip(50) ?? true
private checks: HealthCheck[] = [ private checks: HealthCheck[] = [
{ {

View file

@ -36,6 +36,7 @@ import {
MessagingRelayLists, MessagingRelayLists,
RelayLists, RelayLists,
RoomLists, RoomLists,
SearchRelayLists,
Sync, Sync,
makeRoomKey, makeRoomKey,
} from "@welshman/app" } from "@welshman/app"
@ -53,6 +54,7 @@ import {
relays, relays,
roomLists, roomLists,
router, router,
searchRelayLists,
} from "@app/core" } from "@app/core"
import {LIVEKIT_PARTICIPANTS} from "@app/call" import {LIVEKIT_PARTICIPANTS} from "@app/call"
import {REACTION_KINDS, CONTENT_KINDS, makeCommentFilter} from "@app/content" import {REACTION_KINDS, CONTENT_KINDS, makeCommentFilter} from "@app/content"
@ -100,6 +102,8 @@ const userFollowList = deriveUserItem(FollowLists)
const userMessagingRelayList = deriveUserItem(MessagingRelayLists) const userMessagingRelayList = deriveUserItem(MessagingRelayLists)
const userSearchRelayList = deriveUserItem(SearchRelayLists)
const getSpaceUrls = ($roomList: Maybe<RoomListReader>) => const getSpaceUrls = ($roomList: Maybe<RoomListReader>) =>
PLATFORM_RELAYS.length > 0 ? PLATFORM_RELAYS : ($roomList?.urls() ?? []) PLATFORM_RELAYS.length > 0 ? PLATFORM_RELAYS : ($roomList?.urls() ?? [])
@ -125,9 +129,16 @@ const syncRelays = () => {
} }
}) })
const unsubscribeSearchRelayUrls = userSearchRelayList.subscribe($searchRelayList => {
for (const url of $searchRelayList?.urls() ?? []) {
relays.get().load(url)
}
})
return () => { return () => {
unsubscribePage() unsubscribePage()
unsubscribeSpaceUrls() unsubscribeSpaceUrls()
unsubscribeSearchRelayUrls()
} }
} }
@ -213,6 +224,7 @@ const syncUserData = () => {
roomLists.get().load($pubkey) roomLists.get().load($pubkey)
muteLists.get().load($pubkey) muteLists.get().load($pubkey)
profiles.get().load($pubkey) profiles.get().load($pubkey)
searchRelayLists.get().load($pubkey)
app.get().use(Settings).load($pubkey) app.get().use(Settings).load($pubkey)
} }
} }