@jpzip/jpzip 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/dist/index.cjs +301 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +138 -0
- package/dist/index.d.ts +138 -0
- package/dist/index.js +264 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nadai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# jpzip — TypeScript / JavaScript SDK
|
|
2
|
+
|
|
3
|
+
> 日本の郵便番号を CDN 配信の JSON データから引く SDK。`jpzip.nadai.dev` から取得する。
|
|
4
|
+
|
|
5
|
+
- 配信ドメイン: `https://jpzip.nadai.dev`
|
|
6
|
+
- プロトコル仕様: [`jpzip/spec`](https://github.com/jpzip/spec)
|
|
7
|
+
- データ ETL: [`jpzip/data`](https://github.com/jpzip/data)
|
|
8
|
+
|
|
9
|
+
## インストール
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install @jpzip/jpzip
|
|
13
|
+
# or
|
|
14
|
+
pnpm add @jpzip/jpzip
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 使い方
|
|
18
|
+
|
|
19
|
+
### 関数 API
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { lookup, lookupGroup, lookupAll, preload, getMeta } from '@jpzip/jpzip';
|
|
23
|
+
|
|
24
|
+
const entry = await lookup('2310017');
|
|
25
|
+
// → { prefecture: '神奈川県', city: '横浜市中区', towns: [...], ... }
|
|
26
|
+
|
|
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
|
|
30
|
+
|
|
31
|
+
const all = await lookupAll(); // /all.json (大きい)
|
|
32
|
+
const meta = await getMeta();
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### クラス API (L2 キャッシュ・複数インスタンス用)
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { JpzipClient } from '@jpzip/jpzip';
|
|
39
|
+
|
|
40
|
+
const client = new JpzipClient({
|
|
41
|
+
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() { /* ... */ },
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await client.preload({ scope: 'all' }); // オフラインモード相当
|
|
53
|
+
const entry = await client.lookup('2310017');
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## キャッシュ戦略 (3 層)
|
|
57
|
+
|
|
58
|
+
| 層 | 目的 | 既定 |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| **L1 メモリ LRU** | 同一プロセスの重複 fetch 抑制 | 常時 ON、prefix 100 件保持 |
|
|
61
|
+
| **L2 永続キャッシュ** | 起動またぎ / preload 結果保持 | OFF、`cache` オプションで有効化 |
|
|
62
|
+
| **L3 HTTP** | ブラウザ / OS のキャッシュ | SDK 制御外 |
|
|
63
|
+
|
|
64
|
+
詳しくは [`jpzip/spec` §6.4](https://github.com/jpzip/spec/blob/main/spec/v1/protocol.md) を参照。
|
|
65
|
+
|
|
66
|
+
## API
|
|
67
|
+
|
|
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)` | グローバルシングルトンのオプション差し替え |
|
|
77
|
+
|
|
78
|
+
## 入力検証
|
|
79
|
+
|
|
80
|
+
`lookup()` は `^\d{7}$` にマッチしない入力には fetch せず `null` を返す。
|
|
81
|
+
|
|
82
|
+
## バージョン整合性
|
|
83
|
+
|
|
84
|
+
`getMeta()` 初回呼び出し時、`spec_version` が SDK 対応バージョン (`"1.0"`) と異なる場合は `onSpecMismatch` コールバックを 1 回呼ぶ。`version` (データバージョン) が変わったらキャッシュを自動 invalidate。
|
|
85
|
+
|
|
86
|
+
## ライセンス
|
|
87
|
+
|
|
88
|
+
[MIT](./LICENSE)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
|
|
24
|
+
JpzipClient: () => JpzipClient,
|
|
25
|
+
MemoryLRU: () => MemoryLRU,
|
|
26
|
+
_resetDefaultClient: () => _resetDefaultClient,
|
|
27
|
+
configure: () => configure,
|
|
28
|
+
getMeta: () => getMeta,
|
|
29
|
+
isValidZipcode: () => isValidZipcode,
|
|
30
|
+
lookup: () => lookup,
|
|
31
|
+
lookupAll: () => lookupAll,
|
|
32
|
+
lookupGroup: () => lookupGroup,
|
|
33
|
+
preload: () => preload
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
|
|
37
|
+
// src/cache.ts
|
|
38
|
+
var MemoryLRU = class {
|
|
39
|
+
max;
|
|
40
|
+
map = /* @__PURE__ */ new Map();
|
|
41
|
+
constructor(max = 100) {
|
|
42
|
+
this.max = Math.max(1, max);
|
|
43
|
+
}
|
|
44
|
+
get(key) {
|
|
45
|
+
const value = this.map.get(key);
|
|
46
|
+
if (value === void 0) return void 0;
|
|
47
|
+
this.map.delete(key);
|
|
48
|
+
this.map.set(key, value);
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
set(key, value) {
|
|
52
|
+
if (this.map.has(key)) {
|
|
53
|
+
this.map.delete(key);
|
|
54
|
+
} else if (this.map.size >= this.max) {
|
|
55
|
+
const oldestKey = this.map.keys().next().value;
|
|
56
|
+
if (oldestKey !== void 0) this.map.delete(oldestKey);
|
|
57
|
+
}
|
|
58
|
+
this.map.set(key, value);
|
|
59
|
+
}
|
|
60
|
+
clear() {
|
|
61
|
+
this.map.clear();
|
|
62
|
+
}
|
|
63
|
+
get size() {
|
|
64
|
+
return this.map.size;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// src/fetch.ts
|
|
69
|
+
var MAX_RETRIES = 3;
|
|
70
|
+
var BASE_DELAY_MS = 200;
|
|
71
|
+
async function fetchJSON(url, opts = {}) {
|
|
72
|
+
const f = opts.fetch ?? fetch;
|
|
73
|
+
let lastErr;
|
|
74
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
75
|
+
if (attempt > 0) {
|
|
76
|
+
await sleep(BASE_DELAY_MS * 2 ** attempt);
|
|
77
|
+
}
|
|
78
|
+
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();
|
|
95
|
+
} catch (err) {
|
|
96
|
+
lastErr = err;
|
|
97
|
+
if (err instanceof DOMException && err.name === "AbortError") throw err;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
throw lastErr instanceof Error ? lastErr : new Error(`jpzip: fetch failed for ${url}: ${String(lastErr)}`);
|
|
101
|
+
}
|
|
102
|
+
var fetchDict = (url, opts) => fetchJSON(url, opts);
|
|
103
|
+
var fetchMeta = (url, opts) => fetchJSON(url, opts);
|
|
104
|
+
function sleep(ms) {
|
|
105
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/client.ts
|
|
109
|
+
var DEFAULT_BASE_URL = "https://jpzip.nadai.dev";
|
|
110
|
+
var SUPPORTED_SPEC = "1.0";
|
|
111
|
+
var ZIP_REGEX = /^\d{7}$/;
|
|
112
|
+
var PREFIX_REGEX = /^\d{1,3}$/;
|
|
113
|
+
var JpzipClient = class {
|
|
114
|
+
baseUrl;
|
|
115
|
+
cache;
|
|
116
|
+
fetchImpl;
|
|
117
|
+
mem;
|
|
118
|
+
onSpecMismatch;
|
|
119
|
+
metaPromise = null;
|
|
120
|
+
knownVersion = null;
|
|
121
|
+
constructor(opts = {}) {
|
|
122
|
+
this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
123
|
+
this.cache = opts.cache;
|
|
124
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
125
|
+
this.mem = new MemoryLRU(opts.memoryCacheSize ?? 100);
|
|
126
|
+
this.onSpecMismatch = opts.onSpecMismatch;
|
|
127
|
+
}
|
|
128
|
+
/** Returns the entry for `zipcode`, or null if not found. */
|
|
129
|
+
async lookup(zipcode) {
|
|
130
|
+
if (!ZIP_REGEX.test(zipcode)) return null;
|
|
131
|
+
const prefix = zipcode.slice(0, 3);
|
|
132
|
+
const dict = await this.fetchPrefixDict(prefix);
|
|
133
|
+
if (!dict) return null;
|
|
134
|
+
return dict[zipcode] ?? null;
|
|
135
|
+
}
|
|
136
|
+
/** Returns the dictionary for a 1- or 3-digit prefix (2-digit is fanned out). */
|
|
137
|
+
async lookupGroup(prefix) {
|
|
138
|
+
if (!PREFIX_REGEX.test(prefix)) {
|
|
139
|
+
throw new Error(`jpzip: invalid prefix ${JSON.stringify(prefix)} (must be 1-3 digits)`);
|
|
140
|
+
}
|
|
141
|
+
if (prefix.length === 3) {
|
|
142
|
+
return await this.fetchPrefixDict(prefix) ?? {};
|
|
143
|
+
}
|
|
144
|
+
if (prefix.length === 1) {
|
|
145
|
+
const dict = await this.fetchGroupDict(prefix);
|
|
146
|
+
return dict ?? {};
|
|
147
|
+
}
|
|
148
|
+
const tasks = [];
|
|
149
|
+
for (let i = 0; i < 10; i++) {
|
|
150
|
+
tasks.push(this.fetchPrefixDict(`${prefix}${i}`));
|
|
151
|
+
}
|
|
152
|
+
const dicts = await Promise.all(tasks);
|
|
153
|
+
return Object.assign({}, ...dicts.filter(Boolean));
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Returns the full dataset. The CDN does not expose a single /all.json
|
|
157
|
+
* because the combined file exceeds Cloudflare Pages' 25 MiB per-file
|
|
158
|
+
* limit; instead we fan out across /g/0..9.json in parallel and merge.
|
|
159
|
+
*/
|
|
160
|
+
async lookupAll() {
|
|
161
|
+
const tasks = [];
|
|
162
|
+
for (let i = 0; i < 10; i++) {
|
|
163
|
+
tasks.push(this.fetchGroupDict(String(i)));
|
|
164
|
+
}
|
|
165
|
+
const dicts = await Promise.all(tasks);
|
|
166
|
+
return Object.assign({}, ...dicts.filter(Boolean));
|
|
167
|
+
}
|
|
168
|
+
/** Returns parsed meta.json, or null if the CDN has none yet. */
|
|
169
|
+
async getMeta() {
|
|
170
|
+
if (this.metaPromise === null) {
|
|
171
|
+
this.metaPromise = (async () => {
|
|
172
|
+
const m = await fetchMeta(`${this.baseUrl}/meta.json`, this.fetchOpts());
|
|
173
|
+
if (m && m.spec_version !== SUPPORTED_SPEC) {
|
|
174
|
+
this.onSpecMismatch?.({ expected: SUPPORTED_SPEC, received: m.spec_version });
|
|
175
|
+
}
|
|
176
|
+
if (m && this.knownVersion && this.knownVersion !== m.version) {
|
|
177
|
+
this.mem.clear();
|
|
178
|
+
await this.cache?.clear();
|
|
179
|
+
}
|
|
180
|
+
if (m) this.knownVersion = m.version;
|
|
181
|
+
return m;
|
|
182
|
+
})();
|
|
183
|
+
}
|
|
184
|
+
return this.metaPromise;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* preload pulls the requested scope into both L1 (per-prefix entries) and
|
|
188
|
+
* L2 (when provided) so subsequent reads need no network.
|
|
189
|
+
*/
|
|
190
|
+
async preload(opts) {
|
|
191
|
+
if (opts.scope === "all") {
|
|
192
|
+
const dict = await this.lookupAll();
|
|
193
|
+
const buckets = {};
|
|
194
|
+
for (const [zip, entry] of Object.entries(dict)) {
|
|
195
|
+
const p = zip.slice(0, 3);
|
|
196
|
+
(buckets[p] ??= {})[zip] = entry;
|
|
197
|
+
}
|
|
198
|
+
for (const [p, b] of Object.entries(buckets)) {
|
|
199
|
+
this.mem.set(this.prefixURL(p), b);
|
|
200
|
+
await this.writeL2(this.prefixURL(p), b);
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (PREFIX_REGEX.test(opts.scope)) {
|
|
205
|
+
await this.lookupGroup(opts.scope);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
throw new Error(`jpzip: invalid preload scope ${JSON.stringify(opts.scope)}`);
|
|
209
|
+
}
|
|
210
|
+
/** Clear all SDK-managed caches (L1 + L2). */
|
|
211
|
+
async refresh() {
|
|
212
|
+
this.mem.clear();
|
|
213
|
+
this.metaPromise = null;
|
|
214
|
+
this.knownVersion = null;
|
|
215
|
+
await this.cache?.clear();
|
|
216
|
+
}
|
|
217
|
+
/* ----------------------------- internals ------------------------------ */
|
|
218
|
+
async fetchPrefixDict(prefix) {
|
|
219
|
+
const url = this.prefixURL(prefix);
|
|
220
|
+
const cached = this.mem.get(url);
|
|
221
|
+
if (cached) return cached;
|
|
222
|
+
const fromL2 = await this.readL2(url);
|
|
223
|
+
if (fromL2) {
|
|
224
|
+
this.mem.set(url, fromL2);
|
|
225
|
+
return fromL2;
|
|
226
|
+
}
|
|
227
|
+
const dict = await fetchDict(url, this.fetchOpts());
|
|
228
|
+
if (dict) {
|
|
229
|
+
this.mem.set(url, dict);
|
|
230
|
+
await this.writeL2(url, dict);
|
|
231
|
+
}
|
|
232
|
+
return dict;
|
|
233
|
+
}
|
|
234
|
+
async fetchGroupDict(prefix1) {
|
|
235
|
+
const url = `${this.baseUrl}/g/${prefix1}.json`;
|
|
236
|
+
const cached = this.mem.get(url);
|
|
237
|
+
if (cached) return cached;
|
|
238
|
+
const dict = await fetchDict(url, this.fetchOpts());
|
|
239
|
+
if (dict) this.mem.set(url, dict);
|
|
240
|
+
return dict;
|
|
241
|
+
}
|
|
242
|
+
prefixURL(prefix3) {
|
|
243
|
+
return `${this.baseUrl}/p/${prefix3}.json`;
|
|
244
|
+
}
|
|
245
|
+
fetchOpts() {
|
|
246
|
+
return { fetch: this.fetchImpl };
|
|
247
|
+
}
|
|
248
|
+
async readL2(url) {
|
|
249
|
+
if (!this.cache) return null;
|
|
250
|
+
const bytes = await this.cache.get(url);
|
|
251
|
+
if (!bytes) return null;
|
|
252
|
+
try {
|
|
253
|
+
const text = new TextDecoder().decode(bytes);
|
|
254
|
+
return JSON.parse(text);
|
|
255
|
+
} catch {
|
|
256
|
+
await this.cache.delete(url);
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async writeL2(url, dict) {
|
|
261
|
+
if (!this.cache) return;
|
|
262
|
+
const bytes = new TextEncoder().encode(JSON.stringify(dict));
|
|
263
|
+
await this.cache.set(url, bytes);
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// src/index.ts
|
|
268
|
+
var _default = null;
|
|
269
|
+
function defaultClient() {
|
|
270
|
+
if (_default === null) _default = new JpzipClient();
|
|
271
|
+
return _default;
|
|
272
|
+
}
|
|
273
|
+
function _resetDefaultClient() {
|
|
274
|
+
_default = null;
|
|
275
|
+
}
|
|
276
|
+
function configure(options) {
|
|
277
|
+
_default = new JpzipClient(options);
|
|
278
|
+
}
|
|
279
|
+
var lookup = (zipcode) => defaultClient().lookup(zipcode);
|
|
280
|
+
var lookupGroup = (prefix) => defaultClient().lookupGroup(prefix);
|
|
281
|
+
var lookupAll = () => defaultClient().lookupAll();
|
|
282
|
+
var preload = (opts) => defaultClient().preload(opts);
|
|
283
|
+
var getMeta = () => defaultClient().getMeta();
|
|
284
|
+
function isValidZipcode(zip) {
|
|
285
|
+
return /^\d{7}$/.test(zip);
|
|
286
|
+
}
|
|
287
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
288
|
+
0 && (module.exports = {
|
|
289
|
+
DEFAULT_BASE_URL,
|
|
290
|
+
JpzipClient,
|
|
291
|
+
MemoryLRU,
|
|
292
|
+
_resetDefaultClient,
|
|
293
|
+
configure,
|
|
294
|
+
getMeta,
|
|
295
|
+
isValidZipcode,
|
|
296
|
+
lookup,
|
|
297
|
+
lookupAll,
|
|
298
|
+
lookupGroup,
|
|
299
|
+
preload
|
|
300
|
+
});
|
|
301
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +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":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types mirroring the jpzip protocol specification (spec_version 1.0).
|
|
3
|
+
* Mirror of https://github.com/jpzip/spec/blob/main/schema/v1/zipcode-entry.json
|
|
4
|
+
*/
|
|
5
|
+
interface Town {
|
|
6
|
+
town: string;
|
|
7
|
+
kana: string;
|
|
8
|
+
roma: string;
|
|
9
|
+
note?: string;
|
|
10
|
+
}
|
|
11
|
+
interface ZipcodeEntry {
|
|
12
|
+
prefecture: string;
|
|
13
|
+
prefecture_kana: string;
|
|
14
|
+
prefecture_roma: string;
|
|
15
|
+
prefecture_code: string;
|
|
16
|
+
city: string;
|
|
17
|
+
city_kana: string;
|
|
18
|
+
city_roma: string;
|
|
19
|
+
city_code: string;
|
|
20
|
+
towns: Town[];
|
|
21
|
+
}
|
|
22
|
+
interface Endpoints {
|
|
23
|
+
group: string;
|
|
24
|
+
prefix: string;
|
|
25
|
+
}
|
|
26
|
+
interface Meta {
|
|
27
|
+
version: string;
|
|
28
|
+
generated_at: string;
|
|
29
|
+
spec_version: string;
|
|
30
|
+
total_zipcodes: number;
|
|
31
|
+
prefix_count: number;
|
|
32
|
+
by_pref: Record<string, number>;
|
|
33
|
+
data_source: string;
|
|
34
|
+
endpoints: Endpoints;
|
|
35
|
+
}
|
|
36
|
+
/** Dictionary keyed by 7-digit zipcode string, as published by the CDN. */
|
|
37
|
+
type ZipcodeDict = Record<string, ZipcodeEntry>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Three-layer caching per spec §6.4:
|
|
41
|
+
*
|
|
42
|
+
* - L1 (MemoryLRU) — always on, bounded
|
|
43
|
+
* - L2 (PersistentCache) — opt-in via constructor
|
|
44
|
+
* - L3 (HTTP/fetch) — out of SDK scope
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/** Public interface user-supplied L2 caches must implement. */
|
|
48
|
+
interface PersistentCache {
|
|
49
|
+
get(key: string): Promise<Uint8Array | null>;
|
|
50
|
+
set(key: string, value: Uint8Array): Promise<void>;
|
|
51
|
+
delete(key: string): Promise<void>;
|
|
52
|
+
clear(): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
/** Bounded in-memory LRU keyed by URL path. Values are pre-parsed dicts. */
|
|
55
|
+
declare class MemoryLRU {
|
|
56
|
+
private readonly max;
|
|
57
|
+
private readonly map;
|
|
58
|
+
constructor(max?: number);
|
|
59
|
+
get(key: string): ZipcodeDict | undefined;
|
|
60
|
+
set(key: string, value: ZipcodeDict): void;
|
|
61
|
+
clear(): void;
|
|
62
|
+
get size(): number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
declare const DEFAULT_BASE_URL = "https://jpzip.nadai.dev";
|
|
66
|
+
interface JpzipClientOptions {
|
|
67
|
+
/** Override the CDN origin. Defaults to https://jpzip.nadai.dev */
|
|
68
|
+
baseUrl?: string;
|
|
69
|
+
/** L2 persistent cache. Default off. */
|
|
70
|
+
cache?: PersistentCache;
|
|
71
|
+
/** Override fetch (mainly tests). */
|
|
72
|
+
fetch?: typeof fetch;
|
|
73
|
+
/** L1 LRU capacity in prefix count. Default 100. */
|
|
74
|
+
memoryCacheSize?: number;
|
|
75
|
+
/** Surface a warning to the user when spec_version mismatches. */
|
|
76
|
+
onSpecMismatch?: (info: {
|
|
77
|
+
expected: string;
|
|
78
|
+
received: string;
|
|
79
|
+
}) => void;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* JpzipClient is the SDK entrypoint. The functional shortcuts (`lookup`,
|
|
83
|
+
* `lookupGroup`, …) below delegate to a default singleton instance backed
|
|
84
|
+
* by L1 only.
|
|
85
|
+
*/
|
|
86
|
+
declare class JpzipClient {
|
|
87
|
+
private readonly baseUrl;
|
|
88
|
+
private readonly cache;
|
|
89
|
+
private readonly fetchImpl;
|
|
90
|
+
private readonly mem;
|
|
91
|
+
private readonly onSpecMismatch;
|
|
92
|
+
private metaPromise;
|
|
93
|
+
private knownVersion;
|
|
94
|
+
constructor(opts?: JpzipClientOptions);
|
|
95
|
+
/** Returns the entry for `zipcode`, or null if not found. */
|
|
96
|
+
lookup(zipcode: string): Promise<ZipcodeEntry | null>;
|
|
97
|
+
/** Returns the dictionary for a 1- or 3-digit prefix (2-digit is fanned out). */
|
|
98
|
+
lookupGroup(prefix: string): Promise<ZipcodeDict>;
|
|
99
|
+
/**
|
|
100
|
+
* Returns the full dataset. The CDN does not expose a single /all.json
|
|
101
|
+
* because the combined file exceeds Cloudflare Pages' 25 MiB per-file
|
|
102
|
+
* limit; instead we fan out across /g/0..9.json in parallel and merge.
|
|
103
|
+
*/
|
|
104
|
+
lookupAll(): Promise<ZipcodeDict>;
|
|
105
|
+
/** Returns parsed meta.json, or null if the CDN has none yet. */
|
|
106
|
+
getMeta(): Promise<Meta | null>;
|
|
107
|
+
/**
|
|
108
|
+
* preload pulls the requested scope into both L1 (per-prefix entries) and
|
|
109
|
+
* L2 (when provided) so subsequent reads need no network.
|
|
110
|
+
*/
|
|
111
|
+
preload(opts: {
|
|
112
|
+
scope: 'all';
|
|
113
|
+
} | {
|
|
114
|
+
scope: string;
|
|
115
|
+
}): Promise<void>;
|
|
116
|
+
/** Clear all SDK-managed caches (L1 + L2). */
|
|
117
|
+
refresh(): Promise<void>;
|
|
118
|
+
private fetchPrefixDict;
|
|
119
|
+
private fetchGroupDict;
|
|
120
|
+
private prefixURL;
|
|
121
|
+
private fetchOpts;
|
|
122
|
+
private readL2;
|
|
123
|
+
private writeL2;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Reset the singleton (mainly for tests). */
|
|
127
|
+
declare function _resetDefaultClient(): void;
|
|
128
|
+
/** Configure the singleton's options. Subsequent calls re-create the client. */
|
|
129
|
+
declare function configure(options: ConstructorParameters<typeof JpzipClient>[0]): void;
|
|
130
|
+
declare const lookup: (zipcode: string) => Promise<ZipcodeEntry | null>;
|
|
131
|
+
declare const lookupGroup: (prefix: string) => Promise<ZipcodeDict>;
|
|
132
|
+
declare const lookupAll: () => Promise<ZipcodeDict>;
|
|
133
|
+
declare const preload: (opts: Parameters<JpzipClient["preload"]>[0]) => Promise<void>;
|
|
134
|
+
declare const getMeta: () => Promise<Meta | null>;
|
|
135
|
+
/** Helper: returns true iff `zip` is a syntactically valid 7-digit zipcode. */
|
|
136
|
+
declare function isValidZipcode(zip: string): boolean;
|
|
137
|
+
|
|
138
|
+
export { DEFAULT_BASE_URL, type Endpoints, JpzipClient, type JpzipClientOptions, MemoryLRU, type Meta, type PersistentCache, type Town, type ZipcodeDict, type ZipcodeEntry, _resetDefaultClient, configure, getMeta, isValidZipcode, lookup, lookupAll, lookupGroup, preload };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types mirroring the jpzip protocol specification (spec_version 1.0).
|
|
3
|
+
* Mirror of https://github.com/jpzip/spec/blob/main/schema/v1/zipcode-entry.json
|
|
4
|
+
*/
|
|
5
|
+
interface Town {
|
|
6
|
+
town: string;
|
|
7
|
+
kana: string;
|
|
8
|
+
roma: string;
|
|
9
|
+
note?: string;
|
|
10
|
+
}
|
|
11
|
+
interface ZipcodeEntry {
|
|
12
|
+
prefecture: string;
|
|
13
|
+
prefecture_kana: string;
|
|
14
|
+
prefecture_roma: string;
|
|
15
|
+
prefecture_code: string;
|
|
16
|
+
city: string;
|
|
17
|
+
city_kana: string;
|
|
18
|
+
city_roma: string;
|
|
19
|
+
city_code: string;
|
|
20
|
+
towns: Town[];
|
|
21
|
+
}
|
|
22
|
+
interface Endpoints {
|
|
23
|
+
group: string;
|
|
24
|
+
prefix: string;
|
|
25
|
+
}
|
|
26
|
+
interface Meta {
|
|
27
|
+
version: string;
|
|
28
|
+
generated_at: string;
|
|
29
|
+
spec_version: string;
|
|
30
|
+
total_zipcodes: number;
|
|
31
|
+
prefix_count: number;
|
|
32
|
+
by_pref: Record<string, number>;
|
|
33
|
+
data_source: string;
|
|
34
|
+
endpoints: Endpoints;
|
|
35
|
+
}
|
|
36
|
+
/** Dictionary keyed by 7-digit zipcode string, as published by the CDN. */
|
|
37
|
+
type ZipcodeDict = Record<string, ZipcodeEntry>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Three-layer caching per spec §6.4:
|
|
41
|
+
*
|
|
42
|
+
* - L1 (MemoryLRU) — always on, bounded
|
|
43
|
+
* - L2 (PersistentCache) — opt-in via constructor
|
|
44
|
+
* - L3 (HTTP/fetch) — out of SDK scope
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/** Public interface user-supplied L2 caches must implement. */
|
|
48
|
+
interface PersistentCache {
|
|
49
|
+
get(key: string): Promise<Uint8Array | null>;
|
|
50
|
+
set(key: string, value: Uint8Array): Promise<void>;
|
|
51
|
+
delete(key: string): Promise<void>;
|
|
52
|
+
clear(): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
/** Bounded in-memory LRU keyed by URL path. Values are pre-parsed dicts. */
|
|
55
|
+
declare class MemoryLRU {
|
|
56
|
+
private readonly max;
|
|
57
|
+
private readonly map;
|
|
58
|
+
constructor(max?: number);
|
|
59
|
+
get(key: string): ZipcodeDict | undefined;
|
|
60
|
+
set(key: string, value: ZipcodeDict): void;
|
|
61
|
+
clear(): void;
|
|
62
|
+
get size(): number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
declare const DEFAULT_BASE_URL = "https://jpzip.nadai.dev";
|
|
66
|
+
interface JpzipClientOptions {
|
|
67
|
+
/** Override the CDN origin. Defaults to https://jpzip.nadai.dev */
|
|
68
|
+
baseUrl?: string;
|
|
69
|
+
/** L2 persistent cache. Default off. */
|
|
70
|
+
cache?: PersistentCache;
|
|
71
|
+
/** Override fetch (mainly tests). */
|
|
72
|
+
fetch?: typeof fetch;
|
|
73
|
+
/** L1 LRU capacity in prefix count. Default 100. */
|
|
74
|
+
memoryCacheSize?: number;
|
|
75
|
+
/** Surface a warning to the user when spec_version mismatches. */
|
|
76
|
+
onSpecMismatch?: (info: {
|
|
77
|
+
expected: string;
|
|
78
|
+
received: string;
|
|
79
|
+
}) => void;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* JpzipClient is the SDK entrypoint. The functional shortcuts (`lookup`,
|
|
83
|
+
* `lookupGroup`, …) below delegate to a default singleton instance backed
|
|
84
|
+
* by L1 only.
|
|
85
|
+
*/
|
|
86
|
+
declare class JpzipClient {
|
|
87
|
+
private readonly baseUrl;
|
|
88
|
+
private readonly cache;
|
|
89
|
+
private readonly fetchImpl;
|
|
90
|
+
private readonly mem;
|
|
91
|
+
private readonly onSpecMismatch;
|
|
92
|
+
private metaPromise;
|
|
93
|
+
private knownVersion;
|
|
94
|
+
constructor(opts?: JpzipClientOptions);
|
|
95
|
+
/** Returns the entry for `zipcode`, or null if not found. */
|
|
96
|
+
lookup(zipcode: string): Promise<ZipcodeEntry | null>;
|
|
97
|
+
/** Returns the dictionary for a 1- or 3-digit prefix (2-digit is fanned out). */
|
|
98
|
+
lookupGroup(prefix: string): Promise<ZipcodeDict>;
|
|
99
|
+
/**
|
|
100
|
+
* Returns the full dataset. The CDN does not expose a single /all.json
|
|
101
|
+
* because the combined file exceeds Cloudflare Pages' 25 MiB per-file
|
|
102
|
+
* limit; instead we fan out across /g/0..9.json in parallel and merge.
|
|
103
|
+
*/
|
|
104
|
+
lookupAll(): Promise<ZipcodeDict>;
|
|
105
|
+
/** Returns parsed meta.json, or null if the CDN has none yet. */
|
|
106
|
+
getMeta(): Promise<Meta | null>;
|
|
107
|
+
/**
|
|
108
|
+
* preload pulls the requested scope into both L1 (per-prefix entries) and
|
|
109
|
+
* L2 (when provided) so subsequent reads need no network.
|
|
110
|
+
*/
|
|
111
|
+
preload(opts: {
|
|
112
|
+
scope: 'all';
|
|
113
|
+
} | {
|
|
114
|
+
scope: string;
|
|
115
|
+
}): Promise<void>;
|
|
116
|
+
/** Clear all SDK-managed caches (L1 + L2). */
|
|
117
|
+
refresh(): Promise<void>;
|
|
118
|
+
private fetchPrefixDict;
|
|
119
|
+
private fetchGroupDict;
|
|
120
|
+
private prefixURL;
|
|
121
|
+
private fetchOpts;
|
|
122
|
+
private readL2;
|
|
123
|
+
private writeL2;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Reset the singleton (mainly for tests). */
|
|
127
|
+
declare function _resetDefaultClient(): void;
|
|
128
|
+
/** Configure the singleton's options. Subsequent calls re-create the client. */
|
|
129
|
+
declare function configure(options: ConstructorParameters<typeof JpzipClient>[0]): void;
|
|
130
|
+
declare const lookup: (zipcode: string) => Promise<ZipcodeEntry | null>;
|
|
131
|
+
declare const lookupGroup: (prefix: string) => Promise<ZipcodeDict>;
|
|
132
|
+
declare const lookupAll: () => Promise<ZipcodeDict>;
|
|
133
|
+
declare const preload: (opts: Parameters<JpzipClient["preload"]>[0]) => Promise<void>;
|
|
134
|
+
declare const getMeta: () => Promise<Meta | null>;
|
|
135
|
+
/** Helper: returns true iff `zip` is a syntactically valid 7-digit zipcode. */
|
|
136
|
+
declare function isValidZipcode(zip: string): boolean;
|
|
137
|
+
|
|
138
|
+
export { DEFAULT_BASE_URL, type Endpoints, JpzipClient, type JpzipClientOptions, MemoryLRU, type Meta, type PersistentCache, type Town, type ZipcodeDict, type ZipcodeEntry, _resetDefaultClient, configure, getMeta, isValidZipcode, lookup, lookupAll, lookupGroup, preload };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// src/cache.ts
|
|
2
|
+
var MemoryLRU = class {
|
|
3
|
+
max;
|
|
4
|
+
map = /* @__PURE__ */ new Map();
|
|
5
|
+
constructor(max = 100) {
|
|
6
|
+
this.max = Math.max(1, max);
|
|
7
|
+
}
|
|
8
|
+
get(key) {
|
|
9
|
+
const value = this.map.get(key);
|
|
10
|
+
if (value === void 0) return void 0;
|
|
11
|
+
this.map.delete(key);
|
|
12
|
+
this.map.set(key, value);
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
set(key, value) {
|
|
16
|
+
if (this.map.has(key)) {
|
|
17
|
+
this.map.delete(key);
|
|
18
|
+
} else if (this.map.size >= this.max) {
|
|
19
|
+
const oldestKey = this.map.keys().next().value;
|
|
20
|
+
if (oldestKey !== void 0) this.map.delete(oldestKey);
|
|
21
|
+
}
|
|
22
|
+
this.map.set(key, value);
|
|
23
|
+
}
|
|
24
|
+
clear() {
|
|
25
|
+
this.map.clear();
|
|
26
|
+
}
|
|
27
|
+
get size() {
|
|
28
|
+
return this.map.size;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// src/fetch.ts
|
|
33
|
+
var MAX_RETRIES = 3;
|
|
34
|
+
var BASE_DELAY_MS = 200;
|
|
35
|
+
async function fetchJSON(url, opts = {}) {
|
|
36
|
+
const f = opts.fetch ?? fetch;
|
|
37
|
+
let lastErr;
|
|
38
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
39
|
+
if (attempt > 0) {
|
|
40
|
+
await sleep(BASE_DELAY_MS * 2 ** attempt);
|
|
41
|
+
}
|
|
42
|
+
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();
|
|
59
|
+
} catch (err) {
|
|
60
|
+
lastErr = err;
|
|
61
|
+
if (err instanceof DOMException && err.name === "AbortError") throw err;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
throw lastErr instanceof Error ? lastErr : new Error(`jpzip: fetch failed for ${url}: ${String(lastErr)}`);
|
|
65
|
+
}
|
|
66
|
+
var fetchDict = (url, opts) => fetchJSON(url, opts);
|
|
67
|
+
var fetchMeta = (url, opts) => fetchJSON(url, opts);
|
|
68
|
+
function sleep(ms) {
|
|
69
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/client.ts
|
|
73
|
+
var DEFAULT_BASE_URL = "https://jpzip.nadai.dev";
|
|
74
|
+
var SUPPORTED_SPEC = "1.0";
|
|
75
|
+
var ZIP_REGEX = /^\d{7}$/;
|
|
76
|
+
var PREFIX_REGEX = /^\d{1,3}$/;
|
|
77
|
+
var JpzipClient = class {
|
|
78
|
+
baseUrl;
|
|
79
|
+
cache;
|
|
80
|
+
fetchImpl;
|
|
81
|
+
mem;
|
|
82
|
+
onSpecMismatch;
|
|
83
|
+
metaPromise = null;
|
|
84
|
+
knownVersion = null;
|
|
85
|
+
constructor(opts = {}) {
|
|
86
|
+
this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
87
|
+
this.cache = opts.cache;
|
|
88
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
89
|
+
this.mem = new MemoryLRU(opts.memoryCacheSize ?? 100);
|
|
90
|
+
this.onSpecMismatch = opts.onSpecMismatch;
|
|
91
|
+
}
|
|
92
|
+
/** Returns the entry for `zipcode`, or null if not found. */
|
|
93
|
+
async lookup(zipcode) {
|
|
94
|
+
if (!ZIP_REGEX.test(zipcode)) return null;
|
|
95
|
+
const prefix = zipcode.slice(0, 3);
|
|
96
|
+
const dict = await this.fetchPrefixDict(prefix);
|
|
97
|
+
if (!dict) return null;
|
|
98
|
+
return dict[zipcode] ?? null;
|
|
99
|
+
}
|
|
100
|
+
/** Returns the dictionary for a 1- or 3-digit prefix (2-digit is fanned out). */
|
|
101
|
+
async lookupGroup(prefix) {
|
|
102
|
+
if (!PREFIX_REGEX.test(prefix)) {
|
|
103
|
+
throw new Error(`jpzip: invalid prefix ${JSON.stringify(prefix)} (must be 1-3 digits)`);
|
|
104
|
+
}
|
|
105
|
+
if (prefix.length === 3) {
|
|
106
|
+
return await this.fetchPrefixDict(prefix) ?? {};
|
|
107
|
+
}
|
|
108
|
+
if (prefix.length === 1) {
|
|
109
|
+
const dict = await this.fetchGroupDict(prefix);
|
|
110
|
+
return dict ?? {};
|
|
111
|
+
}
|
|
112
|
+
const tasks = [];
|
|
113
|
+
for (let i = 0; i < 10; i++) {
|
|
114
|
+
tasks.push(this.fetchPrefixDict(`${prefix}${i}`));
|
|
115
|
+
}
|
|
116
|
+
const dicts = await Promise.all(tasks);
|
|
117
|
+
return Object.assign({}, ...dicts.filter(Boolean));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Returns the full dataset. The CDN does not expose a single /all.json
|
|
121
|
+
* because the combined file exceeds Cloudflare Pages' 25 MiB per-file
|
|
122
|
+
* limit; instead we fan out across /g/0..9.json in parallel and merge.
|
|
123
|
+
*/
|
|
124
|
+
async lookupAll() {
|
|
125
|
+
const tasks = [];
|
|
126
|
+
for (let i = 0; i < 10; i++) {
|
|
127
|
+
tasks.push(this.fetchGroupDict(String(i)));
|
|
128
|
+
}
|
|
129
|
+
const dicts = await Promise.all(tasks);
|
|
130
|
+
return Object.assign({}, ...dicts.filter(Boolean));
|
|
131
|
+
}
|
|
132
|
+
/** Returns parsed meta.json, or null if the CDN has none yet. */
|
|
133
|
+
async getMeta() {
|
|
134
|
+
if (this.metaPromise === null) {
|
|
135
|
+
this.metaPromise = (async () => {
|
|
136
|
+
const m = await fetchMeta(`${this.baseUrl}/meta.json`, this.fetchOpts());
|
|
137
|
+
if (m && m.spec_version !== SUPPORTED_SPEC) {
|
|
138
|
+
this.onSpecMismatch?.({ expected: SUPPORTED_SPEC, received: m.spec_version });
|
|
139
|
+
}
|
|
140
|
+
if (m && this.knownVersion && this.knownVersion !== m.version) {
|
|
141
|
+
this.mem.clear();
|
|
142
|
+
await this.cache?.clear();
|
|
143
|
+
}
|
|
144
|
+
if (m) this.knownVersion = m.version;
|
|
145
|
+
return m;
|
|
146
|
+
})();
|
|
147
|
+
}
|
|
148
|
+
return this.metaPromise;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* preload pulls the requested scope into both L1 (per-prefix entries) and
|
|
152
|
+
* L2 (when provided) so subsequent reads need no network.
|
|
153
|
+
*/
|
|
154
|
+
async preload(opts) {
|
|
155
|
+
if (opts.scope === "all") {
|
|
156
|
+
const dict = await this.lookupAll();
|
|
157
|
+
const buckets = {};
|
|
158
|
+
for (const [zip, entry] of Object.entries(dict)) {
|
|
159
|
+
const p = zip.slice(0, 3);
|
|
160
|
+
(buckets[p] ??= {})[zip] = entry;
|
|
161
|
+
}
|
|
162
|
+
for (const [p, b] of Object.entries(buckets)) {
|
|
163
|
+
this.mem.set(this.prefixURL(p), b);
|
|
164
|
+
await this.writeL2(this.prefixURL(p), b);
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (PREFIX_REGEX.test(opts.scope)) {
|
|
169
|
+
await this.lookupGroup(opts.scope);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
throw new Error(`jpzip: invalid preload scope ${JSON.stringify(opts.scope)}`);
|
|
173
|
+
}
|
|
174
|
+
/** Clear all SDK-managed caches (L1 + L2). */
|
|
175
|
+
async refresh() {
|
|
176
|
+
this.mem.clear();
|
|
177
|
+
this.metaPromise = null;
|
|
178
|
+
this.knownVersion = null;
|
|
179
|
+
await this.cache?.clear();
|
|
180
|
+
}
|
|
181
|
+
/* ----------------------------- internals ------------------------------ */
|
|
182
|
+
async fetchPrefixDict(prefix) {
|
|
183
|
+
const url = this.prefixURL(prefix);
|
|
184
|
+
const cached = this.mem.get(url);
|
|
185
|
+
if (cached) return cached;
|
|
186
|
+
const fromL2 = await this.readL2(url);
|
|
187
|
+
if (fromL2) {
|
|
188
|
+
this.mem.set(url, fromL2);
|
|
189
|
+
return fromL2;
|
|
190
|
+
}
|
|
191
|
+
const dict = await fetchDict(url, this.fetchOpts());
|
|
192
|
+
if (dict) {
|
|
193
|
+
this.mem.set(url, dict);
|
|
194
|
+
await this.writeL2(url, dict);
|
|
195
|
+
}
|
|
196
|
+
return dict;
|
|
197
|
+
}
|
|
198
|
+
async fetchGroupDict(prefix1) {
|
|
199
|
+
const url = `${this.baseUrl}/g/${prefix1}.json`;
|
|
200
|
+
const cached = this.mem.get(url);
|
|
201
|
+
if (cached) return cached;
|
|
202
|
+
const dict = await fetchDict(url, this.fetchOpts());
|
|
203
|
+
if (dict) this.mem.set(url, dict);
|
|
204
|
+
return dict;
|
|
205
|
+
}
|
|
206
|
+
prefixURL(prefix3) {
|
|
207
|
+
return `${this.baseUrl}/p/${prefix3}.json`;
|
|
208
|
+
}
|
|
209
|
+
fetchOpts() {
|
|
210
|
+
return { fetch: this.fetchImpl };
|
|
211
|
+
}
|
|
212
|
+
async readL2(url) {
|
|
213
|
+
if (!this.cache) return null;
|
|
214
|
+
const bytes = await this.cache.get(url);
|
|
215
|
+
if (!bytes) return null;
|
|
216
|
+
try {
|
|
217
|
+
const text = new TextDecoder().decode(bytes);
|
|
218
|
+
return JSON.parse(text);
|
|
219
|
+
} catch {
|
|
220
|
+
await this.cache.delete(url);
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async writeL2(url, dict) {
|
|
225
|
+
if (!this.cache) return;
|
|
226
|
+
const bytes = new TextEncoder().encode(JSON.stringify(dict));
|
|
227
|
+
await this.cache.set(url, bytes);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// src/index.ts
|
|
232
|
+
var _default = null;
|
|
233
|
+
function defaultClient() {
|
|
234
|
+
if (_default === null) _default = new JpzipClient();
|
|
235
|
+
return _default;
|
|
236
|
+
}
|
|
237
|
+
function _resetDefaultClient() {
|
|
238
|
+
_default = null;
|
|
239
|
+
}
|
|
240
|
+
function configure(options) {
|
|
241
|
+
_default = new JpzipClient(options);
|
|
242
|
+
}
|
|
243
|
+
var lookup = (zipcode) => defaultClient().lookup(zipcode);
|
|
244
|
+
var lookupGroup = (prefix) => defaultClient().lookupGroup(prefix);
|
|
245
|
+
var lookupAll = () => defaultClient().lookupAll();
|
|
246
|
+
var preload = (opts) => defaultClient().preload(opts);
|
|
247
|
+
var getMeta = () => defaultClient().getMeta();
|
|
248
|
+
function isValidZipcode(zip) {
|
|
249
|
+
return /^\d{7}$/.test(zip);
|
|
250
|
+
}
|
|
251
|
+
export {
|
|
252
|
+
DEFAULT_BASE_URL,
|
|
253
|
+
JpzipClient,
|
|
254
|
+
MemoryLRU,
|
|
255
|
+
_resetDefaultClient,
|
|
256
|
+
configure,
|
|
257
|
+
getMeta,
|
|
258
|
+
isValidZipcode,
|
|
259
|
+
lookup,
|
|
260
|
+
lookupAll,
|
|
261
|
+
lookupGroup,
|
|
262
|
+
preload
|
|
263
|
+
};
|
|
264
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jpzip/jpzip",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Japan postal-code dataset SDK — fetches normalized JSON from jpzip.nadai.dev",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"lint": "tsc --noEmit"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"japan",
|
|
30
|
+
"postal-code",
|
|
31
|
+
"zipcode",
|
|
32
|
+
"郵便番号",
|
|
33
|
+
"address"
|
|
34
|
+
],
|
|
35
|
+
"author": "nadai",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"homepage": "https://jpzip.nadai.dev",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/jpzip/js.git"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/jpzip/js/issues"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^22.10.0",
|
|
50
|
+
"tsup": "^8.3.5",
|
|
51
|
+
"typescript": "^5.7.2",
|
|
52
|
+
"vitest": "^2.1.8"
|
|
53
|
+
}
|
|
54
|
+
}
|