@rebound-dlq/node 0.2.2 → 0.2.4
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 +250 -51
- package/dist/adapters/base.adapter.js +1 -1
- package/dist/core/client.d.ts +4 -0
- package/dist/core/client.js +53 -10
- package/dist/core/constants.d.ts +1 -4
- package/dist/core/constants.js +2 -5
- package/dist/core/endpoint.d.ts +2 -0
- package/dist/core/endpoint.js +19 -0
- package/dist/core/queue.d.ts +24 -1
- package/dist/core/queue.js +349 -37
- package/dist/core/wrapper.js +3 -6
- package/dist/models/payload.d.ts +1 -1
- package/package.json +23 -18
package/README.md
CHANGED
|
@@ -3,66 +3,149 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@rebound-dlq/node)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
O **@rebound-dlq/node**
|
|
6
|
+
O **@rebound-dlq/node** e o SDK oficial em Node.js para integrar sua aplicacao com a plataforma **[Rebound DLQ](https://rebound-dlq.com)**.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Ele captura falhas da sua aplicacao, criptografa o payload localmente e envia o evento para a sua conta em background, sem travar a regra de negocio. A fila e mantida em memoria e o SDK ja entende os cenarios de rede instavel, limite de plano, overage e retomada automatica apos upgrade ou mudanca de configuracao da conta.
|
|
9
9
|
|
|
10
10
|
## Por que usar o Rebound DLQ?
|
|
11
11
|
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
12
|
+
- **Zero latencia no fluxo principal**: o evento e enfileirado em memoria e a sua aplicacao segue executando sem esperar round-trip de rede.
|
|
13
|
+
- **Criptografia local AES-256**: o payload e criptografado antes de sair do seu processo.
|
|
14
|
+
- **Entrega resiliente**: falhas de rede e indisponibilidade temporaria continuam usando retry com backoff exponencial.
|
|
15
|
+
- **Comportamento orientado ao plano da conta**: o SDK sabe quando deve continuar, quando deve aguardar e quando deve descartar eventos acima do limite.
|
|
16
|
+
- **Retomada automatica**: quando a conta faz upgrade ou libera overage, o SDK volta a enviar sozinho.
|
|
15
17
|
|
|
16
|
-
>
|
|
18
|
+
> Importante: este SDK requer uma conta e um projeto ativos no Rebound DLQ.
|
|
17
19
|
|
|
18
|
-
##
|
|
19
|
-
|
|
20
|
-
Abra o terminal e instale o pacote via npm, yarn ou pnpm:
|
|
20
|
+
## Instalacao
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
npm install @rebound-dlq/node
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
##
|
|
26
|
+
## Requisitos
|
|
27
|
+
|
|
28
|
+
Para usar o SDK com o comportamento completo desta versao:
|
|
29
|
+
|
|
30
|
+
- uma chave valida do projeto
|
|
31
|
+
- uma API Rebound atualizada, com suporte a:
|
|
32
|
+
- `POST /dlq-injestion`
|
|
33
|
+
- `POST /dlq-injestion/runtime-state`
|
|
34
|
+
- autenticacao HMAC com `x-api-key`, `x-sdk-timestamp` e `x-sdk-signature`
|
|
35
|
+
- resposta estruturada de bloqueio por limite de plano
|
|
27
36
|
|
|
28
|
-
|
|
37
|
+
Se voce usa a plataforma oficial atualizada da Rebound, nao precisa configurar nada extra no cliente para esse fluxo. O comportamento de buffer, descarte e retomada e automatico.
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+
## Configuracao segura
|
|
40
|
+
|
|
41
|
+
Nunca exponha a chave diretamente no codigo-fonte. Use variaveis de ambiente:
|
|
31
42
|
|
|
32
43
|
```env
|
|
33
|
-
|
|
34
|
-
REBOUND_PROJECT_SECRET="sk_dev_sua_chave_secreta_aqui"
|
|
44
|
+
REBOUND_PROJECT_SECRET="seu_project_secret_aqui"
|
|
35
45
|
```
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
## Executando os examples do repositorio
|
|
48
|
+
|
|
49
|
+
Os arquivos em `examples/` foram deixados simples para teste local. Cada example tem duas variaveis no topo do arquivo:
|
|
50
|
+
|
|
51
|
+
- `PROJECT_SECRET`
|
|
52
|
+
- `API_ENDPOINT`
|
|
40
53
|
|
|
41
|
-
|
|
54
|
+
Edite esses valores diretamente no arquivo que voce quer testar e depois rode o script.
|
|
42
55
|
|
|
43
|
-
|
|
56
|
+
```bash
|
|
57
|
+
npm run example:wrapper
|
|
58
|
+
npm run example:http
|
|
59
|
+
npm run example:kafka
|
|
60
|
+
npm run example:internal
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Se preferir, voce tambem pode chamar o runner diretamente:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
node scripts/run-example.cjs wrapper
|
|
67
|
+
```
|
|
44
68
|
|
|
45
|
-
Se
|
|
69
|
+
Os exemplos encerram sozinhos apos uma pequena janela de observacao para nao ficarem presos em retries de background quando a API estiver offline. Se quiser alterar isso, use `REBOUND_EXAMPLE_GRACE_MS`.
|
|
46
70
|
|
|
47
|
-
|
|
71
|
+
### Fluxo unico de configuracao
|
|
72
|
+
|
|
73
|
+
O SDK agora trabalha com um fluxo unico:
|
|
74
|
+
|
|
75
|
+
- `projectSecret` para autenticar e derivar a criptografia local
|
|
76
|
+
- `endpoint` para definir para qual API Rebound os eventos devem ser enviados
|
|
77
|
+
|
|
78
|
+
Nao existe mais diferenca publica entre ambiente de teste e producao dentro do SDK. Se voce quiser apontar para outra API, basta informar `endpoint` explicitamente.
|
|
79
|
+
|
|
80
|
+
## Interfaces disponiveis
|
|
81
|
+
|
|
82
|
+
### 1. `DlqWrapper`
|
|
83
|
+
|
|
84
|
+
Melhor opcao para encapsular uma funcao que pode falhar.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
48
87
|
import { DlqWrapper } from '@rebound-dlq/node';
|
|
49
88
|
|
|
50
|
-
// Inicializa o SDK consumindo de variáveis de ambiente
|
|
51
89
|
const dlq = new DlqWrapper({
|
|
52
|
-
projectSecret: process.env.REBOUND_PROJECT_SECRET
|
|
90
|
+
projectSecret: process.env.REBOUND_PROJECT_SECRET!,
|
|
53
91
|
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
#### Parametros
|
|
95
|
+
|
|
96
|
+
| Campo | Obrigatorio | Descricao |
|
|
97
|
+
| --- | --- | --- |
|
|
98
|
+
| `projectSecret` | Sim | Chave secreta do projeto usada para criptografia local e autenticacao do SDK. |
|
|
99
|
+
| `endpoint` | Nao | URL base da sua API Rebound, por exemplo `https://api.suaempresa.com/api/v1`. O SDK monta `/dlq-injestion` automaticamente, mas tambem aceita a URL completa por compatibilidade. |
|
|
100
|
+
| `maxFailures` | Nao | Mantido apenas por compatibilidade retroativa. Atualmente nao altera o comportamento da fila. |
|
|
101
|
+
|
|
102
|
+
### 2. `ReboundClient`
|
|
54
103
|
|
|
55
|
-
|
|
56
|
-
|
|
104
|
+
Melhor opcao para enviar payloads DLQ manualmente ou usar os adapters.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { ReboundClient } from '@rebound-dlq/node';
|
|
108
|
+
|
|
109
|
+
const client = new ReboundClient({
|
|
110
|
+
projectSecret: process.env.REBOUND_PROJECT_SECRET!,
|
|
111
|
+
endpoint: process.env.REBOUND_API_ENDPOINT,
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### Parametros
|
|
116
|
+
|
|
117
|
+
| Campo | Obrigatorio | Descricao |
|
|
118
|
+
| --- | --- | --- |
|
|
119
|
+
| `projectSecret` | Sim | Secret do projeto criado no painel, usado para autenticar as chamadas do SDK. |
|
|
120
|
+
| `endpoint` | Nao | URL base da sua API Rebound, por exemplo `https://api.suaempresa.com/api/v1`. O SDK monta `/dlq-injestion` automaticamente, mas tambem aceita a URL completa por compatibilidade. |
|
|
121
|
+
| `timeout` | Nao | Campo reservado no tipo de configuracao. Atualmente nao altera o transporte. |
|
|
122
|
+
|
|
123
|
+
Quando voce usa `ReboundClient` ou um adapter, o SDK converte o payload manual para o contrato de ingestao da API, gera `fingerprint`, criptografa o envelope localmente e envia no mesmo formato aceito pelo backend.
|
|
124
|
+
|
|
125
|
+
## Uso recomendado com `DlqWrapper`
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { DlqWrapper } from '@rebound-dlq/node';
|
|
129
|
+
|
|
130
|
+
const dlq = new DlqWrapper({
|
|
131
|
+
projectSecret: process.env.REBOUND_PROJECT_SECRET!,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
async function processarPagamento(dados: {
|
|
135
|
+
userId: string;
|
|
136
|
+
valor: number;
|
|
137
|
+
tentativa: number;
|
|
138
|
+
modulo: string;
|
|
139
|
+
}) {
|
|
57
140
|
if (dados.valor > 1000) {
|
|
58
|
-
throw new Error('Saldo insuficiente na operadora do
|
|
141
|
+
throw new Error('Saldo insuficiente na operadora do cartao');
|
|
59
142
|
}
|
|
60
|
-
|
|
143
|
+
|
|
144
|
+
return 'Sucesso';
|
|
61
145
|
}
|
|
62
146
|
|
|
63
147
|
async function main() {
|
|
64
|
-
|
|
65
|
-
const payloadQueSeraSalvoNaDLQ = {
|
|
148
|
+
const payload = {
|
|
66
149
|
userId: '123',
|
|
67
150
|
valor: 5000,
|
|
68
151
|
tentativa: 1,
|
|
@@ -70,37 +153,153 @@ async function main() {
|
|
|
70
153
|
};
|
|
71
154
|
|
|
72
155
|
try {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// Protegemos a função com o wrapper. Se der erro dentro de processarPagamento,
|
|
76
|
-
// o dlq envia os detalhes do erro E o payloadQueSeraSalvoNaDLQ para o Rebound automatically!
|
|
77
|
-
const resultado = await dlq.execute(
|
|
78
|
-
() => processarPagamento(payloadQueSeraSalvoNaDLQ),
|
|
79
|
-
payloadQueSeraSalvoNaDLQ
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
console.log('Finalizado com:', resultado);
|
|
83
|
-
|
|
156
|
+
const resultado = await dlq.execute(() => processarPagamento(payload), payload);
|
|
157
|
+
console.log('Resultado:', resultado);
|
|
84
158
|
} catch (erro) {
|
|
85
|
-
|
|
86
|
-
console.log('Aconteceu um erro previsto na aplicação:', erro.message);
|
|
159
|
+
console.error('Erro de negocio capturado pela aplicacao:', (erro as Error).message);
|
|
87
160
|
}
|
|
88
161
|
}
|
|
89
162
|
|
|
90
163
|
main();
|
|
91
164
|
```
|
|
92
165
|
|
|
93
|
-
### O que acontece
|
|
166
|
+
### O que acontece nesse fluxo
|
|
167
|
+
|
|
168
|
+
1. Sua funcao falha e a excecao e capturada.
|
|
169
|
+
2. O payload e criptografado localmente com AES-256.
|
|
170
|
+
3. O evento e colocado em uma fila em memoria.
|
|
171
|
+
4. O SDK envia o evento em background com autenticacao HMAC.
|
|
172
|
+
5. O erro original continua sendo relancado para o seu tratamento normal.
|
|
173
|
+
|
|
174
|
+
## Uso manual com `ReboundClient`
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import { ReboundClient, HTTPAdapter } from '@rebound-dlq/node';
|
|
178
|
+
|
|
179
|
+
const client = new ReboundClient({
|
|
180
|
+
projectSecret: process.env.REBOUND_PROJECT_SECRET!,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const httpDlq = new HTTPAdapter(client);
|
|
184
|
+
|
|
185
|
+
await httpDlq.sendToDLQ({
|
|
186
|
+
payload: {
|
|
187
|
+
orderId: 'ORD-1001',
|
|
188
|
+
amount: 99.9,
|
|
189
|
+
},
|
|
190
|
+
error: new Error('Payment gateway timeout'),
|
|
191
|
+
metadata: {
|
|
192
|
+
endpoint: '/api/payments',
|
|
193
|
+
method: 'POST',
|
|
194
|
+
tenantId: 'acme',
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Comportamento de entrega
|
|
200
|
+
|
|
201
|
+
### Falha de rede ou API indisponivel
|
|
202
|
+
|
|
203
|
+
Para erros transientes, o SDK continua aplicando retry em background com backoff exponencial.
|
|
204
|
+
|
|
205
|
+
### Conta liberada para overage
|
|
206
|
+
|
|
207
|
+
Quando a conta pode cobrar uso extra, a ingestao continua normalmente e o excedente e faturado pela plataforma na fatura Stripe da conta.
|
|
208
|
+
|
|
209
|
+
### Conta bloqueada por limite com `bill_extra` desativado
|
|
210
|
+
|
|
211
|
+
Quando a conta opta por **nao exceder o limite**:
|
|
212
|
+
|
|
213
|
+
- o SDK nao insiste em retry infinito para esse caso
|
|
214
|
+
- o evento atual e descartado
|
|
215
|
+
- novos eventos tambem sao descartados enquanto a conta continuar bloqueada
|
|
216
|
+
- o SDK consulta o estado da conta periodicamente e volta a enviar sozinho quando ela se tornar elegivel de novo
|
|
217
|
+
|
|
218
|
+
Esse comportamento existe para respeitar a decisao explicita da conta de nao gerar custo extra.
|
|
219
|
+
|
|
220
|
+
### Conta bloqueada por limite, mas com escolha de `bill_extra`
|
|
221
|
+
|
|
222
|
+
Quando a conta escolheu **aceitar cobranca extra** e a plataforma ainda responde com bloqueio temporario, o SDK assume que o cliente prefere **nao perder eventos** e usa um buffer temporario:
|
|
223
|
+
|
|
224
|
+
- os eventos ficam em um `blocked buffer` em memoria por ate **15 minutos**
|
|
225
|
+
- o buffer tem limite de **250 eventos por processo e por chave**
|
|
226
|
+
- o SDK faz polling autenticado a cada **60 segundos**
|
|
227
|
+
- quando a conta volta a ficar apta, os eventos buffered sao reenfileirados e enviados em ordem FIFO
|
|
228
|
+
|
|
229
|
+
Se a conta continuar bloqueada alem da janela de buffer, os eventos buffered expiram e sao descartados com log explicito.
|
|
230
|
+
|
|
231
|
+
## Observacao sobre `endpoint`
|
|
232
|
+
|
|
233
|
+
Prefira informar o `endpoint` como **URL base da API**. Exemplo recomendado:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
endpoint: 'https://sua-api.com/api/v1'
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Tambem funciona por compatibilidade:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
endpoint: 'https://sua-api.com/api/v1/dlq-injestion'
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
O SDK adiciona internamente:
|
|
246
|
+
|
|
247
|
+
- `/dlq-injestion`
|
|
248
|
+
- `/dlq-injestion/runtime-state`
|
|
249
|
+
|
|
250
|
+
## Matriz de comportamento
|
|
251
|
+
|
|
252
|
+
| Situacao | Comportamento do SDK |
|
|
253
|
+
| --- | --- |
|
|
254
|
+
| `2xx` da API | remove o item da fila |
|
|
255
|
+
| falha de rede / `5xx` / erro transiente | retry com backoff exponencial |
|
|
256
|
+
| limite de plano com politica `block` | descarta enquanto bloqueado e reconsulta estado |
|
|
257
|
+
| limite de plano com escolha de `bill_extra`, mas bloqueio temporario | usa blocked buffer e reconsulta estado |
|
|
258
|
+
| conta liberada novamente | retoma automaticamente sem reiniciar o processo |
|
|
259
|
+
|
|
260
|
+
## O que o cliente precisa saber sobre limites e cobranca extra
|
|
261
|
+
|
|
262
|
+
Nao existe parametro novo no SDK para habilitar esse comportamento. Ele depende da configuracao da conta no Rebound DLQ:
|
|
263
|
+
|
|
264
|
+
- se a conta habilitar cobranca extra para eventos, a plataforma tenta continuar a ingestao e cobrar o excedente
|
|
265
|
+
- se a conta desabilitar cobranca extra, o SDK entende que pode haver perda de dados acima do limite e passa a descartar enquanto durar o bloqueio
|
|
266
|
+
- se a conta fizer upgrade ou alterar essa politica, o SDK detecta a mudanca automaticamente na proxima sincronizacao
|
|
267
|
+
|
|
268
|
+
Em outras palavras: **a decisao de produto e da conta; o SDK apenas executa essa politica automaticamente**.
|
|
269
|
+
|
|
270
|
+
## Observacoes operacionais importantes
|
|
271
|
+
|
|
272
|
+
- a fila do SDK e **in-memory**
|
|
273
|
+
- eventos pendentes ou buffered **nao sobrevivem** a restart do processo
|
|
274
|
+
- workloads de curta duracao ou ambientes muito efemeros podem perder eventos se o processo encerrar logo apos o enqueue
|
|
275
|
+
- para maior seguranca operacional, prefira rodar o SDK em processos com shutdown gracioso
|
|
276
|
+
- o SDK registra logs no console quando entra em modo de retry, descarte, buffer e retomada
|
|
277
|
+
|
|
278
|
+
## Compatibilidade com APIs antigas
|
|
279
|
+
|
|
280
|
+
Se a API de destino ainda nao suportar o contrato estruturado de bloqueio por limite, o SDK continua tratando respostas nao `2xx` como erros retryaveis. Ou seja: o fluxo novo de `buffer` e `discard` depende da API estar atualizada.
|
|
281
|
+
|
|
282
|
+
## FAQ rapido
|
|
283
|
+
|
|
284
|
+
### Preciso alterar meu codigo para usar o buffer ou o descarte?
|
|
285
|
+
|
|
286
|
+
Nao. Esse comportamento e automatico nesta versao do SDK.
|
|
287
|
+
|
|
288
|
+
### Preciso informar um parametro novo no construtor?
|
|
289
|
+
|
|
290
|
+
Nao. Basta manter `projectSecret` e, se necessario, `endpoint`.
|
|
291
|
+
|
|
292
|
+
### O SDK vai me avisar quando a conta for desbloqueada?
|
|
293
|
+
|
|
294
|
+
Sim. Ele faz polling em background e retoma sozinho quando a API indicar que a conta voltou a poder ingerir eventos.
|
|
295
|
+
|
|
296
|
+
### O buffer guarda tudo indefinidamente?
|
|
94
297
|
|
|
95
|
-
|
|
96
|
-
2. Essa exceção é **capturada sem dor**.
|
|
97
|
-
3. A variável `payloadQueSeraSalvoNaDLQ` entra na **memória RAM do SDK**.
|
|
98
|
-
4. É processada **criptografia local AES-256** no dado, utilizando a sua chave.
|
|
99
|
-
5. Em Background, um processo envia de forma segura este evento para a plataforma Rebound, garantindo entrega do Dead Letter Queue para reprocessamento futuro ou simples visualização pela Dashboard.
|
|
298
|
+
Nao. Ele e temporario, fica apenas em memoria, dura ate 15 minutos e respeita o limite de 250 eventos por processo/chave.
|
|
100
299
|
|
|
101
300
|
## Suporte
|
|
102
301
|
|
|
103
|
-
|
|
302
|
+
Se voce tiver duvidas de integracao, comportamento de limites ou operacao do SDK, abra um chamado pela plataforma oficial da [Rebound DLQ](https://rebound-dlq.com).
|
|
104
303
|
|
|
105
|
-
|
|
106
|
-
|
|
304
|
+
---
|
|
305
|
+
Feito por **Codify Labs / Rebound DLQ**.
|
package/dist/core/client.d.ts
CHANGED
package/dist/core/client.js
CHANGED
|
@@ -1,25 +1,68 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.ReboundClient = void 0;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
4
8
|
const constants_1 = require("./constants");
|
|
9
|
+
const endpoint_1 = require("./endpoint");
|
|
5
10
|
const queue_1 = require("./queue");
|
|
6
11
|
class ReboundClient {
|
|
7
12
|
constructor(config) {
|
|
8
13
|
this.config = config;
|
|
9
|
-
const
|
|
10
|
-
this.baseUrl =
|
|
11
|
-
(isTest
|
|
12
|
-
? constants_1.REBOUND_API_ENDPOINTS.TEST
|
|
13
|
-
: constants_1.REBOUND_API_ENDPOINTS.PRODUCTION);
|
|
14
|
+
const configuredEndpoint = config.endpoint || constants_1.DEFAULT_REBOUND_API_ENDPOINT;
|
|
15
|
+
this.baseUrl = (0, endpoint_1.resolveDlqBaseEndpoint)(configuredEndpoint);
|
|
14
16
|
}
|
|
15
17
|
async send(payload) {
|
|
16
|
-
const endpoint =
|
|
17
|
-
const
|
|
18
|
-
|
|
18
|
+
const endpoint = (0, endpoint_1.resolveDlqIngestionEndpoint)(this.baseUrl);
|
|
19
|
+
const ingestionPayload = this.buildIngestionPayload(payload);
|
|
20
|
+
// Enqueue directly in memory so the application doesn't block on network responses
|
|
21
|
+
queue_1.MemoryQueue.getInstance().enqueue(endpoint, this.config.projectSecret, ingestionPayload);
|
|
22
|
+
}
|
|
23
|
+
buildIngestionPayload(payload) {
|
|
24
|
+
const normalizedError = this.normalizeError(payload.error);
|
|
25
|
+
const envelope = {
|
|
26
|
+
payload: payload.payload,
|
|
27
|
+
source: payload.source,
|
|
28
|
+
metadata: payload.metadata ?? {},
|
|
19
29
|
timestamp: payload.timestamp || Date.now(),
|
|
20
30
|
};
|
|
21
|
-
|
|
22
|
-
|
|
31
|
+
return {
|
|
32
|
+
fingerprint: this.generateFingerprint(normalizedError, payload.source),
|
|
33
|
+
error: normalizedError,
|
|
34
|
+
payload: this.encrypt(envelope),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
normalizeError(error) {
|
|
38
|
+
if (!error) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
message: error.message,
|
|
43
|
+
stack: error.stack,
|
|
44
|
+
name: error.name,
|
|
45
|
+
code: error.code !== undefined ? String(error.code) : undefined,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
generateFingerprint(error, source) {
|
|
49
|
+
const stack = (error?.stack || '').split('\n').slice(0, 3).join('');
|
|
50
|
+
const seed = `${error?.name || 'UnknownError'}${error?.message || ''}${stack}${source}`;
|
|
51
|
+
return crypto_1.default.createHash('sha256').update(seed).digest('hex');
|
|
52
|
+
}
|
|
53
|
+
encrypt(payload) {
|
|
54
|
+
const algorithm = 'aes-256-cbc';
|
|
55
|
+
const key = crypto_1.default
|
|
56
|
+
.createHash('sha256')
|
|
57
|
+
.update(String(this.config.projectSecret))
|
|
58
|
+
.digest('base64')
|
|
59
|
+
.substring(0, 32);
|
|
60
|
+
const iv = crypto_1.default.randomBytes(16);
|
|
61
|
+
const payloadString = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
62
|
+
const cipher = crypto_1.default.createCipheriv(algorithm, Buffer.from(key), iv);
|
|
63
|
+
let encrypted = cipher.update(payloadString, 'utf8', 'hex');
|
|
64
|
+
encrypted += cipher.final('hex');
|
|
65
|
+
return `${iv.toString('hex')}:${encrypted}`;
|
|
23
66
|
}
|
|
24
67
|
}
|
|
25
68
|
exports.ReboundClient = ReboundClient;
|
package/dist/core/constants.d.ts
CHANGED
package/dist/core/constants.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.
|
|
5
|
-
TEST: 'https://rebound-dlq-api-2.vercel.app/api/v1',
|
|
6
|
-
PRODUCTION: 'https://rebound-dlq-api-2.vercel.app/api/v1'
|
|
7
|
-
};
|
|
3
|
+
exports.DEFAULT_REBOUND_API_ENDPOINT = void 0;
|
|
4
|
+
exports.DEFAULT_REBOUND_API_ENDPOINT = 'http://localhost:3001/api/v1';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveDlqIngestionEndpoint = resolveDlqIngestionEndpoint;
|
|
4
|
+
exports.resolveDlqBaseEndpoint = resolveDlqBaseEndpoint;
|
|
5
|
+
const DLQ_INGESTION_SEGMENT = '/dlq-injestion';
|
|
6
|
+
function resolveDlqIngestionEndpoint(endpoint) {
|
|
7
|
+
const normalizedEndpoint = endpoint.replace(/\/+$/, '');
|
|
8
|
+
if (normalizedEndpoint.endsWith(DLQ_INGESTION_SEGMENT)) {
|
|
9
|
+
return normalizedEndpoint;
|
|
10
|
+
}
|
|
11
|
+
return `${normalizedEndpoint}${DLQ_INGESTION_SEGMENT}`;
|
|
12
|
+
}
|
|
13
|
+
function resolveDlqBaseEndpoint(endpoint) {
|
|
14
|
+
const normalizedEndpoint = endpoint.replace(/\/+$/, '');
|
|
15
|
+
if (normalizedEndpoint.endsWith(DLQ_INGESTION_SEGMENT)) {
|
|
16
|
+
return normalizedEndpoint.slice(0, -DLQ_INGESTION_SEGMENT.length);
|
|
17
|
+
}
|
|
18
|
+
return normalizedEndpoint;
|
|
19
|
+
}
|
package/dist/core/queue.d.ts
CHANGED
|
@@ -2,11 +2,34 @@ export declare class MemoryQueue {
|
|
|
2
2
|
private static instance;
|
|
3
3
|
private queue;
|
|
4
4
|
private isProcessing;
|
|
5
|
+
private isProcessingScheduled;
|
|
5
6
|
private baseDelayMs;
|
|
6
7
|
private maxDelayMs;
|
|
8
|
+
private blockedStates;
|
|
7
9
|
private constructor();
|
|
8
10
|
static getInstance(): MemoryQueue;
|
|
9
|
-
enqueue(endpoint: string,
|
|
11
|
+
enqueue(endpoint: string, projectSecret: string, payload: any): void;
|
|
10
12
|
private processQueue;
|
|
11
13
|
private applyBackoff;
|
|
14
|
+
private ensureProcessing;
|
|
15
|
+
private sendSignedJsonRequest;
|
|
16
|
+
private parseBlockedResponse;
|
|
17
|
+
private normalizeBlockedPayload;
|
|
18
|
+
private activateBlockedState;
|
|
19
|
+
private routeBlockedItem;
|
|
20
|
+
private bufferItem;
|
|
21
|
+
private pruneExpiredBufferedItems;
|
|
22
|
+
private scheduleProbe;
|
|
23
|
+
private probeBlockedState;
|
|
24
|
+
private fetchRuntimeState;
|
|
25
|
+
private normalizeRuntimeStateResponse;
|
|
26
|
+
private resumeBlockedState;
|
|
27
|
+
private logBlockedMode;
|
|
28
|
+
private buildQueueKey;
|
|
29
|
+
private buildRuntimeStateEndpoint;
|
|
30
|
+
private normalizeRecheckAfterMs;
|
|
31
|
+
private normalizeBufferWindowMs;
|
|
32
|
+
private isJsonResponse;
|
|
33
|
+
private isRecord;
|
|
34
|
+
private buildRetryWarning;
|
|
12
35
|
}
|
package/dist/core/queue.js
CHANGED
|
@@ -1,12 +1,55 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.MemoryQueue = void 0;
|
|
37
|
+
const crypto = __importStar(require("crypto"));
|
|
38
|
+
const DLQ_INGESTION_BLOCK_CODE = 'DLQ_INGESTION_BLOCKED_LIMIT';
|
|
39
|
+
const DEFAULT_RECHECK_AFTER_MS = 60 * 1000;
|
|
40
|
+
const DEFAULT_BUFFER_WINDOW_MS = 15 * 60 * 1000;
|
|
41
|
+
const MIN_RECHECK_AFTER_MS = 15 * 1000;
|
|
42
|
+
const MIN_BUFFER_WINDOW_MS = 60 * 1000;
|
|
43
|
+
const MAX_BUFFER_WINDOW_MS = 60 * 60 * 1000;
|
|
44
|
+
const MAX_BUFFERED_ITEMS_PER_KEY = 250;
|
|
4
45
|
class MemoryQueue {
|
|
5
46
|
constructor() {
|
|
6
47
|
this.queue = [];
|
|
7
48
|
this.isProcessing = false;
|
|
49
|
+
this.isProcessingScheduled = false;
|
|
8
50
|
this.baseDelayMs = 1000;
|
|
9
51
|
this.maxDelayMs = 60000; // max 60 seconds between retries
|
|
52
|
+
this.blockedStates = new Map();
|
|
10
53
|
}
|
|
11
54
|
static getInstance() {
|
|
12
55
|
if (!MemoryQueue.instance) {
|
|
@@ -14,61 +57,330 @@ class MemoryQueue {
|
|
|
14
57
|
}
|
|
15
58
|
return MemoryQueue.instance;
|
|
16
59
|
}
|
|
17
|
-
enqueue(endpoint,
|
|
18
|
-
|
|
60
|
+
enqueue(endpoint, projectSecret, payload) {
|
|
61
|
+
const item = {
|
|
19
62
|
endpoint,
|
|
20
|
-
|
|
63
|
+
projectSecret,
|
|
21
64
|
payload,
|
|
22
65
|
retryCount: 0
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}, 0);
|
|
66
|
+
};
|
|
67
|
+
const queueKey = this.buildQueueKey(endpoint, projectSecret);
|
|
68
|
+
const blockedState = this.blockedStates.get(queueKey);
|
|
69
|
+
if (blockedState) {
|
|
70
|
+
this.routeBlockedItem(item, blockedState);
|
|
71
|
+
this.scheduleProbe(queueKey, blockedState);
|
|
72
|
+
return;
|
|
31
73
|
}
|
|
74
|
+
this.queue.push(item);
|
|
75
|
+
this.ensureProcessing();
|
|
32
76
|
}
|
|
33
77
|
async processQueue() {
|
|
34
78
|
this.isProcessing = true;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
'Content-Type': 'application/json',
|
|
42
|
-
'x-api-key': `Bearer ${item.apiKey}`
|
|
43
|
-
},
|
|
44
|
-
body: JSON.stringify(item.payload),
|
|
45
|
-
});
|
|
46
|
-
if (response.ok) {
|
|
47
|
-
if (item.retryCount > 0) {
|
|
48
|
-
console.log(`[Rebound SDK] Event successfully delivered after ${item.retryCount} retries.`);
|
|
49
|
-
}
|
|
50
|
-
// Success: Remove item from memory queue
|
|
79
|
+
try {
|
|
80
|
+
while (this.queue.length > 0) {
|
|
81
|
+
const item = this.queue[0]; // FIFO
|
|
82
|
+
const queueKey = this.buildQueueKey(item.endpoint, item.projectSecret);
|
|
83
|
+
const blockedState = this.blockedStates.get(queueKey);
|
|
84
|
+
if (blockedState) {
|
|
51
85
|
this.queue.shift();
|
|
86
|
+
this.routeBlockedItem(item, blockedState);
|
|
87
|
+
this.scheduleProbe(queueKey, blockedState);
|
|
88
|
+
continue;
|
|
52
89
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
90
|
+
try {
|
|
91
|
+
const response = await this.sendSignedJsonRequest(item.endpoint, item.projectSecret, item.payload);
|
|
92
|
+
if (response.ok) {
|
|
93
|
+
if (item.retryCount > 0) {
|
|
94
|
+
console.log(`[Rebound SDK] Event successfully delivered after ${item.retryCount} retries.`);
|
|
95
|
+
}
|
|
96
|
+
this.queue.shift();
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const blockedResponse = await this.parseBlockedResponse(response);
|
|
100
|
+
if (blockedResponse) {
|
|
101
|
+
this.queue.shift();
|
|
102
|
+
this.activateBlockedState(item, blockedResponse);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
await this.applyBackoff(item, {
|
|
106
|
+
reason: 'http',
|
|
107
|
+
statusCode: response.status,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
// Network unstable / Connection refused
|
|
112
|
+
await this.applyBackoff(item, { reason: 'network' });
|
|
56
113
|
}
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
// Network unstable / Connection refused
|
|
60
|
-
await this.applyBackoff(item);
|
|
61
114
|
}
|
|
62
115
|
}
|
|
63
|
-
|
|
116
|
+
finally {
|
|
117
|
+
this.isProcessing = false;
|
|
118
|
+
}
|
|
64
119
|
}
|
|
65
|
-
async applyBackoff(item) {
|
|
120
|
+
async applyBackoff(item, context) {
|
|
66
121
|
item.retryCount++;
|
|
67
122
|
const delay = Math.min(this.baseDelayMs * Math.pow(2, item.retryCount), this.maxDelayMs);
|
|
68
123
|
if (item.retryCount === 1) {
|
|
69
|
-
console.warn(
|
|
124
|
+
console.warn(this.buildRetryWarning(context));
|
|
70
125
|
}
|
|
71
126
|
return new Promise((resolve) => setTimeout(resolve, delay));
|
|
72
127
|
}
|
|
128
|
+
ensureProcessing() {
|
|
129
|
+
if (this.isProcessing || this.isProcessingScheduled) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.isProcessingScheduled = true;
|
|
133
|
+
// Offload to Event Loop so it returns instantly for the main application thread
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
this.isProcessingScheduled = false;
|
|
136
|
+
this.processQueue().catch((err) => {
|
|
137
|
+
console.error('[Rebound SDK] Unexpected queue error:', err);
|
|
138
|
+
});
|
|
139
|
+
}, 0);
|
|
140
|
+
}
|
|
141
|
+
async sendSignedJsonRequest(endpoint, projectSecret, payload) {
|
|
142
|
+
const timestamp = Date.now().toString();
|
|
143
|
+
const payloadString = JSON.stringify(payload);
|
|
144
|
+
const dataToSign = `${timestamp}.${payloadString}`;
|
|
145
|
+
const signature = crypto
|
|
146
|
+
.createHmac('sha256', projectSecret)
|
|
147
|
+
.update(dataToSign)
|
|
148
|
+
.digest('hex');
|
|
149
|
+
return fetch(endpoint, {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: {
|
|
152
|
+
'Content-Type': 'application/json',
|
|
153
|
+
'x-api-key': `Bearer ${projectSecret}`,
|
|
154
|
+
'x-sdk-timestamp': timestamp,
|
|
155
|
+
'x-sdk-signature': signature
|
|
156
|
+
},
|
|
157
|
+
body: payloadString,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
async parseBlockedResponse(response) {
|
|
161
|
+
if (response.status !== 403 || !this.isJsonResponse(response)) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const payload = await response.json();
|
|
166
|
+
return this.normalizeBlockedPayload(payload);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
normalizeBlockedPayload(payload) {
|
|
173
|
+
if (!this.isRecord(payload)) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
if (payload.code !== DLQ_INGESTION_BLOCK_CODE || payload.retryable !== false) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
const queueAction = payload.queueAction === 'buffer' ? 'buffer' : 'discard';
|
|
180
|
+
const bufferWindowMs = queueAction === 'buffer'
|
|
181
|
+
? this.normalizeBufferWindowMs(payload.bufferWindowSeconds)
|
|
182
|
+
: 0;
|
|
183
|
+
return {
|
|
184
|
+
queueAction,
|
|
185
|
+
bufferWindowMs,
|
|
186
|
+
recheckAfterMs: this.normalizeRecheckAfterMs(payload.recheckAfterSeconds),
|
|
187
|
+
currentPeriodEnd: typeof payload.currentPeriodEnd === 'string' ? payload.currentPeriodEnd : null,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
activateBlockedState(item, payload) {
|
|
191
|
+
const queueKey = this.buildQueueKey(item.endpoint, item.projectSecret);
|
|
192
|
+
const previousState = this.blockedStates.get(queueKey) ?? null;
|
|
193
|
+
const previousAction = previousState?.queueAction ?? null;
|
|
194
|
+
const blockedState = previousState ?? {
|
|
195
|
+
endpoint: item.endpoint,
|
|
196
|
+
projectSecret: item.projectSecret,
|
|
197
|
+
queueAction: payload.queueAction,
|
|
198
|
+
bufferWindowMs: payload.bufferWindowMs,
|
|
199
|
+
recheckAfterMs: payload.recheckAfterMs,
|
|
200
|
+
currentPeriodEnd: payload.currentPeriodEnd,
|
|
201
|
+
buffer: [],
|
|
202
|
+
timer: null,
|
|
203
|
+
probeInFlight: false,
|
|
204
|
+
};
|
|
205
|
+
blockedState.queueAction = payload.queueAction;
|
|
206
|
+
blockedState.bufferWindowMs = payload.bufferWindowMs;
|
|
207
|
+
blockedState.recheckAfterMs = payload.recheckAfterMs;
|
|
208
|
+
blockedState.currentPeriodEnd = payload.currentPeriodEnd;
|
|
209
|
+
if (blockedState.queueAction === 'discard' && blockedState.buffer.length > 0) {
|
|
210
|
+
blockedState.buffer = [];
|
|
211
|
+
}
|
|
212
|
+
this.blockedStates.set(queueKey, blockedState);
|
|
213
|
+
this.routeBlockedItem(item, blockedState);
|
|
214
|
+
this.scheduleProbe(queueKey, blockedState);
|
|
215
|
+
if (!previousState || previousAction !== blockedState.queueAction) {
|
|
216
|
+
this.logBlockedMode(blockedState);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
routeBlockedItem(item, blockedState) {
|
|
220
|
+
if (blockedState.queueAction === 'buffer') {
|
|
221
|
+
this.bufferItem(blockedState, item);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
bufferItem(blockedState, item) {
|
|
226
|
+
this.pruneExpiredBufferedItems(blockedState);
|
|
227
|
+
blockedState.buffer.push({
|
|
228
|
+
...item,
|
|
229
|
+
expiresAt: Date.now() + blockedState.bufferWindowMs,
|
|
230
|
+
});
|
|
231
|
+
if (blockedState.buffer.length <= MAX_BUFFERED_ITEMS_PER_KEY) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const overflowCount = blockedState.buffer.length - MAX_BUFFERED_ITEMS_PER_KEY;
|
|
235
|
+
blockedState.buffer.splice(0, overflowCount);
|
|
236
|
+
console.warn(`[Rebound SDK] Buffer cap reached while DLQ ingestion is blocked. Dropped ${overflowCount} older buffered event(s).`);
|
|
237
|
+
}
|
|
238
|
+
pruneExpiredBufferedItems(blockedState) {
|
|
239
|
+
const now = Date.now();
|
|
240
|
+
const originalLength = blockedState.buffer.length;
|
|
241
|
+
blockedState.buffer = blockedState.buffer.filter((item) => item.expiresAt > now);
|
|
242
|
+
const droppedCount = originalLength - blockedState.buffer.length;
|
|
243
|
+
if (droppedCount > 0) {
|
|
244
|
+
console.warn(`[Rebound SDK] Dropped ${droppedCount} buffered event(s) after the temporary blocked window expired.`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
scheduleProbe(queueKey, blockedState) {
|
|
248
|
+
if (blockedState.timer) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
blockedState.timer = setTimeout(() => {
|
|
252
|
+
blockedState.timer = null;
|
|
253
|
+
this.probeBlockedState(queueKey).catch((error) => {
|
|
254
|
+
console.error('[Rebound SDK] Failed to refresh blocked ingestion state:', error);
|
|
255
|
+
});
|
|
256
|
+
}, blockedState.recheckAfterMs);
|
|
257
|
+
}
|
|
258
|
+
async probeBlockedState(queueKey) {
|
|
259
|
+
const blockedState = this.blockedStates.get(queueKey);
|
|
260
|
+
if (!blockedState || blockedState.probeInFlight) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
blockedState.probeInFlight = true;
|
|
264
|
+
try {
|
|
265
|
+
this.pruneExpiredBufferedItems(blockedState);
|
|
266
|
+
const runtimeState = await this.fetchRuntimeState(blockedState);
|
|
267
|
+
if (!runtimeState) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (runtimeState.allowed) {
|
|
271
|
+
this.resumeBlockedState(queueKey, blockedState);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const previousAction = blockedState.queueAction;
|
|
275
|
+
blockedState.queueAction = runtimeState.queueAction;
|
|
276
|
+
blockedState.bufferWindowMs = runtimeState.bufferWindowMs;
|
|
277
|
+
blockedState.recheckAfterMs = runtimeState.recheckAfterMs;
|
|
278
|
+
blockedState.currentPeriodEnd = runtimeState.currentPeriodEnd;
|
|
279
|
+
if (blockedState.queueAction === 'discard' && blockedState.buffer.length > 0) {
|
|
280
|
+
blockedState.buffer = [];
|
|
281
|
+
}
|
|
282
|
+
if (previousAction !== blockedState.queueAction) {
|
|
283
|
+
this.logBlockedMode(blockedState);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
blockedState.probeInFlight = false;
|
|
288
|
+
if (this.blockedStates.has(queueKey)) {
|
|
289
|
+
this.scheduleProbe(queueKey, blockedState);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async fetchRuntimeState(blockedState) {
|
|
294
|
+
const response = await this.sendSignedJsonRequest(this.buildRuntimeStateEndpoint(blockedState.endpoint), blockedState.projectSecret, {});
|
|
295
|
+
if (!response.ok || !this.isJsonResponse(response)) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
const payload = await response.json();
|
|
300
|
+
return this.normalizeRuntimeStateResponse(payload);
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
normalizeRuntimeStateResponse(payload) {
|
|
307
|
+
if (!this.isRecord(payload) || typeof payload.allowed !== 'boolean') {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
if (payload.allowed) {
|
|
311
|
+
return {
|
|
312
|
+
allowed: true,
|
|
313
|
+
queueAction: 'discard',
|
|
314
|
+
bufferWindowMs: 0,
|
|
315
|
+
recheckAfterMs: this.normalizeRecheckAfterMs(payload.recheckAfterSeconds),
|
|
316
|
+
currentPeriodEnd: typeof payload.currentPeriodEnd === 'string' ? payload.currentPeriodEnd : null,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const blockedPayload = this.normalizeBlockedPayload(payload);
|
|
320
|
+
if (!blockedPayload) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
allowed: false,
|
|
325
|
+
...blockedPayload,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
resumeBlockedState(queueKey, blockedState) {
|
|
329
|
+
this.blockedStates.delete(queueKey);
|
|
330
|
+
if (blockedState.timer) {
|
|
331
|
+
clearTimeout(blockedState.timer);
|
|
332
|
+
blockedState.timer = null;
|
|
333
|
+
}
|
|
334
|
+
this.pruneExpiredBufferedItems(blockedState);
|
|
335
|
+
if (blockedState.buffer.length > 0) {
|
|
336
|
+
const bufferedItems = blockedState.buffer.map(({ expiresAt, ...item }) => item);
|
|
337
|
+
this.queue = [...bufferedItems, ...this.queue];
|
|
338
|
+
console.log(`[Rebound SDK] DLQ ingestion resumed. Replaying ${bufferedItems.length} buffered event(s).`);
|
|
339
|
+
}
|
|
340
|
+
if (this.queue.length > 0) {
|
|
341
|
+
this.ensureProcessing();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
logBlockedMode(blockedState) {
|
|
345
|
+
const cycleHint = blockedState.currentPeriodEnd
|
|
346
|
+
? ` Current cycle ends at ${blockedState.currentPeriodEnd}.`
|
|
347
|
+
: '';
|
|
348
|
+
if (blockedState.queueAction === 'buffer') {
|
|
349
|
+
const bufferMinutes = Math.round(blockedState.bufferWindowMs / 60000);
|
|
350
|
+
console.warn(`[Rebound SDK] DLQ ingestion is temporarily blocked by the account plan. Buffering events for up to ${bufferMinutes} minute(s) while the account plan or billing settings are adjusted.${cycleHint}`);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
console.warn(`[Rebound SDK] DLQ ingestion is blocked by the account plan and extra usage is disabled. New events will be discarded until the account becomes eligible again.${cycleHint}`);
|
|
354
|
+
}
|
|
355
|
+
buildQueueKey(endpoint, projectSecret) {
|
|
356
|
+
return `${endpoint}::${projectSecret}`;
|
|
357
|
+
}
|
|
358
|
+
buildRuntimeStateEndpoint(endpoint) {
|
|
359
|
+
return `${endpoint.replace(/\/+$/, '')}/runtime-state`;
|
|
360
|
+
}
|
|
361
|
+
normalizeRecheckAfterMs(value) {
|
|
362
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
363
|
+
return DEFAULT_RECHECK_AFTER_MS;
|
|
364
|
+
}
|
|
365
|
+
return Math.max(Math.trunc(value * 1000), MIN_RECHECK_AFTER_MS);
|
|
366
|
+
}
|
|
367
|
+
normalizeBufferWindowMs(value) {
|
|
368
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
369
|
+
return DEFAULT_BUFFER_WINDOW_MS;
|
|
370
|
+
}
|
|
371
|
+
return Math.min(Math.max(Math.trunc(value * 1000), MIN_BUFFER_WINDOW_MS), MAX_BUFFER_WINDOW_MS);
|
|
372
|
+
}
|
|
373
|
+
isJsonResponse(response) {
|
|
374
|
+
return (response.headers.get('content-type') ?? '').includes('application/json');
|
|
375
|
+
}
|
|
376
|
+
isRecord(value) {
|
|
377
|
+
return typeof value === 'object' && value !== null;
|
|
378
|
+
}
|
|
379
|
+
buildRetryWarning(context) {
|
|
380
|
+
if (context.reason === 'http' && context.statusCode) {
|
|
381
|
+
return `[Rebound SDK] DLQ API rejected the event with HTTP ${context.statusCode}. Retrying in background while the issue persists...`;
|
|
382
|
+
}
|
|
383
|
+
return '[Rebound SDK] Connectivity issue sending DLQ event. Retrying in background to guarantee delivery...';
|
|
384
|
+
}
|
|
73
385
|
}
|
|
74
386
|
exports.MemoryQueue = MemoryQueue;
|
package/dist/core/wrapper.js
CHANGED
|
@@ -6,16 +6,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.DlqWrapper = void 0;
|
|
7
7
|
const crypto_1 = __importDefault(require("crypto"));
|
|
8
8
|
const constants_1 = require("./constants");
|
|
9
|
+
const endpoint_1 = require("./endpoint");
|
|
9
10
|
const queue_1 = require("./queue");
|
|
10
11
|
class DlqWrapper {
|
|
11
12
|
constructor(config) {
|
|
12
13
|
this.projectSecret = config.projectSecret;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
this.endpoint = config.endpoint ||
|
|
16
|
-
(isTest
|
|
17
|
-
? constants_1.REBOUND_API_ENDPOINTS.TEST
|
|
18
|
-
: constants_1.REBOUND_API_ENDPOINTS.PRODUCTION) + '/dlq-injestion'; // Usar o sufixo apropriado
|
|
14
|
+
const configuredEndpoint = config.endpoint || constants_1.DEFAULT_REBOUND_API_ENDPOINT;
|
|
15
|
+
this.endpoint = (0, endpoint_1.resolveDlqIngestionEndpoint)(configuredEndpoint);
|
|
19
16
|
}
|
|
20
17
|
async execute(fn, payload) {
|
|
21
18
|
try {
|
package/dist/models/payload.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@rebound-dlq/node",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"main": "dist/index.js",
|
|
5
|
-
"types": "dist/index.d.ts",
|
|
6
|
-
"files": ["dist"],
|
|
7
|
-
"scripts": {
|
|
8
|
-
"build": "tsc",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
}
|
|
18
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@rebound-dlq/node",
|
|
3
|
+
"version": "0.2.4",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"files": ["dist"],
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"build:examples": "node scripts/run-example.cjs --build-only",
|
|
10
|
+
"dev": "tsc --watch",
|
|
11
|
+
"json-server": "npx json-server --watch mock-server.json --port 3005",
|
|
12
|
+
"example:wrapper": "node scripts/run-example.cjs wrapper-example",
|
|
13
|
+
"example:http": "node scripts/run-example.cjs http-example",
|
|
14
|
+
"example:kafka": "node scripts/run-example.cjs kafka-example",
|
|
15
|
+
"example:internal": "node scripts/run-example.cjs internal-errors-example",
|
|
16
|
+
"test:wrapper": "node scripts/run-example.cjs wrapper-example"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^20.0.0",
|
|
20
|
+
"json-server": "^1.0.0-beta.3",
|
|
21
|
+
"typescript": "^5.0.0"
|
|
22
|
+
}
|
|
23
|
+
}
|