effect-mq 0.1.0 → 0.3.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 (67) hide show
  1. package/README.md +305 -25
  2. package/dist/Job.d.ts +118 -5
  3. package/dist/Job.d.ts.map +1 -1
  4. package/dist/Job.js +119 -4
  5. package/dist/Job.js.map +1 -1
  6. package/dist/JobStore.d.ts +260 -9
  7. package/dist/JobStore.d.ts.map +1 -1
  8. package/dist/JobStore.js +115 -1
  9. package/dist/JobStore.js.map +1 -1
  10. package/dist/MemoryJobStore.d.ts +38 -6
  11. package/dist/MemoryJobStore.d.ts.map +1 -1
  12. package/dist/MemoryJobStore.js +351 -47
  13. package/dist/MemoryJobStore.js.map +1 -1
  14. package/dist/Worker.d.ts +5 -1
  15. package/dist/Worker.d.ts.map +1 -1
  16. package/dist/Worker.js +117 -10
  17. package/dist/Worker.js.map +1 -1
  18. package/dist/{drizzle → drizzle-postgres}/DrizzleJobStore.d.ts +35 -2
  19. package/dist/drizzle-postgres/DrizzleJobStore.d.ts.map +1 -0
  20. package/dist/drizzle-postgres/DrizzleJobStore.js +941 -0
  21. package/dist/drizzle-postgres/DrizzleJobStore.js.map +1 -0
  22. package/dist/drizzle-postgres/index.d.ts.map +1 -0
  23. package/dist/drizzle-postgres/index.js.map +1 -0
  24. package/dist/drizzle-postgres/schema.d.ts +670 -0
  25. package/dist/drizzle-postgres/schema.d.ts.map +1 -0
  26. package/dist/drizzle-postgres/schema.js +150 -0
  27. package/dist/drizzle-postgres/schema.js.map +1 -0
  28. package/dist/redis/RedisJobStore.d.ts +58 -0
  29. package/dist/redis/RedisJobStore.d.ts.map +1 -0
  30. package/dist/redis/RedisJobStore.js +424 -0
  31. package/dist/redis/RedisJobStore.js.map +1 -0
  32. package/dist/redis/index.d.ts +9 -0
  33. package/dist/redis/index.d.ts.map +1 -0
  34. package/dist/redis/index.js +9 -0
  35. package/dist/redis/index.js.map +1 -0
  36. package/dist/redis/scripts.d.ts +181 -0
  37. package/dist/redis/scripts.d.ts.map +1 -0
  38. package/dist/redis/scripts.js +940 -0
  39. package/dist/redis/scripts.js.map +1 -0
  40. package/dist/testing/conformance.d.ts.map +1 -1
  41. package/dist/testing/conformance.js +502 -8
  42. package/dist/testing/conformance.js.map +1 -1
  43. package/package.json +8 -4
  44. package/src/Job.ts +301 -10
  45. package/src/JobStore.ts +373 -9
  46. package/src/MemoryJobStore.ts +440 -53
  47. package/src/Worker.ts +153 -10
  48. package/src/drizzle-postgres/DrizzleJobStore.ts +1311 -0
  49. package/src/drizzle-postgres/schema.ts +279 -0
  50. package/src/redis/RedisJobStore.ts +652 -0
  51. package/src/redis/index.ts +8 -0
  52. package/src/redis/scripts.ts +1055 -0
  53. package/src/testing/conformance.ts +665 -8
  54. package/dist/drizzle/DrizzleJobStore.d.ts.map +0 -1
  55. package/dist/drizzle/DrizzleJobStore.js +0 -426
  56. package/dist/drizzle/DrizzleJobStore.js.map +0 -1
  57. package/dist/drizzle/index.d.ts.map +0 -1
  58. package/dist/drizzle/index.js.map +0 -1
  59. package/dist/drizzle/schema.d.ts +0 -464
  60. package/dist/drizzle/schema.d.ts.map +0 -1
  61. package/dist/drizzle/schema.js +0 -68
  62. package/dist/drizzle/schema.js.map +0 -1
  63. package/src/drizzle/DrizzleJobStore.ts +0 -599
  64. package/src/drizzle/schema.ts +0 -116
  65. /package/dist/{drizzle → drizzle-postgres}/index.d.ts +0 -0
  66. /package/dist/{drizzle → drizzle-postgres}/index.js +0 -0
  67. /package/src/{drizzle → drizzle-postgres}/index.ts +0 -0
@@ -0,0 +1 @@
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;AA+BhE;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,SACxB,MAAM,cACA,MAAM,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAC/C,IAklBF,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;AAiChE;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,SACxB,MAAM,cACA,MAAM,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAC/C,IAiuCF,CAAA"}
@@ -31,6 +31,8 @@ const baseRequest = (overrides) => ({
31
31
  attemptsMax: 1,
32
32
  backoff: undefined,
33
33
  keep: undefined,
34
+ timeoutMs: undefined,
35
+ dedupe: undefined,
34
36
  delayMs: 0,
35
37
  ...overrides
36
38
  });
