@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.
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/discovery/index.cjs +1646 -0
- package/dist/discovery/index.cjs.map +1 -0
- package/dist/discovery/index.d.cts +313 -0
- package/dist/discovery/index.d.ts +313 -0
- package/dist/discovery/index.mjs +1609 -0
- package/dist/discovery/index.mjs.map +1 -0
- package/dist/formats/index.cjs +498 -0
- package/dist/formats/index.cjs.map +1 -0
- package/dist/formats/index.d.cts +97 -0
- package/dist/formats/index.d.ts +97 -0
- package/dist/formats/index.mjs +465 -0
- package/dist/formats/index.mjs.map +1 -0
- package/dist/index.cjs +1624 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +559 -0
- package/dist/index.d.ts +559 -0
- package/dist/index.global.js +1582 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.mjs +1557 -0
- package/dist/index.mjs.map +1 -0
- package/dist/internal/bounds.cjs +125 -0
- package/dist/internal/bounds.cjs.map +1 -0
- package/dist/internal/bounds.d.cts +36 -0
- package/dist/internal/bounds.d.ts +36 -0
- package/dist/internal/bounds.mjs +81 -0
- package/dist/internal/bounds.mjs.map +1 -0
- package/dist/internal/cache/fs.cjs +217 -0
- package/dist/internal/cache/fs.cjs.map +1 -0
- package/dist/internal/cache/fs.d.cts +57 -0
- package/dist/internal/cache/fs.d.ts +57 -0
- package/dist/internal/cache/fs.mjs +179 -0
- package/dist/internal/cache/fs.mjs.map +1 -0
- package/dist/internal/cache/index.browser.cjs +1184 -0
- package/dist/internal/cache/index.browser.cjs.map +1 -0
- package/dist/internal/cache/index.browser.d.cts +20 -0
- package/dist/internal/cache/index.browser.d.ts +20 -0
- package/dist/internal/cache/index.browser.mjs +36 -0
- package/dist/internal/cache/index.browser.mjs.map +1 -0
- package/dist/internal/cache/index.cjs +1389 -0
- package/dist/internal/cache/index.cjs.map +1 -0
- package/dist/internal/cache/index.d.cts +16 -0
- package/dist/internal/cache/index.d.ts +16 -0
- package/dist/internal/cache/index.mjs +40 -0
- package/dist/internal/cache/index.mjs.map +1 -0
- package/dist/internal/chunk-PKJXHY27.mjs +1137 -0
- package/dist/internal/chunk-PKJXHY27.mjs.map +1 -0
- package/dist/internal/convert.cjs +161 -0
- package/dist/internal/convert.cjs.map +1 -0
- package/dist/internal/convert.d.cts +44 -0
- package/dist/internal/convert.d.ts +44 -0
- package/dist/internal/convert.mjs +117 -0
- package/dist/internal/convert.mjs.map +1 -0
- package/dist/internal/fs-O6XR4WWW.mjs +183 -0
- package/dist/internal/fs-O6XR4WWW.mjs.map +1 -0
- package/dist/internal/keys-B7C8C88N.d.cts +191 -0
- package/dist/internal/keys-B7C8C88N.d.ts +191 -0
- package/dist/internal/merge/index.cjs +75 -0
- package/dist/internal/merge/index.cjs.map +1 -0
- package/dist/internal/merge/index.d.cts +74 -0
- package/dist/internal/merge/index.d.ts +74 -0
- package/dist/internal/merge/index.mjs +46 -0
- package/dist/internal/merge/index.mjs.map +1 -0
- package/dist/internal/pairs.cjs +328 -0
- package/dist/internal/pairs.cjs.map +1 -0
- package/dist/internal/pairs.d.cts +105 -0
- package/dist/internal/pairs.d.ts +105 -0
- package/dist/internal/pairs.mjs +298 -0
- package/dist/internal/pairs.mjs.map +1 -0
- package/dist/qc/index.cjs +247 -0
- package/dist/qc/index.cjs.map +1 -0
- package/dist/qc/index.d.cts +140 -0
- package/dist/qc/index.d.ts +140 -0
- package/dist/qc/index.mjs +212 -0
- package/dist/qc/index.mjs.map +1 -0
- package/dist/temporal/index.cjs +504 -0
- package/dist/temporal/index.cjs.map +1 -0
- package/dist/temporal/index.d.cts +121 -0
- package/dist/temporal/index.d.ts +121 -0
- package/dist/temporal/index.mjs +474 -0
- package/dist/temporal/index.mjs.map +1 -0
- package/dist/transforms/index.cjs +399 -0
- package/dist/transforms/index.cjs.map +1 -0
- package/dist/transforms/index.d.cts +193 -0
- package/dist/transforms/index.d.ts +193 -0
- package/dist/transforms/index.mjs +362 -0
- package/dist/transforms/index.mjs.map +1 -0
- package/dist/validator.cjs +1870 -0
- package/dist/validator.cjs.map +1 -0
- package/dist/validator.d.cts +30 -0
- package/dist/validator.d.ts +30 -0
- package/dist/validator.mjs +1843 -0
- package/dist/validator.mjs.map +1 -0
- 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":"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","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
|