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
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 |
|
} |
|
}
|
|
|