Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
e8019fa14b
  1. 2
      src/PageManager.tsx
  2. 2
      src/components/CreateWalletGuideToast/index.tsx
  3. 3
      src/components/FormattedTimestamp/index.tsx
  4. 2
      src/components/LiveActivitiesStrip.tsx
  5. 2
      src/components/NormalFeed/index.tsx
  6. 2
      src/components/NoteOptions/EditOrCloneEventDialog.tsx
  7. 2
      src/components/NoteStats/LikeButton.tsx
  8. 2
      src/components/NoteStats/Likes.tsx
  9. 2
      src/components/NoteStats/RepostButton.tsx
  10. 2
      src/components/PostEditor/PostContent.tsx
  11. 2
      src/components/Sidebar/RssButton.tsx
  12. 2
      src/components/TooManyRelaysAlertDialog/index.tsx
  13. 4
      src/i18n/index.ts
  14. 35
      src/lib/calendar-event.ts
  15. 2
      src/lib/publishing-feedback.tsx
  16. 2
      src/main.tsx
  17. 2
      src/pages/primary/CalendarPrimaryPage.tsx
  18. 2
      src/pages/primary/SpellsPage/useSpellsPageFeed.ts
  19. 2
      src/pages/secondary/RssFeedSettingsPage/index.tsx
  20. 2
      src/providers/FavoriteRelaysActivityProvider.tsx
  21. 2
      src/providers/FavoriteRelaysProvider.tsx
  22. 2
      src/providers/LiveActivitiesProvider.tsx
  23. 2
      src/providers/NostrProvider/index.tsx
  24. 4
      src/providers/UserTrustProvider.tsx
  25. 160
      src/services/client-query.service.ts
  26. 21
      src/services/client.service.ts
  27. 5
      src/services/media-upload.service.ts
  28. 2
      src/services/post-editor-cache.service.ts
  29. 33
      src/services/relay-operation-log.service.ts
  30. 2
      src/services/relay-selection.service.ts

2
src/PageManager.tsx

@ -2,6 +2,7 @@
// making it incompatible with Vite's Fast Refresh auto-detection. Opting into explicit // making it incompatible with Vite's Fast Refresh auto-detection. Opting into explicit
// full-reload mode to suppress the "incompatible export" HMR warning. // full-reload mode to suppress the "incompatible export" HMR warning.
// @refresh reset // @refresh reset
import storage from '@/services/local-storage.service'
import { RefreshButton } from '@/components/RefreshButton' import { RefreshButton } from '@/components/RefreshButton'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
@ -13,7 +14,6 @@ import { NavigationService } from '@/services/navigation.service'
import { ImwaldBrandBar } from '@/assets/Logo' import { ImwaldBrandBar } from '@/assets/Logo'
import LiveActivitiesStrip from '@/components/LiveActivitiesStrip' import LiveActivitiesStrip from '@/components/LiveActivitiesStrip'
import NoteDrawer from '@/components/NoteDrawer' import NoteDrawer from '@/components/NoteDrawer'
import storage from '@/services/local-storage.service'
import client from '@/services/client.service' import client from '@/services/client.service'
import { navigationEventStore } from '@/services/navigation-event-store' import { navigationEventStore } from '@/services/navigation-event-store'
import type { Event } from 'nostr-tools' import type { Event } from 'nostr-tools'

2
src/components/CreateWalletGuideToast/index.tsx

@ -1,7 +1,7 @@
import storage from '@/services/local-storage.service'
import { toWallet } from '@/lib/link' import { toWallet } from '@/lib/link'
import { useSecondaryPage } from '@/contexts/secondary-page-context' import { useSecondaryPage } from '@/contexts/secondary-page-context'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import storage from '@/services/local-storage.service'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'

3
src/components/FormattedTimestamp/index.tsx

