mostlyright 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2917,6 +2917,11 @@ var mostlyright = (() => {
2917
2917
  Number.parseInt(parts[2], 10)
2918
2918
  ];
2919
2919
  }
2920
+ function addOneDay(iso) {
2921
+ const [y, m, d] = splitIso(iso);
2922
+ const next = new Date(Date.UTC(y, m - 1, d + 1));
2923
+ return next.toISOString().slice(0, 10);
2924
+ }
2920
2925
  async function downloadIemAsos(stationCode, start, end, opts = {}) {
2921
2926
  const reportType = opts.reportType ?? 3;
2922
2927
  if (!VALID_REPORT_TYPES.has(reportType)) {
@@ -2926,10 +2931,9 @@ var mostlyright = (() => {
2926
2931
  if (start > end) {
2927
2932
  return [];
2928
2933
  }
2929
- const normalizedStart = `${start.slice(0, 4)}-01-01`;
2930
- const chunks = yearlyChunksExclusiveEnd(normalizedStart, end);
2934
+ const chunks = opts.exactStart ? [[start, addOneDay(end)]] : yearlyChunksExclusiveEnd(`${start.slice(0, 4)}-01-01`, end);
2931
2935
  const politenessMs = opts.politenessMs ?? IEM_POLITE_DELAY_MS;
2932
- const { reportType: _rtDrop, politenessMs: _pmDrop, ...fetchOpts } = opts;
2936
+ const { reportType: _rtDrop, politenessMs: _pmDrop, exactStart: _esDrop, ...fetchOpts } = opts;
2933
2937
  const out = [];
2934
2938
  for (const [chunkStart, chunkEnd] of chunks) {
2935
2939
  const url = buildIemUrl(stationCode, chunkStart, chunkEnd, reportType);
@@ -4075,6 +4079,26 @@ var mostlyright = (() => {
4075
4079
  var SUPPORTED_MODELS = /* @__PURE__ */ new Set(["nbe", "gfs", "lav", "met", "ecm"]);
4076
4080
  var KT_TO_MS2 = 0.5144444;
4077
4081
  var NBE_CYCLE_CUTOVER = Date.UTC(2026, 5 - 1, 5, 0, 0, 0);
4082
+ var MOS_FETCH_CONCURRENCY = 8;
4083
+ async function mapWithConcurrency(items, limit, fn) {
4084
+ const results = new Array(items.length);
4085
+ let cursor = 0;
4086
+ let failed = false;
4087
+ async function worker() {
4088
+ while (cursor < items.length && !failed) {
4089
+ const index = cursor++;
4090
+ try {
4091
+ results[index] = await fn(items[index], index);
4092
+ } catch (err) {
4093
+ failed = true;
4094
+ throw err;
4095
+ }
4096
+ }
4097
+ }
4098
+ const poolSize = Math.min(limit, items.length);
4099
+ await Promise.all(Array.from({ length: poolSize }, () => worker()));
4100
+ return results;
4101
+ }
4078
4102
  function runtimeHoursFor(model, fromDt, toDt) {
4079
4103
  if (model !== "nbe") return [0, 6, 12, 18];
4080
4104
  const fromMs = fromDt.getTime();
@@ -4158,31 +4182,37 @@ var mostlyright = (() => {
4158
4182
  const toDt = parseIsoDate2(toDate, true);
4159
4183
  const hours = runtimeHoursFor(model, fromDt, toDt);
4160
4184
  const retrievedAt = (/* @__PURE__ */ new Date()).toISOString();
4161
- const rows = [];
4162
4185
  const dayMs = 864e5;
4186
+ const urls = [];
4163
4187
  for (let day = fromDt.getTime(); day <= toDt.getTime(); day += dayMs) {
4164
4188
  for (const h of hours) {
4165
4189
  const rt = new Date(day);
4166
4190
  rt.setUTCHours(h, 0, 0, 0);
4167
4191
  if (rt < fromDt || rt > toDt) continue;
4168
- const url = `${IEM_MOS_URL}?station=${encodeURIComponent(
4169
- station
4170
- )}&model=${encodeURIComponent(model.toUpperCase())}&runtime=${encodeURIComponent(
4171
- rt.toISOString()
4172
- )}`;
4173
- const resp = await fetchFn(url);
4174
- if (resp.status === 404) continue;
4175
- if (!resp.ok) {
4176
- throw new Error(`iemMosForecasts: HTTP ${resp.status} on ${url}`);
4177
- }
4178
- const payload = await resp.json();
4179
- for (const raw of payload.data ?? []) {
4180
- const projected = parseRow(raw, station, model, retrievedAt);
4181
- if (projected !== null) rows.push(projected);
4182
- }
4192
+ urls.push(
4193
+ `${IEM_MOS_URL}?station=${encodeURIComponent(
4194
+ station
4195
+ )}&model=${encodeURIComponent(model.toUpperCase())}&runtime=${encodeURIComponent(
4196
+ rt.toISOString()
4197
+ )}`
4198
+ );
4183
4199
  }
4184
4200
  }
4185
- return rows;
4201
+ const perCycle = await mapWithConcurrency(urls, MOS_FETCH_CONCURRENCY, async (url) => {
4202
+ const resp = await fetchFn(url);
4203
+ if (resp.status === 404) return [];
4204
+ if (!resp.ok) {
4205
+ throw new Error(`iemMosForecasts: HTTP ${resp.status} on ${url}`);
4206
+ }
4207
+ const payload = await resp.json();
4208
+ const out = [];
4209
+ for (const raw of payload.data ?? []) {
4210
+ const projected = parseRow(raw, station, model, retrievedAt);
4211
+ if (projected !== null) out.push(projected);
4212
+ }
4213
+ return out;
4214
+ });
4215
+ return perCycle.flat();
4186
4216
  }
4187
4217
 
4188
4218
  // ../weather/src/forecasts/nwp-stub.ts
@@ -4461,6 +4491,15 @@ var mostlyright = (() => {
4461
4491
  const n = typeof value === "number" ? value : Number(value);
4462
4492
  return Number.isFinite(n) ? n : null;
4463
4493
  }
4494
+ function maybeInt(value) {
4495
+ const n = maybeNumber2(value);
4496
+ if (n === null) return null;
4497
+ const floor = Math.floor(n);
4498
+ const diff = n - floor;
4499
+ if (diff < 0.5) return floor;
4500
+ if (diff > 0.5) return floor + 1;
4501
+ return floor % 2 === 0 ? floor : floor + 1;
4502
+ }
4464
4503
  function pickHourlyValue(hourly, key, isPreviousRuns, idx) {
4465
4504
  const arr = isPreviousRuns ? hourly[`${key}_previous_day1`] ?? hourly[key] : hourly[key];
4466
4505
  if (!Array.isArray(arr) || idx >= arr.length) return null;
@@ -4488,13 +4527,16 @@ var mostlyright = (() => {
4488
4527
  const params = new URLSearchParams();
4489
4528
  params.set("latitude", String(lat));
4490
4529
  params.set("longitude", String(lon));
4491
- params.set("start_date", fromDate);
4492
- params.set("end_date", toDate);
4493
4530
  params.set("hourly", buildHourlyParam(endpoint));
4494
4531
  params.set("models", model);
4495
4532
  params.set("timezone", "UTC");
4496
- if (endpoint === OPEN_METEO_SINGLE_RUNS_URL && opts.issuedAt) {
4497
- params.set("run", opts.issuedAt);
4533
+ if (endpoint === OPEN_METEO_SINGLE_RUNS_URL) {
4534
+ if (opts.issuedAt) {
4535
+ params.set("run", opts.issuedAt);
4536
+ }
4537
+ } else {
4538
+ params.set("start_date", fromDate);
4539
+ params.set("end_date", toDate);
4498
4540
  }
4499
4541
  const fetchFn = opts.fetchFn ?? fetch;
4500
4542
  const url = `${endpoint}?${params.toString()}`;
@@ -4553,24 +4595,32 @@ var mostlyright = (() => {
4553
4595
  dewPointC: maybeNumber2(pickHourlyValue(h, "dew_point_2m", isPrev, i)),
4554
4596
  apparentTempC: maybeNumber2(pickHourlyValue(h, "apparent_temperature", isPrev, i)),
4555
4597
  windSpeedMs: maybeNumber2(pickHourlyValue(h, "wind_speed_10m", isPrev, i)),
4556
- windDirDeg: maybeNumber2(pickHourlyValue(h, "wind_direction_10m", isPrev, i)),
4598
+ windDirDeg: maybeInt(pickHourlyValue(h, "wind_direction_10m", isPrev, i)),
4557
4599
  windGustsMs: maybeNumber2(pickHourlyValue(h, "wind_gusts_10m", isPrev, i)),
4558
4600
  precipProbability: popPct === null ? null : popPct / 100,
4559
4601
  precipitationMm: maybeNumber2(pickHourlyValue(h, "precipitation", isPrev, i)),
4560
- cloudCoverPct: maybeNumber2(pickHourlyValue(h, "cloud_cover", isPrev, i)),
4602
+ cloudCoverPct: maybeInt(pickHourlyValue(h, "cloud_cover", isPrev, i)),
4561
4603
  surfacePressureHpa: maybeNumber2(pickHourlyValue(h, "surface_pressure", isPrev, i)),
4562
4604
  pressureMslHpa: maybeNumber2(pickHourlyValue(h, "pressure_msl", isPrev, i)),
4563
4605
  shortwaveRadiationWm2: maybeNumber2(pickHourlyValue(h, "shortwave_radiation", isPrev, i)),
4564
4606
  directRadiationWm2: maybeNumber2(pickHourlyValue(h, "direct_radiation", isPrev, i)),
4565
4607
  capeJkg: maybeNumber2(pickHourlyValue(h, "cape", isPrev, i)),
4566
- freezingLevelM: maybeNumber2(pickHourlyValue(h, "freezing_level_height", isPrev, i)),
4608
+ freezingLevelM: maybeInt(pickHourlyValue(h, "freezing_level_height", isPrev, i)),
4567
4609
  snowDepthM: maybeNumber2(pickHourlyValue(h, "snow_depth", isPrev, i)),
4568
- visibilityM: maybeNumber2(pickHourlyValue(h, "visibility", isPrev, i)),
4569
- weatherCode: maybeNumber2(pickHourlyValue(h, "weather_code", isPrev, i)),
4610
+ visibilityM: maybeInt(pickHourlyValue(h, "visibility", isPrev, i)),
4611
+ weatherCode: maybeInt(pickHourlyValue(h, "weather_code", isPrev, i)),
4570
4612
  source,
4571
4613
  retrievedAt
4572
4614
  });
4573
4615
  }
4616
+ if (source === "open_meteo.single_run" && rows.length > 0) {
4617
+ const loMs = Date.parse(`${fromDate}T00:00:00Z`);
4618
+ const hiMs = Date.parse(`${toDate}T00:00:00Z`) + 864e5;
4619
+ return rows.filter((r) => {
4620
+ const v = Date.parse(r.validAt);
4621
+ return v >= loMs && v < hiMs;
4622
+ });
4623
+ }
4574
4624
  return rows;
4575
4625
  }
4576
4626
 
@@ -6485,7 +6535,8 @@ var mostlyright = (() => {
6485
6535
  if (resolvedStrategy === "exact_window") {
6486
6536
  const chunks = await downloadIemAsos(station, fromDate, toDate, {
6487
6537
  reportType: 3,
6488
- politenessMs: 1e3
6538
+ politenessMs: 1e3,
6539
+ exactStart: true
6489
6540
  });
6490
6541
  for (const chunk of chunks) {
6491
6542
  const rows = parseIemCsv(chunk.csv, { observationTypeOverride: "METAR" });