feat: implement asynchronous webhook service with retries and add environment configuration
This commit is contained in:
@@ -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
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user