Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 0edd49b560af59314c8e2d1278ddf80148282523
parent 6e33696e65aff3fcc0d805c8c13e34f17b33134f
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sat, 18 Apr 2026 02:38:09 -0400

in-memory data again

Diffstat:
Mapp/transcriptCache.ts | 52++++++++++++++++++++++++----------------------------
Aapp/transcriptStore.ts | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dpublic/_headers | 3---
3 files changed, 171 insertions(+), 31 deletions(-)

diff --git a/app/transcriptCache.ts b/app/transcriptCache.ts @@ -1,39 +1,35 @@ "use client"; import type { TranscriptDetail } from "@/lib/transcripts"; +import { idbGet, idbPutBatch } from "./transcriptStore"; -// Cap chosen to cover realistic "recently viewed" sessions (~3 MB at ~30 KB -// per transcript) without pinning the full corpus during a search scan. -const MAX_ENTRIES = 100; - -// Map's insertion order gives us a free FIFO; we promote on access -// (delete + set) to approximate LRU so the most recently used entries stay. -const cache = new Map<string, Promise<TranscriptDetail>>(); - -function promote(slug: string, p: Promise<TranscriptDetail>): void { - cache.delete(slug); - cache.set(slug, p); - while (cache.size > MAX_ENTRIES) { - const oldest = cache.keys().next().value; - if (oldest === undefined) break; - cache.delete(oldest); - } -} +const resolved = new Map<string, TranscriptDetail>(); +const inFlight = new Map<string, Promise<TranscriptDetail>>(); export function fetchTranscript(slug: string): Promise<TranscriptDetail> { - const existing = cache.get(slug); - if (existing) { - promote(slug, existing); - return existing; - } - const p = fetch(`/transcripts/${slug}.json`).then((r) => { - if (!r.ok) throw new Error(`Failed to fetch transcript ${slug}`); - return r.json() as Promise<TranscriptDetail>; + const hit = resolved.get(slug); + if (hit) return Promise.resolve(hit); + const flying = inFlight.get(slug); + if (flying) return flying; + + const p = load(slug).then((detail) => { + resolved.set(slug, detail); + inFlight.delete(slug); + return detail; }); - // Evict on failure so retries can go over the wire again. p.catch(() => { - if (cache.get(slug) === p) cache.delete(slug); + inFlight.delete(slug); }); - promote(slug, p); + inFlight.set(slug, p); return p; } + +async function load(slug: string): Promise<TranscriptDetail> { + const stored = await idbGet(slug); + if (stored) return stored; + const r = await fetch(`/transcripts/${slug}.json`); + if (!r.ok) throw new Error(`Failed to fetch transcript ${slug}`); + const detail = (await r.json()) as TranscriptDetail; + idbPutBatch(detail); + return detail; +} diff --git a/app/transcriptStore.ts b/app/transcriptStore.ts @@ -0,0 +1,147 @@ +"use client"; + +import type { TranscriptDetail } from "@/lib/transcripts"; + +const DB_NAME = "yt-dlp-transcript-browser"; +const DB_VERSION = 1; +const STORE = "transcripts"; + +type Mode = "pending" | "ok" | "unavailable"; + +let mode: Mode = "pending"; +let dbPromise: Promise<IDBDatabase | null> | null = null; + +function openDb(): Promise<IDBDatabase | null> { + if (typeof indexedDB === "undefined") { + mode = "unavailable"; + return Promise.resolve(null); + } + if (dbPromise) return dbPromise; + dbPromise = new Promise<IDBDatabase | null>((resolve) => { + let req: IDBOpenDBRequest; + try { + req = indexedDB.open(DB_NAME, DB_VERSION); + } catch { + downgrade("open threw"); + resolve(null); + return; + } + req.onupgradeneeded = () => { + const db = req.result; + if (!db.objectStoreNames.contains(STORE)) { + db.createObjectStore(STORE, { keyPath: "slug" }); + } + }; + req.onsuccess = () => { + mode = "ok"; + resolve(req.result); + }; + req.onerror = () => { + downgrade("open failed"); + resolve(null); + }; + req.onblocked = () => { + downgrade("open blocked"); + resolve(null); + }; + }); + return dbPromise; +} + +function downgrade(reason: string): void { + if (mode === "unavailable") return; + mode = "unavailable"; + console.warn( + `[transcriptStore] IndexedDB unavailable (${reason}); falling back to network-only.`, + ); +} + +export async function idbGet( + slug: string, +): Promise<TranscriptDetail | null> { + const db = await openDb(); + if (!db) return null; + return new Promise<TranscriptDetail | null>((resolve) => { + let req: IDBRequest<TranscriptDetail | undefined>; + try { + const tx = db.transaction(STORE, "readonly"); + req = tx.objectStore(STORE).get(slug) as IDBRequest< + TranscriptDetail | undefined + >; + } catch { + downgrade("read tx threw"); + resolve(null); + return; + } + req.onsuccess = () => resolve(req.result ?? null); + req.onerror = () => resolve(null); + }); +} + +let pending: TranscriptDetail[] = []; +let flushScheduled = false; +let flushInFlight: Promise<void> | null = null; + +export function idbPutBatch(detail: TranscriptDetail): void { + if (mode === "unavailable") return; + pending.push(detail); + if (flushScheduled) return; + flushScheduled = true; + queueMicrotask(() => { + flushScheduled = false; + void flush(); + }); +} + +async function flush(): Promise<void> { + if (flushInFlight) { + await flushInFlight; + } + if (pending.length === 0) return; + const batch = pending; + pending = []; + flushInFlight = writeBatch(batch).finally(() => { + flushInFlight = null; + if (pending.length > 0 && !flushScheduled) { + flushScheduled = true; + queueMicrotask(() => { + flushScheduled = false; + void flush(); + }); + } + }); + await flushInFlight; +} + +async function writeBatch(batch: TranscriptDetail[]): Promise<void> { + const db = await openDb(); + if (!db) return; + return new Promise<void>((resolve) => { + let tx: IDBTransaction; + try { + tx = db.transaction(STORE, "readwrite"); + } catch { + downgrade("write tx threw"); + resolve(); + return; + } + const store = tx.objectStore(STORE); + for (const entry of batch) { + try { + store.put(entry); + } catch { + // Per-entry errors (e.g. unclonable values) shouldn't fail the batch. + } + } + tx.oncomplete = () => resolve(); + tx.onerror = () => { + // Quota errors land here; degrade silently after one warn. + downgrade("write tx error"); + resolve(); + }; + tx.onabort = () => { + downgrade("write tx abort"); + resolve(); + }; + }); +} diff --git a/public/_headers b/public/_headers @@ -1,2 +0,0 @@ -/transcripts/* - Cache-Control: public, max-age=3600 -\ No newline at end of file