@eusilvio/cep-lookup 2.4.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -47
- package/README.md +109 -210
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/providers/index.cjs +1 -1
- package/dist/providers/index.mjs +1 -1
- package/dist/src/cache/index.d.ts +14 -17
- package/dist/src/cache/index.js +35 -20
- package/dist/src/data/ddd-by-state.d.ts +2 -0
- package/dist/src/data/ddd-by-state.js +12 -0
- package/dist/src/errors.d.ts +35 -0
- package/dist/src/errors.js +82 -0
- package/dist/src/index.d.ts +24 -19
- package/dist/src/index.js +198 -38
- package/dist/src/providers/index.d.ts +1 -0
- package/dist/src/providers/index.js +1 -0
- package/dist/src/providers/opencep.d.ts +10 -0
- package/dist/src/providers/opencep.js +32 -0
- package/dist/src/providers/viacep.js +2 -0
- package/dist/src/types.d.ts +41 -2
- package/package.json +3 -2
package/dist/src/cache/index.js
CHANGED
|
@@ -3,34 +3,49 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.InMemoryCache = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* @class InMemoryCache
|
|
6
|
-
* @description
|
|
6
|
+
* @description In-memory cache with optional TTL and size limit.
|
|
7
7
|
*/
|
|
8
8
|
class InMemoryCache {
|
|
9
|
-
constructor() {
|
|
9
|
+
constructor(options) {
|
|
10
10
|
this.cache = new Map();
|
|
11
|
+
this.ttl = options?.ttl ?? Infinity;
|
|
12
|
+
this.maxSize = options?.maxSize ?? Infinity;
|
|
11
13
|
}
|
|
12
|
-
/**
|
|
13
|
-
* @method get
|
|
14
|
-
* @description Retrieves an address from the cache.
|
|
15
|
-
* @param {string} key - The CEP to look up.
|
|
16
|
-
* @returns {Address | undefined} The cached address or undefined if not found.
|
|
17
|
-
*/
|
|
18
14
|
get(key) {
|
|
19
|
-
|
|
15
|
+
const entry = this.cache.get(key);
|
|
16
|
+
if (!entry)
|
|
17
|
+
return undefined;
|
|
18
|
+
if (this.ttl !== Infinity && Date.now() - entry.timestamp > this.ttl) {
|
|
19
|
+
this.cache.delete(key);
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
return entry.value;
|
|
20
23
|
}
|
|
21
|
-
/**
|
|
22
|
-
* @method set
|
|
23
|
-
* @description Stores an address in the cache.
|
|
24
|
-
* @param {string} key - The CEP to use as the cache key.
|
|
25
|
-
* @param {Address} value - The address to store.
|
|
26
|
-
*/
|
|
27
24
|
set(key, value) {
|
|
28
|
-
this.cache.
|
|
25
|
+
if (this.cache.has(key)) {
|
|
26
|
+
this.cache.delete(key);
|
|
27
|
+
}
|
|
28
|
+
if (this.cache.size >= this.maxSize) {
|
|
29
|
+
const oldestKey = this.cache.keys().next().value;
|
|
30
|
+
if (oldestKey !== undefined) {
|
|
31
|
+
this.cache.delete(oldestKey);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
this.cache.set(key, { value, timestamp: Date.now() });
|
|
35
|
+
}
|
|
36
|
+
delete(key) {
|
|
37
|
+
this.cache.delete(key);
|
|
38
|
+
}
|
|
39
|
+
has(key) {
|
|
40
|
+
if (!this.cache.has(key))
|
|
41
|
+
return false;
|
|
42
|
+
const entry = this.cache.get(key);
|
|
43
|
+
if (this.ttl !== Infinity && Date.now() - entry.timestamp > this.ttl) {
|
|
44
|
+
this.cache.delete(key);
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
29
48
|
}
|
|
30
|
-
/**
|
|
31
|
-
* @method clear
|
|
32
|
-
* @description Clears the entire cache.
|
|
33
|
-
*/
|
|
34
49
|
clear() {
|
|
35
50
|
this.cache.clear();
|
|
36
51
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dddByState = void 0;
|
|
4
|
+
/** DDD da capital de cada estado (fallback para providers que nao retornam DDD) */
|
|
5
|
+
exports.dddByState = {
|
|
6
|
+
AC: "68", AL: "82", AM: "92", AP: "96", BA: "71",
|
|
7
|
+
CE: "85", DF: "61", ES: "27", GO: "62", MA: "98",
|
|
8
|
+
MG: "31", MS: "67", MT: "65", PA: "91", PB: "83",
|
|
9
|
+
PE: "81", PI: "86", PR: "41", RJ: "21", RN: "84",
|
|
10
|
+
RO: "69", RR: "95", RS: "51", SC: "48", SE: "79",
|
|
11
|
+
SP: "11", TO: "63",
|
|
12
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type CepErrorCode = "INVALID_CEP" | "RATE_LIMITED" | "TIMEOUT" | "NOT_FOUND" | "PROVIDER_UNAVAILABLE" | "ALL_PROVIDERS_FAILED" | "UNKNOWN";
|
|
2
|
+
export declare class CepValidationError extends Error {
|
|
3
|
+
readonly cep: string;
|
|
4
|
+
readonly code: CepErrorCode;
|
|
5
|
+
constructor(cep: string);
|
|
6
|
+
}
|
|
7
|
+
export declare class RateLimitError extends Error {
|
|
8
|
+
readonly limit: number;
|
|
9
|
+
readonly window: number;
|
|
10
|
+
readonly code: CepErrorCode;
|
|
11
|
+
constructor(limit: number, window: number);
|
|
12
|
+
}
|
|
13
|
+
export declare class ProviderTimeoutError extends Error {
|
|
14
|
+
readonly provider: string;
|
|
15
|
+
readonly timeout: number;
|
|
16
|
+
readonly code: CepErrorCode;
|
|
17
|
+
constructor(provider: string, timeout: number);
|
|
18
|
+
}
|
|
19
|
+
export declare class CepNotFoundError extends Error {
|
|
20
|
+
readonly cep: string;
|
|
21
|
+
readonly provider?: string;
|
|
22
|
+
readonly code: CepErrorCode;
|
|
23
|
+
constructor(cep: string, provider?: string);
|
|
24
|
+
}
|
|
25
|
+
export declare class AllProvidersFailedError extends Error {
|
|
26
|
+
readonly errors: Error[];
|
|
27
|
+
readonly code: CepErrorCode;
|
|
28
|
+
constructor(errors: Error[]);
|
|
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;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ProviderUnavailableError = exports.AllProvidersFailedError = exports.CepNotFoundError = exports.ProviderTimeoutError = exports.RateLimitError = exports.CepValidationError = void 0;
|
|
4
|
+
exports.normalizeProviderError = normalizeProviderError;
|
|
5
|
+
class CepValidationError extends Error {
|
|
6
|
+
constructor(cep) {
|
|
7
|
+
super("Invalid CEP format. Use either NNNNNNNN or NNNNN-NNN.");
|
|
8
|
+
this.code = "INVALID_CEP";
|
|
9
|
+
this.name = "CepValidationError";
|
|
10
|
+
this.cep = cep;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
exports.CepValidationError = CepValidationError;
|
|
14
|
+
class RateLimitError extends Error {
|
|
15
|
+
constructor(limit, window) {
|
|
16
|
+
super(`Rate limit exceeded: ${limit} requests per ${window}ms.`);
|
|
17
|
+
this.code = "RATE_LIMITED";
|
|
18
|
+
this.name = "RateLimitError";
|
|
19
|
+
this.limit = limit;
|
|
20
|
+
this.window = window;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.RateLimitError = RateLimitError;
|
|
24
|
+
class ProviderTimeoutError extends Error {
|
|
25
|
+
constructor(provider, timeout) {
|
|
26
|
+
super(`Timeout from ${provider}`);
|
|
27
|
+
this.code = "TIMEOUT";
|
|
28
|
+
this.name = "ProviderTimeoutError";
|
|
29
|
+
this.provider = provider;
|
|
30
|
+
this.timeout = timeout;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.ProviderTimeoutError = ProviderTimeoutError;
|
|
34
|
+
class CepNotFoundError extends Error {
|
|
35
|
+
constructor(cep, provider) {
|
|
36
|
+
super("CEP not found");
|
|
37
|
+
this.code = "NOT_FOUND";
|
|
38
|
+
this.name = "CepNotFoundError";
|
|
39
|
+
this.cep = cep;
|
|
40
|
+
this.provider = provider;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.CepNotFoundError = CepNotFoundError;
|
|
44
|
+
class AllProvidersFailedError extends Error {
|
|
45
|
+
constructor(errors) {
|
|
46
|
+
super("All providers failed to resolve the CEP.");
|
|
47
|
+
this.code = "ALL_PROVIDERS_FAILED";
|
|
48
|
+
this.name = "AllProvidersFailedError";
|
|
49
|
+
this.errors = errors;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
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,7 +1,9 @@
|
|
|
1
|
-
import { Address, Fetcher, Provider, CepLookupOptions, BulkCepResult, RateLimitOptions, EventName, EventListener, EventMap } from "./types";
|
|
2
|
-
import { Cache, InMemoryCache } from "./cache";
|
|
3
|
-
|
|
1
|
+
import { Address, Fetcher, Provider, CepLookupOptions, BulkCepResult, RateLimitOptions, EventName, EventListener, EventMap, ProviderHealth, ProviderMetrics, CircuitBreakerOptions } from "./types";
|
|
2
|
+
import { Cache, InMemoryCache, InMemoryCacheOptions } from "./cache";
|
|
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 };
|
|
4
5
|
export { InMemoryCache };
|
|
6
|
+
export { CepValidationError, RateLimitError, ProviderTimeoutError, CepNotFoundError, AllProvidersFailedError, ProviderUnavailableError };
|
|
5
7
|
/**
|
|
6
8
|
* @class CepLookup
|
|
7
9
|
* @description A class for looking up Brazilian postal codes (CEPs) using multiple providers.
|
|
@@ -13,32 +15,35 @@ export declare class CepLookup {
|
|
|
13
15
|
private cache?;
|
|
14
16
|
private rateLimit?;
|
|
15
17
|
private staggerDelay;
|
|
18
|
+
private retries;
|
|
19
|
+
private retryDelay;
|
|
20
|
+
private logger?;
|
|
16
21
|
private requestTimestamps;
|
|
17
22
|
private emitter;
|
|
23
|
+
private circuitBreakerEnabled;
|
|
24
|
+
private circuitFailureThreshold;
|
|
25
|
+
private circuitCooldownMs;
|
|
26
|
+
private providerState;
|
|
18
27
|
constructor(options: CepLookupOptions);
|
|
28
|
+
private log;
|
|
19
29
|
on<T extends EventName>(eventName: T, listener: EventListener<T>): void;
|
|
20
30
|
off<T extends EventName>(eventName: T, listener: EventListener<T>): void;
|
|
21
31
|
/**
|
|
22
32
|
* @method warmup
|
|
23
33
|
* @description Pings providers to determine the fastest one and updates the internal priority order.
|
|
24
34
|
* Useful to call on UI events like 'focus' on the CEP input.
|
|
35
|
+
* @returns {Promise<Provider[]>} The list of providers sorted by latency.
|
|
25
36
|
*/
|
|
26
|
-
warmup(): Promise<
|
|
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[];
|
|
27
45
|
private checkRateLimit;
|
|
28
46
|
lookup<T = Address>(cep: string, mapper?: (address: Address) => T): Promise<T>;
|
|
29
|
-
|
|
47
|
+
private _lookupFromProviders;
|
|
48
|
+
lookupCeps<T = Address>(ceps: string[], concurrency?: number, mapper?: (address: Address) => T): Promise<BulkCepResult<T>[]>;
|
|
30
49
|
}
|
|
31
|
-
/**
|
|
32
|
-
* @deprecated Use `new CepLookup(options).lookup(cep)` instead.
|
|
33
|
-
*/
|
|
34
|
-
export declare function lookupCep<T = Address>(options: CepLookupOptions & {
|
|
35
|
-
cep: string;
|
|
36
|
-
mapper?: (address: Address) => T;
|
|
37
|
-
}): Promise<T>;
|
|
38
|
-
/**
|
|
39
|
-
* @deprecated Use `new CepLookup(options).lookupCeps(ceps)` instead.
|
|
40
|
-
*/
|
|
41
|
-
export declare function lookupCeps(options: CepLookupOptions & {
|
|
42
|
-
ceps: string[];
|
|
43
|
-
concurrency?: number;
|
|
44
|
-
}): Promise<BulkCepResult[]>;
|
package/dist/src/index.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CepLookup = exports.InMemoryCache = void 0;
|
|
4
|
-
exports.lookupCep = lookupCep;
|
|
5
|
-
exports.lookupCeps = lookupCeps;
|
|
3
|
+
exports.CepLookup = exports.ProviderUnavailableError = exports.AllProvidersFailedError = exports.CepNotFoundError = exports.ProviderTimeoutError = exports.RateLimitError = exports.CepValidationError = exports.InMemoryCache = void 0;
|
|
6
4
|
const cache_1 = require("./cache");
|
|
7
5
|
Object.defineProperty(exports, "InMemoryCache", { enumerable: true, get: function () { return cache_1.InMemoryCache; } });
|
|
6
|
+
const errors_1 = require("./errors");
|
|
7
|
+
Object.defineProperty(exports, "CepValidationError", { enumerable: true, get: function () { return errors_1.CepValidationError; } });
|
|
8
|
+
Object.defineProperty(exports, "RateLimitError", { enumerable: true, get: function () { return errors_1.RateLimitError; } });
|
|
9
|
+
Object.defineProperty(exports, "ProviderTimeoutError", { enumerable: true, get: function () { return errors_1.ProviderTimeoutError; } });
|
|
10
|
+
Object.defineProperty(exports, "CepNotFoundError", { enumerable: true, get: function () { return errors_1.CepNotFoundError; } });
|
|
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; } });
|
|
13
|
+
const ddd_by_state_1 = require("./data/ddd-by-state");
|
|
8
14
|
// Minimal EventEmitter for internal use
|
|
9
15
|
class EventEmitter {
|
|
10
16
|
constructor() {
|
|
@@ -41,7 +47,7 @@ class EventEmitter {
|
|
|
41
47
|
function validateCep(cep) {
|
|
42
48
|
const cepRegex = /^(\d{8}|\d{5}-\d{3})$/;
|
|
43
49
|
if (!cepRegex.test(cep)) {
|
|
44
|
-
throw new
|
|
50
|
+
throw new errors_1.CepValidationError(cep);
|
|
45
51
|
}
|
|
46
52
|
return cep.replace("-", "");
|
|
47
53
|
}
|
|
@@ -61,6 +67,19 @@ function sanitizeAddress(address) {
|
|
|
61
67
|
});
|
|
62
68
|
return sanitized;
|
|
63
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* @function enrichAddress
|
|
72
|
+
* @description Enriches an address with DDD fallback when the provider doesn't return it.
|
|
73
|
+
*/
|
|
74
|
+
function enrichAddress(address) {
|
|
75
|
+
if (!address.ddd && address.state) {
|
|
76
|
+
const fallbackDdd = ddd_by_state_1.dddByState[address.state];
|
|
77
|
+
if (fallbackDdd) {
|
|
78
|
+
return { ...address, ddd: fallbackDdd };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return address;
|
|
82
|
+
}
|
|
64
83
|
/**
|
|
65
84
|
* @class CepLookup
|
|
66
85
|
* @description A class for looking up Brazilian postal codes (CEPs) using multiple providers.
|
|
@@ -68,6 +87,7 @@ function sanitizeAddress(address) {
|
|
|
68
87
|
class CepLookup {
|
|
69
88
|
constructor(options) {
|
|
70
89
|
this.requestTimestamps = [];
|
|
90
|
+
this.providerState = new Map();
|
|
71
91
|
this.providers = options.providers;
|
|
72
92
|
this.sortedProviders = [...options.providers];
|
|
73
93
|
this.emitter = new EventEmitter();
|
|
@@ -81,6 +101,26 @@ class CepLookup {
|
|
|
81
101
|
this.cache = options.cache;
|
|
82
102
|
this.rateLimit = options.rateLimit;
|
|
83
103
|
this.staggerDelay = options.staggerDelay ?? 100;
|
|
104
|
+
this.retries = options.retries ?? 0;
|
|
105
|
+
this.retryDelay = options.retryDelay ?? 1000;
|
|
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
|
+
});
|
|
121
|
+
}
|
|
122
|
+
log(msg, data) {
|
|
123
|
+
this.logger?.debug(msg, data);
|
|
84
124
|
}
|
|
85
125
|
on(eventName, listener) {
|
|
86
126
|
this.emitter.on(eventName, listener);
|
|
@@ -92,6 +132,7 @@ class CepLookup {
|
|
|
92
132
|
* @method warmup
|
|
93
133
|
* @description Pings providers to determine the fastest one and updates the internal priority order.
|
|
94
134
|
* Useful to call on UI events like 'focus' on the CEP input.
|
|
135
|
+
* @returns {Promise<Provider[]>} The list of providers sorted by latency.
|
|
95
136
|
*/
|
|
96
137
|
async warmup() {
|
|
97
138
|
const controlCep = "01001000"; // Praça da Sé (Fixed Valid CEP)
|
|
@@ -117,6 +158,101 @@ class CepLookup {
|
|
|
117
158
|
.filter(p => !!p);
|
|
118
159
|
// Abort any lingering requests (though we awaited all)
|
|
119
160
|
controller.abort();
|
|
161
|
+
return this.sortedProviders;
|
|
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
|
+
});
|
|
120
256
|
}
|
|
121
257
|
checkRateLimit() {
|
|
122
258
|
if (!this.rateLimit)
|
|
@@ -125,33 +261,67 @@ class CepLookup {
|
|
|
125
261
|
const windowStart = now - this.rateLimit.per;
|
|
126
262
|
this.requestTimestamps = this.requestTimestamps.filter((ts) => ts > windowStart);
|
|
127
263
|
if (this.requestTimestamps.length >= this.rateLimit.requests) {
|
|
128
|
-
throw new
|
|
264
|
+
throw new errors_1.RateLimitError(this.rateLimit.requests, this.rateLimit.per);
|
|
129
265
|
}
|
|
130
266
|
this.requestTimestamps.push(now);
|
|
131
267
|
}
|
|
132
268
|
async lookup(cep, mapper) {
|
|
133
269
|
this.checkRateLimit();
|
|
134
270
|
const cleanedCep = validateCep(cep);
|
|
271
|
+
this.log('lookup:start', { cep: cleanedCep });
|
|
135
272
|
if (this.cache) {
|
|
136
273
|
const cachedAddress = this.cache.get(cleanedCep);
|
|
137
274
|
if (cachedAddress) {
|
|
275
|
+
this.log('cache:hit', { cep: cleanedCep });
|
|
138
276
|
this.emitter.emit('cache:hit', { cep: cleanedCep });
|
|
139
277
|
return mapper ? mapper(cachedAddress) : cachedAddress;
|
|
140
278
|
}
|
|
141
279
|
}
|
|
280
|
+
let lastError;
|
|
281
|
+
const maxAttempts = 1 + this.retries;
|
|
282
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
283
|
+
if (attempt > 0) {
|
|
284
|
+
const delay = this.retryDelay * Math.pow(2, attempt - 1);
|
|
285
|
+
this.log('retry:attempt', { attempt, cep: cleanedCep, delay });
|
|
286
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
return await this._lookupFromProviders(cleanedCep, mapper);
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
if (error instanceof errors_1.CepValidationError || error instanceof errors_1.RateLimitError) {
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
lastError = error;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
throw lastError;
|
|
299
|
+
}
|
|
300
|
+
async _lookupFromProviders(cleanedCep, mapper) {
|
|
142
301
|
const controller = new AbortController();
|
|
143
302
|
const { signal } = controller;
|
|
144
|
-
|
|
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
|
+
}
|
|
145
312
|
const createProviderPromise = (provider) => {
|
|
146
313
|
const startTime = Date.now();
|
|
147
314
|
const url = provider.buildUrl(cleanedCep);
|
|
315
|
+
this.log('provider:start', { provider: provider.name, cep: cleanedCep });
|
|
148
316
|
const timeoutPromise = new Promise((_, reject) => {
|
|
149
317
|
if (!provider.timeout)
|
|
150
318
|
return;
|
|
151
319
|
const timeoutId = setTimeout(() => {
|
|
152
320
|
signal.removeEventListener('abort', onAbort);
|
|
153
321
|
const duration = Date.now() - startTime;
|
|
154
|
-
const error = new
|
|
322
|
+
const error = new errors_1.ProviderTimeoutError(provider.name, provider.timeout);
|
|
323
|
+
this.recordProviderFailure(provider.name, duration, error);
|
|
324
|
+
this.log('provider:failure', { provider: provider.name, cep: cleanedCep, error: error.message });
|
|
155
325
|
this.emitter.emit('failure', { provider: provider.name, cep: cleanedCep, duration, error });
|
|
156
326
|
reject(error);
|
|
157
327
|
}, provider.timeout);
|
|
@@ -162,7 +332,9 @@ class CepLookup {
|
|
|
162
332
|
.then((response) => provider.transform(response))
|
|
163
333
|
.then((address) => {
|
|
164
334
|
const duration = Date.now() - startTime;
|
|
165
|
-
const sanitizedAddress = sanitizeAddress(address);
|
|
335
|
+
const sanitizedAddress = enrichAddress(sanitizeAddress(address));
|
|
336
|
+
this.recordProviderSuccess(provider.name, duration);
|
|
337
|
+
this.log('provider:success', { provider: provider.name, cep: cleanedCep, duration });
|
|
166
338
|
this.emitter.emit('success', { provider: provider.name, cep: cleanedCep, duration, address: sanitizedAddress });
|
|
167
339
|
if (this.cache) {
|
|
168
340
|
this.cache.set(cleanedCep, sanitizedAddress);
|
|
@@ -171,18 +343,18 @@ class CepLookup {
|
|
|
171
343
|
})
|
|
172
344
|
.catch((error) => {
|
|
173
345
|
const duration = Date.now() - startTime;
|
|
174
|
-
|
|
175
|
-
if (
|
|
176
|
-
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 });
|
|
177
351
|
}
|
|
178
|
-
throw
|
|
352
|
+
throw normalizedError;
|
|
179
353
|
});
|
|
180
354
|
return Promise.race([fetchPromise, timeoutPromise]);
|
|
181
355
|
};
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
const otherProviders = this.sortedProviders.slice(1);
|
|
185
|
-
// If we only have one provider, just execute it
|
|
356
|
+
const bestProvider = selectedProviders[0];
|
|
357
|
+
const otherProviders = selectedProviders.slice(1);
|
|
186
358
|
if (otherProviders.length === 0) {
|
|
187
359
|
try {
|
|
188
360
|
return await createProviderPromise(bestProvider);
|
|
@@ -191,7 +363,6 @@ class CepLookup {
|
|
|
191
363
|
controller.abort();
|
|
192
364
|
}
|
|
193
365
|
}
|
|
194
|
-
// Execute primary and manage staggering
|
|
195
366
|
let staggerTimeout = null;
|
|
196
367
|
let triggerOthers = null;
|
|
197
368
|
const secondaryPromise = new Promise((resolve, reject) => {
|
|
@@ -206,7 +377,6 @@ class CepLookup {
|
|
|
206
377
|
staggerTimeout = setTimeout(triggerOthers, this.staggerDelay);
|
|
207
378
|
});
|
|
208
379
|
const primaryPromise = createProviderPromise(bestProvider).catch((err) => {
|
|
209
|
-
// If primary fails, trigger others immediately
|
|
210
380
|
if (triggerOthers)
|
|
211
381
|
triggerOthers();
|
|
212
382
|
throw err;
|
|
@@ -214,13 +384,17 @@ class CepLookup {
|
|
|
214
384
|
try {
|
|
215
385
|
return await Promise.any([primaryPromise, secondaryPromise]);
|
|
216
386
|
}
|
|
387
|
+
catch (aggregateError) {
|
|
388
|
+
const errors = aggregateError.errors || [aggregateError];
|
|
389
|
+
throw new errors_1.AllProvidersFailedError(errors);
|
|
390
|
+
}
|
|
217
391
|
finally {
|
|
218
392
|
if (staggerTimeout)
|
|
219
393
|
clearTimeout(staggerTimeout);
|
|
220
394
|
controller.abort();
|
|
221
395
|
}
|
|
222
396
|
}
|
|
223
|
-
async lookupCeps(ceps, concurrency = 5) {
|
|
397
|
+
async lookupCeps(ceps, concurrency = 5, mapper) {
|
|
224
398
|
if (!ceps || ceps.length === 0) {
|
|
225
399
|
return [];
|
|
226
400
|
}
|
|
@@ -235,7 +409,11 @@ class CepLookup {
|
|
|
235
409
|
try {
|
|
236
410
|
const address = await this.lookup(cep);
|
|
237
411
|
if (address) {
|
|
238
|
-
results[currentIndex] = {
|
|
412
|
+
results[currentIndex] = {
|
|
413
|
+
cep,
|
|
414
|
+
data: mapper ? mapper(address) : address,
|
|
415
|
+
provider: address.service,
|
|
416
|
+
};
|
|
239
417
|
}
|
|
240
418
|
else {
|
|
241
419
|
throw new Error('No address found');
|
|
@@ -252,21 +430,3 @@ class CepLookup {
|
|
|
252
430
|
}
|
|
253
431
|
}
|
|
254
432
|
exports.CepLookup = CepLookup;
|
|
255
|
-
/**
|
|
256
|
-
* @deprecated Use `new CepLookup(options).lookup(cep)` instead.
|
|
257
|
-
*/
|
|
258
|
-
function lookupCep(options) {
|
|
259
|
-
console.warn("[cep-lookup] The standalone `lookupCep` function is deprecated and will be removed in a future version. Please use `new CepLookup(options).lookup(cep)` instead.");
|
|
260
|
-
const { cep, providers, fetcher, mapper, cache, rateLimit } = options;
|
|
261
|
-
const cepLookup = new CepLookup({ providers, fetcher, cache, rateLimit });
|
|
262
|
-
return cepLookup.lookup(cep, mapper);
|
|
263
|
-
}
|
|
264
|
-
/**
|
|
265
|
-
* @deprecated Use `new CepLookup(options).lookupCeps(ceps)` instead.
|
|
266
|
-
*/
|
|
267
|
-
async function lookupCeps(options) {
|
|
268
|
-
console.warn("[cep-lookup] The standalone `lookupCeps` function is deprecated and will be removed in a future version. Please use `new CepLookup(options).lookupCeps(ceps)` instead.");
|
|
269
|
-
const { ceps, providers, fetcher, cache, concurrency = 5, rateLimit } = options;
|
|
270
|
-
const cepLookup = new CepLookup({ providers, fetcher, cache, rateLimit });
|
|
271
|
-
return cepLookup.lookupCeps(ceps, concurrency);
|
|
272
|
-
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Provider } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* @const {Provider} openCepProvider
|
|
4
|
+
* @description Provider for the OpenCEP service.
|
|
5
|
+
* @property {string} name - "OpenCEP".
|
|
6
|
+
* @property {(cep: string) => string} buildUrl - Constructs the URL for OpenCEP API.
|
|
7
|
+
* @property {(response: any) => Address} transform - Transforms OpenCEP's response into a standardized `Address` object.
|
|
8
|
+
* @throws {Error} If OpenCEP response indicates an error.
|
|
9
|
+
*/
|
|
10
|
+
export declare const openCepProvider: Provider;
|