towards multi-user

This commit is contained in:
Davide Scaini
2026-04-08 19:37:10 +02:00
parent 36a91362d9
commit f76cc0ce7e
18 changed files with 1248 additions and 30 deletions
+56 -8
View File
@@ -58,28 +58,76 @@ function emptyIndex(): BASIndex {
// ── Public API ────────────────────────────────────────────────────────────────
/**
* Load the activity index, merging the server's copy with any locally-stored
* activities. Local entries override server entries with the same ID.
* Resolve shards from a BASIndex into a flat activity list.
*
* Handles two shard types transparently:
* - handle shards: multi-user manifest (url = "{handle}/index.json")
* - year shards: per-user pagination (url = "index-2025.json")
*
* Shard URLs are resolved relative to the index URL that declared them.
* All shard fetches run concurrently. Errors are silently skipped so a
* single unavailable shard doesn't break the whole feed.
*/
export async function loadIndex(baseUrl: string): Promise<BASIndex> {
async function resolveShards(
index: BASIndex,
indexUrl: string,
): Promise<ActivitySummary[]> {
if (!index.shards?.length) return index.activities ?? [];
const base = indexUrl.substring(0, indexUrl.lastIndexOf('/') + 1);
const shardResults = await Promise.allSettled(
index.shards.map(async shard => {
const url = shard.url.startsWith('http') ? shard.url : `${base}${shard.url}`;
const sub = await fetchJSON<BASIndex>(url);
// Recursively resolve nested shards (e.g. user shard that itself paginates)
const activities = await resolveShards(sub, url);
// Tag each activity with the handle declared in the shard entry
if (shard.handle) {
return activities.map(a => ({ ...a, handle: shard.handle }));
}
return activities;
}),
);
const own = index.activities ?? [];
const fromShards = shardResults.flatMap(r => r.status === 'fulfilled' ? r.value : []);
return [...own, ...fromShards];
}
/**
* Load the activity index, resolving any shards (multi-user or pagination),
* then merging with locally-stored activities from IndexedDB.
*
* Single-user indexes with no shards work exactly as before — zero overhead.
*
* @param baseUrl Site base URL (used for IDB local activities)
* @param indexUrl Full URL of the index to load (defaults to baseUrl + data/index.json)
*/
export async function loadIndex(baseUrl: string, indexUrl?: string): Promise<BASIndex> {
indexUrl = indexUrl ?? `${baseUrl}data/index.json`;
const [serverResult, localResult] = await Promise.allSettled([
fetchJSON<BASIndex>(`${baseUrl}data/index.json`),
fetchJSON<BASIndex>(indexUrl),
listLocalActivities(),
]);
const server = serverResult.status === 'fulfilled' ? serverResult.value : null;
const local = localResult.status === 'fulfilled' ? localResult.value : [];
if (local.length === 0) return server ?? emptyIndex();
if (!server) return { ...emptyIndex(), activities: local as ActivitySummary[] };
const serverActivities = server
? await resolveShards(server, indexUrl)
: [];
if (local.length === 0 && !server) return emptyIndex();
// Local overrides server for the same ID; new local entries are appended
const merged = new Map<string, ActivitySummary>();
for (const a of server.activities ?? []) merged.set(a.id, a);
for (const a of serverActivities) merged.set(a.id, a);
for (const a of local as ActivitySummary[]) merged.set(a.id, a);
return {
...server,
...(server ?? emptyIndex()),
activities: [...merged.values()].sort(
(a, b) => (b.started_at ?? '').localeCompare(a.started_at ?? ''),
),