@mostlyrightmd/core 0.1.0-rc.7

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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/dist/discovery/index.cjs +1646 -0
  4. package/dist/discovery/index.cjs.map +1 -0
  5. package/dist/discovery/index.d.cts +313 -0
  6. package/dist/discovery/index.d.ts +313 -0
  7. package/dist/discovery/index.mjs +1609 -0
  8. package/dist/discovery/index.mjs.map +1 -0
  9. package/dist/formats/index.cjs +498 -0
  10. package/dist/formats/index.cjs.map +1 -0
  11. package/dist/formats/index.d.cts +97 -0
  12. package/dist/formats/index.d.ts +97 -0
  13. package/dist/formats/index.mjs +465 -0
  14. package/dist/formats/index.mjs.map +1 -0
  15. package/dist/index.cjs +1624 -0
  16. package/dist/index.cjs.map +1 -0
  17. package/dist/index.d.cts +559 -0
  18. package/dist/index.d.ts +559 -0
  19. package/dist/index.global.js +1582 -0
  20. package/dist/index.global.js.map +1 -0
  21. package/dist/index.mjs +1557 -0
  22. package/dist/index.mjs.map +1 -0
  23. package/dist/internal/bounds.cjs +125 -0
  24. package/dist/internal/bounds.cjs.map +1 -0
  25. package/dist/internal/bounds.d.cts +36 -0
  26. package/dist/internal/bounds.d.ts +36 -0
  27. package/dist/internal/bounds.mjs +81 -0
  28. package/dist/internal/bounds.mjs.map +1 -0
  29. package/dist/internal/cache/fs.cjs +217 -0
  30. package/dist/internal/cache/fs.cjs.map +1 -0
  31. package/dist/internal/cache/fs.d.cts +57 -0
  32. package/dist/internal/cache/fs.d.ts +57 -0
  33. package/dist/internal/cache/fs.mjs +179 -0
  34. package/dist/internal/cache/fs.mjs.map +1 -0
  35. package/dist/internal/cache/index.browser.cjs +1184 -0
  36. package/dist/internal/cache/index.browser.cjs.map +1 -0
  37. package/dist/internal/cache/index.browser.d.cts +20 -0
  38. package/dist/internal/cache/index.browser.d.ts +20 -0
  39. package/dist/internal/cache/index.browser.mjs +36 -0
  40. package/dist/internal/cache/index.browser.mjs.map +1 -0
  41. package/dist/internal/cache/index.cjs +1389 -0
  42. package/dist/internal/cache/index.cjs.map +1 -0
  43. package/dist/internal/cache/index.d.cts +16 -0
  44. package/dist/internal/cache/index.d.ts +16 -0
  45. package/dist/internal/cache/index.mjs +40 -0
  46. package/dist/internal/cache/index.mjs.map +1 -0
  47. package/dist/internal/chunk-PKJXHY27.mjs +1137 -0
  48. package/dist/internal/chunk-PKJXHY27.mjs.map +1 -0
  49. package/dist/internal/convert.cjs +161 -0
  50. package/dist/internal/convert.cjs.map +1 -0
  51. package/dist/internal/convert.d.cts +44 -0
  52. package/dist/internal/convert.d.ts +44 -0
  53. package/dist/internal/convert.mjs +117 -0
  54. package/dist/internal/convert.mjs.map +1 -0
  55. package/dist/internal/fs-O6XR4WWW.mjs +183 -0
  56. package/dist/internal/fs-O6XR4WWW.mjs.map +1 -0
  57. package/dist/internal/keys-B7C8C88N.d.cts +191 -0
  58. package/dist/internal/keys-B7C8C88N.d.ts +191 -0
  59. package/dist/internal/merge/index.cjs +75 -0
  60. package/dist/internal/merge/index.cjs.map +1 -0
  61. package/dist/internal/merge/index.d.cts +74 -0
  62. package/dist/internal/merge/index.d.ts +74 -0
  63. package/dist/internal/merge/index.mjs +46 -0
  64. package/dist/internal/merge/index.mjs.map +1 -0
  65. package/dist/internal/pairs.cjs +328 -0
  66. package/dist/internal/pairs.cjs.map +1 -0
  67. package/dist/internal/pairs.d.cts +105 -0
  68. package/dist/internal/pairs.d.ts +105 -0
  69. package/dist/internal/pairs.mjs +298 -0
  70. package/dist/internal/pairs.mjs.map +1 -0
  71. package/dist/qc/index.cjs +247 -0
  72. package/dist/qc/index.cjs.map +1 -0
  73. package/dist/qc/index.d.cts +140 -0
  74. package/dist/qc/index.d.ts +140 -0
  75. package/dist/qc/index.mjs +212 -0
  76. package/dist/qc/index.mjs.map +1 -0
  77. package/dist/temporal/index.cjs +504 -0
  78. package/dist/temporal/index.cjs.map +1 -0
  79. package/dist/temporal/index.d.cts +121 -0
  80. package/dist/temporal/index.d.ts +121 -0
  81. package/dist/temporal/index.mjs +474 -0
  82. package/dist/temporal/index.mjs.map +1 -0
  83. package/dist/transforms/index.cjs +399 -0
  84. package/dist/transforms/index.cjs.map +1 -0
  85. package/dist/transforms/index.d.cts +193 -0
  86. package/dist/transforms/index.d.ts +193 -0
  87. package/dist/transforms/index.mjs +362 -0
  88. package/dist/transforms/index.mjs.map +1 -0
  89. package/dist/validator.cjs +1870 -0
  90. package/dist/validator.cjs.map +1 -0
  91. package/dist/validator.d.cts +30 -0
  92. package/dist/validator.d.ts +30 -0
  93. package/dist/validator.mjs +1843 -0
  94. package/dist/validator.mjs.map +1 -0
  95. package/package.json +115 -0