@@ -210,8 +212,9 @@ export const jobStoreConformance = (name, storeLayer) => {
210
212
  const { id } = yield* store.enqueue(baseRequest());
211
213
  const claim = yield* store.claim(claimOptions({ lockDurationMs: 10_000 }));
212
214
  assert(claim._tag === "Claimed");
213
- const lost = yield* store.extendLocks([{ id, token: "t-1" }, { id: JobId("ghost"), token: "t-9" }], 20_000);
214
- expect(lost).toEqual(["ghost"]);
215
+ const result = yield* store.extendLocks([{ id, token: "t-1" }, { id: JobId("ghost"), token: "t-9" }], 20_000);
216
+ expect(result.lost).toEqual(["ghost"]);
217
+ expect(result.cancelRequested).toEqual([]);
215
218
  // The extension outlives the original lock duration.
216
219
  yield* TestClock.adjust(15_000);
217
220
  const recovered = yield* store.recoverStalled({ maxStalledCount: 1 });
@@ -317,7 +320,7 @@ export const jobStoreConformance = (name, storeLayer) => {
317
320
  })));
318
321
  it.effect("keep count prunes older terminal records of the same name and state", () => withStore((store) => Effect.gen(function* () {
319
322
  for (let i = 0; i < 4; i++) {
320
- const { id } = yield* store.enqueue(baseRequest({ payload: { n: i }, keep: { count: 2, ageMs: undefined } }));
323
+ const { id } = yield* store.enqueue(baseRequest({ payload: { n: i }, keep: { completed: { count: 2, ageMs: undefined } } }));
321
324
  const claim = yield* store.claim(claimOptions({ token: `t-${i}` }));
322
325
  assert(claim._tag === "Claimed");
323
326
  yield* store.ack(id, `t-${i}`, { _tag: "Complete", exit: null });
@@ -328,7 +331,7 @@ export const jobStoreConformance = (name, storeLayer) => {
328
331
  expect(listed.items.map((job) => job.payload)).toEqual([{ n: 3 }, { n: 2 }]);
329
332
  })));
330
333
  it.effect("keep age prunes terminal records older than the window", () => withStore((store) => Effect.gen(function* () {
331
- const keep = { count: undefined, ageMs: 10_000 };
334
+ const keep = { completed: { count: undefined, ageMs: 10_000 } };
332
335
  const first = yield* store.enqueue(baseRequest({ payload: { n: 1 }, keep }));
333
336
  const claim1 = yield* store.claim(claimOptions());
334
337
  assert(claim1._tag === "Claimed");
@@ -368,8 +371,8 @@ export const jobStoreConformance = (name, storeLayer) => {
368
371
  it.effect("keep count ties on finishedAt keep the most recently acked records", () => withStore((store) => Effect.gen(function* () {
369
372
  // Two jobs acked at the SAME TestClock instant: the tie must break
370
373
  // on enqueue/seq order identically in every driver.
371
- const first = yield* store.enqueue(baseRequest({ payload: { n: 1 }, keep: { count: 1, ageMs: undefined } }));
372
- const second = yield* store.enqueue(baseRequest({ payload: { n: 2 }, keep: { count: 1, ageMs: undefined } }));
374
+ const first = yield* store.enqueue(baseRequest({ payload: { n: 1 }, keep: { completed: { count: 1, ageMs: undefined } } }));
375
+ const second = yield* store.enqueue(baseRequest({ payload: { n: 2 }, keep: { completed: { count: 1, ageMs: undefined } } }));
373
376
  const claimA = yield* store.claim(claimOptions({ token: "t-a" }));
374
377
  const claimB = yield* store.claim(claimOptions({ token: "t-b" }));
375
378
  assert(claimA._tag === "Claimed" && claimB._tag === "Claimed");
@@ -379,7 +382,7 @@ export const jobStoreConformance = (name, storeLayer) => {
379
382
  expect(Option.isSome(yield* store.getJob(second.id))).toBe(true);
380
383
  })));
381
384
  it.effect("keep applies count and age together", () => withStore((store) => Effect.gen(function* () {
382
- const keep = { count: 2, ageMs: 10_000 };
385
+ const keep = { completed: { count: 2, ageMs: 10_000 } };
383
386
  const ids = [];
384
387
  for (let i = 0; i < 3; i++) {
385
388
  const { id } = yield* store.enqueue(baseRequest({ payload: { n: i }, keep }));
@@ -426,7 +429,8 @@ export const jobStoreConformance = (name, storeLayer) => {
426
429
  delayed: 1,
427
430
  active: 1,
428
431
  completed: 0,
429
- failed: 0
432
+ failed: 0,
433
+ cancelled: 0
430
434
  });
431
435
  const other = yield* store.counts(QueueName("other"));
432
436
  expect(other.waiting).toBe(1);
@@ -446,6 +450,496 @@ export const jobStoreConformance = (name, storeLayer) => {
446
450
  it.effect("getAttempts returns an empty ledger for unknown ids", () => withStore((store) => Effect.gen(function* () {
447
451
  expect(yield* store.getAttempts(JobId("nope"))).toEqual([]);
448
452
  })));
453
+ it.effect("cancel makes waiting and delayed jobs terminal with a ledger entry", () => withStore((store) => Effect.gen(function* () {
454
+ const waiting = yield* store.enqueue(baseRequest());
455
+ const delayed = yield* store.enqueue(baseRequest({ delayMs: 60_000 }));
456
+ yield* store.cancel(waiting.id);
457
+ yield* store.cancel(delayed.id);
458
+ for (const id of [waiting.id, delayed.id]) {
459
+ const job = yield* store.getJob(id);
460
+ assert(Option.isSome(job));
461
+ expect(job.value.state).toBe("cancelled");
462
+ expect(job.value.finishedAt).toBeDefined();
463
+ const attempts = yield* store.getAttempts(id);
464
+ expect(attempts.map((attempt) => attempt.outcome)).toEqual(["cancelled"]);
465
+ }
466
+ const claim = yield* store.claim(claimOptions());
467
+ assert(claim._tag === "Empty");
468
+ // Terminal jobs cannot be cancelled again; unknown ids are reported.
469
+ const again = yield* Effect.flip(store.cancel(waiting.id));
470
+ expect(again._tag).toBe("JobNotCancellableError");
471
+ const missing = yield* Effect.flip(store.cancel(JobId("nope")));
472
+ expect(missing._tag).toBe("JobNotFoundError");
473
+ })));
474
+ it.effect("cancel on an active job flags it; the heartbeat reports it; ack Cancelled finishes it", () => withStore((store) => Effect.gen(function* () {
475
+ const { id } = yield* store.enqueue(baseRequest());
476
+ const claim = yield* store.claim(claimOptions());
477
+ assert(claim._tag === "Claimed");
478
+ yield* store.cancel(id);
479
+ const flagged = yield* store.getJob(id);
480
+ assert(Option.isSome(flagged));
481
+ expect(flagged.value.state).toBe("active");
482
+ expect(flagged.value.cancelRequested).toBe(true);
483
+ // The heartbeat surfaces the request instead of extending the lock.
484
+ const heartbeat = yield* store.extendLocks([{ id, token: "t-1" }], 20_000);
485
+ expect(heartbeat.cancelRequested).toEqual([id]);
486
+ expect(heartbeat.lost).toEqual([]);
487
+ yield* store.ack(id, "t-1", { _tag: "Cancelled" });
488
+ const done = yield* store.getJob(id);
489
+ assert(Option.isSome(done));
490
+ expect(done.value.state).toBe("cancelled");
491
+ expect(done.value.cancelRequested).toBe(false);
492
+ const attempts = yield* store.getAttempts(id);
493
+ expect(attempts.map((attempt) => attempt.outcome)).toEqual(["cancelled"]);
494
+ })));
495
+ it.effect("release and stall recovery honour a pending cancel request", () => withStore((store) => Effect.gen(function* () {
496
+ // release path
497
+ const first = yield* store.enqueue(baseRequest());
498
+ const claimA = yield* store.claim(claimOptions());
499
+ assert(claimA._tag === "Claimed");
500
+ yield* store.cancel(first.id);
501
+ yield* store.release(first.id, "t-1");
502
+ const released = yield* store.getJob(first.id);
503
+ assert(Option.isSome(released));
504
+ expect(released.value.state).toBe("cancelled");
505
+ // stall path
506
+ const second = yield* store.enqueue(baseRequest({ payload: { n: 2 } }));
507
+ const claimB = yield* store.claim(claimOptions({ token: "t-2", lockDurationMs: 1_000 }));
508
+ assert(claimB._tag === "Claimed");
509
+ yield* store.cancel(second.id);
510
+ yield* TestClock.adjust(1_000);
511
+ const recovered = yield* store.recoverStalled({ maxStalledCount: 5 });
512
+ // Cancelled-by-recovery jobs are not reported as stalled recoveries.
513
+ expect(recovered).toEqual([]);
514
+ const swept = yield* store.getJob(second.id);
515
+ assert(Option.isSome(swept));
516
+ expect(swept.value.state).toBe("cancelled");
517
+ })));
518
+ it.effect("promote runs a delayed job now and rejects other states", () => withStore((store) => Effect.gen(function* () {
519
+ const { id } = yield* store.enqueue(baseRequest({ delayMs: 60_000 }));
520
+ const early = yield* store.claim(claimOptions());
521
+ assert(early._tag === "Empty");
522
+ yield* store.promote(id);
523
+ const claim = yield* store.claim(claimOptions({ token: "t-2" }));
524
+ assert(claim._tag === "Claimed");
525
+ expect(claim.job.id).toBe(id);
526
+ const wrongState = yield* Effect.flip(store.promote(id));
527
+ expect(wrongState._tag).toBe("JobNotPromotableError");
528
+ const missing = yield* Effect.flip(store.promote(JobId("nope")));
529
+ expect(missing._tag).toBe("JobNotFoundError");
530
+ })));
531
+ it.effect("pause stops claims for a queue until resume", () => withStore((store) => Effect.gen(function* () {
532
+ yield* store.enqueue(baseRequest());
533
+ yield* store.pause(QueueName("default"));
534
+ expect(yield* store.pausedQueues()).toEqual(["default"]);
535
+ const paused = yield* store.claim(claimOptions());
536
+ assert(paused._tag === "Empty");
537
+ // Other queues are unaffected.
538
+ yield* store.enqueue(baseRequest({ queue: QueueName("other") }));
539
+ const other = yield* store.claim(claimOptions({ queue: QueueName("other"), token: "t-2" }));
540
+ assert(other._tag === "Claimed");
541
+ // Resume wakes idle workers and claims flow again.
542
+ const empty = yield* store.claim(claimOptions({ token: "t-3" }));
543
+ assert(empty._tag === "Empty");
544
+ const waiter = yield* Effect.forkChild(store.awaitWake([QueueName("default")], empty.wakeToken));
545
+ yield* Effect.yieldNow;
546
+ yield* store.resume(QueueName("default"));
547
+ yield* Fiber.join(waiter);
548
+ expect(yield* store.pausedQueues()).toEqual([]);
549
+ const resumed = yield* store.claim(claimOptions({ token: "t-4" }));
550
+ assert(resumed._tag === "Claimed");
551
+ })));
552
+ it.effect("schedules: upsert, list, due, conditional advance, remove", () => withStore((store) => Effect.gen(function* () {
553
+ const schedule = {
554
+ key: JobStore.ScheduleKey("TestJob/hourly"),
555
+ jobName: "TestJob",
556
+ queue: QueueName("default"),
557
+ cron: "0 * * * *",
558
+ tz: undefined,
559
+ everyMs: undefined,
560
+ payload: { n: 1 },
561
+ metadata: { source: "conformance" },
562
+ priority: 2,
563
+ attemptsMax: 3,
564
+ backoff: { _tag: "fixed", delayMs: 1_000 },
565
+ keep: undefined,
566
+ timeoutMs: 5_000,
567
+ nextRunAt: 60_000
568
+ };
569
+ yield* store.upsertSchedule(schedule);
570
+ expect(yield* store.listSchedules()).toEqual([schedule]);
571
+ expect(yield* store.listSchedules({ jobName: "OtherJob" })).toEqual([]);
572
+ // Not due yet (TestClock starts at 0).
573
+ expect(yield* store.dueSchedules()).toEqual([]);
574
+ yield* TestClock.adjust(60_000);
575
+ expect(yield* store.dueSchedules()).toEqual([schedule]);
576
+ // Upsert replaces in place.
577
+ yield* store.upsertSchedule({ ...schedule, priority: 9 });
578
+ expect((yield* store.listSchedules())[0]?.priority).toBe(9);
579
+ // Conditional advance: a stale expectation is a no-op.
580
+ yield* store.advanceSchedule(schedule.key, 999, 120_000);
581
+ expect((yield* store.listSchedules())[0]?.nextRunAt).toBe(60_000);
582
+ yield* store.advanceSchedule(schedule.key, 60_000, 120_000);
583
+ expect((yield* store.listSchedules())[0]?.nextRunAt).toBe(120_000);
584
+ expect(yield* store.dueSchedules()).toEqual([]);
585
+ expect(yield* store.removeSchedule(schedule.key)).toBe(true);
586
+ expect(yield* store.removeSchedule(schedule.key)).toBe(false);
587
+ expect(yield* store.listSchedules()).toEqual([]);
588
+ })));
589
+ it.effect("completion wins over a pending cancel request", () => withStore((store) => Effect.gen(function* () {
590
+ const { id } = yield* store.enqueue(baseRequest());
591
+ const claim = yield* store.claim(claimOptions());
592
+ assert(claim._tag === "Claimed");
593
+ yield* store.cancel(id);
594
+ yield* store.ack(id, "t-1", { _tag: "Complete", exit: { ok: true } });
595
+ const job = yield* store.getJob(id);
596
+ assert(Option.isSome(job));
597
+ expect(job.value.state).toBe("completed");
598
+ expect(job.value.cancelRequested).toBe(false);
599
+ })));
600
+ it.effect("a natural failure racing a cancel is cancelled, not revived", () => withStore((store) => Effect.gen(function* () {
601
+ const { id } = yield* store.enqueue(baseRequest({ attemptsMax: 5 }));
602
+ const claim = yield* store.claim(claimOptions());
603
+ assert(claim._tag === "Claimed");
604
+ yield* store.cancel(id);
605
+ // The handler failed on its own before the heartbeat could
606
+ // interrupt it; the retry ack must honour the pending cancel.
607
+ yield* store.ack(id, "t-1", { _tag: "Retry", exit: { boom: 1 }, delayMs: 1_000 });
608
+ const job = yield* store.getJob(id);
609
+ assert(Option.isSome(job));
610
+ expect(job.value.state).toBe("cancelled");
611
+ expect(job.value.cancelRequested).toBe(false);
612
+ const attempts = yield* store.getAttempts(id);
613
+ expect(attempts.map((attempt) => attempt.outcome)).toEqual(["cancelled"]);
614
+ const nothing = yield* store.claim(claimOptions({ token: "t-2" }));
615
+ assert(nothing._tag === "Empty");
616
+ })));
617
+ it.effect("ack Cancelled with a wrong token fails with LockLostError", () => withStore((store) => Effect.gen(function* () {
618
+ const { id } = yield* store.enqueue(baseRequest());
619
+ const claim = yield* store.claim(claimOptions());
620
+ assert(claim._tag === "Claimed");
621
+ yield* store.cancel(id);
622
+ const wrong = yield* Effect.flip(store.ack(id, "not-mine", { _tag: "Cancelled" }));
623
+ expect(wrong._tag).toBe("LockLostError");
624
+ const job = yield* store.getJob(id);
625
+ assert(Option.isSome(job));
626
+ expect(job.value.state).toBe("active");
627
+ expect(job.value.cancelRequested).toBe(true);
628
+ })));
629
+ it.effect("cancel applies the keep retention policy", () => withStore((store) => Effect.gen(function* () {
630
+ const keep = { cancelled: { count: 1 } };
631
+ const ids = [];
632
+ for (let i = 0; i < 3; i++) {
633
+ const { id } = yield* store.enqueue(baseRequest({ payload: { n: i }, keep }));
634
+ ids.push(id);
635
+ yield* store.cancel(id);
636
+ yield* TestClock.adjust(10);
637
+ }
638
+ const listed = yield* store.list({ states: ["cancelled"] });
639
+ expect(listed.items).toHaveLength(1);
640
+ expect(listed.items[0]?.id).toBe(ids[2]);
641
+ })));
642
+ it.effect("upserting an unchanged cadence preserves the next occurrence; a changed one resets it", () => withStore((store) => Effect.gen(function* () {
643
+ const base = {
644
+ key: JobStore.ScheduleKey("TestJob/pulse"),
645
+ jobName: "TestJob",
646
+ queue: QueueName("default"),
647
+ cron: undefined,
648
+ tz: undefined,
649
+ everyMs: 60_000,
650
+ payload: {},
651
+ metadata: {},
652
+ priority: 0,
653
+ attemptsMax: 1,
654
+ backoff: undefined,
655
+ keep: undefined,
656
+ timeoutMs: undefined,
657
+ nextRunAt: 60_000
658
+ };
659
+ yield* store.upsertSchedule(base);
660
+ // A deploy-time re-registration recomputes nextRunAt from "now" —
661
+ // the store must keep the original grid point.
662
+ yield* TestClock.adjust(30_000);
663
+ yield* store.upsertSchedule({ ...base, priority: 5, nextRunAt: 90_000 });
664
+ const preserved = (yield* store.listSchedules())[0];
665
+ expect(preserved?.nextRunAt).toBe(60_000);
666
+ expect(preserved?.priority).toBe(5);
667
+ // Changing the cadence takes the caller's fresh nextRunAt.
668
+ yield* store.upsertSchedule({ ...base, everyMs: 120_000, nextRunAt: 150_000 });
669
+ expect((yield* store.listSchedules())[0]?.nextRunAt).toBe(150_000);
670
+ })));
671
+ it.effect("schedule records round-trip every/tz fields; due order is by nextRunAt; list filters by queue", () => withStore((store) => Effect.gen(function* () {
672
+ const everySchedule = {
673
+ key: JobStore.ScheduleKey("TestJob/every"),
674
+ jobName: "TestJob",
675
+ queue: QueueName("default"),
676
+ cron: undefined,
677
+ tz: undefined,
678
+ everyMs: 90_000,
679
+ payload: { n: 1 },
680
+ metadata: {},
681
+ priority: 0,
682
+ attemptsMax: 1,
683
+ backoff: undefined,
684
+ keep: undefined,
685
+ timeoutMs: undefined,
686
+ nextRunAt: 90_000
687
+ };
688
+ const cronSchedule = {
689
+ ...everySchedule,
690
+ key: JobStore.ScheduleKey("TestJob/tz"),
691
+ queue: QueueName("other"),
692
+ cron: "0 9 * * *",
693
+ tz: "America/New_York",
694
+ everyMs: undefined,
695
+ nextRunAt: 30_000
696
+ };
697
+ yield* store.upsertSchedule(everySchedule);
698
+ yield* store.upsertSchedule(cronSchedule);
699
+ const byQueue = yield* store.listSchedules({ queue: QueueName("other") });
700
+ expect(byQueue).toEqual([cronSchedule]);
701
+ expect(byQueue[0]?.tz).toBe("America/New_York");
702
+ const roundTripped = (yield* store.listSchedules({ queue: QueueName("default") }))[0];
703
+ expect(roundTripped?.everyMs).toBe(90_000);
704
+ // Both due: ordered by nextRunAt ascending.
705
+ yield* TestClock.adjust(90_000);
706
+ const due = yield* store.dueSchedules();
707
+ expect(due.map((schedule) => schedule.key)).toEqual([
708
+ "TestJob/tz",
709
+ "TestJob/every"
710
+ ]);
711
+ })));
712
+ it.effect("counts stay consistent across cancel, retry, keep pruning, and remove", () => withStore((store) => Effect.gen(function* () {
713
+ // Three completions with keep {count: 1}: two get pruned.
714
+ for (let i = 0; i < 3; i++) {
715
+ const { id } = yield* store.enqueue(baseRequest({ payload: { n: i }, keep: { completed: { count: 1 } } }));
716
+ const claim = yield* store.claim(claimOptions({ token: `t-${i}` }));
717
+ assert(claim._tag === "Claimed");
718
+ yield* store.ack(id, `t-${i}`, { _tag: "Complete", exit: undefined });
719
+ yield* TestClock.adjust(10);
720
+ }
721
+ expect((yield* store.counts()).completed).toBe(1);
722
+ // Cancel a waiting job.
723
+ const doomed = yield* store.enqueue(baseRequest({ name: "Doomed" }));
724
+ yield* store.cancel(doomed.id);
725
+ expect((yield* store.counts()).cancelled).toBe(1);
726
+ // Fail a job, then admin-retry it: failed -> waiting.
727
+ const flaky = yield* store.enqueue(baseRequest());
728
+ const claim = yield* store.claim(claimOptions({ token: "t-f" }));
729
+ assert(claim._tag === "Claimed");
730
+ yield* store.ack(flaky.id, "t-f", { _tag: "Fail", exit: undefined });
731
+ expect((yield* store.counts()).failed).toBe(1);
732
+ yield* store.retry(flaky.id);
733
+ // Remove the surviving completed record.
734
+ const completed = yield* store.list({ states: ["completed"] });
735
+ const survivor = completed.items[0];
736
+ assert(survivor !== undefined);
737
+ expect(yield* store.remove(survivor.id)).toBe(true);
738
+ expect(yield* store.counts()).toEqual({
739
+ waiting: 1,
740
+ delayed: 0,
741
+ active: 0,
742
+ completed: 0,
743
+ failed: 0,
744
+ cancelled: 1
745
+ });
746
+ })));
747
+ it.effect("schedule payloads round-trip high-precision numbers and empty arrays exactly", () => withStore((store) => Effect.gen(function* () {
748
+ const payload = { big: 1234567890123456, third: 1 / 3, empty: [] };
749
+ const schedule = {
750
+ key: JobStore.ScheduleKey("TestJob/precise"),
751
+ jobName: "TestJob",
752
+ queue: QueueName("default"),
753
+ cron: undefined,
754
+ tz: undefined,
755
+ everyMs: 60_000,
756
+ payload,
757
+ metadata: {},
758
+ priority: 0,
759
+ attemptsMax: 1,
760
+ backoff: undefined,
761
+ keep: undefined,
762
+ timeoutMs: undefined,
763
+ nextRunAt: 60_000
764
+ };
765
+ yield* store.upsertSchedule(schedule);
766
+ const stored = (yield* store.listSchedules())[0];
767
+ expect(stored?.payload).toEqual(payload);
768
+ })));
769
+ it.effect("dedupe never changes the job id, and keys are scoped per name", () => withStore((store) => Effect.gen(function* () {
770
+ const dedupe = { key: "emp-1", ttlMs: undefined, extend: false, replace: false };
771
+ const first = yield* store.enqueue(baseRequest({ id: JobId("my-ulid-1"), dedupe }));
772
+ expect(first).toEqual({ id: "my-ulid-1", duplicate: false });
773
+ const job = yield* store.getJob(first.id);
774
+ assert(Option.isSome(job));
775
+ expect(job.value.dedupeKey).toBe("emp-1");
776
+ // Same key, same name: deduplicated to the FIRST job's id.
777
+ const second = yield* store.enqueue(baseRequest({ id: JobId("my-ulid-2"), dedupe }));
778
+ expect(second).toEqual({ id: "my-ulid-1", duplicate: true });
779
+ expect(Option.isNone(yield* store.getJob(JobId("my-ulid-2")))).toBe(true);
780
+ // Same key, different name: no interference.
781
+ const other = yield* store.enqueue(baseRequest({ name: "OtherJob", dedupe }));
782
+ expect(other.duplicate).toBe(false);
783
+ })));
784
+ it.effect("pending dedupe holds while the keyed job is unfinished and frees on completion", () => withStore((store) => Effect.gen(function* () {
785
+ const dedupe = { key: "k", ttlMs: undefined, extend: false, replace: false };
786
+ const first = yield* store.enqueue(baseRequest({ dedupe }));
787
+ expect((yield* store.enqueue(baseRequest({ dedupe }))).duplicate).toBe(true);
788
+ // Still deduped while active.
789
+ const claim = yield* store.claim(claimOptions());
790
+ assert(claim._tag === "Claimed");
791
+ expect((yield* store.enqueue(baseRequest({ dedupe }))).duplicate).toBe(true);
792
+ // A terminal ack frees the key immediately.
793
+ yield* store.ack(first.id, "t-1", { _tag: "Complete", exit: undefined });
794
+ const fresh = yield* store.enqueue(baseRequest({ dedupe }));
795
+ expect(fresh.duplicate).toBe(false);
796
+ expect(fresh.id).not.toBe(first.id);
797
+ })));
798
+ it.effect("cancellation also frees a pending dedupe key", () => withStore((store) => Effect.gen(function* () {
799
+ const dedupe = { key: "k", ttlMs: undefined, extend: false, replace: false };
800
+ const first = yield* store.enqueue(baseRequest({ dedupe }));
801
+ yield* store.cancel(first.id);
802
+ expect((yield* store.enqueue(baseRequest({ dedupe }))).duplicate).toBe(false);
803
+ })));
804
+ it.effect("a ttl dedupe window throttles even past completion, then expires", () => withStore((store) => Effect.gen(function* () {
805
+ const dedupe = { key: "k", ttlMs: 60_000, extend: false, replace: false };
806
+ const first = yield* store.enqueue(baseRequest({ dedupe }));
807
+ const claim = yield* store.claim(claimOptions());
808
+ assert(claim._tag === "Claimed");
809
+ yield* store.ack(first.id, "t-1", { _tag: "Complete", exit: undefined });
810
+ // Completed, but the window still throttles...
811
+ yield* TestClock.adjust(30_000);
812
+ expect((yield* store.enqueue(baseRequest({ dedupe }))).duplicate).toBe(true);
813
+ // ...and a plain (non-extend) window is NOT pushed out by drops.
814
+ yield* TestClock.adjust(30_000);
815
+ const fresh = yield* store.enqueue(baseRequest({ dedupe }));
816
+ expect(fresh.duplicate).toBe(false);
817
+ expect(fresh.id).not.toBe(first.id);
818
+ })));
819
+ it.effect("an extend dedupe window is pushed out by each deduplicated enqueue", () => withStore((store) => Effect.gen(function* () {
820
+ const dedupe = { key: "k", ttlMs: 60_000, extend: true, replace: false };
821
+ yield* store.enqueue(baseRequest({ dedupe }));
822
+ yield* TestClock.adjust(45_000);
823
+ expect((yield* store.enqueue(baseRequest({ dedupe }))).duplicate).toBe(true);
824
+ // 75s after the FIRST enqueue — past the original window, inside
825
+ // the extended one.
826
+ yield* TestClock.adjust(30_000);
827
+ expect((yield* store.enqueue(baseRequest({ dedupe }))).duplicate).toBe(true);
828
+ // Past the latest extension: free again.
829
+ yield* TestClock.adjust(60_001);
830
+ expect((yield* store.enqueue(baseRequest({ dedupe }))).duplicate).toBe(false);
831
+ })));
832
+ it.effect("replace dedupe rewrites a still-delayed job in place, latest content wins", () => withStore((store) => Effect.gen(function* () {
833
+ const dedupe = { key: "k", ttlMs: undefined, extend: false, replace: true };
834
+ const first = yield* store.enqueue(baseRequest({ dedupe, payload: { n: 1 }, priority: 1, delayMs: 60_000 }));
835
+ const replaced = yield* store.enqueue(baseRequest({ dedupe, payload: { n: 2 }, priority: 7, delayMs: 5_000 }));
836
+ expect(replaced).toEqual({ id: first.id, duplicate: true });
837
+ const job = yield* store.getJob(first.id);
838
+ assert(Option.isSome(job));
839
+ expect(job.value.payload).toEqual({ n: 2 });
840
+ expect(job.value.priority).toBe(7);
841
+ expect(job.value.state).toBe("delayed");
842
+ // The rewritten delay is live: due after 5s, not the original 60s.
843
+ yield* TestClock.adjust(5_000);
844
+ const claim = yield* store.claim(claimOptions());
845
+ assert(claim._tag === "Claimed");
846
+ expect(claim.job.id).toBe(first.id);
847
+ expect(claim.job.payload).toEqual({ n: 2 });
848
+ // Once claimed (active), replace degrades to plain dedup.
849
+ const during = yield* store.enqueue(baseRequest({ dedupe, payload: { n: 3 } }));
850
+ expect(during).toEqual({ id: first.id, duplicate: true });
851
+ const active = yield* store.getJob(first.id);
852
+ assert(Option.isSome(active));
853
+ expect(active.value.payload).toEqual({ n: 2 });
854
+ })));
855
+ it.effect("an existing explicit id wins over the dedup tree in every driver", () => withStore((store) => Effect.gen(function* () {
856
+ const key = { key: "k", ttlMs: undefined, extend: false, replace: false };
857
+ // X runs under key k and completes, freeing the key but staying in
858
+ // history.
859
+ const x = yield* store.enqueue(baseRequest({ id: JobId("X"), dedupe: key }));
860
+ const claim = yield* store.claim(claimOptions());
861
+ assert(claim._tag === "Claimed");
862
+ yield* store.ack(x.id, "t-1", { _tag: "Complete", exit: undefined });
863
+ // J takes over the key, delayed.
864
+ const j = yield* store.enqueue(baseRequest({ dedupe: key, payload: { n: 9 }, delayMs: 60_000 }));
865
+ // A retried enqueue of X (id exists) must return X untouched — the
866
+ // id check precedes the dedup tree, so J is neither returned nor
867
+ // replaced.
868
+ const retried = yield* store.enqueue(baseRequest({
869
+ id: JobId("X"),
870
+ payload: { n: 1 },
871
+ dedupe: { ...key, replace: true }
872
+ }));
873
+ expect(retried).toEqual({ id: "X", duplicate: true });
874
+ const job = yield* store.getJob(j.id);
875
+ assert(Option.isSome(job));
876
+ expect(job.value.payload).toEqual({ n: 9 });
877
+ expect(job.value.state).toBe("delayed");
878
+ })));
879
+ it.effect("a landed replace re-arms the ttl window", () => withStore((store) => Effect.gen(function* () {
880
+ const dedupe = { key: "k", ttlMs: 60_000, extend: false, replace: true };
881
+ const first = yield* store.enqueue(baseRequest({ dedupe, delayMs: 300_000 }));
882
+ // 50s in: replace lands; the window restarts from here.
883
+ yield* TestClock.adjust(50_000);
884
+ const replaced = yield* store.enqueue(baseRequest({ dedupe, payload: { n: 2 }, delayMs: 300_000 }));
885
+ expect(replaced).toEqual({ id: first.id, duplicate: true });
886
+ // 100s after the FIRST enqueue — past the original window, inside
887
+ // the re-armed one: still the same job.
888
+ yield* TestClock.adjust(50_000);
889
+ const again = yield* store.enqueue(baseRequest({ dedupe, payload: { n: 3 }, delayMs: 300_000 }));
890
+ expect(again).toEqual({ id: first.id, duplicate: true });
891
+ })));
892
+ it.effect("keep policies are independent per terminal state", () => withStore((store) => Effect.gen(function* () {
893
+ // Completed records keep only the newest 1; failed keep everything.
894
+ const keep = { completed: { count: 1 } };
895
+ const completed = [];
896
+ const failed = [];
897
+ for (let i = 0; i < 2; i++) {
898
+ const done = yield* store.enqueue(baseRequest({ payload: { n: i }, keep }));
899
+ const claimA = yield* store.claim(claimOptions({ token: `tc-${i}` }));
900
+ assert(claimA._tag === "Claimed");
901
+ yield* store.ack(done.id, `tc-${i}`, { _tag: "Complete", exit: undefined });
902
+ completed.push(done.id);
903
+ const bad = yield* store.enqueue(baseRequest({ payload: { n: 10 + i }, keep }));
904
+ const claimB = yield* store.claim(claimOptions({ token: `tf-${i}` }));
905
+ assert(claimB._tag === "Claimed");
906
+ yield* store.ack(bad.id, `tf-${i}`, { _tag: "Fail", exit: undefined });
907
+ failed.push(bad.id);
908
+ yield* TestClock.adjust(10);
909
+ }
910
+ const counts = yield* store.counts();
911
+ expect(counts.completed).toBe(1);
912
+ expect(counts.failed).toBe(2);
913
+ expect(Option.isNone(yield* store.getJob(completed[0] ?? JobId("?")))).toBe(true);
914
+ expect(Option.isSome(yield* store.getJob(failed[0] ?? JobId("?")))).toBe(true);
915
+ })));
916
+ it.effect("an enqueue wakes only waiters watching its queue", () => withStore((store) => Effect.gen(function* () {
917
+ const empty = yield* store.claim(claimOptions());
918
+ assert(empty._tag === "Empty");
919
+ let woke = false;
920
+ const waiter = yield* Effect.forkChild(store.awaitWake([QueueName("default")], empty.wakeToken).pipe(Effect.tap(() => Effect.sync(() => void (woke = true)))));
921
+ yield* Effect.yieldNow;
922
+ // Work on ANOTHER queue must not wake the default-queue waiter.
923
+ yield* store.enqueue(baseRequest({ queue: QueueName("other") }));
924
+ for (let i = 0; i < 10; i++) {
925
+ yield* Effect.yieldNow;
926
+ }
927
+ expect(woke).toBe(false);
928
+ // Matching-queue work does.
929
+ yield* store.enqueue(baseRequest());
930
+ yield* Fiber.join(waiter);
931
+ expect(woke).toBe(true);
932
+ })));
933
+ it.effect("delayed jobs still promote to waiting while their queue is paused", () => withStore((store) => Effect.gen(function* () {
934
+ const { id } = yield* store.enqueue(baseRequest({ delayMs: 1_000 }));
935
+ yield* store.pause(QueueName("default"));
936
+ yield* TestClock.adjust(1_000);
937
+ const claim = yield* store.claim(claimOptions());
938
+ assert(claim._tag === "Empty");
939
+ const job = yield* store.getJob(id);
940
+ assert(Option.isSome(job));
941
+ expect(job.value.state).toBe("waiting");
942
+ })));
449
943
  });
450
944
  };
451
945
  //# sourceMappingURL=conformance.js.map