{"version":3,"file":"cache.cjs","names":[],"sources":["../../src/tabular/cache.ts"],"sourcesContent":["/**\n * Keeping model bytes on the device, so the second visit works offline.\n *\n * A prediction that needs the network is not offline inference. The model\n * file is the one asset the app cannot re-derive, so it goes into Cache\n * Storage on first load and is read from there afterwards.\n *\n * Cache-first, not network-first, and deliberately: a model file is\n * immutable for a given version, so revalidating it on every load spends a\n * round trip to learn nothing. Publish a new version under a new URL (or\n * pass `revalidate`) when the model changes.\n */\n\nimport { ModelFetchError } from \"./exceptions\";\n\n/** Cache Storage bucket used when the caller does not name one. */\nexport const DEFAULT_MODEL_CACHE = \"tempest-tabular-models\";\n\n/** Options for {@link fetchModelBytes}. */\nexport interface ModelCacheOptions {\n    /** Cache Storage bucket name. */\n    readonly cacheName?: string;\n    /**\n     * Go to the network first and fall back to the cache.\n     *\n     * For a URL that serves \"whatever is current\" rather than a pinned\n     * version. Costs a round trip on every load when online.\n     */\n    readonly revalidate?: boolean;\n    /** `fetch` options, e.g. credentials for a private model endpoint. */\n    readonly requestInit?: RequestInit;\n}\n\n/**\n * Whether Cache Storage is usable here.\n *\n * Absent in a non-secure context, in Node, and in some private-mode\n * browsers. The module degrades to a plain fetch rather than failing —\n * losing offline support is better than losing inference.\n *\n * @returns `true` when `caches` can be used.\n */\nfunction hasCacheStorage(): boolean {\n    return typeof globalThis.caches !== \"undefined\";\n}\n\n/**\n * Fetch the model bytes, preferring the on-device copy.\n *\n * @example\n * ```ts\n * const bytes = await fetchModelBytes(\"/models/classifier-v3.onnx\");\n * const predictor = await TabularPredictor.create(bytes);\n * ```\n *\n * @param url Where the model lives.\n * @param options Cache bucket, revalidation and `fetch` options.\n * @returns The model bytes.\n * @throws {@link ModelFetchError} when the model is neither cached nor\n *   reachable — which is the \"offline and never warmed\" case, and the\n *   message says so.\n */\nexport async function fetchModelBytes(\n    url: string,\n    options: ModelCacheOptions = {},\n): Promise<Uint8Array> {\n    const cacheName = options.cacheName ?? DEFAULT_MODEL_CACHE;\n\n    if (!hasCacheStorage()) {\n        return await downloadBytes(url, options.requestInit);\n    }\n\n    const cache = await globalThis.caches.open(cacheName);\n\n    if (options.revalidate !== true) {\n        const cached = await cache.match(url);\n        if (cached !== undefined) return new Uint8Array(await cached.arrayBuffer());\n    }\n\n    try {\n        const response = await fetch(url, options.requestInit);\n        if (!response.ok) {\n            throw new ModelFetchError(\n                `Failed to download the model: ${response.status} ${response.statusText}`,\n            );\n        }\n        await cache.put(url, response.clone());\n        return new Uint8Array(await response.arrayBuffer());\n    } catch (error) {\n        const cached = await cache.match(url);\n        if (cached !== undefined) return new Uint8Array(await cached.arrayBuffer());\n        if (error instanceof ModelFetchError) throw error;\n        throw new ModelFetchError(\n            `The model is not cached and could not be downloaded: ${url}. ` +\n                \"Warm the cache while online — prefetch it at install time, or \" +\n                \"precache it in the service worker.\",\n            { cause: error },\n        );\n    }\n}\n\n/**\n * Download without touching the cache.\n *\n * @param url Where the model lives.\n * @param requestInit `fetch` options.\n * @returns The model bytes.\n * @throws {@link ModelFetchError} when the request fails.\n */\nasync function downloadBytes(url: string, requestInit?: RequestInit): Promise<Uint8Array> {\n    try {\n        const response = await fetch(url, requestInit);\n        if (!response.ok) {\n            throw new ModelFetchError(\n                `Failed to download the model: ${response.status} ${response.statusText}`,\n            );\n        }\n        return new Uint8Array(await response.arrayBuffer());\n    } catch (error) {\n        if (error instanceof ModelFetchError) throw error;\n        throw new ModelFetchError(`Failed to download the model: ${url}`, { cause: error });\n    }\n}\n\n/**\n * Whether a model is already on the device.\n *\n * Useful for showing \"available offline\" in the UI, and for deciding\n * whether to prefetch on a metered connection.\n *\n * @param url The model URL.\n * @param cacheName Cache Storage bucket name.\n * @returns `true` when the bytes are cached.\n */\nexport async function isModelCached(\n    url: string,\n    cacheName: string = DEFAULT_MODEL_CACHE,\n): Promise<boolean> {\n    if (!hasCacheStorage()) return false;\n    const cache = await globalThis.caches.open(cacheName);\n    return (await cache.match(url)) !== undefined;\n}\n\n/**\n * Store model bytes without downloading them.\n *\n * For an app that already has the bytes — from a file input, or from a\n * bundle it unpacked — and wants the next load to find them cached.\n *\n * @param url The URL to key the entry under.\n * @param bytes The model bytes.\n * @param cacheName Cache Storage bucket name.\n * @returns `true` when the entry was stored, `false` without Cache Storage.\n */\nexport async function cacheModelBytes(\n    url: string,\n    bytes: Uint8Array,\n    cacheName: string = DEFAULT_MODEL_CACHE,\n): Promise<boolean> {\n    if (!hasCacheStorage()) return false;\n    const cache = await globalThis.caches.open(cacheName);\n    const body = new Uint8Array(bytes).buffer as ArrayBuffer;\n    await cache.put(\n        url,\n        new Response(body, {\n            headers: { \"content-type\": \"application/octet-stream\" },\n        }),\n    );\n    return true;\n}\n\n/**\n * Drop cached models.\n *\n * @param url A specific model to evict; omit to delete the whole bucket.\n * @param cacheName Cache Storage bucket name.\n * @returns `true` when something was deleted.\n */\nexport async function clearModelCache(\n    url?: string,\n    cacheName: string = DEFAULT_MODEL_CACHE,\n): Promise<boolean> {\n    if (!hasCacheStorage()) return false;\n    if (url === undefined) return await globalThis.caches.delete(cacheName);\n    const cache = await globalThis.caches.open(cacheName);\n    return await cache.delete(url);\n}\n"],"mappings":"oCAgBA,IAAa,EAAsB,yBA0BnC,SAAS,GAA2B,CAChC,OAAc,WAAW,SAAW,MACxC,CAkBA,eAAsB,EAClB,EACA,EAA6B,CAAC,EACX,CACnB,IAAM,EAAY,EAAQ,WAAA,yBAE1B,GAAI,CAAC,EAAgB,EACjB,OAAO,MAAM,EAAc,EAAK,EAAQ,WAAW,EAGvD,IAAM,EAAQ,MAAM,WAAW,OAAO,KAAK,CAAS,EAEpD,GAAI,EAAQ,aAAe,GAAM,CAC7B,IAAM,EAAS,MAAM,EAAM,MAAM,CAAG,EACpC,GAAI,IAAW,IAAA,GAAW,OAAO,IAAI,WAAW,MAAM,EAAO,YAAY,CAAC,CAC9E,CAEA,GAAI,CACA,IAAM,EAAW,MAAM,MAAM,EAAK,EAAQ,WAAW,EACrD,GAAI,CAAC,EAAS,GACV,MAAM,IAAI,EAAA,gBACN,iCAAiC,EAAS,OAAO,GAAG,EAAS,YACjE,EAGJ,OADA,MAAM,EAAM,IAAI,EAAK,EAAS,MAAM,CAAC,EAC9B,IAAI,WAAW,MAAM,EAAS,YAAY,CAAC,CACtD,OAAS,EAAO,CACZ,IAAM,EAAS,MAAM,EAAM,MAAM,CAAG,EACpC,GAAI,IAAW,IAAA,GAAW,OAAO,IAAI,WAAW,MAAM,EAAO,YAAY,CAAC,EAE1E,MADI,aAAiB,EAAA,gBAAuB,EACtC,IAAI,EAAA,gBACN,wDAAwD,EAAI,oGAG5D,CAAE,MAAO,CAAM,CACnB,CACJ,CACJ,CAUA,eAAe,EAAc,EAAa,EAAgD,CACtF,GAAI,CACA,IAAM,EAAW,MAAM,MAAM,EAAK,CAAW,EAC7C,GAAI,CAAC,EAAS,GACV,MAAM,IAAI,EAAA,gBACN,iCAAiC,EAAS,OAAO,GAAG,EAAS,YACjE,EAEJ,OAAO,IAAI,WAAW,MAAM,EAAS,YAAY,CAAC,CACtD,OAAS,EAAO,CAEZ,MADI,aAAiB,EAAA,gBAAuB,EACtC,IAAI,EAAA,gBAAgB,iCAAiC,IAAO,CAAE,MAAO,CAAM,CAAC,CACtF,CACJ,CAYA,eAAsB,EAClB,EACA,EAAoB,EACJ,CAGhB,OAFK,EAAgB,EAEb,MAAM,MADM,WAAW,OAAO,KAAK,CAAS,EAAA,CAChC,MAAM,CAAG,IAAO,IAAA,GAFL,EAGnC,CAaA,eAAsB,EAClB,EACA,EACA,EAAoB,EACJ,CAChB,GAAI,CAAC,EAAgB,EAAG,MAAO,GAC/B,IAAM,EAAQ,MAAM,WAAW,OAAO,KAAK,CAAS,EAC9C,EAAO,IAAI,WAAW,CAAK,CAAC,CAAC,OAOnC,OANA,MAAM,EAAM,IACR,EACA,IAAI,SAAS,EAAM,CACf,QAAS,CAAE,eAAgB,0BAA2B,CAC1D,CAAC,CACL,EACO,EACX,CASA,eAAsB,EAClB,EACA,EAAoB,EACJ,CAIhB,OAHK,EAAgB,EACjB,IAAQ,IAAA,GAAkB,MAAM,WAAW,OAAO,OAAO,CAAS,EAE/D,MAAM,MADO,WAAW,OAAO,KAAK,CAAS,EAAA,CACjC,OAAO,CAAG,EAHE,EAInC"}