@rebound-dlq/node 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # Rebound DLQ SDK 🚀
2
+
3
+ O **Rebound DLQ SDK** é uma solução de altíssima performance (Zero Gargalos e Fila em Memória) para capturar falhas em integrações, proteger sua regra de negócio e fazer o reprocessamento de DLQs (Dead Letter Queues) de forma transparente para sua aplicação Node.js.
4
+
5
+ Sua aplicação original não vai travar nem ficar lenta, mesmo se a nossa API cair ou a rede oscilar.
6
+
7
+ ## 📦 1. Instalação
8
+
9
+ Abra o terminal do seu projeto Node.js e instale o pacote:
10
+
11
+ ```bash
12
+ npm install @rebound/dlq-sdk
13
+ ```
14
+
15
+ *(Nota: Se você está rodando um pacote em testes local via `.tgz`, use: `npm install ./caminho/do/seu/rebound-dlq-sdk-0.1.0.tgz`)*
16
+
17
+ ---
18
+
19
+ ## ⚙️ 2. Como Configurar e Usar na Prática
20
+
21
+ A forma ideal de utilizar o SDK é "abraçando" (envolvendo) as funções de risco do seu sistema (pagamentos, cadastros em APIs externas, envio de emails, etc) através do `DlqWrapper`.
22
+
23
+ ### Passo A: Importar e Configurar o SDK
24
+
25
+ No topo do seu arquivo principal (ou no seu arquivo de injeção de dependências), inicialize o Wrapper passando suas credenciais:
26
+
27
+ ```javascript
28
+ // Se você usa CommonJS (require):
29
+ const { DlqWrapper } = require('@rebound/dlq-sdk');
30
+
31
+ // Se você usa ES Modules (import):
32
+ // import { DlqWrapper } from '@rebound/dlq-sdk';
33
+
34
+ const dlqWrapper = new DlqWrapper({
35
+ projectId: "seu-project-id", // Pegue no painel do Rebound
36
+ projectSecret: "sk_dev_sua_chave_secreta", // Use sk_dev_ para testes ou sk_live_ para produção
37
+ });
38
+ ```
39
+
40
+ ### Passo B: Proteger sua Função
41
+
42
+ Em vez de chamar sua função diretamente, chame ela *dentro* do método `.execute()` do SDK.
43
+
44
+ Sempre que a sua função der erro, o SDK silenciosamente vai capturar o payload e enviar pra Rebound em backgroud, enquanto sua aplicação segue a vida imediatamente e de forma limpa.
45
+
46
+ ```javascript
47
+ // Uma função sua que costuma falhar ou chamar serviços externos
48
+ async function minhaFuncaoDeRisco(dados) {
49
+ console.log("Iniciando pagamento...");
50
+ if (dados.valor > 1000) {
51
+ throw new Error("Saldo insuficiente na operadora do cartão");
52
+ }
53
+ return "Sucesso!";
54
+ }
55
+
56
+ async function run() {
57
+ // 1. Defina os dados que você quer salvar caso essa rotina falhe
58
+ const payloadQueSeraSalvoNaDLQ = {
59
+ userId: "123",
60
+ valor: 5000,
61
+ tentativa: 1,
62
+ modulo: "Checkout"
63
+ };
64
+
65
+ try {
66
+ // 2. Execute a sua função DENTRO da proteção do SDK
67
+ const resultado = await dlqWrapper.execute(
68
+ () => minhaFuncaoDeRisco(payloadQueSeraSalvoNaDLQ), // Sua função de negócio
69
+ payloadQueSeraSalvoNaDLQ // O Payload que vai pra nuvem em caso de erro
70
+ );
71
+
72
+ console.log("Deu tudo certo:", resultado);
73
+
74
+ } catch (erro) {
75
+ // 3. Trate o erro localmente como você sempre fez (o App NÃO morre)
76
+ console.log("Erro na aplicação:", erro.message);
77
+
78
+ // 🚀 A MÁGICA:
79
+ // Nesse exato momento, o SDK já pegou esse erro e despachou para o Rebound na nuvem.
80
+ // Se a internet falhou, ele salvou na memória RAM e ficará tentando de novo em background
81
+ // sem travar ou deixar sua API mais lenta!
82
+ }
83
+ }
84
+
85
+ run();
86
+ ```
87
+
88
+ ---
89
+
90
+ ## ⚡ Por que usar nossa DLQ Engine?
91
+
92
+ 1. **In-Memory Zero-Latency**: A comunicação com nossos servidores não engargala os verbos HTTP da sua aplicação. Assim que a exceção bater e você usar o wrapper, ele joga o payload rápido (`O(1)`) pra uma fila inteligente em Memória RAM da sua máquina.
93
+ 2. **Entrega Garantida**: Se a sua infra ou nossa nuvem apresentar lentidão, sua requisição fica salva temporariamente no Node Event Loop da sua aplicação fazendo "Retry Exponencial" silencioso.
94
+ 3. **Criptografia Automática**: Todas as requisições enviadas ao Painel são criptografadas (AES-256) com sua `projectSecret` ainda na sua infraestrutura. Ninguém acessa do outro lado além de você.
95
+
96
+ ### Modos suportados
97
+
98
+ - `sk_dev_*` conectará automaticamente na Sandbox / Local (Porta 3001)
99
+ - `sk_live_*` conectará aos endpoints oficiais estabilizados de produção.
100
+
101
+ ---
102
+ **Suporte Rebound**
103
+ Dúvidas ou problemas? Acesse seu dashboard e abra um chamado com nosso time de engenharia.
@@ -0,0 +1,8 @@
1
+ import { ReboundClient } from '../core/client';
2
+ import { DLQPayload, SendToDLQOptions } from '../models/payload';
3
+ export declare abstract class BaseAdapter {
4
+ protected client: ReboundClient;
5
+ constructor(client: ReboundClient);
6
+ sendToDLQ(options: SendToDLQOptions): Promise<void>;
7
+ protected normalizeError(error?: Error | string): DLQPayload['error'];
8
+ }
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BaseAdapter = void 0;
4
+ class BaseAdapter {
5
+ constructor(client) {
6
+ this.client = client;
7
+ }
8
+ async sendToDLQ(options) {
9
+ const { payload, error, source = 'unknown', metadata = {} } = options;
10
+ const dlqPayload = {
11
+ payload,
12
+ source,
13
+ error: this.normalizeError(error),
14
+ metadata,
15
+ timestamp: Date.now()
16
+ };
17
+ return this.client.send(dlqPayload);
18
+ }
19
+ normalizeError(error) {
20
+ if (!error)
21
+ return undefined;
22
+ if (typeof error === 'string') {
23
+ return {
24
+ message: error,
25
+ name: 'Error'
26
+ };
27
+ }
28
+ return {
29
+ message: error.message,
30
+ stack: error.stack,
31
+ name: error.name,
32
+ code: error.code
33
+ };
34
+ }
35
+ }
36
+ exports.BaseAdapter = BaseAdapter;
@@ -0,0 +1,5 @@
1
+ import { BaseAdapter } from './base.adapter';
2
+ import { SendToDLQOptions } from '../models/payload';
3
+ export declare class HTTPAdapter extends BaseAdapter {
4
+ sendToDLQ(options: SendToDLQOptions): Promise<void>;
5
+ }
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HTTPAdapter = void 0;
4
+ const base_adapter_1 = require("./base.adapter");
5
+ class HTTPAdapter extends base_adapter_1.BaseAdapter {
6
+ async sendToDLQ(options) {
7
+ return super.sendToDLQ({
8
+ ...options,
9
+ source: options.source || 'http',
10
+ metadata: {
11
+ ...options.metadata,
12
+ type: 'http'
13
+ }
14
+ });
15
+ }
16
+ }
17
+ exports.HTTPAdapter = HTTPAdapter;
@@ -0,0 +1,5 @@
1
+ import { BaseAdapter } from './base.adapter';
2
+ import { SendToDLQOptions } from '../models/payload';
3
+ export declare class KafkaAdapter extends BaseAdapter {
4
+ sendToDLQ(options: SendToDLQOptions): Promise<void>;
5
+ }
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KafkaAdapter = void 0;
4
+ const base_adapter_1 = require("./base.adapter");
5
+ class KafkaAdapter extends base_adapter_1.BaseAdapter {
6
+ async sendToDLQ(options) {
7
+ return super.sendToDLQ({
8
+ ...options,
9
+ source: options.source || 'kafka',
10
+ metadata: {
11
+ ...options.metadata,
12
+ type: 'kafka'
13
+ }
14
+ });
15
+ }
16
+ }
17
+ exports.KafkaAdapter = KafkaAdapter;
@@ -0,0 +1,7 @@
1
+ import { DLQPayload, ReboundConfig } from "../models/payload";
2
+ export declare class ReboundClient {
3
+ private config;
4
+ private readonly baseUrl;
5
+ constructor(config: ReboundConfig);
6
+ send(payload: DLQPayload): Promise<void>;
7
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ReboundClient = void 0;
4
+ const constants_1 = require("./constants");
5
+ const queue_1 = require("./queue");
6
+ class ReboundClient {
7
+ constructor(config) {
8
+ this.config = config;
9
+ const isTest = config.apiKey.startsWith('sk_test_');
10
+ this.baseUrl = config.endpoint ||
11
+ (isTest
12
+ ? constants_1.REBOUND_API_ENDPOINTS.TEST
13
+ : constants_1.REBOUND_API_ENDPOINTS.PRODUCTION);
14
+ }
15
+ async send(payload) {
16
+ const endpoint = `${this.baseUrl}/dlq-injestion`;
17
+ const enrichedPayload = {
18
+ ...payload,
19
+ timestamp: payload.timestamp || Date.now(),
20
+ };
21
+ // Enqueue directly in memory so the application doesn't block on network responses
22
+ queue_1.MemoryQueue.getInstance().enqueue(endpoint, this.config.apiKey, enrichedPayload);
23
+ }
24
+ }
25
+ exports.ReboundClient = ReboundClient;
@@ -0,0 +1,4 @@
1
+ export declare const REBOUND_API_ENDPOINTS: {
2
+ readonly TEST: "http://localhost:3001/api/v1/test";
3
+ readonly PRODUCTION: "http://localhost:3001/api/v1";
4
+ };
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.REBOUND_API_ENDPOINTS = void 0;
4
+ exports.REBOUND_API_ENDPOINTS = {
5
+ TEST: 'http://localhost:3001/api/v1/test',
6
+ PRODUCTION: 'http://localhost:3001/api/v1'
7
+ };
@@ -0,0 +1,7 @@
1
+ export declare class ReboundError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export declare class ReboundAPIError extends ReboundError {
5
+ statusCode: number;
6
+ constructor(statusCode: number, message: string);
7
+ }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ReboundAPIError = exports.ReboundError = void 0;
4
+ class ReboundError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = "ReboundError";
8
+ }
9
+ }
10
+ exports.ReboundError = ReboundError;
11
+ class ReboundAPIError extends ReboundError {
12
+ constructor(statusCode, message) {
13
+ super(message);
14
+ this.statusCode = statusCode;
15
+ this.name = "ReboundAPIError";
16
+ }
17
+ }
18
+ exports.ReboundAPIError = ReboundAPIError;
@@ -0,0 +1,12 @@
1
+ export declare class MemoryQueue {
2
+ private static instance;
3
+ private queue;
4
+ private isProcessing;
5
+ private baseDelayMs;
6
+ private maxDelayMs;
7
+ private constructor();
8
+ static getInstance(): MemoryQueue;
9
+ enqueue(endpoint: string, apiKey: string, payload: any): void;
10
+ private processQueue;
11
+ private applyBackoff;
12
+ }
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MemoryQueue = void 0;
4
+ class MemoryQueue {
5
+ constructor() {
6
+ this.queue = [];
7
+ this.isProcessing = false;
8
+ this.baseDelayMs = 1000;
9
+ this.maxDelayMs = 60000; // max 60 seconds between retries
10
+ }
11
+ static getInstance() {
12
+ if (!MemoryQueue.instance) {
13
+ MemoryQueue.instance = new MemoryQueue();
14
+ }
15
+ return MemoryQueue.instance;
16
+ }
17
+ enqueue(endpoint, apiKey, payload) {
18
+ this.queue.push({
19
+ endpoint,
20
+ apiKey,
21
+ payload,
22
+ retryCount: 0
23
+ });
24
+ if (!this.isProcessing) {
25
+ // Offload to Event Loop so it returns instantly for the main application thread
26
+ setTimeout(() => {
27
+ this.processQueue().catch((err) => {
28
+ console.error('[Rebound SDK] Unexpected queue error:', err);
29
+ });
30
+ }, 0);
31
+ }
32
+ }
33
+ async processQueue() {
34
+ this.isProcessing = true;
35
+ while (this.queue.length > 0) {
36
+ const item = this.queue[0]; // FIFO
37
+ try {
38
+ const response = await fetch(item.endpoint, {
39
+ method: 'POST',
40
+ headers: {
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
51
+ this.queue.shift();
52
+ }
53
+ else {
54
+ // API error (ex 429, 500)
55
+ await this.applyBackoff(item);
56
+ }
57
+ }
58
+ catch (error) {
59
+ // Network unstable / Connection refused
60
+ await this.applyBackoff(item);
61
+ }
62
+ }
63
+ this.isProcessing = false;
64
+ }
65
+ async applyBackoff(item) {
66
+ item.retryCount++;
67
+ const delay = Math.min(this.baseDelayMs * Math.pow(2, item.retryCount), this.maxDelayMs);
68
+ if (item.retryCount === 1) {
69
+ console.warn(`[Rebound SDK] Connectivity issue sending DLQ event. Retrying in background to guarantee delivery...`);
70
+ }
71
+ return new Promise((resolve) => setTimeout(resolve, delay));
72
+ }
73
+ }
74
+ exports.MemoryQueue = MemoryQueue;
@@ -0,0 +1,17 @@
1
+ export interface DlqServiceConfig {
2
+ projectId: string;
3
+ projectSecret: string;
4
+ endpoint?: string;
5
+ maxFailures?: number;
6
+ }
7
+ export declare class DlqWrapper {
8
+ private projectId;
9
+ private projectSecret;
10
+ private readonly endpoint;
11
+ constructor(config: DlqServiceConfig);
12
+ execute<T>(fn: () => Promise<T>, payload: any): Promise<T>;
13
+ private reportError;
14
+ private generateFingerprint;
15
+ private encrypt;
16
+ private sendToApi;
17
+ }
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DlqWrapper = void 0;
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ const constants_1 = require("./constants");
9
+ const queue_1 = require("./queue");
10
+ class DlqWrapper {
11
+ constructor(config) {
12
+ this.projectId = config.projectId;
13
+ this.projectSecret = config.projectSecret;
14
+ // Identifica se é ambiente de teste através do projectSecret (mesma lógica da apiKey no ReboundClient)
15
+ const isTest = config.projectSecret.startsWith('sk_dev_');
16
+ this.endpoint = config.endpoint ||
17
+ (isTest
18
+ ? constants_1.REBOUND_API_ENDPOINTS.TEST
19
+ : constants_1.REBOUND_API_ENDPOINTS.PRODUCTION) + '/dlq-injestion'; // Usar o sufixo apropriado
20
+ }
21
+ async execute(fn, payload) {
22
+ try {
23
+ return await fn();
24
+ }
25
+ catch (error) {
26
+ // MemoryQueue handles retries in the background, making this instant
27
+ this.reportError(error, payload).catch(e => {
28
+ // Silently fail if reportError fails, so we don't block the client logic
29
+ console.error("[Rebound SDK] Error building payload to DLQ", e);
30
+ });
31
+ throw error;
32
+ }
33
+ }
34
+ async reportError(error, payload) {
35
+ const fingerprint = this.generateFingerprint(error);
36
+ const encryptedPayload = this.encrypt(payload);
37
+ const data = {
38
+ projectId: this.projectId,
39
+ fingerprint,
40
+ error: {
41
+ name: error.name,
42
+ message: error.message,
43
+ stack: error.stack
44
+ },
45
+ payload: encryptedPayload
46
+ };
47
+ return this.sendToApi(data);
48
+ }
49
+ generateFingerprint(error) {
50
+ const stack = (error.stack || "").split("\n").slice(0, 3).join("");
51
+ return crypto_1.default.createHash('sha256')
52
+ .update(`${error.name}${stack}`)
53
+ .digest('hex');
54
+ }
55
+ encrypt(payload) {
56
+ const algorithm = 'aes-256-cbc';
57
+ // Use projectSecret to derive a 32-byte key.
58
+ // If it's not 32 bytes, we hash it to ensure it is.
59
+ const key = crypto_1.default.createHash('sha256').update(String(this.projectSecret)).digest('base64').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
+ // We return iv and encrypted data together, maybe separated by a colon
66
+ return `${iv.toString('hex')}:${encrypted}`;
67
+ }
68
+ async sendToApi(data) {
69
+ // Rely on MemoryQueue to guarantee delivery and offload network bottlenecks
70
+ queue_1.MemoryQueue.getInstance().enqueue(this.endpoint, this.projectSecret, data);
71
+ }
72
+ }
73
+ exports.DlqWrapper = DlqWrapper;
@@ -0,0 +1,7 @@
1
+ export { ReboundClient } from './core/client';
2
+ export { DlqWrapper } from './core/wrapper';
3
+ export type { DlqServiceConfig } from './core/wrapper';
4
+ export { HTTPAdapter } from './adapters/http.adapter';
5
+ export { KafkaAdapter } from './adapters/kafka.adapter';
6
+ export { ReboundError, ReboundAPIError } from './core/errors';
7
+ export type { DLQPayload, ReboundConfig, SendToDLQOptions } from './models/payload';
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ReboundAPIError = exports.ReboundError = exports.KafkaAdapter = exports.HTTPAdapter = exports.DlqWrapper = exports.ReboundClient = void 0;
4
+ var client_1 = require("./core/client");
5
+ Object.defineProperty(exports, "ReboundClient", { enumerable: true, get: function () { return client_1.ReboundClient; } });
6
+ var wrapper_1 = require("./core/wrapper");
7
+ Object.defineProperty(exports, "DlqWrapper", { enumerable: true, get: function () { return wrapper_1.DlqWrapper; } });
8
+ var http_adapter_1 = require("./adapters/http.adapter");
9
+ Object.defineProperty(exports, "HTTPAdapter", { enumerable: true, get: function () { return http_adapter_1.HTTPAdapter; } });
10
+ var kafka_adapter_1 = require("./adapters/kafka.adapter");
11
+ Object.defineProperty(exports, "KafkaAdapter", { enumerable: true, get: function () { return kafka_adapter_1.KafkaAdapter; } });
12
+ var errors_1 = require("./core/errors");
13
+ Object.defineProperty(exports, "ReboundError", { enumerable: true, get: function () { return errors_1.ReboundError; } });
14
+ Object.defineProperty(exports, "ReboundAPIError", { enumerable: true, get: function () { return errors_1.ReboundAPIError; } });
@@ -0,0 +1,23 @@
1
+ export interface DLQPayload {
2
+ payload: any;
3
+ source: string;
4
+ error?: {
5
+ message: string;
6
+ stack?: string;
7
+ code?: string;
8
+ name?: string;
9
+ };
10
+ metadata?: Record<string, any>;
11
+ timestamp?: number;
12
+ }
13
+ export interface ReboundConfig {
14
+ apiKey: string;
15
+ endpoint?: string;
16
+ timeout?: number;
17
+ }
18
+ export interface SendToDLQOptions {
19
+ payload: any;
20
+ error?: Error | string;
21
+ source?: string;
22
+ metadata?: Record<string, any>;
23
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@rebound-dlq/node",
3
+ "version": "0.1.0",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "files": ["dist"],
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "tsc --watch",
10
+ "json-server": "npx json-server --watch mock-server.json --port 3005",
11
+ "test:wrapper": "npx ts-node examples/wrapper-example.ts"
12
+ },
13
+ "devDependencies": {
14
+ "@types/node": "^20.0.0",
15
+ "json-server": "^1.0.0-beta.3",
16
+ "typescript": "^5.0.0"
17
+ }
18
+ }