@ddd-ts-kit/testing 0.9.0 → 0.11.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 (129) hide show
  1. package/README.md +38 -0
  2. package/dist/contracts/event-store/companion-stores-suite.d.ts +12 -0
  3. package/dist/contracts/event-store/companion-stores-suite.d.ts.map +1 -0
  4. package/dist/contracts/event-store/companion-stores-suite.js +60 -0
  5. package/dist/contracts/event-store/companion-stores-suite.js.map +1 -0
  6. package/dist/contracts/event-store/consignment-registered.d.ts +8 -0
  7. package/dist/contracts/event-store/consignment-registered.d.ts.map +1 -0
  8. package/dist/contracts/event-store/consignment-registered.js +10 -0
  9. package/dist/contracts/event-store/consignment-registered.js.map +1 -0
  10. package/dist/contracts/event-store/consignment-weighed.d.ts +9 -0
  11. package/dist/contracts/event-store/consignment-weighed.d.ts.map +1 -0
  12. package/dist/contracts/event-store/consignment-weighed.js +12 -0
  13. package/dist/contracts/event-store/consignment-weighed.js.map +1 -0
  14. package/dist/contracts/event-store/consignment.d.ts +16 -0
  15. package/dist/contracts/event-store/consignment.d.ts.map +1 -0
  16. package/dist/contracts/event-store/consignment.js +36 -0
  17. package/dist/contracts/event-store/consignment.js.map +1 -0
  18. package/dist/contracts/event-store/consignments.d.ts +10 -0
  19. package/dist/contracts/event-store/consignments.d.ts.map +1 -0
  20. package/dist/contracts/event-store/consignments.js +34 -0
  21. package/dist/contracts/event-store/consignments.js.map +1 -0
  22. package/dist/contracts/event-store/event-of.d.ts +4 -0
  23. package/dist/contracts/event-store/event-of.d.ts.map +1 -0
  24. package/dist/contracts/event-store/event-of.js +13 -0
  25. package/dist/contracts/event-store/event-of.js.map +1 -0
  26. package/dist/contracts/event-store/event-store-under-contract.d.ts +11 -0
  27. package/dist/contracts/event-store/event-store-under-contract.d.ts.map +1 -0
  28. package/dist/contracts/event-store/event-store-under-contract.js +2 -0
  29. package/dist/contracts/event-store/event-store-under-contract.js.map +1 -0
  30. package/dist/contracts/event-store/one-transaction-suite.d.ts +18 -0
  31. package/dist/contracts/event-store/one-transaction-suite.d.ts.map +1 -0
  32. package/dist/contracts/event-store/one-transaction-suite.js +92 -0
  33. package/dist/contracts/event-store/one-transaction-suite.js.map +1 -0
  34. package/dist/contracts/event-store/store-suite.d.ts +10 -0
  35. package/dist/contracts/event-store/store-suite.d.ts.map +1 -0
  36. package/dist/contracts/event-store/store-suite.js +44 -0
  37. package/dist/contracts/event-store/store-suite.js.map +1 -0
  38. package/dist/contracts/event-store/stream-suite.d.ts +11 -0
  39. package/dist/contracts/event-store/stream-suite.d.ts.map +1 -0
  40. package/dist/contracts/event-store/stream-suite.js +91 -0
  41. package/dist/contracts/event-store/stream-suite.js.map +1 -0
  42. package/dist/contracts/event-store-contract.d.ts +20 -0
  43. package/dist/contracts/event-store-contract.d.ts.map +1 -0
  44. package/dist/contracts/event-store-contract.js +21 -0
  45. package/dist/contracts/event-store-contract.js.map +1 -0
  46. package/dist/contracts/event-store-unit-of-work-contract.d.ts +15 -0
  47. package/dist/contracts/event-store-unit-of-work-contract.d.ts.map +1 -0
  48. package/dist/contracts/event-store-unit-of-work-contract.js +10 -0
  49. package/dist/contracts/event-store-unit-of-work-contract.js.map +1 -0
  50. package/dist/contracts/job-store/a-job.d.ts +24 -0
  51. package/dist/contracts/job-store/a-job.d.ts.map +1 -0
  52. package/dist/contracts/job-store/a-job.js +28 -0
  53. package/dist/contracts/job-store/a-job.js.map +1 -0
  54. package/dist/contracts/job-store/claim-suite.d.ts +15 -0
  55. package/dist/contracts/job-store/claim-suite.d.ts.map +1 -0
  56. package/dist/contracts/job-store/claim-suite.js +126 -0
  57. package/dist/contracts/job-store/claim-suite.js.map +1 -0
  58. package/dist/contracts/job-store/job-fixtures.d.ts +5 -0
  59. package/dist/contracts/job-store/job-fixtures.d.ts.map +1 -0
  60. package/dist/contracts/job-store/job-fixtures.js +5 -0
  61. package/dist/contracts/job-store/job-fixtures.js.map +1 -0
  62. package/dist/contracts/job-store/job-store-under-contract.d.ts +61 -0
  63. package/dist/contracts/job-store/job-store-under-contract.d.ts.map +1 -0
  64. package/dist/contracts/job-store/job-store-under-contract.js +2 -0
  65. package/dist/contracts/job-store/job-store-under-contract.js.map +1 -0
  66. package/dist/contracts/job-store/keep-suite.d.ts +11 -0
  67. package/dist/contracts/job-store/keep-suite.d.ts.map +1 -0
  68. package/dist/contracts/job-store/keep-suite.js +55 -0
  69. package/dist/contracts/job-store/keep-suite.js.map +1 -0
  70. package/dist/contracts/job-store/later.d.ts +3 -0
  71. package/dist/contracts/job-store/later.d.ts.map +1 -0
  72. package/dist/contracts/job-store/later.js +6 -0
  73. package/dist/contracts/job-store/later.js.map +1 -0
  74. package/dist/contracts/job-store/report-suite.d.ts +12 -0
  75. package/dist/contracts/job-store/report-suite.d.ts.map +1 -0
  76. package/dist/contracts/job-store/report-suite.js +82 -0
  77. package/dist/contracts/job-store/report-suite.js.map +1 -0
  78. package/dist/contracts/job-store/transaction-suite.d.ts +23 -0
  79. package/dist/contracts/job-store/transaction-suite.d.ts.map +1 -0
  80. package/dist/contracts/job-store/transaction-suite.js +68 -0
  81. package/dist/contracts/job-store/transaction-suite.js.map +1 -0
  82. package/dist/contracts/job-store-contract.d.ts +24 -0
  83. package/dist/contracts/job-store-contract.d.ts.map +1 -0
  84. package/dist/contracts/job-store-contract.js +22 -0
  85. package/dist/contracts/job-store-contract.js.map +1 -0
  86. package/dist/contracts/process-store-contract.d.ts +28 -0
  87. package/dist/contracts/process-store-contract.d.ts.map +1 -0
  88. package/dist/contracts/process-store-contract.js +81 -0
  89. package/dist/contracts/process-store-contract.js.map +1 -0
  90. package/dist/contracts.d.ts +6 -0
  91. package/dist/contracts.d.ts.map +1 -1
  92. package/dist/contracts.js +4 -0
  93. package/dist/contracts.js.map +1 -1
  94. package/dist/event-sourcing/expect-snapshot-equivalence.d.ts +14 -0
  95. package/dist/event-sourcing/expect-snapshot-equivalence.d.ts.map +1 -0
  96. package/dist/event-sourcing/expect-snapshot-equivalence.js +48 -0
  97. package/dist/event-sourcing/expect-snapshot-equivalence.js.map +1 -0
  98. package/dist/event-sourcing/given-events.d.ts +12 -0
  99. package/dist/event-sourcing/given-events.d.ts.map +1 -0
  100. package/dist/event-sourcing/given-events.js +16 -0
  101. package/dist/event-sourcing/given-events.js.map +1 -0
  102. package/dist/event-sourcing/snapshot-equivalence.d.ts +12 -0
  103. package/dist/event-sourcing/snapshot-equivalence.d.ts.map +1 -0
  104. package/dist/event-sourcing/snapshot-equivalence.js +2 -0
  105. package/dist/event-sourcing/snapshot-equivalence.js.map +1 -0
  106. package/dist/index.d.ts +6 -0
  107. package/dist/index.d.ts.map +1 -1
  108. package/dist/index.js +4 -0
  109. package/dist/index.js.map +1 -1
  110. package/dist/processes/command-matcher.d.ts +8 -0
  111. package/dist/processes/command-matcher.d.ts.map +1 -0
  112. package/dist/processes/command-matcher.js +2 -0
  113. package/dist/processes/command-matcher.js.map +1 -0
  114. package/dist/processes/given-process.d.ts +18 -0
  115. package/dist/processes/given-process.d.ts.map +1 -0
  116. package/dist/processes/given-process.js +17 -0
  117. package/dist/processes/given-process.js.map +1 -0
  118. package/dist/processes/process-outcome.d.ts +20 -0
  119. package/dist/processes/process-outcome.d.ts.map +1 -0
  120. package/dist/processes/process-outcome.js +67 -0
  121. package/dist/processes/process-outcome.js.map +1 -0
  122. package/dist/scenarios/event-mismatches.d.ts.map +1 -1
  123. package/dist/scenarios/event-mismatches.js +1 -18
  124. package/dist/scenarios/event-mismatches.js.map +1 -1
  125. package/dist/scenarios/subset-differences.d.ts +3 -0
  126. package/dist/scenarios/subset-differences.d.ts.map +1 -0
  127. package/dist/scenarios/subset-differences.js +21 -0
  128. package/dist/scenarios/subset-differences.js.map +1 -0
  129. package/package.json +6 -5
