@eusilvio/zip-lookup 2.6.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Silvio Campos
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,263 @@
1
+ # @eusilvio/zip-lookup
2
+
3
+ [![NPM Version](https://img.shields.io/npm/v/@eusilvio/zip-lookup.svg)](https://www.npmjs.com/package/@eusilvio/zip-lookup)
4
+ [![Build Status](https://img.shields.io/github/actions/workflow/status/eusilvio/cep-lookup/ci.yml)](https://github.com/eusilvio/cep-lookup/actions)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ US ZIP code lookup engine with multi-provider race, resilience controls, and metrics.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @eusilvio/zip-lookup
13
+ ```
14
+
15
+ ## Features
16
+
17
+ - Multi-provider race strategy.
18
+ - Cache and rate limiting.
19
+ - Retry with exponential backoff.
20
+ - Standardized errors with error codes.
21
+ - Circuit breaker per provider.
22
+ - Provider health score and runtime metrics.
23
+ - Event-based observability.
24
+
25
+ ## Basic Usage
26
+
27
+ ```ts
28
+ import { ZipLookup } from "@eusilvio/zip-lookup";
29
+ import { zippopotamProvider } from "@eusilvio/zip-lookup/providers";
30
+
31
+ const lookup = new ZipLookup({
32
+ providers: [zippopotamProvider],
33
+ });
34
+
35
+ const address = await lookup.lookup("90210");
36
+ console.log(address);
37
+ // {
38
+ // zip: "90210",
39
+ // city: "Beverly Hills",
40
+ // state: "California",
41
+ // stateAbbr: "CA",
42
+ // country: "United States",
43
+ // latitude: "34.0901",
44
+ // longitude: "-118.4065",
45
+ // service: "Zippopotam"
46
+ // }
47
+ ```
48
+
49
+ ## Providers
50
+
51
+ ### Free (no API key)
52
+
53
+ ```ts
54
+ import { zippopotamProvider } from "@eusilvio/zip-lookup/providers";
55
+ ```
56
+
57
+ ### ZipCodeStack (free tier, API key required)
58
+
59
+ Sign up at [zipcodestack.com](https://zipcodestack.com). Returns county and timezone in addition to city/state.
60
+
61
+ ```ts
62
+ import { createZipcodestackProvider } from "@eusilvio/zip-lookup/providers";
63
+
64
+ const provider = createZipcodestackProvider("YOUR_API_KEY");
65
+ ```
66
+
67
+ ### USPS Web Tools (free, API key required)
68
+
69
+ Register at [usps.com/business/web-tools-apis](https://www.usps.com/business/web-tools-apis/). Returns city and state only.
70
+
71
+ ```ts
72
+ import { createUspsProvider } from "@eusilvio/zip-lookup/providers";
73
+
74
+ const provider = createUspsProvider("YOUR_USPS_USERID");
75
+ ```
76
+
77
+ ## Multiple Providers (race strategy)
78
+
79
+ ```ts
80
+ import { ZipLookup } from "@eusilvio/zip-lookup";
81
+ import {
82
+ zippopotamProvider,
83
+ createZipcodestackProvider,
84
+ createUspsProvider,
85
+ } from "@eusilvio/zip-lookup/providers";
86
+
87
+ const lookup = new ZipLookup({
88
+ providers: [
89
+ zippopotamProvider,
90
+ createZipcodestackProvider("YOUR_API_KEY"),
91
+ createUspsProvider("YOUR_USPS_USERID"),
92
+ ],
93
+ staggerDelay: 100, // ms before backup providers are triggered
94
+ });
95
+ ```
96
+
97
+ ## ZIP Code Formats
98
+
99
+ All of the following are accepted and normalized to 5 digits internally:
100
+
101
+ ```ts
102
+ await lookup.lookup("10001"); // 5-digit
103
+ await lookup.lookup("10001-1234"); // ZIP+4 with hyphen
104
+ await lookup.lookup("100011234"); // ZIP+4 without hyphen
105
+ ```
106
+
107
+ ## Error Handling
108
+
109
+ ```ts
110
+ import {
111
+ ZipLookup,
112
+ ZipValidationError,
113
+ ZipNotFoundError,
114
+ ProviderTimeoutError,
115
+ RateLimitError,
116
+ AllProvidersFailedError,
117
+ } from "@eusilvio/zip-lookup";
118
+
119
+ try {
120
+ await lookup.lookup("99999");
121
+ } catch (error) {
122
+ if (error instanceof ZipValidationError) {
123
+ console.log(error.code); // INVALID_ZIP
124
+ } else if (error instanceof ZipNotFoundError) {
125
+ console.log(error.code); // NOT_FOUND
126
+ } else if (error instanceof ProviderTimeoutError) {
127
+ console.log(error.code); // TIMEOUT
128
+ } else if (error instanceof RateLimitError) {
129
+ console.log(error.code); // RATE_LIMITED
130
+ } else if (error instanceof AllProvidersFailedError) {
131
+ console.log(error.code); // ALL_PROVIDERS_FAILED
132
+ }
133
+ }
134
+ ```
135
+
136
+ ## Cache
137
+
138
+ ```ts
139
+ import { ZipLookup, InMemoryCache } from "@eusilvio/zip-lookup";
140
+
141
+ const lookup = new ZipLookup({
142
+ providers: [zippopotamProvider],
143
+ cache: new InMemoryCache({ ttl: 60_000, maxSize: 500 }),
144
+ });
145
+ ```
146
+
147
+ ## Rate Limiting
148
+
149
+ ```ts
150
+ const lookup = new ZipLookup({
151
+ providers: [zippopotamProvider],
152
+ rateLimit: { requests: 10, per: 1000 }, // 10 req/s
153
+ });
154
+ ```
155
+
156
+ ## Circuit Breaker
157
+
158
+ ```ts
159
+ const lookup = new ZipLookup({
160
+ providers: [zippopotamProvider],
161
+ circuitBreaker: {
162
+ enabled: true,
163
+ failureThreshold: 3,
164
+ cooldownMs: 30_000,
165
+ },
166
+ });
167
+ ```
168
+
169
+ ## Warmup
170
+
171
+ Pings all providers and sorts them by latency. Useful to call on input focus.
172
+
173
+ ```ts
174
+ await lookup.warmup();
175
+ ```
176
+
177
+ ## Health and Metrics
178
+
179
+ ```ts
180
+ const health = lookup.getProviderHealth();
181
+ // [{ provider: "Zippopotam", score: 0.96, isOpen: false, avgLatencyMs: 48.2, ... }]
182
+
183
+ const metrics = lookup.getProviderMetrics();
184
+ // [{ provider: "Zippopotam", requests: 5, successes: 5, failures: 0, ... }]
185
+ ```
186
+
187
+ ## Bulk Lookup
188
+
189
+ ```ts
190
+ const results = await lookup.lookupZips(["10001", "90210", "60601"], 3);
191
+ // [{ zip, data, provider }, { zip, data: null, error }, ...]
192
+ ```
193
+
194
+ ## Custom Mapper
195
+
196
+ ```ts
197
+ const city = await lookup.lookup("10001", (addr) => addr.city);
198
+ // "New York City"
199
+ ```
200
+
201
+ ## Events
202
+
203
+ ```ts
204
+ lookup.on("success", ({ provider, zip, duration, address }) => { ... });
205
+ lookup.on("failure", ({ provider, zip, duration, error }) => { ... });
206
+ lookup.on("cache:hit", ({ zip }) => { ... });
207
+
208
+ lookup.off("success", listener);
209
+ ```
210
+
211
+ ## API Summary
212
+
213
+ ### `new ZipLookup(options)`
214
+
215
+ | Option | Type | Default | Description |
216
+ |---|---|---|---|
217
+ | `providers` | `ZipProvider[]` | required | Provider list |
218
+ | `fetcher` | `Fetcher` | `fetch` | Custom HTTP function |
219
+ | `cache` | `ZipCache` | — | Cache implementation |
220
+ | `rateLimit` | `{ requests, per }` | — | Rate limit window |
221
+ | `staggerDelay` | `number` | `100` | ms before backup providers fire |
222
+ | `retries` | `number` | `0` | Retry count after all fail |
223
+ | `retryDelay` | `number` | `1000` | Base retry delay (exponential) |
224
+ | `circuitBreaker` | `CircuitBreakerOptions` | enabled | Resilience per provider |
225
+ | `logger` | `{ debug }` | — | Debug logger |
226
+
227
+ ### Methods
228
+
229
+ - `lookup(zip, mapper?): Promise<ZipAddress>`
230
+ - `lookupZips(zips, concurrency?, mapper?): Promise<BulkZipResult[]>`
231
+ - `warmup(): Promise<ZipProvider[]>`
232
+ - `getProviderHealth(): ProviderHealth[]`
233
+ - `getProviderMetrics(): ProviderMetrics[]`
234
+ - `on(event, listener)` / `off(event, listener)`
235
+
236
+ ## Custom Provider
237
+
238
+ ```ts
239
+ import type { ZipProvider } from "@eusilvio/zip-lookup";
240
+
241
+ const myProvider: ZipProvider = {
242
+ name: "MyProvider",
243
+ timeout: 3000,
244
+ buildUrl: (zip) => `https://my-api.example.com/zip/${zip}`,
245
+ transform: (response) => ({
246
+ zip: response.postal_code,
247
+ city: response.city,
248
+ state: response.state_name,
249
+ stateAbbr: response.state_code,
250
+ country: "United States",
251
+ service: "MyProvider",
252
+ }),
253
+ };
254
+ ```
255
+
256
+ ## Compatibility
257
+
258
+ - Node.js: `20.x`, `22.x`, `24.x`
259
+ - Works in browser environments that support `fetch`
260
+
261
+ ## License
262
+
263
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";var Z=Object.defineProperty;var z=Object.getOwnPropertyDescriptor;var U=Object.getOwnPropertyNames;var B=Object.prototype.hasOwnProperty;var q=(s,e)=>{for(var t in e)Z(s,t,{get:e[t],enumerable:!0})},_=(s,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of U(e))!B.call(s,i)&&i!==t&&Z(s,i,{get:()=>e[i],enumerable:!(r=z(e,i))||r.enumerable});return s};var V=s=>_(Z({},"__esModule",{value:!0}),s);var K={};q(K,{AllProvidersFailedError:()=>v,InMemoryCache:()=>C,ProviderTimeoutError:()=>p,ProviderUnavailableError:()=>P,RateLimitError:()=>E,ZipLookup:()=>F,ZipNotFoundError:()=>y,ZipValidationError:()=>g});module.exports=V(K);var C=class{constructor(e){this.cache=new Map;this.ttl=e?.ttl??1/0,this.maxSize=e?.maxSize??1/0}get(e){let t=this.cache.get(e);if(t){if(this.ttl!==1/0&&Date.now()-t.timestamp>this.ttl){this.cache.delete(e);return}return t.value}}set(e,t){if(this.cache.has(e)&&this.cache.delete(e),this.cache.size>=this.maxSize){let r=this.cache.keys().next().value;r!==void 0&&this.cache.delete(r)}this.cache.set(e,{value:t,timestamp:Date.now()})}delete(e){this.cache.delete(e)}has(e){if(!this.cache.has(e))return!1;let t=this.cache.get(e);return this.ttl!==1/0&&Date.now()-t.timestamp>this.ttl?(this.cache.delete(e),!1):!0}clear(){this.cache.clear()}};var g=class extends Error{constructor(t){super("Invalid ZIP code format. Use NNNNN or NNNNN-NNNN.");this.code="INVALID_ZIP";this.name="ZipValidationError",this.zip=t}},E=class extends Error{constructor(t,r){super(`Rate limit exceeded: ${t} requests per ${r}ms.`);this.code="RATE_LIMITED";this.name="RateLimitError",this.limit=t,this.window=r}},p=class extends Error{constructor(t,r){super(`Timeout from ${t}`);this.code="TIMEOUT";this.name="ProviderTimeoutError",this.provider=t,this.timeout=r}},y=class extends Error{constructor(t,r){super("ZIP code not found");this.code="NOT_FOUND";this.name="ZipNotFoundError",this.zip=t,this.provider=r}},v=class extends Error{constructor(t){super("All providers failed to resolve the ZIP code.");this.code="ALL_PROVIDERS_FAILED";this.name="AllProvidersFailedError",this.errors=t}},P=class extends Error{constructor(t){super(`Provider ${t} is temporarily unavailable (circuit open).`);this.code="PROVIDER_UNAVAILABLE";this.name="ProviderUnavailableError",this.provider=t}};function M(s,e,t){if(s instanceof Error){if(s instanceof g||s instanceof E||s instanceof p||s instanceof y||s instanceof P||s instanceof v)return s;let r=s.message?.toLowerCase?.()||"";return r.includes("zip not found")||r.includes("not found")||r.includes("status: 404")?new y(e,t):(s.name==="AbortError",s)}return new Error(String(s))}var x=class{constructor(){this.listeners={}}on(e,t){this.listeners[e]||(this.listeners[e]=[]),this.listeners[e].push(t)}off(e,t){let r=this.listeners[e];r&&(this.listeners[e]=r.filter(i=>i!==t))}emit(e,t){let r=this.listeners[e];r&&r.forEach(i=>i(t))}};function H(s){if(!/^(\d{5}-\d{4}|\d{9}|\d{5})$/.test(s))throw new g(s);return s.replace("-","").slice(0,5)}function $(s){let e={...s};return Object.keys(e).forEach(t=>{let r=e[t];typeof r=="string"&&(e[t]=r.trim())}),e}var F=class{constructor(e){this.requestTimestamps=[];this.providerState=new Map;this.providers=e.providers,this.sortedProviders=[...e.providers],this.emitter=new x,this.fetcher=e.fetcher||(async(t,r)=>{let i=await fetch(t,{signal:r});if(!i.ok)throw new Error(`HTTP error! status: ${i.status}`);return i.json()}),this.cache=e.cache,this.rateLimit=e.rateLimit,this.staggerDelay=e.staggerDelay??100,this.retries=e.retries??0,this.retryDelay=e.retryDelay??1e3,this.logger=e.logger,this.circuitBreakerEnabled=e.circuitBreaker?.enabled??!0,this.circuitFailureThreshold=e.circuitBreaker?.failureThreshold??3,this.circuitCooldownMs=e.circuitBreaker?.cooldownMs??3e4,this.providers.forEach(t=>{this.providerState.set(t.name,{consecutiveFailures:0,successCount:0,failureCount:0,avgLatencyMs:0,requests:0,timeoutErrors:0,notFoundErrors:0})})}log(e,t){this.logger?.debug(e,t)}on(e,t){this.emitter.on(e,t)}off(e,t){this.emitter.off(e,t)}async warmup(){let e="10001",t=new AbortController,r=this.providers.map(async o=>{let a=Date.now();try{let d=o.buildUrl(e);return await(o.fetcher||this.fetcher)(d,t.signal),{provider:o,duration:Date.now()-a,error:!1}}catch{return{provider:o,duration:1/0,error:!0}}}),c=(await Promise.all(r)).sort((o,a)=>o.duration-a.duration);return this.sortedProviders=c.map(o=>o.provider).filter(o=>!!o),t.abort(),this.sortedProviders}getOrCreateProviderState(e){let t=this.providerState.get(e);if(t)return t;let r={consecutiveFailures:0,successCount:0,failureCount:0,avgLatencyMs:0,requests:0,timeoutErrors:0,notFoundErrors:0};return this.providerState.set(e,r),r}recordProviderSuccess(e,t){let r=this.getOrCreateProviderState(e);r.requests+=1,r.successCount+=1,r.consecutiveFailures=0;let i=r.successCount+r.failureCount;r.avgLatencyMs=i===1?t:(r.avgLatencyMs*(i-1)+t)/i,r.openUntil=void 0}recordProviderFailure(e,t,r){let i=this.getOrCreateProviderState(e);i.requests+=1,i.failureCount+=1,i.consecutiveFailures+=1;let c=i.successCount+i.failureCount;i.avgLatencyMs=c===1?t:(i.avgLatencyMs*(c-1)+t)/c,r instanceof p&&(i.timeoutErrors+=1),r instanceof y&&(i.notFoundErrors+=1),this.circuitBreakerEnabled&&i.consecutiveFailures>=this.circuitFailureThreshold&&(i.openUntil=Date.now()+this.circuitCooldownMs)}isProviderOpen(e){if(!this.circuitBreakerEnabled)return!1;let t=this.getOrCreateProviderState(e);return t.openUntil?Date.now()>=t.openUntil?(t.openUntil=void 0,t.consecutiveFailures=0,!1):!0:!1}scoreProvider(e){let t=this.getOrCreateProviderState(e.name),r=t.successCount+t.failureCount,i=r===0?1:t.successCount/r,c=t.avgLatencyMs>0?Math.min(t.avgLatencyMs/1e3,1):0,o=this.isProviderOpen(e.name)?1:0;return i*.8+(1-c)*.2-o}getProviderHealth(){return this.providers.map(e=>{let t=this.getOrCreateProviderState(e.name);return{provider:e.name,score:Number(this.scoreProvider(e).toFixed(4)),isOpen:this.isProviderOpen(e.name),openUntil:t.openUntil,consecutiveFailures:t.consecutiveFailures,successCount:t.successCount,failureCount:t.failureCount,avgLatencyMs:Number(t.avgLatencyMs.toFixed(2))}}).sort((e,t)=>t.score-e.score)}getProviderMetrics(){return this.providers.map(e=>{let t=this.getOrCreateProviderState(e.name);return{provider:e.name,requests:t.requests,successes:t.successCount,failures:t.failureCount,timeoutErrors:t.timeoutErrors,notFoundErrors:t.notFoundErrors,avgLatencyMs:Number(t.avgLatencyMs.toFixed(2))}})}checkRateLimit(){if(!this.rateLimit)return;let e=Date.now(),t=e-this.rateLimit.per;if(this.requestTimestamps=this.requestTimestamps.filter(r=>r>t),this.requestTimestamps.length>=this.rateLimit.requests)throw new E(this.rateLimit.requests,this.rateLimit.per);this.requestTimestamps.push(e)}async lookup(e,t){this.checkRateLimit();let r=H(e);if(this.log("lookup:start",{zip:r}),this.cache){let o=this.cache.get(r);if(o)return this.log("cache:hit",{zip:r}),this.emitter.emit("cache:hit",{zip:r}),t?t(o):o}let i,c=1+this.retries;for(let o=0;o<c;o++){if(o>0){let a=this.retryDelay*Math.pow(2,o-1);this.log("retry:attempt",{attempt:o,zip:r,delay:a}),await new Promise(d=>setTimeout(d,a))}try{return await this._lookupFromProviders(r,t)}catch(a){if(a instanceof g||a instanceof E)throw a;i=a}}throw i}async _lookupFromProviders(e,t){let r=new AbortController,{signal:i}=r,c=this.sortedProviders.filter(n=>!this.isProviderOpen(n.name)),o=[...c].sort((n,l)=>this.scoreProvider(l)-this.scoreProvider(n)),a=o.length>0?o:[...this.sortedProviders].sort((n,l)=>this.scoreProvider(l)-this.scoreProvider(n));if(a.length===0)throw new v([new P("all")]);if(c.length===0&&this.circuitBreakerEnabled)throw new v(a.map(n=>new P(n.name)));let d=n=>{let l=Date.now(),A=n.buildUrl(e),D=n.fetcher||this.fetcher;this.log("provider:start",{provider:n.name,zip:e});let R=new Promise((b,m)=>{if(!n.timeout)return;let u=setTimeout(()=>{i.removeEventListener("abort",O);let I=Date.now()-l,L=new p(n.name,n.timeout);this.recordProviderFailure(n.name,I,L),this.log("provider:failure",{provider:n.name,zip:e,error:L.message}),this.emitter.emit("failure",{provider:n.name,zip:e,duration:I,error:L}),m(L)},n.timeout),O=()=>clearTimeout(u);i.addEventListener("abort",O,{once:!0})}),S=D(A,i).then(b=>n.transform(b)).then(b=>{let m=Date.now()-l,u=$(b);return this.recordProviderSuccess(n.name,m),this.log("provider:success",{provider:n.name,zip:e,duration:m}),this.emitter.emit("success",{provider:n.name,zip:e,duration:m,address:u}),this.cache&&this.cache.set(e,u),t?t(u):u}).catch(b=>{let m=Date.now()-l,u=M(b,e,n.name);throw u.name!=="AbortError"&&!(u instanceof p)&&(this.recordProviderFailure(n.name,m,u),this.log("provider:failure",{provider:n.name,zip:e,error:u.message}),this.emitter.emit("failure",{provider:n.name,zip:e,duration:m,error:u})),u});return Promise.race([S,R])},f=a[0],h=a.slice(1);if(h.length===0)try{return await d(f)}finally{r.abort()}let w=null,T=null,N=new Promise((n,l)=>{T=()=>{if(w&&clearTimeout(w),i.aborted)return;let A=h.map(d);Promise.any(A).then(n).catch(l)},w=setTimeout(T,this.staggerDelay)}),k=d(f).catch(n=>{throw T&&T(),n});try{return await Promise.any([k,N])}catch(n){let l=n.errors||[n];throw new v(l)}finally{w&&clearTimeout(w),r.abort()}}async lookupZips(e,t=5,r){if(!e||e.length===0)return[];let i=new Array(e.length),c=0,o=async()=>{for(;c<e.length;){let d=c++;if(d>=e.length)break;let f=e[d];try{let h=await this.lookup(f);if(h)i[d]={zip:f,data:r?r(h):h,provider:h.service};else throw new Error("No address found")}catch(h){i[d]={zip:f,data:null,error:h}}}},a=Array.from({length:Math.min(t,e.length)},()=>o());return await Promise.all(a),i.filter(Boolean)}};
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ var A=class{constructor(e){this.cache=new Map;this.ttl=e?.ttl??1/0,this.maxSize=e?.maxSize??1/0}get(e){let t=this.cache.get(e);if(t){if(this.ttl!==1/0&&Date.now()-t.timestamp>this.ttl){this.cache.delete(e);return}return t.value}}set(e,t){if(this.cache.has(e)&&this.cache.delete(e),this.cache.size>=this.maxSize){let r=this.cache.keys().next().value;r!==void 0&&this.cache.delete(r)}this.cache.set(e,{value:t,timestamp:Date.now()})}delete(e){this.cache.delete(e)}has(e){if(!this.cache.has(e))return!1;let t=this.cache.get(e);return this.ttl!==1/0&&Date.now()-t.timestamp>this.ttl?(this.cache.delete(e),!1):!0}clear(){this.cache.clear()}};var E=class extends Error{constructor(t){super("Invalid ZIP code format. Use NNNNN or NNNNN-NNNN.");this.code="INVALID_ZIP";this.name="ZipValidationError",this.zip=t}},y=class extends Error{constructor(t,r){super(`Rate limit exceeded: ${t} requests per ${r}ms.`);this.code="RATE_LIMITED";this.name="RateLimitError",this.limit=t,this.window=r}},v=class extends Error{constructor(t,r){super(`Timeout from ${t}`);this.code="TIMEOUT";this.name="ProviderTimeoutError",this.provider=t,this.timeout=r}},P=class extends Error{constructor(t,r){super("ZIP code not found");this.code="NOT_FOUND";this.name="ZipNotFoundError",this.zip=t,this.provider=r}},f=class extends Error{constructor(t){super("All providers failed to resolve the ZIP code.");this.code="ALL_PROVIDERS_FAILED";this.name="AllProvidersFailedError",this.errors=t}},b=class extends Error{constructor(t){super(`Provider ${t} is temporarily unavailable (circuit open).`);this.code="PROVIDER_UNAVAILABLE";this.name="ProviderUnavailableError",this.provider=t}};function O(n,e,t){if(n instanceof Error){if(n instanceof E||n instanceof y||n instanceof v||n instanceof P||n instanceof b||n instanceof f)return n;let r=n.message?.toLowerCase?.()||"";return r.includes("zip not found")||r.includes("not found")||r.includes("status: 404")?new P(e,t):(n.name==="AbortError",n)}return new Error(String(n))}var Z=class{constructor(){this.listeners={}}on(e,t){this.listeners[e]||(this.listeners[e]=[]),this.listeners[e].push(t)}off(e,t){let r=this.listeners[e];r&&(this.listeners[e]=r.filter(i=>i!==t))}emit(e,t){let r=this.listeners[e];r&&r.forEach(i=>i(t))}};function S(n){if(!/^(\d{5}-\d{4}|\d{9}|\d{5})$/.test(n))throw new E(n);return n.replace("-","").slice(0,5)}function z(n){let e={...n};return Object.keys(e).forEach(t=>{let r=e[t];typeof r=="string"&&(e[t]=r.trim())}),e}var I=class{constructor(e){this.requestTimestamps=[];this.providerState=new Map;this.providers=e.providers,this.sortedProviders=[...e.providers],this.emitter=new Z,this.fetcher=e.fetcher||(async(t,r)=>{let i=await fetch(t,{signal:r});if(!i.ok)throw new Error(`HTTP error! status: ${i.status}`);return i.json()}),this.cache=e.cache,this.rateLimit=e.rateLimit,this.staggerDelay=e.staggerDelay??100,this.retries=e.retries??0,this.retryDelay=e.retryDelay??1e3,this.logger=e.logger,this.circuitBreakerEnabled=e.circuitBreaker?.enabled??!0,this.circuitFailureThreshold=e.circuitBreaker?.failureThreshold??3,this.circuitCooldownMs=e.circuitBreaker?.cooldownMs??3e4,this.providers.forEach(t=>{this.providerState.set(t.name,{consecutiveFailures:0,successCount:0,failureCount:0,avgLatencyMs:0,requests:0,timeoutErrors:0,notFoundErrors:0})})}log(e,t){this.logger?.debug(e,t)}on(e,t){this.emitter.on(e,t)}off(e,t){this.emitter.off(e,t)}async warmup(){let e="10001",t=new AbortController,r=this.providers.map(async o=>{let a=Date.now();try{let d=o.buildUrl(e);return await(o.fetcher||this.fetcher)(d,t.signal),{provider:o,duration:Date.now()-a,error:!1}}catch{return{provider:o,duration:1/0,error:!0}}}),c=(await Promise.all(r)).sort((o,a)=>o.duration-a.duration);return this.sortedProviders=c.map(o=>o.provider).filter(o=>!!o),t.abort(),this.sortedProviders}getOrCreateProviderState(e){let t=this.providerState.get(e);if(t)return t;let r={consecutiveFailures:0,successCount:0,failureCount:0,avgLatencyMs:0,requests:0,timeoutErrors:0,notFoundErrors:0};return this.providerState.set(e,r),r}recordProviderSuccess(e,t){let r=this.getOrCreateProviderState(e);r.requests+=1,r.successCount+=1,r.consecutiveFailures=0;let i=r.successCount+r.failureCount;r.avgLatencyMs=i===1?t:(r.avgLatencyMs*(i-1)+t)/i,r.openUntil=void 0}recordProviderFailure(e,t,r){let i=this.getOrCreateProviderState(e);i.requests+=1,i.failureCount+=1,i.consecutiveFailures+=1;let c=i.successCount+i.failureCount;i.avgLatencyMs=c===1?t:(i.avgLatencyMs*(c-1)+t)/c,r instanceof v&&(i.timeoutErrors+=1),r instanceof P&&(i.notFoundErrors+=1),this.circuitBreakerEnabled&&i.consecutiveFailures>=this.circuitFailureThreshold&&(i.openUntil=Date.now()+this.circuitCooldownMs)}isProviderOpen(e){if(!this.circuitBreakerEnabled)return!1;let t=this.getOrCreateProviderState(e);return t.openUntil?Date.now()>=t.openUntil?(t.openUntil=void 0,t.consecutiveFailures=0,!1):!0:!1}scoreProvider(e){let t=this.getOrCreateProviderState(e.name),r=t.successCount+t.failureCount,i=r===0?1:t.successCount/r,c=t.avgLatencyMs>0?Math.min(t.avgLatencyMs/1e3,1):0,o=this.isProviderOpen(e.name)?1:0;return i*.8+(1-c)*.2-o}getProviderHealth(){return this.providers.map(e=>{let t=this.getOrCreateProviderState(e.name);return{provider:e.name,score:Number(this.scoreProvider(e).toFixed(4)),isOpen:this.isProviderOpen(e.name),openUntil:t.openUntil,consecutiveFailures:t.consecutiveFailures,successCount:t.successCount,failureCount:t.failureCount,avgLatencyMs:Number(t.avgLatencyMs.toFixed(2))}}).sort((e,t)=>t.score-e.score)}getProviderMetrics(){return this.providers.map(e=>{let t=this.getOrCreateProviderState(e.name);return{provider:e.name,requests:t.requests,successes:t.successCount,failures:t.failureCount,timeoutErrors:t.timeoutErrors,notFoundErrors:t.notFoundErrors,avgLatencyMs:Number(t.avgLatencyMs.toFixed(2))}})}checkRateLimit(){if(!this.rateLimit)return;let e=Date.now(),t=e-this.rateLimit.per;if(this.requestTimestamps=this.requestTimestamps.filter(r=>r>t),this.requestTimestamps.length>=this.rateLimit.requests)throw new y(this.rateLimit.requests,this.rateLimit.per);this.requestTimestamps.push(e)}async lookup(e,t){this.checkRateLimit();let r=S(e);if(this.log("lookup:start",{zip:r}),this.cache){let o=this.cache.get(r);if(o)return this.log("cache:hit",{zip:r}),this.emitter.emit("cache:hit",{zip:r}),t?t(o):o}let i,c=1+this.retries;for(let o=0;o<c;o++){if(o>0){let a=this.retryDelay*Math.pow(2,o-1);this.log("retry:attempt",{attempt:o,zip:r,delay:a}),await new Promise(d=>setTimeout(d,a))}try{return await this._lookupFromProviders(r,t)}catch(a){if(a instanceof E||a instanceof y)throw a;i=a}}throw i}async _lookupFromProviders(e,t){let r=new AbortController,{signal:i}=r,c=this.sortedProviders.filter(s=>!this.isProviderOpen(s.name)),o=[...c].sort((s,l)=>this.scoreProvider(l)-this.scoreProvider(s)),a=o.length>0?o:[...this.sortedProviders].sort((s,l)=>this.scoreProvider(l)-this.scoreProvider(s));if(a.length===0)throw new f([new b("all")]);if(c.length===0&&this.circuitBreakerEnabled)throw new f(a.map(s=>new b(s.name)));let d=s=>{let l=Date.now(),C=s.buildUrl(e),k=s.fetcher||this.fetcher;this.log("provider:start",{provider:s.name,zip:e});let D=new Promise((g,m)=>{if(!s.timeout)return;let u=setTimeout(()=>{i.removeEventListener("abort",x);let F=Date.now()-l,L=new v(s.name,s.timeout);this.recordProviderFailure(s.name,F,L),this.log("provider:failure",{provider:s.name,zip:e,error:L.message}),this.emitter.emit("failure",{provider:s.name,zip:e,duration:F,error:L}),m(L)},s.timeout),x=()=>clearTimeout(u);i.addEventListener("abort",x,{once:!0})}),R=k(C,i).then(g=>s.transform(g)).then(g=>{let m=Date.now()-l,u=z(g);return this.recordProviderSuccess(s.name,m),this.log("provider:success",{provider:s.name,zip:e,duration:m}),this.emitter.emit("success",{provider:s.name,zip:e,duration:m,address:u}),this.cache&&this.cache.set(e,u),t?t(u):u}).catch(g=>{let m=Date.now()-l,u=O(g,e,s.name);throw u.name!=="AbortError"&&!(u instanceof v)&&(this.recordProviderFailure(s.name,m,u),this.log("provider:failure",{provider:s.name,zip:e,error:u.message}),this.emitter.emit("failure",{provider:s.name,zip:e,duration:m,error:u})),u});return Promise.race([R,D])},p=a[0],h=a.slice(1);if(h.length===0)try{return await d(p)}finally{r.abort()}let w=null,T=null,M=new Promise((s,l)=>{T=()=>{if(w&&clearTimeout(w),i.aborted)return;let C=h.map(d);Promise.any(C).then(s).catch(l)},w=setTimeout(T,this.staggerDelay)}),N=d(p).catch(s=>{throw T&&T(),s});try{return await Promise.any([N,M])}catch(s){let l=s.errors||[s];throw new f(l)}finally{w&&clearTimeout(w),r.abort()}}async lookupZips(e,t=5,r){if(!e||e.length===0)return[];let i=new Array(e.length),c=0,o=async()=>{for(;c<e.length;){let d=c++;if(d>=e.length)break;let p=e[d];try{let h=await this.lookup(p);if(h)i[d]={zip:p,data:r?r(h):h,provider:h.service};else throw new Error("No address found")}catch(h){i[d]={zip:p,data:null,error:h}}}},a=Array.from({length:Math.min(t,e.length)},()=>o());return await Promise.all(a),i.filter(Boolean)}};export{f as AllProvidersFailedError,A as InMemoryCache,v as ProviderTimeoutError,b as ProviderUnavailableError,y as RateLimitError,I as ZipLookup,P as ZipNotFoundError,E as ZipValidationError};
@@ -0,0 +1 @@
1
+ "use strict";var s=Object.defineProperty;var c=Object.getOwnPropertyDescriptor;var p=Object.getOwnPropertyNames;var u=Object.prototype.hasOwnProperty;var l=(e,t)=>{for(var r in t)s(e,r,{get:t[r],enumerable:!0})},h=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of p(t))!u.call(e,i)&&i!==r&&s(e,i,{get:()=>t[i],enumerable:!(o=c(t,i))||o.enumerable});return e};var m=e=>h(s({},"__esModule",{value:!0}),e);var P={};l(P,{createUspsProvider:()=>y,createZipcodestackProvider:()=>A,zippopotamPlusProvider:()=>g,zippopotamProvider:()=>f});module.exports=m(P);var f={name:"Zippopotam",buildUrl:e=>`https://api.zippopotam.us/us/${e}`,transform:e=>{if(!e||!e.places||e.places.length===0)throw new Error("ZIP not found");let t=e.places[0];return{zip:e["post code"]||"",city:t["place name"]||"",state:t.state||"",stateAbbr:t["state abbreviation"]||"",country:e.country||"United States",latitude:t.latitude||void 0,longitude:t.longitude||void 0,service:"Zippopotam"}}},g={name:"ZippopotamPlus",buildUrl:e=>`https://api.zippopotam.us/us/${e}`,transform:e=>{if(!e||!e.places||e.places.length===0)throw new Error("ZIP not found");let t=e.places[0];return{zip:e["post code"]||"",city:t["place name"]||"",state:t.state||"",stateAbbr:t["state abbreviation"]||"",country:e.country||"United States",latitude:t.latitude||void 0,longitude:t.longitude||void 0,service:"ZippopotamPlus"}}};function A(e){return{name:"ZipCodeStack",buildUrl:t=>`https://api.zipcodestack.com/v1/search?codes=${t}&country=us&apikey=${e}`,transform:t=>{if(!t||!t.results)throw new Error("ZIP not found");let r=Object.keys(t.results);if(r.length===0)throw new Error("ZIP not found");let o=t.results[r[0]];if(!o||o.length===0)throw new Error("ZIP not found");let i=o[0];return{zip:i.postal_code||r[0],city:i.city||"",state:i.state||"",stateAbbr:i.state_code||"",county:i.county||void 0,country:"United States",latitude:i.latitude!=null?String(i.latitude):void 0,longitude:i.longitude!=null?String(i.longitude):void 0,timezone:i.timezone||void 0,service:"ZipCodeStack"}}}}var Z={AL:"Alabama",AK:"Alaska",AZ:"Arizona",AR:"Arkansas",CA:"California",CO:"Colorado",CT:"Connecticut",DE:"Delaware",FL:"Florida",GA:"Georgia",HI:"Hawaii",ID:"Idaho",IL:"Illinois",IN:"Indiana",IA:"Iowa",KS:"Kansas",KY:"Kentucky",LA:"Louisiana",ME:"Maine",MD:"Maryland",MA:"Massachusetts",MI:"Michigan",MN:"Minnesota",MS:"Mississippi",MO:"Missouri",MT:"Montana",NE:"Nebraska",NV:"Nevada",NH:"New Hampshire",NJ:"New Jersey",NM:"New Mexico",NY:"New York",NC:"North Carolina",ND:"North Dakota",OH:"Ohio",OK:"Oklahoma",OR:"Oregon",PA:"Pennsylvania",RI:"Rhode Island",SC:"South Carolina",SD:"South Dakota",TN:"Tennessee",TX:"Texas",UT:"Utah",VT:"Vermont",VA:"Virginia",WA:"Washington",WV:"West Virginia",WI:"Wisconsin",WY:"Wyoming",DC:"District of Columbia",PR:"Puerto Rico",VI:"Virgin Islands",GU:"Guam",AS:"American Samoa",MP:"Northern Mariana Islands"};function a(e,t){let r=e.match(new RegExp(`<${t}[^>]*>([^<]*)<\\/${t}>`));return r?r[1].trim():void 0}function y(e){return{name:"USPS",fetcher:async(r,o)=>{let i=await fetch(r,{signal:o});if(!i.ok)throw new Error(`HTTP error! status: ${i.status}`);return i.text()},buildUrl:r=>{let o=`<CityStateLookupRequest USERID="${e}"><ZipCode ID="0"><Zip5>${r}</Zip5></ZipCode></CityStateLookupRequest>`;return`https://secure.shippingapis.com/ShippingAPI.dll?API=CityStateLookup&XML=${encodeURIComponent(o)}`},transform:r=>{let o=a(r,"Description");if(r.includes("<Error>")&&o)throw new Error(o);let i=a(r,"City"),n=a(r,"State"),d=a(r,"Zip5");if(!i||!n||!d)throw new Error("ZIP not found");return{zip:d,city:i.charAt(0)+i.slice(1).toLowerCase(),state:Z[n]||n,stateAbbr:n,country:"United States",service:"USPS"}}}}
@@ -0,0 +1 @@
1
+ var c={name:"Zippopotam",buildUrl:t=>`https://api.zippopotam.us/us/${t}`,transform:t=>{if(!t||!t.places||t.places.length===0)throw new Error("ZIP not found");let e=t.places[0];return{zip:t["post code"]||"",city:e["place name"]||"",state:e.state||"",stateAbbr:e["state abbreviation"]||"",country:t.country||"United States",latitude:e.latitude||void 0,longitude:e.longitude||void 0,service:"Zippopotam"}}},p={name:"ZippopotamPlus",buildUrl:t=>`https://api.zippopotam.us/us/${t}`,transform:t=>{if(!t||!t.places||t.places.length===0)throw new Error("ZIP not found");let e=t.places[0];return{zip:t["post code"]||"",city:e["place name"]||"",state:e.state||"",stateAbbr:e["state abbreviation"]||"",country:t.country||"United States",latitude:e.latitude||void 0,longitude:e.longitude||void 0,service:"ZippopotamPlus"}}};function l(t){return{name:"ZipCodeStack",buildUrl:e=>`https://api.zipcodestack.com/v1/search?codes=${e}&country=us&apikey=${t}`,transform:e=>{if(!e||!e.results)throw new Error("ZIP not found");let r=Object.keys(e.results);if(r.length===0)throw new Error("ZIP not found");let o=e.results[r[0]];if(!o||o.length===0)throw new Error("ZIP not found");let i=o[0];return{zip:i.postal_code||r[0],city:i.city||"",state:i.state||"",stateAbbr:i.state_code||"",county:i.county||void 0,country:"United States",latitude:i.latitude!=null?String(i.latitude):void 0,longitude:i.longitude!=null?String(i.longitude):void 0,timezone:i.timezone||void 0,service:"ZipCodeStack"}}}}var d={AL:"Alabama",AK:"Alaska",AZ:"Arizona",AR:"Arkansas",CA:"California",CO:"Colorado",CT:"Connecticut",DE:"Delaware",FL:"Florida",GA:"Georgia",HI:"Hawaii",ID:"Idaho",IL:"Illinois",IN:"Indiana",IA:"Iowa",KS:"Kansas",KY:"Kentucky",LA:"Louisiana",ME:"Maine",MD:"Maryland",MA:"Massachusetts",MI:"Michigan",MN:"Minnesota",MS:"Mississippi",MO:"Missouri",MT:"Montana",NE:"Nebraska",NV:"Nevada",NH:"New Hampshire",NJ:"New Jersey",NM:"New Mexico",NY:"New York",NC:"North Carolina",ND:"North Dakota",OH:"Ohio",OK:"Oklahoma",OR:"Oregon",PA:"Pennsylvania",RI:"Rhode Island",SC:"South Carolina",SD:"South Dakota",TN:"Tennessee",TX:"Texas",UT:"Utah",VT:"Vermont",VA:"Virginia",WA:"Washington",WV:"West Virginia",WI:"Wisconsin",WY:"Wyoming",DC:"District of Columbia",PR:"Puerto Rico",VI:"Virgin Islands",GU:"Guam",AS:"American Samoa",MP:"Northern Mariana Islands"};function a(t,e){let r=t.match(new RegExp(`<${e}[^>]*>([^<]*)<\\/${e}>`));return r?r[1].trim():void 0}function m(t){return{name:"USPS",fetcher:async(r,o)=>{let i=await fetch(r,{signal:o});if(!i.ok)throw new Error(`HTTP error! status: ${i.status}`);return i.text()},buildUrl:r=>{let o=`<CityStateLookupRequest USERID="${t}"><ZipCode ID="0"><Zip5>${r}</Zip5></ZipCode></CityStateLookupRequest>`;return`https://secure.shippingapis.com/ShippingAPI.dll?API=CityStateLookup&XML=${encodeURIComponent(o)}`},transform:r=>{let o=a(r,"Description");if(r.includes("<Error>")&&o)throw new Error(o);let i=a(r,"City"),n=a(r,"State"),s=a(r,"Zip5");if(!i||!n||!s)throw new Error("ZIP not found");return{zip:s,city:i.charAt(0)+i.slice(1).toLowerCase(),state:d[n]||n,stateAbbr:n,country:"United States",service:"USPS"}}}}export{m as createUspsProvider,l as createZipcodestackProvider,p as zippopotamPlusProvider,c as zippopotamProvider};
@@ -0,0 +1,25 @@
1
+ import { ZipAddress } from '../types';
2
+ export interface ZipCache {
3
+ get(key: string): ZipAddress | undefined;
4
+ set(key: string, value: ZipAddress): void;
5
+ clear(): void;
6
+ delete?(key: string): void;
7
+ has?(key: string): boolean;
8
+ }
9
+ export interface InMemoryCacheOptions {
10
+ /** Time-to-live in milliseconds. Default: Infinity (no expiry) */
11
+ ttl?: number;
12
+ /** Maximum number of entries. Default: Infinity (no limit) */
13
+ maxSize?: number;
14
+ }
15
+ export declare class InMemoryCache implements ZipCache {
16
+ private cache;
17
+ private ttl;
18
+ private maxSize;
19
+ constructor(options?: InMemoryCacheOptions);
20
+ get(key: string): ZipAddress | undefined;
21
+ set(key: string, value: ZipAddress): void;
22
+ delete(key: string): void;
23
+ has(key: string): boolean;
24
+ clear(): void;
25
+ }
@@ -0,0 +1,35 @@
1
+ export type ZipErrorCode = "INVALID_ZIP" | "RATE_LIMITED" | "TIMEOUT" | "NOT_FOUND" | "PROVIDER_UNAVAILABLE" | "ALL_PROVIDERS_FAILED" | "UNKNOWN";
2
+ export declare class ZipValidationError extends Error {
3
+ readonly zip: string;
4
+ readonly code: ZipErrorCode;
5
+ constructor(zip: string);
6
+ }
7
+ export declare class RateLimitError extends Error {
8
+ readonly limit: number;
9
+ readonly window: number;
10
+ readonly code: ZipErrorCode;
11
+ constructor(limit: number, window: number);
12
+ }
13
+ export declare class ProviderTimeoutError extends Error {
14
+ readonly provider: string;
15
+ readonly timeout: number;
16
+ readonly code: ZipErrorCode;
17
+ constructor(provider: string, timeout: number);
18
+ }
19
+ export declare class ZipNotFoundError extends Error {
20
+ readonly zip: string;
21
+ readonly provider?: string;
22
+ readonly code: ZipErrorCode;
23
+ constructor(zip: string, provider?: string);
24
+ }
25
+ export declare class AllProvidersFailedError extends Error {
26
+ readonly errors: Error[];
27
+ readonly code: ZipErrorCode;
28
+ constructor(errors: Error[]);
29
+ }
30
+ export declare class ProviderUnavailableError extends Error {
31
+ readonly provider: string;
32
+ readonly code: ZipErrorCode;
33
+ constructor(provider: string);
34
+ }
35
+ export declare function normalizeProviderError(error: unknown, zip: string, provider: string): Error;
@@ -0,0 +1,43 @@
1
+ import { ZipAddress, Fetcher, ZipProvider, ZipLookupOptions, BulkZipResult, RateLimitOptions, EventName, EventListener, EventMap, ProviderHealth, ProviderMetrics, CircuitBreakerOptions } from "./types";
2
+ import { ZipCache, InMemoryCache, InMemoryCacheOptions } from "./cache";
3
+ import { ZipValidationError, RateLimitError, ProviderTimeoutError, ZipNotFoundError, AllProvidersFailedError, ProviderUnavailableError } from "./errors";
4
+ export type { ZipAddress, Fetcher, ZipProvider, ZipLookupOptions, BulkZipResult, RateLimitOptions, EventName, EventListener, EventMap, ZipCache, InMemoryCacheOptions, ProviderHealth, ProviderMetrics, CircuitBreakerOptions, };
5
+ export { InMemoryCache };
6
+ export { ZipValidationError, RateLimitError, ProviderTimeoutError, ZipNotFoundError, AllProvidersFailedError, ProviderUnavailableError, };
7
+ export declare class ZipLookup {
8
+ private providers;
9
+ private sortedProviders;
10
+ private fetcher;
11
+ private cache?;
12
+ private rateLimit?;
13
+ private staggerDelay;
14
+ private retries;
15
+ private retryDelay;
16
+ private logger?;
17
+ private requestTimestamps;
18
+ private emitter;
19
+ private circuitBreakerEnabled;
20
+ private circuitFailureThreshold;
21
+ private circuitCooldownMs;
22
+ private providerState;
23
+ constructor(options: ZipLookupOptions);
24
+ private log;
25
+ on<T extends EventName>(eventName: T, listener: EventListener<T>): void;
26
+ off<T extends EventName>(eventName: T, listener: EventListener<T>): void;
27
+ /**
28
+ * Pings providers to determine the fastest one and updates internal priority order.
29
+ * Useful to call on UI events like 'focus' on the ZIP input.
30
+ */
31
+ warmup(): Promise<ZipProvider[]>;
32
+ private getOrCreateProviderState;
33
+ private recordProviderSuccess;
34
+ private recordProviderFailure;
35
+ private isProviderOpen;
36
+ private scoreProvider;
37
+ getProviderHealth(): ProviderHealth[];
38
+ getProviderMetrics(): ProviderMetrics[];
39
+ private checkRateLimit;
40
+ lookup<T = ZipAddress>(zip: string, mapper?: (address: ZipAddress) => T): Promise<T>;
41
+ private _lookupFromProviders;
42
+ lookupZips<T = ZipAddress>(zips: string[], concurrency?: number, mapper?: (address: ZipAddress) => T): Promise<BulkZipResult<T>[]>;
43
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./zippopotam";
2
+ export * from "./zipcodestack";
3
+ export * from "./usps";
@@ -0,0 +1,9 @@
1
+ import { ZipProvider } from "../types";
2
+ /**
3
+ * Creates a USPS CityStateLookup provider.
4
+ * Requires a free USPS Web Tools API key: https://www.usps.com/business/web-tools-apis/
5
+ *
6
+ * Note: USPS only returns city and state — no lat/lng or county.
7
+ * The provider uses a text/xml fetcher internally so no custom global fetcher is needed.
8
+ */
9
+ export declare function createUspsProvider(apiKey: string): ZipProvider;
@@ -0,0 +1,7 @@
1
+ import { ZipProvider } from "../types";
2
+ /**
3
+ * Creates a ZipCodeStack provider.
4
+ * Requires a free API key from https://zipcodestack.com
5
+ * Returns city, state, county, timezone, latitude, longitude.
6
+ */
7
+ export declare function createZipcodestackProvider(apiKey: string): ZipProvider;
@@ -0,0 +1,12 @@
1
+ import { ZipProvider } from "../types";
2
+ /**
3
+ * Free provider with no API key required.
4
+ * Returns city, state, county, latitude and longitude.
5
+ * Docs: https://www.zippopotam.us/
6
+ */
7
+ export declare const zippopotamProvider: ZipProvider;
8
+ /**
9
+ * Same as zippopotamProvider but requests the extended endpoint with
10
+ * additional place data (multiple cities per ZIP when available).
11
+ */
12
+ export declare const zippopotamPlusProvider: ZipProvider;
@@ -0,0 +1,98 @@
1
+ import { ZipCache } from "./cache";
2
+ export interface ZipAddress {
3
+ zip: string;
4
+ city: string;
5
+ state: string;
6
+ stateAbbr: string;
7
+ county?: string;
8
+ country: string;
9
+ latitude?: string;
10
+ longitude?: string;
11
+ timezone?: string;
12
+ service: string;
13
+ }
14
+ export type Fetcher = (url: string, signal?: AbortSignal) => Promise<any>;
15
+ export interface ZipProvider {
16
+ name: string;
17
+ timeout?: number;
18
+ buildUrl: (zip: string) => string;
19
+ transform: (response: any) => ZipAddress;
20
+ /** Override the global fetcher for this provider (e.g. for XML-based APIs). */
21
+ fetcher?: Fetcher;
22
+ }
23
+ export interface RateLimitOptions {
24
+ requests: number;
25
+ per: number;
26
+ }
27
+ export interface ZipLookupOptions {
28
+ providers: ZipProvider[];
29
+ fetcher?: Fetcher;
30
+ cache?: ZipCache;
31
+ rateLimit?: RateLimitOptions;
32
+ staggerDelay?: number;
33
+ /** Number of retries after all providers fail. Default: 0 */
34
+ retries?: number;
35
+ /** Base delay in ms between retries (exponential backoff). Default: 1000 */
36
+ retryDelay?: number;
37
+ /** Optional logger for debug output */
38
+ logger?: {
39
+ debug: (msg: string, data?: Record<string, unknown>) => void;
40
+ };
41
+ /** Circuit breaker options for provider resilience */
42
+ circuitBreaker?: CircuitBreakerOptions;
43
+ }
44
+ export interface BulkZipResult<T = ZipAddress> {
45
+ zip: string;
46
+ data: T | null;
47
+ provider?: string;
48
+ error?: Error;
49
+ }
50
+ export type EventName = 'success' | 'failure' | 'cache:hit';
51
+ export interface SuccessPayload {
52
+ provider: string;
53
+ zip: string;
54
+ duration: number;
55
+ address: ZipAddress;
56
+ }
57
+ export interface FailurePayload {
58
+ provider: string;
59
+ zip: string;
60
+ duration: number;
61
+ error: Error;
62
+ }
63
+ export interface CacheHitPayload {
64
+ zip: string;
65
+ }
66
+ export interface EventMap {
67
+ success: SuccessPayload;
68
+ failure: FailurePayload;
69
+ 'cache:hit': CacheHitPayload;
70
+ }
71
+ export type EventListener<T extends EventName> = (payload: EventMap[T]) => void;
72
+ export interface CircuitBreakerOptions {
73
+ /** Consecutive failures required to open the circuit. Default: 3 */
74
+ failureThreshold?: number;
75
+ /** Cooldown in ms before trying a provider again. Default: 30000 */
76
+ cooldownMs?: number;
77
+ /** Enable/disable circuit breaker. Default: true */
78
+ enabled?: boolean;
79
+ }
80
+ export interface ProviderHealth {
81
+ provider: string;
82
+ score: number;
83
+ isOpen: boolean;
84
+ openUntil?: number;
85
+ consecutiveFailures: number;
86
+ successCount: number;
87
+ failureCount: number;
88
+ avgLatencyMs: number;
89
+ }
90
+ export interface ProviderMetrics {
91
+ provider: string;
92
+ requests: number;
93
+ successes: number;
94
+ failures: number;
95
+ timeoutErrors: number;
96
+ notFoundErrors: number;
97
+ avgLatencyMs: number;
98
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@eusilvio/zip-lookup",
3
+ "version": "2.6.0",
4
+ "description": "Agnostic, performant and flexible US ZIP code lookup library with race strategy and caching.",
5
+ "license": "MIT",
6
+ "author": "Silvio Souza",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/eusilvio/cep-lookup.git",
10
+ "directory": "packages/zip-lookup"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public",
14
+ "registry": "https://registry.npmjs.org/"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/src/index.d.ts",
19
+ "import": "./dist/index.mjs",
20
+ "require": "./dist/index.cjs"
21
+ },
22
+ "./providers": {
23
+ "types": "./dist/src/providers/index.d.ts",
24
+ "import": "./dist/providers/index.mjs",
25
+ "require": "./dist/providers/index.cjs"
26
+ }
27
+ },
28
+ "sideEffects": false,
29
+ "files": [
30
+ "dist",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "scripts": {
35
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
36
+ "build": "npm run clean && tsc --emitDeclarationOnly --outDir dist && esbuild src/index.ts src/providers/index.ts --bundle --platform=neutral --format=cjs --outdir=dist --out-extension:.js=.cjs --minify && esbuild src/index.ts src/providers/index.ts --bundle --platform=neutral --format=esm --outdir=dist --out-extension:.js=.mjs --minify",
37
+ "test": "jest"
38
+ },
39
+ "devDependencies": {
40
+ "@types/jest": "^30.0.0",
41
+ "esbuild": "^0.27.2",
42
+ "jest": "^30.1.3",
43
+ "ts-jest": "^29.4.4",
44
+ "ts-node": "^10.9.2",
45
+ "tsconfig-paths": "^4.2.0",
46
+ "typescript": "^5.9.2"
47
+ }
48
+ }