138 lines
3.9 KiB
JavaScript
138 lines
3.9 KiB
JavaScript
/**
|
|
* @fileoverview JetStream consumer that POSTs Zapier REST Hook payloads.
|
|
* @module workers/webhookWorker
|
|
*/
|
|
|
|
import { createDebugger } from '../debug/logger.js';
|
|
import { SUBJECTS, CONSUMERS, STREAMS } from '../nats/subjects.js';
|
|
import { connectNats, ensureStreams } from '../nats/connection.js';
|
|
import { deliverWebhook } from '../services/webhookService.js';
|
|
import { getActiveWebhooks } from '../store/webhooks.js';
|
|
import { withTrace } from '../debug/trace.js';
|
|
|
|
const log = createDebugger('webhooks');
|
|
|
|
let running = false;
|
|
/** @type {AbortController|null} */
|
|
let abort = null;
|
|
|
|
/**
|
|
* Route job events → per-hook deliver messages (inline or via re-publish).
|
|
* Also handles direct deliver subjects.
|
|
*
|
|
* @param {object} data
|
|
* @param {{ ack: () => Promise<void>, nak: (d?: number) => Promise<void> }} ctrl
|
|
*/
|
|
async function handleDeliver(data, ctrl) {
|
|
await withTrace({ traceId: data.traceId, span: 'webhook-deliver' }, async () => {
|
|
// Event router path: expand tenant hooks
|
|
if (data.event && data.jobId && !data.targetUrl) {
|
|
const hooks = getActiveWebhooks(data.tenantId, data.event);
|
|
for (const hook of hooks) {
|
|
await deliverWebhook(hook.targetUrl, {
|
|
event: data.event,
|
|
jobId: data.jobId,
|
|
tenantId: data.tenantId,
|
|
status: data.status,
|
|
});
|
|
}
|
|
await ctrl.ack();
|
|
return;
|
|
}
|
|
|
|
if (!data.targetUrl) {
|
|
log.warn('deliver missing targetUrl', { dataKeys: Object.keys(data) });
|
|
await ctrl.ack();
|
|
return;
|
|
}
|
|
|
|
const result = await deliverWebhook(data.targetUrl, data.payload ?? data);
|
|
if (result.ok) {
|
|
await ctrl.ack();
|
|
} else {
|
|
log.debug('deliver non-2xx, nak', { status: result.status });
|
|
await ctrl.nak(2000);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Start webhook delivery worker (consumes WEBHOOKS stream + optional events).
|
|
* @returns {Promise<{ stop: () => Promise<void> }>}
|
|
*/
|
|
export async function startWebhookWorker() {
|
|
if (running) {
|
|
return { stop: async () => stopWebhookWorker() };
|
|
}
|
|
|
|
const { js, jsm } = await connectNats();
|
|
await ensureStreams(jsm);
|
|
|
|
// Events consumer → deliver
|
|
try {
|
|
await jsm.consumers.add(STREAMS.ZAPIER_EVENTS, {
|
|
durable_name: CONSUMERS.EVENT_WEBHOOK_ROUTER,
|
|
ack_policy: 'explicit',
|
|
filter_subject: SUBJECTS.JOBS_EVENTS,
|
|
max_deliver: 10,
|
|
});
|
|
} catch (err) {
|
|
log.debug('events consumer may exist', { error: err.message });
|
|
}
|
|
|
|
try {
|
|
await jsm.consumers.add(STREAMS.ZAPIER_WEBHOOKS, {
|
|
durable_name: CONSUMERS.WEBHOOK_DELIVER,
|
|
ack_policy: 'explicit',
|
|
filter_subject: SUBJECTS.WEBHOOKS_DELIVER,
|
|
max_deliver: 10,
|
|
});
|
|
} catch (err) {
|
|
log.debug('webhook consumer may exist', { error: err.message });
|
|
}
|
|
|
|
abort = new AbortController();
|
|
running = true;
|
|
log.info('webhook worker started');
|
|
|
|
const runConsumer = async (stream, durable) => {
|
|
const consumer = await js.consumers.get(stream, durable);
|
|
const messages = await consumer.consume({ max_messages: 10 });
|
|
for await (const msg of messages) {
|
|
if (abort?.signal.aborted) break;
|
|
try {
|
|
const data = JSON.parse(msg.string());
|
|
await handleDeliver(data, {
|
|
ack: () => msg.ack(),
|
|
nak: (d = 1000) => msg.nak(d),
|
|
});
|
|
} catch (err) {
|
|
log.error('webhook handle failed', { error: err.message });
|
|
try {
|
|
msg.nak(1000);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
runConsumer(STREAMS.ZAPIER_EVENTS, CONSUMERS.EVENT_WEBHOOK_ROUTER).catch((err) =>
|
|
log.error('events consumer failed', { error: err.message }),
|
|
);
|
|
runConsumer(STREAMS.ZAPIER_WEBHOOKS, CONSUMERS.WEBHOOK_DELIVER).catch((err) =>
|
|
log.error('webhooks consumer failed', { error: err.message }),
|
|
);
|
|
|
|
return { stop: async () => stopWebhookWorker() };
|
|
}
|
|
|
|
/**
|
|
* @returns {Promise<void>}
|
|
*/
|
|
export async function stopWebhookWorker() {
|
|
abort?.abort();
|
|
abort = null;
|
|
running = false;
|
|
log.info('webhook worker stopped');
|
|
}
|