4f822e1turing-surface-cache: reaction-diffusion solutions at a chosen end time, shared through a cloud cacheJeremy Magland 1/**
2 * The cloud side of the cache.
3 *
4 * Reads are anonymous GETs against the bucket's public base URL: the object
5 * name is a deterministic function of the spec (src/cache/spec.ts), so a
6 * lookup is one fetch and a 404 is a miss — no index, no API.
7 *
8 * Writes go through the tmpbucket Worker
9 * (https://github.com/scratchrealm/tmpbucket): present an API key and a file name,
10 * receive a presigned R2 PUT URL, and upload directly. Only holders of the
11 * key can write; everyone can read.
12 */
13import { cacheFileName, canonicalJson, type CacheSpec } from './spec.ts';
15const PUBLIC_BASE = 'https://tempory.net/tmpbucket/';
16const WORKER_BASE = 'https://tmpbucket.figurl.workers.dev';
17const CONTENT_TYPE = 'application/x-hdf5';
19export interface CacheLookup {
20 fileName: string;
21 url: string;
22 specJson: string;
23}
25export async function lookupFor(spec: CacheSpec): Promise<CacheLookup> {
26 const fileName = await cacheFileName(spec);
27 return { fileName, url: PUBLIC_BASE + fileName, specJson: canonicalJson(spec) };
28}
30/** Fetch a cached solution; null on a miss. Throws on network failure or an
31 * unexpected status, which are reported rather than treated as misses. */
32export async function fetchCached(lookup: CacheLookup): Promise<Uint8Array | null> {
33 const res = await fetch(lookup.url, { cache: 'no-store' });
34 if (res.status === 404) return null;
35 if (!res.ok) throw new Error(`cache read: HTTP ${res.status} for ${lookup.url}`);
36 return new Uint8Array(await res.arrayBuffer());
37}
40 * Is this solution in the cloud? A HEAD is enough. Three answers, not two:
41 * null is "could not tell", which a note may show as nothing at all and a
42 * walk treats as absence — a network hiccup is not evidence that a file is
43 * there, and computing it anyway only costs time and ends in an upload that
44 * overwrites an identical object.
45 */
46export async function headCached(lookup: CacheLookup): Promise<boolean | null> {
47 try {
48 const res = await fetch(lookup.url, { method: 'HEAD', cache: 'no-store' });
49 return res.ok ? true : res.status === 404 ? false : null;
50 } catch {
51 return null;
52 }
53}
55/** headCached, for a caller with nothing to say about "could not tell". */
56export async function isCached(lookup: CacheLookup): Promise<boolean> {
57 return (await headCached(lookup)) === true;
58}
4f822e1turing-surface-cache: reaction-diffusion solutions at a chosen end time, shared through a cloud cacheJeremy Magland 60/** Upload one cache file. Resolves to its public URL. */
61export async function uploadCacheFile(
62 apiKey: string,
63 fileName: string,
64 bytes: Uint8Array,
65): Promise<string> {
66 const res = await fetch(`${WORKER_BASE}/api/upload-url`, {
67 method: 'POST',
68 headers: {
69 Authorization: `Bearer ${apiKey}`,
70 'Content-Type': 'application/json',
71 },
72 body: JSON.stringify({ fileName, contentType: CONTENT_TYPE }),
73 });
74 if (res.status === 401 || res.status === 403) {
75 throw new Error('upload not authorized — check the API key');
76 }
77 if (!res.ok) {
78 let message = `HTTP ${res.status}`;
79 try {
80 const err = (await res.json()) as { message?: string };
81 if (err.message) message = err.message;
82 } catch {
83 // keep the status-only message
84 }
85 throw new Error(`upload-url request failed: ${message}`);
86 }
87 const grant = (await res.json()) as {
88 uploadUrl: string;
89 uploadHeaders?: Record<string, string>;
90 downloadUrl: string;
91 };
92 const put = await fetch(grant.uploadUrl, {
93 method: 'PUT',
94 headers: grant.uploadHeaders ?? { 'Content-Type': CONTENT_TYPE },
95 body: bytes as unknown as BodyInit,
96 });
97 if (!put.ok) throw new Error(`upload PUT failed: HTTP ${put.status}`);
98 return grant.downloadUrl;
99}