diff --git a/src/components/Embedded/EmbeddedNote.tsx b/src/components/Embedded/EmbeddedNote.tsx index 784c6e2c..dbc2c9a6 100644 --- a/src/components/Embedded/EmbeddedNote.tsx +++ b/src/components/Embedded/EmbeddedNote.tsx @@ -2,7 +2,6 @@ import { Skeleton } from '@/components/ui/skeleton' import ExternalLink from '@/components/ExternalLink' import { FAST_READ_RELAY_URLS, - FAST_WRITE_RELAY_URLS, PROFILE_RELAY_URLS, SEARCHABLE_RELAY_URLS, ExtendedKind @@ -537,7 +536,6 @@ function buildEmbedWideRelayUrlsStatic( ...nip66Service.getSearchableRelayUrls(), ...SEARCHABLE_RELAY_URLS, ...FAST_READ_RELAY_URLS, - ...FAST_WRITE_RELAY_URLS, ...PROFILE_RELAY_URLS, ...menuRelayUrls ]) diff --git a/src/components/NoteOptions/useMenuActions.tsx b/src/components/NoteOptions/useMenuActions.tsx index 269f16b7..3d49a3d1 100644 --- a/src/components/NoteOptions/useMenuActions.tsx +++ b/src/components/NoteOptions/useMenuActions.tsx @@ -220,8 +220,7 @@ export function useMenuActions({ const allRelays = [ ...(currentBrowsingRelayUrlsRef.current || []), ...(favoriteRelaysRef.current || []), - ...FAST_READ_RELAY_URLS, - ...FAST_WRITE_RELAY_URLS + ...FAST_READ_RELAY_URLS ] const comprehensiveRelays = Array.from( new Set(allRelays.map(url => normalizeAnyRelayUrl(url)).filter((url): url is string => !!url)) @@ -247,39 +246,47 @@ export function useMenuActions({ if (!pubkey) return try { - // Build comprehensive relay list for pin list fetching - const allRelays = [ - ...(currentBrowsingRelayUrls || []), - ...(favoriteRelays || []), - ...FAST_READ_RELAY_URLS, - ...FAST_READ_RELAY_URLS, - ...FAST_WRITE_RELAY_URLS - ] - - const normalizedRelays = allRelays - .map(url => normalizeAnyRelayUrl(url)) - .filter((url): url is string => !!url) - - const comprehensiveRelays = Array.from(new Set(normalizedRelays)) + const pinListReadRelays = Array.from( + new Set( + [...currentBrowsingRelayUrls, ...favoriteRelays, ...FAST_READ_RELAY_URLS] + .map((url) => normalizeAnyRelayUrl(url)) + .filter((url): url is string => !!url) + ) + ) - const latestPinList = await fetchNewestPinListForPubkey(pubkey, comprehensiveRelays) + const latestPinList = await fetchNewestPinListForPubkey(pubkey, pinListReadRelays) logger.component('PinNote', 'Current pin list event', { hasEvent: !!latestPinList }) const newTags = buildPinListTagsAfterToggle(latestPinList ?? null, event, !isPinned) const successMessage = isPinned ? t('Note unpinned') : t('Note pinned') logger.component('PinNote', 'Pin list tag count after merge', { count: newTags.length }) - + + const publishRelays = Array.from( + new Set([ + ...pinListReadRelays, + ...FAST_WRITE_RELAY_URLS.map((url) => normalizeAnyRelayUrl(url) || url).filter( + (url): url is string => !!url + ) + ]) + ) + // Create and publish the new pin list event - logger.component('PinNote', 'Publishing new pin list event', { tagCount: newTags.length, relayCount: comprehensiveRelays.length }) - const publishedEvent = await publish({ - kind: 10001, - tags: newTags, - content: '', - created_at: Math.floor(Date.now() / 1000) - }, { - specifiedRelayUrls: comprehensiveRelays + logger.component('PinNote', 'Publishing new pin list event', { + tagCount: newTags.length, + relayCount: publishRelays.length }) + const publishedEvent = await publish( + { + kind: 10001, + tags: newTags, + content: '', + created_at: Math.floor(Date.now() / 1000) + }, + { + specifiedRelayUrls: publishRelays + } + ) // Show publishing feedback with relay messages if ((publishedEvent as any)?.relayStatuses) { diff --git a/src/components/SearchResult/index.tsx b/src/components/SearchResult/index.tsx index a179f40b..f1a8dc27 100644 --- a/src/components/SearchResult/index.tsx +++ b/src/components/SearchResult/index.tsx @@ -1,9 +1,4 @@ -import { - FAST_READ_RELAY_URLS, - FAST_WRITE_RELAY_URLS, - NIP_SEARCH_PAGE_KINDS, - SEARCHABLE_RELAY_URLS -} from '@/constants' +import { FAST_READ_RELAY_URLS, NIP_SEARCH_PAGE_KINDS, SEARCHABLE_RELAY_URLS } from '@/constants' import { compareEventsForDTagQuery } from '@/lib/dtag-search' import { TSearchParams } from '@/types' import NormalFeed from '../NormalFeed' @@ -47,7 +42,7 @@ export default function SearchResult({ searchParams }: { searchParams: TSearchPa relays.push(...(favoriteRelays || [])) - relays.push(...FAST_READ_RELAY_URLS, ...FAST_WRITE_RELAY_URLS, ...SEARCHABLE_RELAY_URLS) + relays.push(...FAST_READ_RELAY_URLS, ...SEARCHABLE_RELAY_URLS) const normalized = Array.from( new Set(relays.map((url) => normalizeUrl(url) || url).filter((url): url is string => !!url)) diff --git a/src/lib/relay-list-builder.ts b/src/lib/relay-list-builder.ts index c6eba86b..d7e8fa66 100644 --- a/src/lib/relay-list-builder.ts +++ b/src/lib/relay-list-builder.ts @@ -9,7 +9,7 @@ * - Includes seen relays */ -import { FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS, PROFILE_FETCH_RELAY_URLS, SEARCHABLE_RELAY_URLS } from '@/constants' +import { FAST_READ_RELAY_URLS, PROFILE_FETCH_RELAY_URLS, SEARCHABLE_RELAY_URLS } from '@/constants' import { feedRelayPolicyUrls } from '@/features/feed/relay-policy' import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays' import { urlIsNonLocalForRemoteViewer } from '@/lib/relay-list-sanitize' @@ -57,7 +57,10 @@ export interface RelayListBuilderOptions { includeProfileFetchRelays?: boolean /** Whether to include FAST_READ_RELAY_URLS as fallback */ includeFastReadRelays?: boolean - /** Whether to include FAST_WRITE_RELAY_URLS as fallback */ + /** + * Legacy name: adds {@link FAST_READ_RELAY_URLS} as extra bootstrap mirrors for REQ/read lists + * (historically mis-tagged as “fast write”). + */ includeFastWriteRelays?: boolean /** Whether to include SEARCHABLE_RELAY_URLS - for search */ includeSearchableRelays?: boolean @@ -236,9 +239,9 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio FAST_READ_RELAY_URLS.forEach(addRelay) } - // 8. Fast write relays (for writing) + // 8. Extra fast-read bootstrap mirrors (call sites use legacy `includeFastWriteRelays`) if (includeFastWriteRelays) { - FAST_WRITE_RELAY_URLS.forEach(addRelay) + FAST_READ_RELAY_URLS.forEach(addRelay) } // 9. Searchable relays (for search) diff --git a/src/pages/secondary/RssFeedSettingsPage/index.tsx b/src/pages/secondary/RssFeedSettingsPage/index.tsx index df088c0e..6c4e74c0 100644 --- a/src/pages/secondary/RssFeedSettingsPage/index.tsx +++ b/src/pages/secondary/RssFeedSettingsPage/index.tsx @@ -2,7 +2,7 @@ import storage from '@/services/local-storage.service' import { RefreshButton } from '@/components/RefreshButton' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import { usePrimaryNoteView } from '@/contexts/primary-note-view-context' -import { ExtendedKind, FAST_WRITE_RELAY_URLS, PROFILE_RELAY_URLS } from '@/constants' +import { ExtendedKind, FAST_READ_RELAY_URLS, PROFILE_RELAY_URLS } from '@/constants' import { getLatestEvent } from '@/lib/event' import { forwardRef, useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -121,7 +121,7 @@ const RssFeedSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index } setLoading(true) try { - const events = await queryService.fetchEvents(FAST_WRITE_RELAY_URLS.concat(PROFILE_RELAY_URLS), { + const events = await queryService.fetchEvents(FAST_READ_RELAY_URLS.concat(PROFILE_RELAY_URLS), { kinds: [ExtendedKind.RSS_FEED_LIST], authors: [pubkey], limit: 1 diff --git a/src/providers/FeedProvider.tsx b/src/providers/FeedProvider.tsx index 81d3a6f3..6bc3d218 100644 --- a/src/providers/FeedProvider.tsx +++ b/src/providers/FeedProvider.tsx @@ -1,4 +1,4 @@ -import { FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants' +import { FAST_READ_RELAY_URLS } from '@/constants' import { feedRelayPolicyUrls } from '@/features/feed/relay-policy' import { getRelayListFromEvent, getHttpRelayListFromEvent } from '@/lib/event-metadata' import { buildAllFavoritesFeedRelayUrls } from '@/lib/home-feed-relays' @@ -86,7 +86,7 @@ export function FeedProvider({ children }: { children: ReactNode }) { return { inboxRelayUrls: relayList?.read?.length ? relayList.read : FAST_READ_RELAY_URLS, - outboxRelayUrls: relayList?.write?.length ? relayList.write : FAST_WRITE_RELAY_URLS, + outboxRelayUrls: relayList?.write?.length ? relayList.write : FAST_READ_RELAY_URLS, cacheRelayUrls, httpRelayUrls } diff --git a/src/providers/NostrProvider/index.tsx b/src/providers/NostrProvider/index.tsx index 2838f456..f6d38189 100644 --- a/src/providers/NostrProvider/index.tsx +++ b/src/providers/NostrProvider/index.tsx @@ -5,7 +5,6 @@ import { ACCOUNT_SESSION_NETWORK_HYDRATE_MIN_INTERVAL_MS, DEFAULT_FAVORITE_RELAYS, FAST_READ_RELAY_URLS, - FAST_WRITE_RELAY_URLS, ExtendedKind, PROFILE_FETCH_RELAY_URLS, PROFILE_RELAY_URLS, @@ -412,7 +411,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { }) queryService - .fetchEvents(FAST_WRITE_RELAY_URLS.concat(PROFILE_RELAY_URLS), { + .fetchEvents(FAST_READ_RELAY_URLS.concat(PROFILE_RELAY_URLS), { kinds: [ExtendedKind.RSS_FEED_LIST], authors: [account.pubkey], limit: 1 @@ -505,7 +504,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { const normalizedRelays = [ ...mergedRelayList.write.map((url: string) => normalizeUrl(url) || url), ...mergedRelayList.read.map((url: string) => normalizeUrl(url) || url), - ...FAST_WRITE_RELAY_URLS.map((url: string) => normalizeUrl(url) || url), + ...FAST_READ_RELAY_URLS.map((url: string) => normalizeUrl(url) || url), ...PROFILE_FETCH_RELAY_URLS.map((url: string) => normalizeUrl(url) || url) ] const fetchRelays = Array.from(new Set(normalizedRelays)).slice(0, 16) @@ -670,7 +669,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { ...mergedRelayList.read.map((u) => normalizeUrl(u) || u), ...SEARCHABLE_RELAY_URLS.map((u) => normalizeUrl(u) || u), ...PROFILE_FETCH_RELAY_URLS.map((u) => normalizeUrl(u) || u), - ...FAST_WRITE_RELAY_URLS.map((u) => normalizeUrl(u) || u) + ...FAST_READ_RELAY_URLS.map((u) => normalizeUrl(u) || u) ]) ).filter(Boolean) queryService @@ -866,7 +865,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { ...rl.read.map((u) => normalizeUrl(u) || u), ...SEARCHABLE_RELAY_URLS.map((u) => normalizeUrl(u) || u), ...PROFILE_FETCH_RELAY_URLS.map((u) => normalizeUrl(u) || u), - ...FAST_WRITE_RELAY_URLS.map((u) => normalizeUrl(u) || u) + ...FAST_READ_RELAY_URLS.map((u) => normalizeUrl(u) || u) ]) ).filter(Boolean) return queryService.fetchEvents(relays, { diff --git a/src/services/client-replaceable-events.service.ts b/src/services/client-replaceable-events.service.ts index f2e0ceff..18be9a16 100644 --- a/src/services/client-replaceable-events.service.ts +++ b/src/services/client-replaceable-events.service.ts @@ -1,7 +1,6 @@ import { ExtendedKind, FAST_READ_RELAY_URLS, - FAST_WRITE_RELAY_URLS, MAX_CONCURRENT_RELAY_CONNECTIONS, METADATA_BATCH_AUTHORS_CHUNK, METADATA_BATCH_QUERY_EOSE_TIMEOUT_MS, @@ -653,8 +652,8 @@ export class ReplaceableEventService { includeFastReadRelays: true, includeFavoriteRelays: true, includeLocalRelays: true, - /** Many users publish kind 0 to NIP-65 write relays; batch path skipped these before. */ - includeFastWriteRelays: true, + /** Many users publish kind 0 to NIP-65 write relays; batch path includes public read mirrors via {@link buildComprehensiveRelayList}. */ + includeFastWriteRelays: false, includeSearchableRelays: false, preferPublicReadRelaysEarly: true }) @@ -677,52 +676,39 @@ export class ReplaceableEventService { ) ).filter(Boolean) } else if (kind === kinds.Contacts) { - // Contacts (kind 3): often on write relays; aggregators/profile mirrors also carry copies. + // Contacts (kind 3): aggregators + profile mirrors + fast read. relayUrls = Array.from( new Set( - [ - ...FAST_WRITE_RELAY_URLS, - ...READ_ONLY_RELAY_URLS, - ...PROFILE_FETCH_RELAY_URLS, - ...FAST_READ_RELAY_URLS - ].map((u) => normalizeUrl(u) || u) + [...READ_ONLY_RELAY_URLS, ...PROFILE_FETCH_RELAY_URLS, ...FAST_READ_RELAY_URLS].map( + (u) => normalizeUrl(u) || u + ) ) ).filter(Boolean) } else if (kind === kinds.RelayList) { - // NIP-65 (10002): almost always on the author's write/outbox relays; FAST_READ-only misses most users. + // NIP-65 (10002): aggregators + profile mirrors + fast read. relayUrls = Array.from( new Set( - [ - ...FAST_WRITE_RELAY_URLS, - ...READ_ONLY_RELAY_URLS, - ...PROFILE_FETCH_RELAY_URLS, - ...FAST_READ_RELAY_URLS - ].map((u) => normalizeUrl(u) || u) + [...READ_ONLY_RELAY_URLS, ...PROFILE_FETCH_RELAY_URLS, ...FAST_READ_RELAY_URLS].map( + (u) => normalizeUrl(u) || u + ) ) ).filter(Boolean) } else if (kind === kinds.Mutelist || kind === kinds.BookmarkList) { - // Mute / bookmark lists: same distribution as contacts (writes + mirrors); FAST_READ-only misses many copies. + // Mute / bookmark lists: same distribution as contacts; FAST_READ + mirrors. relayUrls = Array.from( new Set( - [ - ...FAST_WRITE_RELAY_URLS, - ...READ_ONLY_RELAY_URLS, - ...PROFILE_FETCH_RELAY_URLS, - ...FAST_READ_RELAY_URLS - ].map((u) => normalizeUrl(u) || u) + [...READ_ONLY_RELAY_URLS, ...PROFILE_FETCH_RELAY_URLS, ...FAST_READ_RELAY_URLS].map( + (u) => normalizeUrl(u) || u + ) ) ).filter(Boolean) } else if (kind === ExtendedKind.PAYMENT_INFO) { - // NIP-A3 kind 10133: often published to the user's write relays only; FAST_READ alone misses many copies. - // Mirror contacts + pin-list coverage (writes + profile mirrors + aggregators + fast read). + // NIP-A3 kind 10133: aggregators + profile mirrors + fast read. relayUrls = Array.from( new Set( - [ - ...FAST_WRITE_RELAY_URLS, - ...READ_ONLY_RELAY_URLS, - ...PROFILE_FETCH_RELAY_URLS, - ...FAST_READ_RELAY_URLS - ].map((u) => normalizeUrl(u) || u) + [...READ_ONLY_RELAY_URLS, ...PROFILE_FETCH_RELAY_URLS, ...FAST_READ_RELAY_URLS].map( + (u) => normalizeUrl(u) || u + ) ) ).filter(Boolean) } else { @@ -1174,7 +1160,7 @@ export class ReplaceableEventService { includeFavoriteRelays: true, includeProfileFetchRelays: true, includeFastReadRelays: true, - includeFastWriteRelays: true, + includeFastWriteRelays: false, includeSearchableRelays: true, includeLocalRelays: true }) @@ -1400,7 +1386,7 @@ export class ReplaceableEventService { /** * Fetch follow list event. * When relayUrls are provided (e.g. user write + search relays), queries those directly. - * Otherwise uses the default relay set (FAST_WRITE + PROFILE_FETCH + FAST_READ). + * Otherwise uses the default relay set (READ_ONLY + PROFILE_FETCH + FAST_READ). */ /** Hard cap: {@link fetchReplaceableEvent} can otherwise wedge the DataLoader chain when relays never answer. */ private static readonly FETCH_FOLLOW_LIST_REPLACEABLE_TIMEOUT_MS = 14_000 @@ -1557,7 +1543,7 @@ export class ReplaceableEventService { includeFavoriteRelays: true, includeProfileFetchRelays: true, includeFastReadRelays: true, - includeFastWriteRelays: true, + includeFastWriteRelays: false, includeSearchableRelays: true, includeLocalRelays: true }) diff --git a/src/services/media-upload.service.ts b/src/services/media-upload.service.ts index 2f7feba1..990c325f 100644 --- a/src/services/media-upload.service.ts +++ b/src/services/media-upload.service.ts @@ -1,4 +1,4 @@ -/** Compression runs entirely in-app before upload (`compress-upload-media`). Load `local-storage` before `./client.service` (that graph can re-enter here; constructor reads storage). */ +/** Compression runs entirely in-app before upload (`compress-upload-media`). Load `local-storage` before `./client.service`; the default export is lazily constructed so `client`↔`draft-event`↔this module cycles cannot run the constructor before `storage` is initialized. */ import storage from './local-storage.service' import { compressMediaForUpload } from '@/lib/compress-upload-media' import { fetchWithTimeout } from '@/lib/fetch-with-timeout' @@ -312,5 +312,21 @@ class MediaUploadService { } } -const instance = new MediaUploadService() +/** + * Eager `new MediaUploadService()` at module load can run while `storage` is still in the TDZ: + * `client.service` (and its graph) may synchronously pull `draft-event` → this module again + * before static imports have finished binding. Lazily construct on first property access. + */ +function createMediaUploadServiceLazy(): MediaUploadService { + let inner: MediaUploadService | undefined + return new Proxy({} as MediaUploadService, { + get(_target, prop, receiver) { + if (!inner) inner = new MediaUploadService() + const v = Reflect.get(inner, prop, receiver) as unknown + return typeof v === 'function' ? (v as (...args: unknown[]) => unknown).bind(inner) : v + } + }) +} + +const instance = createMediaUploadServiceLazy() export default instance diff --git a/src/services/spell.service.ts b/src/services/spell.service.ts index 3f04a26e..dfa75a8a 100644 --- a/src/services/spell.service.ts +++ b/src/services/spell.service.ts @@ -2,7 +2,7 @@ * NIP-A7 Spells: parse and execute kind 777 events as portable relay query filters. */ -import { ExtendedKind, FAST_WRITE_RELAY_URLS } from '@/constants' +import { ExtendedKind, FAST_READ_RELAY_URLS } from '@/constants' import { getRelayUrlsWithFavoritesFastReadAndInbox } from '@/lib/favorites-feed-relays' import { tagNameEquals } from '@/lib/tag' import logger from '@/lib/logger' @@ -48,9 +48,9 @@ export type SpellExecutionContext = { contacts: string[] } -/** When the spell has no `relays` tag and NIP-65 write list is empty: known-good write relays. */ -function defaultSpellWriteFallbackRelays(): string[] { - return dedupeRelayUrls([...FAST_WRITE_RELAY_URLS]) +/** When the spell has no `relays` tag and NIP-65 write list is empty: known-good read mirrors for REQ. */ +function defaultSpellRelayFallbackRelays(): string[] { + return dedupeRelayUrls([...FAST_READ_RELAY_URLS]) } /** Max kind-777 events to pull when syncing spell definitions from relays (you only). */ @@ -109,15 +109,15 @@ function dedupeRelayUrls(urls: string[]): string[] { export type GetRelaysForSpellOptions = { /** - * When true (default): merge FAST_WRITE after the primary list (REQ feeds) for resilience. - * When false: use only spell `relays` tag, NIP-65 write relays, or write fallback — no extra padding (COUNT). + * When true (default): merge {@link FAST_READ_RELAY_URLS} after the primary list (REQ) for resilience. + * When false: use only spell `relays` tag, NIP-65 write relays, or read fallback — no extra padding (COUNT). */ mergeDefaultReadRelays?: boolean } /** * Get relay URLs for executing a spell: spell `relays` tag, else the user's NIP-65 **write** (outbox) relays. - * Publishing and running spells use outboxes only (plus optional FAST_WRITE padding when mergeDefaults is true). + * Running a spell issues REQ queries; optional {@link FAST_READ_RELAY_URLS} padding helps when primaries are slow. */ export function getRelaysForSpell( spell: Event, @@ -137,10 +137,10 @@ export function getRelaysForSpell( primary = [...context.relayListWrite] } if (!primary.length) { - return defaultSpellWriteFallbackRelays() + return defaultSpellRelayFallbackRelays() } if (mergeDefaults) { - return dedupeRelayUrls([...primary, ...FAST_WRITE_RELAY_URLS]) + return dedupeRelayUrls([...primary, ...FAST_READ_RELAY_URLS]) } return dedupeRelayUrls(primary) }