@starci/jest-preset 2.0.0 → 2.1.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/CHANGELOG.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # Changelog
2
2
 
3
- ## 2.0.0 - unreleased
3
+ ## 2.1.0 - 2026-09-30
4
+
5
+ - Added: the claim side of `recordingOutbox()`, so a claim-side consumer is specced with the kit double the `OUTBOX` token requires (`spec-infra-double-from-kit`) instead of `mock<Outbox>()`. `queueRecords(...records)` fills a backlog that `claimDue(params)` hands out in order (only the asked queues, at most `limit`, removed once claimed; an empty backlog is an empty claim, a record queued twice is a duplicate delivery), `failNext(operation, error)` makes the next `enqueue`, `claimDue`, `complete`, `retry` or `bury` reject once, and `claims`, `completed`, `retried`, `buried` and `backlog` expose what happened. `RecordingOutbox<M, R>` takes the claimed record type as a second parameter; `ClaimedRecord`, `ClaimParams`, `RetryParams`, `BuryParams` and `OutboxOperation` are exported from `./outbox`. `clear()` resets the claim side too. No breaking change.
6
+
7
+ ## 2.0.0 - 2026-09-30
4
8
 
5
9
  - Breaking: `starciJestConfig()` takes no options. The repository `jest.config.js` is a managed file, exactly `module.exports = require("@starci/jest-preset").starciJestConfig()`, rendered by `hfs sync` and compared by `hfs check` (HFS_MANAGED_FILE_DRIFT), so `moduleNameMapper`, `roots`, `tsconfig`, `rootDir`, `unit` and `e2e` overrides are gone. `StarciJestOptions` is removed.
6
10
  - Added: the three path aliases (`@features/*`, `@modules/*`, `@tests/*`) are part of the preset (`MODULE_NAME_MAPPER`), matching the aliases the managed `tsconfig.json` declares.
package/README.md CHANGED
@@ -114,6 +114,13 @@ outbox.messages // what the store would hold: the first write of each (queue
114
114
  outbox.writes // every message written, duplicates included
115
115
  outbox.entries // every write with its manager and `inTransaction`
116
116
  outbox.allInTransaction; outbox.messagesOf("queue"); outbox.clear()
