/** * JMA (Japan Meteorological Agency) warning service. * * Reads JMA's official disaster-prevention XML service: one Atom index listing * roughly seven days of bulletins, then the newest VPWW53 warning document for * the office asked about. * * **Contract, not garnish.** A failed index or document fetch **propagates**. * A fabricated "no warnings in force" built from a fetch that failed is the * worst thing this codebase can emit, so nothing here catches and returns an * empty result. The handler renders the failure. * * **Errors are plain `Error`s with fixed, pre-written messages.** * `ApiServiceName` in `src/errors/ApiError.ts` is a closed union and JMA stays * outside it, following FIRMS and the other peripheral services. No message * here ever carries a URL, a response body, or a raw axios error. * * ## Two clocks on the index, and why * * The index is cached under `CacheConfig.ttl.jmaIndex` (1 hour) — that is * **retention**. Separately, `INDEX_FRESHNESS_MS` (`CacheConfig.ttl.alerts`, * 5 minutes) is how long the entry is served without asking JMA at all — that * is **freshness**, and it is checked from `revalidatedAt` *inside* the entry * rather than by letting the entry expire. * * They have to be separate. A conditional `If-None-Match` returns **304 with * zero bytes**, and the whole value of that is reusing the parse we already * have; a single 5-minute TTL would evict the parse at precisely the moment the * ETag became useful. Age is not a problem for correctness either — a 304 is * JMA stating the index is current, whatever its age. * * The ETag lives **in the cache entry**, never on the instance (G43): a field on * the service is not per-request across an `await`, and two overlapping refreshes * would interleave one request's validator into another's fetch. * * ## What is bounded, and what a bound may never do * * The index is capped by bytes (`JMA_MAX_INDEX_BYTES`) and by entry count * (`JMA_MAX_INDEX_ENTRIES`), both in `jmaParse.ts`. **A trim is a caveat the * renderer shows, never a reason to report an office as having no warnings** * (G8) — `JmaWarningsResult.indexTrimmed` carries it out to be disclosed. * * ## Positive controls on the index * * An upstream that answers HTTP 200 with well-formed, correctly-shaped content * it has stopped updating is invisible to every other check here, and JMA's * `bosai/warning/data/warning/*.json` endpoint is doing exactly that today — * frozen since May 2026 while answering 200. So two properties are asserted * before any answer is derived, and both fail loudly rather than emptily: * * - the index yields **at least one VPWW53 entry**. VPWW53 covers all 58 * offices and carried 2,515 entries in seven days; zero is a fault. * - not every entry's filename failed to parse. All-unparseable means the * filename convention moved, which would otherwise present as "this office * publishes nothing". * * Measured live 2026-09-03: index 5,267,421 bytes decompressed / 271 KB on the * wire, `If-None-Match` returns 304 with zero bytes, one document 25 KB with * `cache-control: max-age=86400` and an immutable timestamped filename. */ import type { JmaWarningDocument } from '../types/jma.js'; /** The long-term index: roughly seven days of bulletins. */ export declare const JMA_INDEX_URL = "https://www.data.jma.go.jp/developer/xml/feed/extra_l.xml"; /** * Public page the licence-mandated attribution points at. * * JMA's Government Standard Terms require `出典:気象庁ホームページ (当該ページのURL)`. * The renderer reproduces that string exactly; this is the URL it carries. */ export declare const JMA_SOURCE_URL = "https://www.jma.go.jp/bosai/warning/"; /** * Above this age, the index's newest warning bulletin — across all of Japan — * is treated as stale, and the renderer discloses rather than reporting an * all-clear. * * Six hours. VPWW53 ran at 2,515 bulletins over seven days when measured, about * one every four minutes nationwide, so a six-hour national silence is far * outside normal operation and is much more likely to mean the feed has stopped * than that Japan has. */ export declare const JMA_INDEX_STALE_AFTER_MS: number; /** One office's warning answer. */ export interface JmaWarningsResult { /** The office asked about. */ officeCode: string; /** * The office's newest VPWW53, or `undefined` when the index carries none for * it. * * `undefined` is **not** an all-clear. All 58 offices publish VPWW53 * continuously, so an office missing from the index means the answer is * unknown, and the renderer discloses that rather than reporting no warnings. */ document?: JmaWarningDocument; /** URL the document came from, for attribution and diagnostics. */ documentUrl?: string; /** `` of the chosen entry, as published. */ documentUpdated?: string; /** `` of the newest VPWW53 entry anywhere in the index — the feed's own pulse. */ newestEntryUpdated?: string; /** True when the newest bulletin nationwide is older than `JMA_INDEX_STALE_AFTER_MS`. */ indexStale: boolean; /** * True when the index's newest entry carries no parseable ``, so the * feed's freshness could not be checked at all. * * Distinct from `indexStale` on purpose. Both mean "we cannot vouch for this * being current", but only `indexStale` means "and we know it is old" — a * feed that dropped or reformatted `` would otherwise read as fresh, * which is the exact shape the staleness check exists to catch (G72). */ indexClockUnknown: boolean; /** True when the entry cap trimmed the index. A caveat to disclose, never an exclusion (G8). */ indexTrimmed: boolean; /** How many entries had an unreadable filename. Nonzero is worth disclosing; all of them is a fault. */ indexUnparsedEntries: number; } export interface JmaServiceConfig { timeout?: number; /** Injectable clock, so freshness and staleness are testable without waiting. */ now?: () => number; } export declare class JmaService { private client; private cache; private inFlight; private now; constructor(config?: JmaServiceConfig); /** Map a request failure to a fixed message. Never includes a URL, a body, or a raw axios error. */ private toJmaError; /** * Log a failure with status and code only. * * The `Error` slot is deliberately left empty: the logger serialises the * message and stack of whatever it is handed, and an axios error carries the * request URL and the response body. */ private logFailure; /** * Cache-then-in-flight-then-fetch. * * The single-flight map is keyed by the cache key, so N concurrent callers * make one request; the entry is deleted in `finally`, so a rejection is * retried rather than remembered. */ private pull; /** * The parsed index, revalidated conditionally when it is older than * `INDEX_FRESHNESS_MS`. * * Always returns the **unfiltered** index. Filtering by office and bulletin * type happens at read time in `getWarnings`, so one cached index serves every * Japanese request rather than one cache entry per office (G6). */ private getIndex; /** * Refuse an index that parsed cleanly but cannot be what it claims to be. * * Both checks fail **loudly**. Either one, treated as an empty index, becomes * "no warnings for your office" — a fabricated all-clear derived from a broken * upstream rather than from Japan being quiet. */ private assertIndexIsUsable; /** Fetch and parse one warning document. Immutable filename, so the 24h document TTL applies. */ private getDocument; /** * The newest VPWW53 warning document for one office. * * Index entries are newest-first, so the first match per office is the * current one. */ getWarnings(officeCode: string): Promise; } //# sourceMappingURL=jma.d.ts.map