@open-mercato/queue 0.6.8-develop.7015.1.af90a2ddc7 → 0.6.8-develop.7019.1.f4c01c4b5c

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/dist/factory.js CHANGED
@@ -18,7 +18,8 @@ function createModuleQueue(name, options) {
18
18
  concurrency: options?.concurrency,
19
19
  attempts: options?.attempts,
20
20
  lockDuration: options?.lockDuration,
21
- maxStalledCount: options?.maxStalledCount
21
+ maxStalledCount: options?.maxStalledCount,
22
+ onJobAbandoned: options?.onJobAbandoned
22
23
  });
23
24
  }
24
25
  return createLocalQueue(name, { concurrency: options?.concurrency });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/factory.ts"],
4
- "sourcesContent": ["import type { Queue, LocalQueueOptions, AsyncQueueOptions, QueueStrategyType } from './types'\nimport { createLocalQueue } from './strategies/local'\nimport { createAsyncQueue } from './strategies/async'\nimport { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'\n\n/**\n * Creates a queue instance with the specified strategy.\n *\n * @template T - The payload type for jobs in this queue\n * @param name - Unique name for the queue\n * @param strategy - Queue strategy: 'local' for file-based, 'async' for BullMQ\n * @param options - Strategy-specific options\n * @returns A Queue instance\n *\n * @example\n * ```typescript\n * // Local file-based queue\n * const localQueue = createQueue<MyJobData>('my-queue', 'local')\n *\n * // BullMQ-based queue\n * const asyncQueue = createQueue<MyJobData>('my-queue', 'async', {\n * connection: { url: 'redis://localhost:6379' },\n * concurrency: 5\n * })\n * ```\n */\nexport function createQueue<T = unknown>(\n name: string,\n strategy: 'local',\n options?: LocalQueueOptions\n): Queue<T>\n\nexport function createQueue<T = unknown>(\n name: string,\n strategy: 'async',\n options?: AsyncQueueOptions\n): Queue<T>\n\n// General overload for dynamic strategy (union type)\nexport function createQueue<T = unknown>(\n name: string,\n strategy: 'local' | 'async',\n options?: LocalQueueOptions | AsyncQueueOptions\n): Queue<T>\n\nexport function createQueue<T = unknown>(\n name: string,\n strategy: 'local' | 'async',\n options?: LocalQueueOptions | AsyncQueueOptions\n): Queue<T> {\n if (strategy === 'async') {\n return createAsyncQueue<T>(name, options as AsyncQueueOptions)\n }\n\n return createLocalQueue<T>(name, options as LocalQueueOptions)\n}\n\n/**\n * Resolve the queue strategy from `QUEUE_STRATEGY`. Defaults to `'local'`.\n */\nexport function resolveQueueStrategy(): QueueStrategyType {\n return process.env.QUEUE_STRATEGY === 'async' ? 'async' : 'local'\n}\n\n/**\n * Create a module-owned queue using the strategy declared in `QUEUE_STRATEGY`.\n *\n * - When `QUEUE_STRATEGY=async`, builds a BullMQ queue and resolves the\n * Redis URL via `getRedisUrlOrThrow('QUEUE')` so missing config fails loudly.\n * - Otherwise builds a local file-based queue.\n *\n * Replaces the boilerplate `process.env.QUEUE_STRATEGY === 'async' ? ... : ...`\n * pattern that every module queue helper used to repeat. Concurrency applies\n * to both strategies so the same number means the same thing in dev and prod.\n *\n * @example\n * ```typescript\n * export function getDataSyncQueue(name: string) {\n * return createModuleQueue<MyJob>(name, { concurrency: 5 })\n * }\n * ```\n */\nexport function createModuleQueue<T = unknown>(\n name: string,\n options?: Pick<AsyncQueueOptions, 'attempts' | 'concurrency' | 'lockDuration' | 'maxStalledCount'>,\n): Queue<T> {\n const strategy = resolveQueueStrategy()\n if (strategy === 'async') {\n return createAsyncQueue<T>(name, {\n connection: { url: getRedisUrlOrThrow('QUEUE') },\n concurrency: options?.concurrency,\n attempts: options?.attempts,\n lockDuration: options?.lockDuration,\n maxStalledCount: options?.maxStalledCount,\n })\n }\n return createLocalQueue<T>(name, { concurrency: options?.concurrency })\n}\n"],
5
- "mappings": "AACA,SAAS,wBAAwB;AACjC,SAAS,wBAAwB;AACjC,SAAS,0BAA0B;AA0C5B,SAAS,YACd,MACA,UACA,SACU;AACV,MAAI,aAAa,SAAS;AACxB,WAAO,iBAAoB,MAAM,OAA4B;AAAA,EAC/D;AAEA,SAAO,iBAAoB,MAAM,OAA4B;AAC/D;AAKO,SAAS,uBAA0C;AACxD,SAAO,QAAQ,IAAI,mBAAmB,UAAU,UAAU;AAC5D;AAoBO,SAAS,kBACd,MACA,SACU;AACV,QAAM,WAAW,qBAAqB;AACtC,MAAI,aAAa,SAAS;AACxB,WAAO,iBAAoB,MAAM;AAAA,MAC/B,YAAY,EAAE,KAAK,mBAAmB,OAAO,EAAE;AAAA,MAC/C,aAAa,SAAS;AAAA,MACtB,UAAU,SAAS;AAAA,MACnB,cAAc,SAAS;AAAA,MACvB,iBAAiB,SAAS;AAAA,IAC5B,CAAC;AAAA,EACH;AACA,SAAO,iBAAoB,MAAM,EAAE,aAAa,SAAS,YAAY,CAAC;AACxE;",
4
+ "sourcesContent": ["import type { Queue, LocalQueueOptions, AsyncQueueOptions, QueueStrategyType } from './types'\nimport { createLocalQueue } from './strategies/local'\nimport { createAsyncQueue } from './strategies/async'\nimport { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'\n\n/**\n * Creates a queue instance with the specified strategy.\n *\n * @template T - The payload type for jobs in this queue\n * @param name - Unique name for the queue\n * @param strategy - Queue strategy: 'local' for file-based, 'async' for BullMQ\n * @param options - Strategy-specific options\n * @returns A Queue instance\n *\n * @example\n * ```typescript\n * // Local file-based queue\n * const localQueue = createQueue<MyJobData>('my-queue', 'local')\n *\n * // BullMQ-based queue\n * const asyncQueue = createQueue<MyJobData>('my-queue', 'async', {\n * connection: { url: 'redis://localhost:6379' },\n * concurrency: 5\n * })\n * ```\n */\nexport function createQueue<T = unknown>(\n name: string,\n strategy: 'local',\n options?: LocalQueueOptions\n): Queue<T>\n\nexport function createQueue<T = unknown>(\n name: string,\n strategy: 'async',\n options?: AsyncQueueOptions\n): Queue<T>\n\n// General overload for dynamic strategy (union type)\nexport function createQueue<T = unknown>(\n name: string,\n strategy: 'local' | 'async',\n options?: LocalQueueOptions | AsyncQueueOptions\n): Queue<T>\n\nexport function createQueue<T = unknown>(\n name: string,\n strategy: 'local' | 'async',\n options?: LocalQueueOptions | AsyncQueueOptions\n): Queue<T> {\n if (strategy === 'async') {\n return createAsyncQueue<T>(name, options as AsyncQueueOptions)\n }\n\n return createLocalQueue<T>(name, options as LocalQueueOptions)\n}\n\n/**\n * Resolve the queue strategy from `QUEUE_STRATEGY`. Defaults to `'local'`.\n */\nexport function resolveQueueStrategy(): QueueStrategyType {\n return process.env.QUEUE_STRATEGY === 'async' ? 'async' : 'local'\n}\n\n/**\n * Create a module-owned queue using the strategy declared in `QUEUE_STRATEGY`.\n *\n * - When `QUEUE_STRATEGY=async`, builds a BullMQ queue and resolves the\n * Redis URL via `getRedisUrlOrThrow('QUEUE')` so missing config fails loudly.\n * - Otherwise builds a local file-based queue.\n *\n * Replaces the boilerplate `process.env.QUEUE_STRATEGY === 'async' ? ... : ...`\n * pattern that every module queue helper used to repeat. Concurrency applies\n * to both strategies so the same number means the same thing in dev and prod.\n *\n * @example\n * ```typescript\n * export function getDataSyncQueue(name: string) {\n * return createModuleQueue<MyJob>(name, { concurrency: 5 })\n * }\n * ```\n */\nexport function createModuleQueue<T = unknown>(\n name: string,\n options?: Pick<\n AsyncQueueOptions,\n 'attempts' | 'concurrency' | 'lockDuration' | 'maxStalledCount' | 'onJobAbandoned'\n >,\n): Queue<T> {\n const strategy = resolveQueueStrategy()\n if (strategy === 'async') {\n return createAsyncQueue<T>(name, {\n connection: { url: getRedisUrlOrThrow('QUEUE') },\n concurrency: options?.concurrency,\n attempts: options?.attempts,\n lockDuration: options?.lockDuration,\n maxStalledCount: options?.maxStalledCount,\n onJobAbandoned: options?.onJobAbandoned,\n })\n }\n // The local strategy runs the handler in-process, so there is no queue that could outlive it and\n // abandon a job \u2014 `onJobAbandoned` has nothing to report and is deliberately not forwarded.\n return createLocalQueue<T>(name, { concurrency: options?.concurrency })\n}\n"],
5
+ "mappings": "AACA,SAAS,wBAAwB;AACjC,SAAS,wBAAwB;AACjC,SAAS,0BAA0B;AA0C5B,SAAS,YACd,MACA,UACA,SACU;AACV,MAAI,aAAa,SAAS;AACxB,WAAO,iBAAoB,MAAM,OAA4B;AAAA,EAC/D;AAEA,SAAO,iBAAoB,MAAM,OAA4B;AAC/D;AAKO,SAAS,uBAA0C;AACxD,SAAO,QAAQ,IAAI,mBAAmB,UAAU,UAAU;AAC5D;AAoBO,SAAS,kBACd,MACA,SAIU;AACV,QAAM,WAAW,qBAAqB;AACtC,MAAI,aAAa,SAAS;AACxB,WAAO,iBAAoB,MAAM;AAAA,MAC/B,YAAY,EAAE,KAAK,mBAAmB,OAAO,EAAE;AAAA,MAC/C,aAAa,SAAS;AAAA,MACtB,UAAU,SAAS;AAAA,MACnB,cAAc,SAAS;AAAA,MACvB,iBAAiB,SAAS;AAAA,MAC1B,gBAAgB,SAAS;AAAA,IAC3B,CAAC;AAAA,EACH;AAGA,SAAO,iBAAoB,MAAM,EAAE,aAAa,SAAS,YAAY,CAAC;AACxE;",
6
6
  "names": []
