notifkit 0.1.4 → 0.1.6

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.
@@ -1,4 +1,4 @@
1
- import { J as PendingMessageScanner, Jt as buildStreamEvent, L as CircuitBreaker, Q as metrics, Qt as OUTBOUND_STREAMS, R as BatchProcessor, W as getPriorityBucket, X as StreamProducer, Y as StreamConsumer, Yt as CONSUMER_GROUPS, an as registry, c as BaseWorker, dn as baseConfigSchema, en as STREAMS, et as createLogger, it as createDatabase, k as RedisClient, l as NonRetryableError, mn as parseConfig, ot as deliveryOutbox, pn as loadEnv, q as globalEmitter, rt as IdempotencyGuard, u as startHealthReporter, ut as scheduledPayloads, x as ContactRepository } from "./src-vG79L-8m.mjs";
1
+ import { J as PendingMessageScanner, Jt as buildStreamEvent, L as CircuitBreaker, Q as metrics, Qt as OUTBOUND_STREAMS, R as BatchProcessor, W as getPriorityBucket, X as StreamProducer, Y as StreamConsumer, Yt as CONSUMER_GROUPS, an as registry, c as BaseWorker, dn as baseConfigSchema, en as STREAMS, et as createLogger, it as createDatabase, k as RedisClient, l as NonRetryableError, mn as parseConfig, ot as deliveryOutbox, pn as loadEnv, q as globalEmitter, rt as IdempotencyGuard, u as startHealthReporter, ut as scheduledPayloads, x as ContactRepository } from "./src-CpUnBl_M.mjs";
2
2
  import { transportRegistry } from "./index.mjs";
3
3
  //#region src/services/delivery/throttle.ts
