feat: implement asynchronous webhook service with retries and add environment configuration

This commit is contained in:
2026-08-19 15:14:22 +05:30
parent ea49fe800f
commit 7bfb786e15
2 changed files with 72 additions and 25 deletions
+23
View File
@@ -0,0 +1,23 @@
# Server
PORT=8080
# Database
DATABASE_URL=postgres://postgres:postgres@localhost:5432/ai_pharmacy
# JWT Authentication
JWT_SECRET=your_jwt_secret_here
# OpenAI
OPENAI_API_KEY=your_openai_api_key_here
# AWS S3
S3_KEY=your_s3_access_key_here
S3_SECRET=your_s3_secret_key_here
S3_BUCKET=your_bucket_name
S3_REGION=us-east-1
S3_BASE_PATH=
S3_PUBLIC_BASE_PATH=
# Webhooks
WEBHOOK_SECRET=your_webhook_secret_here
WEBHOOK_URL=http://localhost:8081/webhooks/pharmacy
+49 -25
View File
@@ -1,15 +1,29 @@
import crypto from 'crypto';
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'test_secret_for_webhooks';
// For demo purposes, we log it or hit a dummy URL.
const WEBHOOK_URL = process.env.WEBHOOK_URL || 'http://localhost:8081/webhooks/pharmacy';
const WEBHOOK_URL = process.env.WEBHOOK_URL || 'http://localhost:8081/webhooks/pharmacy';
const MAX_RETRIES = 3;
export class WebhookService {
async dispatchEvent(event: string, idempotencyKey: string, payload: any) {
/**
* Fire-and-forget: dispatches the webhook in the background.
* Dispensing will NEVER fail because of a webhook issue.
*/
dispatchEvent(event: string, idempotencyKey: string, payload: any) {
// Run in background — no await, no blocking
this._sendWithRetry(event, idempotencyKey, payload).catch((err) => {
console.error(`[WEBHOOK] All retries failed for ${event}: ${err.message}`);
});
}
/**
* Internal: send webhook with exponential backoff retry.
*/
private async _sendWithRetry(event: string, idempotencyKey: string, payload: any) {
const timestamp = Math.floor(Date.now() / 1000);
const bodyString = JSON.stringify(payload);
const dataToSign = `${timestamp}.${bodyString}`;
const signature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(dataToSign)
@@ -17,31 +31,41 @@ export class WebhookService {
const signatureHeader = `t=${timestamp},v1=${signature}`;
console.log(`[WEBHOOK] Dispatching event: ${event} (Idempotency: ${idempotencyKey})`);
try {
// Assuming Node 20+, native fetch is available
const response = await fetch(WEBHOOK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Pharmacy-Event': event,
'X-Pharmacy-Signature': signatureHeader,
'Idempotency-Key': idempotencyKey,
},
body: bodyString,
});
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
console.log(`[WEBHOOK] Attempt ${attempt}/${MAX_RETRIES} — ${event} (${idempotencyKey})`);
if (!response.ok) {
console.error(`[WEBHOOK] Dispatch failed with status: ${response.status}`);
} else {
console.log(`[WEBHOOK] Successfully dispatched event: ${event}`);
const response = await fetch(WEBHOOK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Pharmacy-Event': event,
'X-Pharmacy-Signature': signatureHeader,
'Idempotency-Key': idempotencyKey,
},
body: bodyString,
});
if (response.ok) {
console.log(`[WEBHOOK] ✓ Delivered: ${event}`);
return; // Success — stop retrying
}
console.warn(`[WEBHOOK] Server responded ${response.status} for ${event}`);
} catch (error: any) {
console.error(`[WEBHOOK] Attempt ${attempt} failed: ${error.message}`);
}
// Exponential backoff: 1s, 2s, 4s
if (attempt < MAX_RETRIES) {
const delay = Math.pow(2, attempt - 1) * 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
}
} catch (error: any) {
console.error(`[WEBHOOK] Failed to dispatch webhook: ${error.message}`);
// In a real system, this would queue for retry with exponential backoff.
}
console.error(`[WEBHOOK] ✗ Failed after ${MAX_RETRIES} attempts: ${event}`);
}
}
export const webhookService = new WebhookService();