@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 +106 -213
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/src/errors.d.ts +12 -0
- package/dist/src/errors.js +37 -1
- package/dist/src/index.d.ts +15 -4
- package/dist/src/index.js +130 -7
- package/dist/src/types.d.ts +29 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,28 +1,10 @@
|
|
|
1
1
|
# @eusilvio/cep-lookup
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@eusilvio/cep-lookup)
|
|
4
|
-
[](https://www.npmjs.com/package/@eusilvio/cep-lookup)
|
|
5
4
|
[](https://github.com/eusilvio/cep-lookup/actions)
|
|
6
5
|
[](https://opensource.org/licenses/MIT)
|
|
7
6
|
|
|
8
|
-
|
|
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
|
-
##
|
|
15
|
+
## Features
|
|
34
16
|
|
|
35
|
-
|
|
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
|
-
|
|
25
|
+
## Basic Usage
|
|
38
26
|
|
|
39
|
-
```
|
|
40
|
-
import { CepLookup
|
|
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
|
-
|
|
47
|
-
const cepLookup = new CepLookup({
|
|
31
|
+
const lookup = new CepLookup({
|
|
48
32
|
providers: [viaCepProvider, brasilApiProvider],
|
|
49
33
|
});
|
|
50
34
|
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
39
|
+
## Error Handling
|
|
67
40
|
|
|
68
|
-
```
|
|
69
|
-
import {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
65
|
+
## Circuit Breaker
|
|
104
66
|
|
|
105
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
##
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
##
|
|
112
|
+
## Bulk Lookup
|
|
209
113
|
|
|
210
|
-
|
|
114
|
+
```ts
|
|
115
|
+
const results = await lookup.lookupCeps(["01001-000", "99999-999"], 2);
|
|
116
|
+
```
|
|
211
117
|
|
|
212
|
-
|
|
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
|
-
|
|
120
|
+
### `new CepLookup(options)`
|
|
222
121
|
|
|
223
|
-
|
|
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
|
-
|
|
226
|
-
import { Provider, Address } from "@eusilvio/cep-lookup";
|
|
131
|
+
### Methods
|
|
227
132
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
153
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
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
|
|
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};
|
package/dist/src/errors.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/errors.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
226
|
-
|
|
227
|
-
this.
|
|
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
|
|
352
|
+
throw normalizedError;
|
|
230
353
|
});
|
|
231
354
|
return Promise.race([fetchPromise, timeoutPromise]);
|
|
232
355
|
};
|
|
233
|
-
const bestProvider =
|
|
234
|
-
const otherProviders =
|
|
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);
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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
|
+
}
|