fix(wind): decode in bounded virtual storage and test real canvas retention

This commit is contained in:
Bilawal Sidhu committed 2026-09-15 02:30:29 -05:00
1 parent 26354d5ae1
commit 430dcd4c55
8 files changed
+89 -47

No files matched your search

+19 -41
View File
@@ -1,51 +1,29 @@
import os from 'node:os';
import path from 'node:path';
import { promises as fs } from 'node:fs';
/**
* Lazily created ecCodes WASM instance. The `@meri-imperiumi/eccodes-wasm`
* module is only imported on first use so unit tests can inject a fake
* decoder without loading the WASM (which requires Node >= 24).
* @type {?Promise<object>}
*/
/** Lazy shared ecCodes WASM instance, created only for an active wind request. */
let eccodesPromise = null;
/** @returns {Promise<object>} The shared ecCodes WASM instance. */
function loadEccodes() {
eccodesPromise ??= (async () => {
const module = await import('@meri-imperiumi/eccodes-wasm');
return module.createEccodes();
})();
eccodesPromise ??= import('@meri-imperiumi/eccodes-wasm').then(module => module.createEccodes()).catch(error => {
eccodesPromise = null;
throw error;
});
return eccodesPromise;
}
/**
* Decode one GRIB2 wind message into its grid metadata and values.
*
* The WASM decoder reads from its own mounted filesystem, so the message is
* written to a temp file under `mountDir` and removed in `finally`.
*
* @param {Buffer} buffer - One GRIB2 message (already byte-range extracted).
* @param {{ eccodesModule?: object, mountDir?: string }} [options]
* @returns {Promise<{ni: number, nj: number, lo1: number, la1: number,
* di: number, dj: number, values: ArrayLike<number>, shortName: string,
* level: number, units: string}>}
*/
export async function decodeWindGribMessage(
buffer,
{ eccodesModule, mountDir = os.tmpdir() } = {},
) {
const eccodes = eccodesModule ?? (await loadEccodes());
const directory = await fs.mkdtemp(path.join(mountDir, 'gev-wind-'));
const file = path.join(directory, 'wind.grib2');
await fs.writeFile(file, buffer, { mode: 0o600 });
/** Decode one bounded message in WASM memory without mounting the host filesystem. */
export async function decodeWindGribMessage(buffer, { eccodesModule } = {}) {
if (buffer.byteLength > 8 * 1024 * 1024) throw new Error('Wind GRIB exceeds byte budget');
const eccodes = eccodesModule ?? await loadEccodes();
// Everything after this await is synchronous. A shared virtual file is safe
// across U/V and model requests and its storage is released after each decode.
const file = '/gev-wind.grib2';
let handle = null;
try {
eccodes.mountFilesystem(directory);
eccodes.writeFile(file, buffer);
handle = eccodes.openGrib(file);
const ni = handle.getLong('Ni');
const nj = handle.getLong('Nj');
if (!Number.isInteger(ni) || !Number.isInteger(nj) || ni < 1 || nj < 1 || ni * nj > 2_000_000) throw new Error('Invalid wind GRIB geometry');
return {
ni: handle.getLong('Ni'),
nj: handle.getLong('Nj'),
ni, nj,
lo1: handle.getDouble('longitudeOfFirstGridPointInDegrees'),
la1: handle.getDouble('latitudeOfFirstGridPointInDegrees'),
di: handle.getDouble('iDirectionIncrementInDegrees'),
@@ -56,7 +34,7 @@ export async function decodeWindGribMessage(
units: handle.getString('units'),
};
} finally {
if (handle) handle.delete();
await fs.rm(directory, { recursive: true, force: true }).catch(() => {});
handle?.delete();
eccodes.writeFile(file, new Uint8Array(0));
}
}