From 5859b26fb75a02e6ec0312d6d115e06060837200 Mon Sep 17 00:00:00 2001 From: Rodrigo Mesquita <30835404+rrmesquita@users.noreply.github.com> Date: Tue, 2 Jun 2026 19:03:56 -0300 Subject: [PATCH] fix(redis): preserve empty arrays in payload --- src/drivers/redis_adapter.ts | 23 +++++++++++------ tests/adapter.spec.ts | 48 ++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/drivers/redis_adapter.ts b/src/drivers/redis_adapter.ts index c12a400..4276490 100644 --- a/src/drivers/redis_adapter.ts +++ b/src/drivers/redis_adapter.ts @@ -233,10 +233,13 @@ const ACQUIRE_JOB_SCRIPT = ` }) redis.call('HSET', active_key, job_id, active_data) - -- Return job with acquiredAt - local job = cjson.decode(job_data) - job.acquiredAt = now - return cjson.encode(job) + -- Return the raw stored JSON plus acquiredAt separately. Re-encoding the job + -- through cjson would coerce empty arrays into empty objects, so the payload + -- string is preserved verbatim and merged on the JS side. + return cjson.encode({ + data = job_data, + acquiredAt = now + }) ` /** @@ -517,7 +520,9 @@ const GET_JOB_SCRIPT = ` return cjson.encode({ status = status, - data = cjson.decode(job_data), + -- Return the raw stored JSON for the job data. Re-encoding it through cjson + -- would coerce empty arrays into empty objects. + data = job_data, finishedAt = finished_at, error = error_msg }) @@ -689,7 +694,9 @@ export class RedisAdapter implements Adapter { return null } - return JSON.parse(result as string) + const { data, acquiredAt } = JSON.parse(result as string) as { data: string; acquiredAt: number } + + return { ...JSON.parse(data), acquiredAt } } async completeJob(jobId: string, queue: string, removeOnComplete?: JobRetention): Promise { @@ -785,7 +792,9 @@ export class RedisAdapter implements Adapter { return null } - return JSON.parse(result as string) + const record = JSON.parse(result as string) as Omit & { data: string } + + return { ...record, data: JSON.parse(record.data) as JobData } } push(jobData: JobData): Promise { diff --git a/tests/adapter.spec.ts b/tests/adapter.spec.ts index 4cba891..4640d1a 100644 --- a/tests/adapter.spec.ts +++ b/tests/adapter.spec.ts @@ -416,6 +416,54 @@ test.group('Adapter | Redis', (group) => { const size = await adapter.sizeOf(queue) assert.equal(size, 1) }) + + test('popFrom should preserve an empty array payload instead of coercing it to an object', async ({ + assert, + }) => { + const adapter = new RedisAdapter(connection) + const queue = 'empty-array-payload-queue' + + try { + await adapter.pushOn(queue, { + id: 'empty-array-uuid-1', + name: 'TestJob', + payload: [], + attempts: 0, + }) + + await adapter.pushOn(queue, { + id: 'empty-array-uuid-2', + name: 'TestJob', + payload: { + empty: [], + names: ['Alice', 'Bob'], + deep: { + arr: [], + obj: {}, + } + }, + attempts: 0, + }) + + const simple = (await adapter.popFrom(queue))! + const nested = (await adapter.popFrom(queue))! + + assert.equal(simple.id, 'empty-array-uuid-1') + assert.isArray(simple.payload) + assert.lengthOf(simple.payload as unknown[], 0) + + assert.deepEqual(nested.payload, { + empty: [], + names: ['Alice', 'Bob'], + deep: { + arr: [], + obj: {}, + }, + }) + } finally { + await adapter.destroy() + } + }) }) test.group('Adapter | Knex (SQLite)', (group) => {