@jpzip/jpzip 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +19 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +19 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -75,27 +75,29 @@ async function fetchJSON(url, opts = {}) {
|
|
|
75
75
|
if (attempt > 0) {
|
|
76
76
|
await sleep(BASE_DELAY_MS * 2 ** attempt);
|
|
77
77
|
}
|
|
78
|
+
const init = {
|
|
79
|
+
method: "GET",
|
|
80
|
+
headers: { Accept: "application/json" }
|
|
81
|
+
};
|
|
82
|
+
if (opts.signal !== void 0) init.signal = opts.signal;
|
|
83
|
+
if (opts.noCache) init.cache = "no-cache";
|
|
84
|
+
let res;
|
|
78
85
|
try {
|
|
79
|
-
|
|
80
|
-
method: "GET",
|
|
81
|
-
headers: { Accept: "application/json" }
|
|
82
|
-
};
|
|
83
|
-
if (opts.signal !== void 0) init.signal = opts.signal;
|
|
84
|
-
if (opts.noCache) init.cache = "no-cache";
|
|
85
|
-
const res = await f(url, init);
|
|
86
|
-
if (res.status === 404) return null;
|
|
87
|
-
if (res.status >= 500) {
|
|
88
|
-
lastErr = new Error(`jpzip: ${url} returned ${res.status}`);
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if (!res.ok) {
|
|
92
|
-
throw new Error(`jpzip: ${url} returned ${res.status}`);
|
|
93
|
-
}
|
|
94
|
-
return await res.json();
|
|
86
|
+
res = await f(url, init);
|
|
95
87
|
} catch (err) {
|
|
96
|
-
lastErr = err;
|
|
97
88
|
if (err instanceof DOMException && err.name === "AbortError") throw err;
|
|
89
|
+
lastErr = err;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (res.status === 404) return null;
|
|
93
|
+
if (res.status >= 500) {
|
|
94
|
+
lastErr = new Error(`jpzip: ${url} returned ${res.status}`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!res.ok) {
|
|
98
|
+
throw new Error(`jpzip: ${url} returned ${res.status}`);
|
|
98
99
|
}
|
|
100
|
+
return await res.json();
|
|
99
101
|
}
|
|
100
102
|
throw lastErr instanceof Error ? lastErr : new Error(`jpzip: fetch failed for ${url}: ${String(lastErr)}`);
|
|
101
103
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/cache.ts","../src/fetch.ts","../src/client.ts"],"sourcesContent":["export { JpzipClient, DEFAULT_BASE_URL, type JpzipClientOptions } from './client.js';\nexport { MemoryLRU, type PersistentCache } from './cache.js';\nexport type { ZipcodeEntry, Town, Meta, ZipcodeDict, Endpoints } from './types.js';\n\nimport { JpzipClient } from './client.js';\nimport type { Meta, ZipcodeDict, ZipcodeEntry } from './types.js';\n\n/**\n * The functional helpers below all delegate to a lazily-initialized default\n * client (L1 cache only, default base URL). For multiple SDK instances or\n * an L2 cache, construct `new JpzipClient({...})` yourself.\n */\n\nlet _default: JpzipClient | null = null;\nfunction defaultClient(): JpzipClient {\n if (_default === null) _default = new JpzipClient();\n return _default;\n}\n\n/** Reset the singleton (mainly for tests). */\nexport function _resetDefaultClient(): void {\n _default = null;\n}\n\n/** Configure the singleton's options. Subsequent calls re-create the client. */\nexport function configure(options: ConstructorParameters<typeof JpzipClient>[0]): void {\n _default = new JpzipClient(options);\n}\n\nexport const lookup = (zipcode: string): Promise<ZipcodeEntry | null> =>\n defaultClient().lookup(zipcode);\n\nexport const lookupGroup = (prefix: string): Promise<ZipcodeDict> =>\n defaultClient().lookupGroup(prefix);\n\nexport const lookupAll = (): Promise<ZipcodeDict> => defaultClient().lookupAll();\n\nexport const preload = (opts: Parameters<JpzipClient['preload']>[0]): Promise<void> =>\n defaultClient().preload(opts);\n\nexport const getMeta = (): Promise<Meta | null> => defaultClient().getMeta();\n\n/** Helper: returns true iff `zip` is a syntactically valid 7-digit zipcode. */\nexport function isValidZipcode(zip: string): boolean {\n return /^\\d{7}$/.test(zip);\n}\n","/**\n * Three-layer caching per spec §6.4:\n *\n * - L1 (MemoryLRU) — always on, bounded\n * - L2 (PersistentCache) — opt-in via constructor\n * - L3 (HTTP/fetch) — out of SDK scope\n */\n\nimport type { ZipcodeDict } from './types.js';\n\n/** Public interface user-supplied L2 caches must implement. */\nexport interface PersistentCache {\n get(key: string): Promise<Uint8Array | null>;\n set(key: string, value: Uint8Array): Promise<void>;\n delete(key: string): Promise<void>;\n clear(): Promise<void>;\n}\n\n/** Bounded in-memory LRU keyed by URL path. Values are pre-parsed dicts. */\nexport class MemoryLRU {\n private readonly max: number;\n private readonly map = new Map<string, ZipcodeDict>();\n\n constructor(max = 100) {\n this.max = Math.max(1, max);\n }\n\n get(key: string): ZipcodeDict | undefined {\n const value = this.map.get(key);\n if (value === undefined) return undefined;\n // refresh recency by re-inserting at the tail\n this.map.delete(key);\n this.map.set(key, value);\n return value;\n }\n\n set(key: string, value: ZipcodeDict): void {\n if (this.map.has(key)) {\n this.map.delete(key);\n } else if (this.map.size >= this.max) {\n const oldestKey = this.map.keys().next().value;\n if (oldestKey !== undefined) this.map.delete(oldestKey);\n }\n this.map.set(key, value);\n }\n\n clear(): void {\n this.map.clear();\n }\n\n get size(): number {\n return this.map.size;\n }\n}\n","/** HTTP helpers with exponential-backoff retry for transient 5xx and network errors. */\n\nimport type { ZipcodeDict, Meta } from './types.js';\n\nconst MAX_RETRIES = 3;\nconst BASE_DELAY_MS = 200;\n\nexport interface FetchOptions {\n /** Override the global fetch (mainly for tests). */\n fetch?: typeof fetch;\n /** Pass-through to fetch(). */\n signal?: AbortSignal;\n /** Forces no-cache; useful when the user explicitly refreshes. */\n noCache?: boolean;\n}\n\n/**\n * fetchJSON returns the parsed JSON body for url. On 404 it returns null.\n * On 5xx / network errors it retries up to MAX_RETRIES with exponential backoff.\n * Other 4xx errors throw.\n */\nexport async function fetchJSON<T>(url: string, opts: FetchOptions = {}): Promise<T | null> {\n const f = opts.fetch ?? fetch;\n\n let lastErr: unknown;\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n if (attempt > 0) {\n await sleep(BASE_DELAY_MS * 2 ** attempt);\n }\n try {\n const init: RequestInit = {\n method: 'GET',\n headers: { Accept: 'application/json' },\n };\n if (opts.signal !== undefined) init.signal = opts.signal;\n if (opts.noCache) init.cache = 'no-cache';\n\n const res = await f(url, init);\n if (res.status === 404) return null;\n if (res.status >= 500) {\n lastErr = new Error(`jpzip: ${url} returned ${res.status}`);\n continue;\n }\n if (!res.ok) {\n throw new Error(`jpzip: ${url} returned ${res.status}`);\n }\n return (await res.json()) as T;\n } catch (err) {\n lastErr = err;\n if (err instanceof DOMException && err.name === 'AbortError') throw err;\n }\n }\n throw lastErr instanceof Error\n ? lastErr\n : new Error(`jpzip: fetch failed for ${url}: ${String(lastErr)}`);\n}\n\n/** Convenience aliases with parameterized return types. */\nexport const fetchDict = (url: string, opts?: FetchOptions): Promise<ZipcodeDict | null> =>\n fetchJSON<ZipcodeDict>(url, opts);\n\nexport const fetchMeta = (url: string, opts?: FetchOptions): Promise<Meta | null> =>\n fetchJSON<Meta>(url, opts);\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { MemoryLRU, type PersistentCache } from './cache.js';\nimport { fetchDict, fetchMeta, type FetchOptions } from './fetch.js';\nimport type { Meta, ZipcodeDict, ZipcodeEntry } from './types.js';\n\nexport const DEFAULT_BASE_URL = 'https://jpzip.nadai.dev';\nconst SUPPORTED_SPEC = '1.0';\n\nexport interface JpzipClientOptions {\n /** Override the CDN origin. Defaults to https://jpzip.nadai.dev */\n baseUrl?: string;\n /** L2 persistent cache. Default off. */\n cache?: PersistentCache;\n /** Override fetch (mainly tests). */\n fetch?: typeof fetch;\n /** L1 LRU capacity in prefix count. Default 100. */\n memoryCacheSize?: number;\n /** Surface a warning to the user when spec_version mismatches. */\n onSpecMismatch?: (info: { expected: string; received: string }) => void;\n}\n\nconst ZIP_REGEX = /^\\d{7}$/;\nconst PREFIX_REGEX = /^\\d{1,3}$/;\n\n/**\n * JpzipClient is the SDK entrypoint. The functional shortcuts (`lookup`,\n * `lookupGroup`, …) below delegate to a default singleton instance backed\n * by L1 only.\n */\nexport class JpzipClient {\n private readonly baseUrl: string;\n private readonly cache: PersistentCache | undefined;\n private readonly fetchImpl: typeof fetch;\n private readonly mem: MemoryLRU;\n private readonly onSpecMismatch: JpzipClientOptions['onSpecMismatch'];\n private metaPromise: Promise<Meta | null> | null = null;\n private knownVersion: string | null = null;\n\n constructor(opts: JpzipClientOptions = {}) {\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, '');\n this.cache = opts.cache;\n this.fetchImpl = opts.fetch ?? globalThis.fetch;\n this.mem = new MemoryLRU(opts.memoryCacheSize ?? 100);\n this.onSpecMismatch = opts.onSpecMismatch;\n }\n\n /** Returns the entry for `zipcode`, or null if not found. */\n async lookup(zipcode: string): Promise<ZipcodeEntry | null> {\n if (!ZIP_REGEX.test(zipcode)) return null;\n const prefix = zipcode.slice(0, 3);\n const dict = await this.fetchPrefixDict(prefix);\n if (!dict) return null;\n return dict[zipcode] ?? null;\n }\n\n /** Returns the dictionary for a 1- or 3-digit prefix (2-digit is fanned out). */\n async lookupGroup(prefix: string): Promise<ZipcodeDict> {\n if (!PREFIX_REGEX.test(prefix)) {\n throw new Error(`jpzip: invalid prefix ${JSON.stringify(prefix)} (must be 1-3 digits)`);\n }\n if (prefix.length === 3) {\n return (await this.fetchPrefixDict(prefix)) ?? {};\n }\n if (prefix.length === 1) {\n const dict = await this.fetchGroupDict(prefix);\n return dict ?? {};\n }\n // 2-digit fanout\n const tasks: Promise<ZipcodeDict | null>[] = [];\n for (let i = 0; i < 10; i++) {\n tasks.push(this.fetchPrefixDict(`${prefix}${i}`));\n }\n const dicts = await Promise.all(tasks);\n return Object.assign({}, ...dicts.filter(Boolean)) as ZipcodeDict;\n }\n\n /**\n * Returns the full dataset. The CDN does not expose a single /all.json\n * because the combined file exceeds Cloudflare Pages' 25 MiB per-file\n * limit; instead we fan out across /g/0..9.json in parallel and merge.\n */\n async lookupAll(): Promise<ZipcodeDict> {\n const tasks: Promise<ZipcodeDict | null>[] = [];\n for (let i = 0; i < 10; i++) {\n tasks.push(this.fetchGroupDict(String(i)));\n }\n const dicts = await Promise.all(tasks);\n return Object.assign({}, ...dicts.filter(Boolean)) as ZipcodeDict;\n }\n\n /** Returns parsed meta.json, or null if the CDN has none yet. */\n async getMeta(): Promise<Meta | null> {\n if (this.metaPromise === null) {\n this.metaPromise = (async () => {\n const m = await fetchMeta(`${this.baseUrl}/meta.json`, this.fetchOpts());\n if (m && m.spec_version !== SUPPORTED_SPEC) {\n this.onSpecMismatch?.({ expected: SUPPORTED_SPEC, received: m.spec_version });\n }\n if (m && this.knownVersion && this.knownVersion !== m.version) {\n // data version changed — drop L1 + L2 to avoid stale reads\n this.mem.clear();\n await this.cache?.clear();\n }\n if (m) this.knownVersion = m.version;\n return m;\n })();\n }\n return this.metaPromise;\n }\n\n /**\n * preload pulls the requested scope into both L1 (per-prefix entries) and\n * L2 (when provided) so subsequent reads need no network.\n */\n async preload(opts: { scope: 'all' } | { scope: string }): Promise<void> {\n if (opts.scope === 'all') {\n const dict = await this.lookupAll();\n // Split into prefix buckets and prime L1.\n const buckets: Record<string, ZipcodeDict> = {};\n for (const [zip, entry] of Object.entries(dict)) {\n const p = zip.slice(0, 3);\n (buckets[p] ??= {})[zip] = entry;\n }\n for (const [p, b] of Object.entries(buckets)) {\n this.mem.set(this.prefixURL(p), b);\n await this.writeL2(this.prefixURL(p), b);\n }\n return;\n }\n if (PREFIX_REGEX.test(opts.scope)) {\n await this.lookupGroup(opts.scope);\n return;\n }\n throw new Error(`jpzip: invalid preload scope ${JSON.stringify(opts.scope)}`);\n }\n\n /** Clear all SDK-managed caches (L1 + L2). */\n async refresh(): Promise<void> {\n this.mem.clear();\n this.metaPromise = null;\n this.knownVersion = null;\n await this.cache?.clear();\n }\n\n /* ----------------------------- internals ------------------------------ */\n\n private async fetchPrefixDict(prefix: string): Promise<ZipcodeDict | null> {\n const url = this.prefixURL(prefix);\n const cached = this.mem.get(url);\n if (cached) return cached;\n\n const fromL2 = await this.readL2(url);\n if (fromL2) {\n this.mem.set(url, fromL2);\n return fromL2;\n }\n\n const dict = await fetchDict(url, this.fetchOpts());\n if (dict) {\n this.mem.set(url, dict);\n await this.writeL2(url, dict);\n }\n return dict;\n }\n\n private async fetchGroupDict(prefix1: string): Promise<ZipcodeDict | null> {\n const url = `${this.baseUrl}/g/${prefix1}.json`;\n const cached = this.mem.get(url);\n if (cached) return cached;\n const dict = await fetchDict(url, this.fetchOpts());\n if (dict) this.mem.set(url, dict);\n return dict;\n }\n\n private prefixURL(prefix3: string): string {\n return `${this.baseUrl}/p/${prefix3}.json`;\n }\n\n private fetchOpts(): FetchOptions {\n return { fetch: this.fetchImpl };\n }\n\n private async readL2(url: string): Promise<ZipcodeDict | null> {\n if (!this.cache) return null;\n const bytes = await this.cache.get(url);\n if (!bytes) return null;\n try {\n const text = new TextDecoder().decode(bytes);\n return JSON.parse(text) as ZipcodeDict;\n } catch {\n // corrupt cache — drop the entry and refetch\n await this.cache.delete(url);\n return null;\n }\n }\n\n private async writeL2(url: string, dict: ZipcodeDict): Promise<void> {\n if (!this.cache) return;\n const bytes = new TextEncoder().encode(JSON.stringify(dict));\n await this.cache.set(url, bytes);\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/cache.ts","../src/fetch.ts","../src/client.ts"],"sourcesContent":["export { JpzipClient, DEFAULT_BASE_URL, type JpzipClientOptions } from './client.js';\nexport { MemoryLRU, type PersistentCache } from './cache.js';\nexport type { ZipcodeEntry, Town, Meta, ZipcodeDict, Endpoints } from './types.js';\n\nimport { JpzipClient } from './client.js';\nimport type { Meta, ZipcodeDict, ZipcodeEntry } from './types.js';\n\n/**\n * The functional helpers below all delegate to a lazily-initialized default\n * client (L1 cache only, default base URL). For multiple SDK instances or\n * an L2 cache, construct `new JpzipClient({...})` yourself.\n */\n\nlet _default: JpzipClient | null = null;\nfunction defaultClient(): JpzipClient {\n if (_default === null) _default = new JpzipClient();\n return _default;\n}\n\n/** Reset the singleton (mainly for tests). */\nexport function _resetDefaultClient(): void {\n _default = null;\n}\n\n/** Configure the singleton's options. Subsequent calls re-create the client. */\nexport function configure(options: ConstructorParameters<typeof JpzipClient>[0]): void {\n _default = new JpzipClient(options);\n}\n\nexport const lookup = (zipcode: string): Promise<ZipcodeEntry | null> =>\n defaultClient().lookup(zipcode);\n\nexport const lookupGroup = (prefix: string): Promise<ZipcodeDict> =>\n defaultClient().lookupGroup(prefix);\n\nexport const lookupAll = (): Promise<ZipcodeDict> => defaultClient().lookupAll();\n\nexport const preload = (opts: Parameters<JpzipClient['preload']>[0]): Promise<void> =>\n defaultClient().preload(opts);\n\nexport const getMeta = (): Promise<Meta | null> => defaultClient().getMeta();\n\n/** Helper: returns true iff `zip` is a syntactically valid 7-digit zipcode. */\nexport function isValidZipcode(zip: string): boolean {\n return /^\\d{7}$/.test(zip);\n}\n","/**\n * Three-layer caching per spec §6.4:\n *\n * - L1 (MemoryLRU) — always on, bounded\n * - L2 (PersistentCache) — opt-in via constructor\n * - L3 (HTTP/fetch) — out of SDK scope\n */\n\nimport type { ZipcodeDict } from './types.js';\n\n/** Public interface user-supplied L2 caches must implement. */\nexport interface PersistentCache {\n get(key: string): Promise<Uint8Array | null>;\n set(key: string, value: Uint8Array): Promise<void>;\n delete(key: string): Promise<void>;\n clear(): Promise<void>;\n}\n\n/** Bounded in-memory LRU keyed by URL path. Values are pre-parsed dicts. */\nexport class MemoryLRU {\n private readonly max: number;\n private readonly map = new Map<string, ZipcodeDict>();\n\n constructor(max = 100) {\n this.max = Math.max(1, max);\n }\n\n get(key: string): ZipcodeDict | undefined {\n const value = this.map.get(key);\n if (value === undefined) return undefined;\n // refresh recency by re-inserting at the tail\n this.map.delete(key);\n this.map.set(key, value);\n return value;\n }\n\n set(key: string, value: ZipcodeDict): void {\n if (this.map.has(key)) {\n this.map.delete(key);\n } else if (this.map.size >= this.max) {\n const oldestKey = this.map.keys().next().value;\n if (oldestKey !== undefined) this.map.delete(oldestKey);\n }\n this.map.set(key, value);\n }\n\n clear(): void {\n this.map.clear();\n }\n\n get size(): number {\n return this.map.size;\n }\n}\n","/** HTTP helpers with exponential-backoff retry for transient 5xx and network errors. */\n\nimport type { ZipcodeDict, Meta } from './types.js';\n\nconst MAX_RETRIES = 3;\nconst BASE_DELAY_MS = 200;\n\nexport interface FetchOptions {\n /** Override the global fetch (mainly for tests). */\n fetch?: typeof fetch;\n /** Pass-through to fetch(). */\n signal?: AbortSignal;\n /** Forces no-cache; useful when the user explicitly refreshes. */\n noCache?: boolean;\n}\n\n/**\n * fetchJSON returns the parsed JSON body for url. On 404 it returns null.\n * On 5xx / network errors it retries up to MAX_RETRIES with exponential backoff.\n * Other 4xx errors throw.\n */\nexport async function fetchJSON<T>(url: string, opts: FetchOptions = {}): Promise<T | null> {\n const f = opts.fetch ?? fetch;\n\n let lastErr: unknown;\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n if (attempt > 0) {\n await sleep(BASE_DELAY_MS * 2 ** attempt);\n }\n const init: RequestInit = {\n method: 'GET',\n headers: { Accept: 'application/json' },\n };\n if (opts.signal !== undefined) init.signal = opts.signal;\n if (opts.noCache) init.cache = 'no-cache';\n\n let res: Response;\n try {\n res = await f(url, init);\n } catch (err) {\n // Network-layer failures (DNS, TLS, fetch abort, etc.) — retry,\n // unless the caller aborted us, which should propagate immediately.\n if (err instanceof DOMException && err.name === 'AbortError') throw err;\n lastErr = err;\n continue;\n }\n if (res.status === 404) return null;\n if (res.status >= 500) {\n lastErr = new Error(`jpzip: ${url} returned ${res.status}`);\n continue;\n }\n // Other 4xx are not retried — the request itself is wrong.\n if (!res.ok) {\n throw new Error(`jpzip: ${url} returned ${res.status}`);\n }\n return (await res.json()) as T;\n }\n throw lastErr instanceof Error\n ? lastErr\n : new Error(`jpzip: fetch failed for ${url}: ${String(lastErr)}`);\n}\n\n/** Convenience aliases with parameterized return types. */\nexport const fetchDict = (url: string, opts?: FetchOptions): Promise<ZipcodeDict | null> =>\n fetchJSON<ZipcodeDict>(url, opts);\n\nexport const fetchMeta = (url: string, opts?: FetchOptions): Promise<Meta | null> =>\n fetchJSON<Meta>(url, opts);\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { MemoryLRU, type PersistentCache } from './cache.js';\nimport { fetchDict, fetchMeta, type FetchOptions } from './fetch.js';\nimport type { Meta, ZipcodeDict, ZipcodeEntry } from './types.js';\n\nexport const DEFAULT_BASE_URL = 'https://jpzip.nadai.dev';\nconst SUPPORTED_SPEC = '1.0';\n\nexport interface JpzipClientOptions {\n /** Override the CDN origin. Defaults to https://jpzip.nadai.dev */\n baseUrl?: string;\n /** L2 persistent cache. Default off. */\n cache?: PersistentCache;\n /** Override fetch (mainly tests). */\n fetch?: typeof fetch;\n /** L1 LRU capacity in prefix count. Default 100. */\n memoryCacheSize?: number;\n /** Surface a warning to the user when spec_version mismatches. */\n onSpecMismatch?: (info: { expected: string; received: string }) => void;\n}\n\nconst ZIP_REGEX = /^\\d{7}$/;\nconst PREFIX_REGEX = /^\\d{1,3}$/;\n\n/**\n * JpzipClient is the SDK entrypoint. The functional shortcuts (`lookup`,\n * `lookupGroup`, …) below delegate to a default singleton instance backed\n * by L1 only.\n */\nexport class JpzipClient {\n private readonly baseUrl: string;\n private readonly cache: PersistentCache | undefined;\n private readonly fetchImpl: typeof fetch;\n private readonly mem: MemoryLRU;\n private readonly onSpecMismatch: JpzipClientOptions['onSpecMismatch'];\n private metaPromise: Promise<Meta | null> | null = null;\n private knownVersion: string | null = null;\n\n constructor(opts: JpzipClientOptions = {}) {\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, '');\n this.cache = opts.cache;\n this.fetchImpl = opts.fetch ?? globalThis.fetch;\n this.mem = new MemoryLRU(opts.memoryCacheSize ?? 100);\n this.onSpecMismatch = opts.onSpecMismatch;\n }\n\n /** Returns the entry for `zipcode`, or null if not found. */\n async lookup(zipcode: string): Promise<ZipcodeEntry | null> {\n if (!ZIP_REGEX.test(zipcode)) return null;\n const prefix = zipcode.slice(0, 3);\n const dict = await this.fetchPrefixDict(prefix);\n if (!dict) return null;\n return dict[zipcode] ?? null;\n }\n\n /** Returns the dictionary for a 1- or 3-digit prefix (2-digit is fanned out). */\n async lookupGroup(prefix: string): Promise<ZipcodeDict> {\n if (!PREFIX_REGEX.test(prefix)) {\n throw new Error(`jpzip: invalid prefix ${JSON.stringify(prefix)} (must be 1-3 digits)`);\n }\n if (prefix.length === 3) {\n return (await this.fetchPrefixDict(prefix)) ?? {};\n }\n if (prefix.length === 1) {\n const dict = await this.fetchGroupDict(prefix);\n return dict ?? {};\n }\n // 2-digit fanout\n const tasks: Promise<ZipcodeDict | null>[] = [];\n for (let i = 0; i < 10; i++) {\n tasks.push(this.fetchPrefixDict(`${prefix}${i}`));\n }\n const dicts = await Promise.all(tasks);\n return Object.assign({}, ...dicts.filter(Boolean)) as ZipcodeDict;\n }\n\n /**\n * Returns the full dataset. The CDN does not expose a single /all.json\n * because the combined file exceeds Cloudflare Pages' 25 MiB per-file\n * limit; instead we fan out across /g/0..9.json in parallel and merge.\n */\n async lookupAll(): Promise<ZipcodeDict> {\n const tasks: Promise<ZipcodeDict | null>[] = [];\n for (let i = 0; i < 10; i++) {\n tasks.push(this.fetchGroupDict(String(i)));\n }\n const dicts = await Promise.all(tasks);\n return Object.assign({}, ...dicts.filter(Boolean)) as ZipcodeDict;\n }\n\n /** Returns parsed meta.json, or null if the CDN has none yet. */\n async getMeta(): Promise<Meta | null> {\n if (this.metaPromise === null) {\n this.metaPromise = (async () => {\n const m = await fetchMeta(`${this.baseUrl}/meta.json`, this.fetchOpts());\n if (m && m.spec_version !== SUPPORTED_SPEC) {\n this.onSpecMismatch?.({ expected: SUPPORTED_SPEC, received: m.spec_version });\n }\n if (m && this.knownVersion && this.knownVersion !== m.version) {\n // data version changed — drop L1 + L2 to avoid stale reads\n this.mem.clear();\n await this.cache?.clear();\n }\n if (m) this.knownVersion = m.version;\n return m;\n })();\n }\n return this.metaPromise;\n }\n\n /**\n * preload pulls the requested scope into both L1 (per-prefix entries) and\n * L2 (when provided) so subsequent reads need no network.\n */\n async preload(opts: { scope: 'all' } | { scope: string }): Promise<void> {\n if (opts.scope === 'all') {\n const dict = await this.lookupAll();\n // Split into prefix buckets and prime L1.\n const buckets: Record<string, ZipcodeDict> = {};\n for (const [zip, entry] of Object.entries(dict)) {\n const p = zip.slice(0, 3);\n (buckets[p] ??= {})[zip] = entry;\n }\n for (const [p, b] of Object.entries(buckets)) {\n this.mem.set(this.prefixURL(p), b);\n await this.writeL2(this.prefixURL(p), b);\n }\n return;\n }\n if (PREFIX_REGEX.test(opts.scope)) {\n await this.lookupGroup(opts.scope);\n return;\n }\n throw new Error(`jpzip: invalid preload scope ${JSON.stringify(opts.scope)}`);\n }\n\n /** Clear all SDK-managed caches (L1 + L2). */\n async refresh(): Promise<void> {\n this.mem.clear();\n this.metaPromise = null;\n this.knownVersion = null;\n await this.cache?.clear();\n }\n\n /* ----------------------------- internals ------------------------------ */\n\n private async fetchPrefixDict(prefix: string): Promise<ZipcodeDict | null> {\n const url = this.prefixURL(prefix);\n const cached = this.mem.get(url);\n if (cached) return cached;\n\n const fromL2 = await this.readL2(url);\n if (fromL2) {\n this.mem.set(url, fromL2);\n return fromL2;\n }\n\n const dict = await fetchDict(url, this.fetchOpts());\n if (dict) {\n this.mem.set(url, dict);\n await this.writeL2(url, dict);\n }\n return dict;\n }\n\n private async fetchGroupDict(prefix1: string): Promise<ZipcodeDict | null> {\n const url = `${this.baseUrl}/g/${prefix1}.json`;\n const cached = this.mem.get(url);\n if (cached) return cached;\n const dict = await fetchDict(url, this.fetchOpts());\n if (dict) this.mem.set(url, dict);\n return dict;\n }\n\n private prefixURL(prefix3: string): string {\n return `${this.baseUrl}/p/${prefix3}.json`;\n }\n\n private fetchOpts(): FetchOptions {\n return { fetch: this.fetchImpl };\n }\n\n private async readL2(url: string): Promise<ZipcodeDict | null> {\n if (!this.cache) return null;\n const bytes = await this.cache.get(url);\n if (!bytes) return null;\n try {\n const text = new TextDecoder().decode(bytes);\n return JSON.parse(text) as ZipcodeDict;\n } catch {\n // corrupt cache — drop the entry and refetch\n await this.cache.delete(url);\n return null;\n }\n }\n\n private async writeL2(url: string, dict: ZipcodeDict): Promise<void> {\n if (!this.cache) return;\n const bytes = new TextEncoder().encode(JSON.stringify(dict));\n await this.cache.set(url, bytes);\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -39,27 +39,29 @@ async function fetchJSON(url, opts = {}) {
|
|
|
39
39
|
if (attempt > 0) {
|
|
40
40
|
await sleep(BASE_DELAY_MS * 2 ** attempt);
|
|
41
41
|
}
|
|
42
|
+
const init = {
|
|
43
|
+
method: "GET",
|
|
44
|
+
headers: { Accept: "application/json" }
|
|
45
|
+
};
|
|
46
|
+
if (opts.signal !== void 0) init.signal = opts.signal;
|
|
47
|
+
if (opts.noCache) init.cache = "no-cache";
|
|
48
|
+
let res;
|
|
42
49
|
try {
|
|
43
|
-
|
|
44
|
-
method: "GET",
|
|
45
|
-
headers: { Accept: "application/json" }
|
|
46
|
-
};
|
|
47
|
-
if (opts.signal !== void 0) init.signal = opts.signal;
|
|
48
|
-
if (opts.noCache) init.cache = "no-cache";
|
|
49
|
-
const res = await f(url, init);
|
|
50
|
-
if (res.status === 404) return null;
|
|
51
|
-
if (res.status >= 500) {
|
|
52
|
-
lastErr = new Error(`jpzip: ${url} returned ${res.status}`);
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
if (!res.ok) {
|
|
56
|
-
throw new Error(`jpzip: ${url} returned ${res.status}`);
|
|
57
|
-
}
|
|
58
|
-
return await res.json();
|
|
50
|
+
res = await f(url, init);
|
|
59
51
|
} catch (err) {
|
|
60
|
-
lastErr = err;
|
|
61
52
|
if (err instanceof DOMException && err.name === "AbortError") throw err;
|
|
53
|
+
lastErr = err;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (res.status === 404) return null;
|
|
57
|
+
if (res.status >= 500) {
|
|
58
|
+
lastErr = new Error(`jpzip: ${url} returned ${res.status}`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
throw new Error(`jpzip: ${url} returned ${res.status}`);
|
|
62
63
|
}
|
|
64
|
+
return await res.json();
|
|
63
65
|
}
|
|
64
66
|
throw lastErr instanceof Error ? lastErr : new Error(`jpzip: fetch failed for ${url}: ${String(lastErr)}`);
|
|
65
67
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cache.ts","../src/fetch.ts","../src/client.ts","../src/index.ts"],"sourcesContent":["/**\n * Three-layer caching per spec §6.4:\n *\n * - L1 (MemoryLRU) — always on, bounded\n * - L2 (PersistentCache) — opt-in via constructor\n * - L3 (HTTP/fetch) — out of SDK scope\n */\n\nimport type { ZipcodeDict } from './types.js';\n\n/** Public interface user-supplied L2 caches must implement. */\nexport interface PersistentCache {\n get(key: string): Promise<Uint8Array | null>;\n set(key: string, value: Uint8Array): Promise<void>;\n delete(key: string): Promise<void>;\n clear(): Promise<void>;\n}\n\n/** Bounded in-memory LRU keyed by URL path. Values are pre-parsed dicts. */\nexport class MemoryLRU {\n private readonly max: number;\n private readonly map = new Map<string, ZipcodeDict>();\n\n constructor(max = 100) {\n this.max = Math.max(1, max);\n }\n\n get(key: string): ZipcodeDict | undefined {\n const value = this.map.get(key);\n if (value === undefined) return undefined;\n // refresh recency by re-inserting at the tail\n this.map.delete(key);\n this.map.set(key, value);\n return value;\n }\n\n set(key: string, value: ZipcodeDict): void {\n if (this.map.has(key)) {\n this.map.delete(key);\n } else if (this.map.size >= this.max) {\n const oldestKey = this.map.keys().next().value;\n if (oldestKey !== undefined) this.map.delete(oldestKey);\n }\n this.map.set(key, value);\n }\n\n clear(): void {\n this.map.clear();\n }\n\n get size(): number {\n return this.map.size;\n }\n}\n","/** HTTP helpers with exponential-backoff retry for transient 5xx and network errors. */\n\nimport type { ZipcodeDict, Meta } from './types.js';\n\nconst MAX_RETRIES = 3;\nconst BASE_DELAY_MS = 200;\n\nexport interface FetchOptions {\n /** Override the global fetch (mainly for tests). */\n fetch?: typeof fetch;\n /** Pass-through to fetch(). */\n signal?: AbortSignal;\n /** Forces no-cache; useful when the user explicitly refreshes. */\n noCache?: boolean;\n}\n\n/**\n * fetchJSON returns the parsed JSON body for url. On 404 it returns null.\n * On 5xx / network errors it retries up to MAX_RETRIES with exponential backoff.\n * Other 4xx errors throw.\n */\nexport async function fetchJSON<T>(url: string, opts: FetchOptions = {}): Promise<T | null> {\n const f = opts.fetch ?? fetch;\n\n let lastErr: unknown;\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n if (attempt > 0) {\n await sleep(BASE_DELAY_MS * 2 ** attempt);\n }\n try {\n const init: RequestInit = {\n method: 'GET',\n headers: { Accept: 'application/json' },\n };\n if (opts.signal !== undefined) init.signal = opts.signal;\n if (opts.noCache) init.cache = 'no-cache';\n\n const res = await f(url, init);\n if (res.status === 404) return null;\n if (res.status >= 500) {\n lastErr = new Error(`jpzip: ${url} returned ${res.status}`);\n continue;\n }\n if (!res.ok) {\n throw new Error(`jpzip: ${url} returned ${res.status}`);\n }\n return (await res.json()) as T;\n } catch (err) {\n lastErr = err;\n if (err instanceof DOMException && err.name === 'AbortError') throw err;\n }\n }\n throw lastErr instanceof Error\n ? lastErr\n : new Error(`jpzip: fetch failed for ${url}: ${String(lastErr)}`);\n}\n\n/** Convenience aliases with parameterized return types. */\nexport const fetchDict = (url: string, opts?: FetchOptions): Promise<ZipcodeDict | null> =>\n fetchJSON<ZipcodeDict>(url, opts);\n\nexport const fetchMeta = (url: string, opts?: FetchOptions): Promise<Meta | null> =>\n fetchJSON<Meta>(url, opts);\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { MemoryLRU, type PersistentCache } from './cache.js';\nimport { fetchDict, fetchMeta, type FetchOptions } from './fetch.js';\nimport type { Meta, ZipcodeDict, ZipcodeEntry } from './types.js';\n\nexport const DEFAULT_BASE_URL = 'https://jpzip.nadai.dev';\nconst SUPPORTED_SPEC = '1.0';\n\nexport interface JpzipClientOptions {\n /** Override the CDN origin. Defaults to https://jpzip.nadai.dev */\n baseUrl?: string;\n /** L2 persistent cache. Default off. */\n cache?: PersistentCache;\n /** Override fetch (mainly tests). */\n fetch?: typeof fetch;\n /** L1 LRU capacity in prefix count. Default 100. */\n memoryCacheSize?: number;\n /** Surface a warning to the user when spec_version mismatches. */\n onSpecMismatch?: (info: { expected: string; received: string }) => void;\n}\n\nconst ZIP_REGEX = /^\\d{7}$/;\nconst PREFIX_REGEX = /^\\d{1,3}$/;\n\n/**\n * JpzipClient is the SDK entrypoint. The functional shortcuts (`lookup`,\n * `lookupGroup`, …) below delegate to a default singleton instance backed\n * by L1 only.\n */\nexport class JpzipClient {\n private readonly baseUrl: string;\n private readonly cache: PersistentCache | undefined;\n private readonly fetchImpl: typeof fetch;\n private readonly mem: MemoryLRU;\n private readonly onSpecMismatch: JpzipClientOptions['onSpecMismatch'];\n private metaPromise: Promise<Meta | null> | null = null;\n private knownVersion: string | null = null;\n\n constructor(opts: JpzipClientOptions = {}) {\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, '');\n this.cache = opts.cache;\n this.fetchImpl = opts.fetch ?? globalThis.fetch;\n this.mem = new MemoryLRU(opts.memoryCacheSize ?? 100);\n this.onSpecMismatch = opts.onSpecMismatch;\n }\n\n /** Returns the entry for `zipcode`, or null if not found. */\n async lookup(zipcode: string): Promise<ZipcodeEntry | null> {\n if (!ZIP_REGEX.test(zipcode)) return null;\n const prefix = zipcode.slice(0, 3);\n const dict = await this.fetchPrefixDict(prefix);\n if (!dict) return null;\n return dict[zipcode] ?? null;\n }\n\n /** Returns the dictionary for a 1- or 3-digit prefix (2-digit is fanned out). */\n async lookupGroup(prefix: string): Promise<ZipcodeDict> {\n if (!PREFIX_REGEX.test(prefix)) {\n throw new Error(`jpzip: invalid prefix ${JSON.stringify(prefix)} (must be 1-3 digits)`);\n }\n if (prefix.length === 3) {\n return (await this.fetchPrefixDict(prefix)) ?? {};\n }\n if (prefix.length === 1) {\n const dict = await this.fetchGroupDict(prefix);\n return dict ?? {};\n }\n // 2-digit fanout\n const tasks: Promise<ZipcodeDict | null>[] = [];\n for (let i = 0; i < 10; i++) {\n tasks.push(this.fetchPrefixDict(`${prefix}${i}`));\n }\n const dicts = await Promise.all(tasks);\n return Object.assign({}, ...dicts.filter(Boolean)) as ZipcodeDict;\n }\n\n /**\n * Returns the full dataset. The CDN does not expose a single /all.json\n * because the combined file exceeds Cloudflare Pages' 25 MiB per-file\n * limit; instead we fan out across /g/0..9.json in parallel and merge.\n */\n async lookupAll(): Promise<ZipcodeDict> {\n const tasks: Promise<ZipcodeDict | null>[] = [];\n for (let i = 0; i < 10; i++) {\n tasks.push(this.fetchGroupDict(String(i)));\n }\n const dicts = await Promise.all(tasks);\n return Object.assign({}, ...dicts.filter(Boolean)) as ZipcodeDict;\n }\n\n /** Returns parsed meta.json, or null if the CDN has none yet. */\n async getMeta(): Promise<Meta | null> {\n if (this.metaPromise === null) {\n this.metaPromise = (async () => {\n const m = await fetchMeta(`${this.baseUrl}/meta.json`, this.fetchOpts());\n if (m && m.spec_version !== SUPPORTED_SPEC) {\n this.onSpecMismatch?.({ expected: SUPPORTED_SPEC, received: m.spec_version });\n }\n if (m && this.knownVersion && this.knownVersion !== m.version) {\n // data version changed — drop L1 + L2 to avoid stale reads\n this.mem.clear();\n await this.cache?.clear();\n }\n if (m) this.knownVersion = m.version;\n return m;\n })();\n }\n return this.metaPromise;\n }\n\n /**\n * preload pulls the requested scope into both L1 (per-prefix entries) and\n * L2 (when provided) so subsequent reads need no network.\n */\n async preload(opts: { scope: 'all' } | { scope: string }): Promise<void> {\n if (opts.scope === 'all') {\n const dict = await this.lookupAll();\n // Split into prefix buckets and prime L1.\n const buckets: Record<string, ZipcodeDict> = {};\n for (const [zip, entry] of Object.entries(dict)) {\n const p = zip.slice(0, 3);\n (buckets[p] ??= {})[zip] = entry;\n }\n for (const [p, b] of Object.entries(buckets)) {\n this.mem.set(this.prefixURL(p), b);\n await this.writeL2(this.prefixURL(p), b);\n }\n return;\n }\n if (PREFIX_REGEX.test(opts.scope)) {\n await this.lookupGroup(opts.scope);\n return;\n }\n throw new Error(`jpzip: invalid preload scope ${JSON.stringify(opts.scope)}`);\n }\n\n /** Clear all SDK-managed caches (L1 + L2). */\n async refresh(): Promise<void> {\n this.mem.clear();\n this.metaPromise = null;\n this.knownVersion = null;\n await this.cache?.clear();\n }\n\n /* ----------------------------- internals ------------------------------ */\n\n private async fetchPrefixDict(prefix: string): Promise<ZipcodeDict | null> {\n const url = this.prefixURL(prefix);\n const cached = this.mem.get(url);\n if (cached) return cached;\n\n const fromL2 = await this.readL2(url);\n if (fromL2) {\n this.mem.set(url, fromL2);\n return fromL2;\n }\n\n const dict = await fetchDict(url, this.fetchOpts());\n if (dict) {\n this.mem.set(url, dict);\n await this.writeL2(url, dict);\n }\n return dict;\n }\n\n private async fetchGroupDict(prefix1: string): Promise<ZipcodeDict | null> {\n const url = `${this.baseUrl}/g/${prefix1}.json`;\n const cached = this.mem.get(url);\n if (cached) return cached;\n const dict = await fetchDict(url, this.fetchOpts());\n if (dict) this.mem.set(url, dict);\n return dict;\n }\n\n private prefixURL(prefix3: string): string {\n return `${this.baseUrl}/p/${prefix3}.json`;\n }\n\n private fetchOpts(): FetchOptions {\n return { fetch: this.fetchImpl };\n }\n\n private async readL2(url: string): Promise<ZipcodeDict | null> {\n if (!this.cache) return null;\n const bytes = await this.cache.get(url);\n if (!bytes) return null;\n try {\n const text = new TextDecoder().decode(bytes);\n return JSON.parse(text) as ZipcodeDict;\n } catch {\n // corrupt cache — drop the entry and refetch\n await this.cache.delete(url);\n return null;\n }\n }\n\n private async writeL2(url: string, dict: ZipcodeDict): Promise<void> {\n if (!this.cache) return;\n const bytes = new TextEncoder().encode(JSON.stringify(dict));\n await this.cache.set(url, bytes);\n }\n}\n","export { JpzipClient, DEFAULT_BASE_URL, type JpzipClientOptions } from './client.js';\nexport { MemoryLRU, type PersistentCache } from './cache.js';\nexport type { ZipcodeEntry, Town, Meta, ZipcodeDict, Endpoints } from './types.js';\n\nimport { JpzipClient } from './client.js';\nimport type { Meta, ZipcodeDict, ZipcodeEntry } from './types.js';\n\n/**\n * The functional helpers below all delegate to a lazily-initialized default\n * client (L1 cache only, default base URL). For multiple SDK instances or\n * an L2 cache, construct `new JpzipClient({...})` yourself.\n */\n\nlet _default: JpzipClient | null = null;\nfunction defaultClient(): JpzipClient {\n if (_default === null) _default = new JpzipClient();\n return _default;\n}\n\n/** Reset the singleton (mainly for tests). */\nexport function _resetDefaultClient(): void {\n _default = null;\n}\n\n/** Configure the singleton's options. Subsequent calls re-create the client. */\nexport function configure(options: ConstructorParameters<typeof JpzipClient>[0]): void {\n _default = new JpzipClient(options);\n}\n\nexport const lookup = (zipcode: string): Promise<ZipcodeEntry | null> =>\n defaultClient().lookup(zipcode);\n\nexport const lookupGroup = (prefix: string): Promise<ZipcodeDict> =>\n defaultClient().lookupGroup(prefix);\n\nexport const lookupAll = (): Promise<ZipcodeDict> => defaultClient().lookupAll();\n\nexport const preload = (opts: Parameters<JpzipClient['preload']>[0]): Promise<void> =>\n defaultClient().preload(opts);\n\nexport const getMeta = (): Promise<Meta | null> => defaultClient().getMeta();\n\n/** Helper: returns true iff `zip` is a syntactically valid 7-digit zipcode. */\nexport function isValidZipcode(zip: string): boolean {\n return /^\\d{7}$/.test(zip);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/cache.ts","../src/fetch.ts","../src/client.ts","../src/index.ts"],"sourcesContent":["/**\n * Three-layer caching per spec §6.4:\n *\n * - L1 (MemoryLRU) — always on, bounded\n * - L2 (PersistentCache) — opt-in via constructor\n * - L3 (HTTP/fetch) — out of SDK scope\n */\n\nimport type { ZipcodeDict } from './types.js';\n\n/** Public interface user-supplied L2 caches must implement. */\nexport interface PersistentCache {\n get(key: string): Promise<Uint8Array | null>;\n set(key: string, value: Uint8Array): Promise<void>;\n delete(key: string): Promise<void>;\n clear(): Promise<void>;\n}\n\n/** Bounded in-memory LRU keyed by URL path. Values are pre-parsed dicts. */\nexport class MemoryLRU {\n private readonly max: number;\n private readonly map = new Map<string, ZipcodeDict>();\n\n constructor(max = 100) {\n this.max = Math.max(1, max);\n }\n\n get(key: string): ZipcodeDict | undefined {\n const value = this.map.get(key);\n if (value === undefined) return undefined;\n // refresh recency by re-inserting at the tail\n this.map.delete(key);\n this.map.set(key, value);\n return value;\n }\n\n set(key: string, value: ZipcodeDict): void {\n if (this.map.has(key)) {\n this.map.delete(key);\n } else if (this.map.size >= this.max) {\n const oldestKey = this.map.keys().next().value;\n if (oldestKey !== undefined) this.map.delete(oldestKey);\n }\n this.map.set(key, value);\n }\n\n clear(): void {\n this.map.clear();\n }\n\n get size(): number {\n return this.map.size;\n }\n}\n","/** HTTP helpers with exponential-backoff retry for transient 5xx and network errors. */\n\nimport type { ZipcodeDict, Meta } from './types.js';\n\nconst MAX_RETRIES = 3;\nconst BASE_DELAY_MS = 200;\n\nexport interface FetchOptions {\n /** Override the global fetch (mainly for tests). */\n fetch?: typeof fetch;\n /** Pass-through to fetch(). */\n signal?: AbortSignal;\n /** Forces no-cache; useful when the user explicitly refreshes. */\n noCache?: boolean;\n}\n\n/**\n * fetchJSON returns the parsed JSON body for url. On 404 it returns null.\n * On 5xx / network errors it retries up to MAX_RETRIES with exponential backoff.\n * Other 4xx errors throw.\n */\nexport async function fetchJSON<T>(url: string, opts: FetchOptions = {}): Promise<T | null> {\n const f = opts.fetch ?? fetch;\n\n let lastErr: unknown;\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n if (attempt > 0) {\n await sleep(BASE_DELAY_MS * 2 ** attempt);\n }\n const init: RequestInit = {\n method: 'GET',\n headers: { Accept: 'application/json' },\n };\n if (opts.signal !== undefined) init.signal = opts.signal;\n if (opts.noCache) init.cache = 'no-cache';\n\n let res: Response;\n try {\n res = await f(url, init);\n } catch (err) {\n // Network-layer failures (DNS, TLS, fetch abort, etc.) — retry,\n // unless the caller aborted us, which should propagate immediately.\n if (err instanceof DOMException && err.name === 'AbortError') throw err;\n lastErr = err;\n continue;\n }\n if (res.status === 404) return null;\n if (res.status >= 500) {\n lastErr = new Error(`jpzip: ${url} returned ${res.status}`);\n continue;\n }\n // Other 4xx are not retried — the request itself is wrong.\n if (!res.ok) {\n throw new Error(`jpzip: ${url} returned ${res.status}`);\n }\n return (await res.json()) as T;\n }\n throw lastErr instanceof Error\n ? lastErr\n : new Error(`jpzip: fetch failed for ${url}: ${String(lastErr)}`);\n}\n\n/** Convenience aliases with parameterized return types. */\nexport const fetchDict = (url: string, opts?: FetchOptions): Promise<ZipcodeDict | null> =>\n fetchJSON<ZipcodeDict>(url, opts);\n\nexport const fetchMeta = (url: string, opts?: FetchOptions): Promise<Meta | null> =>\n fetchJSON<Meta>(url, opts);\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { MemoryLRU, type PersistentCache } from './cache.js';\nimport { fetchDict, fetchMeta, type FetchOptions } from './fetch.js';\nimport type { Meta, ZipcodeDict, ZipcodeEntry } from './types.js';\n\nexport const DEFAULT_BASE_URL = 'https://jpzip.nadai.dev';\nconst SUPPORTED_SPEC = '1.0';\n\nexport interface JpzipClientOptions {\n /** Override the CDN origin. Defaults to https://jpzip.nadai.dev */\n baseUrl?: string;\n /** L2 persistent cache. Default off. */\n cache?: PersistentCache;\n /** Override fetch (mainly tests). */\n fetch?: typeof fetch;\n /** L1 LRU capacity in prefix count. Default 100. */\n memoryCacheSize?: number;\n /** Surface a warning to the user when spec_version mismatches. */\n onSpecMismatch?: (info: { expected: string; received: string }) => void;\n}\n\nconst ZIP_REGEX = /^\\d{7}$/;\nconst PREFIX_REGEX = /^\\d{1,3}$/;\n\n/**\n * JpzipClient is the SDK entrypoint. The functional shortcuts (`lookup`,\n * `lookupGroup`, …) below delegate to a default singleton instance backed\n * by L1 only.\n */\nexport class JpzipClient {\n private readonly baseUrl: string;\n private readonly cache: PersistentCache | undefined;\n private readonly fetchImpl: typeof fetch;\n private readonly mem: MemoryLRU;\n private readonly onSpecMismatch: JpzipClientOptions['onSpecMismatch'];\n private metaPromise: Promise<Meta | null> | null = null;\n private knownVersion: string | null = null;\n\n constructor(opts: JpzipClientOptions = {}) {\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, '');\n this.cache = opts.cache;\n this.fetchImpl = opts.fetch ?? globalThis.fetch;\n this.mem = new MemoryLRU(opts.memoryCacheSize ?? 100);\n this.onSpecMismatch = opts.onSpecMismatch;\n }\n\n /** Returns the entry for `zipcode`, or null if not found. */\n async lookup(zipcode: string): Promise<ZipcodeEntry | null> {\n if (!ZIP_REGEX.test(zipcode)) return null;\n const prefix = zipcode.slice(0, 3);\n const dict = await this.fetchPrefixDict(prefix);\n if (!dict) return null;\n return dict[zipcode] ?? null;\n }\n\n /** Returns the dictionary for a 1- or 3-digit prefix (2-digit is fanned out). */\n async lookupGroup(prefix: string): Promise<ZipcodeDict> {\n if (!PREFIX_REGEX.test(prefix)) {\n throw new Error(`jpzip: invalid prefix ${JSON.stringify(prefix)} (must be 1-3 digits)`);\n }\n if (prefix.length === 3) {\n return (await this.fetchPrefixDict(prefix)) ?? {};\n }\n if (prefix.length === 1) {\n const dict = await this.fetchGroupDict(prefix);\n return dict ?? {};\n }\n // 2-digit fanout\n const tasks: Promise<ZipcodeDict | null>[] = [];\n for (let i = 0; i < 10; i++) {\n tasks.push(this.fetchPrefixDict(`${prefix}${i}`));\n }\n const dicts = await Promise.all(tasks);\n return Object.assign({}, ...dicts.filter(Boolean)) as ZipcodeDict;\n }\n\n /**\n * Returns the full dataset. The CDN does not expose a single /all.json\n * because the combined file exceeds Cloudflare Pages' 25 MiB per-file\n * limit; instead we fan out across /g/0..9.json in parallel and merge.\n */\n async lookupAll(): Promise<ZipcodeDict> {\n const tasks: Promise<ZipcodeDict | null>[] = [];\n for (let i = 0; i < 10; i++) {\n tasks.push(this.fetchGroupDict(String(i)));\n }\n const dicts = await Promise.all(tasks);\n return Object.assign({}, ...dicts.filter(Boolean)) as ZipcodeDict;\n }\n\n /** Returns parsed meta.json, or null if the CDN has none yet. */\n async getMeta(): Promise<Meta | null> {\n if (this.metaPromise === null) {\n this.metaPromise = (async () => {\n const m = await fetchMeta(`${this.baseUrl}/meta.json`, this.fetchOpts());\n if (m && m.spec_version !== SUPPORTED_SPEC) {\n this.onSpecMismatch?.({ expected: SUPPORTED_SPEC, received: m.spec_version });\n }\n if (m && this.knownVersion && this.knownVersion !== m.version) {\n // data version changed — drop L1 + L2 to avoid stale reads\n this.mem.clear();\n await this.cache?.clear();\n }\n if (m) this.knownVersion = m.version;\n return m;\n })();\n }\n return this.metaPromise;\n }\n\n /**\n * preload pulls the requested scope into both L1 (per-prefix entries) and\n * L2 (when provided) so subsequent reads need no network.\n */\n async preload(opts: { scope: 'all' } | { scope: string }): Promise<void> {\n if (opts.scope === 'all') {\n const dict = await this.lookupAll();\n // Split into prefix buckets and prime L1.\n const buckets: Record<string, ZipcodeDict> = {};\n for (const [zip, entry] of Object.entries(dict)) {\n const p = zip.slice(0, 3);\n (buckets[p] ??= {})[zip] = entry;\n }\n for (const [p, b] of Object.entries(buckets)) {\n this.mem.set(this.prefixURL(p), b);\n await this.writeL2(this.prefixURL(p), b);\n }\n return;\n }\n if (PREFIX_REGEX.test(opts.scope)) {\n await this.lookupGroup(opts.scope);\n return;\n }\n throw new Error(`jpzip: invalid preload scope ${JSON.stringify(opts.scope)}`);\n }\n\n /** Clear all SDK-managed caches (L1 + L2). */\n async refresh(): Promise<void> {\n this.mem.clear();\n this.metaPromise = null;\n this.knownVersion = null;\n await this.cache?.clear();\n }\n\n /* ----------------------------- internals ------------------------------ */\n\n private async fetchPrefixDict(prefix: string): Promise<ZipcodeDict | null> {\n const url = this.prefixURL(prefix);\n const cached = this.mem.get(url);\n if (cached) return cached;\n\n const fromL2 = await this.readL2(url);\n if (fromL2) {\n this.mem.set(url, fromL2);\n return fromL2;\n }\n\n const dict = await fetchDict(url, this.fetchOpts());\n if (dict) {\n this.mem.set(url, dict);\n await this.writeL2(url, dict);\n }\n return dict;\n }\n\n private async fetchGroupDict(prefix1: string): Promise<ZipcodeDict | null> {\n const url = `${this.baseUrl}/g/${prefix1}.json`;\n const cached = this.mem.get(url);\n if (cached) return cached;\n const dict = await fetchDict(url, this.fetchOpts());\n if (dict) this.mem.set(url, dict);\n return dict;\n }\n\n private prefixURL(prefix3: string): string {\n return `${this.baseUrl}/p/${prefix3}.json`;\n }\n\n private fetchOpts(): FetchOptions {\n return { fetch: this.fetchImpl };\n }\n\n private async readL2(url: string): Promise<ZipcodeDict | null> {\n if (!this.cache) return null;\n const bytes = await this.cache.get(url);\n if (!bytes) return null;\n try {\n const text = new TextDecoder().decode(bytes);\n return JSON.parse(text) as ZipcodeDict;\n } catch {\n // corrupt cache — drop the entry and refetch\n await this.cache.delete(url);\n return null;\n }\n }\n\n private async writeL2(url: string, dict: ZipcodeDict): Promise<void> {\n if (!this.cache) return;\n const bytes = new TextEncoder().encode(JSON.stringify(dict));\n await this.cache.set(url, bytes);\n }\n}\n","export { JpzipClient, DEFAULT_BASE_URL, type JpzipClientOptions } from './client.js';\nexport { MemoryLRU, type PersistentCache } from './cache.js';\nexport type { ZipcodeEntry, Town, Meta, ZipcodeDict, Endpoints } from './types.js';\n\nimport { JpzipClient } from './client.js';\nimport type { Meta, ZipcodeDict, ZipcodeEntry } from './types.js';\n\n/**\n * The functional helpers below all delegate to a lazily-initialized default\n * client (L1 cache only, default base URL). For multiple SDK instances or\n * an L2 cache, construct `new JpzipClient({...})` yourself.\n */\n\nlet _default: JpzipClient | null = null;\nfunction defaultClient(): JpzipClient {\n if (_default === null) _default = new JpzipClient();\n return _default;\n}\n\n/** Reset the singleton (mainly for tests). */\nexport function _resetDefaultClient(): void {\n _default = null;\n}\n\n/** Configure the singleton's options. Subsequent calls re-create the client. */\nexport function configure(options: ConstructorParameters<typeof JpzipClient>[0]): void {\n _default = new JpzipClient(options);\n}\n\nexport const lookup = (zipcode: string): Promise<ZipcodeEntry | null> =>\n defaultClient().lookup(zipcode);\n\nexport const lookupGroup = (prefix: string): Promise<ZipcodeDict> =>\n defaultClient().lookupGroup(prefix);\n\nexport const lookupAll = (): Promise<ZipcodeDict> => defaultClient().lookupAll();\n\nexport const preload = (opts: Parameters<JpzipClient['preload']>[0]): Promise<void> =>\n defaultClient().preload(opts);\n\nexport const getMeta = (): Promise<Meta | null> => defaultClient().getMeta();\n\n/** Helper: returns true iff `zip` is a syntactically valid 7-digit zipcode. */\nexport function isValidZipcode(zip: string): boolean {\n return /^\\d{7}$/.test(zip);\n}\n"],"mappings":"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","names":[]}
|