mostlyright 1.5.1 → 1.6.0

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.
@@ -63,6 +63,13 @@ var mostlyright = (() => {
63
63
  }
64
64
  });
65
65
 
66
+ // ../core/src/schemas/generated/forecast_nwp.v1.ts
67
+ var init_forecast_nwp_v1 = __esm({
68
+ "../core/src/schemas/generated/forecast_nwp.v1.ts"() {
69
+ "use strict";
70
+ }
71
+ });
72
+
66
73
  // ../core/src/schemas/generated/index.ts
67
74
  var init_generated = __esm({
68
75
  "../core/src/schemas/generated/index.ts"() {
@@ -73,6 +80,7 @@ var mostlyright = (() => {
73
80
  init_settlement_cli_v1();
74
81
  init_observation_ledger_v1();
75
82
  init_observation_qc_v1();
83
+ init_forecast_nwp_v1();
76
84
  }
77
85
  });
78
86
 
@@ -2917,6 +2925,11 @@ var mostlyright = (() => {
2917
2925
  Number.parseInt(parts[2], 10)
2918
2926
  ];
2919
2927
  }
2928
+ function addOneDay(iso) {
2929
+ const [y, m, d] = splitIso(iso);
2930
+ const next = new Date(Date.UTC(y, m - 1, d + 1));
2931
+ return next.toISOString().slice(0, 10);
2932
+ }
2920
2933
  async function downloadIemAsos(stationCode, start, end, opts = {}) {
2921
2934
  const reportType = opts.reportType ?? 3;
2922
2935
  if (!VALID_REPORT_TYPES.has(reportType)) {
@@ -2926,10 +2939,9 @@ var mostlyright = (() => {
2926
2939
  if (start > end) {
2927
2940
  return [];
2928
2941
  }
2929
- const normalizedStart = `${start.slice(0, 4)}-01-01`;
2930
- const chunks = yearlyChunksExclusiveEnd(normalizedStart, end);
2942
+ const chunks = opts.exactStart ? [[start, addOneDay(end)]] : yearlyChunksExclusiveEnd(`${start.slice(0, 4)}-01-01`, end);
2931
2943
  const politenessMs = opts.politenessMs ?? IEM_POLITE_DELAY_MS;
2932
- const { reportType: _rtDrop, politenessMs: _pmDrop, ...fetchOpts } = opts;
2944
+ const { reportType: _rtDrop, politenessMs: _pmDrop, exactStart: _esDrop, ...fetchOpts } = opts;
2933
2945
  const out = [];
2934
2946
  for (const [chunkStart, chunkEnd] of chunks) {
2935
2947
  const url = buildIemUrl(stationCode, chunkStart, chunkEnd, reportType);
@@ -4075,6 +4087,26 @@ var mostlyright = (() => {
4075
4087
  var SUPPORTED_MODELS = /* @__PURE__ */ new Set(["nbe", "gfs", "lav", "met", "ecm"]);
4076
4088
  var KT_TO_MS2 = 0.5144444;
4077
4089
  var NBE_CYCLE_CUTOVER = Date.UTC(2026, 5 - 1, 5, 0, 0, 0);
4090
+ var MOS_FETCH_CONCURRENCY = 8;
4091
+ async function mapWithConcurrency(items, limit, fn) {
4092
+ const results = new Array(items.length);
4093
+ let cursor = 0;
4094
+ let failed = false;
4095
+ async function worker() {
4096
+ while (cursor < items.length && !failed) {
4097
+ const index = cursor++;
4098
+ try {
4099
+ results[index] = await fn(items[index], index);
4100
+ } catch (err) {
4101
+ failed = true;
4102
+ throw err;
4103
+ }
4104
+ }
4105
+ }
4106
+ const poolSize = Math.min(limit, items.length);
4107
+ await Promise.all(Array.from({ length: poolSize }, () => worker()));
4108
+ return results;
4109
+ }
4078
4110
  function runtimeHoursFor(model, fromDt, toDt) {
4079
4111
  if (model !== "nbe") return [0, 6, 12, 18];
4080
4112
  const fromMs = fromDt.getTime();
@@ -4158,31 +4190,37 @@ var mostlyright = (() => {
4158
4190
  const toDt = parseIsoDate2(toDate, true);
4159
4191
  const hours = runtimeHoursFor(model, fromDt, toDt);
4160
4192
  const retrievedAt = (/* @__PURE__ */ new Date()).toISOString();
4161
- const rows = [];
4162
4193
  const dayMs = 864e5;
4194
+ const urls = [];
4163
4195
  for (let day = fromDt.getTime(); day <= toDt.getTime(); day += dayMs) {
4164
4196
  for (const h of hours) {
4165
4197
  const rt = new Date(day);
4166
4198
  rt.setUTCHours(h, 0, 0, 0);
4167
4199
  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
- }
4200
+ urls.push(
4201
+ `${IEM_MOS_URL}?station=${encodeURIComponent(
4202
+ station
4203
+ )}&model=${encodeURIComponent(model.toUpperCase())}&runtime=${encodeURIComponent(
4204
+ rt.toISOString()
4205
+ )}`
4206
+ );
4183
4207
  }
4184
4208
  }
4185
- return rows;
4209
+ const perCycle = await mapWithConcurrency(urls, MOS_FETCH_CONCURRENCY, async (url) => {
4210
+ const resp = await fetchFn(url);
4211
+ if (resp.status === 404) return [];
4212
+ if (!resp.ok) {
4213
+ throw new Error(`iemMosForecasts: HTTP ${resp.status} on ${url}`);
4214
+ }
4215
+ const payload = await resp.json();
4216
+ const out = [];
4217
+ for (const raw of payload.data ?? []) {
4218
+ const projected = parseRow(raw, station, model, retrievedAt);
4219
+ if (projected !== null) out.push(projected);
4220
+ }
4221
+ return out;
4222
+ });
4223
+ return perCycle.flat();
4186
4224
  }
4187
4225
 
4188
4226
  // ../weather/src/forecasts/nwp-stub.ts
@@ -4461,6 +4499,15 @@ var mostlyright = (() => {
4461
4499
  const n = typeof value === "number" ? value : Number(value);
4462
4500
  return Number.isFinite(n) ? n : null;
4463
4501
  }
4502
+ function maybeInt(value) {
4503
+ const n = maybeNumber2(value);
4504
+ if (n === null) return null;
4505
+ const floor = Math.floor(n);
4506
+ const diff = n - floor;
4507
+ if (diff < 0.5) return floor;
4508
+ if (diff > 0.5) return floor + 1;
4509
+ return floor % 2 === 0 ? floor : floor + 1;
4510
+ }
4464
4511
  function pickHourlyValue(hourly, key, isPreviousRuns, idx) {
4465
4512
  const arr = isPreviousRuns ? hourly[`${key}_previous_day1`] ?? hourly[key] : hourly[key];
4466
4513
  if (!Array.isArray(arr) || idx >= arr.length) return null;
@@ -4556,20 +4603,20 @@ var mostlyright = (() => {
4556
4603
  dewPointC: maybeNumber2(pickHourlyValue(h, "dew_point_2m", isPrev, i)),
4557
4604
  apparentTempC: maybeNumber2(pickHourlyValue(h, "apparent_temperature", isPrev, i)),
4558
4605
  windSpeedMs: maybeNumber2(pickHourlyValue(h, "wind_speed_10m", isPrev, i)),
4559
- windDirDeg: maybeNumber2(pickHourlyValue(h, "wind_direction_10m", isPrev, i)),
4606
+ windDirDeg: maybeInt(pickHourlyValue(h, "wind_direction_10m", isPrev, i)),
4560
4607
  windGustsMs: maybeNumber2(pickHourlyValue(h, "wind_gusts_10m", isPrev, i)),
4561
4608
  precipProbability: popPct === null ? null : popPct / 100,
4562
4609
  precipitationMm: maybeNumber2(pickHourlyValue(h, "precipitation", isPrev, i)),
4563
- cloudCoverPct: maybeNumber2(pickHourlyValue(h, "cloud_cover", isPrev, i)),
4610
+ cloudCoverPct: maybeInt(pickHourlyValue(h, "cloud_cover", isPrev, i)),
4564
4611
  surfacePressureHpa: maybeNumber2(pickHourlyValue(h, "surface_pressure", isPrev, i)),
4565
4612
  pressureMslHpa: maybeNumber2(pickHourlyValue(h, "pressure_msl", isPrev, i)),
4566
4613
  shortwaveRadiationWm2: maybeNumber2(pickHourlyValue(h, "shortwave_radiation", isPrev, i)),
4567
4614
  directRadiationWm2: maybeNumber2(pickHourlyValue(h, "direct_radiation", isPrev, i)),
4568
4615
  capeJkg: maybeNumber2(pickHourlyValue(h, "cape", isPrev, i)),
4569
- freezingLevelM: maybeNumber2(pickHourlyValue(h, "freezing_level_height", isPrev, i)),
4616
+ freezingLevelM: maybeInt(pickHourlyValue(h, "freezing_level_height", isPrev, i)),
4570
4617
  snowDepthM: maybeNumber2(pickHourlyValue(h, "snow_depth", isPrev, i)),
4571
- visibilityM: maybeNumber2(pickHourlyValue(h, "visibility", isPrev, i)),
4572
- weatherCode: maybeNumber2(pickHourlyValue(h, "weather_code", isPrev, i)),
4618
+ visibilityM: maybeInt(pickHourlyValue(h, "visibility", isPrev, i)),
4619
+ weatherCode: maybeInt(pickHourlyValue(h, "weather_code", isPrev, i)),
4573
4620
  source,
4574
4621
  retrievedAt
4575
4622
  });
@@ -6496,7 +6543,8 @@ var mostlyright = (() => {
6496
6543
  if (resolvedStrategy === "exact_window") {
6497
6544
  const chunks = await downloadIemAsos(station, fromDate, toDate, {
6498
6545
  reportType: 3,
6499
- politenessMs: 1e3
6546
+ politenessMs: 1e3,
6547
+ exactStart: true
6500
6548
  });
6501
6549
  for (const chunk of chunks) {
6502
6550
  const rows = parseIemCsv(chunk.csv, { observationTypeOverride: "METAR" });