perf: combined feed index for multi-user global feed
Instead of the browser resolving 20+ user shards recursively (~27 MB), generate a pre-sorted feed.json at merge time with 50 activities per page. The global feed loads one ~30 KB file on first paint; "Load more" fetches subsequent pages (feed-2.json, feed-3.json, etc.). Per-user profile pages still use year-sharded loadIndexPaged as before.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import type { ActivitySummary, BASIndex, Sport } from '../lib/types';
|
||||
import { formatDistance, formatDuration, formatElevation, formatDate, isUnlisted, sportIcon, sportColor, sportLabel } from '../lib/format';
|
||||
import { loadIndexPaged, loadShardActivities } from '../lib/dataloader';
|
||||
import { loadIndexPaged, loadShardActivities, loadCombinedFeed, loadCombinedFeedPage } from '../lib/dataloader';
|
||||
|
||||
/** Render preview_coords as an SVG polyline path string. */
|
||||
function trackPath(coords: [number, number][] | null, w: number, h: number): string {
|
||||
@@ -45,6 +45,9 @@
|
||||
let error = '';
|
||||
let mounted = false;
|
||||
let pendingShards: string[] = [];
|
||||
/** Remaining combined-feed pages (multi-user global feed). */
|
||||
let feedNextPage = 0;
|
||||
let feedTotalPages = 0;
|
||||
/** Logged-in handle — resolved async via bincio:me event. */
|
||||
let me: string = '';
|
||||
|
||||
@@ -61,20 +64,26 @@
|
||||
$: filtered = sport === 'all' ? withPrivacy : withPrivacy.filter(a => a.sport === sport);
|
||||
$: visible = filtered.slice(0, shown);
|
||||
$: canShowMore = shown < filtered.length;
|
||||
$: hasMore = canShowMore || pendingShards.length > 0;
|
||||
$: hasMore = canShowMore || pendingShards.length > 0 || feedNextPage > 0;
|
||||
|
||||
async function loadMore() {
|
||||
if (canShowMore) {
|
||||
shown += PAGE_SIZE;
|
||||
return;
|
||||
}
|
||||
if (!pendingShards.length) return;
|
||||
loadingMore = true;
|
||||
const url = pendingShards[0];
|
||||
pendingShards = pendingShards.slice(1);
|
||||
try {
|
||||
const fresh = await loadShardActivities(url);
|
||||
// Merge avoiding duplicates (IDB activities may already be present)
|
||||
let fresh: ActivitySummary[] = [];
|
||||
if (feedNextPage > 0) {
|
||||
fresh = await loadCombinedFeedPage(base, feedNextPage);
|
||||
feedNextPage = feedNextPage < feedTotalPages ? feedNextPage + 1 : 0;
|
||||
} else if (pendingShards.length) {
|
||||
const url = pendingShards[0];
|
||||
pendingShards = pendingShards.slice(1);
|
||||
fresh = await loadShardActivities(url);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
const existing = new Map(all.map(a => [a.id, a]));
|
||||
for (const a of fresh) if (!existing.has(a.id)) existing.set(a.id, a);
|
||||
all = [...existing.values()].sort((a, b) =>
|
||||
@@ -82,7 +91,7 @@
|
||||
);
|
||||
shown += PAGE_SIZE;
|
||||
} catch {
|
||||
// shard load failed — don't block the user
|
||||
// load failed — don't block the user
|
||||
} finally {
|
||||
loadingMore = false;
|
||||
}
|
||||
@@ -109,6 +118,17 @@
|
||||
}
|
||||
|
||||
try {
|
||||
const isGlobalFeed = !profileIndexUrl && !filterHandle;
|
||||
if (isGlobalFeed) {
|
||||
const combined = await loadCombinedFeed(base);
|
||||
if (combined) {
|
||||
all = combined.activities;
|
||||
feedTotalPages = combined.remainingPages + 1;
|
||||
feedNextPage = combined.remainingPages > 0 ? 2 : 0;
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
const indexUrl = profileIndexUrl
|
||||
? `${base}data/${profileIndexUrl}`
|
||||
: `${base}data/index.json`;
|
||||
@@ -263,6 +283,8 @@
|
||||
Loading…
|
||||
{:else if canShowMore}
|
||||
Load more ({filtered.length - shown} remaining)
|
||||
{:else if feedNextPage > 0}
|
||||
Load more activities
|
||||
{:else}
|
||||
Load older activities ({pendingShards.length} more {pendingShards.length === 1 ? 'year' : 'years'})
|
||||
{/if}
|
||||
|
||||
@@ -253,6 +253,49 @@ 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.
|
||||
*
|
||||
* Falls back to the full shard-resolution path if feed.json doesn't exist
|
||||
* (single-user installs, older data).
|
||||
*/
|
||||
export async function loadCombinedFeed(
|
||||
baseUrl: string,
|
||||
): Promise<{ activities: ActivitySummary[]; remainingPages: number } | null> {
|
||||
try {
|
||||
const feed = await fetchJSON<FeedPage>(`${baseUrl}data/feed.json`);
|
||||
return {
|
||||
activities: feed.activities ?? [],
|
||||
remainingPages: (feed.total_pages ?? 1) - 1,
|
||||
};
|
||||
} 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.
|
||||
|
||||
Reference in New Issue
Block a user