@@ -0,0 +1,183 @@
1
+ // src/internal/cache/fs.ts
2
+ import { randomUUID } from "node:crypto";
3
+ import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import * as properLockfile from "proper-lockfile";
7
+ var _legacyCacheDirWarned = false;
8
+ function defaultFsRoot() {
9
+ const canonical = process.env.MOSTLYRIGHT_CACHE_DIR;
10
+ if (canonical !== void 0 && canonical.length > 0) return canonical;
11
+ const legacy = process.env.TRADEWINDS_CACHE_DIR;
12
+ if (legacy !== void 0 && legacy.length > 0) {
13
+ if (!_legacyCacheDirWarned) {
14
+ console.warn(
15
+ "TRADEWINDS_CACHE_DIR is deprecated; use MOSTLYRIGHT_CACHE_DIR. Support will be removed in vts-0.3. Run: mv ~/.tradewinds ~/.mostlyright"
16
+ );
17
+ _legacyCacheDirWarned = true;
18
+ }
19
+ return legacy;
20
+ }
21
+ return join(homedir(), ".mostlyright", "cache-ts");
22
+ }
23
+ function _resetLegacyCacheDirWarn() {
24
+ _legacyCacheDirWarned = false;
25
+ }
26
+ var FsStore = class {
27
+ #root;
28
+ // In-process per-key promise chain. proper-lockfile guarantees
29
+ // cross-process exclusion but its retry-based contention resolution
30
+ // is order-non-deterministic — two in-process callers racing on
31
+ // `lock()` may acquire in either order. Layering an in-process chain
32
+ // ensures strict FIFO for callers within the same Node process (and
33
+ // serves as a cheap fast-path: only one of N in-process callers ever
34
+ // actually contends with proper-lockfile).
35
+ #chain = /* @__PURE__ */ new Map();
36
+ constructor(opts = {}) {
37
+ this.#root = opts.root ?? defaultFsRoot();
38
+ }
39
+ /**
40
+ * Path resolver — `key` is sanitized via `encodeURIComponent` so that
41
+ *
42
+ * 1. `:` `/` `\` cannot escape the root (all percent-encoded), and
43
+ * 2. the key → file mapping is INJECTIVE: distinct keys always map to
44
+ * distinct files.
45
+ *
46
+ * Iter-13 C16 fix: the previous implementation collapsed `:` / `/` / `\`
47
+ * to the literal substring `"__"`, which is a lossy mapping — `"a:b"`,
48
+ * `"a/b"`, and the literal `"a__b"` all hashed to `a__b.json`, so one
49
+ * key's write would silently overwrite (and corrupt subsequent reads of)
50
+ * another key. `encodeURIComponent` is bijective on string inputs and
51
+ * filesystem-safe on every platform we ship (POSIX + Windows): the
52
+ * characters it leaves unescaped (alphanumerics, `-._~!*'()`) are all
53
+ * legal in NTFS, APFS, and ext4 filenames, and `%` is itself legal.
54
+ *
55
+ * BREAKING in v0.1.0: on-disk cache files written by any prior
56
+ * pre-release of this package use the old `__`-replacement scheme and
57
+ * are unreadable after upgrade. This is acceptable for a local-first
58
+ * cache: entries are regenerated on demand from live data, and the
59
+ * cache directory can be safely deleted by the user.
60
+ */
61
+ #pathFor(key) {
62
+ const safe = encodeURIComponent(key);
63
+ return join(this.#root, `${safe}.json`);
64
+ }
65
+ async get(key) {
66
+ const p = this.#pathFor(key);
67
+ let raw;
68
+ try {
69
+ raw = await readFile(p, "utf8");
70
+ } catch (e) {
71
+ const code = e.code;
72
+ if (code === "ENOENT") return null;
73
+ throw e;
74
+ }
75
+ let entry;
76
+ try {
77
+ entry = JSON.parse(raw);
78
+ } catch {
79
+ return null;
80
+ }
81
+ if (entry.expiresAt !== void 0 && Date.now() >= entry.expiresAt) {
82
+ try {
83
+ await rm(p, { force: true });
84
+ } catch {
85
+ }
86
+ return null;
87
+ }
88
+ return entry.value;
89
+ }
90
+ async set(key, value, opts) {
91
+ const p = this.#pathFor(key);
92
+ await mkdir(dirname(p), { recursive: true });
93
+ const entry = opts?.ttlMs !== void 0 ? { value, expiresAt: Date.now() + opts.ttlMs } : { value };
94
+ const tmp = `${p}.${randomUUID()}.tmp`;
95
+ try {
96
+ await writeFile(tmp, JSON.stringify(entry), "utf8");
97
+ await rename(tmp, p);
98
+ } catch (e) {
99
+ try {
100
+ await rm(tmp, { force: true });
101
+ } catch {
102
+ }
103
+ throw e;
104
+ }
105
+ }
106
+ async delete(key) {
107
+ const p = this.#pathFor(key);
108
+ try {
109
+ await rm(p, { force: true });
110
+ } catch (e) {
111
+ const code = e.code;
112
+ if (code === "ENOENT") return;
113
+ throw e;
114
+ }
115
+ }
116
+ /**
117
+ * Enumerate keys whose stored files exist under the cache root and whose
118
+ * decoded form starts with `prefix`.
119
+ *
120
+ * Returns an empty list if the root directory does not exist (cold cache).
121
+ *
122
+ * TS-W6 Wave 1: used by `availability()` to count observation months and
123
+ * climate years for a station. The file→key mapping is the inverse of
124
+ * `#pathFor` (encodeURIComponent → strip `.json` → decodeURIComponent).
125
+ */
126
+ async listKeys(prefix) {
127
+ let entries;
128
+ try {
129
+ entries = await readdir(this.#root);
130
+ } catch (e) {
131
+ const code = e.code;
132
+ if (code === "ENOENT") return Object.freeze([]);
133
+ throw e;
134
+ }
135
+ const out = [];
136
+ for (const name of entries) {
137
+ if (!name.endsWith(".json")) continue;
138
+ const encoded = name.slice(0, -".json".length);
139
+ let decoded;
140
+ try {
141
+ decoded = decodeURIComponent(encoded);
142
+ } catch {
143
+ continue;
144
+ }
145
+ if (decoded.startsWith(prefix)) {
146
+ out.push(decoded);
147
+ }
148
+ }
149
+ return Object.freeze(out);
150
+ }
151
+ async withLock(key, fn) {
152
+ const p = this.#pathFor(key);
153
+ const prev = this.#chain.get(key) ?? Promise.resolve();
154
+ const run = async () => {
155
+ await mkdir(dirname(p), { recursive: true });
156
+ const release = await properLockfile.lock(p, {
157
+ realpath: false,
158
+ retries: { retries: 5, minTimeout: 20, maxTimeout: 200 }
159
+ });
160
+ try {
161
+ return await fn();
162
+ } finally {
163
+ await release();
164
+ }
165
+ };
166
+ const next = prev.then(run, run);
167
+ const absorbed = next.then(
168
+ () => void 0,
169
+ () => void 0
170
+ );
171
+ this.#chain.set(key, absorbed);
172
+ absorbed.finally(() => {
173
+ if (this.#chain.get(key) === absorbed) this.#chain.delete(key);
174
+ });
175
+ return next;
176
+ }
177
+ };
178
+ export {
179
+ FsStore,
180
+ _resetLegacyCacheDirWarn,
181
+ defaultFsRoot
182
+ };
183
+ //# sourceMappingURL=fs-O6XR4WWW.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/internal/cache/fs.ts"],"sourcesContent":["// FsStore — node:fs/promises + proper-lockfile CacheStore for Node runtimes.\n//\n// Path layout under the configured root:\n//\n// <root>/<sanitized-key>.json\n//\n// Where `<root>` defaults to\n// `$MOSTLYRIGHT_CACHE_DIR ?? $TRADEWINDS_CACHE_DIR (legacy + warn) ??\n// $HOME/.mostlyright/cache-ts` (per TS-CACHE-02 — distinct from Python's\n// `.mostlyright/cache` so the JSON envelopes here can't shadow Python's\n// parquet files). Phase 12 W4 + review-iter2: mirrors the Python back-compat\n// shim semantics — canonical → legacy + DeprecationWarning → default.\n//\n// Atomic write: payload is written to `<path>.tmp` then renamed onto\n// `<path>` (POSIX-atomic; Windows-safe via `fs.rename`).\n//\n// withLock uses proper-lockfile against a `<path>.lock` sidecar so two\n// concurrent writers serialize per-key. The lock is taken with\n// `realpath: false` so we can lock a path whose target may not yet\n// exist.\n//\n// Divergence from Python: the Python cache is parquet + per-month station\n// files keyed by (station, year, month); TS is JSON + per-key files keyed\n// by the caller's opaque string. Station-aware key generation lives in\n// `keys.ts` (plan 03).\n\nimport { randomUUID } from \"node:crypto\";\nimport { mkdir, readFile, readdir, rename, rm, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\nimport * as properLockfile from \"proper-lockfile\";\n\nimport type { CacheEntry, CacheSetOptions, CacheStore } from \"./types.js\";\n\n/**\n * Resolve the cache root on each call (not cached at module load) so tests\n * can `vi.stubEnv(\"MOSTLYRIGHT_CACHE_DIR\", ...)` between cases without a\n * module reload.\n *\n * Resolution order (Phase 12 W4 + review-iter2 — mirrors Python shim):\n * 1. `MOSTLYRIGHT_CACHE_DIR` env var (canonical, post-Phase-12).\n * 2. `TRADEWINDS_CACHE_DIR` env var (legacy; emits a one-time deprecation\n * `console.warn`; scheduled for removal in vts-0.3).\n * 3. `~/.mostlyright/cache-ts` (per TS-CACHE-02 — DISTINCT from Python's\n * `~/.mostlyright/cache` so JSON envelopes here can't shadow Python's\n * parquet files).\n */\nlet _legacyCacheDirWarned = false;\n\nexport function defaultFsRoot(): string {\n const canonical = process.env.MOSTLYRIGHT_CACHE_DIR;\n if (canonical !== undefined && canonical.length > 0) return canonical;\n const legacy = process.env.TRADEWINDS_CACHE_DIR;\n if (legacy !== undefined && legacy.length > 0) {\n if (!_legacyCacheDirWarned) {\n console.warn(\n \"TRADEWINDS_CACHE_DIR is deprecated; use MOSTLYRIGHT_CACHE_DIR. \" +\n \"Support will be removed in vts-0.3. \" +\n \"Run: mv ~/.tradewinds ~/.mostlyright\",\n );\n _legacyCacheDirWarned = true;\n }\n return legacy;\n }\n return join(homedir(), \".mostlyright\", \"cache-ts\");\n}\n\n/** Reset the one-time TRADEWINDS_CACHE_DIR deprecation latch — TEST USE ONLY. */\nexport function _resetLegacyCacheDirWarn(): void {\n _legacyCacheDirWarned = false;\n}\n\nexport interface FsStoreOptions {\n /** Override root directory. Defaults to {@link defaultFsRoot}. */\n readonly root?: string;\n}\n\n/**\n * Node-side CacheStore. Each key maps to one JSON file under the root.\n */\nexport class FsStore implements CacheStore {\n readonly #root: string;\n // In-process per-key promise chain. proper-lockfile guarantees\n // cross-process exclusion but its retry-based contention resolution\n // is order-non-deterministic — two in-process callers racing on\n // `lock()` may acquire in either order. Layering an in-process chain\n // ensures strict FIFO for callers within the same Node process (and\n // serves as a cheap fast-path: only one of N in-process callers ever\n // actually contends with proper-lockfile).\n readonly #chain = new Map<string, Promise<unknown>>();\n\n constructor(opts: FsStoreOptions = {}) {\n this.#root = opts.root ?? defaultFsRoot();\n }\n\n /**\n * Path resolver — `key` is sanitized via `encodeURIComponent` so that\n *\n * 1. `:` `/` `\\` cannot escape the root (all percent-encoded), and\n * 2. the key → file mapping is INJECTIVE: distinct keys always map to\n * distinct files.\n *\n * Iter-13 C16 fix: the previous implementation collapsed `:` / `/` / `\\`\n * to the literal substring `\"__\"`, which is a lossy mapping — `\"a:b\"`,\n * `\"a/b\"`, and the literal `\"a__b\"` all hashed to `a__b.json`, so one\n * key's write would silently overwrite (and corrupt subsequent reads of)\n * another key. `encodeURIComponent` is bijective on string inputs and\n * filesystem-safe on every platform we ship (POSIX + Windows): the\n * characters it leaves unescaped (alphanumerics, `-._~!*'()`) are all\n * legal in NTFS, APFS, and ext4 filenames, and `%` is itself legal.\n *\n * BREAKING in v0.1.0: on-disk cache files written by any prior\n * pre-release of this package use the old `__`-replacement scheme and\n * are unreadable after upgrade. This is acceptable for a local-first\n * cache: entries are regenerated on demand from live data, and the\n * cache directory can be safely deleted by the user.\n */\n #pathFor(key: string): string {\n const safe = encodeURIComponent(key);\n return join(this.#root, `${safe}.json`);\n }\n\n async get<T = unknown>(key: string): Promise<T | null> {\n const p = this.#pathFor(key);\n let raw: string;\n try {\n raw = await readFile(p, \"utf8\");\n } catch (e: unknown) {\n const code = (e as { code?: string }).code;\n if (code === \"ENOENT\") return null;\n throw e;\n }\n let entry: CacheEntry<T>;\n try {\n entry = JSON.parse(raw) as CacheEntry<T>;\n } catch {\n // Corrupt cache entry — treat as miss; do NOT throw. Caller\n // re-fetches and overwrites.\n return null;\n }\n if (entry.expiresAt !== undefined && Date.now() >= entry.expiresAt) {\n // Lazy-evict: best-effort unlink, ignore failures.\n try {\n await rm(p, { force: true });\n } catch {\n // ignore\n }\n return null;\n }\n return entry.value as T;\n }\n\n async set<T = unknown>(key: string, value: T, opts?: CacheSetOptions): Promise<void> {\n const p = this.#pathFor(key);\n await mkdir(dirname(p), { recursive: true });\n const entry: CacheEntry<T> =\n opts?.ttlMs !== undefined ? { value, expiresAt: Date.now() + opts.ttlMs } : { value };\n // Codex iter-2 C6: use a UNIQUE temp filename per write. Two concurrent\n // `set(\"same-key\", ...)` calls would otherwise race on the shared\n // `<path>.tmp`: writer A creates `<path>.tmp` and renames it to\n // `<path>`; writer B's subsequent rename then fails with ENOENT\n // because A's rename moved B's-in-progress temp away. With a unique\n // per-write suffix, each writer owns its own temp file; rename-into-\n // place stays atomic on POSIX (last-rename-wins semantics — any of\n // the N concurrent writers' value will be the final cache contents,\n // documented at the test that covers this).\n const tmp = `${p}.${randomUUID()}.tmp`;\n try {\n await writeFile(tmp, JSON.stringify(entry), \"utf8\");\n await rename(tmp, p);\n } catch (e) {\n // Best-effort cleanup if rename failed (e.g. permissions). Don't\n // let a stale unique-temp file leak.\n try {\n await rm(tmp, { force: true });\n } catch {\n // ignore\n }\n throw e;\n }\n }\n\n async delete(key: string): Promise<void> {\n const p = this.#pathFor(key);\n try {\n await rm(p, { force: true });\n } catch (e: unknown) {\n const code = (e as { code?: string }).code;\n if (code === \"ENOENT\") return;\n throw e;\n }\n }\n\n /**\n * Enumerate keys whose stored files exist under the cache root and whose\n * decoded form starts with `prefix`.\n *\n * Returns an empty list if the root directory does not exist (cold cache).\n *\n * TS-W6 Wave 1: used by `availability()` to count observation months and\n * climate years for a station. The file→key mapping is the inverse of\n * `#pathFor` (encodeURIComponent → strip `.json` → decodeURIComponent).\n */\n async listKeys(prefix: string): Promise<ReadonlyArray<string>> {\n let entries: string[];\n try {\n entries = await readdir(this.#root);\n } catch (e: unknown) {\n const code = (e as { code?: string }).code;\n if (code === \"ENOENT\") return Object.freeze([]);\n throw e;\n }\n const out: string[] = [];\n for (const name of entries) {\n if (!name.endsWith(\".json\")) continue;\n // Ignore the proper-lockfile lock sidecars and our own in-flight\n // unique-temp files (`<key>.json.<uuid>.tmp`) — they end with `.tmp`\n // not `.json`, so the suffix filter above already excludes them.\n // The lock directories proper-lockfile creates end with `.json.lock`\n // (a directory entry), also excluded by the `.json` suffix check.\n const encoded = name.slice(0, -\".json\".length);\n let decoded: string;\n try {\n decoded = decodeURIComponent(encoded);\n } catch {\n // Defensive: skip files whose names don't decode (manual placements,\n // partial writes, etc).\n continue;\n }\n if (decoded.startsWith(prefix)) {\n out.push(decoded);\n }\n }\n return Object.freeze(out);\n }\n\n async withLock<T>(key: string, fn: () => Promise<T>): Promise<T> {\n const p = this.#pathFor(key);\n // Chain in-process callers FIFO. Cross-process exclusion is layered\n // on top via proper-lockfile inside `run()`.\n const prev = this.#chain.get(key) ?? Promise.resolve();\n const run = async (): Promise<T> => {\n await mkdir(dirname(p), { recursive: true });\n const release = await properLockfile.lock(p, {\n realpath: false,\n retries: { retries: 5, minTimeout: 20, maxTimeout: 200 },\n });\n try {\n return await fn();\n } finally {\n await release();\n }\n };\n const next = prev.then(run, run);\n // Absorber tail so the chain doesn't leak unhandled rejections.\n const absorbed = next.then(\n () => undefined,\n () => undefined,\n );\n this.#chain.set(key, absorbed);\n absorbed.finally(() => {\n if (this.#chain.get(key) === absorbed) this.#chain.delete(key);\n });\n return next;\n }\n}\n"],"mappings":";AA0BA,SAAS,kBAAkB;AAC3B,SAAS,OAAO,UAAU,SAAS,QAAQ,IAAI,iBAAiB;AAChE,SAAS,eAAe;AACxB,SAAS,SAAS,YAAY;AAE9B,YAAY,oBAAoB;AAiBhC,IAAI,wBAAwB;AAErB,SAAS,gBAAwB;AACtC,QAAM,YAAY,QAAQ,IAAI;AAC9B,MAAI,cAAc,UAAa,UAAU,SAAS,EAAG,QAAO;AAC5D,QAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,WAAW,UAAa,OAAO,SAAS,GAAG;AAC7C,QAAI,CAAC,uBAAuB;AAC1B,cAAQ;AAAA,QACN;AAAA,MAGF;AACA,8BAAwB;AAAA,IAC1B;AACA,WAAO;AAAA,EACT;AACA,SAAO,KAAK,QAAQ,GAAG,gBAAgB,UAAU;AACnD;AAGO,SAAS,2BAAiC;AAC/C,0BAAwB;AAC1B;AAUO,IAAM,UAAN,MAAoC;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,SAAS,oBAAI,IAA8B;AAAA,EAEpD,YAAY,OAAuB,CAAC,GAAG;AACrC,SAAK,QAAQ,KAAK,QAAQ,cAAc;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAwBA,SAAS,KAAqB;AAC5B,UAAM,OAAO,mBAAmB,GAAG;AACnC,WAAO,KAAK,KAAK,OAAO,GAAG,IAAI,OAAO;AAAA,EACxC;AAAA,EAEA,MAAM,IAAiB,KAAgC;AACrD,UAAM,IAAI,KAAK,SAAS,GAAG;AAC3B,QAAI;AACJ,QAAI;AACF,YAAM,MAAM,SAAS,GAAG,MAAM;AAAA,IAChC,SAAS,GAAY;AACnB,YAAM,OAAQ,EAAwB;AACtC,UAAI,SAAS,SAAU,QAAO;AAC9B,YAAM;AAAA,IACR;AACA,QAAI;AACJ,QAAI;AACF,cAAQ,KAAK,MAAM,GAAG;AAAA,IACxB,QAAQ;AAGN,aAAO;AAAA,IACT;AACA,QAAI,MAAM,cAAc,UAAa,KAAK,IAAI,KAAK,MAAM,WAAW;AAElE,UAAI;AACF,cAAM,GAAG,GAAG,EAAE,OAAO,KAAK,CAAC;AAAA,MAC7B,QAAQ;AAAA,MAER;AACA,aAAO;AAAA,IACT;AACA,WAAO,MAAM;AAAA,EACf;AAAA,EAEA,MAAM,IAAiB,KAAa,OAAU,MAAuC;AACnF,UAAM,IAAI,KAAK,SAAS,GAAG;AAC3B,UAAM,MAAM,QAAQ,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC;AAC3C,UAAM,QACJ,MAAM,UAAU,SAAY,EAAE,OAAO,WAAW,KAAK,IAAI,IAAI,KAAK,MAAM,IAAI,EAAE,MAAM;AAUtF,UAAM,MAAM,GAAG,CAAC,IAAI,WAAW,CAAC;AAChC,QAAI;AACF,YAAM,UAAU,KAAK,KAAK,UAAU,KAAK,GAAG,MAAM;AAClD,YAAM,OAAO,KAAK,CAAC;AAAA,IACrB,SAAS,GAAG;AAGV,UAAI;AACF,cAAM,GAAG,KAAK,EAAE,OAAO,KAAK,CAAC;AAAA,MAC/B,QAAQ;AAAA,MAER;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,OAAO,KAA4B;AACvC,UAAM,IAAI,KAAK,SAAS,GAAG;AAC3B,QAAI;AACF,YAAM,GAAG,GAAG,EAAE,OAAO,KAAK,CAAC;AAAA,IAC7B,SAAS,GAAY;AACnB,YAAM,OAAQ,EAAwB;AACtC,UAAI,SAAS,SAAU;AACvB,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,SAAS,QAAgD;AAC7D,QAAI;AACJ,QAAI;AACF,gBAAU,MAAM,QAAQ,KAAK,KAAK;AAAA,IACpC,SAAS,GAAY;AACnB,YAAM,OAAQ,EAAwB;AACtC,UAAI,SAAS,SAAU,QAAO,OAAO,OAAO,CAAC,CAAC;AAC9C,YAAM;AAAA,IACR;AACA,UAAM,MAAgB,CAAC;AACvB,eAAW,QAAQ,SAAS;AAC1B,UAAI,CAAC,KAAK,SAAS,OAAO,EAAG;AAM7B,YAAM,UAAU,KAAK,MAAM,GAAG,CAAC,QAAQ,MAAM;AAC7C,UAAI;AACJ,UAAI;AACF,kBAAU,mBAAmB,OAAO;AAAA,MACtC,QAAQ;AAGN;AAAA,MACF;AACA,UAAI,QAAQ,WAAW,MAAM,GAAG;AAC9B,YAAI,KAAK,OAAO;AAAA,MAClB;AAAA,IACF;AACA,WAAO,OAAO,OAAO,GAAG;AAAA,EAC1B;AAAA,EAEA,MAAM,SAAY,KAAa,IAAkC;AAC/D,UAAM,IAAI,KAAK,SAAS,GAAG;AAG3B,UAAM,OAAO,KAAK,OAAO,IAAI,GAAG,KAAK,QAAQ,QAAQ;AACrD,UAAM,MAAM,YAAwB;AAClC,YAAM,MAAM,QAAQ,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC;AAC3C,YAAM,UAAU,MAAqB,oBAAK,GAAG;AAAA,QAC3C,UAAU;AAAA,QACV,SAAS,EAAE,SAAS,GAAG,YAAY,IAAI,YAAY,IAAI;AAAA,MACzD,CAAC;AACD,UAAI;AACF,eAAO,MAAM,GAAG;AAAA,MAClB,UAAE;AACA,cAAM,QAAQ;AAAA,MAChB;AAAA,IACF;AACA,UAAM,OAAO,KAAK,KAAK,KAAK,GAAG;AAE/B,UAAM,WAAW,KAAK;AAAA,MACpB,MAAM;AAAA,MACN,MAAM;AAAA,IACR;AACA,SAAK,OAAO,IAAI,KAAK,QAAQ;AAC7B,aAAS,QAAQ,MAAM;AACrB,UAAI,KAAK,OAAO,IAAI,GAAG,MAAM,SAAU,MAAK,OAAO,OAAO,GAAG;AAAA,IAC/D,CAAC;AACD,WAAO;AAAA,EACT;AACF;","names":[]}
@@ -0,0 +1,191 @@
1
+ /** Cache entry envelope with optional TTL. */
2
+ interface CacheEntry<T = unknown> {
3
+ readonly value: T;
4
+ /** Epoch ms when the entry expires. Absence = no expiry. */
5
+ readonly expiresAt?: number;
6
+ }
7
+ /** Optional setters for cache writes. */
8
+ interface CacheSetOptions {
9
+ /** Time-to-live in milliseconds. Implementations may honor or ignore. */
10
+ readonly ttlMs?: number;
11
+ }
12
+ /**
13
+ * Pluggable key/value cache contract used throughout the SDK.
14
+ *
15
+ * All methods are async — concrete implementations may resolve immediately
16
+ * (MemoryStore) or do I/O (FsStore / IndexedDBStore).
17
+ *
18
+ * Semantic contract:
19
+ * - `get<T>(key)` returns the stored value or `null` on miss. NEVER throws
20
+ * on miss.
21
+ * - `set<T>(key, value, opts?)` overwrites. ttlMs is implementation-honored
22
+ * (MemoryStore + IndexedDBStore honor it; FsStore ignores in v0.1).
23
+ * - `delete(key)` is a no-op on miss; returns void.
24
+ * - `withLock<T>(key, fn)` runs `fn` under a key-scoped exclusive lock and
25
+ * releases on settle (resolve OR throw). Nested calls to the same key
26
+ * serialize; calls to different keys MAY run in parallel.
27
+ */
28
+ interface CacheStore {
29
+ get<T = unknown>(key: string): Promise<T | null>;
30
+ set<T = unknown>(key: string, value: T, opts?: CacheSetOptions): Promise<void>;
31
+ delete(key: string): Promise<void>;
32
+ withLock<T>(key: string, fn: () => Promise<T>): Promise<T>;
33
+ }
34
+ /**
35
+ * Canonical lock identifier for a given cache key.
36
+ *
37
+ * Pure — same key → same lock id. Used by FsStore (proper-lockfile sidecar)
38
+ * and IndexedDBStore (`navigator.locks.request(...)` name).
39
+ */
40
+ declare function lockKeyFor(key: string): string;
41
+
42
+ /**
43
+ * In-memory cache. Per-instance state; two MemoryStore instances do NOT
44
+ * share state.
45
+ *
46
+ * - Values cloned via `structuredClone` so post-`set` mutation can't leak.
47
+ * - ttlMs honored with lazy eviction on `get`.
48
+ * - withLock serializes nested calls via a per-key promise chain.
49
+ */
50
+ declare class MemoryStore implements CacheStore {
51
+ #private;
52
+ get<T = unknown>(key: string): Promise<T | null>;
53
+ set<T = unknown>(key: string, value: T, opts?: CacheSetOptions): Promise<void>;
54
+ delete(key: string): Promise<void>;
55
+ /**
56
+ * Enumerate live (non-expired) keys with the given prefix.
57
+ *
58
+ * TS-W6 Wave 1: `availability()` uses this to count cached observation
59
+ * months and climate years per station. Expired entries are evicted as a
60
+ * side effect (same lazy-eviction policy as `.get`).
61
+ */
62
+ listKeys(prefix: string): Promise<ReadonlyArray<string>>;
63
+ withLock<T>(key: string, fn: () => Promise<T>): Promise<T>;
64
+ }
65
+
66
+ /** Canonical DB name. Re-exported via the cache barrel. */
67
+ declare const DB_NAME = "mostlyright-cache-v1";
68
+ interface IndexedDBStoreOptions {
69
+ /** Override the DB name. Tests pass unique values per case so they don't pollute each other. */
70
+ readonly dbName?: string;
71
+ }
72
+ /**
73
+ * Browser CacheStore backed by IndexedDB (via idb) + Web Locks API.
74
+ *
75
+ * When `navigator.locks` is unavailable (jsdom, edge runtimes without
76
+ * Web Locks), falls back to a per-key in-process promise chain.
77
+ */
78
+ declare class IndexedDBStore implements CacheStore {
79
+ #private;
80
+ constructor(opts?: IndexedDBStoreOptions);
81
+ get<T = unknown>(key: string): Promise<T | null>;
82
+ set<T = unknown>(key: string, value: T, opts?: CacheSetOptions): Promise<void>;
83
+ delete(key: string): Promise<void>;
84
+ /**
85
+ * Enumerate keys with the given prefix using IndexedDB's bounded range
86
+ * query. Live keys only — expired entries are lazy-evicted on read by
87
+ * `get()`, so a stale-but-not-yet-evicted entry can appear here; callers
88
+ * who care about expiration should `get()` to confirm.
89
+ *
90
+ * TS-W6 Wave 1: `availability()` uses this to count observation months and
91
+ * climate years for a station.
92
+ */
93
+ listKeys(prefix: string): Promise<ReadonlyArray<string>>;
94
+ withLock<T>(key: string, fn: () => Promise<T>): Promise<T>;
95
+ }
96
+
97
+ /**
98
+ * True iff `(year, month)` is the current LST month for `station`.
99
+ *
100
+ * Mirrors Python `_is_current_lst_month`. The current month is mutable
101
+ * (observations still arriving) — caching it would serve stale data.
102
+ */
103
+ declare function shouldSkipCacheForCurrentLstMonth(station: string, year: number, month: number, now?: Date): boolean;
104
+ /**
105
+ * True iff `year` is the current LST year for `station`. Annual analog of
106
+ * the monthly variant — gates the climate cache.
107
+ */
108
+ declare function shouldSkipCacheForCurrentLstYear(station: string, year: number, now?: Date): boolean;
109
+ /**
110
+ * True iff `(year, month)` is a **strictly past** UTC month relative to
111
+ * `now` — i.e. cacheable on the strictest possible temporal axis.
112
+ *
113
+ * iter-12 C14: `shouldSkipCacheForCurrentLstMonth` and `isMonthVolatile`
114
+ * (lives in `meta/src/research.ts`) only catch the *current* LST month
115
+ * and the immediate post-month volatile tail. Both predicates return
116
+ * false for months that lie in the FUTURE relative to `now`, or for the
117
+ * current UTC month when the station's LST is still in the prior UTC
118
+ * month (negative tz offsets near UTC midnight). An empty / partial
119
+ * fetch for such a month would be persisted and later served as
120
+ * "complete." `isWritableMonth` is a stricter additional gate: it
121
+ * requires the (year, month) to be lexicographically less than the
122
+ * UTC current month, so neither future months nor the partial current
123
+ * UTC month are ever cacheable — regardless of any station's LST.
124
+ *
125
+ * Mirrors Python `cache.py:_is_current_lst_month`'s implicit invariant
126
+ * (Python paths use parquet-on-disk which can't be written for future
127
+ * dates because the cache root never spawns those years). TS callers
128
+ * MUST gate cache reads AND writes on this predicate before applying
129
+ * the LST / volatile-window gates.
130
+ */
131
+ declare function isWritableMonth(year: number, month: number, now: Date): boolean;
132
+ /**
133
+ * True iff `year` is a **strictly past** UTC year relative to `now` —
134
+ * the annual analog of `isWritableMonth`.
135
+ *
136
+ * iter-12 C15: `shouldSkipCacheForCurrentLstYear` only catches the
137
+ * current LST year. It misses (a) future years, which would silently
138
+ * cache empty/incomplete data, and (b) the UTC Jan-1 boundary window
139
+ * where the station's LST is still in the prior calendar year (negative
140
+ * tz offsets) but the UTC year has already rolled over — without this
141
+ * gate the new UTC year, which is mutable, could be written. Stricter
142
+ * additional gate: require `year < now.getUTCFullYear()`. TS callers
143
+ * MUST gate cache reads AND writes on this predicate before applying
144
+ * the LST / volatile-window gates.
145
+ */
146
+ declare function isWritableYear(year: number, now: Date): boolean;
147
+ /**
148
+ * True iff `source` ends with `.live`.
149
+ *
150
+ * Mirrors Python `_is_live_source` byte-equivalently — accepts null /
151
+ * undefined / empty (returns false in all three cases).
152
+ */
153
+ declare function isLiveSource(source: string | null | undefined): boolean;
154
+ /**
155
+ * **TS-NEW** addition per TS-CACHE-02: archive endpoints within `days` days
156
+ * of `archiveAsOf` are treated as volatile (some sources amend their
157
+ * published data for ~30 days post-event). NOT a Python port today — file
158
+ * a CROSS-SDK-SYNC parity ticket if Python adopts it.
159
+ *
160
+ * Returns true iff `eventDate` falls within `[archiveAsOf - days, archiveAsOf]`
161
+ * (inclusive at both endpoints — an event exactly `days` days before
162
+ * `archiveAsOf` is still volatile and MUST be re-fetched).
163
+ *
164
+ * Events AFTER `archiveAsOf` are never volatile by this rule (deltaDays < 0).
165
+ */
166
+ declare function isWithinVolatileWindow(eventDate: string, archiveAsOf: string, days?: number): boolean;
167
+
168
+ /**
169
+ * Build the canonical observations cache key.
170
+ *
171
+ * Examples:
172
+ * `cacheKeyForObservations("KNYC", 2025, 1)` →
173
+ * `"mostlyright:v1:observations:KNYC:2025:01"`.
174
+ * `cacheKeyForObservations("KNYC", 2025, 1, "iem")` →
175
+ * `"mostlyright:v1:observations:KNYC:2025:01:iem"`.
176
+ *
177
+ * The `source` segment (optional, lowercase alphanumeric / hyphen /
178
+ * underscore) namespaces per-source pre-merge chunks so IEM ASOS and
179
+ * GHCNh writes for the same `(station, year, month)` do not collide.
180
+ * Omit for back-compat (sentinel preloads, fixture replays).
181
+ */
182
+ declare function cacheKeyForObservations(station: string, year: number, month: number, source?: string): string;
183
+ /**
184
+ * Build the canonical climate cache key (annual).
185
+ *
186
+ * Example: `cacheKeyForClimate("KNYC", 2025)` →
187
+ * `"mostlyright:v1:climate:KNYC:2025"`.
188
+ */
189
+ declare function cacheKeyForClimate(station: string, year: number): string;
190
+
191
+ export { type CacheEntry as C, DB_NAME as D, IndexedDBStore as I, MemoryStore as M, type CacheSetOptions as a, type CacheStore as b, type IndexedDBStoreOptions as c, cacheKeyForClimate as d, cacheKeyForObservations as e, isWithinVolatileWindow as f, isWritableMonth as g, isWritableYear as h, isLiveSource as i, shouldSkipCacheForCurrentLstYear as j, lockKeyFor as l, shouldSkipCacheForCurrentLstMonth as s };
@@ -0,0 +1,191 @@
1
+ /** Cache entry envelope with optional TTL. */
2
+ interface CacheEntry<T = unknown> {
3
+ readonly value: T;
4
+ /** Epoch ms when the entry expires. Absence = no expiry. */
5
+ readonly expiresAt?: number;
6
+ }
7
+ /** Optional setters for cache writes. */
8
+ interface CacheSetOptions {
9
+ /** Time-to-live in milliseconds. Implementations may honor or ignore. */
10
+ readonly ttlMs?: number;
11
+ }
12
+ /**
13
+ * Pluggable key/value cache contract used throughout the SDK.
14
+ *
15
+ * All methods are async — concrete implementations may resolve immediately
16
+ * (MemoryStore) or do I/O (FsStore / IndexedDBStore).
17
+ *
18
+ * Semantic contract:
19
+ * - `get<T>(key)` returns the stored value or `null` on miss. NEVER throws
20
+ * on miss.
21
+ * - `set<T>(key, value, opts?)` overwrites. ttlMs is implementation-honored
22
+ * (MemoryStore + IndexedDBStore honor it; FsStore ignores in v0.1).
23
+ * - `delete(key)` is a no-op on miss; returns void.
24
+ * - `withLock<T>(key, fn)` runs `fn` under a key-scoped exclusive lock and
25
+ * releases on settle (resolve OR throw). Nested calls to the same key
26
+ * serialize; calls to different keys MAY run in parallel.
27
+ */
28
+ interface CacheStore {
29
+ get<T = unknown>(key: string): Promise<T | null>;
30
+ set<T = unknown>(key: string, value: T, opts?: CacheSetOptions): Promise<void>;
31
+ delete(key: string): Promise<void>;
32
+ withLock<T>(key: string, fn: () => Promise<T>): Promise<T>;
33
+ }
34
+ /**
35
+ * Canonical lock identifier for a given cache key.
36
+ *
37
+ * Pure — same key → same lock id. Used by FsStore (proper-lockfile sidecar)
38
+ * and IndexedDBStore (`navigator.locks.request(...)` name).
39
+ */
40
+ declare function lockKeyFor(key: string): string;
41
+
42
+ /**
43
+ * In-memory cache. Per-instance state; two MemoryStore instances do NOT
44
+ * share state.
45
+ *
46
+ * - Values cloned via `structuredClone` so post-`set` mutation can't leak.
47
+ * - ttlMs honored with lazy eviction on `get`.
48
+ * - withLock serializes nested calls via a per-key promise chain.
49
+ */
50
+ declare class MemoryStore implements CacheStore {
51
+ #private;
52
+ get<T = unknown>(key: string): Promise<T | null>;
53
+ set<T = unknown>(key: string, value: T, opts?: CacheSetOptions): Promise<void>;
54
+ delete(key: string): Promise<void>;
55
+ /**
56
+ * Enumerate live (non-expired) keys with the given prefix.
57
+ *
58
+ * TS-W6 Wave 1: `availability()` uses this to count cached observation
59
+ * months and climate years per station. Expired entries are evicted as a
60
+ * side effect (same lazy-eviction policy as `.get`).
61
+ */
62
+ listKeys(prefix: string): Promise<ReadonlyArray<string>>;
63
+ withLock<T>(key: string, fn: () => Promise<T>): Promise<T>;
64
+ }
65
+
66
+ /** Canonical DB name. Re-exported via the cache barrel. */
67
+ declare const DB_NAME = "mostlyright-cache-v1";
68
+ interface IndexedDBStoreOptions {
69
+ /** Override the DB name. Tests pass unique values per case so they don't pollute each other. */
70
+ readonly dbName?: string;
71
+ }
72
+ /**
73
+ * Browser CacheStore backed by IndexedDB (via idb) + Web Locks API.
74
+ *
75
+ * When `navigator.locks` is unavailable (jsdom, edge runtimes without
76
+ * Web Locks), falls back to a per-key in-process promise chain.
77
+ */
78
+ declare class IndexedDBStore implements CacheStore {
79
+ #private;
80
+ constructor(opts?: IndexedDBStoreOptions);
81
+ get<T = unknown>(key: string): Promise<T | null>;
82
+ set<T = unknown>(key: string, value: T, opts?: CacheSetOptions): Promise<void>;
83
+ delete(key: string): Promise<void>;
84
+ /**
85
+ * Enumerate keys with the given prefix using IndexedDB's bounded range
86
+ * query. Live keys only — expired entries are lazy-evicted on read by
87
+ * `get()`, so a stale-but-not-yet-evicted entry can appear here; callers
88
+ * who care about expiration should `get()` to confirm.
89
+ *
90
+ * TS-W6 Wave 1: `availability()` uses this to count observation months and
91
+ * climate years for a station.
92
+ */
93
+ listKeys(prefix: string): Promise<ReadonlyArray<string>>;
94
+ withLock<T>(key: string, fn: () => Promise<T>): Promise<T>;
95
+ }
96
+
97
+ /**
98
+ * True iff `(year, month)` is the current LST month for `station`.
99
+ *
100
+ * Mirrors Python `_is_current_lst_month`. The current month is mutable
101
+ * (observations still arriving) — caching it would serve stale data.
102
+ */
103
+ declare function shouldSkipCacheForCurrentLstMonth(station: string, year: number, month: number, now?: Date): boolean;
104
+ /**
105
+ * True iff `year` is the current LST year for `station`. Annual analog of
106
+ * the monthly variant — gates the climate cache.
107
+ */
108
+ declare function shouldSkipCacheForCurrentLstYear(station: string, year: number, now?: Date): boolean;
109
+ /**
110
+ * True iff `(year, month)` is a **strictly past** UTC month relative to
111
+ * `now` — i.e. cacheable on the strictest possible temporal axis.
112
+ *
113
+ * iter-12 C14: `shouldSkipCacheForCurrentLstMonth` and `isMonthVolatile`
114
+ * (lives in `meta/src/research.ts`) only catch the *current* LST month
115
+ * and the immediate post-month volatile tail. Both predicates return
116
+ * false for months that lie in the FUTURE relative to `now`, or for the
117
+ * current UTC month when the station's LST is still in the prior UTC
118
+ * month (negative tz offsets near UTC midnight). An empty / partial
119
+ * fetch for such a month would be persisted and later served as
120
+ * "complete." `isWritableMonth` is a stricter additional gate: it
121
+ * requires the (year, month) to be lexicographically less than the
122
+ * UTC current month, so neither future months nor the partial current
123
+ * UTC month are ever cacheable — regardless of any station's LST.
124
+ *
125
+ * Mirrors Python `cache.py:_is_current_lst_month`'s implicit invariant
126
+ * (Python paths use parquet-on-disk which can't be written for future
127
+ * dates because the cache root never spawns those years). TS callers
128
+ * MUST gate cache reads AND writes on this predicate before applying
129
+ * the LST / volatile-window gates.
130
+ */
131
+ declare function isWritableMonth(year: number, month: number, now: Date): boolean;
132
+ /**
133
+ * True iff `year` is a **strictly past** UTC year relative to `now` —
134
+ * the annual analog of `isWritableMonth`.
135
+ *
136
+ * iter-12 C15: `shouldSkipCacheForCurrentLstYear` only catches the
137
+ * current LST year. It misses (a) future years, which would silently
138
+ * cache empty/incomplete data, and (b) the UTC Jan-1 boundary window
139
+ * where the station's LST is still in the prior calendar year (negative
140
+ * tz offsets) but the UTC year has already rolled over — without this
141
+ * gate the new UTC year, which is mutable, could be written. Stricter
142
+ * additional gate: require `year < now.getUTCFullYear()`. TS callers
143
+ * MUST gate cache reads AND writes on this predicate before applying
144
+ * the LST / volatile-window gates.
145
+ */
146
+ declare function isWritableYear(year: number, now: Date): boolean;
147
+ /**
148
+ * True iff `source` ends with `.live`.
149
+ *
150
+ * Mirrors Python `_is_live_source` byte-equivalently — accepts null /
151
+ * undefined / empty (returns false in all three cases).
152
+ */
153
+ declare function isLiveSource(source: string | null | undefined): boolean;
154
+ /**
155
+ * **TS-NEW** addition per TS-CACHE-02: archive endpoints within `days` days
156
+ * of `archiveAsOf` are treated as volatile (some sources amend their
157
+ * published data for ~30 days post-event). NOT a Python port today — file
158
+ * a CROSS-SDK-SYNC parity ticket if Python adopts it.
159
+ *
160
+ * Returns true iff `eventDate` falls within `[archiveAsOf - days, archiveAsOf]`
161
+ * (inclusive at both endpoints — an event exactly `days` days before
162
+ * `archiveAsOf` is still volatile and MUST be re-fetched).
163
+ *
164
+ * Events AFTER `archiveAsOf` are never volatile by this rule (deltaDays < 0).
165
+ */
166
+ declare function isWithinVolatileWindow(eventDate: string, archiveAsOf: string, days?: number): boolean;
167
+
168
+ /**
169
+ * Build the canonical observations cache key.
170
+ *
171
+ * Examples:
172
+ * `cacheKeyForObservations("KNYC", 2025, 1)` →
173
+ * `"mostlyright:v1:observations:KNYC:2025:01"`.
174
+ * `cacheKeyForObservations("KNYC", 2025, 1, "iem")` →
175
+ * `"mostlyright:v1:observations:KNYC:2025:01:iem"`.
176
+ *
177
+ * The `source` segment (optional, lowercase alphanumeric / hyphen /
178
+ * underscore) namespaces per-source pre-merge chunks so IEM ASOS and
179
+ * GHCNh writes for the same `(station, year, month)` do not collide.
180
+ * Omit for back-compat (sentinel preloads, fixture replays).
181
+ */
182
+ declare function cacheKeyForObservations(station: string, year: number, month: number, source?: string): string;
183
+ /**
184
+ * Build the canonical climate cache key (annual).
185
+ *
186
+ * Example: `cacheKeyForClimate("KNYC", 2025)` →
187
+ * `"mostlyright:v1:climate:KNYC:2025"`.
188
+ */
189
+ declare function cacheKeyForClimate(station: string, year: number): string;
190
+
191
+ export { type CacheEntry as C, DB_NAME as D, IndexedDBStore as I, MemoryStore as M, type CacheSetOptions as a, type CacheStore as b, type IndexedDBStoreOptions as c, cacheKeyForClimate as d, cacheKeyForObservations as e, isWithinVolatileWindow as f, isWritableMonth as g, isWritableYear as h, isLiveSource as i, shouldSkipCacheForCurrentLstYear as j, lockKeyFor as l, shouldSkipCacheForCurrentLstMonth as s };
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/internal/merge/index.ts
21
+ var merge_exports = {};
22
+ __export(merge_exports, {
23
+ SOURCE_PRIORITY: () => SOURCE_PRIORITY,
24
+ mergeClimate: () => mergeClimate,
25
+ mergeObservations: () => mergeObservations
26
+ });
27
+ module.exports = __toCommonJS(merge_exports);
28
+
29
+ // src/internal/merge/observations.ts
30
+ var SOURCE_PRIORITY = Object.freeze({
31
+ awc: 3,
32
+ iem: 2,
33
+ ghcnh: 1
34
+ });
35
+ function mergeObservations(rows) {
36
+ const best = /* @__PURE__ */ new Map();
37
+ for (const row of rows) {
38
+ const key = `${row.station_code}\0${row.observed_at}\0${row.observation_type}`;
39
+ const existing = best.get(key);
40
+ if (existing === void 0) {
41
+ best.set(key, row);
42
+ continue;
43
+ }
44
+ const priority = SOURCE_PRIORITY[row.source] ?? 0;
45
+ const existingPriority = SOURCE_PRIORITY[existing.source] ?? 0;
46
+ if (priority > existingPriority) {
47
+ best.set(key, row);
48
+ }
49
+ }
50
+ return Array.from(best.values());
51
+ }
52
+
53
+ // src/internal/merge/climate.ts
54
+ function mergeClimate(rows) {
55
+ const best = /* @__PURE__ */ new Map();
56
+ for (const row of rows) {
57
+ const key = `${row.station_code}\0${row.observation_date}`;
58
+ const existing = best.get(key);
59
+ if (existing === void 0) {
60
+ best.set(key, row);
61
+ continue;
62
+ }
63
+ if (row.report_type_priority > existing.report_type_priority) {
64
+ best.set(key, row);
65
+ }
66
+ }
67
+ return Array.from(best.values());
68
+ }
69
+ // Annotate the CommonJS export names for ESM import in node:
70
+ 0 && (module.exports = {
71
+ SOURCE_PRIORITY,
72
+ mergeClimate,
73
+ mergeObservations
74
+ });
75
+ //# sourceMappingURL=index.cjs.map