Browse Source

implement http polling

imwald
Silberengel 4 weeks ago
parent
commit
07ecafad12
  1. 7
      src/components/Explore/ExploreRelayReviews.tsx
  2. 3
      src/components/GifPicker/index.tsx
  3. 5
      src/components/MemePicker/index.tsx
  4. 6
      src/components/PostEditor/PostRelaySelector.tsx
  5. 9
      src/components/RssArticleWebBookmarks/index.tsx
  6. 9
      src/constants.ts
  7. 5
      src/hooks/useFetchCalendarRsvps.tsx
  8. 11
      src/lib/favorites-feed-relays.ts
  9. 11
      src/pages/primary/NoteListPage/FollowingFeed.tsx
  10. 3
      src/pages/primary/SpellsPage/CreateSpellDialog.tsx
  11. 15
      src/pages/primary/SpellsPage/index.tsx
  12. 9
      src/pages/secondary/FollowSetsSettingsPage/index.tsx
  13. 11
      src/pages/secondary/NoteListPage/index.tsx
  14. 13
      src/providers/LiveActivitiesProvider.tsx
  15. 217
      src/services/client.service.ts
  16. 3
      src/services/note-stats.service.ts

7
src/components/Explore/ExploreRelayReviews.tsx

@ -3,7 +3,10 @@ import { Skeleton } from '@/components/ui/skeleton' @@ -3,7 +3,10 @@ import { Skeleton } from '@/components/ui/skeleton'
import { ExtendedKind } from '@/constants'
import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
import { getRelayUrlFromRelayReviewEvent } from '@/lib/event-metadata'
import { getRelayUrlsWithFavoritesFastReadAndInbox } from '@/lib/favorites-feed-relays'
import {
getRelayUrlsWithFavoritesFastReadAndInbox,
userReadRelaysWithHttp
} from '@/lib/favorites-feed-relays'
import { appendCuratedReadOnlyRelays } from '@/pages/primary/SpellsPage/fauxSpellFeeds'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider'
@ -65,7 +68,7 @@ export default function ExploreRelayReviews() { @@ -65,7 +68,7 @@ export default function ExploreRelayReviews() {
getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
relayList?.read ?? [],
userReadRelaysWithHttp(relayList),
{
userWriteRelays: relayList?.write ?? [],
maxRelays: EXPLORE_REVIEWS_MAX_RELAYS,

3
src/components/GifPicker/index.tsx

@ -10,6 +10,7 @@ import { Label } from '@/components/ui/label' @@ -10,6 +10,7 @@ import { Label } from '@/components/ui/label'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Skeleton } from '@/components/ui/skeleton'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { useNostr } from '@/providers/NostrProvider'
import { ExtendedKind, FAST_WRITE_RELAY_URLS, GIF_RELAY_URLS } from '@/constants'
import { cn } from '@/lib/utils'
@ -60,7 +61,7 @@ export default function GifPicker({ @@ -60,7 +61,7 @@ export default function GifPicker({
const fileInputRef = useRef<HTMLInputElement | null>(null)
const gifbuddyPopupRef = useRef<Window | null>(null)
const userReadRelays = relayList?.read ?? []
const userReadRelays = useMemo(() => userReadRelaysWithHttp(relayList), [relayList])
const userWriteRelays = relayList?.write ?? []
/** Paste / upload: GIF discovery relays + user writes (unchanged). */

5
src/components/MemePicker/index.tsx

@ -10,6 +10,7 @@ import { Label } from '@/components/ui/label' @@ -10,6 +10,7 @@ import { Label } from '@/components/ui/label'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Skeleton } from '@/components/ui/skeleton'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { useNostr } from '@/providers/NostrProvider'
import { ExtendedKind, GIF_RELAY_URLS } from '@/constants'
import { normalizeUrl } from '@/lib/url'
@ -22,7 +23,7 @@ import { @@ -22,7 +23,7 @@ import {
} from '@/services/meme.service'
import mediaUpload from '@/services/media-upload.service'
import { ExternalLink, X } from 'lucide-react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
@ -79,7 +80,7 @@ export default function MemePicker({ @@ -79,7 +80,7 @@ export default function MemePicker({
const fileInputRef = useRef<HTMLInputElement | null>(null)
const memeamigoPopupRef = useRef<Window | null>(null)
const userReadRelays = relayList?.read ?? []
const userReadRelays = useMemo(() => userReadRelaysWithHttp(relayList), [relayList])
const userWriteRelays = relayList?.write ?? []
const loadMemes = useCallback(

6
src/components/PostEditor/PostRelaySelector.tsx

@ -13,6 +13,7 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' @@ -13,6 +13,7 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useNostr } from '@/providers/NostrProvider'
import { getRelayListFromEvent } from '@/lib/event-metadata'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import indexedDb from '@/services/indexed-db.service'
import { Check, ChevronDown, Server } from 'lucide-react'
import { NostrEvent } from 'nostr-tools'
@ -60,6 +61,7 @@ export default function PostRelaySelector({ @@ -60,6 +61,7 @@ export default function PostRelaySelector({
useCurrentRelays() // Keep this hook call for any side effects
const { relaySets, favoriteRelays, blockedRelays } = useFavoriteRelays()
const { pubkey, relayList } = useNostr()
const userReadRelaysForSelection = useMemo(() => userReadRelaysWithHttp(relayList), [relayList])
const [selectedRelayUrls, setSelectedRelayUrls] = useState<string[]>([])
const [selectableRelays, setSelectableRelays] = useState<string[]>([])
const [relayTypes, setRelayTypes] = useState<Record<string, RelaySourceType>>({})
@ -220,7 +222,7 @@ export default function PostRelaySelector({ @@ -220,7 +222,7 @@ export default function PostRelaySelector({
const result = await relaySelectionService.selectRelays({
userWriteRelays,
userHttpWriteRelays: relayList?.httpWrite ?? [],
userReadRelays: relayList?.read || [],
userReadRelays: userReadRelaysForSelection,
favoriteRelays: memoizedFavoriteRelays,
blockedRelays: memoizedBlockedRelays,
relaySets: memoizedRelaySets,
@ -328,7 +330,7 @@ export default function PostRelaySelector({ @@ -328,7 +330,7 @@ export default function PostRelaySelector({
const result = await relaySelectionService.selectRelays({
userWriteRelays,
userHttpWriteRelays: relayList?.httpWrite ?? [],
userReadRelays: relayList?.read || [],
userReadRelays: userReadRelaysForSelection,
favoriteRelays: memoizedFavoriteRelays,
blockedRelays: memoizedBlockedRelays,
relaySets: memoizedRelaySets,

9
src/components/RssArticleWebBookmarks/index.tsx

@ -5,7 +5,10 @@ import { Separator } from '@/components/ui/separator' @@ -5,7 +5,10 @@ import { Separator } from '@/components/ui/separator'
import { Textarea } from '@/components/ui/textarea'
import { ExtendedKind } from '@/constants'
import { createWebBookmarkDraftEvent } from '@/lib/draft-event'
import { getRelayUrlsWithFavoritesFastReadAndInbox } from '@/lib/favorites-feed-relays'
import {
getRelayUrlsWithFavoritesFastReadAndInbox,
userReadRelaysWithHttp
} from '@/lib/favorites-feed-relays'
import logger from '@/lib/logger'
import { showPublishingError } from '@/lib/publishing-feedback'
import {
@ -40,11 +43,11 @@ export default function RssArticleWebBookmarks({ articleUrl }: { articleUrl: str @@ -40,11 +43,11 @@ export default function RssArticleWebBookmarks({ articleUrl }: { articleUrl: str
}, [canonical])
const relayUrls = useMemo(() => {
const read = relayList?.read ?? []
const read = userReadRelaysWithHttp(relayList)
const base = getRelayUrlsWithFavoritesFastReadAndInbox(favoriteRelays, blockedRelays, read, {})
if (!base.length) return []
return appendCuratedReadOnlyRelays(base, blockedRelays)
}, [favoriteRelays, blockedRelays, relayList?.read])
}, [favoriteRelays, blockedRelays, relayList])
const [mine, setMine] = useState<Event[]>([])
const [loading, setLoading] = useState(false)

9
src/constants.ts

@ -93,6 +93,15 @@ export const RELAY_NIP42_PUBLISH_ACK_TIMEOUT_MS = 90_000 @@ -93,6 +93,15 @@ export const RELAY_NIP42_PUBLISH_ACK_TIMEOUT_MS = 90_000
/** Multi-relay queries and timeline initial REQ: after the first event, wait this long then close (query) or finalize EOSE (live feed) while keeping the subscription open for new events. */
export const FIRST_RELAY_RESULT_GRACE_MS = 2000
/**
* Timelines that include HTTP index relays: interval between periodic `query()` polls while the WebSocket
* subscription stays open (HTTP relays do not receive live `EVENT` over REQ).
*/
export const HTTP_TIMELINE_POLL_INTERVAL_MS = 45_000
/** Subtracted from the polling `since` cursor so borderline events are not missed between polls. */
export const HTTP_TIMELINE_POLL_SINCE_OVERLAP_SEC = 120
/** Legacy name: was used to cap spell NoteList skeleton time; loading now ends on EOSE / first events / safety timeouts. Kept for forks. */
export const SPELL_FEED_LOADING_MAX_MS = 1000

5
src/hooks/useFetchCalendarRsvps.tsx

@ -8,6 +8,7 @@ import { Event } from 'nostr-tools' @@ -8,6 +8,7 @@ import { Event } from 'nostr-tools'
import { useEffect, useState } from 'react'
import { normalizeUrl } from '@/lib/url'
import { FAST_READ_RELAY_URLS } from '@/constants'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { tagNameEquals } from '@/lib/tag'
function getRsvpStatus(rsvp: Event): 'accepted' | 'tentative' | 'declined' | undefined {
@ -39,7 +40,7 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) { @@ -39,7 +40,7 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) {
setIsFetching(true)
const coordinate = getReplaceableCoordinateFromEvent(calendarEvent)
const userRead = relayList?.read ?? []
const userRead = userReadRelaysWithHttp(relayList)
const baseUrls = new Set<string>([
...FAST_READ_RELAY_URLS.map((url) => normalizeUrl(url) || url),
...userRead.map((url) => normalizeUrl(url) || url)
@ -86,7 +87,7 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) { @@ -86,7 +87,7 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) {
return () => {
cancelled = true
}
}, [calendarEvent?.id, calendarEvent?.kind, calendarEvent?.pubkey, relayList?.read])
}, [calendarEvent?.id, calendarEvent?.kind, calendarEvent?.pubkey, relayList])
// When we publish an RSVP, NostrProvider calls client.emitNewEvent(event). Merge it into rsvps so the UI updates immediately.
useEffect(() => {

11
src/lib/favorites-feed-relays.ts

@ -25,6 +25,17 @@ const blockedSet = (blockedRelays: string[]) => @@ -25,6 +25,17 @@ const blockedSet = (blockedRelays: string[]) =>
* {@link DEFAULT_FAVORITE_RELAYS}. Same list drives the favorites tier in REQ/publish prioritization and the
* all-favorites home feed.
*/
/**
* NIP-65 `read` plus HTTP index inboxes (kind 10243) for feed REQ / query URL lists.
*/
export function userReadRelaysWithHttp(
relayList: { read?: string[]; httpRead?: string[] } | undefined | null
): string[] {
const http = relayList?.httpRead ?? []
const read = relayList?.read ?? []
return dedupeNormalizeRelayUrlsOrdered([...http, ...read])
}
export function getFavoritesFeedRelayUrls(
favoriteRelays: string[],
blockedRelays: string[]

11
src/pages/primary/NoteListPage/FollowingFeed.tsx

@ -1,6 +1,9 @@ @@ -1,6 +1,9 @@
import NormalFeed from '@/components/NormalFeed'
import type { TNoteListRef } from '@/components/NoteList'
import { augmentSubRequestsWithFavoritesFastReadAndInbox } from '@/lib/favorites-feed-relays'
import {
augmentSubRequestsWithFavoritesFastReadAndInbox,
userReadRelaysWithHttp
} from '@/lib/favorites-feed-relays'
import { buildFollowingFeedDeltaSubRequests } from '@/lib/following-feed-delta'
import { getPubkeysFromPTags } from '@/lib/tag'
import { normalizeUrl } from '@/lib/url'
@ -46,12 +49,12 @@ const FollowingFeed = forwardRef< @@ -46,12 +49,12 @@ const FollowingFeed = forwardRef<
)
const relayReadKey = useMemo(
() =>
[...(relayList?.read ?? [])]
[...userReadRelaysWithHttp(relayList)]
.map((u) => normalizeUrl(u) || u)
.filter(Boolean)
.sort()
.join('\0'),
[relayList?.read]
[relayList]
)
const relayWriteKey = useMemo(
() =>
@ -84,7 +87,7 @@ const FollowingFeed = forwardRef< @@ -84,7 +87,7 @@ const FollowingFeed = forwardRef<
raw,
favoriteRelays,
blockedRelays,
relayList?.read ?? [],
userReadRelaysWithHttp(relayList),
{ userWriteRelays: relayList?.write ?? [] }
)

3
src/pages/primary/SpellsPage/CreateSpellDialog.tsx

@ -24,6 +24,7 @@ import { useNostr } from '@/providers/NostrProvider' @@ -24,6 +24,7 @@ import { useNostr } from '@/providers/NostrProvider'
import { showPublishingError, showSimplePublishSuccess } from '@/lib/publishing-feedback'
import { eventService } from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { getRelaysForSpellCatalogSync } from '@/services/spell.service'
import { Info, Minus, Plus, X } from 'lucide-react'
import { useTranslation } from 'react-i18next'
@ -323,7 +324,7 @@ export default function CreateSpellDialog({ @@ -323,7 +324,7 @@ export default function CreateSpellDialog({
const { draft, notices, pendingATags } = applyListEventToSpellDraft(base, ev)
setForm(draft)
setListImportNotices(notices)
const urls = getRelaysForSpellCatalogSync(favoriteRelays, blockedRelays, relayList?.read ?? [], {
const urls = getRelaysForSpellCatalogSync(favoriteRelays, blockedRelays, userReadRelaysWithHttp(relayList), {
userWriteRelays: relayList?.write ?? []
})
if (pendingATags.length === 0) return

15
src/pages/primary/SpellsPage/index.tsx

@ -55,7 +55,8 @@ import { getPubkeysFromPTags } from '@/lib/tag' @@ -55,7 +55,8 @@ import { getPubkeysFromPTags } from '@/lib/tag'
import { formatPubkey, normalizeHexPubkey } from '@/lib/pubkey'
import {
augmentSubRequestsWithFavoritesFastReadAndInbox,
getRelayUrlsWithFavoritesFastReadAndInbox
getRelayUrlsWithFavoritesFastReadAndInbox,
userReadRelaysWithHttp
} from '@/lib/favorites-feed-relays'
import {
computeKind777SpellFeedSubscriptionKey,
@ -490,7 +491,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -490,7 +491,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
const feedUrls = getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
relayList?.read ?? [],
userReadRelaysWithHttp(relayList),
{ userWriteRelays: relayList?.write ?? [] }
)
if (!feedUrls.length) {
@ -598,7 +599,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -598,7 +599,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
return
}
const urls = getRelaysForSpellCatalogSync(favoriteRelays, blockedRelays, relayList?.read ?? [], {
const urls = getRelaysForSpellCatalogSync(favoriteRelays, blockedRelays, userReadRelaysWithHttp(relayList), {
userWriteRelays: relayList?.write ?? []
})
const catalogAuthors = buildSpellCatalogAuthors(pubkey, contacts)
@ -760,7 +761,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -760,7 +761,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
raw,
favoriteRelays,
blockedRelays,
relayList?.read ?? [],
userReadRelaysWithHttp(relayList),
{ userWriteRelays: relayList?.write ?? [] }
)
try {
@ -843,7 +844,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -843,7 +844,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
const feedUrls = getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
relayList?.read ?? [],
userReadRelaysWithHttp(relayList),
{
userWriteRelays: relayList?.write ?? [],
applySocialKindBlockedFilter: false
@ -868,7 +869,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -868,7 +869,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
raw,
favoriteRelays,
blockedRelays,
relayList?.read ?? [],
userReadRelaysWithHttp(relayList),
{ userWriteRelays: relayList?.write ?? [] }
).map((r) => ({ ...r, reasonLabel: t('Added from follows and contact lists') }))
@ -987,7 +988,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -987,7 +988,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
const feedUrls = getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
relayList?.read ?? [],
userReadRelaysWithHttp(relayList),
{
userWriteRelays: relayList?.write ?? [],
applySocialKindBlockedFilter: fauxSpellSkipSocialKindBlocked ? false : undefined

9
src/pages/secondary/FollowSetsSettingsPage/index.tsx

@ -34,7 +34,10 @@ import { randomString } from '@/lib/random' @@ -34,7 +34,10 @@ import { randomString } from '@/lib/random'
import { showPublishingError } from '@/lib/publishing-feedback'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { usePrimaryNoteView } from '@/contexts/primary-note-view-context'
import { getRelayUrlsWithFavoritesFastReadAndInbox } from '@/lib/favorites-feed-relays'
import {
getRelayUrlsWithFavoritesFastReadAndInbox,
userReadRelaysWithHttp
} from '@/lib/favorites-feed-relays'
import { createFollowSetDraftEvent } from '@/lib/draft-event'
import { filterEventsExcludingTombstones } from '@/lib/event'
import logger from '@/lib/logger'
@ -84,11 +87,11 @@ const FollowSetsSettingsPage = forwardRef( @@ -84,11 +87,11 @@ const FollowSetsSettingsPage = forwardRef(
const feedUrls = getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
relayList?.read ?? [],
userReadRelaysWithHttp(relayList),
{ userWriteRelays: relayList?.write ?? [] }
)
return appendCuratedReadOnlyRelays(feedUrls, blockedRelays)
}, [favoriteRelays, blockedRelays, relayList?.read, relayList?.write])
}, [favoriteRelays, blockedRelays, relayList])
const loadLists = useCallback(async () => {
if (!pubkey) {

11
src/pages/secondary/NoteListPage/index.tsx

@ -6,7 +6,8 @@ import { Button } from '@/components/ui/button' @@ -6,7 +6,8 @@ import { Button } from '@/components/ui/button'
import { isSocialKindBlockedKind, NIP_SEARCH_DOCUMENT_KINDS, SEARCHABLE_RELAY_URLS } from '@/constants'
import {
augmentSubRequestsWithFavoritesFastReadAndInbox,
getRelayUrlsWithFavoritesFastReadAndInbox
getRelayUrlsWithFavoritesFastReadAndInbox,
userReadRelaysWithHttp
} from '@/lib/favorites-feed-relays'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { toProfileList } from '@/lib/link'
@ -100,7 +101,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid @@ -100,7 +101,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
urls: getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
relayList?.read ?? [],
userReadRelaysWithHttp(relayList),
readUrlOpts
)
}
@ -143,7 +144,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid @@ -143,7 +144,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
urls: getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
relayList?.read ?? [],
userReadRelaysWithHttp(relayList),
{ userWriteRelays: relayList?.write ?? [] }
)
}
@ -175,7 +176,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid @@ -175,7 +176,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
raw,
favoriteRelays,
blockedRelays,
relayList?.read ?? [],
userReadRelaysWithHttp(relayList),
{ userWriteRelays: relayList?.write ?? [] }
)
)
@ -202,7 +203,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid @@ -202,7 +203,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
const relayUrls = getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
relayList?.read ?? [],
userReadRelaysWithHttp(relayList),
readUrlOpts
)
const mergedReqKinds = Array.from(

13
src/providers/LiveActivitiesProvider.tsx

@ -6,11 +6,20 @@ import { @@ -6,11 +6,20 @@ import {
resolveParentSpacesForLiveActivities,
type TLiveActivityItem
} from '@/lib/live-activities'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import logger from '@/lib/logger'
import client from '@/services/client.service'
import storage from '@/services/local-storage.service'
import { registerLiveActivitiesPrewarmCallback } from '@/services/live-activities-prewarm-bridge'
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState
} from 'react'
import { useFavoriteRelays } from './FavoriteRelaysProvider'
import { useFollowListOptional } from './FollowListProvider'
import { useNostr } from './NostrProvider'
@ -47,7 +56,7 @@ export function LiveActivitiesProvider({ children }: { children: React.ReactNode @@ -47,7 +56,7 @@ export function LiveActivitiesProvider({ children }: { children: React.ReactNode
const [items, setItems] = useState<TLiveActivityItem[]>([])
const [loading, setLoading] = useState(false)
const relayRead = relayList?.read ?? []
const relayRead = useMemo(() => userReadRelaysWithHttp(relayList), [relayList])
const relayWrite = relayList?.write ?? []
const refresh = useCallback(async () => {

217
src/services/client.service.ts

@ -4,6 +4,8 @@ import { @@ -4,6 +4,8 @@ import {
FAST_WRITE_RELAY_URLS,
DOCUMENT_RELAY_URLS,
FIRST_RELAY_RESULT_GRACE_MS,
HTTP_TIMELINE_POLL_INTERVAL_MS,
HTTP_TIMELINE_POLL_SINCE_OVERLAP_SEC,
isDocumentRelayKind,
isSocialKindBlockedKind,
relayFilterIncludesDocumentRelayKind,
@ -2286,6 +2288,23 @@ class ClientService extends EventTarget { @@ -2286,6 +2288,23 @@ class ClientService extends EventTarget {
let eosedAt: number | null = null
let eventIds = new Set<string>()
const httpTimelinePollBases = Array.from(
new Set(
relays
.filter((u) => isHttpRelayUrl(u))
.map((u) => normalizeHttpRelayUrl(u) || u)
.filter(Boolean)
)
)
let httpPollIntervalId: ReturnType<typeof setInterval> | null = null
let httpPollCursorUnix = 0
const clearHttpTimelinePoll = () => {
if (httpPollIntervalId != null) {
clearInterval(httpPollIntervalId)
httpPollIntervalId = null
}
}
let firstResultGraceTimer: ReturnType<typeof setTimeout> | null = null
const clearFirstResultGraceTimer = () => {
if (firstResultGraceTimer != null) {
@ -2358,6 +2377,108 @@ class ClientService extends EventTarget { @@ -2358,6 +2377,108 @@ class ClientService extends EventTarget {
logger.warn('[ClientService] Timeline disk hydrate failed', err)
}
const applySubscribedTimelineEvent = (evt: NEvent) => {
that.addEventToCache(evt)
if (!eosedAt) {
if (eventIds.has(evt.id)) return
eventIds.add(evt.id)
events.push(evt)
flushStreamingSnapshot()
armFirstResultGraceAfterFirstEvent()
return
}
if (eventIds.has(evt.id)) return
const wallClockAtEose = eosedAt
const isBacklogStraggler = evt.created_at + TIMELINE_STRAGGLER_MAX_AGE_SEC < wallClockAtEose
if (isBacklogStraggler) {
eventIds.add(evt.id)
events.push(evt)
if (needSort) {
events = events.sort((a, b) => b.created_at - a.created_at).slice(0, filter.limit)
}
eventIds = new Set(events.map((e) => e.id))
onEvents([...events], false)
const timeline = that.timelines[key]
if (timeline && !Array.isArray(timeline)) {
timeline.refs = events
.map((e) => [e.id, e.created_at] as TTimelineRef)
.sort((a, b) => b[1] - a[1])
that.scheduleTimelinePersist(key)
}
return
}
eventIds.add(evt.id)
onNew(evt)
const timeline = that.timelines[key]
if (!timeline || Array.isArray(timeline)) {
return
}
if (timeline.refs.length === 0) {
timeline.refs = events.map((e) => [e.id, e.created_at] as TTimelineRef).sort((a, b) => b[1] - a[1])
that.scheduleTimelinePersist(key)
return
}
let idx = 0
for (const ref of timeline.refs) {
if (evt.created_at > ref[1] || (evt.created_at === ref[1] && evt.id < ref[0])) {
break
}
if (evt.created_at === ref[1] && evt.id === ref[0]) {
return
}
idx++
}
if (idx >= timeline.refs.length) return
timeline.refs.splice(idx, 0, [evt.id, evt.created_at])
that.scheduleTimelinePersist(key)
}
const runHttpTimelinePollQuery = async (pollFilter: Filter) => {
if (httpTimelinePollBases.length === 0) return
try {
await this.query(
httpTimelinePollBases,
pollFilter,
(evt: NEvent) => {
applySubscribedTimelineEvent(evt)
},
{
firstRelayResultGraceMs: false,
globalTimeout: 25_000,
eoseTimeout: 2500
}
)
} catch (err) {
logger.debug('[ClientService] HTTP index timeline poll failed', err)
}
}
const armHttpTimelinePollingAfterInitial = () => {
clearHttpTimelinePoll()
if (httpTimelinePollBases.length === 0) return
const newestCreated = events.length > 0 ? Math.max(...events.map((e) => e.created_at)) : 0
httpPollCursorUnix = Math.max(eosedAt ?? 0, newestCreated)
httpPollIntervalId = setInterval(() => {
const base = { ...(filter as Filter) } as Filter & { until?: number }
delete base.until
const since = Math.max(0, httpPollCursorUnix - HTTP_TIMELINE_POLL_SINCE_OVERLAP_SEC)
const pollLimit = Math.min(Math.max(filter.limit ?? 200, 1), 500)
const pollFilter: Filter = { ...base, since, limit: pollLimit }
void runHttpTimelinePollQuery(pollFilter).then(() => {
httpPollCursorUnix = dayjs().unix()
})
}, HTTP_TIMELINE_POLL_INTERVAL_MS)
}
const handleTimelineEose = (eosed: boolean) => {
if (!eosed) return
if (eosedAt != null) return
@ -2367,6 +2488,7 @@ class ClientService extends EventTarget { @@ -2367,6 +2488,7 @@ class ClientService extends EventTarget {
eosedAt = dayjs().unix()
if (!needSort) {
armHttpTimelinePollingAfterInitial()
return onEvents([...events], true)
}
@ -2378,7 +2500,7 @@ class ClientService extends EventTarget { @@ -2378,7 +2500,7 @@ class ClientService extends EventTarget {
that.timelines[key] = {
refs: events.map((evt) => [evt.id, evt.created_at]),
filter,
urls
urls: relays
}
} else if (tl.refs.length === 0) {
tl.refs = events.map((evt) => [evt.id, evt.created_at] as TTimelineRef)
@ -2393,6 +2515,7 @@ class ClientService extends EventTarget { @@ -2393,6 +2515,7 @@ class ClientService extends EventTarget {
tl.refs = newRefs.concat(tl.refs)
}
}
armHttpTimelinePollingAfterInitial()
onEvents([...events], true)
that.scheduleTimelinePersist(key)
}
@ -2400,80 +2523,24 @@ class ClientService extends EventTarget { @@ -2400,80 +2523,24 @@ class ClientService extends EventTarget {
const subCloser = this.subscribe(relays, filter, {
startLogin,
onevent: (evt: NEvent) => {
that.addEventToCache(evt)
// not eosed yet, push to events
if (!eosedAt) {
if (eventIds.has(evt.id)) return
eventIds.add(evt.id)
events.push(evt)
flushStreamingSnapshot()
armFirstResultGraceAfterFirstEvent()
return
}
if (eventIds.has(evt.id)) return
const wallClockAtEose = eosedAt
const isBacklogStraggler =
evt.created_at + TIMELINE_STRAGGLER_MAX_AGE_SEC < wallClockAtEose
if (isBacklogStraggler) {
eventIds.add(evt.id)
events.push(evt)
if (needSort) {
events = events.sort((a, b) => b.created_at - a.created_at).slice(0, filter.limit)
}
eventIds = new Set(events.map((e) => e.id))
onEvents([...events], false)
const timeline = that.timelines[key]
if (timeline && !Array.isArray(timeline)) {
timeline.refs = events
.map((e) => [e.id, e.created_at] as TTimelineRef)
.sort((a, b) => b[1] - a[1])
that.scheduleTimelinePersist(key)
}
return
}
eventIds.add(evt.id)
onNew(evt)
const timeline = that.timelines[key]
if (!timeline || Array.isArray(timeline)) {
return
}
if (timeline.refs.length === 0) {
timeline.refs = events.map((e) => [e.id, e.created_at] as TTimelineRef).sort((a, b) => b[1] - a[1])
that.scheduleTimelinePersist(key)
return
}
let idx = 0
for (const ref of timeline.refs) {
if (evt.created_at > ref[1] || (evt.created_at === ref[1] && evt.id < ref[0])) {
break
}
if (evt.created_at === ref[1] && evt.id === ref[0]) {
return
}
idx++
}
if (idx >= timeline.refs.length) return
timeline.refs.splice(idx, 0, [evt.id, evt.created_at])
that.scheduleTimelinePersist(key)
applySubscribedTimelineEvent(evt)
},
oneose: handleTimelineEose,
onclose: onClose
},
relayReqLog)
if (httpTimelinePollBases.length > 0) {
const backfillFilter = { ...(filter as Filter) } as Filter & { until?: number }
delete backfillFilter.until
void runHttpTimelinePollQuery(backfillFilter)
}
return {
timelineKey: key,
closer: () => {
clearFirstResultGraceTimer()
clearHttpTimelinePoll()
onEvents = () => {}
onNew = () => {}
subCloser.close()
@ -2602,8 +2669,19 @@ class ClientService extends EventTarget { @@ -2602,8 +2669,19 @@ class ClientService extends EventTarget {
} = {}
) {
const originalDedupedRelays = Array.from(new Set(urls))
let relays = originalDedupedRelays.filter((url) => !isHttpRelayUrl(url))
if (relays.length === 0) relays = [...FAST_READ_RELAY_URLS]
const httpRelayBases = Array.from(
new Set(
originalDedupedRelays
.filter((u) => isHttpRelayUrl(u))
.map((u) => normalizeHttpRelayUrl(u) || u)
.filter(Boolean)
)
)
const wsOriginal = originalDedupedRelays.filter((url) => !isHttpRelayUrl(url))
let relays = [...wsOriginal]
if (relays.length === 0 && httpRelayBases.length === 0) {
relays = [...FAST_READ_RELAY_URLS]
}
const filters = Array.isArray(filter) ? filter : [filter]
relays = withDocumentRelayUrlsForFilters(relays, filters)
const stripSocialBlockedRelays =
@ -2612,10 +2690,11 @@ class ClientService extends EventTarget { @@ -2612,10 +2690,11 @@ class ClientService extends EventTarget {
if (stripSocialBlockedRelays) {
const socialKindBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u))
const stripped = relays.filter((url) => !socialKindBlockedSet.has(normalizeUrl(url) || url))
relays = relaysAfterSocialKindBlockedStrip(originalDedupedRelays, stripped)
relays = relaysAfterSocialKindBlockedStrip(wsOriginal, stripped)
}
relays = this.relayUrlsAfterStrikesOrRecover(relays)
const events = await this.queryService.query(relays, filter, onevent, {
const queryRelays = dedupeNormalizeRelayUrlsOrdered([...relays, ...httpRelayBases])
const events = await this.queryService.query(queryRelays, filter, onevent, {
eoseTimeout,
globalTimeout,
firstRelayResultGraceMs,

3
src/services/note-stats.service.ts

@ -23,6 +23,7 @@ import { @@ -23,6 +23,7 @@ import {
getWebExternalReactionTargetUrl,
rssArticleStableEventId
} from '@/lib/rss-article'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { getEmojiInfosFromEmojiTags, getFirstHexEventIdFromETags, tagNameEquals } from '@/lib/tag'
import { normalizeUrl } from '@/lib/url'
import client, { eventService } from '@/services/client.service'
@ -285,7 +286,7 @@ class NoteStatsService { @@ -285,7 +286,7 @@ class NoteStatsService {
client.fetchRelayList(event.pubkey),
new Promise<{ read?: string[] }>((r) => setTimeout(() => r({}), 2000))
])
;(relayList?.read ?? []).slice(0, 10).forEach(add)
userReadRelaysWithHttp(relayList).slice(0, 10).forEach(add)
} catch {
// ignore
}

Loading…
Cancel
Save