buoydata 0.1.4 → 0.2.1

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.
package/README.md CHANGED
@@ -74,6 +74,18 @@ Builds the realtime2 URL for a buoy and file type.
74
74
  buildRealtimeUrl(buoyId: string, type?: string, baseUrl?: string): string
75
75
  ```
76
76
 
77
+ ### fetchBuoyList and fetchStationIndex
78
+
79
+ Retrieve station IDs using the NDBC active station XML feed (no longer 404-prone). You can optionally include inactive stations from the station catalog, and `fetchStationIndex` gives you an `isActive` helper without needing to reparse data.
80
+
81
+ ```ts
82
+ const active = await fetchBuoyList(); // active IDs only
83
+ const all = await fetchBuoyList({ includeInactive: true }); // full catalog
84
+
85
+ const index = await fetchStationIndex();
86
+ index.isActive('46026'); // true/false
87
+ ```
88
+
77
89
  ### parseRealtimeData
78
90
 
79
91
  Parses a realtime2 text file into typed `Measurement` objects. Standard fields are mapped into structured measurement fields. Unknown columns are ignored unless `includeUnknownFields` is enabled.
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class p{constructor(e,r){this.maxSize=e,this.ttlMs=r,this.items=new Map}get(e){const r=this.items.get(e);if(!r)return;const n=Date.now();if(r.expiresAt<=n){this.items.delete(e);return}return this.items.delete(e),this.items.set(e,r),r.value}set(e,r){const n=Date.now();this.items.has(e)&&this.items.delete(e),this.items.set(e,{value:r,expiresAt:n+this.ttlMs}),this.prune(n)}prune(e){for(const[r,n]of this.items)n.expiresAt>e||this.items.delete(r);for(;this.items.size>this.maxSize;){const r=this.items.keys().next().value;if(r===void 0)break;this.items.delete(r)}}}function w(t={}){const e=new URLSearchParams;for(const[n,s]of Object.entries(t))if(s!=null){if(Array.isArray(s)){s.forEach(i=>{e.append(n,String(i))});continue}e.append(n,String(s))}const r=e.toString();return r?`?${r}`:""}function g(t,e="",r={}){const n=t.endsWith("/")?t:`${t}/`,s=new URL(e,n),i=w(r);return s.search=i,s.toString()}const T="https://www.ndbc.noaa.gov/data/realtime2/",A=5*60*1e3,M=256,h=new p(M,A);function S(t,e="txt",r=T){const s=`${t.toUpperCase()}.${e}`;return g(r,s)}async function I(t){const{buoyId:e,type:r="txt",fetch:n=fetch,requestInit:s,baseUrl:i=T}=t,a=e.toUpperCase();if(!n)throw new Error("No fetch implementation available.");const o=`${i}|${a}|${r}`,c=h.get(o);if(c!==void 0)return c;const u=S(a,r,i),m=await n(u,s);if(!m.ok)throw new Error(`Failed to fetch ${u}: ${m.status}`);const l=await m.text();return h.set(o,l),l}const P="https://www.ndbc.noaa.gov/activestations.txt",_=4*60*60*1e3,R=4,b=new p(R,_);function U(t){const e=new Set;return t.split(/\r?\n/).map(r=>r.trim()).filter(r=>r.length>0).forEach(r=>{if(r.startsWith("#"))return;const[n]=r.split(/\s+/);if(!n)return;const s=n.toUpperCase();s==="STATION"||s==="STN"||/^[A-Z0-9]{3,10}$/.test(s)&&e.add(s)}),Array.from(e)}async function v(t={}){const{fetch:e=fetch,requestInit:r,url:n=P}=t;if(!e)throw new Error("No fetch implementation available.");const s=b.get(n);if(s!==void 0)return s;const i=await e(n,r);if(!i.ok)throw new Error(`Failed to fetch ${n}: ${i.status}`);const a=await i.text(),o=U(a);return b.set(n,o),o}const C=["MM"];function x(t,e){return!!(e.includes(t)||/^9{2,}(\.0+|\.9+)?$/.test(t))}function $(t,e){if(x(t,e.missingTokens))return e.missingValue;if(e.coerceNumbers){const r=Number(t);if(!Number.isNaN(r))return r}return t}function d(t,e={}){const r={coerceNumbers:e.coerceNumbers??!0,missingValue:e.missingValue??null,missingTokens:e.missingTokens??C};return t.trim().split(/\s+/).filter(Boolean).map(n=>$(n,r))}function k(t,e){return t.startsWith(`${e} `)}function B(t){return t.split(/\r?\n/).map(e=>e.trim()).filter(e=>e.length>0)}function y(t,e={}){const r=e.commentPrefix??"#",n=B(t).filter(N=>!k(N,r));if(n.length===0)return{headers:[],units:[],rows:[],rawRows:[]};const s=n[0]??"",i={coerceNumbers:!1,missingValue:null,missingTokens:[]},a=d(s,i).map(String);let o=[],c=1;const u=n[1];u&&u.startsWith(r)&&(o=d(u,i).map(N=>{const f=String(N);return f.startsWith(r)?f.slice(r.length):f}),c=2);const m={coerceNumbers:e.coerceNumbers,missingValue:e.missingValue,missingTokens:e.missingTokens},l=n.slice(c),L=l.map(N=>d(N,m));return{headers:a,units:o,rows:L,rawRows:l}}function D(t){const{headers:e,rows:r}=t;return r.map(n=>{const s={};return e.forEach((i,a)=>{s[i]=n[a]??null}),s})}function E(){return{airTemperature:Number.NaN,day:Number.NaN,dewpointTemperature:Number.NaN,hour:Number.NaN,minute:Number.NaN,month:Number.NaN,pressureTendancy:Number.NaN,seaLevelPressure:Number.NaN,stationVisibility:Number.NaN,water:{averagePeriod:Number.NaN,dominantDirection:Number.NaN,dominantPeriod:Number.NaN,significantHeight:Number.NaN,surfaceTemperature:Number.NaN,tide:Number.NaN},wind:{averageSpeed:Number.NaN,direction:Number.NaN,peakGustSpeed:Number.NaN},year:Number.NaN}}const V={"#YY":(t,e)=>{t.year=Number(e)},YY:(t,e)=>{t.year=Number(e)},MM:(t,e)=>{t.month=Number(e)},DD:(t,e)=>{t.day=Number(e)},hh:(t,e)=>{t.hour=Number(e)},mm:(t,e)=>{t.minute=Number(e)},APD:(t,e)=>{t.water.averagePeriod=Number(e)},ATMP:(t,e)=>{t.airTemperature=Number(e)},DEWP:(t,e)=>{t.dewpointTemperature=Number(e)},DPD:(t,e)=>{t.water.dominantPeriod=Number(e)},GST:(t,e)=>{t.wind.peakGustSpeed=Number(e)},MWD:(t,e)=>{t.water.dominantDirection=Number(e)},PRES:(t,e)=>{t.seaLevelPressure=Number(e)},PTDY:(t,e)=>{t.pressureTendancy=Number(e)},TIDE:(t,e)=>{t.water.tide=Number(e)},VIS:(t,e)=>{t.stationVisibility=Number(e)},WDIR:(t,e)=>{t.wind.direction=Number(e)},WSPD:(t,e)=>{t.wind.averageSpeed=Number(e)},WTMP:(t,e)=>{t.water.surfaceTemperature=Number(e)},WVHT:(t,e)=>{t.water.significantHeight=Number(e)}};function W(t){const e=E();return Object.entries(t).forEach(([r,n])=>{const s=V[r];s&&s(e,n)}),e}function O(t,e,r={}){const n=y(e,{...r,missingValue:r.missingValue??Number.NaN}),s=D(n),i=s.map(a=>W(a));if(r.includeUnknownFields){const a=i.map((o,c)=>{const u=s[c];return{...o,...u}});return{id:t,measurements:a}}return{id:t,measurements:i}}function z(t){const{year:e,month:r,day:n,hour:s,minute:i}=t;return new Date(Date.UTC(e,r-1,n,s,i))}exports.buildRealtimeUrl=S;exports.buildURL=g;exports.createMeasurement=E;exports.fetchBuoyList=v;exports.fetchRealtimeData=I;exports.formatQueryParams=w;exports.getMeasurementDate=z;exports.objectifyTable=D;exports.parseRealtimeData=O;exports.parseRealtimeTable=y;exports.parseRow=d;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class h{constructor(e,n){this.maxSize=e,this.ttlMs=n,this.items=new Map}get(e){const n=this.items.get(e);if(!n)return;const r=Date.now();if(n.expiresAt<=r){this.items.delete(e);return}return this.items.delete(e),this.items.set(e,n),n.value}set(e,n){const r=Date.now();this.items.has(e)&&this.items.delete(e),this.items.set(e,{value:n,expiresAt:r+this.ttlMs}),this.prune(r)}prune(e){for(const[n,r]of this.items)r.expiresAt>e||this.items.delete(n);for(;this.items.size>this.maxSize;){const n=this.items.keys().next().value;if(n===void 0)break;this.items.delete(n)}}}function T(t={}){const e=new URLSearchParams;for(const[r,s]of Object.entries(t))if(s!=null){if(Array.isArray(s)){s.forEach(i=>{e.append(r,String(i))});continue}e.append(r,String(s))}const n=e.toString();return n?`?${n}`:""}function g(t,e="",n={}){const r=t.endsWith("/")?t:`${t}/`,s=new URL(e,r),i=T(n);return s.search=i,s.toString()}const A="https://www.ndbc.noaa.gov/data/realtime2/",C=5*60*1e3,L=1024,b=new h(L,C);function S(t,e="txt",n=A){const s=`${t.toUpperCase()}.${e}`;return g(n,s)}async function D(t){const{buoyId:e,type:n="txt",fetch:r=fetch,requestInit:s,baseUrl:i=A}=t,a=e.toUpperCase();if(!r)throw new Error("No fetch implementation available.");const c=`${i}|${a}|${n}`,u=b.get(c);if(u!==void 0)return u;const o=S(a,n,i),m=await r(o,s);if(!m.ok)throw new Error(`Failed to fetch ${o}: ${m.status}`);const d=await m.text();return b.set(c,d),d}const M="https://www.ndbc.noaa.gov/activestations.xml",x="https://www.ndbc.noaa.gov/data/stations/station_table.txt",P=10*60*1e3,R=8,U=12*60*60*1e3,V=8,p=new h(R,P),w=new h(V,U);function $(t){const e=[],n=new Set,r=/<station[^>]*\bid="([A-Za-z0-9]{3,10})"/g;let s;for(;(s=r.exec(t))!==null;){const i=s[1];if(!i)continue;const a=i.toUpperCase();n.has(a)||(n.add(a),e.push(a))}return{ids:e,set:n}}function k(t){const e=[];return t.split(/\r?\n/).forEach(n=>{if(!n||n.startsWith("#"))return;const r=n.indexOf("|"),i=(r===-1?n:n.slice(0,r)).trim().toUpperCase();/^[A-Z0-9]{3,10}$/.test(i)&&e.push(i)}),e}async function B(t,e,n){const r=p.get(e);if(r!==void 0)return r;const s=await t(e,n);if(!s.ok)throw new Error(`Failed to fetch ${e}: ${s.status}`);const i=await s.text(),a=$(i);return p.set(e,a),a}async function H(t,e,n){const r=w.get(e);if(r!==void 0)return r;const s=await t(e,n);if(!s.ok)throw new Error(`Failed to fetch ${e}: ${s.status}`);const i=await s.text(),a=k(i);return w.set(e,a),a}async function E(t={}){const{fetch:e=fetch,requestInit:n,activeUrl:r=M,stationTableUrl:s=x,includeInactive:i}=t;if(!e)throw new Error("No fetch implementation available.");const a=await B(e,r,n),c=i?await H(e,s,n):a.ids,u=a.set;return{active:a.ids,all:c,isActive:o=>u.has(o.toUpperCase())}}async function O(t={}){const e=await E(t);return t.includeInactive?Array.from(e.all):Array.from(e.active)}const W=["MM"];function z(t,e){return!!(e.includes(t)||/^9{2,}(\.0+|\.9+)?$/.test(t))}function F(t,e){if(z(t,e.missingTokens))return e.missingValue;if(e.coerceNumbers){const n=Number(t);if(!Number.isNaN(n))return n}return t}function N(t,e={}){const n={coerceNumbers:e.coerceNumbers??!0,missingValue:e.missingValue??null,missingTokens:e.missingTokens??W};return t.trim().split(/\s+/).filter(Boolean).map(r=>F(r,n))}function j(t,e){return t.startsWith(`${e} `)}function G(t){return t.split(/\r?\n/).map(e=>e.trim()).filter(e=>e.length>0)}function y(t,e={}){const n=e.commentPrefix??"#",r=G(t).filter(l=>!j(l,n));if(r.length===0)return{headers:[],units:[],rows:[],rawRows:[]};const s=r[0]??"",i={coerceNumbers:!1,missingValue:null,missingTokens:[]},a=N(s,i).map(String);let c=[],u=1;const o=r[1];o&&o.startsWith(n)&&(c=N(o,i).map(l=>{const f=String(l);return f.startsWith(n)?f.slice(n.length):f}),u=2);const m={coerceNumbers:e.coerceNumbers,missingValue:e.missingValue,missingTokens:e.missingTokens},d=r.slice(u),v=d.map(l=>N(l,m));return{headers:a,units:c,rows:v,rawRows:d}}function I(t){const{headers:e,rows:n}=t;return n.map(r=>{const s={};return e.forEach((i,a)=>{s[i]=r[a]??null}),s})}function _(){return{airTemperature:Number.NaN,day:Number.NaN,dewpointTemperature:Number.NaN,hour:Number.NaN,minute:Number.NaN,month:Number.NaN,pressureTendancy:Number.NaN,seaLevelPressure:Number.NaN,stationVisibility:Number.NaN,water:{averagePeriod:Number.NaN,dominantDirection:Number.NaN,dominantPeriod:Number.NaN,significantHeight:Number.NaN,surfaceTemperature:Number.NaN,tide:Number.NaN},wind:{averageSpeed:Number.NaN,direction:Number.NaN,peakGustSpeed:Number.NaN},year:Number.NaN}}const Y={"#YY":(t,e)=>{t.year=Number(e)},YY:(t,e)=>{t.year=Number(e)},MM:(t,e)=>{t.month=Number(e)},DD:(t,e)=>{t.day=Number(e)},hh:(t,e)=>{t.hour=Number(e)},mm:(t,e)=>{t.minute=Number(e)},APD:(t,e)=>{t.water.averagePeriod=Number(e)},ATMP:(t,e)=>{t.airTemperature=Number(e)},DEWP:(t,e)=>{t.dewpointTemperature=Number(e)},DPD:(t,e)=>{t.water.dominantPeriod=Number(e)},GST:(t,e)=>{t.wind.peakGustSpeed=Number(e)},MWD:(t,e)=>{t.water.dominantDirection=Number(e)},PRES:(t,e)=>{t.seaLevelPressure=Number(e)},PTDY:(t,e)=>{t.pressureTendancy=Number(e)},TIDE:(t,e)=>{t.water.tide=Number(e)},VIS:(t,e)=>{t.stationVisibility=Number(e)},WDIR:(t,e)=>{t.wind.direction=Number(e)},WSPD:(t,e)=>{t.wind.averageSpeed=Number(e)},WTMP:(t,e)=>{t.water.surfaceTemperature=Number(e)},WVHT:(t,e)=>{t.water.significantHeight=Number(e)}};function Z(t){const e=_();return Object.entries(t).forEach(([n,r])=>{const s=Y[n];s&&s(e,r)}),e}function q(t,e,n={}){const r=y(e,{...n,missingValue:n.missingValue??Number.NaN}),s=I(r),i=s.map(a=>Z(a));if(n.includeUnknownFields){const a=i.map((c,u)=>{const o=s[u];return{...c,...o}});return{id:t,measurements:a}}return{id:t,measurements:i}}function X(t){const{year:e,month:n,day:r,hour:s,minute:i}=t;return new Date(Date.UTC(e,n-1,r,s,i))}exports.buildRealtimeUrl=S;exports.buildURL=g;exports.createMeasurement=_;exports.fetchBuoyList=O;exports.fetchRealtimeData=D;exports.fetchStationIndex=E;exports.formatQueryParams=T;exports.getMeasurementDate=X;exports.objectifyTable=I;exports.parseRealtimeData=q;exports.parseRealtimeTable=y;exports.parseRow=N;
2
2
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":["../src/utils/lru.ts","../src/utils/url.ts","../src/realtime/fetch.ts","../src/stations/list.ts","../src/realtime/parser.ts","../src/utils/date.ts"],"sourcesContent":["export class LruCache<K, V> {\n private readonly items = new Map<K, { value: V; expiresAt: number }>();\n\n constructor(\n private readonly maxSize: number,\n private readonly ttlMs: number,\n ) {}\n\n get(key: K): V | undefined {\n const entry = this.items.get(key);\n if (!entry) {\n return undefined;\n }\n\n const now = Date.now();\n if (entry.expiresAt <= now) {\n this.items.delete(key);\n return undefined;\n }\n\n this.items.delete(key);\n this.items.set(key, entry);\n return entry.value;\n }\n\n set(key: K, value: V): void {\n const now = Date.now();\n if (this.items.has(key)) {\n this.items.delete(key);\n }\n\n this.items.set(key, { value, expiresAt: now + this.ttlMs });\n this.prune(now);\n }\n\n private prune(now: number): void {\n for (const [key, entry] of this.items) {\n if (entry.expiresAt > now) {\n continue;\n }\n this.items.delete(key);\n }\n\n while (this.items.size > this.maxSize) {\n const oldestKey = this.items.keys().next().value as K | undefined;\n if (oldestKey === undefined) {\n break;\n }\n this.items.delete(oldestKey);\n }\n }\n}\n","export type QueryParamValue =\n | string\n | number\n | boolean\n | null\n | undefined\n | Array<string | number | boolean>;\n\nexport type QueryParams = Record<string, QueryParamValue>;\n\nexport function formatQueryParams(params: QueryParams = {}): string {\n const searchParams = new URLSearchParams();\n\n for (const [key, value] of Object.entries(params)) {\n if (value === null || value === undefined) {\n continue;\n }\n\n if (Array.isArray(value)) {\n value.forEach(item => {\n searchParams.append(key, String(item));\n });\n continue;\n }\n\n searchParams.append(key, String(value));\n }\n\n const query = searchParams.toString();\n return query ? `?${query}` : '';\n}\n\nexport function buildURL(\n base: string,\n path = '',\n params: QueryParams = {},\n): string {\n const normalizedBase = base.endsWith('/') ? base : `${base}/`;\n const url = new URL(path, normalizedBase);\n const query = formatQueryParams(params);\n url.search = query;\n return url.toString();\n}\n","import { LruCache } from '../utils/lru';\nimport { buildURL } from '../utils/url';\n\nexport interface FetchRealtimeOptions {\n buoyId: string;\n type?: string;\n fetch?: typeof fetch;\n requestInit?: RequestInit;\n baseUrl?: string;\n}\n\nconst DEFAULT_BASE_URL = 'https://www.ndbc.noaa.gov/data/realtime2/';\nconst CACHE_TTL_MS = 5 * 60 * 1000;\nconst CACHE_MAX_SIZE = 256;\nconst REALTIME_CACHE = new LruCache<string, string>(\n CACHE_MAX_SIZE,\n CACHE_TTL_MS,\n);\n\nexport function buildRealtimeUrl(\n buoyId: string,\n type = 'txt',\n baseUrl = DEFAULT_BASE_URL,\n): string {\n const normalizedBuoyId = buoyId.toUpperCase();\n const filename = `${normalizedBuoyId}.${type}`;\n return buildURL(baseUrl, filename);\n}\n\nexport async function fetchRealtimeData(\n options: FetchRealtimeOptions,\n): Promise<string> {\n const {\n buoyId,\n type = 'txt',\n fetch: fetchImpl = fetch,\n requestInit,\n baseUrl = DEFAULT_BASE_URL,\n } = options;\n const normalizedBuoyId = buoyId.toUpperCase();\n\n if (!fetchImpl) {\n throw new Error('No fetch implementation available.');\n }\n\n const cacheKey = `${baseUrl}|${normalizedBuoyId}|${type}`;\n const cached = REALTIME_CACHE.get(cacheKey);\n if (cached !== undefined) {\n return cached;\n }\n\n const url = buildRealtimeUrl(normalizedBuoyId, type, baseUrl);\n const response = await fetchImpl(url, requestInit);\n\n if (!response.ok) {\n throw new Error(`Failed to fetch ${url}: ${response.status}`);\n }\n\n const body = await response.text();\n REALTIME_CACHE.set(cacheKey, body);\n return body;\n}\n","import { LruCache } from '../utils/lru';\n\nexport interface FetchBuoyListOptions {\n fetch?: typeof fetch;\n requestInit?: RequestInit;\n url?: string;\n}\n\nconst DEFAULT_BUOY_LIST_URL = 'https://www.ndbc.noaa.gov/activestations.txt';\nconst BUOY_LIST_CACHE_TTL_MS = 4 * 60 * 60 * 1000;\nconst BUOY_LIST_CACHE_MAX_SIZE = 4;\nconst BUOY_LIST_CACHE = new LruCache<string, string[]>(\n BUOY_LIST_CACHE_MAX_SIZE,\n BUOY_LIST_CACHE_TTL_MS,\n);\n\nfunction parseBuoyList(rawText: string): string[] {\n const ids = new Set<string>();\n\n rawText\n .split(/\\r?\\n/)\n .map(line => line.trim())\n .filter(line => line.length > 0)\n .forEach(line => {\n if (line.startsWith('#')) {\n return;\n }\n\n const [token] = line.split(/\\s+/);\n if (!token) {\n return;\n }\n\n const normalized = token.toUpperCase();\n if (normalized === 'STATION' || normalized === 'STN') {\n return;\n }\n\n if (!/^[A-Z0-9]{3,10}$/.test(normalized)) {\n return;\n }\n\n ids.add(normalized);\n });\n\n return Array.from(ids);\n}\n\nexport async function fetchBuoyList(\n options: FetchBuoyListOptions = {},\n): Promise<string[]> {\n const { fetch: fetchImpl = fetch, requestInit, url = DEFAULT_BUOY_LIST_URL } =\n options;\n\n if (!fetchImpl) {\n throw new Error('No fetch implementation available.');\n }\n\n const cached = BUOY_LIST_CACHE.get(url);\n if (cached !== undefined) {\n return cached;\n }\n\n const response = await fetchImpl(url, requestInit);\n if (!response.ok) {\n throw new Error(`Failed to fetch ${url}: ${response.status}`);\n }\n\n const body = await response.text();\n const list = parseBuoyList(body);\n BUOY_LIST_CACHE.set(url, list);\n return list;\n}\n","import { BuoyData, Measurement } from '../models/measurement';\nimport { ParsedValue, RealtimeRecord, RealtimeTable } from '../models/table';\n\nexport interface ParseRowOptions {\n coerceNumbers?: boolean;\n missingValue?: number | null;\n missingTokens?: string[];\n}\n\nexport interface ParseRealtimeTableOptions extends ParseRowOptions {\n commentPrefix?: string;\n}\n\nexport interface ParseRealtimeDataOptions extends ParseRealtimeTableOptions {\n includeUnknownFields?: boolean;\n}\n\nconst DEFAULT_MISSING_TOKENS = ['MM'];\n\nfunction isMissingToken(value: string, missingTokens: string[]): boolean {\n if (missingTokens.includes(value)) {\n return true;\n }\n\n // NDBC missing values are often 9s (e.g. 99, 999, 9999, 99.0).\n if (/^9{2,}(\\.0+|\\.9+)?$/.test(value)) {\n return true;\n }\n\n return false;\n}\n\nfunction coerceValue(\n raw: string,\n options: Required<ParseRowOptions>,\n): ParsedValue {\n if (isMissingToken(raw, options.missingTokens)) {\n return options.missingValue;\n }\n\n if (options.coerceNumbers) {\n const numeric = Number(raw);\n if (!Number.isNaN(numeric)) {\n return numeric;\n }\n }\n\n return raw;\n}\n\nexport function parseRow(\n rawRow: string,\n options: ParseRowOptions = {},\n): ParsedValue[] {\n const resolved: Required<ParseRowOptions> = {\n coerceNumbers: options.coerceNumbers ?? true,\n missingValue: options.missingValue ?? null,\n missingTokens: options.missingTokens ?? DEFAULT_MISSING_TOKENS,\n };\n\n return rawRow\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n .map(value => coerceValue(value, resolved));\n}\n\nfunction isCommentLine(line: string, commentPrefix: string): boolean {\n return line.startsWith(`${commentPrefix} `);\n}\n\nfunction normalizeLines(rawText: string): string[] {\n return rawText\n .split(/\\r?\\n/)\n .map(line => line.trim())\n .filter(line => line.length > 0);\n}\n\nexport function parseRealtimeTable(\n rawText: string,\n options: ParseRealtimeTableOptions = {},\n): RealtimeTable {\n const commentPrefix = options.commentPrefix ?? '#';\n const lines = normalizeLines(rawText).filter(\n line => !isCommentLine(line, commentPrefix),\n );\n\n if (lines.length === 0) {\n return { headers: [], units: [], rows: [], rawRows: [] };\n }\n\n const headerLine = lines[0] ?? '';\n const headerOptions: ParseRowOptions = {\n coerceNumbers: false,\n missingValue: null,\n missingTokens: [],\n };\n const headers = parseRow(headerLine, headerOptions).map(String);\n\n let units: string[] = [];\n let dataStartIndex = 1;\n\n const unitLine = lines[1];\n if (unitLine && unitLine.startsWith(commentPrefix)) {\n units = parseRow(unitLine, headerOptions).map(token => {\n const text = String(token);\n return text.startsWith(commentPrefix)\n ? text.slice(commentPrefix.length)\n : text;\n });\n dataStartIndex = 2;\n }\n\n const rowOptions: ParseRowOptions = {\n coerceNumbers: options.coerceNumbers,\n missingValue: options.missingValue,\n missingTokens: options.missingTokens,\n };\n\n const dataRows = lines.slice(dataStartIndex);\n const rows = dataRows.map(row => parseRow(row, rowOptions));\n\n return {\n headers,\n units,\n rows,\n rawRows: dataRows,\n };\n}\n\nexport function objectifyTable(table: RealtimeTable): RealtimeRecord[] {\n const { headers, rows } = table;\n return rows.map(row => {\n const record: RealtimeRecord = {};\n headers.forEach((header, index) => {\n record[header] = row[index] ?? null;\n });\n return record;\n });\n}\n\nexport function createMeasurement(): Measurement {\n return {\n airTemperature: Number.NaN,\n day: Number.NaN,\n dewpointTemperature: Number.NaN,\n hour: Number.NaN,\n minute: Number.NaN,\n month: Number.NaN,\n pressureTendancy: Number.NaN,\n seaLevelPressure: Number.NaN,\n stationVisibility: Number.NaN,\n water: {\n averagePeriod: Number.NaN,\n dominantDirection: Number.NaN,\n dominantPeriod: Number.NaN,\n significantHeight: Number.NaN,\n surfaceTemperature: Number.NaN,\n tide: Number.NaN,\n },\n wind: {\n averageSpeed: Number.NaN,\n direction: Number.NaN,\n peakGustSpeed: Number.NaN,\n },\n year: Number.NaN,\n };\n}\n\nconst FIELD_MAPPINGS: Record<string, (m: Measurement, value: ParsedValue) => void> = {\n '#YY': (m, value) => {\n m.year = Number(value);\n },\n YY: (m, value) => {\n m.year = Number(value);\n },\n MM: (m, value) => {\n m.month = Number(value);\n },\n DD: (m, value) => {\n m.day = Number(value);\n },\n hh: (m, value) => {\n m.hour = Number(value);\n },\n mm: (m, value) => {\n m.minute = Number(value);\n },\n APD: (m, value) => {\n m.water.averagePeriod = Number(value);\n },\n ATMP: (m, value) => {\n m.airTemperature = Number(value);\n },\n DEWP: (m, value) => {\n m.dewpointTemperature = Number(value);\n },\n DPD: (m, value) => {\n m.water.dominantPeriod = Number(value);\n },\n GST: (m, value) => {\n m.wind.peakGustSpeed = Number(value);\n },\n MWD: (m, value) => {\n m.water.dominantDirection = Number(value);\n },\n PRES: (m, value) => {\n m.seaLevelPressure = Number(value);\n },\n PTDY: (m, value) => {\n m.pressureTendancy = Number(value);\n },\n TIDE: (m, value) => {\n m.water.tide = Number(value);\n },\n VIS: (m, value) => {\n m.stationVisibility = Number(value);\n },\n WDIR: (m, value) => {\n m.wind.direction = Number(value);\n },\n WSPD: (m, value) => {\n m.wind.averageSpeed = Number(value);\n },\n WTMP: (m, value) => {\n m.water.surfaceTemperature = Number(value);\n },\n WVHT: (m, value) => {\n m.water.significantHeight = Number(value);\n },\n};\n\nfunction toMeasurement(record: RealtimeRecord): Measurement {\n const measurement = createMeasurement();\n Object.entries(record).forEach(([field, value]) => {\n const mapper = FIELD_MAPPINGS[field];\n if (mapper) {\n mapper(measurement, value);\n }\n });\n return measurement;\n}\n\nexport function parseRealtimeData(\n buoyId: string,\n rawText: string,\n options: ParseRealtimeDataOptions = {},\n): BuoyData {\n const table = parseRealtimeTable(rawText, {\n ...options,\n missingValue: options.missingValue ?? Number.NaN,\n });\n const records = objectifyTable(table);\n\n const measurements = records.map(record => toMeasurement(record));\n\n if (options.includeUnknownFields) {\n const measurementsWithUnknowns = measurements.map((measurement, index) => {\n const record = records[index];\n return { ...measurement, ...record } as Measurement;\n });\n\n return {\n id: buoyId,\n measurements: measurementsWithUnknowns,\n };\n }\n\n return {\n id: buoyId,\n measurements,\n };\n}\n","import { Measurement } from '../models/measurement';\n\n/**\n * Returns a UTC Date instance for a buoy measurement.\n */\nexport function getMeasurementDate(measurement: Measurement): Date {\n const { year, month, day, hour, minute } = measurement;\n return new Date(Date.UTC(year, month - 1, day, hour, minute));\n}\n"],"names":["LruCache","maxSize","ttlMs","key","entry","now","value","oldestKey","formatQueryParams","params","searchParams","item","query","buildURL","base","path","normalizedBase","url","DEFAULT_BASE_URL","CACHE_TTL_MS","CACHE_MAX_SIZE","REALTIME_CACHE","buildRealtimeUrl","buoyId","type","baseUrl","filename","fetchRealtimeData","options","fetchImpl","requestInit","normalizedBuoyId","cacheKey","cached","response","body","DEFAULT_BUOY_LIST_URL","BUOY_LIST_CACHE_TTL_MS","BUOY_LIST_CACHE_MAX_SIZE","BUOY_LIST_CACHE","parseBuoyList","rawText","ids","line","token","normalized","fetchBuoyList","list","DEFAULT_MISSING_TOKENS","isMissingToken","missingTokens","coerceValue","raw","numeric","parseRow","rawRow","resolved","isCommentLine","commentPrefix","normalizeLines","parseRealtimeTable","lines","headerLine","headerOptions","headers","units","dataStartIndex","unitLine","text","rowOptions","dataRows","rows","row","objectifyTable","table","record","header","index","createMeasurement","FIELD_MAPPINGS","m","toMeasurement","measurement","field","mapper","parseRealtimeData","records","measurements","measurementsWithUnknowns","getMeasurementDate","year","month","day","hour","minute"],"mappings":"gFAAO,MAAMA,CAAe,CAG1B,YACmBC,EACAC,EACjB,CAFiB,KAAA,QAAAD,EACA,KAAA,MAAAC,EAJnB,KAAiB,UAAY,GAK1B,CAEH,IAAIC,EAAuB,CACzB,MAAMC,EAAQ,KAAK,MAAM,IAAID,CAAG,EAChC,GAAI,CAACC,EACH,OAGF,MAAMC,EAAM,KAAK,IAAA,EACjB,GAAID,EAAM,WAAaC,EAAK,CAC1B,KAAK,MAAM,OAAOF,CAAG,EACrB,MACF,CAEA,YAAK,MAAM,OAAOA,CAAG,EACrB,KAAK,MAAM,IAAIA,EAAKC,CAAK,EAClBA,EAAM,KACf,CAEA,IAAID,EAAQG,EAAgB,CAC1B,MAAMD,EAAM,KAAK,IAAA,EACb,KAAK,MAAM,IAAIF,CAAG,GACpB,KAAK,MAAM,OAAOA,CAAG,EAGvB,KAAK,MAAM,IAAIA,EAAK,CAAE,MAAAG,EAAO,UAAWD,EAAM,KAAK,MAAO,EAC1D,KAAK,MAAMA,CAAG,CAChB,CAEQ,MAAMA,EAAmB,CAC/B,SAAW,CAACF,EAAKC,CAAK,IAAK,KAAK,MAC1BA,EAAM,UAAYC,GAGtB,KAAK,MAAM,OAAOF,CAAG,EAGvB,KAAO,KAAK,MAAM,KAAO,KAAK,SAAS,CACrC,MAAMI,EAAY,KAAK,MAAM,KAAA,EAAO,OAAO,MAC3C,GAAIA,IAAc,OAChB,MAEF,KAAK,MAAM,OAAOA,CAAS,CAC7B,CACF,CACF,CCzCO,SAASC,EAAkBC,EAAsB,GAAY,CAClE,MAAMC,EAAe,IAAI,gBAEzB,SAAW,CAACP,EAAKG,CAAK,IAAK,OAAO,QAAQG,CAAM,EAC9C,GAAIH,GAAU,KAId,IAAI,MAAM,QAAQA,CAAK,EAAG,CACxBA,EAAM,QAAQK,GAAQ,CACpBD,EAAa,OAAOP,EAAK,OAAOQ,CAAI,CAAC,CACvC,CAAC,EACD,QACF,CAEAD,EAAa,OAAOP,EAAK,OAAOG,CAAK,CAAC,EAGxC,MAAMM,EAAQF,EAAa,SAAA,EAC3B,OAAOE,EAAQ,IAAIA,CAAK,GAAK,EAC/B,CAEO,SAASC,EACdC,EACAC,EAAO,GACPN,EAAsB,CAAA,EACd,CACR,MAAMO,EAAiBF,EAAK,SAAS,GAAG,EAAIA,EAAO,GAAGA,CAAI,IACpDG,EAAM,IAAI,IAAIF,EAAMC,CAAc,EAClCJ,EAAQJ,EAAkBC,CAAM,EACtC,OAAAQ,EAAI,OAASL,EACNK,EAAI,SAAA,CACb,CC/BA,MAAMC,EAAmB,4CACnBC,EAAe,EAAI,GAAK,IACxBC,EAAiB,IACjBC,EAAiB,IAAIrB,EACzBoB,EACAD,CACF,EAEO,SAASG,EACdC,EACAC,EAAO,MACPC,EAAUP,EACF,CAER,MAAMQ,EAAW,GADQH,EAAO,YAAA,CACI,IAAIC,CAAI,GAC5C,OAAOX,EAASY,EAASC,CAAQ,CACnC,CAEA,eAAsBC,EACpBC,EACiB,CACjB,KAAM,CACJ,OAAAL,EACA,KAAAC,EAAO,MACP,MAAOK,EAAY,MACnB,YAAAC,EACA,QAAAL,EAAUP,CAAA,EACRU,EACEG,EAAmBR,EAAO,YAAA,EAEhC,GAAI,CAACM,EACH,MAAM,IAAI,MAAM,oCAAoC,EAGtD,MAAMG,EAAW,GAAGP,CAAO,IAAIM,CAAgB,IAAIP,CAAI,GACjDS,EAASZ,EAAe,IAAIW,CAAQ,EAC1C,GAAIC,IAAW,OACb,OAAOA,EAGT,MAAMhB,EAAMK,EAAiBS,EAAkBP,EAAMC,CAAO,EACtDS,EAAW,MAAML,EAAUZ,EAAKa,CAAW,EAEjD,GAAI,CAACI,EAAS,GACZ,MAAM,IAAI,MAAM,mBAAmBjB,CAAG,KAAKiB,EAAS,MAAM,EAAE,EAG9D,MAAMC,EAAO,MAAMD,EAAS,KAAA,EAC5B,OAAAb,EAAe,IAAIW,EAAUG,CAAI,EAC1BA,CACT,CCrDA,MAAMC,EAAwB,+CACxBC,EAAyB,EAAI,GAAK,GAAK,IACvCC,EAA2B,EAC3BC,EAAkB,IAAIvC,EAC1BsC,EACAD,CACF,EAEA,SAASG,EAAcC,EAA2B,CAChD,MAAMC,MAAU,IAEhB,OAAAD,EACG,MAAM,OAAO,EACb,IAAIE,GAAQA,EAAK,KAAA,CAAM,EACvB,UAAeA,EAAK,OAAS,CAAC,EAC9B,QAAQA,GAAQ,CACf,GAAIA,EAAK,WAAW,GAAG,EACrB,OAGF,KAAM,CAACC,CAAK,EAAID,EAAK,MAAM,KAAK,EAChC,GAAI,CAACC,EACH,OAGF,MAAMC,EAAaD,EAAM,YAAA,EACrBC,IAAe,WAAaA,IAAe,OAI1C,mBAAmB,KAAKA,CAAU,GAIvCH,EAAI,IAAIG,CAAU,CACpB,CAAC,EAEI,MAAM,KAAKH,CAAG,CACvB,CAEA,eAAsBI,EACpBlB,EAAgC,GACb,CACnB,KAAM,CAAE,MAAOC,EAAY,MAAO,YAAAC,EAAa,IAAAb,EAAMmB,GACnDR,EAEF,GAAI,CAACC,EACH,MAAM,IAAI,MAAM,oCAAoC,EAGtD,MAAMI,EAASM,EAAgB,IAAItB,CAAG,EACtC,GAAIgB,IAAW,OACb,OAAOA,EAGT,MAAMC,EAAW,MAAML,EAAUZ,EAAKa,CAAW,EACjD,GAAI,CAACI,EAAS,GACZ,MAAM,IAAI,MAAM,mBAAmBjB,CAAG,KAAKiB,EAAS,MAAM,EAAE,EAG9D,MAAMC,EAAO,MAAMD,EAAS,KAAA,EACtBa,EAAOP,EAAcL,CAAI,EAC/B,OAAAI,EAAgB,IAAItB,EAAK8B,CAAI,EACtBA,CACT,CCvDA,MAAMC,EAAyB,CAAC,IAAI,EAEpC,SAASC,EAAe3C,EAAe4C,EAAkC,CAMvE,MALI,GAAAA,EAAc,SAAS5C,CAAK,GAK5B,sBAAsB,KAAKA,CAAK,EAKtC,CAEA,SAAS6C,EACPC,EACAxB,EACa,CACb,GAAIqB,EAAeG,EAAKxB,EAAQ,aAAa,EAC3C,OAAOA,EAAQ,aAGjB,GAAIA,EAAQ,cAAe,CACzB,MAAMyB,EAAU,OAAOD,CAAG,EAC1B,GAAI,CAAC,OAAO,MAAMC,CAAO,EACvB,OAAOA,CAEX,CAEA,OAAOD,CACT,CAEO,SAASE,EACdC,EACA3B,EAA2B,GACZ,CACf,MAAM4B,EAAsC,CAC1C,cAAe5B,EAAQ,eAAiB,GACxC,aAAcA,EAAQ,cAAgB,KACtC,cAAeA,EAAQ,eAAiBoB,CAAA,EAG1C,OAAOO,EACJ,KAAA,EACA,MAAM,KAAK,EACX,OAAO,OAAO,EACd,IAAIjD,GAAS6C,EAAY7C,EAAOkD,CAAQ,CAAC,CAC9C,CAEA,SAASC,EAAcd,EAAce,EAAgC,CACnE,OAAOf,EAAK,WAAW,GAAGe,CAAa,GAAG,CAC5C,CAEA,SAASC,EAAelB,EAA2B,CACjD,OAAOA,EACJ,MAAM,OAAO,EACb,IAAIE,GAAQA,EAAK,KAAA,CAAM,EACvB,OAAOA,GAAQA,EAAK,OAAS,CAAC,CACnC,CAEO,SAASiB,EACdnB,EACAb,EAAqC,GACtB,CACf,MAAM8B,EAAgB9B,EAAQ,eAAiB,IACzCiC,EAAQF,EAAelB,CAAO,EAAE,OACpCE,GAAQ,CAACc,EAAcd,EAAMe,CAAa,CAAA,EAG5C,GAAIG,EAAM,SAAW,EACnB,MAAO,CAAE,QAAS,CAAA,EAAI,MAAO,CAAA,EAAI,KAAM,CAAA,EAAI,QAAS,EAAC,EAGvD,MAAMC,EAAaD,EAAM,CAAC,GAAK,GACzBE,EAAiC,CACrC,cAAe,GACf,aAAc,KACd,cAAe,CAAA,CAAC,EAEZC,EAAUV,EAASQ,EAAYC,CAAa,EAAE,IAAI,MAAM,EAE9D,IAAIE,EAAkB,CAAA,EAClBC,EAAiB,EAErB,MAAMC,EAAWN,EAAM,CAAC,EACpBM,GAAYA,EAAS,WAAWT,CAAa,IAC/CO,EAAQX,EAASa,EAAUJ,CAAa,EAAE,IAAInB,GAAS,CACrD,MAAMwB,EAAO,OAAOxB,CAAK,EACzB,OAAOwB,EAAK,WAAWV,CAAa,EAChCU,EAAK,MAAMV,EAAc,MAAM,EAC/BU,CACN,CAAC,EACDF,EAAiB,GAGnB,MAAMG,EAA8B,CAClC,cAAezC,EAAQ,cACvB,aAAcA,EAAQ,aACtB,cAAeA,EAAQ,aAAA,EAGnB0C,EAAWT,EAAM,MAAMK,CAAc,EACrCK,EAAOD,EAAS,OAAWhB,EAASkB,EAAKH,CAAU,CAAC,EAE1D,MAAO,CACL,QAAAL,EACA,MAAAC,EACA,KAAAM,EACA,QAASD,CAAA,CAEb,CAEO,SAASG,EAAeC,EAAwC,CACrE,KAAM,CAAE,QAAAV,EAAS,KAAAO,CAAA,EAASG,EAC1B,OAAOH,EAAK,IAAIC,GAAO,CACrB,MAAMG,EAAyB,CAAA,EAC/B,OAAAX,EAAQ,QAAQ,CAACY,EAAQC,IAAU,CACjCF,EAAOC,CAAM,EAAIJ,EAAIK,CAAK,GAAK,IACjC,CAAC,EACMF,CACT,CAAC,CACH,CAEO,SAASG,GAAiC,CAC/C,MAAO,CACL,eAAgB,OAAO,IACvB,IAAK,OAAO,IACZ,oBAAqB,OAAO,IAC5B,KAAM,OAAO,IACb,OAAQ,OAAO,IACf,MAAO,OAAO,IACd,iBAAkB,OAAO,IACzB,iBAAkB,OAAO,IACzB,kBAAmB,OAAO,IAC1B,MAAO,CACL,cAAe,OAAO,IACtB,kBAAmB,OAAO,IAC1B,eAAgB,OAAO,IACvB,kBAAmB,OAAO,IAC1B,mBAAoB,OAAO,IAC3B,KAAM,OAAO,GAAA,EAEf,KAAM,CACJ,aAAc,OAAO,IACrB,UAAW,OAAO,IAClB,cAAe,OAAO,GAAA,EAExB,KAAM,OAAO,GAAA,CAEjB,CAEA,MAAMC,EAA+E,CACnF,MAAO,CAACC,EAAG1E,IAAU,CACnB0E,EAAE,KAAO,OAAO1E,CAAK,CACvB,EACA,GAAI,CAAC0E,EAAG1E,IAAU,CAChB0E,EAAE,KAAO,OAAO1E,CAAK,CACvB,EACA,GAAI,CAAC0E,EAAG1E,IAAU,CAChB0E,EAAE,MAAQ,OAAO1E,CAAK,CACxB,EACA,GAAI,CAAC0E,EAAG1E,IAAU,CAChB0E,EAAE,IAAM,OAAO1E,CAAK,CACtB,EACA,GAAI,CAAC0E,EAAG1E,IAAU,CAChB0E,EAAE,KAAO,OAAO1E,CAAK,CACvB,EACA,GAAI,CAAC0E,EAAG1E,IAAU,CAChB0E,EAAE,OAAS,OAAO1E,CAAK,CACzB,EACA,IAAK,CAAC0E,EAAG1E,IAAU,CACjB0E,EAAE,MAAM,cAAgB,OAAO1E,CAAK,CACtC,EACA,KAAM,CAAC0E,EAAG1E,IAAU,CAClB0E,EAAE,eAAiB,OAAO1E,CAAK,CACjC,EACA,KAAM,CAAC0E,EAAG1E,IAAU,CAClB0E,EAAE,oBAAsB,OAAO1E,CAAK,CACtC,EACA,IAAK,CAAC0E,EAAG1E,IAAU,CACjB0E,EAAE,MAAM,eAAiB,OAAO1E,CAAK,CACvC,EACA,IAAK,CAAC0E,EAAG1E,IAAU,CACjB0E,EAAE,KAAK,cAAgB,OAAO1E,CAAK,CACrC,EACA,IAAK,CAAC0E,EAAG1E,IAAU,CACjB0E,EAAE,MAAM,kBAAoB,OAAO1E,CAAK,CAC1C,EACA,KAAM,CAAC0E,EAAG1E,IAAU,CAClB0E,EAAE,iBAAmB,OAAO1E,CAAK,CACnC,EACA,KAAM,CAAC0E,EAAG1E,IAAU,CAClB0E,EAAE,iBAAmB,OAAO1E,CAAK,CACnC,EACA,KAAM,CAAC0E,EAAG1E,IAAU,CAClB0E,EAAE,MAAM,KAAO,OAAO1E,CAAK,CAC7B,EACA,IAAK,CAAC0E,EAAG1E,IAAU,CACjB0E,EAAE,kBAAoB,OAAO1E,CAAK,CACpC,EACA,KAAM,CAAC0E,EAAG1E,IAAU,CAClB0E,EAAE,KAAK,UAAY,OAAO1E,CAAK,CACjC,EACA,KAAM,CAAC0E,EAAG1E,IAAU,CAClB0E,EAAE,KAAK,aAAe,OAAO1E,CAAK,CACpC,EACA,KAAM,CAAC0E,EAAG1E,IAAU,CAClB0E,EAAE,MAAM,mBAAqB,OAAO1E,CAAK,CAC3C,EACA,KAAM,CAAC0E,EAAG1E,IAAU,CAClB0E,EAAE,MAAM,kBAAoB,OAAO1E,CAAK,CAC1C,CACF,EAEA,SAAS2E,EAAcN,EAAqC,CAC1D,MAAMO,EAAcJ,EAAA,EACpB,cAAO,QAAQH,CAAM,EAAE,QAAQ,CAAC,CAACQ,EAAO7E,CAAK,IAAM,CACjD,MAAM8E,EAASL,EAAeI,CAAK,EAC/BC,GACFA,EAAOF,EAAa5E,CAAK,CAE7B,CAAC,EACM4E,CACT,CAEO,SAASG,EACd9D,EACAkB,EACAb,EAAoC,CAAA,EAC1B,CACV,MAAM8C,EAAQd,EAAmBnB,EAAS,CACxC,GAAGb,EACH,aAAcA,EAAQ,cAAgB,OAAO,GAAA,CAC9C,EACK0D,EAAUb,EAAeC,CAAK,EAE9Ba,EAAeD,EAAQ,IAAIX,GAAUM,EAAcN,CAAM,CAAC,EAEhE,GAAI/C,EAAQ,qBAAsB,CAChC,MAAM4D,EAA2BD,EAAa,IAAI,CAACL,EAAaL,IAAU,CACxE,MAAMF,EAASW,EAAQT,CAAK,EAC5B,MAAO,CAAE,GAAGK,EAAa,GAAGP,CAAA,CAC9B,CAAC,EAED,MAAO,CACL,GAAIpD,EACJ,aAAciE,CAAA,CAElB,CAEA,MAAO,CACL,GAAIjE,EACJ,aAAAgE,CAAA,CAEJ,CC3QO,SAASE,EAAmBP,EAAgC,CACjE,KAAM,CAAE,KAAAQ,EAAM,MAAAC,EAAO,IAAAC,EAAK,KAAAC,EAAM,OAAAC,GAAWZ,EAC3C,OAAO,IAAI,KAAK,KAAK,IAAIQ,EAAMC,EAAQ,EAAGC,EAAKC,EAAMC,CAAM,CAAC,CAC9D"}
1
+ {"version":3,"file":"index.cjs","sources":["../src/utils/lru.ts","../src/utils/url.ts","../src/realtime/fetch.ts","../src/stations/list.ts","../src/realtime/parser.ts","../src/utils/date.ts"],"sourcesContent":["export class LruCache<K, V> {\n private readonly items = new Map<K, { value: V; expiresAt: number }>();\n\n constructor(\n private readonly maxSize: number,\n private readonly ttlMs: number,\n ) {}\n\n get(key: K): V | undefined {\n const entry = this.items.get(key);\n if (!entry) {\n return undefined;\n }\n\n const now = Date.now();\n if (entry.expiresAt <= now) {\n this.items.delete(key);\n return undefined;\n }\n\n this.items.delete(key);\n this.items.set(key, entry);\n return entry.value;\n }\n\n set(key: K, value: V): void {\n const now = Date.now();\n if (this.items.has(key)) {\n this.items.delete(key);\n }\n\n this.items.set(key, { value, expiresAt: now + this.ttlMs });\n this.prune(now);\n }\n\n private prune(now: number): void {\n for (const [key, entry] of this.items) {\n if (entry.expiresAt > now) {\n continue;\n }\n this.items.delete(key);\n }\n\n while (this.items.size > this.maxSize) {\n const oldestKey = this.items.keys().next().value as K | undefined;\n if (oldestKey === undefined) {\n break;\n }\n this.items.delete(oldestKey);\n }\n }\n}\n","export type QueryParamValue =\n | string\n | number\n | boolean\n | null\n | undefined\n | Array<string | number | boolean>;\n\nexport type QueryParams = Record<string, QueryParamValue>;\n\nexport function formatQueryParams(params: QueryParams = {}): string {\n const searchParams = new URLSearchParams();\n\n for (const [key, value] of Object.entries(params)) {\n if (value === null || value === undefined) {\n continue;\n }\n\n if (Array.isArray(value)) {\n value.forEach(item => {\n searchParams.append(key, String(item));\n });\n continue;\n }\n\n searchParams.append(key, String(value));\n }\n\n const query = searchParams.toString();\n return query ? `?${query}` : '';\n}\n\nexport function buildURL(\n base: string,\n path = '',\n params: QueryParams = {},\n): string {\n const normalizedBase = base.endsWith('/') ? base : `${base}/`;\n const url = new URL(path, normalizedBase);\n const query = formatQueryParams(params);\n url.search = query;\n return url.toString();\n}\n","import { LruCache } from '../utils/lru';\nimport { buildURL } from '../utils/url';\n\nexport interface FetchRealtimeOptions {\n buoyId: string;\n type?: string;\n fetch?: typeof fetch;\n requestInit?: RequestInit;\n baseUrl?: string;\n}\n\nconst DEFAULT_BASE_URL = 'https://www.ndbc.noaa.gov/data/realtime2/';\nconst CACHE_TTL_MS = 5 * 60 * 1000;\nconst CACHE_MAX_SIZE = 1024;\nconst REALTIME_CACHE = new LruCache<string, string>(\n CACHE_MAX_SIZE,\n CACHE_TTL_MS,\n);\n\nexport function buildRealtimeUrl(\n buoyId: string,\n type = 'txt',\n baseUrl = DEFAULT_BASE_URL,\n): string {\n const normalizedBuoyId = buoyId.toUpperCase();\n const filename = `${normalizedBuoyId}.${type}`;\n return buildURL(baseUrl, filename);\n}\n\nexport async function fetchRealtimeData(\n options: FetchRealtimeOptions,\n): Promise<string> {\n const {\n buoyId,\n type = 'txt',\n fetch: fetchImpl = fetch,\n requestInit,\n baseUrl = DEFAULT_BASE_URL,\n } = options;\n const normalizedBuoyId = buoyId.toUpperCase();\n\n if (!fetchImpl) {\n throw new Error('No fetch implementation available.');\n }\n\n const cacheKey = `${baseUrl}|${normalizedBuoyId}|${type}`;\n const cached = REALTIME_CACHE.get(cacheKey);\n if (cached !== undefined) {\n return cached;\n }\n\n const url = buildRealtimeUrl(normalizedBuoyId, type, baseUrl);\n const response = await fetchImpl(url, requestInit);\n\n if (!response.ok) {\n throw new Error(`Failed to fetch ${url}: ${response.status}`);\n }\n\n const body = await response.text();\n REALTIME_CACHE.set(cacheKey, body);\n return body;\n}\n","import { LruCache } from '../utils/lru';\n\nexport interface FetchBuoyListOptions {\n fetch?: typeof fetch;\n requestInit?: RequestInit;\n /**\n * URL for the active station XML feed. Defaults to NDBC.\n */\n activeUrl?: string;\n /**\n * URL for the full station catalog. Defaults to NDBC.\n */\n stationTableUrl?: string;\n /**\n * When true, includes inactive stations from the catalog.\n */\n includeInactive?: boolean;\n}\n\nexport interface FetchStationIndexOptions extends FetchBuoyListOptions {}\n\nexport interface StationIndex {\n /** Active station IDs from the XML feed. */\n active: readonly string[];\n /** All stations from the catalog (includes inactive when available). */\n all: readonly string[];\n /** Efficient membership check for active stations. */\n isActive: (id: string) => boolean;\n}\n\nconst DEFAULT_ACTIVE_STATIONS_URL = 'https://www.ndbc.noaa.gov/activestations.xml';\nconst DEFAULT_STATION_TABLE_URL =\n 'https://www.ndbc.noaa.gov/data/stations/station_table.txt';\n\nconst ACTIVE_CACHE_TTL_MS = 10 * 60 * 1000;\nconst ACTIVE_CACHE_MAX_SIZE = 8;\nconst STATION_TABLE_CACHE_TTL_MS = 12 * 60 * 60 * 1000;\nconst STATION_TABLE_CACHE_MAX_SIZE = 8;\n\nconst ACTIVE_CACHE = new LruCache<\n string,\n { ids: readonly string[]; set: ReadonlySet<string> }\n>(ACTIVE_CACHE_MAX_SIZE, ACTIVE_CACHE_TTL_MS);\n\nconst STATION_TABLE_CACHE = new LruCache<string, readonly string[]>(\n STATION_TABLE_CACHE_MAX_SIZE,\n STATION_TABLE_CACHE_TTL_MS,\n);\n\nfunction parseActiveStationsXml(rawText: string): { ids: string[]; set: Set<string> } {\n const ids: string[] = [];\n const set = new Set<string>();\n const regex = /<station[^>]*\\bid=\"([A-Za-z0-9]{3,10})\"/g;\n let match: RegExpExecArray | null;\n\n while ((match = regex.exec(rawText)) !== null) {\n const rawId = match[1];\n if (!rawId) {\n continue;\n }\n const id = rawId.toUpperCase();\n if (set.has(id)) {\n continue;\n }\n set.add(id);\n ids.push(id);\n }\n\n return { ids, set };\n}\n\nfunction parseStationTable(rawText: string): string[] {\n const ids: string[] = [];\n\n rawText.split(/\\r?\\n/).forEach(line => {\n if (!line || line.startsWith('#')) {\n return;\n }\n\n const pipeIndex = line.indexOf('|');\n const token = pipeIndex === -1 ? line : line.slice(0, pipeIndex);\n const id = token.trim().toUpperCase();\n\n if (!/^[A-Z0-9]{3,10}$/.test(id)) {\n return;\n }\n\n ids.push(id);\n });\n\n return ids;\n}\n\nasync function fetchActiveStations(\n fetchImpl: typeof fetch,\n activeUrl: string,\n requestInit?: RequestInit,\n): Promise<{ ids: readonly string[]; set: ReadonlySet<string> }> {\n const cached = ACTIVE_CACHE.get(activeUrl);\n if (cached !== undefined) {\n return cached;\n }\n\n const response = await fetchImpl(activeUrl, requestInit);\n if (!response.ok) {\n throw new Error(`Failed to fetch ${activeUrl}: ${response.status}`);\n }\n\n const body = await response.text();\n const parsed = parseActiveStationsXml(body);\n ACTIVE_CACHE.set(activeUrl, parsed);\n return parsed;\n}\n\nasync function fetchStationTable(\n fetchImpl: typeof fetch,\n stationTableUrl: string,\n requestInit?: RequestInit,\n): Promise<readonly string[]> {\n const cached = STATION_TABLE_CACHE.get(stationTableUrl);\n if (cached !== undefined) {\n return cached;\n }\n\n const response = await fetchImpl(stationTableUrl, requestInit);\n if (!response.ok) {\n throw new Error(`Failed to fetch ${stationTableUrl}: ${response.status}`);\n }\n\n const body = await response.text();\n const parsed = parseStationTable(body);\n STATION_TABLE_CACHE.set(stationTableUrl, parsed);\n return parsed;\n}\n\nexport async function fetchStationIndex(\n options: FetchStationIndexOptions = {},\n): Promise<StationIndex> {\n const {\n fetch: fetchImpl = fetch,\n requestInit,\n activeUrl = DEFAULT_ACTIVE_STATIONS_URL,\n stationTableUrl = DEFAULT_STATION_TABLE_URL,\n includeInactive,\n } = options;\n\n if (!fetchImpl) {\n throw new Error('No fetch implementation available.');\n }\n\n const active = await fetchActiveStations(fetchImpl, activeUrl, requestInit);\n\n const all = includeInactive\n ? await fetchStationTable(fetchImpl, stationTableUrl, requestInit)\n : active.ids;\n\n const activeSet = active.set;\n\n return {\n active: active.ids,\n all,\n isActive: (id: string) => activeSet.has(id.toUpperCase()),\n };\n}\n\nexport async function fetchBuoyList(\n options: FetchBuoyListOptions = {},\n): Promise<string[]> {\n const index = await fetchStationIndex(options);\n return options.includeInactive ? Array.from(index.all) : Array.from(index.active);\n}\n","import { BuoyData, Measurement } from '../models/measurement';\nimport { ParsedValue, RealtimeRecord, RealtimeTable } from '../models/table';\n\nexport interface ParseRowOptions {\n coerceNumbers?: boolean;\n missingValue?: number | null;\n missingTokens?: string[];\n}\n\nexport interface ParseRealtimeTableOptions extends ParseRowOptions {\n commentPrefix?: string;\n}\n\nexport interface ParseRealtimeDataOptions extends ParseRealtimeTableOptions {\n includeUnknownFields?: boolean;\n}\n\nconst DEFAULT_MISSING_TOKENS = ['MM'];\n\nfunction isMissingToken(value: string, missingTokens: string[]): boolean {\n if (missingTokens.includes(value)) {\n return true;\n }\n\n // NDBC missing values are often 9s (e.g. 99, 999, 9999, 99.0).\n if (/^9{2,}(\\.0+|\\.9+)?$/.test(value)) {\n return true;\n }\n\n return false;\n}\n\nfunction coerceValue(\n raw: string,\n options: Required<ParseRowOptions>,\n): ParsedValue {\n if (isMissingToken(raw, options.missingTokens)) {\n return options.missingValue;\n }\n\n if (options.coerceNumbers) {\n const numeric = Number(raw);\n if (!Number.isNaN(numeric)) {\n return numeric;\n }\n }\n\n return raw;\n}\n\nexport function parseRow(\n rawRow: string,\n options: ParseRowOptions = {},\n): ParsedValue[] {\n const resolved: Required<ParseRowOptions> = {\n coerceNumbers: options.coerceNumbers ?? true,\n missingValue: options.missingValue ?? null,\n missingTokens: options.missingTokens ?? DEFAULT_MISSING_TOKENS,\n };\n\n return rawRow\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n .map(value => coerceValue(value, resolved));\n}\n\nfunction isCommentLine(line: string, commentPrefix: string): boolean {\n return line.startsWith(`${commentPrefix} `);\n}\n\nfunction normalizeLines(rawText: string): string[] {\n return rawText\n .split(/\\r?\\n/)\n .map(line => line.trim())\n .filter(line => line.length > 0);\n}\n\nexport function parseRealtimeTable(\n rawText: string,\n options: ParseRealtimeTableOptions = {},\n): RealtimeTable {\n const commentPrefix = options.commentPrefix ?? '#';\n const lines = normalizeLines(rawText).filter(\n line => !isCommentLine(line, commentPrefix),\n );\n\n if (lines.length === 0) {\n return { headers: [], units: [], rows: [], rawRows: [] };\n }\n\n const headerLine = lines[0] ?? '';\n const headerOptions: ParseRowOptions = {\n coerceNumbers: false,\n missingValue: null,\n missingTokens: [],\n };\n const headers = parseRow(headerLine, headerOptions).map(String);\n\n let units: string[] = [];\n let dataStartIndex = 1;\n\n const unitLine = lines[1];\n if (unitLine && unitLine.startsWith(commentPrefix)) {\n units = parseRow(unitLine, headerOptions).map(token => {\n const text = String(token);\n return text.startsWith(commentPrefix)\n ? text.slice(commentPrefix.length)\n : text;\n });\n dataStartIndex = 2;\n }\n\n const rowOptions: ParseRowOptions = {\n coerceNumbers: options.coerceNumbers,\n missingValue: options.missingValue,\n missingTokens: options.missingTokens,\n };\n\n const dataRows = lines.slice(dataStartIndex);\n const rows = dataRows.map(row => parseRow(row, rowOptions));\n\n return {\n headers,\n units,\n rows,\n rawRows: dataRows,\n };\n}\n\nexport function objectifyTable(table: RealtimeTable): RealtimeRecord[] {\n const { headers, rows } = table;\n return rows.map(row => {\n const record: RealtimeRecord = {};\n headers.forEach((header, index) => {\n record[header] = row[index] ?? null;\n });\n return record;\n });\n}\n\nexport function createMeasurement(): Measurement {\n return {\n airTemperature: Number.NaN,\n day: Number.NaN,\n dewpointTemperature: Number.NaN,\n hour: Number.NaN,\n minute: Number.NaN,\n month: Number.NaN,\n pressureTendancy: Number.NaN,\n seaLevelPressure: Number.NaN,\n stationVisibility: Number.NaN,\n water: {\n averagePeriod: Number.NaN,\n dominantDirection: Number.NaN,\n dominantPeriod: Number.NaN,\n significantHeight: Number.NaN,\n surfaceTemperature: Number.NaN,\n tide: Number.NaN,\n },\n wind: {\n averageSpeed: Number.NaN,\n direction: Number.NaN,\n peakGustSpeed: Number.NaN,\n },\n year: Number.NaN,\n };\n}\n\nconst FIELD_MAPPINGS: Record<string, (m: Measurement, value: ParsedValue) => void> = {\n '#YY': (m, value) => {\n m.year = Number(value);\n },\n YY: (m, value) => {\n m.year = Number(value);\n },\n MM: (m, value) => {\n m.month = Number(value);\n },\n DD: (m, value) => {\n m.day = Number(value);\n },\n hh: (m, value) => {\n m.hour = Number(value);\n },\n mm: (m, value) => {\n m.minute = Number(value);\n },\n APD: (m, value) => {\n m.water.averagePeriod = Number(value);\n },\n ATMP: (m, value) => {\n m.airTemperature = Number(value);\n },\n DEWP: (m, value) => {\n m.dewpointTemperature = Number(value);\n },\n DPD: (m, value) => {\n m.water.dominantPeriod = Number(value);\n },\n GST: (m, value) => {\n m.wind.peakGustSpeed = Number(value);\n },\n MWD: (m, value) => {\n m.water.dominantDirection = Number(value);\n },\n PRES: (m, value) => {\n m.seaLevelPressure = Number(value);\n },\n PTDY: (m, value) => {\n m.pressureTendancy = Number(value);\n },\n TIDE: (m, value) => {\n m.water.tide = Number(value);\n },\n VIS: (m, value) => {\n m.stationVisibility = Number(value);\n },\n WDIR: (m, value) => {\n m.wind.direction = Number(value);\n },\n WSPD: (m, value) => {\n m.wind.averageSpeed = Number(value);\n },\n WTMP: (m, value) => {\n m.water.surfaceTemperature = Number(value);\n },\n WVHT: (m, value) => {\n m.water.significantHeight = Number(value);\n },\n};\n\nfunction toMeasurement(record: RealtimeRecord): Measurement {\n const measurement = createMeasurement();\n Object.entries(record).forEach(([field, value]) => {\n const mapper = FIELD_MAPPINGS[field];\n if (mapper) {\n mapper(measurement, value);\n }\n });\n return measurement;\n}\n\nexport function parseRealtimeData(\n buoyId: string,\n rawText: string,\n options: ParseRealtimeDataOptions = {},\n): BuoyData {\n const table = parseRealtimeTable(rawText, {\n ...options,\n missingValue: options.missingValue ?? Number.NaN,\n });\n const records = objectifyTable(table);\n\n const measurements = records.map(record => toMeasurement(record));\n\n if (options.includeUnknownFields) {\n const measurementsWithUnknowns = measurements.map((measurement, index) => {\n const record = records[index];\n return { ...measurement, ...record } as Measurement;\n });\n\n return {\n id: buoyId,\n measurements: measurementsWithUnknowns,\n };\n }\n\n return {\n id: buoyId,\n measurements,\n };\n}\n","import { Measurement } from '../models/measurement';\n\n/**\n * Returns a UTC Date instance for a buoy measurement.\n */\nexport function getMeasurementDate(measurement: Measurement): Date {\n const { year, month, day, hour, minute } = measurement;\n return new Date(Date.UTC(year, month - 1, day, hour, minute));\n}\n"],"names":["LruCache","maxSize","ttlMs","key","entry","now","value","oldestKey","formatQueryParams","params","searchParams","item","query","buildURL","base","path","normalizedBase","url","DEFAULT_BASE_URL","CACHE_TTL_MS","CACHE_MAX_SIZE","REALTIME_CACHE","buildRealtimeUrl","buoyId","type","baseUrl","filename","fetchRealtimeData","options","fetchImpl","requestInit","normalizedBuoyId","cacheKey","cached","response","body","DEFAULT_ACTIVE_STATIONS_URL","DEFAULT_STATION_TABLE_URL","ACTIVE_CACHE_TTL_MS","ACTIVE_CACHE_MAX_SIZE","STATION_TABLE_CACHE_TTL_MS","STATION_TABLE_CACHE_MAX_SIZE","ACTIVE_CACHE","STATION_TABLE_CACHE","parseActiveStationsXml","rawText","ids","set","regex","match","rawId","id","parseStationTable","line","pipeIndex","fetchActiveStations","activeUrl","parsed","fetchStationTable","stationTableUrl","fetchStationIndex","includeInactive","active","all","activeSet","fetchBuoyList","index","DEFAULT_MISSING_TOKENS","isMissingToken","missingTokens","coerceValue","raw","numeric","parseRow","rawRow","resolved","isCommentLine","commentPrefix","normalizeLines","parseRealtimeTable","lines","headerLine","headerOptions","headers","units","dataStartIndex","unitLine","token","text","rowOptions","dataRows","rows","row","objectifyTable","table","record","header","createMeasurement","FIELD_MAPPINGS","m","toMeasurement","measurement","field","mapper","parseRealtimeData","records","measurements","measurementsWithUnknowns","getMeasurementDate","year","month","day","hour","minute"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  export type { BuoyData, Measurement, WaterMeasurement, WindMeasurement } from './models/measurement';
2
2
  export type { ParsedValue, RealtimeRecord, RealtimeTable } from './models/table';
3
3
  export type { FetchRealtimeOptions, } from './realtime/fetch';
4
- export type { FetchBuoyListOptions } from './stations/list';
4
+ export type { FetchBuoyListOptions, FetchStationIndexOptions, StationIndex, } from './stations/list';
5
5
  export type { ParseRowOptions, ParseRealtimeTableOptions, ParseRealtimeDataOptions, } from './realtime/parser';
6
6
  export { fetchRealtimeData, buildRealtimeUrl } from './realtime/fetch';
7
- export { fetchBuoyList } from './stations/list';
7
+ export { fetchBuoyList, fetchStationIndex } from './stations/list';
8
8
  export { parseRealtimeData, parseRealtimeTable, parseRow, objectifyTable, createMeasurement, } from './realtime/parser';
9
9
  export { getMeasurementDate } from './utils/date';
10
10
  export { buildURL, formatQueryParams } from './utils/url';
package/dist/index.js CHANGED
@@ -1,175 +1,212 @@
1
- class p {
2
- constructor(e, r) {
3
- this.maxSize = e, this.ttlMs = r, this.items = /* @__PURE__ */ new Map();
1
+ class h {
2
+ constructor(e, n) {
3
+ this.maxSize = e, this.ttlMs = n, this.items = /* @__PURE__ */ new Map();
4
4
  }
5
5
  get(e) {
6
- const r = this.items.get(e);
7
- if (!r)
6
+ const n = this.items.get(e);
7
+ if (!n)
8
8
  return;
9
- const n = Date.now();
10
- if (r.expiresAt <= n) {
9
+ const r = Date.now();
10
+ if (n.expiresAt <= r) {
11
11
  this.items.delete(e);
12
12
  return;
13
13
  }
14
- return this.items.delete(e), this.items.set(e, r), r.value;
14
+ return this.items.delete(e), this.items.set(e, n), n.value;
15
15
  }
16
- set(e, r) {
17
- const n = Date.now();
18
- this.items.has(e) && this.items.delete(e), this.items.set(e, { value: r, expiresAt: n + this.ttlMs }), this.prune(n);
16
+ set(e, n) {
17
+ const r = Date.now();
18
+ this.items.has(e) && this.items.delete(e), this.items.set(e, { value: n, expiresAt: r + this.ttlMs }), this.prune(r);
19
19
  }
20
20
  prune(e) {
21
- for (const [r, n] of this.items)
22
- n.expiresAt > e || this.items.delete(r);
21
+ for (const [n, r] of this.items)
22
+ r.expiresAt > e || this.items.delete(n);
23
23
  for (; this.items.size > this.maxSize; ) {
24
- const r = this.items.keys().next().value;
25
- if (r === void 0)
24
+ const n = this.items.keys().next().value;
25
+ if (n === void 0)
26
26
  break;
27
- this.items.delete(r);
27
+ this.items.delete(n);
28
28
  }
29
29
  }
30
30
  }
31
- function T(t = {}) {
31
+ function g(t = {}) {
32
32
  const e = new URLSearchParams();
33
- for (const [n, s] of Object.entries(t))
33
+ for (const [r, s] of Object.entries(t))
34
34
  if (s != null) {
35
35
  if (Array.isArray(s)) {
36
36
  s.forEach((i) => {
37
- e.append(n, String(i));
37
+ e.append(r, String(i));
38
38
  });
39
39
  continue;
40
40
  }
41
- e.append(n, String(s));
41
+ e.append(r, String(s));
42
42
  }
43
- const r = e.toString();
44
- return r ? `?${r}` : "";
43
+ const n = e.toString();
44
+ return n ? `?${n}` : "";
45
45
  }
46
- function S(t, e = "", r = {}) {
47
- const n = t.endsWith("/") ? t : `${t}/`, s = new URL(e, n), i = T(r);
46
+ function S(t, e = "", n = {}) {
47
+ const r = t.endsWith("/") ? t : `${t}/`, s = new URL(e, r), i = g(n);
48
48
  return s.search = i, s.toString();
49
49
  }
50
- const w = "https://www.ndbc.noaa.gov/data/realtime2/", y = 5 * 60 * 1e3, E = 256, h = new p(
51
- E,
52
- y
50
+ const T = "https://www.ndbc.noaa.gov/data/realtime2/", E = 5 * 60 * 1e3, y = 1024, b = new h(
51
+ y,
52
+ E
53
53
  );
54
- function L(t, e = "txt", r = w) {
54
+ function _(t, e = "txt", n = T) {
55
55
  const s = `${t.toUpperCase()}.${e}`;
56
- return S(r, s);
56
+ return S(n, s);
57
57
  }
58
- async function B(t) {
58
+ async function Z(t) {
59
59
  const {
60
60
  buoyId: e,
61
- type: r = "txt",
62
- fetch: n = fetch,
61
+ type: n = "txt",
62
+ fetch: r = fetch,
63
63
  requestInit: s,
64
- baseUrl: i = w
64
+ baseUrl: i = T
65
65
  } = t, a = e.toUpperCase();
66
- if (!n)
66
+ if (!r)
67
67
  throw new Error("No fetch implementation available.");
68
- const o = `${i}|${a}|${r}`, c = h.get(o);
69
- if (c !== void 0)
70
- return c;
71
- const u = L(a, r, i), m = await n(u, s);
68
+ const c = `${i}|${a}|${n}`, u = b.get(c);
69
+ if (u !== void 0)
70
+ return u;
71
+ const o = _(a, n, i), m = await r(o, s);
72
72
  if (!m.ok)
73
- throw new Error(`Failed to fetch ${u}: ${m.status}`);
74
- const N = await m.text();
75
- return h.set(o, N), N;
73
+ throw new Error(`Failed to fetch ${o}: ${m.status}`);
74
+ const d = await m.text();
75
+ return b.set(c, d), d;
76
76
  }
77
- const A = "https://www.ndbc.noaa.gov/activestations.txt", D = 4 * 60 * 60 * 1e3, I = 4, b = new p(
78
- I,
77
+ const I = "https://www.ndbc.noaa.gov/activestations.xml", C = "https://www.ndbc.noaa.gov/data/stations/station_table.txt", v = 10 * 60 * 1e3, L = 8, D = 12 * 60 * 60 * 1e3, x = 8, w = new h(L, v), p = new h(
78
+ x,
79
79
  D
80
80
  );
81
- function _(t) {
82
- const e = /* @__PURE__ */ new Set();
83
- return t.split(/\r?\n/).map((r) => r.trim()).filter((r) => r.length > 0).forEach((r) => {
84
- if (r.startsWith("#"))
85
- return;
86
- const [n] = r.split(/\s+/);
87
- if (!n)
81
+ function M(t) {
82
+ const e = [], n = /* @__PURE__ */ new Set(), r = /<station[^>]*\bid="([A-Za-z0-9]{3,10})"/g;
83
+ let s;
84
+ for (; (s = r.exec(t)) !== null; ) {
85
+ const i = s[1];
86
+ if (!i)
87
+ continue;
88
+ const a = i.toUpperCase();
89
+ n.has(a) || (n.add(a), e.push(a));
90
+ }
91
+ return { ids: e, set: n };
92
+ }
93
+ function P(t) {
94
+ const e = [];
95
+ return t.split(/\r?\n/).forEach((n) => {
96
+ if (!n || n.startsWith("#"))
88
97
  return;
89
- const s = n.toUpperCase();
90
- s === "STATION" || s === "STN" || /^[A-Z0-9]{3,10}$/.test(s) && e.add(s);
91
- }), Array.from(e);
98
+ const r = n.indexOf("|"), i = (r === -1 ? n : n.slice(0, r)).trim().toUpperCase();
99
+ /^[A-Z0-9]{3,10}$/.test(i) && e.push(i);
100
+ }), e;
101
+ }
102
+ async function R(t, e, n) {
103
+ const r = w.get(e);
104
+ if (r !== void 0)
105
+ return r;
106
+ const s = await t(e, n);
107
+ if (!s.ok)
108
+ throw new Error(`Failed to fetch ${e}: ${s.status}`);
109
+ const i = await s.text(), a = M(i);
110
+ return w.set(e, a), a;
92
111
  }
93
- async function W(t = {}) {
94
- const { fetch: e = fetch, requestInit: r, url: n = A } = t;
112
+ async function V(t, e, n) {
113
+ const r = p.get(e);
114
+ if (r !== void 0)
115
+ return r;
116
+ const s = await t(e, n);
117
+ if (!s.ok)
118
+ throw new Error(`Failed to fetch ${e}: ${s.status}`);
119
+ const i = await s.text(), a = P(i);
120
+ return p.set(e, a), a;
121
+ }
122
+ async function U(t = {}) {
123
+ const {
124
+ fetch: e = fetch,
125
+ requestInit: n,
126
+ activeUrl: r = I,
127
+ stationTableUrl: s = C,
128
+ includeInactive: i
129
+ } = t;
95
130
  if (!e)
96
131
  throw new Error("No fetch implementation available.");
97
- const s = b.get(n);
98
- if (s !== void 0)
99
- return s;
100
- const i = await e(n, r);
101
- if (!i.ok)
102
- throw new Error(`Failed to fetch ${n}: ${i.status}`);
103
- const a = await i.text(), o = _(a);
104
- return b.set(n, o), o;
132
+ const a = await R(e, r, n), c = i ? await V(e, s, n) : a.ids, u = a.set;
133
+ return {
134
+ active: a.ids,
135
+ all: c,
136
+ isActive: (o) => u.has(o.toUpperCase())
137
+ };
105
138
  }
106
- const M = ["MM"];
107
- function P(t, e) {
139
+ async function q(t = {}) {
140
+ const e = await U(t);
141
+ return t.includeInactive ? Array.from(e.all) : Array.from(e.active);
142
+ }
143
+ const $ = ["MM"];
144
+ function k(t, e) {
108
145
  return !!(e.includes(t) || /^9{2,}(\.0+|\.9+)?$/.test(t));
109
146
  }
110
- function v(t, e) {
111
- if (P(t, e.missingTokens))
147
+ function H(t, e) {
148
+ if (k(t, e.missingTokens))
112
149
  return e.missingValue;
113
150
  if (e.coerceNumbers) {
114
- const r = Number(t);
115
- if (!Number.isNaN(r))
116
- return r;
151
+ const n = Number(t);
152
+ if (!Number.isNaN(n))
153
+ return n;
117
154
  }
118
155
  return t;
119
156
  }
120
157
  function f(t, e = {}) {
121
- const r = {
158
+ const n = {
122
159
  coerceNumbers: e.coerceNumbers ?? !0,
123
160
  missingValue: e.missingValue ?? null,
124
- missingTokens: e.missingTokens ?? M
161
+ missingTokens: e.missingTokens ?? $
125
162
  };
126
- return t.trim().split(/\s+/).filter(Boolean).map((n) => v(n, r));
163
+ return t.trim().split(/\s+/).filter(Boolean).map((r) => H(r, n));
127
164
  }
128
- function U(t, e) {
165
+ function W(t, e) {
129
166
  return t.startsWith(`${e} `);
130
167
  }
131
- function C(t) {
168
+ function B(t) {
132
169
  return t.split(/\r?\n/).map((e) => e.trim()).filter((e) => e.length > 0);
133
170
  }
134
- function x(t, e = {}) {
135
- const r = e.commentPrefix ?? "#", n = C(t).filter(
136
- (l) => !U(l, r)
171
+ function O(t, e = {}) {
172
+ const n = e.commentPrefix ?? "#", r = B(t).filter(
173
+ (N) => !W(N, n)
137
174
  );
138
- if (n.length === 0)
175
+ if (r.length === 0)
139
176
  return { headers: [], units: [], rows: [], rawRows: [] };
140
- const s = n[0] ?? "", i = {
177
+ const s = r[0] ?? "", i = {
141
178
  coerceNumbers: !1,
142
179
  missingValue: null,
143
180
  missingTokens: []
144
181
  }, a = f(s, i).map(String);
145
- let o = [], c = 1;
146
- const u = n[1];
147
- u && u.startsWith(r) && (o = f(u, i).map((l) => {
148
- const d = String(l);
149
- return d.startsWith(r) ? d.slice(r.length) : d;
150
- }), c = 2);
182
+ let c = [], u = 1;
183
+ const o = r[1];
184
+ o && o.startsWith(n) && (c = f(o, i).map((N) => {
185
+ const l = String(N);
186
+ return l.startsWith(n) ? l.slice(n.length) : l;
187
+ }), u = 2);
151
188
  const m = {
152
189
  coerceNumbers: e.coerceNumbers,
153
190
  missingValue: e.missingValue,
154
191
  missingTokens: e.missingTokens
155
- }, N = n.slice(c), g = N.map((l) => f(l, m));
192
+ }, d = r.slice(u), A = d.map((N) => f(N, m));
156
193
  return {
157
194
  headers: a,
158
- units: o,
159
- rows: g,
160
- rawRows: N
195
+ units: c,
196
+ rows: A,
197
+ rawRows: d
161
198
  };
162
199
  }
163
- function R(t) {
164
- const { headers: e, rows: r } = t;
165
- return r.map((n) => {
200
+ function z(t) {
201
+ const { headers: e, rows: n } = t;
202
+ return n.map((r) => {
166
203
  const s = {};
167
204
  return e.forEach((i, a) => {
168
- s[i] = n[a] ?? null;
205
+ s[i] = r[a] ?? null;
169
206
  }), s;
170
207
  });
171
208
  }
172
- function $() {
209
+ function F() {
173
210
  return {
174
211
  airTemperature: Number.NaN,
175
212
  day: Number.NaN,
@@ -196,7 +233,7 @@ function $() {
196
233
  year: Number.NaN
197
234
  };
198
235
  }
199
- const k = {
236
+ const G = {
200
237
  "#YY": (t, e) => {
201
238
  t.year = Number(e);
202
239
  },
@@ -258,22 +295,22 @@ const k = {
258
295
  t.water.significantHeight = Number(e);
259
296
  }
260
297
  };
261
- function V(t) {
262
- const e = $();
263
- return Object.entries(t).forEach(([r, n]) => {
264
- const s = k[r];
265
- s && s(e, n);
298
+ function Y(t) {
299
+ const e = F();
300
+ return Object.entries(t).forEach(([n, r]) => {
301
+ const s = G[n];
302
+ s && s(e, r);
266
303
  }), e;
267
304
  }
268
- function O(t, e, r = {}) {
269
- const n = x(e, {
270
- ...r,
271
- missingValue: r.missingValue ?? Number.NaN
272
- }), s = R(n), i = s.map((a) => V(a));
273
- if (r.includeUnknownFields) {
274
- const a = i.map((o, c) => {
275
- const u = s[c];
276
- return { ...o, ...u };
305
+ function X(t, e, n = {}) {
306
+ const r = O(e, {
307
+ ...n,
308
+ missingValue: n.missingValue ?? Number.NaN
309
+ }), s = z(r), i = s.map((a) => Y(a));
310
+ if (n.includeUnknownFields) {
311
+ const a = i.map((c, u) => {
312
+ const o = s[u];
313
+ return { ...c, ...o };
277
314
  });
278
315
  return {
279
316
  id: t,
@@ -285,21 +322,22 @@ function O(t, e, r = {}) {
285
322
  measurements: i
286
323
  };
287
324
  }
288
- function z(t) {
289
- const { year: e, month: r, day: n, hour: s, minute: i } = t;
290
- return new Date(Date.UTC(e, r - 1, n, s, i));
325
+ function j(t) {
326
+ const { year: e, month: n, day: r, hour: s, minute: i } = t;
327
+ return new Date(Date.UTC(e, n - 1, r, s, i));
291
328
  }
292
329
  export {
293
- L as buildRealtimeUrl,
330
+ _ as buildRealtimeUrl,
294
331
  S as buildURL,
295
- $ as createMeasurement,
296
- W as fetchBuoyList,
297
- B as fetchRealtimeData,
298
- T as formatQueryParams,
299
- z as getMeasurementDate,
300
- R as objectifyTable,
301
- O as parseRealtimeData,
302
- x as parseRealtimeTable,
332
+ F as createMeasurement,
333
+ q as fetchBuoyList,
334
+ Z as fetchRealtimeData,
335
+ U as fetchStationIndex,
336
+ g as formatQueryParams,
337
+ j as getMeasurementDate,
338
+ z as objectifyTable,
339
+ X as parseRealtimeData,
340
+ O as parseRealtimeTable,
303
341
  f as parseRow
304
342
  };
305
343
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/utils/lru.ts","../src/utils/url.ts","../src/realtime/fetch.ts","../src/stations/list.ts","../src/realtime/parser.ts","../src/utils/date.ts"],"sourcesContent":["export class LruCache<K, V> {\n private readonly items = new Map<K, { value: V; expiresAt: number }>();\n\n constructor(\n private readonly maxSize: number,\n private readonly ttlMs: number,\n ) {}\n\n get(key: K): V | undefined {\n const entry = this.items.get(key);\n if (!entry) {\n return undefined;\n }\n\n const now = Date.now();\n if (entry.expiresAt <= now) {\n this.items.delete(key);\n return undefined;\n }\n\n this.items.delete(key);\n this.items.set(key, entry);\n return entry.value;\n }\n\n set(key: K, value: V): void {\n const now = Date.now();\n if (this.items.has(key)) {\n this.items.delete(key);\n }\n\n this.items.set(key, { value, expiresAt: now + this.ttlMs });\n this.prune(now);\n }\n\n private prune(now: number): void {\n for (const [key, entry] of this.items) {\n if (entry.expiresAt > now) {\n continue;\n }\n this.items.delete(key);\n }\n\n while (this.items.size > this.maxSize) {\n const oldestKey = this.items.keys().next().value as K | undefined;\n if (oldestKey === undefined) {\n break;\n }\n this.items.delete(oldestKey);\n }\n }\n}\n","export type QueryParamValue =\n | string\n | number\n | boolean\n | null\n | undefined\n | Array<string | number | boolean>;\n\nexport type QueryParams = Record<string, QueryParamValue>;\n\nexport function formatQueryParams(params: QueryParams = {}): string {\n const searchParams = new URLSearchParams();\n\n for (const [key, value] of Object.entries(params)) {\n if (value === null || value === undefined) {\n continue;\n }\n\n if (Array.isArray(value)) {\n value.forEach(item => {\n searchParams.append(key, String(item));\n });\n continue;\n }\n\n searchParams.append(key, String(value));\n }\n\n const query = searchParams.toString();\n return query ? `?${query}` : '';\n}\n\nexport function buildURL(\n base: string,\n path = '',\n params: QueryParams = {},\n): string {\n const normalizedBase = base.endsWith('/') ? base : `${base}/`;\n const url = new URL(path, normalizedBase);\n const query = formatQueryParams(params);\n url.search = query;\n return url.toString();\n}\n","import { LruCache } from '../utils/lru';\nimport { buildURL } from '../utils/url';\n\nexport interface FetchRealtimeOptions {\n buoyId: string;\n type?: string;\n fetch?: typeof fetch;\n requestInit?: RequestInit;\n baseUrl?: string;\n}\n\nconst DEFAULT_BASE_URL = 'https://www.ndbc.noaa.gov/data/realtime2/';\nconst CACHE_TTL_MS = 5 * 60 * 1000;\nconst CACHE_MAX_SIZE = 256;\nconst REALTIME_CACHE = new LruCache<string, string>(\n CACHE_MAX_SIZE,\n CACHE_TTL_MS,\n);\n\nexport function buildRealtimeUrl(\n buoyId: string,\n type = 'txt',\n baseUrl = DEFAULT_BASE_URL,\n): string {\n const normalizedBuoyId = buoyId.toUpperCase();\n const filename = `${normalizedBuoyId}.${type}`;\n return buildURL(baseUrl, filename);\n}\n\nexport async function fetchRealtimeData(\n options: FetchRealtimeOptions,\n): Promise<string> {\n const {\n buoyId,\n type = 'txt',\n fetch: fetchImpl = fetch,\n requestInit,\n baseUrl = DEFAULT_BASE_URL,\n } = options;\n const normalizedBuoyId = buoyId.toUpperCase();\n\n if (!fetchImpl) {\n throw new Error('No fetch implementation available.');\n }\n\n const cacheKey = `${baseUrl}|${normalizedBuoyId}|${type}`;\n const cached = REALTIME_CACHE.get(cacheKey);\n if (cached !== undefined) {\n return cached;\n }\n\n const url = buildRealtimeUrl(normalizedBuoyId, type, baseUrl);\n const response = await fetchImpl(url, requestInit);\n\n if (!response.ok) {\n throw new Error(`Failed to fetch ${url}: ${response.status}`);\n }\n\n const body = await response.text();\n REALTIME_CACHE.set(cacheKey, body);\n return body;\n}\n","import { LruCache } from '../utils/lru';\n\nexport interface FetchBuoyListOptions {\n fetch?: typeof fetch;\n requestInit?: RequestInit;\n url?: string;\n}\n\nconst DEFAULT_BUOY_LIST_URL = 'https://www.ndbc.noaa.gov/activestations.txt';\nconst BUOY_LIST_CACHE_TTL_MS = 4 * 60 * 60 * 1000;\nconst BUOY_LIST_CACHE_MAX_SIZE = 4;\nconst BUOY_LIST_CACHE = new LruCache<string, string[]>(\n BUOY_LIST_CACHE_MAX_SIZE,\n BUOY_LIST_CACHE_TTL_MS,\n);\n\nfunction parseBuoyList(rawText: string): string[] {\n const ids = new Set<string>();\n\n rawText\n .split(/\\r?\\n/)\n .map(line => line.trim())\n .filter(line => line.length > 0)\n .forEach(line => {\n if (line.startsWith('#')) {\n return;\n }\n\n const [token] = line.split(/\\s+/);\n if (!token) {\n return;\n }\n\n const normalized = token.toUpperCase();\n if (normalized === 'STATION' || normalized === 'STN') {\n return;\n }\n\n if (!/^[A-Z0-9]{3,10}$/.test(normalized)) {\n return;\n }\n\n ids.add(normalized);\n });\n\n return Array.from(ids);\n}\n\nexport async function fetchBuoyList(\n options: FetchBuoyListOptions = {},\n): Promise<string[]> {\n const { fetch: fetchImpl = fetch, requestInit, url = DEFAULT_BUOY_LIST_URL } =\n options;\n\n if (!fetchImpl) {\n throw new Error('No fetch implementation available.');\n }\n\n const cached = BUOY_LIST_CACHE.get(url);\n if (cached !== undefined) {\n return cached;\n }\n\n const response = await fetchImpl(url, requestInit);\n if (!response.ok) {\n throw new Error(`Failed to fetch ${url}: ${response.status}`);\n }\n\n const body = await response.text();\n const list = parseBuoyList(body);\n BUOY_LIST_CACHE.set(url, list);\n return list;\n}\n","import { BuoyData, Measurement } from '../models/measurement';\nimport { ParsedValue, RealtimeRecord, RealtimeTable } from '../models/table';\n\nexport interface ParseRowOptions {\n coerceNumbers?: boolean;\n missingValue?: number | null;\n missingTokens?: string[];\n}\n\nexport interface ParseRealtimeTableOptions extends ParseRowOptions {\n commentPrefix?: string;\n}\n\nexport interface ParseRealtimeDataOptions extends ParseRealtimeTableOptions {\n includeUnknownFields?: boolean;\n}\n\nconst DEFAULT_MISSING_TOKENS = ['MM'];\n\nfunction isMissingToken(value: string, missingTokens: string[]): boolean {\n if (missingTokens.includes(value)) {\n return true;\n }\n\n // NDBC missing values are often 9s (e.g. 99, 999, 9999, 99.0).\n if (/^9{2,}(\\.0+|\\.9+)?$/.test(value)) {\n return true;\n }\n\n return false;\n}\n\nfunction coerceValue(\n raw: string,\n options: Required<ParseRowOptions>,\n): ParsedValue {\n if (isMissingToken(raw, options.missingTokens)) {\n return options.missingValue;\n }\n\n if (options.coerceNumbers) {\n const numeric = Number(raw);\n if (!Number.isNaN(numeric)) {\n return numeric;\n }\n }\n\n return raw;\n}\n\nexport function parseRow(\n rawRow: string,\n options: ParseRowOptions = {},\n): ParsedValue[] {\n const resolved: Required<ParseRowOptions> = {\n coerceNumbers: options.coerceNumbers ?? true,\n missingValue: options.missingValue ?? null,\n missingTokens: options.missingTokens ?? DEFAULT_MISSING_TOKENS,\n };\n\n return rawRow\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n .map(value => coerceValue(value, resolved));\n}\n\nfunction isCommentLine(line: string, commentPrefix: string): boolean {\n return line.startsWith(`${commentPrefix} `);\n}\n\nfunction normalizeLines(rawText: string): string[] {\n return rawText\n .split(/\\r?\\n/)\n .map(line => line.trim())\n .filter(line => line.length > 0);\n}\n\nexport function parseRealtimeTable(\n rawText: string,\n options: ParseRealtimeTableOptions = {},\n): RealtimeTable {\n const commentPrefix = options.commentPrefix ?? '#';\n const lines = normalizeLines(rawText).filter(\n line => !isCommentLine(line, commentPrefix),\n );\n\n if (lines.length === 0) {\n return { headers: [], units: [], rows: [], rawRows: [] };\n }\n\n const headerLine = lines[0] ?? '';\n const headerOptions: ParseRowOptions = {\n coerceNumbers: false,\n missingValue: null,\n missingTokens: [],\n };\n const headers = parseRow(headerLine, headerOptions).map(String);\n\n let units: string[] = [];\n let dataStartIndex = 1;\n\n const unitLine = lines[1];\n if (unitLine && unitLine.startsWith(commentPrefix)) {\n units = parseRow(unitLine, headerOptions).map(token => {\n const text = String(token);\n return text.startsWith(commentPrefix)\n ? text.slice(commentPrefix.length)\n : text;\n });\n dataStartIndex = 2;\n }\n\n const rowOptions: ParseRowOptions = {\n coerceNumbers: options.coerceNumbers,\n missingValue: options.missingValue,\n missingTokens: options.missingTokens,\n };\n\n const dataRows = lines.slice(dataStartIndex);\n const rows = dataRows.map(row => parseRow(row, rowOptions));\n\n return {\n headers,\n units,\n rows,\n rawRows: dataRows,\n };\n}\n\nexport function objectifyTable(table: RealtimeTable): RealtimeRecord[] {\n const { headers, rows } = table;\n return rows.map(row => {\n const record: RealtimeRecord = {};\n headers.forEach((header, index) => {\n record[header] = row[index] ?? null;\n });\n return record;\n });\n}\n\nexport function createMeasurement(): Measurement {\n return {\n airTemperature: Number.NaN,\n day: Number.NaN,\n dewpointTemperature: Number.NaN,\n hour: Number.NaN,\n minute: Number.NaN,\n month: Number.NaN,\n pressureTendancy: Number.NaN,\n seaLevelPressure: Number.NaN,\n stationVisibility: Number.NaN,\n water: {\n averagePeriod: Number.NaN,\n dominantDirection: Number.NaN,\n dominantPeriod: Number.NaN,\n significantHeight: Number.NaN,\n surfaceTemperature: Number.NaN,\n tide: Number.NaN,\n },\n wind: {\n averageSpeed: Number.NaN,\n direction: Number.NaN,\n peakGustSpeed: Number.NaN,\n },\n year: Number.NaN,\n };\n}\n\nconst FIELD_MAPPINGS: Record<string, (m: Measurement, value: ParsedValue) => void> = {\n '#YY': (m, value) => {\n m.year = Number(value);\n },\n YY: (m, value) => {\n m.year = Number(value);\n },\n MM: (m, value) => {\n m.month = Number(value);\n },\n DD: (m, value) => {\n m.day = Number(value);\n },\n hh: (m, value) => {\n m.hour = Number(value);\n },\n mm: (m, value) => {\n m.minute = Number(value);\n },\n APD: (m, value) => {\n m.water.averagePeriod = Number(value);\n },\n ATMP: (m, value) => {\n m.airTemperature = Number(value);\n },\n DEWP: (m, value) => {\n m.dewpointTemperature = Number(value);\n },\n DPD: (m, value) => {\n m.water.dominantPeriod = Number(value);\n },\n GST: (m, value) => {\n m.wind.peakGustSpeed = Number(value);\n },\n MWD: (m, value) => {\n m.water.dominantDirection = Number(value);\n },\n PRES: (m, value) => {\n m.seaLevelPressure = Number(value);\n },\n PTDY: (m, value) => {\n m.pressureTendancy = Number(value);\n },\n TIDE: (m, value) => {\n m.water.tide = Number(value);\n },\n VIS: (m, value) => {\n m.stationVisibility = Number(value);\n },\n WDIR: (m, value) => {\n m.wind.direction = Number(value);\n },\n WSPD: (m, value) => {\n m.wind.averageSpeed = Number(value);\n },\n WTMP: (m, value) => {\n m.water.surfaceTemperature = Number(value);\n },\n WVHT: (m, value) => {\n m.water.significantHeight = Number(value);\n },\n};\n\nfunction toMeasurement(record: RealtimeRecord): Measurement {\n const measurement = createMeasurement();\n Object.entries(record).forEach(([field, value]) => {\n const mapper = FIELD_MAPPINGS[field];\n if (mapper) {\n mapper(measurement, value);\n }\n });\n return measurement;\n}\n\nexport function parseRealtimeData(\n buoyId: string,\n rawText: string,\n options: ParseRealtimeDataOptions = {},\n): BuoyData {\n const table = parseRealtimeTable(rawText, {\n ...options,\n missingValue: options.missingValue ?? Number.NaN,\n });\n const records = objectifyTable(table);\n\n const measurements = records.map(record => toMeasurement(record));\n\n if (options.includeUnknownFields) {\n const measurementsWithUnknowns = measurements.map((measurement, index) => {\n const record = records[index];\n return { ...measurement, ...record } as Measurement;\n });\n\n return {\n id: buoyId,\n measurements: measurementsWithUnknowns,\n };\n }\n\n return {\n id: buoyId,\n measurements,\n };\n}\n","import { Measurement } from '../models/measurement';\n\n/**\n * Returns a UTC Date instance for a buoy measurement.\n */\nexport function getMeasurementDate(measurement: Measurement): Date {\n const { year, month, day, hour, minute } = measurement;\n return new Date(Date.UTC(year, month - 1, day, hour, minute));\n}\n"],"names":["LruCache","maxSize","ttlMs","key","entry","now","value","oldestKey","formatQueryParams","params","searchParams","item","query","buildURL","base","path","normalizedBase","url","DEFAULT_BASE_URL","CACHE_TTL_MS","CACHE_MAX_SIZE","REALTIME_CACHE","buildRealtimeUrl","buoyId","type","baseUrl","filename","fetchRealtimeData","options","fetchImpl","requestInit","normalizedBuoyId","cacheKey","cached","response","body","DEFAULT_BUOY_LIST_URL","BUOY_LIST_CACHE_TTL_MS","BUOY_LIST_CACHE_MAX_SIZE","BUOY_LIST_CACHE","parseBuoyList","rawText","ids","line","token","normalized","fetchBuoyList","list","DEFAULT_MISSING_TOKENS","isMissingToken","missingTokens","coerceValue","raw","numeric","parseRow","rawRow","resolved","isCommentLine","commentPrefix","normalizeLines","parseRealtimeTable","lines","headerLine","headerOptions","headers","units","dataStartIndex","unitLine","text","rowOptions","dataRows","rows","row","objectifyTable","table","record","header","index","createMeasurement","FIELD_MAPPINGS","m","toMeasurement","measurement","field","mapper","parseRealtimeData","records","measurements","measurementsWithUnknowns","getMeasurementDate","year","month","day","hour","minute"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../src/utils/lru.ts","../src/utils/url.ts","../src/realtime/fetch.ts","../src/stations/list.ts","../src/realtime/parser.ts","../src/utils/date.ts"],"sourcesContent":["export class LruCache<K, V> {\n private readonly items = new Map<K, { value: V; expiresAt: number }>();\n\n constructor(\n private readonly maxSize: number,\n private readonly ttlMs: number,\n ) {}\n\n get(key: K): V | undefined {\n const entry = this.items.get(key);\n if (!entry) {\n return undefined;\n }\n\n const now = Date.now();\n if (entry.expiresAt <= now) {\n this.items.delete(key);\n return undefined;\n }\n\n this.items.delete(key);\n this.items.set(key, entry);\n return entry.value;\n }\n\n set(key: K, value: V): void {\n const now = Date.now();\n if (this.items.has(key)) {\n this.items.delete(key);\n }\n\n this.items.set(key, { value, expiresAt: now + this.ttlMs });\n this.prune(now);\n }\n\n private prune(now: number): void {\n for (const [key, entry] of this.items) {\n if (entry.expiresAt > now) {\n continue;\n }\n this.items.delete(key);\n }\n\n while (this.items.size > this.maxSize) {\n const oldestKey = this.items.keys().next().value as K | undefined;\n if (oldestKey === undefined) {\n break;\n }\n this.items.delete(oldestKey);\n }\n }\n}\n","export type QueryParamValue =\n | string\n | number\n | boolean\n | null\n | undefined\n | Array<string | number | boolean>;\n\nexport type QueryParams = Record<string, QueryParamValue>;\n\nexport function formatQueryParams(params: QueryParams = {}): string {\n const searchParams = new URLSearchParams();\n\n for (const [key, value] of Object.entries(params)) {\n if (value === null || value === undefined) {\n continue;\n }\n\n if (Array.isArray(value)) {\n value.forEach(item => {\n searchParams.append(key, String(item));\n });\n continue;\n }\n\n searchParams.append(key, String(value));\n }\n\n const query = searchParams.toString();\n return query ? `?${query}` : '';\n}\n\nexport function buildURL(\n base: string,\n path = '',\n params: QueryParams = {},\n): string {\n const normalizedBase = base.endsWith('/') ? base : `${base}/`;\n const url = new URL(path, normalizedBase);\n const query = formatQueryParams(params);\n url.search = query;\n return url.toString();\n}\n","import { LruCache } from '../utils/lru';\nimport { buildURL } from '../utils/url';\n\nexport interface FetchRealtimeOptions {\n buoyId: string;\n type?: string;\n fetch?: typeof fetch;\n requestInit?: RequestInit;\n baseUrl?: string;\n}\n\nconst DEFAULT_BASE_URL = 'https://www.ndbc.noaa.gov/data/realtime2/';\nconst CACHE_TTL_MS = 5 * 60 * 1000;\nconst CACHE_MAX_SIZE = 1024;\nconst REALTIME_CACHE = new LruCache<string, string>(\n CACHE_MAX_SIZE,\n CACHE_TTL_MS,\n);\n\nexport function buildRealtimeUrl(\n buoyId: string,\n type = 'txt',\n baseUrl = DEFAULT_BASE_URL,\n): string {\n const normalizedBuoyId = buoyId.toUpperCase();\n const filename = `${normalizedBuoyId}.${type}`;\n return buildURL(baseUrl, filename);\n}\n\nexport async function fetchRealtimeData(\n options: FetchRealtimeOptions,\n): Promise<string> {\n const {\n buoyId,\n type = 'txt',\n fetch: fetchImpl = fetch,\n requestInit,\n baseUrl = DEFAULT_BASE_URL,\n } = options;\n const normalizedBuoyId = buoyId.toUpperCase();\n\n if (!fetchImpl) {\n throw new Error('No fetch implementation available.');\n }\n\n const cacheKey = `${baseUrl}|${normalizedBuoyId}|${type}`;\n const cached = REALTIME_CACHE.get(cacheKey);\n if (cached !== undefined) {\n return cached;\n }\n\n const url = buildRealtimeUrl(normalizedBuoyId, type, baseUrl);\n const response = await fetchImpl(url, requestInit);\n\n if (!response.ok) {\n throw new Error(`Failed to fetch ${url}: ${response.status}`);\n }\n\n const body = await response.text();\n REALTIME_CACHE.set(cacheKey, body);\n return body;\n}\n","import { LruCache } from '../utils/lru';\n\nexport interface FetchBuoyListOptions {\n fetch?: typeof fetch;\n requestInit?: RequestInit;\n /**\n * URL for the active station XML feed. Defaults to NDBC.\n */\n activeUrl?: string;\n /**\n * URL for the full station catalog. Defaults to NDBC.\n */\n stationTableUrl?: string;\n /**\n * When true, includes inactive stations from the catalog.\n */\n includeInactive?: boolean;\n}\n\nexport interface FetchStationIndexOptions extends FetchBuoyListOptions {}\n\nexport interface StationIndex {\n /** Active station IDs from the XML feed. */\n active: readonly string[];\n /** All stations from the catalog (includes inactive when available). */\n all: readonly string[];\n /** Efficient membership check for active stations. */\n isActive: (id: string) => boolean;\n}\n\nconst DEFAULT_ACTIVE_STATIONS_URL = 'https://www.ndbc.noaa.gov/activestations.xml';\nconst DEFAULT_STATION_TABLE_URL =\n 'https://www.ndbc.noaa.gov/data/stations/station_table.txt';\n\nconst ACTIVE_CACHE_TTL_MS = 10 * 60 * 1000;\nconst ACTIVE_CACHE_MAX_SIZE = 8;\nconst STATION_TABLE_CACHE_TTL_MS = 12 * 60 * 60 * 1000;\nconst STATION_TABLE_CACHE_MAX_SIZE = 8;\n\nconst ACTIVE_CACHE = new LruCache<\n string,\n { ids: readonly string[]; set: ReadonlySet<string> }\n>(ACTIVE_CACHE_MAX_SIZE, ACTIVE_CACHE_TTL_MS);\n\nconst STATION_TABLE_CACHE = new LruCache<string, readonly string[]>(\n STATION_TABLE_CACHE_MAX_SIZE,\n STATION_TABLE_CACHE_TTL_MS,\n);\n\nfunction parseActiveStationsXml(rawText: string): { ids: string[]; set: Set<string> } {\n const ids: string[] = [];\n const set = new Set<string>();\n const regex = /<station[^>]*\\bid=\"([A-Za-z0-9]{3,10})\"/g;\n let match: RegExpExecArray | null;\n\n while ((match = regex.exec(rawText)) !== null) {\n const rawId = match[1];\n if (!rawId) {\n continue;\n }\n const id = rawId.toUpperCase();\n if (set.has(id)) {\n continue;\n }\n set.add(id);\n ids.push(id);\n }\n\n return { ids, set };\n}\n\nfunction parseStationTable(rawText: string): string[] {\n const ids: string[] = [];\n\n rawText.split(/\\r?\\n/).forEach(line => {\n if (!line || line.startsWith('#')) {\n return;\n }\n\n const pipeIndex = line.indexOf('|');\n const token = pipeIndex === -1 ? line : line.slice(0, pipeIndex);\n const id = token.trim().toUpperCase();\n\n if (!/^[A-Z0-9]{3,10}$/.test(id)) {\n return;\n }\n\n ids.push(id);\n });\n\n return ids;\n}\n\nasync function fetchActiveStations(\n fetchImpl: typeof fetch,\n activeUrl: string,\n requestInit?: RequestInit,\n): Promise<{ ids: readonly string[]; set: ReadonlySet<string> }> {\n const cached = ACTIVE_CACHE.get(activeUrl);\n if (cached !== undefined) {\n return cached;\n }\n\n const response = await fetchImpl(activeUrl, requestInit);\n if (!response.ok) {\n throw new Error(`Failed to fetch ${activeUrl}: ${response.status}`);\n }\n\n const body = await response.text();\n const parsed = parseActiveStationsXml(body);\n ACTIVE_CACHE.set(activeUrl, parsed);\n return parsed;\n}\n\nasync function fetchStationTable(\n fetchImpl: typeof fetch,\n stationTableUrl: string,\n requestInit?: RequestInit,\n): Promise<readonly string[]> {\n const cached = STATION_TABLE_CACHE.get(stationTableUrl);\n if (cached !== undefined) {\n return cached;\n }\n\n const response = await fetchImpl(stationTableUrl, requestInit);\n if (!response.ok) {\n throw new Error(`Failed to fetch ${stationTableUrl}: ${response.status}`);\n }\n\n const body = await response.text();\n const parsed = parseStationTable(body);\n STATION_TABLE_CACHE.set(stationTableUrl, parsed);\n return parsed;\n}\n\nexport async function fetchStationIndex(\n options: FetchStationIndexOptions = {},\n): Promise<StationIndex> {\n const {\n fetch: fetchImpl = fetch,\n requestInit,\n activeUrl = DEFAULT_ACTIVE_STATIONS_URL,\n stationTableUrl = DEFAULT_STATION_TABLE_URL,\n includeInactive,\n } = options;\n\n if (!fetchImpl) {\n throw new Error('No fetch implementation available.');\n }\n\n const active = await fetchActiveStations(fetchImpl, activeUrl, requestInit);\n\n const all = includeInactive\n ? await fetchStationTable(fetchImpl, stationTableUrl, requestInit)\n : active.ids;\n\n const activeSet = active.set;\n\n return {\n active: active.ids,\n all,\n isActive: (id: string) => activeSet.has(id.toUpperCase()),\n };\n}\n\nexport async function fetchBuoyList(\n options: FetchBuoyListOptions = {},\n): Promise<string[]> {\n const index = await fetchStationIndex(options);\n return options.includeInactive ? Array.from(index.all) : Array.from(index.active);\n}\n","import { BuoyData, Measurement } from '../models/measurement';\nimport { ParsedValue, RealtimeRecord, RealtimeTable } from '../models/table';\n\nexport interface ParseRowOptions {\n coerceNumbers?: boolean;\n missingValue?: number | null;\n missingTokens?: string[];\n}\n\nexport interface ParseRealtimeTableOptions extends ParseRowOptions {\n commentPrefix?: string;\n}\n\nexport interface ParseRealtimeDataOptions extends ParseRealtimeTableOptions {\n includeUnknownFields?: boolean;\n}\n\nconst DEFAULT_MISSING_TOKENS = ['MM'];\n\nfunction isMissingToken(value: string, missingTokens: string[]): boolean {\n if (missingTokens.includes(value)) {\n return true;\n }\n\n // NDBC missing values are often 9s (e.g. 99, 999, 9999, 99.0).\n if (/^9{2,}(\\.0+|\\.9+)?$/.test(value)) {\n return true;\n }\n\n return false;\n}\n\nfunction coerceValue(\n raw: string,\n options: Required<ParseRowOptions>,\n): ParsedValue {\n if (isMissingToken(raw, options.missingTokens)) {\n return options.missingValue;\n }\n\n if (options.coerceNumbers) {\n const numeric = Number(raw);\n if (!Number.isNaN(numeric)) {\n return numeric;\n }\n }\n\n return raw;\n}\n\nexport function parseRow(\n rawRow: string,\n options: ParseRowOptions = {},\n): ParsedValue[] {\n const resolved: Required<ParseRowOptions> = {\n coerceNumbers: options.coerceNumbers ?? true,\n missingValue: options.missingValue ?? null,\n missingTokens: options.missingTokens ?? DEFAULT_MISSING_TOKENS,\n };\n\n return rawRow\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n .map(value => coerceValue(value, resolved));\n}\n\nfunction isCommentLine(line: string, commentPrefix: string): boolean {\n return line.startsWith(`${commentPrefix} `);\n}\n\nfunction normalizeLines(rawText: string): string[] {\n return rawText\n .split(/\\r?\\n/)\n .map(line => line.trim())\n .filter(line => line.length > 0);\n}\n\nexport function parseRealtimeTable(\n rawText: string,\n options: ParseRealtimeTableOptions = {},\n): RealtimeTable {\n const commentPrefix = options.commentPrefix ?? '#';\n const lines = normalizeLines(rawText).filter(\n line => !isCommentLine(line, commentPrefix),\n );\n\n if (lines.length === 0) {\n return { headers: [], units: [], rows: [], rawRows: [] };\n }\n\n const headerLine = lines[0] ?? '';\n const headerOptions: ParseRowOptions = {\n coerceNumbers: false,\n missingValue: null,\n missingTokens: [],\n };\n const headers = parseRow(headerLine, headerOptions).map(String);\n\n let units: string[] = [];\n let dataStartIndex = 1;\n\n const unitLine = lines[1];\n if (unitLine && unitLine.startsWith(commentPrefix)) {\n units = parseRow(unitLine, headerOptions).map(token => {\n const text = String(token);\n return text.startsWith(commentPrefix)\n ? text.slice(commentPrefix.length)\n : text;\n });\n dataStartIndex = 2;\n }\n\n const rowOptions: ParseRowOptions = {\n coerceNumbers: options.coerceNumbers,\n missingValue: options.missingValue,\n missingTokens: options.missingTokens,\n };\n\n const dataRows = lines.slice(dataStartIndex);\n const rows = dataRows.map(row => parseRow(row, rowOptions));\n\n return {\n headers,\n units,\n rows,\n rawRows: dataRows,\n };\n}\n\nexport function objectifyTable(table: RealtimeTable): RealtimeRecord[] {\n const { headers, rows } = table;\n return rows.map(row => {\n const record: RealtimeRecord = {};\n headers.forEach((header, index) => {\n record[header] = row[index] ?? null;\n });\n return record;\n });\n}\n\nexport function createMeasurement(): Measurement {\n return {\n airTemperature: Number.NaN,\n day: Number.NaN,\n dewpointTemperature: Number.NaN,\n hour: Number.NaN,\n minute: Number.NaN,\n month: Number.NaN,\n pressureTendancy: Number.NaN,\n seaLevelPressure: Number.NaN,\n stationVisibility: Number.NaN,\n water: {\n averagePeriod: Number.NaN,\n dominantDirection: Number.NaN,\n dominantPeriod: Number.NaN,\n significantHeight: Number.NaN,\n surfaceTemperature: Number.NaN,\n tide: Number.NaN,\n },\n wind: {\n averageSpeed: Number.NaN,\n direction: Number.NaN,\n peakGustSpeed: Number.NaN,\n },\n year: Number.NaN,\n };\n}\n\nconst FIELD_MAPPINGS: Record<string, (m: Measurement, value: ParsedValue) => void> = {\n '#YY': (m, value) => {\n m.year = Number(value);\n },\n YY: (m, value) => {\n m.year = Number(value);\n },\n MM: (m, value) => {\n m.month = Number(value);\n },\n DD: (m, value) => {\n m.day = Number(value);\n },\n hh: (m, value) => {\n m.hour = Number(value);\n },\n mm: (m, value) => {\n m.minute = Number(value);\n },\n APD: (m, value) => {\n m.water.averagePeriod = Number(value);\n },\n ATMP: (m, value) => {\n m.airTemperature = Number(value);\n },\n DEWP: (m, value) => {\n m.dewpointTemperature = Number(value);\n },\n DPD: (m, value) => {\n m.water.dominantPeriod = Number(value);\n },\n GST: (m, value) => {\n m.wind.peakGustSpeed = Number(value);\n },\n MWD: (m, value) => {\n m.water.dominantDirection = Number(value);\n },\n PRES: (m, value) => {\n m.seaLevelPressure = Number(value);\n },\n PTDY: (m, value) => {\n m.pressureTendancy = Number(value);\n },\n TIDE: (m, value) => {\n m.water.tide = Number(value);\n },\n VIS: (m, value) => {\n m.stationVisibility = Number(value);\n },\n WDIR: (m, value) => {\n m.wind.direction = Number(value);\n },\n WSPD: (m, value) => {\n m.wind.averageSpeed = Number(value);\n },\n WTMP: (m, value) => {\n m.water.surfaceTemperature = Number(value);\n },\n WVHT: (m, value) => {\n m.water.significantHeight = Number(value);\n },\n};\n\nfunction toMeasurement(record: RealtimeRecord): Measurement {\n const measurement = createMeasurement();\n Object.entries(record).forEach(([field, value]) => {\n const mapper = FIELD_MAPPINGS[field];\n if (mapper) {\n mapper(measurement, value);\n }\n });\n return measurement;\n}\n\nexport function parseRealtimeData(\n buoyId: string,\n rawText: string,\n options: ParseRealtimeDataOptions = {},\n): BuoyData {\n const table = parseRealtimeTable(rawText, {\n ...options,\n missingValue: options.missingValue ?? Number.NaN,\n });\n const records = objectifyTable(table);\n\n const measurements = records.map(record => toMeasurement(record));\n\n if (options.includeUnknownFields) {\n const measurementsWithUnknowns = measurements.map((measurement, index) => {\n const record = records[index];\n return { ...measurement, ...record } as Measurement;\n });\n\n return {\n id: buoyId,\n measurements: measurementsWithUnknowns,\n };\n }\n\n return {\n id: buoyId,\n measurements,\n };\n}\n","import { Measurement } from '../models/measurement';\n\n/**\n * Returns a UTC Date instance for a buoy measurement.\n */\nexport function getMeasurementDate(measurement: Measurement): Date {\n const { year, month, day, hour, minute } = measurement;\n return new Date(Date.UTC(year, month - 1, day, hour, minute));\n}\n"],"names":["LruCache","maxSize","ttlMs","key","entry","now","value","oldestKey","formatQueryParams","params","searchParams","item","query","buildURL","base","path","normalizedBase","url","DEFAULT_BASE_URL","CACHE_TTL_MS","CACHE_MAX_SIZE","REALTIME_CACHE","buildRealtimeUrl","buoyId","type","baseUrl","filename","fetchRealtimeData","options","fetchImpl","requestInit","normalizedBuoyId","cacheKey","cached","response","body","DEFAULT_ACTIVE_STATIONS_URL","DEFAULT_STATION_TABLE_URL","ACTIVE_CACHE_TTL_MS","ACTIVE_CACHE_MAX_SIZE","STATION_TABLE_CACHE_TTL_MS","STATION_TABLE_CACHE_MAX_SIZE","ACTIVE_CACHE","STATION_TABLE_CACHE","parseActiveStationsXml","rawText","ids","set","regex","match","rawId","id","parseStationTable","line","pipeIndex","fetchActiveStations","activeUrl","parsed","fetchStationTable","stationTableUrl","fetchStationIndex","includeInactive","active","all","activeSet","fetchBuoyList","index","DEFAULT_MISSING_TOKENS","isMissingToken","missingTokens","coerceValue","raw","numeric","parseRow","rawRow","resolved","isCommentLine","commentPrefix","normalizeLines","parseRealtimeTable","lines","headerLine","headerOptions","headers","units","dataStartIndex","unitLine","token","text","rowOptions","dataRows","rows","row","objectifyTable","table","record","header","createMeasurement","FIELD_MAPPINGS","m","toMeasurement","measurement","field","mapper","parseRealtimeData","records","measurements","measurementsWithUnknowns","getMeasurementDate","year","month","day","hour","minute"],"mappings":"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"}
@@ -1,6 +1,28 @@
1
1
  export interface FetchBuoyListOptions {
2
2
  fetch?: typeof fetch;
3
3
  requestInit?: RequestInit;
4
- url?: string;
4
+ /**
5
+ * URL for the active station XML feed. Defaults to NDBC.
6
+ */
7
+ activeUrl?: string;
8
+ /**
9
+ * URL for the full station catalog. Defaults to NDBC.
10
+ */
11
+ stationTableUrl?: string;
12
+ /**
13
+ * When true, includes inactive stations from the catalog.
14
+ */
15
+ includeInactive?: boolean;
5
16
  }
17
+ export interface FetchStationIndexOptions extends FetchBuoyListOptions {
18
+ }
19
+ export interface StationIndex {
20
+ /** Active station IDs from the XML feed. */
21
+ active: readonly string[];
22
+ /** All stations from the catalog (includes inactive when available). */
23
+ all: readonly string[];
24
+ /** Efficient membership check for active stations. */
25
+ isActive: (id: string) => boolean;
26
+ }
27
+ export declare function fetchStationIndex(options?: FetchStationIndexOptions): Promise<StationIndex>;
6
28
  export declare function fetchBuoyList(options?: FetchBuoyListOptions): Promise<string[]>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buoydata",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "Modern TypeScript SDK for NDBC realtime buoy data.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",