@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 +265 -0
- package/README.md +231 -49
- 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/README.ja.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
## jpzip-js
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@jpzip/jpzip)
|
|
4
|
+
[](https://www.npmjs.com/package/@jpzip/jpzip)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](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
|
-
|
|
1
|
+
## jpzip-js
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@jpzip/jpzip)
|
|
4
|
+
[](https://www.npmjs.com/package/@jpzip/jpzip)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://github.com/jpzip/js/actions/workflows/test.yml)
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
|
42
|
+
import { lookup } from '@jpzip/jpzip';
|
|
23
43
|
|
|
24
44
|
const entry = await lookup('2310017');
|
|
25
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
152
|
+
const client = new JpzipClient({
|
|
153
|
+
memoryCacheSize: 1024,
|
|
154
|
+
cache: fileCache,
|
|
155
|
+
});
|
|
30
156
|
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
//
|
|
44
|
-
cache:
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
229
|
+
`ZipcodeEntry`, `Town`, `Meta`, `ZipcodeDict`, `Endpoints`, `JpzipClientOptions`,
|
|
230
|
+
`PersistentCache` — all importable from `@jpzip/jpzip`.
|
|
65
231
|
|
|
66
|
-
##
|
|
232
|
+
## Why jpzip-js?
|
|
67
233
|
|
|
68
|
-
|
|
|
69
|
-
|
|
70
|
-
|
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
-
| `
|
|
75
|
-
|
|
|
76
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":[]}
|