Files
clawhdf5/examples/wasm-viewer/viewer-lib.js
T
osobhandClaude Opus 5.5 5107583b97 wasm: openUrl reads remote files by HTTP range requests (range-read M4)
openUrl(url, opts) returns a RemoteFile with the methods of H5File
(kind, list, info, attrs, attrErrors, read, readHyperslab), each a
promise, and stats(). It runs every call through the restartable
LazyStorage: a pass that misses reports the byte ranges, js/remote.js
fetches them with fetch() and Range headers (six at a time), and the
pass is re-run. This keeps the main thread free without a Worker or
synchronous XHR (h5wasm's lazy files need both), as the design doc
recommends; the cost is re-running a pass per wave of misses.

Every answer is checked: a 206 with exactly the bytes asked for, and
the same ETag/Last-Modified and length as at open, else an error (never
data). A server that ignores Range (200) is downloaded whole, up to
maxDownload (512 MiB), unless fallback: "error". Options: blockSize,
cacheSize, headers, credentials, parallel, fetch.

test/serve.py is a range-capable static server with request counting
(and /norange/ for a server without range support). test.mjs repeats
every fixture check on files opened by URL (1 MiB and 512 B blocks),
checks the request budget on a 200 MB h5py file (list, three small
reads and a window of the big dataset: 5 requests, 6 MiB), the
download fallback, and HTTP errors, changed files and wrong answers;
with CLAWHDF5_WASM_CORPUS every corpus file is compared with open(bytes).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 06:44:45 -05:00

99 lines
3.4 KiB
JavaScript

// DOM-free helpers for the viewer, so Node can test them (test/test.mjs).
/** Child path of `name` in the group at `parent`. */
export function joinPath(parent, name) {
return parent === "/" ? `/${name}` : `${parent}/${name}`;
}
/** One value as display text. */
export function formatValue(v) {
if (v === null || v === undefined) return "";
if (typeof v === "bigint") return v.toString();
if (typeof v === "number") {
if (Number.isInteger(v)) return String(v);
return String(Number(v.toPrecision(7)));
}
if (typeof v === "string") return JSON.stringify(v);
if (ArrayBuffer.isView(v) || Array.isArray(v)) {
const items = Array.from(v.slice(0, 16), formatValue);
if (v.length > 16) items.push(`… (${v.length} values)`);
return `[${items.join(", ")}]`;
}
return String(v);
}
/**
* The window of a dataset to show: a hyperslab over its `shape` with the
* last dimension as columns, the one before as rows, and any leading
* dimensions held at `fixed` indices. `row`/`col` are the window's top-left
* corner. Returns null for a scalar (read it whole).
*/
export function viewWindow(shape, { row = 0, col = 0, rows = 50, cols = 12, fixed = [] } = {}) {
const rank = shape.length;
if (rank === 0) return null;
const clamp = (x, n) => Math.max(0, Math.min(x, Math.max(0, n - 1)));
if (rank === 1) {
const r0 = clamp(row, shape[0]);
const n = Math.max(0, Math.min(rows, shape[0] - r0));
return { start: [r0], count: [n], rows: n, cols: 1, row: r0, col: 0 };
}
const lead = shape.slice(0, rank - 2).map((n, i) => clamp(fixed[i] ?? 0, n));
const nr = shape[rank - 2];
const nc = shape[rank - 1];
const r0 = clamp(row, nr);
const c0 = clamp(col, nc);
const r = Math.max(0, Math.min(rows, nr - r0));
const c = Math.max(0, Math.min(cols, nc - c0));
return {
start: [...lead, r0, c0],
count: [...lead.map(() => 1), r, c],
rows: r,
cols: c,
row: r0,
col: c0,
};
}
/**
* Split row-major `data` into `rows` x `cols` cells of display text; each
* cell holds `per` consecutive values (the elements of an array datatype).
*/
export function toRows(data, rows, cols, per = 1) {
const out = [];
for (let r = 0; r < rows; r++) {
const row = [];
for (let c = 0; c < cols; c++) {
const i = (r * cols + c) * per;
row.push(per === 1 ? formatValue(data[i]) : formatValue(data.slice(i, i + per)));
}
out.push(row);
}
return out;
}
/** Number of values an array datatype packs into each element. */
export function perElement(elementShape) {
return elementShape.reduce((a, b) => a * b, 1);
}
/** A byte count for people: `1.5 KiB`, `200 MiB`. */
export function formatBytes(n) {
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
let i = 0;
let v = n;
while (v >= 1024 && i < units.length - 1) {
v /= 1024;
i++;
}
const s = i === 0 ? String(v) : v < 10 ? String(Number(v.toFixed(1))) : String(Math.round(v));
return `${s} ${units[i]}`;
}
/** What reading a remote file has cost, from `RemoteFile.stats()`. */
export function formatStats({ lazy, requests, bytesFetched, size }) {
if (!lazy) return `downloaded whole (${formatBytes(size)}): the server does not support range requests`;
const pct = size > 0 ? (100 * bytesFetched) / size : 0;
return `${requests} request${requests === 1 ? "" : "s"}, ${formatBytes(bytesFetched)} of ` +
`${formatBytes(size)} fetched (${pct.toFixed(1)}%)`;
}