@eusilvio/cep-lookup 2.5.1 → 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/README.md CHANGED
@@ -1,28 +1,10 @@
1
1
  # @eusilvio/cep-lookup
2
2
 
3
3
  [![NPM Version](https://img.shields.io/npm/v/@eusilvio/cep-lookup.svg)](https://www.npmjs.com/package/@eusilvio/cep-lookup)
4
- [![NPM Unpacked Size](https://img.shields.io/npm/unpacked-size/@eusilvio/cep-lookup)](https://www.npmjs.com/package/@eusilvio/cep-lookup)
5
4
  [![Build Status](https://img.shields.io/github/actions/workflow/status/eusilvio/cep-lookup/ci.yml)](https://github.com/eusilvio/cep-lookup/actions)
6
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
6
 
8
- A modern, flexible, and agnostic CEP (Brazilian postal code) lookup library written in TypeScript.
9
-
10
- ## About
11
-
12
- `@eusilvio/cep-lookup` was created to solve address lookup from a CEP in a different way. Instead of relying on a single data source, it queries multiple services simultaneously and returns the response from the fastest one.
13
-
14
- Its agnostic design allows it to be used in any JavaScript environment with any HTTP client, and its powerful "mapper" system allows you to format the data output exactly as you need.
15
-
16
- ## Key Features
17
-
18
- - **Multiple Providers (Race Strategy)**: Queries multiple CEP APIs at the same time and uses the first valid response.
19
- - **Class-Based API**: Create a reusable instance with your settings.
20
- - **Bulk Lookups**: Efficiently look up multiple CEPs with a single method call.
21
- - **Customizable Return Format**: Provide a `mapper` function to transform the address data into any format your application needs.
22
- - **HTTP Client Agnostic**: You provide the fetch function, giving you full control over the requests. Defaults to global `fetch` if not provided.
23
- - **Modular and Extensible Architecture**: Adding a new CEP data source is trivial.
24
- - **Fully Typed**: Developed with TypeScript to ensure type safety and a great developer experience.
25
- - **Caching**: Built-in support for caching to avoid repeated requests for the same CEP.
7
+ Core CEP lookup engine with multi-provider race, resilience controls, and metrics.
26
8
 
27
9
  ## Installation
28
10
 
@@ -30,231 +12,142 @@ Its agnostic design allows it to be used in any JavaScript environment with any
30
12
  npm install @eusilvio/cep-lookup
31
13
  ```
32
14
 
33
- ## How to Use
15
+ ## Features
34
16
 
35
- `@eusilvio/cep-lookup` is designed to be straightforward. You create a reusable instance of the `CepLookup` class with your desired settings and use its methods to look up single or multiple CEPs. The library also includes a simple in-memory cache to avoid repeated requests, which you can use or replace with your own implementation.
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.
36
24
 
37
- ### Example 1: Basic Usage
25
+ ## Basic Usage
38
26
 
39
- ```typescript
40
- import { CepLookup, Address } from "@eusilvio/cep-lookup";
41
- import {
42
- viaCepProvider,
43
- brasilApiProvider,
44
- } from "@eusilvio/cep-lookup/providers";
27
+ ```ts
28
+ import { CepLookup } from "@eusilvio/cep-lookup";
29
+ import { viaCepProvider, brasilApiProvider } from "@eusilvio/cep-lookup/providers";
45
30
 
46
- // 1. Create an instance of CepLookup
47
- const cepLookup = new CepLookup({
31
+ const lookup = new CepLookup({
48
32
  providers: [viaCepProvider, brasilApiProvider],
49
33
  });
50
34
 
51
- // 2. Look up a CEP
52
- cepLookup.lookup("01001-000").then((address: Address) => {
53
- console.log("Address found:", address);
54
- // Output:
55
- // {
56
- // cep: '01001-000',
57
- // state: 'SP',
58
- // city: 'São Paulo',
59
- // neighborhood: 'Sé',
60
- // street: 'Praça da Sé',
61
- // service: 'ViaCEP'
62
- // }
63
- });
35
+ const address = await lookup.lookup("01001-000");
36
+ console.log(address);
64
37
  ```
65
38
 
66
- ### Example 2: Custom Return with `mapper`
39
+ ## Error Handling
67
40
 
68
- ```typescript
69
- import { CepLookup, Address } from "@eusilvio/cep-lookup";
70
- import { viaCepProvider } from "@eusilvio/cep-lookup/providers";
71
-
72
- const cepLookup = new CepLookup({
73
- providers: [viaCepProvider],
74
- });
75
-
76
- // 1. Define your "mapper" function
77
- interface CustomAddress {
78
- postalCode: string;
79
- fullAddress: string;
80
- source: string;
41
+ ```ts
42
+ import {
43
+ CepLookup,
44
+ CepNotFoundError,
45
+ ProviderTimeoutError,
46
+ RateLimitError,
47
+ AllProvidersFailedError,
48
+ } from "@eusilvio/cep-lookup";
49
+
50
+ try {
51
+ await lookup.lookup("01001000");
52
+ } catch (error) {
53
+ if (error instanceof CepNotFoundError) {
54
+ console.log(error.code); // NOT_FOUND
55
+ } else if (error instanceof ProviderTimeoutError) {
56
+ console.log(error.code); // TIMEOUT
57
+ } else if (error instanceof RateLimitError) {
58
+ console.log(error.code); // RATE_LIMITED
59
+ } else if (error instanceof AllProvidersFailedError) {
60
+ console.log(error.code); // ALL_PROVIDERS_FAILED
61
+ }
81
62
  }
82
-
83
- const myMapper = (address: Address): CustomAddress => {
84
- return {
85
- postalCode: address.cep,
86
- fullAddress: `${address.street}, ${address.neighborhood} - ${address.city}/${address.state}`,
87
- source: address.service,
88
- };
89
- };
90
-
91
- // 2. Look up a CEP with the mapper
92
- cepLookup.lookup("01001-000", myMapper).then((customAddress: CustomAddress) => {
93
- console.log("Address found (custom format):", customAddress);
94
- // Output:
95
- // {
96
- // postalCode: '01001-000',
97
- // fullAddress: 'Praça da Sé, Sé - São Paulo/SP',
98
- // source: 'ViaCEP'
99
- // }
100
- });
101
63
  ```
102
64
 
103
- ### Example 3: Bulk CEP Lookup
65
+ ## Circuit Breaker
104
66
 
105
- For scenarios where you need to query multiple CEPs at once, you can use the `lookupCeps` method. It processes the CEPs in parallel with a configurable concurrency limit to avoid overwhelming the providers.
106
-
107
- ```typescript
108
- import { CepLookup, BulkCepResult } from "@eusilvio/cep-lookup";
109
- import {
110
- viaCepProvider,
111
- brasilApiProvider,
112
- } from "@eusilvio/cep-lookup/providers";
113
-
114
- const cepsToLookup = ["01001-000", "99999-999", "04538-132"];
115
-
116
- // 1. Create an instance with your settings
117
- const cepLookup = new CepLookup({
67
+ ```ts
68
+ const lookup = new CepLookup({
118
69
  providers: [viaCepProvider, brasilApiProvider],
119
- });
120
-
121
- // 2. Look up multiple CEPs
122
- cepLookup.lookupCeps(cepsToLookup, 2).then((results: BulkCepResult[]) => {
123
- console.log("Bulk lookup results:", results);
124
- // Output:
125
- // [
126
- // {
127
- // cep: '01001-000',
128
- // data: { cep: '01001-000', state: 'SP', city: 'São Paulo', ... },
129
- // provider: 'ViaCEP'
130
- // },
131
- // {
132
- // cep: '99999-999',
133
- // data: null,
134
- // error: [Error: All providers failed to find the CEP]
135
- // },
136
- // {
137
- // cep: '04538-132',
138
- // data: { cep: '04538-132', state: 'SP', city: 'São Paulo', ... },
139
- // provider: 'BrasilAPI'
140
- // }
141
- // ]
70
+ circuitBreaker: {
71
+ enabled: true,
72
+ failureThreshold: 3,
73
+ cooldownMs: 30_000,
74
+ },
142
75
  });
143
76
  ```
144
77
 
145
- ## API
146
-
147
- ### `new CepLookup(options)`
148
-
149
- Creates a new `CepLookup` instance.
150
-
151
- - `options`: A configuration object.
152
- - `providers` (Provider[], **required**): An array of providers that will be queried.
153
- - `fetcher` (Fetcher, _optional_): Your asynchronous function that fetches data from a URL. Defaults to global `fetch` if not provided.
154
- - `cache` (Cache, _optional_): An instance of a cache that implements the `Cache` interface. Use `InMemoryCache` for a simple in-memory cache.
155
- - `rateLimit` ({ requests: number, per: number }, _optional_): Configures an in-memory rate limiter (e.g., `{ requests: 10, per: 1000 }` for 10 requests per second).
156
- - `staggerDelay` (number, _optional_): Time in milliseconds to wait for the fastest provider before triggering backups (default: `100`).
157
-
158
- ### `cepLookup.warmup()`
159
-
160
- Pings all providers to determine the fastest one for the current environment. Call this during idle UI time (like when a user focuses a CEP input field) to optimize the subsequent `lookup` call.
161
-
162
- ### `cepLookup.lookup<T = Address>(cep, mapper?): Promise<T>`
163
-
164
- Returns a `Promise` that resolves to the address in the default format (`Address`) or in the custom format `T` if a `mapper` is provided.
165
-
166
- - `cep` (string, **required**): The CEP to be queried.
167
- - `mapper` ((address: Address) => T, _optional_): A function that receives the default `Address` object and transforms it into a new format `T`.
168
-
169
- ### `cepLookup.lookupCeps(ceps, concurrency?): Promise<BulkCepResult[]>`
170
-
171
- Looks up multiple CEPs in bulk. Returns a `Promise` that resolves to an array of `BulkCepResult` objects, one for each queried CEP.
172
-
173
- - `ceps` (string[], **required**): An array of CEP strings to be queried.
174
- - `concurrency` (number, _optional_): The number of parallel requests to make. Defaults to `5`.
175
-
176
- > **Note on Deprecated Functions:**
177
- > Standalone `lookupCep` and `lookupCeps` functions are deprecated and will be removed in a future version. Please use the methods on a `CepLookup` instance instead.
178
-
179
- ### Observability Events API
180
-
181
- Version 2.0.0 introduced an event-based API to monitor the library's behavior. You can listen to events to gather metrics on provider performance, latency, and errors.
182
-
183
- ```typescript
184
- const cepLookup = new CepLookup({ providers: [...] });
185
-
186
- // Fired for each successful provider response
187
- cepLookup.on('success', ({ provider, cep, duration }) => {
188
- console.log(`[${provider}] Success for CEP ${cep} in ${duration}ms`);
189
- // myMetrics.timing('cep.latency', duration, { provider });
190
- });
191
-
192
- // Fired for each failed provider response
193
- cepLookup.on('failure', ({ provider, cep, error }) => {
194
- console.error(`[${provider}] Failure for CEP ${cep}: ${error.message}`);
195
- // myMetrics.increment('cep.failure', { provider });
196
- });
197
-
198
- // Fired when a CEP is resolved from the cache
199
- cepLookup.on('cache:hit', ({ cep }) => {
200
- console.log(`[Cache] CEP ${cep} found in cache.`);
201
- // myMetrics.increment('cep.cache_hit');
202
- });
203
-
204
- // The lookup call remains the same
205
- cepLookup.lookup("01001-000");
78
+ ## Health and SLA Metrics
79
+
80
+ ```ts
81
+ const health = lookup.getProviderHealth();
82
+ /*
83
+ [
84
+ {
85
+ provider: 'ViaCEP',
86
+ score: 0.94,
87
+ isOpen: false,
88
+ successCount: 12,
89
+ failureCount: 1,
90
+ avgLatencyMs: 52.11,
91
+ ...
92
+ }
93
+ ]
94
+ */
95
+
96
+ const metrics = lookup.getProviderMetrics();
97
+ /*
98
+ [
99
+ {
100
+ provider: 'ViaCEP',
101
+ requests: 13,
102
+ successes: 12,
103
+ failures: 1,
104
+ timeoutErrors: 0,
105
+ notFoundErrors: 1,
106
+ avgLatencyMs: 52.11
107
+ }
108
+ ]
109
+ */
206
110
  ```
207
111
 
208
- ## Examples
112
+ ## Bulk Lookup
209
113
 
210
- You can find more detailed examples in the `examples/` directory:
114
+ ```ts
115
+ const results = await lookup.lookupCeps(["01001-000", "99999-999"], 2);
116
+ ```
211
117
 
212
- - **Basic Usage**: `examples/example.ts`
213
- - **Bulk Lookup**: `examples/bulk-example.ts`
214
- - **Custom Provider**: `examples/custom-provider-example.ts`
215
- - **Node.js Usage**: `examples/node-example.ts`
216
- - **React Component**: `examples/react-example.tsx`
217
- - **React Hook**: `examples/react-hook-example.ts`
218
- - **Angular Component/Service**: `examples/angular-example.ts`
219
- - **Cache Usage**: `examples/cache-example.ts`
118
+ ## API Summary
220
119
 
221
- ## Creating a Custom Provider
120
+ ### `new CepLookup(options)`
222
121
 
223
- Your custom provider must always transform the API response to the library's default `Address` interface. The user's `mapper` will handle the final customization.
122
+ - `providers`: required provider list.
123
+ - `fetcher`: optional custom HTTP fetch function.
124
+ - `cache`: optional cache implementation.
125
+ - `rateLimit`: `{ requests, per }`.
126
+ - `staggerDelay`: delay before backup providers.
127
+ - `retries`: retry count after failure.
128
+ - `retryDelay`: base retry delay in ms.
129
+ - `circuitBreaker`: `{ enabled, failureThreshold, cooldownMs }`.
224
130
 
225
- ```typescript
226
- import { Provider, Address } from "@eusilvio/cep-lookup";
131
+ ### Methods
227
132
 
228
- const myCustomProvider: Provider = {
229
- name: "MyCustomAPI",
230
- buildUrl: (cep: string) => `https://myapi.com/cep/${cep}`,
231
- transform: (response: any): Address => {
232
- // Transforms the response from "MyCustomAPI" to the "Address" format
233
- return {
234
- cep: response.postal_code,
235
- state: response.data.state_short,
236
- city: response.data.city_name,
237
- neighborhood: response.data.neighborhood,
238
- street: response.data.street_name,
239
- service: "MyCustomAPI",
240
- };
241
- },
242
- };
243
- ```
244
-
245
- ## Running Tests
246
-
247
- ```bash
248
- npm test
249
- ```
133
+ - `lookup(cep, mapper?)`
134
+ - `lookupCeps(ceps, concurrency?)`
135
+ - `warmup()`
136
+ - `getProviderHealth()`
137
+ - `getProviderMetrics()`
138
+ - `on(event, listener)` / `off(event, listener)`
250
139
 
251
140
  ## Compatibility and support
252
141
 
253
142
  - Node.js: `20.x`, `22.x`, `24.x`
254
- - React package: `react >= 16.8`
255
- - Vue package: `vue ^3`
256
143
  - Maintenance policy: [SUPPORT.md](../../SUPPORT.md)
257
144
 
145
+ ## Production docs
146
+
147
+ - [Best Practices](../../docs/BEST_PRACTICES.md)
148
+ - [Migration Guide](../../docs/MIGRATION.md)
149
+ - [Cookbook](../../docs/COOKBOOK.md)
150
+
258
151
  ## License
259
152
 
260
- Distributed under the MIT License.
153
+ MIT
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var A=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var S=Object.getOwnPropertyNames;var I=Object.prototype.hasOwnProperty;var z=(s,e)=>{for(var t in e)A(s,t,{get:e[t],enumerable:!0})},B=(s,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of S(e))!I.call(s,i)&&i!==t&&A(s,i,{get:()=>e[i],enumerable:!(r=O(e,i))||r.enumerable});return s};var q=s=>B(A({},"__esModule",{value:!0}),s);var U={};z(U,{AllProvidersFailedError:()=>w,CepLookup:()=>L,CepNotFoundError:()=>T,CepValidationError:()=>v,InMemoryCache:()=>E,ProviderTimeoutError:()=>y,RateLimitError:()=>g});module.exports=q(U);var E=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 v=class extends Error{constructor(e){super("Invalid CEP format. Use either NNNNNNNN or NNNNN-NNN."),this.name="CepValidationError",this.cep=e}},g=class extends Error{constructor(e,t){super(`Rate limit exceeded: ${e} requests per ${t}ms.`),this.name="RateLimitError",this.limit=e,this.window=t}},y=class extends Error{constructor(e,t){super(`Timeout from ${e}`),this.name="ProviderTimeoutError",this.provider=e,this.timeout=t}},T=class extends Error{constructor(e,t){super("CEP not found"),this.name="CepNotFoundError",this.cep=e,this.provider=t}},w=class extends Error{constructor(e){super("All providers failed to resolve the CEP."),this.name="AllProvidersFailedError",this.errors=e}};var N={AC:"68",AL:"82",AM:"92",AP:"96",BA:"71",CE:"85",DF:"61",ES:"27",GO:"62",MA:"98",MG:"31",MS:"67",MT:"65",PA:"91",PB:"83",PE:"81",PI:"86",PR:"41",RJ:"21",RN:"84",RO:"69",RR:"95",RS:"51",SC:"48",SE:"79",SP:"11",TO:"63"};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 F(s){if(!/^(\d{8}|\d{5}-\d{3})$/.test(s))throw new v(s);return s.replace("-","")}function $(s){let e={...s};return Object.keys(e).forEach(t=>{let r=e[t];typeof r=="string"&&(e[t]=r.trim())}),e}function K(s){if(!s.ddd&&s.state){let e=N[s.state];if(e)return{...s,ddd:e}}return s}var L=class{constructor(e){this.requestTimestamps=[];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}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="01001000",t=new AbortController,r=this.providers.map(async n=>{let a=Date.now();try{let c=n.buildUrl(e);return await this.fetcher(c,t.signal),{provider:n,duration:Date.now()-a,error:!1}}catch{return{provider:n,duration:1/0,error:!0}}}),d=(await Promise.all(r)).sort((n,a)=>n.duration-a.duration);return this.sortedProviders=d.map(n=>n.provider).filter(n=>!!n),t.abort(),this.sortedProviders}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 g(this.rateLimit.requests,this.rateLimit.per);this.requestTimestamps.push(e)}async lookup(e,t){this.checkRateLimit();let r=F(e);if(this.log("lookup:start",{cep:r}),this.cache){let n=this.cache.get(r);if(n)return this.log("cache:hit",{cep:r}),this.emitter.emit("cache:hit",{cep:r}),t?t(n):n}let i,d=1+this.retries;for(let n=0;n<d;n++){if(n>0){let a=this.retryDelay*Math.pow(2,n-1);this.log("retry:attempt",{attempt:n,cep:r,delay:a}),await new Promise(c=>setTimeout(c,a))}try{return await this._lookupFromProviders(r,t)}catch(a){if(a instanceof v||a instanceof g)throw a;i=a}}throw i}async _lookupFromProviders(e,t){let r=new AbortController,{signal:i}=r,d=o=>{let m=Date.now(),b=o.buildUrl(e);this.log("provider:start",{provider:o.name,cep:e});let C=new Promise((l,p)=>{if(!o.timeout)return;let f=setTimeout(()=>{i.removeEventListener("abort",R);let M=Date.now()-m,P=new y(o.name,o.timeout);this.log("provider:failure",{provider:o.name,cep:e,error:P.message}),this.emitter.emit("failure",{provider:o.name,cep:e,duration:M,error:P}),p(P)},o.timeout),R=()=>clearTimeout(f);i.addEventListener("abort",R,{once:!0})}),D=this.fetcher(b,i).then(l=>o.transform(l)).then(l=>{let p=Date.now()-m,f=K($(l));return this.log("provider:success",{provider:o.name,cep:e,duration:p}),this.emitter.emit("success",{provider:o.name,cep:e,duration:p,address:f}),this.cache&&this.cache.set(e,f),t?t(f):f}).catch(l=>{let p=Date.now()-m;throw!l.message.includes("Timeout from")&&l.name!=="AbortError"&&(this.log("provider:failure",{provider:o.name,cep:e,error:l.message}),this.emitter.emit("failure",{provider:o.name,cep:e,duration:p,error:l})),l});return Promise.race([D,C])},n=this.sortedProviders[0],a=this.sortedProviders.slice(1);if(a.length===0)try{return await d(n)}finally{r.abort()}let c=null,h=null,u=new Promise((o,m)=>{h=()=>{if(c&&clearTimeout(c),i.aborted)return;let b=a.map(d);Promise.any(b).then(o).catch(m)},c=setTimeout(h,this.staggerDelay)}),k=d(n).catch(o=>{throw h&&h(),o});try{return await Promise.any([k,u])}catch(o){let m=o.errors||[o];throw new w(m)}finally{c&&clearTimeout(c),r.abort()}}async lookupCeps(e,t=5,r){if(!e||e.length===0)return[];let i=new Array(e.length),d=0,n=async()=>{for(;d<e.length;){let c=d++;if(c>=e.length)break;let h=e[c];try{let u=await this.lookup(h);if(u)i[c]={cep:h,data:r?r(u):u,provider:u.service};else throw new Error("No address found")}catch(u){i[c]={cep:h,data:null,error:u}}}},a=Array.from({length:Math.min(t,e.length)},()=>n());return await Promise.all(a),i.filter(Boolean)}};
1
+ "use strict";var x=Object.defineProperty;var U=Object.getOwnPropertyDescriptor;var q=Object.getOwnPropertyNames;var _=Object.prototype.hasOwnProperty;var z=(i,e)=>{for(var t in e)x(i,t,{get:e[t],enumerable:!0})},V=(i,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of q(e))!_.call(i,s)&&s!==t&&x(i,s,{get:()=>e[s],enumerable:!(r=U(e,s))||r.enumerable});return i};var H=i=>V(x({},"__esModule",{value:!0}),i);var G={};z(G,{AllProvidersFailedError:()=>p,CepLookup:()=>O,CepNotFoundError:()=>E,CepValidationError:()=>f,InMemoryCache:()=>A,ProviderTimeoutError:()=>v,ProviderUnavailableError:()=>P,RateLimitError:()=>g});module.exports=H(G);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 f=class extends Error{constructor(t){super("Invalid CEP format. Use either NNNNNNNN or NNNNN-NNN.");this.code="INVALID_CEP";this.name="CepValidationError",this.cep=t}},g=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}},E=class extends Error{constructor(t,r){super("CEP not found");this.code="NOT_FOUND";this.name="CepNotFoundError",this.cep=t,this.provider=r}},p=class extends Error{constructor(t){super("All providers failed to resolve the CEP.");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 F(i,e,t){if(i instanceof Error){if(i instanceof f||i instanceof g||i instanceof v||i instanceof E||i instanceof P||i instanceof p)return i;let r=i.message?.toLowerCase?.()||"";return r.includes("cep not found")||r.includes("not found")||r.includes("status: 404")?new E(e,t):(i.name==="AbortError",i)}return new Error(String(i))}var S={AC:"68",AL:"82",AM:"92",AP:"96",BA:"71",CE:"85",DF:"61",ES:"27",GO:"62",MA:"98",MG:"31",MS:"67",MT:"65",PA:"91",PB:"83",PE:"81",PI:"86",PR:"41",RJ:"21",RN:"84",RO:"69",RR:"95",RS:"51",SC:"48",SE:"79",SP:"11",TO:"63"};var M=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(s=>s!==t))}emit(e,t){let r=this.listeners[e];r&&r.forEach(s=>s(t))}};function $(i){if(!/^(\d{8}|\d{5}-\d{3})$/.test(i))throw new f(i);return i.replace("-","")}function K(i){let e={...i};return Object.keys(e).forEach(t=>{let r=e[t];typeof r=="string"&&(e[t]=r.trim())}),e}function j(i){if(!i.ddd&&i.state){let e=S[i.state];if(e)return{...i,ddd:e}}return i}var O=class{constructor(e){this.requestTimestamps=[];this.providerState=new Map;this.providers=e.providers,this.sortedProviders=[...e.providers],this.emitter=new M,this.fetcher=e.fetcher||(async(t,r)=>{let s=await fetch(t,{signal:r});if(!s.ok)throw new Error(`HTTP error! status: ${s.status}`);return s.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="01001000",t=new AbortController,r=this.providers.map(async o=>{let a=Date.now();try{let u=o.buildUrl(e);return await this.fetcher(u,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 s=r.successCount+r.failureCount;r.avgLatencyMs=s===1?t:(r.avgLatencyMs*(s-1)+t)/s,r.openUntil=void 0}recordProviderFailure(e,t,r){let s=this.getOrCreateProviderState(e);s.requests+=1,s.failureCount+=1,s.consecutiveFailures+=1;let c=s.successCount+s.failureCount;s.avgLatencyMs=c===1?t:(s.avgLatencyMs*(c-1)+t)/c,r instanceof v&&(s.timeoutErrors+=1),r instanceof E&&(s.notFoundErrors+=1),this.circuitBreakerEnabled&&s.consecutiveFailures>=this.circuitFailureThreshold&&(s.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,s=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 s*.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 g(this.rateLimit.requests,this.rateLimit.per);this.requestTimestamps.push(e)}async lookup(e,t){this.checkRateLimit();let r=$(e);if(this.log("lookup:start",{cep:r}),this.cache){let o=this.cache.get(r);if(o)return this.log("cache:hit",{cep:r}),this.emitter.emit("cache:hit",{cep:r}),t?t(o):o}let s,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,cep:r,delay:a}),await new Promise(u=>setTimeout(u,a))}try{return await this._lookupFromProviders(r,t)}catch(a){if(a instanceof f||a instanceof g)throw a;s=a}}throw s}async _lookupFromProviders(e,t){let r=new AbortController,{signal:s}=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 p([new P("all")]);if(c.length===0&&this.circuitBreakerEnabled)throw new p(a.map(n=>new P(n.name)));let u=n=>{let l=Date.now(),L=n.buildUrl(e);this.log("provider:start",{provider:n.name,cep:e});let I=new Promise((b,m)=>{if(!n.timeout)return;let d=setTimeout(()=>{s.removeEventListener("abort",R);let N=Date.now()-l,T=new v(n.name,n.timeout);this.recordProviderFailure(n.name,N,T),this.log("provider:failure",{provider:n.name,cep:e,error:T.message}),this.emitter.emit("failure",{provider:n.name,cep:e,duration:N,error:T}),m(T)},n.timeout),R=()=>clearTimeout(d);s.addEventListener("abort",R,{once:!0})}),B=this.fetcher(L,s).then(b=>n.transform(b)).then(b=>{let m=Date.now()-l,d=j(K(b));return this.recordProviderSuccess(n.name,m),this.log("provider:success",{provider:n.name,cep:e,duration:m}),this.emitter.emit("success",{provider:n.name,cep:e,duration:m,address:d}),this.cache&&this.cache.set(e,d),t?t(d):d}).catch(b=>{let m=Date.now()-l,d=F(b,e,n.name);throw d.name!=="AbortError"&&!(d instanceof v)&&(this.recordProviderFailure(n.name,m,d),this.log("provider:failure",{provider:n.name,cep:e,error:d.message}),this.emitter.emit("failure",{provider:n.name,cep:e,duration:m,error:d})),d});return Promise.race([B,I])},y=a[0],h=a.slice(1);if(h.length===0)try{return await u(y)}finally{r.abort()}let w=null,C=null,k=new Promise((n,l)=>{C=()=>{if(w&&clearTimeout(w),s.aborted)return;let L=h.map(u);Promise.any(L).then(n).catch(l)},w=setTimeout(C,this.staggerDelay)}),D=u(y).catch(n=>{throw C&&C(),n});try{return await Promise.any([D,k])}catch(n){let l=n.errors||[n];throw new p(l)}finally{w&&clearTimeout(w),r.abort()}}async lookupCeps(e,t=5,r){if(!e||e.length===0)return[];let s=new Array(e.length),c=0,o=async()=>{for(;c<e.length;){let u=c++;if(u>=e.length)break;let y=e[u];try{let h=await this.lookup(y);if(h)s[u]={cep:y,data:r?r(h):h,provider:h.service};else throw new Error("No address found")}catch(h){s[u]={cep:y,data:null,error:h}}}},a=Array.from({length:Math.min(t,e.length)},()=>o());return await Promise.all(a),s.filter(Boolean)}};
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- var b=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 v=class extends Error{constructor(e){super("Invalid CEP format. Use either NNNNNNNN or NNNNN-NNN."),this.name="CepValidationError",this.cep=e}},g=class extends Error{constructor(e,t){super(`Rate limit exceeded: ${e} requests per ${t}ms.`),this.name="RateLimitError",this.limit=e,this.window=t}},y=class extends Error{constructor(e,t){super(`Timeout from ${e}`),this.name="ProviderTimeoutError",this.provider=e,this.timeout=t}},P=class extends Error{constructor(e,t){super("CEP not found"),this.name="CepNotFoundError",this.cep=e,this.provider=t}},w=class extends Error{constructor(e){super("All providers failed to resolve the CEP."),this.name="AllProvidersFailedError",this.errors=e}};var L={AC:"68",AL:"82",AM:"92",AP:"96",BA:"71",CE:"85",DF:"61",ES:"27",GO:"62",MA:"98",MG:"31",MS:"67",MT:"65",PA:"91",PB:"83",PE:"81",PI:"86",PR:"41",RJ:"21",RN:"84",RO:"69",RR:"95",RS:"51",SC:"48",SE:"79",SP:"11",TO:"63"};var A=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 M(o){if(!/^(\d{8}|\d{5}-\d{3})$/.test(o))throw new v(o);return o.replace("-","")}function O(o){let e={...o};return Object.keys(e).forEach(t=>{let r=e[t];typeof r=="string"&&(e[t]=r.trim())}),e}function S(o){if(!o.ddd&&o.state){let e=L[o.state];if(e)return{...o,ddd:e}}return o}var R=class{constructor(e){this.requestTimestamps=[];this.providers=e.providers,this.sortedProviders=[...e.providers],this.emitter=new A,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}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="01001000",t=new AbortController,r=this.providers.map(async s=>{let a=Date.now();try{let c=s.buildUrl(e);return await this.fetcher(c,t.signal),{provider:s,duration:Date.now()-a,error:!1}}catch{return{provider:s,duration:1/0,error:!0}}}),d=(await Promise.all(r)).sort((s,a)=>s.duration-a.duration);return this.sortedProviders=d.map(s=>s.provider).filter(s=>!!s),t.abort(),this.sortedProviders}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 g(this.rateLimit.requests,this.rateLimit.per);this.requestTimestamps.push(e)}async lookup(e,t){this.checkRateLimit();let r=M(e);if(this.log("lookup:start",{cep:r}),this.cache){let s=this.cache.get(r);if(s)return this.log("cache:hit",{cep:r}),this.emitter.emit("cache:hit",{cep:r}),t?t(s):s}let i,d=1+this.retries;for(let s=0;s<d;s++){if(s>0){let a=this.retryDelay*Math.pow(2,s-1);this.log("retry:attempt",{attempt:s,cep:r,delay:a}),await new Promise(c=>setTimeout(c,a))}try{return await this._lookupFromProviders(r,t)}catch(a){if(a instanceof v||a instanceof g)throw a;i=a}}throw i}async _lookupFromProviders(e,t){let r=new AbortController,{signal:i}=r,d=n=>{let m=Date.now(),E=n.buildUrl(e);this.log("provider:start",{provider:n.name,cep:e});let k=new Promise((l,p)=>{if(!n.timeout)return;let f=setTimeout(()=>{i.removeEventListener("abort",x);let D=Date.now()-m,T=new y(n.name,n.timeout);this.log("provider:failure",{provider:n.name,cep:e,error:T.message}),this.emitter.emit("failure",{provider:n.name,cep:e,duration:D,error:T}),p(T)},n.timeout),x=()=>clearTimeout(f);i.addEventListener("abort",x,{once:!0})}),C=this.fetcher(E,i).then(l=>n.transform(l)).then(l=>{let p=Date.now()-m,f=S(O(l));return this.log("provider:success",{provider:n.name,cep:e,duration:p}),this.emitter.emit("success",{provider:n.name,cep:e,duration:p,address:f}),this.cache&&this.cache.set(e,f),t?t(f):f}).catch(l=>{let p=Date.now()-m;throw!l.message.includes("Timeout from")&&l.name!=="AbortError"&&(this.log("provider:failure",{provider:n.name,cep:e,error:l.message}),this.emitter.emit("failure",{provider:n.name,cep:e,duration:p,error:l})),l});return Promise.race([C,k])},s=this.sortedProviders[0],a=this.sortedProviders.slice(1);if(a.length===0)try{return await d(s)}finally{r.abort()}let c=null,h=null,u=new Promise((n,m)=>{h=()=>{if(c&&clearTimeout(c),i.aborted)return;let E=a.map(d);Promise.any(E).then(n).catch(m)},c=setTimeout(h,this.staggerDelay)}),N=d(s).catch(n=>{throw h&&h(),n});try{return await Promise.any([N,u])}catch(n){let m=n.errors||[n];throw new w(m)}finally{c&&clearTimeout(c),r.abort()}}async lookupCeps(e,t=5,r){if(!e||e.length===0)return[];let i=new Array(e.length),d=0,s=async()=>{for(;d<e.length;){let c=d++;if(c>=e.length)break;let h=e[c];try{let u=await this.lookup(h);if(u)i[c]={cep:h,data:r?r(u):u,provider:u.service};else throw new Error("No address found")}catch(u){i[c]={cep:h,data:null,error:u}}}},a=Array.from({length:Math.min(t,e.length)},()=>s());return await Promise.all(a),i.filter(Boolean)}};export{w as AllProvidersFailedError,R as CepLookup,P as CepNotFoundError,v as CepValidationError,b as InMemoryCache,y as ProviderTimeoutError,g as RateLimitError};
1
+ var L=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 CEP format. Use either NNNNNNNN or NNNNN-NNN.");this.code="INVALID_CEP";this.name="CepValidationError",this.cep=t}},P=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}},y=class extends Error{constructor(t,r){super("CEP not found");this.code="NOT_FOUND";this.name="CepNotFoundError",this.cep=t,this.provider=r}},p=class extends Error{constructor(t){super("All providers failed to resolve the CEP.");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 R(i,e,t){if(i instanceof Error){if(i instanceof E||i instanceof P||i instanceof v||i instanceof y||i instanceof b||i instanceof p)return i;let r=i.message?.toLowerCase?.()||"";return r.includes("cep not found")||r.includes("not found")||r.includes("status: 404")?new y(e,t):(i.name==="AbortError",i)}return new Error(String(i))}var N={AC:"68",AL:"82",AM:"92",AP:"96",BA:"71",CE:"85",DF:"61",ES:"27",GO:"62",MA:"98",MG:"31",MS:"67",MT:"65",PA:"91",PB:"83",PE:"81",PI:"86",PR:"41",RJ:"21",RN:"84",RO:"69",RR:"95",RS:"51",SC:"48",SE:"79",SP:"11",TO:"63"};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(s=>s!==t))}emit(e,t){let r=this.listeners[e];r&&r.forEach(s=>s(t))}};function B(i){if(!/^(\d{8}|\d{5}-\d{3})$/.test(i))throw new E(i);return i.replace("-","")}function U(i){let e={...i};return Object.keys(e).forEach(t=>{let r=e[t];typeof r=="string"&&(e[t]=r.trim())}),e}function q(i){if(!i.ddd&&i.state){let e=N[i.state];if(e)return{...i,ddd:e}}return i}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 s=await fetch(t,{signal:r});if(!s.ok)throw new Error(`HTTP error! status: ${s.status}`);return s.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="01001000",t=new AbortController,r=this.providers.map(async o=>{let a=Date.now();try{let u=o.buildUrl(e);return await this.fetcher(u,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 s=r.successCount+r.failureCount;r.avgLatencyMs=s===1?t:(r.avgLatencyMs*(s-1)+t)/s,r.openUntil=void 0}recordProviderFailure(e,t,r){let s=this.getOrCreateProviderState(e);s.requests+=1,s.failureCount+=1,s.consecutiveFailures+=1;let c=s.successCount+s.failureCount;s.avgLatencyMs=c===1?t:(s.avgLatencyMs*(c-1)+t)/c,r instanceof v&&(s.timeoutErrors+=1),r instanceof y&&(s.notFoundErrors+=1),this.circuitBreakerEnabled&&s.consecutiveFailures>=this.circuitFailureThreshold&&(s.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,s=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 s*.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 P(this.rateLimit.requests,this.rateLimit.per);this.requestTimestamps.push(e)}async lookup(e,t){this.checkRateLimit();let r=B(e);if(this.log("lookup:start",{cep:r}),this.cache){let o=this.cache.get(r);if(o)return this.log("cache:hit",{cep:r}),this.emitter.emit("cache:hit",{cep:r}),t?t(o):o}let s,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,cep:r,delay:a}),await new Promise(u=>setTimeout(u,a))}try{return await this._lookupFromProviders(r,t)}catch(a){if(a instanceof E||a instanceof P)throw a;s=a}}throw s}async _lookupFromProviders(e,t){let r=new AbortController,{signal:s}=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 p([new b("all")]);if(c.length===0&&this.circuitBreakerEnabled)throw new p(a.map(n=>new b(n.name)));let u=n=>{let l=Date.now(),A=n.buildUrl(e);this.log("provider:start",{provider:n.name,cep:e});let D=new Promise((g,m)=>{if(!n.timeout)return;let d=setTimeout(()=>{s.removeEventListener("abort",M);let O=Date.now()-l,T=new v(n.name,n.timeout);this.recordProviderFailure(n.name,O,T),this.log("provider:failure",{provider:n.name,cep:e,error:T.message}),this.emitter.emit("failure",{provider:n.name,cep:e,duration:O,error:T}),m(T)},n.timeout),M=()=>clearTimeout(d);s.addEventListener("abort",M,{once:!0})}),I=this.fetcher(A,s).then(g=>n.transform(g)).then(g=>{let m=Date.now()-l,d=q(U(g));return this.recordProviderSuccess(n.name,m),this.log("provider:success",{provider:n.name,cep:e,duration:m}),this.emitter.emit("success",{provider:n.name,cep:e,duration:m,address:d}),this.cache&&this.cache.set(e,d),t?t(d):d}).catch(g=>{let m=Date.now()-l,d=R(g,e,n.name);throw d.name!=="AbortError"&&!(d instanceof v)&&(this.recordProviderFailure(n.name,m,d),this.log("provider:failure",{provider:n.name,cep:e,error:d.message}),this.emitter.emit("failure",{provider:n.name,cep:e,duration:m,error:d})),d});return Promise.race([I,D])},f=a[0],h=a.slice(1);if(h.length===0)try{return await u(f)}finally{r.abort()}let w=null,C=null,S=new Promise((n,l)=>{C=()=>{if(w&&clearTimeout(w),s.aborted)return;let A=h.map(u);Promise.any(A).then(n).catch(l)},w=setTimeout(C,this.staggerDelay)}),k=u(f).catch(n=>{throw C&&C(),n});try{return await Promise.any([k,S])}catch(n){let l=n.errors||[n];throw new p(l)}finally{w&&clearTimeout(w),r.abort()}}async lookupCeps(e,t=5,r){if(!e||e.length===0)return[];let s=new Array(e.length),c=0,o=async()=>{for(;c<e.length;){let u=c++;if(u>=e.length)break;let f=e[u];try{let h=await this.lookup(f);if(h)s[u]={cep:f,data:r?r(h):h,provider:h.service};else throw new Error("No address found")}catch(h){s[u]={cep:f,data:null,error:h}}}},a=Array.from({length:Math.min(t,e.length)},()=>o());return await Promise.all(a),s.filter(Boolean)}};export{p as AllProvidersFailedError,F as CepLookup,y as CepNotFoundError,E as CepValidationError,L as InMemoryCache,v as ProviderTimeoutError,b as ProviderUnavailableError,P as RateLimitError};
@@ -1,23 +1,35 @@
1
+ export type CepErrorCode = "INVALID_CEP" | "RATE_LIMITED" | "TIMEOUT" | "NOT_FOUND" | "PROVIDER_UNAVAILABLE" | "ALL_PROVIDERS_FAILED" | "UNKNOWN";
1
2
  export declare class CepValidationError extends Error {
2
3
  readonly cep: string;
4
+ readonly code: CepErrorCode;
3
5
  constructor(cep: string);
4
6
  }
5
7
  export declare class RateLimitError extends Error {
6
8
  readonly limit: number;
7
9
  readonly window: number;
10
+ readonly code: CepErrorCode;
8
11
  constructor(limit: number, window: number);
9
12
  }
10
13
  export declare class ProviderTimeoutError extends Error {
11
14
  readonly provider: string;
12
15
  readonly timeout: number;
16
+ readonly code: CepErrorCode;
13
17
  constructor(provider: string, timeout: number);
14
18
  }
15
19
  export declare class CepNotFoundError extends Error {
16
20
  readonly cep: string;
17
21
  readonly provider?: string;
22
+ readonly code: CepErrorCode;
18
23
  constructor(cep: string, provider?: string);
19
24
  }
20
25
  export declare class AllProvidersFailedError extends Error {
21
26
  readonly errors: Error[];
27
+ readonly code: CepErrorCode;
22
28
  constructor(errors: Error[]);
23
29
  }
30
+ export declare class ProviderUnavailableError extends Error {
31
+ readonly provider: string;
32
+ readonly code: CepErrorCode;
33
+ constructor(provider: string);
34
+ }
35
+ export declare function normalizeProviderError(error: unknown, cep: string, provider: string): Error;
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AllProvidersFailedError = exports.CepNotFoundError = exports.ProviderTimeoutError = exports.RateLimitError = exports.CepValidationError = void 0;
3
+ exports.ProviderUnavailableError = exports.AllProvidersFailedError = exports.CepNotFoundError = exports.ProviderTimeoutError = exports.RateLimitError = exports.CepValidationError = void 0;
4
+ exports.normalizeProviderError = normalizeProviderError;
4
5
  class CepValidationError extends Error {
5
6
  constructor(cep) {
6
7
  super("Invalid CEP format. Use either NNNNNNNN or NNNNN-NNN.");
8
+ this.code = "INVALID_CEP";
7
9
  this.name = "CepValidationError";
8
10
  this.cep = cep;
9
11
  }
@@ -12,6 +14,7 @@ exports.CepValidationError = CepValidationError;
12
14
  class RateLimitError extends Error {
13
15
  constructor(limit, window) {
14
16
  super(`Rate limit exceeded: ${limit} requests per ${window}ms.`);
17
+ this.code = "RATE_LIMITED";
15
18
  this.name = "RateLimitError";
16
19
  this.limit = limit;
17
20
  this.window = window;
@@ -21,6 +24,7 @@ exports.RateLimitError = RateLimitError;
21
24
  class ProviderTimeoutError extends Error {
22
25
  constructor(provider, timeout) {
23
26
  super(`Timeout from ${provider}`);
27
+ this.code = "TIMEOUT";
24
28
  this.name = "ProviderTimeoutError";
25
29
  this.provider = provider;
26
30
  this.timeout = timeout;
@@ -30,6 +34,7 @@ exports.ProviderTimeoutError = ProviderTimeoutError;
30
34
  class CepNotFoundError extends Error {
31
35
  constructor(cep, provider) {
32
36
  super("CEP not found");
37
+ this.code = "NOT_FOUND";
33
38
  this.name = "CepNotFoundError";
34
39
  this.cep = cep;
35
40
  this.provider = provider;
@@ -39,8 +44,39 @@ exports.CepNotFoundError = CepNotFoundError;
39
44
  class AllProvidersFailedError extends Error {
40
45
  constructor(errors) {
41
46
  super("All providers failed to resolve the CEP.");
47
+ this.code = "ALL_PROVIDERS_FAILED";
42
48
  this.name = "AllProvidersFailedError";
43
49
  this.errors = errors;
44
50
  }
45
51
  }
46
52
  exports.AllProvidersFailedError = AllProvidersFailedError;
53
+ class ProviderUnavailableError extends Error {
54
+ constructor(provider) {
55
+ super(`Provider ${provider} is temporarily unavailable (circuit open).`);
56
+ this.code = "PROVIDER_UNAVAILABLE";
57
+ this.name = "ProviderUnavailableError";
58
+ this.provider = provider;
59
+ }
60
+ }
61
+ exports.ProviderUnavailableError = ProviderUnavailableError;
62
+ function normalizeProviderError(error, cep, provider) {
63
+ if (error instanceof Error) {
64
+ if (error instanceof CepValidationError ||
65
+ error instanceof RateLimitError ||
66
+ error instanceof ProviderTimeoutError ||
67
+ error instanceof CepNotFoundError ||
68
+ error instanceof ProviderUnavailableError ||
69
+ error instanceof AllProvidersFailedError) {
70
+ return error;
71
+ }
72
+ const message = error.message?.toLowerCase?.() || "";
73
+ if (message.includes("cep not found") || message.includes("not found") || message.includes("status: 404")) {
74
+ return new CepNotFoundError(cep, provider);
75
+ }
76
+ if (error.name === "AbortError") {
77
+ return error;
78
+ }
79
+ return error;
80
+ }
81
+ return new Error(String(error));
82
+ }
@@ -1,9 +1,9 @@
1
- import { Address, Fetcher, Provider, CepLookupOptions, BulkCepResult, RateLimitOptions, EventName, EventListener, EventMap } from "./types";
1
+ import { Address, Fetcher, Provider, CepLookupOptions, BulkCepResult, RateLimitOptions, EventName, EventListener, EventMap, ProviderHealth, ProviderMetrics, CircuitBreakerOptions } from "./types";
2
2
  import { Cache, InMemoryCache, InMemoryCacheOptions } from "./cache";
3
- import { CepValidationError, RateLimitError, ProviderTimeoutError, CepNotFoundError, AllProvidersFailedError } from "./errors";
4
- export type { Address, Fetcher, Provider, CepLookupOptions, BulkCepResult, RateLimitOptions, EventName, EventListener, EventMap, Cache, InMemoryCacheOptions };
3
+ import { CepValidationError, RateLimitError, ProviderTimeoutError, CepNotFoundError, AllProvidersFailedError, ProviderUnavailableError } from "./errors";
4
+ export type { Address, Fetcher, Provider, CepLookupOptions, BulkCepResult, RateLimitOptions, EventName, EventListener, EventMap, Cache, InMemoryCacheOptions, ProviderHealth, ProviderMetrics, CircuitBreakerOptions };
5
5
  export { InMemoryCache };
6
- export { CepValidationError, RateLimitError, ProviderTimeoutError, CepNotFoundError, AllProvidersFailedError };
6
+ export { CepValidationError, RateLimitError, ProviderTimeoutError, CepNotFoundError, AllProvidersFailedError, ProviderUnavailableError };
7
7
  /**
8
8
  * @class CepLookup
9
9
  * @description A class for looking up Brazilian postal codes (CEPs) using multiple providers.
@@ -20,6 +20,10 @@ export declare class CepLookup {
20
20
  private logger?;
21
21
  private requestTimestamps;
22
22
  private emitter;
23
+ private circuitBreakerEnabled;
24
+ private circuitFailureThreshold;
25
+ private circuitCooldownMs;
26
+ private providerState;
23
27
  constructor(options: CepLookupOptions);
24
28
  private log;
25
29
  on<T extends EventName>(eventName: T, listener: EventListener<T>): void;
@@ -31,6 +35,13 @@ export declare class CepLookup {
31
35
  * @returns {Promise<Provider[]>} The list of providers sorted by latency.
32
36
  */
33
37
  warmup(): Promise<Provider[]>;
38
+ private getOrCreateProviderState;
39
+ private recordProviderSuccess;
40
+ private recordProviderFailure;
41
+ private isProviderOpen;
42
+ private scoreProvider;
43
+ getProviderHealth(): ProviderHealth[];
44
+ getProviderMetrics(): ProviderMetrics[];
34
45
  private checkRateLimit;
35
46
  lookup<T = Address>(cep: string, mapper?: (address: Address) => T): Promise<T>;
36
47
  private _lookupFromProviders;
package/dist/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CepLookup = exports.AllProvidersFailedError = exports.CepNotFoundError = exports.ProviderTimeoutError = exports.RateLimitError = exports.CepValidationError = exports.InMemoryCache = void 0;
3
+ exports.CepLookup = exports.ProviderUnavailableError = exports.AllProvidersFailedError = exports.CepNotFoundError = exports.ProviderTimeoutError = exports.RateLimitError = exports.CepValidationError = exports.InMemoryCache = void 0;
4
4
  const cache_1 = require("./cache");
5
5
  Object.defineProperty(exports, "InMemoryCache", { enumerable: true, get: function () { return cache_1.InMemoryCache; } });
6
6
  const errors_1 = require("./errors");
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "RateLimitError", { enumerable: true, get: functi
9
9
  Object.defineProperty(exports, "ProviderTimeoutError", { enumerable: true, get: function () { return errors_1.ProviderTimeoutError; } });
10
10
  Object.defineProperty(exports, "CepNotFoundError", { enumerable: true, get: function () { return errors_1.CepNotFoundError; } });
11
11
  Object.defineProperty(exports, "AllProvidersFailedError", { enumerable: true, get: function () { return errors_1.AllProvidersFailedError; } });
12
+ Object.defineProperty(exports, "ProviderUnavailableError", { enumerable: true, get: function () { return errors_1.ProviderUnavailableError; } });
12
13
  const ddd_by_state_1 = require("./data/ddd-by-state");
13
14
  // Minimal EventEmitter for internal use
14
15
  class EventEmitter {
@@ -86,6 +87,7 @@ function enrichAddress(address) {
86
87
  class CepLookup {
87
88
  constructor(options) {
88
89
  this.requestTimestamps = [];
90
+ this.providerState = new Map();
89
91
  this.providers = options.providers;
90
92
  this.sortedProviders = [...options.providers];
91
93
  this.emitter = new EventEmitter();
@@ -102,6 +104,20 @@ class CepLookup {
102
104
  this.retries = options.retries ?? 0;
103
105
  this.retryDelay = options.retryDelay ?? 1000;
104
106
  this.logger = options.logger;
107
+ this.circuitBreakerEnabled = options.circuitBreaker?.enabled ?? true;
108
+ this.circuitFailureThreshold = options.circuitBreaker?.failureThreshold ?? 3;
109
+ this.circuitCooldownMs = options.circuitBreaker?.cooldownMs ?? 30000;
110
+ this.providers.forEach((provider) => {
111
+ this.providerState.set(provider.name, {
112
+ consecutiveFailures: 0,
113
+ successCount: 0,
114
+ failureCount: 0,
115
+ avgLatencyMs: 0,
116
+ requests: 0,
117
+ timeoutErrors: 0,
118
+ notFoundErrors: 0,
119
+ });
120
+ });
105
121
  }
106
122
  log(msg, data) {
107
123
  this.logger?.debug(msg, data);
@@ -144,6 +160,100 @@ class CepLookup {
144
160
  controller.abort();
145
161
  return this.sortedProviders;
146
162
  }
163
+ getOrCreateProviderState(providerName) {
164
+ const existing = this.providerState.get(providerName);
165
+ if (existing)
166
+ return existing;
167
+ const created = {
168
+ consecutiveFailures: 0,
169
+ successCount: 0,
170
+ failureCount: 0,
171
+ avgLatencyMs: 0,
172
+ requests: 0,
173
+ timeoutErrors: 0,
174
+ notFoundErrors: 0,
175
+ };
176
+ this.providerState.set(providerName, created);
177
+ return created;
178
+ }
179
+ recordProviderSuccess(providerName, durationMs) {
180
+ const state = this.getOrCreateProviderState(providerName);
181
+ state.requests += 1;
182
+ state.successCount += 1;
183
+ state.consecutiveFailures = 0;
184
+ const n = state.successCount + state.failureCount;
185
+ state.avgLatencyMs = n === 1 ? durationMs : ((state.avgLatencyMs * (n - 1)) + durationMs) / n;
186
+ state.openUntil = undefined;
187
+ }
188
+ recordProviderFailure(providerName, durationMs, error) {
189
+ const state = this.getOrCreateProviderState(providerName);
190
+ state.requests += 1;
191
+ state.failureCount += 1;
192
+ state.consecutiveFailures += 1;
193
+ const n = state.successCount + state.failureCount;
194
+ state.avgLatencyMs = n === 1 ? durationMs : ((state.avgLatencyMs * (n - 1)) + durationMs) / n;
195
+ if (error instanceof errors_1.ProviderTimeoutError) {
196
+ state.timeoutErrors += 1;
197
+ }
198
+ if (error instanceof errors_1.CepNotFoundError) {
199
+ state.notFoundErrors += 1;
200
+ }
201
+ if (this.circuitBreakerEnabled && state.consecutiveFailures >= this.circuitFailureThreshold) {
202
+ state.openUntil = Date.now() + this.circuitCooldownMs;
203
+ }
204
+ }
205
+ isProviderOpen(providerName) {
206
+ if (!this.circuitBreakerEnabled)
207
+ return false;
208
+ const state = this.getOrCreateProviderState(providerName);
209
+ if (!state.openUntil)
210
+ return false;
211
+ if (Date.now() >= state.openUntil) {
212
+ state.openUntil = undefined;
213
+ state.consecutiveFailures = 0;
214
+ return false;
215
+ }
216
+ return true;
217
+ }
218
+ scoreProvider(provider) {
219
+ const state = this.getOrCreateProviderState(provider.name);
220
+ const total = state.successCount + state.failureCount;
221
+ const successRate = total === 0 ? 1 : state.successCount / total;
222
+ const latencyPenalty = state.avgLatencyMs > 0 ? Math.min(state.avgLatencyMs / 1000, 1) : 0;
223
+ const openPenalty = this.isProviderOpen(provider.name) ? 1 : 0;
224
+ return (successRate * 0.8) + ((1 - latencyPenalty) * 0.2) - openPenalty;
225
+ }
226
+ getProviderHealth() {
227
+ return this.providers
228
+ .map((provider) => {
229
+ const state = this.getOrCreateProviderState(provider.name);
230
+ return {
231
+ provider: provider.name,
232
+ score: Number(this.scoreProvider(provider).toFixed(4)),
233
+ isOpen: this.isProviderOpen(provider.name),
234
+ openUntil: state.openUntil,
235
+ consecutiveFailures: state.consecutiveFailures,
236
+ successCount: state.successCount,
237
+ failureCount: state.failureCount,
238
+ avgLatencyMs: Number(state.avgLatencyMs.toFixed(2)),
239
+ };
240
+ })
241
+ .sort((a, b) => b.score - a.score);
242
+ }
243
+ getProviderMetrics() {
244
+ return this.providers.map((provider) => {
245
+ const state = this.getOrCreateProviderState(provider.name);
246
+ return {
247
+ provider: provider.name,
248
+ requests: state.requests,
249
+ successes: state.successCount,
250
+ failures: state.failureCount,
251
+ timeoutErrors: state.timeoutErrors,
252
+ notFoundErrors: state.notFoundErrors,
253
+ avgLatencyMs: Number(state.avgLatencyMs.toFixed(2)),
254
+ };
255
+ });
256
+ }
147
257
  checkRateLimit() {
148
258
  if (!this.rateLimit)
149
259
  return;
@@ -190,6 +300,15 @@ class CepLookup {
190
300
  async _lookupFromProviders(cleanedCep, mapper) {
191
301
  const controller = new AbortController();
192
302
  const { signal } = controller;
303
+ const availableProviders = this.sortedProviders.filter((provider) => !this.isProviderOpen(provider.name));
304
+ const providersByHealth = [...availableProviders].sort((a, b) => this.scoreProvider(b) - this.scoreProvider(a));
305
+ const selectedProviders = providersByHealth.length > 0 ? providersByHealth : [...this.sortedProviders].sort((a, b) => this.scoreProvider(b) - this.scoreProvider(a));
306
+ if (selectedProviders.length === 0) {
307
+ throw new errors_1.AllProvidersFailedError([new errors_1.ProviderUnavailableError("all")]);
308
+ }
309
+ if (availableProviders.length === 0 && this.circuitBreakerEnabled) {
310
+ throw new errors_1.AllProvidersFailedError(selectedProviders.map((p) => new errors_1.ProviderUnavailableError(p.name)));
311
+ }
193
312
  const createProviderPromise = (provider) => {
194
313
  const startTime = Date.now();
195
314
  const url = provider.buildUrl(cleanedCep);
@@ -201,6 +320,7 @@ class CepLookup {
201
320
  signal.removeEventListener('abort', onAbort);
202
321
  const duration = Date.now() - startTime;
203
322
  const error = new errors_1.ProviderTimeoutError(provider.name, provider.timeout);
323
+ this.recordProviderFailure(provider.name, duration, error);
204
324
  this.log('provider:failure', { provider: provider.name, cep: cleanedCep, error: error.message });
205
325
  this.emitter.emit('failure', { provider: provider.name, cep: cleanedCep, duration, error });
206
326
  reject(error);
@@ -213,6 +333,7 @@ class CepLookup {
213
333
  .then((address) => {
214
334
  const duration = Date.now() - startTime;
215
335
  const sanitizedAddress = enrichAddress(sanitizeAddress(address));
336
+ this.recordProviderSuccess(provider.name, duration);
216
337
  this.log('provider:success', { provider: provider.name, cep: cleanedCep, duration });
217
338
  this.emitter.emit('success', { provider: provider.name, cep: cleanedCep, duration, address: sanitizedAddress });
218
339
  if (this.cache) {
@@ -222,16 +343,18 @@ class CepLookup {
222
343
  })
223
344
  .catch((error) => {
224
345
  const duration = Date.now() - startTime;
225
- if (!error.message.includes('Timeout from') && error.name !== 'AbortError') {
226
- this.log('provider:failure', { provider: provider.name, cep: cleanedCep, error: error.message });
227
- this.emitter.emit('failure', { provider: provider.name, cep: cleanedCep, duration, error });
346
+ const normalizedError = (0, errors_1.normalizeProviderError)(error, cleanedCep, provider.name);
347
+ if (normalizedError.name !== 'AbortError' && !(normalizedError instanceof errors_1.ProviderTimeoutError)) {
348
+ this.recordProviderFailure(provider.name, duration, normalizedError);
349
+ this.log('provider:failure', { provider: provider.name, cep: cleanedCep, error: normalizedError.message });
350
+ this.emitter.emit('failure', { provider: provider.name, cep: cleanedCep, duration, error: normalizedError });
228
351
  }
229
- throw error;
352
+ throw normalizedError;
230
353
  });
231
354
  return Promise.race([fetchPromise, timeoutPromise]);
232
355
  };
233
- const bestProvider = this.sortedProviders[0];
234
- const otherProviders = this.sortedProviders.slice(1);
356
+ const bestProvider = selectedProviders[0];
357
+ const otherProviders = selectedProviders.slice(1);
235
358
  if (otherProviders.length === 0) {
236
359
  try {
237
360
  return await createProviderPromise(bestProvider);
@@ -54,6 +54,8 @@ export interface CepLookupOptions {
54
54
  logger?: {
55
55
  debug: (msg: string, data?: Record<string, unknown>) => void;
56
56
  };
57
+ /** Circuit breaker options for provider resilience */
58
+ circuitBreaker?: CircuitBreakerOptions;
57
59
  }
58
60
  /**
59
61
  * @interface BulkCepResult
@@ -87,3 +89,30 @@ export interface EventMap {
87
89
  'cache:hit': CacheHitPayload;
88
90
  }
89
91
  export type EventListener<T extends EventName> = (payload: EventMap[T]) => void;
92
+ export interface CircuitBreakerOptions {
93
+ /** Consecutive failures required to open the circuit. Default: 3 */
94
+ failureThreshold?: number;
95
+ /** Cooldown in ms before trying a provider again. Default: 30000 */
96
+ cooldownMs?: number;
97
+ /** Enable/disable circuit breaker. Default: true */
98
+ enabled?: boolean;
99
+ }
100
+ export interface ProviderHealth {
101
+ provider: string;
102
+ score: number;
103
+ isOpen: boolean;
104
+ openUntil?: number;
105
+ consecutiveFailures: number;
106
+ successCount: number;
107
+ failureCount: number;
108
+ avgLatencyMs: number;
109
+ }
110
+ export interface ProviderMetrics {
111
+ provider: string;
112
+ requests: number;
113
+ successes: number;
114
+ failures: number;
115
+ timeoutErrors: number;
116
+ notFoundErrors: number;
117
+ avgLatencyMs: number;
118
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eusilvio/cep-lookup",
3
- "version": "2.5.1",
3
+ "version": "2.6.0",
4
4
  "description": "A agnostic, performant and flexible CEP lookup library with race strategy and caching.",
5
5
  "license": "MIT",
6
6
  "author": "Silvio Souza",