feat(feed): bound feed lifecycle to 30-day window with nonblocking refresh

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.
This commit is contained in:
2026-08-12 10:13:20 -04:00
parent e09ae15e32
commit deac6081ca
23 changed files with 2226 additions and 169 deletions

View File

@@ -0,0 +1,53 @@
import { Show } from "solid-js";
import { useFeedStore } from "@/stores/feed";
import { useSearchStore } from "@/stores/search";
import { useDownloadStore } from "@/stores/download";
import { useActivityStore } from "@/stores/activity";
import { LoadingIndicator } from "@/components/LoadingIndicator";
/**
* GlobalActivityIndicator — one global top-right signal that ANY feed
* refresh, fetch-more, subscribe fetch, search, or download is in flight.
* Per-page spinners are unchanged; this overlays the content row and status
* bar as a single app-wide "something is happening" indicator.
*/
export function GlobalActivityIndicator() {
const feedStore = useFeedStore();
const searchStore = useSearchStore();
const downloadStore = useDownloadStore();
const activity = useActivityStore();
/** True while any tracked activity is in flight */
const isActive = () =>
feedStore.isLoadingFeeds() ||
feedStore.isLoadingMore() ||
searchStore.isSearching() ||
downloadStore.getActiveCount() + downloadStore.getQueue().length > 0 ||
activity.isActive();
/** Label priority: downloads in flight > latest tracked activity >
* generic loading (only reachable when an isLoading/isSearching flag is
* on but nothing else is). */
const label = () => {
const activeCount = downloadStore.getActiveCount();
const queueLength = downloadStore.getQueue().length;
if (activeCount + queueLength > 0) {
return `Downloading ${activeCount}${
queueLength > 0 ? ` +${queueLength} queued` : ""
}`;
}
if (activity.isActive()) {
const latest = activity.labels().at(-1);
return `${latest ?? "Loading"}`;
}
return "Loading…";
};
return (
<Show when={isActive()}>
<box position="absolute" top={0} right={0} paddingRight={1}>
<LoadingIndicator label={label()} />
</box>
</Show>
);
}