4
4
  const LUA_THROTTLE = `
@@ -629,4 +629,4 @@ async function stopDeliveryWorker() {
629
629
  //#endregion
630
630
  export { startDeliveryWorker, stopDeliveryWorker };
631
631
 
632
- //# sourceMappingURL=main-CiigNpsP.mjs.map
632
+ //# sourceMappingURL=main-DdLzptZE.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"main-CiigNpsP.mjs","names":[],"sources":["../src/services/delivery/throttle.ts","../src/services/delivery/main.ts"],"sourcesContent":["import type { Redis } from \"ioredis\";\n\nexport interface ThrottleResult {\n allowed: boolean;\n retryAfterMs: number;\n}\n\nconst LUA_THROTTLE = `\n local key = KEYS[1]\n local now = tonumber(ARGV[1])\n local windowSeconds = tonumber(ARGV[2])\n local limit = tonumber(ARGV[3])\n local member = ARGV[4]\n\n local clearBefore = now - (windowSeconds * 1000)\n \n -- Cleanup expired scores\n redis.call('ZREMRANGEBYSCORE', key, 0, clearBefore)\n \n -- Get current count\n local count = redis.call('ZCARD', key)\n \n if count >= limit then\n -- Find the oldest score\n local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')\n if oldest and oldest[2] then\n return {0, tonumber(oldest[2])}\n end\n return {0, now}\n end\n \n -- Add new request\n redis.call('ZADD', key, now, member)\n redis.call('EXPIRE', key, windowSeconds * 2)\n return {1, 0}\n`;\n\nexport async function throttleProvider(\n redis: Redis,\n channel: string,\n config: { limit: number; windowSeconds: number },\n logger: any,\n): Promise<ThrottleResult> {\n const key = `rate-limit:provider:${channel}`;\n const now = Date.now();\n const zmember = `${now}:${Math.random()}`;\n\n const result = (await redis.eval(\n LUA_THROTTLE,\n 1,\n key,\n now.toString(),\n config.windowSeconds.toString(),\n config.limit.toString(),\n zmember,\n )) as [number, number];\n\n const allowed = result[0] === 1;\n const oldestTimestamp = result[1];\n\n let retryAfterMs = 0;\n if (!allowed) {\n retryAfterMs = Math.max(0, oldestTimestamp + config.windowSeconds * 1000 - now);\n logger.warn(\n { channel, limit: config.limit, windowSeconds: config.windowSeconds, retryAfterMs },\n \"Provider rate limit hit — task must be rescheduled\",\n );\n }\n\n return { allowed, retryAfterMs };\n}\n","import { loadEnv, parseConfig, baseConfigSchema } from \"@/index.js\";\nimport { createLogger } from \"@/index.js\";\nimport { RedisClient, type Redis } from \"@/index.js\";\nimport {\n StreamConsumer,\n PendingMessageScanner,\n type StreamMessage,\n StreamProducer,\n} from \"@/index.js\";\nimport { BaseWorker } from \"@/index.js\";\nimport {\n STREAMS,\n OUTBOUND_STREAMS,\n CONSUMER_GROUPS,\n registry,\n buildStreamEvent,\n type NotificationDispatchedPayload,\n} from \"@/index.js\";\nimport { type StreamName } from \"@/contracts/streams.js\";\nimport { createDatabase } from \"@/db/index.js\";\nimport { deliveryOutbox, scheduledPayloads } from \"@/db/schema.js\";\nimport { ContactRepository, IdempotencyGuard } from \"@/index.js\";\nimport { transportRegistry } from \"@/index.js\";\nimport { globalEmitter, getPriorityBucket, type WorkerOptions } from \"@/shared/index.js\";\nimport { startHealthReporter, NonRetryableError } from \"@/workers/index.js\";\nimport { BatchProcessor, CircuitBreaker } from \"@/shared/index.js\";\nimport { throttleProvider } from \"./throttle.js\";\nimport { metrics } from \"@/metrics/index.js\";\n\n// ─── App-specific config ────────────────────────────────────────────────────\n\nconst deliveryConfigSchema = baseConfigSchema.extend({});\n\n// ─── Bootstrap ─────────────────────────────────────────────────────────────\n\nloadEnv();\nconst config = parseConfig(deliveryConfigSchema, process.env);\n\nlet logger: ReturnType<typeof createLogger>;\nlet redis: RedisClient;\nlet sql: any;\nlet db: any;\n\nlet consumer: StreamConsumer;\nlet pendingScanner: PendingMessageScanner;\nlet worker: BaseWorker;\nlet scheduledProducer: StreamProducer;\nlet enrichedProducers: Record<string, StreamProducer>;\nlet healthInterval: NodeJS.Timeout | null = null;\n\n(global as any)._telemetry = (global as any)._telemetry || {\n count: 0,\n insert: 0,\n provider: 0,\n flush: 0,\n ack: 0,\n dequeue: 0,\n dbupdate: 0,\n flushCount: 0,\n};\n\nexport interface DeliveryWorkerOptions extends WorkerOptions {\n transportRegistry: any;\n idempotency: any;\n redis: Redis;\n scheduledProducer: any;\n enrichedProducers: any;\n contactRepo: any;\n eventsProducer: any;\n globalEmitter: any;\n db: any;\n}\n\nexport class DeliveryWorker extends BaseWorker {\n private readonly transportRegistry: any;\n private readonly idempotency: any;\n private readonly redisCli: Redis;\n private readonly scheduledProducer: any;\n private readonly enrichedProducers: any;\n private readonly contactRepo: any;\n private readonly eventsProducer: any;\n private readonly globalEmitter: any;\n private readonly db: any;\n\n private eventProcessor: BatchProcessor<any, void>;\n private outboxUpdateProcessor: BatchProcessor<any, void>;\n private outboxInsertProcessor: BatchProcessor<any, boolean>;\n private breakers = new Map<string, CircuitBreaker>();\n\n constructor(options: DeliveryWorkerOptions) {\n super(options);\n this.transportRegistry = options.transportRegistry;\n this.idempotency = options.idempotency;\n this.redisCli = options.redis;\n this.scheduledProducer = options.scheduledProducer;\n this.enrichedProducers = options.enrichedProducers;\n this.contactRepo = options.contactRepo;\n this.eventsProducer = options.eventsProducer;\n this.globalEmitter = options.globalEmitter;\n this.db = options.db;\n\n this.eventProcessor = new BatchProcessor<any, void>(1000, 100, async (events) => {\n await this.eventsProducer.publishBatch(events);\n return events.map(() => undefined as void);\n });\n\n this.outboxUpdateProcessor = new BatchProcessor<any, void>(500, 100, async (updates) => {\n const { sql } = await import(\"drizzle-orm\");\n const values = updates.map((update) => ({\n taskId: update.taskId,\n channel: update.channel,\n destination: update.destination,\n providerMessageId: update.providerMessageId,\n }));\n\n const tDbUpdateStart = Date.now();\n await this.db\n .insert(deliveryOutbox)\n .values(values)\n .onConflictDoUpdate({\n target: [deliveryOutbox.taskId, deliveryOutbox.channel, deliveryOutbox.destination],\n set: { providerMessageId: sql`EXCLUDED.provider_message_id` },\n })\n .catch((e: any) => this.logger.error({ err: e }, \"background update failed\"));\n\n (global as any)._telemetry.dbupdate += Date.now() - tDbUpdateStart;\n (global as any)._telemetry.flushCount++;\n return updates.map(() => undefined as void);\n });\n\n this.outboxInsertProcessor = new BatchProcessor(500, 10, async (tasks) => {\n const values = tasks.map((task: any) => ({\n taskId: task.taskId,\n channel: task.channel,\n destination: task.destination,\n }));\n\n await this.db.insert(deliveryOutbox).values(values).onConflictDoNothing();\n return tasks.map(() => true);\n });\n }\n\n override async stop(): Promise<void> {\n await Promise.all([\n this.eventProcessor.flush(),\n this.outboxUpdateProcessor.flush(),\n this.outboxInsertProcessor.flush(),\n ]);\n await super.stop();\n }\n\n private getBreaker(name: string): CircuitBreaker {\n let breaker = this.breakers.get(name);\n if (!breaker) {\n breaker = new CircuitBreaker({ failureThreshold: 5, resetTimeoutMs: 30000 });\n this.breakers.set(name, breaker);\n }\n return breaker;\n }\n\n async process(message: StreamMessage, attempt: number = 1): Promise<void> {\n const { event } = message;\n const publishPromises: Promise<void>[] = [];\n\n const payloadResult = registry.safeParsePayload(\"notification.dispatched\", event.payload);\n if (!payloadResult.success) {\n this.logger.warn(\n { messageId: message.id, issues: payloadResult.error.issues },\n \"invalid notification.dispatched payload — skipping\",\n );\n return;\n }\n\n const task = payloadResult.data as NotificationDispatchedPayload;\n\n const fallbackToNextChannel = async (reason: string) => {\n if (task.fallbackChain && task.fallbackChain.length > 0 && task.recipient) {\n const nextChannel = task.fallbackChain[0];\n const remainingChain = task.fallbackChain.slice(1);\n\n const fallbackPayload: any = {\n // NotificationEnrichedPayload\n projectId: task.projectId,\n rawEventId: task.enrichedEventId,\n recipientId: task.recipientId,\n channel: nextChannel,\n priority: task.priority,\n templateId: task.templateId,\n templateVariables: task.templateVariables,\n aiPrompts: task.aiPrompts,\n recipient: task.recipient,\n scheduledAt: undefined,\n fallbackChain: remainingChain.length > 0 ? remainingChain : undefined,\n };\n\n const p = getPriorityBucket(task.priority);\n const producer = this.enrichedProducers[p] ?? this.enrichedProducers[\"normal\"]!;\n await producer.publish(\n buildStreamEvent(\n \"notification.enriched\",\n fallbackPayload,\n \"delivery\",\n event.metadata.traceId,\n ),\n );\n\n this.logger.info(\n { taskId: task.taskId, reason, nextChannel, traceId: event.metadata.traceId },\n \"Delivery failed completely, rolling over to next channel in fallback chain\",\n );\n return true; // Indicates fallback was triggered\n }\n return false; // No fallback\n };\n\n const transports = this.transportRegistry.getAll(task.channel);\n\n if (transports.length === 0) {\n this.logger.warn(\n { taskId: task.taskId, channel: task.channel },\n \"no transport registered for channel — dropping\",\n );\n const fallbackTriggered = await fallbackToNextChannel(\"no_transport\");\n if (!fallbackTriggered) {\n this.globalEmitter.emit(\n \"delivery:failed\",\n task.taskId,\n \"no transport\",\n task.channel,\n task.projectId,\n );\n await this.eventProcessor.add(\n buildStreamEvent(\n \"notification.failed\",\n {\n projectId: task.projectId,\n taskId: task.taskId,\n enrichedEventId: task.enrichedEventId,\n recipientId: task.recipientId,\n channel: task.channel,\n failureReason: \"no transport registered for channel\",\n failureCode: \"no_transport\",\n retryable: false,\n attempt,\n templateId: task.templateId,\n workflowInstanceId:\n event.metadata.source === \"workflow\" ? event.metadata.traceId : undefined,\n campaignId: task.campaignId,\n },\n \"delivery\",\n event.metadata.traceId,\n ),\n );\n }\n return;\n }\n\n const limitConfig = transports[0].limits;\n if (limitConfig) {\n const { allowed, retryAfterMs } = await throttleProvider(\n this.redisCli,\n task.channel,\n limitConfig,\n this.logger,\n );\n if (!allowed) {\n task.throttleAttemptCount = (task.throttleAttemptCount ?? 0) + 1;\n const maxAttempts = task.deliveryOptions?.maxAttempts ?? 3;\n\n if (task.throttleAttemptCount > maxAttempts) {\n this.logger.warn(\n { messageId: message.id, taskId: task.taskId, attempts: task.throttleAttemptCount },\n \"provider rate limit max attempts exceeded\",\n );\n const fallbackTriggered = await fallbackToNextChannel(\"provider_throttle_exceeded\");\n if (!fallbackTriggered) {\n this.globalEmitter.emit(\n \"delivery:failed\",\n task.taskId,\n \"provider throttle exceeded\",\n task.channel,\n task.projectId,\n );\n await this.eventProcessor.add(\n buildStreamEvent(\n \"notification.failed\",\n {\n projectId: task.projectId,\n taskId: task.taskId,\n enrichedEventId: task.enrichedEventId,\n recipientId: task.recipientId,\n channel: task.channel,\n failureReason: \"provider rate limit max attempts exceeded\",\n failureCode: \"provider_throttle_exceeded\",\n retryable: false,\n attempt,\n templateId: task.templateId,\n workflowInstanceId:\n event.metadata.source === \"workflow\" ? event.metadata.traceId : undefined,\n campaignId: task.campaignId,\n },\n \"delivery\",\n event.metadata.traceId,\n ),\n );\n }\n return;\n }\n\n const { sql } = await import(\"drizzle-orm\");\n await this.db\n .insert(scheduledPayloads)\n .values({\n taskId: task.taskId,\n payload: task,\n })\n .onConflictDoUpdate({\n target: scheduledPayloads.taskId,\n set: { payload: sql`EXCLUDED.payload` },\n });\n\n await this.scheduledProducer.publish(\n buildStreamEvent(\n \"notification.scheduled\",\n {\n projectId: task.projectId,\n enrichedEventId: task.enrichedEventId,\n taskId: task.taskId,\n scheduledAt: new Date(Date.now() + retryAfterMs).toISOString(),\n throttleAttemptCount: task.throttleAttemptCount,\n },\n \"delivery\",\n `${task.taskId}:throttle:${Date.now()}`,\n ),\n );\n\n return;\n }\n }\n\n const tInsertStart = Date.now();\n const idempotencyKey = task.taskId;\n if (!(await this.idempotency.checkAndMark(idempotencyKey, 60))) {\n this.logger.info(\n { messageId: message.id, taskId: task.taskId, channel: task.channel, attempt },\n \"duplicate delivery — skipping\",\n );\n return;\n }\n\n try {\n await this.outboxInsertProcessor.add(task);\n const insertTime = Date.now() - tInsertStart;\n\n publishPromises.push(\n this.eventProcessor.add(\n buildStreamEvent(\n \"notification.dispatched\",\n {\n projectId: task.projectId,\n taskId: task.taskId,\n enrichedEventId: task.enrichedEventId,\n recipientId: task.recipientId,\n channel: task.channel,\n templateId: task.templateId,\n attempt,\n workflowInstanceId:\n event.metadata.source === \"workflow\" ? event.metadata.traceId : undefined,\n campaignId: task.campaignId,\n },\n \"delivery\",\n event.metadata.traceId,\n ),\n ),\n );\n\n // For push: we just need to send to the pre-resolved destination,\n // but if it's invalid, we deactivate it.\n if (task.channel === \"push\") {\n let lastResult: any = { success: false, error: \"No transports\" };\n\n for (const transport of transports) {\n try {\n const tProv = Date.now();\n const breaker = this.getBreaker(`${task.channel}:${transport.constructor.name}`);\n\n lastResult = await breaker.execute(async () => {\n const timeoutMs = task.deliveryOptions?.timeoutMs ?? 10_000;\n const controller = new AbortController();\n const timeout = setTimeout(() => {\n controller.abort(new Error(`Transport timeout after ${timeoutMs}ms`));\n }, timeoutMs);\n\n try {\n (task as any).signal = controller.signal;\n const res: any = await Promise.race([\n transport.send(task),\n new Promise((_, reject) => {\n if (controller.signal.aborted) return reject(controller.signal.reason);\n controller.signal.addEventListener(\"abort\", () =>\n reject(controller.signal.reason),\n );\n }),\n ]);\n if (!res.success && !res.invalidToken) {\n throw new Error(res.error ?? \"Transport failed\");\n }\n return res;\n } finally {\n clearTimeout(timeout);\n }\n });\n\n (global as any)._telemetry.provider += Date.now() - tProv;\n if (lastResult.success || lastResult.invalidToken) break;\n } catch (err: any) {\n lastResult = { success: false, error: err.message };\n }\n }\n\n if (lastResult.invalidToken) {\n await this.contactRepo.deactivate(\n task.projectId,\n task.recipientId,\n task.channel as any,\n task.destination,\n );\n this.logger.info(\n { channel: task.channel, target: task.destination },\n \"deactivated invalid contact target\",\n );\n this.globalEmitter.emit(\n \"delivery:failed\",\n task.taskId,\n \"invalidToken\",\n task.channel,\n task.projectId,\n );\n metrics.deliveryFailed.inc({ channel: task.channel, reason: \"invalid_token\" });\n\n publishPromises.push(\n this.eventProcessor.add(\n buildStreamEvent(\n \"notification.failed\",\n {\n projectId: task.projectId,\n taskId: task.taskId,\n enrichedEventId: task.enrichedEventId,\n recipientId: task.recipientId,\n channel: task.channel,\n failureReason: \"invalid_token\",\n failureCode: \"push_failure\",\n retryable: false,\n attempt,\n templateId: task.templateId,\n workflowInstanceId:\n event.metadata.source === \"workflow\" ? event.metadata.traceId : undefined,\n campaignId: task.campaignId,\n },\n \"delivery\",\n event.metadata.traceId,\n ),\n ),\n );\n\n const fallbackTriggered = await fallbackToNextChannel(\"invalid_token\");\n if (!fallbackTriggered) {\n throw new NonRetryableError(\"Push delivery failed: invalid token\");\n }\n } else if (lastResult.success) {\n // Record providerMessageId in outbox before publishing event\n const providerMessageId = lastResult.providerMessageId || \"push-success\";\n\n publishPromises.push(\n this.outboxUpdateProcessor.add({\n taskId: task.taskId,\n channel: task.channel,\n destination: task.destination,\n providerMessageId,\n }),\n );\n\n this.logger.debug(\n { taskId: task.taskId, messageId: providerMessageId },\n \"push delivered\",\n );\n this.globalEmitter.emit(\n \"delivery:delivered\",\n task.taskId,\n providerMessageId,\n task.channel,\n task.projectId,\n );\n metrics.deliverySuccess.inc({ channel: task.channel });\n\n publishPromises.push(\n this.eventProcessor.add(\n buildStreamEvent(\n \"notification.delivered\",\n {\n projectId: task.projectId,\n taskId: task.taskId,\n enrichedEventId: task.enrichedEventId,\n channel: task.channel,\n deliveredAt: new Date().toISOString(),\n providerMessageId,\n templateId: task.templateId,\n workflowInstanceId:\n event.metadata.source === \"workflow\" ? event.metadata.traceId : undefined,\n campaignId: task.campaignId,\n },\n \"delivery\",\n event.metadata.traceId,\n ),\n ),\n );\n } else {\n this.logger.warn(\n { taskId: task.taskId, error: lastResult.error },\n \"push delivery failed\",\n );\n this.globalEmitter.emit(\n \"delivery:failed\",\n task.taskId,\n lastResult.error ?? \"push delivery failed\",\n task.channel,\n task.projectId,\n );\n metrics.deliveryFailed.inc({ channel: task.channel, reason: \"push_error\" });\n\n publishPromises.push(\n this.eventProcessor.add(\n buildStreamEvent(\n \"notification.failed\",\n {\n projectId: task.projectId,\n taskId: task.taskId,\n enrichedEventId: task.enrichedEventId,\n recipientId: task.recipientId,\n channel: task.channel,\n failureReason: lastResult.error ?? \"push delivery failed\",\n failureCode: \"push_failure\",\n retryable: false,\n attempt,\n templateId: task.templateId,\n workflowInstanceId:\n event.metadata.source === \"workflow\" ? event.metadata.traceId : undefined,\n campaignId: task.campaignId,\n },\n \"delivery\",\n event.metadata.traceId,\n ),\n ),\n );\n\n const fallbackTriggered = await fallbackToNextChannel(\"push_delivery_failed\");\n if (!fallbackTriggered) {\n throw new NonRetryableError(lastResult.error ?? \"Push delivery failed\");\n }\n }\n } else {\n // Other channels (email, sms, webhook, whatsapp) use the pre-resolved destination\n let result: any = { success: false, error: \"No transports\" };\n for (const transport of transports) {\n try {\n const tProv = Date.now();\n const breaker = this.getBreaker(`${task.channel}:${transport.constructor.name}`);\n\n result = await breaker.execute(async () => {\n const timeoutMs = task.deliveryOptions?.timeoutMs ?? 10_000;\n const controller = new AbortController();\n const timeout = setTimeout(() => {\n controller.abort(new Error(`Transport timeout after ${timeoutMs}ms`));\n }, timeoutMs);\n\n try {\n (task as any).signal = controller.signal;\n const res: any = await Promise.race([\n transport.send(task),\n new Promise((_, reject) => {\n if (controller.signal.aborted) return reject(controller.signal.reason);\n controller.signal.addEventListener(\"abort\", () =>\n reject(controller.signal.reason),\n );\n }),\n ]);\n if (!res.success) {\n throw new Error(res.error ?? \"Transport failed\");\n }\n return res;\n } finally {\n clearTimeout(timeout);\n }\n });\n\n (global as any)._telemetry.provider += Date.now() - tProv;\n if (result.success) break;\n } catch (err: any) {\n result = { success: false, error: err.message };\n }\n }\n\n if (result.success) {\n // Record providerMessageId in outbox before publishing event\n const providerMessageId = result.providerMessageId || \"success\";\n\n publishPromises.push(\n this.outboxUpdateProcessor.add({\n taskId: task.taskId,\n channel: task.channel,\n destination: task.destination,\n providerMessageId,\n }),\n );\n\n this.globalEmitter.emit(\n \"delivery:delivered\",\n task.taskId,\n providerMessageId,\n task.channel,\n task.projectId,\n );\n metrics.deliverySuccess.inc({ channel: task.channel });\n\n publishPromises.push(\n this.eventProcessor.add(\n buildStreamEvent(\n \"notification.delivered\",\n {\n projectId: task.projectId,\n taskId: task.taskId,\n enrichedEventId: task.enrichedEventId,\n channel: task.channel,\n deliveredAt: new Date().toISOString(),\n providerMessageId,\n templateId: task.templateId,\n workflowInstanceId:\n event.metadata.source === \"workflow\" ? event.metadata.traceId : undefined,\n campaignId: task.campaignId,\n },\n \"delivery\",\n event.metadata.traceId,\n ),\n ),\n );\n } else {\n publishPromises.push(\n this.eventProcessor.add(\n buildStreamEvent(\n \"notification.failed\",\n {\n projectId: task.projectId,\n taskId: task.taskId,\n enrichedEventId: task.enrichedEventId,\n channel: task.channel,\n failureReason: result.error ?? \"delivery failed completely\",\n failureCode: \"provider_error\",\n retryable: false,\n attempt,\n templateId: task.templateId,\n workflowInstanceId:\n event.metadata.source === \"workflow\" ? event.metadata.traceId : undefined,\n campaignId: task.campaignId,\n },\n \"delivery\",\n event.metadata.traceId,\n ),\n ),\n );\n }\n\n if (!result.success) {\n this.logger.warn(\n { taskId: task.taskId, channel: task.channel, error: result.error },\n \"delivery failed completely across providers\",\n );\n this.globalEmitter.emit(\n \"delivery:failed\",\n task.taskId,\n result.error ?? \"delivery failed completely\",\n task.channel,\n task.projectId,\n );\n metrics.deliveryFailed.inc({ channel: task.channel, reason: \"provider_error\" });\n\n const fallbackTriggered = await fallbackToNextChannel(\"all_providers_failed\");\n if (!fallbackTriggered) {\n throw new NonRetryableError(result.error ?? \"delivery failed\");\n }\n } else {\n this.logger.info(\n { taskId: task.taskId, channel: task.channel, messageId: result.providerMessageId },\n \"notification delivered\",\n );\n }\n }\n\n const tFlushStart = Date.now();\n await Promise.all(publishPromises).catch((err) => {\n this.logger.error(\n { err, taskId: task.taskId },\n \"failed to publish post-dispatch events, swallowing error to prevent duplicate delivery\",\n );\n });\n const flushTime = Date.now() - tFlushStart;\n\n await this.idempotency.markProcessed(idempotencyKey);\n\n const t = ((global as any)._telemetry = (global as any)._telemetry || {\n count: 0,\n insert: 0,\n provider: 0,\n flush: 0,\n ack: 0,\n });\n t.count++;\n t.insert += insertTime;\n t.flush += flushTime;\n\n if (t.count % 1000 === 0) {\n this.logger.debug(\n `[Metrics 1000 msgs] Dequeue: ${t.dequeue / 1000}ms, DB Insert: ${t.insert / 1000}ms, Provider: ${t.provider / 1000}ms, Wait for Flush: ${t.flush / 1000}ms, Ack: ${t.ack / 1000}ms | DB Update (avg per flush): ${t.dbupdate / Math.max(1, t.flushCount)}ms`,\n );\n t.count = 0;\n t.insert = 0;\n t.provider = 0;\n t.flush = 0;\n t.dequeue = 0;\n t.ack = 0;\n t.dbupdate = 0;\n t.flushCount = 0;\n }\n } catch (err) {\n await this.idempotency.unmark(idempotencyKey).catch(() => {});\n throw err;\n }\n }\n}\n\nexport async function startDeliveryWorker() {\n logger = createLogger({ name: \"delivery\", level: config.LOG_LEVEL });\n\n redis = new RedisClient({ url: config.REDIS_URL, name: \"delivery\", logger });\n const dbData = createDatabase({ url: config.DATABASE_URL, applicationName: \"delivery\", logger });\n sql = dbData.sql;\n db = dbData.db;\n consumer = new StreamConsumer({\n redis: redis.native,\n stream: OUTBOUND_STREAMS as unknown as StreamName[],\n group: CONSUMER_GROUPS.DELIVERY,\n consumer: `delivery-${process.pid}`,\n dlqStream: STREAMS.DEAD_LETTER,\n batchSize: config.WORKER_CONCURRENCY,\n logger,\n });\n\n scheduledProducer = new StreamProducer({\n redis: redis.native,\n stream: STREAMS.SCHEDULED,\n logger,\n });\n\n pendingScanner = new PendingMessageScanner({\n redis: redis.native,\n stream: OUTBOUND_STREAMS as unknown as StreamName[],\n group: CONSUMER_GROUPS.DELIVERY,\n consumer: `delivery-${process.pid}`,\n logger,\n });\n\n enrichedProducers = {\n critical: new StreamProducer({\n redis: redis.native,\n stream: STREAMS.ENRICHED_CRITICAL,\n logger,\n }),\n normal: new StreamProducer({ redis: redis.native, stream: STREAMS.ENRICHED_NORMAL, logger }),\n low: new StreamProducer({ redis: redis.native, stream: STREAMS.ENRICHED_LOW, logger }),\n };\n\n const contactRepo = new ContactRepository(db);\n\n const idempotency = new IdempotencyGuard({\n redis: redis.native,\n keyPrefix: \"notif:processed:delivery\",\n ttlSeconds: 86_400,\n });\n\n // ─── Stage 3: Delivery Worker ───────────────────────────────────────────────\n //\n // Pipeline:\n // 1. Parse payload as notification.dispatched\n // 2. Resolve active device tokens from DB\n // 3. Send via registered transport\n // 4. Deactivate invalid tokens in DB\n\n const eventsProducer = new StreamProducer({\n redis: redis.native,\n stream: STREAMS.EVENTS_INBOUND,\n logger,\n });\n\n worker = new DeliveryWorker({\n consumer,\n pendingScanner,\n logger,\n concurrency: config.WORKER_CONCURRENCY,\n transportRegistry,\n idempotency,\n redis: redis.native,\n scheduledProducer,\n enrichedProducers,\n contactRepo,\n eventsProducer,\n globalEmitter,\n db,\n });\n\n // ─── Health check interval ──────────────────────────────────────────────────\n\n healthInterval = startHealthReporter(\"delivery\", worker, redis, logger);\n\n logger.info(\n { env: config.NODE_ENV, channels: transportRegistry.registeredChannels() },\n \"delivery starting\",\n );\n await worker.start();\n}\n\n// ─── Shutdown ──────────────────────────────────────────────────────────────\n\nexport async function stopDeliveryWorker(): Promise<void> {\n logger?.info(\"shutdown initiated\");\n if (healthInterval) {\n clearInterval(healthInterval);\n healthInterval = null;\n }\n if (worker) await worker.stop();\n if (sql) await sql.end();\n if (redis) await redis.disconnect();\n logger?.info(\"delivery stopped\");\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"main-DdLzptZE.mjs","names":[],"sources":["../src/services/delivery/throttle.ts","../src/services/delivery/main.ts"],"sourcesContent":["import type { Redis } from \"ioredis\";\n\nexport interface ThrottleResult {\n allowed: boolean;\n retryAfterMs: number;\n}\n\nconst LUA_THROTTLE = `\n local key = KEYS[1]\n local now = tonumber(ARGV[1])\n local windowSeconds = tonumber(ARGV[2])\n local limit = tonumber(ARGV[3])\n local member = ARGV[4]\n\n local clearBefore = now - (windowSeconds * 1000)\n \n -- Cleanup expired scores\n redis.call('ZREMRANGEBYSCORE', key, 0, clearBefore)\n \n -- Get current count\n local count = redis.call('ZCARD', key)\n \n if count >= limit then\n -- Find the oldest score\n local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')\n if oldest and oldest[2] then\n return {0, tonumber(oldest[2])}\n end\n return {0, now}\n end\n \n -- Add new request\n redis.call('ZADD', key, now, member)\n redis.call('EXPIRE', key, windowSeconds * 2)\n return {1, 0}\n`;\n\nexport async function throttleProvider(\n redis: Redis,\n channel: string,\n config: { limit: number; windowSeconds: number },\n logger: any,\n): Promise<ThrottleResult> {\n const key = `rate-limit:provider:${channel}`;\n const now = Date.now();\n const zmember = `${now}:${Math.random()}`;\n\n const result = (await redis.eval(\n LUA_THROTTLE,\n 1,\n key,\n now.toString(),\n config.windowSeconds.toString(),\n config.limit.toString(),\n zmember,\n )) as [number, number];\n\n const allowed = result[0] === 1;\n const oldestTimestamp = result[1];\n\n let retryAfterMs = 0;\n if (!allowed) {\n retryAfterMs = Math.max(0, oldestTimestamp + config.windowSeconds * 1000 - now);\n logger.warn(\n { channel, limit: config.limit, windowSeconds: config.windowSeconds, retryAfterMs },\n \"Provider rate limit hit — task must be rescheduled\",\n );\n }\n\n return { allowed, retryAfterMs };\n}\n","import { loadEnv, parseConfig, baseConfigSchema } from \"@/index.js\";\nimport { createLogger } from \"@/index.js\";\nimport { RedisClient, type Redis } from \"@/index.js\";\nimport {\n StreamConsumer,\n PendingMessageScanner,\n type StreamMessage,\n StreamProducer,\n} from \"@/index.js\";\nimport { BaseWorker } from \"@/index.js\";\nimport {\n STREAMS,\n OUTBOUND_STREAMS,\n CONSUMER_GROUPS,\n registry,\n buildStreamEvent,\n type NotificationDispatchedPayload,\n} from \"@/index.js\";\nimport { type StreamName } from \"@/contracts/streams.js\";\nimport { createDatabase } from \"@/db/index.js\";\nimport { deliveryOutbox, scheduledPayloads } from \"@/db/schema.js\";\nimport { ContactRepository, IdempotencyGuard } from \"@/index.js\";\nimport { transportRegistry } from \"@/index.js\";\nimport { globalEmitter, getPriorityBucket, type WorkerOptions } from \"@/shared/index.js\";\nimport { startHealthReporter, NonRetryableError } from \"@/workers/index.js\";\nimport { BatchProcessor, CircuitBreaker } from \"@/shared/index.js\";\nimport { throttleProvider } from \"./throttle.js\";\nimport { metrics } from \"@/metrics/index.js\";\n\n// ─── App-specific config ────────────────────────────────────────────────────\n\nconst deliveryConfigSchema = baseConfigSchema.extend({});\n\n// ─── Bootstrap ─────────────────────────────────────────────────────────────\n\nloadEnv();\nconst config = parseConfig(deliveryConfigSchema, process.env);\n\nlet logger: ReturnType<typeof createLogger>;\nlet redis: RedisClient;\nlet sql: any;\nlet db: any;\n\nlet consumer: StreamConsumer;\nlet pendingScanner: PendingMessageScanner;\nlet worker: BaseWorker;\nlet scheduledProducer: StreamProducer;\nlet enrichedProducers: Record<string, StreamProducer>;\nlet healthInterval: NodeJS.Timeout | null = null;\n\n(global as any)._telemetry = (global as any)._telemetry || {\n count: 0,\n insert: 0,\n provider: 0,\n flush: 0,\n ack: 0,\n dequeue: 0,\n dbupdate: 0,\n flushCount: 0,\n};\n\nexport interface DeliveryWorkerOptions extends WorkerOptions {\n transportRegistry: any;\n idempotency: any;\n redis: Redis;\n scheduledProducer: any;\n enrichedProducers: any;\n contactRepo: any;\n eventsProducer: any;\n globalEmitter: any;\n db: any;\n}\n\nexport class DeliveryWorker extends BaseWorker {\n private readonly transportRegistry: any;\n private readonly idempotency: any;\n private readonly redisCli: Redis;\n private readonly scheduledProducer: any;\n private readonly enrichedProducers: any;\n private readonly contactRepo: any;\n private readonly eventsProducer: any;\n private readonly globalEmitter: any;\n private readonly db: any;\n\n private eventProcessor: BatchProcessor<any, void>;\n private outboxUpdateProcessor: BatchProcessor<any, void>;\n private outboxInsertProcessor: BatchProcessor<any, boolean>;\n private breakers = new Map<string, CircuitBreaker>();\n\n constructor(options: DeliveryWorkerOptions) {\n super(options);\n this.transportRegistry = options.transportRegistry;\n this.idempotency = options.idempotency;\n this.redisCli = options.redis;\n this.scheduledProducer = options.scheduledProducer;\n this.enrichedProducers = options.enrichedProducers;\n this.contactRepo = options.contactRepo;\n this.eventsProducer = options.eventsProducer;\n this.globalEmitter = options.globalEmitter;\n this.db = options.db;\n\n this.eventProcessor = new BatchProcessor<any, void>(1000, 100, async (events) => {\n await this.eventsProducer.publishBatch(events);\n return events.map(() => undefined as void);\n });\n\n this.outboxUpdateProcessor = new BatchProcessor<any, void>(500, 100, async (updates) => {\n const { sql } = await import(\"drizzle-orm\");\n const values = updates.map((update) => ({\n taskId: update.taskId,\n channel: update.channel,\n destination: update.destination,\n providerMessageId: update.providerMessageId,\n }));\n\n const tDbUpdateStart = Date.now();\n await this.db\n .insert(deliveryOutbox)\n .values(values)\n .onConflictDoUpdate({\n target: [deliveryOutbox.taskId, deliveryOutbox.channel, deliveryOutbox.destination],\n set: { providerMessageId: sql`EXCLUDED.provider_message_id` },\n })\n .catch((e: any) => this.logger.error({ err: e }, \"background update failed\"));\n\n (global as any)._telemetry.dbupdate += Date.now() - tDbUpdateStart;\n (global as any)._telemetry.flushCount++;\n return updates.map(() => undefined as void);\n });\n\n this.outboxInsertProcessor = new BatchProcessor(500, 10, async (tasks) => {\n const values = tasks.map((task: any) => ({\n taskId: task.taskId,\n channel: task.channel,\n destination: task.destination,\n }));\n\n await this.db.insert(deliveryOutbox).values(values).onConflictDoNothing();\n return tasks.map(() => true);\n });\n }\n\n override async stop(): Promise<void> {\n await Promise.all([\n this.eventProcessor.flush(),\n this.outboxUpdateProcessor.flush(),\n this.outboxInsertProcessor.flush(),\n ]);\n await super.stop();\n }\n\n private getBreaker(name: string): CircuitBreaker {\n let breaker = this.breakers.get(name);\n if (!breaker) {\n breaker = new CircuitBreaker({ failureThreshold: 5, resetTimeoutMs: 30000 });\n this.breakers.set(name, breaker);\n }\n return breaker;\n }\n\n async process(message: StreamMessage, attempt: number = 1): Promise<void> {\n const { event } = message;\n const publishPromises: Promise<void>[] = [];\n\n const payloadResult = registry.safeParsePayload(\"notification.dispatched\", event.payload);\n if (!payloadResult.success) {\n this.logger.warn(\n { messageId: message.id, issues: payloadResult.error.issues },\n \"invalid notification.dispatched payload — skipping\",\n );\n return;\n }\n\n const task = payloadResult.data as NotificationDispatchedPayload;\n\n const fallbackToNextChannel = async (reason: string) => {\n if (task.fallbackChain && task.fallbackChain.length > 0 && task.recipient) {\n const nextChannel = task.fallbackChain[0];\n const remainingChain = task.fallbackChain.slice(1);\n\n const fallbackPayload: any = {\n // NotificationEnrichedPayload\n projectId: task.projectId,\n rawEventId: task.enrichedEventId,\n recipientId: task.recipientId,\n channel: nextChannel,\n priority: task.priority,\n templateId: task.templateId,\n templateVariables: task.templateVariables,\n aiPrompts: task.aiPrompts,\n recipient: task.recipient,\n scheduledAt: undefined,\n fallbackChain: remainingChain.length > 0 ? remainingChain : undefined,\n };\n\n const p = getPriorityBucket(task.priority);\n const producer = this.enrichedProducers[p] ?? this.enrichedProducers[\"normal\"]!;\n await producer.publish(\n buildStreamEvent(\n \"notification.enriched\",\n fallbackPayload,\n \"delivery\",\n event.metadata.traceId,\n ),\n );\n\n this.logger.info(\n { taskId: task.taskId, reason, nextChannel, traceId: event.metadata.traceId },\n \"Delivery failed completely, rolling over to next channel in fallback chain\",\n );\n return true; // Indicates fallback was triggered\n }\n return false; // No fallback\n };\n\n const transports = this.transportRegistry.getAll(task.channel);\n\n if (transports.length === 0) {\n this.logger.warn(\n { taskId: task.taskId, channel: task.channel },\n \"no transport registered for channel — dropping\",\n );\n const fallbackTriggered = await fallbackToNextChannel(\"no_transport\");\n if (!fallbackTriggered) {\n this.globalEmitter.emit(\n \"delivery:failed\",\n task.taskId,\n \"no transport\",\n task.channel,\n task.projectId,\n );\n await this.eventProcessor.add(\n buildStreamEvent(\n \"notification.failed\",\n {\n projectId: task.projectId,\n taskId: task.taskId,\n enrichedEventId: task.enrichedEventId,\n recipientId: task.recipientId,\n channel: task.channel,\n failureReason: \"no transport registered for channel\",\n failureCode: \"no_transport\",\n retryable: false,\n attempt,\n templateId: task.templateId,\n workflowInstanceId:\n event.metadata.source === \"workflow\" ? event.metadata.traceId : undefined,\n campaignId: task.campaignId,\n },\n \"delivery\",\n event.metadata.traceId,\n ),\n );\n }\n return;\n }\n\n const limitConfig = transports[0].limits;\n if (limitConfig) {\n const { allowed, retryAfterMs } = await throttleProvider(\n this.redisCli,\n task.channel,\n limitConfig,\n this.logger,\n );\n if (!allowed) {\n task.throttleAttemptCount = (task.throttleAttemptCount ?? 0) + 1;\n const maxAttempts = task.deliveryOptions?.maxAttempts ?? 3;\n\n if (task.throttleAttemptCount > maxAttempts) {\n this.logger.warn(\n { messageId: message.id, taskId: task.taskId, attempts: task.throttleAttemptCount },\n \"provider rate limit max attempts exceeded\",\n );\n const fallbackTriggered = await fallbackToNextChannel(\"provider_throttle_exceeded\");\n if (!fallbackTriggered) {\n this.globalEmitter.emit(\n \"delivery:failed\",\n task.taskId,\n \"provider throttle exceeded\",\n task.channel,\n task.projectId,\n );\n await this.eventProcessor.add(\n buildStreamEvent(\n \"notification.failed\",\n {\n projectId: task.projectId,\n taskId: task.taskId,\n enrichedEventId: task.enrichedEventId,\n recipientId: task.recipientId,\n channel: task.channel,\n failureReason: \"provider rate limit max attempts exceeded\",\n failureCode: \"provider_throttle_exceeded\",\n retryable: false,\n attempt,\n templateId: task.templateId,\n workflowInstanceId:\n event.metadata.source === \"workflow\" ? event.metadata.traceId : undefined,\n campaignId: task.campaignId,\n },\n \"delivery\",\n event.metadata.traceId,\n ),\n );\n }\n return;\n }\n\n const { sql } = await import(\"drizzle-orm\");\n await this.db\n .insert(scheduledPayloads)\n .values({\n taskId: task.taskId,\n payload: task,\n })\n .onConflictDoUpdate({\n target: scheduledPayloads.taskId,\n set: { payload: sql`EXCLUDED.payload` },\n });\n\n await this.scheduledProducer.publish(\n buildStreamEvent(\n \"notification.scheduled\",\n {\n projectId: task.projectId,\n enrichedEventId: task.enrichedEventId,\n taskId: task.taskId,\n scheduledAt: new Date(Date.now() + retryAfterMs).toISOString(),\n throttleAttemptCount: task.throttleAttemptCount,\n },\n \"delivery\",\n `${task.taskId}:throttle:${Date.now()}`,\n ),\n );\n\n return;\n }\n }\n\n const tInsertStart = Date.now();\n const idempotencyKey = task.taskId;\n if (!(await this.idempotency.checkAndMark(idempotencyKey, 60))) {\n this.logger.info(\n { messageId: message.id, taskId: task.taskId, channel: task.channel, attempt },\n \"duplicate delivery — skipping\",\n );\n return;\n }\n\n try {\n await this.outboxInsertProcessor.add(task);\n const insertTime = Date.now() - tInsertStart;\n\n publishPromises.push(\n this.eventProcessor.add(\n buildStreamEvent(\n \"notification.dispatched\",\n {\n projectId: task.projectId,\n taskId: task.taskId,\n enrichedEventId: task.enrichedEventId,\n recipientId: task.recipientId,\n channel: task.channel,\n templateId: task.templateId,\n attempt,\n workflowInstanceId:\n event.metadata.source === \"workflow\" ? event.metadata.traceId : undefined,\n campaignId: task.campaignId,\n },\n \"delivery\",\n event.metadata.traceId,\n ),\n ),\n );\n\n // For push: we just need to send to the pre-resolved destination,\n // but if it's invalid, we deactivate it.\n if (task.channel === \"push\") {\n let lastResult: any = { success: false, error: \"No transports\" };\n\n for (const transport of transports) {\n try {\n const tProv = Date.now();\n const breaker = this.getBreaker(`${task.channel}:${transport.constructor.name}`);\n\n lastResult = await breaker.execute(async () => {\n const timeoutMs = task.deliveryOptions?.timeoutMs ?? 10_000;\n const controller = new AbortController();\n const timeout = setTimeout(() => {\n controller.abort(new Error(`Transport timeout after ${timeoutMs}ms`));\n }, timeoutMs);\n\n try {\n (task as any).signal = controller.signal;\n const res: any = await Promise.race([\n transport.send(task),\n new Promise((_, reject) => {\n if (controller.signal.aborted) return reject(controller.signal.reason);\n controller.signal.addEventListener(\"abort\", () =>\n reject(controller.signal.reason),\n );\n }),\n ]);\n if (!res.success && !res.invalidToken) {\n throw new Error(res.error ?? \"Transport failed\");\n }\n return res;\n } finally {\n clearTimeout(timeout);\n }\n });\n\n (global as any)._telemetry.provider += Date.now() - tProv;\n if (lastResult.success || lastResult.invalidToken) break;\n } catch (err: any) {\n lastResult = { success: false, error: err.message };\n }\n }\n\n if (lastResult.invalidToken) {\n await this.contactRepo.deactivate(\n task.projectId,\n task.recipientId,\n task.channel as any,\n task.destination,\n );\n this.logger.info(\n { channel: task.channel, target: task.destination },\n \"deactivated invalid contact target\",\n );\n this.globalEmitter.emit(\n \"delivery:failed\",\n task.taskId,\n \"invalidToken\",\n task.channel,\n task.projectId,\n );\n metrics.deliveryFailed.inc({ channel: task.channel, reason: \"invalid_token\" });\n\n publishPromises.push(\n this.eventProcessor.add(\n buildStreamEvent(\n \"notification.failed\",\n {\n projectId: task.projectId,\n taskId: task.taskId,\n enrichedEventId: task.enrichedEventId,\n recipientId: task.recipientId,\n channel: task.channel,\n failureReason: \"invalid_token\",\n failureCode: \"push_failure\",\n retryable: false,\n attempt,\n templateId: task.templateId,\n workflowInstanceId:\n event.metadata.source === \"workflow\" ? event.metadata.traceId : undefined,\n campaignId: task.campaignId,\n },\n \"delivery\",\n event.metadata.traceId,\n ),\n ),\n );\n\n const fallbackTriggered = await fallbackToNextChannel(\"invalid_token\");\n if (!fallbackTriggered) {\n throw new NonRetryableError(\"Push delivery failed: invalid token\");\n }\n } else if (lastResult.success) {\n // Record providerMessageId in outbox before publishing event\n const providerMessageId = lastResult.providerMessageId || \"push-success\";\n\n publishPromises.push(\n this.outboxUpdateProcessor.add({\n taskId: task.taskId,\n channel: task.channel,\n destination: task.destination,\n providerMessageId,\n }),\n );\n\n this.logger.debug(\n { taskId: task.taskId, messageId: providerMessageId },\n \"push delivered\",\n );\n this.globalEmitter.emit(\n \"delivery:delivered\",\n task.taskId,\n providerMessageId,\n task.channel,\n task.projectId,\n );\n metrics.deliverySuccess.inc({ channel: task.channel });\n\n publishPromises.push(\n this.eventProcessor.add(\n buildStreamEvent(\n \"notification.delivered\",\n {\n projectId: task.projectId,\n taskId: task.taskId,\n enrichedEventId: task.enrichedEventId,\n channel: task.channel,\n deliveredAt: new Date().toISOString(),\n providerMessageId,\n templateId: task.templateId,\n workflowInstanceId:\n event.metadata.source === \"workflow\" ? event.metadata.traceId : undefined,\n campaignId: task.campaignId,\n },\n \"delivery\",\n event.metadata.traceId,\n ),\n ),\n );\n } else {\n this.logger.warn(\n { taskId: task.taskId, error: lastResult.error },\n \"push delivery failed\",\n );\n this.globalEmitter.emit(\n \"delivery:failed\",\n task.taskId,\n lastResult.error ?? \"push delivery failed\",\n task.channel,\n task.projectId,\n );\n metrics.deliveryFailed.inc({ channel: task.channel, reason: \"push_error\" });\n\n publishPromises.push(\n this.eventProcessor.add(\n buildStreamEvent(\n \"notification.failed\",\n {\n projectId: task.projectId,\n taskId: task.taskId,\n enrichedEventId: task.enrichedEventId,\n recipientId: task.recipientId,\n channel: task.channel,\n failureReason: lastResult.error ?? \"push delivery failed\",\n failureCode: \"push_failure\",\n retryable: false,\n attempt,\n templateId: task.templateId,\n workflowInstanceId:\n event.metadata.source === \"workflow\" ? event.metadata.traceId : undefined,\n campaignId: task.campaignId,\n },\n \"delivery\",\n event.metadata.traceId,\n ),\n ),\n );\n\n const fallbackTriggered = await fallbackToNextChannel(\"push_delivery_failed\");\n if (!fallbackTriggered) {\n throw new NonRetryableError(lastResult.error ?? \"Push delivery failed\");\n }\n }\n } else {\n // Other channels (email, sms, webhook, whatsapp) use the pre-resolved destination\n let result: any = { success: false, error: \"No transports\" };\n for (const transport of transports) {\n try {\n const tProv = Date.now();\n const breaker = this.getBreaker(`${task.channel}:${transport.constructor.name}`);\n\n result = await breaker.execute(async () => {\n const timeoutMs = task.deliveryOptions?.timeoutMs ?? 10_000;\n const controller = new AbortController();\n const timeout = setTimeout(() => {\n controller.abort(new Error(`Transport timeout after ${timeoutMs}ms`));\n }, timeoutMs);\n\n try {\n (task as any).signal = controller.signal;\n const res: any = await Promise.race([\n transport.send(task),\n new Promise((_, reject) => {\n if (controller.signal.aborted) return reject(controller.signal.reason);\n controller.signal.addEventListener(\"abort\", () =>\n reject(controller.signal.reason),\n );\n }),\n ]);\n if (!res.success) {\n throw new Error(res.error ?? \"Transport failed\");\n }\n return res;\n } finally {\n clearTimeout(timeout);\n }\n });\n\n (global as any)._telemetry.provider += Date.now() - tProv;\n if (result.success) break;\n } catch (err: any) {\n result = { success: false, error: err.message };\n }\n }\n\n if (result.success) {\n // Record providerMessageId in outbox before publishing event\n const providerMessageId = result.providerMessageId || \"success\";\n\n publishPromises.push(\n this.outboxUpdateProcessor.add({\n taskId: task.taskId,\n channel: task.channel,\n destination: task.destination,\n providerMessageId,\n }),\n );\n\n this.globalEmitter.emit(\n \"delivery:delivered\",\n task.taskId,\n providerMessageId,\n task.channel,\n task.projectId,\n );\n metrics.deliverySuccess.inc({ channel: task.channel });\n\n publishPromises.push(\n this.eventProcessor.add(\n buildStreamEvent(\n \"notification.delivered\",\n {\n projectId: task.projectId,\n taskId: task.taskId,\n enrichedEventId: task.enrichedEventId,\n channel: task.channel,\n deliveredAt: new Date().toISOString(),\n providerMessageId,\n templateId: task.templateId,\n workflowInstanceId:\n event.metadata.source === \"workflow\" ? event.metadata.traceId : undefined,\n campaignId: task.campaignId,\n },\n \"delivery\",\n event.metadata.traceId,\n ),\n ),\n );\n } else {\n publishPromises.push(\n this.eventProcessor.add(\n buildStreamEvent(\n \"notification.failed\",\n {\n projectId: task.projectId,\n taskId: task.taskId,\n enrichedEventId: task.enrichedEventId,\n channel: task.channel,\n failureReason: result.error ?? \"delivery failed completely\",\n failureCode: \"provider_error\",\n retryable: false,\n attempt,\n templateId: task.templateId,\n workflowInstanceId:\n event.metadata.source === \"workflow\" ? event.metadata.traceId : undefined,\n campaignId: task.campaignId,\n },\n \"delivery\",\n event.metadata.traceId,\n ),\n ),\n );\n }\n\n if (!result.success) {\n this.logger.warn(\n { taskId: task.taskId, channel: task.channel, error: result.error },\n \"delivery failed completely across providers\",\n );\n this.globalEmitter.emit(\n \"delivery:failed\",\n task.taskId,\n result.error ?? \"delivery failed completely\",\n task.channel,\n task.projectId,\n );\n metrics.deliveryFailed.inc({ channel: task.channel, reason: \"provider_error\" });\n\n const fallbackTriggered = await fallbackToNextChannel(\"all_providers_failed\");\n if (!fallbackTriggered) {\n throw new NonRetryableError(result.error ?? \"delivery failed\");\n }\n } else {\n this.logger.info(\n { taskId: task.taskId, channel: task.channel, messageId: result.providerMessageId },\n \"notification delivered\",\n );\n }\n }\n\n const tFlushStart = Date.now();\n await Promise.all(publishPromises).catch((err) => {\n this.logger.error(\n { err, taskId: task.taskId },\n \"failed to publish post-dispatch events, swallowing error to prevent duplicate delivery\",\n );\n });\n const flushTime = Date.now() - tFlushStart;\n\n await this.idempotency.markProcessed(idempotencyKey);\n\n const t = ((global as any)._telemetry = (global as any)._telemetry || {\n count: 0,\n insert: 0,\n provider: 0,\n flush: 0,\n ack: 0,\n });\n t.count++;\n t.insert += insertTime;\n t.flush += flushTime;\n\n if (t.count % 1000 === 0) {\n this.logger.debug(\n `[Metrics 1000 msgs] Dequeue: ${t.dequeue / 1000}ms, DB Insert: ${t.insert / 1000}ms, Provider: ${t.provider / 1000}ms, Wait for Flush: ${t.flush / 1000}ms, Ack: ${t.ack / 1000}ms | DB Update (avg per flush): ${t.dbupdate / Math.max(1, t.flushCount)}ms`,\n );\n t.count = 0;\n t.insert = 0;\n t.provider = 0;\n t.flush = 0;\n t.dequeue = 0;\n t.ack = 0;\n t.dbupdate = 0;\n t.flushCount = 0;\n }\n } catch (err) {\n await this.idempotency.unmark(idempotencyKey).catch(() => {});\n throw err;\n }\n }\n}\n\nexport async function startDeliveryWorker() {\n logger = createLogger({ name: \"delivery\", level: config.LOG_LEVEL });\n\n redis = new RedisClient({ url: config.REDIS_URL, name: \"delivery\", logger });\n const dbData = createDatabase({ url: config.DATABASE_URL, applicationName: \"delivery\", logger });\n sql = dbData.sql;\n db = dbData.db;\n consumer = new StreamConsumer({\n redis: redis.native,\n stream: OUTBOUND_STREAMS as unknown as StreamName[],\n group: CONSUMER_GROUPS.DELIVERY,\n consumer: `delivery-${process.pid}`,\n dlqStream: STREAMS.DEAD_LETTER,\n batchSize: config.WORKER_CONCURRENCY,\n logger,\n });\n\n scheduledProducer = new StreamProducer({\n redis: redis.native,\n stream: STREAMS.SCHEDULED,\n logger,\n });\n\n pendingScanner = new PendingMessageScanner({\n redis: redis.native,\n stream: OUTBOUND_STREAMS as unknown as StreamName[],\n group: CONSUMER_GROUPS.DELIVERY,\n consumer: `delivery-${process.pid}`,\n logger,\n });\n\n enrichedProducers = {\n critical: new StreamProducer({\n redis: redis.native,\n stream: STREAMS.ENRICHED_CRITICAL,\n logger,\n }),\n normal: new StreamProducer({ redis: redis.native, stream: STREAMS.ENRICHED_NORMAL, logger }),\n low: new StreamProducer({ redis: redis.native, stream: STREAMS.ENRICHED_LOW, logger }),\n };\n\n const contactRepo = new ContactRepository(db);\n\n const idempotency = new IdempotencyGuard({\n redis: redis.native,\n keyPrefix: \"notif:processed:delivery\",\n ttlSeconds: 86_400,\n });\n\n // ─── Stage 3: Delivery Worker ───────────────────────────────────────────────\n //\n // Pipeline:\n // 1. Parse payload as notification.dispatched\n // 2. Resolve active device tokens from DB\n // 3. Send via registered transport\n // 4. Deactivate invalid tokens in DB\n\n const eventsProducer = new StreamProducer({\n redis: redis.native,\n stream: STREAMS.EVENTS_INBOUND,\n logger,\n });\n\n worker = new DeliveryWorker({\n consumer,\n pendingScanner,\n logger,\n concurrency: config.WORKER_CONCURRENCY,\n transportRegistry,\n idempotency,\n redis: redis.native,\n scheduledProducer,\n enrichedProducers,\n contactRepo,\n eventsProducer,\n globalEmitter,\n db,\n });\n\n // ─── Health check interval ──────────────────────────────────────────────────\n\n healthInterval = startHealthReporter(\"delivery\", worker, redis, logger);\n\n logger.info(\n { env: config.NODE_ENV, channels: transportRegistry.registeredChannels() },\n \"delivery starting\",\n );\n await worker.start();\n}\n\n// ─── Shutdown ──────────────────────────────────────────────────────────────\n\nexport async function stopDeliveryWorker(): Promise<void> {\n logger?.info(\"shutdown initiated\");\n if (healthInterval) {\n clearInterval(healthInterval);\n healthInterval = null;\n }\n if (worker) await worker.stop();\n if (sql) await sql.end();\n if (redis) await redis.disconnect();\n logger?.info(\"delivery stopped\");\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { J as PendingMessageScanner, Jt as buildStreamEvent, X as StreamProducer, Y as StreamConsumer, Yt as CONSUMER_GROUPS, _t as workflowWaiters, c as BaseWorker, en as STREAMS, et as createLogger, gt as workflowSteps, hn as readBaseConfig, ht as workflowInstances, it as createDatabase, k as RedisClient, pn as loadEnv, st as messageLogs, u as startHealthReporter } from "./src-vG79L-8m.mjs";
1
+ import { J as PendingMessageScanner, Jt as buildStreamEvent, X as StreamProducer, Y as StreamConsumer, Yt as CONSUMER_GROUPS, _t as workflowWaiters, c as BaseWorker, en as STREAMS, et as createLogger, gt as workflowSteps, hn as readBaseConfig, ht as workflowInstances, it as createDatabase, k as RedisClient, pn as loadEnv, st as messageLogs, u as startHealthReporter } from "./src-CpUnBl_M.mjs";
2
2
  import { and, eq, gt, isNull, or } from "drizzle-orm";
3
3
  //#region src/services/events/main.ts
4
4
  loadEnv();
@@ -231,4 +231,4 @@ async function stopEventWorker() {
231
231
  //#endregion
232
232
  export { startEventWorker, stopEventWorker };
233
233
 
234
- //# sourceMappingURL=main-DmCPcxOc.mjs.map
234
+ //# sourceMappingURL=main-DfrrNKp9.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"main-DmCPcxOc.mjs","names":["sql"],"sources":["../src/services/events/main.ts"],"sourcesContent":["import { loadEnv, readBaseConfig } from \"@/index.js\";\nimport { createLogger } from \"@/index.js\";\nimport { RedisClient } from \"@/index.js\";\nimport {\n StreamConsumer,\n PendingMessageScanner,\n StreamProducer,\n type StreamMessage,\n} from \"@/index.js\";\nimport { BaseWorker } from \"@/index.js\";\nimport { STREAMS, CONSUMER_GROUPS, buildStreamEvent } from \"@/index.js\";\nimport { type StreamName } from \"@/contracts/streams.js\";\nimport { createDatabase } from \"@/db/index.js\";\nimport { workflowWaiters, workflowSteps, workflowInstances, messageLogs } from \"@/db/schema.js\";\nimport { eq, and, or, isNull, gt } from \"drizzle-orm\";\nimport { type WorkerOptions } from \"@/shared/index.js\";\nimport { startHealthReporter } from \"@/workers/index.js\";\n\nloadEnv();\nconst config = readBaseConfig();\nlet logger: ReturnType<typeof createLogger>;\nlet redis: RedisClient;\nlet sql: any;\nlet db: any;\n\nlet consumer: StreamConsumer;\nlet pendingScanner: PendingMessageScanner;\nlet worker: BaseWorker;\nlet healthInterval: NodeJS.Timeout | null = null;\n\nlet workflowProducer: StreamProducer;\n\nexport interface EventWorkerOptions extends WorkerOptions {\n db: any;\n workflowProducer: any;\n}\n\nexport class EventWorker extends BaseWorker {\n private readonly dbConn: any;\n private readonly workflowProducer: any;\n private messageLogBuffer: { log: any; resolve: () => void; reject: (err: any) => void }[] = [];\n private eventBuffer: {\n producer: any;\n event: any;\n resolve: () => void;\n reject: (err: any) => void;\n }[] = [];\n private flushInterval: NodeJS.Timeout | null = null;\n private isFlushing = false;\n\n constructor(options: EventWorkerOptions) {\n super(options);\n this.dbConn = options.db;\n this.workflowProducer = options.workflowProducer;\n this.startFlushInterval();\n }\n\n private startFlushInterval() {\n this.flushInterval = setInterval(() => {\n void this.flushLogs();\n }, config.LOG_FLUSH_INTERVAL_MS);\n }\n\n private async flushLogs() {\n if (this.isFlushing || (this.messageLogBuffer.length === 0 && this.eventBuffer.length === 0))\n return;\n this.isFlushing = true;\n\n const startTime = Date.now();\n const batch = this.messageLogBuffer;\n this.messageLogBuffer = [];\n\n const events = this.eventBuffer;\n this.eventBuffer = [];\n\n try {\n if (batch.length > 0) {\n await this.dbConn\n .insert(messageLogs)\n .values(batch.map((b) => b.log))\n .onConflictDoNothing(); // Idempotent insert\n for (const b of batch) b.resolve();\n\n this.logger.info(\n {\n batchSize: batch.length,\n latencyMs: Date.now() - startTime,\n },\n \"flushed delivery logs to database\",\n );\n }\n\n if (events.length > 0) {\n const byProducer = new Map<any, typeof events>();\n for (const item of events) {\n if (!byProducer.has(item.producer)) byProducer.set(item.producer, []);\n byProducer.get(item.producer)!.push(item);\n }\n for (const [producer, evts] of byProducer) {\n await producer.publishBatch(evts.map((e) => e.event));\n for (const e of evts) e.resolve();\n }\n }\n } catch (error: any) {\n this.logger.error(\n {\n batchSize: batch.length,\n error: error.message,\n },\n \"failed to flush delivery logs — dropping batch\",\n );\n for (const b of batch) b.reject(error);\n for (const e of events) e.reject(error);\n } finally {\n this.isFlushing = false;\n }\n }\n\n override async stop(): Promise<void> {\n if (this.flushInterval) clearInterval(this.flushInterval);\n await super.stop();\n await this.flushLogs();\n }\n\n override async process(message: StreamMessage): Promise<void> {\n const { event } = message;\n const publishPromises: Promise<void>[] = [];\n\n if (\n event.type === \"notification.dispatched\" ||\n event.type === \"notification.delivered\" ||\n event.type === \"notification.failed\"\n ) {\n const payload = event.payload as any;\n if (this.messageLogBuffer.length >= config.LOG_BUFFER_MAX_SIZE) {\n this.logger.warn(\n { dropped: 1, bufferSize: this.messageLogBuffer.length },\n \"log buffer full — dropping oldest delivery log\",\n );\n const dropped = this.messageLogBuffer.shift(); // Drop oldest to apply back-pressure/prevent OOM\n if (dropped) dropped.reject(new Error(\"Dropped due to buffer overflow\"));\n }\n\n const status =\n event.type === \"notification.dispatched\"\n ? \"dispatched\"\n : event.type === \"notification.delivered\"\n ? \"delivered\"\n : \"failed\";\n\n const kind = event.type === \"notification.dispatched\" ? \"dispatched\" : \"attempt\";\n\n publishPromises.push(\n new Promise((resolve, reject) => {\n this.messageLogBuffer.push({\n log: {\n projectId: payload.projectId,\n taskId: payload.taskId,\n providerMessageId: payload.providerMessageId || null,\n channel: payload.channel,\n attempt: payload.attempt || 1,\n kind,\n status,\n templateId: payload.templateId || null,\n workflowInstanceId: payload.workflowInstanceId || null,\n campaignId: payload.campaignId || null,\n },\n resolve,\n reject,\n });\n }),\n );\n await Promise.all(publishPromises);\n return;\n }\n\n if (event.type !== \"event.received\") {\n return;\n }\n\n const payload = event.payload as any;\n const eventName = payload.eventName;\n const eventData = payload.payload;\n const projectId = payload.projectId;\n\n if (!eventName || !projectId) {\n this.logger.warn(\"Missing eventName or projectId in payload\");\n return;\n }\n\n // Find matching waiters\n const now = new Date();\n const waiters = await this.dbConn\n .select()\n .from(workflowWaiters)\n .where(\n and(\n eq(workflowWaiters.eventName, eventName),\n eq(workflowWaiters.projectId, projectId),\n or(isNull(workflowWaiters.expiresAt), gt(workflowWaiters.expiresAt, now)),\n ),\n );\n\n const data: Record<string, any> = eventData && typeof eventData === \"object\" ? eventData : {};\n\n for (const waiter of waiters) {\n // Check match criteria\n let matched = true;\n const criteria = waiter.matchCriteria as Record<string, any>;\n if (criteria && typeof criteria === \"object\") {\n for (const [k, v] of Object.entries(criteria)) {\n // simple dot notation check for a flat object or 1 level deep\n let dataValue = data[k];\n if (k.includes(\".\")) {\n const parts = k.split(\".\");\n dataValue = data;\n for (const p of parts) {\n if (dataValue) dataValue = dataValue[p];\n }\n }\n if (dataValue !== v) {\n matched = false;\n break;\n }\n }\n }\n\n if (matched) {\n this.logger.info(\n { instanceId: waiter.instanceId, eventName },\n \"Event matched a sleeping workflow\",\n );\n\n // The resume event must carry everything WorkflowWorker.process requires\n // (name, instanceId, projectId) or it is dropped and the instance — whose\n // waiter we are about to delete — is stranded forever.\n const instance = (\n await this.dbConn\n .select()\n .from(workflowInstances)\n .where(eq(workflowInstances.id, waiter.instanceId))\n .limit(1)\n )[0];\n\n if (!instance) {\n this.logger.warn(\n { instanceId: waiter.instanceId },\n \"waiter references a missing workflow instance — dropping waiter\",\n );\n await this.dbConn.delete(workflowWaiters).where(eq(workflowWaiters.id, waiter.id));\n continue;\n }\n\n // Find the step that is waiting\n const steps = await this.dbConn\n .select()\n .from(workflowSteps)\n .where(\n and(\n eq(workflowSteps.instanceId, waiter.instanceId),\n eq(workflowSteps.action, \"waitForEvent\"),\n ),\n );\n\n // Get the latest one that has no output\n const pendingStep = steps.find((s: any) => s.output === null);\n\n if (pendingStep) {\n // Update the step with the event payload\n await this.dbConn\n .update(workflowSteps)\n .set({ output: data })\n .where(eq(workflowSteps.id, pendingStep.id));\n }\n\n // Wake up workflow. Publish BEFORE deleting the waiter: a crash after the\n // delete but before the publish would leave the instance unrecoverable,\n // whereas a duplicate resume is idempotent (the instance lock and the\n // replayed step outputs both absorb it).\n publishPromises.push(\n new Promise((resolve, reject) => {\n this.eventBuffer.push({\n producer: this.workflowProducer,\n event: buildStreamEvent(\n \"workflow.resumed\",\n {\n projectId: instance.projectId,\n instanceId: waiter.instanceId,\n name: instance.name,\n input: instance.input ?? {},\n reason: \"event_matched\",\n },\n \"events\",\n event.metadata.traceId,\n ),\n resolve,\n reject,\n });\n }),\n );\n\n // Delete the waiter\n await this.dbConn.delete(workflowWaiters).where(eq(workflowWaiters.id, waiter.id));\n\n // The timer ZSET entry is left to expire: the poll loop re-checks the\n // instance status and drops the entry if it is no longer pending.\n }\n }\n\n await Promise.all(publishPromises);\n }\n}\n\nexport async function startEventWorker() {\n logger = createLogger({ name: \"event-worker\", level: config.LOG_LEVEL });\n redis = new RedisClient({ url: config.REDIS_URL, name: \"events\", logger });\n const dbData = createDatabase({ url: config.DATABASE_URL, applicationName: \"events\", logger });\n sql = dbData.sql;\n db = dbData.db;\n workflowProducer = new StreamProducer({\n redis: redis.native,\n stream: STREAMS.WORKFLOW_INBOUND,\n logger,\n });\n consumer = new StreamConsumer({\n redis: redis.native,\n stream: STREAMS.EVENTS_INBOUND as StreamName,\n group: CONSUMER_GROUPS.EVENTS as any,\n consumer: `events-${process.pid}`,\n dlqStream: STREAMS.DEAD_LETTER,\n batchSize: config.WORKER_CONCURRENCY,\n logger,\n });\n\n pendingScanner = new PendingMessageScanner({\n redis: redis.native,\n stream: STREAMS.EVENTS_INBOUND as StreamName,\n group: CONSUMER_GROUPS.EVENTS as any,\n consumer: `events-${process.pid}`,\n logger,\n });\n\n worker = new EventWorker({\n consumer,\n pendingScanner,\n logger,\n concurrency: config.WORKER_CONCURRENCY,\n db,\n workflowProducer,\n });\n\n healthInterval = startHealthReporter(\"events\", worker, redis, logger);\n\n logger.info(\"event worker starting\");\n await worker.start();\n}\n\nexport async function stopEventWorker(): Promise<void> {\n logger?.info(\"shutdown initiated\");\n if (healthInterval) clearInterval(healthInterval);\n if (worker) await worker.stop();\n if (sql) await sql.end();\n if (redis) await redis.disconnect();\n logger?.info(\"event worker stopped\");\n}\n"],"mappings":";;;AAkBA,QAAQ;AACR,MAAM,SAAS,eAAe;AAC9B,IAAI;AACJ,IAAI;AACJ,IAAIA;AACJ,IAAI;AAEJ,IAAI;AACJ,IAAI;AACJ,IAAI;AACJ,IAAI,iBAAwC;AAE5C,IAAI;AAOJ,IAAa,cAAb,cAAiC,WAAW;CAC1C;CACA;CACA,mBAA4F,CAAC;CAC7F,cAKM,CAAC;CACP,gBAA+C;CAC/C,aAAqB;CAErB,YAAY,SAA6B;EACvC,MAAM,OAAO;EACb,KAAK,SAAS,QAAQ;EACtB,KAAK,mBAAmB,QAAQ;EAChC,KAAK,mBAAmB;CAC1B;CAEA,qBAA6B;EAC3B,KAAK,gBAAgB,kBAAkB;GACrC,KAAU,UAAU;EACtB,GAAG,OAAO,qBAAqB;CACjC;CAEA,MAAc,YAAY;EACxB,IAAI,KAAK,cAAe,KAAK,iBAAiB,WAAW,KAAK,KAAK,YAAY,WAAW,GACxF;EACF,KAAK,aAAa;EAElB,MAAM,YAAY,KAAK,IAAI;EAC3B,MAAM,QAAQ,KAAK;EACnB,KAAK,mBAAmB,CAAC;EAEzB,MAAM,SAAS,KAAK;EACpB,KAAK,cAAc,CAAC;EAEpB,IAAI;GACF,IAAI,MAAM,SAAS,GAAG;IACpB,MAAM,KAAK,OACR,OAAO,WAAW,CAAC,CACnB,OAAO,MAAM,KAAK,MAAM,EAAE,GAAG,CAAC,CAAC,CAC/B,oBAAoB;IACvB,KAAK,MAAM,KAAK,OAAO,EAAE,QAAQ;IAEjC,KAAK,OAAO,KACV;KACE,WAAW,MAAM;KACjB,WAAW,KAAK,IAAI,IAAI;IAC1B,GACA,mCACF;GACF;GAEA,IAAI,OAAO,SAAS,GAAG;IACrB,MAAM,6BAAa,IAAI,IAAwB;IAC/C,KAAK,MAAM,QAAQ,QAAQ;KACzB,IAAI,CAAC,WAAW,IAAI,KAAK,QAAQ,GAAG,WAAW,IAAI,KAAK,UAAU,CAAC,CAAC;KACpE,WAAW,IAAI,KAAK,QAAQ,CAAC,CAAE,KAAK,IAAI;IAC1C;IACA,KAAK,MAAM,CAAC,UAAU,SAAS,YAAY;KACzC,MAAM,SAAS,aAAa,KAAK,KAAK,MAAM,EAAE,KAAK,CAAC;KACpD,KAAK,MAAM,KAAK,MAAM,EAAE,QAAQ;IAClC;GACF;EACF,SAAS,OAAY;GACnB,KAAK,OAAO,MACV;IACE,WAAW,MAAM;IACjB,OAAO,MAAM;GACf,GACA,gDACF;GACA,KAAK,MAAM,KAAK,OAAO,EAAE,OAAO,KAAK;GACrC,KAAK,MAAM,KAAK,QAAQ,EAAE,OAAO,KAAK;EACxC,UAAU;GACR,KAAK,aAAa;EACpB;CACF;CAEA,MAAe,OAAsB;EACnC,IAAI,KAAK,eAAe,cAAc,KAAK,aAAa;EACxD,MAAM,MAAM,KAAK;EACjB,MAAM,KAAK,UAAU;CACvB;CAEA,MAAe,QAAQ,SAAuC;EAC5D,MAAM,EAAE,UAAU;EAClB,MAAM,kBAAmC,CAAC;EAE1C,IACE,MAAM,SAAS,6BACf,MAAM,SAAS,4BACf,MAAM,SAAS,uBACf;GACA,MAAM,UAAU,MAAM;GACtB,IAAI,KAAK,iBAAiB,UAAU,OAAO,qBAAqB;IAC9D,KAAK,OAAO,KACV;KAAE,SAAS;KAAG,YAAY,KAAK,iBAAiB;IAAO,GACvD,gDACF;IACA,MAAM,UAAU,KAAK,iBAAiB,MAAM;IAC5C,IAAI,SAAS,QAAQ,uBAAO,IAAI,MAAM,gCAAgC,CAAC;GACzE;GAEA,MAAM,SACJ,MAAM,SAAS,4BACX,eACA,MAAM,SAAS,2BACb,cACA;GAER,MAAM,OAAO,MAAM,SAAS,4BAA4B,eAAe;GAEvE,gBAAgB,KACd,IAAI,SAAS,SAAS,WAAW;IAC/B,KAAK,iBAAiB,KAAK;KACzB,KAAK;MACH,WAAW,QAAQ;MACnB,QAAQ,QAAQ;MAChB,mBAAmB,QAAQ,qBAAqB;MAChD,SAAS,QAAQ;MACjB,SAAS,QAAQ,WAAW;MAC5B;MACA;MACA,YAAY,QAAQ,cAAc;MAClC,oBAAoB,QAAQ,sBAAsB;MAClD,YAAY,QAAQ,cAAc;KACpC;KACA;KACA;IACF,CAAC;GACH,CAAC,CACH;GACA,MAAM,QAAQ,IAAI,eAAe;GACjC;EACF;EAEA,IAAI,MAAM,SAAS,kBACjB;EAGF,MAAM,UAAU,MAAM;EACtB,MAAM,YAAY,QAAQ;EAC1B,MAAM,YAAY,QAAQ;EAC1B,MAAM,YAAY,QAAQ;EAE1B,IAAI,CAAC,aAAa,CAAC,WAAW;GAC5B,KAAK,OAAO,KAAK,2CAA2C;GAC5D;EACF;EAGA,MAAM,sBAAM,IAAI,KAAK;EACrB,MAAM,UAAU,MAAM,KAAK,OACxB,OAAO,CAAC,CACR,KAAK,eAAe,CAAC,CACrB,MACC,IACE,GAAG,gBAAgB,WAAW,SAAS,GACvC,GAAG,gBAAgB,WAAW,SAAS,GACvC,GAAG,OAAO,gBAAgB,SAAS,GAAG,GAAG,gBAAgB,WAAW,GAAG,CAAC,CAC1E,CACF;EAEF,MAAM,OAA4B,aAAa,OAAO,cAAc,WAAW,YAAY,CAAC;EAE5F,KAAK,MAAM,UAAU,SAAS;GAE5B,IAAI,UAAU;GACd,MAAM,WAAW,OAAO;GACxB,IAAI,YAAY,OAAO,aAAa,UAClC,KAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,QAAQ,GAAG;IAE7C,IAAI,YAAY,KAAK;IACrB,IAAI,EAAE,SAAS,GAAG,GAAG;KACnB,MAAM,QAAQ,EAAE,MAAM,GAAG;KACzB,YAAY;KACZ,KAAK,MAAM,KAAK,OACd,IAAI,WAAW,YAAY,UAAU;IAEzC;IACA,IAAI,cAAc,GAAG;KACnB,UAAU;KACV;IACF;GACF;GAGF,IAAI,SAAS;IACX,KAAK,OAAO,KACV;KAAE,YAAY,OAAO;KAAY;IAAU,GAC3C,mCACF;IAKA,MAAM,YACJ,MAAM,KAAK,OACR,OAAO,CAAC,CACR,KAAK,iBAAiB,CAAC,CACvB,MAAM,GAAG,kBAAkB,IAAI,OAAO,UAAU,CAAC,CAAC,CAClD,MAAM,CAAC,EAAA,CACV;IAEF,IAAI,CAAC,UAAU;KACb,KAAK,OAAO,KACV,EAAE,YAAY,OAAO,WAAW,GAChC,iEACF;KACA,MAAM,KAAK,OAAO,OAAO,eAAe,CAAC,CAAC,MAAM,GAAG,gBAAgB,IAAI,OAAO,EAAE,CAAC;KACjF;IACF;IAcA,MAAM,eAAc,MAXA,KAAK,OACtB,OAAO,CAAC,CACR,KAAK,aAAa,CAAC,CACnB,MACC,IACE,GAAG,cAAc,YAAY,OAAO,UAAU,GAC9C,GAAG,cAAc,QAAQ,cAAc,CACzC,CACF,EAAA,CAGwB,MAAM,MAAW,EAAE,WAAW,IAAI;IAE5D,IAAI,aAEF,MAAM,KAAK,OACR,OAAO,aAAa,CAAC,CACrB,IAAI,EAAE,QAAQ,KAAK,CAAC,CAAC,CACrB,MAAM,GAAG,cAAc,IAAI,YAAY,EAAE,CAAC;IAO/C,gBAAgB,KACd,IAAI,SAAS,SAAS,WAAW;KAC/B,KAAK,YAAY,KAAK;MACpB,UAAU,KAAK;MACf,OAAO,iBACL,oBACA;OACE,WAAW,SAAS;OACpB,YAAY,OAAO;OACnB,MAAM,SAAS;OACf,OAAO,SAAS,SAAS,CAAC;OAC1B,QAAQ;MACV,GACA,UACA,MAAM,SAAS,OACjB;MACA;MACA;KACF,CAAC;IACH,CAAC,CACH;IAGA,MAAM,KAAK,OAAO,OAAO,eAAe,CAAC,CAAC,MAAM,GAAG,gBAAgB,IAAI,OAAO,EAAE,CAAC;GAInF;EACF;EAEA,MAAM,QAAQ,IAAI,eAAe;CACnC;AACF;AAEA,eAAsB,mBAAmB;CACvC,SAAS,aAAa;EAAE,MAAM;EAAgB,OAAO,OAAO;CAAU,CAAC;CACvE,QAAQ,IAAI,YAAY;EAAE,KAAK,OAAO;EAAW,MAAM;EAAU;CAAO,CAAC;CACzE,MAAM,SAAS,eAAe;EAAE,KAAK,OAAO;EAAc,iBAAiB;EAAU;CAAO,CAAC;CAC7F,QAAM,OAAO;CACb,KAAK,OAAO;CACZ,mBAAmB,IAAI,eAAe;EACpC,OAAO,MAAM;EACb,QAAQ,QAAQ;EAChB;CACF,CAAC;CACD,WAAW,IAAI,eAAe;EAC5B,OAAO,MAAM;EACb,QAAQ,QAAQ;EAChB,OAAO,gBAAgB;EACvB,UAAU,UAAU,QAAQ;EAC5B,WAAW,QAAQ;EACnB,WAAW,OAAO;EAClB;CACF,CAAC;CAED,iBAAiB,IAAI,sBAAsB;EACzC,OAAO,MAAM;EACb,QAAQ,QAAQ;EAChB,OAAO,gBAAgB;EACvB,UAAU,UAAU,QAAQ;EAC5B;CACF,CAAC;CAED,SAAS,IAAI,YAAY;EACvB;EACA;EACA;EACA,aAAa,OAAO;EACpB;EACA;CACF,CAAC;CAED,iBAAiB,oBAAoB,UAAU,QAAQ,OAAO,MAAM;CAEpE,OAAO,KAAK,uBAAuB;CACnC,MAAM,OAAO,MAAM;AACrB;AAEA,eAAsB,kBAAiC;CACrD,QAAQ,KAAK,oBAAoB;CACjC,IAAI,gBAAgB,cAAc,cAAc;CAChD,IAAI,QAAQ,MAAM,OAAO,KAAK;CAC9B,IAAIA,OAAK,MAAMA,MAAI,IAAI;CACvB,IAAI,OAAO,MAAM,MAAM,WAAW;CAClC,QAAQ,KAAK,sBAAsB;AACrC"}
1
+ {"version":3,"file":"main-DfrrNKp9.mjs","names":["sql"],"sources":["../src/services/events/main.ts"],"sourcesContent":["import { loadEnv, readBaseConfig } from \"@/index.js\";\nimport { createLogger } from \"@/index.js\";\nimport { RedisClient } from \"@/index.js\";\nimport {\n StreamConsumer,\n PendingMessageScanner,\n StreamProducer,\n type StreamMessage,\n} from \"@/index.js\";\nimport { BaseWorker } from \"@/index.js\";\nimport { STREAMS, CONSUMER_GROUPS, buildStreamEvent } from \"@/index.js\";\nimport { type StreamName } from \"@/contracts/streams.js\";\nimport { createDatabase } from \"@/db/index.js\";\nimport { workflowWaiters, workflowSteps, workflowInstances, messageLogs } from \"@/db/schema.js\";\nimport { eq, and, or, isNull, gt } from \"drizzle-orm\";\nimport { type WorkerOptions } from \"@/shared/index.js\";\nimport { startHealthReporter } from \"@/workers/index.js\";\n\nloadEnv();\nconst config = readBaseConfig();\nlet logger: ReturnType<typeof createLogger>;\nlet redis: RedisClient;\nlet sql: any;\nlet db: any;\n\nlet consumer: StreamConsumer;\nlet pendingScanner: PendingMessageScanner;\nlet worker: BaseWorker;\nlet healthInterval: NodeJS.Timeout | null = null;\n\nlet workflowProducer: StreamProducer;\n\nexport interface EventWorkerOptions extends WorkerOptions {\n db: any;\n workflowProducer: any;\n}\n\nexport class EventWorker extends BaseWorker {\n private readonly dbConn: any;\n private readonly workflowProducer: any;\n private messageLogBuffer: { log: any; resolve: () => void; reject: (err: any) => void }[] = [];\n private eventBuffer: {\n producer: any;\n event: any;\n resolve: () => void;\n reject: (err: any) => void;\n }[] = [];\n private flushInterval: NodeJS.Timeout | null = null;\n private isFlushing = false;\n\n constructor(options: EventWorkerOptions) {\n super(options);\n this.dbConn = options.db;\n this.workflowProducer = options.workflowProducer;\n this.startFlushInterval();\n }\n\n private startFlushInterval() {\n this.flushInterval = setInterval(() => {\n void this.flushLogs();\n }, config.LOG_FLUSH_INTERVAL_MS);\n }\n\n private async flushLogs() {\n if (this.isFlushing || (this.messageLogBuffer.length === 0 && this.eventBuffer.length === 0))\n return;\n this.isFlushing = true;\n\n const startTime = Date.now();\n const batch = this.messageLogBuffer;\n this.messageLogBuffer = [];\n\n const events = this.eventBuffer;\n this.eventBuffer = [];\n\n try {\n if (batch.length > 0) {\n await this.dbConn\n .insert(messageLogs)\n .values(batch.map((b) => b.log))\n .onConflictDoNothing(); // Idempotent insert\n for (const b of batch) b.resolve();\n\n this.logger.info(\n {\n batchSize: batch.length,\n latencyMs: Date.now() - startTime,\n },\n \"flushed delivery logs to database\",\n );\n }\n\n if (events.length > 0) {\n const byProducer = new Map<any, typeof events>();\n for (const item of events) {\n if (!byProducer.has(item.producer)) byProducer.set(item.producer, []);\n byProducer.get(item.producer)!.push(item);\n }\n for (const [producer, evts] of byProducer) {\n await producer.publishBatch(evts.map((e) => e.event));\n for (const e of evts) e.resolve();\n }\n }\n } catch (error: any) {\n this.logger.error(\n {\n batchSize: batch.length,\n error: error.message,\n },\n \"failed to flush delivery logs — dropping batch\",\n );\n for (const b of batch) b.reject(error);\n for (const e of events) e.reject(error);\n } finally {\n this.isFlushing = false;\n }\n }\n\n override async stop(): Promise<void> {\n if (this.flushInterval) clearInterval(this.flushInterval);\n await super.stop();\n await this.flushLogs();\n }\n\n override async process(message: StreamMessage): Promise<void> {\n const { event } = message;\n const publishPromises: Promise<void>[] = [];\n\n if (\n event.type === \"notification.dispatched\" ||\n event.type === \"notification.delivered\" ||\n event.type === \"notification.failed\"\n ) {\n const payload = event.payload as any;\n if (this.messageLogBuffer.length >= config.LOG_BUFFER_MAX_SIZE) {\n this.logger.warn(\n { dropped: 1, bufferSize: this.messageLogBuffer.length },\n \"log buffer full — dropping oldest delivery log\",\n );\n const dropped = this.messageLogBuffer.shift(); // Drop oldest to apply back-pressure/prevent OOM\n if (dropped) dropped.reject(new Error(\"Dropped due to buffer overflow\"));\n }\n\n const status =\n event.type === \"notification.dispatched\"\n ? \"dispatched\"\n : event.type === \"notification.delivered\"\n ? \"delivered\"\n : \"failed\";\n\n const kind = event.type === \"notification.dispatched\" ? \"dispatched\" : \"attempt\";\n\n publishPromises.push(\n new Promise((resolve, reject) => {\n this.messageLogBuffer.push({\n log: {\n projectId: payload.projectId,\n taskId: payload.taskId,\n providerMessageId: payload.providerMessageId || null,\n channel: payload.channel,\n attempt: payload.attempt || 1,\n kind,\n status,\n templateId: payload.templateId || null,\n workflowInstanceId: payload.workflowInstanceId || null,\n campaignId: payload.campaignId || null,\n },\n resolve,\n reject,\n });\n }),\n );\n await Promise.all(publishPromises);\n return;\n }\n\n if (event.type !== \"event.received\") {\n return;\n }\n\n const payload = event.payload as any;\n const eventName = payload.eventName;\n const eventData = payload.payload;\n const projectId = payload.projectId;\n\n if (!eventName || !projectId) {\n this.logger.warn(\"Missing eventName or projectId in payload\");\n return;\n }\n\n // Find matching waiters\n const now = new Date();\n const waiters = await this.dbConn\n .select()\n .from(workflowWaiters)\n .where(\n and(\n eq(workflowWaiters.eventName, eventName),\n eq(workflowWaiters.projectId, projectId),\n or(isNull(workflowWaiters.expiresAt), gt(workflowWaiters.expiresAt, now)),\n ),\n );\n\n const data: Record<string, any> = eventData && typeof eventData === \"object\" ? eventData : {};\n\n for (const waiter of waiters) {\n // Check match criteria\n let matched = true;\n const criteria = waiter.matchCriteria as Record<string, any>;\n if (criteria && typeof criteria === \"object\") {\n for (const [k, v] of Object.entries(criteria)) {\n // simple dot notation check for a flat object or 1 level deep\n let dataValue = data[k];\n if (k.includes(\".\")) {\n const parts = k.split(\".\");\n dataValue = data;\n for (const p of parts) {\n if (dataValue) dataValue = dataValue[p];\n }\n }\n if (dataValue !== v) {\n matched = false;\n break;\n }\n }\n }\n\n if (matched) {\n this.logger.info(\n { instanceId: waiter.instanceId, eventName },\n \"Event matched a sleeping workflow\",\n );\n\n // The resume event must carry everything WorkflowWorker.process requires\n // (name, instanceId, projectId) or it is dropped and the instance — whose\n // waiter we are about to delete — is stranded forever.\n const instance = (\n await this.dbConn\n .select()\n .from(workflowInstances)\n .where(eq(workflowInstances.id, waiter.instanceId))\n .limit(1)\n )[0];\n\n if (!instance) {\n this.logger.warn(\n { instanceId: waiter.instanceId },\n \"waiter references a missing workflow instance — dropping waiter\",\n );\n await this.dbConn.delete(workflowWaiters).where(eq(workflowWaiters.id, waiter.id));\n continue;\n }\n\n // Find the step that is waiting\n const steps = await this.dbConn\n .select()\n .from(workflowSteps)\n .where(\n and(\n eq(workflowSteps.instanceId, waiter.instanceId),\n eq(workflowSteps.action, \"waitForEvent\"),\n ),\n );\n\n // Get the latest one that has no output\n const pendingStep = steps.find((s: any) => s.output === null);\n\n if (pendingStep) {\n // Update the step with the event payload\n await this.dbConn\n .update(workflowSteps)\n .set({ output: data })\n .where(eq(workflowSteps.id, pendingStep.id));\n }\n\n // Wake up workflow. Publish BEFORE deleting the waiter: a crash after the\n // delete but before the publish would leave the instance unrecoverable,\n // whereas a duplicate resume is idempotent (the instance lock and the\n // replayed step outputs both absorb it).\n publishPromises.push(\n new Promise((resolve, reject) => {\n this.eventBuffer.push({\n producer: this.workflowProducer,\n event: buildStreamEvent(\n \"workflow.resumed\",\n {\n projectId: instance.projectId,\n instanceId: waiter.instanceId,\n name: instance.name,\n input: instance.input ?? {},\n reason: \"event_matched\",\n },\n \"events\",\n event.metadata.traceId,\n ),\n resolve,\n reject,\n });\n }),\n );\n\n // Delete the waiter\n await this.dbConn.delete(workflowWaiters).where(eq(workflowWaiters.id, waiter.id));\n\n // The timer ZSET entry is left to expire: the poll loop re-checks the\n // instance status and drops the entry if it is no longer pending.\n }\n }\n\n await Promise.all(publishPromises);\n }\n}\n\nexport async function startEventWorker() {\n logger = createLogger({ name: \"event-worker\", level: config.LOG_LEVEL });\n redis = new RedisClient({ url: config.REDIS_URL, name: \"events\", logger });\n const dbData = createDatabase({ url: config.DATABASE_URL, applicationName: \"events\", logger });\n sql = dbData.sql;\n db = dbData.db;\n workflowProducer = new StreamProducer({\n redis: redis.native,\n stream: STREAMS.WORKFLOW_INBOUND,\n logger,\n });\n consumer = new StreamConsumer({\n redis: redis.native,\n stream: STREAMS.EVENTS_INBOUND as StreamName,\n group: CONSUMER_GROUPS.EVENTS as any,\n consumer: `events-${process.pid}`,\n dlqStream: STREAMS.DEAD_LETTER,\n batchSize: config.WORKER_CONCURRENCY,\n logger,\n });\n\n pendingScanner = new PendingMessageScanner({\n redis: redis.native,\n stream: STREAMS.EVENTS_INBOUND as StreamName,\n group: CONSUMER_GROUPS.EVENTS as any,\n consumer: `events-${process.pid}`,\n logger,\n });\n\n worker = new EventWorker({\n consumer,\n pendingScanner,\n logger,\n concurrency: config.WORKER_CONCURRENCY,\n db,\n workflowProducer,\n });\n\n healthInterval = startHealthReporter(\"events\", worker, redis, logger);\n\n logger.info(\"event worker starting\");\n await worker.start();\n}\n\nexport async function stopEventWorker(): Promise<void> {\n logger?.info(\"shutdown initiated\");\n if (healthInterval) clearInterval(healthInterval);\n if (worker) await worker.stop();\n if (sql) await sql.end();\n if (redis) await redis.disconnect();\n logger?.info(\"event worker stopped\");\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { E as UserRepository, J as PendingMessageScanner, Jt as buildStreamEvent, S as PreferenceRepository, T as TemplateRepository, W as getPriorityBucket, X as StreamProducer, Y as StreamConsumer, Yt as CONSUMER_GROUPS, Zt as INBOUND_STREAMS, an as registry, c as BaseWorker, en as STREAMS, et as createLogger, g as TemplateCache, hn as readBaseConfig, it as createDatabase, k as RedisClient, pn as loadEnv, rt as IdempotencyGuard, u as startHealthReporter, x as ContactRepository } from "./src-vG79L-8m.mjs";
1
+ import { E as UserRepository, J as PendingMessageScanner, Jt as buildStreamEvent, S as PreferenceRepository, T as TemplateRepository, W as getPriorityBucket, X as StreamProducer, Y as StreamConsumer, Yt as CONSUMER_GROUPS, Zt as INBOUND_STREAMS, an as registry, c as BaseWorker, en as STREAMS, et as createLogger, g as TemplateCache, hn as readBaseConfig, it as createDatabase, k as RedisClient, pn as loadEnv, rt as IdempotencyGuard, u as startHealthReporter, x as ContactRepository } from "./src-CpUnBl_M.mjs";
2
2
  //#region src/services/enricher/main.ts
3
3
  loadEnv();
4
4
  const config = readBaseConfig();
@@ -430,4 +430,4 @@ async function stopEnricherWorker() {
430
430
  //#endregion
431
431
  export { startEnricherWorker, stopEnricherWorker };
432
432
 
433
- //# sourceMappingURL=main-DeNFQ-UL.mjs.map
433
+ //# sourceMappingURL=main-DsDkyBZi.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"main-DeNFQ-UL.mjs","names":[],"sources":["../src/services/enricher/main.ts"],"sourcesContent":["import { loadEnv, readBaseConfig } from \"@/index.js\";\nimport { createLogger } from \"@/index.js\";\nimport { RedisClient } from \"@/index.js\";\nimport {\n StreamConsumer,\n PendingMessageScanner,\n StreamProducer,\n type StreamMessage,\n} from \"@/index.js\";\nimport { BaseWorker } from \"@/index.js\";\nimport {\n STREAMS,\n INBOUND_STREAMS,\n CONSUMER_GROUPS,\n registry,\n buildStreamEvent,\n type NotificationCreatedPayload,\n type NotificationEnrichedPayload,\n} from \"@/index.js\";\nimport { type StreamName } from \"@/contracts/streams.js\";\nimport { IdempotencyGuard } from \"@/index.js\";\nimport { createDatabase } from \"@/db/index.js\";\nimport {\n UserRepository,\n PreferenceRepository,\n TemplateRepository,\n ContactRepository,\n} from \"@/index.js\";\nimport { TemplateCache } from \"@/templates/index.js\";\nimport { getPriorityBucket, type WorkerOptions } from \"@/shared/index.js\";\nimport { startHealthReporter } from \"@/workers/index.js\";\n\n// ─── Bootstrap ─────────────────────────────────────────────────────────────\n\nloadEnv();\nconst config = readBaseConfig();\n\nlet logger: ReturnType<typeof createLogger>;\nlet redis: RedisClient;\nlet sql: any;\nlet db: any;\n\nlet consumer: StreamConsumer;\nlet pendingScanner: PendingMessageScanner;\nlet worker: BaseWorker;\nlet healthInterval: NodeJS.Timeout | null = null;\n\nexport interface EnricherWorkerOptions extends WorkerOptions {\n producers: any;\n idempotency: any;\n userRepo: any;\n prefRepo: any;\n contactRepo: any;\n templateCache: TemplateCache;\n}\n\nexport class EnricherWorker extends BaseWorker {\n private readonly producers: any;\n private readonly idempotency: any;\n private readonly userRepo: any;\n private readonly prefRepo: any;\n private readonly contactRepo: any;\n private readonly templateCache: TemplateCache;\n\n private userBatch: {\n projectId: string;\n userId: string;\n resolve: (p: any) => void;\n reject: (e: any) => void;\n }[] = [];\n private batchTimer: NodeJS.Timeout | null = null;\n private eventBuffer: {\n producer: any;\n event: any;\n resolve: () => void;\n reject: (e: any) => void;\n }[] = [];\n private flushTimer: NodeJS.Timeout | null = null;\n\n private contactBatch: {\n projectId: string;\n userIds: string[];\n resolve: (c: Map<string, any[]>) => void;\n reject: (e: any) => void;\n }[] = [];\n private contactBatchTimer: NodeJS.Timeout | null = null;\n\n private async loadContacts(projectId: string, userIds: string[]): Promise<Map<string, any[]>> {\n return new Promise((resolve, reject) => {\n this.contactBatch.push({ projectId, userIds, resolve, reject });\n if (this.contactBatch.length >= 500) {\n if (this.contactBatchTimer) clearTimeout(this.contactBatchTimer);\n void this.flushContactBatch();\n } else if (!this.contactBatchTimer) {\n this.contactBatchTimer = setTimeout(() => void this.flushContactBatch(), 10);\n }\n });\n }\n\n private async flushContactBatch(): Promise<void> {\n const batch = this.contactBatch;\n this.contactBatch = [];\n this.contactBatchTimer = null;\n if (batch.length === 0) return;\n\n try {\n const byProject = new Map<string, typeof batch>();\n for (const b of batch) {\n if (!byProject.has(b.projectId)) byProject.set(b.projectId, []);\n byProject.get(b.projectId)!.push(b);\n }\n\n for (const [projectId, items] of byProject) {\n const userIds = Array.from(new Set(items.flatMap((i) => i.userIds)));\n const contactsMap = await this.contactRepo.findActiveByUserIds(projectId, userIds);\n for (const item of items) {\n item.resolve(contactsMap);\n }\n }\n } catch (err) {\n for (const b of batch) b.reject(err);\n }\n }\n\n constructor(options: EnricherWorkerOptions) {\n super(options);\n this.producers = options.producers;\n this.idempotency = options.idempotency;\n this.userRepo = options.userRepo;\n this.prefRepo = options.prefRepo;\n this.contactRepo = options.contactRepo;\n this.templateCache = options.templateCache;\n\n this.flushTimer = setInterval(() => void this.flushWorkerBuffers(), 100);\n }\n\n override async stop(): Promise<void> {\n if (this.batchTimer) {\n clearTimeout(this.batchTimer);\n this.batchTimer = null;\n }\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n if (this.contactBatchTimer) {\n clearTimeout(this.contactBatchTimer);\n this.contactBatchTimer = null;\n }\n await this.flushContactBatch();\n await this.flushUserBatch();\n await this.flushWorkerBuffers();\n await super.stop();\n }\n\n private async flushWorkerBuffers(): Promise<void> {\n if (this.eventBuffer.length === 0) return;\n const events = this.eventBuffer;\n this.eventBuffer = [];\n\n try {\n const byProducer = new Map<any, typeof events>();\n for (const e of events) {\n if (!byProducer.has(e.producer)) byProducer.set(e.producer, []);\n byProducer.get(e.producer)!.push(e);\n }\n for (const [producer, batch] of byProducer) {\n await producer.publishBatch(batch.map((b) => b.event));\n for (const b of batch) b.resolve();\n }\n } catch (err: any) {\n this.logger.error({ err }, \"failed to flush events in EnricherWorker\");\n for (const e of events) e.reject(err);\n }\n }\n\n private async loadUser(projectId: string, userId: string): Promise<any> {\n return new Promise((resolve, reject) => {\n this.userBatch.push({ projectId, userId, resolve, reject });\n if (this.userBatch.length >= 500) {\n if (this.batchTimer) clearTimeout(this.batchTimer);\n void this.flushUserBatch();\n } else if (!this.batchTimer) {\n this.batchTimer = setTimeout(() => {\n void this.flushUserBatch();\n }, 10);\n }\n });\n }\n\n private async flushUserBatch() {\n const batch = this.userBatch;\n this.userBatch = [];\n this.batchTimer = null;\n\n const byProject = new Map<string, typeof batch>();\n for (const b of batch) {\n if (!byProject.has(b.projectId)) byProject.set(b.projectId, []);\n byProject.get(b.projectId)!.push(b);\n }\n\n for (const [projectId, reqs] of byProject.entries()) {\n try {\n const uniqueIds = Array.from(new Set(reqs.map((r) => r.userId)));\n const profiles = await this.userRepo.findRecordsByIds(projectId, uniqueIds);\n const profileMap = new Map(profiles.map((p: any) => [p.userId, p]));\n\n for (const req of reqs) {\n req.resolve(profileMap.get(req.userId) || null);\n }\n } catch (err) {\n for (const req of reqs) req.reject(err);\n }\n }\n }\n\n async process(message: StreamMessage): Promise<void> {\n const { event } = message;\n const publishPromises: Promise<void>[] = [];\n\n let isRequested = true;\n const requestedResult = registry.safeParsePayload(\"notification.requested\", event.payload);\n let createdResult: any = null;\n if (!requestedResult.success) {\n createdResult = registry.safeParsePayload(\"notification.created\", event.payload);\n isRequested = false;\n }\n\n if (!isRequested && (!createdResult || !createdResult.success)) {\n const issues = createdResult\n ? createdResult.error.issues\n : (requestedResult as any).error.issues;\n this.logger.warn({ messageId: message.id, issues }, \"invalid payload — skipping\");\n return;\n }\n\n // Handle legacy notification.created\n if (!isRequested) {\n const raw = createdResult.data as NotificationCreatedPayload;\n const dedupeId = `${raw.projectId}:${raw.idempotencyKey ?? event.id}`;\n if (!(await this.idempotency.checkAndMark(dedupeId, 60))) return;\n try {\n const profile = await this.userRepo.findRecordById(raw.projectId, raw.recipientId);\n if (!profile) return;\n\n const prefs = await this.prefRepo.findByUserId(raw.projectId, raw.recipientId);\n const optedOutTypes = new Set(\n prefs.filter((p: any) => !p.optedIn).map((p: any) => p.eventType),\n );\n\n const enrichedPayload: NotificationEnrichedPayload = {\n projectId: raw.projectId,\n rawEventId: event.id,\n recipientId: raw.recipientId,\n channel: raw.channel,\n priority: raw.priority,\n templateId: raw.templateId,\n templateVariables: raw.payload,\n recipient: {\n id: profile.userId,\n email: profile.email ?? undefined,\n locale: profile.language ?? \"en\",\n timezone: profile.timezone ?? \"UTC\",\n preferences: {\n optedOut:\n optedOutTypes.has(event.type) || profile.preferences.topics?.[event.type] === false,\n channels: Object.entries(profile.preferences.channels ?? {})\n .filter(([_, enabled]) => !enabled)\n .map(([channel]) => channel as any),\n quietHours: profile.preferences.quietHours,\n },\n },\n // No campaignId: this is the legacy `notification.created` path,\n // which predates campaigns and carries no label to attribute to.\n scheduledAt: raw.scheduledAt,\n };\n\n const p = getPriorityBucket(raw.priority);\n const producer = this.producers[p] ?? this.producers[\"normal\"]!;\n\n publishPromises.push(\n new Promise((resolve, reject) => {\n this.eventBuffer.push({\n producer,\n event: buildStreamEvent(\n \"notification.enriched\",\n enrichedPayload as Record<string, unknown>,\n \"enricher\",\n event.metadata.traceId,\n ),\n resolve,\n reject,\n });\n }),\n );\n this.logger.info(\n { messageId: message.id, eventId: event.id, recipientId: raw.recipientId },\n \"event enriched\",\n );\n } catch (err) {\n throw err;\n }\n await Promise.all(publishPromises).catch(async (err) => {\n await this.idempotency.unmark(dedupeId).catch(() => {});\n throw err;\n });\n await this.idempotency.markProcessed(dedupeId);\n return;\n }\n\n // Handle new notification.requested\n const raw = (requestedResult as any).data;\n const dedupeId = `${raw.projectId}:${raw.idempotencyKey ?? event.id}`;\n if (!(await this.idempotency.checkAndMark(dedupeId, 60))) return;\n try {\n let userIds: string[] = [];\n if (raw.target.type === \"user\") {\n userIds = [raw.target.userId];\n } else if (raw.target.type === \"segment\") {\n userIds = await this.userRepo.findUsersBySegment(raw.projectId, raw.target.segment);\n this.logger.info(\n { segment: raw.target.segment, count: userIds.length },\n \"Resolved segment\",\n );\n } else if (raw.target.type === \"topic\") {\n userIds = await this.userRepo.findUsersByTopic(raw.projectId, raw.target.topic);\n this.logger.info({ topic: raw.target.topic, count: userIds.length }, \"Resolved topic\");\n } else {\n this.logger.warn({ target: raw.target }, \"Segment/topic resolution not fully implemented\");\n // Stub: maybe resolve later\n }\n\n const maxUsers = readBaseConfig().SEGMENT_MAX_USERS;\n if (userIds.length > maxUsers) {\n this.logger.error(\n { count: userIds.length, max: maxUsers, projectId: raw.projectId, eventId: event.id },\n \"Segment fan-out exceeds maximum allowed limit\",\n );\n const p = getPriorityBucket(raw.priority ?? \"normal\");\n const producer = this.producers[p] ?? this.producers.normal;\n await producer.publish(\n buildStreamEvent(\n \"notification.failed\",\n {\n projectId: raw.projectId,\n rawEventId: event.id,\n error: `Segment fan-out of ${userIds.length} exceeds limit of ${maxUsers}`,\n },\n \"enricher\",\n event.metadata.traceId,\n ),\n );\n return;\n }\n\n // Topic opt-outs are keyed on the TEMPLATE's topics, not the envelope type.\n // `event.type` here is \"notification.requested\", which no user ever sets a\n // preference against, so keying on it silently disabled every opt-out.\n const template = raw.templateId\n ? await this.templateCache.getCachedTemplate(raw.projectId, raw.templateId)\n : null;\n const topics: string[] = template?.topics ?? [];\n\n const channels =\n raw.channels && raw.channels.length > 0 ? raw.channels : ([\"email\"] as any[]);\n const isFallback = (raw as any).fallback === true;\n\n // If fallback is true, we only emit the first channel, and pass the rest in fallbackChain.\n // If fallback is false, we emit all channels concurrently.\n const channelsToProcess = isFallback ? [channels[0]] : channels;\n const fallbackChain = isFallback ? channels.slice(1) : undefined;\n\n const chunkArray = <T>(arr: T[], size: number) =>\n Array.from({ length: Math.ceil(arr.length / size) }, (v, i) =>\n arr.slice(i * size, i * size + size),\n );\n\n const chunks = chunkArray(userIds, 500);\n\n for (const chunk of chunks) {\n const profiles = (\n await Promise.all(chunk.map((id) => this.loadUser(raw.projectId, id)))\n ).filter(Boolean);\n const contactsByUser = await this.loadContacts(\n raw.projectId,\n profiles.map((profile: any) => profile.userId),\n );\n\n const batchedEvents: Record<\n \"critical\" | \"high\" | \"normal\" | \"low\",\n Omit<any, \"id\" | \"timestamp\">[]\n > = {\n critical: [],\n high: [],\n normal: [],\n low: [],\n };\n\n for (const profile of profiles) {\n for (const channel of channelsToProcess) {\n const contacts = contactsByUser.get(profile.userId) ?? [];\n const channelContacts = contacts.filter((contact: any) => contact.channel === channel);\n // Push resolves its active tokens at send time so token invalidation\n // remains current. Other channels need one task per address.\n const destinations =\n channel === \"push\" ? [undefined] : channelContacts.map((c: any) => c.target);\n if (destinations.length === 0) {\n this.logger.info(\n { recipientId: profile.userId, channel },\n \"no active contact for channel\",\n );\n continue;\n }\n for (const destination of destinations) {\n const enrichedPayload: NotificationEnrichedPayload = {\n projectId: raw.projectId,\n rawEventId: event.id,\n recipientId: profile.userId,\n channel: channel,\n priority: \"normal\",\n templateId: raw.templateId,\n templateVariables: raw.data,\n aiPrompts: raw.aiPrompts,\n recipient: {\n id: profile.userId,\n email:\n channel === \"email\"\n ? (destination ?? profile.email ?? undefined)\n : (profile.email ?? undefined),\n phone: channel === \"sms\" || channel === \"whatsapp\" ? destination : undefined,\n webhook: channel === \"webhook\" ? destination : undefined,\n telegram: channel === \"telegram\" ? destination : undefined,\n discord: channel === \"discord\" ? destination : undefined,\n slack: channel === \"slack\" ? destination : undefined,\n locale: profile.language ?? \"en\",\n timezone: profile.timezone ?? \"UTC\",\n preferences: {\n // Opted out if the user disabled ANY topic this template carries.\n optedOut: topics.some((t) => profile.preferences.topics?.[t] === false),\n channels: Object.entries(profile.preferences.channels ?? {})\n .filter(([_, enabled]) => !enabled)\n .map(([channel]) => channel as any),\n quietHours: profile.preferences.quietHours,\n },\n },\n scheduledAt: raw.scheduledAt,\n fallbackChain: fallbackChain?.length ? fallbackChain : undefined,\n campaignId: raw.campaignId,\n };\n\n const msgPriority = raw.priority ?? \"normal\";\n const p = getPriorityBucket(msgPriority);\n\n enrichedPayload.priority = msgPriority;\n\n batchedEvents[p].push(\n buildStreamEvent(\n \"notification.enriched\",\n enrichedPayload as Record<string, unknown>,\n \"enricher\",\n event.metadata.traceId,\n ),\n );\n }\n }\n }\n\n for (const p of [\"critical\", \"normal\", \"low\"] as const) {\n if (batchedEvents[p].length > 0) {\n const producer = this.producers[p] ?? this.producers.normal;\n for (const ev of batchedEvents[p]) {\n publishPromises.push(\n new Promise((resolve, reject) => {\n this.eventBuffer.push({ producer, event: ev, resolve, reject });\n }),\n );\n }\n }\n }\n }\n\n this.logger.info(\n {\n messageId: message.id,\n eventId: event.id,\n target: raw.target.type,\n traceId: event.metadata.traceId,\n },\n \"event enriched\",\n );\n } catch (err) {\n throw err;\n }\n\n await Promise.all(publishPromises).catch(async (err) => {\n await this.idempotency.unmark(dedupeId).catch(() => {});\n throw err;\n });\n\n await this.idempotency.markProcessed(dedupeId);\n }\n}\n\nexport async function startEnricherWorker() {\n logger = createLogger({ name: \"enricher\", level: config.LOG_LEVEL });\n redis = new RedisClient({ url: config.REDIS_URL, name: \"enricher\", logger });\n const dbData = createDatabase({ url: config.DATABASE_URL, applicationName: \"enricher\", logger });\n sql = dbData.sql;\n db = dbData.db;\n consumer = new StreamConsumer({\n redis: redis.native,\n stream: INBOUND_STREAMS as unknown as StreamName[],\n group: CONSUMER_GROUPS.ENRICHER,\n consumer: `enricher-${process.pid}`,\n dlqStream: STREAMS.DEAD_LETTER,\n batchSize: config.WORKER_CONCURRENCY,\n logger,\n });\n\n pendingScanner = new PendingMessageScanner({\n redis: redis.native,\n stream: INBOUND_STREAMS as unknown as StreamName[],\n group: CONSUMER_GROUPS.ENRICHER,\n consumer: `enricher-${process.pid}`,\n logger,\n });\n\n const producers = {\n critical: new StreamProducer({\n redis: redis.native,\n stream: STREAMS.ENRICHED_CRITICAL,\n logger,\n }),\n normal: new StreamProducer({ redis: redis.native, stream: STREAMS.ENRICHED_NORMAL, logger }),\n low: new StreamProducer({ redis: redis.native, stream: STREAMS.ENRICHED_LOW, logger }),\n };\n\n const idempotency = new IdempotencyGuard({\n redis: redis.native,\n keyPrefix: \"notif:processed:enricher\",\n ttlSeconds: 86_400,\n });\n\n const userRepo = new UserRepository(db);\n const prefRepo = new PreferenceRepository(db);\n const contactRepo = new ContactRepository(db);\n const templateCache = new TemplateCache(new TemplateRepository(db));\n\n // ─── Stage 1: Context Enricher ──────────────────────────────────────────────\n //\n // Pipeline:\n // 1. Parse payload as notification.created\n // 2. Idempotency check — drop if already processed\n // 3. Load user profile (language, timezone) from DB\n // 4. Load all stored preferences for this user\n // 5. Publish notification.enriched to ENRICHED stream\n\n // 5. Publish notification.enriched to ENRICHED stream\n\n worker = new EnricherWorker({\n consumer,\n pendingScanner,\n logger,\n maxRetriesBeforeDlq: 5,\n concurrency: config.WORKER_CONCURRENCY,\n producers,\n idempotency,\n userRepo,\n prefRepo,\n contactRepo,\n templateCache,\n });\n\n // ─── Health check interval ──────────────────────────────────────────────────\n\n healthInterval = startHealthReporter(\"enricher\", worker, redis, logger);\n\n logger.info({ env: config.NODE_ENV }, \"enricher starting\");\n await worker.start();\n}\n\n// ─── Shutdown ──────────────────────────────────────────────────────────────\n\nexport async function stopEnricherWorker(): Promise<void> {\n logger?.info(\"shutdown initiated\");\n if (healthInterval) {\n clearInterval(healthInterval);\n healthInterval = null;\n }\n if (worker) await worker.stop();\n if (sql) await sql.end();\n if (redis) await redis.disconnect();\n logger?.info(\"enricher stopped\");\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"main-DsDkyBZi.mjs","names":[],"sources":["../src/services/enricher/main.ts"],"sourcesContent":["import { loadEnv, readBaseConfig } from \"@/index.js\";\nimport { createLogger } from \"@/index.js\";\nimport { RedisClient } from \"@/index.js\";\nimport {\n StreamConsumer,\n PendingMessageScanner,\n StreamProducer,\n type StreamMessage,\n} from \"@/index.js\";\nimport { BaseWorker } from \"@/index.js\";\nimport {\n STREAMS,\n INBOUND_STREAMS,\n CONSUMER_GROUPS,\n registry,\n buildStreamEvent,\n type NotificationCreatedPayload,\n type NotificationEnrichedPayload,\n} from \"@/index.js\";\nimport { type StreamName } from \"@/contracts/streams.js\";\nimport { IdempotencyGuard } from \"@/index.js\";\nimport { createDatabase } from \"@/db/index.js\";\nimport {\n UserRepository,\n PreferenceRepository,\n TemplateRepository,\n ContactRepository,\n} from \"@/index.js\";\nimport { TemplateCache } from \"@/templates/index.js\";\nimport { getPriorityBucket, type WorkerOptions } from \"@/shared/index.js\";\nimport { startHealthReporter } from \"@/workers/index.js\";\n\n// ─── Bootstrap ─────────────────────────────────────────────────────────────\n\nloadEnv();\nconst config = readBaseConfig();\n\nlet logger: ReturnType<typeof createLogger>;\nlet redis: RedisClient;\nlet sql: any;\nlet db: any;\n\nlet consumer: StreamConsumer;\nlet pendingScanner: PendingMessageScanner;\nlet worker: BaseWorker;\nlet healthInterval: NodeJS.Timeout | null = null;\n\nexport interface EnricherWorkerOptions extends WorkerOptions {\n producers: any;\n idempotency: any;\n userRepo: any;\n prefRepo: any;\n contactRepo: any;\n templateCache: TemplateCache;\n}\n\nexport class EnricherWorker extends BaseWorker {\n private readonly producers: any;\n private readonly idempotency: any;\n private readonly userRepo: any;\n private readonly prefRepo: any;\n private readonly contactRepo: any;\n private readonly templateCache: TemplateCache;\n\n private userBatch: {\n projectId: string;\n userId: string;\n resolve: (p: any) => void;\n reject: (e: any) => void;\n }[] = [];\n private batchTimer: NodeJS.Timeout | null = null;\n private eventBuffer: {\n producer: any;\n event: any;\n resolve: () => void;\n reject: (e: any) => void;\n }[] = [];\n private flushTimer: NodeJS.Timeout | null = null;\n\n private contactBatch: {\n projectId: string;\n userIds: string[];\n resolve: (c: Map<string, any[]>) => void;\n reject: (e: any) => void;\n }[] = [];\n private contactBatchTimer: NodeJS.Timeout | null = null;\n\n private async loadContacts(projectId: string, userIds: string[]): Promise<Map<string, any[]>> {\n return new Promise((resolve, reject) => {\n this.contactBatch.push({ projectId, userIds, resolve, reject });\n if (this.contactBatch.length >= 500) {\n if (this.contactBatchTimer) clearTimeout(this.contactBatchTimer);\n void this.flushContactBatch();\n } else if (!this.contactBatchTimer) {\n this.contactBatchTimer = setTimeout(() => void this.flushContactBatch(), 10);\n }\n });\n }\n\n private async flushContactBatch(): Promise<void> {\n const batch = this.contactBatch;\n this.contactBatch = [];\n this.contactBatchTimer = null;\n if (batch.length === 0) return;\n\n try {\n const byProject = new Map<string, typeof batch>();\n for (const b of batch) {\n if (!byProject.has(b.projectId)) byProject.set(b.projectId, []);\n byProject.get(b.projectId)!.push(b);\n }\n\n for (const [projectId, items] of byProject) {\n const userIds = Array.from(new Set(items.flatMap((i) => i.userIds)));\n const contactsMap = await this.contactRepo.findActiveByUserIds(projectId, userIds);\n for (const item of items) {\n item.resolve(contactsMap);\n }\n }\n } catch (err) {\n for (const b of batch) b.reject(err);\n }\n }\n\n constructor(options: EnricherWorkerOptions) {\n super(options);\n this.producers = options.producers;\n this.idempotency = options.idempotency;\n this.userRepo = options.userRepo;\n this.prefRepo = options.prefRepo;\n this.contactRepo = options.contactRepo;\n this.templateCache = options.templateCache;\n\n this.flushTimer = setInterval(() => void this.flushWorkerBuffers(), 100);\n }\n\n override async stop(): Promise<void> {\n if (this.batchTimer) {\n clearTimeout(this.batchTimer);\n this.batchTimer = null;\n }\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n if (this.contactBatchTimer) {\n clearTimeout(this.contactBatchTimer);\n this.contactBatchTimer = null;\n }\n await this.flushContactBatch();\n await this.flushUserBatch();\n await this.flushWorkerBuffers();\n await super.stop();\n }\n\n private async flushWorkerBuffers(): Promise<void> {\n if (this.eventBuffer.length === 0) return;\n const events = this.eventBuffer;\n this.eventBuffer = [];\n\n try {\n const byProducer = new Map<any, typeof events>();\n for (const e of events) {\n if (!byProducer.has(e.producer)) byProducer.set(e.producer, []);\n byProducer.get(e.producer)!.push(e);\n }\n for (const [producer, batch] of byProducer) {\n await producer.publishBatch(batch.map((b) => b.event));\n for (const b of batch) b.resolve();\n }\n } catch (err: any) {\n this.logger.error({ err }, \"failed to flush events in EnricherWorker\");\n for (const e of events) e.reject(err);\n }\n }\n\n private async loadUser(projectId: string, userId: string): Promise<any> {\n return new Promise((resolve, reject) => {\n this.userBatch.push({ projectId, userId, resolve, reject });\n if (this.userBatch.length >= 500) {\n if (this.batchTimer) clearTimeout(this.batchTimer);\n void this.flushUserBatch();\n } else if (!this.batchTimer) {\n this.batchTimer = setTimeout(() => {\n void this.flushUserBatch();\n }, 10);\n }\n });\n }\n\n private async flushUserBatch() {\n const batch = this.userBatch;\n this.userBatch = [];\n this.batchTimer = null;\n\n const byProject = new Map<string, typeof batch>();\n for (const b of batch) {\n if (!byProject.has(b.projectId)) byProject.set(b.projectId, []);\n byProject.get(b.projectId)!.push(b);\n }\n\n for (const [projectId, reqs] of byProject.entries()) {\n try {\n const uniqueIds = Array.from(new Set(reqs.map((r) => r.userId)));\n const profiles = await this.userRepo.findRecordsByIds(projectId, uniqueIds);\n const profileMap = new Map(profiles.map((p: any) => [p.userId, p]));\n\n for (const req of reqs) {\n req.resolve(profileMap.get(req.userId) || null);\n }\n } catch (err) {\n for (const req of reqs) req.reject(err);\n }\n }\n }\n\n async process(message: StreamMessage): Promise<void> {\n const { event } = message;\n const publishPromises: Promise<void>[] = [];\n\n let isRequested = true;\n const requestedResult = registry.safeParsePayload(\"notification.requested\", event.payload);\n let createdResult: any = null;\n if (!requestedResult.success) {\n createdResult = registry.safeParsePayload(\"notification.created\", event.payload);\n isRequested = false;\n }\n\n if (!isRequested && (!createdResult || !createdResult.success)) {\n const issues = createdResult\n ? createdResult.error.issues\n : (requestedResult as any).error.issues;\n this.logger.warn({ messageId: message.id, issues }, \"invalid payload — skipping\");\n return;\n }\n\n // Handle legacy notification.created\n if (!isRequested) {\n const raw = createdResult.data as NotificationCreatedPayload;\n const dedupeId = `${raw.projectId}:${raw.idempotencyKey ?? event.id}`;\n if (!(await this.idempotency.checkAndMark(dedupeId, 60))) return;\n try {\n const profile = await this.userRepo.findRecordById(raw.projectId, raw.recipientId);\n if (!profile) return;\n\n const prefs = await this.prefRepo.findByUserId(raw.projectId, raw.recipientId);\n const optedOutTypes = new Set(\n prefs.filter((p: any) => !p.optedIn).map((p: any) => p.eventType),\n );\n\n const enrichedPayload: NotificationEnrichedPayload = {\n projectId: raw.projectId,\n rawEventId: event.id,\n recipientId: raw.recipientId,\n channel: raw.channel,\n priority: raw.priority,\n templateId: raw.templateId,\n templateVariables: raw.payload,\n recipient: {\n id: profile.userId,\n email: profile.email ?? undefined,\n locale: profile.language ?? \"en\",\n timezone: profile.timezone ?? \"UTC\",\n preferences: {\n optedOut:\n optedOutTypes.has(event.type) || profile.preferences.topics?.[event.type] === false,\n channels: Object.entries(profile.preferences.channels ?? {})\n .filter(([_, enabled]) => !enabled)\n .map(([channel]) => channel as any),\n quietHours: profile.preferences.quietHours,\n },\n },\n // No campaignId: this is the legacy `notification.created` path,\n // which predates campaigns and carries no label to attribute to.\n scheduledAt: raw.scheduledAt,\n };\n\n const p = getPriorityBucket(raw.priority);\n const producer = this.producers[p] ?? this.producers[\"normal\"]!;\n\n publishPromises.push(\n new Promise((resolve, reject) => {\n this.eventBuffer.push({\n producer,\n event: buildStreamEvent(\n \"notification.enriched\",\n enrichedPayload as Record<string, unknown>,\n \"enricher\",\n event.metadata.traceId,\n ),\n resolve,\n reject,\n });\n }),\n );\n this.logger.info(\n { messageId: message.id, eventId: event.id, recipientId: raw.recipientId },\n \"event enriched\",\n );\n } catch (err) {\n throw err;\n }\n await Promise.all(publishPromises).catch(async (err) => {\n await this.idempotency.unmark(dedupeId).catch(() => {});\n throw err;\n });\n await this.idempotency.markProcessed(dedupeId);\n return;\n }\n\n // Handle new notification.requested\n const raw = (requestedResult as any).data;\n const dedupeId = `${raw.projectId}:${raw.idempotencyKey ?? event.id}`;\n if (!(await this.idempotency.checkAndMark(dedupeId, 60))) return;\n try {\n let userIds: string[] = [];\n if (raw.target.type === \"user\") {\n userIds = [raw.target.userId];\n } else if (raw.target.type === \"segment\") {\n userIds = await this.userRepo.findUsersBySegment(raw.projectId, raw.target.segment);\n this.logger.info(\n { segment: raw.target.segment, count: userIds.length },\n \"Resolved segment\",\n );\n } else if (raw.target.type === \"topic\") {\n userIds = await this.userRepo.findUsersByTopic(raw.projectId, raw.target.topic);\n this.logger.info({ topic: raw.target.topic, count: userIds.length }, \"Resolved topic\");\n } else {\n this.logger.warn({ target: raw.target }, \"Segment/topic resolution not fully implemented\");\n // Stub: maybe resolve later\n }\n\n const maxUsers = readBaseConfig().SEGMENT_MAX_USERS;\n if (userIds.length > maxUsers) {\n this.logger.error(\n { count: userIds.length, max: maxUsers, projectId: raw.projectId, eventId: event.id },\n \"Segment fan-out exceeds maximum allowed limit\",\n );\n const p = getPriorityBucket(raw.priority ?? \"normal\");\n const producer = this.producers[p] ?? this.producers.normal;\n await producer.publish(\n buildStreamEvent(\n \"notification.failed\",\n {\n projectId: raw.projectId,\n rawEventId: event.id,\n error: `Segment fan-out of ${userIds.length} exceeds limit of ${maxUsers}`,\n },\n \"enricher\",\n event.metadata.traceId,\n ),\n );\n return;\n }\n\n // Topic opt-outs are keyed on the TEMPLATE's topics, not the envelope type.\n // `event.type` here is \"notification.requested\", which no user ever sets a\n // preference against, so keying on it silently disabled every opt-out.\n const template = raw.templateId\n ? await this.templateCache.getCachedTemplate(raw.projectId, raw.templateId)\n : null;\n const topics: string[] = template?.topics ?? [];\n\n const channels =\n raw.channels && raw.channels.length > 0 ? raw.channels : ([\"email\"] as any[]);\n const isFallback = (raw as any).fallback === true;\n\n // If fallback is true, we only emit the first channel, and pass the rest in fallbackChain.\n // If fallback is false, we emit all channels concurrently.\n const channelsToProcess = isFallback ? [channels[0]] : channels;\n const fallbackChain = isFallback ? channels.slice(1) : undefined;\n\n const chunkArray = <T>(arr: T[], size: number) =>\n Array.from({ length: Math.ceil(arr.length / size) }, (v, i) =>\n arr.slice(i * size, i * size + size),\n );\n\n const chunks = chunkArray(userIds, 500);\n\n for (const chunk of chunks) {\n const profiles = (\n await Promise.all(chunk.map((id) => this.loadUser(raw.projectId, id)))\n ).filter(Boolean);\n const contactsByUser = await this.loadContacts(\n raw.projectId,\n profiles.map((profile: any) => profile.userId),\n );\n\n const batchedEvents: Record<\n \"critical\" | \"high\" | \"normal\" | \"low\",\n Omit<any, \"id\" | \"timestamp\">[]\n > = {\n critical: [],\n high: [],\n normal: [],\n low: [],\n };\n\n for (const profile of profiles) {\n for (const channel of channelsToProcess) {\n const contacts = contactsByUser.get(profile.userId) ?? [];\n const channelContacts = contacts.filter((contact: any) => contact.channel === channel);\n // Push resolves its active tokens at send time so token invalidation\n // remains current. Other channels need one task per address.\n const destinations =\n channel === \"push\" ? [undefined] : channelContacts.map((c: any) => c.target);\n if (destinations.length === 0) {\n this.logger.info(\n { recipientId: profile.userId, channel },\n \"no active contact for channel\",\n );\n continue;\n }\n for (const destination of destinations) {\n const enrichedPayload: NotificationEnrichedPayload = {\n projectId: raw.projectId,\n rawEventId: event.id,\n recipientId: profile.userId,\n channel: channel,\n priority: \"normal\",\n templateId: raw.templateId,\n templateVariables: raw.data,\n aiPrompts: raw.aiPrompts,\n recipient: {\n id: profile.userId,\n email:\n channel === \"email\"\n ? (destination ?? profile.email ?? undefined)\n : (profile.email ?? undefined),\n phone: channel === \"sms\" || channel === \"whatsapp\" ? destination : undefined,\n webhook: channel === \"webhook\" ? destination : undefined,\n telegram: channel === \"telegram\" ? destination : undefined,\n discord: channel === \"discord\" ? destination : undefined,\n slack: channel === \"slack\" ? destination : undefined,\n locale: profile.language ?? \"en\",\n timezone: profile.timezone ?? \"UTC\",\n preferences: {\n // Opted out if the user disabled ANY topic this template carries.\n optedOut: topics.some((t) => profile.preferences.topics?.[t] === false),\n channels: Object.entries(profile.preferences.channels ?? {})\n .filter(([_, enabled]) => !enabled)\n .map(([channel]) => channel as any),\n quietHours: profile.preferences.quietHours,\n },\n },\n scheduledAt: raw.scheduledAt,\n fallbackChain: fallbackChain?.length ? fallbackChain : undefined,\n campaignId: raw.campaignId,\n };\n\n const msgPriority = raw.priority ?? \"normal\";\n const p = getPriorityBucket(msgPriority);\n\n enrichedPayload.priority = msgPriority;\n\n batchedEvents[p].push(\n buildStreamEvent(\n \"notification.enriched\",\n enrichedPayload as Record<string, unknown>,\n \"enricher\",\n event.metadata.traceId,\n ),\n );\n }\n }\n }\n\n for (const p of [\"critical\", \"normal\", \"low\"] as const) {\n if (batchedEvents[p].length > 0) {\n const producer = this.producers[p] ?? this.producers.normal;\n for (const ev of batchedEvents[p]) {\n publishPromises.push(\n new Promise((resolve, reject) => {\n this.eventBuffer.push({ producer, event: ev, resolve, reject });\n }),\n );\n }\n }\n }\n }\n\n this.logger.info(\n {\n messageId: message.id,\n eventId: event.id,\n target: raw.target.type,\n traceId: event.metadata.traceId,\n },\n \"event enriched\",\n );\n } catch (err) {\n throw err;\n }\n\n await Promise.all(publishPromises).catch(async (err) => {\n await this.idempotency.unmark(dedupeId).catch(() => {});\n throw err;\n });\n\n await this.idempotency.markProcessed(dedupeId);\n }\n}\n\nexport async function startEnricherWorker() {\n logger = createLogger({ name: \"enricher\", level: config.LOG_LEVEL });\n redis = new RedisClient({ url: config.REDIS_URL, name: \"enricher\", logger });\n const dbData = createDatabase({ url: config.DATABASE_URL, applicationName: \"enricher\", logger });\n sql = dbData.sql;\n db = dbData.db;\n consumer = new StreamConsumer({\n redis: redis.native,\n stream: INBOUND_STREAMS as unknown as StreamName[],\n group: CONSUMER_GROUPS.ENRICHER,\n consumer: `enricher-${process.pid}`,\n dlqStream: STREAMS.DEAD_LETTER,\n batchSize: config.WORKER_CONCURRENCY,\n logger,\n });\n\n pendingScanner = new PendingMessageScanner({\n redis: redis.native,\n stream: INBOUND_STREAMS as unknown as StreamName[],\n group: CONSUMER_GROUPS.ENRICHER,\n consumer: `enricher-${process.pid}`,\n logger,\n });\n\n const producers = {\n critical: new StreamProducer({\n redis: redis.native,\n stream: STREAMS.ENRICHED_CRITICAL,\n logger,\n }),\n normal: new StreamProducer({ redis: redis.native, stream: STREAMS.ENRICHED_NORMAL, logger }),\n low: new StreamProducer({ redis: redis.native, stream: STREAMS.ENRICHED_LOW, logger }),\n };\n\n const idempotency = new IdempotencyGuard({\n redis: redis.native,\n keyPrefix: \"notif:processed:enricher\",\n ttlSeconds: 86_400,\n });\n\n const userRepo = new UserRepository(db);\n const prefRepo = new PreferenceRepository(db);\n const contactRepo = new ContactRepository(db);\n const templateCache = new TemplateCache(new TemplateRepository(db));\n\n // ─── Stage 1: Context Enricher ──────────────────────────────────────────────\n //\n // Pipeline:\n // 1. Parse payload as notification.created\n // 2. Idempotency check — drop if already processed\n // 3. Load user profile (language, timezone) from DB\n // 4. Load all stored preferences for this user\n // 5. Publish notification.enriched to ENRICHED stream\n\n // 5. Publish notification.enriched to ENRICHED stream\n\n worker = new EnricherWorker({\n consumer,\n pendingScanner,\n logger,\n maxRetriesBeforeDlq: 5,\n concurrency: config.WORKER_CONCURRENCY,\n producers,\n idempotency,\n userRepo,\n prefRepo,\n contactRepo,\n templateCache,\n });\n\n // ─── Health check interval ──────────────────────────────────────────────────\n\n healthInterval = startHealthReporter(\"enricher\", worker, redis, logger);\n\n logger.info({ env: config.NODE_ENV }, \"enricher starting\");\n await worker.start();\n}\n\n// ─── Shutdown ──────────────────────────────────────────────────────────────\n\nexport async function stopEnricherWorker(): Promise<void> {\n logger?.info(\"shutdown initiated\");\n if (healthInterval) {\n clearInterval(healthInterval);\n healthInterval = null;\n }\n if (worker) await worker.stop();\n if (sql) await sql.end();\n if (redis) await redis.disconnect();\n logger?.info(\"enricher stopped\");\n}\n"],"mappings":"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"}