You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

97 lines
4.0 KiB

import type { TFeedSubRequest } from '@/types'
import { normalizeUrl } from '@/lib/url'
import type { Event, Filter } from 'nostr-tools'
import { tagNameEquals } from '@/lib/tag'
/** Canonical JSON for a REQ filter so subscription identity ignores object identity / key order. */
export function stableSpellFeedFilterKey(filter: Filter): string {
const entries = Object.entries(filter)
.filter(([, v]) => v !== undefined)
.sort(([a], [b]) => a.localeCompare(b))
return JSON.stringify(Object.fromEntries(entries))
}
/**
* Single string identity for spell / faux-spell `subRequests`.
* Pass from SpellsPage into NoteList as `feedSubscriptionKey` so timeline subscription does not
* restart when parent passes a new `subRequests` array reference with identical REQ shape.
*/
export function computeSpellSubRequestsIdentityKey(subRequests: TFeedSubRequest[]): string {
if (!subRequests.length) return ''
return JSON.stringify(
subRequests.map((req) => ({
urls: [...req.urls].map((u) => normalizeUrl(u) || u).filter(Boolean).sort(),
filter: stableSpellFeedFilterKey(req.filter)
}))
)
}
/**
* Kind-777 spell feed key: use raw `since` / `until` tag strings plus filters **without** resolved unix
* timestamps. Resolved times change on every memo recompute (`resolveRelativeTime` uses `Date.now()`),
* which was restarting NoteList subscriptions every tick → endless loading.
*/
export function computeKind777SpellFeedSubscriptionKey(spell: Event, subRequests: TFeedSubRequest[]): string {
const sinceRaw = spell.tags.find(tagNameEquals('since'))?.[1] ?? ''
const untilRaw = spell.tags.find(tagNameEquals('until'))?.[1] ?? ''
const sansTime = subRequests.map((req) => {
const { since: _s, until: _u, ...rest } = req.filter as Filter & { since?: number; until?: number }
return {
urls: [...req.urls].map((u) => normalizeUrl(u) || u).filter(Boolean).sort(),
filter: stableSpellFeedFilterKey(rest as Filter)
}
})
return `${spell.id}|sinceRaw:${sinceRaw}|untilRaw:${untilRaw}|${JSON.stringify(sansTime)}`
}
/**
* True when `nextKey` is the same REQ filters as `prevKey` but with a strict superset of relay URLs
* in at least one request slot (e.g. Explore relay reviews: bootstrap relays → full list).
*/
export function isRelayUrlStrictSupersetIdentityKey(prevKey: string | null, nextKey: string): boolean {
if (!prevKey || prevKey === nextKey) return false
try {
type Item = { urls: string[]; filter: string }
const prev = JSON.parse(prevKey) as Item[]
const next = JSON.parse(nextKey) as Item[]
if (!Array.isArray(prev) || !Array.isArray(next) || prev.length !== next.length) return false
let sawStrictGrowth = false
for (let i = 0; i < prev.length; i++) {
if (prev[i].filter !== next[i].filter) return false
const ps = new Set(prev[i].urls)
const ns = new Set(next[i].urls)
for (const u of ps) {
if (!ns.has(u)) return false
}
if (ns.size > ps.size) sawStrictGrowth = true
}
return sawStrictGrowth
} catch {
return false
}
}
/**
* True when parsed {@link computeSpellSubRequestsIdentityKey} payloads match per-slot REQ `filter` strings
* but relay URL lists may differ (reorder, NIP-65 refinement, different cap slices).
* Use with {@link preserveTimelineOnSubRequestsChange} so a provisional relay stack can hand off to a refined
* stack without clearing rows or flashing the loading state.
*/
export function isSpellSubRequestsSameFiltersDifferentRelays(
prevKey: string | null,
nextKey: string
): boolean {
if (!prevKey || prevKey === nextKey) return false
try {
type Item = { urls: string[]; filter: string }
const prev = JSON.parse(prevKey) as Item[]
const next = JSON.parse(nextKey) as Item[]
if (!Array.isArray(prev) || !Array.isArray(next) || prev.length !== next.length) return false
for (let i = 0; i < prev.length; i++) {
if (prev[i].filter !== next[i].filter) return false
}
return true
} catch {
return false
}
}