notifkit 0.1.9 → 0.1.10
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/README.md +66 -72
- package/dist/index.d.mts +10 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{main-JdOvJ70b.mjs → main-39DtKrX4.mjs} +2 -2
- package/dist/{main-JdOvJ70b.mjs.map → main-39DtKrX4.mjs.map} +1 -1
- package/dist/{main-AO0JaY5z.mjs → main-B3iQrl3t.mjs} +2 -2
- package/dist/{main-AO0JaY5z.mjs.map → main-B3iQrl3t.mjs.map} +1 -1
- package/dist/{main-CyeQ_yDt.mjs → main-BM8DH3v3.mjs} +2 -2
- package/dist/{main-CyeQ_yDt.mjs.map → main-BM8DH3v3.mjs.map} +1 -1
- package/dist/{main-D19Wxs0x.mjs → main-BgmT1MPe.mjs} +2 -2
- package/dist/{main-D19Wxs0x.mjs.map → main-BgmT1MPe.mjs.map} +1 -1
- package/dist/{main-BPbPnBIG.mjs → main-BiFiOX9D.mjs} +2 -2
- package/dist/{main-BPbPnBIG.mjs.map → main-BiFiOX9D.mjs.map} +1 -1
- package/dist/{main-DSmh1e-V.mjs → main-C9s-2ArH.mjs} +2 -2
- package/dist/{main-DSmh1e-V.mjs.map → main-C9s-2ArH.mjs.map} +1 -1
- package/dist/{main-BMYSYcv2.mjs → main-Cqo3rSb5.mjs} +2 -2
- package/dist/{main-BMYSYcv2.mjs.map → main-Cqo3rSb5.mjs.map} +1 -1
- package/dist/{main-4Wyge9ms.mjs → main-Dz2sUh3c.mjs} +2 -2
- package/dist/{main-4Wyge9ms.mjs.map → main-Dz2sUh3c.mjs.map} +1 -1
- package/dist/{src-CGmXRfsM.mjs → src-QiTu5e_a.mjs} +20 -17
- package/dist/{src-CGmXRfsM.mjs.map → src-QiTu5e_a.mjs.map} +1 -1
- package/package.json +1 -1
- package/scripts/create-admin.mjs +8 -11
- package/src/server.ts +10 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as RedisClient, C as PreferenceRepository, D as UserRepository, E as TemplateRepository, G as getPriorityBucket, Qt as CONSUMER_GROUPS, S as ContactRepository, X as StreamConsumer, Y as PendingMessageScanner, Z as StreamProducer, Zt as buildStreamEvent, at as createDatabase, c as BaseWorker, cn as registry, en as INBOUND_STREAMS, g as TemplateCache, gn as loadEnv, it as IdempotencyGuard, rn as STREAMS, tt as createLogger, u as startHealthReporter, vn as readBaseConfig } from "./src-
|
|
1
|
+
import { A as RedisClient, C as PreferenceRepository, D as UserRepository, E as TemplateRepository, G as getPriorityBucket, Qt as CONSUMER_GROUPS, S as ContactRepository, X as StreamConsumer, Y as PendingMessageScanner, Z as StreamProducer, Zt as buildStreamEvent, at as createDatabase, c as BaseWorker, cn as registry, en as INBOUND_STREAMS, g as TemplateCache, gn as loadEnv, it as IdempotencyGuard, rn as STREAMS, tt as createLogger, u as startHealthReporter, vn as readBaseConfig } from "./src-QiTu5e_a.mjs";
|
|
2
2
|
//#region src/services/enricher/main.ts
|
|
3
3
|
loadEnv();
|
|
4
4
|
const config = readBaseConfig();
|
|
@@ -430,4 +430,4 @@ async function stopEnricherWorker() {
|
|
|
430
430
|
//#endregion
|
|
431
431
|
export { startEnricherWorker, stopEnricherWorker };
|
|
432
432
|
|
|
433
|
-
//# sourceMappingURL=main-
|
|
433
|
+
//# sourceMappingURL=main-B3iQrl3t.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main-AO0JaY5z.mjs","names":[],"sources":["../src/services/enricher/main.ts"],"sourcesContent":["import { loadEnv, readBaseConfig } from \"@/index.js\";\nimport { createLogger } from \"@/index.js\";\nimport { RedisClient } from \"@/index.js\";\nimport {\n StreamConsumer,\n PendingMessageScanner,\n StreamProducer,\n type StreamMessage,\n} from \"@/index.js\";\nimport { BaseWorker } from \"@/index.js\";\nimport {\n STREAMS,\n INBOUND_STREAMS,\n CONSUMER_GROUPS,\n registry,\n buildStreamEvent,\n type NotificationCreatedPayload,\n type NotificationEnrichedPayload,\n} from \"@/index.js\";\nimport { type StreamName } from \"@/contracts/streams.js\";\nimport { IdempotencyGuard } from \"@/index.js\";\nimport { createDatabase } from \"@/db/index.js\";\nimport {\n UserRepository,\n PreferenceRepository,\n TemplateRepository,\n ContactRepository,\n} from \"@/index.js\";\nimport { TemplateCache } from \"@/templates/index.js\";\nimport { getPriorityBucket, type WorkerOptions } from \"@/shared/index.js\";\nimport { startHealthReporter } from \"@/workers/index.js\";\n\n// ─── Bootstrap ─────────────────────────────────────────────────────────────\n\nloadEnv();\nconst config = readBaseConfig();\n\nlet logger: ReturnType<typeof createLogger>;\nlet redis: RedisClient;\nlet sql: any;\nlet db: any;\n\nlet consumer: StreamConsumer;\nlet pendingScanner: PendingMessageScanner;\nlet worker: BaseWorker;\nlet healthInterval: NodeJS.Timeout | null = null;\n\nexport interface EnricherWorkerOptions extends WorkerOptions {\n producers: any;\n idempotency: any;\n userRepo: any;\n prefRepo: any;\n contactRepo: any;\n templateCache: TemplateCache;\n}\n\nexport class EnricherWorker extends BaseWorker {\n private readonly producers: any;\n private readonly idempotency: any;\n private readonly userRepo: any;\n private readonly prefRepo: any;\n private readonly contactRepo: any;\n private readonly templateCache: TemplateCache;\n\n private userBatch: {\n projectId: string;\n userId: string;\n resolve: (p: any) => void;\n reject: (e: any) => void;\n }[] = [];\n private batchTimer: NodeJS.Timeout | null = null;\n private eventBuffer: {\n producer: any;\n event: any;\n resolve: () => void;\n reject: (e: any) => void;\n }[] = [];\n private flushTimer: NodeJS.Timeout | null = null;\n\n private contactBatch: {\n projectId: string;\n userIds: string[];\n resolve: (c: Map<string, any[]>) => void;\n reject: (e: any) => void;\n }[] = [];\n private contactBatchTimer: NodeJS.Timeout | null = null;\n\n private async loadContacts(projectId: string, userIds: string[]): Promise<Map<string, any[]>> {\n return new Promise((resolve, reject) => {\n this.contactBatch.push({ projectId, userIds, resolve, reject });\n if (this.contactBatch.length >= 500) {\n if (this.contactBatchTimer) clearTimeout(this.contactBatchTimer);\n void this.flushContactBatch();\n } else if (!this.contactBatchTimer) {\n this.contactBatchTimer = setTimeout(() => void this.flushContactBatch(), 10);\n }\n });\n }\n\n private async flushContactBatch(): Promise<void> {\n const batch = this.contactBatch;\n this.contactBatch = [];\n this.contactBatchTimer = null;\n if (batch.length === 0) return;\n\n try {\n const byProject = new Map<string, typeof batch>();\n for (const b of batch) {\n if (!byProject.has(b.projectId)) byProject.set(b.projectId, []);\n byProject.get(b.projectId)!.push(b);\n }\n\n for (const [projectId, items] of byProject) {\n const userIds = Array.from(new Set(items.flatMap((i) => i.userIds)));\n const contactsMap = await this.contactRepo.findActiveByUserIds(projectId, userIds);\n for (const item of items) {\n item.resolve(contactsMap);\n }\n }\n } catch (err) {\n for (const b of batch) b.reject(err);\n }\n }\n\n constructor(options: EnricherWorkerOptions) {\n super(options);\n this.producers = options.producers;\n this.idempotency = options.idempotency;\n this.userRepo = options.userRepo;\n this.prefRepo = options.prefRepo;\n this.contactRepo = options.contactRepo;\n this.templateCache = options.templateCache;\n\n this.flushTimer = setInterval(() => void this.flushWorkerBuffers(), 100);\n }\n\n override async stop(): Promise<void> {\n if (this.batchTimer) {\n clearTimeout(this.batchTimer);\n this.batchTimer = null;\n }\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n if (this.contactBatchTimer) {\n clearTimeout(this.contactBatchTimer);\n this.contactBatchTimer = null;\n }\n await this.flushContactBatch();\n await this.flushUserBatch();\n await this.flushWorkerBuffers();\n await super.stop();\n }\n\n private async flushWorkerBuffers(): Promise<void> {\n if (this.eventBuffer.length === 0) return;\n const events = this.eventBuffer;\n this.eventBuffer = [];\n\n try {\n const byProducer = new Map<any, typeof events>();\n for (const e of events) {\n if (!byProducer.has(e.producer)) byProducer.set(e.producer, []);\n byProducer.get(e.producer)!.push(e);\n }\n for (const [producer, batch] of byProducer) {\n await producer.publishBatch(batch.map((b) => b.event));\n for (const b of batch) b.resolve();\n }\n } catch (err: any) {\n this.logger.error({ err }, \"failed to flush events in EnricherWorker\");\n for (const e of events) e.reject(err);\n }\n }\n\n private async loadUser(projectId: string, userId: string): Promise<any> {\n return new Promise((resolve, reject) => {\n this.userBatch.push({ projectId, userId, resolve, reject });\n if (this.userBatch.length >= 500) {\n if (this.batchTimer) clearTimeout(this.batchTimer);\n void this.flushUserBatch();\n } else if (!this.batchTimer) {\n this.batchTimer = setTimeout(() => {\n void this.flushUserBatch();\n }, 10);\n }\n });\n }\n\n private async flushUserBatch() {\n const batch = this.userBatch;\n this.userBatch = [];\n this.batchTimer = null;\n\n const byProject = new Map<string, typeof batch>();\n for (const b of batch) {\n if (!byProject.has(b.projectId)) byProject.set(b.projectId, []);\n byProject.get(b.projectId)!.push(b);\n }\n\n for (const [projectId, reqs] of byProject.entries()) {\n try {\n const uniqueIds = Array.from(new Set(reqs.map((r) => r.userId)));\n const profiles = await this.userRepo.findRecordsByIds(projectId, uniqueIds);\n const profileMap = new Map(profiles.map((p: any) => [p.userId, p]));\n\n for (const req of reqs) {\n req.resolve(profileMap.get(req.userId) || null);\n }\n } catch (err) {\n for (const req of reqs) req.reject(err);\n }\n }\n }\n\n async process(message: StreamMessage): Promise<void> {\n const { event } = message;\n const publishPromises: Promise<void>[] = [];\n\n let isRequested = true;\n const requestedResult = registry.safeParsePayload(\"notification.requested\", event.payload);\n let createdResult: any = null;\n if (!requestedResult.success) {\n createdResult = registry.safeParsePayload(\"notification.created\", event.payload);\n isRequested = false;\n }\n\n if (!isRequested && (!createdResult || !createdResult.success)) {\n const issues = createdResult\n ? createdResult.error.issues\n : (requestedResult as any).error.issues;\n this.logger.warn({ messageId: message.id, issues }, \"invalid payload — skipping\");\n return;\n }\n\n // Handle legacy notification.created\n if (!isRequested) {\n const raw = createdResult.data as NotificationCreatedPayload;\n const dedupeId = `${raw.projectId}:${raw.idempotencyKey ?? event.id}`;\n if (!(await this.idempotency.checkAndMark(dedupeId, 60))) return;\n try {\n const profile = await this.userRepo.findRecordById(raw.projectId, raw.recipientId);\n if (!profile) return;\n\n const prefs = await this.prefRepo.findByUserId(raw.projectId, raw.recipientId);\n const optedOutTypes = new Set(\n prefs.filter((p: any) => !p.optedIn).map((p: any) => p.eventType),\n );\n\n const enrichedPayload: NotificationEnrichedPayload = {\n projectId: raw.projectId,\n rawEventId: event.id,\n recipientId: raw.recipientId,\n channel: raw.channel,\n priority: raw.priority,\n templateId: raw.templateId,\n templateVariables: raw.payload,\n recipient: {\n id: profile.userId,\n email: profile.email ?? undefined,\n locale: profile.language ?? \"en\",\n timezone: profile.timezone ?? \"UTC\",\n preferences: {\n optedOut:\n optedOutTypes.has(event.type) || profile.preferences.topics?.[event.type] === false,\n channels: Object.entries(profile.preferences.channels ?? {})\n .filter(([_, enabled]) => !enabled)\n .map(([channel]) => channel as any),\n quietHours: profile.preferences.quietHours,\n },\n },\n // No campaignId: this is the legacy `notification.created` path,\n // which predates campaigns and carries no label to attribute to.\n scheduledAt: raw.scheduledAt,\n };\n\n const p = getPriorityBucket(raw.priority);\n const producer = this.producers[p] ?? this.producers[\"normal\"]!;\n\n publishPromises.push(\n new Promise((resolve, reject) => {\n this.eventBuffer.push({\n producer,\n event: buildStreamEvent(\n \"notification.enriched\",\n enrichedPayload as Record<string, unknown>,\n \"enricher\",\n event.metadata.traceId,\n ),\n resolve,\n reject,\n });\n }),\n );\n this.logger.info(\n { messageId: message.id, eventId: event.id, recipientId: raw.recipientId },\n \"event enriched\",\n );\n } catch (err) {\n throw err;\n }\n await Promise.all(publishPromises).catch(async (err) => {\n await this.idempotency.unmark(dedupeId).catch(() => {});\n throw err;\n });\n await this.idempotency.markProcessed(dedupeId);\n return;\n }\n\n // Handle new notification.requested\n const raw = (requestedResult as any).data;\n const dedupeId = `${raw.projectId}:${raw.idempotencyKey ?? event.id}`;\n if (!(await this.idempotency.checkAndMark(dedupeId, 60))) return;\n try {\n let userIds: string[] = [];\n if (raw.target.type === \"user\") {\n userIds = [raw.target.userId];\n } else if (raw.target.type === \"segment\") {\n userIds = await this.userRepo.findUsersBySegment(raw.projectId, raw.target.segment);\n this.logger.info(\n { segment: raw.target.segment, count: userIds.length },\n \"Resolved segment\",\n );\n } else if (raw.target.type === \"topic\") {\n userIds = await this.userRepo.findUsersByTopic(raw.projectId, raw.target.topic);\n this.logger.info({ topic: raw.target.topic, count: userIds.length }, \"Resolved topic\");\n } else {\n this.logger.warn({ target: raw.target }, \"Segment/topic resolution not fully implemented\");\n // Stub: maybe resolve later\n }\n\n const maxUsers = readBaseConfig().SEGMENT_MAX_USERS;\n if (userIds.length > maxUsers) {\n this.logger.error(\n { count: userIds.length, max: maxUsers, projectId: raw.projectId, eventId: event.id },\n \"Segment fan-out exceeds maximum allowed limit\",\n );\n const p = getPriorityBucket(raw.priority ?? \"normal\");\n const producer = this.producers[p] ?? this.producers.normal;\n await producer.publish(\n buildStreamEvent(\n \"notification.failed\",\n {\n projectId: raw.projectId,\n rawEventId: event.id,\n error: `Segment fan-out of ${userIds.length} exceeds limit of ${maxUsers}`,\n },\n \"enricher\",\n event.metadata.traceId,\n ),\n );\n return;\n }\n\n // Topic opt-outs are keyed on the TEMPLATE's topics, not the envelope type.\n // `event.type` here is \"notification.requested\", which no user ever sets a\n // preference against, so keying on it silently disabled every opt-out.\n const template = raw.templateId\n ? await this.templateCache.getCachedTemplate(raw.projectId, raw.templateId)\n : null;\n const topics: string[] = template?.topics ?? [];\n\n const channels =\n raw.channels && raw.channels.length > 0 ? raw.channels : ([\"email\"] as any[]);\n const isFallback = (raw as any).fallback === true;\n\n // If fallback is true, we only emit the first channel, and pass the rest in fallbackChain.\n // If fallback is false, we emit all channels concurrently.\n const channelsToProcess = isFallback ? [channels[0]] : channels;\n const fallbackChain = isFallback ? channels.slice(1) : undefined;\n\n const chunkArray = <T>(arr: T[], size: number) =>\n Array.from({ length: Math.ceil(arr.length / size) }, (v, i) =>\n arr.slice(i * size, i * size + size),\n );\n\n const chunks = chunkArray(userIds, 500);\n\n for (const chunk of chunks) {\n const profiles = (\n await Promise.all(chunk.map((id) => this.loadUser(raw.projectId, id)))\n ).filter(Boolean);\n const contactsByUser = await this.loadContacts(\n raw.projectId,\n profiles.map((profile: any) => profile.userId),\n );\n\n const batchedEvents: Record<\n \"critical\" | \"high\" | \"normal\" | \"low\",\n Omit<any, \"id\" | \"timestamp\">[]\n > = {\n critical: [],\n high: [],\n normal: [],\n low: [],\n };\n\n for (const profile of profiles) {\n for (const channel of channelsToProcess) {\n const contacts = contactsByUser.get(profile.userId) ?? [];\n const channelContacts = contacts.filter((contact: any) => contact.channel === channel);\n // Push resolves its active tokens at send time so token invalidation\n // remains current. Other channels need one task per address.\n const destinations =\n channel === \"push\" ? [undefined] : channelContacts.map((c: any) => c.target);\n if (destinations.length === 0) {\n this.logger.info(\n { recipientId: profile.userId, channel },\n \"no active contact for channel\",\n );\n continue;\n }\n for (const destination of destinations) {\n const enrichedPayload: NotificationEnrichedPayload = {\n projectId: raw.projectId,\n rawEventId: event.id,\n recipientId: profile.userId,\n channel: channel,\n priority: \"normal\",\n templateId: raw.templateId,\n templateVariables: raw.data,\n aiPrompts: raw.aiPrompts,\n recipient: {\n id: profile.userId,\n email:\n channel === \"email\"\n ? (destination ?? profile.email ?? undefined)\n : (profile.email ?? undefined),\n phone: channel === \"sms\" || channel === \"whatsapp\" ? destination : undefined,\n webhook: channel === \"webhook\" ? destination : undefined,\n telegram: channel === \"telegram\" ? destination : undefined,\n discord: channel === \"discord\" ? destination : undefined,\n slack: channel === \"slack\" ? destination : undefined,\n locale: profile.language ?? \"en\",\n timezone: profile.timezone ?? \"UTC\",\n preferences: {\n // Opted out if the user disabled ANY topic this template carries.\n optedOut: topics.some((t) => profile.preferences.topics?.[t] === false),\n channels: Object.entries(profile.preferences.channels ?? {})\n .filter(([_, enabled]) => !enabled)\n .map(([channel]) => channel as any),\n quietHours: profile.preferences.quietHours,\n },\n },\n scheduledAt: raw.scheduledAt,\n fallbackChain: fallbackChain?.length ? fallbackChain : undefined,\n campaignId: raw.campaignId,\n };\n\n const msgPriority = raw.priority ?? \"normal\";\n const p = getPriorityBucket(msgPriority);\n\n enrichedPayload.priority = msgPriority;\n\n batchedEvents[p].push(\n buildStreamEvent(\n \"notification.enriched\",\n enrichedPayload as Record<string, unknown>,\n \"enricher\",\n event.metadata.traceId,\n ),\n );\n }\n }\n }\n\n for (const p of [\"critical\", \"normal\", \"low\"] as const) {\n if (batchedEvents[p].length > 0) {\n const producer = this.producers[p] ?? this.producers.normal;\n for (const ev of batchedEvents[p]) {\n publishPromises.push(\n new Promise((resolve, reject) => {\n this.eventBuffer.push({ producer, event: ev, resolve, reject });\n }),\n );\n }\n }\n }\n }\n\n this.logger.info(\n {\n messageId: message.id,\n eventId: event.id,\n target: raw.target.type,\n traceId: event.metadata.traceId,\n },\n \"event enriched\",\n );\n } catch (err) {\n throw err;\n }\n\n await Promise.all(publishPromises).catch(async (err) => {\n await this.idempotency.unmark(dedupeId).catch(() => {});\n throw err;\n });\n\n await this.idempotency.markProcessed(dedupeId);\n }\n}\n\nexport async function startEnricherWorker() {\n logger = createLogger({ name: \"enricher\", level: config.LOG_LEVEL });\n redis = new RedisClient({ url: config.REDIS_URL, name: \"enricher\", logger });\n const dbData = createDatabase({ url: config.DATABASE_URL, applicationName: \"enricher\", logger });\n sql = dbData.sql;\n db = dbData.db;\n consumer = new StreamConsumer({\n redis: redis.native,\n stream: INBOUND_STREAMS as unknown as StreamName[],\n group: CONSUMER_GROUPS.ENRICHER,\n consumer: `enricher-${process.pid}`,\n dlqStream: STREAMS.DEAD_LETTER,\n batchSize: config.WORKER_CONCURRENCY,\n logger,\n });\n\n pendingScanner = new PendingMessageScanner({\n redis: redis.native,\n stream: INBOUND_STREAMS as unknown as StreamName[],\n group: CONSUMER_GROUPS.ENRICHER,\n consumer: `enricher-${process.pid}`,\n logger,\n });\n\n const producers = {\n critical: new StreamProducer({\n redis: redis.native,\n stream: STREAMS.ENRICHED_CRITICAL,\n logger,\n }),\n normal: new StreamProducer({ redis: redis.native, stream: STREAMS.ENRICHED_NORMAL, logger }),\n low: new StreamProducer({ redis: redis.native, stream: STREAMS.ENRICHED_LOW, logger }),\n };\n\n const idempotency = new IdempotencyGuard({\n redis: redis.native,\n keyPrefix: \"notif:processed:enricher\",\n ttlSeconds: 86_400,\n });\n\n const userRepo = new UserRepository(db);\n const prefRepo = new PreferenceRepository(db);\n const contactRepo = new ContactRepository(db);\n const templateCache = new TemplateCache(new TemplateRepository(db));\n\n // ─── Stage 1: Context Enricher ──────────────────────────────────────────────\n //\n // Pipeline:\n // 1. Parse payload as notification.created\n // 2. Idempotency check — drop if already processed\n // 3. Load user profile (language, timezone) from DB\n // 4. Load all stored preferences for this user\n // 5. Publish notification.enriched to ENRICHED stream\n\n // 5. Publish notification.enriched to ENRICHED stream\n\n worker = new EnricherWorker({\n consumer,\n pendingScanner,\n logger,\n maxRetriesBeforeDlq: 5,\n concurrency: config.WORKER_CONCURRENCY,\n producers,\n idempotency,\n userRepo,\n prefRepo,\n contactRepo,\n templateCache,\n });\n\n // ─── Health check interval ──────────────────────────────────────────────────\n\n healthInterval = startHealthReporter(\"enricher\", worker, redis, logger);\n\n logger.info({ env: config.NODE_ENV }, \"enricher starting\");\n await worker.start();\n}\n\n// ─── Shutdown ──────────────────────────────────────────────────────────────\n\nexport async function stopEnricherWorker(): Promise<void> {\n logger?.info(\"shutdown initiated\");\n if (healthInterval) {\n clearInterval(healthInterval);\n healthInterval = null;\n }\n if (worker) await worker.stop();\n if (sql) await sql.end();\n if (redis) await redis.disconnect();\n logger?.info(\"enricher stopped\");\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"main-B3iQrl3t.mjs","names":[],"sources":["../src/services/enricher/main.ts"],"sourcesContent":["import { loadEnv, readBaseConfig } from \"@/index.js\";\nimport { createLogger } from \"@/index.js\";\nimport { RedisClient } from \"@/index.js\";\nimport {\n StreamConsumer,\n PendingMessageScanner,\n StreamProducer,\n type StreamMessage,\n} from \"@/index.js\";\nimport { BaseWorker } from \"@/index.js\";\nimport {\n STREAMS,\n INBOUND_STREAMS,\n CONSUMER_GROUPS,\n registry,\n buildStreamEvent,\n type NotificationCreatedPayload,\n type NotificationEnrichedPayload,\n} from \"@/index.js\";\nimport { type StreamName } from \"@/contracts/streams.js\";\nimport { IdempotencyGuard } from \"@/index.js\";\nimport { createDatabase } from \"@/db/index.js\";\nimport {\n UserRepository,\n PreferenceRepository,\n TemplateRepository,\n ContactRepository,\n} from \"@/index.js\";\nimport { TemplateCache } from \"@/templates/index.js\";\nimport { getPriorityBucket, type WorkerOptions } from \"@/shared/index.js\";\nimport { startHealthReporter } from \"@/workers/index.js\";\n\n// ─── Bootstrap ─────────────────────────────────────────────────────────────\n\nloadEnv();\nconst config = readBaseConfig();\n\nlet logger: ReturnType<typeof createLogger>;\nlet redis: RedisClient;\nlet sql: any;\nlet db: any;\n\nlet consumer: StreamConsumer;\nlet pendingScanner: PendingMessageScanner;\nlet worker: BaseWorker;\nlet healthInterval: NodeJS.Timeout | null = null;\n\nexport interface EnricherWorkerOptions extends WorkerOptions {\n producers: any;\n idempotency: any;\n userRepo: any;\n prefRepo: any;\n contactRepo: any;\n templateCache: TemplateCache;\n}\n\nexport class EnricherWorker extends BaseWorker {\n private readonly producers: any;\n private readonly idempotency: any;\n private readonly userRepo: any;\n private readonly prefRepo: any;\n private readonly contactRepo: any;\n private readonly templateCache: TemplateCache;\n\n private userBatch: {\n projectId: string;\n userId: string;\n resolve: (p: any) => void;\n reject: (e: any) => void;\n }[] = [];\n private batchTimer: NodeJS.Timeout | null = null;\n private eventBuffer: {\n producer: any;\n event: any;\n resolve: () => void;\n reject: (e: any) => void;\n }[] = [];\n private flushTimer: NodeJS.Timeout | null = null;\n\n private contactBatch: {\n projectId: string;\n userIds: string[];\n resolve: (c: Map<string, any[]>) => void;\n reject: (e: any) => void;\n }[] = [];\n private contactBatchTimer: NodeJS.Timeout | null = null;\n\n private async loadContacts(projectId: string, userIds: string[]): Promise<Map<string, any[]>> {\n return new Promise((resolve, reject) => {\n this.contactBatch.push({ projectId, userIds, resolve, reject });\n if (this.contactBatch.length >= 500) {\n if (this.contactBatchTimer) clearTimeout(this.contactBatchTimer);\n void this.flushContactBatch();\n } else if (!this.contactBatchTimer) {\n this.contactBatchTimer = setTimeout(() => void this.flushContactBatch(), 10);\n }\n });\n }\n\n private async flushContactBatch(): Promise<void> {\n const batch = this.contactBatch;\n this.contactBatch = [];\n this.contactBatchTimer = null;\n if (batch.length === 0) return;\n\n try {\n const byProject = new Map<string, typeof batch>();\n for (const b of batch) {\n if (!byProject.has(b.projectId)) byProject.set(b.projectId, []);\n byProject.get(b.projectId)!.push(b);\n }\n\n for (const [projectId, items] of byProject) {\n const userIds = Array.from(new Set(items.flatMap((i) => i.userIds)));\n const contactsMap = await this.contactRepo.findActiveByUserIds(projectId, userIds);\n for (const item of items) {\n item.resolve(contactsMap);\n }\n }\n } catch (err) {\n for (const b of batch) b.reject(err);\n }\n }\n\n constructor(options: EnricherWorkerOptions) {\n super(options);\n this.producers = options.producers;\n this.idempotency = options.idempotency;\n this.userRepo = options.userRepo;\n this.prefRepo = options.prefRepo;\n this.contactRepo = options.contactRepo;\n this.templateCache = options.templateCache;\n\n this.flushTimer = setInterval(() => void this.flushWorkerBuffers(), 100);\n }\n\n override async stop(): Promise<void> {\n if (this.batchTimer) {\n clearTimeout(this.batchTimer);\n this.batchTimer = null;\n }\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n if (this.contactBatchTimer) {\n clearTimeout(this.contactBatchTimer);\n this.contactBatchTimer = null;\n }\n await this.flushContactBatch();\n await this.flushUserBatch();\n await this.flushWorkerBuffers();\n await super.stop();\n }\n\n private async flushWorkerBuffers(): Promise<void> {\n if (this.eventBuffer.length === 0) return;\n const events = this.eventBuffer;\n this.eventBuffer = [];\n\n try {\n const byProducer = new Map<any, typeof events>();\n for (const e of events) {\n if (!byProducer.has(e.producer)) byProducer.set(e.producer, []);\n byProducer.get(e.producer)!.push(e);\n }\n for (const [producer, batch] of byProducer) {\n await producer.publishBatch(batch.map((b) => b.event));\n for (const b of batch) b.resolve();\n }\n } catch (err: any) {\n this.logger.error({ err }, \"failed to flush events in EnricherWorker\");\n for (const e of events) e.reject(err);\n }\n }\n\n private async loadUser(projectId: string, userId: string): Promise<any> {\n return new Promise((resolve, reject) => {\n this.userBatch.push({ projectId, userId, resolve, reject });\n if (this.userBatch.length >= 500) {\n if (this.batchTimer) clearTimeout(this.batchTimer);\n void this.flushUserBatch();\n } else if (!this.batchTimer) {\n this.batchTimer = setTimeout(() => {\n void this.flushUserBatch();\n }, 10);\n }\n });\n }\n\n private async flushUserBatch() {\n const batch = this.userBatch;\n this.userBatch = [];\n this.batchTimer = null;\n\n const byProject = new Map<string, typeof batch>();\n for (const b of batch) {\n if (!byProject.has(b.projectId)) byProject.set(b.projectId, []);\n byProject.get(b.projectId)!.push(b);\n }\n\n for (const [projectId, reqs] of byProject.entries()) {\n try {\n const uniqueIds = Array.from(new Set(reqs.map((r) => r.userId)));\n const profiles = await this.userRepo.findRecordsByIds(projectId, uniqueIds);\n const profileMap = new Map(profiles.map((p: any) => [p.userId, p]));\n\n for (const req of reqs) {\n req.resolve(profileMap.get(req.userId) || null);\n }\n } catch (err) {\n for (const req of reqs) req.reject(err);\n }\n }\n }\n\n async process(message: StreamMessage): Promise<void> {\n const { event } = message;\n const publishPromises: Promise<void>[] = [];\n\n let isRequested = true;\n const requestedResult = registry.safeParsePayload(\"notification.requested\", event.payload);\n let createdResult: any = null;\n if (!requestedResult.success) {\n createdResult = registry.safeParsePayload(\"notification.created\", event.payload);\n isRequested = false;\n }\n\n if (!isRequested && (!createdResult || !createdResult.success)) {\n const issues = createdResult\n ? createdResult.error.issues\n : (requestedResult as any).error.issues;\n this.logger.warn({ messageId: message.id, issues }, \"invalid payload — skipping\");\n return;\n }\n\n // Handle legacy notification.created\n if (!isRequested) {\n const raw = createdResult.data as NotificationCreatedPayload;\n const dedupeId = `${raw.projectId}:${raw.idempotencyKey ?? event.id}`;\n if (!(await this.idempotency.checkAndMark(dedupeId, 60))) return;\n try {\n const profile = await this.userRepo.findRecordById(raw.projectId, raw.recipientId);\n if (!profile) return;\n\n const prefs = await this.prefRepo.findByUserId(raw.projectId, raw.recipientId);\n const optedOutTypes = new Set(\n prefs.filter((p: any) => !p.optedIn).map((p: any) => p.eventType),\n );\n\n const enrichedPayload: NotificationEnrichedPayload = {\n projectId: raw.projectId,\n rawEventId: event.id,\n recipientId: raw.recipientId,\n channel: raw.channel,\n priority: raw.priority,\n templateId: raw.templateId,\n templateVariables: raw.payload,\n recipient: {\n id: profile.userId,\n email: profile.email ?? undefined,\n locale: profile.language ?? \"en\",\n timezone: profile.timezone ?? \"UTC\",\n preferences: {\n optedOut:\n optedOutTypes.has(event.type) || profile.preferences.topics?.[event.type] === false,\n channels: Object.entries(profile.preferences.channels ?? {})\n .filter(([_, enabled]) => !enabled)\n .map(([channel]) => channel as any),\n quietHours: profile.preferences.quietHours,\n },\n },\n // No campaignId: this is the legacy `notification.created` path,\n // which predates campaigns and carries no label to attribute to.\n scheduledAt: raw.scheduledAt,\n };\n\n const p = getPriorityBucket(raw.priority);\n const producer = this.producers[p] ?? this.producers[\"normal\"]!;\n\n publishPromises.push(\n new Promise((resolve, reject) => {\n this.eventBuffer.push({\n producer,\n event: buildStreamEvent(\n \"notification.enriched\",\n enrichedPayload as Record<string, unknown>,\n \"enricher\",\n event.metadata.traceId,\n ),\n resolve,\n reject,\n });\n }),\n );\n this.logger.info(\n { messageId: message.id, eventId: event.id, recipientId: raw.recipientId },\n \"event enriched\",\n );\n } catch (err) {\n throw err;\n }\n await Promise.all(publishPromises).catch(async (err) => {\n await this.idempotency.unmark(dedupeId).catch(() => {});\n throw err;\n });\n await this.idempotency.markProcessed(dedupeId);\n return;\n }\n\n // Handle new notification.requested\n const raw = (requestedResult as any).data;\n const dedupeId = `${raw.projectId}:${raw.idempotencyKey ?? event.id}`;\n if (!(await this.idempotency.checkAndMark(dedupeId, 60))) return;\n try {\n let userIds: string[] = [];\n if (raw.target.type === \"user\") {\n userIds = [raw.target.userId];\n } else if (raw.target.type === \"segment\") {\n userIds = await this.userRepo.findUsersBySegment(raw.projectId, raw.target.segment);\n this.logger.info(\n { segment: raw.target.segment, count: userIds.length },\n \"Resolved segment\",\n );\n } else if (raw.target.type === \"topic\") {\n userIds = await this.userRepo.findUsersByTopic(raw.projectId, raw.target.topic);\n this.logger.info({ topic: raw.target.topic, count: userIds.length }, \"Resolved topic\");\n } else {\n this.logger.warn({ target: raw.target }, \"Segment/topic resolution not fully implemented\");\n // Stub: maybe resolve later\n }\n\n const maxUsers = readBaseConfig().SEGMENT_MAX_USERS;\n if (userIds.length > maxUsers) {\n this.logger.error(\n { count: userIds.length, max: maxUsers, projectId: raw.projectId, eventId: event.id },\n \"Segment fan-out exceeds maximum allowed limit\",\n );\n const p = getPriorityBucket(raw.priority ?? \"normal\");\n const producer = this.producers[p] ?? this.producers.normal;\n await producer.publish(\n buildStreamEvent(\n \"notification.failed\",\n {\n projectId: raw.projectId,\n rawEventId: event.id,\n error: `Segment fan-out of ${userIds.length} exceeds limit of ${maxUsers}`,\n },\n \"enricher\",\n event.metadata.traceId,\n ),\n );\n return;\n }\n\n // Topic opt-outs are keyed on the TEMPLATE's topics, not the envelope type.\n // `event.type` here is \"notification.requested\", which no user ever sets a\n // preference against, so keying on it silently disabled every opt-out.\n const template = raw.templateId\n ? await this.templateCache.getCachedTemplate(raw.projectId, raw.templateId)\n : null;\n const topics: string[] = template?.topics ?? [];\n\n const channels =\n raw.channels && raw.channels.length > 0 ? raw.channels : ([\"email\"] as any[]);\n const isFallback = (raw as any).fallback === true;\n\n // If fallback is true, we only emit the first channel, and pass the rest in fallbackChain.\n // If fallback is false, we emit all channels concurrently.\n const channelsToProcess = isFallback ? [channels[0]] : channels;\n const fallbackChain = isFallback ? channels.slice(1) : undefined;\n\n const chunkArray = <T>(arr: T[], size: number) =>\n Array.from({ length: Math.ceil(arr.length / size) }, (v, i) =>\n arr.slice(i * size, i * size + size),\n );\n\n const chunks = chunkArray(userIds, 500);\n\n for (const chunk of chunks) {\n const profiles = (\n await Promise.all(chunk.map((id) => this.loadUser(raw.projectId, id)))\n ).filter(Boolean);\n const contactsByUser = await this.loadContacts(\n raw.projectId,\n profiles.map((profile: any) => profile.userId),\n );\n\n const batchedEvents: Record<\n \"critical\" | \"high\" | \"normal\" | \"low\",\n Omit<any, \"id\" | \"timestamp\">[]\n > = {\n critical: [],\n high: [],\n normal: [],\n low: [],\n };\n\n for (const profile of profiles) {\n for (const channel of channelsToProcess) {\n const contacts = contactsByUser.get(profile.userId) ?? [];\n const channelContacts = contacts.filter((contact: any) => contact.channel === channel);\n // Push resolves its active tokens at send time so token invalidation\n // remains current. Other channels need one task per address.\n const destinations =\n channel === \"push\" ? [undefined] : channelContacts.map((c: any) => c.target);\n if (destinations.length === 0) {\n this.logger.info(\n { recipientId: profile.userId, channel },\n \"no active contact for channel\",\n );\n continue;\n }\n for (const destination of destinations) {\n const enrichedPayload: NotificationEnrichedPayload = {\n projectId: raw.projectId,\n rawEventId: event.id,\n recipientId: profile.userId,\n channel: channel,\n priority: \"normal\",\n templateId: raw.templateId,\n templateVariables: raw.data,\n aiPrompts: raw.aiPrompts,\n recipient: {\n id: profile.userId,\n email:\n channel === \"email\"\n ? (destination ?? profile.email ?? undefined)\n : (profile.email ?? undefined),\n phone: channel === \"sms\" || channel === \"whatsapp\" ? destination : undefined,\n webhook: channel === \"webhook\" ? destination : undefined,\n telegram: channel === \"telegram\" ? destination : undefined,\n discord: channel === \"discord\" ? destination : undefined,\n slack: channel === \"slack\" ? destination : undefined,\n locale: profile.language ?? \"en\",\n timezone: profile.timezone ?? \"UTC\",\n preferences: {\n // Opted out if the user disabled ANY topic this template carries.\n optedOut: topics.some((t) => profile.preferences.topics?.[t] === false),\n channels: Object.entries(profile.preferences.channels ?? {})\n .filter(([_, enabled]) => !enabled)\n .map(([channel]) => channel as any),\n quietHours: profile.preferences.quietHours,\n },\n },\n scheduledAt: raw.scheduledAt,\n fallbackChain: fallbackChain?.length ? fallbackChain : undefined,\n campaignId: raw.campaignId,\n };\n\n const msgPriority = raw.priority ?? \"normal\";\n const p = getPriorityBucket(msgPriority);\n\n enrichedPayload.priority = msgPriority;\n\n batchedEvents[p].push(\n buildStreamEvent(\n \"notification.enriched\",\n enrichedPayload as Record<string, unknown>,\n \"enricher\",\n event.metadata.traceId,\n ),\n );\n }\n }\n }\n\n for (const p of [\"critical\", \"normal\", \"low\"] as const) {\n if (batchedEvents[p].length > 0) {\n const producer = this.producers[p] ?? this.producers.normal;\n for (const ev of batchedEvents[p]) {\n publishPromises.push(\n new Promise((resolve, reject) => {\n this.eventBuffer.push({ producer, event: ev, resolve, reject });\n }),\n );\n }\n }\n }\n }\n\n this.logger.info(\n {\n messageId: message.id,\n eventId: event.id,\n target: raw.target.type,\n traceId: event.metadata.traceId,\n },\n \"event enriched\",\n );\n } catch (err) {\n throw err;\n }\n\n await Promise.all(publishPromises).catch(async (err) => {\n await this.idempotency.unmark(dedupeId).catch(() => {});\n throw err;\n });\n\n await this.idempotency.markProcessed(dedupeId);\n }\n}\n\nexport async function startEnricherWorker() {\n logger = createLogger({ name: \"enricher\", level: config.LOG_LEVEL });\n redis = new RedisClient({ url: config.REDIS_URL, name: \"enricher\", logger });\n const dbData = createDatabase({ url: config.DATABASE_URL, applicationName: \"enricher\", logger });\n sql = dbData.sql;\n db = dbData.db;\n consumer = new StreamConsumer({\n redis: redis.native,\n stream: INBOUND_STREAMS as unknown as StreamName[],\n group: CONSUMER_GROUPS.ENRICHER,\n consumer: `enricher-${process.pid}`,\n dlqStream: STREAMS.DEAD_LETTER,\n batchSize: config.WORKER_CONCURRENCY,\n logger,\n });\n\n pendingScanner = new PendingMessageScanner({\n redis: redis.native,\n stream: INBOUND_STREAMS as unknown as StreamName[],\n group: CONSUMER_GROUPS.ENRICHER,\n consumer: `enricher-${process.pid}`,\n logger,\n });\n\n const producers = {\n critical: new StreamProducer({\n redis: redis.native,\n stream: STREAMS.ENRICHED_CRITICAL,\n logger,\n }),\n normal: new StreamProducer({ redis: redis.native, stream: STREAMS.ENRICHED_NORMAL, logger }),\n low: new StreamProducer({ redis: redis.native, stream: STREAMS.ENRICHED_LOW, logger }),\n };\n\n const idempotency = new IdempotencyGuard({\n redis: redis.native,\n keyPrefix: \"notif:processed:enricher\",\n ttlSeconds: 86_400,\n });\n\n const userRepo = new UserRepository(db);\n const prefRepo = new PreferenceRepository(db);\n const contactRepo = new ContactRepository(db);\n const templateCache = new TemplateCache(new TemplateRepository(db));\n\n // ─── Stage 1: Context Enricher ──────────────────────────────────────────────\n //\n // Pipeline:\n // 1. Parse payload as notification.created\n // 2. Idempotency check — drop if already processed\n // 3. Load user profile (language, timezone) from DB\n // 4. Load all stored preferences for this user\n // 5. Publish notification.enriched to ENRICHED stream\n\n // 5. Publish notification.enriched to ENRICHED stream\n\n worker = new EnricherWorker({\n consumer,\n pendingScanner,\n logger,\n maxRetriesBeforeDlq: 5,\n concurrency: config.WORKER_CONCURRENCY,\n producers,\n idempotency,\n userRepo,\n prefRepo,\n contactRepo,\n templateCache,\n });\n\n // ─── Health check interval ──────────────────────────────────────────────────\n\n healthInterval = startHealthReporter(\"enricher\", worker, redis, logger);\n\n logger.info({ env: config.NODE_ENV }, \"enricher starting\");\n await worker.start();\n}\n\n// ─── Shutdown ──────────────────────────────────────────────────────────────\n\nexport async function stopEnricherWorker(): Promise<void> {\n logger?.info(\"shutdown initiated\");\n if (healthInterval) {\n clearInterval(healthInterval);\n healthInterval = null;\n }\n if (worker) await worker.stop();\n if (sql) await sql.end();\n if (redis) await redis.disconnect();\n logger?.info(\"enricher stopped\");\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as metrics, A as RedisClient, G as getPriorityBucket, J as globalEmitter, Qt as CONSUMER_GROUPS, R as CircuitBreaker, S as ContactRepository, X as StreamConsumer, Y as PendingMessageScanner, Z as StreamProducer, Zt as buildStreamEvent, _n as parseConfig, at as createDatabase, c as BaseWorker, cn as registry, dt as scheduledPayloads, gn as loadEnv, it as IdempotencyGuard, l as NonRetryableError, mn as baseConfigSchema, rn as STREAMS, st as deliveryOutbox, tn as OUTBOUND_STREAMS, tt as createLogger, u as startHealthReporter, z as BatchProcessor } from "./src-
|
|
1
|
+
import { $ as metrics, A as RedisClient, G as getPriorityBucket, J as globalEmitter, Qt as CONSUMER_GROUPS, R as CircuitBreaker, S as ContactRepository, X as StreamConsumer, Y as PendingMessageScanner, Z as StreamProducer, Zt as buildStreamEvent, _n as parseConfig, at as createDatabase, c as BaseWorker, cn as registry, dt as scheduledPayloads, gn as loadEnv, it as IdempotencyGuard, l as NonRetryableError, mn as baseConfigSchema, rn as STREAMS, st as deliveryOutbox, tn as OUTBOUND_STREAMS, tt as createLogger, u as startHealthReporter, z as BatchProcessor } from "./src-QiTu5e_a.mjs";
|
|
2
2
|
import { transportRegistry } from "./index.mjs";
|
|
3
3
|
//#region src/services/delivery/throttle.ts
|
|
4
4
|
const LUA_THROTTLE = `
|
|
@@ -629,4 +629,4 @@ async function stopDeliveryWorker() {
|
|
|
629
629
|
//#endregion
|
|
630
630
|
export { startDeliveryWorker, stopDeliveryWorker };
|
|
631
631
|
|
|
632
|
-
//# sourceMappingURL=main-
|
|
632
|
+
//# sourceMappingURL=main-BM8DH3v3.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main-CyeQ_yDt.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-BM8DH3v3.mjs","names":[],"sources":["../src/services/delivery/throttle.ts","../src/services/delivery/main.ts"],"sourcesContent":["import type { Redis } from \"ioredis\";\n\nexport interface ThrottleResult {\n allowed: boolean;\n retryAfterMs: number;\n}\n\nconst LUA_THROTTLE = `\n local key = KEYS[1]\n local now = tonumber(ARGV[1])\n local windowSeconds = tonumber(ARGV[2])\n local limit = tonumber(ARGV[3])\n local member = ARGV[4]\n\n local clearBefore = now - (windowSeconds * 1000)\n \n -- Cleanup expired scores\n redis.call('ZREMRANGEBYSCORE', key, 0, clearBefore)\n \n -- Get current count\n local count = redis.call('ZCARD', key)\n \n if count >= limit then\n -- Find the oldest score\n local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')\n if oldest and oldest[2] then\n return {0, tonumber(oldest[2])}\n end\n return {0, now}\n end\n \n -- Add new request\n redis.call('ZADD', key, now, member)\n redis.call('EXPIRE', key, windowSeconds * 2)\n return {1, 0}\n`;\n\nexport async function throttleProvider(\n redis: Redis,\n channel: string,\n config: { limit: number; windowSeconds: number },\n logger: any,\n): Promise<ThrottleResult> {\n const key = `rate-limit:provider:${channel}`;\n const now = Date.now();\n const zmember = `${now}:${Math.random()}`;\n\n const result = (await redis.eval(\n LUA_THROTTLE,\n 1,\n key,\n now.toString(),\n config.windowSeconds.toString(),\n config.limit.toString(),\n zmember,\n )) as [number, number];\n\n const allowed = result[0] === 1;\n const oldestTimestamp = result[1];\n\n let retryAfterMs = 0;\n if (!allowed) {\n retryAfterMs = Math.max(0, oldestTimestamp + config.windowSeconds * 1000 - now);\n logger.warn(\n { channel, limit: config.limit, windowSeconds: config.windowSeconds, retryAfterMs },\n \"Provider rate limit hit — task must be rescheduled\",\n );\n }\n\n return { allowed, retryAfterMs };\n}\n","import { loadEnv, parseConfig, baseConfigSchema } from \"@/index.js\";\nimport { createLogger } from \"@/index.js\";\nimport { RedisClient, type Redis } from \"@/index.js\";\nimport {\n StreamConsumer,\n PendingMessageScanner,\n type StreamMessage,\n StreamProducer,\n} from \"@/index.js\";\nimport { BaseWorker } from \"@/index.js\";\nimport {\n STREAMS,\n OUTBOUND_STREAMS,\n CONSUMER_GROUPS,\n registry,\n buildStreamEvent,\n type NotificationDispatchedPayload,\n} from \"@/index.js\";\nimport { type StreamName } from \"@/contracts/streams.js\";\nimport { createDatabase } from \"@/db/index.js\";\nimport { deliveryOutbox, scheduledPayloads } from \"@/db/schema.js\";\nimport { ContactRepository, IdempotencyGuard } from \"@/index.js\";\nimport { transportRegistry } from \"@/index.js\";\nimport { globalEmitter, getPriorityBucket, type WorkerOptions } from \"@/shared/index.js\";\nimport { startHealthReporter, NonRetryableError } from \"@/workers/index.js\";\nimport { BatchProcessor, CircuitBreaker } from \"@/shared/index.js\";\nimport { throttleProvider } from \"./throttle.js\";\nimport { metrics } from \"@/metrics/index.js\";\n\n// ─── App-specific config ────────────────────────────────────────────────────\n\nconst deliveryConfigSchema = baseConfigSchema.extend({});\n\n// ─── Bootstrap ─────────────────────────────────────────────────────────────\n\nloadEnv();\nconst config = parseConfig(deliveryConfigSchema, process.env);\n\nlet logger: ReturnType<typeof createLogger>;\nlet redis: RedisClient;\nlet sql: any;\nlet db: any;\n\nlet consumer: StreamConsumer;\nlet pendingScanner: PendingMessageScanner;\nlet worker: BaseWorker;\nlet scheduledProducer: StreamProducer;\nlet enrichedProducers: Record<string, StreamProducer>;\nlet healthInterval: NodeJS.Timeout | null = null;\n\n(global as any)._telemetry = (global as any)._telemetry || {\n count: 0,\n insert: 0,\n provider: 0,\n flush: 0,\n ack: 0,\n dequeue: 0,\n dbupdate: 0,\n flushCount: 0,\n};\n\nexport interface DeliveryWorkerOptions extends WorkerOptions {\n transportRegistry: any;\n idempotency: any;\n redis: Redis;\n scheduledProducer: any;\n enrichedProducers: any;\n contactRepo: any;\n eventsProducer: any;\n globalEmitter: any;\n db: any;\n}\n\nexport class DeliveryWorker extends BaseWorker {\n private readonly transportRegistry: any;\n private readonly idempotency: any;\n private readonly redisCli: Redis;\n private readonly scheduledProducer: any;\n private readonly enrichedProducers: any;\n private readonly contactRepo: any;\n private readonly eventsProducer: any;\n private readonly globalEmitter: any;\n private readonly db: any;\n\n private eventProcessor: BatchProcessor<any, void>;\n private outboxUpdateProcessor: BatchProcessor<any, void>;\n private outboxInsertProcessor: BatchProcessor<any, boolean>;\n private breakers = new Map<string, CircuitBreaker>();\n\n constructor(options: DeliveryWorkerOptions) {\n super(options);\n this.transportRegistry = options.transportRegistry;\n this.idempotency = options.idempotency;\n this.redisCli = options.redis;\n this.scheduledProducer = options.scheduledProducer;\n this.enrichedProducers = options.enrichedProducers;\n this.contactRepo = options.contactRepo;\n this.eventsProducer = options.eventsProducer;\n this.globalEmitter = options.globalEmitter;\n this.db = options.db;\n\n this.eventProcessor = new BatchProcessor<any, void>(1000, 100, async (events) => {\n await this.eventsProducer.publishBatch(events);\n return events.map(() => undefined as void);\n });\n\n this.outboxUpdateProcessor = new BatchProcessor<any, void>(500, 100, async (updates) => {\n const { sql } = await import(\"drizzle-orm\");\n const values = updates.map((update) => ({\n taskId: update.taskId,\n channel: update.channel,\n destination: update.destination,\n providerMessageId: update.providerMessageId,\n }));\n\n const tDbUpdateStart = Date.now();\n await this.db\n .insert(deliveryOutbox)\n .values(values)\n .onConflictDoUpdate({\n target: [deliveryOutbox.taskId, deliveryOutbox.channel, deliveryOutbox.destination],\n set: { providerMessageId: sql`EXCLUDED.provider_message_id` },\n })\n .catch((e: any) => this.logger.error({ err: e }, \"background update failed\"));\n\n (global as any)._telemetry.dbupdate += Date.now() - tDbUpdateStart;\n (global as any)._telemetry.flushCount++;\n return updates.map(() => undefined as void);\n });\n\n this.outboxInsertProcessor = new BatchProcessor(500, 10, async (tasks) => {\n const values = tasks.map((task: any) => ({\n taskId: task.taskId,\n channel: task.channel,\n destination: task.destination,\n }));\n\n await this.db.insert(deliveryOutbox).values(values).onConflictDoNothing();\n return tasks.map(() => true);\n });\n }\n\n override async stop(): Promise<void> {\n await Promise.all([\n this.eventProcessor.flush(),\n this.outboxUpdateProcessor.flush(),\n this.outboxInsertProcessor.flush(),\n ]);\n await super.stop();\n }\n\n private getBreaker(name: string): CircuitBreaker {\n let breaker = this.breakers.get(name);\n if (!breaker) {\n breaker = new CircuitBreaker({ failureThreshold: 5, resetTimeoutMs: 30000 });\n this.breakers.set(name, breaker);\n }\n return breaker;\n }\n\n async process(message: StreamMessage, attempt: number = 1): Promise<void> {\n const { event } = message;\n const publishPromises: Promise<void>[] = [];\n\n const payloadResult = registry.safeParsePayload(\"notification.dispatched\", event.payload);\n if (!payloadResult.success) {\n this.logger.warn(\n { messageId: message.id, issues: payloadResult.error.issues },\n \"invalid notification.dispatched payload — skipping\",\n );\n return;\n }\n\n const task = payloadResult.data as NotificationDispatchedPayload;\n\n const fallbackToNextChannel = async (reason: string) => {\n if (task.fallbackChain && task.fallbackChain.length > 0 && task.recipient) {\n const nextChannel = task.fallbackChain[0];\n const remainingChain = task.fallbackChain.slice(1);\n\n const fallbackPayload: any = {\n // NotificationEnrichedPayload\n projectId: task.projectId,\n rawEventId: task.enrichedEventId,\n recipientId: task.recipientId,\n channel: nextChannel,\n priority: task.priority,\n templateId: task.templateId,\n templateVariables: task.templateVariables,\n aiPrompts: task.aiPrompts,\n recipient: task.recipient,\n scheduledAt: undefined,\n fallbackChain: remainingChain.length > 0 ? remainingChain : undefined,\n };\n\n const p = getPriorityBucket(task.priority);\n const producer = this.enrichedProducers[p] ?? this.enrichedProducers[\"normal\"]!;\n await producer.publish(\n buildStreamEvent(\n \"notification.enriched\",\n fallbackPayload,\n \"delivery\",\n event.metadata.traceId,\n ),\n );\n\n this.logger.info(\n { taskId: task.taskId, reason, nextChannel, traceId: event.metadata.traceId },\n \"Delivery failed completely, rolling over to next channel in fallback chain\",\n );\n return true; // Indicates fallback was triggered\n }\n return false; // No fallback\n };\n\n const transports = this.transportRegistry.getAll(task.channel);\n\n if (transports.length === 0) {\n this.logger.warn(\n { taskId: task.taskId, channel: task.channel },\n \"no transport registered for channel — dropping\",\n );\n const fallbackTriggered = await fallbackToNextChannel(\"no_transport\");\n if (!fallbackTriggered) {\n this.globalEmitter.emit(\n \"delivery:failed\",\n task.taskId,\n \"no transport\",\n task.channel,\n task.projectId,\n );\n await this.eventProcessor.add(\n buildStreamEvent(\n \"notification.failed\",\n {\n projectId: task.projectId,\n taskId: task.taskId,\n enrichedEventId: task.enrichedEventId,\n recipientId: task.recipientId,\n channel: task.channel,\n failureReason: \"no transport registered for channel\",\n failureCode: \"no_transport\",\n retryable: false,\n attempt,\n templateId: task.templateId,\n workflowInstanceId:\n event.metadata.source === \"workflow\" ? event.metadata.traceId : undefined,\n campaignId: task.campaignId,\n },\n \"delivery\",\n event.metadata.traceId,\n ),\n );\n }\n return;\n }\n\n const limitConfig = transports[0].limits;\n if (limitConfig) {\n const { allowed, retryAfterMs } = await throttleProvider(\n this.redisCli,\n task.channel,\n limitConfig,\n this.logger,\n );\n if (!allowed) {\n task.throttleAttemptCount = (task.throttleAttemptCount ?? 0) + 1;\n const maxAttempts = task.deliveryOptions?.maxAttempts ?? 3;\n\n if (task.throttleAttemptCount > maxAttempts) {\n this.logger.warn(\n { messageId: message.id, taskId: task.taskId, attempts: task.throttleAttemptCount },\n \"provider rate limit max attempts exceeded\",\n );\n const fallbackTriggered = await fallbackToNextChannel(\"provider_throttle_exceeded\");\n if (!fallbackTriggered) {\n this.globalEmitter.emit(\n \"delivery:failed\",\n task.taskId,\n \"provider throttle exceeded\",\n task.channel,\n task.projectId,\n );\n await this.eventProcessor.add(\n buildStreamEvent(\n \"notification.failed\",\n {\n projectId: task.projectId,\n taskId: task.taskId,\n enrichedEventId: task.enrichedEventId,\n recipientId: task.recipientId,\n channel: task.channel,\n failureReason: \"provider rate limit max attempts exceeded\",\n failureCode: \"provider_throttle_exceeded\",\n retryable: false,\n attempt,\n templateId: task.templateId,\n workflowInstanceId:\n event.metadata.source === \"workflow\" ? event.metadata.traceId : undefined,\n campaignId: task.campaignId,\n },\n \"delivery\",\n event.metadata.traceId,\n ),\n );\n }\n return;\n }\n\n const { sql } = await import(\"drizzle-orm\");\n await this.db\n .insert(scheduledPayloads)\n .values({\n taskId: task.taskId,\n payload: task,\n })\n .onConflictDoUpdate({\n target: scheduledPayloads.taskId,\n set: { payload: sql`EXCLUDED.payload` },\n });\n\n await this.scheduledProducer.publish(\n buildStreamEvent(\n \"notification.scheduled\",\n {\n projectId: task.projectId,\n enrichedEventId: task.enrichedEventId,\n taskId: task.taskId,\n scheduledAt: new Date(Date.now() + retryAfterMs).toISOString(),\n throttleAttemptCount: task.throttleAttemptCount,\n },\n \"delivery\",\n `${task.taskId}:throttle:${Date.now()}`,\n ),\n );\n\n return;\n }\n }\n\n const tInsertStart = Date.now();\n const idempotencyKey = task.taskId;\n if (!(await this.idempotency.checkAndMark(idempotencyKey, 60))) {\n this.logger.info(\n { messageId: message.id, taskId: task.taskId, channel: task.channel, attempt },\n \"duplicate delivery — skipping\",\n );\n return;\n }\n\n try {\n await this.outboxInsertProcessor.add(task);\n const insertTime = Date.now() - tInsertStart;\n\n publishPromises.push(\n this.eventProcessor.add(\n buildStreamEvent(\n \"notification.dispatched\",\n {\n projectId: task.projectId,\n taskId: task.taskId,\n enrichedEventId: task.enrichedEventId,\n recipientId: task.recipientId,\n channel: task.channel,\n templateId: task.templateId,\n attempt,\n workflowInstanceId:\n event.metadata.source === \"workflow\" ? event.metadata.traceId : undefined,\n campaignId: task.campaignId,\n },\n \"delivery\",\n event.metadata.traceId,\n ),\n ),\n );\n\n // For push: we just need to send to the pre-resolved destination,\n // but if it's invalid, we deactivate it.\n if (task.channel === \"push\") {\n let lastResult: any = { success: false, error: \"No transports\" };\n\n for (const transport of transports) {\n try {\n const tProv = Date.now();\n const breaker = this.getBreaker(`${task.channel}:${transport.constructor.name}`);\n\n lastResult = await breaker.execute(async () => {\n const timeoutMs = task.deliveryOptions?.timeoutMs ?? 10_000;\n const controller = new AbortController();\n const timeout = setTimeout(() => {\n controller.abort(new Error(`Transport timeout after ${timeoutMs}ms`));\n }, timeoutMs);\n\n try {\n (task as any).signal = controller.signal;\n const res: any = await Promise.race([\n transport.send(task),\n new Promise((_, reject) => {\n if (controller.signal.aborted) return reject(controller.signal.reason);\n controller.signal.addEventListener(\"abort\", () =>\n reject(controller.signal.reason),\n );\n }),\n ]);\n if (!res.success && !res.invalidToken) {\n throw new Error(res.error ?? \"Transport failed\");\n }\n return res;\n } finally {\n clearTimeout(timeout);\n }\n });\n\n (global as any)._telemetry.provider += Date.now() - tProv;\n if (lastResult.success || lastResult.invalidToken) break;\n } catch (err: any) {\n lastResult = { success: false, error: err.message };\n }\n }\n\n if (lastResult.invalidToken) {\n await this.contactRepo.deactivate(\n task.projectId,\n task.recipientId,\n task.channel as any,\n task.destination,\n );\n this.logger.info(\n { channel: task.channel, target: task.destination },\n \"deactivated invalid contact target\",\n );\n this.globalEmitter.emit(\n \"delivery:failed\",\n task.taskId,\n \"invalidToken\",\n task.channel,\n task.projectId,\n );\n metrics.deliveryFailed.inc({ channel: task.channel, reason: \"invalid_token\" });\n\n publishPromises.push(\n this.eventProcessor.add(\n buildStreamEvent(\n \"notification.failed\",\n {\n projectId: task.projectId,\n taskId: task.taskId,\n enrichedEventId: task.enrichedEventId,\n recipientId: task.recipientId,\n channel: task.channel,\n failureReason: \"invalid_token\",\n failureCode: \"push_failure\",\n retryable: false,\n attempt,\n templateId: task.templateId,\n workflowInstanceId:\n event.metadata.source === \"workflow\" ? event.metadata.traceId : undefined,\n campaignId: task.campaignId,\n },\n \"delivery\",\n event.metadata.traceId,\n ),\n ),\n );\n\n const fallbackTriggered = await fallbackToNextChannel(\"invalid_token\");\n if (!fallbackTriggered) {\n throw new NonRetryableError(\"Push delivery failed: invalid token\");\n }\n } else if (lastResult.success) {\n // Record providerMessageId in outbox before publishing event\n const providerMessageId = lastResult.providerMessageId || \"push-success\";\n\n publishPromises.push(\n this.outboxUpdateProcessor.add({\n taskId: task.taskId,\n channel: task.channel,\n destination: task.destination,\n providerMessageId,\n }),\n );\n\n this.logger.debug(\n { taskId: task.taskId, messageId: providerMessageId },\n \"push delivered\",\n );\n this.globalEmitter.emit(\n \"delivery:delivered\",\n task.taskId,\n providerMessageId,\n task.channel,\n task.projectId,\n );\n metrics.deliverySuccess.inc({ channel: task.channel });\n\n publishPromises.push(\n this.eventProcessor.add(\n buildStreamEvent(\n \"notification.delivered\",\n {\n projectId: task.projectId,\n taskId: task.taskId,\n enrichedEventId: task.enrichedEventId,\n channel: task.channel,\n deliveredAt: new Date().toISOString(),\n providerMessageId,\n templateId: task.templateId,\n workflowInstanceId:\n event.metadata.source === \"workflow\" ? event.metadata.traceId : undefined,\n campaignId: task.campaignId,\n },\n \"delivery\",\n event.metadata.traceId,\n ),\n ),\n );\n } else {\n this.logger.warn(\n { taskId: task.taskId, error: lastResult.error },\n \"push delivery failed\",\n );\n this.globalEmitter.emit(\n \"delivery:failed\",\n task.taskId,\n lastResult.error ?? \"push delivery failed\",\n task.channel,\n task.projectId,\n );\n metrics.deliveryFailed.inc({ channel: task.channel, reason: \"push_error\" });\n\n publishPromises.push(\n this.eventProcessor.add(\n buildStreamEvent(\n \"notification.failed\",\n {\n projectId: task.projectId,\n taskId: task.taskId,\n enrichedEventId: task.enrichedEventId,\n recipientId: task.recipientId,\n channel: task.channel,\n failureReason: lastResult.error ?? \"push delivery failed\",\n failureCode: \"push_failure\",\n retryable: false,\n attempt,\n templateId: task.templateId,\n workflowInstanceId:\n event.metadata.source === \"workflow\" ? event.metadata.traceId : undefined,\n campaignId: task.campaignId,\n },\n \"delivery\",\n event.metadata.traceId,\n ),\n ),\n );\n\n const fallbackTriggered = await fallbackToNextChannel(\"push_delivery_failed\");\n if (!fallbackTriggered) {\n throw new NonRetryableError(lastResult.error ?? \"Push delivery failed\");\n }\n }\n } else {\n // Other channels (email, sms, webhook, whatsapp) use the pre-resolved destination\n let result: any = { success: false, error: \"No transports\" };\n for (const transport of transports) {\n try {\n const tProv = Date.now();\n const breaker = this.getBreaker(`${task.channel}:${transport.constructor.name}`);\n\n result = await breaker.execute(async () => {\n const timeoutMs = task.deliveryOptions?.timeoutMs ?? 10_000;\n const controller = new AbortController();\n const timeout = setTimeout(() => {\n controller.abort(new Error(`Transport timeout after ${timeoutMs}ms`));\n }, timeoutMs);\n\n try {\n (task as any).signal = controller.signal;\n const res: any = await Promise.race([\n transport.send(task),\n new Promise((_, reject) => {\n if (controller.signal.aborted) return reject(controller.signal.reason);\n controller.signal.addEventListener(\"abort\", () =>\n reject(controller.signal.reason),\n );\n }),\n ]);\n if (!res.success) {\n throw new Error(res.error ?? \"Transport failed\");\n }\n return res;\n } finally {\n clearTimeout(timeout);\n }\n });\n\n (global as any)._telemetry.provider += Date.now() - tProv;\n if (result.success) break;\n } catch (err: any) {\n result = { success: false, error: err.message };\n }\n }\n\n if (result.success) {\n // Record providerMessageId in outbox before publishing event\n const providerMessageId = result.providerMessageId || \"success\";\n\n publishPromises.push(\n this.outboxUpdateProcessor.add({\n taskId: task.taskId,\n channel: task.channel,\n destination: task.destination,\n providerMessageId,\n }),\n );\n\n this.globalEmitter.emit(\n \"delivery:delivered\",\n task.taskId,\n providerMessageId,\n task.channel,\n task.projectId,\n );\n metrics.deliverySuccess.inc({ channel: task.channel });\n\n publishPromises.push(\n this.eventProcessor.add(\n buildStreamEvent(\n \"notification.delivered\",\n {\n projectId: task.projectId,\n taskId: task.taskId,\n enrichedEventId: task.enrichedEventId,\n channel: task.channel,\n deliveredAt: new Date().toISOString(),\n providerMessageId,\n templateId: task.templateId,\n workflowInstanceId:\n event.metadata.source === \"workflow\" ? event.metadata.traceId : undefined,\n campaignId: task.campaignId,\n },\n \"delivery\",\n event.metadata.traceId,\n ),\n ),\n );\n } else {\n publishPromises.push(\n this.eventProcessor.add(\n buildStreamEvent(\n \"notification.failed\",\n {\n projectId: task.projectId,\n taskId: task.taskId,\n enrichedEventId: task.enrichedEventId,\n channel: task.channel,\n failureReason: result.error ?? \"delivery failed completely\",\n failureCode: \"provider_error\",\n retryable: false,\n attempt,\n templateId: task.templateId,\n workflowInstanceId:\n event.metadata.source === \"workflow\" ? event.metadata.traceId : undefined,\n campaignId: task.campaignId,\n },\n \"delivery\",\n event.metadata.traceId,\n ),\n ),\n );\n }\n\n if (!result.success) {\n this.logger.warn(\n { taskId: task.taskId, channel: task.channel, error: result.error },\n \"delivery failed completely across providers\",\n );\n this.globalEmitter.emit(\n \"delivery:failed\",\n task.taskId,\n result.error ?? \"delivery failed completely\",\n task.channel,\n task.projectId,\n );\n metrics.deliveryFailed.inc({ channel: task.channel, reason: \"provider_error\" });\n\n const fallbackTriggered = await fallbackToNextChannel(\"all_providers_failed\");\n if (!fallbackTriggered) {\n throw new NonRetryableError(result.error ?? \"delivery failed\");\n }\n } else {\n this.logger.info(\n { taskId: task.taskId, channel: task.channel, messageId: result.providerMessageId },\n \"notification delivered\",\n );\n }\n }\n\n const tFlushStart = Date.now();\n await Promise.all(publishPromises).catch((err) => {\n this.logger.error(\n { err, taskId: task.taskId },\n \"failed to publish post-dispatch events, swallowing error to prevent duplicate delivery\",\n );\n });\n const flushTime = Date.now() - tFlushStart;\n\n await this.idempotency.markProcessed(idempotencyKey);\n\n const t = ((global as any)._telemetry = (global as any)._telemetry || {\n count: 0,\n insert: 0,\n provider: 0,\n flush: 0,\n ack: 0,\n });\n t.count++;\n t.insert += insertTime;\n t.flush += flushTime;\n\n if (t.count % 1000 === 0) {\n this.logger.debug(\n `[Metrics 1000 msgs] Dequeue: ${t.dequeue / 1000}ms, DB Insert: ${t.insert / 1000}ms, Provider: ${t.provider / 1000}ms, Wait for Flush: ${t.flush / 1000}ms, Ack: ${t.ack / 1000}ms | DB Update (avg per flush): ${t.dbupdate / Math.max(1, t.flushCount)}ms`,\n );\n t.count = 0;\n t.insert = 0;\n t.provider = 0;\n t.flush = 0;\n t.dequeue = 0;\n t.ack = 0;\n t.dbupdate = 0;\n t.flushCount = 0;\n }\n } catch (err) {\n await this.idempotency.unmark(idempotencyKey).catch(() => {});\n throw err;\n }\n }\n}\n\nexport async function startDeliveryWorker() {\n logger = createLogger({ name: \"delivery\", level: config.LOG_LEVEL });\n\n redis = new RedisClient({ url: config.REDIS_URL, name: \"delivery\", logger });\n const dbData = createDatabase({ url: config.DATABASE_URL, applicationName: \"delivery\", logger });\n sql = dbData.sql;\n db = dbData.db;\n consumer = new StreamConsumer({\n redis: redis.native,\n stream: OUTBOUND_STREAMS as unknown as StreamName[],\n group: CONSUMER_GROUPS.DELIVERY,\n consumer: `delivery-${process.pid}`,\n dlqStream: STREAMS.DEAD_LETTER,\n batchSize: config.WORKER_CONCURRENCY,\n logger,\n });\n\n scheduledProducer = new StreamProducer({\n redis: redis.native,\n stream: STREAMS.SCHEDULED,\n logger,\n });\n\n pendingScanner = new PendingMessageScanner({\n redis: redis.native,\n stream: OUTBOUND_STREAMS as unknown as StreamName[],\n group: CONSUMER_GROUPS.DELIVERY,\n consumer: `delivery-${process.pid}`,\n logger,\n });\n\n enrichedProducers = {\n critical: new StreamProducer({\n redis: redis.native,\n stream: STREAMS.ENRICHED_CRITICAL,\n logger,\n }),\n normal: new StreamProducer({ redis: redis.native, stream: STREAMS.ENRICHED_NORMAL, logger }),\n low: new StreamProducer({ redis: redis.native, stream: STREAMS.ENRICHED_LOW, logger }),\n };\n\n const contactRepo = new ContactRepository(db);\n\n const idempotency = new IdempotencyGuard({\n redis: redis.native,\n keyPrefix: \"notif:processed:delivery\",\n ttlSeconds: 86_400,\n });\n\n // ─── Stage 3: Delivery Worker ───────────────────────────────────────────────\n //\n // Pipeline:\n // 1. Parse payload as notification.dispatched\n // 2. Resolve active device tokens from DB\n // 3. Send via registered transport\n // 4. Deactivate invalid tokens in DB\n\n const eventsProducer = new StreamProducer({\n redis: redis.native,\n stream: STREAMS.EVENTS_INBOUND,\n logger,\n });\n\n worker = new DeliveryWorker({\n consumer,\n pendingScanner,\n logger,\n concurrency: config.WORKER_CONCURRENCY,\n transportRegistry,\n idempotency,\n redis: redis.native,\n scheduledProducer,\n enrichedProducers,\n contactRepo,\n eventsProducer,\n globalEmitter,\n db,\n });\n\n // ─── Health check interval ──────────────────────────────────────────────────\n\n healthInterval = startHealthReporter(\"delivery\", worker, redis, logger);\n\n logger.info(\n { env: config.NODE_ENV, channels: transportRegistry.registeredChannels() },\n \"delivery starting\",\n );\n await worker.start();\n}\n\n// ─── Shutdown ──────────────────────────────────────────────────────────────\n\nexport async function stopDeliveryWorker(): Promise<void> {\n logger?.info(\"shutdown initiated\");\n if (healthInterval) {\n clearInterval(healthInterval);\n healthInterval = null;\n }\n if (worker) await worker.stop();\n if (sql) await sql.end();\n if (redis) await redis.disconnect();\n logger?.info(\"delivery stopped\");\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as RedisClient, H as LUA_RENEW_LOCK, Qt as CONSUMER_GROUPS, V as LUA_RELEASE_LOCK, W as LUA_SCHEDULER_POLL, X as StreamConsumer, Y as PendingMessageScanner, Z as StreamProducer, Zt as buildStreamEvent, _t as workflowSteps, a as SuspendExecutionError, at as createDatabase, c as BaseWorker, gn as loadEnv, gt as workflowInstances, ht as workflowDefinitions, i as workflowRegistry, o as buildStepNotifyPayload, rn as STREAMS, tt as createLogger, u as startHealthReporter, vn as readBaseConfig, vt as workflowWaiters } from "./src-
|
|
1
|
+
import { A as RedisClient, H as LUA_RENEW_LOCK, Qt as CONSUMER_GROUPS, V as LUA_RELEASE_LOCK, W as LUA_SCHEDULER_POLL, X as StreamConsumer, Y as PendingMessageScanner, Z as StreamProducer, Zt as buildStreamEvent, _t as workflowSteps, a as SuspendExecutionError, at as createDatabase, c as BaseWorker, gn as loadEnv, gt as workflowInstances, ht as workflowDefinitions, i as workflowRegistry, o as buildStepNotifyPayload, rn as STREAMS, tt as createLogger, u as startHealthReporter, vn as readBaseConfig, vt as workflowWaiters } from "./src-QiTu5e_a.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { and, eq } from "drizzle-orm";
|
|
4
4
|
//#region src/services/workflow/main.ts
|
|
@@ -389,4 +389,4 @@ async function stopWorkflowWorker() {
|
|
|
389
389
|
//#endregion
|
|
390
390
|
export { startWorkflowWorker, stopWorkflowWorker };
|
|
391
391
|
|
|
392
|
-
//# sourceMappingURL=main-
|
|
392
|
+
//# sourceMappingURL=main-BgmT1MPe.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main-D19Wxs0x.mjs","names":["sql"],"sources":["../src/services/workflow/main.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { loadEnv, readBaseConfig } from \"@/index.js\";\nimport { createLogger } from \"@/index.js\";\nimport { RedisClient, type Redis } 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 {\n workflowInstances,\n workflowSteps,\n workflowWaiters,\n workflowDefinitions,\n} from \"@/db/schema.js\";\nimport { eq, and } from \"drizzle-orm\";\nimport {\n workflowRegistry,\n SuspendExecutionError,\n buildStepNotifyPayload,\n type WorkflowContext,\n type WorkflowStepContext,\n} from \"@/workflows/index.js\";\nimport {\n type WorkerOptions,\n LUA_SCHEDULER_POLL,\n LUA_RELEASE_LOCK,\n LUA_RENEW_LOCK,\n} from \"@/shared/index.js\";\nimport { startHealthReporter } from \"@/workers/index.js\";\n\n/** How long a workflow instance lock is held before it self-expires. */\nconst WORKFLOW_LOCK_TTL_SECONDS = 60;\n/** Renew the lock well inside its TTL so long-running handlers keep it. */\nconst WORKFLOW_LOCK_RENEW_MS = (WORKFLOW_LOCK_TTL_SECONDS / 3) * 1000;\n/** How long a claimed workflow timer stays invisible to other pollers. */\nconst WORKFLOW_TIMER_VISIBILITY_MS = 60_000;\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;\nlet pollInterval: NodeJS.Timeout | null = null;\nlet reaperInterval: NodeJS.Timeout | null = null;\n\nlet notificationProducer: StreamProducer;\nlet workflowProducer: StreamProducer;\n\nexport interface WorkflowWorkerOptions extends WorkerOptions {\n redis: Redis;\n db: any;\n workflowProducer: any;\n notificationProducer: any;\n}\n\nexport class WorkflowWorker extends BaseWorker {\n private readonly redisCli: Redis;\n private readonly dbConn: any;\n private readonly workflowProducer: any;\n private readonly notificationProducer: any;\n\n private eventBuffer: {\n producer: any;\n event: any;\n resolve: (result?: { messageId: string; notificationId: string }) => void;\n reject: (err: any) => void;\n }[] = [];\n private flushTimer: NodeJS.Timeout | null = null;\n\n constructor(options: WorkflowWorkerOptions) {\n super(options);\n this.redisCli = options.redis;\n this.dbConn = options.db;\n this.workflowProducer = options.workflowProducer;\n this.notificationProducer = options.notificationProducer;\n\n this.flushTimer = setInterval(() => void this.flushWorkerBuffers(), 100);\n }\n\n override async stop(): Promise<void> {\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n await this.flushWorkerBuffers();\n await super.stop();\n }\n\n private async flushWorkerBuffers(): Promise<void> {\n if (this.eventBuffer.length === 0) return;\n\n const events = this.eventBuffer;\n this.eventBuffer = [];\n\n try {\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\n for (const [producer, batch] of byProducer) {\n const { messageIds, eventIds } = await producer.publishBatch(batch.map((b) => b.event));\n for (let i = 0; i < batch.length; i++) {\n const mId = messageIds[i];\n const eId = eventIds[i];\n if (mId && eId) batch[i]!.resolve({ messageId: mId, notificationId: eId });\n }\n }\n } catch (err: any) {\n this.logger.error({ err }, \"failed to flush workflow worker buffer\");\n for (const e of events) e.reject(err);\n }\n }\n\n async process(message: StreamMessage): Promise<void> {\n const { event } = message;\n const publishPromises: Promise<void>[] = [];\n\n if (event.type !== \"workflow.triggered\" && event.type !== \"workflow.resumed\") {\n return;\n }\n\n const payload = event.payload as any;\n const name = payload.name;\n const instanceId = payload.instanceId;\n\n if (!name || !instanceId || !payload.projectId) {\n this.logger.warn(\"Missing name, instanceId, or projectId in workflow event\");\n return;\n }\n\n let handler = workflowRegistry.get(name);\n if (!handler) {\n // Fallback: check dynamic JSON workflows\n const defRows = await this.dbConn\n .select()\n .from(workflowDefinitions)\n .where(\n and(\n eq(workflowDefinitions.projectId, payload.projectId),\n eq(workflowDefinitions.name, name),\n ),\n )\n .limit(1);\n\n if (defRows.length === 0) {\n this.logger.warn(\n { name, projectId: payload.projectId },\n \"No handler or dynamic definition found for workflow\",\n );\n return;\n }\n\n const def = defRows[0];\n handler = async ({ step }) => {\n const steps = def.steps as any[];\n for (const stepDef of steps) {\n if (stepDef.action === \"notify\") {\n // A payload target wins; naming none inherits the instance user.\n await step.notify(stepDef.payload);\n } else if (stepDef.action === \"wait\") {\n await step.wait(stepDef.duration);\n } else if (stepDef.action === \"waitForEvent\") {\n await step.waitForEvent(stepDef.event, stepDef.options);\n } else {\n this.logger.warn(\n { action: (stepDef as any)?.action, name },\n \"unknown workflow step action\",\n );\n }\n }\n };\n }\n\n const lockKey = `lock:workflow:${instanceId}`;\n const lockToken = randomUUID();\n const acquired = await this.redisCli.set(\n lockKey,\n lockToken,\n \"EX\",\n WORKFLOW_LOCK_TTL_SECONDS,\n \"NX\",\n );\n if (!acquired) {\n this.logger.info({ instanceId }, \"Workflow is locked by another process, skipping\");\n return;\n }\n\n // Keep the lock alive while the handler runs; without this a handler that\n // outlives the TTL lets a second resume execute the same steps in parallel.\n const renewTimer = setInterval(() => {\n void this.redisCli\n .eval(LUA_RENEW_LOCK, 1, lockKey, lockToken, String(WORKFLOW_LOCK_TTL_SECONDS))\n .catch((err: unknown) => {\n this.logger.warn({ err, instanceId }, \"failed to renew workflow lock\");\n });\n }, WORKFLOW_LOCK_RENEW_MS);\n\n try {\n let instance = (\n await this.dbConn\n .select()\n .from(workflowInstances)\n .where(eq(workflowInstances.id, instanceId))\n .limit(1)\n )[0];\n if (!instance) {\n const rows = await this.dbConn\n .insert(workflowInstances)\n .values({\n id: instanceId,\n projectId: payload.projectId,\n name: name,\n status: \"pending\",\n input: payload.input || {},\n })\n .returning();\n instance = rows[0]!;\n }\n\n if (instance.status !== \"pending\") {\n this.logger.info({ instanceId }, \"Workflow is not pending, skipping\");\n return;\n }\n\n await this.dbConn\n .update(workflowInstances)\n .set({ status: \"running\" })\n .where(eq(workflowInstances.id, instanceId));\n\n // Load existing steps\n const existingSteps = await this.dbConn\n .select()\n .from(workflowSteps)\n .where(eq(workflowSteps.instanceId, instanceId));\n const stepOutputMap = new Map<string, any>();\n for (const s of existingSteps) {\n stepOutputMap.set(s.stepIndex, s.output);\n }\n\n let currentStepIndex = 0;\n\n const stepProxy: WorkflowStepContext = {\n notify: async (args) => {\n const stepId = String(currentStepIndex++);\n if (stepOutputMap.has(stepId)) return stepOutputMap.get(stepId);\n\n // The step payload is the same shape as notify(), but the wire event\n // is not — translate rather than spread.\n const requested = buildStepNotifyPayload(\n args,\n instance!.input,\n payload.projectId,\n `wf-${instanceId}-${stepId}`,\n );\n\n const result = await new Promise<{ messageId: string; notificationId: string }>(\n (resolve, reject) => {\n this.eventBuffer.push({\n producer: this.notificationProducer,\n event: buildStreamEvent(\n \"notification.requested\",\n requested as unknown as Record<string, unknown>,\n \"workflow\",\n event.metadata.traceId,\n ),\n resolve: resolve as any,\n reject,\n });\n },\n );\n\n const output = {\n success: true,\n messageId: result.messageId,\n notificationId: result.notificationId,\n };\n\n await this.dbConn.insert(workflowSteps).values({\n instanceId: instance!.id,\n projectId: payload.projectId,\n stepIndex: stepId,\n action: \"notify\",\n output,\n });\n\n return output;\n },\n wait: async (duration) => {\n const stepId = String(currentStepIndex++);\n if (stepOutputMap.has(stepId)) return;\n\n // Simple duration parse (e.g. '2h' -> ms)\n let ms = 0;\n if (duration.endsWith(\"d\")) ms = parseInt(duration) * 24 * 60 * 60 * 1000;\n else if (duration.endsWith(\"h\")) ms = parseInt(duration) * 60 * 60 * 1000;\n else if (duration.endsWith(\"m\")) ms = parseInt(duration) * 60 * 1000;\n else if (duration.endsWith(\"s\")) ms = parseInt(duration) * 1000;\n else throw new Error(`Invalid wait duration: ${duration}`);\n\n const resumeAt = Date.now() + ms;\n\n // Persist the wake-up signal before committing the suspended state.\n // If the process dies after this point the source event is retried;\n // if it dies after the database write, the timer is already durable.\n await this.redisCli.zadd(\n \"notif:workflow:timers\",\n resumeAt,\n JSON.stringify({\n instanceId: instance!.id,\n name: instance!.name,\n projectId: payload.projectId,\n input: instance!.input,\n }),\n );\n\n await this.dbConn.insert(workflowSteps).values({\n instanceId: instance!.id,\n projectId: payload.projectId,\n stepIndex: stepId,\n action: \"wait\",\n output: { scheduledAt: resumeAt },\n });\n\n throw new SuspendExecutionError(\"wait\", { duration });\n },\n waitForEvent: async (eventName, options) => {\n const stepId = String(currentStepIndex++);\n if (stepOutputMap.has(stepId)) {\n const out = stepOutputMap.get(stepId);\n if (out && typeof out === \"object\" && (out as any).timedOut === true) {\n return null;\n }\n return out;\n }\n\n options = options || {};\n options.timeout = options.timeout || \"24h\";\n options.match = options.match || {};\n\n let ms = 0;\n if (options.timeout.endsWith(\"d\")) ms = parseInt(options.timeout) * 24 * 60 * 60 * 1000;\n else if (options.timeout.endsWith(\"h\")) ms = parseInt(options.timeout) * 60 * 60 * 1000;\n else if (options.timeout.endsWith(\"m\")) ms = parseInt(options.timeout) * 60 * 1000;\n else if (options.timeout.endsWith(\"s\")) ms = parseInt(options.timeout) * 1000;\n else throw new Error(`Invalid waitForEvent timeout: ${options.timeout}`);\n\n const resumeAt = Date.now() + ms;\n\n await this.redisCli.zadd(\n \"notif:workflow:timers\",\n resumeAt,\n JSON.stringify({\n instanceId: instance!.id,\n name: instance!.name,\n projectId: payload.projectId,\n input: instance!.input,\n isEventTimeout: true,\n eventName,\n stepId,\n }),\n );\n\n // Register waiter\n await this.dbConn.insert(workflowWaiters).values({\n instanceId: instance!.id,\n projectId: payload.projectId,\n eventName: eventName,\n matchCriteria: options.match,\n expiresAt: new Date(resumeAt),\n });\n\n // Register step as pending event\n await this.dbConn.insert(workflowSteps).values({\n instanceId: instance!.id,\n projectId: payload.projectId,\n stepIndex: stepId,\n action: \"waitForEvent\",\n output: null, // this will be updated by event worker or timeout\n });\n\n throw new SuspendExecutionError(\"waitForEvent\", { eventName });\n },\n run: async (stepName, fn) => {\n const stepId = String(currentStepIndex++);\n if (stepOutputMap.has(stepId)) return stepOutputMap.get(stepId);\n\n const result = await fn();\n await this.dbConn.insert(workflowSteps).values({\n instanceId: instance!.id,\n projectId: payload.projectId,\n stepIndex: stepId,\n action: \"run\",\n output: result,\n });\n return result;\n },\n };\n\n const ctx: WorkflowContext = {\n step: stepProxy,\n event: (instance!.input as any) || { user: { id: \"unknown\" } },\n };\n\n try {\n await handler(ctx);\n // If we reach here, workflow completed\n await this.dbConn\n .update(workflowInstances)\n .set({ status: \"completed\" })\n .where(eq(workflowInstances.id, instance!.id));\n this.logger.info({ instanceId: instance!.id }, \"Workflow completed successfully\");\n } catch (err: any) {\n if (err instanceof SuspendExecutionError || err.name === \"SuspendExecutionError\") {\n await this.dbConn\n .update(workflowInstances)\n .set({ status: \"pending\" })\n .where(eq(workflowInstances.id, instance!.id));\n this.logger.info({ instanceId: instance!.id, reason: err.reason }, \"Workflow suspended\");\n } else {\n this.logger.error({ err, instanceId: instance!.id }, \"Workflow failed\");\n await this.dbConn\n .update(workflowInstances)\n .set({ status: \"failed\" })\n .where(eq(workflowInstances.id, instance!.id));\n }\n }\n } finally {\n clearInterval(renewTimer);\n // Compare-and-delete: never release a lock a later process re-acquired.\n await this.redisCli.eval(LUA_RELEASE_LOCK, 1, lockKey, lockToken);\n }\n\n await Promise.all(publishPromises);\n }\n}\n\nexport function __injectForTests(r: any, d: any, wp: any, np: any) {\n redis = r;\n db = d;\n workflowProducer = wp;\n notificationProducer = np;\n}\n\nexport async function startWorkflowWorker() {\n logger = createLogger({ name: \"workflow-worker\", level: config.LOG_LEVEL });\n redis = new RedisClient({ url: config.REDIS_URL, name: \"workflow\", logger });\n const dbData = createDatabase({ url: config.DATABASE_URL, applicationName: \"workflow\", logger });\n sql = dbData.sql;\n db = dbData.db;\n notificationProducer = new StreamProducer({\n redis: redis.native,\n stream: STREAMS.INBOUND_NORMAL,\n logger,\n });\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.WORKFLOW_INBOUND as StreamName,\n group: CONSUMER_GROUPS.WORKFLOW as any,\n consumer: `workflow-${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.WORKFLOW_INBOUND as StreamName,\n group: CONSUMER_GROUPS.WORKFLOW as any,\n consumer: `workflow-${process.pid}`,\n logger,\n });\n\n worker = new WorkflowWorker({\n consumer,\n pendingScanner,\n logger,\n concurrency: config.WORKER_CONCURRENCY,\n redis: redis.native,\n db,\n workflowProducer,\n notificationProducer,\n });\n\n // Polling loop for timers\n pollInterval = setInterval(() => {\n void (async () => {\n try {\n const now = Date.now();\n const tasks = (await redis.native.eval(\n LUA_SCHEDULER_POLL,\n 1,\n \"notif:workflow:timers\",\n now,\n 100,\n WORKFLOW_TIMER_VISIBILITY_MS,\n )) as string[];\n\n for (const taskStr of tasks) {\n const task = JSON.parse(taskStr);\n\n // Check if workflow is already completed/failed\n const inst = (\n await db\n .select()\n .from(workflowInstances)\n .where(eq(workflowInstances.id, task.instanceId))\n .limit(1)\n )[0];\n if (!inst || inst.status === \"completed\" || inst.status === \"failed\") {\n await redis.native.zrem(\"notif:workflow:timers\", taskStr);\n continue;\n }\n // A timer can be observed while the worker is still persisting the\n // corresponding suspension. Leave the claimed member in place; its\n // visibility timeout will make it eligible once the instance becomes\n // pending instead of losing the wake-up signal.\n if (inst.status === \"running\") continue;\n\n if (task.isEventTimeout) {\n // It's a timeout for waitForEvent. Clean up only this step's waiter\n const deleted = await db\n .delete(workflowWaiters)\n .where(\n and(\n eq(workflowWaiters.instanceId, task.instanceId),\n eq(workflowWaiters.eventName, task.eventName),\n ),\n )\n .returning();\n\n // If the waiter was already deleted (by EventWorker when event arrived before timeout),\n // this timer is stale. Clean up from Redis and skip re-resuming the workflow.\n if (deleted.length === 0) {\n await redis.native.zrem(\"notif:workflow:timers\", taskStr);\n continue;\n }\n\n // Record timedOut state on the pending waitForEvent step\n const steps = await db\n .select()\n .from(workflowSteps)\n .where(\n and(\n eq(workflowSteps.instanceId, task.instanceId),\n eq(workflowSteps.action, \"waitForEvent\"),\n ),\n );\n const pendingStep = steps.find((s: any) => s.output === null);\n if (pendingStep) {\n await db\n .update(workflowSteps)\n .set({ output: { timedOut: true } })\n .where(eq(workflowSteps.id, pendingStep.id));\n }\n }\n\n await workflowProducer.publish(\n buildStreamEvent(\"workflow.resumed\", task, \"scheduler\", undefined),\n );\n await redis.native.zrem(\"notif:workflow:timers\", taskStr);\n logger.info({ instanceId: task.instanceId }, \"Workflow resumed from timer\");\n }\n } catch (err) {\n logger.error({ err }, \"error in workflow polling loop\");\n }\n })();\n }, 5000);\n\n reaperInterval = setInterval(() => {\n void (async () => {\n try {\n const runningInstances = await db\n .select({ id: workflowInstances.id })\n .from(workflowInstances)\n .where(eq(workflowInstances.status, \"running\"));\n for (const inst of runningInstances) {\n const lockKey = `lock:workflow:${inst.id}`;\n const hasLock = await redis.native.exists(lockKey);\n if (!hasLock) {\n await db\n .update(workflowInstances)\n .set({ status: \"pending\" })\n .where(eq(workflowInstances.id, inst.id));\n logger.info({ instanceId: inst.id }, \"Reaped stuck workflow instance (lock expired)\");\n }\n }\n } catch (err) {\n logger.error({ err }, \"error in stuck workflow reaper\");\n }\n })();\n }, 60000);\n\n healthInterval = startHealthReporter(\"workflow\", worker, redis, logger);\n\n logger.info(\"workflow worker starting\");\n await worker.start();\n}\n\nexport async function stopWorkflowWorker(): Promise<void> {\n logger?.info(\"shutdown initiated\");\n if (healthInterval) clearInterval(healthInterval);\n if (pollInterval) clearInterval(pollInterval);\n if (reaperInterval) clearInterval(reaperInterval);\n if (worker) await worker.stop();\n if (sql) await sql.end();\n if (redis) await redis.disconnect();\n logger?.info(\"workflow worker stopped\");\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"main-BgmT1MPe.mjs","names":["sql"],"sources":["../src/services/workflow/main.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { loadEnv, readBaseConfig } from \"@/index.js\";\nimport { createLogger } from \"@/index.js\";\nimport { RedisClient, type Redis } 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 {\n workflowInstances,\n workflowSteps,\n workflowWaiters,\n workflowDefinitions,\n} from \"@/db/schema.js\";\nimport { eq, and } from \"drizzle-orm\";\nimport {\n workflowRegistry,\n SuspendExecutionError,\n buildStepNotifyPayload,\n type WorkflowContext,\n type WorkflowStepContext,\n} from \"@/workflows/index.js\";\nimport {\n type WorkerOptions,\n LUA_SCHEDULER_POLL,\n LUA_RELEASE_LOCK,\n LUA_RENEW_LOCK,\n} from \"@/shared/index.js\";\nimport { startHealthReporter } from \"@/workers/index.js\";\n\n/** How long a workflow instance lock is held before it self-expires. */\nconst WORKFLOW_LOCK_TTL_SECONDS = 60;\n/** Renew the lock well inside its TTL so long-running handlers keep it. */\nconst WORKFLOW_LOCK_RENEW_MS = (WORKFLOW_LOCK_TTL_SECONDS / 3) * 1000;\n/** How long a claimed workflow timer stays invisible to other pollers. */\nconst WORKFLOW_TIMER_VISIBILITY_MS = 60_000;\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;\nlet pollInterval: NodeJS.Timeout | null = null;\nlet reaperInterval: NodeJS.Timeout | null = null;\n\nlet notificationProducer: StreamProducer;\nlet workflowProducer: StreamProducer;\n\nexport interface WorkflowWorkerOptions extends WorkerOptions {\n redis: Redis;\n db: any;\n workflowProducer: any;\n notificationProducer: any;\n}\n\nexport class WorkflowWorker extends BaseWorker {\n private readonly redisCli: Redis;\n private readonly dbConn: any;\n private readonly workflowProducer: any;\n private readonly notificationProducer: any;\n\n private eventBuffer: {\n producer: any;\n event: any;\n resolve: (result?: { messageId: string; notificationId: string }) => void;\n reject: (err: any) => void;\n }[] = [];\n private flushTimer: NodeJS.Timeout | null = null;\n\n constructor(options: WorkflowWorkerOptions) {\n super(options);\n this.redisCli = options.redis;\n this.dbConn = options.db;\n this.workflowProducer = options.workflowProducer;\n this.notificationProducer = options.notificationProducer;\n\n this.flushTimer = setInterval(() => void this.flushWorkerBuffers(), 100);\n }\n\n override async stop(): Promise<void> {\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n await this.flushWorkerBuffers();\n await super.stop();\n }\n\n private async flushWorkerBuffers(): Promise<void> {\n if (this.eventBuffer.length === 0) return;\n\n const events = this.eventBuffer;\n this.eventBuffer = [];\n\n try {\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\n for (const [producer, batch] of byProducer) {\n const { messageIds, eventIds } = await producer.publishBatch(batch.map((b) => b.event));\n for (let i = 0; i < batch.length; i++) {\n const mId = messageIds[i];\n const eId = eventIds[i];\n if (mId && eId) batch[i]!.resolve({ messageId: mId, notificationId: eId });\n }\n }\n } catch (err: any) {\n this.logger.error({ err }, \"failed to flush workflow worker buffer\");\n for (const e of events) e.reject(err);\n }\n }\n\n async process(message: StreamMessage): Promise<void> {\n const { event } = message;\n const publishPromises: Promise<void>[] = [];\n\n if (event.type !== \"workflow.triggered\" && event.type !== \"workflow.resumed\") {\n return;\n }\n\n const payload = event.payload as any;\n const name = payload.name;\n const instanceId = payload.instanceId;\n\n if (!name || !instanceId || !payload.projectId) {\n this.logger.warn(\"Missing name, instanceId, or projectId in workflow event\");\n return;\n }\n\n let handler = workflowRegistry.get(name);\n if (!handler) {\n // Fallback: check dynamic JSON workflows\n const defRows = await this.dbConn\n .select()\n .from(workflowDefinitions)\n .where(\n and(\n eq(workflowDefinitions.projectId, payload.projectId),\n eq(workflowDefinitions.name, name),\n ),\n )\n .limit(1);\n\n if (defRows.length === 0) {\n this.logger.warn(\n { name, projectId: payload.projectId },\n \"No handler or dynamic definition found for workflow\",\n );\n return;\n }\n\n const def = defRows[0];\n handler = async ({ step }) => {\n const steps = def.steps as any[];\n for (const stepDef of steps) {\n if (stepDef.action === \"notify\") {\n // A payload target wins; naming none inherits the instance user.\n await step.notify(stepDef.payload);\n } else if (stepDef.action === \"wait\") {\n await step.wait(stepDef.duration);\n } else if (stepDef.action === \"waitForEvent\") {\n await step.waitForEvent(stepDef.event, stepDef.options);\n } else {\n this.logger.warn(\n { action: (stepDef as any)?.action, name },\n \"unknown workflow step action\",\n );\n }\n }\n };\n }\n\n const lockKey = `lock:workflow:${instanceId}`;\n const lockToken = randomUUID();\n const acquired = await this.redisCli.set(\n lockKey,\n lockToken,\n \"EX\",\n WORKFLOW_LOCK_TTL_SECONDS,\n \"NX\",\n );\n if (!acquired) {\n this.logger.info({ instanceId }, \"Workflow is locked by another process, skipping\");\n return;\n }\n\n // Keep the lock alive while the handler runs; without this a handler that\n // outlives the TTL lets a second resume execute the same steps in parallel.\n const renewTimer = setInterval(() => {\n void this.redisCli\n .eval(LUA_RENEW_LOCK, 1, lockKey, lockToken, String(WORKFLOW_LOCK_TTL_SECONDS))\n .catch((err: unknown) => {\n this.logger.warn({ err, instanceId }, \"failed to renew workflow lock\");\n });\n }, WORKFLOW_LOCK_RENEW_MS);\n\n try {\n let instance = (\n await this.dbConn\n .select()\n .from(workflowInstances)\n .where(eq(workflowInstances.id, instanceId))\n .limit(1)\n )[0];\n if (!instance) {\n const rows = await this.dbConn\n .insert(workflowInstances)\n .values({\n id: instanceId,\n projectId: payload.projectId,\n name: name,\n status: \"pending\",\n input: payload.input || {},\n })\n .returning();\n instance = rows[0]!;\n }\n\n if (instance.status !== \"pending\") {\n this.logger.info({ instanceId }, \"Workflow is not pending, skipping\");\n return;\n }\n\n await this.dbConn\n .update(workflowInstances)\n .set({ status: \"running\" })\n .where(eq(workflowInstances.id, instanceId));\n\n // Load existing steps\n const existingSteps = await this.dbConn\n .select()\n .from(workflowSteps)\n .where(eq(workflowSteps.instanceId, instanceId));\n const stepOutputMap = new Map<string, any>();\n for (const s of existingSteps) {\n stepOutputMap.set(s.stepIndex, s.output);\n }\n\n let currentStepIndex = 0;\n\n const stepProxy: WorkflowStepContext = {\n notify: async (args) => {\n const stepId = String(currentStepIndex++);\n if (stepOutputMap.has(stepId)) return stepOutputMap.get(stepId);\n\n // The step payload is the same shape as notify(), but the wire event\n // is not — translate rather than spread.\n const requested = buildStepNotifyPayload(\n args,\n instance!.input,\n payload.projectId,\n `wf-${instanceId}-${stepId}`,\n );\n\n const result = await new Promise<{ messageId: string; notificationId: string }>(\n (resolve, reject) => {\n this.eventBuffer.push({\n producer: this.notificationProducer,\n event: buildStreamEvent(\n \"notification.requested\",\n requested as unknown as Record<string, unknown>,\n \"workflow\",\n event.metadata.traceId,\n ),\n resolve: resolve as any,\n reject,\n });\n },\n );\n\n const output = {\n success: true,\n messageId: result.messageId,\n notificationId: result.notificationId,\n };\n\n await this.dbConn.insert(workflowSteps).values({\n instanceId: instance!.id,\n projectId: payload.projectId,\n stepIndex: stepId,\n action: \"notify\",\n output,\n });\n\n return output;\n },\n wait: async (duration) => {\n const stepId = String(currentStepIndex++);\n if (stepOutputMap.has(stepId)) return;\n\n // Simple duration parse (e.g. '2h' -> ms)\n let ms = 0;\n if (duration.endsWith(\"d\")) ms = parseInt(duration) * 24 * 60 * 60 * 1000;\n else if (duration.endsWith(\"h\")) ms = parseInt(duration) * 60 * 60 * 1000;\n else if (duration.endsWith(\"m\")) ms = parseInt(duration) * 60 * 1000;\n else if (duration.endsWith(\"s\")) ms = parseInt(duration) * 1000;\n else throw new Error(`Invalid wait duration: ${duration}`);\n\n const resumeAt = Date.now() + ms;\n\n // Persist the wake-up signal before committing the suspended state.\n // If the process dies after this point the source event is retried;\n // if it dies after the database write, the timer is already durable.\n await this.redisCli.zadd(\n \"notif:workflow:timers\",\n resumeAt,\n JSON.stringify({\n instanceId: instance!.id,\n name: instance!.name,\n projectId: payload.projectId,\n input: instance!.input,\n }),\n );\n\n await this.dbConn.insert(workflowSteps).values({\n instanceId: instance!.id,\n projectId: payload.projectId,\n stepIndex: stepId,\n action: \"wait\",\n output: { scheduledAt: resumeAt },\n });\n\n throw new SuspendExecutionError(\"wait\", { duration });\n },\n waitForEvent: async (eventName, options) => {\n const stepId = String(currentStepIndex++);\n if (stepOutputMap.has(stepId)) {\n const out = stepOutputMap.get(stepId);\n if (out && typeof out === \"object\" && (out as any).timedOut === true) {\n return null;\n }\n return out;\n }\n\n options = options || {};\n options.timeout = options.timeout || \"24h\";\n options.match = options.match || {};\n\n let ms = 0;\n if (options.timeout.endsWith(\"d\")) ms = parseInt(options.timeout) * 24 * 60 * 60 * 1000;\n else if (options.timeout.endsWith(\"h\")) ms = parseInt(options.timeout) * 60 * 60 * 1000;\n else if (options.timeout.endsWith(\"m\")) ms = parseInt(options.timeout) * 60 * 1000;\n else if (options.timeout.endsWith(\"s\")) ms = parseInt(options.timeout) * 1000;\n else throw new Error(`Invalid waitForEvent timeout: ${options.timeout}`);\n\n const resumeAt = Date.now() + ms;\n\n await this.redisCli.zadd(\n \"notif:workflow:timers\",\n resumeAt,\n JSON.stringify({\n instanceId: instance!.id,\n name: instance!.name,\n projectId: payload.projectId,\n input: instance!.input,\n isEventTimeout: true,\n eventName,\n stepId,\n }),\n );\n\n // Register waiter\n await this.dbConn.insert(workflowWaiters).values({\n instanceId: instance!.id,\n projectId: payload.projectId,\n eventName: eventName,\n matchCriteria: options.match,\n expiresAt: new Date(resumeAt),\n });\n\n // Register step as pending event\n await this.dbConn.insert(workflowSteps).values({\n instanceId: instance!.id,\n projectId: payload.projectId,\n stepIndex: stepId,\n action: \"waitForEvent\",\n output: null, // this will be updated by event worker or timeout\n });\n\n throw new SuspendExecutionError(\"waitForEvent\", { eventName });\n },\n run: async (stepName, fn) => {\n const stepId = String(currentStepIndex++);\n if (stepOutputMap.has(stepId)) return stepOutputMap.get(stepId);\n\n const result = await fn();\n await this.dbConn.insert(workflowSteps).values({\n instanceId: instance!.id,\n projectId: payload.projectId,\n stepIndex: stepId,\n action: \"run\",\n output: result,\n });\n return result;\n },\n };\n\n const ctx: WorkflowContext = {\n step: stepProxy,\n event: (instance!.input as any) || { user: { id: \"unknown\" } },\n };\n\n try {\n await handler(ctx);\n // If we reach here, workflow completed\n await this.dbConn\n .update(workflowInstances)\n .set({ status: \"completed\" })\n .where(eq(workflowInstances.id, instance!.id));\n this.logger.info({ instanceId: instance!.id }, \"Workflow completed successfully\");\n } catch (err: any) {\n if (err instanceof SuspendExecutionError || err.name === \"SuspendExecutionError\") {\n await this.dbConn\n .update(workflowInstances)\n .set({ status: \"pending\" })\n .where(eq(workflowInstances.id, instance!.id));\n this.logger.info({ instanceId: instance!.id, reason: err.reason }, \"Workflow suspended\");\n } else {\n this.logger.error({ err, instanceId: instance!.id }, \"Workflow failed\");\n await this.dbConn\n .update(workflowInstances)\n .set({ status: \"failed\" })\n .where(eq(workflowInstances.id, instance!.id));\n }\n }\n } finally {\n clearInterval(renewTimer);\n // Compare-and-delete: never release a lock a later process re-acquired.\n await this.redisCli.eval(LUA_RELEASE_LOCK, 1, lockKey, lockToken);\n }\n\n await Promise.all(publishPromises);\n }\n}\n\nexport function __injectForTests(r: any, d: any, wp: any, np: any) {\n redis = r;\n db = d;\n workflowProducer = wp;\n notificationProducer = np;\n}\n\nexport async function startWorkflowWorker() {\n logger = createLogger({ name: \"workflow-worker\", level: config.LOG_LEVEL });\n redis = new RedisClient({ url: config.REDIS_URL, name: \"workflow\", logger });\n const dbData = createDatabase({ url: config.DATABASE_URL, applicationName: \"workflow\", logger });\n sql = dbData.sql;\n db = dbData.db;\n notificationProducer = new StreamProducer({\n redis: redis.native,\n stream: STREAMS.INBOUND_NORMAL,\n logger,\n });\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.WORKFLOW_INBOUND as StreamName,\n group: CONSUMER_GROUPS.WORKFLOW as any,\n consumer: `workflow-${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.WORKFLOW_INBOUND as StreamName,\n group: CONSUMER_GROUPS.WORKFLOW as any,\n consumer: `workflow-${process.pid}`,\n logger,\n });\n\n worker = new WorkflowWorker({\n consumer,\n pendingScanner,\n logger,\n concurrency: config.WORKER_CONCURRENCY,\n redis: redis.native,\n db,\n workflowProducer,\n notificationProducer,\n });\n\n // Polling loop for timers\n pollInterval = setInterval(() => {\n void (async () => {\n try {\n const now = Date.now();\n const tasks = (await redis.native.eval(\n LUA_SCHEDULER_POLL,\n 1,\n \"notif:workflow:timers\",\n now,\n 100,\n WORKFLOW_TIMER_VISIBILITY_MS,\n )) as string[];\n\n for (const taskStr of tasks) {\n const task = JSON.parse(taskStr);\n\n // Check if workflow is already completed/failed\n const inst = (\n await db\n .select()\n .from(workflowInstances)\n .where(eq(workflowInstances.id, task.instanceId))\n .limit(1)\n )[0];\n if (!inst || inst.status === \"completed\" || inst.status === \"failed\") {\n await redis.native.zrem(\"notif:workflow:timers\", taskStr);\n continue;\n }\n // A timer can be observed while the worker is still persisting the\n // corresponding suspension. Leave the claimed member in place; its\n // visibility timeout will make it eligible once the instance becomes\n // pending instead of losing the wake-up signal.\n if (inst.status === \"running\") continue;\n\n if (task.isEventTimeout) {\n // It's a timeout for waitForEvent. Clean up only this step's waiter\n const deleted = await db\n .delete(workflowWaiters)\n .where(\n and(\n eq(workflowWaiters.instanceId, task.instanceId),\n eq(workflowWaiters.eventName, task.eventName),\n ),\n )\n .returning();\n\n // If the waiter was already deleted (by EventWorker when event arrived before timeout),\n // this timer is stale. Clean up from Redis and skip re-resuming the workflow.\n if (deleted.length === 0) {\n await redis.native.zrem(\"notif:workflow:timers\", taskStr);\n continue;\n }\n\n // Record timedOut state on the pending waitForEvent step\n const steps = await db\n .select()\n .from(workflowSteps)\n .where(\n and(\n eq(workflowSteps.instanceId, task.instanceId),\n eq(workflowSteps.action, \"waitForEvent\"),\n ),\n );\n const pendingStep = steps.find((s: any) => s.output === null);\n if (pendingStep) {\n await db\n .update(workflowSteps)\n .set({ output: { timedOut: true } })\n .where(eq(workflowSteps.id, pendingStep.id));\n }\n }\n\n await workflowProducer.publish(\n buildStreamEvent(\"workflow.resumed\", task, \"scheduler\", undefined),\n );\n await redis.native.zrem(\"notif:workflow:timers\", taskStr);\n logger.info({ instanceId: task.instanceId }, \"Workflow resumed from timer\");\n }\n } catch (err) {\n logger.error({ err }, \"error in workflow polling loop\");\n }\n })();\n }, 5000);\n\n reaperInterval = setInterval(() => {\n void (async () => {\n try {\n const runningInstances = await db\n .select({ id: workflowInstances.id })\n .from(workflowInstances)\n .where(eq(workflowInstances.status, \"running\"));\n for (const inst of runningInstances) {\n const lockKey = `lock:workflow:${inst.id}`;\n const hasLock = await redis.native.exists(lockKey);\n if (!hasLock) {\n await db\n .update(workflowInstances)\n .set({ status: \"pending\" })\n .where(eq(workflowInstances.id, inst.id));\n logger.info({ instanceId: inst.id }, \"Reaped stuck workflow instance (lock expired)\");\n }\n }\n } catch (err) {\n logger.error({ err }, \"error in stuck workflow reaper\");\n }\n })();\n }, 60000);\n\n healthInterval = startHealthReporter(\"workflow\", worker, redis, logger);\n\n logger.info(\"workflow worker starting\");\n await worker.start();\n}\n\nexport async function stopWorkflowWorker(): Promise<void> {\n logger?.info(\"shutdown initiated\");\n if (healthInterval) clearInterval(healthInterval);\n if (pollInterval) clearInterval(pollInterval);\n if (reaperInterval) clearInterval(reaperInterval);\n if (worker) await worker.stop();\n if (sql) await sql.end();\n if (redis) await redis.disconnect();\n logger?.info(\"workflow worker stopped\");\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $t as ENRICHED_STREAMS, A as RedisClient, E as TemplateRepository, G as getPriorityBucket, J as globalEmitter, K as normaliseTarget, L as DataLoader, M as UserThrottle, Qt as CONSUMER_GROUPS, S as ContactRepository, X as StreamConsumer, Y as PendingMessageScanner, Z as StreamProducer, Zt as buildStreamEvent, at as createDatabase, b as renderWithTemplate, c as BaseWorker, cn as registry, d as buildUnsubscribeHeaders, dt as scheduledPayloads, ft as suppressions, g as TemplateCache, gn as loadEnv, it as IdempotencyGuard, j as ProjectSettingsCache, nn as PUBSUB_CHANNELS, rn as STREAMS, tt as createLogger, u as startHealthReporter, vn as readBaseConfig, w as ProjectRepository } from "./src-
|
|
1
|
+
import { $t as ENRICHED_STREAMS, A as RedisClient, E as TemplateRepository, G as getPriorityBucket, J as globalEmitter, K as normaliseTarget, L as DataLoader, M as UserThrottle, Qt as CONSUMER_GROUPS, S as ContactRepository, X as StreamConsumer, Y as PendingMessageScanner, Z as StreamProducer, Zt as buildStreamEvent, at as createDatabase, b as renderWithTemplate, c as BaseWorker, cn as registry, d as buildUnsubscribeHeaders, dt as scheduledPayloads, ft as suppressions, g as TemplateCache, gn as loadEnv, it as IdempotencyGuard, j as ProjectSettingsCache, nn as PUBSUB_CHANNELS, rn as STREAMS, tt as createLogger, u as startHealthReporter, vn as readBaseConfig, w as ProjectRepository } from "./src-QiTu5e_a.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { and, eq } from "drizzle-orm";
|
|
4
4
|
//#region src/services/engine/main.ts
|
|
@@ -568,4 +568,4 @@ async function stopEngineWorker() {
|
|
|
568
568
|
//#endregion
|
|
569
569
|
export { startEngineWorker, stopEngineWorker };
|
|
570
570
|
|
|
571
|
-
//# sourceMappingURL=main-
|
|
571
|
+
//# sourceMappingURL=main-BiFiOX9D.mjs.map
|