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.
- package/README.md +305 -25
- package/dist/Job.d.ts +118 -5
- package/dist/Job.d.ts.map +1 -1
- package/dist/Job.js +119 -4
- package/dist/Job.js.map +1 -1
- package/dist/JobStore.d.ts +260 -9
- package/dist/JobStore.d.ts.map +1 -1
- package/dist/JobStore.js +115 -1
- package/dist/JobStore.js.map +1 -1
- package/dist/MemoryJobStore.d.ts +38 -6
- package/dist/MemoryJobStore.d.ts.map +1 -1
- package/dist/MemoryJobStore.js +351 -47
- package/dist/MemoryJobStore.js.map +1 -1
- package/dist/Worker.d.ts +5 -1
- package/dist/Worker.d.ts.map +1 -1
- package/dist/Worker.js +117 -10
- package/dist/Worker.js.map +1 -1
- package/dist/{drizzle → drizzle-postgres}/DrizzleJobStore.d.ts +35 -2
- package/dist/drizzle-postgres/DrizzleJobStore.d.ts.map +1 -0
- package/dist/drizzle-postgres/DrizzleJobStore.js +941 -0
- package/dist/drizzle-postgres/DrizzleJobStore.js.map +1 -0
- package/dist/drizzle-postgres/index.d.ts.map +1 -0
- package/dist/drizzle-postgres/index.js.map +1 -0
- package/dist/drizzle-postgres/schema.d.ts +670 -0
- package/dist/drizzle-postgres/schema.d.ts.map +1 -0
- package/dist/drizzle-postgres/schema.js +150 -0
- package/dist/drizzle-postgres/schema.js.map +1 -0
- package/dist/redis/RedisJobStore.d.ts +58 -0
- package/dist/redis/RedisJobStore.d.ts.map +1 -0
- package/dist/redis/RedisJobStore.js +424 -0
- package/dist/redis/RedisJobStore.js.map +1 -0
- package/dist/redis/index.d.ts +9 -0
- package/dist/redis/index.d.ts.map +1 -0
- package/dist/redis/index.js +9 -0
- package/dist/redis/index.js.map +1 -0
- package/dist/redis/scripts.d.ts +181 -0
- package/dist/redis/scripts.d.ts.map +1 -0
- package/dist/redis/scripts.js +940 -0
- package/dist/redis/scripts.js.map +1 -0
- package/dist/testing/conformance.d.ts.map +1 -1
- package/dist/testing/conformance.js +502 -8
- package/dist/testing/conformance.js.map +1 -1
- package/package.json +8 -4
- package/src/Job.ts +301 -10
- package/src/JobStore.ts +373 -9
- package/src/MemoryJobStore.ts +440 -53
- package/src/Worker.ts +153 -10
- package/src/drizzle-postgres/DrizzleJobStore.ts +1311 -0
- package/src/drizzle-postgres/schema.ts +279 -0
- package/src/redis/RedisJobStore.ts +652 -0
- package/src/redis/index.ts +8 -0
- package/src/redis/scripts.ts +1055 -0
- package/src/testing/conformance.ts +665 -8
- package/dist/drizzle/DrizzleJobStore.d.ts.map +0 -1
- package/dist/drizzle/DrizzleJobStore.js +0 -426
- package/dist/drizzle/DrizzleJobStore.js.map +0 -1
- package/dist/drizzle/index.d.ts.map +0 -1
- package/dist/drizzle/index.js.map +0 -1
- package/dist/drizzle/schema.d.ts +0 -464
- package/dist/drizzle/schema.d.ts.map +0 -1
- package/dist/drizzle/schema.js +0 -68
- package/dist/drizzle/schema.js.map +0 -1
- package/src/drizzle/DrizzleJobStore.ts +0 -599
- package/src/drizzle/schema.ts +0 -116
- /package/dist/{drizzle → drizzle-postgres}/index.d.ts +0 -0
- /package/dist/{drizzle → drizzle-postgres}/index.js +0 -0
- /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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,6BAA6B,CAAA;AAEnD;;;GAGG;AACH,MAAM,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0Hf,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CACjC,CACE,MAAc,EACd,MAAc,EACd,EAAU,EACV,IAAY,EACZ,KAAa,EACb,WAAmB,EACnB,YAAoB,EACpB,QAAgB,EAChB,WAAmB,EACnB,WAAmB,EACnB,QAAgB,EAChB,SAAiB,EACjB,OAAe,EACf,GAAW,EACX,SAAiB,EACjB,WAAmB,EACnB,YAAoB,EACpB,aAAqB,EACrB,EAAE,CAAC;IACH,MAAM;IACN,MAAM;IACN,EAAE;IACF,IAAI;IACJ,KAAK;IACL,WAAW;IACX,YAAY;IACZ,QAAQ;IACR,WAAW;IACX,WAAW;IACX,QAAQ;IACR,SAAS;IACT,OAAO;IACP,GAAG;IACH,SAAS;IACT,WAAW;IACX,YAAY;IACZ,aAAa;CACd,EACD;IACE,YAAY,EAAE,CAAC;IACf,GAAG,EAAE,GAAG,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkGlB;CACE,CACF,CAAC,cAAc,EAAU,CAAA;AAE1B;;;;;GAKG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAC/B,CAAC,MAAc,EAAE,KAAa,EAAE,SAAiB,EAAE,KAAa,EAAE,cAAsB,EAAE,GAAW,EAAE,EAAE,CAAC;IACxG,MAAM;IACN,KAAK;IACL,SAAS;IACT,KAAK;IACL,cAAc;IACd,GAAG;CACJ,EACD;IACE,YAAY,EAAE,CAAC;IACf,GAAG,EAAE,GAAG,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyDlB;CACE,CACF,CAAC,cAAc,EAAU,CAAA;AAE1B;;;;GAIG;AACH,MAAM,CAAC,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAC7B,CAAC,MAAc,EAAE,EAAU,EAAE,KAAa,EAAE,UAAkB,EAAE,QAAgB,EAAE,OAAe,EAAE,GAAW,EAAE,EAAE,CAAC;IACjH,MAAM;IACN,EAAE;IACF,KAAK;IACL,UAAU;IACV,QAAQ;IACR,OAAO;IACP,GAAG;CACJ,EACD;IACE,YAAY,EAAE,CAAC;IACf,GAAG,EAAE,GAAG,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2DlB;CACE,CACF,CAAC,cAAc,EAAU,CAAA;AAE1B;;;GAGG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CACjC,CAAC,MAAc,EAAE,EAAU,EAAE,KAAa,EAAE,GAAW,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,EACpF;IACE,YAAY,EAAE,CAAC;IACf,GAAG,EAAE,GAAG,OAAO;;;;;;;;;;;;;;;;;;;;;;CAsBlB;CACE,CACF,CAAC,cAAc,EAAU,CAAA;AAE1B;;;GAGG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,CAAC,MAAM,CACrC,CAAC,MAAc,EAAE,SAAiB,EAAE,UAAkB,EAAE,GAAW,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,CAAC,EAC5G;IACE,YAAY,EAAE,CAAC;IACf,GAAG,EAAE,GAAG,OAAO;;;;;;;;;;;;;;;;;CAiBlB;CACE,CACF,CAAC,cAAc,EAAU,CAAA;AAE1B;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,CAAC,MAAM,CACxC,CAAC,MAAc,EAAE,eAAuB,EAAE,GAAW,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,CAAC,EACxF;IACE,YAAY,EAAE,CAAC;IACf,GAAG,EAAE,GAAG,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0ClB;CACE,CACF,CAAC,cAAc,EAAU,CAAA;AAE1B,sEAAsE;AACtE,MAAM,CAAC,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAChC,CAAC,MAAc,EAAE,EAAU,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,EAC5C;IACE,YAAY,EAAE,CAAC;IACf,GAAG,EAAE,GAAG,OAAO;;;;CAIlB;CACE,CACF,CAAC,cAAc,EAAU,CAAA;AAE1B;;;GAGG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAC9B,CAAC,MAAc,EAAE,WAAmB,EAAE,MAAc,EAAE,KAAa,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,EAC5G;IACE,YAAY,EAAE,CAAC;IACf,GAAG,EAAE,GAAG,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgElB;CACE,CACF,CAAC,cAAc,EAAU,CAAA;AAE1B,mDAAmD;AACnD,MAAM,CAAC,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAChC,CAAC,MAAc,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,EAC5B;IACE,YAAY,EAAE,CAAC;IACf,GAAG,EAAE,GAAG,OAAO;;;;CAIlB;CACE,CACF,CAAC,cAAc,EAAU,CAAA;AAE1B,uEAAuE;AACvE,MAAM,CAAC,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAChC,CAAC,MAAc,EAAE,EAAU,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,EAC5C;IACE,YAAY,EAAE,CAAC;IACf,GAAG,EAAE,GAAG,OAAO;;;;;;;;CAQlB;CACE,CACF,CAAC,cAAc,EAAU,CAAA;AAE1B,sEAAsE;AACtE,MAAM,CAAC,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAC/B,CAAC,MAAc,EAAE,EAAU,EAAE,GAAW,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,EAAE,GAAG,CAAC,EAC9D;IACE,YAAY,EAAE,CAAC;IACf,GAAG,EAAE,GAAG,OAAO;;;;;;;;;;;;;;;;;;;;;CAqBlB;CACE,CACF,CAAC,cAAc,EAAU,CAAA;AAE1B;;;GAGG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAChC,CAAC,MAAc,EAAE,EAAU,EAAE,GAAW,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,EAAE,GAAG,CAAC,EAC9D;IACE,YAAY,EAAE,CAAC;IACf,GAAG,EAAE,GAAG,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BlB;CACE,CACF,CAAC,cAAc,EAAU,CAAA;AAE1B,yDAAyD;AACzD,MAAM,CAAC,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CACjC,CAAC,MAAc,EAAE,EAAU,EAAE,GAAW,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,EAAE,GAAG,CAAC,EAC9D;IACE,YAAY,EAAE,CAAC;IACf,GAAG,EAAE,GAAG,OAAO;;;;;;;;;;;;;;;CAelB;CACE,CACF,CAAC,cAAc,EAAU,CAAA;AAE1B;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,CAAC,MAAM,CACxC,CACE,MAAc,EACd,GAAW,EACX,OAAe,EACf,KAAa,EACb,IAAY,EACZ,EAAU,EACV,OAAe,EACf,WAAmB,EACnB,YAAoB,EACpB,QAAgB,EAChB,WAAmB,EACnB,WAAmB,EACnB,QAAgB,EAChB,SAAiB,EACjB,SAAiB,EACjB,EAAE,CAAC;IACH,MAAM;IACN,GAAG;IACH,OAAO;IACP,KAAK;IACL,IAAI;IACJ,EAAE;IACF,OAAO;IACP,WAAW;IACX,YAAY;IACZ,QAAQ;IACR,WAAW;IACX,WAAW;IACX,QAAQ;IACR,SAAS;IACT,SAAS;CACV,EACD;IACE,YAAY,EAAE,CAAC;IACf,GAAG,EAAE,GAAG,OAAO;;;;;;;;;;;;;;;;;;;;;;CAsBlB;CACE,CACF,CAAC,cAAc,EAAU,CAAA;AAE1B,sDAAsD;AACtD,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,CAAC,MAAM,CACxC,CAAC,MAAc,EAAE,GAAW,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,GAAG,CAAC,EAC9C;IACE,YAAY,EAAE,CAAC;IACf,GAAG,EAAE,GAAG,OAAO;;;;CAIlB;CACE,CACF,CAAC,cAAc,EAAU,CAAA;AAE1B,yEAAyE;AACzE,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,CAAC,MAAM,CACvC,CAAC,MAAc,EAAE,WAAmB,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,WAAW,CAAC,EAC9D;IACE,YAAY,EAAE,CAAC;IACf,GAAG,EAAE,GAAG,OAAO;;;;;;;;;;;;;;;CAelB;CACE,CACF,CAAC,cAAc,EAAU,CAAA;AAE1B,gEAAgE;AAChE,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,CAAC,MAAM,CACtC,CAAC,MAAc,EAAE,GAAW,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,GAAG,CAAC,EAC9C;IACE,YAAY,EAAE,CAAC;IACf,GAAG,EAAE,GAAG,OAAO;;;;;;;;CAQlB;CACE,CACF,CAAC,cAAc,EAAU,CAAA;AAE1B,gFAAgF;AAChF,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,CAAC,MAAM,CACzC,CAAC,MAAc,EAAE,GAAW,EAAE,aAAqB,EAAE,SAAiB,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,GAAG,EAAE,aAAa,EAAE,SAAS,CAAC,EAClH;IACE,YAAY,EAAE,CAAC;IACf,GAAG,EAAE,GAAG,OAAO;;;;;;;;CAQlB;CACE,CACF,CAAC,cAAc,EAAU,CAAA;AAE1B;;;;;GAKG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CACpC,CAAC,MAAc,EAAE,KAAa,EAAE,KAAa,EAAE,KAAa,EAAE,MAAc,EAAE,GAAW,EAAE,EAAE,CAAC;IAC5F,MAAM;IACN,KAAK;IACL,KAAK;IACL,KAAK;IACL,MAAM;IACN,GAAG;CACJ,EACD;IACE,YAAY,EAAE,CAAC;IACf,GAAG,EAAE,GAAG,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6ClB;CACE,CACF,CAAC,cAAc,EAAU,CAAA;AAE1B;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,CAAC,MAAM,CACtC,CAAC,MAAc,EAAE,KAAa,EAAE,GAAW,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,CAAC,EACpE;IACE,YAAY,EAAE,CAAC;IACf,GAAG,EAAE,GAAG,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmClB;CACE,CACF,CAAC,cAAc,EAAU,CAAA"}
|
|
@@ -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;
|
|
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
|
|
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
|