style(wind): adopt automatically discovered runtime formatting

This commit is contained in:
Bilawal Sidhu committed 2026-09-15 10:54:09 -05:00
1 parent a5dad07cc4
commit 9ae79cf34a
13 files changed
+421 -97

No files matched your search

+7 -2
View File
@@ -7,9 +7,14 @@ export function gfsObjectKey({ date, hour, forecastHour = 0 }) {
}
/** Select the latest GFS cycle that should be available. */
export function selectLatestGfsCycle(nowMs, { availabilityLagMs = 5 * 3600_000 } = {}) {
export function selectLatestGfsCycle(
nowMs,
{ availabilityLagMs = 5 * 3600_000 } = {},
) {
const d = new Date(nowMs - availabilityLagMs);
const date = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
const date = new Date(
Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()),
);
const hour = Math.floor(d.getUTCHours() / 6) * 6;
date.setUTCHours(hour);
return { date: date.toISOString().slice(0, 10).replaceAll('-', ''), hour };
+19 -8
View File
@@ -1,17 +1,20 @@
/** Lazy shared ecCodes WASM instance, created only for an active wind request. */
let eccodesPromise = null;
function loadEccodes() {
eccodesPromise ??= import('@meri-imperiumi/eccodes-wasm').then(module => module.createEccodes()).catch(error => {
eccodesPromise = null;
throw error;
});
eccodesPromise ??= import('@meri-imperiumi/eccodes-wasm')
.then((module) => module.createEccodes())
.catch((error) => {
eccodesPromise = null;
throw error;
});
return eccodesPromise;
}
/** 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();
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';
@@ -21,9 +24,17 @@ export async function decodeWindGribMessage(buffer, { eccodesModule } = {}) {
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');
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, nj,
ni,
nj,
lo1: handle.getDouble('longitudeOfFirstGridPointInDegrees'),
la1: handle.getDouble('latitudeOfFirstGridPointInDegrees'),
di: handle.getDouble('iDirectionIncrementInDegrees'),
+53 -9
View File
@@ -41,7 +41,10 @@ export function parseGfsIdx(text) {
* @param {{level?: string}} [options]
* @returns {{u: {start: number, end: number}, v: {start: number, end: number}}}
*/
export function windMessageRanges(parsed, { level = '10 m above ground' } = {}) {
export function windMessageRanges(
parsed,
{ level = '10 m above ground' } = {},
) {
const range = (variable) => {
const index = parsed.messages.findIndex(
(message) => message.variable === variable && message.level === level,
@@ -57,7 +60,10 @@ export function windMessageRanges(parsed, { level = '10 m above ground' } = {})
/** Fetch a bounded text response as a Buffer. */
export async function fetchText({ url, fetchImpl = fetch, signal }) {
const response = await fetchImpl(url, { signal, redirect: 'error' });
if (!response.ok) { await response.body?.cancel(); throw new Error('Wind upstream unavailable'); }
if (!response.ok) {
await response.body?.cancel();
throw new Error('Wind upstream unavailable');
}
return Buffer.from(await readWindBody(response, 2 * 1024 * 1024, signal));
}
@@ -73,7 +79,13 @@ export async function fetchRange({
signal,
maxBytes = 8 * 1024 * 1024,
}) {
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start || end - start + 1 > maxBytes)
if (
!Number.isSafeInteger(start) ||
!Number.isSafeInteger(end) ||
start < 0 ||
end < start ||
end - start + 1 > maxBytes
)
throw new Error('range too large');
const response = await fetchImpl(url, {
signal,
@@ -82,19 +94,35 @@ export async function fetchRange({
});
const validStatus =
response.status === 206 || (response.status === 200 && start === 0);
if (!validStatus || !response.ok) { await response.body?.cancel(); throw new Error('Wind upstream unavailable'); }
const buffer = Buffer.from(await readWindBody(response, end - start + 1, signal));
if (!validStatus || !response.ok) {
await response.body?.cancel();
throw new Error('Wind upstream unavailable');
}
const buffer = Buffer.from(
await readWindBody(response, end - start + 1, signal),
);
if (buffer.length !== end - start + 1)
throw new Error('invalid range length');
return buffer;
}
import { GFS_BUCKET, gfsObjectKey, selectLatestGfsCycle, nearestGfsStep } from './catalog.js';
import {
GFS_BUCKET,
gfsObjectKey,
selectLatestGfsCycle,
nearestGfsStep,
} from './catalog.js';
import { decodeWindGribMessage } from './decode.js';
import { resampleWindGrid } from './grid.js';
/** Fetch and decode the GFS 10 m wind field valid closest to now. */
export async function fetchGfsWind({ fetchImpl = fetch, now = () => Date.now(), targetDx = 1, decodeImpl = decodeWindGribMessage, signal } = {}) {
export async function fetchGfsWind({
fetchImpl = fetch,
now = () => Date.now(),
targetDx = 1,
decodeImpl = decodeWindGribMessage,
signal,
} = {}) {
const nowMs = now();
const cycle = selectLatestGfsCycle(nowMs);
const runMs = Date.UTC(
@@ -114,8 +142,24 @@ export async function fetchGfsWind({ fetchImpl = fetch, now = () => Date.now(),
fetchRange({ url: base, ...ranges.v, fetchImpl, signal }),
]);
const [u, v] = await Promise.all([decodeImpl(uBuffer), decodeImpl(vBuffer)]);
const grid = resampleWindGrid({ u: u.values, v: v.values, ni: u.ni, nj: u.nj, lo1: u.lo1, la1: u.la1, di: u.di, dj: u.dj, dx: targetDx, dy: targetDx });
const grid = resampleWindGrid({
u: u.values,
v: v.values,
ni: u.ni,
nj: u.nj,
lo1: u.lo1,
la1: u.la1,
di: u.di,
dj: u.dj,
dx: targetDx,
dy: targetDx,
});
const runIso = new Date(runMs).toISOString();
const validIso = new Date(runMs + forecastHour * 3600_000).toISOString();
return { cycle: { ...cycle, forecastHour, runIso, validIso }, level: '10 m above ground', units: 'm/s', grid };
return {
cycle: { ...cycle, forecastHour, runIso, validIso },
level: '10 m above ground',
units: 'm/s',
grid,
};
}
+16 -1
View File
@@ -21,7 +21,22 @@ export function resampleWindGrid({
dx = 1,
dy = 1,
}) {
if (!Number.isInteger(ni) || !Number.isInteger(nj) || ni < 1 || nj < 1 || ni * nj > 2_000_000 || u?.length !== ni * nj || v?.length !== ni * nj || ![lo1, la1, di, dj, dx, dy].every(Number.isFinite) || di <= 0 || dj <= 0 || dx < 0.25 || dy < 0.25 || dx > 180 || dy > 180)
if (
!Number.isInteger(ni) ||
!Number.isInteger(nj) ||
ni < 1 ||
nj < 1 ||
ni * nj > 2_000_000 ||
u?.length !== ni * nj ||
v?.length !== ni * nj ||
![lo1, la1, di, dj, dx, dy].every(Number.isFinite) ||
di <= 0 ||
dj <= 0 ||
dx < 0.25 ||
dy < 0.25 ||
dx > 180 ||
dy > 180
)
throw new Error('Invalid wind grid geometry');
const nx = Math.round(360 / dx);
const ny = Math.round(180 / dy) + 1;
+2 -1
View File
@@ -89,7 +89,8 @@ export function ifsWindRanges(entries) {
export function nearestIfsStep(hours, cycleHour) {
const max = cycleHour === 6 || cycleHour === 18 ? 144 : 360;
const value = Math.max(0, Number.isFinite(hours) ? hours : 0);
const step = value <= 144 ? Math.round(value / 3) * 3 : Math.round(value / 6) * 6;
const step =
value <= 144 ? Math.round(value / 3) * 3 : Math.round(value / 6) * 6;
return Math.max(0, Math.min(max, step));
}