diff --git a/src/queue.ts b/src/queue.ts index 0411613..8af13f8 100644 --- a/src/queue.ts +++ b/src/queue.ts @@ -38,9 +38,10 @@ export function createQueues(cfg: Config): Record> { } /** - * Job id = `${channel}:${notificationId}` so BullMQ itself dedupes enqueues: + * Job id = `${channel}_${notificationId}` so BullMQ itself dedupes enqueues: * re-polling the same pending doc will not create a second job. + * (BullMQ rejects custom job ids containing ':', hence the underscore.) */ export function jobId(job: SendJob): string { - return `${job.channel}:${job.notificationId}`; + return `${job.channel}_${job.notificationId}`; } diff --git a/test/queue.test.ts b/test/queue.test.ts new file mode 100644 index 0000000..bf1cfdb --- /dev/null +++ b/test/queue.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest'; +import { jobId, QUEUE_NAMES } from '../src/queue.js'; + +describe('jobId', () => { + it('is deterministic per (channel, notificationId) for dedupe', () => { + const a = jobId({ channel: 'fcm', notificationId: 'abc' }); + const b = jobId({ channel: 'fcm', notificationId: 'abc' }); + expect(a).toBe(b); + }); + it('never contains ":" (BullMQ rejects custom ids with a colon)', () => { + expect(jobId({ channel: 'fcm', notificationId: '6a540e689f1e1fd0f9a98788' })).not.toContain(':'); + expect(jobId({ channel: 'email', notificationId: 'x' })).not.toContain(':'); + }); + it('distinguishes channels for the same notification', () => { + expect(jobId({ channel: 'fcm', notificationId: 'x' })).not.toBe(jobId({ channel: 'email', notificationId: 'x' })); + }); + it('queue names are colon-free too', () => { + expect(QUEUE_NAMES.fcm).not.toContain(':'); + expect(QUEUE_NAMES.email).not.toContain(':'); + }); +});