7
7
  }
@@ -4,6 +4,20 @@ import { attachTraceMetadata, runJobInTrace } from "../tracing.js";
4
4
  import { createLogger } from "@open-mercato/shared/lib/logger";
5
5
  const packageLogger = createLogger("queue");
6
6
  const REMOVABLE_JOB_STATES = ["waiting", "delayed", "prioritized", "paused", "waiting-children"];
7
+ const ABANDONED_JOB_REASONS = [
8
+ "job stalled more than allowable limit",
9
+ "job started more than allowable limit"
10
+ ];
11
+ function isAbandonedJobReason(message) {
12
+ return ABANDONED_JOB_REASONS.includes(message);
13
+ }
14
+ const ABANDON_REPORT_ACK_KEY = "abandonReportedAt";
15
+ const ABANDONED_JOB_SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
16
+ const ABANDONED_JOB_DRAIN_TIMEOUT_MS = 5e3;
17
+ function resolveSweepIntervalMs() {
18
+ const configured = Number.parseInt(process.env.QUEUE_ABANDONED_SWEEP_INTERVAL_MS ?? "", 10);
19
+ return Number.isFinite(configured) && configured > 0 ? configured : ABANDONED_JOB_SWEEP_INTERVAL_MS;
20
+ }
7
21
  function payloadMatchesScope(payload, scope) {
8
22
  if (!payload || typeof payload !== "object") return false;
9
23
  const scopedPayload = payload;
@@ -40,10 +54,72 @@ function createAsyncQueue(name, options) {
40
54
  const attempts = options?.attempts ?? 3;
41
55
  const lockDuration = options?.lockDuration;
42
56
  const maxStalledCount = options?.maxStalledCount;
57
+ const onJobAbandoned = options?.onJobAbandoned;
43
58
  const logger = packageLogger.child({ queue: name });
44
59
  let bullQueue = null;
45
60
  let bullWorker = null;
46
61
  let bullmqModule = null;
62
+ let abandonedSweepTimer = null;
63
+ let closing = false;
64
+ const pendingAbandonedReports = /* @__PURE__ */ new Set();
65
+ const inFlightAbandonedJobIds = /* @__PURE__ */ new Set();
66
+ async function acknowledgeAbandonedReport(job) {
67
+ if (!job.data || typeof job.updateData !== "function") return;
68
+ await job.updateData({
69
+ ...job.data,
70
+ metadata: { ...job.data.metadata ?? {}, [ABANDON_REPORT_ACK_KEY]: (/* @__PURE__ */ new Date()).toISOString() }
71
+ });
72
+ }
73
+ function reportAbandonedJob(job, reason) {
74
+ if (!onJobAbandoned) return null;
75
+ const payload = job.data;
76
+ if (!payload) return null;
77
+ if (payload.metadata && payload.metadata[ABANDON_REPORT_ACK_KEY]) return null;
78
+ if (inFlightAbandonedJobIds.has(payload.id)) return null;
79
+ inFlightAbandonedJobIds.add(payload.id);
80
+ const jobId = job.id ?? null;
81
+ logger.warn("Job abandoned by the queue without running its handler", { jobId, reason });
82
+ const report = (async () => {
83
+ try {
84
+ await onJobAbandoned(payload, { jobId, reason });
85
+ } catch (hookError) {
86
+ logger.error("onJobAbandoned handler threw; the report stays unacknowledged and the sweep will retry it", {
87
+ jobId,
88
+ err: hookError
89
+ });
90
+ return;
91
+ }
92
+ try {
93
+ await acknowledgeAbandonedReport(job);
94
+ } catch (ackError) {
95
+ logger.error("Failed to acknowledge an abandoned-job report; the sweep may repeat it", {
96
+ jobId,
97
+ err: ackError
98
+ });
99
+ }
100
+ })().finally(() => {
101
+ inFlightAbandonedJobIds.delete(payload.id);
102
+ });
103
+ pendingAbandonedReports.add(report);
104
+ void report.then(() => pendingAbandonedReports.delete(report));
105
+ return report;
106
+ }
107
+ async function sweepAbandonedJobs() {
108
+ if (!onJobAbandoned || closing) return;
109
+ try {
110
+ const queue = await getQueue();
111
+ const failedJobs = await queue.getJobs(["failed"], 0, -1);
112
+ if (closing) return;
113
+ for (const failedJob of failedJobs) {
114
+ if (closing) return;
115
+ const reason = failedJob.failedReason ?? "";
116
+ if (!isAbandonedJobReason(reason)) continue;
117
+ await reportAbandonedJob(failedJob, reason);
118
+ }
119
+ } catch (sweepError) {
120
+ logger.error("Abandoned-job sweep failed", { err: sweepError });
121
+ }
122
+ }
47
123
  let telemetryPromise = null;
48
124
  async function getBullMQ() {
49
125
  if (!bullmqModule) {
@@ -99,7 +175,7 @@ function createAsyncQueue(name, options) {
99
175
  });
100
176
  return job.id ?? jobData.id;
101
177
  }
102
- async function process(handler) {
178
+ async function process2(handler) {
103
179
  const { Worker } = await getBullMQ();
104
180
  const telemetry = await getQueueTelemetry();
105
181
  bullWorker = new Worker(
@@ -130,9 +206,13 @@ function createAsyncQueue(name, options) {
130
206
  logger.info("Job completed", { jobId: jobWithId.id });
131
207
  });
132
208
  bullWorker.on("failed", (job, err) => {
133
- const jobWithId = job;
209
+ const failedJob = job;
134
210
  const error = err;
135
- logger.error("Job failed", { jobId: jobWithId?.id, err: error });
211
+ logger.error("Job failed", { jobId: failedJob?.id, err: error });
212
+ if (!onJobAbandoned) return;
213
+ if (!isAbandonedJobReason(error?.message ?? "")) return;
214
+ if (!failedJob?.data) return;
215
+ reportAbandonedJob(failedJob, error.message);
136
216
  });
137
217
  bullWorker.on("stalled", (jobId) => {
138
218
  logger.warn("Job stalled and will be redelivered \u2014 the handler may run concurrently with a previous delivery", {
@@ -143,6 +223,13 @@ function createAsyncQueue(name, options) {
143
223
  const error = err;
144
224
  logger.error("Worker error", { err: error });
145
225
  });
226
+ if (onJobAbandoned) {
227
+ void sweepAbandonedJobs();
228
+ abandonedSweepTimer = setInterval(() => {
229
+ void sweepAbandonedJobs();
230
+ }, resolveSweepIntervalMs());
231
+ abandonedSweepTimer.unref?.();
232
+ }
146
233
  logger.info("Worker started", { concurrency });
147
234
  return { processed: -1, failed: -1, lastJobId: void 0 };
148
235
  }
@@ -166,10 +253,27 @@ function createAsyncQueue(name, options) {
166
253
  return { removed };
167
254
  }
168
255
  async function close() {
256
+ closing = true;
257
+ if (abandonedSweepTimer) {
258
+ clearInterval(abandonedSweepTimer);
259
+ abandonedSweepTimer = null;
260
+ }
169
261
  if (bullWorker) {
170
262
  await bullWorker.close();
171
263
  bullWorker = null;
172
264
  }
265
+ if (pendingAbandonedReports.size) {
266
+ const drained = Promise.all([...pendingAbandonedReports]).then(() => true);
267
+ const expired = new Promise((resolve) => {
268
+ const timer = setTimeout(() => resolve(false), ABANDONED_JOB_DRAIN_TIMEOUT_MS);
269
+ timer.unref?.();
270
+ });
271
+ if (!await Promise.race([drained, expired])) {
272
+ logger.warn("Abandoned-job reports still in flight at shutdown; the sweep will retry them", {
273
+ pending: pendingAbandonedReports.size
274
+ });
275
+ }
276
+ }
173
277
  if (bullQueue) {
174
278
  await bullQueue.close();
175
279
  bullQueue = null;
@@ -189,7 +293,7 @@ function createAsyncQueue(name, options) {
189
293
  name,
190
294
  strategy: "async",
191
295
  enqueue,
192
- process,
296
+ process: process2,
193
297
  clear,
194
298
  removeQueuedJobsByScope,
195
299
  close,
@@ -197,6 +301,9 @@ function createAsyncQueue(name, options) {
197
301
  };
198
302
  }
199
303
  export {
304
+ ABANDONED_JOB_DRAIN_TIMEOUT_MS,
305
+ ABANDONED_JOB_REASONS,
306
+ ABANDONED_JOB_SWEEP_INTERVAL_MS,
200
307
  createAsyncQueue
201
308
  };
202
309
  //# sourceMappingURL=async.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/strategies/async.ts"],
4
- "sourcesContent": ["import type { Queue, QueuedJob, JobHandler, AsyncQueueOptions, ProcessResult, EnqueueOptions, QueueJobScope } from '../types'\nimport { getRedisUrlOrThrow, parseRedisUrl, REDIS_WIRE_PROTOCOL } from '@open-mercato/shared/lib/redis/connection'\nimport type { RedisProtocolVersion } from '@open-mercato/shared/lib/redis/connection'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { attachTraceMetadata, runJobInTrace } from '../tracing'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst packageLogger = createLogger('queue')\n\n// BullMQ interface types - we define the shape we use to maintain type safety\n// while keeping bullmq as an optional peer dependency\ntype ConnectionOptions = {\n host?: string\n port?: number\n username?: string\n password?: string\n db?: number\n tls?: Record<string, unknown>\n family?: number\n protocol?: RedisProtocolVersion\n}\n\ninterface BullQueueInterface<T> {\n add: (\n name: string,\n data: T,\n opts?: {\n removeOnComplete?: boolean\n removeOnFail?: number\n delay?: number\n attempts?: number\n backoff?: { type: string; delay: number }\n },\n ) => Promise<{ id?: string }>\n obliterate: (opts?: { force?: boolean }) => Promise<void>\n close: () => Promise<void>\n getJobCounts: (...states: string[]) => Promise<Record<string, number>>\n getJobs: (types: string[], start?: number, end?: number) => Promise<Array<{\n data?: T\n remove: () => Promise<void>\n }>>\n}\n\ninterface BullWorkerInterface {\n on: (event: string, handler: (...args: unknown[]) => void) => void\n close: () => Promise<void>\n}\n\ninterface BullMQModule {\n Queue: new <T>(name: string, opts: { connection: ConnectionOptions; telemetry?: unknown }) => BullQueueInterface<T>\n Worker: new <T>(\n name: string,\n processor: (job: { id?: string; data: T; attemptsMade: number }) => Promise<void>,\n opts: {\n connection: ConnectionOptions\n concurrency: number\n telemetry?: unknown\n lockDuration?: number\n maxStalledCount?: number\n }\n ) => BullWorkerInterface\n}\n\n/** The `bullmq-otel` package (optional). Loaded only when an OTLP backend is active. */\ntype BullMQOtelModule = { BullMQOtel: new (tracerName: string) => object }\n\nconst REMOVABLE_JOB_STATES = ['waiting', 'delayed', 'prioritized', 'paused', 'waiting-children']\n\nfunction payloadMatchesScope(payload: unknown, scope: QueueJobScope): boolean {\n if (!payload || typeof payload !== 'object') return false\n const scopedPayload = payload as { tenantId?: unknown; organizationId?: unknown; jobType?: unknown }\n if (scopedPayload.tenantId !== scope.tenantId) return false\n if (scope.organizationId !== undefined) {\n if ((scopedPayload.organizationId ?? null) !== scope.organizationId) return false\n }\n if (scope.jobTypes?.length) {\n return typeof scopedPayload.jobType === 'string' && scope.jobTypes.includes(scopedPayload.jobType)\n }\n return true\n}\n\n/**\n * Resolves Redis connection options from various sources.\n *\n * BullMQ expects ioredis connection fields rather than a nested URL string.\n * Parse URL-based configuration at this boundary while keeping the public\n * queue API compatible with existing `{ url }` callers.\n */\nfunction resolveConnection(options?: AsyncQueueOptions['connection']): ConnectionOptions {\n if (options?.url) {\n return parseRedisUrl(options.url)\n }\n\n if (options?.host) {\n return {\n host: options.host,\n port: options.port ?? 6379,\n username: options.username,\n password: options.password,\n db: options.db,\n tls: options.tls,\n family: options.family,\n protocol: REDIS_WIRE_PROTOCOL,\n }\n }\n\n return parseRedisUrl(getRedisUrlOrThrow('QUEUE'))\n}\n\n/**\n * Creates a BullMQ-based async queue.\n *\n * This strategy provides:\n * - Persistent job storage in Redis\n * - Automatic retries with exponential backoff\n * - Concurrent job processing\n * - Job prioritization and scheduling\n *\n * @template T - The payload type for jobs\n * @param name - Queue name\n * @param options - Async queue options\n */\nexport function createAsyncQueue<T = unknown>(\n name: string,\n options?: AsyncQueueOptions\n): Queue<T> {\n const connection = resolveConnection(options?.connection)\n const concurrency = options?.concurrency ?? 1\n const attempts = options?.attempts ?? 3\n const lockDuration = options?.lockDuration\n const maxStalledCount = options?.maxStalledCount\n const logger = packageLogger.child({ queue: name })\n\n let bullQueue: BullQueueInterface<QueuedJob<T>> | null = null\n let bullWorker: BullWorkerInterface | null = null\n let bullmqModule: BullMQModule | null = null\n // Resolved once: a BullMQOtel instance (delegate async tracing to BullMQ) or\n // undefined (use our own metadata._trace carrier instead). Memoized as the\n // in-flight promise so concurrent first-time callers share one resolution.\n let telemetryPromise: Promise<object | undefined> | null = null\n\n // -------------------------------------------------------------------------\n // Lazy BullMQ initialization\n // -------------------------------------------------------------------------\n\n async function getBullMQ(): Promise<BullMQModule> {\n if (!bullmqModule) {\n try {\n bullmqModule = await import('bullmq') as unknown as BullMQModule\n } catch {\n throw new Error(\n 'BullMQ is required for async queue strategy. Install it with: npm install bullmq'\n )\n }\n }\n return bullmqModule\n }\n\n /**\n * When an OTLP backend is active, delegate async-queue tracing to `bullmq-otel`\n * (richer BullMQ-internal spans: add / process / wait / attempts). Returns\n * `undefined` \u2014 meaning \"use our own `metadata._trace` carrier\" \u2014 when telemetry\n * is off, a non-OTEL backend is selected, or `bullmq-otel` isn't installed. (The\n * `local` strategy always uses our carrier; it isn't BullMQ, so `bullmq-otel`\n * cannot instrument it.)\n */\n async function getQueueTelemetry(): Promise<object | undefined> {\n if (!telemetryPromise) {\n telemetryPromise = (async () => {\n if (!getTelemetryRuntime()?.canUseGlobalTracePropagation()) return undefined\n try {\n const mod = (await import('bullmq-otel')) as unknown as BullMQOtelModule\n return new mod.BullMQOtel('open-mercato')\n } catch {\n packageLogger.warn('bullmq-otel not available; using built-in trace carrier', { queue: name })\n return undefined\n }\n })()\n }\n return telemetryPromise\n }\n\n async function getQueue(): Promise<BullQueueInterface<QueuedJob<T>>> {\n if (!bullQueue) {\n const { Queue: BullQueueClass } = await getBullMQ()\n const telemetry = await getQueueTelemetry()\n bullQueue = new BullQueueClass<QueuedJob<T>>(name, { connection, ...(telemetry ? { telemetry } : {}) })\n }\n return bullQueue\n }\n\n // -------------------------------------------------------------------------\n // Queue Implementation\n // -------------------------------------------------------------------------\n\n async function enqueue(data: T, options?: EnqueueOptions): Promise<string> {\n const queue = await getQueue()\n // When bullmq-otel handles propagation, don't also attach our carrier.\n const telemetry = await getQueueTelemetry()\n const metadata = telemetry ? undefined : attachTraceMetadata(undefined)\n const jobData: QueuedJob<T> = {\n id: crypto.randomUUID(),\n payload: data,\n createdAt: new Date().toISOString(),\n ...(metadata ? { metadata } : {}),\n }\n\n const job = await queue.add(jobData.id, jobData, {\n delay: options?.delayMs && options.delayMs > 0 ? options.delayMs : undefined,\n removeOnComplete: true,\n removeOnFail: 1000,\n attempts,\n backoff: { type: 'exponential', delay: 1000 },\n })\n\n return job.id ?? jobData.id\n }\n\n async function process(handler: JobHandler<T>): Promise<ProcessResult> {\n const { Worker } = await getBullMQ()\n const telemetry = await getQueueTelemetry()\n\n // Create worker that processes jobs\n bullWorker = new Worker<QueuedJob<T>>(\n name,\n async (job) => {\n const jobData = job.data\n const ctx = {\n jobId: job.id ?? jobData.id,\n attemptNumber: job.attemptsMade + 1,\n queueName: name,\n }\n // With bullmq-otel active, BullMQ owns the process span and active\n // context (the handler's pg/undici spans nest under it). Otherwise\n // continue the trace from our own carrier.\n if (telemetry) {\n await handler(jobData, ctx)\n } else {\n await runJobInTrace(name, jobData.metadata, () => handler(jobData, ctx))\n }\n },\n {\n connection,\n concurrency,\n ...(telemetry ? { telemetry } : {}),\n ...(lockDuration !== undefined ? { lockDuration } : {}),\n ...(maxStalledCount !== undefined ? { maxStalledCount } : {}),\n }\n )\n\n // Set up event handlers\n bullWorker.on('completed', (job) => {\n const jobWithId = job as { id?: string }\n logger.info('Job completed', { jobId: jobWithId.id })\n })\n\n bullWorker.on('failed', (job, err) => {\n const jobWithId = job as { id?: string } | undefined\n const error = err as Error\n logger.error('Job failed', { jobId: jobWithId?.id, err: error })\n })\n\n // A stalled job is redelivered under the same id while the previous worker\n // may still be running it, so this is the signal that a handler is about to\n // be executed twice. BullMQ's docs require surfacing it: without this line\n // duplicate processing is invisible.\n bullWorker.on('stalled', (jobId) => {\n logger.warn('Job stalled and will be redelivered \u2014 the handler may run concurrently with a previous delivery', {\n jobId: typeof jobId === 'string' ? jobId : null,\n })\n })\n\n bullWorker.on('error', (err) => {\n const error = err as Error\n logger.error('Worker error', { err: error })\n })\n\n logger.info('Worker started', { concurrency })\n\n // For async strategy, return a sentinel result indicating worker mode\n // processed=-1 signals that this is a continuous worker, not a batch process\n return { processed: -1, failed: -1, lastJobId: undefined }\n }\n\n async function clear(): Promise<{ removed: number }> {\n const queue = await getQueue()\n\n // Obliterate removes all jobs from the queue\n await queue.obliterate({ force: true })\n\n return { removed: -1 } // BullMQ obliterate doesn't return count\n }\n\n async function removeQueuedJobsByScope(scope: QueueJobScope): Promise<{ removed: number }> {\n const queue = await getQueue()\n const jobs = await queue.getJobs(REMOVABLE_JOB_STATES, 0, -1)\n let removed = 0\n\n for (const job of jobs) {\n if (!payloadMatchesScope(job.data?.payload, scope)) continue\n try {\n await job.remove()\n removed++\n } catch {\n // The job may have started between enumeration and removal. In-flight\n // cancellation is handled by the caller's lock/heartbeat contract.\n }\n }\n\n return { removed }\n }\n\n async function close(): Promise<void> {\n if (bullWorker) {\n await bullWorker.close()\n bullWorker = null\n }\n if (bullQueue) {\n await bullQueue.close()\n bullQueue = null\n }\n }\n\n async function getJobCounts(): Promise<{\n waiting: number\n active: number\n completed: number\n failed: number\n }> {\n const queue = await getQueue()\n const counts = await queue.getJobCounts('waiting', 'active', 'completed', 'failed')\n return {\n waiting: counts.waiting ?? 0,\n active: counts.active ?? 0,\n completed: counts.completed ?? 0,\n failed: counts.failed ?? 0,\n }\n }\n\n return {\n name,\n strategy: 'async',\n enqueue,\n process,\n clear,\n removeQueuedJobsByScope,\n close,\n getJobCounts,\n }\n}\n"],
5
- "mappings": "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",
6
- "names": ["options"]
4
+ "sourcesContent": ["import type { Queue, QueuedJob, JobHandler, AsyncQueueOptions, ProcessResult, EnqueueOptions, QueueJobScope } from '../types'\nimport { getRedisUrlOrThrow, parseRedisUrl, REDIS_WIRE_PROTOCOL } from '@open-mercato/shared/lib/redis/connection'\nimport type { RedisProtocolVersion } from '@open-mercato/shared/lib/redis/connection'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { attachTraceMetadata, runJobInTrace } from '../tracing'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst packageLogger = createLogger('queue')\n\n// BullMQ interface types - we define the shape we use to maintain type safety\n// while keeping bullmq as an optional peer dependency\ntype ConnectionOptions = {\n host?: string\n port?: number\n username?: string\n password?: string\n db?: number\n tls?: Record<string, unknown>\n family?: number\n protocol?: RedisProtocolVersion\n}\n\ninterface BullQueueInterface<T> {\n add: (\n name: string,\n data: T,\n opts?: {\n removeOnComplete?: boolean\n removeOnFail?: number\n delay?: number\n attempts?: number\n backoff?: { type: string; delay: number }\n },\n ) => Promise<{ id?: string }>\n obliterate: (opts?: { force?: boolean }) => Promise<void>\n close: () => Promise<void>\n getJobCounts: (...states: string[]) => Promise<Record<string, number>>\n getJobs: (types: string[], start?: number, end?: number) => Promise<Array<{\n id?: string\n data?: T\n failedReason?: string\n remove: () => Promise<void>\n updateData?: (data: T) => Promise<void>\n }>>\n}\n\ninterface BullWorkerInterface {\n on: (event: string, handler: (...args: unknown[]) => void) => void\n close: () => Promise<void>\n}\n\ninterface BullMQModule {\n Queue: new <T>(name: string, opts: { connection: ConnectionOptions; telemetry?: unknown }) => BullQueueInterface<T>\n Worker: new <T>(\n name: string,\n processor: (job: { id?: string; data: T; attemptsMade: number }) => Promise<void>,\n opts: {\n connection: ConnectionOptions\n concurrency: number\n telemetry?: unknown\n lockDuration?: number\n maxStalledCount?: number\n }\n ) => BullWorkerInterface\n}\n\n/** The `bullmq-otel` package (optional). Loaded only when an OTLP backend is active. */\ntype BullMQOtelModule = { BullMQOtel: new (tracerName: string) => object }\n\nconst REMOVABLE_JOB_STATES = ['waiting', 'delayed', 'prioritized', 'paused', 'waiting-children']\n\n/**\n * The failures BullMQ records when it gives up on a job *before* handing it to the processor.\n *\n * Both are written as a `defa` (deferred failure) marker on the job, after which the next worker\n * short-circuits in `Worker.processJob` via `getUnrecoverableErrorMessage` and fails the job without\n * calling the handler. The first comes from the stalled-job script once a job's cumulative stall\n * count passes `maxStalledCount`; the second from `maxStartedAttempts`.\n *\n * Matching the reason is what tells \"the queue abandoned this\" from \"the handler ran and threw\", and\n * it is deliberately stateless: the alternative \u2014 tracking which jobs this process has entered \u2014 can\n * only answer \"did the handler run *here*\", which is the wrong question the moment more than one\n * worker is running. `bullmq-abandoned-reasons.test.ts` asserts these strings still exist in the\n * installed BullMQ, so an upgrade that renames them fails loudly instead of silently disabling the\n * callback.\n */\nexport const ABANDONED_JOB_REASONS = [\n 'job stalled more than allowable limit',\n 'job started more than allowable limit',\n] as const\n\nfunction isAbandonedJobReason(message: string): boolean {\n return (ABANDONED_JOB_REASONS as readonly string[]).includes(message)\n}\n\n/**\n * Metadata key written onto the stored job once `onJobAbandoned` has completed for it.\n *\n * BullMQ's failed set is the durable record of abandoned jobs (`removeOnFail` keeps them), so it\n * doubles as the dead-letter queue for reports: the sweep re-delivers any abandoned job that does not\n * carry this marker. The marker \u2014 not `job.remove()` \u2014 is the acknowledgement, so the failed job\n * itself survives for diagnosis.\n */\nconst ABANDON_REPORT_ACK_KEY = 'abandonReportedAt'\n// NOTE for anyone adding a retry action: the marker lives inside the job's own payload envelope, so a\n// job retried from admin tooling carries it into its next life and a second abandonment of that job\n// would never be reported. A retry path must clear `metadata.abandonReportedAt` when it re-enqueues.\n\n/**\n * How often a worker re-sweeps the failed set for unacknowledged abandoned jobs.\n *\n * Override with `QUEUE_ABANDONED_SWEEP_INTERVAL_MS` to trade recovery latency against Redis chatter.\n */\nexport const ABANDONED_JOB_SWEEP_INTERVAL_MS = 5 * 60 * 1000\n\n/**\n * How long `close()` waits for in-flight reports before giving up on them.\n *\n * Bounded on purpose: the hook reaches a database, and a shutdown during an infrastructure incident\n * is exactly when that write can hang rather than fail. An unbounded wait would turn a graceful\n * shutdown into a SIGKILL and skip the telemetry flush that follows it. Abandoning the wait is safe\n * because delivery is at-least-once \u2014 an unacknowledged report is re-delivered by the next worker's\n * start-up sweep, the same path that covers a process which died mid-report.\n */\nexport const ABANDONED_JOB_DRAIN_TIMEOUT_MS = 5000\n\nfunction resolveSweepIntervalMs(): number {\n const configured = Number.parseInt(process.env.QUEUE_ABANDONED_SWEEP_INTERVAL_MS ?? '', 10)\n return Number.isFinite(configured) && configured > 0 ? configured : ABANDONED_JOB_SWEEP_INTERVAL_MS\n}\n\nfunction payloadMatchesScope(payload: unknown, scope: QueueJobScope): boolean {\n if (!payload || typeof payload !== 'object') return false\n const scopedPayload = payload as { tenantId?: unknown; organizationId?: unknown; jobType?: unknown }\n if (scopedPayload.tenantId !== scope.tenantId) return false\n if (scope.organizationId !== undefined) {\n if ((scopedPayload.organizationId ?? null) !== scope.organizationId) return false\n }\n if (scope.jobTypes?.length) {\n return typeof scopedPayload.jobType === 'string' && scope.jobTypes.includes(scopedPayload.jobType)\n }\n return true\n}\n\n/**\n * Resolves Redis connection options from various sources.\n *\n * BullMQ expects ioredis connection fields rather than a nested URL string.\n * Parse URL-based configuration at this boundary while keeping the public\n * queue API compatible with existing `{ url }` callers.\n */\nfunction resolveConnection(options?: AsyncQueueOptions['connection']): ConnectionOptions {\n if (options?.url) {\n return parseRedisUrl(options.url)\n }\n\n if (options?.host) {\n return {\n host: options.host,\n port: options.port ?? 6379,\n username: options.username,\n password: options.password,\n db: options.db,\n tls: options.tls,\n family: options.family,\n protocol: REDIS_WIRE_PROTOCOL,\n }\n }\n\n return parseRedisUrl(getRedisUrlOrThrow('QUEUE'))\n}\n\n/**\n * Creates a BullMQ-based async queue.\n *\n * This strategy provides:\n * - Persistent job storage in Redis\n * - Automatic retries with exponential backoff\n * - Concurrent job processing\n * - Job prioritization and scheduling\n *\n * @template T - The payload type for jobs\n * @param name - Queue name\n * @param options - Async queue options\n */\nexport function createAsyncQueue<T = unknown>(\n name: string,\n options?: AsyncQueueOptions\n): Queue<T> {\n const connection = resolveConnection(options?.connection)\n const concurrency = options?.concurrency ?? 1\n const attempts = options?.attempts ?? 3\n const lockDuration = options?.lockDuration\n const maxStalledCount = options?.maxStalledCount\n const onJobAbandoned = options?.onJobAbandoned\n const logger = packageLogger.child({ queue: name })\n\n let bullQueue: BullQueueInterface<QueuedJob<T>> | null = null\n let bullWorker: BullWorkerInterface | null = null\n let bullmqModule: BullMQModule | null = null\n let abandonedSweepTimer: ReturnType<typeof setInterval> | null = null\n let closing = false\n\n // In-flight `onJobAbandoned` calls. Detached from the caller that started them (the 'failed'\n // listener or the sweep), so `close()` drains them rather than letting a deploy truncate a repair\n // mid-write. The id set stops the two callers from double-reporting a job inside one process.\n const pendingAbandonedReports = new Set<Promise<void>>()\n const inFlightAbandonedJobIds = new Set<string>()\n\n type AbandonedJobRecord = {\n id?: string\n data?: QueuedJob<T>\n updateData?: (data: QueuedJob<T>) => Promise<void>\n }\n\n // The acknowledgement that makes delivery at-least-once: written only after the callback returns,\n // so a callback that threw or a process that died mid-report leaves the job unmarked and a later\n // sweep retries it. Requires the driver to expose `updateData`; without it the report simply stays\n // unacknowledged and repeats, which the idempotency contract permits.\n async function acknowledgeAbandonedReport(job: AbandonedJobRecord): Promise<void> {\n if (!job.data || typeof job.updateData !== 'function') return\n await job.updateData({\n ...job.data,\n metadata: { ...(job.data.metadata ?? {}), [ABANDON_REPORT_ACK_KEY]: new Date().toISOString() },\n })\n }\n\n function reportAbandonedJob(job: AbandonedJobRecord, reason: string): Promise<void> | null {\n if (!onJobAbandoned) return null\n const payload = job.data\n if (!payload) return null\n if (payload.metadata && payload.metadata[ABANDON_REPORT_ACK_KEY]) return null\n if (inFlightAbandonedJobIds.has(payload.id)) return null\n inFlightAbandonedJobIds.add(payload.id)\n\n const jobId = job.id ?? null\n logger.warn('Job abandoned by the queue without running its handler', { jobId, reason })\n const report = (async () => {\n try {\n await onJobAbandoned(payload, { jobId, reason })\n } catch (hookError) {\n logger.error('onJobAbandoned handler threw; the report stays unacknowledged and the sweep will retry it', {\n jobId,\n err: hookError as Error,\n })\n return\n }\n try {\n await acknowledgeAbandonedReport(job)\n } catch (ackError) {\n logger.error('Failed to acknowledge an abandoned-job report; the sweep may repeat it', {\n jobId,\n err: ackError as Error,\n })\n }\n })().finally(() => {\n inFlightAbandonedJobIds.delete(payload.id)\n })\n pendingAbandonedReports.add(report)\n void report.then(() => pendingAbandonedReports.delete(report))\n return report\n }\n\n // The failed set is this strategy's dead-letter queue for abandoned jobs. Enumerating it on worker\n // start and on an interval, and re-delivering anything unacknowledged, is what upgrades the\n // 'failed'-listener fast path from at-most-once to at-least-once: a report lost to a crash is\n // simply still unmarked when the next sweep looks. Residual loss: `removeOnFail` caps the set, so\n // a job evicted before any sweep sees it is gone for good.\n async function sweepAbandonedJobs(): Promise<void> {\n if (!onJobAbandoned || closing) return\n try {\n const queue = await getQueue()\n const failedJobs = await queue.getJobs(['failed'], 0, -1)\n // Re-checked after the awaits: a sweep already past its guard when `close()` ran would\n // otherwise start a report the drain has stopped waiting for. The next start-up sweep\n // re-delivers it, so stopping here loses nothing.\n if (closing) return\n for (const failedJob of failedJobs) {\n // Re-checked every iteration for the same reason: a long fan-out must not outlive the drain.\n if (closing) return\n const reason = failedJob.failedReason ?? ''\n if (!isAbandonedJobReason(reason)) continue\n // Awaited one at a time. Each report opens a request container and writes to the database, and\n // the worst case for this loop is the first worker start after the feature ships, on a\n // deployment that has been accumulating abandoned jobs \u2014 the largest backlog, on the process\n // least able to absorb it. The sweep is a recovery path with no latency requirement (five\n // minutes late is its normal mode), so pacing costs nothing worth having.\n await reportAbandonedJob(failedJob, reason)\n }\n } catch (sweepError) {\n logger.error('Abandoned-job sweep failed', { err: sweepError as Error })\n }\n }\n // Resolved once: a BullMQOtel instance (delegate async tracing to BullMQ) or\n // undefined (use our own metadata._trace carrier instead). Memoized as the\n // in-flight promise so concurrent first-time callers share one resolution.\n let telemetryPromise: Promise<object | undefined> | null = null\n\n // -------------------------------------------------------------------------\n // Lazy BullMQ initialization\n // -------------------------------------------------------------------------\n\n async function getBullMQ(): Promise<BullMQModule> {\n if (!bullmqModule) {\n try {\n bullmqModule = await import('bullmq') as unknown as BullMQModule\n } catch {\n throw new Error(\n 'BullMQ is required for async queue strategy. Install it with: npm install bullmq'\n )\n }\n }\n return bullmqModule\n }\n\n /**\n * When an OTLP backend is active, delegate async-queue tracing to `bullmq-otel`\n * (richer BullMQ-internal spans: add / process / wait / attempts). Returns\n * `undefined` \u2014 meaning \"use our own `metadata._trace` carrier\" \u2014 when telemetry\n * is off, a non-OTEL backend is selected, or `bullmq-otel` isn't installed. (The\n * `local` strategy always uses our carrier; it isn't BullMQ, so `bullmq-otel`\n * cannot instrument it.)\n */\n async function getQueueTelemetry(): Promise<object | undefined> {\n if (!telemetryPromise) {\n telemetryPromise = (async () => {\n if (!getTelemetryRuntime()?.canUseGlobalTracePropagation()) return undefined\n try {\n const mod = (await import('bullmq-otel')) as unknown as BullMQOtelModule\n return new mod.BullMQOtel('open-mercato')\n } catch {\n packageLogger.warn('bullmq-otel not available; using built-in trace carrier', { queue: name })\n return undefined\n }\n })()\n }\n return telemetryPromise\n }\n\n async function getQueue(): Promise<BullQueueInterface<QueuedJob<T>>> {\n if (!bullQueue) {\n const { Queue: BullQueueClass } = await getBullMQ()\n const telemetry = await getQueueTelemetry()\n bullQueue = new BullQueueClass<QueuedJob<T>>(name, { connection, ...(telemetry ? { telemetry } : {}) })\n }\n return bullQueue\n }\n\n // -------------------------------------------------------------------------\n // Queue Implementation\n // -------------------------------------------------------------------------\n\n async function enqueue(data: T, options?: EnqueueOptions): Promise<string> {\n const queue = await getQueue()\n // When bullmq-otel handles propagation, don't also attach our carrier.\n const telemetry = await getQueueTelemetry()\n const metadata = telemetry ? undefined : attachTraceMetadata(undefined)\n const jobData: QueuedJob<T> = {\n id: crypto.randomUUID(),\n payload: data,\n createdAt: new Date().toISOString(),\n ...(metadata ? { metadata } : {}),\n }\n\n const job = await queue.add(jobData.id, jobData, {\n delay: options?.delayMs && options.delayMs > 0 ? options.delayMs : undefined,\n removeOnComplete: true,\n removeOnFail: 1000,\n attempts,\n backoff: { type: 'exponential', delay: 1000 },\n })\n\n return job.id ?? jobData.id\n }\n\n async function process(handler: JobHandler<T>): Promise<ProcessResult> {\n const { Worker } = await getBullMQ()\n const telemetry = await getQueueTelemetry()\n\n // Create worker that processes jobs\n bullWorker = new Worker<QueuedJob<T>>(\n name,\n async (job) => {\n const jobData = job.data\n const ctx = {\n jobId: job.id ?? jobData.id,\n attemptNumber: job.attemptsMade + 1,\n queueName: name,\n }\n // With bullmq-otel active, BullMQ owns the process span and active\n // context (the handler's pg/undici spans nest under it). Otherwise\n // continue the trace from our own carrier.\n if (telemetry) {\n await handler(jobData, ctx)\n } else {\n await runJobInTrace(name, jobData.metadata, () => handler(jobData, ctx))\n }\n },\n {\n connection,\n concurrency,\n ...(telemetry ? { telemetry } : {}),\n ...(lockDuration !== undefined ? { lockDuration } : {}),\n ...(maxStalledCount !== undefined ? { maxStalledCount } : {}),\n }\n )\n\n // Set up event handlers\n bullWorker.on('completed', (job) => {\n const jobWithId = job as { id?: string }\n logger.info('Job completed', { jobId: jobWithId.id })\n })\n\n bullWorker.on('failed', (job, err) => {\n const failedJob = job as AbandonedJobRecord | undefined\n const error = err as Error\n logger.error('Job failed', { jobId: failedJob?.id, err: error })\n\n if (!onJobAbandoned) return\n // Any other reason means a handler ran and threw. That failure is the handler's own and it has\n // already had its chance to record it.\n if (!isAbandonedJobReason(error?.message ?? '')) return\n // No payload means the queue could not give us the job at all. There is nothing to hand the\n // callback and nothing it could repair, so reporting could only ever be a false alarm \u2014 the\n // 'Job failed' line above still records it.\n if (!failedJob?.data) return\n\n // The fast path: report the moment the abandonment is observed. `reportAbandonedJob` runs the\n // callback detached with its own try/catch \u2014 this is an EventEmitter, where an unhandled\n // rejection is fatal to the process \u2014 and acknowledges the job only afterwards, so a report\n // lost here is retried by the sweep.\n reportAbandonedJob(failedJob, error.message)\n })\n\n // A stalled job is redelivered under the same id while the previous worker\n // may still be running it, so this is the signal that a handler is about to\n // be executed twice. BullMQ's docs require surfacing it: without this line\n // duplicate processing is invisible.\n bullWorker.on('stalled', (jobId) => {\n logger.warn('Job stalled and will be redelivered \u2014 the handler may run concurrently with a previous delivery', {\n jobId: typeof jobId === 'string' ? jobId : null,\n })\n })\n\n bullWorker.on('error', (err) => {\n const error = err as Error\n logger.error('Worker error', { err: error })\n })\n\n if (onJobAbandoned) {\n // Sweep immediately so reports lost to a previous process's crash are re-delivered as soon as\n // a worker is back, then keep re-sweeping for anything the fast path loses while running.\n // The timer is unref'd so it never holds the process open.\n void sweepAbandonedJobs()\n abandonedSweepTimer = setInterval(() => {\n void sweepAbandonedJobs()\n }, resolveSweepIntervalMs())\n ;(abandonedSweepTimer as unknown as { unref?: () => void }).unref?.()\n }\n\n logger.info('Worker started', { concurrency })\n\n // For async strategy, return a sentinel result indicating worker mode\n // processed=-1 signals that this is a continuous worker, not a batch process\n return { processed: -1, failed: -1, lastJobId: undefined }\n }\n\n async function clear(): Promise<{ removed: number }> {\n const queue = await getQueue()\n\n // Obliterate removes all jobs from the queue\n await queue.obliterate({ force: true })\n\n return { removed: -1 } // BullMQ obliterate doesn't return count\n }\n\n async function removeQueuedJobsByScope(scope: QueueJobScope): Promise<{ removed: number }> {\n const queue = await getQueue()\n const jobs = await queue.getJobs(REMOVABLE_JOB_STATES, 0, -1)\n let removed = 0\n\n for (const job of jobs) {\n if (!payloadMatchesScope(job.data?.payload, scope)) continue\n try {\n await job.remove()\n removed++\n } catch {\n // The job may have started between enumeration and removal. In-flight\n // cancellation is handled by the caller's lock/heartbeat contract.\n }\n }\n\n return { removed }\n }\n\n async function close(): Promise<void> {\n closing = true\n if (abandonedSweepTimer) {\n clearInterval(abandonedSweepTimer)\n abandonedSweepTimer = null\n }\n if (bullWorker) {\n await bullWorker.close()\n bullWorker = null\n }\n // Drain any abandonment report still in flight, so a deploy-time shutdown cannot cut off the very\n // repair the callback exists to perform. Bounded: the hook writes to a database, and a shutdown\n // during an incident is exactly when that write can hang instead of failing. Giving up costs\n // nothing permanent \u2014 an unacknowledged report is re-delivered by the next start-up sweep.\n if (pendingAbandonedReports.size) {\n const drained = Promise.all([...pendingAbandonedReports]).then(() => true)\n const expired = new Promise<boolean>((resolve) => {\n const timer = setTimeout(() => resolve(false), ABANDONED_JOB_DRAIN_TIMEOUT_MS)\n ;(timer as unknown as { unref?: () => void }).unref?.()\n })\n if (!(await Promise.race([drained, expired]))) {\n logger.warn('Abandoned-job reports still in flight at shutdown; the sweep will retry them', {\n pending: pendingAbandonedReports.size,\n })\n }\n }\n if (bullQueue) {\n await bullQueue.close()\n bullQueue = null\n }\n }\n\n async function getJobCounts(): Promise<{\n waiting: number\n active: number\n completed: number\n failed: number\n }> {\n const queue = await getQueue()\n const counts = await queue.getJobCounts('waiting', 'active', 'completed', 'failed')\n return {\n waiting: counts.waiting ?? 0,\n active: counts.active ?? 0,\n completed: counts.completed ?? 0,\n failed: counts.failed ?? 0,\n }\n }\n\n return {\n name,\n strategy: 'async',\n enqueue,\n process,\n clear,\n removeQueuedJobsByScope,\n close,\n getJobCounts,\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["options", "process"]
7
7
  }
@@ -73,6 +73,7 @@ async function runWorker(options) {
73
73
  concurrency = 1,
74
74
  lockDuration,
75
75
  maxStalledCount,
76
+ onJobAbandoned,
76
77
  gracefulShutdown = true,
77
78
  background = false,
78
79
  strategy: strategyOption
@@ -87,7 +88,8 @@ async function runWorker(options) {
87
88
  connection,
88
89
  concurrency,
89
90
  lockDuration,
90
- maxStalledCount
91
+ maxStalledCount,
92
+ onJobAbandoned
91
93
  });
92
94
  if (gracefulShutdown) {
93
95
  managedQueues.add(queue);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/worker/runner.ts"],
4
- "sourcesContent": ["import { createQueue } from '../factory'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n getTelemetryRuntime,\n isTelemetryBackendEnabled,\n} from '@open-mercato/shared/lib/telemetry/runtime'\nimport type { Queue, JobHandler, AsyncQueueOptions, QueueStrategyType } from '../types'\n\nconst logger = createLogger('queue').child({ component: 'worker' })\n\n/**\n * Options for running a queue worker.\n */\nexport type WorkerRunnerOptions<T = unknown> = {\n /** Name of the queue to process */\n queueName: string\n /** Handler function to process each job */\n handler: JobHandler<T>\n /** Redis connection options (only used for async strategy) */\n connection?: AsyncQueueOptions['connection']\n /** Number of concurrent jobs to process */\n concurrency?: number\n /** How long a job lock is held before the job counts as stalled, in ms. */\n lockDuration?: number\n /** Number of stalled-job recoveries BullMQ permits before failing a job. */\n maxStalledCount?: number\n /** Whether to set up graceful shutdown handlers */\n gracefulShutdown?: boolean\n /** If true, don't block - return immediately after starting processing (for multi-queue mode) */\n background?: boolean\n /** Queue strategy to use. Defaults to QUEUE_STRATEGY env var or 'local' */\n strategy?: QueueStrategyType\n}\n\nconst managedQueues = new Set<Queue<unknown>>()\nconst managedShutdownHooks = new Set<() => Promise<void> | void>()\nlet shutdownHandlersRegistered = false\nlet shutdownInProgress = false\n\nfunction unregisterShutdownHandlers(sigtermHandler: () => void, sigintHandler: () => void): void {\n process.off('SIGTERM', sigtermHandler)\n process.off('SIGINT', sigintHandler)\n shutdownHandlersRegistered = false\n}\n\nfunction registerShutdownHandlers(): void {\n if (shutdownHandlersRegistered) return\n\n const shutdown = async (signal: string) => {\n if (shutdownInProgress) return\n shutdownInProgress = true\n\n logger.info('Received signal, shutting down gracefully', { signal })\n\n let hasError = false\n for (const queue of managedQueues) {\n try {\n await queue.close()\n } catch (error) {\n hasError = true\n logger.error('Error during shutdown', { err: error })\n }\n }\n\n managedQueues.clear()\n for (const hook of managedShutdownHooks) {\n try {\n await hook()\n } catch (error) {\n hasError = true\n logger.error('Error during shutdown hook', { err: error })\n }\n }\n managedShutdownHooks.clear()\n unregisterShutdownHandlers(sigtermHandler, sigintHandler)\n shutdownInProgress = false\n\n // Flush buffered spans/logs before the process dies. A worker never returns\n // from run(), so bin.ts's post-run shutdownTelemetry() is unreachable on this\n // path \u2014 without this, the BatchSpanProcessor's ~5s tail is dropped on every\n // restart/redeploy. Idempotent and a no-op when telemetry is off; a flush\n // failure must not turn a clean shutdown into a failed one.\n try {\n await getTelemetryRuntime()?.shutdown()\n } catch (error) {\n logger.error('Error flushing telemetry during shutdown', { err: error })\n }\n\n if (!hasError) {\n logger.info('Worker closed successfully')\n }\n\n process.exit(hasError ? 1 : 0)\n }\n\n const sigtermHandler = () => {\n void shutdown('SIGTERM')\n }\n\n const sigintHandler = () => {\n void shutdown('SIGINT')\n }\n\n process.on('SIGTERM', sigtermHandler)\n process.on('SIGINT', sigintHandler)\n shutdownHandlersRegistered = true\n}\n\n/**\n * Register a process-local service that must stop before a worker exits.\n * The returned callback removes the hook when the service is stopped early.\n */\nexport function registerWorkerShutdownHook(hook: () => Promise<void> | void): () => void {\n managedShutdownHooks.add(hook)\n return () => managedShutdownHooks.delete(hook)\n}\n\n/**\n * Runs a queue worker that processes jobs continuously.\n *\n * This function:\n * 1. Creates an async queue instance\n * 2. Starts a BullMQ worker\n * 3. Sets up graceful shutdown on SIGTERM/SIGINT\n * 4. Keeps the process running until shutdown\n *\n * @template T - The job payload type\n * @param options - Worker configuration\n *\n * @example\n * ```typescript\n * import { runWorker } from '@open-mercato/queue/worker'\n *\n * await runWorker({\n * queueName: 'events',\n * handler: async (job, ctx) => {\n * console.log(`Processing ${ctx.jobId}:`, job.payload)\n * },\n * connection: { url: process.env.REDIS_URL },\n * concurrency: 5,\n * })\n * ```\n */\nexport async function runWorker<T = unknown>(\n options: WorkerRunnerOptions<T>\n): Promise<void> {\n const {\n queueName,\n handler,\n connection,\n concurrency = 1,\n lockDuration,\n maxStalledCount,\n gracefulShutdown = true,\n background = false,\n strategy: strategyOption,\n } = options\n\n // Worker processes don't run Next's instrumentation hook, so initialize\n // telemetry here \u2014 this is the single bootstrap every standalone worker passes\n // through. Import the telemetry package only for an explicit enabled backend;\n // with the default/unset backend the worker never evaluates the package.\n if (!getTelemetryRuntime() && isTelemetryBackendEnabled()) {\n const { initTelemetry } = await import('@open-mercato/telemetry')\n await initTelemetry()\n }\n\n // Determine queue strategy from option, env var, or default to 'local'\n const strategy: QueueStrategyType = strategyOption\n ?? (process.env.QUEUE_STRATEGY === 'async' ? 'async' : 'local')\n\n logger.info('Starting worker for queue', { queueName, strategy })\n\n const queue = createQueue<T>(queueName, strategy, {\n connection,\n concurrency,\n lockDuration,\n maxStalledCount,\n })\n\n // Set up graceful shutdown\n if (gracefulShutdown) {\n managedQueues.add(queue as Queue<unknown>)\n registerShutdownHandlers()\n }\n\n // Start processing\n await queue.process(handler)\n\n logger.info('Worker running', { concurrency })\n\n if (background) {\n // Return immediately for multi-queue mode\n return\n }\n\n logger.info('Press Ctrl+C to stop')\n\n // Keep the process alive (single-queue mode)\n await new Promise(() => {\n // This promise never resolves, keeping the worker running\n })\n}\n\n/**\n * Creates a worker handler that routes jobs to specific handlers based on job type.\n *\n * @template T - Base job payload type (must include a 'type' field)\n * @param handlers - Map of job types to their handlers\n *\n * @example\n * ```typescript\n * const handler = createRoutedHandler({\n * 'user.created': async (job) => { ... },\n * 'order.placed': async (job) => { ... },\n * })\n *\n * await runWorker({ queueName: 'events', handler })\n * ```\n */\nexport function createRoutedHandler<T extends { type: string }>(\n handlers: Record<string, JobHandler<T>>\n): JobHandler<T> {\n return async (job, ctx) => {\n const type = job.payload.type\n const handler = handlers[type]\n\n if (!handler) {\n logger.warn('No handler registered for job type', { type })\n return\n }\n\n await handler(job, ctx)\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createQueue } from '../factory'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n getTelemetryRuntime,\n isTelemetryBackendEnabled,\n} from '@open-mercato/shared/lib/telemetry/runtime'\nimport type { Queue, JobHandler, AsyncQueueOptions, QueueStrategyType } from '../types'\n\nconst logger = createLogger('queue').child({ component: 'worker' })\n\n/**\n * Options for running a queue worker.\n */\nexport type WorkerRunnerOptions<T = unknown> = {\n /** Name of the queue to process */\n queueName: string\n /** Handler function to process each job */\n handler: JobHandler<T>\n /** Redis connection options (only used for async strategy) */\n connection?: AsyncQueueOptions['connection']\n /** Number of concurrent jobs to process */\n concurrency?: number\n /** How long a job lock is held before the job counts as stalled, in ms. */\n lockDuration?: number\n /** Number of stalled-job recoveries BullMQ permits before failing a job. */\n maxStalledCount?: number\n /** Called when the queue abandons a job without running the handler. */\n onJobAbandoned?: AsyncQueueOptions['onJobAbandoned']\n /** Whether to set up graceful shutdown handlers */\n gracefulShutdown?: boolean\n /** If true, don't block - return immediately after starting processing (for multi-queue mode) */\n background?: boolean\n /** Queue strategy to use. Defaults to QUEUE_STRATEGY env var or 'local' */\n strategy?: QueueStrategyType\n}\n\nconst managedQueues = new Set<Queue<unknown>>()\nconst managedShutdownHooks = new Set<() => Promise<void> | void>()\nlet shutdownHandlersRegistered = false\nlet shutdownInProgress = false\n\nfunction unregisterShutdownHandlers(sigtermHandler: () => void, sigintHandler: () => void): void {\n process.off('SIGTERM', sigtermHandler)\n process.off('SIGINT', sigintHandler)\n shutdownHandlersRegistered = false\n}\n\nfunction registerShutdownHandlers(): void {\n if (shutdownHandlersRegistered) return\n\n const shutdown = async (signal: string) => {\n if (shutdownInProgress) return\n shutdownInProgress = true\n\n logger.info('Received signal, shutting down gracefully', { signal })\n\n let hasError = false\n for (const queue of managedQueues) {\n try {\n await queue.close()\n } catch (error) {\n hasError = true\n logger.error('Error during shutdown', { err: error })\n }\n }\n\n managedQueues.clear()\n for (const hook of managedShutdownHooks) {\n try {\n await hook()\n } catch (error) {\n hasError = true\n logger.error('Error during shutdown hook', { err: error })\n }\n }\n managedShutdownHooks.clear()\n unregisterShutdownHandlers(sigtermHandler, sigintHandler)\n shutdownInProgress = false\n\n // Flush buffered spans/logs before the process dies. A worker never returns\n // from run(), so bin.ts's post-run shutdownTelemetry() is unreachable on this\n // path \u2014 without this, the BatchSpanProcessor's ~5s tail is dropped on every\n // restart/redeploy. Idempotent and a no-op when telemetry is off; a flush\n // failure must not turn a clean shutdown into a failed one.\n try {\n await getTelemetryRuntime()?.shutdown()\n } catch (error) {\n logger.error('Error flushing telemetry during shutdown', { err: error })\n }\n\n if (!hasError) {\n logger.info('Worker closed successfully')\n }\n\n process.exit(hasError ? 1 : 0)\n }\n\n const sigtermHandler = () => {\n void shutdown('SIGTERM')\n }\n\n const sigintHandler = () => {\n void shutdown('SIGINT')\n }\n\n process.on('SIGTERM', sigtermHandler)\n process.on('SIGINT', sigintHandler)\n shutdownHandlersRegistered = true\n}\n\n/**\n * Register a process-local service that must stop before a worker exits.\n * The returned callback removes the hook when the service is stopped early.\n */\nexport function registerWorkerShutdownHook(hook: () => Promise<void> | void): () => void {\n managedShutdownHooks.add(hook)\n return () => managedShutdownHooks.delete(hook)\n}\n\n/**\n * Runs a queue worker that processes jobs continuously.\n *\n * This function:\n * 1. Creates an async queue instance\n * 2. Starts a BullMQ worker\n * 3. Sets up graceful shutdown on SIGTERM/SIGINT\n * 4. Keeps the process running until shutdown\n *\n * @template T - The job payload type\n * @param options - Worker configuration\n *\n * @example\n * ```typescript\n * import { runWorker } from '@open-mercato/queue/worker'\n *\n * await runWorker({\n * queueName: 'events',\n * handler: async (job, ctx) => {\n * console.log(`Processing ${ctx.jobId}:`, job.payload)\n * },\n * connection: { url: process.env.REDIS_URL },\n * concurrency: 5,\n * })\n * ```\n */\nexport async function runWorker<T = unknown>(\n options: WorkerRunnerOptions<T>\n): Promise<void> {\n const {\n queueName,\n handler,\n connection,\n concurrency = 1,\n lockDuration,\n maxStalledCount,\n onJobAbandoned,\n gracefulShutdown = true,\n background = false,\n strategy: strategyOption,\n } = options\n\n // Worker processes don't run Next's instrumentation hook, so initialize\n // telemetry here \u2014 this is the single bootstrap every standalone worker passes\n // through. Import the telemetry package only for an explicit enabled backend;\n // with the default/unset backend the worker never evaluates the package.\n if (!getTelemetryRuntime() && isTelemetryBackendEnabled()) {\n const { initTelemetry } = await import('@open-mercato/telemetry')\n await initTelemetry()\n }\n\n // Determine queue strategy from option, env var, or default to 'local'\n const strategy: QueueStrategyType = strategyOption\n ?? (process.env.QUEUE_STRATEGY === 'async' ? 'async' : 'local')\n\n logger.info('Starting worker for queue', { queueName, strategy })\n\n const queue = createQueue<T>(queueName, strategy, {\n connection,\n concurrency,\n lockDuration,\n maxStalledCount,\n onJobAbandoned,\n })\n\n // Set up graceful shutdown\n if (gracefulShutdown) {\n managedQueues.add(queue as Queue<unknown>)\n registerShutdownHandlers()\n }\n\n // Start processing\n await queue.process(handler)\n\n logger.info('Worker running', { concurrency })\n\n if (background) {\n // Return immediately for multi-queue mode\n return\n }\n\n logger.info('Press Ctrl+C to stop')\n\n // Keep the process alive (single-queue mode)\n await new Promise(() => {\n // This promise never resolves, keeping the worker running\n })\n}\n\n/**\n * Creates a worker handler that routes jobs to specific handlers based on job type.\n *\n * @template T - Base job payload type (must include a 'type' field)\n * @param handlers - Map of job types to their handlers\n *\n * @example\n * ```typescript\n * const handler = createRoutedHandler({\n * 'user.created': async (job) => { ... },\n * 'order.placed': async (job) => { ... },\n * })\n *\n * await runWorker({ queueName: 'events', handler })\n * ```\n */\nexport function createRoutedHandler<T extends { type: string }>(\n handlers: Record<string, JobHandler<T>>\n): JobHandler<T> {\n return async (job, ctx) => {\n const type = job.payload.type\n const handler = handlers[type]\n\n if (!handler) {\n logger.warn('No handler registered for job type', { type })\n return\n }\n\n await handler(job, ctx)\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/queue",
3
- "version": "0.6.8-develop.7015.1.af90a2ddc7",
3
+ "version": "0.6.8-develop.7019.1.f4c01c4b5c",
4
4
  "license": "MIT",
5
5
  "description": "Multi-strategy job queue with local and BullMQ support",
6
6
  "type": "module",
@@ -56,8 +56,8 @@
56
56
  "access": "public"
57
57
  },
58
58
  "dependencies": {
59
- "@open-mercato/shared": "0.6.8-develop.7015.1.af90a2ddc7",
60
- "@open-mercato/telemetry": "0.6.8-develop.7015.1.af90a2ddc7"
59
+ "@open-mercato/shared": "0.6.8-develop.7019.1.f4c01c4b5c",
60
+ "@open-mercato/telemetry": "0.6.8-develop.7019.1.f4c01c4b5c"
61
61
  },
62
62
  "repository": {
63
63
  "type": "git",