mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
wip(mobile): checkpoint engine and experience round three in progress
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VVYRAqiQj9Y1V9ZfG1RcDH
This commit is contained in:
83 files changed
+50005
-416
No files matched your search
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Instant first frame (DESIGN.md §12.8): the last good situations and their
|
||||
* thumbnails, so the feed paints before any live feed answers.
|
||||
*
|
||||
* 1. IndexedDB: the last live situations() result in this browser
|
||||
* 2. else the bundled snapshot, mobile/demo/snapshot.json (real data,
|
||||
* captured by mobile/demo/capture-snapshot.mjs; see `capturedAt`)
|
||||
*
|
||||
* Thumbnails: the last rendered JPEG per situation id from the Cache API
|
||||
* (secure contexts), else the bundled mobile/demo/thumbs/<id>.jpg.
|
||||
*/
|
||||
import { idbGet, idbPut } from './idb.js';
|
||||
|
||||
const SNAPSHOT_URL = new URL('../demo/snapshot.json', import.meta.url).href;
|
||||
const BUNDLED_THUMBS = import.meta.glob('../demo/thumbs/*.jpg', {
|
||||
eager: true,
|
||||
query: '?url',
|
||||
import: 'default',
|
||||
});
|
||||
const LAST_CACHE = 'gev-thumbs-last-v1';
|
||||
const cacheApi = () =>
|
||||
typeof caches !== 'undefined' && globalThis.isSecureContext;
|
||||
|
||||
const bundledThumb = new Map();
|
||||
for (const [path, url] of Object.entries(BUNDLED_THUMBS)) {
|
||||
const id = decodeURIComponent(
|
||||
path
|
||||
.split('/')
|
||||
.pop()
|
||||
.replace(/\.jpg$/, ''),
|
||||
);
|
||||
bundledThumb.set(id, url);
|
||||
}
|
||||
const cachedThumb = new Map(); // id → blob: URL from the Cache API
|
||||
|
||||
export function thumbFileName(id) {
|
||||
return `${String(id).replace(/[^\w.-]+/g, '_')}.jpg`;
|
||||
}
|
||||
|
||||
function bundledFor(id) {
|
||||
return (
|
||||
bundledThumb.get(thumbFileName(id).replace(/\.jpg$/, '')) ||
|
||||
bundledThumb.get(id) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/** Sync: the best cached thumbnail URL for a situation, or null. */
|
||||
export function cachedThumbnail(situation) {
|
||||
const id = situation?.id;
|
||||
if (!id) return null;
|
||||
return cachedThumb.get(id) || bundledFor(id) || situation.thumbUrl || null;
|
||||
}
|
||||
|
||||
/** Remember the last rendered thumbnail for an id (Cache API). */
|
||||
export async function rememberThumbnail(id, blob) {
|
||||
if (!cacheApi() || !blob) return;
|
||||
try {
|
||||
const c = await caches.open(LAST_CACHE);
|
||||
await c.put(
|
||||
`${location.origin}/__gev-last/${encodeURIComponent(id)}`,
|
||||
new Response(blob, { headers: { 'Content-Type': 'image/jpeg' } }),
|
||||
);
|
||||
} catch {
|
||||
/* quota or private mode */
|
||||
}
|
||||
}
|
||||
|
||||
async function hydrateThumbs(list) {
|
||||
if (!cacheApi()) return;
|
||||
try {
|
||||
const c = await caches.open(LAST_CACHE);
|
||||
await Promise.all(
|
||||
list.map(async (s) => {
|
||||
if (cachedThumb.has(s.id)) return;
|
||||
const hit = await c.match(
|
||||
`${location.origin}/__gev-last/${encodeURIComponent(s.id)}`,
|
||||
);
|
||||
if (hit) cachedThumb.set(s.id, URL.createObjectURL(await hit.blob()));
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
let bundledPromise = null;
|
||||
function loadBundled() {
|
||||
if (!bundledPromise)
|
||||
bundledPromise = fetch(SNAPSHOT_URL)
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.catch(() => null);
|
||||
return bundledPromise;
|
||||
}
|
||||
// Start the bundled fetch at import so a cached call never waits on it.
|
||||
loadBundled();
|
||||
|
||||
function decorate(list, capturedAt, origin) {
|
||||
return list.map((s) => ({
|
||||
...s,
|
||||
cached: true,
|
||||
cacheOrigin: origin,
|
||||
capturedAt,
|
||||
live: false,
|
||||
thumbUrl: cachedThumb.get(s.id) || bundledFor(s.id) || s.thumbUrl || null,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* The last good situations, fast. Resolves [] only when there is nothing at
|
||||
* all (no IndexedDB snapshot and no bundled file).
|
||||
*/
|
||||
export async function loadCachedSituations() {
|
||||
const stored = await Promise.race([
|
||||
idbGet('kv', 'situations'),
|
||||
new Promise((r) => setTimeout(() => r(null), 60)),
|
||||
]);
|
||||
if (stored?.list?.length) {
|
||||
hydrateThumbs(stored.list);
|
||||
return decorate(stored.list, stored.capturedAt, 'device');
|
||||
}
|
||||
const bundled = await loadBundled();
|
||||
if (bundled?.situations?.length)
|
||||
return decorate(
|
||||
bundled.situations,
|
||||
Date.parse(bundled.capturedAt) || bundled.capturedAt,
|
||||
'bundled',
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
/** Store a live situations() result as the next first frame. */
|
||||
export function saveSituations(list) {
|
||||
if (!list?.length) return;
|
||||
const clean = list.map((s) => {
|
||||
const { thumbUrl, cached, cacheOrigin, ...rest } = s;
|
||||
void thumbUrl;
|
||||
void cached;
|
||||
void cacheOrigin;
|
||||
return rest;
|
||||
});
|
||||
idbPut('kv', 'situations', { capturedAt: Date.now(), list: clean }).catch(
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
|
||||
export const hasBundledThumb = (id) => Boolean(bundledFor(id));
|
||||
Reference in new issue
Block a user