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:
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