@@ -0,0 +1,91 @@
1
+ import { CONCURRENCY_CONFLICT } from '@ddd-ts-kit/kernel';
2
+ import { ContractSuite } from '../contract-suite.js';
3
+ import { failureOf } from '../failure-of.js';
4
+ import { eventOf } from './event-of.js';
5
+ /** One stream: appended at the version it was read at, read back in order, refused when somebody appended first. */
6
+ export class StreamSuite extends ContractSuite {
7
+ tests() {
8
+ this.appendsAndReads();
9
+ this.readsWhatWasAppended();
10
+ this.refusesStaleVersions();
11
+ this.keepsStreamsApart();
12
+ }
13
+ appendsAndReads() {
14
+ this.it('creates a stream at version 0 and gives its events back in order, numbered from 1', async () => {
15
+ const appended = await this.subject.append('consignment-c1', 0, [eventOf('e1'), eventOf('e2')]);
16
+ const read = await this.subject.readStream('consignment-c1');
17
+ this.expect(appended.version).toBe(2);
18
+ this.expect(read.map(({ id, version, stream }) => ({ id, version, stream }))).toEqual([
19
+ { id: 'e1', version: 1, stream: 'consignment-c1' },
20
+ { id: 'e2', version: 2, stream: 'consignment-c1' },
21
+ ]);
22
+ });
23
+ this.it('gives back the type, the version of the shape, the data, the metadata and the instant as appended', async () => {
24
+ const data = {
25
+ label: 'é ✓',
26
+ weight: 12.5,
27
+ fragile: true,
28
+ none: null,
29
+ items: [{ sku: 'a' }],
30
+ nested: { depth: 2 },
31
+ };
32
+ await this.subject.append('consignment-c1', 0, [{ ...eventOf('e1', data), schemaVersion: 3 }]);
33
+ const [read] = await this.subject.readStream('consignment-c1');
34
+ this.expect(read?.type).toBe('shipping.consignment.weighed');
35
+ this.expect(read?.schemaVersion).toBe(3);
36
+ this.expect(read?.data).toEqual(data);
37
+ this.expect(read?.metadata).toEqual({ aggregateId: 'c1', correlationId: 'corr-e1' });
38
+ this.expect(read?.occurredAt.toISOString()).toBe('2026-03-01T10:00:00.000Z');
39
+ });
40
+ }
41
+ readsWhatWasAppended() {
42
+ this.it('appends to a stream at the version it has, and reads it after a version', async () => {
43
+ await this.subject.append('consignment-c1', 0, [eventOf('e1')]);
44
+ await this.subject.append('consignment-c1', 1, [eventOf('e2'), eventOf('e3')]);
45
+ const after = await this.subject.readStream('consignment-c1', 1);
46
+ this.expect(after.map(({ id, version }) => `${id}@${version}`)).toEqual(['e2@2', 'e3@3']);
47
+ });
48
+ this.it('appends nothing at the version the stream has, and refuses nothing at another one', async () => {
49
+ await this.subject.append('consignment-c1', 0, [eventOf('e1')]);
50
+ const unchanged = await this.subject.append('consignment-c1', 1, []);
51
+ const refused = await failureOf(() => this.subject.append('consignment-c1', 0, []));
52
+ this.expect(unchanged).toEqual({ version: 1, position: 1 });
53
+ this.expect(codeOf(refused)).toBe(CONCURRENCY_CONFLICT);
54
+ });
55
+ this.it('reads an unknown stream as empty', async () => {
56
+ this.expect(await this.subject.readStream('consignment-unknown')).toEqual([]);
57
+ });
58
+ }
59
+ refusesStaleVersions() {
60
+ this.it('refuses with a concurrency conflict an append at a version the stream no longer has, and keeps none of it', async () => {
61
+ await this.subject.append('consignment-c1', 0, [eventOf('e1')]);
62
+ const behind = await failureOf(() => this.subject.append('consignment-c1', 0, [eventOf('e2')]));
63
+ const ahead = await failureOf(() => this.subject.append('consignment-c1', 2, [eventOf('e3')]));
64
+ this.expect([codeOf(behind), codeOf(ahead)]).toEqual([CONCURRENCY_CONFLICT, CONCURRENCY_CONFLICT]);
65
+ this.expect((await this.subject.readStream('consignment-c1')).map(({ id }) => id)).toEqual(['e1']);
66
+ });
67
+ this.it('lets one of two appends at the same version through, and refuses the other', async () => {
68
+ await this.subject.append('consignment-c1', 0, [eventOf('e1')]);
69
+ const outcomes = await Promise.all([
70
+ failureOf(() => this.subject.append('consignment-c1', 1, [eventOf('left')])),
71
+ failureOf(() => this.subject.append('consignment-c1', 1, [eventOf('right')])),
72
+ ]);
73
+ this.expect(outcomes.filter((failure) => failure === undefined).length).toBe(1);
74
+ this.expect(outcomes.map(codeOf).filter((code) => code === CONCURRENCY_CONFLICT).length).toBe(1);
75
+ this.expect((await this.subject.readStream('consignment-c1')).length).toBe(2);
76
+ });
77
+ }
78
+ keepsStreamsApart() {
79
+ this.it('numbers each stream on its own', async () => {
80
+ await this.subject.append('consignment-c1', 0, [eventOf('a1'), eventOf('a2')]);
81
+ await this.subject.append('consignment-c2', 0, [eventOf('b1')]);
82
+ const second = await this.subject.readStream('consignment-c2');
83
+ this.expect(second.map(({ id, version }) => `${id}@${version}`)).toEqual(['b1@1']);
84
+ this.expect((await this.subject.readStream('consignment-c1')).length).toBe(2);
85
+ });
86
+ }
87
+ }
88
+ function codeOf(failure) {
89
+ return typeof failure === 'object' && failure !== null && 'code' in failure ? failure.code : undefined;
90
+ }
91
+ //# sourceMappingURL=stream-suite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream-suite.js","sourceRoot":"","sources":["../../../src/contracts/event-store/stream-suite.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import type { EventSourcingStores, EventStore } from '@ddd-ts-kit/kernel';
2
+ import type { Awaitable } from './awaitable.js';
3
+ import type { ContractRunner } from './contract-runner.js';
4
+ export interface EventStoreContractOptions {
5
+ readonly name: string;
6
+ /** A fresh, empty store; called before each test. */
7
+ createStore(): Awaitable<EventStore>;
8
+ /** The snapshot, checkpoint and key stores that come with it, fresh and empty; their suite runs when given. */
9
+ readonly createCompanions?: () => Awaitable<Pick<EventSourcingStores, 'snapshots' | 'checkpoints' | 'subjectKeys'>>;
10
+ }
11
+ /**
12
+ * What every adapter of the kernel `EventStore` port guarantees, the in-memory one included:
13
+ * an append at the version the stream still has, a concurrency conflict otherwise and nothing
14
+ * kept of it, each stream in its order, the whole store in the order of the appends with a
15
+ * position, and a read from a position. With `createCompanions`, the stores that come with it:
16
+ * snapshots never replaced by an older one, checkpoints moved from where they stand, keys of data
17
+ * subjects destroyed by an erasure.
18
+ */
19
+ export declare function eventStoreContract(runner: ContractRunner, options: EventStoreContractOptions): void;
20
+ //# sourceMappingURL=event-store-contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-store-contract.d.ts","sourceRoot":"","sources":["../../src/contracts/event-store-contract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAK3D,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,WAAW,IAAI,SAAS,CAAC,UAAU,CAAC,CAAC;IACrC,+GAA+G;IAC/G,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,SAAS,CAAC,IAAI,CAAC,mBAAmB,EAAE,WAAW,GAAG,aAAa,GAAG,aAAa,CAAC,CAAC,CAAC;CACrH;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,yBAAyB,GAAG,IAAI,CAanG"}
@@ -0,0 +1,21 @@
1
+ import { CompanionStoresSuite } from './event-store/companion-stores-suite.js';
2
+ import { StoreSuite } from './event-store/store-suite.js';
3
+ import { StreamSuite } from './event-store/stream-suite.js';
4
+ /**
5
+ * What every adapter of the kernel `EventStore` port guarantees, the in-memory one included:
6
+ * an append at the version the stream still has, a concurrency conflict otherwise and nothing
7
+ * kept of it, each stream in its order, the whole store in the order of the appends with a
8
+ * position, and a read from a position. With `createCompanions`, the stores that come with it:
9
+ * snapshots never replaced by an older one, checkpoints moved from where they stand, keys of data
10
+ * subjects destroyed by an erasure.
11
+ */
12
+ export function eventStoreContract(runner, options) {
13
+ const create = () => options.createStore();
14
+ new StreamSuite(runner, create).describe(`${options.name} (event store contract: streams)`);
15
+ new StoreSuite(runner, create).describe(`${options.name} (event store contract: the whole store)`);
16
+ const { createCompanions } = options;
17
+ if (createCompanions !== undefined) {
18
+ new CompanionStoresSuite(runner, () => createCompanions()).describe(`${options.name} (event store contract: snapshots, checkpoints, keys)`);
19
+ }
20
+ }
21
+ //# sourceMappingURL=event-store-contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-store-contract.js","sourceRoot":"","sources":["../../src/contracts/event-store-contract.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,oBAAoB,EAAE,MAAM,yCAAyC,CAAC;AAC/E,OAAO,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAU5D;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAsB,EAAE,OAAkC;IAC3F,MAAM,MAAM,GAAG,GAA0B,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;IAElE,IAAI,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,IAAI,kCAAkC,CAAC,CAAC;IAC5F,IAAI,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,IAAI,0CAA0C,CAAC,CAAC;IAEnG,MAAM,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC;IAErC,IAAI,gBAAgB,KAAK,SAAS,EAAE,CAAC;QACnC,IAAI,oBAAoB,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,gBAAgB,EAAE,CAAC,CAAC,QAAQ,CACjE,GAAG,OAAO,CAAC,IAAI,uDAAuD,CACvE,CAAC;IACJ,CAAC;AACH,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { Awaitable } from './awaitable.js';
2
+ import type { ContractRunner } from './contract-runner.js';
3
+ import type { EventStoreUnderContract } from './event-store/event-store-under-contract.js';
4
+ export interface EventStoreUnitOfWorkContractOptions {
5
+ readonly name: string;
6
+ /** An empty event store and an empty outbox of one database, bound to one unit of work; called before each test. */
7
+ createSubject(): Awaitable<EventStoreUnderContract>;
8
+ }
9
+ /**
10
+ * No double write for an event-sourced aggregate: what its save appends to its stream and what the
11
+ * publisher of its events writes to the outbox commit in one transaction, so nothing is published
12
+ * for an append that was rolled back or refused. Held by every transactional event store.
13
+ */
14
+ export declare function eventStoreUnitOfWorkContract(runner: ContractRunner, options: EventStoreUnitOfWorkContractOptions): void;
15
+ //# sourceMappingURL=event-store-unit-of-work-contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-store-unit-of-work-contract.d.ts","sourceRoot":"","sources":["../../src/contracts/event-store-unit-of-work-contract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,6CAA6C,CAAC;AAG3F,MAAM,WAAW,mCAAmC;IAClD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oHAAoH;IACpH,aAAa,IAAI,SAAS,CAAC,uBAAuB,CAAC,CAAC;CACrD;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,mCAAmC,GAC3C,IAAI,CAIN"}
@@ -0,0 +1,10 @@
1
+ import { OneTransactionSuite } from './event-store/one-transaction-suite.js';
2
+ /**
3
+ * No double write for an event-sourced aggregate: what its save appends to its stream and what the
4
+ * publisher of its events writes to the outbox commit in one transaction, so nothing is published
5
+ * for an append that was rolled back or refused. Held by every transactional event store.
6
+ */
7
+ export function eventStoreUnitOfWorkContract(runner, options) {
8
+ new OneTransactionSuite(runner, () => options.createSubject()).describe(`${options.name} (event store contract: one transaction with the outbox)`);
9
+ }
10
+ //# sourceMappingURL=event-store-unit-of-work-contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-store-unit-of-work-contract.js","sourceRoot":"","sources":["../../src/contracts/event-store-unit-of-work-contract.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAQ7E;;;;GAIG;AACH,MAAM,UAAU,4BAA4B,CAC1C,MAAsB,EACtB,OAA4C;IAE5C,IAAI,mBAAmB,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC,QAAQ,CACrE,GAAG,OAAO,CAAC,IAAI,0DAA0D,CAC1E,CAAC;AACJ,CAAC"}
@@ -0,0 +1,24 @@
1
+ import type { EnqueuedBy } from '@ddd-ts-kit/kernel';
2
+ export interface JobOverrides {
3
+ readonly name?: string;
4
+ readonly queue?: string;
5
+ readonly runAt?: Date;
6
+ readonly idempotencyKey?: string;
7
+ readonly payload?: unknown;
8
+ /** The tenant of the work that enqueues it: `tenant-3` by default, `null` for none. */
9
+ readonly tenantId?: string | null;
10
+ readonly subject?: string;
11
+ }
12
+ /** A job of the contract: `billing.compile-statements`, due at `NOW` unless told otherwise. */
13
+ export declare function aJob(id: string, overrides?: JobOverrides): {
14
+ id: string;
15
+ name: string;
16
+ queue: string;
17
+ payload: {};
18
+ enqueuedBy: EnqueuedBy;
19
+ enqueuedAt: Date;
20
+ runAt: Date;
21
+ idempotencyKey?: string;
22
+ subject?: string;
23
+ };
24
+ //# sourceMappingURL=a-job.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a-job.d.ts","sourceRoot":"","sources":["../../../src/contracts/job-store/a-job.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAOrD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC;IACtB,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,uFAAuF;IACvF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,+FAA+F;AAC/F,wBAAgB,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,GAAE,YAAiB;;;;;;;;;;EAc5D"}
@@ -0,0 +1,28 @@
1
+ import { NOW, QUEUE } from './job-fixtures.js';
2
+ const TENANT = 'tenant-3';
3
+ const TRACEPARENT = '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01';
4
+ /** A job of the contract: `billing.compile-statements`, due at `NOW` unless told otherwise. */
5
+ export function aJob(id, overrides = {}) {
6
+ const { name = 'billing.compile-statements', queue = QUEUE, runAt = NOW, idempotencyKey, subject } = overrides;
7
+ return {
8
+ id,
9
+ name,
10
+ queue,
11
+ payload: overrides.payload ?? { month: '2026-02', customers: ['c-1', 'c-2'], draft: false, total: 12.5 },
12
+ enqueuedBy: enqueuedBy(overrides.tenantId === undefined ? TENANT : overrides.tenantId),
13
+ enqueuedAt: NOW,
14
+ runAt,
15
+ ...(idempotencyKey === undefined ? {} : { idempotencyKey }),
16
+ ...(subject === undefined ? {} : { subject }),
17
+ };
18
+ }
19
+ function enqueuedBy(tenantId) {
20
+ const tenant = tenantId === null ? {} : { tenantId };
21
+ return {
22
+ correlationId: 'corr-1',
23
+ actor: { id: 'user-7', type: 'user', ...tenant, roles: ['accountant'], attributes: {} },
24
+ ...tenant,
25
+ traceparent: TRACEPARENT,
26
+ };
27
+ }
28
+ //# sourceMappingURL=a-job.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a-job.js","sourceRoot":"","sources":["../../../src/contracts/job-store/a-job.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE/C,MAAM,MAAM,GAAG,UAAU,CAAC;AAE1B,MAAM,WAAW,GAAG,yDAAyD,CAAC;AAa9E,+FAA+F;AAC/F,MAAM,UAAU,IAAI,CAAC,EAAU,EAAE,SAAS,GAAiB,EAAE;IAC3D,MAAM,EAAE,IAAI,GAAG,4BAA4B,EAAE,KAAK,GAAG,KAAK,EAAE,KAAK,GAAG,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,GAAG,SAAS,CAAC;IAE/G,OAAO;QACL,EAAE;QACF,IAAI;QACJ,KAAK;QACL,OAAO,EAAE,SAAS,CAAC,OAAO,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE;QACxG,UAAU,EAAE,UAAU,CAAC,SAAS,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC;QACtF,UAAU,EAAE,GAAG;QACf,KAAK;QACL,GAAG,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC;QAC3D,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC;KAC9C,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,QAAuB;IACzC,MAAM,MAAM,GAAG,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC;IAErD,OAAO;QACL,aAAa,EAAE,QAAQ;QACvB,KAAK,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,KAAK,EAAE,CAAC,YAAY,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE;QACvF,GAAG,MAAM;QACT,WAAW,EAAE,WAAW;KACzB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,15 @@
1
+ import { ContractSuite } from '../contract-suite.js';
2
+ import type { JobStoreUnderContract } from './job-store-under-contract.js';
3
+ /** What `add` keeps and what `claim` hands out: due jobs of the queue, earliest first, each to one claim. */
4
+ export declare class ClaimSuite extends ContractSuite<JobStoreUnderContract> {
5
+ protected tests(): void;
6
+ private handsOutWhatWasAdded;
7
+ private respectsTheInstant;
8
+ private dropsDuplicates;
9
+ private keepsTheKeysOfTenantsApart;
10
+ private leasesWhatItHandsOut;
11
+ private addMany;
12
+ private claim;
13
+ private claimedIds;
14
+ }
15
+ //# sourceMappingURL=claim-suite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claim-suite.d.ts","sourceRoot":"","sources":["../../../src/contracts/job-store/claim-suite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAIrD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAE3E,6GAA6G;AAC7G,qBAAa,UAAW,SAAQ,aAAa,CAAC,qBAAqB,CAAC;IAClE,SAAS,CAAC,KAAK,IAAI,IAAI,CAMtB;IAED,OAAO,CAAC,oBAAoB;IAsC5B,OAAO,CAAC,kBAAkB;IAgB1B,OAAO,CAAC,eAAe;IAgBvB,OAAO,CAAC,0BAA0B;IA+BlC,OAAO,CAAC,oBAAoB;YA+Bd,OAAO;IAMrB,OAAO,CAAC,KAAK;YAIC,UAAU;CAGzB"}
@@ -0,0 +1,126 @@
1
+ import { ContractSuite } from '../contract-suite.js';
2
+ import { aJob } from './a-job.js';
3
+ import { LEASE_MS, NOW, QUEUE } from './job-fixtures.js';
4
+ import { later } from './later.js';
5
+ /** What `add` keeps and what `claim` hands out: due jobs of the queue, earliest first, each to one claim. */
6
+ export class ClaimSuite extends ContractSuite {
7
+ tests() {
8
+ this.handsOutWhatWasAdded();
9
+ this.respectsTheInstant();
10
+ this.dropsDuplicates();
11
+ this.keepsTheKeysOfTenantsApart();
12
+ this.leasesWhatItHandsOut();
13
+ }
14
+ handsOutWhatWasAdded() {
15
+ this.it('starts empty', async () => {
16
+ this.expect(await this.subject.stats(QUEUE)).toEqual({ waiting: 0, running: 0, dead: 0 });
17
+ this.expect(await this.claimedIds(10, NOW)).toEqual([]);
18
+ this.expect(await this.subject.find('unknown')).toBe(undefined);
19
+ });
20
+ this.it('hands out the job exactly as added, the attempt counted, and shows it running', async () => {
21
+ const added = await this.subject.add(aJob('job-1'));
22
+ const [claimed] = await this.claim(10, NOW);
23
+ const { enqueuedBy, payload, name } = aJob('job-1');
24
+ this.expect(added).toEqual({ id: 'job-1', duplicate: false });
25
+ this.expect(claimed).toEqual({
26
+ id: 'job-1',
27
+ name,
28
+ queue: QUEUE,
29
+ payload,
30
+ enqueuedBy,
31
+ enqueuedAt: NOW,
32
+ attempt: 1,
33
+ });
34
+ this.expect((await this.subject.find('job-1'))?.status).toBe('running');
35
+ this.expect(await this.subject.stats(QUEUE)).toEqual({ waiting: 0, running: 1, dead: 0 });
36
+ });
37
+ this.it('hands out the earliest first and only the jobs of the queue asked', async () => {
38
+ await this.subject.add(aJob('job-1', { runAt: later(-1_000) }));
39
+ await this.subject.add(aJob('job-2', { runAt: later(-3_000) }));
40
+ await this.subject.add(aJob('job-3', { name: 'audit.verify-chain', queue: 'audit' }));
41
+ await this.subject.add(aJob('job-4', { runAt: later(-1_000) }));
42
+ this.expect(await this.claimedIds(10, NOW)).toEqual(['job-2', 'job-1', 'job-4']);
43
+ });
44
+ }
45
+ respectsTheInstant() {
46
+ this.it('keeps a job due later until its instant', async () => {
47
+ await this.subject.add(aJob('job-1', { runAt: later(60_000) }));
48
+ this.expect(await this.claimedIds(10, later(59_999))).toEqual([]);
49
+ this.expect(await this.claimedIds(10, later(60_000))).toEqual(['job-1']);
50
+ });
51
+ this.it('honours the limit and leaves the rest to the next claim', async () => {
52
+ await this.addMany(['job-1', 'job-2', 'job-3']);
53
+ this.expect(await this.claimedIds(2, NOW)).toEqual(['job-1', 'job-2']);
54
+ this.expect(await this.claimedIds(2, NOW)).toEqual(['job-3']);
55
+ });
56
+ }
57
+ dropsDuplicates() {
58
+ this.it('answers the job already kept for the same name and idempotency key, and adds nothing', async () => {
59
+ await this.subject.add(aJob('job-1', { idempotencyKey: 'feb' }));
60
+ const again = await this.subject.add(aJob('job-2', { idempotencyKey: 'feb' }));
61
+ const otherName = await this.subject.add(aJob('job-3', { idempotencyKey: 'feb', name: 'billing.send-statements' }));
62
+ this.expect(again).toEqual({ id: 'job-1', duplicate: true });
63
+ this.expect(otherName).toEqual({ id: 'job-3', duplicate: false });
64
+ this.expect(await this.subject.find('job-2')).toBe(undefined);
65
+ });
66
+ }
67
+ keepsTheKeysOfTenantsApart() {
68
+ this.it('keeps the idempotency keys of two tenants, and of work without a tenant, apart', async () => {
69
+ await this.subject.add(aJob('job-1', { idempotencyKey: 'feb' }));
70
+ const otherTenant = await this.subject.add(aJob('job-2', { idempotencyKey: 'feb', tenantId: 'tenant-4' }));
71
+ const noTenant = await this.subject.add(aJob('job-3', { idempotencyKey: 'feb', tenantId: null }));
72
+ const noTenantAgain = await this.subject.add(aJob('job-4', { idempotencyKey: 'feb', tenantId: null }));
73
+ this.expect([otherTenant, noTenant, noTenantAgain]).toEqual([
74
+ { id: 'job-2', duplicate: false },
75
+ { id: 'job-3', duplicate: false },
76
+ { id: 'job-3', duplicate: true },
77
+ ]);
78
+ });
79
+ this.it('shows the tenant and the subject of a job, never its payload', async () => {
80
+ await this.subject.add(aJob('job-1', { subject: 'customer-9' }));
81
+ await this.subject.add(aJob('job-2', { tenantId: null }));
82
+ const [first, second] = [await this.subject.find('job-1'), await this.subject.find('job-2')];
83
+ this.expect([first?.tenantId, first?.subject, first !== undefined && 'payload' in first]).toEqual([
84
+ 'tenant-3',
85
+ 'customer-9',
86
+ false,
87
+ ]);
88
+ this.expect([second?.tenantId, second?.subject]).toEqual([undefined, undefined]);
89
+ });
90
+ }
91
+ leasesWhatItHandsOut() {
92
+ this.it('hides a claimed job until its lease ends, then hands it out again, one attempt more', async () => {
93
+ await this.subject.add(aJob('job-1'));
94
+ await this.claim(10, NOW);
95
+ this.expect(await this.claimedIds(10, later(LEASE_MS - 1))).toEqual([]);
96
+ const [again] = await this.claim(10, later(LEASE_MS));
97
+ this.expect(again?.attempt).toBe(2);
98
+ });
99
+ this.it('keeps a job hidden while its heartbeats extend the lease', async () => {
100
+ await this.subject.add(aJob('job-1'));
101
+ await this.claim(10, NOW);
102
+ this.expect(await this.subject.heartbeat({ id: 'job-1', attempt: 1 }, later(LEASE_MS * 2))).toBe(true);
103
+ this.expect(await this.claimedIds(10, later(LEASE_MS))).toEqual([]);
104
+ this.expect(await this.subject.heartbeat({ id: 'job-1', attempt: 2 }, later(LEASE_MS * 3))).toBe(false);
105
+ });
106
+ this.it('never hands the same job to two concurrent claims', async () => {
107
+ await this.addMany(['job-1', 'job-2', 'job-3', 'job-4', 'job-5', 'job-6']);
108
+ const batches = await Promise.all([this.claimedIds(4, NOW), this.claimedIds(4, NOW)]);
109
+ const ids = batches.flat();
110
+ this.expect(ids.length).toBe(6);
111
+ this.expect(new Set(ids).size).toBe(6);
112
+ });
113
+ }
114
+ async addMany(ids) {
115
+ for (const id of ids) {
116
+ await this.subject.add(aJob(id));
117
+ }
118
+ }
119
+ claim(limit, now) {
120
+ return this.subject.claim({ queue: QUEUE, limit, now, leaseMs: LEASE_MS });
121
+ }
122
+ async claimedIds(limit, now) {
123
+ return (await this.claim(limit, now)).map((job) => job.id);
124
+ }
125
+ }
126
+ //# sourceMappingURL=claim-suite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claim-suite.js","sourceRoot":"","sources":["../../../src/contracts/job-store/claim-suite.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /** The instants and the queue every job of the contract uses. */
2
+ export declare const NOW: Date;
3
+ export declare const LEASE_MS = 30000;
4
+ export declare const QUEUE = "billing";
5
+ //# sourceMappingURL=job-fixtures.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-fixtures.d.ts","sourceRoot":"","sources":["../../../src/contracts/job-store/job-fixtures.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,eAAO,MAAM,GAAG,MAAuC,CAAC;AAExD,eAAO,MAAM,QAAQ,QAAS,CAAC;AAE/B,eAAO,MAAM,KAAK,YAAY,CAAC"}
@@ -0,0 +1,5 @@
1
+ /** The instants and the queue every job of the contract uses. */
2
+ export const NOW = new Date('2026-03-01T10:00:00.000Z');
3
+ export const LEASE_MS = 30_000;
4
+ export const QUEUE = 'billing';
5
+ //# sourceMappingURL=job-fixtures.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-fixtures.js","sourceRoot":"","sources":["../../../src/contracts/job-store/job-fixtures.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,MAAM,CAAC,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,0BAA0B,CAAC,CAAC;AAExD,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC;AAE/B,MAAM,CAAC,MAAM,KAAK,GAAG,SAAS,CAAC"}
@@ -0,0 +1,61 @@
1
+ import type { EnqueuedBy, EnqueuedJob, JobProgress, QueuedJob } from '@ddd-ts-kit/kernel';
2
+ interface Lease {
3
+ readonly id: string;
4
+ readonly attempt: number;
5
+ }
6
+ interface Claimed {
7
+ readonly id: string;
8
+ readonly name: string;
9
+ readonly queue: string;
10
+ readonly payload: unknown;
11
+ readonly enqueuedBy: EnqueuedBy;
12
+ readonly enqueuedAt: Date;
13
+ readonly attempt: number;
14
+ readonly checkpoint?: unknown;
15
+ }
16
+ /**
17
+ * The job store port of `@ddd-ts-kit/scheduling`, restated structurally so that this package
18
+ * does not depend on it. Any `JobStore` fits.
19
+ */
20
+ export interface JobStoreUnderContract {
21
+ add(job: {
22
+ readonly id: string;
23
+ readonly name: string;
24
+ readonly queue: string;
25
+ readonly payload: unknown;
26
+ readonly enqueuedBy: EnqueuedBy;
27
+ readonly enqueuedAt: Date;
28
+ readonly runAt: Date;
29
+ readonly idempotencyKey?: string;
30
+ readonly subject?: string;
31
+ }): Promise<EnqueuedJob>;
32
+ claim(request: {
33
+ queue: string;
34
+ limit: number;
35
+ now: Date;
36
+ leaseMs: number;
37
+ }): Promise<Claimed[]>;
38
+ heartbeat(lease: Lease, leaseUntil: Date): Promise<boolean>;
39
+ update(lease: Lease, update: {
40
+ progress?: JobProgress;
41
+ checkpoint?: unknown;
42
+ }, leaseUntil: Date): Promise<boolean>;
43
+ complete(lease: Lease, at: Date): Promise<boolean>;
44
+ fail(lease: Lease, failure: {
45
+ errorCode: string;
46
+ retryAt: Date | null;
47
+ at: Date;
48
+ }): Promise<boolean>;
49
+ release(lease: Lease, runAt: Date): Promise<boolean>;
50
+ cancel(id: string, at: Date): Promise<boolean>;
51
+ find(id: string): Promise<QueuedJob | undefined>;
52
+ retryDead(ids: readonly string[], now: Date): Promise<number>;
53
+ purgeFinished(before: Date): Promise<number>;
54
+ stats(queue: string): Promise<{
55
+ readonly waiting: number;
56
+ readonly running: number;
57
+ readonly dead: number;
58
+ }>;
59
+ }
60
+ export {};
61
+ //# sourceMappingURL=job-store-under-contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-store-under-contract.d.ts","sourceRoot":"","sources":["../../../src/contracts/job-store/job-store-under-contract.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=job-store-under-contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-store-under-contract.js","sourceRoot":"","sources":["../../../src/contracts/job-store/job-store-under-contract.ts"],"names":[],"mappings":""}
@@ -0,0 +1,11 @@
1
+ import { ContractSuite } from '../contract-suite.js';
2
+ import type { JobStoreUnderContract } from './job-store-under-contract.js';
3
+ /** Cancellation and housekeeping. */
4
+ export declare class KeepSuite extends ContractSuite<JobStoreUnderContract> {
5
+ protected tests(): void;
6
+ private cancels;
7
+ private purges;
8
+ private finished;
9
+ private claim;
10
+ }
11
+ //# sourceMappingURL=keep-suite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keep-suite.d.ts","sourceRoot":"","sources":["../../../src/contracts/job-store/keep-suite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAIrD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAE3E,qCAAqC;AACrC,qBAAa,SAAU,SAAQ,aAAa,CAAC,qBAAqB,CAAC;IACjE,SAAS,CAAC,KAAK,IAAI,IAAI,CAGtB;IAED,OAAO,CAAC,OAAO;IA+Bf,OAAO,CAAC,MAAM;YAcA,QAAQ;IAMtB,OAAO,CAAC,KAAK;CAGd"}
@@ -0,0 +1,55 @@
1
+ import { ContractSuite } from '../contract-suite.js';
2
+ import { aJob } from './a-job.js';
3
+ import { LEASE_MS, NOW, QUEUE } from './job-fixtures.js';
4
+ import { later } from './later.js';
5
+ /** Cancellation and housekeeping. */
6
+ export class KeepSuite extends ContractSuite {
7
+ tests() {
8
+ this.cancels();
9
+ this.purges();
10
+ }
11
+ cancels() {
12
+ this.it('cancels a waiting job: it is never handed out', async () => {
13
+ await this.subject.add(aJob('job-1', { runAt: later(60_000) }));
14
+ this.expect(await this.subject.cancel('job-1', NOW)).toBe(true);
15
+ const job = await this.subject.find('job-1');
16
+ this.expect([job?.status, job?.finishedAt]).toEqual(['cancelled', NOW]);
17
+ this.expect(await this.claim(later(60_000))).toEqual([]);
18
+ });
19
+ this.it('cancels a running job: its next report is refused', async () => {
20
+ await this.subject.add(aJob('job-1'));
21
+ await this.claim(NOW);
22
+ this.expect(await this.subject.cancel('job-1', NOW)).toBe(true);
23
+ this.expect(await this.subject.heartbeat({ id: 'job-1', attempt: 1 }, later(LEASE_MS))).toBe(false);
24
+ this.expect(await this.subject.complete({ id: 'job-1', attempt: 1 }, NOW)).toBe(false);
25
+ });
26
+ this.it('cannot cancel what is over or unknown', async () => {
27
+ await this.subject.add(aJob('job-1'));
28
+ await this.claim(NOW);
29
+ await this.subject.complete({ id: 'job-1', attempt: 1 }, NOW);
30
+ this.expect(await this.subject.cancel('job-1', NOW)).toBe(false);
31
+ this.expect(await this.subject.cancel('unknown', NOW)).toBe(false);
32
+ });
33
+ }
34
+ purges() {
35
+ this.it('purges what succeeded or was cancelled before the instant, and keeps the dead', async () => {
36
+ await this.finished('job-1', (lease) => this.subject.complete(lease, NOW));
37
+ await this.finished('job-2', (lease) => this.subject.fail(lease, { errorCode: 'X', retryAt: null, at: NOW }));
38
+ await this.subject.add(aJob('job-3', { runAt: later(60_000) }));
39
+ await this.subject.cancel('job-3', later(1_000));
40
+ this.expect(await this.subject.purgeFinished(NOW)).toBe(0);
41
+ this.expect(await this.subject.purgeFinished(later(1_001))).toBe(2);
42
+ this.expect(await this.subject.find('job-1')).toBe(undefined);
43
+ this.expect((await this.subject.find('job-2'))?.status).toBe('dead');
44
+ });
45
+ }
46
+ async finished(id, finish) {
47
+ await this.subject.add(aJob(id));
48
+ await this.claim(NOW);
49
+ await finish({ id, attempt: 1 });
50
+ }
51
+ claim(now) {
52
+ return this.subject.claim({ queue: QUEUE, limit: 10, now, leaseMs: LEASE_MS });
53
+ }
54
+ }
55
+ //# sourceMappingURL=keep-suite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keep-suite.js","sourceRoot":"","sources":["../../../src/contracts/job-store/keep-suite.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ /** That many milliseconds after `NOW`. */
2
+ export declare function later(ms: number): Date;
3
+ //# sourceMappingURL=later.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"later.d.ts","sourceRoot":"","sources":["../../../src/contracts/job-store/later.ts"],"names":[],"mappings":"AAEA,0CAA0C;AAC1C,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAEtC"}