Persisted feeds keep only episodes from the last 30 days (plus completed downloads); older episodes live in volatile memory and survive refreshes via union merge, with per-feed in-memory caches capped at 500. Refresh batches run at FETCH_CONCURRENCY=4 with per-feed incremental apply (no Promise.all barrier), config.json writes are trailing-edge debounced (250ms, immediate flushPendingSave for unsubscribes), and cold fetch-more refetches abort at FETCH_TIMEOUT_MS. A shared activity store powers a global top-right indicator covering refresh, fetch-more, subscribe, search, and downloads. Also includes the in-flight incremental RSS parsing (chunked with event-loop yields) and refresh spinner work this tree already carried.
27 lines
934 B
TypeScript
27 lines
934 B
TypeScript
import type { Episode } from "../types/episode"
|
|
|
|
/** Sort key for an episode's pubDate — missing/invalid dates sort as NEWEST
|
|
* (Infinity) so undated episodes float to the top instead of dropping into
|
|
* the oldest slot. */
|
|
const ts = (ep: Episode): number => {
|
|
const t = ep.pubDate?.getTime()
|
|
return t === undefined || Number.isNaN(t) ? Infinity : t
|
|
}
|
|
|
|
/**
|
|
* Union of two episode lists keyed by id — on collision the fetched copy
|
|
* wins (fresh metadata). Result is sorted newest-first by pubDate and capped
|
|
* at `cap` entries (oldest dropped). Never mutates either input.
|
|
*/
|
|
export function mergeEpisodes(
|
|
existing: Episode[],
|
|
fetched: Episode[],
|
|
cap: number,
|
|
): Episode[] {
|
|
const byId = new Map<string, Episode>()
|
|
for (const ep of existing) byId.set(ep.id, ep)
|
|
for (const ep of fetched) byId.set(ep.id, ep)
|
|
const sorted = [...byId.values()].sort((a, b) => ts(b) - ts(a))
|
|
return sorted.slice(0, cap)
|
|
}
|