/** * The cloud side of the cache. * * Reads are anonymous GETs against the bucket's public base URL: the object * name is a deterministic function of the spec (src/cache/spec.ts), so a * lookup is one fetch and a 404 is a miss — no index, no API. * * Writes go through the tmpbucket Worker * (https://github.com/scratchrealm/tmpbucket): present an API key and a file name, * receive a presigned R2 PUT URL, and upload directly. Only holders of the * key can write; everyone can read. */ import { APP_NAME, FORMAT_VERSION, cacheFileName, canonicalJson, type CacheSpec } from './spec.ts'; const PUBLIC_BASE = 'https://tempory.net/tmpbucket/'; const WORKER_BASE = 'https://tmpbucket.figurl.workers.dev'; const CONTENT_TYPE = 'application/x-hdf5'; export interface CacheLookup { fileName: string; url: string; specJson: string; } export async function lookupFor(spec: CacheSpec): Promise { const fileName = await cacheFileName(spec); return { fileName, url: PUBLIC_BASE + fileName, specJson: canonicalJson(spec) }; } /** Fetch a cached solution; null on a miss. Throws on network failure or an * unexpected status, which are reported rather than treated as misses. */ export async function fetchCached(lookup: CacheLookup): Promise { const res = await fetch(lookup.url, { cache: 'no-store' }); if (res.status === 404) return null; if (!res.ok) throw new Error(`cache read: HTTP ${res.status} for ${lookup.url}`); return new Uint8Array(await res.arrayBuffer()); } /** * Is this solution in the cloud? A HEAD is enough. Three answers, not two: * null is "could not tell", which a note may show as nothing at all and a * walk treats as absence — a network hiccup is not evidence that a file is * there, and computing it anyway only costs time and ends in an upload that * overwrites an identical object. */ export async function headCached(lookup: CacheLookup): Promise { try { const res = await fetch(lookup.url, { method: 'HEAD', cache: 'no-store' }); return res.ok ? true : res.status === 404 ? false : null; } catch { return null; } } /** headCached, for a caller with nothing to say about "could not tell". */ export async function isCached(lookup: CacheLookup): Promise { return (await headCached(lookup)) === true; } /** Ask the Worker for a presigned PUT. Nothing is written until it is used. */ function requestUploadUrl(apiKey: string, fileName: string): Promise { return fetch(`${WORKER_BASE}/api/upload-url`, { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ fileName, contentType: CONTENT_TYPE }), }); } /** * Would this key be allowed to upload? Asking for a grant and not using it * writes nothing, and answers in one request — worth doing when a key is * entered, since the alternative is finding out after the first run. * Throws if the Worker cannot be reached at all, which is not the key's fault. */ export async function verifyApiKey(apiKey: string): Promise { const res = await requestUploadUrl(apiKey, `${APP_NAME}/v${FORMAT_VERSION}/.keycheck`); if (res.status === 401 || res.status === 403) return false; if (!res.ok) throw new Error(`upload-url request failed: HTTP ${res.status}`); return true; } /** Upload one cache file. Resolves to its public URL. */ export async function uploadCacheFile( apiKey: string, fileName: string, bytes: Uint8Array, ): Promise { const res = await requestUploadUrl(apiKey, fileName); if (res.status === 401 || res.status === 403) { throw new Error('upload not authorized — check the API key'); } if (!res.ok) { let message = `HTTP ${res.status}`; try { const err = (await res.json()) as { message?: string }; if (err.message) message = err.message; } catch { // keep the status-only message } throw new Error(`upload-url request failed: ${message}`); } const grant = (await res.json()) as { uploadUrl: string; uploadHeaders?: Record; downloadUrl: string; }; const put = await fetch(grant.uploadUrl, { method: 'PUT', headers: grant.uploadHeaders ?? { 'Content-Type': CONTENT_TYPE }, body: bytes as unknown as BodyInit, }); if (!put.ok) throw new Error(`upload PUT failed: HTTP ${put.status}`); return grant.downloadUrl; }