effect-mq 0.3.2 → 0.4.0

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.
Files changed (42) hide show
  1. package/README.md +102 -14
  2. package/dist/Job.d.ts +131 -9
  3. package/dist/Job.d.ts.map +1 -1
  4. package/dist/Job.js +81 -5
  5. package/dist/Job.js.map +1 -1
  6. package/dist/JobStore.d.ts +63 -2
  7. package/dist/JobStore.d.ts.map +1 -1
  8. package/dist/JobStore.js.map +1 -1
  9. package/dist/MemoryJobStore.d.ts.map +1 -1
  10. package/dist/MemoryJobStore.js +159 -119
  11. package/dist/MemoryJobStore.js.map +1 -1
  12. package/dist/Worker.d.ts +18 -0
  13. package/dist/Worker.d.ts.map +1 -1
  14. package/dist/Worker.js +37 -9
  15. package/dist/Worker.js.map +1 -1
  16. package/dist/drizzle-postgres/DrizzleJobStore.d.ts.map +1 -1
  17. package/dist/drizzle-postgres/DrizzleJobStore.js +239 -49
  18. package/dist/drizzle-postgres/DrizzleJobStore.js.map +1 -1
  19. package/dist/drizzle-postgres/schema.d.ts +2 -0
  20. package/dist/drizzle-postgres/schema.d.ts.map +1 -1
  21. package/dist/drizzle-postgres/schema.js +1 -0
  22. package/dist/drizzle-postgres/schema.js.map +1 -1
  23. package/dist/redis/RedisJobStore.d.ts.map +1 -1
  24. package/dist/redis/RedisJobStore.js +173 -36
  25. package/dist/redis/RedisJobStore.js.map +1 -1
  26. package/dist/redis/scripts.d.ts +24 -1
  27. package/dist/redis/scripts.d.ts.map +1 -1
  28. package/dist/redis/scripts.js +126 -21
  29. package/dist/redis/scripts.js.map +1 -1
  30. package/dist/testing/conformance.d.ts.map +1 -1
  31. package/dist/testing/conformance.js +266 -0
  32. package/dist/testing/conformance.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/Job.ts +268 -13
  35. package/src/JobStore.ts +77 -2
  36. package/src/MemoryJobStore.ts +102 -53
  37. package/src/Worker.ts +61 -9
  38. package/src/drizzle-postgres/DrizzleJobStore.ts +273 -66
  39. package/src/drizzle-postgres/schema.ts +2 -0
  40. package/src/redis/RedisJobStore.ts +236 -47
  41. package/src/redis/scripts.ts +153 -21
  42. package/src/testing/conformance.ts +350 -0
@@ -158,6 +158,30 @@ local function finishCancelled(id, queue, name, startedAt, now, nowStr)
158
158
  releaseDedupe(name, redis.call("HGET", jk, "dedupeKey"), id, now)
159
159
  applyKeep(name, "cancelled", redis.call("HGET", jk, "keep"), now)
160
160
  end
