@allanfsouza/aether-sdk 2.4.7 → 2.4.9
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/dist/http-client.d.ts +24 -6
- package/dist/http-client.js +149 -27
- package/package.json +1 -1
- package/src/http-client.ts +215 -31
package/dist/http-client.d.ts
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
|
-
import { type AxiosInstance } from "axios";
|
|
1
|
+
import { type AxiosInstance, type AxiosError, type AxiosRequestConfig } from "axios";
|
|
2
2
|
import type { PlataformaClient } from "./index.js";
|
|
3
|
+
export interface RetryConfig {
|
|
4
|
+
/** Número máximo de tentativas (padrão: 3) */
|
|
5
|
+
maxRetries: number;
|
|
6
|
+
/** Delay base em ms para exponential backoff (padrão: 1000) */
|
|
7
|
+
baseDelay: number;
|
|
8
|
+
/** Delay máximo em ms (padrão: 30000) */
|
|
9
|
+
maxDelay: number;
|
|
10
|
+
/** Status codes que devem ser retried (padrão: [408, 429, 500, 502, 503, 504]) */
|
|
11
|
+
retryableStatuses: number[];
|
|
12
|
+
/** Métodos HTTP que podem ser retried (padrão: GET, HEAD, OPTIONS, PUT, DELETE) */
|
|
13
|
+
retryableMethods: string[];
|
|
14
|
+
/** Habilita retry para erros de rede (padrão: true) */
|
|
15
|
+
retryOnNetworkError: boolean;
|
|
16
|
+
/** Callback chamado antes de cada retry */
|
|
17
|
+
onRetry?: (retryCount: number, error: AxiosError, config: AxiosRequestConfig) => void;
|
|
18
|
+
}
|
|
19
|
+
export declare const DEFAULT_RETRY_CONFIG: RetryConfig;
|
|
3
20
|
/**
|
|
4
|
-
* Cria uma instância do Axios pré-configurada
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
21
|
+
* Cria uma instância do Axios pré-configurada com:
|
|
22
|
+
* - Injeção automática de headers (Auth, Project)
|
|
23
|
+
* - Refresh automático de token (401)
|
|
24
|
+
* - Retry automático com exponential backoff
|
|
25
|
+
* - Tratamento de erros padronizado
|
|
8
26
|
*/
|
|
9
|
-
export declare function createHttpClient(client: PlataformaClient): AxiosInstance;
|
|
27
|
+
export declare function createHttpClient(client: PlataformaClient, retryConfig?: Partial<RetryConfig>): AxiosInstance;
|
package/dist/http-client.js
CHANGED
|
@@ -1,13 +1,88 @@
|
|
|
1
1
|
// src/http-client.ts
|
|
2
|
+
// [MELHORIA] SDK com Retry Automático e Exponential Backoff
|
|
3
|
+
// Data: 09/12/2025
|
|
2
4
|
import axios from "axios";
|
|
3
5
|
import { handleAxiosError } from "./errors.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
export const DEFAULT_RETRY_CONFIG = {
|
|
7
|
+
maxRetries: 3,
|
|
8
|
+
baseDelay: 1000,
|
|
9
|
+
maxDelay: 30000,
|
|
10
|
+
retryableStatuses: [408, 429, 500, 502, 503, 504],
|
|
11
|
+
retryableMethods: ["GET", "HEAD", "OPTIONS", "PUT", "DELETE"],
|
|
12
|
+
retryOnNetworkError: true,
|
|
13
|
+
};
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// HELPERS
|
|
16
|
+
// =============================================================================
|
|
17
|
+
/**
|
|
18
|
+
* Calcula o delay com exponential backoff + jitter
|
|
19
|
+
*/
|
|
20
|
+
function calculateDelay(retryCount, config) {
|
|
21
|
+
// Exponential: baseDelay * 2^retryCount
|
|
22
|
+
const exponentialDelay = config.baseDelay * Math.pow(2, retryCount);
|
|
23
|
+
// Adiciona jitter (±25%) para evitar thundering herd
|
|
24
|
+
const jitter = exponentialDelay * 0.25 * (Math.random() * 2 - 1);
|
|
25
|
+
// Limita ao maxDelay
|
|
26
|
+
return Math.min(exponentialDelay + jitter, config.maxDelay);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Verifica se o erro é retryable
|
|
30
|
+
*/
|
|
31
|
+
function isRetryable(error, config) {
|
|
32
|
+
// Erro de rede (sem response)
|
|
33
|
+
if (!error.response && config.retryOnNetworkError) {
|
|
34
|
+
// Verifica se é erro de rede real (não cancelamento)
|
|
35
|
+
if (error.code === "ECONNABORTED" || error.code === "ETIMEDOUT" ||
|
|
36
|
+
error.code === "ENOTFOUND" || error.code === "ENETUNREACH" ||
|
|
37
|
+
error.message === "Network Error") {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Verifica status code
|
|
42
|
+
const status = error.response?.status;
|
|
43
|
+
if (status && config.retryableStatuses.includes(status)) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Verifica se o método HTTP pode ser retried
|
|
50
|
+
*/
|
|
51
|
+
function isMethodRetryable(method, config) {
|
|
52
|
+
if (!method)
|
|
53
|
+
return false;
|
|
54
|
+
return config.retryableMethods.includes(method.toUpperCase());
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Extrai Retry-After header (em ms)
|
|
58
|
+
*/
|
|
59
|
+
function getRetryAfterMs(error) {
|
|
60
|
+
const retryAfter = error.response?.headers?.["retry-after"];
|
|
61
|
+
if (!retryAfter)
|
|
62
|
+
return null;
|
|
63
|
+
// Se for número, é segundos
|
|
64
|
+
const seconds = parseInt(retryAfter, 10);
|
|
65
|
+
if (!isNaN(seconds)) {
|
|
66
|
+
return seconds * 1000;
|
|
67
|
+
}
|
|
68
|
+
// Se for data HTTP, calcula diferença
|
|
69
|
+
const date = new Date(retryAfter);
|
|
70
|
+
if (!isNaN(date.getTime())) {
|
|
71
|
+
return Math.max(0, date.getTime() - Date.now());
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
8
75
|
/**
|
|
9
|
-
*
|
|
76
|
+
* Aguarda um tempo determinado
|
|
10
77
|
*/
|
|
78
|
+
function sleep(ms) {
|
|
79
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
80
|
+
}
|
|
81
|
+
// =============================================================================
|
|
82
|
+
// FLAGS GLOBAIS (Refresh Token)
|
|
83
|
+
// =============================================================================
|
|
84
|
+
let isRefreshing = false;
|
|
85
|
+
let failedQueue = [];
|
|
11
86
|
const processQueue = (error, token = null) => {
|
|
12
87
|
failedQueue.forEach((prom) => {
|
|
13
88
|
if (error) {
|
|
@@ -19,15 +94,21 @@ const processQueue = (error, token = null) => {
|
|
|
19
94
|
});
|
|
20
95
|
failedQueue = [];
|
|
21
96
|
};
|
|
97
|
+
// =============================================================================
|
|
98
|
+
// CRIAR HTTP CLIENT
|
|
99
|
+
// =============================================================================
|
|
22
100
|
/**
|
|
23
|
-
* Cria uma instância do Axios pré-configurada
|
|
24
|
-
* -
|
|
25
|
-
* -
|
|
26
|
-
* -
|
|
101
|
+
* Cria uma instância do Axios pré-configurada com:
|
|
102
|
+
* - Injeção automática de headers (Auth, Project)
|
|
103
|
+
* - Refresh automático de token (401)
|
|
104
|
+
* - Retry automático com exponential backoff
|
|
105
|
+
* - Tratamento de erros padronizado
|
|
27
106
|
*/
|
|
28
|
-
export function createHttpClient(client) {
|
|
107
|
+
export function createHttpClient(client, retryConfig = {}) {
|
|
108
|
+
const config = { ...DEFAULT_RETRY_CONFIG, ...retryConfig };
|
|
29
109
|
const http = axios.create({
|
|
30
110
|
baseURL: `${client.apiUrl}/v1`,
|
|
111
|
+
timeout: 30000, // 30 segundos
|
|
31
112
|
headers: {
|
|
32
113
|
"Content-Type": "application/json",
|
|
33
114
|
},
|
|
@@ -36,37 +117,84 @@ export function createHttpClient(client) {
|
|
|
36
117
|
// INTERCEPTOR DE REQUEST
|
|
37
118
|
// Injeta token e projectId em todas as requisições
|
|
38
119
|
// ===========================================================================
|
|
39
|
-
http.interceptors.request.use((
|
|
120
|
+
http.interceptors.request.use((reqConfig) => {
|
|
40
121
|
const token = client.getToken();
|
|
41
122
|
if (token) {
|
|
42
|
-
|
|
123
|
+
reqConfig.headers.Authorization = `Bearer ${token}`;
|
|
43
124
|
}
|
|
44
125
|
if (client.projectId) {
|
|
45
|
-
|
|
126
|
+
reqConfig.headers["X-Project-ID"] = client.projectId;
|
|
127
|
+
}
|
|
128
|
+
// Inicializa contador de retry
|
|
129
|
+
if (reqConfig._retryCount === undefined) {
|
|
130
|
+
reqConfig._retryCount = 0;
|
|
46
131
|
}
|
|
47
|
-
return
|
|
132
|
+
return reqConfig;
|
|
48
133
|
});
|
|
49
134
|
// ===========================================================================
|
|
50
135
|
// INTERCEPTOR DE RESPONSE
|
|
51
|
-
//
|
|
136
|
+
// 1. Retry automático para erros de rede/servidor
|
|
137
|
+
// 2. Refresh automático de token (401)
|
|
52
138
|
// ===========================================================================
|
|
53
139
|
http.interceptors.response.use((response) => response, async (error) => {
|
|
54
140
|
const originalRequest = error.config;
|
|
55
|
-
|
|
141
|
+
if (!originalRequest) {
|
|
142
|
+
return handleAxiosError(error);
|
|
143
|
+
}
|
|
144
|
+
// -----------------------------------------------------------------
|
|
145
|
+
// PARTE 1: RETRY AUTOMÁTICO (erros de rede e servidor)
|
|
146
|
+
// -----------------------------------------------------------------
|
|
147
|
+
const retryCount = originalRequest._retryCount || 0;
|
|
148
|
+
const canRetry = retryCount < config.maxRetries;
|
|
149
|
+
const isRetryableError = isRetryable(error, config);
|
|
150
|
+
const isRetryableMethod = isMethodRetryable(originalRequest.method, config);
|
|
151
|
+
// Não faz retry para 401 (tratado pelo refresh token)
|
|
152
|
+
const is401 = error.response?.status === 401;
|
|
153
|
+
if (canRetry && isRetryableError && isRetryableMethod && !is401) {
|
|
154
|
+
originalRequest._retryCount = retryCount + 1;
|
|
155
|
+
// Calcula delay (respeita Retry-After se presente)
|
|
156
|
+
const delay = getRetryAfterMs(error) || calculateDelay(retryCount, config);
|
|
157
|
+
// Callback de retry (para logging/métricas)
|
|
158
|
+
if (config.onRetry) {
|
|
159
|
+
config.onRetry(originalRequest._retryCount, error, originalRequest);
|
|
160
|
+
}
|
|
161
|
+
// Emite evento para monitoramento
|
|
162
|
+
if (typeof window !== "undefined") {
|
|
163
|
+
window.dispatchEvent(new CustomEvent("aether:retry", {
|
|
164
|
+
detail: {
|
|
165
|
+
attempt: originalRequest._retryCount,
|
|
166
|
+
maxRetries: config.maxRetries,
|
|
167
|
+
delay,
|
|
168
|
+
url: originalRequest.url,
|
|
169
|
+
method: originalRequest.method,
|
|
170
|
+
status: error.response?.status,
|
|
171
|
+
error: error.message,
|
|
172
|
+
}
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
// Log em desenvolvimento
|
|
176
|
+
if (process.env.NODE_ENV === "development") {
|
|
177
|
+
console.log(`[Aether SDK] Retry ${originalRequest._retryCount}/${config.maxRetries}`, `em ${delay}ms para ${originalRequest.method} ${originalRequest.url}`, `(${error.response?.status || error.message})`);
|
|
178
|
+
}
|
|
179
|
+
// Aguarda e refaz a requisição
|
|
180
|
+
await sleep(delay);
|
|
181
|
+
return http(originalRequest);
|
|
182
|
+
}
|
|
183
|
+
// -----------------------------------------------------------------
|
|
184
|
+
// PARTE 2: REFRESH TOKEN (erro 401)
|
|
185
|
+
// -----------------------------------------------------------------
|
|
56
186
|
const isUnauthorized = error.response?.status === 401;
|
|
57
|
-
const isRetry = originalRequest
|
|
58
|
-
const isAuthRoute = originalRequest
|
|
187
|
+
const isRetry = originalRequest._retry;
|
|
188
|
+
const isAuthRoute = originalRequest.url?.includes("/auth/");
|
|
59
189
|
// Não tenta refresh se:
|
|
60
190
|
// - Não é 401
|
|
61
|
-
// - Já é um retry
|
|
191
|
+
// - Já é um retry de refresh
|
|
62
192
|
// - É uma rota de auth (login, register, refresh)
|
|
63
|
-
// - Não tem refresh token disponível
|
|
64
193
|
if (!isUnauthorized || isRetry || isAuthRoute) {
|
|
65
194
|
return handleAxiosError(error);
|
|
66
195
|
}
|
|
67
196
|
const refreshToken = client.getRefreshToken();
|
|
68
197
|
if (!refreshToken) {
|
|
69
|
-
// Sem refresh token, limpa sessão e propaga erro
|
|
70
198
|
client.clearSession();
|
|
71
199
|
return handleAxiosError(error);
|
|
72
200
|
}
|
|
@@ -85,26 +213,20 @@ export function createHttpClient(client) {
|
|
|
85
213
|
originalRequest._retry = true;
|
|
86
214
|
isRefreshing = true;
|
|
87
215
|
try {
|
|
88
|
-
// Chama endpoint de refresh diretamente (sem interceptor)
|
|
89
216
|
const { data } = await axios.post(`${client.apiUrl}/v1/auth/refresh`, { refreshToken }, { headers: { "Content-Type": "application/json" } });
|
|
90
217
|
const newAccessToken = data.accessToken;
|
|
91
218
|
const newRefreshToken = data.refreshToken;
|
|
92
|
-
// Atualiza tokens no client (persiste no localStorage)
|
|
93
219
|
client.setToken(newAccessToken);
|
|
94
220
|
if (newRefreshToken) {
|
|
95
221
|
client.setRefreshToken(newRefreshToken);
|
|
96
222
|
}
|
|
97
|
-
// Processa fila de requisições pendentes
|
|
98
223
|
processQueue(null, newAccessToken);
|
|
99
|
-
// Refaz a requisição original com novo token
|
|
100
224
|
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
|
|
101
225
|
return http(originalRequest);
|
|
102
226
|
}
|
|
103
227
|
catch (refreshError) {
|
|
104
|
-
// Refresh falhou - sessão expirada
|
|
105
228
|
processQueue(refreshError, null);
|
|
106
229
|
client.clearSession();
|
|
107
|
-
// Emite evento de sessão expirada (se houver listener)
|
|
108
230
|
if (typeof window !== "undefined") {
|
|
109
231
|
window.dispatchEvent(new CustomEvent("aether:session-expired"));
|
|
110
232
|
}
|
package/package.json
CHANGED
package/src/http-client.ts
CHANGED
|
@@ -1,24 +1,135 @@
|
|
|
1
1
|
// src/http-client.ts
|
|
2
|
+
// [MELHORIA] SDK com Retry Automático e Exponential Backoff
|
|
3
|
+
// Data: 09/12/2025
|
|
4
|
+
|
|
2
5
|
import axios, {
|
|
3
6
|
type AxiosInstance,
|
|
4
7
|
type AxiosError,
|
|
8
|
+
type AxiosRequestConfig,
|
|
5
9
|
type InternalAxiosRequestConfig,
|
|
6
10
|
} from "axios";
|
|
7
11
|
import type { PlataformaClient } from "./index.js";
|
|
8
12
|
import { handleAxiosError } from "./errors.js";
|
|
9
13
|
|
|
10
|
-
//
|
|
11
|
-
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// CONFIGURAÇÕES DE RETRY
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
export interface RetryConfig {
|
|
19
|
+
/** Número máximo de tentativas (padrão: 3) */
|
|
20
|
+
maxRetries: number;
|
|
21
|
+
/** Delay base em ms para exponential backoff (padrão: 1000) */
|
|
22
|
+
baseDelay: number;
|
|
23
|
+
/** Delay máximo em ms (padrão: 30000) */
|
|
24
|
+
maxDelay: number;
|
|
25
|
+
/** Status codes que devem ser retried (padrão: [408, 429, 500, 502, 503, 504]) */
|
|
26
|
+
retryableStatuses: number[];
|
|
27
|
+
/** Métodos HTTP que podem ser retried (padrão: GET, HEAD, OPTIONS, PUT, DELETE) */
|
|
28
|
+
retryableMethods: string[];
|
|
29
|
+
/** Habilita retry para erros de rede (padrão: true) */
|
|
30
|
+
retryOnNetworkError: boolean;
|
|
31
|
+
/** Callback chamado antes de cada retry */
|
|
32
|
+
onRetry?: (retryCount: number, error: AxiosError, config: AxiosRequestConfig) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const DEFAULT_RETRY_CONFIG: RetryConfig = {
|
|
36
|
+
maxRetries: 3,
|
|
37
|
+
baseDelay: 1000,
|
|
38
|
+
maxDelay: 30000,
|
|
39
|
+
retryableStatuses: [408, 429, 500, 502, 503, 504],
|
|
40
|
+
retryableMethods: ["GET", "HEAD", "OPTIONS", "PUT", "DELETE"],
|
|
41
|
+
retryOnNetworkError: true,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// HELPERS
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Calcula o delay com exponential backoff + jitter
|
|
50
|
+
*/
|
|
51
|
+
function calculateDelay(retryCount: number, config: RetryConfig): number {
|
|
52
|
+
// Exponential: baseDelay * 2^retryCount
|
|
53
|
+
const exponentialDelay = config.baseDelay * Math.pow(2, retryCount);
|
|
54
|
+
|
|
55
|
+
// Adiciona jitter (±25%) para evitar thundering herd
|
|
56
|
+
const jitter = exponentialDelay * 0.25 * (Math.random() * 2 - 1);
|
|
57
|
+
|
|
58
|
+
// Limita ao maxDelay
|
|
59
|
+
return Math.min(exponentialDelay + jitter, config.maxDelay);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Verifica se o erro é retryable
|
|
64
|
+
*/
|
|
65
|
+
function isRetryable(error: AxiosError, config: RetryConfig): boolean {
|
|
66
|
+
// Erro de rede (sem response)
|
|
67
|
+
if (!error.response && config.retryOnNetworkError) {
|
|
68
|
+
// Verifica se é erro de rede real (não cancelamento)
|
|
69
|
+
if (error.code === "ECONNABORTED" || error.code === "ETIMEDOUT" ||
|
|
70
|
+
error.code === "ENOTFOUND" || error.code === "ENETUNREACH" ||
|
|
71
|
+
error.message === "Network Error") {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
12
75
|
|
|
13
|
-
//
|
|
76
|
+
// Verifica status code
|
|
77
|
+
const status = error.response?.status;
|
|
78
|
+
if (status && config.retryableStatuses.includes(status)) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Verifica se o método HTTP pode ser retried
|
|
87
|
+
*/
|
|
88
|
+
function isMethodRetryable(method: string | undefined, config: RetryConfig): boolean {
|
|
89
|
+
if (!method) return false;
|
|
90
|
+
return config.retryableMethods.includes(method.toUpperCase());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Extrai Retry-After header (em ms)
|
|
95
|
+
*/
|
|
96
|
+
function getRetryAfterMs(error: AxiosError): number | null {
|
|
97
|
+
const retryAfter = error.response?.headers?.["retry-after"];
|
|
98
|
+
|
|
99
|
+
if (!retryAfter) return null;
|
|
100
|
+
|
|
101
|
+
// Se for número, é segundos
|
|
102
|
+
const seconds = parseInt(retryAfter, 10);
|
|
103
|
+
if (!isNaN(seconds)) {
|
|
104
|
+
return seconds * 1000;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Se for data HTTP, calcula diferença
|
|
108
|
+
const date = new Date(retryAfter);
|
|
109
|
+
if (!isNaN(date.getTime())) {
|
|
110
|
+
return Math.max(0, date.getTime() - Date.now());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Aguarda um tempo determinado
|
|
118
|
+
*/
|
|
119
|
+
function sleep(ms: number): Promise<void> {
|
|
120
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// =============================================================================
|
|
124
|
+
// FLAGS GLOBAIS (Refresh Token)
|
|
125
|
+
// =============================================================================
|
|
126
|
+
|
|
127
|
+
let isRefreshing = false;
|
|
14
128
|
let failedQueue: Array<{
|
|
15
129
|
resolve: (token: string) => void;
|
|
16
130
|
reject: (error: any) => void;
|
|
17
131
|
}> = [];
|
|
18
132
|
|
|
19
|
-
/**
|
|
20
|
-
* Processa a fila de requisições após o refresh.
|
|
21
|
-
*/
|
|
22
133
|
const processQueue = (error: any, token: string | null = null) => {
|
|
23
134
|
failedQueue.forEach((prom) => {
|
|
24
135
|
if (error) {
|
|
@@ -30,15 +141,35 @@ const processQueue = (error: any, token: string | null = null) => {
|
|
|
30
141
|
failedQueue = [];
|
|
31
142
|
};
|
|
32
143
|
|
|
144
|
+
// =============================================================================
|
|
145
|
+
// EXTENDED CONFIG TYPE
|
|
146
|
+
// =============================================================================
|
|
147
|
+
|
|
148
|
+
interface ExtendedAxiosRequestConfig extends InternalAxiosRequestConfig {
|
|
149
|
+
_retry?: boolean;
|
|
150
|
+
_retryCount?: number;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// =============================================================================
|
|
154
|
+
// CRIAR HTTP CLIENT
|
|
155
|
+
// =============================================================================
|
|
156
|
+
|
|
33
157
|
/**
|
|
34
|
-
* Cria uma instância do Axios pré-configurada
|
|
35
|
-
* -
|
|
36
|
-
* -
|
|
37
|
-
* -
|
|
158
|
+
* Cria uma instância do Axios pré-configurada com:
|
|
159
|
+
* - Injeção automática de headers (Auth, Project)
|
|
160
|
+
* - Refresh automático de token (401)
|
|
161
|
+
* - Retry automático com exponential backoff
|
|
162
|
+
* - Tratamento de erros padronizado
|
|
38
163
|
*/
|
|
39
|
-
export function createHttpClient(
|
|
164
|
+
export function createHttpClient(
|
|
165
|
+
client: PlataformaClient,
|
|
166
|
+
retryConfig: Partial<RetryConfig> = {}
|
|
167
|
+
): AxiosInstance {
|
|
168
|
+
const config: RetryConfig = { ...DEFAULT_RETRY_CONFIG, ...retryConfig };
|
|
169
|
+
|
|
40
170
|
const http = axios.create({
|
|
41
171
|
baseURL: `${client.apiUrl}/v1`,
|
|
172
|
+
timeout: 30000, // 30 segundos
|
|
42
173
|
headers: {
|
|
43
174
|
"Content-Type": "application/json",
|
|
44
175
|
},
|
|
@@ -48,47 +179,106 @@ export function createHttpClient(client: PlataformaClient): AxiosInstance {
|
|
|
48
179
|
// INTERCEPTOR DE REQUEST
|
|
49
180
|
// Injeta token e projectId em todas as requisições
|
|
50
181
|
// ===========================================================================
|
|
51
|
-
http.interceptors.request.use((
|
|
182
|
+
http.interceptors.request.use((reqConfig) => {
|
|
52
183
|
const token = client.getToken();
|
|
53
184
|
if (token) {
|
|
54
|
-
|
|
185
|
+
reqConfig.headers.Authorization = `Bearer ${token}`;
|
|
55
186
|
}
|
|
56
187
|
|
|
57
188
|
if (client.projectId) {
|
|
58
|
-
|
|
189
|
+
reqConfig.headers["X-Project-ID"] = client.projectId;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Inicializa contador de retry
|
|
193
|
+
if ((reqConfig as ExtendedAxiosRequestConfig)._retryCount === undefined) {
|
|
194
|
+
(reqConfig as ExtendedAxiosRequestConfig)._retryCount = 0;
|
|
59
195
|
}
|
|
60
196
|
|
|
61
|
-
return
|
|
197
|
+
return reqConfig;
|
|
62
198
|
});
|
|
63
199
|
|
|
64
200
|
// ===========================================================================
|
|
65
201
|
// INTERCEPTOR DE RESPONSE
|
|
66
|
-
//
|
|
202
|
+
// 1. Retry automático para erros de rede/servidor
|
|
203
|
+
// 2. Refresh automático de token (401)
|
|
67
204
|
// ===========================================================================
|
|
68
205
|
http.interceptors.response.use(
|
|
69
206
|
(response) => response,
|
|
70
207
|
async (error: AxiosError) => {
|
|
71
|
-
const originalRequest = error.config as
|
|
72
|
-
|
|
73
|
-
|
|
208
|
+
const originalRequest = error.config as ExtendedAxiosRequestConfig;
|
|
209
|
+
|
|
210
|
+
if (!originalRequest) {
|
|
211
|
+
return handleAxiosError(error);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// -----------------------------------------------------------------
|
|
215
|
+
// PARTE 1: RETRY AUTOMÁTICO (erros de rede e servidor)
|
|
216
|
+
// -----------------------------------------------------------------
|
|
217
|
+
const retryCount = originalRequest._retryCount || 0;
|
|
218
|
+
const canRetry = retryCount < config.maxRetries;
|
|
219
|
+
const isRetryableError = isRetryable(error, config);
|
|
220
|
+
const isRetryableMethod = isMethodRetryable(originalRequest.method, config);
|
|
221
|
+
|
|
222
|
+
// Não faz retry para 401 (tratado pelo refresh token)
|
|
223
|
+
const is401 = error.response?.status === 401;
|
|
224
|
+
|
|
225
|
+
if (canRetry && isRetryableError && isRetryableMethod && !is401) {
|
|
226
|
+
originalRequest._retryCount = retryCount + 1;
|
|
227
|
+
|
|
228
|
+
// Calcula delay (respeita Retry-After se presente)
|
|
229
|
+
const delay = getRetryAfterMs(error) || calculateDelay(retryCount, config);
|
|
230
|
+
|
|
231
|
+
// Callback de retry (para logging/métricas)
|
|
232
|
+
if (config.onRetry) {
|
|
233
|
+
config.onRetry(originalRequest._retryCount, error, originalRequest);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Emite evento para monitoramento
|
|
237
|
+
if (typeof window !== "undefined") {
|
|
238
|
+
window.dispatchEvent(new CustomEvent("aether:retry", {
|
|
239
|
+
detail: {
|
|
240
|
+
attempt: originalRequest._retryCount,
|
|
241
|
+
maxRetries: config.maxRetries,
|
|
242
|
+
delay,
|
|
243
|
+
url: originalRequest.url,
|
|
244
|
+
method: originalRequest.method,
|
|
245
|
+
status: error.response?.status,
|
|
246
|
+
error: error.message,
|
|
247
|
+
}
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Log em desenvolvimento
|
|
252
|
+
if (process.env.NODE_ENV === "development") {
|
|
253
|
+
console.log(
|
|
254
|
+
`[Aether SDK] Retry ${originalRequest._retryCount}/${config.maxRetries}`,
|
|
255
|
+
`em ${delay}ms para ${originalRequest.method} ${originalRequest.url}`,
|
|
256
|
+
`(${error.response?.status || error.message})`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Aguarda e refaz a requisição
|
|
261
|
+
await sleep(delay);
|
|
262
|
+
return http(originalRequest);
|
|
263
|
+
}
|
|
74
264
|
|
|
75
|
-
//
|
|
265
|
+
// -----------------------------------------------------------------
|
|
266
|
+
// PARTE 2: REFRESH TOKEN (erro 401)
|
|
267
|
+
// -----------------------------------------------------------------
|
|
76
268
|
const isUnauthorized = error.response?.status === 401;
|
|
77
|
-
const isRetry = originalRequest
|
|
78
|
-
const isAuthRoute = originalRequest
|
|
269
|
+
const isRetry = originalRequest._retry;
|
|
270
|
+
const isAuthRoute = originalRequest.url?.includes("/auth/");
|
|
79
271
|
|
|
80
272
|
// Não tenta refresh se:
|
|
81
273
|
// - Não é 401
|
|
82
|
-
// - Já é um retry
|
|
274
|
+
// - Já é um retry de refresh
|
|
83
275
|
// - É uma rota de auth (login, register, refresh)
|
|
84
|
-
// - Não tem refresh token disponível
|
|
85
276
|
if (!isUnauthorized || isRetry || isAuthRoute) {
|
|
86
277
|
return handleAxiosError(error);
|
|
87
278
|
}
|
|
88
279
|
|
|
89
280
|
const refreshToken = client.getRefreshToken();
|
|
90
281
|
if (!refreshToken) {
|
|
91
|
-
// Sem refresh token, limpa sessão e propaga erro
|
|
92
282
|
client.clearSession();
|
|
93
283
|
return handleAxiosError(error);
|
|
94
284
|
}
|
|
@@ -110,7 +300,6 @@ export function createHttpClient(client: PlataformaClient): AxiosInstance {
|
|
|
110
300
|
isRefreshing = true;
|
|
111
301
|
|
|
112
302
|
try {
|
|
113
|
-
// Chama endpoint de refresh diretamente (sem interceptor)
|
|
114
303
|
const { data } = await axios.post(
|
|
115
304
|
`${client.apiUrl}/v1/auth/refresh`,
|
|
116
305
|
{ refreshToken },
|
|
@@ -120,24 +309,19 @@ export function createHttpClient(client: PlataformaClient): AxiosInstance {
|
|
|
120
309
|
const newAccessToken = data.accessToken;
|
|
121
310
|
const newRefreshToken = data.refreshToken;
|
|
122
311
|
|
|
123
|
-
// Atualiza tokens no client (persiste no localStorage)
|
|
124
312
|
client.setToken(newAccessToken);
|
|
125
313
|
if (newRefreshToken) {
|
|
126
314
|
client.setRefreshToken(newRefreshToken);
|
|
127
315
|
}
|
|
128
316
|
|
|
129
|
-
// Processa fila de requisições pendentes
|
|
130
317
|
processQueue(null, newAccessToken);
|
|
131
318
|
|
|
132
|
-
// Refaz a requisição original com novo token
|
|
133
319
|
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
|
|
134
320
|
return http(originalRequest);
|
|
135
321
|
} catch (refreshError: any) {
|
|
136
|
-
// Refresh falhou - sessão expirada
|
|
137
322
|
processQueue(refreshError, null);
|
|
138
323
|
client.clearSession();
|
|
139
324
|
|
|
140
|
-
// Emite evento de sessão expirada (se houver listener)
|
|
141
325
|
if (typeof window !== "undefined") {
|
|
142
326
|
window.dispatchEvent(new CustomEvent("aether:session-expired"));
|
|
143
327
|
}
|