Global feed: switch from sequential pages to month-based BAS shards

feed.json is now a BAS shard index pointing to feed-YYYY-MM.json files
(~150 activities / ~25 KB gzip each) instead of 400+ sequential feed-N.json
pages. The frontend can now jump directly to a specific month when filtering
by year or date range, without loading all newer data first.

- merge.py: write_combined_feed groups by YYYY-MM and emits a shard index
- dataloader.ts: isYearShardUrl matches feed-YYYY-MM.json; loadCombinedFeed
  returns pendingShards; FeedPage interface and loadCombinedFeedPage removed
- ActivityFeed.svelte: _yearFromShard handles both index-YYYY and feed-YYYY-MM;
  feedNextPage/feedTotalPages/loadingAllFeedPages removed; infinite-loop bug
  fixed (toLoad.length guard before setting loadingAllShards); onMount uses
  pendingShards from loadCombinedFeed
This commit is contained in:
Davide Scaini
2026-05-15 10:25:01 +02:00
parent d3bce49445
commit fe437626e6
3 changed files with 69 additions and 122 deletions
+10 -32
View File
@@ -58,7 +58,7 @@ function emptyIndex(): BASIndex {
// ── Helpers ───────────────────────────────────────────────────────────────────
function isYearShardUrl(url: string): boolean {
return /(?:^|\/)index-\d{4}\.json$/.test(url);
return /(?:^|\/)(?:index-\d{4}|feed-\d{4}-\d{2})\.json$/.test(url);
}
function rewriteActivityUrls(a: ActivitySummary, shardBase: string): ActivitySummary {
@@ -253,50 +253,28 @@ export async function loadShardActivities(shardUrl: string): Promise<ActivitySum
}
}
interface FeedPage {
page: number;
total_pages: number;
total_activities: number;
activities: ActivitySummary[];
}
/**
* Load the combined feed (multi-user global feed). Returns the first page of
* activities pre-sorted across all users, plus remaining page count.
* Load the combined feed (multi-user global feed).
*
* Falls back to the full shard-resolution path if feed.json doesn't exist
* (single-user installs, older data).
* feed.json is now a BAS shard index: the most-recent year shard is fetched
* immediately; older shards are returned as pendingShards for lazy loading.
* Returns null if feed.json doesn't exist (single-user installs).
*/
export async function loadCombinedFeed(
baseUrl: string,
): Promise<{ activities: ActivitySummary[]; remainingPages: number; totalActivities: number } | null> {
): Promise<{ activities: ActivitySummary[]; pendingShards: string[]; totalActivities: number } | null> {
try {
const feed = await fetchJSON<FeedPage>(`${baseUrl}data/feed.json`);
const { index, pendingShards } = await loadIndexPaged(baseUrl, `${baseUrl}data/feed.json`);
return {
activities: feed.activities ?? [],
remainingPages: (feed.total_pages ?? 1) - 1,
totalActivities: feed.total_activities ?? 0,
activities: index.activities,
pendingShards,
totalActivities: (index as any).total_activities ?? index.activities.length,
};
} catch {
return null;
}
}
/**
* Load a subsequent page of the combined feed (feed-2.json, feed-3.json, etc.).
*/
export async function loadCombinedFeedPage(
baseUrl: string,
page: number,
): Promise<ActivitySummary[]> {
try {
const feed = await fetchJSON<FeedPage>(`${baseUrl}data/feed-${page}.json`);
return feed.activities ?? [];
} catch {
return [];
}
}
/**
* Load a single activity detail, checking IndexedDB first so locally-converted
* activities are available offline.