@open-mercato/queue 0.6.7-develop.6862.1.c11a64ce0a → 0.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/queue",
3
- "version": "0.6.7-develop.6862.1.c11a64ce0a",
3
+ "version": "0.6.7",
4
4
  "license": "MIT",
5
5
  "description": "Multi-strategy job queue with local and BullMQ support",
6
6
  "type": "module",
@@ -34,35 +34,29 @@
34
34
  }
35
35
  },
36
36
  "peerDependencies": {
37
- "bullmq": "^5.0.0 || ^6.0.0",
38
- "bullmq-otel": "^1.3.0"
37
+ "bullmq": "^5.0.0"
39
38
  },
40
39
  "peerDependenciesMeta": {
41
40
  "bullmq": {
42
41
  "optional": true
43
- },
44
- "bullmq-otel": {
45
- "optional": true
46
42
  }
47
43
  },
48
44
  "devDependencies": {
49
45
  "@types/jest": "^30.0.0",
50
- "@types/node": "^26.1.2",
46
+ "@types/node": "^26.0.1",
51
47
  "jest": "^30.4.2",
52
- "ts-jest": "^29.4.12",
48
+ "ts-jest": "^29.4.11",
53
49
  "typescript": "7.0.2"
54
50
  },
55
51
  "publishConfig": {
56
52
  "access": "public"
57
53
  },
58
54
  "dependencies": {
59
- "@open-mercato/shared": "0.6.7-develop.6862.1.c11a64ce0a",
60
- "@open-mercato/telemetry": "0.6.7-develop.6862.1.c11a64ce0a"
55
+ "@open-mercato/shared": "0.6.7"
61
56
  },
62
57
  "repository": {
63
58
  "type": "git",
64
59
  "url": "https://github.com/open-mercato/open-mercato",
65
60
  "directory": "packages/queue"
66
- },
67
- "stableVersion": "0.6.6"
61
+ }
68
62
  }
@@ -18,7 +18,6 @@ const workerOn = jest.fn()
18
18
 
19
19
  jest.mock('@open-mercato/shared/lib/redis/connection', () => ({
20
20
  getRedisUrlOrThrow: jest.fn(),
21
- parseRedisUrl: jest.requireActual('@open-mercato/shared/lib/redis/connection').parseRedisUrl,
22
21
  }))
23
22
 
