flotilla/src/app/healthChecks.ts

167 lines
5.5 KiB
TypeScript
Raw Normal View History

import {derived, get, writable} from "svelte/store"
import {assoc, sample} from "@welshman/lib"
2026-03-19 20:26:52 +00:00
import {
2026-07-28 16:16:26 +00:00
MessagingRelayLists,
RelayLists,
Relays,
SearchRelayLists,
projection,
publish,
2026-03-19 20:26:52 +00:00
} from "@welshman/app"
2026-07-28 16:16:26 +00:00
import type {IApp, Projection} from "@welshman/app"
import KeyDownload from "@app/components/KeyDownload.svelte"
import {session, usePlugin} from "@app/core"
import {DEFAULT_RELAYS, DEFAULT_MESSAGING_RELAYS} from "@app/env"
import {pushModal} from "@app/modal"
2026-03-19 20:26:52 +00:00
export type HealthCheckContext = {
readRelays: string[]
writeRelays: string[]
messagingRelays: string[]
searchRelays: string[]
}
export type HealthCheck = {
id: string
2026-03-19 20:26:52 +00:00
title: string
description: string
action: string
isPending: (context: HealthCheckContext) => boolean
apply: (context: HealthCheckContext) => unknown
}
export const forceHealthChecks = writable<Record<string, boolean>>({})
2026-07-28 16:16:26 +00:00
export class HealthChecks {
context: Projection<HealthCheckContext>
pending: Projection<HealthCheck[]>
2026-03-19 20:26:52 +00:00
2026-07-28 16:16:26 +00:00
constructor(private readonly app: IApp) {
const pubkey = app.user?.pubkey ?? ""
2026-03-19 20:26:52 +00:00
2026-07-28 16:16:26 +00:00
this.context = projection(
derived(
[
app.use(RelayLists).index.$,
app.use(SearchRelayLists).index.$,
app.use(MessagingRelayLists).index.$,
],
([$relayLists, $searchRelayLists, $messagingRelayLists]) => {
const relayList = $relayLists.get(pubkey)
2026-03-19 20:26:52 +00:00
2026-07-28 16:16:26 +00:00
return {
readRelays: relayList?.readUrls() ?? [],
writeRelays: relayList?.writeUrls() ?? [],
searchRelays: $searchRelayLists.get(pubkey)?.urls() ?? [],
messagingRelays: $messagingRelayLists.get(pubkey)?.urls() ?? [],
}
},
),
)
2026-03-19 20:26:52 +00:00
this.pending = projection(
derived([this.context.$, forceHealthChecks], ([$context, $forceHealthChecks]) =>
this.checks.filter(check => $forceHealthChecks[check.id] ?? check.isPending($context)),
),
2026-07-28 16:16:26 +00:00
)
}
private supportsSearch = (url: string) => this.app.use(Relays).get(url)?.hasNip(50)
private checks: HealthCheck[] = [
{
id: "missing-inbox-relays",
2026-07-28 16:16:26 +00:00
title: "Missing Inbox Relays",
description: "Other people aren't currently able to reliably tag you in public notes.",
action: "Update",
isPending: context => context.readRelays.length <= 1,
apply: () => this.app.use(RelayLists).setReadUrls(DEFAULT_RELAYS).then(publish),
},
{
id: "missing-outbox-relays",
2026-07-28 16:16:26 +00:00
title: "Missing Outbox Relays",
description: "Other people aren't currently able to reliably find your public notes.",
action: "Update",
isPending: context => context.writeRelays.length <= 1,
apply: () => this.app.use(RelayLists).setWriteUrls(DEFAULT_RELAYS).then(publish),
},
{
id: "missing-dm-relays",
2026-07-28 16:16:26 +00:00
title: "Missing DM Relays",
description: "You aren't currently able to reliably send or receive direct messages.",
action: "Update",
isPending: context => context.messagingRelays.length <= 1,
apply: () =>
this.app.use(MessagingRelayLists).setUrls(DEFAULT_MESSAGING_RELAYS).then(publish),
},
{
id: "too-many-inbox-relays",
2026-07-28 16:16:26 +00:00
title: "Too Many Inbox Relays",
description:
"You have more inbox relays than is really necessary, which can affect resource usage.",
action: "Prune Selections",
isPending: context => context.readRelays.length > 8,
apply: context =>
this.app.use(RelayLists).setReadUrls(sample(5, context.readRelays)).then(publish),
},
{
id: "too-many-outbox-relays",
2026-07-28 16:16:26 +00:00
title: "Too Many Outbox Relays",
description:
"You have more outbox relays than is really necessary, which can affect resource usage.",
action: "Prune Selections",
isPending: context => context.writeRelays.length > 8,
apply: context =>
this.app.use(RelayLists).setWriteUrls(sample(5, context.writeRelays)).then(publish),
},
{
id: "too-many-dm-relays",
2026-07-28 16:16:26 +00:00
title: "Too Many DM Relays",
description:
"You have more DM relays than is really necessary, which can affect resource usage.",
action: "Prune Selections",
isPending: context => context.messagingRelays.length > 8,
apply: context =>
this.app.use(MessagingRelayLists).setUrls(sample(5, context.messagingRelays)).then(publish),
},
{
id: "invalid-search-relays",
2026-07-28 16:16:26 +00:00
title: "Invalid Search Relays",
description: "Some of your search relays don't support search.",
action: "Remove Invalid",
isPending: context => context.searchRelays.some(url => !this.supportsSearch(url)),
apply: context =>
this.app
.use(SearchRelayLists)
.setUrls(context.searchRelays.filter(this.supportsSearch))
.then(publish),
},
{
id: "backup-key",
title: "Back Up Your Key",
description: "Save a backup of your private key in a secure place.",
action: "Back Up",
isPending: () => false,
apply: () => {
const {secret} = session.get()!.data as {secret: string}
pushModal(KeyDownload, {
secret,
next: () => {
forceHealthChecks.update(assoc("backup-key", false))
history.back()
},
submitText: "Done",
})
},
},
2026-07-28 16:16:26 +00:00
]
isPending = (healthCheck: HealthCheck) =>
get(forceHealthChecks)[healthCheck.id] ?? healthCheck.isPending(this.context.get())
2026-07-28 16:16:26 +00:00
apply = (healthCheck: HealthCheck) => healthCheck.apply(this.context.get())
}
export const healthChecks = usePlugin(HealthChecks)