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.
69 lines
2.4 KiB
69 lines
2.4 KiB
import type { TRelayThreadHeatBubble, TRelayThreadHeatEdge } from '@/lib/relay-thread-heat' |
|
|
|
const CACHE_V = 1 as const |
|
|
|
export type TRelayThreadHeatMapCacheEnvelope = { |
|
v: typeof CACHE_V |
|
/** When the merge finished (ms since epoch). */ |
|
builtAtMs: number |
|
bubbles: TRelayThreadHeatBubble[] |
|
/** Links between bubbles (same as live merge); absent in older cache payloads. */ |
|
edges?: TRelayThreadHeatEdge[] |
|
} |
|
|
|
/** Stable short digest for SETTINGS key segments (FNV-1a 32-bit). */ |
|
export function digestHeatMapKeyPart(s: string): string { |
|
let h = 2166136261 |
|
for (let i = 0; i < s.length; i++) { |
|
h ^= s.charCodeAt(i) |
|
h = Math.imul(h, 16777619) |
|
} |
|
return (h >>> 0).toString(36).padStart(6, '0') |
|
} |
|
|
|
export function relayThreadHeatMapSettingKey( |
|
pubkey: string, |
|
relayUrls: readonly string[], |
|
followPubkeys: readonly string[], |
|
/** Serialized home kind-picker state so cache invalidates when feed filters change. */ |
|
feedFilterKey: string, |
|
/** Sorted mute pubkeys so cache invalidates when mutes change. */ |
|
muteFingerprint: string |
|
): string { |
|
const pk = pubkey.trim().toLowerCase() |
|
const relayKey = digestHeatMapKeyPart([...relayUrls].sort().join('\n')) |
|
const followKey = digestHeatMapKeyPart( |
|
[...followPubkeys] |
|
.map((p) => p.trim().toLowerCase()) |
|
.filter(Boolean) |
|
.sort() |
|
.join('\n') |
|
) |
|
const feedKey = digestHeatMapKeyPart(feedFilterKey) |
|
const muteKey = digestHeatMapKeyPart(muteFingerprint) |
|
return `relayHeatV${CACHE_V}:${pk}:${relayKey}:${followKey}:${feedKey}:${muteKey}` |
|
} |
|
|
|
export function parseRelayThreadHeatMapCache(raw: string | null): TRelayThreadHeatMapCacheEnvelope | null { |
|
if (raw == null || raw === '') return null |
|
try { |
|
const o = JSON.parse(raw) as Partial<TRelayThreadHeatMapCacheEnvelope> |
|
if (o.v !== CACHE_V || !Array.isArray(o.bubbles)) return null |
|
const edgesRaw = o.edges |
|
const edges: TRelayThreadHeatEdge[] | undefined = Array.isArray(edgesRaw) |
|
? edgesRaw.filter( |
|
(e: unknown): e is TRelayThreadHeatEdge => |
|
e != null && |
|
typeof (e as TRelayThreadHeatEdge).a === 'string' && |
|
typeof (e as TRelayThreadHeatEdge).b === 'string' |
|
) |
|
: undefined |
|
return { v: CACHE_V, builtAtMs: typeof o.builtAtMs === 'number' ? o.builtAtMs : 0, bubbles: o.bubbles, edges } |
|
} catch { |
|
return null |
|
} |
|
} |
|
|
|
export function serializeRelayThreadHeatMapCache(envelope: TRelayThreadHeatMapCacheEnvelope): string { |
|
return JSON.stringify(envelope) |
|
}
|
|
|