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,11 +1,25 @@
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
|
||||||
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'test_secret_for_webhooks';
|
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 {
|
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 timestamp = Math.floor(Date.now() / 1000);
|
||||||
const bodyString = JSON.stringify(payload);
|
const bodyString = JSON.stringify(payload);
|
||||||
const dataToSign = `${timestamp}.${bodyString}`;
|
const dataToSign = `${timestamp}.${bodyString}`;
|
||||||
@@ -17,10 +31,10 @@ export class WebhookService {
|
|||||||
|
|
||||||
const signatureHeader = `t=${timestamp},v1=${signature}`;
|
const signatureHeader = `t=${timestamp},v1=${signature}`;
|
||||||
|
|
||||||
console.log(`[WEBHOOK] Dispatching event: ${event} (Idempotency: ${idempotencyKey})`);
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Assuming Node 20+, native fetch is available
|
console.log(`[WEBHOOK] Attempt ${attempt}/${MAX_RETRIES} — ${event} (${idempotencyKey})`);
|
||||||
|
|
||||||
const response = await fetch(WEBHOOK_URL, {
|
const response = await fetch(WEBHOOK_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -32,16 +46,26 @@ export class WebhookService {
|
|||||||
body: bodyString,
|
body: bodyString,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (response.ok) {
|
||||||
console.error(`[WEBHOOK] Dispatch failed with status: ${response.status}`);
|
console.log(`[WEBHOOK] ✓ Delivered: ${event}`);
|
||||||
} else {
|
return; // Success — stop retrying
|
||||||
console.log(`[WEBHOOK] Successfully dispatched event: ${event}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.warn(`[WEBHOOK] Server responded ${response.status} for ${event}`);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(`[WEBHOOK] Failed to dispatch webhook: ${error.message}`);
|
console.error(`[WEBHOOK] Attempt ${attempt} failed: ${error.message}`);
|
||||||
// In a real system, this would queue for retry with exponential backoff.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Exponential backoff: 1s, 2s, 4s
|
||||||
|
if (attempt < MAX_RETRIES) {
|
||||||
|
const delay = Math.pow(2, attempt - 1) * 1000;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`[WEBHOOK] ✗ Failed after ${MAX_RETRIES} attempts: ${event}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const webhookService = new WebhookService();
|
export const webhookService = new WebhookService();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user