@motiadev/adapter-bullmq-events 0.13.0-beta.162-717198

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 (44) hide show
  1. package/LICENSE +93 -0
  2. package/README.md +222 -0
  3. package/dist/bullmq-event-adapter.d.ts +27 -0
  4. package/dist/bullmq-event-adapter.d.ts.map +1 -0
  5. package/dist/bullmq-event-adapter.js +75 -0
  6. package/dist/config-builder.d.ts +6 -0
  7. package/dist/config-builder.d.ts.map +1 -0
  8. package/dist/config-builder.js +29 -0
  9. package/dist/connection-manager.d.ts +10 -0
  10. package/dist/connection-manager.d.ts.map +1 -0
  11. package/dist/connection-manager.js +39 -0
  12. package/dist/constants.d.ts +14 -0
  13. package/dist/constants.d.ts.map +1 -0
  14. package/dist/constants.js +16 -0
  15. package/dist/dlq-manager.d.ts +22 -0
  16. package/dist/dlq-manager.d.ts.map +1 -0
  17. package/dist/dlq-manager.js +112 -0
  18. package/dist/errors.d.ts +14 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +35 -0
  21. package/dist/index.d.ts +7 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +11 -0
  24. package/dist/queue-manager.d.ts +20 -0
  25. package/dist/queue-manager.d.ts.map +1 -0
  26. package/dist/queue-manager.js +85 -0
  27. package/dist/types.d.ts +23 -0
  28. package/dist/types.d.ts.map +1 -0
  29. package/dist/types.js +2 -0
  30. package/dist/worker-manager.d.ts +38 -0
  31. package/dist/worker-manager.d.ts.map +1 -0
  32. package/dist/worker-manager.js +136 -0
  33. package/package.json +25 -0
  34. package/src/bullmq-event-adapter.ts +105 -0
  35. package/src/config-builder.ts +41 -0
  36. package/src/connection-manager.ts +40 -0
  37. package/src/constants.ts +14 -0
  38. package/src/dlq-manager.ts +151 -0
  39. package/src/errors.ts +33 -0
  40. package/src/index.ts +6 -0
  41. package/src/queue-manager.ts +107 -0
  42. package/src/types.ts +24 -0
  43. package/src/worker-manager.ts +200 -0
  44. package/tsconfig.json +19 -0