@ -25,6 +25,9 @@ function FormattedTimestampContent({
short?: boolean short?: boolean
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
if (!Number.isFinite(timestamp)) {
return '\u2014'
}
const time = dayjs(timestamp * 1000) const time = dayjs(timestamp * 1000)
const now = dayjs() const now = dayjs()

2
src/components/LiveActivitiesStrip.tsx

@ -1,10 +1,10 @@
import storage from '@/services/local-storage.service'
import { LIVE_ACTIVITIES_SLIDE_INTERVAL_MS } from '@/lib/live-activities' import { LIVE_ACTIVITIES_SLIDE_INTERVAL_MS } from '@/lib/live-activities'
import { toNote } from '@/lib/link' import { toNote } from '@/lib/link'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useSmartNoteNavigation } from '@/PageManager' import { useSmartNoteNavigation } from '@/PageManager'
import { useLiveActivitiesOptional } from '@/providers/useLiveActivities' import { useLiveActivitiesOptional } from '@/providers/useLiveActivities'
import { useUserPreferencesOptional } from '@/providers/UserPreferencesProvider' import { useUserPreferencesOptional } from '@/providers/UserPreferencesProvider'
import storage from '@/services/local-storage.service'
import { ExternalLink } from 'lucide-react' import { ExternalLink } from 'lucide-react'
import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react' import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'

2
src/components/NormalFeed/index.tsx

@ -1,9 +1,9 @@
import storage from '@/services/local-storage.service'
import NoteList, { TNoteListRef } from '@/components/NoteList' import NoteList, { TNoteListRef } from '@/components/NoteList'
import { RefreshButton } from '@/components/RefreshButton' import { RefreshButton } from '@/components/RefreshButton'
import Tabs, { TabDefinition } from '@/components/Tabs' import Tabs, { TabDefinition } from '@/components/Tabs'
import { useKindFilterOrDefaults } from '@/providers/KindFilterProvider' import { useKindFilterOrDefaults } from '@/providers/KindFilterProvider'
import { useUserTrust } from '@/contexts/user-trust-context' import { useUserTrust } from '@/contexts/user-trust-context'
import storage from '@/services/local-storage.service'
import { PROFILE_MEDIA_TAB_KINDS, FAST_READ_RELAY_URLS } from '@/constants' import { PROFILE_MEDIA_TAB_KINDS, FAST_READ_RELAY_URLS } from '@/constants'
import { isWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay' import { isWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay'
import type { TPrimaryPageName } from '@/PageManager' import type { TPrimaryPageName } from '@/PageManager'

2
src/components/NoteOptions/EditOrCloneEventDialog.tsx

@ -1,3 +1,4 @@
import storage from '@/services/local-storage.service'
import { Card } from '@/components/ui/card' import { Card } from '@/components/ui/card'
import { import {
Dialog, Dialog,
@ -39,7 +40,6 @@ import {
} from '@/lib/publishing-feedback' } from '@/lib/publishing-feedback'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import storage from '@/services/local-storage.service'
import postEditorCache from '@/services/post-editor-cache.service' import postEditorCache from '@/services/post-editor-cache.service'
import type { TDraftEvent } from '@/types' import type { TDraftEvent } from '@/types'
import dayjs from 'dayjs' import dayjs from 'dayjs'

2
src/components/NoteStats/LikeButton.tsx

@ -1,3 +1,4 @@
import storage from '@/services/local-storage.service'
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer' import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer'
import { import {
DropdownMenu, DropdownMenu,
@ -26,7 +27,6 @@ import { useUserTrust } from '@/contexts/user-trust-context'
import { eventService } from '@/services/client.service' import { eventService } from '@/services/client.service'
import noteStatsService from '@/services/note-stats.service' import noteStatsService from '@/services/note-stats.service'
import type { TNoteStats } from '@/services/note-stats.service' import type { TNoteStats } from '@/services/note-stats.service'
import storage from '@/services/local-storage.service'
import { TEmoji } from '@/types' import { TEmoji } from '@/types'
import { SmilePlus } from 'lucide-react' import { SmilePlus } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'

2
src/components/NoteStats/Likes.tsx

@ -1,3 +1,4 @@
import storage from '@/services/local-storage.service'
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card' import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
@ -9,7 +10,6 @@ import { useNostr } from '@/providers/NostrProvider'
import { useUserTrust } from '@/contexts/user-trust-context' import { useUserTrust } from '@/contexts/user-trust-context'
import noteStatsService from '@/services/note-stats.service' import noteStatsService from '@/services/note-stats.service'
import type { TNoteStats } from '@/services/note-stats.service' import type { TNoteStats } from '@/services/note-stats.service'
import storage from '@/services/local-storage.service'
import { TEmoji } from '@/types' import { TEmoji } from '@/types'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useMemo, useRef, useState } from 'react' import { useMemo, useRef, useState } from 'react'

2
src/components/NoteStats/RepostButton.tsx

@ -1,3 +1,4 @@
import storage from '@/services/local-storage.service'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerOverlay } from '@/components/ui/drawer' import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerOverlay } from '@/components/ui/drawer'
@ -17,7 +18,6 @@ import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useUserTrust } from '@/contexts/user-trust-context' import { useUserTrust } from '@/contexts/user-trust-context'
import noteStatsService from '@/services/note-stats.service' import noteStatsService from '@/services/note-stats.service'
import type { TNoteStats } from '@/services/note-stats.service' import type { TNoteStats } from '@/services/note-stats.service'
import storage from '@/services/local-storage.service'
import { PencilLine, Repeat } from 'lucide-react' import { PencilLine, Repeat } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'

2
src/components/PostEditor/PostContent.tsx

@ -1,3 +1,4 @@
import storage from '@/services/local-storage.service'
import Note from '@/components/Note' import Note from '@/components/Note'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
@ -51,7 +52,6 @@ import { cleanUrl, rewritePlainTextHttpUrls } from '@/lib/url'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { LoginRequiredError } from '@/lib/nostr-errors' import { LoginRequiredError } from '@/lib/nostr-errors'
import postEditorCache from '@/services/post-editor-cache.service' import postEditorCache from '@/services/post-editor-cache.service'
import storage from '@/services/local-storage.service'
import { TPollCreateData } from '@/types' import { TPollCreateData } from '@/types'
import { import {
Book, Book,

2
src/components/Sidebar/RssButton.tsx

@ -1,9 +1,9 @@
import storage from '@/services/local-storage.service'
import { usePrimaryPage } from '@/contexts/primary-page-context' import { usePrimaryPage } from '@/contexts/primary-page-context'
import { usePrimaryNoteView } from '@/contexts/primary-note-view-context' import { usePrimaryNoteView } from '@/contexts/primary-note-view-context'
import { Rss } from 'lucide-react' import { Rss } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import SidebarItem from './SidebarItem' import SidebarItem from './SidebarItem'
import storage from '@/services/local-storage.service'
export default function RssButton() { export default function RssButton() {
const { t } = useTranslation() const { t } = useTranslation()

2
src/components/TooManyRelaysAlertDialog/index.tsx

@ -1,3 +1,4 @@
import storage from '@/services/local-storage.service'
import { import {
AlertDialog, AlertDialog,
AlertDialogContent, AlertDialogContent,
@ -19,7 +20,6 @@ import { toRelaySettings } from '@/lib/link'
import { useSecondaryPage } from '@/contexts/secondary-page-context' import { useSecondaryPage } from '@/contexts/secondary-page-context'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import storage from '@/services/local-storage.service'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'

4
src/i18n/index.ts

@ -65,6 +65,10 @@ export function initI18n(): Promise<void> {
}) })
i18n.services.formatter?.add('date', (timestamp, lng) => { i18n.services.formatter?.add('date', (timestamp, lng) => {
const n = Number(timestamp)
if (!Number.isFinite(n)) {
return '\u2014'
}
switch (lng) { switch (lng) {
case 'zh': case 'zh':
return dayjs(timestamp).format('YYYY年MM月DD日') return dayjs(timestamp).format('YYYY年MM月DD日')

35
src/lib/calendar-event.ts

@ -240,10 +240,24 @@ export function stripCalendarEventRedundantTopicHashtagLines(
const CALENDAR_DISPLAY_LOCALE = 'en-US' const CALENDAR_DISPLAY_LOCALE = 'en-US'
/** Safe fallback when `Date` is invalid — avoids `Intl.DateTimeFormat#formatToParts` throwing. */
const INVALID_DATE_PARTS: Record<Intl.DateTimeFormatPartTypes, string> = new Proxy(
{} as Record<Intl.DateTimeFormatPartTypes, string>,
{
get(_target, prop: string | symbol) {
if (typeof prop !== 'string') return '\u2014'
if (prop === 'literal') return ''
if (prop === 'dayPeriod' || prop === 'timeZoneName') return ''
return '\u2014'
}
}
)
function readFormatParts( function readFormatParts(
d: Date, d: Date,
opts: Intl.DateTimeFormatOptions opts: Intl.DateTimeFormatOptions
): Record<Intl.DateTimeFormatPartTypes, string> { ): Record<Intl.DateTimeFormatPartTypes, string> {
if (!Number.isFinite(d.getTime())) return INVALID_DATE_PARTS
const out: Partial<Record<Intl.DateTimeFormatPartTypes, string>> = {} const out: Partial<Record<Intl.DateTimeFormatPartTypes, string>> = {}
for (const p of new Intl.DateTimeFormat(CALENDAR_DISPLAY_LOCALE, opts).formatToParts(d)) { for (const p of new Intl.DateTimeFormat(CALENDAR_DISPLAY_LOCALE, opts).formatToParts(d)) {
if (p.type !== 'literal') out[p.type] = p.value if (p.type !== 'literal') out[p.type] = p.value
@ -256,7 +270,11 @@ function readFormatParts(
* (e.g. `May 13, 2025 10:30 am EST`) in the viewer's local zone avoids DD/MM vs MM/DD ambiguity. * (e.g. `May 13, 2025 10:30 am EST`) in the viewer's local zone avoids DD/MM vs MM/DD ambiguity.
*/ */
export function formatCalendarTime(ts: number): string { export function formatCalendarTime(ts: number): string {
const d = new Date(ts * 1000) if (!Number.isFinite(ts)) return '\u2014'
const ms = ts * 1000
if (!Number.isFinite(ms)) return '\u2014'
const d = new Date(ms)
if (!Number.isFinite(d.getTime())) return '\u2014'
const p = readFormatParts(d, { const p = readFormatParts(d, {
month: 'long', month: 'long',
day: 'numeric', day: 'numeric',
@ -313,6 +331,7 @@ export function formatCalendarTimeRange(start: number, end: number | undefined):
export function formatCalendarDate(dateStr: string): string { export function formatCalendarDate(dateStr: string): string {
if (!dateStr) return '' if (!dateStr) return ''
const d = new Date(dateStr + 'T12:00:00') const d = new Date(dateStr + 'T12:00:00')
if (!Number.isFinite(d.getTime())) return ''
const p = readFormatParts(d, { month: 'long', day: 'numeric', year: 'numeric' }) const p = readFormatParts(d, { month: 'long', day: 'numeric', year: 'numeric' })
return `${p.month} ${p.day}, ${p.year}` return `${p.month} ${p.day}, ${p.year}`
} }
@ -331,7 +350,13 @@ const NIP52_SECONDS_PER_DAY = 86400
function nip52DayIndexToUtcCalendarParts(dayIndex: number): { month: string; day: string; year: string } { function nip52DayIndexToUtcCalendarParts(dayIndex: number): { month: string; day: string; year: string } {
const ms = dayIndex * NIP52_SECONDS_PER_DAY * 1000 const ms = dayIndex * NIP52_SECONDS_PER_DAY * 1000
if (!Number.isFinite(ms)) {
return { month: '\u2014', day: '\u2014', year: '\u2014' }
}
const d = new Date(ms) const d = new Date(ms)
if (!Number.isFinite(d.getTime())) {
return { month: '\u2014', day: '\u2014', year: '\u2014' }
}
const parts = new Intl.DateTimeFormat(CALENDAR_DISPLAY_LOCALE, { const parts = new Intl.DateTimeFormat(CALENDAR_DISPLAY_LOCALE, {
month: 'long', month: 'long',
day: 'numeric', day: 'numeric',
@ -507,8 +532,10 @@ function toYmdLocal(d: Date): string {
/** Compact week banner for sidebar (en-US month names). */ /** Compact week banner for sidebar (en-US month names). */
export function formatSidebarWeekLabel(weekStartMs: number, weekEndExclusiveMs: number): string { export function formatSidebarWeekLabel(weekStartMs: number, weekEndExclusiveMs: number): string {
if (!Number.isFinite(weekStartMs) || !Number.isFinite(weekEndExclusiveMs)) return ''
const start = new Date(weekStartMs) const start = new Date(weekStartMs)
const last = new Date(weekEndExclusiveMs) const last = new Date(weekEndExclusiveMs)
if (!Number.isFinite(start.getTime()) || !Number.isFinite(last.getTime())) return ''
last.setDate(last.getDate() - 1) last.setDate(last.getDate() - 1)
const y1 = start.getFullYear() const y1 = start.getFullYear()
const y2 = last.getFullYear() const y2 = last.getFullYear()
@ -542,8 +569,9 @@ export function formatCalendarSidebarRow(event: Event): string {
} }
return a return a
} }
if (m.start == null || Number.isNaN(m.start)) return '' if (m.start == null || Number.isNaN(m.start) || !Number.isFinite(m.start)) return ''
const d = new Date(m.start * 1000) const d = new Date(m.start * 1000)
if (!Number.isFinite(d.getTime())) return ''
const p = readFormatParts(d, { const p = readFormatParts(d, {
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',
@ -554,8 +582,9 @@ export function formatCalendarSidebarRow(event: Event): string {
}) })
const ap = (p.dayPeriod ?? '').toLowerCase() const ap = (p.dayPeriod ?? '').toLowerCase()
const base = `${p.month} ${p.day} · ${p.hour}:${p.minute} ${ap} ${p.timeZoneName ?? ''}`.trim() const base = `${p.month} ${p.day} · ${p.hour}:${p.minute} ${ap} ${p.timeZoneName ?? ''}`.trim()
if (m.end != null && !Number.isNaN(m.end) && m.end > m.start) { if (m.end != null && !Number.isNaN(m.end) && Number.isFinite(m.end) && m.end > m.start) {
const d2 = new Date(m.end * 1000) const d2 = new Date(m.end * 1000)
if (!Number.isFinite(d2.getTime())) return base
const p2 = readFormatParts(d2, { const p2 = readFormatParts(d2, {
hour: 'numeric', hour: 'numeric',
minute: '2-digit', minute: '2-digit',

2
src/lib/publishing-feedback.tsx

@ -1,9 +1,9 @@
import storage from '@/services/local-storage.service'
import RelayStatusDisplay from '@/components/RelayStatusDisplay' import RelayStatusDisplay from '@/components/RelayStatusDisplay'
import { CheckCircle2 } from 'lucide-react' import { CheckCircle2 } from 'lucide-react'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { useContext } from 'react' import { useContext } from 'react'
import { FavoriteRelaysContext } from '@/providers/favorite-relays-context' import { FavoriteRelaysContext } from '@/providers/favorite-relays-context'
import storage from '@/services/local-storage.service'
import { toast } from 'sonner' import { toast } from 'sonner'
export type PublishSuccessSubtleDetail = { message?: string } export type PublishSuccessSubtleDetail = { message?: string }

2
src/main.tsx

@ -1,6 +1,7 @@
import './index.css' import './index.css'
import './polyfill' import './polyfill'
import './lib/error-suppression' import './lib/error-suppression'
import storage from './services/local-storage.service'
import './services/lightning.service' import './services/lightning.service'
import './lib/debug-utils' import './lib/debug-utils'
import { fetchWithTimeout } from './lib/fetch-with-timeout' import { fetchWithTimeout } from './lib/fetch-with-timeout'
@ -10,7 +11,6 @@ import { createRoot } from 'react-dom/client'
import App from './App.tsx' import App from './App.tsx'
import { ErrorBoundary } from './components/ErrorBoundary.tsx' import { ErrorBoundary } from './components/ErrorBoundary.tsx'
import { initI18n } from './i18n' import { initI18n } from './i18n'
import storage from './services/local-storage.service'
import { restoreSessionFeedSnapshotsAfterHardRefresh } from './services/session-feed-snapshot.service' import { restoreSessionFeedSnapshotsAfterHardRefresh } from './services/session-feed-snapshot.service'
import { installStaleBuildChunkRecovery } from './lib/stale-chunk-recovery' import { installStaleBuildChunkRecovery } from './lib/stale-chunk-recovery'

2
src/pages/primary/CalendarPrimaryPage.tsx

@ -1,3 +1,4 @@
import storage from '@/services/local-storage.service'
import PrimaryPageLayout, { type TPrimaryPageLayoutRef } from '@/layouts/PrimaryPageLayout' import PrimaryPageLayout, { type TPrimaryPageLayoutRef } from '@/layouts/PrimaryPageLayout'
import { import {
calendarOccurrenceOverlapsRange, calendarOccurrenceOverlapsRange,
@ -20,7 +21,6 @@ import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import client from '@/services/client.service' import client from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service' import indexedDb from '@/services/indexed-db.service'
import storage from '@/services/local-storage.service'
import { CALENDAR_EVENT_KINDS, ExtendedKind } from '@/constants' import { CALENDAR_EVENT_KINDS, ExtendedKind } from '@/constants'
import { TPageRef } from '@/types' import { TPageRef } from '@/types'
import { CalendarEventCoverImage } from '@/components/CalendarEventCoverImage' import { CalendarEventCoverImage } from '@/components/CalendarEventCoverImage'

2
src/pages/primary/SpellsPage/useSpellsPageFeed.ts

@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import type { Event } from 'nostr-tools' import type { Event } from 'nostr-tools'
import { kinds as nostrKinds } from 'nostr-tools' import { kinds as nostrKinds } from 'nostr-tools'
import storage from '@/services/local-storage.service'
import { ExtendedKind, DEFAULT_FEED_SHOW_KINDS } from '@/constants' import { ExtendedKind, DEFAULT_FEED_SHOW_KINDS } from '@/constants'
import { getPubkeysFromPTags } from '@/lib/tag' import { getPubkeysFromPTags } from '@/lib/tag'
import { normalizeUrl } from '@/lib/url' import { normalizeUrl } from '@/lib/url'
@ -36,7 +37,6 @@ import {
import { getRelaysForSpell, spellEventToFilter } from '@/services/spell.service' import { getRelaysForSpell, spellEventToFilter } from '@/services/spell.service'
import type { TFeedSubRequest } from '@/types' import type { TFeedSubRequest } from '@/types'
import { isFollowFeedFauxSpellId } from './fauxSpellConfig' import { isFollowFeedFauxSpellId } from './fauxSpellConfig'
import storage from '@/services/local-storage.service'
/** `fetchReplaceableEvent(kind 3)` / relay-list hydration can hang; never block the Following spell on it. */ /** `fetchReplaceableEvent(kind 3)` / relay-list hydration can hang; never block the Following spell on it. */
const FOLLOWING_FETCH_FOLLOWINGS_TIMEOUT_MS = 10_000 const FOLLOWING_FETCH_FOLLOWINGS_TIMEOUT_MS = 10_000

2
src/pages/secondary/RssFeedSettingsPage/index.tsx

@ -1,3 +1,4 @@
import storage from '@/services/local-storage.service'
import { RefreshButton } from '@/components/RefreshButton' import { RefreshButton } from '@/components/RefreshButton'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { usePrimaryNoteView } from '@/contexts/primary-note-view-context' import { usePrimaryNoteView } from '@/contexts/primary-note-view-context'
@ -11,7 +12,6 @@ import { Skeleton } from '@/components/ui/skeleton'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import storage from '@/services/local-storage.service'
import { createRssFeedListDraftEvent } from '@/lib/draft-event' import { createRssFeedListDraftEvent } from '@/lib/draft-event'
import { showPublishingFeedback, showSimplePublishSuccess, showPublishingError } from '@/lib/publishing-feedback' import { showPublishingFeedback, showSimplePublishSuccess, showPublishingError } from '@/lib/publishing-feedback'
import { CloudUpload, Trash2, Plus, Download, Upload } from 'lucide-react' import { CloudUpload, Trash2, Plus, Download, Upload } from 'lucide-react'

2
src/providers/FavoriteRelaysActivityProvider.tsx

@ -1,3 +1,4 @@
import storage from '@/services/local-storage.service'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { ExtendedKind, NIP71_VIDEO_KINDS } from '@/constants' import { ExtendedKind, NIP71_VIDEO_KINDS } from '@/constants'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays' import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
@ -12,7 +13,6 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { queryService, replaceableEventService } from '@/services/client.service' import { queryService, replaceableEventService } from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service' import indexedDb from '@/services/indexed-db.service'
import storage from '@/services/local-storage.service'
import type { Event } from 'nostr-tools' import type { Event } from 'nostr-tools'
import { kinds } from 'nostr-tools' import { kinds } from 'nostr-tools'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'

2
src/providers/FavoriteRelaysProvider.tsx

@ -1,4 +1,5 @@
import { FAST_READ_RELAY_URLS, DEFAULT_FAVORITE_RELAYS } from '@/constants' import { FAST_READ_RELAY_URLS, DEFAULT_FAVORITE_RELAYS } from '@/constants'
import storage from '@/services/local-storage.service'
import { createFavoriteRelaysDraftEvent, createBlockedRelaysDraftEvent, createRelaySetDraftEvent } from '@/lib/draft-event' import { createFavoriteRelaysDraftEvent, createBlockedRelaysDraftEvent, createRelaySetDraftEvent } from '@/lib/draft-event'
import { getReplaceableEventIdentifier } from '@/lib/event' import { getReplaceableEventIdentifier } from '@/lib/event'
import { getRelaySetFromEvent } from '@/lib/event-metadata' import { getRelaySetFromEvent } from '@/lib/event-metadata'
@ -6,7 +7,6 @@ import { randomString } from '@/lib/random'
import { isHttpRelayUrl, isWebsocketUrl, normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url' import { isHttpRelayUrl, isWebsocketUrl, normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url'
import { queryService } from '@/services/client.service' import { queryService } from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service' import indexedDb from '@/services/indexed-db.service'
import storage from '@/services/local-storage.service'
import { TRelaySet } from '@/types' import { TRelaySet } from '@/types'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'

2
src/providers/LiveActivitiesProvider.tsx

@ -1,3 +1,4 @@
import storage from '@/services/local-storage.service'
import { import {
buildLiveActivitiesRelayUrls, buildLiveActivitiesRelayUrls,
filterLiveActivityItemsByReachableMedia, filterLiveActivityItemsByReachableMedia,
@ -11,7 +12,6 @@ import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import client from '@/services/client.service' import client from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service' import indexedDb from '@/services/indexed-db.service'
import storage from '@/services/local-storage.service'
import { registerLiveActivitiesPrewarmCallback } from '@/services/live-activities-prewarm-bridge' import { registerLiveActivitiesPrewarmCallback } from '@/services/live-activities-prewarm-bridge'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { LiveActivitiesContext } from './live-activities-context' import { LiveActivitiesContext } from './live-activities-context'

2
src/providers/NostrProvider/index.tsx

@ -1,3 +1,4 @@
import storage from '@/services/local-storage.service'
import LoginDialog from '@/components/LoginDialog' import LoginDialog from '@/components/LoginDialog'
import NcryptsecPasswordPrompt from '@/components/NcryptsecPasswordPrompt' import NcryptsecPasswordPrompt from '@/components/NcryptsecPasswordPrompt'
import { import {
@ -34,7 +35,6 @@ import { queryService, replaceableEventService } from '@/services/client.service
import customEmojiService from '@/services/custom-emoji.service' import customEmojiService from '@/services/custom-emoji.service'
import indexedDb from '@/services/indexed-db.service' import indexedDb from '@/services/indexed-db.service'
import postEditorCache from '@/services/post-editor-cache.service' import postEditorCache from '@/services/post-editor-cache.service'
import storage from '@/services/local-storage.service'
import noteStatsService from '@/services/note-stats.service' import noteStatsService from '@/services/note-stats.service'
import { import {
ISigner, ISigner,

4
src/providers/UserTrustProvider.tsx

@ -1,7 +1,5 @@
import { replaceableEventService } from '@/services/client.service'
import { getPubkeysFromPTags } from '@/lib/tag'
import { kinds } from 'nostr-tools'
import storage from '@/services/local-storage.service' import storage from '@/services/local-storage.service'
import { replaceableEventService } from '@/services/client.service'
import { UserTrustContext } from '@/contexts/user-trust-context' import { UserTrustContext } from '@/contexts/user-trust-context'
import { type ReactNode, useCallback, useEffect, useState } from 'react' import { type ReactNode, useCallback, useEffect, useState } from 'react'
import { useNostr } from './NostrProvider' import { useNostr } from './NostrProvider'

160
src/services/client-query.service.ts

@ -29,7 +29,13 @@ import { applyRelayNip42AckTimeout } from '@/lib/relay-nip42-tuning'
import { isIndexRelayTransportFailure, queryIndexRelay } from '@/lib/index-relay-http' import { isIndexRelayTransportFailure, queryIndexRelay } from '@/lib/index-relay-http'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { isHttpRelayUrl, normalizeHttpRelayUrl, normalizeUrl } from '@/lib/url' import { isHttpRelayUrl, normalizeHttpRelayUrl, normalizeUrl } from '@/lib/url'
import { RelaySubscribeOpBatch } from '@/services/relay-operation-log.service' import {
RelaySubscribeOpBatch,
compactFilterForRelayLog,
humanizeSubscribeTerminalDetail,
relayHostForSubscribeLog,
type RelayOpTerminalRow
} from '@/services/relay-operation-log.service'
import { patchRelayNoticeForFetchFailures } from '@/services/relay-notice-fetch-failure' import { patchRelayNoticeForFetchFailures } from '@/services/relay-notice-fetch-failure'
import type { Filter, Event as NEvent } from 'nostr-tools' import type { Filter, Event as NEvent } from 'nostr-tools'
import { SimplePool, EventTemplate, VerifiedEvent, nip19 } from 'nostr-tools' import { SimplePool, EventTemplate, VerifiedEvent, nip19 } from 'nostr-tools'
@ -47,6 +53,74 @@ function filterForRelay(f: Filter, relaySupportsSearch: boolean): Filter {
const HEX_EVENT_ID_RE = /^[0-9a-f]{64}$/i const HEX_EVENT_ID_RE = /^[0-9a-f]{64}$/i
let queryReqSeq = 0
function logQueryReqConsolidatedEnd(
reqId: number,
source: string,
inputRelays: string[],
httpBases: string[],
events: NEvent[],
terminals: RelayOpTerminalRow[],
getSeenForEvent: (eventId: string) => string[]
): void {
const kindHistogram: Record<string, number> = {}
for (const e of events) {
const k = String(e.kind)
kindHistogram[k] = (kindHistogram[k] ?? 0) + 1
}
const norm = (u: string) => normalizeUrl(u) || u
type Row = {
url: string
host: string
terminal?: RelayOpTerminalRow['outcome']
detail?: string
eventsReturned: number
}
const byKey = new Map<string, Row>()
const rowFor = (url: string): Row => {
const key = norm(url)
let r = byKey.get(key)
if (!r) {
r = { url: key, host: relayHostForSubscribeLog(key), eventsReturned: 0 }
byKey.set(key, r)
}
return r
}
for (const t of terminals) {
const r = rowFor(t.relayUrl)
r.terminal = t.outcome
r.detail = humanizeSubscribeTerminalDetail(t.outcome, t.detail)
}
for (const e of events) {
const seen = getSeenForEvent(e.id)
for (const u of seen) {
rowFor(u).eventsReturned += 1
}
}
for (const u of inputRelays) {
rowFor(u)
}
for (const b of httpBases) {
rowFor(b)
}
const perRelay = [...byKey.values()].sort((a, b) => a.host.localeCompare(b.host))
logger.debug('[QueryService] req_end', {
reqId,
source,
eventCount: events.length,
kindHistogram,
perRelay
})
}
function decodeEventRefForETagFilter(raw: string): string | null { function decodeEventRefForETagFilter(raw: string): string | null {
const trimmed = raw.trim() const trimmed = raw.trim()
if (!trimmed) return null if (!trimmed) return null
@ -314,19 +388,6 @@ export class QueryService {
const replaceableRace = options?.replaceableRace ?? false const replaceableRace = options?.replaceableRace ?? false
const replaceableRaceWaitMs = options?.replaceableRaceWaitMs ?? FIRST_RELAY_RESULT_GRACE_MS const replaceableRaceWaitMs = options?.replaceableRaceWaitMs ?? FIRST_RELAY_RESULT_GRACE_MS
const immediateReturn = options?.immediateReturn ?? false const immediateReturn = options?.immediateReturn ?? false
const isExternalSearch = eoseTimeout > 1000
if (isExternalSearch) {
logger.debug('query: Starting external relay search', {
relayCount: urls.length,
relays: urls,
eoseTimeout,
globalTimeout,
replaceableRace,
immediateReturn,
filter: sanitizedFilters
})
}
const filtersForGrace = sanitizedFilters const filtersForGrace = sanitizedFilters
const maxLimitForGrace = Math.max(...filtersForGrace.map((f) => (f.limit ?? 0) as number), 0) const maxLimitForGrace = Math.max(...filtersForGrace.map((f) => (f.limit ?? 0) as number), 0)
@ -352,6 +413,22 @@ export class QueryService {
) )
const wsQueryUrls = urls.filter((u) => !isHttpRelayUrl(u)) const wsQueryUrls = urls.filter((u) => !isHttpRelayUrl(u))
const reqId = ++queryReqSeq
const source = options?.relayOpSource ?? 'QueryService.query'
const inputRelaysOrdered = Array.from(new Set(urls.map((u) => normalizeUrl(u) || u).filter(Boolean)))
const wsRelayCandidates = Array.from(new Set(wsQueryUrls.map((u) => normalizeUrl(u) || u).filter(Boolean)))
logger.debug('[QueryService] req_begin', {
reqId,
source,
relays: inputRelaysOrdered,
httpRelayBases,
wsRelayCandidates,
filters: sanitizedFilters.map(compactFilterForRelayLog),
eoseTimeout,
globalTimeout
})
return await new Promise<NEvent[]>((resolve) => { return await new Promise<NEvent[]>((resolve) => {
const events: NEvent[] = [] const events: NEvent[] = []
const abortHttp = new AbortController() const abortHttp = new AbortController()
@ -364,6 +441,27 @@ export class QueryService {
let firstResultTime: number | null = null let firstResultTime: number | null = null
let globalTimeoutId: ReturnType<typeof setTimeout> | null = null let globalTimeoutId: ReturnType<typeof setTimeout> | null = null
let queryFinalizing = false let queryFinalizing = false
let resolvedSnapshot: NEvent[] = []
let reqEndLogged = false
let fallbackReqEndTimer: ReturnType<typeof setTimeout> | null = null
const emitReqEnd = (terminals: RelayOpTerminalRow[], snapshot: NEvent[]) => {
if (reqEndLogged) return
reqEndLogged = true
if (fallbackReqEndTimer != null) {
clearTimeout(fallbackReqEndTimer)
fallbackReqEndTimer = null
}
logQueryReqConsolidatedEnd(
reqId,
source,
inputRelaysOrdered,
httpRelayBases,
snapshot,
terminals,
(id) => this.getSeenEventRelayUrls(id)
)
}
const httpInflight = const httpInflight =
httpRelayBases.length === 0 httpRelayBases.length === 0
@ -388,9 +486,7 @@ export class QueryService {
} catch (e) { } catch (e) {
if ((e as Error).name === 'AbortError') return if ((e as Error).name === 'AbortError') return
relaySessionStrikes.recordReadFailure(base, 'http') relaySessionStrikes.recordReadFailure(base, 'http')
if (isIndexRelayTransportFailure(e)) { if (!isIndexRelayTransportFailure(e)) {
logger.debug('[QueryService] HTTP index relay unreachable', { base, error: e })
} else {
logger.warn('[QueryService] HTTP index relay query failed', { base, error: e }) logger.warn('[QueryService] HTTP index relay query failed', { base, error: e })
} }
} }
@ -443,10 +539,20 @@ export class QueryService {
if (replaceableRaceTimeoutId) clearTimeout(replaceableRaceTimeoutId) if (replaceableRaceTimeoutId) clearTimeout(replaceableRaceTimeoutId)
if (globalTimeoutId) clearTimeout(globalTimeoutId) if (globalTimeoutId) clearTimeout(globalTimeoutId)
sub.close()
const resolvedList = const resolvedList =
replaceableRace && events.length > 0 ? resolveReplaceableRaceEvents() : events replaceableRace && events.length > 0 ? resolveReplaceableRaceEvents() : events
resolvedSnapshot = resolvedList
if (wsQueryUrls.length === 0) {
emitReqEnd([], resolvedList)
} else {
fallbackReqEndTimer = setTimeout(() => {
emitReqEnd([], resolvedList)
}, Math.min(globalTimeout + 2500, 45_000))
}
sub.close()
resolve(resolvedList) resolve(resolvedList)
} }
@ -557,7 +663,7 @@ export class QueryService {
} }
} }
}, },
{ source: options?.relayOpSource ?? 'QueryService.query', logLevel: 'debug' } { source: options?.relayOpSource ?? 'QueryService.query', logLevel: 'debug', quiet: true, onBatchEnd: (rows) => emitReqEnd(rows, resolvedSnapshot) }
) )
const sub = { const sub = {
@ -578,7 +684,13 @@ export class QueryService {
urls: string[], urls: string[],
filter: Filter | Filter[], filter: Filter | Filter[],
callbacks: SubscribeCallbacks, callbacks: SubscribeCallbacks,
relayOpMeta?: { source: string; logLevel?: 'info' | 'debug' } relayOpMeta?: {
source: string
logLevel?: 'info' | 'debug'
/** When true, suppress `[RelayOp] batch_begin` / `batch_end` (used by {@link QueryService.query}). */
quiet?: boolean
onBatchEnd?: (rows: RelayOpTerminalRow[]) => void
}
): { close: () => void } { ): { close: () => void } {
const filters = sanitizeFiltersBeforeReq(filter) const filters = sanitizeFiltersBeforeReq(filter)
if (filters.length === 0) { if (filters.length === 0) {
@ -645,7 +757,11 @@ export class QueryService {
const opSource = relayOpMeta?.source ?? 'QueryService.subscribe' const opSource = relayOpMeta?.source ?? 'QueryService.subscribe'
const opBatch = const opBatch =
groupedRequests.length > 0 groupedRequests.length > 0
? new RelaySubscribeOpBatch(opSource, groupedRequests, { logLevel: relayOpMeta?.logLevel }) ? new RelaySubscribeOpBatch(opSource, groupedRequests, {
logLevel: relayOpMeta?.logLevel,
quiet: relayOpMeta?.quiet,
onBatchEnd: relayOpMeta?.onBatchEnd
})
: null : null
opBatch?.logBegin() opBatch?.logBegin()

21
src/services/client.service.ts

@ -3398,27 +3398,6 @@ class ClientService extends EventTarget {
relayOpSource: 'ClientService.searchProfiles' relayOpSource: 'ClientService.searchProfiles'
}) })
/** Which relays actually delivered each kind-0 id (for tuning SEARCHABLE_RELAY_URLS). DEBUG only. */
if (searchStr.length > 0) {
const relayHitCounts = new Map<string, number>()
for (const e of events) {
if (e.kind !== kinds.Metadata) continue
for (const u of this.queryService.getSeenEventRelayUrls(e.id)) {
const n = (normalizeUrl(u) || u).trim()
if (!n) continue
relayHitCounts.set(n, (relayHitCounts.get(n) ?? 0) + 1)
}
}
if (relayHitCounts.size > 0) {
const relayHits = [...relayHitCounts.entries()].sort((a, b) => b[1] - a[1])
logger.debug('[ClientService.searchProfiles] kind=0 deliveries by relay URL (count = events relay sent for this query)', {
searchPreview: searchStr.slice(0, 80),
totalKind0Events: events.filter((e) => e.kind === kinds.Metadata).length,
relayHits: Object.fromEntries(relayHits)
})
}
}
const byPk = new Map<string, NEvent>() const byPk = new Map<string, NEvent>()
for (const e of events) { for (const e of events) {
if (e.kind !== kinds.Metadata) continue if (e.kind !== kinds.Metadata) continue

5
src/services/media-upload.service.ts

@ -1,4 +1,5 @@
/** Compression runs entirely in-app before upload (`compress-upload-media`). */ /** 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). */
import storage from './local-storage.service'
import { compressMediaForUpload } from '@/lib/compress-upload-media' import { compressMediaForUpload } from '@/lib/compress-upload-media'
import { fetchWithTimeout } from '@/lib/fetch-with-timeout' import { fetchWithTimeout } from '@/lib/fetch-with-timeout'
import logger from '@/lib/logger' import logger from '@/lib/logger'
@ -12,8 +13,6 @@ import { simplifyUrl } from '@/lib/url'
import { TDraftEvent, TMediaUploadServiceConfig } from '@/types' import { TDraftEvent, TMediaUploadServiceConfig } from '@/types'
import { BlossomClient } from 'blossom-client-sdk' import { BlossomClient } from 'blossom-client-sdk'
import { z } from 'zod' import { z } from 'zod'
/** Must run before `./client.service` — that graph can synchronously re-enter this module; `storage` must be bound first (constructor reads it at module bottom). */
import storage from './local-storage.service'
import client from './client.service' import client from './client.service'
type UploadOptions = { type UploadOptions = {

2
src/services/post-editor-cache.service.ts

@ -1,7 +1,7 @@
import storage from '@/services/local-storage.service'
import { StorageKey } from '@/constants' import { StorageKey } from '@/constants'
import type { AdvancedEventLabSlice } from '@/lib/advanced-event-lab-slice' import type { AdvancedEventLabSlice } from '@/lib/advanced-event-lab-slice'
import { parseEditorJsonToText } from '@/lib/tiptap' import { parseEditorJsonToText } from '@/lib/tiptap'
import storage from '@/services/local-storage.service'
import { TPollCreateData } from '@/types' import { TPollCreateData } from '@/types'
import { Content } from '@tiptap/react' import { Content } from '@tiptap/react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'

33
src/services/relay-operation-log.service.ts

@ -31,7 +31,11 @@ export function compactFilterForRelayLog(f: Filter): Record<string, unknown> {
if (f['#p']?.length) out.pTagCount = f['#p'].length if (f['#p']?.length) out.pTagCount = f['#p'].length
if (f['#e']?.length) out.eTagCount = f['#e'].length if (f['#e']?.length) out.eTagCount = f['#e'].length
if (f['#t']?.length) out.tTagCount = f['#t'].length if (f['#t']?.length) out.tTagCount = f['#t'].length
if (f.search) out.search = true if (typeof f.search === 'string' && f.search.length > 0) {
out.searchPreview = f.search.length > 120 ? `${f.search.slice(0, 117)}` : f.search
} else if (f.search) {
out.search = true
}
return out return out
} }
@ -49,7 +53,7 @@ export interface RelayOpTerminalRow {
type GroupedRelayRow = { url: string; filters: Filter[] } type GroupedRelayRow = { url: string; filters: Filter[] }
/** Short host label for subscribe REQ logs (same as publish). */ /** Short host label for subscribe REQ logs (same as publish). */
function relayHostForSubscribeLog(url: string): string { export function relayHostForSubscribeLog(url: string): string {
return relayHostForPublishLog(url) return relayHostForPublishLog(url)
} }
@ -137,6 +141,8 @@ function groupTerminalsByOutcome(rows: RelayOpTerminalRow[]): Record<string, { c
export type RelaySubscribeOpBatchOptions = { export type RelaySubscribeOpBatchOptions = {
/** `info` logs every REQ wave at INFO; default `debug` keeps subscribe noise behind jumble-debug / VITE_DEBUG. */ /** `info` logs every REQ wave at INFO; default `debug` keeps subscribe noise behind jumble-debug / VITE_DEBUG. */
logLevel?: 'info' | 'debug' logLevel?: 'info' | 'debug'
/** When true, skip `[RelayOp] batch_begin` / `batch_end` lines (e.g. when {@link QueryService.query} logs `req_begin`/`req_end`). */
quiet?: boolean
/** Invoked once when this REQ wave finishes (same `rows` as `batch_end` / `terminals`). */ /** Invoked once when this REQ wave finishes (same `rows` as `batch_end` / `terminals`). */
onBatchEnd?: (rows: RelayOpTerminalRow[]) => void onBatchEnd?: (rows: RelayOpTerminalRow[]) => void
} }
@ -181,6 +187,7 @@ export class RelaySubscribeOpBatch {
private readonly source: string private readonly source: string
private readonly grouped: GroupedRelayRow[] private readonly grouped: GroupedRelayRow[]
private readonly logLevel: 'info' | 'debug' private readonly logLevel: 'info' | 'debug'
private readonly quiet: boolean
private readonly onBatchEnd?: (rows: RelayOpTerminalRow[]) => void private readonly onBatchEnd?: (rows: RelayOpTerminalRow[]) => void
private readonly terminal = new Map<number, RelayOpTerminalRow>() private readonly terminal = new Map<number, RelayOpTerminalRow>()
private endLogged = false private endLogged = false
@ -191,6 +198,7 @@ export class RelaySubscribeOpBatch {
this.source = source this.source = source
this.grouped = grouped this.grouped = grouped
this.logLevel = options?.logLevel ?? 'debug' this.logLevel = options?.logLevel ?? 'debug'
this.quiet = options?.quiet ?? false
this.onBatchEnd = options?.onBatchEnd this.onBatchEnd = options?.onBatchEnd
} }
@ -203,6 +211,7 @@ export class RelaySubscribeOpBatch {
} }
logBegin(): void { logBegin(): void {
if (this.quiet) return
const uniqueRelays = [...new Set(this.grouped.map((g) => g.url))] const uniqueRelays = [...new Set(this.grouped.map((g) => g.url))]
this.logLine('[RelayOp] batch_begin', { this.logLine('[RelayOp] batch_begin', {
batchId: this.batchId, batchId: this.batchId,
@ -285,15 +294,17 @@ export class RelaySubscribeOpBatch {
timeoutCount: nTimeout timeoutCount: nTimeout
} }
if (this.logLevel === 'debug') { if (!this.quiet) {
this.logLine('[RelayOp] batch_end', { if (this.logLevel === 'debug') {
...compact, this.logLine('[RelayOp] batch_end', {
readableSummary, ...compact,
byOutcome: groupTerminalsByOutcome(rows), readableSummary,
terminals: rows byOutcome: groupTerminalsByOutcome(rows),
}) terminals: rows
} else { })
logger.info(`[RelayOp] batch_end — ${headline}\n${readableSummary}`, compact) } else {
logger.info(`[RelayOp] batch_end — ${headline}\n${readableSummary}`, compact)
}
} }
this.onBatchEnd?.(rows) this.onBatchEnd?.(rows)

2
src/services/relay-selection.service.ts

@ -1,5 +1,6 @@
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { ExtendedKind, FAST_WRITE_RELAY_URLS, RANDOM_PUBLISH_RELAY_COUNT, READ_ONLY_RELAY_URLS } from '@/constants' import { ExtendedKind, FAST_WRITE_RELAY_URLS, RANDOM_PUBLISH_RELAY_COUNT, READ_ONLY_RELAY_URLS } from '@/constants'
import storage from '@/services/local-storage.service'
import { NOSTR_URI_FOR_REPLY_PUBKEYS_REGEX } from '@/lib/content-patterns' import { NOSTR_URI_FOR_REPLY_PUBKEYS_REGEX } from '@/lib/content-patterns'
import client from '@/services/client.service' import client from '@/services/client.service'
import { eventService } from '@/services/client.service' import { eventService } from '@/services/client.service'
@ -10,7 +11,6 @@ import indexedDb from '@/services/indexed-db.service'
import { getHttpRelayListFromEvent, getRelayListFromEvent } from '@/lib/event-metadata' import { getHttpRelayListFromEvent, getRelayListFromEvent } from '@/lib/event-metadata'
import { stripLocalNetworkRelaysFromRelayList } from '@/lib/relay-list-sanitize' import { stripLocalNetworkRelaysFromRelayList } from '@/lib/relay-list-sanitize'
import nip66Service from '@/services/nip66.service' import nip66Service from '@/services/nip66.service'
import storage from '@/services/local-storage.service'
export interface RelaySelectionContext { export interface RelaySelectionContext {
// User's own relays // User's own relays

Loading…
Cancel
Save