161
+ -- Insert one fresh job row plus every index entry. String params are stored
162
+ -- verbatim (payload/metadata/backoff/keep/trace are pre-encoded JSON, "" =
163
+ -- absent); priority/delayMs/now numeric-coercible.
164
+ local function insertJobRow(id, name, queue, payloadJson, metadataJson, priority,
165
+ attemptsMax, backoffJson, keepJson, timeoutMs, dedupeKey, traceJson, delayMs, now, nowStr)
166
+ local seq = redis.call("INCR", prefix .. ":seq")
167
+ local state = delayMs > 0 and "delayed" or "waiting"
168
+ local runAt = now + delayMs
169
+ redis.call("HSET", jobKey(id),
170
+ "id", id, "name", name, "queue", queue,
171
+ "payload", payloadJson, "metadata", metadataJson, "state", state,
172
+ "priority", priority, "attemptsMax", attemptsMax, "attemptsMade", "0", "stalledCount", "0",
173
+ "backoff", backoffJson, "keep", keepJson, "timeoutMs", timeoutMs,
174
+ "cancelRequested", "0", "dedupeKey", dedupeKey, "trace", traceJson, "runAt", fmt(runAt), "enqueuedAt", nowStr,
175
+ "processedAt", "", "finishedAt", "", "exit", "", "failedReason", "",
176
+ "lockToken", "", "lockExpiresAt", "", "seq", fmt(seq))
177
+ redis.call("ZADD", prefix .. ":all", now, id)
178
+ if state == "waiting" then
179
+ addWaiting(queue, tonumber(priority), seq, id)
180
+ else
181
+ redis.call("ZADD", delayedKey(queue), runAt, id)
182
+ end
183
+ countsAdd(queue, state, 1)
184
+ end
161
185
  `;
162
186
  /**
163
187
  * enqueue(prefix, idMode, id, name, queue, payloadJson, metadataJson,
@@ -165,7 +189,7 @@ end
165
189
  * idMode: "user" (dedup no-op), "generated" (collision -> retry sentinel),
166
190
  * "auto" (j-<seq>, in-script collision loop).
167
191
  */
168
- export const enqueue = Redis.script((prefix, idMode, id, name, queue, payloadJson, metadataJson, priority, attemptsMax, backoffJson, keepJson, timeoutMs, delayMs, now, dedupeKey, dedupeTtlMs, dedupeExtend, dedupeReplace) => [
192
+ export const enqueue = Redis.script((prefix, idMode, id, name, queue, payloadJson, metadataJson, priority, attemptsMax, backoffJson, keepJson, timeoutMs, delayMs, now, dedupeKey, dedupeTtlMs, dedupeExtend, dedupeReplace, traceJson) => [
169
193
  prefix,
170
194
  idMode,
171
195
  id,
@@ -183,7 +207,8 @@ export const enqueue = Redis.script((prefix, idMode, id, name, queue, payloadJso
183
207
  dedupeKey,
184
208
  dedupeTtlMs,
185
209
  dedupeExtend,
186
- dedupeReplace
210
+ dedupeReplace,
211
+ traceJson
187
212
  ], {
188
213
  numberOfKeys: 0,
189
214
  lua: `${HELPERS}
@@ -221,7 +246,7 @@ if dKey ~= "" then
221
246
  local newRunAt = now + delayMs
222
247
  redis.call("HSET", kjk, "payload", ARGV[6], "metadata", ARGV[7], "priority", ARGV[8],
223
248
  "attemptsMax", ARGV[9], "backoff", ARGV[10], "keep", ARGV[11], "timeoutMs", ARGV[12],
224
- "runAt", fmt(newRunAt))
249
+ "trace", ARGV[19], "runAt", fmt(newRunAt))
225
250
  local keyedQueue = redis.call("HGET", kjk, "queue")
226
251
  redis.call("ZADD", delayedKey(keyedQueue), newRunAt, entryJob)
227
252
  -- A landed replace re-arms the ttl window.
@@ -255,24 +280,8 @@ if idMode == "auto" then
255
280
  end
256
281
  if id == "" then return '{"error":"id"}' end
257
282
  end
258
- local seq = redis.call("INCR", prefix .. ":seq")
259
- local state = delayMs > 0 and "delayed" or "waiting"
260
- local runAt = now + delayMs
261
- redis.call("HSET", jobKey(id),
262
- "id", id, "name", ARGV[4], "queue", queue,
263
- "payload", ARGV[6], "metadata", ARGV[7], "state", state,
264
- "priority", ARGV[8], "attemptsMax", ARGV[9], "attemptsMade", "0", "stalledCount", "0",
265
- "backoff", ARGV[10], "keep", ARGV[11], "timeoutMs", ARGV[12],
266
- "cancelRequested", "0", "dedupeKey", dKey, "runAt", fmt(runAt), "enqueuedAt", nowStr,
267
- "processedAt", "", "finishedAt", "", "exit", "", "failedReason", "",
268
- "lockToken", "", "lockExpiresAt", "", "seq", fmt(seq))
269
- redis.call("ZADD", prefix .. ":all", now, id)
270
- if state == "waiting" then
271
- addWaiting(queue, tonumber(ARGV[8]), seq, id)
272
- else
273
- redis.call("ZADD", delayedKey(queue), runAt, id)
274
- end
275
- countsAdd(queue, state, 1)
283
+ insertJobRow(id, ARGV[4], queue, ARGV[6], ARGV[7], ARGV[8], ARGV[9], ARGV[10], ARGV[11],
284
+ ARGV[12], dKey, ARGV[19], delayMs, now, nowStr)
276
285
  if dKey ~= "" then
