Map tiles: new lib/tileCache.ts (Dexie SkateSpotsTilesDB, bounded to ~3000 tiles) and lib/cachedTileLayer.ts, a Leaflet TileLayer that reads through it (cached -> fetch+cache -> plain URL fallback). Wired into both maps (SpotMap, RouteDialog). Chosen over a service worker so it works in the Capacitor WebView too, where a SW never sees tile requests. Photos: useUploadFile drops from three Blossom hosts to two and records ['fallback', '<mirror>/<sha256>']. BlossomUploader signs once and Promise.any PUTs to every server in parallel, so the old three-host list was triple- uploading every photo (the 'tries them in order' comment was wrong). Read side: lib/imageFallback.ts derives same-hash mirrors and ImageWithFallback retries them before showing the placeholder - one choke point for ~20 photo call sites, so a dead host no longer blanks a spot.
51 lines
1.7 KiB
TypeScript
51 lines
1.7 KiB
TypeScript
/**
|
|
* Photo redundancy helpers.
|
|
*
|
|
* SkateSpots uploads images to Blossom, which is CONTENT-ADDRESSED: a blob's URL
|
|
* is `<host>/<sha256>` (optionally with a file extension). That means a mirror of
|
|
* the same file on another known host is derivable from the URL alone — no extra
|
|
* bookkeeping needed — so a dead photo host never has to leave a blank spot.
|
|
*
|
|
* We only derive mirrors for Blossom-style hash paths; anything else is left
|
|
* untouched (an external image URL has no meaningful alternative location).
|
|
*/
|
|
|
|
/** Hosts the app uploads to, and therefore the hosts we can mirror across. */
|
|
export const BLOSSOM_MIRROR_HOSTS = ['blossom.primal.net', 'blossom.band'];
|
|
|
|
/** `<64-hex>` or `<64-hex>.<ext>` */
|
|
const HASH_RE = /^([0-9a-f]{64})(\.[a-z0-9]+)?$/i;
|
|
|
|
/**
|
|
* Ordered candidate URLs for an image: the original first, then any explicitly
|
|
* supplied fallbacks, then same-hash mirrors on the other known Blossom hosts.
|
|
*/
|
|
export function imageCandidates(
|
|
src: string | undefined | null,
|
|
extra: string[] = [],
|
|
): string[] {
|
|
const out: string[] = [];
|
|
const add = (url?: string | null) => {
|
|
if (typeof url === 'string' && url && !out.includes(url)) out.push(url);
|
|
};
|
|
|
|
add(src);
|
|
for (const url of extra) add(url);
|
|
if (!src) return out;
|
|
|
|
try {
|
|
const parsed = new URL(src);
|
|
const last = parsed.pathname.split('/').filter(Boolean).pop() ?? '';
|
|
const match = HASH_RE.exec(last);
|
|
if (match) {
|
|
for (const host of BLOSSOM_MIRROR_HOSTS) {
|
|
if (host === parsed.hostname) continue;
|
|
add(`${parsed.protocol}//${host}/${match[1]}${match[2] ?? ''}`);
|
|
}
|
|
}
|
|
} catch {
|
|
// Relative or malformed URL — nothing to mirror.
|
|
}
|
|
|
|
return out;
|
|
}
|