24
23
  jest.mock('bullmq', () => {
@@ -61,7 +60,7 @@ describe('Queue - async strategy', () => {
61
60
  getRedisUrlOrThrowMock.mockReturnValue('rediss://default:secret@example.com:6380/1')
62
61
  })
63
62
 
64
- it('passes parsed Redis connection fields to BullMQ for env-based async config', async () => {
63
+ it('passes the full Redis URL to BullMQ when using env-based async config', async () => {
65
64
  const queue = createQueue<{ value: number }>('test-queue', 'async', {
66
65
  concurrency: 3,
67
66
  })
@@ -70,35 +69,19 @@ describe('Queue - async strategy', () => {
70
69
  await queue.process(async () => {})
71
70
 
72
71
  expect(queueCtor).toHaveBeenCalledWith('test-queue', {
73
- connection: {
74
- host: 'example.com',
75
- port: 6380,
76
- username: 'default',
77
- password: 'secret',
78
- db: 1,
79
- tls: {},
80
- family: undefined,
81
- },
72
+ connection: { url: 'rediss://default:secret@example.com:6380/1' },
82
73
  })
83
74
  expect(workerCtor).toHaveBeenCalledWith(
84
75
  'test-queue',
85
76
  expect.any(Function),
86
77
  {
87
- connection: {
88
- host: 'example.com',
89
- port: 6380,
90
- username: 'default',
91
- password: 'secret',
92
- db: 1,
93
- tls: {},
94
- family: undefined,
95
- },
78
+ connection: { url: 'rediss://default:secret@example.com:6380/1' },
96
79
  concurrency: 3,
97
80
  },
98
81
  )
99
82
  })
100
83
 
101
- it('preserves URL connection semantics when converting to BullMQ fields', async () => {
84
+ it('preserves an explicit Redis URL without converting it to host/port fields', async () => {
102
85
  const queue = createQueue<{ value: number }>('test-queue', 'async', {
103
86
  connection: {
104
87
  url: 'rediss://user:secret@example.com:6380/4?family=6',
@@ -108,15 +91,7 @@ describe('Queue - async strategy', () => {
108
91
  await queue.enqueue({ value: 42 })
109
92
 
110
93
  expect(queueCtor).toHaveBeenCalledWith('test-queue', {
111
- connection: {
112
- host: 'example.com',
113
- port: 6380,
114
- username: 'user',
115
- password: 'secret',
116
- db: 4,
117
- tls: {},
118
- family: 6,
119
- },
94
+ connection: { url: 'rediss://user:secret@example.com:6380/4?family=6' },
120
95
  })
121
96
  })
122
97
 
@@ -139,38 +114,6 @@ describe('Queue - async strategy', () => {
139
114
  await queue.close()
140
115
  })
141
116
 
142
- it('threads queue retry, lock-duration and stalled-job options to BullMQ', async () => {
143
- const queue = createQueue<{ value: number }>('test-queue', 'async', {
144
- attempts: 5,
145
- lockDuration: 120_000,
146
- maxStalledCount: 10,
147
- })
148
-
149
- await queue.enqueue({ value: 42 })
150
- await queue.process(async () => {})
151
-
152
- expect(queueAdd).toHaveBeenCalledWith(
153
- expect.any(String),
154
- expect.objectContaining({ payload: { value: 42 } }),
155
- expect.objectContaining({ attempts: 5 }),
156
- )
157
- expect(workerCtor).toHaveBeenCalledWith(
158
- 'test-queue',
159
- expect.any(Function),
160
- expect.objectContaining({ lockDuration: 120_000, maxStalledCount: 10 }),
161
- )
162
- })
163
-
164
- it('leaves BullMQ on its own lock and stall defaults when the options are unset', async () => {
165
- const queue = createQueue<{ value: number }>('test-queue', 'async', {})
166
-
167
- await queue.process(async () => {})
168
-
169
- const workerOptions = workerCtor.mock.calls[0]?.[2] as Record<string, unknown>
170
- expect(workerOptions).not.toHaveProperty('lockDuration')
171
- expect(workerOptions).not.toHaveProperty('maxStalledCount')
172
- })
173
-
174
117
  it('removeQueuedJobsByScope removes only queued jobs matching tenant scope', async () => {
175
118
  const removeMatching = jest.fn(async () => {})
176
119
  const removeAutoIndex = jest.fn(async () => {})
@@ -1,9 +1,8 @@
1
1
  import { resolveQueueStrategy, createModuleQueue } from '../factory'
2
- import { getRedisUrlOrThrow, parseRedisUrl } from '@open-mercato/shared/lib/redis/connection'
2
+ import { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'
3
3
 
4
4
  jest.mock('@open-mercato/shared/lib/redis/connection', () => ({
5
5
  getRedisUrlOrThrow: jest.fn(),
6
- parseRedisUrl: jest.fn(),
7
6
  }))
8
7
 
9
8
  jest.mock('bullmq', () => {
@@ -59,12 +58,10 @@ describe('resolveQueueStrategy', () => {
59
58
  describe('createModuleQueue', () => {
60
59
  const originalEnv = process.env.QUEUE_STRATEGY
61
60
  const getRedisUrlOrThrowMock = getRedisUrlOrThrow as jest.MockedFunction<typeof getRedisUrlOrThrow>
62
- const parseRedisUrlMock = parseRedisUrl as jest.MockedFunction<typeof parseRedisUrl>
63
61
 
64
62
  beforeEach(() => {
65
63
  jest.clearAllMocks()
66
64
  getRedisUrlOrThrowMock.mockReturnValue('redis://localhost:6379')
67
- parseRedisUrlMock.mockReturnValue({ host: 'localhost', port: 6379 })
68
65
  })
69
66
 
70
67
  afterEach(() => {
@@ -88,7 +85,6 @@ describe('createModuleQueue', () => {
88
85
  expect(queue.strategy).toBe('async')
89
86
  expect(queue.name).toBe('test-queue')
90
87
  expect(getRedisUrlOrThrowMock).toHaveBeenCalledWith('QUEUE')
91
- expect(parseRedisUrlMock).toHaveBeenCalledWith('redis://localhost:6379')
92
88
  })
93
89
 
94
90
  it('passes concurrency to local strategy', () => {
@@ -2,22 +2,7 @@ import fs from 'node:fs'
2
2
  import os from 'node:os'
3
3
  import path from 'node:path'
4
4
  import { createQueue } from '../factory'
5
- import { __resetPendingProbeBullMQCache, getQueuePendingProbe } from '../pending-probe'
6
-
7
- const asyncQueueConstructor = jest.fn()
8
- const asyncQueueClose = jest.fn(async () => {})
9
- const asyncQueueGetJobCounts = jest.fn(async () => ({ waiting: 1, delayed: 0, active: 0 }))
10
-
11
- jest.mock('bullmq', () => ({
12
- Queue: class MockQueue {
13
- constructor(name: string, options: unknown) {
14
- asyncQueueConstructor(name, options)
15
- }
16
-
17
- getJobCounts = asyncQueueGetJobCounts
18
- close = asyncQueueClose
19
- },
20
- }))
5
+ import { getQueuePendingProbe } from '../pending-probe'
21
6
 
22
7
  describe('getQueuePendingProbe — local strategy', () => {
23
8
  const origCwd = process.cwd()
@@ -110,11 +95,6 @@ describe('getQueuePendingProbe — local strategy', () => {
110
95
  })
111
96
 
112
97
  describe('getQueuePendingProbe — async strategy', () => {
113
- beforeEach(() => {
114
- jest.clearAllMocks()
115
- __resetPendingProbeBullMQCache()
116
- })
117
-
118
98
  it('reports an error when QUEUE Redis URL is unset and no connection override is provided', async () => {
119
99
  const original = process.env.QUEUE_REDIS_URL
120
100
  const fallback = process.env.REDIS_URL
@@ -129,23 +109,4 @@ describe('getQueuePendingProbe — async strategy', () => {
129
109
  if (fallback !== undefined) process.env.REDIS_URL = fallback
130
110
  }
131
111
  })
132
-
133
- it('converts a URL override to BullMQ connection fields', async () => {
134
- const probe = await getQueuePendingProbe('async-probe', 'async', {
135
- connection: { url: 'rediss://probe:secret@example.com:6380/3?family=6' },
136
- })
137
-
138
- expect(probe).toEqual(expect.objectContaining({ error: false, ready: 1 }))
139
- expect(asyncQueueConstructor).toHaveBeenCalledWith('async-probe', {
140
- connection: {
141
- host: 'example.com',
142
- port: 6380,
143
- username: 'probe',
144
- password: 'secret',
145
- db: 3,
146
- tls: {},
147
- family: 6,
148
- },
149
- })
150
- })
151
112
  })
package/src/factory.ts CHANGED
@@ -82,16 +82,13 @@ export function resolveQueueStrategy(): QueueStrategyType {
82
82
  */
83
83
  export function createModuleQueue<T = unknown>(
84
84
  name: string,
85
- options?: Pick<AsyncQueueOptions, 'attempts' | 'concurrency' | 'lockDuration' | 'maxStalledCount'>,
85
+ options?: { concurrency?: number },
86
86
  ): Queue<T> {
87
87
  const strategy = resolveQueueStrategy()
88
88
  if (strategy === 'async') {
89
89
  return createAsyncQueue<T>(name, {
90
90
  connection: { url: getRedisUrlOrThrow('QUEUE') },
91
91
  concurrency: options?.concurrency,
92
- attempts: options?.attempts,
93
- lockDuration: options?.lockDuration,
94
- maxStalledCount: options?.maxStalledCount,
95
92
  })
96
93
  }
97
94
  return createLocalQueue<T>(name, { concurrency: options?.concurrency })
@@ -149,16 +149,14 @@ async function probeAsyncQueue(
149
149
  return errorResult(queueName, 'async', new Error('bullmq is not installed'))
150
150
  }
151
151
 
152
- const { getRedisUrl, parseRedisUrl } = await import('@open-mercato/shared/lib/redis/connection')
152
+ const { getRedisUrl } = await import('@open-mercato/shared/lib/redis/connection')
153
153
  let connection = options?.connection
154
154
  if (!connection) {
155
155
  const url = getRedisUrl('QUEUE')
156
156
  if (!url) {
157
157
  return errorResult(queueName, 'async', new Error('QUEUE Redis URL is not configured'))
158
158
  }
159
- connection = parseRedisUrl(url)
160
- } else if (connection.url) {
161
- connection = parseRedisUrl(connection.url)
159
+ connection = { url }
162
160
  }
163
161
 
164
162
  let queue: InstanceType<BullMQModuleShape['Queue']> | null = null
@@ -1,7 +1,5 @@
1
1
  import type { Queue, QueuedJob, JobHandler, AsyncQueueOptions, ProcessResult, EnqueueOptions, QueueJobScope } from '../types'
2
- import { getRedisUrlOrThrow, parseRedisUrl } from '@open-mercato/shared/lib/redis/connection'
3
- import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
4
- import { attachTraceMetadata, runJobInTrace } from '../tracing'
2
+ import { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'
5
3
  import { createLogger } from '@open-mercato/shared/lib/logger'
6
4
 
7
5
  const packageLogger = createLogger('queue')
@@ -9,13 +7,13 @@ const packageLogger = createLogger('queue')
9
7
  // BullMQ interface types - we define the shape we use to maintain type safety
10
8
  // while keeping bullmq as an optional peer dependency
11
9
  type ConnectionOptions = {
10
+ url?: string
12
11
  host?: string
13
12
  port?: number
14
13
  username?: string
15
14
  password?: string
16
15
  db?: number
17
16
  tls?: Record<string, unknown>
18
- family?: number
19
17
  }
20
18
 
21
19
  interface BullQueueInterface<T> {
@@ -45,23 +43,14 @@ interface BullWorkerInterface {
45
43
  }
46
44
 
47
45
  interface BullMQModule {
48
- Queue: new <T>(name: string, opts: { connection: ConnectionOptions; telemetry?: unknown }) => BullQueueInterface<T>
46
+ Queue: new <T>(name: string, opts: { connection: ConnectionOptions }) => BullQueueInterface<T>
49
47
  Worker: new <T>(
50
48
  name: string,
51
49
  processor: (job: { id?: string; data: T; attemptsMade: number }) => Promise<void>,
52
- opts: {
53
- connection: ConnectionOptions
54
- concurrency: number
55
- telemetry?: unknown
56
- lockDuration?: number
57
- maxStalledCount?: number
58
- }
50
+ opts: { connection: ConnectionOptions; concurrency: number }
59
51
  ) => BullWorkerInterface
60
52
  }
61
53
 
62
- /** The `bullmq-otel` package (optional). Loaded only when an OTLP backend is active. */
63
- type BullMQOtelModule = { BullMQOtel: new (tracerName: string) => object }
64
-
65
54
  const REMOVABLE_JOB_STATES = ['waiting', 'delayed', 'prioritized', 'paused', 'waiting-children']
66
55
 
67
56
  function payloadMatchesScope(payload: unknown, scope: QueueJobScope): boolean {
@@ -80,13 +69,13 @@ function payloadMatchesScope(payload: unknown, scope: QueueJobScope): boolean {
80
69
  /**
81
70
  * Resolves Redis connection options from various sources.
82
71
  *
83
- * BullMQ expects ioredis connection fields rather than a nested URL string.
84
- * Parse URL-based configuration at this boundary while keeping the public
85
- * queue API compatible with existing `{ url }` callers.
72
+ * BullMQ expects an ioredis-compatible connection object. Preserve the full
73
+ * Redis URL under the `url` key so rediss://, username, database, and query
74
+ * params are not lost in translation.
86
75
  */
87
76
  function resolveConnection(options?: AsyncQueueOptions['connection']): ConnectionOptions {
88
77
  if (options?.url) {
89
- return parseRedisUrl(options.url)
78
+ return { url: options.url }
90
79
  }
91
80
 
92
81
  if (options?.host) {
@@ -97,11 +86,10 @@ function resolveConnection(options?: AsyncQueueOptions['connection']): Connectio
97
86
  password: options.password,
98
87
  db: options.db,
99
88
  tls: options.tls,
100
- family: options.family,
101
89
  }
102
90
  }
103
91
 
104
- return parseRedisUrl(getRedisUrlOrThrow('QUEUE'))
92
+ return { url: getRedisUrlOrThrow('QUEUE') }
105
93
  }
106
94
 
107
95
  /**
@@ -123,18 +111,11 @@ export function createAsyncQueue<T = unknown>(
123
111
  ): Queue<T> {
124
112
  const connection = resolveConnection(options?.connection)
125
113
  const concurrency = options?.concurrency ?? 1
126
- const attempts = options?.attempts ?? 3
127
- const lockDuration = options?.lockDuration
128
- const maxStalledCount = options?.maxStalledCount
129
114
  const logger = packageLogger.child({ queue: name })
130
115
 
131
116
  let bullQueue: BullQueueInterface<QueuedJob<T>> | null = null
132
117
  let bullWorker: BullWorkerInterface | null = null
133
118
  let bullmqModule: BullMQModule | null = null
134
- // Resolved once: a BullMQOtel instance (delegate async tracing to BullMQ) or
135
- // undefined (use our own metadata._trace carrier instead). Memoized as the
136
- // in-flight promise so concurrent first-time callers share one resolution.
137
- let telemetryPromise: Promise<object | undefined> | null = null
138
119
 
139
120
  // -------------------------------------------------------------------------
140
121
  // Lazy BullMQ initialization
@@ -153,35 +134,10 @@ export function createAsyncQueue<T = unknown>(
153
134
  return bullmqModule
154
135
  }
155
136
 
156
- /**
157
- * When an OTLP backend is active, delegate async-queue tracing to `bullmq-otel`
158
- * (richer BullMQ-internal spans: add / process / wait / attempts). Returns
159
- * `undefined` — meaning "use our own `metadata._trace` carrier" — when telemetry
160
- * is off, a non-OTEL backend is selected, or `bullmq-otel` isn't installed. (The
161
- * `local` strategy always uses our carrier; it isn't BullMQ, so `bullmq-otel`
162
- * cannot instrument it.)
163
- */
164
- async function getQueueTelemetry(): Promise<object | undefined> {
165
- if (!telemetryPromise) {
166
- telemetryPromise = (async () => {
167
- if (!getTelemetryRuntime()?.canUseGlobalTracePropagation()) return undefined
168
- try {
169
- const mod = (await import('bullmq-otel')) as unknown as BullMQOtelModule
170
- return new mod.BullMQOtel('open-mercato')
171
- } catch {
172
- packageLogger.warn('bullmq-otel not available; using built-in trace carrier', { queue: name })
173
- return undefined
174
- }
175
- })()
176
- }
177
- return telemetryPromise
178
- }
179
-
180
137
  async function getQueue(): Promise<BullQueueInterface<QueuedJob<T>>> {
181
138
  if (!bullQueue) {
182
139
  const { Queue: BullQueueClass } = await getBullMQ()
183
- const telemetry = await getQueueTelemetry()
184
- bullQueue = new BullQueueClass<QueuedJob<T>>(name, { connection, ...(telemetry ? { telemetry } : {}) })
140
+ bullQueue = new BullQueueClass<QueuedJob<T>>(name, { connection })
185
141
  }
186
142
  return bullQueue
187
143
  }
@@ -192,21 +148,17 @@ export function createAsyncQueue<T = unknown>(
192
148
 
193
149
  async function enqueue(data: T, options?: EnqueueOptions): Promise<string> {
194
150
  const queue = await getQueue()
195
- // When bullmq-otel handles propagation, don't also attach our carrier.
196
- const telemetry = await getQueueTelemetry()
197
- const metadata = telemetry ? undefined : attachTraceMetadata(undefined)
198
151
  const jobData: QueuedJob<T> = {
199
152
  id: crypto.randomUUID(),
200
153
  payload: data,
201
154
  createdAt: new Date().toISOString(),
202
- ...(metadata ? { metadata } : {}),
203
155
  }
204
156
 
205
157
  const job = await queue.add(jobData.id, jobData, {
206
158
  delay: options?.delayMs && options.delayMs > 0 ? options.delayMs : undefined,
207
159
  removeOnComplete: true,
208
160
  removeOnFail: 1000,
209
- attempts,
161
+ attempts: 3,
210
162
  backoff: { type: 'exponential', delay: 1000 },
211
163
  })
212
164
 
@@ -215,33 +167,21 @@ export function createAsyncQueue<T = unknown>(
215
167
 
216
168
  async function process(handler: JobHandler<T>): Promise<ProcessResult> {
217
169
  const { Worker } = await getBullMQ()
218
- const telemetry = await getQueueTelemetry()
219
170
 
220
171
  // Create worker that processes jobs
221
172
  bullWorker = new Worker<QueuedJob<T>>(
222
173
  name,
223
174
  async (job) => {
224
175
  const jobData = job.data
225
- const ctx = {
176
+ await handler(jobData, {
226
177
  jobId: job.id ?? jobData.id,
227
178
  attemptNumber: job.attemptsMade + 1,
228
179
  queueName: name,
229
- }
230
- // With bullmq-otel active, BullMQ owns the process span and active
231
- // context (the handler's pg/undici spans nest under it). Otherwise
232
- // continue the trace from our own carrier.
233
- if (telemetry) {
234
- await handler(jobData, ctx)
235
- } else {
236
- await runJobInTrace(name, jobData.metadata, () => handler(jobData, ctx))
237
- }
180
+ })
238
181
  },
239
182
  {
240
183
  connection,
241
184
  concurrency,
242
- ...(telemetry ? { telemetry } : {}),
243
- ...(lockDuration !== undefined ? { lockDuration } : {}),
244
- ...(maxStalledCount !== undefined ? { maxStalledCount } : {}),
245
185
  }
246
186
  )
247
187
 
@@ -257,16 +197,6 @@ export function createAsyncQueue<T = unknown>(
257
197
  logger.error('Job failed', { jobId: jobWithId?.id, err: error })
258
198
  })
259
199
 
260
- // A stalled job is redelivered under the same id while the previous worker
261
- // may still be running it, so this is the signal that a handler is about to
262
- // be executed twice. BullMQ's docs require surfacing it: without this line
263
- // duplicate processing is invisible.
264
- bullWorker.on('stalled', (jobId) => {
265
- logger.warn('Job stalled and will be redelivered — the handler may run concurrently with a previous delivery', {
266
- jobId: typeof jobId === 'string' ? jobId : null,
267
- })
268
- })
269
-
270
200
  bullWorker.on('error', (err) => {
271
201
  const error = err as Error
272
202
  logger.error('Worker error', { err: error })
@@ -3,7 +3,6 @@ import path from 'node:path'
3
3
  import crypto from 'node:crypto'
4
4
  import { createLogger } from '@open-mercato/shared/lib/logger'
5
5
  import type { Queue, QueuedJob, JobHandler, LocalQueueOptions, ProcessOptions, ProcessResult, EnqueueOptions, QueueJobScope } from '../types'
6
- import { attachTraceMetadata, runJobInTrace } from '../tracing'
7
6
 
8
7
  const packageLogger = createLogger('queue')
9
8
 
@@ -50,18 +49,9 @@ const fsp = fs.promises
50
49
  * - Jobs are processed sequentially (concurrency option is for logging/compatibility only)
51
50
  * - Not suitable for production or multi-process environments
52
51
  *
53
- * Failed jobs are retried up to `DEFAULT_MAX_ATTEMPTS` times with exponential backoff.
54
- * **This strategy keeps no failed-job store**: once attempts are exhausted the job is
55
- * removed from `queue.json` and only counted in `state.failedCount`, so the payload is
56
- * lost and the failure survives solely as an error log line. The `async` strategy keeps
57
- * only a bounded inspection window: `removeOnFail: 1000` retains the most recent 1000
58
- * failures and removes older ones as later failures arrive. Workflows that require
59
- * no-loss persistence must write their own durable record before enqueueing, regardless
60
- * of strategy.
61
- *
62
- * `DEFAULT_MAX_ATTEMPTS` is a module constant, not a per-job option — callers cannot
63
- * request a different attempt count. (`async` likewise hard-codes `attempts: 3`.)
64
- * See the retry handling in `process()` below.
52
+ * Failed jobs are retried up to `DEFAULT_MAX_ATTEMPTS` times with exponential
53
+ * backoff and moved to a dead-letter store once attempts are exhausted (see the
54
+ * retry handling in `process()` below).
65
55
  *
66
56
  * All file I/O is asynchronous (`fs.promises.*`) so queue operations do not
67
57
  * block the Node.js event loop. A per-queue promise chain serializes
@@ -206,13 +196,11 @@ export function createLocalQueue<T = unknown>(
206
196
  const availableAt = options?.delayMs && options.delayMs > 0
207
197
  ? new Date(Date.now() + options.delayMs).toISOString()
208
198
  : undefined
209
- const metadata = attachTraceMetadata(undefined)
210
199
  const job: StoredJob<T> = {
211
200
  id: generateId(),
212
201
  payload: data,
213
202
  createdAt: new Date().toISOString(),
214
203
  ...(availableAt ? { availableAt } : {}),
215
- ...(metadata ? { metadata } : {}),
216
204
  }
217
205
  await withFileLock(async () => {
218
206
  const jobs = await readQueue()
@@ -258,14 +246,12 @@ export function createLocalQueue<T = unknown>(
258
246
  for (const job of jobsToProcess) {
259
247
  const attemptNumber = (job.attemptCount ?? 0) + 1
260
248
  try {
261
- await runJobInTrace(name, job.metadata, () =>
262
- Promise.resolve(
263
- handler(job, {
264
- jobId: job.id,
265
- attemptNumber,
266
- queueName: name,
267
- })
268
- )
249
+ await Promise.resolve(
250
+ handler(job, {
251
+ jobId: job.id,
252
+ attemptNumber,
253
+ queueName: name,
254
+ })
269
255
  )
270
256
  processed++
271
257
  lastJobId = job.id
@@ -276,7 +262,7 @@ export function createLocalQueue<T = unknown>(
276
262
  failed++
277
263
  lastJobId = job.id
278
264
  if (attemptNumber >= DEFAULT_MAX_ATTEMPTS) {
279
- logger.error('Job exhausted all attempts; dropping it (no dead-letter store)', { jobId: job.id, maxAttempts: DEFAULT_MAX_ATTEMPTS })
265
+ logger.error('Job exhausted all attempts, moving to dead letter', { jobId: job.id, maxAttempts: DEFAULT_MAX_ATTEMPTS })
280
266
  deadJobIds.add(job.id)
281
267
  } else {
282
268
  const backoffMs = RETRY_BACKOFF_BASE_MS * Math.pow(2, attemptNumber - 1)
package/src/types.ts CHANGED
@@ -81,8 +81,6 @@ export type RedisConnectionOptions = {
81
81
  db?: number
82
82
  /** TLS configuration for rediss / encrypted Redis */
83
83
  tls?: Record<string, unknown>
84
- /** IP family used by Redis DNS resolution */
85
- family?: number
86
84
  }
87
85
 
88
86
  /**
@@ -93,12 +91,6 @@ export type AsyncQueueOptions = {
93
91
  connection?: RedisConnectionOptions
94
92
  /** Number of concurrent job processors. Defaults to 1 */
95
93
  concurrency?: number
96
- /** Number of attempts for newly enqueued jobs. Defaults to 3. */
97
- attempts?: number
98
- /** How long a job lock is held before the job counts as stalled, in ms. Defaults to 30000. */
99
- lockDuration?: number
100
- /** Number of stalled-job recoveries BullMQ permits before failing a job. Defaults to 1. */
101
- maxStalledCount?: number
102
94
  }
103
95
 
104
96
  /**
@@ -258,10 +250,6 @@ export type WorkerMeta = {
258
250
  id?: string
259
251
  /** Worker concurrency (default: 1) */
260
252
  concurrency?: number
261
- /** How long a job lock is held before the job counts as stalled, in ms. */
262
- lockDuration?: number
263
- /** Number of stalled-job recoveries BullMQ permits before failing a job. */
264
- maxStalledCount?: number
265
253
  }
266
254
 
267
255
  /**
@@ -277,8 +265,4 @@ export type WorkerDescriptor<T = unknown> = {
277
265
  handler: JobHandler<T>
278
266
  /** Concurrency level */
279
267
  concurrency: number
280
- /** How long a job lock is held before the job counts as stalled, in ms. */
281
- lockDuration?: number
282
- /** Number of stalled-job recoveries BullMQ permits before failing a job. */
283
- maxStalledCount?: number
284
268
  }