277
286
  local sk = dedupeStoreKey(name, dKey)
278
287
  redis.call("DEL", sk)
@@ -831,6 +840,102 @@ redis.call("ZADD", prefix .. ":schedules", tonumber(ARGV[4]), key)
831
840
  return "1"
832
841
  `
833
842
  }).withReturnType();
843
+ /**
844
+ * tickSchedule(prefix, key, expectedRunAt, nextRunAt, id, name, queue,
845
+ * payloadJson, metadataJson, priority, attemptsMax, backoffJson, keepJson,
846
+ * timeoutMs, traceJson, delayMs, now) -> "1" fired | "0"
847
+ * Atomic occurrence claim: the nextRunAt CAS and the tick job's insert run
848
+ * in one script, so a stale sweeper can never re-fire a slot — even after
849
+ * retention pruned the previous slot's job row.
850
+ */
851
+ export const tickSchedule = Redis.script((prefix, key, expectedRunAt, nextRunAt, id, name, queue, payloadJson, metadataJson, priority, attemptsMax, backoffJson, keepJson, timeoutMs, traceJson, delayMs, now) => [
852
+ prefix,
853
+ key,
854
+ expectedRunAt,
855
+ nextRunAt,
856
+ id,
857
+ name,
858
+ queue,
859
+ payloadJson,
860
+ metadataJson,
861
+ priority,
862
+ attemptsMax,
863
+ backoffJson,
864
+ keepJson,
865
+ timeoutMs,
866
+ traceJson,
867
+ delayMs,
868
+ now
869
+ ], {
870
+ numberOfKeys: 0,
871
+ lua: `${HELPERS}
872
+ local key = ARGV[2]
873
+ local sk = prefix .. ":schedule:" .. key
874
+ local current = redis.call("HGET", sk, "nextRunAt")
875
+ if current == false or tonumber(current) ~= tonumber(ARGV[3]) then return "0" end
876
+ redis.call("HSET", sk, "nextRunAt", fmt(tonumber(ARGV[4])))
877
+ redis.call("ZADD", prefix .. ":schedules", tonumber(ARGV[4]), key)
878
+ local id = ARGV[5]
879
+ -- Pre-existing slot row (pre-0.4 crash between enqueue and advance): the
880
+ -- schedule still advances, but nothing new fires.
881
+ if redis.call("EXISTS", jobKey(id)) == 1 then return "0" end
882
+ insertJobRow(id, ARGV[6], ARGV[7], ARGV[8], ARGV[9], ARGV[10], ARGV[11], ARGV[12], ARGV[13],
883
+ ARGV[14], "", ARGV[15], tonumber(ARGV[16]), tonumber(ARGV[17]), ARGV[17])
884
+ return "1"
885
+ `
886
+ }).withReturnType();
887
+ /**
888
+ * enqueueMany(prefix, now, count, ...items) -> JSON array of per-item results
889
+ * ({id, duplicate} | {collision} | {error}). Items are 13-ARGV strides:
890
+ * idMode, id, name, queue, payloadJson, metadataJson, priority, attemptsMax,
891
+ * backoffJson, keepJson, timeoutMs, traceJson, delayMs. Plain (non-dedup)
892
+ * items only — the caller routes dedup items through \`enqueue\`.
893
+ */
894
+ export const enqueueMany = Redis.script((prefix, now, count, items) => [prefix, now, count, ...items], {
895
+ numberOfKeys: 0,
896
+ lua: `${HELPERS}
897
+ local now = tonumber(ARGV[2])
898
+ local nowStr = ARGV[2]
899
+ local count = tonumber(ARGV[3])
900
+ local out = {}
901
+ for i = 0, count - 1 do
902
+ local base = 3 + i * 13
903
+ local idMode = ARGV[base + 1]
904
+ local id = ARGV[base + 2]
905
+ local result
906
+ if idMode ~= "auto" and redis.call("EXISTS", jobKey(id)) == 1 then
907
+ -- Sequential in-script processing makes intra-batch repeats of one user
908
+ -- id resolve exactly like separate enqueues: first inserts, rest dedup.
909
+ if idMode == "user" then
910
+ result = '{"id":' .. cjson.encode(id) .. ',"duplicate":true}'
911
+ else
912
+ result = '{"collision":true}'
913
+ end
914
+ else
915
+ if idMode == "auto" then
916
+ id = ""
917
+ for a = 1, 5 do
918
+ local candidate = "j-" .. fmt(redis.call("INCR", prefix .. ":seq"))
919
+ if redis.call("EXISTS", jobKey(candidate)) == 0 then
920
+ id = candidate
921
+ break
922
+ end
923
+ end
924
+ end
925
+ if id == "" then
926
+ result = '{"error":"id"}'
927
+ else
928
+ insertJobRow(id, ARGV[base + 3], ARGV[base + 4], ARGV[base + 5], ARGV[base + 6],
929
+ ARGV[base + 7], ARGV[base + 8], ARGV[base + 9], ARGV[base + 10], ARGV[base + 11],
930
+ "", ARGV[base + 12], tonumber(ARGV[base + 13]), now, nowStr)
931
+ result = '{"id":' .. cjson.encode(id) .. ',"duplicate":false}'
932
+ end
933
+ end
934
+ out[#out + 1] = result
935
+ end
936
+ return "[" .. table.concat(out, ",") .. "]"
937
+ `
938
+ }).withReturnType();
834
939
  /**
835
940
  * sweepState(prefix, state, ttlMs, limit, offset, now) -> {scanned, deleted}
836
941
  * One bounded page over a terminal state's finished zset, deleting rows past
@@ -1 +1 @@
1
- {"version":3,"file":"scripts.js","sourceRoot":"","sources":["../../src/redis/scripts.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"scripts.js","sourceRoot":"","sources":["../../src/redis/scripts.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"conformance.d.ts","sourceRoot":"","sources":["../../src/testing/conformance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAE1C,OAAO,EAAuB,KAAK,KAAK,EAAU,MAAM,QAAQ,CAAA;AAiChE;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,SACxB,MAAM,cACA,MAAM,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAC/C,IAiuCF,CAAA"}
1
+ {"version":3,"file":"conformance.d.ts","sourceRoot":"","sources":["../../src/testing/conformance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAE1C,OAAO,EAAuB,KAAK,KAAK,EAAU,MAAM,QAAQ,CAAA;AAkChE;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,SACxB,MAAM,cACA,MAAM,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAC/C,IA8jDF,CAAA"}
@@ -33,6 +33,7 @@ const baseRequest = (overrides) => ({
33
33
  keep: undefined,
34
34
  timeoutMs: undefined,
35
35
  dedupe: undefined,
36
+ trace: undefined,
36
37
  delayMs: 0,
37
38
  ...overrides
38
39
  });
@@ -586,6 +587,238 @@ export const jobStoreConformance = (name, storeLayer) => {
586
587
  expect(yield* store.removeSchedule(schedule.key)).toBe(false);
587
588
  expect(yield* store.listSchedules()).toEqual([]);
588
589
  })));
590
+ const minutelySchedule = () => ({
591
+ key: JobStore.ScheduleKey("TestJob/minutely"),
592
+ jobName: "TestJob",
593
+ queue: QueueName("default"),
594
+ cron: undefined,
595
+ tz: undefined,
596
+ everyMs: 60_000,
597
+ payload: { n: 7 },
598
+ metadata: {},
599
+ priority: 0,
600
+ attemptsMax: 1,
601
+ backoff: undefined,
602
+ keep: undefined,
603
+ timeoutMs: undefined,
604
+ nextRunAt: 60_000
605
+ });
606
+ it.effect("tickSchedule fires a slot exactly once and advances atomically", () => withStore((store) => Effect.gen(function* () {
607
+ const schedule = minutelySchedule();
608
+ yield* store.upsertSchedule(schedule);
609
+ yield* TestClock.adjust(60_000);
610
+ const request = baseRequest({
611
+ id: JobId("sched/TestJob/minutely/60000"),
612
+ payload: { n: 7 }
613
+ });
614
+ expect(yield* store.tickSchedule(schedule.key, 60_000, 120_000, request)).toBe(true);
615
+ expect((yield* store.listSchedules())[0]?.nextRunAt).toBe(120_000);
616
+ expect((yield* store.counts()).waiting).toBe(1);
617
+ // A concurrent sweeper holding the same stale expectation loses the
618
+ // CAS: nothing fires, nothing advances further.
619
+ expect(yield* store.tickSchedule(schedule.key, 60_000, 180_000, request)).toBe(false);
620
+ expect((yield* store.listSchedules())[0]?.nextRunAt).toBe(120_000);
621
+ expect((yield* store.counts()).waiting).toBe(1);
622
+ const claim = yield* store.claim(claimOptions());
623
+ assert(claim._tag === "Claimed");
624
+ expect(claim.job.id).toBe("sched/TestJob/minutely/60000");
625
+ expect(claim.job.payload).toEqual({ n: 7 });
626
+ })));
627
+ it.effect("a stale tick cannot re-fire a slot whose job was pruned", () => withStore((store) => Effect.gen(function* () {
628
+ const schedule = minutelySchedule();
629
+ yield* store.upsertSchedule(schedule);
630
+ yield* TestClock.adjust(60_000);
631
+ const slotId = JobId("sched/TestJob/minutely/60000");
632
+ const request = baseRequest({ id: slotId, payload: { n: 7 } });
633
+ expect(yield* store.tickSchedule(schedule.key, 60_000, 120_000, request)).toBe(true);
634
+ // Retention prunes the slot job — the old non-atomic design would
635
+ // now let a stale sweeper re-insert it. The CAS must still refuse.
636
+ expect(yield* store.remove(slotId)).toBe(true);
637
+ expect(yield* store.tickSchedule(schedule.key, 60_000, 180_000, request)).toBe(false);
638
+ expect((yield* store.counts()).waiting).toBe(0);
639
+ expect((yield* store.listSchedules())[0]?.nextRunAt).toBe(120_000);
640
+ })));
641
+ it.effect("tickSchedule advances without firing when the slot job already exists", () => withStore((store) => Effect.gen(function* () {
642
+ const schedule = minutelySchedule();
643
+ yield* store.upsertSchedule(schedule);
644
+ yield* TestClock.adjust(60_000);
645
+ const slotId = JobId("sched/TestJob/minutely/60000");
646
+ // A pre-0.4 worker crashed between its enqueue and advance: the
647
+ // slot row exists but nextRunAt is stale. The tick must advance the
648
+ // schedule (or it stays due forever) yet report nothing new fired.
649
+ yield* store.enqueue(baseRequest({ id: slotId, payload: { n: 7 } }));
650
+ const request = baseRequest({ id: slotId, payload: { n: 7 } });
651
+ expect(yield* store.tickSchedule(schedule.key, 60_000, 120_000, request)).toBe(false);
652
+ expect((yield* store.listSchedules())[0]?.nextRunAt).toBe(120_000);
653
+ expect((yield* store.counts()).waiting).toBe(1);
654
+ })));
655
+ it.effect("tickSchedule rejects requests without an id and unknown keys", () => withStore((store) => Effect.gen(function* () {
656
+ const missing = yield* Effect.flip(store.tickSchedule(JobStore.ScheduleKey("TestJob/minutely"), 60_000, 120_000, baseRequest()));
657
+ expect(missing._tag).toBe("JobStoreError");
658
+ expect(yield* store.tickSchedule(JobStore.ScheduleKey("ghost"), 0, 60_000, baseRequest({ id: JobId("sched/ghost/0") }))).toBe(false);
659
+ expect((yield* store.counts()).waiting).toBe(0);
660
+ })));
661
+ it.effect("tickSchedule wakes parked takers", () => withStore((store) => Effect.gen(function* () {
662
+ const schedule = minutelySchedule();
663
+ yield* store.upsertSchedule(schedule);
664
+ const empty = yield* store.claim(claimOptions());
665
+ assert(empty._tag === "Empty");
666
+ const waiter = yield* Effect.forkChild(store.awaitWake([QueueName("default")], empty.wakeToken));
667
+ yield* Effect.yieldNow;
668
+ yield* TestClock.adjust(60_000);
669
+ const request = baseRequest({
670
+ id: JobId("sched/TestJob/minutely/60000"),
671
+ payload: { n: 7 }
672
+ });
673
+ expect(yield* store.tickSchedule(schedule.key, 60_000, 120_000, request)).toBe(true);
674
+ yield* Fiber.join(waiter);
675
+ const claim = yield* store.claim(claimOptions({ token: "t-2" }));
676
+ assert(claim._tag === "Claimed");
677
+ })));
678
+ it.effect("enqueueMany inserts a batch with positional results", () => withStore((store) => Effect.gen(function* () {
679
+ const results = yield* store.enqueueMany([
680
+ baseRequest({ payload: { n: 1 } }),
681
+ baseRequest({ payload: { n: 2 }, delayMs: 60_000 }),
682
+ baseRequest({ id: JobId("batch-3"), payload: { n: 3 }, priority: 5 })
683
+ ]);
684
+ expect(results).toHaveLength(3);
685
+ expect(results.map((result) => result.duplicate)).toEqual([false, false, false]);
686
+ expect(new Set(results.map((result) => result.id)).size).toBe(3);
687
+ expect(results[2]?.id).toBe("batch-3");
688
+ const counts = yield* store.counts();
689
+ expect(counts.waiting).toBe(2);
690
+ expect(counts.delayed).toBe(1);
691
+ const explicit = yield* store.getJob(JobId("batch-3"));
692
+ assert(Option.isSome(explicit));
693
+ expect(explicit.value.payload).toEqual({ n: 3 });
694
+ expect(explicit.value.priority).toBe(5);
695
+ const delayedId = results[1]?.id;
696
+ assert(delayedId !== undefined);
697
+ const delayed = yield* store.getJob(delayedId);
698
+ assert(Option.isSome(delayed));
699
+ expect(delayed.value.state).toBe("delayed");
700
+ expect(delayed.value.runAt).toBe(60_000);
701
+ // Priority order survives the batch path.
702
+ const claim = yield* store.claim(claimOptions());
703
+ assert(claim._tag === "Claimed");
704
+ expect(claim.job.id).toBe("batch-3");
705
+ })));
706
+ it.effect("enqueueMany reports duplicates positionally without clobbering", () => withStore((store) => Effect.gen(function* () {
707
+ yield* store.enqueue(baseRequest({ id: JobId("dup-1"), payload: { n: 0 } }));
708
+ const results = yield* store.enqueueMany([
709
+ baseRequest({ payload: { n: 1 } }),
710
+ baseRequest({ id: JobId("dup-1"), payload: { n: 99 } }),
711
+ baseRequest({ payload: { n: 2 } })
712
+ ]);
713
+ expect(results.map((result) => result.duplicate)).toEqual([false, true, false]);
714
+ expect(results[1]?.id).toBe("dup-1");
715
+ const kept = yield* store.getJob(JobId("dup-1"));
716
+ assert(Option.isSome(kept));
717
+ expect(kept.value.payload).toEqual({ n: 0 });
718
+ expect((yield* store.counts()).waiting).toBe(3);
719
+ })));
720
+ it.effect("enqueueMany resolves intra-batch repeats of one id like separate enqueues", () => withStore((store) => Effect.gen(function* () {
721
+ const results = yield* store.enqueueMany([
722
+ baseRequest({ id: JobId("same"), payload: { n: 1 } }),
723
+ baseRequest({ id: JobId("same"), payload: { n: 2 } })
724
+ ]);
725
+ expect(results.map((result) => result.duplicate)).toEqual([false, true]);
726
+ expect(results[1]?.id).toBe("same");
727
+ expect((yield* store.counts()).waiting).toBe(1);
728
+ const job = yield* store.getJob(JobId("same"));
729
+ assert(Option.isSome(job));
730
+ expect(job.value.payload).toEqual({ n: 1 });
731
+ })));
732
+ it.effect("enqueueMany applies per-item dedup policies in order", () => withStore((store) => Effect.gen(function* () {
733
+ const dedupe = { key: "k", ttlMs: undefined, extend: false, replace: false };
734
+ const results = yield* store.enqueueMany([
735
+ baseRequest({ payload: { n: 1 }, dedupe }),
736
+ baseRequest({ payload: { n: 2 } }),
737
+ baseRequest({ payload: { n: 3 }, dedupe })
738
+ ]);
739
+ expect(results.map((result) => result.duplicate)).toEqual([false, false, true]);
740
+ expect(results[2]?.id).toBe(results[0]?.id);
741
+ expect((yield* store.counts()).waiting).toBe(2);
742
+ })));
743
+ it.effect("enqueueMany with an empty batch returns an empty array", () => withStore((store) => Effect.gen(function* () {
744
+ expect(yield* store.enqueueMany([])).toEqual([]);
745
+ })));
746
+ // Pins the hand-duplicated field plumbing in batch/tick insert paths
747
+ // (positional Lua ARGV strides, multi-row VALUES lists): every persisted
748
+ // field must come out identical to the single-enqueue path. A swapped
749
+ // pair of ARGVs or a dropped column fails this even though the simpler
750
+ // tests (payload/priority/state only) stay green.
751
+ it.effect("batch and tick inserts persist every field exactly like enqueue", () => withStore((store) => Effect.gen(function* () {
752
+ const richRequest = (id) => baseRequest({
753
+ id: JobId(id),
754
+ payload: { big: 1234567890123456, nested: { arr: [1, 2, 3] } },
755
+ metadata: { tenant: "acme", region: "us" },
756
+ priority: 3,
757
+ attemptsMax: 4,
758
+ backoff: { _tag: "fixed", delayMs: 2_000 },
759
+ keep: { completed: { count: 2, ageMs: undefined } },
760
+ timeoutMs: 9_000,
761
+ trace: { traceId: "trace-1", spanId: "span-1", sampled: true, delayed: false }
762
+ });
763
+ yield* store.enqueue(richRequest("rich-single"));
764
+ expect(yield* store.enqueueMany([richRequest("rich-batch")]))
765
+ .toEqual([{ id: "rich-batch", duplicate: false }]);
766
+ const schedule = {
767
+ ...minutelySchedule(),
768
+ key: JobStore.ScheduleKey("TestJob/parity"),
769
+ nextRunAt: 0
770
+ };
771
+ yield* store.upsertSchedule(schedule);
772
+ expect(yield* store.tickSchedule(schedule.key, 0, 60_000, richRequest("rich-tick")))
773
+ .toBe(true);
774
+ const project = (job) => ({
775
+ name: job.name,
776
+ queue: job.queue,
777
+ state: job.state,
778
+ payload: job.payload,
779
+ metadata: job.metadata,
780
+ priority: job.priority,
781
+ attemptsMax: job.attemptsMax,
782
+ attemptsMade: job.attemptsMade,
783
+ backoff: job.backoff,
784
+ keep: job.keep,
785
+ timeoutMs: job.timeoutMs,
786
+ cancelRequested: job.cancelRequested,
787
+ trace: job.trace,
788
+ runAt: job.runAt,
789
+ enqueuedAt: job.enqueuedAt
790
+ });
791
+ const single = yield* store.getJob(JobId("rich-single"));
792
+ assert(Option.isSome(single));
793
+ const expected = project(single.value);
794
+ // The reference row itself must carry the rich fields — otherwise
795
+ // three empty rows would compare equal and prove nothing.
796
+ expect(expected.backoff).toEqual({ _tag: "fixed", delayMs: 2_000 });
797
+ expect(expected.keep).toEqual({ completed: { count: 2 } });
798
+ expect(expected.timeoutMs).toBe(9_000);
799
+ expect(expected.trace).toEqual({
800
+ traceId: "trace-1",
801
+ spanId: "span-1",
802
+ sampled: true,
803
+ delayed: false
804
+ });
805
+ expect(expected.payload).toEqual({ big: 1234567890123456, nested: { arr: [1, 2, 3] } });
806
+ for (const id of [JobId("rich-batch"), JobId("rich-tick")]) {
807
+ const job = yield* store.getJob(id);
808
+ assert(Option.isSome(job));
809
+ expect(project(job.value)).toEqual(expected);
810
+ }
811
+ })));
812
+ it.effect("enqueueMany wakes parked takers", () => withStore((store) => Effect.gen(function* () {
813
+ const empty = yield* store.claim(claimOptions());
814
+ assert(empty._tag === "Empty");
815
+ const waiter = yield* Effect.forkChild(store.awaitWake([QueueName("default")], empty.wakeToken));
816
+ yield* Effect.yieldNow;
817
+ yield* store.enqueueMany([baseRequest()]);
818
+ yield* Fiber.join(waiter);
819
+ const claim = yield* store.claim(claimOptions({ token: "t-2" }));
820
+ assert(claim._tag === "Claimed");
821
+ })));
589
822
  it.effect("completion wins over a pending cancel request", () => withStore((store) => Effect.gen(function* () {
590
823
  const { id } = yield* store.enqueue(baseRequest());
591
824
  const claim = yield* store.claim(claimOptions());
@@ -930,6 +1163,39 @@ export const jobStoreConformance = (name, storeLayer) => {
930
1163
  yield* Fiber.join(waiter);
931
1164
  expect(woke).toBe(true);
932
1165
  })));
1166
+ it.effect("cancelByDedupe cancels pending keyed jobs and is idempotent", () => withStore((store) => Effect.gen(function* () {
1167
+ const dedupe = { key: "emp-1", ttlMs: undefined, extend: false, replace: false };
1168
+ // Unknown key: nothing pending, no error.
1169
+ expect(yield* store.cancelByDedupe("TestJob", "emp-1")).toBe(false);
1170
+ // Delayed keyed job: cancelled terminally.
1171
+ const delayed = yield* store.enqueue(baseRequest({ dedupe, delayMs: 60_000 }));
1172
+ expect(yield* store.cancelByDedupe("TestJob", "emp-1")).toBe(true);
1173
+ const job = yield* store.getJob(delayed.id);
1174
+ assert(Option.isSome(job));
1175
+ expect(job.value.state).toBe("cancelled");
1176
+ expect(yield* store.cancelByDedupe("TestJob", "emp-1")).toBe(false);
1177
+ // Active keyed job: flagged for the heartbeat.
1178
+ const active = yield* store.enqueue(baseRequest({ dedupe, payload: { n: 2 } }));
1179
+ const claim = yield* store.claim(claimOptions());
1180
+ assert(claim._tag === "Claimed");
1181
+ expect(yield* store.cancelByDedupe("TestJob", "emp-1")).toBe(true);
1182
+ const flagged = yield* store.getJob(active.id);
1183
+ assert(Option.isSome(flagged));
1184
+ expect(flagged.value.cancelRequested).toBe(true);
1185
+ // Name scoping: same key under another name is untouched.
1186
+ expect(yield* store.cancelByDedupe("OtherJob", "emp-1")).toBe(false);
1187
+ })));
1188
+ it.effect("the producer's trace context round-trips on the record", () => withStore((store) => Effect.gen(function* () {
1189
+ const trace = { traceId: "trace-abc", spanId: "span-def", sampled: true, delayed: false };
1190
+ const { id } = yield* store.enqueue(baseRequest({ trace }));
1191
+ const job = yield* store.getJob(id);
1192
+ assert(Option.isSome(job));
1193
+ expect(job.value.trace).toEqual(trace);
1194
+ const bare = yield* store.enqueue(baseRequest({ payload: { n: 2 } }));
1195
+ const none = yield* store.getJob(bare.id);
1196
+ assert(Option.isSome(none));
1197
+ expect(none.value.trace).toBeUndefined();
1198
+ })));
933
1199
  it.effect("delayed jobs still promote to waiting while their queue is paused", () => withStore((store) => Effect.gen(function* () {
934
1200
  const { id } = yield* store.enqueue(baseRequest({ delayMs: 1_000 }));
935
1201
  yield* store.pause(QueueName("default"));