117
+
118
+ // the claim side: script what a worker's claim returns
119
+ outbox.queueRecords(record) // the backlog claimDue hands out, in order, filtered by queue and cut at limit
120
+ outbox.queueRecords(record, record) // a duplicate delivery
121
+ await outbox.claimDue(params) // [] when the backlog is empty
122
+ outbox.failNext("complete", new Error("db down")) // the next call of that operation rejects once
123
+ outbox.claims; outbox.completed; outbox.retried; outbox.buried; outbox.backlog
117
124
  ```
118
125
 
119
126
  ## `builder`, `fakeIds` and the Outcome matchers
package/outbox.cjs CHANGED
@@ -7,13 +7,32 @@
7
7
  * would hold: one message per (queue, eventId), like the real unique key, so a duplicate write keeps one. `writes` is every
8
8
  * call, duplicates included, and `entries` adds `inTransaction`: whether the manager was the view a `fakeTransaction` body
9
9
  * received, so a spec proves a message is written in the transaction of the change that caused it.
10
+ *
11
+ * The claim side scripts what a worker's claim returns. `queueRecords(...records)` fills the backlog; `claimDue(params)` hands
12
+ * out, in order, up to `params.limit` backlog records whose queue is in `params.queues` and removes them, so an empty backlog is
13
+ * an empty claim and queueing the same record twice is a duplicate delivery. `claims` keeps every claim's params, `completed`,
14
+ * `retried` and `buried` keep what the worker reported, and `failNext(operation, error)` makes the next call of that operation
15
+ * reject once, so a spec drives the failure path.
10
16
  */
11
17
  const { isTransactionManager } = require("./entity-manager.cjs")
12
18
 
13
19
  function recordingOutbox() {
14
20
  const written = []
21
+ const backlog = []
22
+ const claims = []
23
+ const completed = []
24
+ const retried = []
25
+ const buried = []
26
+ const failures = new Map()
27
+ const failIfScripted = (operation) => {
28
+ if (!failures.has(operation)) return
29
+ const error = failures.get(operation)
30
+ failures.delete(operation)
31
+ throw error
32
+ }
15
33
  return {
16
34
  async enqueue(manager, message) {
35
+ failIfScripted("enqueue")
17
36
  written.push({ message, manager, inTransaction: isTransactionManager(manager) })
18
37
  },
19
38
  /** The messages the store would hold: the first write of each (queue, eventId). */
@@ -44,8 +63,61 @@ function recordingOutbox() {
44
63
  messagesOf(queue) {
45
64
  return this.messages.filter((message) => message.queue === queue)
46
65
  },
66
+ /** Adds records to the backlog `claimDue` hands out. */
67
+ queueRecords(...records) {
68
+ backlog.push(...records)
69
+ },
70
+ async claimDue(params) {
71
+ claims.push(params)
72
+ failIfScripted("claimDue")
73
+ const claimed = []
74
+ for (const record of [...backlog]) {
75
+ if (claimed.length >= params.limit) break
76
+ if (!params.queues.includes(record.queue)) continue
77
+ claimed.push(record)
78
+ backlog.splice(backlog.indexOf(record), 1)
79
+ }
80
+ return claimed
81
+ },
82
+ async complete(id) {
83
+ failIfScripted("complete")
84
+ completed.push(id)
85
+ },
86
+ async retry(params) {
87
+ failIfScripted("retry")
88
+ retried.push(params)
89
+ },
90
+ async bury(params) {
91
+ failIfScripted("bury")
92
+ buried.push(params)
93
+ },
94
+ /** Makes the next call of `operation` reject with `error`, once. */
95
+ failNext(operation, error) {
96
+ failures.set(operation, error)
97
+ },
98
+ /** The params of every `claimDue` call, in order. */
99
+ get claims() {
100
+ return [...claims]
101
+ },
102
+ /** The ids passed to `complete`, in order. */
103
+ get completed() {
104
+ return [...completed]
105
+ },
106
+ /** The params of every `retry`, in order. */
107
+ get retried() {
108
+ return [...retried]
109
+ },
110
+ /** The params of every `bury`, in order. */
111
+ get buried() {
112
+ return [...buried]
113
+ },
114
+ /** The records still waiting for a claim. */
115
+ get backlog() {
116
+ return [...backlog]
117
+ },
47
118
  clear() {
48
- written.length = 0
119
+ for (const list of [written, backlog, claims, completed, retried, buried]) list.length = 0
120
+ failures.clear()
49
121
  },
50
122
  }
51
123
  }
package/outbox.d.ts CHANGED
@@ -12,9 +12,37 @@ export interface OutboxWrite<M extends RecordedMessage = RecordedMessage> {
12
12
  /** Whether the manager was the view a `fakeTransaction` body received. */
13
13
  readonly inTransaction: boolean
14
14
  }
15
+ /** A message a worker has claimed, as the outbox port returns it. */
16
+ export interface ClaimedRecord {
17
+ readonly id: string
18
+ readonly queue: string
19
+ readonly eventId: string
20
+ readonly payload: unknown
21
+ readonly attempts: number
22
+ }
23
+ /** What a worker asks for when it claims due messages. */
24
+ export interface ClaimParams {
25
+ readonly at: Date
26
+ readonly queues: ReadonlyArray<string>
27
+ readonly limit: number
28
+ readonly visibilityMs: number
29
+ }
30
+ /** What a worker reports when a delivery failed and should be tried again. */
31
+ export interface RetryParams {
32
+ readonly id: string
33
+ readonly at: Date
34
+ readonly error: string
35
+ }
36
+ /** What a worker reports when a message will not be delivered any more. */
37
+ export interface BuryParams {
38
+ readonly id: string
39
+ readonly error: string
40
+ }
41
+ /** The outbox operations `failNext` can make reject. */
42
+ export type OutboxOperation = "enqueue" | "claimDue" | "complete" | "retry" | "bury"
15
43
 
16
- /** An outbox double that records writes. */
17
- export interface RecordingOutbox<M extends RecordedMessage = RecordedMessage> {
44
+ /** An outbox double that records writes and scripts the claim side. */
45
+ export interface RecordingOutbox<M extends RecordedMessage = RecordedMessage, R extends ClaimedRecord = ClaimedRecord> {
18
46
  enqueue(manager: unknown, message: M): Promise<void>
19
47
  /** What the store would hold: the first write of each (queue, eventId). */
20
48
  readonly messages: ReadonlyArray<M>
@@ -25,8 +53,27 @@ export interface RecordingOutbox<M extends RecordedMessage = RecordedMessage> {
25
53
  /** True when something was written and every write went through a `fakeTransaction` manager. */
26
54
  readonly allInTransaction: boolean
27
55
  messagesOf(queue: string): ReadonlyArray<M>
56
+ /** Adds records to the backlog `claimDue` hands out; the same record twice is a duplicate delivery. */
57
+ queueRecords(...records: ReadonlyArray<R>): void
58
+ /** Hands out, in order, up to `limit` backlog records of the asked queues and removes them; an empty backlog is an empty claim. */
59
+ claimDue(params: ClaimParams): Promise<Array<R>>
60
+ complete(id: string): Promise<void>
61
+ retry(params: RetryParams): Promise<void>
62
+ bury(params: BuryParams): Promise<void>
63
+ /** Makes the next call of `operation` reject with `error`, once. */
64
+ failNext(operation: OutboxOperation, error: Error): void
65
+ /** The params of every `claimDue` call. */
66
+ readonly claims: ReadonlyArray<ClaimParams>
67
+ /** The ids passed to `complete`. */
68
+ readonly completed: ReadonlyArray<string>
69
+ /** The params of every `retry`. */
70
+ readonly retried: ReadonlyArray<RetryParams>
71
+ /** The params of every `bury`. */
72
+ readonly buried: ReadonlyArray<BuryParams>
73
+ /** The records still waiting for a claim. */
74
+ readonly backlog: ReadonlyArray<R>
28
75
  clear(): void
29
76
  }
30
77
 
31
78
  /** The outbox double. */
32
- export declare function recordingOutbox<M extends RecordedMessage = RecordedMessage>(): RecordingOutbox<M>
79
+ export declare function recordingOutbox<M extends RecordedMessage = RecordedMessage, R extends ClaimedRecord = ClaimedRecord>(): RecordingOutbox<M, R>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/jest-preset",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "The StarCi jest config for Nest repositories (unit, integration, e2e and contract projects, ts-jest with diagnostics off, per-file 100 coverage of *.service.ts only) and the one shared unit-test double kit: typed mock<T>(), mockEntityManager(), fakeTransaction(), fakeCache(), fakeLock(), recordingOutbox(), FakeClock, fakeIds(), builder() and the Outcome matchers toBeRefused/toSucceedWith.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,