Download web/src/lib/skill-runtime-cache.ts from zlcy/li: direct link, hf CLI and curl.
- Browser
- Download file 5.89 kB
-
https://huggingface.co/spaces/zlcy/li/resolve/main/web/src/lib/skill-runtime-cache.ts
- Command line
-
hf download hf://spaces/zlcy/li/web/src/lib/skill-runtime-cache.ts
-
curl -L -o skill-runtime-cache.ts https://huggingface.co/spaces/zlcy/li/resolve/main/web/src/lib/skill-runtime-cache.ts
5.89 kB
| "use client"; | |
| import localforage from "localforage"; | |
| import { fetchSkillRuntimeConditional, type SkillRuntime } from "@/lib/api"; | |
| /** | |
| * A Skill runtime is a pure function of the files inside its directory, so the | |
| * previous result stays valid until those files change. The catalog already | |
| * carries each Skill's `guide_version`, which lets a cached entry be checked | |
| * against the installed files without an extra round trip. | |
| * | |
| * Opens are served in this order: | |
| * 1. memory hit that still matches the catalog version -> no request at all | |
| * 2. IndexedDB hit -> paint immediately, then revalidate with If-None-Match | |
| * 3. network fetch -> store for the next open | |
| */ | |
| const MEMORY_LIMIT = 24; | |
| const STORAGE_LIMIT = 24; | |
| const FRESH_MS = 60_000; | |
| const STORAGE_NAME = "chatgpt2api"; | |
| const STORE_NAME = "skill-runtime"; | |
| const INDEX_KEY = "__index__"; | |
| export type SkillRuntimeCacheEntry = { | |
| version: string; | |
| etag: string; | |
| savedAt: number; | |
| runtime: SkillRuntime; | |
| }; | |
| const memory = new Map<string, SkillRuntimeCacheEntry>(); | |
| const inflight = new Map<string, Promise<SkillRuntime>>(); | |
| const storage = localforage.createInstance({ name: STORAGE_NAME, storeName: STORE_NAME }); | |
| let storageReady: Promise<void> | null = null; | |
| let storageDisabled = false; | |
| function ensureStorage() { | |
| if (storageDisabled) return Promise.resolve(); | |
| if (!storageReady) { | |
| storageReady = storage.ready().then(() => undefined).catch(() => { | |
| // Private browsing or a blocked storage backend: keep working in memory. | |
| storageDisabled = true; | |
| }); | |
| } | |
| return storageReady; | |
| } | |
| async function readIndex(): Promise<string[]> { | |
| try { | |
| const value = await storage.getItem<string[]>(INDEX_KEY); | |
| if (!Array.isArray(value)) return []; | |
| return value.filter((item): item is string => typeof item === "string"); | |
| } catch { | |
| return []; | |
| } | |
| } | |
| async function writeIndex(ids: string[]) { | |
| try { | |
| await storage.setItem(INDEX_KEY, ids); | |
| } catch { | |
| // A full quota must never break the detail dialog. | |
| } | |
| } | |
| function rememberInMemory(skillId: string, entry: SkillRuntimeCacheEntry) { | |
| memory.delete(skillId); | |
| memory.set(skillId, entry); | |
| while (memory.size > MEMORY_LIMIT) { | |
| const oldest = memory.keys().next().value; | |
| if (oldest === undefined) break; | |
| memory.delete(oldest); | |
| } | |
| } | |
| async function readEntry(skillId: string): Promise<SkillRuntimeCacheEntry | null> { | |
| const inMemory = memory.get(skillId); | |
| if (inMemory) return inMemory; | |
| await ensureStorage(); | |
| if (storageDisabled) return null; | |
| try { | |
| const value = await storage.getItem<SkillRuntimeCacheEntry>(skillId); | |
| if (value && value.runtime) { | |
| rememberInMemory(skillId, value); | |
| return value; | |
| } | |
| } catch { | |
| return null; | |
| } | |
| return null; | |
| } | |
| async function writeEntry(skillId: string, entry: SkillRuntimeCacheEntry) { | |
| rememberInMemory(skillId, entry); | |
| await ensureStorage(); | |
| if (storageDisabled) return; | |
| try { | |
| await storage.setItem(skillId, entry); | |
| const ids = await readIndex(); | |
| const next = [skillId, ...ids.filter((item) => item !== skillId)]; | |
| const dropped = next.slice(STORAGE_LIMIT); | |
| await writeIndex(next.slice(0, STORAGE_LIMIT)); | |
| await Promise.all(dropped.map((id) => storage.removeItem(id))); | |
| } catch { | |
| // Ignore quota errors: the in-memory copy still serves this session. | |
| } | |
| } | |
| /** Cached runtime for instant paint, without touching the network. */ | |
| export function peekSkillRuntime(skillId: string): Promise<SkillRuntimeCacheEntry | null> { | |
| return readEntry(skillId); | |
| } | |
| /** | |
| * True when a cached entry matches the version the catalog reports and was | |
| * written recently enough that no revalidation is expected at all. | |
| */ | |
| export function isSkillRuntimeFresh(entry: SkillRuntimeCacheEntry | null, expectedVersion = "") { | |
| if (!entry || !expectedVersion || entry.version !== expectedVersion) return false; | |
| return Date.now() - entry.savedAt < FRESH_MS; | |
| } | |
| export function forgetSkillRuntime(skillId: string) { | |
| memory.delete(skillId); | |
| void (async () => { | |
| await ensureStorage(); | |
| if (storageDisabled) return; | |
| try { | |
| await storage.removeItem(skillId); | |
| const ids = await readIndex(); | |
| await writeIndex(ids.filter((item) => item !== skillId)); | |
| } catch { | |
| // Nothing to clean up. | |
| } | |
| })(); | |
| } | |
| async function revalidateSkillRuntime(skillId: string, expectedVersion: string): Promise<SkillRuntime> { | |
| const cached = await readEntry(skillId); | |
| const versionMatches = Boolean(cached && expectedVersion && cached.version === expectedVersion); | |
| if (cached && versionMatches && Date.now() - cached.savedAt < FRESH_MS) { | |
| return cached.runtime; | |
| } | |
| const result = await fetchSkillRuntimeConditional(skillId, cached?.etag || ""); | |
| if (result.notModified) { | |
| if (!cached) throw new Error("Skill 数据读取失败"); | |
| // The server confirmed the files are unchanged; refresh recency and adopt | |
| // the catalog version so the next open can skip the request entirely. | |
| await writeEntry(skillId, { | |
| ...cached, | |
| version: expectedVersion || cached.version, | |
| etag: result.etag || cached.etag, | |
| savedAt: Date.now(), | |
| }); | |
| return cached.runtime; | |
| } | |
| const runtime = result.runtime; | |
| if (!runtime) throw new Error("Skill 数据读取失败"); | |
| await writeEntry(skillId, { | |
| version: expectedVersion, | |
| etag: result.etag, | |
| savedAt: Date.now(), | |
| runtime, | |
| }); | |
| return runtime; | |
| } | |
| /** Load a Skill runtime, reusing the cache and de-duplicating concurrent opens. */ | |
| export function loadSkillRuntime(skillId: string, expectedVersion = ""): Promise<SkillRuntime> { | |
| const pending = inflight.get(skillId); | |
| if (pending) return pending; | |
| const task = revalidateSkillRuntime(skillId, expectedVersion).finally(() => { | |
| if (inflight.get(skillId) === task) inflight.delete(skillId); | |
| }); | |
| inflight.set(skillId, task); | |
| return task; | |
| } | |