@@ -0,0 +1,41 @@
1
+ import {
2
+ DEFAULT_ATTEMPTS,
3
+ DEFAULT_BACKOFF_DELAY,
4
+ DEFAULT_CONCURRENCY,
5
+ DEFAULT_DLQ_SUFFIX,
6
+ DEFAULT_DLQ_TTL,
7
+ DEFAULT_PREFIX,
8
+ DEFAULT_REMOVE_ON_COMPLETE_COUNT,
9
+ DEFAULT_REMOVE_ON_FAIL_COUNT,
10
+ } from './constants'
11
+ import type { BullMQEventAdapterConfig } from './types'
12
+
13
+ export type MergedConfig = Required<Pick<BullMQEventAdapterConfig, 'defaultJobOptions' | 'prefix' | 'concurrency'>> & {
14
+ dlq: Required<NonNullable<BullMQEventAdapterConfig['dlq']>>
15
+ }
16
+
17
+ export function buildConfig(config: BullMQEventAdapterConfig): MergedConfig {
18
+ return {
19
+ concurrency: config.concurrency ?? DEFAULT_CONCURRENCY,
20
+ defaultJobOptions: {
21
+ attempts: DEFAULT_ATTEMPTS,
22
+ backoff: {
23
+ type: 'fixed',
24
+ delay: DEFAULT_BACKOFF_DELAY,
25
+ },
26
+ removeOnComplete: {
27
+ count: DEFAULT_REMOVE_ON_COMPLETE_COUNT,
28
+ },
29
+ removeOnFail: {
30
+ count: DEFAULT_REMOVE_ON_FAIL_COUNT,
31
+ },
32
+ ...config.defaultJobOptions,
33
+ },
34
+ prefix: config.prefix ?? DEFAULT_PREFIX,
35
+ dlq: {
36
+ enabled: config.dlq?.enabled ?? true,
37
+ ttl: config.dlq?.ttl ?? DEFAULT_DLQ_TTL,
38
+ suffix: config.dlq?.suffix ?? DEFAULT_DLQ_SUFFIX,
39
+ },
40
+ }
41
+ }
@@ -0,0 +1,40 @@
1
+ import IORedis, { type Redis } from 'ioredis'
2
+ import { ConnectionError } from './errors'
3
+ import type { BullMQConnectionConfig } from './types'
4
+
5
+ export class ConnectionManager {
6
+ readonly connection: Redis
7
+ readonly ownsConnection: boolean
8
+
9
+ constructor(config: BullMQConnectionConfig) {
10
+ if (config instanceof IORedis) {
11
+ this.connection = config
12
+ this.ownsConnection = false
13
+ } else {
14
+ this.connection = new IORedis({
15
+ maxRetriesPerRequest: null,
16
+ ...config,
17
+ })
18
+ this.ownsConnection = true
19
+ }
20
+
21
+ this.setupEventHandlers()
22
+ }
23
+
24
+ private setupEventHandlers(): void {
25
+ this.connection.on('error', (err: Error) => {
26
+ const error = new ConnectionError(err.message, err)
27
+ console.error('[BullMQ] Connection error:', error)
28
+ })
29
+
30
+ this.connection.on('close', () => {
31
+ console.warn('[BullMQ] Connection closed')
32
+ })
33
+ }
34
+
35
+ async close(): Promise<void> {
36
+ if (this.ownsConnection && this.connection.status !== 'end') {
37
+ await this.connection.quit()
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,14 @@
1
+ export const DEFAULT_CONCURRENCY = 5
2
+ export const DEFAULT_ATTEMPTS = 3
3
+ export const DEFAULT_BACKOFF_DELAY = 2000
4
+ export const DEFAULT_REMOVE_ON_COMPLETE_COUNT = 1000
5
+ export const DEFAULT_REMOVE_ON_FAIL_COUNT = 5000
6
+ export const DEFAULT_PREFIX = 'motia'
7
+ export const FIFO_CONCURRENCY = 1
8
+ export const MILLISECONDS_PER_SECOND = 1000
9
+ export const SECONDS_PER_DAY = 86400
10
+ export const DEFAULT_DLQ_TTL = 30 * SECONDS_PER_DAY
11
+ export const DEFAULT_DLQ_SUFFIX = '.dlq'
12
+ export const DLQ_JOB_PREFIX = 'dlq-'
13
+
14
+ export const LOG_PREFIX = '[BullMQ]'
@@ -0,0 +1,151 @@
1
+ import type { Event } from '@motiadev/core'
2
+ import { Queue } from 'bullmq'
3
+ import type { Redis } from 'ioredis'
4
+ import type { MergedConfig } from './config-builder'
5
+ import { DLQ_JOB_PREFIX, LOG_PREFIX, MILLISECONDS_PER_SECOND } from './constants'
6
+ import { QueueCreationError } from './errors'
7
+
8
+ type DLQJobData<TData> = {
9
+ originalEvent: Event<TData>
10
+ failureReason: string
11
+ failureTimestamp: number
12
+ attemptsMade: number
13
+ originalJobId?: string
14
+ }
15
+
16
+ export class DLQManager {
17
+ private readonly dlqQueues: Map<string, Queue> = new Map()
18
+ private readonly connection: Redis
19
+ private readonly config: MergedConfig
20
+
21
+ constructor(connection: Redis, config: MergedConfig) {
22
+ this.connection = connection
23
+ this.config = config
24
+ }
25
+
26
+ getDLQQueueName(topic: string, stepName: string): string {
27
+ const baseQueueName = `${topic}.${stepName}`
28
+ return `${baseQueueName}${this.config.dlq.suffix}`
29
+ }
30
+
31
+ private getOrCreateDLQQueue(queueName: string): Queue {
32
+ if (!this.dlqQueues.has(queueName)) {
33
+ const ttlMs = this.config.dlq.ttl * MILLISECONDS_PER_SECOND
34
+ const queue = new Queue(queueName, {
35
+ connection: this.connection,
36
+ prefix: this.config.prefix,
37
+ defaultJobOptions: {
38
+ removeOnComplete: {
39
+ age: ttlMs,
40
+ },
41
+ removeOnFail: {
42
+ age: ttlMs,
43
+ },
44
+ },
45
+ })
46
+
47
+ queue.on('error', (err: Error) => {
48
+ console.error(`${LOG_PREFIX} DLQ error for ${queueName}:`, err)
49
+ })
50
+
51
+ this.dlqQueues.set(queueName, queue)
52
+ }
53
+
54
+ const queue = this.dlqQueues.get(queueName)
55
+ if (!queue) {
56
+ throw new QueueCreationError(queueName)
57
+ }
58
+ return queue
59
+ }
60
+
61
+ async moveToDLQ<TData>(
62
+ topic: string,
63
+ stepName: string,
64
+ event: Event<TData>,
65
+ error: Error,
66
+ attemptsMade: number,
67
+ originalJobId?: string,
68
+ ): Promise<void> {
69
+ if (!this.config.dlq!.enabled) {
70
+ console.warn(`${LOG_PREFIX} DLQ is disabled, skipping move to DLQ for topic ${topic}, step ${stepName}`)
71
+ return
72
+ }
73
+
74
+ try {
75
+ const dlqQueueName = this.getDLQQueueName(topic, stepName)
76
+ const dlqQueue = this.getOrCreateDLQQueue(dlqQueueName)
77
+
78
+ const sanitizedEvent = {
79
+ topic: event.topic,
80
+ data: event.data,
81
+ traceId: event.traceId || 'unknown',
82
+ ...(event.flows && { flows: event.flows }),
83
+ ...(event.messageGroupId && { messageGroupId: event.messageGroupId }),
84
+ } as Event<TData>
85
+
86
+ const dlqJobData: DLQJobData<TData> = {
87
+ originalEvent: sanitizedEvent,
88
+ failureReason: error.message || 'Unknown error',
89
+ failureTimestamp: Date.now(),
90
+ attemptsMade,
91
+ ...(originalJobId && { originalJobId }),
92
+ }
93
+
94
+ const jobOptions = originalJobId ? { jobId: `${DLQ_JOB_PREFIX}${originalJobId}` } : {}
95
+
96
+ await dlqQueue.add(`${topic}.dlq`, dlqJobData, jobOptions)
97
+
98
+ console.warn(`${LOG_PREFIX} Moved failed job to DLQ: ${dlqQueueName}`, {
99
+ topic,
100
+ stepName,
101
+ attemptsMade,
102
+ error: error.message,
103
+ })
104
+ } catch (err) {
105
+ const dlqError = err instanceof Error ? err : new Error(String(err))
106
+ console.error(`${LOG_PREFIX} Failed to move job to DLQ for topic ${topic}, step ${stepName}:`, dlqError)
107
+ }
108
+ }
109
+
110
+ async closeDLQQueue(queueName: string): Promise<void> {
111
+ const queue = this.dlqQueues.get(queueName)
112
+ if (queue) {
113
+ await queue.close()
114
+ this.dlqQueues.delete(queueName)
115
+ }
116
+ }
117
+
118
+ async closeAll(): Promise<void> {
119
+ const promises = Array.from(this.dlqQueues.values()).map((queue) =>
120
+ queue.close().catch((err) => {
121
+ console.error(`${LOG_PREFIX} Error closing DLQ queue:`, err)
122
+ }),
123
+ )
124
+ await Promise.allSettled(promises)
125
+ this.dlqQueues.clear()
126
+ }
127
+
128
+ getDLQQueue(queueName: string): Queue | undefined {
129
+ return this.dlqQueues.get(queueName)
130
+ }
131
+
132
+ getOrCreateDLQ(queueName: string): Queue {
133
+ return this.getOrCreateDLQQueue(queueName)
134
+ }
135
+
136
+ listDLQQueueNames(): string[] {
137
+ return Array.from(this.dlqQueues.keys())
138
+ }
139
+
140
+ getDLQSuffix(): string {
141
+ return this.config.dlq.suffix
142
+ }
143
+
144
+ getPrefix(): string {
145
+ return this.config.prefix
146
+ }
147
+
148
+ getConnection(): Redis {
149
+ return this.connection
150
+ }
151
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,33 @@
1
+ export class BullMQAdapterError extends Error {
2
+ constructor(
3
+ message: string,
4
+ public readonly cause?: Error,
5
+ ) {
6
+ super(message)
7
+ this.name = 'BullMQAdapterError'
8
+ if (cause) {
9
+ this.stack = `${this.stack}\nCaused by: ${cause.stack}`
10
+ }
11
+ }
12
+ }
13
+
14
+ export class QueueCreationError extends BullMQAdapterError {
15
+ constructor(queueName: string, cause?: Error) {
16
+ super(`Failed to create queue: ${queueName}`, cause)
17
+ this.name = 'QueueCreationError'
18
+ }
19
+ }
20
+
21
+ export class WorkerCreationError extends BullMQAdapterError {
22
+ constructor(topic: string, stepName: string, cause?: Error) {
23
+ super(`Failed to create worker for topic ${topic}, step ${stepName}`, cause)
24
+ this.name = 'WorkerCreationError'
25
+ }
26
+ }
27
+
28
+ export class ConnectionError extends BullMQAdapterError {
29
+ constructor(message: string, cause?: Error) {
30
+ super(`Connection error: ${message}`, cause)
31
+ this.name = 'ConnectionError'
32
+ }
33
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { BullMQEventAdapter } from './bullmq-event-adapter'
2
+ export { DLQManager } from './dlq-manager'
3
+ export { QueueManager } from './queue-manager'
4
+ export type { BullMQConnectionConfig, BullMQEventAdapterConfig } from './types'
5
+ export type { SubscriberInfo } from './worker-manager'
6
+ export { WorkerManager } from './worker-manager'
@@ -0,0 +1,107 @@
1
+ import type { Event } from '@motiadev/core'
2
+ import { Queue } from 'bullmq'
3
+ import type { Redis } from 'ioredis'
4
+ import type { MergedConfig } from './config-builder'
5
+ import { MILLISECONDS_PER_SECOND } from './constants'
6
+ import { QueueCreationError } from './errors'
7
+ import type { SubscriberInfo } from './worker-manager'
8
+
9
+ export class QueueManager {
10
+ private readonly queues: Map<string, Queue> = new Map()
11
+ private readonly connection: Redis
12
+ private readonly config: MergedConfig
13
+
14
+ constructor(connection: Redis, config: MergedConfig) {
15
+ this.connection = connection
16
+ this.config = config
17
+ }
18
+
19
+ getQueue(queueName: string): Queue {
20
+ if (!this.queues.has(queueName)) {
21
+ const queue = new Queue(queueName, {
22
+ connection: this.connection,
23
+ prefix: this.config.prefix,
24
+ defaultJobOptions: this.config.defaultJobOptions,
25
+ })
26
+
27
+ queue.on('error', (err: Error) => {
28
+ console.error(`[BullMQ] Queue error for ${queueName}:`, err)
29
+ })
30
+
31
+ this.queues.set(queueName, queue)
32
+ }
33
+
34
+ const queue = this.queues.get(queueName)
35
+ if (!queue) {
36
+ throw new QueueCreationError(queueName)
37
+ }
38
+ return queue
39
+ }
40
+
41
+ getQueueName(topic: string, stepName: string): string {
42
+ return `${topic}.${stepName}`
43
+ }
44
+
45
+ async enqueueToAll<TData>(event: Event<TData>, subscribers: SubscriberInfo[]): Promise<void> {
46
+ const promises = subscribers.map((subscriber) => {
47
+ const queueName = this.getQueueName(subscriber.topic, subscriber.stepName)
48
+ const queue = this.getQueue(queueName)
49
+ const jobId = event.messageGroupId ? `${queueName}.${event.messageGroupId}` : undefined
50
+
51
+ const jobData = {
52
+ topic: event.topic,
53
+ data: event.data,
54
+ traceId: event.traceId,
55
+ flows: event.flows,
56
+ messageGroupId: event.messageGroupId,
57
+ }
58
+
59
+ const maxRetries = subscriber.queueConfig?.maxRetries
60
+ const attempts = maxRetries != null ? maxRetries + 1 : this.config.defaultJobOptions.attempts
61
+ const delay = subscriber.queueConfig?.delaySeconds
62
+ ? subscriber.queueConfig.delaySeconds * MILLISECONDS_PER_SECOND
63
+ : undefined
64
+
65
+ const jobOptions = {
66
+ jobId,
67
+ attempts,
68
+ backoff: this.config.defaultJobOptions.backoff,
69
+ delay,
70
+ }
71
+
72
+ return queue.add(event.topic, jobData, jobOptions).then(() => undefined)
73
+ })
74
+
75
+ await Promise.all(promises)
76
+ }
77
+
78
+ async closeQueue(queueName: string): Promise<void> {
79
+ const queue = this.queues.get(queueName)
80
+ if (queue) {
81
+ await queue.close()
82
+ this.queues.delete(queueName)
83
+ }
84
+ }
85
+
86
+ async closeAll(): Promise<void> {
87
+ const promises = Array.from(this.queues.values()).map((queue) =>
88
+ queue.close().catch((err) => {
89
+ console.error(`[BullMQ] Error closing queue:`, err)
90
+ }),
91
+ )
92
+ await Promise.allSettled(promises)
93
+ this.queues.clear()
94
+ }
95
+
96
+ listQueueNames(): string[] {
97
+ return Array.from(this.queues.keys())
98
+ }
99
+
100
+ getPrefix(): string {
101
+ return this.config.prefix
102
+ }
103
+
104
+ getConnection(): Redis {
105
+ return this.connection
106
+ }
107
+ }
package/src/types.ts ADDED
@@ -0,0 +1,24 @@
1
+ import type { KeepJobs } from 'bullmq'
2
+ import type { Redis, RedisOptions } from 'ioredis'
3
+
4
+ export type BullMQConnectionConfig = Redis | RedisOptions
5
+
6
+ export interface BullMQEventAdapterConfig {
7
+ connection: BullMQConnectionConfig
8
+ concurrency?: number
9
+ defaultJobOptions?: {
10
+ attempts?: number
11
+ backoff?: {
12
+ type: 'fixed' | 'exponential'
13
+ delay: number
14
+ }
15
+ removeOnComplete?: KeepJobs
16
+ removeOnFail?: KeepJobs
17
+ }
18
+ prefix?: string
19
+ dlq?: {
20
+ enabled?: boolean
21
+ ttl?: number
22
+ suffix?: string
23
+ }
24
+ }
@@ -0,0 +1,200 @@
1
+ import type { Event, QueueConfig, SubscriptionHandle } from '@motiadev/core'
2
+ import { type Job, Worker } from 'bullmq'
3
+ import type { Redis } from 'ioredis'
4
+ import { v4 as uuidv4 } from 'uuid'
5
+ import type { MergedConfig } from './config-builder'
6
+ import { FIFO_CONCURRENCY, MILLISECONDS_PER_SECOND } from './constants'
7
+ import type { DLQManager } from './dlq-manager'
8
+ import { WorkerCreationError } from './errors'
9
+
10
+ export type SubscriberInfo = {
11
+ topic: string
12
+ stepName: string
13
+ queueConfig?: QueueConfig
14
+ }
15
+
16
+ type WorkerInfo = {
17
+ worker: Worker
18
+ topic: string
19
+ stepName: string
20
+ handle: SubscriptionHandle
21
+ queueConfig?: QueueConfig
22
+ }
23
+
24
+ type JobData<TData> = {
25
+ topic: string
26
+ data: TData
27
+ traceId: string
28
+ flows?: string[]
29
+ messageGroupId?: string
30
+ }
31
+
32
+ export class WorkerManager {
33
+ private readonly workers: Map<string, WorkerInfo> = new Map()
34
+ private readonly topicSubscriptions: Map<string, Set<string>> = new Map()
35
+ private readonly connection: Redis
36
+ private readonly config: MergedConfig
37
+ private readonly getQueueName: (topic: string, stepName: string) => string
38
+ private readonly dlqManager: DLQManager | null
39
+
40
+ constructor(
41
+ connection: Redis,
42
+ config: MergedConfig,
43
+ getQueueName: (topic: string, stepName: string) => string,
44
+ dlqManager?: DLQManager,
45
+ ) {
46
+ this.connection = connection
47
+ this.config = config
48
+ this.getQueueName = getQueueName
49
+ this.dlqManager = dlqManager ?? null
50
+ }
51
+
52
+ createWorker<TData>(
53
+ topic: string,
54
+ stepName: string,
55
+ handler: (event: Event<TData>) => void | Promise<void>,
56
+ options?: QueueConfig,
57
+ ): SubscriptionHandle {
58
+ const id = uuidv4()
59
+ const queueName = this.getQueueName(topic, stepName)
60
+
61
+ this.addTopicSubscription(topic, id)
62
+
63
+ const concurrency = options?.type === 'fifo' ? FIFO_CONCURRENCY : this.config.concurrency
64
+ const attempts = options?.maxRetries != null ? options.maxRetries + 1 : this.config.defaultJobOptions.attempts
65
+ const lockDuration = options?.visibilityTimeout ? options.visibilityTimeout * MILLISECONDS_PER_SECOND : undefined
66
+
67
+ const worker = new Worker(
68
+ queueName,
69
+ async (job: Job<JobData<TData>>) => {
70
+ const eventData = job.data
71
+ const event = {
72
+ topic: eventData.topic,
73
+ data: eventData.data,
74
+ traceId: eventData.traceId,
75
+ flows: eventData.flows,
76
+ messageGroupId: eventData.messageGroupId,
77
+ } as Event<TData>
78
+ await handler(event)
79
+ },
80
+ {
81
+ connection: this.connection,
82
+ prefix: this.config.prefix,
83
+ concurrency,
84
+ lockDuration,
85
+ removeOnComplete: this.config.defaultJobOptions.removeOnComplete,
86
+ removeOnFail: this.config.defaultJobOptions.removeOnFail,
87
+ },
88
+ )
89
+
90
+ this.setupWorkerHandlers(worker, topic, stepName, attempts ?? 3)
91
+
92
+ const handle: SubscriptionHandle = {
93
+ topic,
94
+ id,
95
+ unsubscribe: async () => {
96
+ await this.removeWorker(handle.id)
97
+ },
98
+ }
99
+
100
+ const workerInfo: WorkerInfo = {
101
+ worker,
102
+ topic,
103
+ stepName,
104
+ handle,
105
+ queueConfig: options,
106
+ }
107
+
108
+ this.workers.set(id, workerInfo)
109
+ return handle
110
+ }
111
+
112
+ getSubscribers(topic: string): SubscriberInfo[] {
113
+ const subscriptionIds = this.topicSubscriptions.get(topic)
114
+ if (!subscriptionIds || subscriptionIds.size === 0) {
115
+ return []
116
+ }
117
+
118
+ return Array.from(subscriptionIds)
119
+ .map((id) => this.workers.get(id))
120
+ .filter((info): info is WorkerInfo => info !== undefined)
121
+ .map((info) => ({ topic: info.topic, stepName: info.stepName, queueConfig: info.queueConfig }))
122
+ }
123
+
124
+ getWorkerInfo(id: string): WorkerInfo | undefined {
125
+ return this.workers.get(id)
126
+ }
127
+
128
+ async removeWorker(id: string): Promise<void> {
129
+ const workerInfo = this.workers.get(id)
130
+ if (!workerInfo) {
131
+ return
132
+ }
133
+
134
+ this.removeTopicSubscription(workerInfo.topic, id)
135
+ await workerInfo.worker.close()
136
+ this.workers.delete(id)
137
+ }
138
+
139
+ async closeAll(): Promise<void> {
140
+ const promises = Array.from(this.workers.values()).map((info) =>
141
+ info.worker.close().catch((err) => {
142
+ console.error(`[BullMQ] Error closing worker for topic ${info.topic}, step ${info.stepName}:`, err)
143
+ }),
144
+ )
145
+ await Promise.allSettled(promises)
146
+ this.workers.clear()
147
+ this.topicSubscriptions.clear()
148
+ }
149
+
150
+ getSubscriptionCount(topic: string): number {
151
+ return Array.from(this.workers.values()).filter((w) => w.topic === topic).length
152
+ }
153
+
154
+ listTopics(): string[] {
155
+ return Array.from(new Set(Array.from(this.workers.values()).map((w) => w.topic)))
156
+ }
157
+
158
+ private addTopicSubscription(topic: string, id: string): void {
159
+ if (!this.topicSubscriptions.has(topic)) {
160
+ this.topicSubscriptions.set(topic, new Set())
161
+ }
162
+ this.topicSubscriptions.get(topic)?.add(id)
163
+ }
164
+
165
+ private removeTopicSubscription(topic: string, id: string): void {
166
+ const subscriptions = this.topicSubscriptions.get(topic)
167
+ if (subscriptions) {
168
+ subscriptions.delete(id)
169
+ if (subscriptions.size === 0) {
170
+ this.topicSubscriptions.delete(topic)
171
+ }
172
+ }
173
+ }
174
+
175
+ private setupWorkerHandlers(worker: Worker, topic: string, stepName: string, attempts: number): void {
176
+ worker.on('error', (err: Error) => {
177
+ const error = new WorkerCreationError(topic, stepName, err)
178
+ console.error(`[BullMQ] Worker error for topic ${topic}, step ${stepName}:`, error)
179
+ })
180
+ worker.on('failed', async (job: Job<JobData<unknown>> | undefined, err: Error) => {
181
+ if (job) {
182
+ const attemptsMade = job.attemptsMade || 0
183
+ if (attemptsMade >= attempts) {
184
+ if (this.dlqManager) {
185
+ const eventData = job.data
186
+ const event = {
187
+ topic: eventData.topic || topic,
188
+ data: eventData.data,
189
+ traceId: eventData.traceId || 'unknown',
190
+ ...(eventData.flows && { flows: eventData.flows }),
191
+ ...(eventData.messageGroupId && { messageGroupId: eventData.messageGroupId }),
192
+ } as Event<unknown>
193
+
194
+ await this.dlqManager.moveToDLQ(topic, stepName, event, err, attemptsMade, job.id)
195
+ }
196
+ }
197
+ }
198
+ })
199
+ }
200
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "outDir": "./dist",
9
+ "rootDir": "./src",
10
+ "strict": true,
11
+ "esModuleInterop": true,
12
+ "skipLibCheck": true,
13
+ "forceConsistentCasingInFileNames": true,
14
+ "resolveJsonModule": true,
15
+ "moduleResolution": "node"
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }