@open-mercato/queue 0.4.11-develop.2235.49f509443f → 0.4.11-develop.2245.a1059680ca
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 +17 -1
- package/dist/factory.js.map +2 -2
- package/dist/index.js +3 -1
- package/dist/index.js.map +2 -2
- package/dist/strategies/async.js +2 -2
- package/dist/strategies/async.js.map +2 -2
- package/package.json +2 -2
- package/src/__tests__/async.strategy.test.ts +4 -4
- package/src/__tests__/factory.test.ts +96 -0
- package/src/factory.ts +41 -1
- package/src/index.ts +1 -1
- package/src/strategies/async.ts +2 -2
package/dist/factory.js
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
import { createLocalQueue } from "./strategies/local.js";
|
|
2
2
|
import { createAsyncQueue } from "./strategies/async.js";
|
|
3
|
+
import { getRedisUrlOrThrow } from "@open-mercato/shared/lib/redis/connection";
|
|
3
4
|
function createQueue(name, strategy, options) {
|
|
4
5
|
if (strategy === "async") {
|
|
5
6
|
return createAsyncQueue(name, options);
|
|
6
7
|
}
|
|
7
8
|
return createLocalQueue(name, options);
|
|
8
9
|
}
|
|
10
|
+
function resolveQueueStrategy() {
|
|
11
|
+
return process.env.QUEUE_STRATEGY === "async" ? "async" : "local";
|
|
12
|
+
}
|
|
13
|
+
function createModuleQueue(name, options) {
|
|
14
|
+
const strategy = resolveQueueStrategy();
|
|
15
|
+
if (strategy === "async") {
|
|
16
|
+
return createAsyncQueue(name, {
|
|
17
|
+
connection: { url: getRedisUrlOrThrow("QUEUE") },
|
|
18
|
+
concurrency: options?.concurrency
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return createLocalQueue(name, { concurrency: options?.concurrency });
|
|
22
|
+
}
|
|
9
23
|
export {
|
|
10
|
-
|
|
24
|
+
createModuleQueue,
|
|
25
|
+
createQueue,
|
|
26
|
+
resolveQueueStrategy
|
|
11
27
|
};
|
|
12
28
|
//# sourceMappingURL=factory.js.map
|
package/dist/factory.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/factory.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Queue, LocalQueueOptions, AsyncQueueOptions } from './types'\nimport { createLocalQueue } from './strategies/local'\nimport { createAsyncQueue } from './strategies/async'\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"],
|
|
5
|
-
"mappings": "AACA,SAAS,wBAAwB;AACjC,SAAS,wBAAwB;
|
|
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?: { concurrency?: number },\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 })\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,IACxB,CAAC;AAAA,EACH;AACA,SAAO,iBAAoB,MAAM,EAAE,aAAa,SAAS,YAAY,CAAC;AACxE;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export * from "./types.js";
|
|
2
|
-
import { createQueue } from "./factory.js";
|
|
2
|
+
import { createQueue, createModuleQueue, resolveQueueStrategy } from "./factory.js";
|
|
3
3
|
export * from "./worker/registry.js";
|
|
4
4
|
import { runWorker, createRoutedHandler } from "./worker/runner.js";
|
|
5
5
|
export {
|
|
6
|
+
createModuleQueue,
|
|
6
7
|
createQueue,
|
|
7
8
|
createRoutedHandler,
|
|
9
|
+
resolveQueueStrategy,
|
|
8
10
|
runWorker
|
|
9
11
|
};
|
|
10
12
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * @open-mercato/queue\n *\n * Multi-strategy job queue package supporting local (file-based) and async (BullMQ) strategies.\n *\n * @example\n * ```typescript\n * import { createQueue } from '@open-mercato/queue'\n *\n * // Create a local queue\n * const queue = createQueue<{ userId: string }>('my-queue', 'local')\n *\n * // Enqueue a job\n * await queue.enqueue({ userId: '123' })\n *\n * // Process jobs\n * await queue.process(async (job, ctx) => {\n * console.log(`Processing job ${ctx.jobId}:`, job.payload)\n * })\n * ```\n */\n\nexport * from './types'\nexport { createQueue } from './factory'\n\n// Worker utilities\nexport * from './worker/registry'\nexport { runWorker, createRoutedHandler } from './worker/runner'\n"],
|
|
5
|
-
"mappings": "AAsBA,cAAc;AACd,SAAS,mBAAmB;
|
|
4
|
+
"sourcesContent": ["/**\n * @open-mercato/queue\n *\n * Multi-strategy job queue package supporting local (file-based) and async (BullMQ) strategies.\n *\n * @example\n * ```typescript\n * import { createQueue } from '@open-mercato/queue'\n *\n * // Create a local queue\n * const queue = createQueue<{ userId: string }>('my-queue', 'local')\n *\n * // Enqueue a job\n * await queue.enqueue({ userId: '123' })\n *\n * // Process jobs\n * await queue.process(async (job, ctx) => {\n * console.log(`Processing job ${ctx.jobId}:`, job.payload)\n * })\n * ```\n */\n\nexport * from './types'\nexport { createQueue, createModuleQueue, resolveQueueStrategy } from './factory'\n\n// Worker utilities\nexport * from './worker/registry'\nexport { runWorker, createRoutedHandler } from './worker/runner'\n"],
|
|
5
|
+
"mappings": "AAsBA,cAAc;AACd,SAAS,aAAa,mBAAmB,4BAA4B;AAGrE,cAAc;AACd,SAAS,WAAW,2BAA2B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/strategies/async.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getRedisUrlOrThrow } from "@open-mercato/shared/lib/redis/connection";
|
|
2
2
|
function resolveConnection(options) {
|
|
3
3
|
if (options?.url) {
|
|
4
4
|
return { url: options.url };
|
|
@@ -13,7 +13,7 @@ function resolveConnection(options) {
|
|
|
13
13
|
tls: options.tls
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
-
return { url:
|
|
16
|
+
return { url: getRedisUrlOrThrow("QUEUE") };
|
|
17
17
|
}
|
|
18
18
|
function createAsyncQueue(name, options) {
|
|
19
19
|
const connection = resolveConnection(options?.connection);
|
|
@@ -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 } from '../types'\nimport {
|
|
5
|
-
"mappings": "AACA,SAAS,
|
|
4
|
+
"sourcesContent": ["import type { Queue, QueuedJob, JobHandler, AsyncQueueOptions, ProcessResult, EnqueueOptions } from '../types'\nimport { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'\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 url?: string\n host?: string\n port?: number\n username?: string\n password?: string\n db?: number\n tls?: Record<string, unknown>\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}\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 }) => BullQueueInterface<T>\n Worker: new <T>(\n name: string,\n processor: (job: { id?: string; data: T; attemptsMade: number }) => Promise<void>,\n opts: { connection: ConnectionOptions; concurrency: number }\n ) => BullWorkerInterface\n}\n\n/**\n * Resolves Redis connection options from various sources.\n *\n * BullMQ expects an ioredis-compatible connection object. Preserve the full\n * Redis URL under the `url` key so rediss://, username, database, and query\n * params are not lost in translation.\n */\nfunction resolveConnection(options?: AsyncQueueOptions['connection']): ConnectionOptions {\n if (options?.url) {\n return { url: 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 }\n }\n\n return { url: 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\n let bullQueue: BullQueueInterface<QueuedJob<T>> | null = null\n let bullWorker: BullWorkerInterface | null = null\n let bullmqModule: BullMQModule | 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 async function getQueue(): Promise<BullQueueInterface<QueuedJob<T>>> {\n if (!bullQueue) {\n const { Queue: BullQueueClass } = await getBullMQ()\n bullQueue = new BullQueueClass<QueuedJob<T>>(name, { connection })\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 const jobData: QueuedJob<T> = {\n id: crypto.randomUUID(),\n payload: data,\n createdAt: new Date().toISOString(),\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: 3,\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\n // Create worker that processes jobs\n bullWorker = new Worker<QueuedJob<T>>(\n name,\n async (job) => {\n const jobData = job.data\n await handler(jobData, {\n jobId: job.id ?? jobData.id,\n attemptNumber: job.attemptsMade + 1,\n queueName: name,\n })\n },\n {\n connection,\n concurrency,\n }\n )\n\n // Set up event handlers\n bullWorker.on('completed', (job) => {\n const jobWithId = job as { id?: string }\n console.log(`[queue:${name}] Job ${jobWithId.id} completed`)\n })\n\n bullWorker.on('failed', (job, err) => {\n const jobWithId = job as { id?: string } | undefined\n const error = err as Error\n console.error(`[queue:${name}] Job ${jobWithId?.id} failed:`, error.message)\n })\n\n bullWorker.on('error', (err) => {\n const error = err as Error\n console.error(`[queue:${name}] Worker error:`, error.message)\n })\n\n console.log(`[queue:${name}] Worker started with concurrency ${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 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 close,\n getJobCounts,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["options"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/queue",
|
|
3
|
-
"version": "0.4.11-develop.
|
|
3
|
+
"version": "0.4.11-develop.2245.a1059680ca",
|
|
4
4
|
"description": "Multi-strategy job queue with local and BullMQ support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@open-mercato/shared": "0.4.11-develop.
|
|
53
|
+
"@open-mercato/shared": "0.4.11-develop.2245.a1059680ca"
|
|
54
54
|
},
|
|
55
55
|
"stableVersion": "0.4.10"
|
|
56
56
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createQueue } from '../factory'
|
|
2
|
-
import {
|
|
2
|
+
import { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'
|
|
3
3
|
|
|
4
4
|
const queueCtor = jest.fn()
|
|
5
5
|
const workerCtor = jest.fn()
|
|
@@ -16,7 +16,7 @@ const workerClose = jest.fn(async () => {})
|
|
|
16
16
|
const workerOn = jest.fn()
|
|
17
17
|
|
|
18
18
|
jest.mock('@open-mercato/shared/lib/redis/connection', () => ({
|
|
19
|
-
|
|
19
|
+
getRedisUrlOrThrow: jest.fn(),
|
|
20
20
|
}))
|
|
21
21
|
|
|
22
22
|
jest.mock('bullmq', () => {
|
|
@@ -51,11 +51,11 @@ jest.mock('bullmq', () => {
|
|
|
51
51
|
})
|
|
52
52
|
|
|
53
53
|
describe('Queue - async strategy', () => {
|
|
54
|
-
const
|
|
54
|
+
const getRedisUrlOrThrowMock = getRedisUrlOrThrow as jest.MockedFunction<typeof getRedisUrlOrThrow>
|
|
55
55
|
|
|
56
56
|
beforeEach(() => {
|
|
57
57
|
jest.clearAllMocks()
|
|
58
|
-
|
|
58
|
+
getRedisUrlOrThrowMock.mockReturnValue('rediss://default:secret@example.com:6380/1')
|
|
59
59
|
})
|
|
60
60
|
|
|
61
61
|
it('passes the full Redis URL to BullMQ when using env-based async config', async () => {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { resolveQueueStrategy, createModuleQueue } from '../factory'
|
|
2
|
+
import { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'
|
|
3
|
+
|
|
4
|
+
jest.mock('@open-mercato/shared/lib/redis/connection', () => ({
|
|
5
|
+
getRedisUrlOrThrow: jest.fn(),
|
|
6
|
+
}))
|
|
7
|
+
|
|
8
|
+
jest.mock('bullmq', () => {
|
|
9
|
+
class MockQueue<T> {
|
|
10
|
+
constructor(_name: string, _opts: unknown) {}
|
|
11
|
+
add = jest.fn(async () => ({ id: 'bull-job-id' }))
|
|
12
|
+
close = jest.fn(async () => {})
|
|
13
|
+
obliterate = jest.fn(async () => {})
|
|
14
|
+
getJobCounts = jest.fn(async () => ({ waiting: 0, active: 0, completed: 0, failed: 0 }))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class MockWorker<T> {
|
|
18
|
+
constructor(_name: string, _processor: unknown, _opts: unknown) {}
|
|
19
|
+
on = jest.fn()
|
|
20
|
+
close = jest.fn(async () => {})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return { Queue: MockQueue, Worker: MockWorker }
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('resolveQueueStrategy', () => {
|
|
27
|
+
const originalEnv = process.env.QUEUE_STRATEGY
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
if (originalEnv === undefined) {
|
|
31
|
+
delete process.env.QUEUE_STRATEGY
|
|
32
|
+
} else {
|
|
33
|
+
process.env.QUEUE_STRATEGY = originalEnv
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('returns "local" when QUEUE_STRATEGY is not set', () => {
|
|
38
|
+
delete process.env.QUEUE_STRATEGY
|
|
39
|
+
expect(resolveQueueStrategy()).toBe('local')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('returns "local" when QUEUE_STRATEGY is "local"', () => {
|
|
43
|
+
process.env.QUEUE_STRATEGY = 'local'
|
|
44
|
+
expect(resolveQueueStrategy()).toBe('local')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('returns "async" when QUEUE_STRATEGY is "async"', () => {
|
|
48
|
+
process.env.QUEUE_STRATEGY = 'async'
|
|
49
|
+
expect(resolveQueueStrategy()).toBe('async')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('returns "local" for any unrecognized value', () => {
|
|
53
|
+
process.env.QUEUE_STRATEGY = 'unknown'
|
|
54
|
+
expect(resolveQueueStrategy()).toBe('local')
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('createModuleQueue', () => {
|
|
59
|
+
const originalEnv = process.env.QUEUE_STRATEGY
|
|
60
|
+
const getRedisUrlOrThrowMock = getRedisUrlOrThrow as jest.MockedFunction<typeof getRedisUrlOrThrow>
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
jest.clearAllMocks()
|
|
64
|
+
getRedisUrlOrThrowMock.mockReturnValue('redis://localhost:6379')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
if (originalEnv === undefined) {
|
|
69
|
+
delete process.env.QUEUE_STRATEGY
|
|
70
|
+
} else {
|
|
71
|
+
process.env.QUEUE_STRATEGY = originalEnv
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('creates a local queue when QUEUE_STRATEGY is not set', () => {
|
|
76
|
+
delete process.env.QUEUE_STRATEGY
|
|
77
|
+
const queue = createModuleQueue('test-queue')
|
|
78
|
+
expect(queue.strategy).toBe('local')
|
|
79
|
+
expect(queue.name).toBe('test-queue')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('creates an async queue when QUEUE_STRATEGY is "async"', () => {
|
|
83
|
+
process.env.QUEUE_STRATEGY = 'async'
|
|
84
|
+
const queue = createModuleQueue('test-queue', { concurrency: 5 })
|
|
85
|
+
expect(queue.strategy).toBe('async')
|
|
86
|
+
expect(queue.name).toBe('test-queue')
|
|
87
|
+
expect(getRedisUrlOrThrowMock).toHaveBeenCalledWith('QUEUE')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('passes concurrency to local strategy', () => {
|
|
91
|
+
delete process.env.QUEUE_STRATEGY
|
|
92
|
+
const queue = createModuleQueue('test-queue', { concurrency: 3 })
|
|
93
|
+
expect(queue.strategy).toBe('local')
|
|
94
|
+
expect(queue.name).toBe('test-queue')
|
|
95
|
+
})
|
|
96
|
+
})
|
package/src/factory.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { Queue, LocalQueueOptions, AsyncQueueOptions } from './types'
|
|
1
|
+
import type { Queue, LocalQueueOptions, AsyncQueueOptions, QueueStrategyType } from './types'
|
|
2
2
|
import { createLocalQueue } from './strategies/local'
|
|
3
3
|
import { createAsyncQueue } from './strategies/async'
|
|
4
|
+
import { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Creates a queue instance with the specified strategy.
|
|
@@ -53,3 +54,42 @@ export function createQueue<T = unknown>(
|
|
|
53
54
|
|
|
54
55
|
return createLocalQueue<T>(name, options as LocalQueueOptions)
|
|
55
56
|
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the queue strategy from `QUEUE_STRATEGY`. Defaults to `'local'`.
|
|
60
|
+
*/
|
|
61
|
+
export function resolveQueueStrategy(): QueueStrategyType {
|
|
62
|
+
return process.env.QUEUE_STRATEGY === 'async' ? 'async' : 'local'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create a module-owned queue using the strategy declared in `QUEUE_STRATEGY`.
|
|
67
|
+
*
|
|
68
|
+
* - When `QUEUE_STRATEGY=async`, builds a BullMQ queue and resolves the
|
|
69
|
+
* Redis URL via `getRedisUrlOrThrow('QUEUE')` so missing config fails loudly.
|
|
70
|
+
* - Otherwise builds a local file-based queue.
|
|
71
|
+
*
|
|
72
|
+
* Replaces the boilerplate `process.env.QUEUE_STRATEGY === 'async' ? ... : ...`
|
|
73
|
+
* pattern that every module queue helper used to repeat. Concurrency applies
|
|
74
|
+
* to both strategies so the same number means the same thing in dev and prod.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* export function getDataSyncQueue(name: string) {
|
|
79
|
+
* return createModuleQueue<MyJob>(name, { concurrency: 5 })
|
|
80
|
+
* }
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export function createModuleQueue<T = unknown>(
|
|
84
|
+
name: string,
|
|
85
|
+
options?: { concurrency?: number },
|
|
86
|
+
): Queue<T> {
|
|
87
|
+
const strategy = resolveQueueStrategy()
|
|
88
|
+
if (strategy === 'async') {
|
|
89
|
+
return createAsyncQueue<T>(name, {
|
|
90
|
+
connection: { url: getRedisUrlOrThrow('QUEUE') },
|
|
91
|
+
concurrency: options?.concurrency,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
return createLocalQueue<T>(name, { concurrency: options?.concurrency })
|
|
95
|
+
}
|
package/src/index.ts
CHANGED
package/src/strategies/async.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Queue, QueuedJob, JobHandler, AsyncQueueOptions, ProcessResult, EnqueueOptions } from '../types'
|
|
2
|
-
import {
|
|
2
|
+
import { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'
|
|
3
3
|
|
|
4
4
|
// BullMQ interface types - we define the shape we use to maintain type safety
|
|
5
5
|
// while keeping bullmq as an optional peer dependency
|
|
@@ -67,7 +67,7 @@ function resolveConnection(options?: AsyncQueueOptions['connection']): Connectio
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
return { url:
|
|
70
|
+
return { url: getRedisUrlOrThrow('QUEUE') }
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|