notifkit 0.1.6 → 0.1.7

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 +1 @@
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
+ {"version":3,"file":"main-pFAfCkVX.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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"}
@@ -291,6 +291,7 @@ const ContactChannelSchema = z.enum([
291
291
  "sms",
292
292
  "push",
293
293
  "webhook",
294
+ "in-app",
294
295
  "telegram",
295
296
  "discord",
296
297
  "whatsapp",
@@ -298,14 +299,24 @@ const ContactChannelSchema = z.enum([
298
299
  ]);
299
300
  /** Accept a single value or an array; always normalise to a non-empty array. */
300
301
  const stringOrArray = z.union([z.string().min(1), z.array(z.string().min(1))]).transform((v) => Array.isArray(v) ? v : [v]);
302
+ const UserContactInputSchema = z.object({
303
+ channel: ContactChannelSchema,
304
+ target: z.string().min(1),
305
+ label: z.string().optional(),
306
+ isPrimary: z.boolean().optional(),
307
+ enabled: z.boolean().optional(),
308
+ preferences: PreferencesSchema.optional()
309
+ });
301
310
  /**
302
- * addUser({ id, email, phone, pushToken, segments, preferences })
303
- * email / phone / pushToken accept a single string or an array.
311
+ * addUser({ id, contacts, email, phone, pushToken, segments, preferences })
312
+ * email / phone / pushToken accept a single string or an array for backwards compatibility.
313
+ * Prefer `contacts: [{ channel, target }]`.
304
314
  */
305
315
  const AddUserSchema = z.object({
306
316
  id: z.string().min(1),
307
317
  language: z.string().optional(),
308
318
  timezone: z.string().optional(),
319
+ contacts: z.array(UserContactInputSchema).optional(),
309
320
  email: stringOrArray.optional(),
310
321
  phone: stringOrArray.optional(),
311
322
  pushToken: stringOrArray.optional(),
@@ -316,18 +327,23 @@ const AddUserSchema = z.object({
316
327
  const UpdateUserSchema = z.object({
317
328
  language: z.string().optional(),
318
329
  timezone: z.string().optional(),
330
+ contacts: z.array(UserContactInputSchema).optional(),
319
331
  email: stringOrArray.optional(),
320
332
  phone: stringOrArray.optional(),
321
333
  pushToken: stringOrArray.optional(),
322
334
  segments: z.array(z.string().min(1)).optional(),
323
335
  preferences: PreferencesSchema.optional()
324
336
  });
325
- /** addUserContact(userId, channel, { target, preferences }) channel carried in body. */
337
+ /** addUserContact(userId, channel, { target, preferences, label, isPrimary, enabled }) */
326
338
  const AddContactSchema = z.object({
327
339
  channel: ContactChannelSchema,
328
340
  target: z.string().min(1),
341
+ label: z.string().optional(),
342
+ isPrimary: z.boolean().optional(),
343
+ enabled: z.boolean().optional(),
329
344
  preferences: PreferencesSchema.optional()
330
345
  });
346
+ const BatchAddContactsSchema = z.union([AddContactSchema, z.array(AddContactSchema).min(1)]);
331
347
  const TemplateSchema = z.object({
332
348
  id: z.string().min(1),
333
349
  channel: NotificationChannelSchema,
@@ -341,6 +357,7 @@ const InlineUserSchema = z.object({
341
357
  id: z.string().min(1),
342
358
  language: z.string().optional(),
343
359
  timezone: z.string().optional(),
360
+ contacts: z.array(UserContactInputSchema).optional(),
344
361
  email: stringOrArray.optional(),
345
362
  phone: stringOrArray.optional(),
346
363
  pushToken: stringOrArray.optional(),
@@ -3064,29 +3081,41 @@ var NotifkitClient = class {
3064
3081
  }
3065
3082
  return data;
3066
3083
  }
3067
- /** Sync templates with the server. */
3084
+ /** Sync templates with the server. Accepts either { templates: [...] } or an array of templates directly. */
3068
3085
  async syncTemplates(input) {
3069
- return this.request("/v1/templates", "PUT", input);
3086
+ const payload = Array.isArray(input) ? { templates: input } : input;
3087
+ return this.request("/v1/templates", "PUT", payload);
3088
+ }
3089
+ async addUser(idOrInput, contacts, options) {
3090
+ if (typeof idOrInput === "string") return this.request("/v1/users", "POST", {
3091
+ id: idOrInput,
3092
+ contacts: contacts ?? [],
3093
+ ...options
3094
+ });
3095
+ return this.request("/v1/users", "POST", idOrInput);
3070
3096
  }
3071
- /** Create/upsert a user profile and contacts. */
3072
- async addUser(input) {
3073
- return this.request("/v1/users", "POST", input);
3097
+ async identify(idOrInput, contacts, options) {
3098
+ return this.addUser(idOrInput, contacts, options);
3074
3099
  }
3075
3100
  /** Update user profile. */
3076
3101
  async updateUser(id, input) {
3077
- return this.request(`/v1/users/${id}`, "PATCH", input);
3102
+ return this.request(`/v1/users/${encodeURIComponent(id)}`, "PATCH", input);
3078
3103
  }
3079
3104
  /** Delete user profile. */
3080
3105
  async deleteUser(id) {
3081
- return this.request(`/v1/users/${id}`, "DELETE");
3106
+ return this.request(`/v1/users/${encodeURIComponent(id)}`, "DELETE");
3082
3107
  }
3083
- /** Add contact targets to user profile. */
3108
+ /** Add contact target to user profile. */
3084
3109
  async addContact(userId, input) {
3085
- return this.request(`/v1/users/${userId}/contacts`, "POST", input);
3110
+ return this.request(`/v1/users/${encodeURIComponent(userId)}/contacts`, "POST", input);
3111
+ }
3112
+ /** Add multiple contact targets to user profile in a batch. */
3113
+ async addContacts(userId, contacts) {
3114
+ return this.request(`/v1/users/${encodeURIComponent(userId)}/contacts`, "POST", contacts);
3086
3115
  }
3087
3116
  /** Delete a specific contact channel target. */
3088
3117
  async deleteContact(userId, channel, target) {
3089
- return this.request(`/v1/users/${userId}/contacts/${channel}/${target}`, "DELETE");
3118
+ return this.request(`/v1/users/${encodeURIComponent(userId)}/contacts/${encodeURIComponent(channel)}/${encodeURIComponent(target)}`, "DELETE");
3090
3119
  }
3091
3120
  /** Request a notification dispatch. */
3092
3121
  async notify(input) {
@@ -3119,11 +3148,11 @@ var NotifkitClient = class {
3119
3148
  }
3120
3149
  /** Get a workflow instance by ID. */
3121
3150
  async getWorkflow(instanceId) {
3122
- return this.request(`/v1/workflows/instances/${instanceId}`, "GET");
3151
+ return this.request(`/v1/workflows/instances/${encodeURIComponent(instanceId)}`, "GET");
3123
3152
  }
3124
3153
  /** Cancel a running/suspended workflow instance. */
3125
3154
  async cancelWorkflow(instanceId) {
3126
- return this.request(`/v1/workflows/instances/${instanceId}`, "DELETE");
3155
+ return this.request(`/v1/workflows/instances/${encodeURIComponent(instanceId)}`, "DELETE");
3127
3156
  }
3128
3157
  /** Get notification logs for the project. */
3129
3158
  async getNotificationLogs(options) {
@@ -3159,11 +3188,11 @@ var NotifkitClient = class {
3159
3188
  }
3160
3189
  /** Delete a template. */
3161
3190
  async deleteTemplate(id) {
3162
- return this.request(`/v1/templates/${id}`, "DELETE");
3191
+ return this.request(`/v1/templates/${encodeURIComponent(id)}`, "DELETE");
3163
3192
  }
3164
3193
  /** Get a user's contacts. */
3165
3194
  async getUserContacts(userId) {
3166
- return this.request(`/v1/users/${userId}/contacts`, "GET");
3195
+ return this.request(`/v1/users/${encodeURIComponent(userId)}/contacts`, "GET");
3167
3196
  }
3168
3197
  /** List projects (Admin only). */
3169
3198
  async listProjects() {
@@ -3171,23 +3200,23 @@ var NotifkitClient = class {
3171
3200
  }
3172
3201
  /** Delete a project (Admin only). */
3173
3202
  async deleteProject(id) {
3174
- return this.request(`/v1/projects/${id}`, "DELETE");
3203
+ return this.request(`/v1/projects/${encodeURIComponent(id)}`, "DELETE");
3175
3204
  }
3176
3205
  /** Create a new project API key (Admin only). */
3177
3206
  async createProjectKey(id, input) {
3178
- return this.request(`/v1/projects/${id}/keys`, "POST", input || {});
3207
+ return this.request(`/v1/projects/${encodeURIComponent(id)}/keys`, "POST", input || {});
3179
3208
  }
3180
3209
  /** List project API keys (Admin only). */
3181
3210
  async listProjectKeys(id) {
3182
- return this.request(`/v1/projects/${id}/keys`, "GET");
3211
+ return this.request(`/v1/projects/${encodeURIComponent(id)}/keys`, "GET");
3183
3212
  }
3184
3213
  /** Delete a project API key (Admin only). */
3185
3214
  async deleteProjectKey(id, keyId) {
3186
- return this.request(`/v1/projects/${id}/keys/${keyId}`, "DELETE");
3215
+ return this.request(`/v1/projects/${encodeURIComponent(id)}/keys/${encodeURIComponent(keyId)}`, "DELETE");
3187
3216
  }
3188
3217
  /** Update project settings (Admin only). */
3189
3218
  async updateProject(id, input) {
3190
- return this.request(`/v1/projects/${id}`, "PATCH", input);
3219
+ return this.request(`/v1/projects/${encodeURIComponent(id)}`, "PATCH", input);
3191
3220
  }
3192
3221
  /** List unique segment tags. */
3193
3222
  async listSegments() {
@@ -3373,35 +3402,35 @@ var NotifkitServer = class extends EventEmitter {
3373
3402
  }
3374
3403
  const startupPromises = [];
3375
3404
  if (services.includes("api")) {
3376
- const { startApiServer } = await import("./main-Db2f2-sW.mjs");
3405
+ const { startApiServer } = await import("./main-CFSukWm8.mjs");
3377
3406
  startupPromises.push(startApiServer());
3378
3407
  }
3379
3408
  if (services.includes("delivery")) {
3380
- const { startDeliveryWorker } = await import("./main-DdLzptZE.mjs");
3409
+ const { startDeliveryWorker } = await import("./main-pFAfCkVX.mjs");
3381
3410
  startupPromises.push(startDeliveryWorker());
3382
3411
  }
3383
3412
  if (services.includes("engine")) {
3384
- const { startEngineWorker } = await import("./main-CWGEKpLU.mjs");
3413
+ const { startEngineWorker } = await import("./main-Yorxzw0Z.mjs");
3385
3414
  startupPromises.push(startEngineWorker());
3386
3415
  }
3387
3416
  if (services.includes("enricher")) {
3388
- const { startEnricherWorker } = await import("./main-DsDkyBZi.mjs");
3417
+ const { startEnricherWorker } = await import("./main-Bgi2wWck.mjs");
3389
3418
  startupPromises.push(startEnricherWorker());
3390
3419
  }
3391
3420
  if (services.includes("scheduler")) {
3392
- const { startSchedulerWorker } = await import("./main-Cwu4iQXc.mjs");
3421
+ const { startSchedulerWorker } = await import("./main-DFMHcN_d.mjs");
3393
3422
  startupPromises.push(startSchedulerWorker());
3394
3423
  }
3395
3424
  if (services.includes("ai")) {
3396
- const { startAiWorker } = await import("./main-DDbquL89.mjs");
3425
+ const { startAiWorker } = await import("./main-D6SG3Isk.mjs");
3397
3426
  startupPromises.push(startAiWorker());
3398
3427
  }
3399
3428
  if (services.includes("workflow")) {
3400
- const { startWorkflowWorker } = await import("./main-BnIX5nfp.mjs");
3429
+ const { startWorkflowWorker } = await import("./main-CjbVdUqa.mjs");
3401
3430
  startupPromises.push(startWorkflowWorker());
3402
3431
  }
3403
3432
  if (services.includes("events")) {
3404
- const { startEventWorker } = await import("./main-DfrrNKp9.mjs");
3433
+ const { startEventWorker } = await import("./main-CRPxM-O3.mjs");
3405
3434
  startupPromises.push(startEventWorker());
3406
3435
  }
3407
3436
  const handleSignal = async (signal) => {
@@ -3441,35 +3470,35 @@ var NotifkitServer = class extends EventEmitter {
3441
3470
  "events"
3442
3471
  ] : this.options.services;
3443
3472
  if (services.includes("api")) {
3444
- const { stopApiServer } = await import("./main-Db2f2-sW.mjs");
3473
+ const { stopApiServer } = await import("./main-CFSukWm8.mjs");
3445
3474
  await stopApiServer();
3446
3475
  }
3447
3476
  if (services.includes("delivery")) {
3448
- const { stopDeliveryWorker } = await import("./main-DdLzptZE.mjs");
3477
+ const { stopDeliveryWorker } = await import("./main-pFAfCkVX.mjs");
3449
3478
  await stopDeliveryWorker();
3450
3479
  }
3451
3480
  if (services.includes("engine")) {
3452
- const { stopEngineWorker } = await import("./main-CWGEKpLU.mjs");
3481
+ const { stopEngineWorker } = await import("./main-Yorxzw0Z.mjs");
3453
3482
  await stopEngineWorker();
3454
3483
  }
3455
3484
  if (services.includes("enricher")) {
3456
- const { stopEnricherWorker } = await import("./main-DsDkyBZi.mjs");
3485
+ const { stopEnricherWorker } = await import("./main-Bgi2wWck.mjs");
3457
3486
  await stopEnricherWorker();
3458
3487
  }
3459
3488
  if (services.includes("scheduler")) {
3460
- const { stopSchedulerWorker } = await import("./main-Cwu4iQXc.mjs");
3489
+ const { stopSchedulerWorker } = await import("./main-DFMHcN_d.mjs");
3461
3490
  await stopSchedulerWorker();
3462
3491
  }
3463
3492
  if (services.includes("ai")) {
3464
- const { stopAiWorker } = await import("./main-DDbquL89.mjs");
3493
+ const { stopAiWorker } = await import("./main-D6SG3Isk.mjs");
3465
3494
  await stopAiWorker();
3466
3495
  }
3467
3496
  if (services.includes("workflow")) {
3468
- const { stopWorkflowWorker } = await import("./main-BnIX5nfp.mjs");
3497
+ const { stopWorkflowWorker } = await import("./main-CjbVdUqa.mjs");
3469
3498
  await stopWorkflowWorker();
3470
3499
  }
3471
3500
  if (services.includes("events")) {
3472
- const { stopEventWorker } = await import("./main-DfrrNKp9.mjs");
3501
+ const { stopEventWorker } = await import("./main-CRPxM-O3.mjs");
3473
3502
  await stopEventWorker();
3474
3503
  }
3475
3504
  if (this.pgContainer) {
@@ -3483,6 +3512,6 @@ var NotifkitServer = class extends EventEmitter {
3483
3512
  }
3484
3513
  };
3485
3514
  //#endregion
3486
- export { childLogger as $, PUBSUB_CHANNELS as $t, ProjectSettingsCache as A, NotificationRequestedPayloadSchema as At, LUA_RELEASE_LOCK as B, QuietHoursSchema as Bt, ProjectRepository as C, DeliveryOptionsSchema as Ct, WorkflowRepository as D, NotificationEnrichedPayloadSchema as Dt, UserRepository as E, NotificationScheduledPayloadSchema as Et, sleep as F, CreateWorkflowSchema as Ft, normaliseTarget as G, UpdateUserSchema as Gt, LUA_SCHEDULER_CLAIM as H, TemplateSchema as Ht, DataLoader as I, IngestEventSchema as It, PendingMessageScanner as J, buildStreamEvent as Jt, LRUCache as K, WorkflowNotifyPayloadSchema as Kt, CircuitBreaker as L, InlineUserSchema as Lt, AppError as M, AddContactSchema as Mt, ValidationError as N, AddUserSchema as Nt, Redis as O, RecipientProfileSchema as Ot, generateId as P, ContactChannelSchema as Pt, metrics as Q, OUTBOUND_STREAMS as Qt, BatchProcessor as R, NotifyRequestSchema as Rt, PreferenceRepository as S, NotificationDeliveredPayloadSchema as St, TemplateRepository as T, RenderedContentSchema as Tt, LUA_SCHEDULER_POLL as U, TriggerWorkflowSchema as Ut, LUA_RENEW_LOCK as V, SyncTemplatesSchema as Vt, getPriorityBucket as W, UpdateProjectSchema as Wt, StreamProducer as X, ENRICHED_STREAMS as Xt, StreamConsumer as Y, CONSUMER_GROUPS as Yt, getMetricsRegistry as Z, INBOUND_STREAMS as Zt, escapeHeader as _, setGlobalConfig as _n, workflowWaiters as _t, SuspendExecutionError as a, registry as an, runMigrations as at, renderWithTemplate as b, NotificationSkippedPayloadSchema as bt, BaseWorker as c, NotificationPrioritySchema as cn, projectApiKeys as ct, buildUnsubscribeHeaders as d, baseConfigSchema as dn, suppressions as dt, STREAMS as en, createLogger as et, signUnsubscribeToken as f, getAiConfig as fn, userTopicPreferences as ft, TemplateCache as g, setAiConfig as gn, workflowSteps as gt, templateRegistry as h, readBaseConfig as hn, workflowInstances as ht, workflowRegistry as i, EventRegistry as in, createDatabase as it, UserThrottle as j, NotificationTargetSchema as jt, RedisClient as k, NotificationCreatedPayloadSchema as kt, NonRetryableError as l, NotificationStatusSchema as ln, projects as lt, renderTemplate as m, parseConfig as mn, workflowDefinitions as mt, NotifkitClient as n, StreamEventMetadataSchema as nn, withRequestId as nt, buildStepNotifyPayload as o, EventMetadataSchema as on, deliveryOutbox as ot, verifyUnsubscribeToken as p, loadEnv as pn, users as pt, globalEmitter as q, WorkflowStepSchema as qt, workflow as r, StreamEventSchema as rn, IdempotencyGuard as rt, resolveStepTarget as s, NotificationChannelSchema as sn, messageLogs as st, NotifkitServer as t, EventEnvelopeSchema as tn, withContext as tt, startHealthReporter as u, AI_DEFAULTS as un, scheduledPayloads as ut, escapeHtml as v, NotificationAiPendingPayloadSchema as vt, SegmentRepository as w, NotificationDispatchedPayloadSchema as wt, ContactRepository as x, NotificationFailedPayloadSchema as xt, interpolate as y, NotificationCanceledPayloadSchema as yt, AsyncSemaphore as z, PreferencesSchema as zt };
3515
+ export { childLogger as $, INBOUND_STREAMS as $t, ProjectSettingsCache as A, NotificationRequestedPayloadSchema as At, LUA_RELEASE_LOCK as B, PreferencesSchema as Bt, ProjectRepository as C, DeliveryOptionsSchema as Ct, WorkflowRepository as D, NotificationEnrichedPayloadSchema as Dt, UserRepository as E, NotificationScheduledPayloadSchema as Et, sleep as F, ContactChannelSchema as Ft, normaliseTarget as G, UpdateProjectSchema as Gt, LUA_SCHEDULER_CLAIM as H, SyncTemplatesSchema as Ht, DataLoader as I, CreateWorkflowSchema as It, PendingMessageScanner as J, WorkflowNotifyPayloadSchema as Jt, LRUCache as K, UpdateUserSchema as Kt, CircuitBreaker as L, IngestEventSchema as Lt, AppError as M, AddContactSchema as Mt, ValidationError as N, AddUserSchema as Nt, Redis as O, RecipientProfileSchema as Ot, generateId as P, BatchAddContactsSchema as Pt, metrics as Q, ENRICHED_STREAMS as Qt, BatchProcessor as R, InlineUserSchema as Rt, PreferenceRepository as S, NotificationDeliveredPayloadSchema as St, TemplateRepository as T, RenderedContentSchema as Tt, LUA_SCHEDULER_POLL as U, TemplateSchema as Ut, LUA_RENEW_LOCK as V, QuietHoursSchema as Vt, getPriorityBucket as W, TriggerWorkflowSchema as Wt, StreamProducer as X, buildStreamEvent as Xt, StreamConsumer as Y, WorkflowStepSchema as Yt, getMetricsRegistry as Z, CONSUMER_GROUPS as Zt, escapeHeader as _, readBaseConfig as _n, workflowWaiters as _t, SuspendExecutionError as a, StreamEventSchema as an, runMigrations as at, renderWithTemplate as b, NotificationSkippedPayloadSchema as bt, BaseWorker as c, EventMetadataSchema as cn, projectApiKeys as ct, buildUnsubscribeHeaders as d, NotificationStatusSchema as dn, suppressions as dt, OUTBOUND_STREAMS as en, createLogger as et, signUnsubscribeToken as f, AI_DEFAULTS as fn, userTopicPreferences as ft, TemplateCache as g, parseConfig as gn, workflowSteps as gt, templateRegistry as h, loadEnv as hn, workflowInstances as ht, workflowRegistry as i, StreamEventMetadataSchema as in, createDatabase as it, UserThrottle as j, NotificationTargetSchema as jt, RedisClient as k, NotificationCreatedPayloadSchema as kt, NonRetryableError as l, NotificationChannelSchema as ln, projects as lt, renderTemplate as m, getAiConfig as mn, workflowDefinitions as mt, NotifkitClient as n, STREAMS as nn, withRequestId as nt, buildStepNotifyPayload as o, EventRegistry as on, deliveryOutbox as ot, verifyUnsubscribeToken as p, baseConfigSchema as pn, users as pt, globalEmitter as q, UserContactInputSchema as qt, workflow as r, EventEnvelopeSchema as rn, IdempotencyGuard as rt, resolveStepTarget as s, registry as sn, messageLogs as st, NotifkitServer as t, PUBSUB_CHANNELS as tn, withContext as tt, startHealthReporter as u, NotificationPrioritySchema as un, scheduledPayloads as ut, escapeHtml as v, setAiConfig as vn, NotificationAiPendingPayloadSchema as vt, SegmentRepository as w, NotificationDispatchedPayloadSchema as wt, ContactRepository as x, NotificationFailedPayloadSchema as xt, interpolate as y, setGlobalConfig as yn, NotificationCanceledPayloadSchema as yt, AsyncSemaphore as z, NotifyRequestSchema as zt };
3487
3516
 
3488
- //# sourceMappingURL=src-CpUnBl_M.mjs.map
3517
+ //# sourceMappingURL=src-CPMwsUCJ.mjs.map