@jpzip/jpzip 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ja.md ADDED
@@ -0,0 +1,265 @@
1
+ ## jpzip-js
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@jpzip/jpzip.svg)](https://www.npmjs.com/package/@jpzip/jpzip)
4
+ [![types: included](https://img.shields.io/npm/types/@jpzip/jpzip.svg)](https://www.npmjs.com/package/@jpzip/jpzip)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+ [![Test](https://github.com/jpzip/js/actions/workflows/test.yml/badge.svg)](https://github.com/jpzip/js/actions/workflows/test.yml)
7
+
8
+ > **jpzip** の TypeScript / JavaScript SDK — 無料・無制限の日本郵便番号 API。
9
+ > 日本郵便の `KEN_ALL.csv` / `KEN_ALL_ROME.csv` を JSON 正規化し CDN 配信。
10
+
11
+ [English](./README.md) | **日本語**
12
+
13
+ `@jpzip/jpzip` は `jpzip.nadai.dev` から日本の郵便番号 120,677 件を引く TypeScript SDK です。
14
+ 登録不要、レート制限なし、API キー不要。
15
+
16
+ - 🇯🇵 **全件収録** — 漢字・カナ・ローマ字・自治体コード(JIS X 0401 / 総務省地方公共団体コード)
17
+ - ⚡️ **高速** — L1 LRU + 任意の L2 永続キャッシュ。`preload` でネットワーク往復なしのルックアップが可能
18
+ - 🛡️ **堅牢** — 5xx / ネットワーク失敗時は指数バックオフで最大 3 回リトライ
19
+ - 🪶 **ランタイム依存ゼロ** — プラットフォーム標準の `fetch` のみ使用
20
+ - 🧰 **型安全** — TypeScript ファーストクラス、ESM + CJS デュアルビルド
21
+ - 🆓 **永久無料** — Cloudflare Pages 無料枠で運用(課金軸が存在しない)
22
+ - 🔌 **同一 API** — [全 jpzip SDK](#他言語版) で API が揃う
23
+
24
+ ## 必要環境
25
+
26
+ Node.js 18+(または `fetch` が標準搭載されたランタイム — Bun、Deno、モダンブラウザ、Cloudflare Workers、Vercel Edge)。
27
+
28
+ ## インストール
29
+
30
+ ```bash
31
+ npm install @jpzip/jpzip
32
+ # または
33
+ pnpm add @jpzip/jpzip
34
+ # または
35
+ yarn add @jpzip/jpzip
36
+ ```
37
+
38
+ ## クイックスタート
39
+
40
+ ```ts
41
+ import { lookup } from '@jpzip/jpzip';
42
+
43
+ const entry = await lookup('2310017');
44
+ if (entry === null) {
45
+ console.log('見つかりません');
46
+ } else {
47
+ console.log(entry.prefecture, entry.city, entry.towns[0].town);
48
+ // 出力: 神奈川県 横浜市中区 港町
49
+ }
50
+ ```
51
+
52
+ ローマ字・自治体コードも同じエントリに含まれます:
53
+
54
+ ```ts
55
+ console.log(entry.prefecture_roma, entry.city_roma, entry.towns[0].roma);
56
+ // 出力: Kanagawa Ken Yokohama Shi Naka Ku Minatocho
57
+
58
+ console.log(entry.prefecture_code, entry.city_code);
59
+ // 出力: 14 14104
60
+ ```
61
+
62
+ ## ユースケース
63
+
64
+ ### 郵便番号ルックアップ HTTP エンドポイント (Hono)
65
+
66
+ ```ts
67
+ import { Hono } from 'hono';
68
+ import { lookup } from '@jpzip/jpzip';
69
+
70
+ const app = new Hono();
71
+
72
+ app.get('/api/zipcode/:code', async (c) => {
73
+ const entry = await lookup(c.req.param('code'));
74
+ if (entry === null) return c.notFound();
75
+ return c.json(entry);
76
+ });
77
+
78
+ export default app;
79
+ ```
80
+
81
+ Node / Bun / Cloudflare Workers / Vercel Edge でそのまま動作します。
82
+
83
+ ### 郵便番号ルックアップ HTTP エンドポイント (Express)
84
+
85
+ ```ts
86
+ import express from 'express';
87
+ import { lookup } from '@jpzip/jpzip';
88
+
89
+ const app = express();
90
+
91
+ app.get('/api/zipcode/:code', async (req, res, next) => {
92
+ try {
93
+ const entry = await lookup(req.params.code);
94
+ if (entry === null) return res.status(404).end();
95
+ res.json(entry);
96
+ } catch (err) {
97
+ next(err);
98
+ }
99
+ });
100
+
101
+ app.listen(3000);
102
+ ```
103
+
104
+ ### CSV のバッチ検証
105
+
106
+ ```ts
107
+ import { lookupAll } from '@jpzip/jpzip';
108
+
109
+ const all = await lookupAll(); // 全件をメモリに展開(JSON 約 37 MiB)
110
+ for (const zip of csvZipcodes) {
111
+ if (!(zip in all)) {
112
+ console.warn(`不正な郵便番号: ${zip}`);
113
+ }
114
+ }
115
+ ```
116
+
117
+ ### キャッシュからの提供(任意の L2 バックエンド)
118
+
119
+ データは 948 個の 3 桁 prefix バケットに分割されています。デフォルト L1 (100 件) は
120
+ ホットなバケットを保持しますが、全件を常駐させるには L2 を併用するか
121
+ `memoryCacheSize` を 948 超に設定してください。
122
+
123
+ ```ts
124
+ import { JpzipClient, type PersistentCache } from '@jpzip/jpzip';
125
+ import { readFile, writeFile, unlink, rm } from 'node:fs/promises';
126
+ import { join } from 'node:path';
127
+ import { createHash } from 'node:crypto';
128
+
129
+ const dir = '.jpzip-cache';
130
+ const path = (key: string) => join(dir, createHash('sha1').update(key).digest('hex'));
131
+
132
+ const fileCache: PersistentCache = {
133
+ async get(key) {
134
+ try {
135
+ return await readFile(path(key));
136
+ } catch {
137
+ return null;
138
+ }
139
+ },
140
+ async set(key, value) {
141
+ await writeFile(path(key), value);
142
+ },
143
+ async delete(key) {
144
+ await unlink(path(key)).catch(() => {});
145
+ },
146
+ async clear() {
147
+ await rm(dir, { recursive: true, force: true });
148
+ },
149
+ };
150
+
151
+ const client = new JpzipClient({
152
+ memoryCacheSize: 1024,
153
+ cache: fileCache,
154
+ });
155
+
156
+ await client.preload({ scope: 'all' });
157
+ // 以降の lookup は L1/L2 で完結し、ネットワークにアクセスしない
158
+ const entry = await client.lookup('2310017');
159
+ ```
160
+
161
+ ## API リファレンス
162
+
163
+ ### 関数(モジュールレベル、内部の default `JpzipClient` シングルトンを共有)
164
+
165
+ | 関数 | 説明 |
166
+ |---|---|
167
+ | `lookup(zipcode)` | 7 桁の郵便番号で 1 件引く。見つからない / 不正な入力は `null`(不正入力時はネットワーク不使用)。 |
168
+ | `lookupGroup(prefix)` | 1〜3 桁の prefix で引く。1 桁は `/g/{d}.json` を 1 回、3 桁は `/p/{ddd}.json` を 1 回、2 桁は 10 並列 fetch して結合。数字以外を含む入力では throw。 |
169
+ | `lookupAll()` | `/g/0..9.json` を並列取得して全件(120k 件、約 37 MiB)を返す。 |
170
+ | `getMeta()` | データバージョン・生成日時・都道府県別件数・spec version。client の `refresh()` を呼ぶまで結果をキャッシュ。 |
171
+ | `preload({ scope })` | `'all'` または 1〜3 桁の prefix で L1(L2 設定時は L2 も)を温める。 |
172
+ | `isValidZipcode(zip)` | 純粋な書式チェック(`^\d{7}$`)。ネットワーク不使用。 |
173
+ | `configure(options)` | シングルトンを差し替え、以降の関数呼び出しに新オプションを適用。 |
174
+
175
+ ### `JpzipClient`(高度な用途)
176
+
177
+ L2 キャッシュ、`fetch` 差し替え、配信元変更、複数の独立キャッシュが必要な場合にインスタンスを直接生成します:
178
+
179
+ ```ts
180
+ import { JpzipClient } from '@jpzip/jpzip';
181
+
182
+ const client = new JpzipClient({
183
+ baseUrl: 'https://jpzip.nadai.dev',
184
+ fetch: globalThis.fetch, // 任意で差し替え
185
+ memoryCacheSize: 200, // L1 容量(prefix バケット数)、デフォルト 100
186
+ cache: myPersistentCache, // L2(任意)
187
+ onSpecMismatch: ({ expected, received }) => {
188
+ console.warn(`jpzip spec 不一致: SDK=${expected} server=${received}`);
189
+ },
190
+ });
191
+ ```
192
+
193
+ `JpzipClient` は `lookup` / `lookupGroup` / `lookupAll` / `getMeta` / `preload` に加えて:
194
+
195
+ | メソッド | 説明 |
196
+ |---|---|
197
+ | `client.refresh()` | L1(L2 設定時は L2 も)を消し、キャッシュ済み meta を破棄。 |
198
+
199
+ `getMeta()` が `/meta.json` の `version` 変更を検知すると L1/L2 が自動クリアされます。
200
+ データ切り替えに追従するには `getMeta()` を定期的に呼んでください。
201
+
202
+ ### エラー
203
+
204
+ - `lookup()` は「見つからない」「書式不正」いずれの場合も throw せず `null` を返します。
205
+ - `lookupGroup(prefix)` は `prefix` が `/^\d{1,3}$/` に一致しない場合 `Error` を throw。
206
+ - ネットワーク失敗と 5xx は最大 3 回試行(初回 + リトライ 2 回)、指数バックオフのスリープは 400ms / 800ms。404 以外の 4xx は即時 throw、404 は `null` 返却。`AbortError` はリトライせず再 throw。
207
+
208
+ ### `PersistentCache` インターフェース
209
+
210
+ 任意の L2 バックエンド(ファイル / IndexedDB / Redis / Cloudflare KV など)を渡せます:
211
+
212
+ ```ts
213
+ export interface PersistentCache {
214
+ get(key: string): Promise<Uint8Array | null>;
215
+ set(key: string, value: Uint8Array): Promise<void>;
216
+ delete(key: string): Promise<void>;
217
+ clear(): Promise<void>;
218
+ }
219
+ ```
220
+
221
+ キーは prefix バケットの完全 URL(例: `https://jpzip.nadai.dev/p/231.json`)、値は生 JSON バイト列。
222
+
223
+ ### エクスポートされる型
224
+
225
+ `ZipcodeEntry` / `Town` / `Meta` / `ZipcodeDict` / `Endpoints` / `JpzipClientOptions` / `PersistentCache` — すべて `@jpzip/jpzip` から import 可能。
226
+
227
+ ## なぜ jpzip-js か
228
+
229
+ | | **jpzip-js** | [jpostcode][jpostcode] | [jposta][jposta] | [@ken-all/kenall][kenall] |
230
+ |---|---|---|---|---|
231
+ | ローマ字(`Yokohama Shi`) | ✅ | ❌ | ❌ | ⚠️ オプション |
232
+ | 自治体コード(JIS / 総務省) | ✅ | ❌ | ⚠️ `prefNum` のみ | ✅ |
233
+ | カナ | ✅ | ✅ | ❌ | ✅ |
234
+ | ビルドに数 MB のデータを同梱しない | ✅ CDN fetch | ⚠️ npm 版は同梱 | ❌ JSON 同梱 | ✅ |
235
+ | `npm install` 不要で月次更新 | ✅ CDN で自動 | ❌ 月次再公開 | ❌ 手動 | ✅ |
236
+ | API キー不要 | ✅ | ✅ | ✅ | ❌ 必須 |
237
+ | レート制限なし | ✅ | ✅ | ✅ | ⚠️ プラン依存 |
238
+ | L1 + 差し替え可能な L2 | ✅ | ❌ | ❌ | ❌ |
239
+ | ランタイム依存ゼロ | ✅ | ✅ | ✅ | ❌ (`zod`) |
240
+ | ESM + CJS + TypeScript 型 | ✅ | ✅ | ✅ | ✅ |
241
+
242
+ [jpostcode]: https://www.npmjs.com/package/jpostcode
243
+ [jposta]: https://www.npmjs.com/package/jposta
244
+ [kenall]: https://www.npmjs.com/package/@ken-all/kenall
245
+
246
+ ## 他言語版
247
+
248
+ 全 SDK で同一の API を提供しています:
249
+
250
+ [Go](https://github.com/jpzip/go) · [Python](https://github.com/jpzip/python) · [Rust](https://github.com/jpzip/rust) · [Ruby](https://github.com/jpzip/ruby) · [PHP](https://github.com/jpzip/php) · [Swift](https://github.com/jpzip/swift) · [Dart](https://github.com/jpzip/dart)
251
+
252
+ ## 関連リソース
253
+
254
+ - **Web サイト** — https://jpzip.nadai.dev
255
+ - **プロトコル仕様** — [jpzip/spec](https://github.com/jpzip/spec)
256
+ - **データ ETL** — [jpzip/data](https://github.com/jpzip/data)
257
+ - **MCP サーバー** — [jpzip/mcp](https://github.com/jpzip/mcp) — Claude / ChatGPT / Cursor から jpzip を呼ぶ
258
+
259
+ ## キーワード
260
+
261
+ 日本郵便番号, 郵便番号, KEN_ALL, KEN_ALL_ROME, 住所バリデーション, 住所検索, japanese postal code, japan zipcode, postal code lookup typescript, typescript japanese address, node.js zipcode, hono 郵便番号, express 郵便番号, cloudflare workers 住所, JIS X 0401, 総務省地方公共団体コード
262
+
263
+ ## ライセンス
264
+
265
+ [MIT](./LICENSE)
package/README.md CHANGED
@@ -1,88 +1,270 @@
1
- # jpzip — TypeScript / JavaScript SDK
1
+ ## jpzip-js
2
2
 
3
- > 日本の郵便番号を CDN 配信の JSON データから引く SDK。`jpzip.nadai.dev` から取得する。
3
+ [![npm version](https://img.shields.io/npm/v/@jpzip/jpzip.svg)](https://www.npmjs.com/package/@jpzip/jpzip)
4
+ [![types: included](https://img.shields.io/npm/types/@jpzip/jpzip.svg)](https://www.npmjs.com/package/@jpzip/jpzip)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+ [![Test](https://github.com/jpzip/js/actions/workflows/test.yml/badge.svg)](https://github.com/jpzip/js/actions/workflows/test.yml)
4
7
 
5
- - 配信ドメイン: `https://jpzip.nadai.dev`
6
- - プロトコル仕様: [`jpzip/spec`](https://github.com/jpzip/spec)
7
- - データ ETL: [`jpzip/data`](https://github.com/jpzip/data)
8
+ > TypeScript / JavaScript SDK for **jpzip** — a free, unlimited Japanese postal code (郵便番号) API.
9
+ > 日本の全郵便番号 120,677 件を CDN 配信 JSON から引く TypeScript SDK。
8
10
 
9
- ## インストール
11
+ **English** | [日本語](./README.ja.md)
10
12
 
11
- ```sh
13
+ `@jpzip/jpzip` looks up Japanese postal codes (郵便番号) from `jpzip.nadai.dev`,
14
+ a CDN-hosted dataset built from Japan Post's `KEN_ALL.csv` and `KEN_ALL_ROME.csv`
15
+ normalized to JSON. No registration, no rate limits, no API key.
16
+
17
+ - 🇯🇵 **Complete dataset** — 120,677 entries with kanji, kana, romaji, and government codes (JIS X 0401 / 総務省地方公共団体コード)
18
+ - ⚡️ **Fast** — L1 LRU + optional L2 persistent cache; `preload` to serve lookups without per-request network round-trips
19
+ - 🛡️ **Resilient** — up to 3 attempts with exponential backoff on 5xx / network failures
20
+ - 🪶 **Zero runtime deps** — uses the platform `fetch` only
21
+ - 🧰 **Typed end to end** — first-class TypeScript, ESM + CJS dual build
22
+ - 🆓 **Free forever** — backed by Cloudflare Pages' free tier (no billing axis exists)
23
+ - 🔌 **Drop-in** — same API surface across [every jpzip SDK](#other-languages)
24
+
25
+ ## Requirements
26
+
27
+ Node.js 18+ (or any runtime with a global `fetch` — Bun, Deno, modern browsers, Cloudflare Workers, Vercel Edge).
28
+
29
+ ## Install
30
+
31
+ ```bash
12
32
  npm install @jpzip/jpzip
13
33
  # or
14
34
  pnpm add @jpzip/jpzip
35
+ # or
36
+ yarn add @jpzip/jpzip
15
37
  ```
16
38
 
17
- ## 使い方
18
-
19
- ### 関数 API
39
+ ## Quick Start
20
40
 
21
41
  ```ts
22
- import { lookup, lookupGroup, lookupAll, preload, getMeta } from '@jpzip/jpzip';
42
+ import { lookup } from '@jpzip/jpzip';
23
43
 
24
44
  const entry = await lookup('2310017');
25
- // { prefecture: '神奈川県', city: '横浜市中区', towns: [...], ... }
45
+ if (entry === null) {
46
+ console.log('not found');
47
+ } else {
48
+ console.log(entry.prefecture, entry.city, entry.towns[0].town);
49
+ // Output: 神奈川県 横浜市中区 港町
50
+ }
51
+ ```
52
+
53
+ Romaji and government codes are included on the same entry:
54
+
55
+ ```ts
56
+ console.log(entry.prefecture_roma, entry.city_roma, entry.towns[0].roma);
57
+ // Output: Kanagawa Ken Yokohama Shi Naka Ku Minatocho
58
+
59
+ console.log(entry.prefecture_code, entry.city_code);
60
+ // Output: 14 14104
61
+ ```
62
+
63
+ ## Use Cases
64
+
65
+ ### Zipcode lookup HTTP endpoint (Hono)
66
+
67
+ ```ts
68
+ import { Hono } from 'hono';
69
+ import { lookup } from '@jpzip/jpzip';
70
+
71
+ const app = new Hono();
72
+
73
+ app.get('/api/zipcode/:code', async (c) => {
74
+ const entry = await lookup(c.req.param('code'));
75
+ if (entry === null) return c.notFound();
76
+ return c.json(entry);
77
+ });
78
+
79
+ export default app;
80
+ ```
81
+
82
+ Works unchanged on Node, Bun, Cloudflare Workers, and Vercel Edge.
83
+
84
+ ### Zipcode lookup HTTP endpoint (Express)
85
+
86
+ ```ts
87
+ import express from 'express';
88
+ import { lookup } from '@jpzip/jpzip';
89
+
90
+ const app = express();
91
+
92
+ app.get('/api/zipcode/:code', async (req, res, next) => {
93
+ try {
94
+ const entry = await lookup(req.params.code);
95
+ if (entry === null) return res.status(404).end();
96
+ res.json(entry);
97
+ } catch (err) {
98
+ next(err);
99
+ }
100
+ });
101
+
102
+ app.listen(3000);
103
+ ```
104
+
105
+ ### Batch validation
106
+
107
+ ```ts
108
+ import { lookupAll } from '@jpzip/jpzip';
109
+
110
+ const all = await lookupAll(); // entire dataset in memory (~37 MiB JSON)
111
+ for (const zip of csvZipcodes) {
112
+ if (!(zip in all)) {
113
+ console.warn(`invalid zipcode: ${zip}`);
114
+ }
115
+ }
116
+ ```
117
+
118
+ ### Serve lookups from cache (BYO L2 backend)
119
+
120
+ The dataset is partitioned into 948 three-digit prefix buckets. The default
121
+ L1 (100 entries) keeps the hottest buckets; to cache the whole dataset, pair
122
+ `preload({ scope: 'all' })` with an L2 cache or raise `memoryCacheSize` above 948.
123
+
124
+ ```ts
125
+ import { JpzipClient, type PersistentCache } from '@jpzip/jpzip';
126
+ import { readFile, writeFile, unlink, rm } from 'node:fs/promises';
127
+ import { join } from 'node:path';
128
+ import { createHash } from 'node:crypto';
129
+
130
+ const dir = '.jpzip-cache';
131
+ const path = (key: string) => join(dir, createHash('sha1').update(key).digest('hex'));
132
+
133
+ const fileCache: PersistentCache = {
134
+ async get(key) {
135
+ try {
136
+ return await readFile(path(key));
137
+ } catch {
138
+ return null;
139
+ }
140
+ },
141
+ async set(key, value) {
142
+ await writeFile(path(key), value);
143
+ },
144
+ async delete(key) {
145
+ await unlink(path(key)).catch(() => {});
146
+ },
147
+ async clear() {
148
+ await rm(dir, { recursive: true, force: true });
149
+ },
150
+ };
26
151
 
27
- const dict231 = await lookupGroup('231'); // /p/231.json
28
- const dict23 = await lookupGroup('23'); // /p/230.json - /p/239.json を並列 fetch
29
- const dict2 = await lookupGroup('2'); // /g/2.json
152
+ const client = new JpzipClient({
153
+ memoryCacheSize: 1024,
154
+ cache: fileCache,
155
+ });
30
156
 
31
- const all = await lookupAll(); // /all.json (大きい)
32
- const meta = await getMeta();
157
+ await client.preload({ scope: 'all' });
158
+ // Subsequent lookups are served from L1/L2 without hitting the network.
159
+ const entry = await client.lookup('2310017');
33
160
  ```
34
161
 
35
- ### クラス API (L2 キャッシュ・複数インスタンス用)
162
+ ## API Reference
163
+
164
+ ### Functions (module-level, share a default `JpzipClient` singleton)
165
+
166
+ | Function | Description |
167
+ |---|---|
168
+ | `lookup(zipcode)` | Look up a single 7-digit zipcode. Returns `null` for not-found or malformed input (no network call for malformed input). |
169
+ | `lookupGroup(prefix)` | Look up by 1-, 2-, or 3-digit prefix. 1-digit fetches `/g/{d}.json`; 3-digit fetches `/p/{ddd}.json`; 2-digit fans out into 10 parallel 3-digit fetches and merges. Throws on non-digit input. |
170
+ | `lookupAll()` | Fetch entire dataset (120k entries, ~37 MiB) in parallel across `/g/0..9.json`. |
171
+ | `getMeta()` | Dataset version, generated-at, per-prefecture counts, spec version. Result is cached until `refresh()` is called on the client. |
172
+ | `preload({ scope })` | Warm L1 (and L2 when configured) for `'all'` or a specific 1-3 digit prefix. |
173
+ | `isValidZipcode(zip)` | Pure syntax check (`^\d{7}$`) — no network. |
174
+ | `configure(options)` | Replace the singleton with a new `JpzipClient` configured with `options`. |
175
+
176
+ ### `JpzipClient` (advanced)
177
+
178
+ Construct an instance for L2 caching, custom `fetch`, alternate base URL, or
179
+ multiple isolated caches:
36
180
 
37
181
  ```ts
38
182
  import { JpzipClient } from '@jpzip/jpzip';
39
183
 
40
184
  const client = new JpzipClient({
41
185
  baseUrl: 'https://jpzip.nadai.dev',
42
- memoryCacheSize: 200,
43
- // 永続キャッシュ (Cache インターフェースを満たす任意の実装)
44
- cache: {
45
- async get(k) { /* ... */ return null; },
46
- async set(k, v) { /* ... */ },
47
- async delete(k) { /* ... */ },
48
- async clear() { /* ... */ },
186
+ fetch: globalThis.fetch, // optional override
187
+ memoryCacheSize: 200, // L1 capacity in prefix buckets, default 100
188
+ cache: myPersistentCache, // optional L2
189
+ onSpecMismatch: ({ expected, received }) => {
190
+ console.warn(`jpzip spec mismatch: SDK=${expected} server=${received}`);
49
191
  },
50
192
  });
193
+ ```
51
194
 
52
- await client.preload({ scope: 'all' }); // オフラインモード相当
53
- const entry = await client.lookup('2310017');
195
+ `JpzipClient` exposes `lookup` / `lookupGroup` / `lookupAll` / `getMeta` / `preload` plus:
196
+
197
+ | Method | Description |
198
+ |---|---|
199
+ | `client.refresh()` | Wipe L1 (and L2 when configured) and forget the cached meta. |
200
+
201
+ When `getMeta()` observes that `/meta.json`'s `version` has changed since the last
202
+ successful fetch, L1 and L2 are cleared automatically — call `getMeta()` periodically
203
+ to pick up dataset rollovers.
204
+
205
+ ### Errors
206
+
207
+ - `lookup()` returns `null` rather than throwing for both "not found" and malformed input.
208
+ - `lookupGroup(prefix)` throws `Error` if `prefix` doesn't match `/^\d{1,3}$/`.
209
+ - Transient network failures and 5xx responses are retried up to 3 attempts (initial + 2 retries) with exponential backoff sleeps of 400ms and 800ms. 4xx responses other than 404 throw immediately; 404 yields `null`. `AbortError` propagates without retry.
210
+
211
+ ### `PersistentCache` interface
212
+
213
+ Bring your own L2 backend (file system, IndexedDB, Redis, Cloudflare KV, etc.):
214
+
215
+ ```ts
216
+ export interface PersistentCache {
217
+ get(key: string): Promise<Uint8Array | null>;
218
+ set(key: string, value: Uint8Array): Promise<void>;
219
+ delete(key: string): Promise<void>;
220
+ clear(): Promise<void>;
221
+ }
54
222
  ```
55
223
 
56
- ## キャッシュ戦略 (3 )
224
+ Keys are the full bucket URLs (e.g. `https://jpzip.nadai.dev/p/231.json`); values
225
+ are raw JSON bytes.
57
226
 
58
- | | 目的 | 既定 |
59
- |---|---|---|
60
- | **L1 メモリ LRU** | 同一プロセスの重複 fetch 抑制 | 常時 ON、prefix 100 件保持 |
61
- | **L2 永続キャッシュ** | 起動またぎ / preload 結果保持 | OFF、`cache` オプションで有効化 |
62
- | **L3 HTTP** | ブラウザ / OS のキャッシュ | SDK 制御外 |
227
+ ### Exported types
63
228
 
64
- 詳しくは [`jpzip/spec` §6.4](https://github.com/jpzip/spec/blob/main/spec/v1/protocol.md) を参照。
229
+ `ZipcodeEntry`, `Town`, `Meta`, `ZipcodeDict`, `Endpoints`, `JpzipClientOptions`,
230
+ `PersistentCache` — all importable from `@jpzip/jpzip`.
65
231
 
66
- ## API
232
+ ## Why jpzip-js?
67
233
 
68
- | 関数 | 説明 |
69
- |---|---|
70
- | `lookup(zipcode)` | 単一 zipcode を引く。`null` は「見つからない」 |
71
- | `lookupGroup(prefix)` | 1〜3 桁の prefix 配下を返す |
72
- | `lookupAll()` | 全件辞書 (preload 用) |
73
- | `preload({scope})` | `'all'` または prefix SDK 内キャッシュに格納 |
74
- | `getMeta()` | `/meta.json` を取得・キャッシュ |
75
- | `isValidZipcode(zip)` | 7 桁数字フォーマット検証 |
76
- | `configure(options)` | グローバルシングルトンのオプション差し替え |
234
+ | | **jpzip-js** | [jpostcode][jpostcode] | [jposta][jposta] | [@ken-all/kenall][kenall] |
235
+ |---|---|---|---|---|
236
+ | Romaji (`Yokohama Shi`) | | | | ⚠️ Optional field |
237
+ | Government codes (JIS / 総務省) | | | ⚠️ `prefNum` only | ✅ |
238
+ | Kana | | | ❌ | ✅ |
239
+ | No bundled multi-MB data in your build | CDN fetch | ⚠️ npm version embeds data | ❌ Embedded JSON | ✅ |
240
+ | Monthly updates without re-`npm install` | Auto via CDN | ❌ Republished monthly | ❌ Manual | ✅ |
241
+ | No API key | | | ✅ | ❌ Required |
242
+ | Rate-limit-free | | ✅ | ✅ | ⚠️ Plan-based quota |
243
+ | L1 + pluggable L2 cache | ✅ | ❌ | ❌ | ❌ |
244
+ | Zero runtime deps | ✅ | ✅ | ✅ | ❌ (`zod`) |
245
+ | ESM + CJS + TypeScript types | ✅ | ✅ | ✅ | ✅ |
246
+
247
+ [jpostcode]: https://www.npmjs.com/package/jpostcode
248
+ [jposta]: https://www.npmjs.com/package/jposta
249
+ [kenall]: https://www.npmjs.com/package/@ken-all/kenall
250
+
251
+ ## Other Languages
252
+
253
+ Same API surface across all SDKs:
254
+
255
+ [Go](https://github.com/jpzip/go) · [Python](https://github.com/jpzip/python) · [Rust](https://github.com/jpzip/rust) · [Ruby](https://github.com/jpzip/ruby) · [PHP](https://github.com/jpzip/php) · [Swift](https://github.com/jpzip/swift) · [Dart](https://github.com/jpzip/dart)
77
256
 
78
- ## 入力検証
257
+ ## Resources
79
258
 
80
- `lookup()` `^\d{7}$` にマッチしない入力には fetch せず `null` を返す。
259
+ - **Website** https://jpzip.nadai.dev
260
+ - **Protocol spec** — [jpzip/spec](https://github.com/jpzip/spec)
261
+ - **Data ETL** — [jpzip/data](https://github.com/jpzip/data)
262
+ - **MCP server** — [jpzip/mcp](https://github.com/jpzip/mcp) — use jpzip from Claude / ChatGPT / Cursor
81
263
 
82
- ## バージョン整合性
264
+ ## Keywords
83
265
 
84
- `getMeta()` 初回呼び出し時、`spec_version` SDK 対応バージョン (`"1.0"`) と異なる場合は `onSpecMismatch` コールバックを 1 回呼ぶ。`version` (データバージョン) が変わったらキャッシュを自動 invalidate。
266
+ japanese postal code, japan zipcode, 郵便番号, KEN_ALL, KEN_ALL_ROME, address validation, postal code lookup typescript, typescript japanese address, node.js zipcode, hono postal code, express zipcode, cloudflare workers japan address, JIS X 0401, 総務省地方公共団体コード
85
267
 
86
- ## ライセンス
268
+ ## License
87
269
 
88
270
  [MIT](./LICENSE)
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
- const init = {
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
  }
@@ -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
- const init = {
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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jpzip/jpzip",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Japan postal-code dataset SDK — fetches normalized JSON from jpzip.nadai.dev",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",