notifkit 0.1.3 → 0.1.4

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.
Files changed (95) hide show
  1. package/README.md +95 -79
  2. package/dist/index.d.mts +199 -135
  3. package/dist/index.d.mts.map +1 -1
  4. package/dist/index.mjs +1 -1
  5. package/dist/index.mjs.map +1 -1
  6. package/dist/{main-DtHWhueo.mjs → main-40zwq6b0.mjs} +28 -3
  7. package/dist/{main-DtHWhueo.mjs.map → main-40zwq6b0.mjs.map} +1 -1
  8. package/dist/{main-DyfbnJc3.mjs → main-BFre2-HQ.mjs} +2 -2
  9. package/dist/{main-DyfbnJc3.mjs.map → main-BFre2-HQ.mjs.map} +1 -1
  10. package/dist/{main-CAH0_Q6d.mjs → main-BNJtzY61.mjs} +3 -3
  11. package/dist/main-BNJtzY61.mjs.map +1 -0
  12. package/dist/{main-B561M1d3.mjs → main-BOPMYqsW.mjs} +2 -2
  13. package/dist/{main-B561M1d3.mjs.map → main-BOPMYqsW.mjs.map} +1 -1
  14. package/dist/{main-CCfc45ev.mjs → main-CiigNpsP.mjs} +7 -4
  15. package/dist/main-CiigNpsP.mjs.map +1 -0
  16. package/dist/{main-Ce9dcrsg.mjs → main-DeNFQ-UL.mjs} +6 -3
  17. package/dist/{main-Ce9dcrsg.mjs.map → main-DeNFQ-UL.mjs.map} +1 -1
  18. package/dist/{main-B-jwm8ED.mjs → main-DmCPcxOc.mjs} +2 -2
  19. package/dist/{main-B-jwm8ED.mjs.map → main-DmCPcxOc.mjs.map} +1 -1
  20. package/dist/{main-C45e7grq.mjs → main-DvgJSm11.mjs} +2 -2
  21. package/dist/{main-C45e7grq.mjs.map → main-DvgJSm11.mjs.map} +1 -1
  22. package/dist/{src-C-PfEDMY.mjs → src-vG79L-8m.mjs} +57 -26
  23. package/dist/src-vG79L-8m.mjs.map +1 -0
  24. package/drizzle/0002_wide_colleen_wing.sql +2 -0
  25. package/drizzle/0003_skinny_daimon_hellstrom.sql +1 -0
  26. package/drizzle/0004_pretty_bruce_banner.sql +1 -0
  27. package/drizzle/meta/0002_snapshot.json +1460 -0
  28. package/drizzle/meta/0003_snapshot.json +1460 -0
  29. package/drizzle/meta/0004_snapshot.json +1470 -0
  30. package/drizzle/meta/_journal.json +21 -0
  31. package/package.json +2 -1
  32. package/src/client.ts +412 -0
  33. package/src/config/index.ts +107 -0
  34. package/src/contracts/common.ts +28 -0
  35. package/src/contracts/envelope.ts +31 -0
  36. package/src/contracts/events/notification-ai-pending.ts +18 -0
  37. package/src/contracts/events/notification-canceled.ts +7 -0
  38. package/src/contracts/events/notification-created.ts +14 -0
  39. package/src/contracts/events/notification-delivered.ts +17 -0
  40. package/src/contracts/events/notification-dispatched.ts +45 -0
  41. package/src/contracts/events/notification-enriched.ts +46 -0
  42. package/src/contracts/events/notification-failed.ts +19 -0
  43. package/src/contracts/events/notification-requested.ts +36 -0
  44. package/src/contracts/events/notification-scheduled.ts +9 -0
  45. package/src/contracts/events/notification-skipped.ts +9 -0
  46. package/src/contracts/helpers.ts +21 -0
  47. package/src/contracts/index.ts +46 -0
  48. package/src/contracts/metadata.ts +10 -0
  49. package/src/contracts/registry.ts +88 -0
  50. package/src/contracts/sdk.ts +242 -0
  51. package/src/contracts/streams.ts +62 -0
  52. package/src/db/index.ts +69 -0
  53. package/src/db/schema.ts +412 -0
  54. package/src/idempotency/index.ts +50 -0
  55. package/src/index.ts +19 -0
  56. package/src/logger/index.ts +60 -0
  57. package/src/metrics/index.ts +53 -0
  58. package/src/queue/index.ts +501 -0
  59. package/src/rate-limiter/index.ts +210 -0
  60. package/src/redis/index.ts +89 -0
  61. package/src/repositories/index.ts +1246 -0
  62. package/src/server.ts +277 -0
  63. package/src/services/ai/main.ts +404 -0
  64. package/src/services/api/handlers.ts +1734 -0
  65. package/src/services/api/http.ts +64 -0
  66. package/src/services/api/main.ts +693 -0
  67. package/src/services/api/router.ts +82 -0
  68. package/src/services/delivery/main.ts +842 -0
  69. package/src/services/delivery/throttle.ts +71 -0
  70. package/src/services/engine/main.ts +827 -0
  71. package/src/services/enricher/main.ts +594 -0
  72. package/src/services/events/main.ts +365 -0
  73. package/src/services/scheduler/main.ts +319 -0
  74. package/src/services/workflow/main.ts +627 -0
  75. package/src/shared/batch-processor.ts +67 -0
  76. package/src/shared/cache.ts +47 -0
  77. package/src/shared/circuit-breaker.ts +74 -0
  78. package/src/shared/dataloader.ts +41 -0
  79. package/src/shared/events.ts +3 -0
  80. package/src/shared/index.ts +39 -0
  81. package/src/shared/semaphore.ts +33 -0
  82. package/src/shared/utils.ts +64 -0
  83. package/src/templates/cache.ts +32 -0
  84. package/src/templates/index.ts +69 -0
  85. package/src/templates/render.ts +128 -0
  86. package/src/transport/index.ts +96 -0
  87. package/src/unsubscribe/index.ts +127 -0
  88. package/src/workers/health.ts +31 -0
  89. package/src/workers/index.ts +266 -0
  90. package/src/workflows/index.ts +2 -0
  91. package/src/workflows/registry.ts +21 -0
  92. package/src/workflows/sdk.ts +106 -0
  93. package/dist/main-CAH0_Q6d.mjs.map +0 -1
  94. package/dist/main-CCfc45ev.mjs.map +0 -1
  95. package/dist/src-C-PfEDMY.mjs.map +0 -1
@@ -1,4 +1,4 @@
1
- import { E as UserRepository, J as PendingMessageScanner, Jt as buildStreamEvent, S as PreferenceRepository, T as TemplateRepository, W as getPriorityBucket, X as StreamProducer, Y as StreamConsumer, Yt as CONSUMER_GROUPS, Zt as INBOUND_STREAMS, an as registry, c as BaseWorker, en as STREAMS, et as createLogger, g as TemplateCache, hn as readBaseConfig, it as createDatabase, k as RedisClient, pn as loadEnv, rt as IdempotencyGuard, u as startHealthReporter, x as ContactRepository } from "./src-C-PfEDMY.mjs";
1
+ import { E as UserRepository, J as PendingMessageScanner, Jt as buildStreamEvent, S as PreferenceRepository, T as TemplateRepository, W as getPriorityBucket, X as StreamProducer, Y as StreamConsumer, Yt as CONSUMER_GROUPS, Zt as INBOUND_STREAMS, an as registry, c as BaseWorker, en as STREAMS, et as createLogger, g as TemplateCache, hn as readBaseConfig, it as createDatabase, k as RedisClient, pn as loadEnv, rt as IdempotencyGuard, u as startHealthReporter, x as ContactRepository } from "./src-vG79L-8m.mjs";
2
2
  //#region src/services/enricher/main.ts
3
3
  loadEnv();
4
4
  const config = readBaseConfig();
