Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
b12356bfa3
  1. 4
      package-lock.json
  2. 2
      package.json
  3. 27
      src/components/NormalFeed/index.tsx
  4. 30
      src/components/NoteList/index.tsx
  5. 29
      src/components/Relay/index.tsx
  6. 48
      src/services/client.service.ts

4
package-lock.json generated

@ -1,12 +1,12 @@
{ {
"name": "imwald", "name": "imwald",
"version": "23.10.0", "version": "23.10.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "imwald", "name": "imwald",
"version": "23.10.0", "version": "23.10.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@asciidoctor/core": "^3.0.4", "@asciidoctor/core": "^3.0.4",

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "imwald", "name": "imwald",
"version": "23.10.0", "version": "23.10.1",
"description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery", "description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery",
"private": true, "private": true,
"type": "module", "type": "module",

27
src/components/NormalFeed/index.tsx

@ -16,7 +16,6 @@ import {
forwardRef, forwardRef,
useCallback, useCallback,
useEffect, useEffect,
useLayoutEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
@ -121,6 +120,10 @@ const NormalFeed = forwardRef<TNoteListRef, {
timelinePublicReadFallback?: boolean timelinePublicReadFallback?: boolean
/** When the feed is empty and terminal, {@link NoteList} can show an Alexandria search link (hashtag / d-tag pages). */ /** When the feed is empty and terminal, {@link NoteList} can show an Alexandria search link (hashtag / d-tag pages). */
alexandriaEmptyUrl?: string | null alexandriaEmptyUrl?: string | null
/**
* Single-relay explore: only events from that relays live REQ (no session/IDB prime, no prefetch to other relays).
*/
relayAuthoritativeFeedOnly?: boolean
}>(function NormalFeed( }>(function NormalFeed(
{ {
subRequests, subRequests,
@ -156,7 +159,8 @@ const NormalFeed = forwardRef<TNoteListRef, {
extraShouldHideGalleryEvent, extraShouldHideGalleryEvent,
oneShotMergedCap, oneShotMergedCap,
timelinePublicReadFallback = false, timelinePublicReadFallback = false,
alexandriaEmptyUrl = null alexandriaEmptyUrl = null,
relayAuthoritativeFeedOnly = false
}, },
ref ref
) { ) {
@ -324,8 +328,15 @@ const NormalFeed = forwardRef<TNoteListRef, {
const mergeFilterWithTabsRow = const mergeFilterWithTabsRow =
showFeedClientFilter && ((isMainFeed && !!setSubHeader) || renderTabsInFeed) showFeedClientFilter && ((isMainFeed && !!setSubHeader) || renderTabsInFeed)
/** Same row for multi-relay and single-relay chips: Notes/Replies + refresh + kind picker (REQ may stay kindless for single relay; NoteList filters client-side). */ /**
useLayoutEffect(() => { * Push the tab row into {@link PrimaryPageLayout} subHeader. Use `useEffect` (not `useLayoutEffect`) so
* parent `setHomeSubHeader` runs after paint; synchronous layout updates here caused React #185
* (maximum update depth) when navigating onto the home feed after other primaries (e.g. notifications).
* Intentionally omit `tabsElement` from deps covered by `listMode` + `subHeaderFilterDepsKey`.
* Omit `onSubHeaderRefresh` / `onFeedFilterTabRowSlotRef`: only embedded in `tabsElement`; unstable
* identities there would retrigger every render and loop with parent state.
*/
useEffect(() => {
if (!isMainFeed || !setSubHeader) return if (!isMainFeed || !setSubHeader) return
if (mergeFilterWithTabsRow) { if (mergeFilterWithTabsRow) {
setSubHeader( setSubHeader(
@ -341,19 +352,14 @@ const NormalFeed = forwardRef<TNoteListRef, {
setSubHeader(tabsElement) setSubHeader(tabsElement)
} }
return () => setSubHeader(null) return () => setSubHeader(null)
// Intentionally omit `tabsElement`: same semantics are covered by listMode + subHeaderFilterDepsKey.
// Listing tabsElement here can retrigger the effect every render if its useMemo input references churn,
// which calls setSubHeader repeatedly → parent state → maximum update depth (#185).
}, [ }, [
isMainFeed, isMainFeed,
setSubHeader, setSubHeader,
listMode, listMode,
isWispTrendingOnlyFeed, isWispTrendingOnlyFeed,
subHeaderFilterDepsKey, subHeaderFilterDepsKey,
onSubHeaderRefresh,
allowKindlessRelayExplore, allowKindlessRelayExplore,
mergeFilterWithTabsRow, mergeFilterWithTabsRow
onFeedFilterTabRowSlotRef
]) ])
return ( return (
@ -414,6 +420,7 @@ const NormalFeed = forwardRef<TNoteListRef, {
oneShotMergedCap={oneShotMergedCap} oneShotMergedCap={oneShotMergedCap}
timelinePublicReadFallback={timelinePublicReadFallback && listMode === 'postsAndReplies'} timelinePublicReadFallback={timelinePublicReadFallback && listMode === 'postsAndReplies'}
alexandriaEmptyUrl={alexandriaEmptyUrl} alexandriaEmptyUrl={alexandriaEmptyUrl}
relayAuthoritativeFeedOnly={relayAuthoritativeFeedOnly}
/> />
</div> </div>
</> </>

30
src/components/NoteList/index.tsx

@ -772,6 +772,12 @@ const NoteList = forwardRef(
* {@link client.fetchEvents} against {@link FAST_READ_RELAY_URLS} so the feed is not stuck on stale cache only. * {@link client.fetchEvents} against {@link FAST_READ_RELAY_URLS} so the feed is not stuck on stale cache only.
*/ */
timelinePublicReadFallback = false, timelinePublicReadFallback = false,
/**
* Explore single-relay feed: paint only live events from that relays REQ no session snapshot, no disk
* prime merge, no {@link ClientService.subscribeTimeline} IndexedDB hydrate / persist, no profile prefetch
* fan-out to other relays.
*/
relayAuthoritativeFeedOnly = false,
/** /**
* When set and the timeline is empty (after relays finish), show a link to Alexandria with a matching query * When set and the timeline is empty (after relays finish), show a link to Alexandria with a matching query
* (hashtag / d-tag browse from {@link NormalFeed}). * (hashtag / d-tag browse from {@link NormalFeed}).
@ -833,6 +839,8 @@ const NoteList = forwardRef(
/** When true, render events as an Instagram-style 3-column square media grid. */ /** When true, render events as an Instagram-style 3-column square media grid. */
gridLayout?: boolean gridLayout?: boolean
timelinePublicReadFallback?: boolean timelinePublicReadFallback?: boolean
/** Single-relay explore: only show events returned by that relay (no session/IDB/local merge). */
relayAuthoritativeFeedOnly?: boolean
/** Optional Alexandria `/events` URL when this feed’s timeline is empty (search / tag browse). */ /** Optional Alexandria `/events` URL when this feed’s timeline is empty (search / tag browse). */
alexandriaEmptyUrl?: string | null alexandriaEmptyUrl?: string | null
}, },
@ -928,6 +936,8 @@ const NoteList = forwardRef(
const [feedSubscribeRelayOutcomes, setFeedSubscribeRelayOutcomes] = useState<RelayOpTerminalRow[]>([]) const [feedSubscribeRelayOutcomes, setFeedSubscribeRelayOutcomes] = useState<RelayOpTerminalRow[]>([])
/** One-shot per timeline init: after an all-failed relay wave, try {@link FAST_READ_RELAY_URLS}. */ /** One-shot per timeline init: after an all-failed relay wave, try {@link FAST_READ_RELAY_URLS}. */
const publicReadFallbackAttemptedRef = useRef(false) const publicReadFallbackAttemptedRef = useRef(false)
const relayAuthoritativeFeedOnlyRef = useRef(relayAuthoritativeFeedOnly)
relayAuthoritativeFeedOnlyRef.current = relayAuthoritativeFeedOnly
/** /**
* Bumped when {@link feedPaintLiveRelayDoneRef} becomes true so the empty-feed toast effect re-runs. * Bumped when {@link feedPaintLiveRelayDoneRef} becomes true so the empty-feed toast effect re-runs.
* (Loading clears when subscribe wires; merged EOSE arrives later.) * (Loading clears when subscribe wires; merged EOSE arrives later.)
@ -1893,6 +1903,7 @@ const NoteList = forwardRef(
setLoading(true) setLoading(true)
let diskPrimeCancelled = false let diskPrimeCancelled = false
const primeDiskWhileAwaitingRelayProbe = async () => { const primeDiskWhileAwaitingRelayProbe = async () => {
if (relayAuthoritativeFeedOnlyRef.current) return
try { try {
const mapped = stripNostrLandAggrFromTimelineSubRequests( const mapped = stripNostrLandAggrFromTimelineSubRequests(
feedSubscriptionKey, feedSubscriptionKey,
@ -1974,7 +1985,9 @@ const NoteList = forwardRef(
eventsRef.current.length > 0 eventsRef.current.length > 0
const sessionSnap = const sessionSnap =
!userPulledRefresh ? getSessionFeedSnapshot(sessionSnapshotIdentityKey) : undefined !userPulledRefresh && !relayAuthoritativeFeedOnlyRef.current
? getSessionFeedSnapshot(sessionSnapshotIdentityKey)
: undefined
const restoredFromSession = !keepExistingTimelineEvents && !!(sessionSnap?.length) const restoredFromSession = !keepExistingTimelineEvents && !!(sessionSnap?.length)
const seeAllNoSpell = seeAllFeedEventsRef.current && !useFilterAsIsRef.current const seeAllNoSpell = seeAllFeedEventsRef.current && !useFilterAsIsRef.current
@ -2074,6 +2087,7 @@ const NoteList = forwardRef(
* {@link onEvents} so rows appear as soon as local sources resolve. * {@link onEvents} so rows appear as soon as local sources resolve.
*/ */
const startNonBlockingTimelineDiskPrime = () => { const startNonBlockingTimelineDiskPrime = () => {
if (relayAuthoritativeFeedOnlyRef.current) return
if (oneShotFetch || mappedSubRequests.length === 0) return if (oneShotFetch || mappedSubRequests.length === 0) return
if (isSpellPageLocalWarmup) return if (isSpellPageLocalWarmup) return
const diskReq = mappedSubRequests as Array<{ urls: string[]; filter: TSubRequestFilter }> const diskReq = mappedSubRequests as Array<{ urls: string[]; filter: TSubRequestFilter }>
@ -2802,6 +2816,7 @@ const NoteList = forwardRef(
timelinePrefetchDebounceRef.current = setTimeout(() => { timelinePrefetchDebounceRef.current = setTimeout(() => {
timelinePrefetchDebounceRef.current = null timelinePrefetchDebounceRef.current = null
if (!effectActive) return if (!effectActive) return
if (relayAuthoritativeFeedOnlyRef.current) return
const evs = lastEventsForTimelinePrefetchRef.current const evs = lastEventsForTimelinePrefetchRef.current
if (evs.length === 0) return if (evs.length === 0) return
@ -2997,6 +3012,7 @@ const NoteList = forwardRef(
startLogin, startLogin,
needSort: !areAlgoRelays, needSort: !areAlgoRelays,
firstRelayResultGraceMs: FIRST_RELAY_RESULT_GRACE_MS, firstRelayResultGraceMs: FIRST_RELAY_RESULT_GRACE_MS,
relayAuthoritativeTimeline: relayAuthoritativeFeedOnlyRef.current,
onRelaySubscribeWaveComplete: (rows) => { onRelaySubscribeWaveComplete: (rows) => {
if (!effectActive) return if (!effectActive) return
setFeedSubscribeRelayOutcomes(rows) setFeedSubscribeRelayOutcomes(rows)
@ -3051,7 +3067,9 @@ const NoteList = forwardRef(
setProgressiveLayersSearching(false) setProgressiveLayersSearching(false)
followingFeedDeltaCloserRef.current?.() followingFeedDeltaCloserRef.current?.()
followingFeedDeltaCloserRef.current = null followingFeedDeltaCloserRef.current = null
if (!relayAuthoritativeFeedOnlyRef.current) {
setSessionFeedSnapshot(snapshotKeyForCleanup, eventsRef.current) setSessionFeedSnapshot(snapshotKeyForCleanup, eventsRef.current)
}
if (kindlessEoseTimeoutRef.current) { if (kindlessEoseTimeoutRef.current) {
clearTimeout(kindlessEoseTimeoutRef.current) clearTimeout(kindlessEoseTimeoutRef.current)
kindlessEoseTimeoutRef.current = null kindlessEoseTimeoutRef.current = null
@ -3097,7 +3115,8 @@ const NoteList = forwardRef(
onSingleRelayKindlessEmpty, onSingleRelayKindlessEmpty,
mapLiveSubRequestsForTimeline, mapLiveSubRequestsForTimeline,
progressiveWarmupQuery, progressiveWarmupQuery,
hostPrimaryPageName hostPrimaryPageName,
relayAuthoritativeFeedOnly
]) ])
useEffect(() => { useEffect(() => {
@ -3316,7 +3335,8 @@ const NoteList = forwardRef(
{ {
startLogin, startLogin,
needSort: !areAlgoRelays, needSort: !areAlgoRelays,
firstRelayResultGraceMs: FIRST_RELAY_RESULT_GRACE_MS firstRelayResultGraceMs: FIRST_RELAY_RESULT_GRACE_MS,
relayAuthoritativeTimeline: relayAuthoritativeFeedOnlyRef.current
} }
) )
if (!deltaActive) { if (!deltaActive) {
@ -3522,6 +3542,7 @@ const NoteList = forwardRef(
]) ])
useEffect(() => { useEffect(() => {
if (relayAuthoritativeFeedOnly) return
if (!timelinePublicReadFallback) return if (!timelinePublicReadFallback) return
if (feedSubscriptionKey === 'home-all-favorites') return if (feedSubscriptionKey === 'home-all-favorites') return
if (oneShotFetch || areAlgoRelays) return if (oneShotFetch || areAlgoRelays) return
@ -3609,7 +3630,8 @@ const NoteList = forwardRef(
mapLiveSubRequestsForTimeline, mapLiveSubRequestsForTimeline,
effectiveShowKinds, effectiveShowKinds,
allowKindlessRelayExplore, allowKindlessRelayExplore,
timelineSubscriptionKey timelineSubscriptionKey,
relayAuthoritativeFeedOnly
]) ])
useEffect(() => { useEffect(() => {

29
src/components/Relay/index.tsx

@ -7,9 +7,10 @@ import type { TPrimaryPageName } from '@/PageManager'
import { SINGLE_RELAY_KINDLESS_REQ_LIMIT } from '@/constants' import { SINGLE_RELAY_KINDLESS_REQ_LIMIT } from '@/constants'
import { isLocalNetworkUrl, normalizeAnyRelayUrl } from '@/lib/url' import { isLocalNetworkUrl, normalizeAnyRelayUrl } from '@/lib/url'
import { useCurrentRelays } from '@/providers/CurrentRelaysProvider' import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
import { useKindFilterOrDefaults } from '@/providers/KindFilterProvider'
import client from '@/services/client.service' import client from '@/services/client.service'
import type { TFeedSubRequest } from '@/types' import type { TFeedSubRequest } from '@/types'
import type { Event } from 'nostr-tools' import { kinds, type Event } from 'nostr-tools'
import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import NotFound from '../NotFound' import NotFound from '../NotFound'
@ -20,6 +21,7 @@ const Relay = forwardRef<
>(function Relay({ url, className, hostPrimaryPageName }, ref) { >(function Relay({ url, className, hostPrimaryPageName }, ref) {
const { t } = useTranslation() const { t } = useTranslation()
const { addRelayUrls, removeRelayUrls } = useCurrentRelays() const { addRelayUrls, removeRelayUrls } = useCurrentRelays()
const { showKinds } = useKindFilterOrDefaults()
const normalizedUrl = useMemo(() => (url ? normalizeAnyRelayUrl(url) : undefined), [url]) const normalizedUrl = useMemo(() => (url ? normalizeAnyRelayUrl(url) : undefined), [url])
const { relayInfo } = useFetchRelayInfo(normalizedUrl) const { relayInfo } = useFetchRelayInfo(normalizedUrl)
const [searchInput, setSearchInput] = useState('') const [searchInput, setSearchInput] = useState('')
@ -66,18 +68,32 @@ const Relay = forwardRef<
} }
}, [normalizedUrl, noteListRef]) }, [normalizedUrl, noteListRef])
/** Default browse: explicit kinds (many strfry / small relays never return a useful kindless global REQ). */
const relayBrowseKinds = useMemo(
() => (showKinds.length > 0 ? showKinds : [kinds.ShortTextNote]),
[showKinds]
)
const relayFeedSubRequests = useMemo<TFeedSubRequest[]>(() => { const relayFeedSubRequests = useMemo<TFeedSubRequest[]>(() => {
if (!normalizedUrl) return [] if (!normalizedUrl) return []
const q = debouncedInput.trim() const q = debouncedInput.trim()
if (q) {
return [ return [
{ {
urls: [normalizedUrl], urls: [normalizedUrl],
filter: q filter: { search: q, limit: SINGLE_RELAY_KINDLESS_REQ_LIMIT }
? { search: q, limit: SINGLE_RELAY_KINDLESS_REQ_LIMIT }
: { limit: SINGLE_RELAY_KINDLESS_REQ_LIMIT }
} }
] ]
}, [normalizedUrl, debouncedInput]) }
return [
{
urls: [normalizedUrl],
filter: { kinds: [...relayBrowseKinds], limit: SINGLE_RELAY_KINDLESS_REQ_LIMIT }
}
]
}, [normalizedUrl, debouncedInput, relayBrowseKinds])
const allowKindlessRelayExplore = debouncedInput.trim().length > 0
/** When we know delivery relays, drop rows that never arrived from this feed’s relay (stale cache / mis-tagged). */ /** When we know delivery relays, drop rows that never arrived from this feed’s relay (stale cache / mis-tagged). */
const relaySeenMatchKey = useMemo( const relaySeenMatchKey = useMemo(
@ -117,12 +133,13 @@ const Relay = forwardRef<
ref={noteListRef} ref={noteListRef}
subRequests={relayFeedSubRequests} subRequests={relayFeedSubRequests}
useFilterAsIs useFilterAsIs
allowKindlessRelayExplore allowKindlessRelayExplore={allowKindlessRelayExplore}
showAllKinds showAllKinds
showFeedClientFilter showFeedClientFilter
hostPrimaryPageName={hostPrimaryPageName} hostPrimaryPageName={hostPrimaryPageName}
extraShouldHideEvent={shouldHideEventNotFromThisRelay} extraShouldHideEvent={shouldHideEventNotFromThisRelay}
extraShouldHideRepliesEvent={shouldHideEventNotFromThisRelay} extraShouldHideRepliesEvent={shouldHideEventNotFromThisRelay}
relayAuthoritativeFeedOnly
/> />
</div> </div>
) )

48
src/services/client.service.ts

@ -349,6 +349,8 @@ class ClientService extends EventTarget {
refs: TTimelineRef[] refs: TTimelineRef[]
filter: TSubRequestFilter filter: TSubRequestFilter
urls: string[] urls: string[]
/** When true, skip writing this shard to IndexedDB via {@link scheduleTimelinePersist} (relay-authoritative feeds). */
disablePersist?: boolean
} }
| string[] | string[]
| undefined | undefined
@ -2146,14 +2148,20 @@ class ClientService extends EventTarget {
startLogin, startLogin,
needSort = true, needSort = true,
firstRelayResultGraceMs = FIRST_RELAY_RESULT_GRACE_MS, firstRelayResultGraceMs = FIRST_RELAY_RESULT_GRACE_MS,
onRelaySubscribeWaveComplete onRelaySubscribeWaveComplete,
relayAuthoritativeTimeline = false
}: { }: {
startLogin?: () => void startLogin?: () => void
needSort?: boolean needSort?: boolean
/** Passed to each shard’s {@link ClientService._subscribeTimeline}: 2s after first event completes initial load if EOSE is slower. */ /** Passed to each shard’s {@link ClientService._subscribeTimeline}: 2s after first event completes initial load if EOSE is slower. */
firstRelayResultGraceMs?: number firstRelayResultGraceMs?: number
/** After every timeline shard’s REQ wave has ended (per-relay EOSE / close / timeout), merged rows in shard order. */ /** After every timeline shard’s REQ wave has ended (per-relay EOSE / close / timeout), merged rows in shard order. */
onRelaySubscribeWaveComplete?: (rows: RelayOpTerminalRow[]) => void onRelaySubscribeWaveComplete?: (rows: RelayOpTerminalRow[]) => void,
/**
* Single-relay what this relay stores feeds: skip IndexedDB + session snapshot hydrate before the live REQ,
* skip persisting this shard, and do not widen an empty shard to {@link FAST_READ_RELAY_URLS}.
*/
relayAuthoritativeTimeline?: boolean
} = {} } = {}
) { ) {
const timelineBatchId = `tl-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}` const timelineBatchId = `tl-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`
@ -2296,6 +2304,7 @@ class ClientService extends EventTarget {
startLogin, startLogin,
needSort, needSort,
firstRelayResultGraceMs, firstRelayResultGraceMs,
relayAuthoritativeTimeline,
relayReqLog: { relayReqLog: {
groupId: `${timelineBatchId}:shard${shardIndex}`, groupId: `${timelineBatchId}:shard${shardIndex}`,
onBatchEnd: onShardSubscribeBatchEnd onBatchEnd: onShardSubscribeBatchEnd
@ -2778,13 +2787,16 @@ class ClientService extends EventTarget {
* every slow/hung relay. Real EOSE still clears the timer and completes earlier if all relays finish first. * every slow/hung relay. Real EOSE still clears the timer and completes earlier if all relays finish first.
*/ */
firstRelayResultGraceMs = FIRST_RELAY_RESULT_GRACE_MS, firstRelayResultGraceMs = FIRST_RELAY_RESULT_GRACE_MS,
relayReqLog relayReqLog,
relayAuthoritativeTimeline = false
}: { }: {
startLogin?: () => void startLogin?: () => void
needSort?: boolean needSort?: boolean
firstRelayResultGraceMs?: number firstRelayResultGraceMs?: number
/** Correlate {@link ClientService.subscribe} logs with a timeline shard */ /** Correlate {@link ClientService.subscribe} logs with a timeline shard */
relayReqLog?: { groupId: string; onBatchEnd?: (rows: RelayOpTerminalRow[]) => void } relayReqLog?: { groupId: string; onBatchEnd?: (rows: RelayOpTerminalRow[]) => void },
/** See {@link ClientService.subscribeTimeline} third-arg `relayAuthoritativeTimeline`. */
relayAuthoritativeTimeline?: boolean
} = {} } = {}
) { ) {
let relays = Array.from(new Set(urls)) let relays = Array.from(new Set(urls))
@ -2795,7 +2807,7 @@ class ClientService extends EventTarget {
} }
if (relayFiltersUseCapitalLetterTagKeys(filter as Filter)) { if (relayFiltersUseCapitalLetterTagKeys(filter as Filter)) {
relays = relayUrlsStripExtendedTagReqBlocked(relays) relays = relayUrlsStripExtendedTagReqBlocked(relays)
if (relays.length === 0 && navigator.onLine) { if (relays.length === 0 && navigator.onLine && !relayAuthoritativeTimeline) {
relays = relayUrlsStripExtendedTagReqBlocked([...FAST_READ_RELAY_URLS]) relays = relayUrlsStripExtendedTagReqBlocked([...FAST_READ_RELAY_URLS])
} }
} }
@ -2806,7 +2818,8 @@ class ClientService extends EventTarget {
this.timelines[key] = { this.timelines[key] = {
refs: [], refs: [],
filter, filter,
urls: relays urls: relays,
...(relayAuthoritativeTimeline ? { disablePersist: true as const } : {})
} }
timeline = this.timelines[key] timeline = this.timelines[key]
} else { } else {
@ -2817,10 +2830,18 @@ class ClientService extends EventTarget {
timeline.filter = filter timeline.filter = filter
timeline.urls = relays timeline.urls = relays
timeline.refs = [] timeline.refs = []
if (relayAuthoritativeTimeline) {
timeline.disablePersist = true
} else {
delete timeline.disablePersist
}
} }
// eslint-disable-next-line @typescript-eslint/no-this-alias // eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this const that = this
const maybePersistTimeline = () => {
if (!relayAuthoritativeTimeline) that.scheduleTimelinePersist(key)
}
let events: NEvent[] = [] let events: NEvent[] = []
/** `null` until initial backlog is considered complete; then wall-clock unix at completion (for straggler vs live). */ /** `null` until initial backlog is considered complete; then wall-clock unix at completion (for straggler vs live). */
let eosedAt: number | null = null let eosedAt: number | null = null
@ -2898,6 +2919,7 @@ class ClientService extends EventTarget {
} }
try { try {
if (!relayAuthoritativeTimeline) {
const st = await indexedDb.getTimelinePersistedState(key) const st = await indexedDb.getTimelinePersistedState(key)
if (st?.refs?.length) { if (st?.refs?.length) {
const hexIds = st.refs.map((r) => r[0]) const hexIds = st.refs.map((r) => r[0])
@ -2918,6 +2940,7 @@ class ClientService extends EventTarget {
} }
flushStreamingSnapshot() flushStreamingSnapshot()
} }
}
} catch (err) { } catch (err) {
logger.warn('[ClientService] Timeline disk hydrate failed', err) logger.warn('[ClientService] Timeline disk hydrate failed', err)
} }
@ -2952,7 +2975,7 @@ class ClientService extends EventTarget {
timeline.refs = events timeline.refs = events
.map((e) => [e.id, e.created_at] as TTimelineRef) .map((e) => [e.id, e.created_at] as TTimelineRef)
.sort((a, b) => b[1] - a[1]) .sort((a, b) => b[1] - a[1])
that.scheduleTimelinePersist(key) maybePersistTimeline()
} }
return return
} }
@ -2967,7 +2990,7 @@ class ClientService extends EventTarget {
if (timeline.refs.length === 0) { if (timeline.refs.length === 0) {
timeline.refs = events.map((e) => [e.id, e.created_at] as TTimelineRef).sort((a, b) => b[1] - a[1]) timeline.refs = events.map((e) => [e.id, e.created_at] as TTimelineRef).sort((a, b) => b[1] - a[1])
that.scheduleTimelinePersist(key) maybePersistTimeline()
return return
} }
@ -2983,7 +3006,7 @@ class ClientService extends EventTarget {
} }
// idx === refs.length → strictly older than tail; splice appends (previous early-return dropped these). // idx === refs.length → strictly older than tail; splice appends (previous early-return dropped these).
timeline.refs.splice(idx, 0, [evt.id, evt.created_at]) timeline.refs.splice(idx, 0, [evt.id, evt.created_at])
that.scheduleTimelinePersist(key) maybePersistTimeline()
} }
const runHttpTimelinePollQuery = async (pollFilter: Filter) => { const runHttpTimelinePollQuery = async (pollFilter: Filter) => {
@ -3045,7 +3068,8 @@ class ClientService extends EventTarget {
that.timelines[key] = { that.timelines[key] = {
refs: events.map((evt) => [evt.id, evt.created_at]), refs: events.map((evt) => [evt.id, evt.created_at]),
filter, filter,
urls: relays urls: relays,
...(relayAuthoritativeTimeline ? { disablePersist: true as const } : {})
} }
} else if (tl.refs.length === 0) { } else if (tl.refs.length === 0) {
tl.refs = events.map((evt) => [evt.id, evt.created_at] as TTimelineRef) tl.refs = events.map((evt) => [evt.id, evt.created_at] as TTimelineRef)
@ -3062,7 +3086,7 @@ class ClientService extends EventTarget {
} }
armHttpTimelinePollingAfterInitial() armHttpTimelinePollingAfterInitial()
onEvents([...events], true) onEvents([...events], true)
that.scheduleTimelinePersist(key) maybePersistTimeline()
} }
// HTTP index relays are handled via httpTimelinePollBases above — never pass them to the WS subscribe path. // HTTP index relays are handled via httpTimelinePollBases above — never pass them to the WS subscribe path.
@ -3163,7 +3187,9 @@ class ClientService extends EventTarget {
timeline.refs.push(...newRefs) timeline.refs.push(...newRefs)
} }
if (!timeline.disablePersist) {
this.scheduleTimelinePersist(key) this.scheduleTimelinePersist(key)
}
return events return events
} }

Loading…
Cancel
Save