fruit-fly-simulation / src /data-loader.js
Xenova's picture
Xenova HF Staff
Upload 227 files
cc38b25 verified
Raw History Blame Contribute Delete
5.68 kB
// The page supplies the public asset root to its bundled worker. This keeps
// runtime downloads correct in development and under any production subpath.
let assetBase;
export function configureAssetBase(url) {
assetBase = new URL(url).href;
}
export function assetURL(path) {
if (!assetBase) throw Error('Asset base has not been configured');
return new URL(path, assetBase).href;
}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const transient = new Set([408, 429, 500, 502, 503, 504]);
/** Retry the same file, honoring server cooldowns. Downloaded chunks survive retries/reloads. */
export async function requestBytes(
url,
{ fetcher = fetch, wait = sleep, notice = () => {}, attempts = 6 } = {},
) {
url = assetURL(url);
for (let attempt = 0; attempt < attempts; attempt++) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 45000);
let delay = 0;
try {
const response = await fetcher(url, { signal: controller.signal });
if (response.ok) return await response.arrayBuffer();
const retryAfter = response.headers.get('Retry-After');
const seconds = retryAfter === null ? NaN : Number(retryAfter);
delay = Number.isFinite(seconds)
? seconds * 1000
: Math.max(0, Date.parse(retryAfter) - Date.now()) || 0;
await response.body?.cancel();
if (!transient.has(response.status))
throw Object.assign(
Error(`Data download failed (HTTP ${response.status}). Please try again later.`),
{ permanent: true },
);
if (attempt === attempts - 1)
throw Object.assign(
Error(
'The data server is still busy. Try loading again shortly; saved chunks will be reused.',
),
{ permanent: true },
);
} catch (error) {
if (error.permanent) throw error;
if (attempt === attempts - 1)
throw Error(
'The download was interrupted. Check your connection and try again; saved chunks will be reused.',
);
} finally {
clearTimeout(timer);
}
delay = Math.max(delay, Math.min(30000, 1000 * 2 ** attempt));
// Heartbeats keep the initialization watchdog informed during Retry-After waits.
while (delay > 0) {
notice(
`Download paused · retrying in ${Math.ceil(delay / 1000)}s. Completed files are kept.`,
);
const step = Math.min(delay, 10000);
await wait(step);
delay -= step;
}
}
}
export async function loadGraph(progress = () => {}, notice = () => {}) {
const manifestBytes = await requestBytes('./data/manifest.json', { notice });
const manifest = JSON.parse(new TextDecoder().decode(manifestBytes));
let cache;
try {
cache = await globalThis.caches?.open('malecns-verified-data-v1');
} catch {
/* Private browsing or storage restrictions: proceed without persistent cache. */
}
const manifestHash = await digest(manifestBytes);
let lastRequest = 0;
const pacedFetch = async (...args) => {
await sleep(Math.max(0, 250 - (Date.now() - lastRequest)));
lastRequest = Date.now();
return fetch(...args);
};
async function unpack(file, hash) {
const url = './data/' + file,
key = new URL(assetURL(url));
key.searchParams.set('content', hash ?? manifestHash);
let bytes;
try {
const saved = await cache?.match(key.href);
if (saved) bytes = await saved.arrayBuffer();
} catch {
/* Cache is optional. */
}
if (bytes && hash && (await digest(bytes)) !== hash) {
await cache?.delete(key.href).catch(() => {});
bytes = null;
}
if (!bytes) {
bytes = await requestBytes(url, { notice, fetcher: pacedFetch });
if (hash && (await digest(bytes)) !== hash)
throw Error('A downloaded data file failed its checksum. Please retry loading.');
}
let unpacked;
try {
unpacked = await new Response(
new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip')),
).arrayBuffer();
} catch {
try {
await cache?.delete(key.href);
} catch {}
throw Error('A data file could not be decompressed. Retry loading to download it again.');
}
try {
if (!(await cache?.match(key.href))) await cache?.put(key.href, new Response(bytes));
} catch {
/* Storage is optional. */
}
return unpacked;
}
notice('Loading saved files and downloading remaining connectivity…');
const neurons = JSON.parse(new TextDecoder().decode(await unpack(manifest.metadata)));
const graph = {
n: manifest.neurons,
neurons,
manifest,
sign: Int32Array.from(neurons, (row) => row[5]),
};
let done = 0;
const total = manifest.arrays.reduce((sum, array) => sum + array.parts.length, 0);
for (const array of manifest.arrays) {
const values = new Uint32Array(array.length);
let offset = 0;
for (const part of array.parts) {
const chunk = new Uint32Array(await unpack(part.file, part.sha256));
values.set(chunk, offset);
offset += chunk.length;
progress(++done / total);
}
if (offset !== values.length) throw Error('Invalid data length for ' + array.name);
graph[array.name] = values;
}
if (
graph.offsets.length !== graph.n + 1 ||
graph.offsets[graph.n] !== graph.sources.length ||
graph.counts.length !== graph.sources.length
)
throw Error('Invalid CSR structure');
return graph;
}
async function digest(bytes) {
return Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)), (x) =>
x.toString(16).padStart(2, '0'),
).join('');
}