@@ -285,8 +285,11 @@ var EnricherWorker = class extends BaseWorker {
285
285
  recipient: {
286
286
  id: profile.userId,
287
287
  email: channel === "email" ? destination ?? profile.email ?? void 0 : profile.email ?? void 0,
288
- phone: channel === "sms" ? destination : void 0,
288
+ phone: channel === "sms" || channel === "whatsapp" ? destination : void 0,
289
289
  webhook: channel === "webhook" ? destination : void 0,
290
+ telegram: channel === "telegram" ? destination : void 0,
291
+ discord: channel === "discord" ? destination : void 0,
292
+ slack: channel === "slack" ? destination : void 0,
290
293
  locale: profile.language ?? "en",
291
294
  timezone: profile.timezone ?? "UTC",
292
295
  preferences: {
@@ -427,4 +430,4 @@ async function stopEnricherWorker() {
427
430
  //#endregion
428
431
  export { startEnricherWorker, stopEnricherWorker };
429
432
 
430
- //# sourceMappingURL=main-Ce9dcrsg.mjs.map
433
+ //# sourceMappingURL=main-DeNFQ-UL.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"main-Ce9dcrsg.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\" ? destination : undefined,\n webhook: channel === \"webhook\" ? 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-DeNFQ-UL.mjs","names":[],"sources":["../src/services/enricher/main.ts"],"sourcesContent":["import { loadEnv, readBaseConfig } from \"@/index.js\";\nimport { createLogger } from \"@/index.js\";\nimport { RedisClient } from \"@/index.js\";\nimport {\n StreamConsumer,\n PendingMessageScanner,\n StreamProducer,\n type StreamMessage,\n} from \"@/index.js\";\nimport { BaseWorker } from \"@/index.js\";\nimport {\n STREAMS,\n INBOUND_STREAMS,\n CONSUMER_GROUPS,\n registry,\n buildStreamEvent,\n type NotificationCreatedPayload,\n type NotificationEnrichedPayload,\n} from \"@/index.js\";\nimport { type StreamName } from \"@/contracts/streams.js\";\nimport { IdempotencyGuard } from \"@/index.js\";\nimport { createDatabase } from \"@/db/index.js\";\nimport {\n UserRepository,\n PreferenceRepository,\n TemplateRepository,\n ContactRepository,\n} from \"@/index.js\";\nimport { TemplateCache } from \"@/templates/index.js\";\nimport { getPriorityBucket, type WorkerOptions } from \"@/shared/index.js\";\nimport { startHealthReporter } from \"@/workers/index.js\";\n\n// ─── Bootstrap ─────────────────────────────────────────────────────────────\n\nloadEnv();\nconst config = readBaseConfig();\n\nlet logger: ReturnType<typeof createLogger>;\nlet redis: RedisClient;\nlet sql: any;\nlet db: any;\n\nlet consumer: StreamConsumer;\nlet pendingScanner: PendingMessageScanner;\nlet worker: BaseWorker;\nlet healthInterval: NodeJS.Timeout | null = null;\n\nexport interface EnricherWorkerOptions extends WorkerOptions {\n producers: any;\n idempotency: any;\n userRepo: any;\n prefRepo: any;\n contactRepo: any;\n templateCache: TemplateCache;\n}\n\nexport class EnricherWorker extends BaseWorker {\n private readonly producers: any;\n private readonly idempotency: any;\n private readonly userRepo: any;\n private readonly prefRepo: any;\n private readonly contactRepo: any;\n private readonly templateCache: TemplateCache;\n\n private userBatch: {\n projectId: string;\n userId: string;\n resolve: (p: any) => void;\n reject: (e: any) => void;\n }[] = [];\n private batchTimer: NodeJS.Timeout | null = null;\n private eventBuffer: {\n producer: any;\n event: any;\n resolve: () => void;\n reject: (e: any) => void;\n }[] = [];\n private flushTimer: NodeJS.Timeout | null = null;\n\n private contactBatch: {\n projectId: string;\n userIds: string[];\n resolve: (c: Map<string, any[]>) => void;\n reject: (e: any) => void;\n }[] = [];\n private contactBatchTimer: NodeJS.Timeout | null = null;\n\n private async loadContacts(projectId: string, userIds: string[]): Promise<Map<string, any[]>> {\n return new Promise((resolve, reject) => {\n this.contactBatch.push({ projectId, userIds, resolve, reject });\n if (this.contactBatch.length >= 500) {\n if (this.contactBatchTimer) clearTimeout(this.contactBatchTimer);\n void this.flushContactBatch();\n } else if (!this.contactBatchTimer) {\n this.contactBatchTimer = setTimeout(() => void this.flushContactBatch(), 10);\n }\n });\n }\n\n private async flushContactBatch(): Promise<void> {\n const batch = this.contactBatch;\n this.contactBatch = [];\n this.contactBatchTimer = null;\n if (batch.length === 0) return;\n\n try {\n const byProject = new Map<string, typeof batch>();\n for (const b of batch) {\n if (!byProject.has(b.projectId)) byProject.set(b.projectId, []);\n byProject.get(b.projectId)!.push(b);\n }\n\n for (const [projectId, items] of byProject) {\n const userIds = Array.from(new Set(items.flatMap((i) => i.userIds)));\n const contactsMap = await this.contactRepo.findActiveByUserIds(projectId, userIds);\n for (const item of items) {\n item.resolve(contactsMap);\n }\n }\n } catch (err) {\n for (const b of batch) b.reject(err);\n }\n }\n\n constructor(options: EnricherWorkerOptions) {\n super(options);\n this.producers = options.producers;\n this.idempotency = options.idempotency;\n this.userRepo = options.userRepo;\n this.prefRepo = options.prefRepo;\n this.contactRepo = options.contactRepo;\n this.templateCache = options.templateCache;\n\n this.flushTimer = setInterval(() => void this.flushWorkerBuffers(), 100);\n }\n\n override async stop(): Promise<void> {\n if (this.batchTimer) {\n clearTimeout(this.batchTimer);\n this.batchTimer = null;\n }\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n if (this.contactBatchTimer) {\n clearTimeout(this.contactBatchTimer);\n this.contactBatchTimer = null;\n }\n await this.flushContactBatch();\n await this.flushUserBatch();\n await this.flushWorkerBuffers();\n await super.stop();\n }\n\n private async flushWorkerBuffers(): Promise<void> {\n if (this.eventBuffer.length === 0) return;\n const events = this.eventBuffer;\n this.eventBuffer = [];\n\n try {\n const byProducer = new Map<any, typeof events>();\n for (const e of events) {\n if (!byProducer.has(e.producer)) byProducer.set(e.producer, []);\n byProducer.get(e.producer)!.push(e);\n }\n for (const [producer, batch] of byProducer) {\n await producer.publishBatch(batch.map((b) => b.event));\n for (const b of batch) b.resolve();\n }\n } catch (err: any) {\n this.logger.error({ err }, \"failed to flush events in EnricherWorker\");\n for (const e of events) e.reject(err);\n }\n }\n\n private async loadUser(projectId: string, userId: string): Promise<any> {\n return new Promise((resolve, reject) => {\n this.userBatch.push({ projectId, userId, resolve, reject });\n if (this.userBatch.length >= 500) {\n if (this.batchTimer) clearTimeout(this.batchTimer);\n void this.flushUserBatch();\n } else if (!this.batchTimer) {\n this.batchTimer = setTimeout(() => {\n void this.flushUserBatch();\n }, 10);\n }\n });\n }\n\n private async flushUserBatch() {\n const batch = this.userBatch;\n this.userBatch = [];\n this.batchTimer = null;\n\n const byProject = new Map<string, typeof batch>();\n for (const b of batch) {\n if (!byProject.has(b.projectId)) byProject.set(b.projectId, []);\n byProject.get(b.projectId)!.push(b);\n }\n\n for (const [projectId, reqs] of byProject.entries()) {\n try {\n const uniqueIds = Array.from(new Set(reqs.map((r) => r.userId)));\n const profiles = await this.userRepo.findRecordsByIds(projectId, uniqueIds);\n const profileMap = new Map(profiles.map((p: any) => [p.userId, p]));\n\n for (const req of reqs) {\n req.resolve(profileMap.get(req.userId) || null);\n }\n } catch (err) {\n for (const req of reqs) req.reject(err);\n }\n }\n }\n\n async process(message: StreamMessage): Promise<void> {\n const { event } = message;\n const publishPromises: Promise<void>[] = [];\n\n let isRequested = true;\n const requestedResult = registry.safeParsePayload(\"notification.requested\", event.payload);\n let createdResult: any = null;\n if (!requestedResult.success) {\n createdResult = registry.safeParsePayload(\"notification.created\", event.payload);\n isRequested = false;\n }\n\n if (!isRequested && (!createdResult || !createdResult.success)) {\n const issues = createdResult\n ? createdResult.error.issues\n : (requestedResult as any).error.issues;\n this.logger.warn({ messageId: message.id, issues }, \"invalid payload — skipping\");\n return;\n }\n\n // Handle legacy notification.created\n if (!isRequested) {\n const raw = createdResult.data as NotificationCreatedPayload;\n const dedupeId = `${raw.projectId}:${raw.idempotencyKey ?? event.id}`;\n if (!(await this.idempotency.checkAndMark(dedupeId, 60))) return;\n try {\n const profile = await this.userRepo.findRecordById(raw.projectId, raw.recipientId);\n if (!profile) return;\n\n const prefs = await this.prefRepo.findByUserId(raw.projectId, raw.recipientId);\n const optedOutTypes = new Set(\n prefs.filter((p: any) => !p.optedIn).map((p: any) => p.eventType),\n );\n\n const enrichedPayload: NotificationEnrichedPayload = {\n projectId: raw.projectId,\n rawEventId: event.id,\n recipientId: raw.recipientId,\n channel: raw.channel,\n priority: raw.priority,\n templateId: raw.templateId,\n templateVariables: raw.payload,\n recipient: {\n id: profile.userId,\n email: profile.email ?? undefined,\n locale: profile.language ?? \"en\",\n timezone: profile.timezone ?? \"UTC\",\n preferences: {\n optedOut:\n optedOutTypes.has(event.type) || profile.preferences.topics?.[event.type] === false,\n channels: Object.entries(profile.preferences.channels ?? {})\n .filter(([_, enabled]) => !enabled)\n .map(([channel]) => channel as any),\n quietHours: profile.preferences.quietHours,\n },\n },\n // No campaignId: this is the legacy `notification.created` path,\n // which predates campaigns and carries no label to attribute to.\n scheduledAt: raw.scheduledAt,\n };\n\n const p = getPriorityBucket(raw.priority);\n const producer = this.producers[p] ?? this.producers[\"normal\"]!;\n\n publishPromises.push(\n new Promise((resolve, reject) => {\n this.eventBuffer.push({\n producer,\n event: buildStreamEvent(\n \"notification.enriched\",\n enrichedPayload as Record<string, unknown>,\n \"enricher\",\n event.metadata.traceId,\n ),\n resolve,\n reject,\n });\n }),\n );\n this.logger.info(\n { messageId: message.id, eventId: event.id, recipientId: raw.recipientId },\n \"event enriched\",\n );\n } catch (err) {\n throw err;\n }\n await Promise.all(publishPromises).catch(async (err) => {\n await this.idempotency.unmark(dedupeId).catch(() => {});\n throw err;\n });\n await this.idempotency.markProcessed(dedupeId);\n return;\n }\n\n // Handle new notification.requested\n const raw = (requestedResult as any).data;\n const dedupeId = `${raw.projectId}:${raw.idempotencyKey ?? event.id}`;\n if (!(await this.idempotency.checkAndMark(dedupeId, 60))) return;\n try {\n let userIds: string[] = [];\n if (raw.target.type === \"user\") {\n userIds = [raw.target.userId];\n } else if (raw.target.type === \"segment\") {\n userIds = await this.userRepo.findUsersBySegment(raw.projectId, raw.target.segment);\n this.logger.info(\n { segment: raw.target.segment, count: userIds.length },\n \"Resolved segment\",\n );\n } else if (raw.target.type === \"topic\") {\n userIds = await this.userRepo.findUsersByTopic(raw.projectId, raw.target.topic);\n this.logger.info({ topic: raw.target.topic, count: userIds.length }, \"Resolved topic\");\n } else {\n this.logger.warn({ target: raw.target }, \"Segment/topic resolution not fully implemented\");\n // Stub: maybe resolve later\n }\n\n const maxUsers = readBaseConfig().SEGMENT_MAX_USERS;\n if (userIds.length > maxUsers) {\n this.logger.error(\n { count: userIds.length, max: maxUsers, projectId: raw.projectId, eventId: event.id },\n \"Segment fan-out exceeds maximum allowed limit\",\n );\n const p = getPriorityBucket(raw.priority ?? \"normal\");\n const producer = this.producers[p] ?? this.producers.normal;\n await producer.publish(\n buildStreamEvent(\n \"notification.failed\",\n {\n projectId: raw.projectId,\n rawEventId: event.id,\n error: `Segment fan-out of ${userIds.length} exceeds limit of ${maxUsers}`,\n },\n \"enricher\",\n event.metadata.traceId,\n ),\n );\n return;\n }\n\n // Topic opt-outs are keyed on the TEMPLATE's topics, not the envelope type.\n // `event.type` here is \"notification.requested\", which no user ever sets a\n // preference against, so keying on it silently disabled every opt-out.\n const template = raw.templateId\n ? await this.templateCache.getCachedTemplate(raw.projectId, raw.templateId)\n : null;\n const topics: string[] = template?.topics ?? [];\n\n const channels =\n raw.channels && raw.channels.length > 0 ? raw.channels : ([\"email\"] as any[]);\n const isFallback = (raw as any).fallback === true;\n\n // If fallback is true, we only emit the first channel, and pass the rest in fallbackChain.\n // If fallback is false, we emit all channels concurrently.\n const channelsToProcess = isFallback ? [channels[0]] : channels;\n const fallbackChain = isFallback ? channels.slice(1) : undefined;\n\n const chunkArray = <T>(arr: T[], size: number) =>\n Array.from({ length: Math.ceil(arr.length / size) }, (v, i) =>\n arr.slice(i * size, i * size + size),\n );\n\n const chunks = chunkArray(userIds, 500);\n\n for (const chunk of chunks) {\n const profiles = (\n await Promise.all(chunk.map((id) => this.loadUser(raw.projectId, id)))\n ).filter(Boolean);\n const contactsByUser = await this.loadContacts(\n raw.projectId,\n profiles.map((profile: any) => profile.userId),\n );\n\n const batchedEvents: Record<\n \"critical\" | \"high\" | \"normal\" | \"low\",\n Omit<any, \"id\" | \"timestamp\">[]\n > = {\n critical: [],\n high: [],\n normal: [],\n low: [],\n };\n\n for (const profile of profiles) {\n for (const channel of channelsToProcess) {\n const contacts = contactsByUser.get(profile.userId) ?? [];\n const channelContacts = contacts.filter((contact: any) => contact.channel === channel);\n // Push resolves its active tokens at send time so token invalidation\n // remains current. Other channels need one task per address.\n const destinations =\n channel === \"push\" ? [undefined] : channelContacts.map((c: any) => c.target);\n if (destinations.length === 0) {\n this.logger.info(\n { recipientId: profile.userId, channel },\n \"no active contact for channel\",\n );\n continue;\n }\n for (const destination of destinations) {\n const enrichedPayload: NotificationEnrichedPayload = {\n projectId: raw.projectId,\n rawEventId: event.id,\n recipientId: profile.userId,\n channel: channel,\n priority: \"normal\",\n templateId: raw.templateId,\n templateVariables: raw.data,\n aiPrompts: raw.aiPrompts,\n recipient: {\n id: profile.userId,\n email:\n channel === \"email\"\n ? (destination ?? profile.email ?? undefined)\n : (profile.email ?? undefined),\n phone: channel === \"sms\" || channel === \"whatsapp\" ? destination : undefined,\n webhook: channel === \"webhook\" ? destination : undefined,\n telegram: channel === \"telegram\" ? destination : undefined,\n discord: channel === \"discord\" ? destination : undefined,\n slack: channel === \"slack\" ? destination : undefined,\n locale: profile.language ?? \"en\",\n timezone: profile.timezone ?? \"UTC\",\n preferences: {\n // Opted out if the user disabled ANY topic this template carries.\n optedOut: topics.some((t) => profile.preferences.topics?.[t] === false),\n channels: Object.entries(profile.preferences.channels ?? {})\n .filter(([_, enabled]) => !enabled)\n .map(([channel]) => channel as any),\n quietHours: profile.preferences.quietHours,\n },\n },\n scheduledAt: raw.scheduledAt,\n fallbackChain: fallbackChain?.length ? fallbackChain : undefined,\n campaignId: raw.campaignId,\n };\n\n const msgPriority = raw.priority ?? \"normal\";\n const p = getPriorityBucket(msgPriority);\n\n enrichedPayload.priority = msgPriority;\n\n batchedEvents[p].push(\n buildStreamEvent(\n \"notification.enriched\",\n enrichedPayload as Record<string, unknown>,\n \"enricher\",\n event.metadata.traceId,\n ),\n );\n }\n }\n }\n\n for (const p of [\"critical\", \"normal\", \"low\"] as const) {\n if (batchedEvents[p].length > 0) {\n const producer = this.producers[p] ?? this.producers.normal;\n for (const ev of batchedEvents[p]) {\n publishPromises.push(\n new Promise((resolve, reject) => {\n this.eventBuffer.push({ producer, event: ev, resolve, reject });\n }),\n );\n }\n }\n }\n }\n\n this.logger.info(\n {\n messageId: message.id,\n eventId: event.id,\n target: raw.target.type,\n traceId: event.metadata.traceId,\n },\n \"event enriched\",\n );\n } catch (err) {\n throw err;\n }\n\n await Promise.all(publishPromises).catch(async (err) => {\n await this.idempotency.unmark(dedupeId).catch(() => {});\n throw err;\n });\n\n await this.idempotency.markProcessed(dedupeId);\n }\n}\n\nexport async function startEnricherWorker() {\n logger = createLogger({ name: \"enricher\", level: config.LOG_LEVEL });\n redis = new RedisClient({ url: config.REDIS_URL, name: \"enricher\", logger });\n const dbData = createDatabase({ url: config.DATABASE_URL, applicationName: \"enricher\", logger });\n sql = dbData.sql;\n db = dbData.db;\n consumer = new StreamConsumer({\n redis: redis.native,\n stream: INBOUND_STREAMS as unknown as StreamName[],\n group: CONSUMER_GROUPS.ENRICHER,\n consumer: `enricher-${process.pid}`,\n dlqStream: STREAMS.DEAD_LETTER,\n batchSize: config.WORKER_CONCURRENCY,\n logger,\n });\n\n pendingScanner = new PendingMessageScanner({\n redis: redis.native,\n stream: INBOUND_STREAMS as unknown as StreamName[],\n group: CONSUMER_GROUPS.ENRICHER,\n consumer: `enricher-${process.pid}`,\n logger,\n });\n\n const producers = {\n critical: new StreamProducer({\n redis: redis.native,\n stream: STREAMS.ENRICHED_CRITICAL,\n logger,\n }),\n normal: new StreamProducer({ redis: redis.native, stream: STREAMS.ENRICHED_NORMAL, logger }),\n low: new StreamProducer({ redis: redis.native, stream: STREAMS.ENRICHED_LOW, logger }),\n };\n\n const idempotency = new IdempotencyGuard({\n redis: redis.native,\n keyPrefix: \"notif:processed:enricher\",\n ttlSeconds: 86_400,\n });\n\n const userRepo = new UserRepository(db);\n const prefRepo = new PreferenceRepository(db);\n const contactRepo = new ContactRepository(db);\n const templateCache = new TemplateCache(new TemplateRepository(db));\n\n // ─── Stage 1: Context Enricher ──────────────────────────────────────────────\n //\n // Pipeline:\n // 1. Parse payload as notification.created\n // 2. Idempotency check — drop if already processed\n // 3. Load user profile (language, timezone) from DB\n // 4. Load all stored preferences for this user\n // 5. Publish notification.enriched to ENRICHED stream\n\n // 5. Publish notification.enriched to ENRICHED stream\n\n worker = new EnricherWorker({\n consumer,\n pendingScanner,\n logger,\n maxRetriesBeforeDlq: 5,\n concurrency: config.WORKER_CONCURRENCY,\n producers,\n idempotency,\n userRepo,\n prefRepo,\n contactRepo,\n templateCache,\n });\n\n // ─── Health check interval ──────────────────────────────────────────────────\n\n healthInterval = startHealthReporter(\"enricher\", worker, redis, logger);\n\n logger.info({ env: config.NODE_ENV }, \"enricher starting\");\n await worker.start();\n}\n\n// ─── Shutdown ──────────────────────────────────────────────────────────────\n\nexport async function stopEnricherWorker(): Promise<void> {\n logger?.info(\"shutdown initiated\");\n if (healthInterval) {\n clearInterval(healthInterval);\n healthInterval = null;\n }\n if (worker) await worker.stop();\n if (sql) await sql.end();\n if (redis) await redis.disconnect();\n logger?.info(\"enricher stopped\");\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { J as PendingMessageScanner, Jt as buildStreamEvent, X as StreamProducer, Y as StreamConsumer, Yt as CONSUMER_GROUPS, _t as workflowWaiters, c as BaseWorker, en as STREAMS, et as createLogger, gt as workflowSteps, hn as readBaseConfig, ht as workflowInstances, it as createDatabase, k as RedisClient, pn as loadEnv, st as messageLogs, u as startHealthReporter } from "./src-C-PfEDMY.mjs";
1
+ import { J as PendingMessageScanner, Jt as buildStreamEvent, X as StreamProducer, Y as StreamConsumer, Yt as CONSUMER_GROUPS, _t as workflowWaiters, c as BaseWorker, en as STREAMS, et as createLogger, gt as workflowSteps, hn as readBaseConfig, ht as workflowInstances, it as createDatabase, k as RedisClient, pn as loadEnv, st as messageLogs, u as startHealthReporter } from "./src-vG79L-8m.mjs";
2
2
  import { and, eq, gt, isNull, or } from "drizzle-orm";
3
3
  //#region src/services/events/main.ts
4
4
  loadEnv();
@@ -231,4 +231,4 @@ async function stopEventWorker() {
231
231
  //#endregion
232
232
  export { startEventWorker, stopEventWorker };
233
233
 
234
- //# sourceMappingURL=main-B-jwm8ED.mjs.map
234
+ //# sourceMappingURL=main-DmCPcxOc.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"main-B-jwm8ED.mjs","names":["sql"],"sources":["../src/services/events/main.ts"],"sourcesContent":["import { loadEnv, readBaseConfig } from \"@/index.js\";\nimport { createLogger } from \"@/index.js\";\nimport { RedisClient } from \"@/index.js\";\nimport {\n StreamConsumer,\n PendingMessageScanner,\n StreamProducer,\n type StreamMessage,\n} from \"@/index.js\";\nimport { BaseWorker } from \"@/index.js\";\nimport { STREAMS, CONSUMER_GROUPS, buildStreamEvent } from \"@/index.js\";\nimport { type StreamName } from \"@/contracts/streams.js\";\nimport { createDatabase } from \"@/db/index.js\";\nimport { workflowWaiters, workflowSteps, workflowInstances, messageLogs } from \"@/db/schema.js\";\nimport { eq, and, or, isNull, gt } from \"drizzle-orm\";\nimport { type WorkerOptions } from \"@/shared/index.js\";\nimport { startHealthReporter } from \"@/workers/index.js\";\n\nloadEnv();\nconst config = readBaseConfig();\nlet logger: ReturnType<typeof createLogger>;\nlet redis: RedisClient;\nlet sql: any;\nlet db: any;\n\nlet consumer: StreamConsumer;\nlet pendingScanner: PendingMessageScanner;\nlet worker: BaseWorker;\nlet healthInterval: NodeJS.Timeout | null = null;\n\nlet workflowProducer: StreamProducer;\n\nexport interface EventWorkerOptions extends WorkerOptions {\n db: any;\n workflowProducer: any;\n}\n\nexport class EventWorker extends BaseWorker {\n private readonly dbConn: any;\n private readonly workflowProducer: any;\n private messageLogBuffer: { log: any; resolve: () => void; reject: (err: any) => void }[] = [];\n private eventBuffer: {\n producer: any;\n event: any;\n resolve: () => void;\n reject: (err: any) => void;\n }[] = [];\n private flushInterval: NodeJS.Timeout | null = null;\n private isFlushing = false;\n\n constructor(options: EventWorkerOptions) {\n super(options);\n this.dbConn = options.db;\n this.workflowProducer = options.workflowProducer;\n this.startFlushInterval();\n }\n\n private startFlushInterval() {\n this.flushInterval = setInterval(() => {\n void this.flushLogs();\n }, config.LOG_FLUSH_INTERVAL_MS);\n }\n\n private async flushLogs() {\n if (this.isFlushing || (this.messageLogBuffer.length === 0 && this.eventBuffer.length === 0))\n return;\n this.isFlushing = true;\n\n const startTime = Date.now();\n const batch = this.messageLogBuffer;\n this.messageLogBuffer = [];\n\n const events = this.eventBuffer;\n this.eventBuffer = [];\n\n try {\n if (batch.length > 0) {\n await this.dbConn\n .insert(messageLogs)\n .values(batch.map((b) => b.log))\n .onConflictDoNothing(); // Idempotent insert\n for (const b of batch) b.resolve();\n\n this.logger.info(\n {\n batchSize: batch.length,\n latencyMs: Date.now() - startTime,\n },\n \"flushed delivery logs to database\",\n );\n }\n\n if (events.length > 0) {\n const byProducer = new Map<any, typeof events>();\n for (const item of events) {\n if (!byProducer.has(item.producer)) byProducer.set(item.producer, []);\n byProducer.get(item.producer)!.push(item);\n }\n for (const [producer, evts] of byProducer) {\n await producer.publishBatch(evts.map((e) => e.event));\n for (const e of evts) e.resolve();\n }\n }\n } catch (error: any) {\n this.logger.error(\n {\n batchSize: batch.length,\n error: error.message,\n },\n \"failed to flush delivery logs — dropping batch\",\n );\n for (const b of batch) b.reject(error);\n for (const e of events) e.reject(error);\n } finally {\n this.isFlushing = false;\n }\n }\n\n override async stop(): Promise<void> {\n if (this.flushInterval) clearInterval(this.flushInterval);\n await super.stop();\n await this.flushLogs();\n }\n\n override async process(message: StreamMessage): Promise<void> {\n const { event } = message;\n const publishPromises: Promise<void>[] = [];\n\n if (\n event.type === \"notification.dispatched\" ||\n event.type === \"notification.delivered\" ||\n event.type === \"notification.failed\"\n ) {\n const payload = event.payload as any;\n if (this.messageLogBuffer.length >= config.LOG_BUFFER_MAX_SIZE) {\n this.logger.warn(\n { dropped: 1, bufferSize: this.messageLogBuffer.length },\n \"log buffer full — dropping oldest delivery log\",\n );\n const dropped = this.messageLogBuffer.shift(); // Drop oldest to apply back-pressure/prevent OOM\n if (dropped) dropped.reject(new Error(\"Dropped due to buffer overflow\"));\n }\n\n const status =\n event.type === \"notification.dispatched\"\n ? \"dispatched\"\n : event.type === \"notification.delivered\"\n ? \"delivered\"\n : \"failed\";\n\n const kind = event.type === \"notification.dispatched\" ? \"dispatched\" : \"attempt\";\n\n publishPromises.push(\n new Promise((resolve, reject) => {\n this.messageLogBuffer.push({\n log: {\n projectId: payload.projectId,\n taskId: payload.taskId,\n providerMessageId: payload.providerMessageId || null,\n channel: payload.channel,\n attempt: payload.attempt || 1,\n kind,\n status,\n templateId: payload.templateId || null,\n workflowInstanceId: payload.workflowInstanceId || null,\n campaignId: payload.campaignId || null,\n },\n resolve,\n reject,\n });\n }),\n );\n await Promise.all(publishPromises);\n return;\n }\n\n if (event.type !== \"event.received\") {\n return;\n }\n\n const payload = event.payload as any;\n const eventName = payload.eventName;\n const eventData = payload.payload;\n const projectId = payload.projectId;\n\n if (!eventName || !projectId) {\n this.logger.warn(\"Missing eventName or projectId in payload\");\n return;\n }\n\n // Find matching waiters\n const now = new Date();\n const waiters = await this.dbConn\n .select()\n .from(workflowWaiters)\n .where(\n and(\n eq(workflowWaiters.eventName, eventName),\n eq(workflowWaiters.projectId, projectId),\n or(isNull(workflowWaiters.expiresAt), gt(workflowWaiters.expiresAt, now)),\n ),\n );\n\n const data: Record<string, any> = eventData && typeof eventData === \"object\" ? eventData : {};\n\n for (const waiter of waiters) {\n // Check match criteria\n let matched = true;\n const criteria = waiter.matchCriteria as Record<string, any>;\n if (criteria && typeof criteria === \"object\") {\n for (const [k, v] of Object.entries(criteria)) {\n // simple dot notation check for a flat object or 1 level deep\n let dataValue = data[k];\n if (k.includes(\".\")) {\n const parts = k.split(\".\");\n dataValue = data;\n for (const p of parts) {\n if (dataValue) dataValue = dataValue[p];\n }\n }\n if (dataValue !== v) {\n matched = false;\n break;\n }\n }\n }\n\n if (matched) {\n this.logger.info(\n { instanceId: waiter.instanceId, eventName },\n \"Event matched a sleeping workflow\",\n );\n\n // The resume event must carry everything WorkflowWorker.process requires\n // (name, instanceId, projectId) or it is dropped and the instance — whose\n // waiter we are about to delete — is stranded forever.\n const instance = (\n await this.dbConn\n .select()\n .from(workflowInstances)\n .where(eq(workflowInstances.id, waiter.instanceId))\n .limit(1)\n )[0];\n\n if (!instance) {\n this.logger.warn(\n { instanceId: waiter.instanceId },\n \"waiter references a missing workflow instance — dropping waiter\",\n );\n await this.dbConn.delete(workflowWaiters).where(eq(workflowWaiters.id, waiter.id));\n continue;\n }\n\n // Find the step that is waiting\n const steps = await this.dbConn\n .select()\n .from(workflowSteps)\n .where(\n and(\n eq(workflowSteps.instanceId, waiter.instanceId),\n eq(workflowSteps.action, \"waitForEvent\"),\n ),\n );\n\n // Get the latest one that has no output\n const pendingStep = steps.find((s: any) => s.output === null);\n\n if (pendingStep) {\n // Update the step with the event payload\n await this.dbConn\n .update(workflowSteps)\n .set({ output: data })\n .where(eq(workflowSteps.id, pendingStep.id));\n }\n\n // Wake up workflow. Publish BEFORE deleting the waiter: a crash after the\n // delete but before the publish would leave the instance unrecoverable,\n // whereas a duplicate resume is idempotent (the instance lock and the\n // replayed step outputs both absorb it).\n publishPromises.push(\n new Promise((resolve, reject) => {\n this.eventBuffer.push({\n producer: this.workflowProducer,\n event: buildStreamEvent(\n \"workflow.resumed\",\n {\n projectId: instance.projectId,\n instanceId: waiter.instanceId,\n name: instance.name,\n input: instance.input ?? {},\n reason: \"event_matched\",\n },\n \"events\",\n event.metadata.traceId,\n ),\n resolve,\n reject,\n });\n }),\n );\n\n // Delete the waiter\n await this.dbConn.delete(workflowWaiters).where(eq(workflowWaiters.id, waiter.id));\n\n // The timer ZSET entry is left to expire: the poll loop re-checks the\n // instance status and drops the entry if it is no longer pending.\n }\n }\n\n await Promise.all(publishPromises);\n }\n}\n\nexport async function startEventWorker() {\n logger = createLogger({ name: \"event-worker\", level: config.LOG_LEVEL });\n redis = new RedisClient({ url: config.REDIS_URL, name: \"events\", logger });\n const dbData = createDatabase({ url: config.DATABASE_URL, applicationName: \"events\", logger });\n sql = dbData.sql;\n db = dbData.db;\n workflowProducer = new StreamProducer({\n redis: redis.native,\n stream: STREAMS.WORKFLOW_INBOUND,\n logger,\n });\n consumer = new StreamConsumer({\n redis: redis.native,\n stream: STREAMS.EVENTS_INBOUND as StreamName,\n group: CONSUMER_GROUPS.EVENTS as any,\n consumer: `events-${process.pid}`,\n dlqStream: STREAMS.DEAD_LETTER,\n batchSize: config.WORKER_CONCURRENCY,\n logger,\n });\n\n pendingScanner = new PendingMessageScanner({\n redis: redis.native,\n stream: STREAMS.EVENTS_INBOUND as StreamName,\n group: CONSUMER_GROUPS.EVENTS as any,\n consumer: `events-${process.pid}`,\n logger,\n });\n\n worker = new EventWorker({\n consumer,\n pendingScanner,\n logger,\n concurrency: config.WORKER_CONCURRENCY,\n db,\n workflowProducer,\n });\n\n healthInterval = startHealthReporter(\"events\", worker, redis, logger);\n\n logger.info(\"event worker starting\");\n await worker.start();\n}\n\nexport async function stopEventWorker(): Promise<void> {\n logger?.info(\"shutdown initiated\");\n if (healthInterval) clearInterval(healthInterval);\n if (worker) await worker.stop();\n if (sql) await sql.end();\n if (redis) await redis.disconnect();\n logger?.info(\"event worker stopped\");\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"main-DmCPcxOc.mjs","names":["sql"],"sources":["../src/services/events/main.ts"],"sourcesContent":["import { loadEnv, readBaseConfig } from \"@/index.js\";\nimport { createLogger } from \"@/index.js\";\nimport { RedisClient } from \"@/index.js\";\nimport {\n StreamConsumer,\n PendingMessageScanner,\n StreamProducer,\n type StreamMessage,\n} from \"@/index.js\";\nimport { BaseWorker } from \"@/index.js\";\nimport { STREAMS, CONSUMER_GROUPS, buildStreamEvent } from \"@/index.js\";\nimport { type StreamName } from \"@/contracts/streams.js\";\nimport { createDatabase } from \"@/db/index.js\";\nimport { workflowWaiters, workflowSteps, workflowInstances, messageLogs } from \"@/db/schema.js\";\nimport { eq, and, or, isNull, gt } from \"drizzle-orm\";\nimport { type WorkerOptions } from \"@/shared/index.js\";\nimport { startHealthReporter } from \"@/workers/index.js\";\n\nloadEnv();\nconst config = readBaseConfig();\nlet logger: ReturnType<typeof createLogger>;\nlet redis: RedisClient;\nlet sql: any;\nlet db: any;\n\nlet consumer: StreamConsumer;\nlet pendingScanner: PendingMessageScanner;\nlet worker: BaseWorker;\nlet healthInterval: NodeJS.Timeout | null = null;\n\nlet workflowProducer: StreamProducer;\n\nexport interface EventWorkerOptions extends WorkerOptions {\n db: any;\n workflowProducer: any;\n}\n\nexport class EventWorker extends BaseWorker {\n private readonly dbConn: any;\n private readonly workflowProducer: any;\n private messageLogBuffer: { log: any; resolve: () => void; reject: (err: any) => void }[] = [];\n private eventBuffer: {\n producer: any;\n event: any;\n resolve: () => void;\n reject: (err: any) => void;\n }[] = [];\n private flushInterval: NodeJS.Timeout | null = null;\n private isFlushing = false;\n\n constructor(options: EventWorkerOptions) {\n super(options);\n this.dbConn = options.db;\n this.workflowProducer = options.workflowProducer;\n this.startFlushInterval();\n }\n\n private startFlushInterval() {\n this.flushInterval = setInterval(() => {\n void this.flushLogs();\n }, config.LOG_FLUSH_INTERVAL_MS);\n }\n\n private async flushLogs() {\n if (this.isFlushing || (this.messageLogBuffer.length === 0 && this.eventBuffer.length === 0))\n return;\n this.isFlushing = true;\n\n const startTime = Date.now();\n const batch = this.messageLogBuffer;\n this.messageLogBuffer = [];\n\n const events = this.eventBuffer;\n this.eventBuffer = [];\n\n try {\n if (batch.length > 0) {\n await this.dbConn\n .insert(messageLogs)\n .values(batch.map((b) => b.log))\n .onConflictDoNothing(); // Idempotent insert\n for (const b of batch) b.resolve();\n\n this.logger.info(\n {\n batchSize: batch.length,\n latencyMs: Date.now() - startTime,\n },\n \"flushed delivery logs to database\",\n );\n }\n\n if (events.length > 0) {\n const byProducer = new Map<any, typeof events>();\n for (const item of events) {\n if (!byProducer.has(item.producer)) byProducer.set(item.producer, []);\n byProducer.get(item.producer)!.push(item);\n }\n for (const [producer, evts] of byProducer) {\n await producer.publishBatch(evts.map((e) => e.event));\n for (const e of evts) e.resolve();\n }\n }\n } catch (error: any) {\n this.logger.error(\n {\n batchSize: batch.length,\n error: error.message,\n },\n \"failed to flush delivery logs — dropping batch\",\n );\n for (const b of batch) b.reject(error);\n for (const e of events) e.reject(error);\n } finally {\n this.isFlushing = false;\n }\n }\n\n override async stop(): Promise<void> {\n if (this.flushInterval) clearInterval(this.flushInterval);\n await super.stop();\n await this.flushLogs();\n }\n\n override async process(message: StreamMessage): Promise<void> {\n const { event } = message;\n const publishPromises: Promise<void>[] = [];\n\n if (\n event.type === \"notification.dispatched\" ||\n event.type === \"notification.delivered\" ||\n event.type === \"notification.failed\"\n ) {\n const payload = event.payload as any;\n if (this.messageLogBuffer.length >= config.LOG_BUFFER_MAX_SIZE) {\n this.logger.warn(\n { dropped: 1, bufferSize: this.messageLogBuffer.length },\n \"log buffer full — dropping oldest delivery log\",\n );\n const dropped = this.messageLogBuffer.shift(); // Drop oldest to apply back-pressure/prevent OOM\n if (dropped) dropped.reject(new Error(\"Dropped due to buffer overflow\"));\n }\n\n const status =\n event.type === \"notification.dispatched\"\n ? \"dispatched\"\n : event.type === \"notification.delivered\"\n ? \"delivered\"\n : \"failed\";\n\n const kind = event.type === \"notification.dispatched\" ? \"dispatched\" : \"attempt\";\n\n publishPromises.push(\n new Promise((resolve, reject) => {\n this.messageLogBuffer.push({\n log: {\n projectId: payload.projectId,\n taskId: payload.taskId,\n providerMessageId: payload.providerMessageId || null,\n channel: payload.channel,\n attempt: payload.attempt || 1,\n kind,\n status,\n templateId: payload.templateId || null,\n workflowInstanceId: payload.workflowInstanceId || null,\n campaignId: payload.campaignId || null,\n },\n resolve,\n reject,\n });\n }),\n );\n await Promise.all(publishPromises);\n return;\n }\n\n if (event.type !== \"event.received\") {\n return;\n }\n\n const payload = event.payload as any;\n const eventName = payload.eventName;\n const eventData = payload.payload;\n const projectId = payload.projectId;\n\n if (!eventName || !projectId) {\n this.logger.warn(\"Missing eventName or projectId in payload\");\n return;\n }\n\n // Find matching waiters\n const now = new Date();\n const waiters = await this.dbConn\n .select()\n .from(workflowWaiters)\n .where(\n and(\n eq(workflowWaiters.eventName, eventName),\n eq(workflowWaiters.projectId, projectId),\n or(isNull(workflowWaiters.expiresAt), gt(workflowWaiters.expiresAt, now)),\n ),\n );\n\n const data: Record<string, any> = eventData && typeof eventData === \"object\" ? eventData : {};\n\n for (const waiter of waiters) {\n // Check match criteria\n let matched = true;\n const criteria = waiter.matchCriteria as Record<string, any>;\n if (criteria && typeof criteria === \"object\") {\n for (const [k, v] of Object.entries(criteria)) {\n // simple dot notation check for a flat object or 1 level deep\n let dataValue = data[k];\n if (k.includes(\".\")) {\n const parts = k.split(\".\");\n dataValue = data;\n for (const p of parts) {\n if (dataValue) dataValue = dataValue[p];\n }\n }\n if (dataValue !== v) {\n matched = false;\n break;\n }\n }\n }\n\n if (matched) {\n this.logger.info(\n { instanceId: waiter.instanceId, eventName },\n \"Event matched a sleeping workflow\",\n );\n\n // The resume event must carry everything WorkflowWorker.process requires\n // (name, instanceId, projectId) or it is dropped and the instance — whose\n // waiter we are about to delete — is stranded forever.\n const instance = (\n await this.dbConn\n .select()\n .from(workflowInstances)\n .where(eq(workflowInstances.id, waiter.instanceId))\n .limit(1)\n )[0];\n\n if (!instance) {\n this.logger.warn(\n { instanceId: waiter.instanceId },\n \"waiter references a missing workflow instance — dropping waiter\",\n );\n await this.dbConn.delete(workflowWaiters).where(eq(workflowWaiters.id, waiter.id));\n continue;\n }\n\n // Find the step that is waiting\n const steps = await this.dbConn\n .select()\n .from(workflowSteps)\n .where(\n and(\n eq(workflowSteps.instanceId, waiter.instanceId),\n eq(workflowSteps.action, \"waitForEvent\"),\n ),\n );\n\n // Get the latest one that has no output\n const pendingStep = steps.find((s: any) => s.output === null);\n\n if (pendingStep) {\n // Update the step with the event payload\n await this.dbConn\n .update(workflowSteps)\n .set({ output: data })\n .where(eq(workflowSteps.id, pendingStep.id));\n }\n\n // Wake up workflow. Publish BEFORE deleting the waiter: a crash after the\n // delete but before the publish would leave the instance unrecoverable,\n // whereas a duplicate resume is idempotent (the instance lock and the\n // replayed step outputs both absorb it).\n publishPromises.push(\n new Promise((resolve, reject) => {\n this.eventBuffer.push({\n producer: this.workflowProducer,\n event: buildStreamEvent(\n \"workflow.resumed\",\n {\n projectId: instance.projectId,\n instanceId: waiter.instanceId,\n name: instance.name,\n input: instance.input ?? {},\n reason: \"event_matched\",\n },\n \"events\",\n event.metadata.traceId,\n ),\n resolve,\n reject,\n });\n }),\n );\n\n // Delete the waiter\n await this.dbConn.delete(workflowWaiters).where(eq(workflowWaiters.id, waiter.id));\n\n // The timer ZSET entry is left to expire: the poll loop re-checks the\n // instance status and drops the entry if it is no longer pending.\n }\n }\n\n await Promise.all(publishPromises);\n }\n}\n\nexport async function startEventWorker() {\n logger = createLogger({ name: \"event-worker\", level: config.LOG_LEVEL });\n redis = new RedisClient({ url: config.REDIS_URL, name: \"events\", logger });\n const dbData = createDatabase({ url: config.DATABASE_URL, applicationName: \"events\", logger });\n sql = dbData.sql;\n db = dbData.db;\n workflowProducer = new StreamProducer({\n redis: redis.native,\n stream: STREAMS.WORKFLOW_INBOUND,\n logger,\n });\n consumer = new StreamConsumer({\n redis: redis.native,\n stream: STREAMS.EVENTS_INBOUND as StreamName,\n group: CONSUMER_GROUPS.EVENTS as any,\n consumer: `events-${process.pid}`,\n dlqStream: STREAMS.DEAD_LETTER,\n batchSize: config.WORKER_CONCURRENCY,\n logger,\n });\n\n pendingScanner = new PendingMessageScanner({\n redis: redis.native,\n stream: STREAMS.EVENTS_INBOUND as StreamName,\n group: CONSUMER_GROUPS.EVENTS as any,\n consumer: `events-${process.pid}`,\n logger,\n });\n\n worker = new EventWorker({\n consumer,\n pendingScanner,\n logger,\n concurrency: config.WORKER_CONCURRENCY,\n db,\n workflowProducer,\n });\n\n healthInterval = startHealthReporter(\"events\", worker, redis, logger);\n\n logger.info(\"event worker starting\");\n await worker.start();\n}\n\nexport async function stopEventWorker(): Promise<void> {\n logger?.info(\"shutdown initiated\");\n if (healthInterval) clearInterval(healthInterval);\n if (worker) await worker.stop();\n if (sql) await sql.end();\n if (redis) await redis.disconnect();\n logger?.info(\"event worker stopped\");\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { J as PendingMessageScanner, Jt as buildStreamEvent, U as LUA_SCHEDULER_POLL, W as getPriorityBucket, X as StreamProducer, Y as StreamConsumer, Yt as CONSUMER_GROUPS, an as registry, c as BaseWorker, en as STREAMS, et as createLogger, hn as readBaseConfig, it as createDatabase, k as RedisClient, pn as loadEnv, u as startHealthReporter, ut as scheduledPayloads } from "./src-C-PfEDMY.mjs";
1
+ import { J as PendingMessageScanner, Jt as buildStreamEvent, U as LUA_SCHEDULER_POLL, W as getPriorityBucket, X as StreamProducer, Y as StreamConsumer, Yt as CONSUMER_GROUPS, an as registry, c as BaseWorker, en as STREAMS, et as createLogger, hn as readBaseConfig, it as createDatabase, k as RedisClient, pn as loadEnv, u as startHealthReporter, ut as scheduledPayloads } from "./src-vG79L-8m.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { inArray } from "drizzle-orm";
4
4
  //#region src/services/scheduler/main.ts
@@ -221,4 +221,4 @@ async function stopSchedulerWorker() {
221
221
  //#endregion
222
222
  export { startSchedulerWorker, stopSchedulerWorker };
223
223
 
224
- //# sourceMappingURL=main-C45e7grq.mjs.map
224
+ //# sourceMappingURL=main-DvgJSm11.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"main-C45e7grq.mjs","names":["sql"],"sources":["../src/services/scheduler/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 {\n STREAMS,\n CONSUMER_GROUPS,\n registry,\n buildStreamEvent,\n type NotificationScheduledPayload,\n} from \"@/index.js\";\nimport { getPriorityBucket, type WorkerOptions, LUA_SCHEDULER_POLL } from \"@/shared/index.js\";\nimport { createDatabase } from \"@/db/index.js\";\nimport { scheduledPayloads } from \"@/db/schema.js\";\nimport { inArray } from \"drizzle-orm\";\nimport { startHealthReporter } from \"@/workers/index.js\";\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;\nlet pollTimeout: ReturnType<typeof setTimeout> | null = null;\nlet isPolling = false;\n\nexport interface SchedulerWorkerOptions extends WorkerOptions {\n registry: any;\n redis: Redis;\n}\n\nexport class SchedulerWorker extends BaseWorker {\n private readonly registry: any;\n private readonly redisCli: Redis;\n\n constructor(options: SchedulerWorkerOptions) {\n super(options);\n this.registry = options.registry;\n this.redisCli = options.redis;\n }\n async process(message: StreamMessage): Promise<void> {\n const { event } = message;\n\n const payloadResult = this.registry.safeParsePayload(\"notification.scheduled\", event.payload);\n if (!payloadResult.success) {\n this.logger.warn(\n { messageId: message.id, issues: payloadResult.error.issues },\n \"invalid notification.scheduled payload — skipping\",\n );\n return;\n }\n\n const scheduled = payloadResult.data as NotificationScheduledPayload;\n const scheduledAt = new Date(scheduled.scheduledAt).getTime();\n\n const taskData = JSON.stringify({\n taskId: scheduled.taskId,\n enrichedEventId: scheduled.enrichedEventId,\n traceId: event.metadata.traceId,\n });\n\n const shard = parseInt(scheduled.taskId.slice(-1), 16) || 0;\n const zsetKey = `notif:scheduled:zset:${shard}`;\n await this.redisCli.zadd(zsetKey, scheduledAt, taskData);\n this.logger.debug(\n { taskId: scheduled.taskId, scheduledAt: scheduled.scheduledAt, shard },\n \"scheduled task queued in ZSET\",\n );\n }\n}\n\nexport async function executeSchedulerPoll(\n redis: Redis,\n outboundProducers: any,\n logger: any,\n db: any,\n): Promise<boolean> {\n const lockKey = \"notif:lock:scheduler:poll\";\n const lockOwner = randomUUID();\n let acquired = false;\n try {\n const lockAcquired = await redis.set(lockKey, lockOwner, \"EX\", 30, \"NX\");\n if (lockAcquired !== \"OK\") {\n return false; // Another instance is currently polling\n }\n acquired = true;\n\n const now = Date.now();\n const visibilityTimeout = 60000; // 60 seconds\n const perShardLimit = 100;\n const pipeline = redis.pipeline();\n\n for (let i = 0; i < 16; i++) {\n pipeline.eval(\n LUA_SCHEDULER_POLL,\n 1,\n `notif:scheduled:zset:${i}`,\n now,\n perShardLimit,\n visibilityTimeout,\n );\n }\n\n const results = await pipeline.exec();\n const perShard = results?.map((res: any) => (res[0] ? [] : ((res[1] ?? []) as string[]))) ?? [];\n const tasks = perShard.flat();\n // A shard that came back full may still have due tasks behind it, which is\n // what tells the caller to poll again rather than wait for the next tick.\n const anyShardFull = perShard.some((shard) => shard.length >= perShardLimit);\n\n if (tasks.length === 0) return false;\n\n const parsedTasks = tasks\n .map((t) => {\n try {\n return { taskStr: t, ...JSON.parse(t) };\n } catch (err) {\n logger.error({ taskStr: t, err }, \"failed to parse scheduled task JSON\");\n return null;\n }\n })\n .filter(Boolean) as any[];\n if (parsedTasks.length === 0) return false;\n\n const taskIds = parsedTasks.map((t) => t.taskId);\n const dbPayloads = await db\n .select()\n .from(scheduledPayloads)\n .where(inArray(scheduledPayloads.taskId, taskIds));\n const payloadMap = new Map(dbPayloads.map((row: any) => [row.taskId, row.payload]));\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 const cleanupPipeline = redis.pipeline();\n const dbCleanupIds: string[] = [];\n\n for (let i = 0; i < parsedTasks.length; i++) {\n const { taskStr, taskId, traceId } = parsedTasks[i];\n const payload = payloadMap.get(taskId);\n\n if (!payload) {\n logger.warn({ taskId }, \"scheduled payload not found in Postgres — skipping release\");\n const shard = parseInt(taskId.slice(-1), 16) || 0;\n cleanupPipeline.zrem(`notif:scheduled:zset:${shard}`, taskStr);\n continue;\n }\n\n const dispatchPayload = payload as any;\n const p = getPriorityBucket(dispatchPayload.priority);\n\n batchedEvents[p].push(\n buildStreamEvent(\"notification.dispatched\", dispatchPayload, \"scheduler\", traceId),\n );\n\n dbCleanupIds.push(taskId);\n const shard = parseInt(taskId.slice(-1), 16) || 0;\n cleanupPipeline.zrem(`notif:scheduled:zset:${shard}`, taskStr);\n }\n\n // Every bucket, not a hardcoded subset: a task binned into one that is not\n // published here has already been added to the cleanup list, so it would be\n // deleted from Redis and Postgres without ever being sent.\n for (const p of Object.keys(batchedEvents) as (keyof typeof batchedEvents)[]) {\n if (batchedEvents[p].length > 0) {\n const producer = outboundProducers[p] ?? outboundProducers[\"normal\"]!;\n await producer!.publishBatch(batchedEvents[p]);\n }\n }\n\n if (dbCleanupIds.length > 0) {\n try {\n await db.delete(scheduledPayloads).where(inArray(scheduledPayloads.taskId, dbCleanupIds));\n } catch (err) {\n logger.error(\n { err },\n \"failed to delete scheduled payloads from Postgres, continuing with Redis cleanup\",\n );\n }\n }\n await cleanupPipeline.exec();\n\n logger.info({ count: parsedTasks.length }, \"scheduled tasks released to outbound\");\n\n // \"Poll again immediately\" — the caller loops while this is true. The old\n // test was `tasks.length === 100` against a total drawn from 16 shards, so\n // a real backlog reported \"nothing more\" and waited for the next tick.\n return anyShardFull;\n } catch (err) {\n logger.error({ err }, \"error in scheduler polling loop\");\n return false;\n } finally {\n if (acquired) {\n // Delete the lock only if we still own it\n const lua = `\n if redis.call(\"get\", KEYS[1]) == ARGV[1] then\n return redis.call(\"del\", KEYS[1])\n else\n return 0\n end\n `;\n await redis.eval(lua, 1, lockKey, lockOwner).catch(() => {});\n }\n }\n}\n\nexport async function startSchedulerWorker() {\n logger = createLogger({ name: \"scheduler\", level: config.LOG_LEVEL });\n redis = new RedisClient({ url: config.REDIS_URL, name: \"scheduler\", logger });\n const dbData = createDatabase({ url: config.DATABASE_URL, applicationName: \"scheduler\", logger });\n sql = dbData.sql;\n db = dbData.db;\n consumer = new StreamConsumer({\n redis: redis.native,\n stream: STREAMS.SCHEDULED,\n group: CONSUMER_GROUPS.SCHEDULER,\n consumer: `scheduler-${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.SCHEDULED,\n group: CONSUMER_GROUPS.SCHEDULER,\n consumer: `scheduler-${process.pid}`,\n logger,\n });\n\n const outboundProducers = {\n critical: new StreamProducer({\n redis: redis.native,\n stream: STREAMS.OUTBOUND_CRITICAL,\n logger,\n }),\n normal: new StreamProducer({ redis: redis.native, stream: STREAMS.OUTBOUND_NORMAL, logger }),\n low: new StreamProducer({ redis: redis.native, stream: STREAMS.OUTBOUND_LOW, logger }),\n };\n\n // ─── Stage 4a: Scheduled notification processor ─────────────────────────────\n //\n // Reads from SCHEDULED stream, parses, and queues tasks in a Redis ZSET.\n // A background polling interval releases tasks when they are due.\n\n worker = new SchedulerWorker({\n consumer,\n pendingScanner,\n logger,\n concurrency: config.WORKER_CONCURRENCY,\n registry,\n redis: redis.native,\n });\n\n // ─── Polling Loop ─────────────────────────────────────────────────────────\n isPolling = true;\n const pollLoop = async (): Promise<void> => {\n if (!isPolling) return;\n try {\n const hasMore = await executeSchedulerPoll(redis.native, outboundProducers, logger, db);\n if (isPolling) {\n pollTimeout = setTimeout(() => void pollLoop(), hasMore ? 0 : 5000);\n }\n } catch (err) {\n logger.error({ err }, \"scheduler poll loop error\");\n if (isPolling) {\n pollTimeout = setTimeout(() => void pollLoop(), 5000);\n }\n }\n };\n void pollLoop();\n\n // ─── Health check interval ──────────────────────────────────────────────────\n\n healthInterval = startHealthReporter(\"scheduler\", worker, redis, logger);\n\n logger.info({ env: config.NODE_ENV }, \"scheduler starting\");\n await worker.start();\n}\n\n// ─── Shutdown ──────────────────────────────────────────────────────────────\n\nexport async function stopSchedulerWorker(): Promise<void> {\n logger?.info(\"shutdown initiated\");\n if (healthInterval) {\n clearInterval(healthInterval);\n healthInterval = null;\n }\n if (pollTimeout) {\n clearTimeout(pollTimeout);\n pollTimeout = null;\n }\n isPolling = false;\n if (worker) await worker.stop();\n if (sql) await sql.end();\n if (redis) await redis.disconnect();\n logger?.info(\"scheduler stopped\");\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"main-DvgJSm11.mjs","names":["sql"],"sources":["../src/services/scheduler/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 {\n STREAMS,\n CONSUMER_GROUPS,\n registry,\n buildStreamEvent,\n type NotificationScheduledPayload,\n} from \"@/index.js\";\nimport { getPriorityBucket, type WorkerOptions, LUA_SCHEDULER_POLL } from \"@/shared/index.js\";\nimport { createDatabase } from \"@/db/index.js\";\nimport { scheduledPayloads } from \"@/db/schema.js\";\nimport { inArray } from \"drizzle-orm\";\nimport { startHealthReporter } from \"@/workers/index.js\";\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;\nlet pollTimeout: ReturnType<typeof setTimeout> | null = null;\nlet isPolling = false;\n\nexport interface SchedulerWorkerOptions extends WorkerOptions {\n registry: any;\n redis: Redis;\n}\n\nexport class SchedulerWorker extends BaseWorker {\n private readonly registry: any;\n private readonly redisCli: Redis;\n\n constructor(options: SchedulerWorkerOptions) {\n super(options);\n this.registry = options.registry;\n this.redisCli = options.redis;\n }\n async process(message: StreamMessage): Promise<void> {\n const { event } = message;\n\n const payloadResult = this.registry.safeParsePayload(\"notification.scheduled\", event.payload);\n if (!payloadResult.success) {\n this.logger.warn(\n { messageId: message.id, issues: payloadResult.error.issues },\n \"invalid notification.scheduled payload — skipping\",\n );\n return;\n }\n\n const scheduled = payloadResult.data as NotificationScheduledPayload;\n const scheduledAt = new Date(scheduled.scheduledAt).getTime();\n\n const taskData = JSON.stringify({\n taskId: scheduled.taskId,\n enrichedEventId: scheduled.enrichedEventId,\n traceId: event.metadata.traceId,\n });\n\n const shard = parseInt(scheduled.taskId.slice(-1), 16) || 0;\n const zsetKey = `notif:scheduled:zset:${shard}`;\n await this.redisCli.zadd(zsetKey, scheduledAt, taskData);\n this.logger.debug(\n { taskId: scheduled.taskId, scheduledAt: scheduled.scheduledAt, shard },\n \"scheduled task queued in ZSET\",\n );\n }\n}\n\nexport async function executeSchedulerPoll(\n redis: Redis,\n outboundProducers: any,\n logger: any,\n db: any,\n): Promise<boolean> {\n const lockKey = \"notif:lock:scheduler:poll\";\n const lockOwner = randomUUID();\n let acquired = false;\n try {\n const lockAcquired = await redis.set(lockKey, lockOwner, \"EX\", 30, \"NX\");\n if (lockAcquired !== \"OK\") {\n return false; // Another instance is currently polling\n }\n acquired = true;\n\n const now = Date.now();\n const visibilityTimeout = 60000; // 60 seconds\n const perShardLimit = 100;\n const pipeline = redis.pipeline();\n\n for (let i = 0; i < 16; i++) {\n pipeline.eval(\n LUA_SCHEDULER_POLL,\n 1,\n `notif:scheduled:zset:${i}`,\n now,\n perShardLimit,\n visibilityTimeout,\n );\n }\n\n const results = await pipeline.exec();\n const perShard = results?.map((res: any) => (res[0] ? [] : ((res[1] ?? []) as string[]))) ?? [];\n const tasks = perShard.flat();\n // A shard that came back full may still have due tasks behind it, which is\n // what tells the caller to poll again rather than wait for the next tick.\n const anyShardFull = perShard.some((shard) => shard.length >= perShardLimit);\n\n if (tasks.length === 0) return false;\n\n const parsedTasks = tasks\n .map((t) => {\n try {\n return { taskStr: t, ...JSON.parse(t) };\n } catch (err) {\n logger.error({ taskStr: t, err }, \"failed to parse scheduled task JSON\");\n return null;\n }\n })\n .filter(Boolean) as any[];\n if (parsedTasks.length === 0) return false;\n\n const taskIds = parsedTasks.map((t) => t.taskId);\n const dbPayloads = await db\n .select()\n .from(scheduledPayloads)\n .where(inArray(scheduledPayloads.taskId, taskIds));\n const payloadMap = new Map(dbPayloads.map((row: any) => [row.taskId, row.payload]));\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 const cleanupPipeline = redis.pipeline();\n const dbCleanupIds: string[] = [];\n\n for (let i = 0; i < parsedTasks.length; i++) {\n const { taskStr, taskId, traceId } = parsedTasks[i];\n const payload = payloadMap.get(taskId);\n\n if (!payload) {\n logger.warn({ taskId }, \"scheduled payload not found in Postgres — skipping release\");\n const shard = parseInt(taskId.slice(-1), 16) || 0;\n cleanupPipeline.zrem(`notif:scheduled:zset:${shard}`, taskStr);\n continue;\n }\n\n const dispatchPayload = payload as any;\n const p = getPriorityBucket(dispatchPayload.priority);\n\n batchedEvents[p].push(\n buildStreamEvent(\"notification.dispatched\", dispatchPayload, \"scheduler\", traceId),\n );\n\n dbCleanupIds.push(taskId);\n const shard = parseInt(taskId.slice(-1), 16) || 0;\n cleanupPipeline.zrem(`notif:scheduled:zset:${shard}`, taskStr);\n }\n\n // Every bucket, not a hardcoded subset: a task binned into one that is not\n // published here has already been added to the cleanup list, so it would be\n // deleted from Redis and Postgres without ever being sent.\n for (const p of Object.keys(batchedEvents) as (keyof typeof batchedEvents)[]) {\n if (batchedEvents[p].length > 0) {\n const producer = outboundProducers[p] ?? outboundProducers[\"normal\"]!;\n await producer!.publishBatch(batchedEvents[p]);\n }\n }\n\n if (dbCleanupIds.length > 0) {\n try {\n await db.delete(scheduledPayloads).where(inArray(scheduledPayloads.taskId, dbCleanupIds));\n } catch (err) {\n logger.error(\n { err },\n \"failed to delete scheduled payloads from Postgres, continuing with Redis cleanup\",\n );\n }\n }\n await cleanupPipeline.exec();\n\n logger.info({ count: parsedTasks.length }, \"scheduled tasks released to outbound\");\n\n // \"Poll again immediately\" — the caller loops while this is true. The old\n // test was `tasks.length === 100` against a total drawn from 16 shards, so\n // a real backlog reported \"nothing more\" and waited for the next tick.\n return anyShardFull;\n } catch (err) {\n logger.error({ err }, \"error in scheduler polling loop\");\n return false;\n } finally {\n if (acquired) {\n // Delete the lock only if we still own it\n const lua = `\n if redis.call(\"get\", KEYS[1]) == ARGV[1] then\n return redis.call(\"del\", KEYS[1])\n else\n return 0\n end\n `;\n await redis.eval(lua, 1, lockKey, lockOwner).catch(() => {});\n }\n }\n}\n\nexport async function startSchedulerWorker() {\n logger = createLogger({ name: \"scheduler\", level: config.LOG_LEVEL });\n redis = new RedisClient({ url: config.REDIS_URL, name: \"scheduler\", logger });\n const dbData = createDatabase({ url: config.DATABASE_URL, applicationName: \"scheduler\", logger });\n sql = dbData.sql;\n db = dbData.db;\n consumer = new StreamConsumer({\n redis: redis.native,\n stream: STREAMS.SCHEDULED,\n group: CONSUMER_GROUPS.SCHEDULER,\n consumer: `scheduler-${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.SCHEDULED,\n group: CONSUMER_GROUPS.SCHEDULER,\n consumer: `scheduler-${process.pid}`,\n logger,\n });\n\n const outboundProducers = {\n critical: new StreamProducer({\n redis: redis.native,\n stream: STREAMS.OUTBOUND_CRITICAL,\n logger,\n }),\n normal: new StreamProducer({ redis: redis.native, stream: STREAMS.OUTBOUND_NORMAL, logger }),\n low: new StreamProducer({ redis: redis.native, stream: STREAMS.OUTBOUND_LOW, logger }),\n };\n\n // ─── Stage 4a: Scheduled notification processor ─────────────────────────────\n //\n // Reads from SCHEDULED stream, parses, and queues tasks in a Redis ZSET.\n // A background polling interval releases tasks when they are due.\n\n worker = new SchedulerWorker({\n consumer,\n pendingScanner,\n logger,\n concurrency: config.WORKER_CONCURRENCY,\n registry,\n redis: redis.native,\n });\n\n // ─── Polling Loop ─────────────────────────────────────────────────────────\n isPolling = true;\n const pollLoop = async (): Promise<void> => {\n if (!isPolling) return;\n try {\n const hasMore = await executeSchedulerPoll(redis.native, outboundProducers, logger, db);\n if (isPolling) {\n pollTimeout = setTimeout(() => void pollLoop(), hasMore ? 0 : 5000);\n }\n } catch (err) {\n logger.error({ err }, \"scheduler poll loop error\");\n if (isPolling) {\n pollTimeout = setTimeout(() => void pollLoop(), 5000);\n }\n }\n };\n void pollLoop();\n\n // ─── Health check interval ──────────────────────────────────────────────────\n\n healthInterval = startHealthReporter(\"scheduler\", worker, redis, logger);\n\n logger.info({ env: config.NODE_ENV }, \"scheduler starting\");\n await worker.start();\n}\n\n// ─── Shutdown ──────────────────────────────────────────────────────────────\n\nexport async function stopSchedulerWorker(): Promise<void> {\n logger?.info(\"shutdown initiated\");\n if (healthInterval) {\n clearInterval(healthInterval);\n healthInterval = null;\n }\n if (pollTimeout) {\n clearTimeout(pollTimeout);\n pollTimeout = null;\n }\n isPolling = false;\n if (worker) await worker.stop();\n if (sql) await sql.end();\n if (redis) await redis.disconnect();\n logger?.info(\"scheduler stopped\");\n}\n"],"mappings":"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"}
@@ -116,7 +116,11 @@ const NotificationChannelSchema = z.enum([
116
116
  "sms",
117
117
  "push",
118
118
  "webhook",
119
- "in-app"
119
+ "in-app",
120
+ "telegram",
121
+ "discord",
122
+ "whatsapp",
123
+ "slack"
120
124
  ]);
121
125
  const NotificationPrioritySchema = z.enum([
122
126
  "low",
@@ -281,12 +285,16 @@ const PreferencesSchema = z.object({
281
285
  topics: z.record(z.string(), z.boolean()).optional(),
282
286
  quietHours: z.array(QuietHoursSchema).optional()
283
287
  });
284
- /** Channels that carry an addressable target (email address, phone, push token, url). */
288
+ /** Channels that carry an addressable target (email address, phone, push token, url, chat id). */
285
289
  const ContactChannelSchema = z.enum([
286
290
  "email",
287
291
  "sms",
288
292
  "push",
289
- "webhook"
293
+ "webhook",
294
+ "telegram",
295
+ "discord",
296
+ "whatsapp",
297
+ "slack"
290
298
  ]);
291
299
  /** Accept a single value or an array; always normalise to a non-empty array. */
292
300
  const stringOrArray = z.union([z.string().min(1), z.array(z.string().min(1))]).transform((v) => Array.isArray(v) ? v : [v]);
@@ -501,6 +509,9 @@ const RecipientProfileSchema = z.object({
501
509
  webhook: z.string().url().optional(),
502
510
  pushTokens: z.array(z.string()).optional(),
503
511
  pushToken: z.string().optional(),
512
+ telegram: z.string().optional(),
513
+ discord: z.string().url().optional(),
514
+ slack: z.string().optional(),
504
515
  locale: z.string().default("en"),
505
516
  timezone: z.string().default("UTC"),
506
517
  preferences: z.object({
@@ -700,7 +711,11 @@ const channelEnum = pgEnum("channel", [
700
711
  "sms",
701
712
  "push",
702
713
  "webhook",
703
- "in-app"
714
+ "in-app",
715
+ "telegram",
716
+ "discord",
717
+ "whatsapp",
718
+ "slack"
704
719
  ]);
705
720
  const projects = pgTable("projects", {
706
721
  id: uuid("id").primaryKey().defaultRandom(),
@@ -1855,9 +1870,16 @@ var RedisClient = class {
1855
1870
  }
1856
1871
  }
1857
1872
  async disconnect() {
1873
+ if (this.isClosing) return;
1858
1874
  this.isClosing = true;
1859
1875
  this.logger?.info("disconnecting redis");
1860
- await this.native.quit();
1876
+ try {
1877
+ if (this.native.status !== "end" && this.native.status !== "close") await this.native.quit();
1878
+ } catch {
1879
+ try {
1880
+ this.native.disconnect();
1881
+ } catch {}
1882
+ }
1861
1883
  this.logger?.info("redis disconnected");
1862
1884
  }
1863
1885
  };
@@ -3270,6 +3292,8 @@ var NotifkitServer = class extends EventEmitter {
3270
3292
  redisContainer = null;
3271
3293
  eventCleanupFns = [];
3272
3294
  signalHandlersAttached = false;
3295
+ sigintHandler;
3296
+ sigtermHandler;
3273
3297
  logger;
3274
3298
  constructor(options) {
3275
3299
  super();
@@ -3349,35 +3373,35 @@ var NotifkitServer = class extends EventEmitter {
3349
3373
  }
3350
3374
  const startupPromises = [];
3351
3375
  if (services.includes("api")) {
3352
- const { startApiServer } = await import("./main-DtHWhueo.mjs");
3376
+ const { startApiServer } = await import("./main-40zwq6b0.mjs");
3353
3377
  startupPromises.push(startApiServer());
3354
3378
  }
3355
3379
  if (services.includes("delivery")) {
3356
- const { startDeliveryWorker } = await import("./main-CCfc45ev.mjs");
3380
+ const { startDeliveryWorker } = await import("./main-CiigNpsP.mjs");
3357
3381
  startupPromises.push(startDeliveryWorker());
3358
3382
  }
3359
3383
  if (services.includes("engine")) {
3360
- const { startEngineWorker } = await import("./main-B561M1d3.mjs");
3384
+ const { startEngineWorker } = await import("./main-BOPMYqsW.mjs");
3361
3385
  startupPromises.push(startEngineWorker());
3362
3386
  }
3363
3387
  if (services.includes("enricher")) {
3364
- const { startEnricherWorker } = await import("./main-Ce9dcrsg.mjs");
3388
+ const { startEnricherWorker } = await import("./main-DeNFQ-UL.mjs");
3365
3389
  startupPromises.push(startEnricherWorker());
3366
3390
  }
3367
3391
  if (services.includes("scheduler")) {
3368
- const { startSchedulerWorker } = await import("./main-C45e7grq.mjs");
3392
+ const { startSchedulerWorker } = await import("./main-DvgJSm11.mjs");
3369
3393
  startupPromises.push(startSchedulerWorker());
3370
3394
  }
3371
3395
  if (services.includes("ai")) {
3372
- const { startAiWorker } = await import("./main-CAH0_Q6d.mjs");
3396
+ const { startAiWorker } = await import("./main-BNJtzY61.mjs");
3373
3397
  startupPromises.push(startAiWorker());
3374
3398
  }
3375
3399
  if (services.includes("workflow")) {
3376
- const { startWorkflowWorker } = await import("./main-DyfbnJc3.mjs");
3400
+ const { startWorkflowWorker } = await import("./main-BFre2-HQ.mjs");
3377
3401
  startupPromises.push(startWorkflowWorker());
3378
3402
  }
3379
3403
  if (services.includes("events")) {
3380
- const { startEventWorker } = await import("./main-B-jwm8ED.mjs");
3404
+ const { startEventWorker } = await import("./main-DmCPcxOc.mjs");
3381
3405
  startupPromises.push(startEventWorker());
3382
3406
  }
3383
3407
  const handleSignal = async (signal) => {
@@ -3386,17 +3410,24 @@ var NotifkitServer = class extends EventEmitter {
3386
3410
  process.exit(0);
3387
3411
  };
3388
3412
  if (!this.signalHandlersAttached) {
3389
- process.once("SIGINT", () => {
3413
+ this.sigintHandler = () => {
3390
3414
  handleSignal("SIGINT");
3391
- });
3392
- process.once("SIGTERM", () => {
3415
+ };
3416
+ this.sigtermHandler = () => {
3393
3417
  handleSignal("SIGTERM");
3394
- });
3418
+ };
3419
+ process.once("SIGINT", this.sigintHandler);
3420
+ process.once("SIGTERM", this.sigtermHandler);
3395
3421
  this.signalHandlersAttached = true;
3396
3422
  }
3397
3423
  await Promise.all(startupPromises);
3398
3424
  }
3399
3425
  async stop() {
3426
+ if (this.signalHandlersAttached) {
3427
+ if (this.sigintHandler) process.removeListener("SIGINT", this.sigintHandler);
3428
+ if (this.sigtermHandler) process.removeListener("SIGTERM", this.sigtermHandler);
3429
+ this.signalHandlersAttached = false;
3430
+ }
3400
3431
  for (const cleanup of this.eventCleanupFns) cleanup();
3401
3432
  this.eventCleanupFns = [];
3402
3433
  const services = this.options.services.includes("all") ? [
@@ -3410,35 +3441,35 @@ var NotifkitServer = class extends EventEmitter {
3410
3441
  "events"
3411
3442
  ] : this.options.services;
3412
3443
  if (services.includes("api")) {
3413
- const { stopApiServer } = await import("./main-DtHWhueo.mjs");
3444
+ const { stopApiServer } = await import("./main-40zwq6b0.mjs");
3414
3445
  await stopApiServer();
3415
3446
  }
3416
3447
  if (services.includes("delivery")) {
3417
- const { stopDeliveryWorker } = await import("./main-CCfc45ev.mjs");
3448
+ const { stopDeliveryWorker } = await import("./main-CiigNpsP.mjs");
3418
3449
  await stopDeliveryWorker();
3419
3450
  }
3420
3451
  if (services.includes("engine")) {
3421
- const { stopEngineWorker } = await import("./main-B561M1d3.mjs");
3452
+ const { stopEngineWorker } = await import("./main-BOPMYqsW.mjs");
3422
3453
  await stopEngineWorker();
3423
3454
  }
3424
3455
  if (services.includes("enricher")) {
3425
- const { stopEnricherWorker } = await import("./main-Ce9dcrsg.mjs");
3456
+ const { stopEnricherWorker } = await import("./main-DeNFQ-UL.mjs");
3426
3457
  await stopEnricherWorker();
3427
3458
  }
3428
3459
  if (services.includes("scheduler")) {
3429
- const { stopSchedulerWorker } = await import("./main-C45e7grq.mjs");
3460
+ const { stopSchedulerWorker } = await import("./main-DvgJSm11.mjs");
3430
3461
  await stopSchedulerWorker();
3431
3462
  }
3432
3463
  if (services.includes("ai")) {
3433
- const { stopAiWorker } = await import("./main-CAH0_Q6d.mjs");
3464
+ const { stopAiWorker } = await import("./main-BNJtzY61.mjs");
3434
3465
  await stopAiWorker();
3435
3466
  }
3436
3467
  if (services.includes("workflow")) {
3437
- const { stopWorkflowWorker } = await import("./main-DyfbnJc3.mjs");
3468
+ const { stopWorkflowWorker } = await import("./main-BFre2-HQ.mjs");
3438
3469
  await stopWorkflowWorker();
3439
3470
  }
3440
3471
  if (services.includes("events")) {
3441
- const { stopEventWorker } = await import("./main-B-jwm8ED.mjs");
3472
+ const { stopEventWorker } = await import("./main-DmCPcxOc.mjs");
3442
3473
  await stopEventWorker();
3443
3474
  }
3444
3475
  if (this.pgContainer) {
@@ -3454,4 +3485,4 @@ var NotifkitServer = class extends EventEmitter {
3454
3485
  //#endregion
3455
3486
  export { childLogger as $, PUBSUB_CHANNELS as $t, ProjectSettingsCache as A, NotificationRequestedPayloadSchema as At, LUA_RELEASE_LOCK as B, QuietHoursSchema as Bt, ProjectRepository as C, DeliveryOptionsSchema as Ct, WorkflowRepository as D, NotificationEnrichedPayloadSchema as Dt, UserRepository as E, NotificationScheduledPayloadSchema as Et, sleep as F, CreateWorkflowSchema as Ft, normaliseTarget as G, UpdateUserSchema as Gt, LUA_SCHEDULER_CLAIM as H, TemplateSchema as Ht, DataLoader as I, IngestEventSchema as It, PendingMessageScanner as J, buildStreamEvent as Jt, LRUCache as K, WorkflowNotifyPayloadSchema as Kt, CircuitBreaker as L, InlineUserSchema as Lt, AppError as M, AddContactSchema as Mt, ValidationError as N, AddUserSchema as Nt, Redis as O, RecipientProfileSchema as Ot, generateId as P, ContactChannelSchema as Pt, metrics as Q, OUTBOUND_STREAMS as Qt, BatchProcessor as R, NotifyRequestSchema as Rt, PreferenceRepository as S, NotificationDeliveredPayloadSchema as St, TemplateRepository as T, RenderedContentSchema as Tt, LUA_SCHEDULER_POLL as U, TriggerWorkflowSchema as Ut, LUA_RENEW_LOCK as V, SyncTemplatesSchema as Vt, getPriorityBucket as W, UpdateProjectSchema as Wt, StreamProducer as X, ENRICHED_STREAMS as Xt, StreamConsumer as Y, CONSUMER_GROUPS as Yt, getMetricsRegistry as Z, INBOUND_STREAMS as Zt, escapeHeader as _, setGlobalConfig as _n, workflowWaiters as _t, SuspendExecutionError as a, registry as an, runMigrations as at, renderWithTemplate as b, NotificationSkippedPayloadSchema as bt, BaseWorker as c, NotificationPrioritySchema as cn, projectApiKeys as ct, buildUnsubscribeHeaders as d, baseConfigSchema as dn, suppressions as dt, STREAMS as en, createLogger as et, signUnsubscribeToken as f, getAiConfig as fn, userTopicPreferences as ft, TemplateCache as g, setAiConfig as gn, workflowSteps as gt, templateRegistry as h, readBaseConfig as hn, workflowInstances as ht, workflowRegistry as i, EventRegistry as in, createDatabase as it, UserThrottle as j, NotificationTargetSchema as jt, RedisClient as k, NotificationCreatedPayloadSchema as kt, NonRetryableError as l, NotificationStatusSchema as ln, projects as lt, renderTemplate as m, parseConfig as mn, workflowDefinitions as mt, NotifkitClient as n, StreamEventMetadataSchema as nn, withRequestId as nt, buildStepNotifyPayload as o, EventMetadataSchema as on, deliveryOutbox as ot, verifyUnsubscribeToken as p, loadEnv as pn, users as pt, globalEmitter as q, WorkflowStepSchema as qt, workflow as r, StreamEventSchema as rn, IdempotencyGuard as rt, resolveStepTarget as s, NotificationChannelSchema as sn, messageLogs as st, NotifkitServer as t, EventEnvelopeSchema as tn, withContext as tt, startHealthReporter as u, AI_DEFAULTS as un, scheduledPayloads as ut, escapeHtml as v, NotificationAiPendingPayloadSchema as vt, SegmentRepository as w, NotificationDispatchedPayloadSchema as wt, ContactRepository as x, NotificationFailedPayloadSchema as xt, interpolate as y, NotificationCanceledPayloadSchema as yt, AsyncSemaphore as z, PreferencesSchema as zt };
3456
3487
 
3457
- //# sourceMappingURL=src-C-PfEDMY.mjs.map
3488
+ //# sourceMappingURL=src-vG79L-8m.mjs.map