home: fan out single-relay queries to fix load waterfall
The home page serialized its first paint behind relay.ditto.pub:
useCampaignLists queried that one relay via nostr.relay(DITTO_RELAY)
(awaited, up to an 8s timeout) and every hero campaign was gated on
its result, so a slow ditto.pub stalled the whole page. Connection
sharing made it worse — pooled queries multiplexed onto the same
stalled socket.
Switch the home-critical moderation/list/discovery hooks from
single-relay nostr.relay(DITTO_RELAY) calls to the parallel pooled
nostr.query() fan-out:
- useCampaignLists: authors:[curator] filter enforces trust; relay
pin was unnecessary and headed the waterfall.
- useCampaignModeration: authors:[moderators] filter enforces trust.
- useFeaturedOrganizations: per-author filters enforce curation.
- useDiscoverCommunities: global discovery — fan-out broadens coverage.
useDashboardCounts stays pinned: NIP-45 COUNT is a single-relay
primitive and isn't mergeable across relays, and it's off the home
critical path.
Regression-of: 3d825aef
This commit is contained in:
@@ -10,7 +10,6 @@ import {
|
||||
foldCampaignLists,
|
||||
} from '@/lib/campaignLists';
|
||||
import { LIST_CURATOR_PUBKEY } from '@/lib/agoraDefaults';
|
||||
import { DITTO_RELAY } from '@/lib/appRelays';
|
||||
|
||||
import type { NostrEvent } from '@nostrify/nostrify';
|
||||
|
||||
@@ -34,7 +33,20 @@ interface UseCampaignListsResult {
|
||||
* follow-pack member is trusted to sign approve / hide labels.
|
||||
*
|
||||
* Because the curator is a hardcoded constant, this query depends on no
|
||||
* other query — it fires on first paint with no waterfall.
|
||||
* other query — it fires on first paint.
|
||||
*
|
||||
* **Relay fan-out.** This used to query `relay.ditto.pub` directly (a
|
||||
* single-relay `nostr.relay(...)` call) to avoid a fast empty EOSE from a
|
||||
* less-populated relay racing the surface to "no lists." But this query
|
||||
* sits at the *head* of the home-page waterfall — every hero campaign is
|
||||
* gated on its result (see `CampaignsPage`/`useCampaigns`) — so a slow
|
||||
* `relay.ditto.pub` stalled the entire first paint. We now fan out to the
|
||||
* whole read pool via `nostr.query`. The `authors: [LIST_CURATOR_PUBKEY]`
|
||||
* filter is what enforces the trust model; correctness no longer depends
|
||||
* on hitting one specific relay, and the curated relay is still in the
|
||||
* fan-out so its events are found. The pool accumulates events across
|
||||
* relays until first EOSE (+ the pool's eoseTimeout), so a late event from
|
||||
* the curated relay still folds in on the next tick.
|
||||
*
|
||||
* Lists *and* the index are pulled in a single filter via
|
||||
* `'#t': [LIST_HASHTAG, LIST_INDEX_HASHTAG]` so there's only one
|
||||
@@ -46,12 +58,7 @@ export function useCampaignLists() {
|
||||
const query = useQuery<UseCampaignListsResult>({
|
||||
queryKey: ['campaign-lists', LIST_CURATOR_PUBKEY],
|
||||
queryFn: async ({ signal }) => {
|
||||
// Query the canonical app relay directly. The same reasoning as
|
||||
// `useCampaignModerators` applies: a fast empty EOSE from a
|
||||
// less-populated relay should not race the moderation surface to
|
||||
// "no lists" while the curated relay still holds them.
|
||||
const relay = nostr.relay(DITTO_RELAY);
|
||||
const events = await relay.query(
|
||||
const events = await nostr.query(
|
||||
[
|
||||
{
|
||||
kinds: [CAMPAIGN_LIST_KIND],
|
||||
@@ -60,7 +67,7 @@ export function useCampaignLists() {
|
||||
limit: 500,
|
||||
},
|
||||
],
|
||||
{ signal: AbortSignal.any([signal, AbortSignal.timeout(8000)]) },
|
||||
{ signal },
|
||||
);
|
||||
return foldCampaignLists(events);
|
||||
},
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { useNostrPublish } from './useNostrPublish';
|
||||
import { useCampaignModerators } from './useCampaignModerators';
|
||||
import { DITTO_RELAY } from '@/lib/appRelays';
|
||||
import { CAMPAIGN_KIND } from '@/lib/campaign';
|
||||
import {
|
||||
AGORA_MODERATION_NAMESPACE,
|
||||
@@ -62,8 +61,12 @@ export function useCampaignModeration() {
|
||||
if (!moderators || moderators.length === 0) {
|
||||
return { ...EMPTY_MODERATION_DATA, moderators: [] };
|
||||
}
|
||||
const relay = nostr.relay(DITTO_RELAY);
|
||||
const events = await relay.query(
|
||||
// Fan out to the whole read pool rather than pinning a single relay.
|
||||
// The `authors: moderators` filter enforces the trust model, so
|
||||
// querying more relays only improves coverage — and it keeps this
|
||||
// moderation surface off the single-relay critical path that was
|
||||
// serializing the home page behind relay.ditto.pub.
|
||||
const events = await nostr.query(
|
||||
[
|
||||
{
|
||||
kinds: [LABEL_KIND],
|
||||
@@ -76,7 +79,7 @@ export function useCampaignModeration() {
|
||||
limit: 2000,
|
||||
},
|
||||
],
|
||||
{ signal: AbortSignal.any([signal, AbortSignal.timeout(8000)]) },
|
||||
{ signal },
|
||||
);
|
||||
return foldModerationLabels(events, moderators, CAMPAIGN_KIND);
|
||||
},
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useNostr } from '@nostrify/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { NostrEvent } from '@nostrify/nostrify';
|
||||
|
||||
import { DITTO_RELAY } from '@/lib/appRelays';
|
||||
import {
|
||||
COMMUNITY_DEFINITION_KIND,
|
||||
parseCommunityEvent,
|
||||
@@ -38,13 +37,15 @@ interface UseDiscoverCommunitiesOptions {
|
||||
export function useDiscoverCommunities(options: UseDiscoverCommunitiesOptions = {}) {
|
||||
const { limit = 24, enabled = true } = options;
|
||||
const { nostr } = useNostr();
|
||||
const relay = nostr.relay(DITTO_RELAY);
|
||||
|
||||
return useQuery<ParsedCommunity[]>({
|
||||
queryKey: ['discover-communities', limit],
|
||||
enabled,
|
||||
queryFn: async ({ signal }) => {
|
||||
const events = await relay.query(
|
||||
// Global discovery (no `authors:` filter), so fan out to the whole
|
||||
// read pool: more relays means broader community coverage, and it
|
||||
// keeps Discover off the single-relay critical path.
|
||||
const events = await nostr.query(
|
||||
[{ kinds: [COMMUNITY_DEFINITION_KIND], limit }],
|
||||
{ signal },
|
||||
);
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import type { NostrEvent, NostrFilter } from '@nostrify/nostrify';
|
||||
|
||||
import { useOrganizationModeration } from '@/hooks/useOrganizationModeration';
|
||||
import { DITTO_RELAY } from '@/lib/appRelays';
|
||||
import {
|
||||
COMMUNITY_DEFINITION_KIND,
|
||||
parseCommunityEvent,
|
||||
@@ -50,7 +49,6 @@ function parseCoord(coord: string): { pubkey: string; dTag: string } | null {
|
||||
*/
|
||||
export function useFeaturedOrganizations() {
|
||||
const { nostr } = useNostr();
|
||||
const relay = nostr.relay(DITTO_RELAY);
|
||||
const { data: moderation, isReady: moderationReady } = useOrganizationModeration();
|
||||
|
||||
// Derive the curated coord set: featured minus hidden, sorted by the
|
||||
@@ -102,8 +100,11 @@ export function useFeaturedOrganizations() {
|
||||
}),
|
||||
);
|
||||
|
||||
const combinedSignal = AbortSignal.any([signal, AbortSignal.timeout(8000)]);
|
||||
const events = await relay.query(filters, { signal: combinedSignal });
|
||||
// Fan out to the whole read pool. Each filter pins `authors:`, so the
|
||||
// curation is still enforced by the moderation labels (the `featured`
|
||||
// labels are themselves moderator-authored) — querying more relays only
|
||||
// improves coverage and keeps this off the single-relay critical path.
|
||||
const events = await nostr.query(filters, { signal });
|
||||
|
||||
// Latest-wins dedupe of addressable revisions, then index by coord so
|
||||
// we can return them in the moderator-controlled `featuredOrder`.
|
||||
|
||||
Reference in New Issue
Block a user