notifkit 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/dist/index.d.mts +9973 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +38 -0
- package/dist/index.mjs.map +1 -0
- package/dist/main-4H6vNXvy.mjs +392 -0
- package/dist/main-4H6vNXvy.mjs.map +1 -0
- package/dist/main-BHYZfBBq.mjs +224 -0
- package/dist/main-BHYZfBBq.mjs.map +1 -0
- package/dist/main-BIcKzWHE.mjs +430 -0
- package/dist/main-BIcKzWHE.mjs.map +1 -0
- package/dist/main-ClEeP5qw.mjs +629 -0
- package/dist/main-ClEeP5qw.mjs.map +1 -0
- package/dist/main-D-oWWzR3.mjs +234 -0
- package/dist/main-D-oWWzR3.mjs.map +1 -0
- package/dist/main-Dlfy9mWs.mjs +571 -0
- package/dist/main-Dlfy9mWs.mjs.map +1 -0
- package/dist/main-Dztc2dqR.mjs +294 -0
- package/dist/main-Dztc2dqR.mjs.map +1 -0
- package/dist/main-Ok9cQJ7q.mjs +1636 -0
- package/dist/main-Ok9cQJ7q.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/src-DrSN2wCg.mjs +3424 -0
- package/dist/src-DrSN2wCg.mjs.map +1 -0
- package/drizzle/0000_spotty_jack_flag.sql +189 -0
- package/drizzle/0001_stale_shotgun.sql +17 -0
- package/drizzle/meta/0000_snapshot.json +1314 -0
- package/drizzle/meta/0001_snapshot.json +1460 -0
- package/drizzle/meta/_journal.json +20 -0
- package/package.json +110 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main-ClEeP5qw.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 \"push\",\n task.destination,\n );\n this.logger.info({ token: task.destination }, \"deactivated invalid push token\");\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) 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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"}
|
|
@@ -0,0 +1,234 @@
|
|
|
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-DrSN2wCg.mjs";
|
|
2
|
+
import { and, eq, gt, isNull, or } from "drizzle-orm";
|
|
3
|
+
//#region src/services/events/main.ts
|
|
4
|
+
loadEnv();
|
|
5
|
+
const config = readBaseConfig();
|
|
6
|
+
let logger;
|
|
7
|
+
let redis;
|
|
8
|
+
let sql$1;
|
|
9
|
+
let db;
|
|
10
|
+
let consumer;
|
|
11
|
+
let pendingScanner;
|
|
12
|
+
let worker;
|
|
13
|
+
let healthInterval = null;
|
|
14
|
+
let workflowProducer;
|
|
15
|
+
var EventWorker = class extends BaseWorker {
|
|
16
|
+
dbConn;
|
|
17
|
+
workflowProducer;
|
|
18
|
+
messageLogBuffer = [];
|
|
19
|
+
eventBuffer = [];
|
|
20
|
+
flushInterval = null;
|
|
21
|
+
isFlushing = false;
|
|
22
|
+
constructor(options) {
|
|
23
|
+
super(options);
|
|
24
|
+
this.dbConn = options.db;
|
|
25
|
+
this.workflowProducer = options.workflowProducer;
|
|
26
|
+
this.startFlushInterval();
|
|
27
|
+
}
|
|
28
|
+
startFlushInterval() {
|
|
29
|
+
this.flushInterval = setInterval(() => {
|
|
30
|
+
this.flushLogs();
|
|
31
|
+
}, config.LOG_FLUSH_INTERVAL_MS);
|
|
32
|
+
}
|
|
33
|
+
async flushLogs() {
|
|
34
|
+
if (this.isFlushing || this.messageLogBuffer.length === 0 && this.eventBuffer.length === 0) return;
|
|
35
|
+
this.isFlushing = true;
|
|
36
|
+
const startTime = Date.now();
|
|
37
|
+
const batch = this.messageLogBuffer;
|
|
38
|
+
this.messageLogBuffer = [];
|
|
39
|
+
const events = this.eventBuffer;
|
|
40
|
+
this.eventBuffer = [];
|
|
41
|
+
try {
|
|
42
|
+
if (batch.length > 0) {
|
|
43
|
+
await this.dbConn.insert(messageLogs).values(batch.map((b) => b.log)).onConflictDoNothing();
|
|
44
|
+
for (const b of batch) b.resolve();
|
|
45
|
+
this.logger.info({
|
|
46
|
+
batchSize: batch.length,
|
|
47
|
+
latencyMs: Date.now() - startTime
|
|
48
|
+
}, "flushed delivery logs to database");
|
|
49
|
+
}
|
|
50
|
+
if (events.length > 0) {
|
|
51
|
+
const byProducer = /* @__PURE__ */ new Map();
|
|
52
|
+
for (const item of events) {
|
|
53
|
+
if (!byProducer.has(item.producer)) byProducer.set(item.producer, []);
|
|
54
|
+
byProducer.get(item.producer).push(item);
|
|
55
|
+
}
|
|
56
|
+
for (const [producer, evts] of byProducer) {
|
|
57
|
+
await producer.publishBatch(evts.map((e) => e.event));
|
|
58
|
+
for (const e of evts) e.resolve();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
this.logger.error({
|
|
63
|
+
batchSize: batch.length,
|
|
64
|
+
error: error.message
|
|
65
|
+
}, "failed to flush delivery logs — dropping batch");
|
|
66
|
+
for (const b of batch) b.reject(error);
|
|
67
|
+
for (const e of events) e.reject(error);
|
|
68
|
+
} finally {
|
|
69
|
+
this.isFlushing = false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async stop() {
|
|
73
|
+
if (this.flushInterval) clearInterval(this.flushInterval);
|
|
74
|
+
await super.stop();
|
|
75
|
+
await this.flushLogs();
|
|
76
|
+
}
|
|
77
|
+
async process(message) {
|
|
78
|
+
const { event } = message;
|
|
79
|
+
const publishPromises = [];
|
|
80
|
+
if (event.type === "notification.dispatched" || event.type === "notification.delivered" || event.type === "notification.failed") {
|
|
81
|
+
const payload = event.payload;
|
|
82
|
+
if (this.messageLogBuffer.length >= config.LOG_BUFFER_MAX_SIZE) {
|
|
83
|
+
this.logger.warn({
|
|
84
|
+
dropped: 1,
|
|
85
|
+
bufferSize: this.messageLogBuffer.length
|
|
86
|
+
}, "log buffer full — dropping oldest delivery log");
|
|
87
|
+
const dropped = this.messageLogBuffer.shift();
|
|
88
|
+
if (dropped) dropped.reject(/* @__PURE__ */ new Error("Dropped due to buffer overflow"));
|
|
89
|
+
}
|
|
90
|
+
const status = event.type === "notification.dispatched" ? "dispatched" : event.type === "notification.delivered" ? "delivered" : "failed";
|
|
91
|
+
const kind = event.type === "notification.dispatched" ? "dispatched" : "attempt";
|
|
92
|
+
publishPromises.push(new Promise((resolve, reject) => {
|
|
93
|
+
this.messageLogBuffer.push({
|
|
94
|
+
log: {
|
|
95
|
+
projectId: payload.projectId,
|
|
96
|
+
taskId: payload.taskId,
|
|
97
|
+
providerMessageId: payload.providerMessageId || null,
|
|
98
|
+
channel: payload.channel,
|
|
99
|
+
attempt: payload.attempt || 1,
|
|
100
|
+
kind,
|
|
101
|
+
status,
|
|
102
|
+
templateId: payload.templateId || null,
|
|
103
|
+
workflowInstanceId: payload.workflowInstanceId || null,
|
|
104
|
+
campaignId: payload.campaignId || null
|
|
105
|
+
},
|
|
106
|
+
resolve,
|
|
107
|
+
reject
|
|
108
|
+
});
|
|
109
|
+
}));
|
|
110
|
+
await Promise.all(publishPromises);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (event.type !== "event.received") return;
|
|
114
|
+
const payload = event.payload;
|
|
115
|
+
const eventName = payload.eventName;
|
|
116
|
+
const eventData = payload.payload;
|
|
117
|
+
const projectId = payload.projectId;
|
|
118
|
+
if (!eventName || !projectId) {
|
|
119
|
+
this.logger.warn("Missing eventName or projectId in payload");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const now = /* @__PURE__ */ new Date();
|
|
123
|
+
const waiters = await this.dbConn.select().from(workflowWaiters).where(and(eq(workflowWaiters.eventName, eventName), eq(workflowWaiters.projectId, projectId), or(isNull(workflowWaiters.expiresAt), gt(workflowWaiters.expiresAt, now))));
|
|
124
|
+
const data = eventData && typeof eventData === "object" ? eventData : {};
|
|
125
|
+
for (const waiter of waiters) {
|
|
126
|
+
let matched = true;
|
|
127
|
+
const criteria = waiter.matchCriteria;
|
|
128
|
+
if (criteria && typeof criteria === "object") for (const [k, v] of Object.entries(criteria)) {
|
|
129
|
+
let dataValue = data[k];
|
|
130
|
+
if (k.includes(".")) {
|
|
131
|
+
const parts = k.split(".");
|
|
132
|
+
dataValue = data;
|
|
133
|
+
for (const p of parts) if (dataValue) dataValue = dataValue[p];
|
|
134
|
+
}
|
|
135
|
+
if (dataValue !== v) {
|
|
136
|
+
matched = false;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (matched) {
|
|
141
|
+
this.logger.info({
|
|
142
|
+
instanceId: waiter.instanceId,
|
|
143
|
+
eventName
|
|
144
|
+
}, "Event matched a sleeping workflow");
|
|
145
|
+
const instance = (await this.dbConn.select().from(workflowInstances).where(eq(workflowInstances.id, waiter.instanceId)).limit(1))[0];
|
|
146
|
+
if (!instance) {
|
|
147
|
+
this.logger.warn({ instanceId: waiter.instanceId }, "waiter references a missing workflow instance — dropping waiter");
|
|
148
|
+
await this.dbConn.delete(workflowWaiters).where(eq(workflowWaiters.id, waiter.id));
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const pendingStep = (await this.dbConn.select().from(workflowSteps).where(and(eq(workflowSteps.instanceId, waiter.instanceId), eq(workflowSteps.action, "waitForEvent")))).find((s) => s.output === null);
|
|
152
|
+
if (pendingStep) await this.dbConn.update(workflowSteps).set({ output: data }).where(eq(workflowSteps.id, pendingStep.id));
|
|
153
|
+
publishPromises.push(new Promise((resolve, reject) => {
|
|
154
|
+
this.eventBuffer.push({
|
|
155
|
+
producer: this.workflowProducer,
|
|
156
|
+
event: buildStreamEvent("workflow.resumed", {
|
|
157
|
+
projectId: instance.projectId,
|
|
158
|
+
instanceId: waiter.instanceId,
|
|
159
|
+
name: instance.name,
|
|
160
|
+
input: instance.input ?? {},
|
|
161
|
+
reason: "event_matched"
|
|
162
|
+
}, "events", event.metadata.traceId),
|
|
163
|
+
resolve,
|
|
164
|
+
reject
|
|
165
|
+
});
|
|
166
|
+
}));
|
|
167
|
+
await this.dbConn.delete(workflowWaiters).where(eq(workflowWaiters.id, waiter.id));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
await Promise.all(publishPromises);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
async function startEventWorker() {
|
|
174
|
+
logger = createLogger({
|
|
175
|
+
name: "event-worker",
|
|
176
|
+
level: config.LOG_LEVEL
|
|
177
|
+
});
|
|
178
|
+
redis = new RedisClient({
|
|
179
|
+
url: config.REDIS_URL,
|
|
180
|
+
name: "events",
|
|
181
|
+
logger
|
|
182
|
+
});
|
|
183
|
+
const dbData = createDatabase({
|
|
184
|
+
url: config.DATABASE_URL,
|
|
185
|
+
applicationName: "events",
|
|
186
|
+
logger
|
|
187
|
+
});
|
|
188
|
+
sql$1 = dbData.sql;
|
|
189
|
+
db = dbData.db;
|
|
190
|
+
workflowProducer = new StreamProducer({
|
|
191
|
+
redis: redis.native,
|
|
192
|
+
stream: STREAMS.WORKFLOW_INBOUND,
|
|
193
|
+
logger
|
|
194
|
+
});
|
|
195
|
+
consumer = new StreamConsumer({
|
|
196
|
+
redis: redis.native,
|
|
197
|
+
stream: STREAMS.EVENTS_INBOUND,
|
|
198
|
+
group: CONSUMER_GROUPS.EVENTS,
|
|
199
|
+
consumer: `events-${process.pid}`,
|
|
200
|
+
dlqStream: STREAMS.DEAD_LETTER,
|
|
201
|
+
batchSize: config.WORKER_CONCURRENCY,
|
|
202
|
+
logger
|
|
203
|
+
});
|
|
204
|
+
pendingScanner = new PendingMessageScanner({
|
|
205
|
+
redis: redis.native,
|
|
206
|
+
stream: STREAMS.EVENTS_INBOUND,
|
|
207
|
+
group: CONSUMER_GROUPS.EVENTS,
|
|
208
|
+
consumer: `events-${process.pid}`,
|
|
209
|
+
logger
|
|
210
|
+
});
|
|
211
|
+
worker = new EventWorker({
|
|
212
|
+
consumer,
|
|
213
|
+
pendingScanner,
|
|
214
|
+
logger,
|
|
215
|
+
concurrency: config.WORKER_CONCURRENCY,
|
|
216
|
+
db,
|
|
217
|
+
workflowProducer
|
|
218
|
+
});
|
|
219
|
+
healthInterval = startHealthReporter("events", worker, redis, logger);
|
|
220
|
+
logger.info("event worker starting");
|
|
221
|
+
await worker.start();
|
|
222
|
+
}
|
|
223
|
+
async function stopEventWorker() {
|
|
224
|
+
logger?.info("shutdown initiated");
|
|
225
|
+
if (healthInterval) clearInterval(healthInterval);
|
|
226
|
+
if (worker) await worker.stop();
|
|
227
|
+
if (sql$1) await sql$1.end();
|
|
228
|
+
if (redis) await redis.disconnect();
|
|
229
|
+
logger?.info("event worker stopped");
|
|
230
|
+
}
|
|
231
|
+
//#endregion
|
|
232
|
+
export { startEventWorker, stopEventWorker };
|
|
233
|
+
|
|
234
|
+
//# sourceMappingURL=main-D-oWWzR3.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main-D-oWWzR3.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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"}
|