perf: year-shard index.json to cut initial load from MBs to ~1 year
merge_all/_merged/index.json is now a shard manifest; activities are
split into index-{year}.json files. The feed loads only the most-recent
year on first paint (~200 activities instead of all of them). Older
years are fetched lazily when the user clicks "Load older activities".
Also strips best_efforts / best_climb_m / source from shard files —
these fields are aggregation inputs only, never read by the feed UI.
This commit is contained in:
+108
-7
@@ -55,6 +55,22 @@ function emptyIndex(): BASIndex {
|
||||
};
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function isYearShardUrl(url: string): boolean {
|
||||
return /(?:^|\/)index-\d{4}\.json$/.test(url);
|
||||
}
|
||||
|
||||
function rewriteActivityUrls(a: ActivitySummary, shardBase: string): ActivitySummary {
|
||||
return {
|
||||
...a,
|
||||
detail_url: a.detail_url && !a.detail_url.startsWith('http')
|
||||
? `${shardBase}${a.detail_url}` : a.detail_url,
|
||||
track_url: a.track_url && !a.track_url.startsWith('http')
|
||||
? `${shardBase}${a.track_url}` : a.track_url,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -87,14 +103,8 @@ async function resolveShards(
|
||||
// Rewrite relative detail_url / track_url to be absolute so they can be
|
||||
// fetched correctly regardless of where the root index lives.
|
||||
return activities.map(a => ({
|
||||
...a,
|
||||
...rewriteActivityUrls(a, shardBase),
|
||||
...(shard.handle ? { handle: shard.handle } : {}),
|
||||
detail_url: a.detail_url && !a.detail_url.startsWith('http')
|
||||
? `${shardBase}${a.detail_url}`
|
||||
: a.detail_url,
|
||||
track_url: a.track_url && !a.track_url.startsWith('http')
|
||||
? `${shardBase}${a.track_url}`
|
||||
: a.track_url,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
@@ -150,6 +160,97 @@ export async function loadIndex(baseUrl: string, indexUrl?: string): Promise<BAS
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Like loadIndex but only fetches the most-recent year shard immediately.
|
||||
* Returns the first-page activities plus a list of remaining shard URLs that
|
||||
* can be fetched on demand (e.g. when the user clicks "Load more").
|
||||
*
|
||||
* Falls back to full eager loading for non-year shard manifests (multi-user
|
||||
* combined feed) so the behaviour is identical to loadIndex in those cases.
|
||||
*/
|
||||
export async function loadIndexPaged(
|
||||
baseUrl: string,
|
||||
indexUrl?: string,
|
||||
): Promise<{ index: BASIndex; pendingShards: string[] }> {
|
||||
indexUrl = indexUrl ?? `${baseUrl}data/index.json`;
|
||||
|
||||
const [serverResult, localResult] = await Promise.allSettled([
|
||||
fetchJSON<BASIndex>(indexUrl),
|
||||
listLocalActivities(),
|
||||
]);
|
||||
|
||||
const server = serverResult.status === 'fulfilled' ? serverResult.value : null;
|
||||
const local = localResult.status === 'fulfilled' ? localResult.value : [];
|
||||
|
||||
if (!server && local.length === 0) return { index: emptyIndex(), pendingShards: [] };
|
||||
|
||||
const base = indexUrl.substring(0, indexUrl.lastIndexOf('/') + 1);
|
||||
const allShards = server?.shards ?? [];
|
||||
|
||||
const yearShards = allShards.filter(s => isYearShardUrl(s.url));
|
||||
const otherShards = allShards.filter(s => !isYearShardUrl(s.url));
|
||||
|
||||
// ── Year-sharded index (single-user or profile page) ───────────────────────
|
||||
// Load only the first (most-recent) year shard; return the rest as pending.
|
||||
let yearFirstActivities: ActivitySummary[] = [];
|
||||
let pendingShards: string[] = [];
|
||||
|
||||
if (yearShards.length > 0) {
|
||||
const sorted = [...yearShards].sort((a, b) => b.url.localeCompare(a.url));
|
||||
const firstUrl = sorted[0].url.startsWith('http') ? sorted[0].url : `${base}${sorted[0].url}`;
|
||||
const shardBase = firstUrl.substring(0, firstUrl.lastIndexOf('/') + 1);
|
||||
try {
|
||||
const first = await fetchJSON<BASIndex>(firstUrl);
|
||||
yearFirstActivities = (first.activities ?? []).map(a => rewriteActivityUrls(a, shardBase));
|
||||
} catch (e) {
|
||||
console.error('[bincio] first year shard failed:', sorted[0].url, e);
|
||||
}
|
||||
pendingShards = sorted.slice(1).map(s =>
|
||||
s.url.startsWith('http') ? s.url : `${base}${s.url}`,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Non-year shards (multi-user manifest) — loaded eagerly as before ───────
|
||||
let otherActivities: ActivitySummary[] = [];
|
||||
if (otherShards.length > 0) {
|
||||
const otherIndex: BASIndex = { ...(server ?? emptyIndex()), shards: otherShards };
|
||||
otherActivities = await resolveShards(otherIndex, indexUrl);
|
||||
}
|
||||
|
||||
// ── Own activities (legacy flat index with no shards) ──────────────────────
|
||||
const ownActivities = allShards.length === 0 ? (server?.activities ?? []) : [];
|
||||
|
||||
// Merge: server + local (local overrides server for same id)
|
||||
const serverActivities = [...ownActivities, ...otherActivities, ...yearFirstActivities];
|
||||
const merged = new Map<string, ActivitySummary>();
|
||||
for (const a of serverActivities) merged.set(a.id, a);
|
||||
for (const a of local as ActivitySummary[]) merged.set(a.id, a);
|
||||
|
||||
return {
|
||||
index: {
|
||||
...(server ?? emptyIndex()),
|
||||
activities: [...merged.values()].sort(
|
||||
(a, b) => (b.started_at ?? '').localeCompare(a.started_at ?? ''),
|
||||
),
|
||||
},
|
||||
pendingShards,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch activities from a single year shard URL (absolute).
|
||||
* Used by ActivityFeed to lazily load older years when "Load more" is clicked.
|
||||
*/
|
||||
export async function loadShardActivities(shardUrl: string): Promise<ActivitySummary[]> {
|
||||
try {
|
||||
const data = await fetchJSON<BASIndex>(shardUrl);
|
||||
const base = shardUrl.substring(0, shardUrl.lastIndexOf('/') + 1);
|
||||
return (data.activities ?? []).map(a => rewriteActivityUrls(a, base));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a single activity detail, checking IndexedDB first so locally-converted
|
||||
* activities are available offline.
|
||||
|
||||
Reference in New Issue
Block a user