@starci/jest-preset 2.3.6 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +1 -1
  3. package/README.md +6 -6
  4. package/{builders.cjs → dist/builders.d.ts} +2 -7
  5. package/dist/builders.js +15 -0
  6. package/dist/cache.d.ts +48 -0
  7. package/dist/cache.js +68 -0
  8. package/dist/clock.d.ts +20 -0
  9. package/dist/clock.js +32 -0
  10. package/dist/entity-manager.d.ts +114 -0
  11. package/dist/entity-manager.js +138 -0
  12. package/dist/event-bus.d.ts +52 -0
  13. package/dist/event-bus.js +106 -0
  14. package/dist/ids.d.ts +21 -0
  15. package/dist/ids.js +42 -0
  16. package/dist/inbox.d.ts +65 -0
  17. package/dist/inbox.js +173 -0
  18. package/dist/index.d.ts +114 -0
  19. package/dist/index.js +289 -0
  20. package/dist/lcov.d.ts +16 -0
  21. package/dist/lcov.js +25 -0
  22. package/dist/lock.d.ts +37 -0
  23. package/dist/lock.js +57 -0
  24. package/dist/matchers.d.ts +27 -0
  25. package/dist/matchers.js +36 -0
  26. package/dist/merge.d.ts +14 -0
  27. package/dist/merge.js +55 -0
  28. package/dist/mock.d.ts +21 -0
  29. package/dist/mock.js +52 -0
  30. package/dist/queue.d.ts +30 -0
  31. package/dist/queue.js +50 -0
  32. package/dist/sonar-exclusions.d.ts +7 -0
  33. package/dist/sonar-exclusions.js +8 -0
  34. package/dist/unit-metadata.d.ts +23 -0
  35. package/dist/unit-metadata.js +71 -0
  36. package/dist/world-runner.d.ts +42 -0
  37. package/dist/world-runner.js +142 -0
  38. package/package.json +52 -32
  39. package/builders.d.ts +0 -2
  40. package/cache.cjs +0 -67
  41. package/cache.d.ts +0 -31
  42. package/clock.cjs +0 -34
  43. package/clock.d.ts +0 -13
  44. package/entity-manager.cjs +0 -146
  45. package/entity-manager.d.ts +0 -103
  46. package/event-bus.cjs +0 -100
  47. package/event-bus.d.ts +0 -54
  48. package/ids.cjs +0 -41
  49. package/ids.d.ts +0 -13
  50. package/inbox.cjs +0 -162
  51. package/inbox.d.ts +0 -58
  52. package/index.cjs +0 -295
  53. package/index.d.ts +0 -38
  54. package/lcov.cjs +0 -25
  55. package/lock.cjs +0 -56
  56. package/lock.d.ts +0 -26
  57. package/matchers.cjs +0 -45
  58. package/matchers.d.ts +0 -17
  59. package/merge.cjs +0 -45
  60. package/mock.cjs +0 -53
  61. package/mock.d.ts +0 -12
  62. package/queue.cjs +0 -51
  63. package/queue.d.ts +0 -26
  64. package/sonar-exclusions.cjs +0 -8
  65. package/unit-metadata.cjs +0 -63
  66. package/world-runner.cjs +0 -142
  67. /package/{unit-metadata.generated.cjs → dist/unit-metadata.generated.js} +0 -0
package/clock.cjs DELETED
@@ -1,34 +0,0 @@
1
- "use strict"
2
-
3
- /**
4
- * `FakeClock` -- the test double for the injected `Clock` port (`platform/clock`).
5
- *
6
- * Business code asks its `Clock` port for the time (`clock.now()`) instead of reading `Date.now()` or
7
- * `new Date()` ambiently (`no-ambient-clock`, R79). A spec then drives time explicitly: it starts the
8
- * clock at a chosen instant, advances it by a duration, or sets it to a new instant, all without a real
9
- * sleep and without a race against the wall clock.
10
- */
11
-
12
- class FakeClock {
13
- /** @param {Date | number | string} [at] - The instant the clock starts at; defaults to now, once, at construction. */
14
- constructor(at) {
15
- this._now = at === undefined ? new Date() : new Date(at)
16
- }
17
-
18
- /** The current instant, matching the `Clock` port's `now(): Date`. */
19
- now() {
20
- return new Date(this._now.getTime())
21
- }
22
-
23
- /** Moves the clock to `at`. */
24
- set(at) {
25
- this._now = new Date(at)
26
- }
27
-
28
- /** Moves the clock forward by `ms` milliseconds (negative moves it back). */
29
- advance(ms) {
30
- this._now = new Date(this._now.getTime() + ms)
31
- }
32
- }
33
-
34
- module.exports = { FakeClock }
package/clock.d.ts DELETED
@@ -1,13 +0,0 @@
1
- /** The test double for the injected `Clock` port: `{ now(): Date }`, plus spec-only time control. */
2
- export declare class FakeClock {
3
- constructor(at?: Date | number | string)
4
-
5
- /** The current instant. Matches the `Clock` port's `now(): Date`, so `FakeClock` satisfies it structurally. */
6
- now(): Date
7
-
8
- /** Moves the clock to `at`. */
9
- set(at: Date | number | string): void
10
-
11
- /** Moves the clock forward by `ms` milliseconds (negative moves it back). */
12
- advance(ms: number): void
13
- }
@@ -1,146 +0,0 @@
1
- "use strict"
2
-
3
- /**
4
- * `mockEntityManager()` and `fakeTransaction()` -- the one typed double of a typeorm `EntityManager`.
5
- *
6
- * `mockEntityManager(stubs)` takes the answers a spec describes as `{ method: [Entity, result] }` (`query` takes
7
- * `[sql, rows]`; several answers of one method are a list of pairs). Every method is a `jest.fn()`, so
8
- * `expect(em.save).toHaveBeenCalledWith(...)` works, and a method the subject calls that the spec did NOT stub throws
9
- * `mockEntityManager: em.<method>() was called but this spec did not stub it`. The absence of a stub is therefore the
10
- * proof that the subject made no such call. A method stubbed for another entity (or another sql text) throws too and names
11
- * both. Answers of one method and one entity are handed out in order and the last one repeats. There is no typeorm import
12
- * here: the types come from `entity-manager.d.ts` (type-only), the runtime is a Proxy.
13
- *
14
- * `create(Entity, plain)` is pure construction and works without a stub. `transaction` needs `fakeTransaction`.
15
- *
16
- * `fakeTransaction(em)` makes `em.transaction(work)` (and `em.transaction(isolation, work)`) run `work` with a scoped view
17
- * of the same double and record how it ended (`commit` when `work` resolves, `rollback` when it throws and the error is
18
- * rethrown) and which writes it made, so a spec can assert that a rolled-back transaction committed nothing.
19
- */
20
- const { createMock, jestFn } = require("./mock.cjs")
21
-
22
- /** The manager views handed to a transaction body; `recordingEventBus` and `recordingQueueOutbox` ask whether a manager is one of them. */
23
- const TRANSACTION_MANAGERS = new WeakSet()
24
-
25
- /** Methods that change data; a write made inside a transaction is recorded on it. */
26
- const WRITE_METHODS = new Set([
27
- "save", "insert", "update", "delete", "remove", "softRemove", "softDelete", "recover", "restore", "upsert",
28
- "increment", "decrement", "clear",
29
- ])
30
- const WRITE_SQL = /^\s*(insert|update|delete|upsert|merge|truncate)\b/i
31
-
32
- const describeTarget = (target) => (typeof target === "function" ? target.name || "<anonymous class>" : JSON.stringify(String(target)).slice(0, 80))
33
-
34
- const isPair = (value) => Array.isArray(value) && value.length === 2 && (typeof value[0] === "function" || typeof value[0] === "string")
35
-
36
- /** `{ method: [target, result] | [[target, result], ...] }` as `Map<method, Array<{ target, result, used }>>`. */
37
- function tableOf(stubs) {
38
- const table = new Map()
39
- for (const [method, value] of Object.entries(stubs)) {
40
- const pairs = isPair(value) ? [value] : value
41
- if (!Array.isArray(pairs) || !pairs.every(isPair)) {
42
- throw new Error(`mockEntityManager: the stub of em.${method}() must be [Entity, result] (em.query: [sql, rows]) or a list of such pairs`)
43
- }
44
- table.set(method, pairs.map(([target, result]) => ({ target, result, used: false })))
45
- }
46
- return table
47
- }
48
-
49
- /**
50
- * The kit over a function factory (`makeFn(implementation)`); `mockEntityManager` and `fakeTransaction` below bind it to
51
- * `jest.fn`. The seam lets the package spec run the kit without a jest runtime.
52
- */
53
- function createEntityManagerKit(makeFn) {
54
- function answerOf(method, entries) {
55
- if (entries === undefined) {
56
- return () => {
57
- throw new Error(
58
- `mockEntityManager: em.${method}() was called but this spec did not stub it. Stub it with mockEntityManager({ ${method}: [Entity, result] }); an unstubbed call means the subject reached the database where the spec says it must not.`,
59
- )
60
- }
61
- }
62
- return async (...args) => {
63
- const matches = entries.filter((entry) => entry.target === args[0])
64
- if (matches.length === 0) {
65
- throw new Error(
66
- `mockEntityManager: em.${method}(${describeTarget(args[0])}) was called but this spec stubbed em.${method} only for ${entries.map((entry) => describeTarget(entry.target)).join(", ")}`,
67
- )
68
- }
69
- const entry = matches.find((candidate) => !candidate.used) ?? matches[matches.length - 1]
70
- entry.used = true
71
- return entry.result
72
- }
73
- }
74
-
75
- /** @param {Record<string, unknown>} [stubs] */
76
- function mockEntityManager(stubs = {}) {
77
- const table = tableOf(stubs)
78
- const create = makeFn((target, plain) => Object.assign(new target(), plain))
79
- return createMock((method) => makeFn(answerOf(method, table.get(method))))({ create })
80
- }
81
-
82
- /** @param {object} [em] - The double to attach the transaction to; a fresh `mockEntityManager()` by default. */
83
- function fakeTransaction(em = mockEntityManager()) {
84
- const runs = []
85
- const scopedFor = (run) => {
86
- const view = new Proxy(em, {
87
- get(target, property, receiver) {
88
- if (property === "queryRunner") return { get isTransactionActive() { return run.outcome === "pending" } }
89
- const value = Reflect.get(target, property, receiver)
90
- if (property === "transaction") return (...args) => args[args.length - 1](view)
91
- if (typeof property !== "string" || typeof value !== "function") return value
92
- if (!WRITE_METHODS.has(property) && property !== "query") return value
93
- return (...args) => {
94
- if (property !== "query" || WRITE_SQL.test(String(args[0]))) run.writes.push({ method: property, args })
95
- return value(...args)
96
- }
97
- },
98
- })
99
- TRANSACTION_MANAGERS.add(view)
100
- return view
101
- }
102
- em.transaction = makeFn(async (...args) => {
103
- const work = args[args.length - 1]
104
- if (typeof work !== "function") throw new Error("fakeTransaction: em.transaction() needs a callback as its last argument")
105
- const run = { outcome: "pending", writes: [] }
106
- runs.push(run)
107
- try {
108
- const result = await work(scopedFor(run))
109
- run.outcome = "commit"
110
- return result
111
- } catch (error) {
112
- run.outcome = "rollback"
113
- throw error
114
- }
115
- })
116
- const writesOf = (outcome) => runs.filter((run) => run.outcome === outcome).flatMap((run) => run.writes)
117
- return {
118
- em,
119
- /** How each transaction ended, in call order: "commit" or "rollback". */
120
- get outcomes() {
121
- return runs.map((run) => run.outcome)
122
- },
123
- get commits() {
124
- return runs.filter((run) => run.outcome === "commit").length
125
- },
126
- get rollbacks() {
127
- return runs.filter((run) => run.outcome === "rollback").length
128
- },
129
- /** The writes (`{ method, args }`) that ended in a commit, in call order. */
130
- get committedWrites() {
131
- return writesOf("commit")
132
- },
133
- /** The writes that a rollback discarded: made by a body that then threw. */
134
- get rolledBackWrites() {
135
- return writesOf("rollback")
136
- },
137
- }
138
- }
139
-
140
- return { mockEntityManager, fakeTransaction }
141
- }
142
-
143
- /** Whether `manager` is the scoped view a `fakeTransaction` body received. */
144
- const isTransactionManager = (manager) => typeof manager === "object" && manager !== null && TRANSACTION_MANAGERS.has(manager)
145
-
146
- module.exports = { ...createEntityManagerKit(jestFn), createEntityManagerKit, isTransactionManager }
@@ -1,103 +0,0 @@
1
- /// <reference types="jest" />
2
- import type { DeleteResult, EntityManager, InsertResult, UpdateResult } from "typeorm"
3
- import type { MockOf } from "./mock"
4
-
5
- /** A typed double of `EntityManager`: assignable wherever an `EntityManager` is required, every method a jest mock. */
6
- export type MockEntityManager = MockOf<EntityManager>
7
-
8
- /** An entity class. */
9
- export type EntityClass<E extends object = object> = new (...args: never[]) => E
10
-
11
- type Row<C extends EntityClass> = InstanceType<C>
12
- type Write<T> = Partial<Omit<T, "raw">> & { readonly raw?: unknown }
13
-
14
- /** The result type of each entity-taking `EntityManager` method, for the entity class `C`. */
15
- export interface EntityAnswers<C extends EntityClass> {
16
- findOne: Row<C> | null
17
- findOneBy: Row<C> | null
18
- findOneOrFail: Row<C>
19
- findOneByOrFail: Row<C>
20
- find: ReadonlyArray<Row<C>>
21
- findBy: ReadonlyArray<Row<C>>
22
- findAndCount: readonly [ReadonlyArray<Row<C>>, number]
23
- findAndCountBy: readonly [ReadonlyArray<Row<C>>, number]
24
- count: number
25
- countBy: number
26
- exists: boolean
27
- existsBy: boolean
28
- save: Row<C> | ReadonlyArray<Row<C>>
29
- remove: Row<C> | ReadonlyArray<Row<C>>
30
- softRemove: Row<C> | ReadonlyArray<Row<C>>
31
- recover: Row<C> | ReadonlyArray<Row<C>>
32
- insert: Write<InsertResult>
33
- update: Write<UpdateResult>
34
- upsert: Write<InsertResult>
35
- delete: Write<DeleteResult>
36
- softDelete: Write<UpdateResult>
37
- restore: Write<UpdateResult>
38
- increment: Write<UpdateResult>
39
- decrement: Write<UpdateResult>
40
- }
41
-
42
- /** The methods a spec may stub as `[Entity, result]`. */
43
- export type EntityMethod = keyof EntityAnswers<EntityClass>
44
-
45
- type Pair<M extends EntityMethod, C extends EntityClass> = readonly [entity: C, result: EntityAnswers<C>[M]]
46
- type Untyped = readonly [unknown, unknown]
47
-
48
- /** One stub: `[Entity, result]` (`query`: `[sql, rows]`), or a list of such pairs answered in order. */
49
- export type StubOf<M extends string, V> = M extends "query"
50
- ? readonly [sql: string, rows: unknown] | ReadonlyArray<readonly [sql: string, rows: unknown]>
51
- : M extends EntityMethod
52
- ? V extends readonly [infer C extends EntityClass, unknown]
53
- ? Pair<M, C>
54
- : V extends ReadonlyArray<readonly [infer C extends EntityClass, unknown]>
55
- ? ReadonlyArray<Pair<M, C>>
56
- : never
57
- : never
58
-
59
- /** The stubs of a spec, checked entry by entry against the entity each one names. */
60
- export type StubsOf<S> = { readonly [M in keyof S & string]: StubOf<M, S[M]> }
61
-
62
- /**
63
- * A strict `EntityManager` double. Describe what the subject should see, `{ findOne: [OrderEntity, order] }`; any method
64
- * the subject calls that the spec did not stub throws an error naming the method, and a wrong entity or sql text throws
65
- * too. Results are typed against the entity: `findOne: [OrderEntity, 5]` is a compile error.
66
- */
67
- export declare function mockEntityManager<
68
- const S extends { readonly [M in string]?: Untyped | ReadonlyArray<Untyped> } = Record<never, never>,
69
- >(stubs?: S & StubsOf<S>): MockEntityManager
70
-
71
- /** A write made inside a transaction. */
72
- export interface TransactionWrite {
73
- readonly method: string
74
- readonly args: ReadonlyArray<unknown>
75
- }
76
-
77
- /** The transaction attached to a double by `fakeTransaction`. */
78
- export interface FakeTransaction<M extends MockEntityManager = MockEntityManager> {
79
- /** The double whose `transaction(work)` runs `work`. */
80
- readonly em: M
81
- /** How each transaction ended, in call order. */
82
- readonly outcomes: ReadonlyArray<"commit" | "rollback" | "pending">
83
- /** Number of transactions that committed. */
84
- readonly commits: number
85
- /** Number of transactions that rolled back (the callback threw; the error was rethrown). */
86
- readonly rollbacks: number
87
- /** The writes of the transactions that committed. */
88
- readonly committedWrites: ReadonlyArray<TransactionWrite>
89
- /** The writes of the transactions that rolled back: what a real rollback would have discarded. */
90
- readonly rolledBackWrites: ReadonlyArray<TransactionWrite>
91
- }
92
-
93
- /** Makes `em.transaction(work)` run `work` with a scoped view whose queryRunner stays active until commit or rollback. */
94
- export declare function fakeTransaction<M extends MockEntityManager = MockEntityManager>(em?: M): FakeTransaction<M>
95
-
96
- /** Whether `manager` is the view a `fakeTransaction` body received (used by `recordingEventBus` and `recordingQueueOutbox`). */
97
- export declare function isTransactionManager(manager: unknown): boolean
98
-
99
- /** The kit over a function factory, for callers that bring their own (used by the package spec). */
100
- export declare function createEntityManagerKit(makeFn: (implementation?: (...args: never[]) => unknown) => unknown): {
101
- mockEntityManager: typeof mockEntityManager
102
- fakeTransaction: typeof fakeTransaction
103
- }
package/event-bus.cjs DELETED
@@ -1,100 +0,0 @@
1
- "use strict"
2
-
3
- /**
4
- * `recordingEventBus()` -- an `EventBus` double that records what a domain service publishes, so a spec asserts on `bus.events`.
5
- *
6
- * `publish(event, tx)` records the event with the manager it was written through. `events` is what the outbox would hold: one
7
- * event per (event name, event id), like the real unique key, so a duplicate publication keeps one. `writes` is every call, duplicates
8
- * included, and `entries` adds `inTransaction`: whether `tx` was the view a `fakeTransaction` body received, so a spec proves an event
9
- * is published in the transaction of the change that caused it. The name of an event is the static `eventName` of its class.
10
- *
11
- * The read side is scripted: `setPendingRetries(count)` sets what `pendingRetries(eventClass)` answers, `deadLetters(eventClass)`
12
- * returns what `queueDeadLetters(...letters)` filled, `requeue(id)` records the id, and `failNext(operation, error)` makes the next
13
- * call of that operation reject once, so a spec drives the failure path.
14
- */
15
- const { isTransactionManager } = require("./entity-manager.cjs")
16
-
17
- const nameOf = (event) => event.constructor.eventName
18
-
19
- function recordingEventBus() {
20
- const written = []
21
- const letters = []
22
- const requeued = []
23
- const failures = new Map()
24
- let pending = 0
25
- const failIfScripted = (operation) => {
26
- if (!failures.has(operation)) return
27
- const error = failures.get(operation)
28
- failures.delete(operation)
29
- throw error
30
- }
31
- return {
32
- async publish(event, tx) {
33
- failIfScripted("publish")
34
- written.push({ event, tx, inTransaction: isTransactionManager(tx) })
35
- },
36
- async pendingRetries() {
37
- failIfScripted("pendingRetries")
38
- return pending
39
- },
40
- async deadLetters(eventClass) {
41
- failIfScripted("deadLetters")
42
- return letters.filter((letter) => letter.eventName === eventClass.eventName)
43
- },
44
- async requeue(id) {
45
- failIfScripted("requeue")
46
- requeued.push(id)
47
- },
48
- /** The events the outbox would hold: the first publication of each (event name, event id). */
49
- get events() {
50
- const seen = new Set()
51
- return written
52
- .filter(({ event }) => {
53
- const key = `${nameOf(event)}\u0000${event.eventId}`
54
- if (seen.has(key)) return false
55
- seen.add(key)
56
- return true
57
- })
58
- .map(({ event }) => event)
59
- },
60
- /** Every publication, in order: `{ event, tx, inTransaction }`. */
61
- get entries() {
62
- return [...written]
63
- },
64
- /** Every event published, duplicates included. */
65
- get writes() {
66
- return written.map(({ event }) => event)
67
- },
68
- /** True when something was published and every publication went through a transaction manager. */
69
- get allInTransaction() {
70
- return written.length > 0 && written.every((entry) => entry.inTransaction)
71
- },
72
- /** The stored events of one class. */
73
- eventsOf(eventClass) {
74
- return this.events.filter((event) => nameOf(event) === eventClass.eventName)
75
- },
76
- /** Sets the answer of `pendingRetries`. */
77
- setPendingRetries(count) {
78
- pending = count
79
- },
80
- /** Adds dead letters `deadLetters` hands out. */
81
- queueDeadLetters(...items) {
82
- letters.push(...items)
83
- },
84
- /** The ids passed to `requeue`, in order. */
85
- get requeued() {
86
- return [...requeued]
87
- },
88
- /** Makes the next call of `operation` reject with `error`, once. */
89
- failNext(operation, error) {
90
- failures.set(operation, error)
91
- },
92
- clear() {
93
- for (const list of [written, letters, requeued]) list.length = 0
94
- pending = 0
95
- failures.clear()
96
- },
97
- }
98
- }
99
-
100
- module.exports = { recordingEventBus }
package/event-bus.d.ts DELETED
@@ -1,54 +0,0 @@
1
- /** An event as the bus port takes it: a class instance with a stable id; its name is the static `eventName` of its class. */
2
- export interface RecordedEvent {
3
- readonly eventId: string
4
- }
5
- /** The static side of an event class. */
6
- export interface RecordedEventClass {
7
- readonly eventName: string
8
- }
9
- /** One recorded publication. */
10
- export interface EventWrite<E extends RecordedEvent = RecordedEvent> {
11
- readonly event: E
12
- readonly tx: unknown
13
- /** Whether `tx` was the view a `fakeTransaction` body received. */
14
- readonly inTransaction: boolean
15
- }
16
- /** A dead letter as the bus port returns it. */
17
- export interface RecordedDeadLetter {
18
- readonly id: string
19
- readonly eventName: string
20
- readonly eventId: string
21
- readonly reason: string
22
- readonly attempts: number
23
- }
24
- /** The bus operations `failNext` can make reject. */
25
- export type EventBusOperation = "publish" | "pendingRetries" | "deadLetters" | "requeue"
26
-
27
- /** An `EventBus` double that records publications and scripts the read side. */
28
- export interface RecordingEventBus<E extends RecordedEvent = RecordedEvent> {
29
- publish(event: E, tx: unknown): Promise<void>
30
- pendingRetries(eventClass: RecordedEventClass): Promise<number>
31
- deadLetters(eventClass: RecordedEventClass): Promise<ReadonlyArray<RecordedDeadLetter>>
32
- requeue(id: string): Promise<void>
33
- /** What the outbox would hold: the first publication of each (event name, event id). */
34
- readonly events: ReadonlyArray<E>
35
- /** Every publication with its manager, duplicates included. */
36
- readonly entries: ReadonlyArray<EventWrite<E>>
37
- /** Every event published, duplicates included. */
38
- readonly writes: ReadonlyArray<E>
39
- /** True when something was published and every publication went through a `fakeTransaction` manager. */
40
- readonly allInTransaction: boolean
41
- eventsOf(eventClass: RecordedEventClass): ReadonlyArray<E>
42
- /** Sets the answer of `pendingRetries`. */
43
- setPendingRetries(count: number): void
44
- /** Adds dead letters `deadLetters` hands out. */
45
- queueDeadLetters(...items: ReadonlyArray<RecordedDeadLetter>): void
46
- /** The ids passed to `requeue`. */
47
- readonly requeued: ReadonlyArray<string>
48
- /** Makes the next call of `operation` reject with `error`, once. */
49
- failNext(operation: EventBusOperation, error: Error): void
50
- clear(): void
51
- }
52
-
53
- /** The event bus double. */
54
- export declare function recordingEventBus<E extends RecordedEvent = RecordedEvent>(): RecordingEventBus<E>
package/ids.cjs DELETED
@@ -1,41 +0,0 @@
1
- "use strict"
2
-
3
- /**
4
- * `fakeIds()` -- the test double for the platform ids port (`{ next(): string }`; the production adapter is
5
- * `randomUUID()` behind the port). Ids are deterministic and UUID-shaped, so a spec can assert on the exact persisted id:
6
- * the first is `00000000-0000-4000-8000-000000000001`, the second `...0002`, and so on. A `prefix` (1 to 8 hex digits)
7
- * changes the leading group so two generators in one spec never collide.
8
- */
9
- class FakeIds {
10
- /** @param {string} [prefix] - 1 to 8 hex digits for the first group; default "00000000". */
11
- constructor(prefix = "00000000") {
12
- if (!/^[0-9a-f]{1,8}$/i.test(prefix)) throw new Error("fakeIds: the prefix is 1 to 8 hex digits")
13
- this._prefix = prefix.padStart(8, "0").toLowerCase()
14
- this._issued = []
15
- }
16
-
17
- /** The next id. */
18
- next() {
19
- const n = String(this._issued.length + 1).padStart(12, "0")
20
- const id = `${this._prefix}-0000-4000-8000-${n}`
21
- this._issued.push(id)
22
- return id
23
- }
24
-
25
- /** Every id handed out so far, in order. */
26
- get issued() {
27
- return [...this._issued]
28
- }
29
-
30
- /** Starts the sequence over. */
31
- reset() {
32
- this._issued = []
33
- }
34
- }
35
-
36
- /** @param {string} [prefix] */
37
- function fakeIds(prefix) {
38
- return new FakeIds(prefix)
39
- }
40
-
41
- module.exports = { fakeIds, FakeIds }
package/ids.d.ts DELETED
@@ -1,13 +0,0 @@
1
- /** The platform ids port a spec injects `fakeIds()` for: `{ next(): string }`. */
2
- export declare class FakeIds {
3
- constructor(prefix?: string)
4
- /** The next deterministic UUID-shaped id. */
5
- next(): string
6
- /** Every id handed out so far, in order. */
7
- readonly issued: ReadonlyArray<string>
8
- /** Starts the sequence over. */
9
- reset(): void
10
- }
11
-
12
- /** A deterministic id generator: `00000000-0000-4000-8000-000000000001`, `...0002`, ... */
13
- export declare function fakeIds(prefix?: string): FakeIds
package/inbox.cjs DELETED
@@ -1,162 +0,0 @@
1
- "use strict"
2
-
3
- const { isDeepStrictEqual } = require("node:util")
4
- const { isTransactionManager } = require("./entity-manager.cjs")
5
- const CLIENT_SWEEP_BATCH = 500
6
-
7
- /** The fake carries the production refusal code without depending on an application's error family. */
8
- const inboxError = (code) => Object.assign(new Error(code), { code })
9
-
10
- /** Clone JSON values directly so toJSON cannot change the request or the stored outcome. */
11
- const snapshot = (value, ancestors = new Set()) => {
12
- if (value === null || typeof value === "string" || typeof value === "boolean") return value
13
- if (typeof value === "number" && Number.isFinite(value)) return value === 0 ? 0 : value
14
- if (typeof value !== "object" || ancestors.has(value)) throw inboxError("IDEMPOTENCY_REQUEST_INVALID")
15
- ancestors.add(value)
16
- let result
17
- if (Array.isArray(value)) result = Array.from(value, (member) => snapshot(member, ancestors))
18
- else {
19
- const prototype = Object.getPrototypeOf(value)
20
- if (prototype !== Object.prototype && prototype !== null) throw inboxError("IDEMPOTENCY_REQUEST_INVALID")
21
- result = Object.fromEntries(Object.entries(value).map(([key, member]) => [key, snapshot(member, ancestors)]))
22
- }
23
- ancestors.delete(value)
24
- return result
25
- }
26
-
27
- /**
28
- * `fakeInbox(clock?)` records delivery and client claims, so a spec asserts deduplication and the transaction passed by
29
- * its service. Client calls replay JSON outcomes; `clock` gives retention tests a driven claim timestamp.
30
- *
31
- * `claim(source, eventId)` answers `true` for the first call of a (source, eventId) pair and `false` for every later one, like the
32
- * real unique key; `release(source, eventId)` gives an ordinary delivery claim back while preserving client outcomes.
33
- * `claims` is every claim in order, `claimed` the pairs that are held now, `released` every release, `seen(source, eventId)` marks a
34
- * pair as already delivered (a redelivery), and `failNext(operation, error)` makes the next call of that operation reject once, so
35
- * a spec drives the failure path.
36
- */
37
- function fakeInbox(clock) {
38
- const held = new Set()
39
- const claims = []
40
- const released = []
41
- const clients = new Map()
42
- const clientEntries = []
43
- const sweeps = []
44
- const failures = new Map()
45
- const keyOf = (source, eventId) => `${source}\u0000${eventId}`
46
- const failIfScripted = (operation) => {
47
- if (!failures.has(operation)) return
48
- const error = failures.get(operation)
49
- failures.delete(operation)
50
- throw error
51
- }
52
- return {
53
- async claim(source, eventId) {
54
- failIfScripted("claim")
55
- claims.push({ source, eventId })
56
- const key = keyOf(source, eventId)
57
- if (held.has(key)) return false
58
- held.add(key)
59
- return true
60
- },
61
- async release(source, eventId) {
62
- failIfScripted("release")
63
- released.push({ source, eventId })
64
- const key = keyOf(source, eventId)
65
- if (!clients.has(key)) held.delete(key)
66
- },
67
- async runClient(request, tx, effect) {
68
- failIfScripted("runClient")
69
- const { principalId, operation, idempotencyKey } = request
70
- if (!principalId.trim() || principalId.includes(":") || !operation.trim() || operation.includes(":")
71
- || !idempotencyKey.trim() || idempotencyKey.length > 64) {
72
- throw inboxError("IDEMPOTENCY_REQUEST_INVALID")
73
- }
74
- const source = `client:${principalId}:${operation}`
75
- const key = keyOf(source, idempotencyKey)
76
- const body = snapshot(request.request)
77
- claims.push({ source, eventId: idempotencyKey })
78
- clientEntries.push({ source, eventId: idempotencyKey, tx, inTransaction: isTransactionManager(tx) })
79
- const previous = clients.get(key)
80
- if (previous !== undefined) {
81
- if (!isDeepStrictEqual(previous.request, body)) throw inboxError("IDEMPOTENCY_KEY_REUSED")
82
- return snapshot(await previous.result)
83
- }
84
- if (held.has(key)) throw inboxError("IDEMPOTENCY_KEY_REUSED")
85
- const entry = { request: body, claimedAt: clock === undefined ? new Date(0) : clock.now(), completed: false }
86
- held.add(key)
87
- clients.set(key, entry)
88
- entry.result = Promise.resolve().then(effect).then((value) => {
89
- let stored
90
- try {
91
- stored = snapshot(value)
92
- } catch (cause) {
93
- throw Object.assign(inboxError("INBOX_OUTCOME_INVALID"), { cause })
94
- }
95
- entry.completed = true
96
- return stored
97
- }).catch((error) => {
98
- if (clients.get(key) === entry) {
99
- clients.delete(key)
100
- held.delete(key)
101
- }
102
- throw error
103
- })
104
- return snapshot(await entry.result)
105
- },
106
- async sweepClients(cutoff, tx) {
107
- sweeps.push({ cutoff: new Date(cutoff), tx, inTransaction: isTransactionManager(tx) })
108
- failIfScripted("sweepClients")
109
- let swept = 0
110
- for (const [key, entry] of clients) {
111
- if (swept === CLIENT_SWEEP_BATCH) break
112
- if (!entry.completed || entry.claimedAt >= cutoff) continue
113
- clients.delete(key)
114
- held.delete(key)
115
- swept += 1
116
- }
117
- return swept
118
- },
119
- /** Marks a pair as already claimed, so the next `claim` of it answers `false` (a redelivery). */
120
- seen(source, eventId) {
121
- held.add(keyOf(source, eventId))
122
- },
123
- /** Makes the next call of `operation` reject with `error`, once. */
124
- failNext(operation, error) {
125
- failures.set(operation, error)
126
- },
127
- /** Every `claim`, in order, duplicates included. */
128
- get claims() {
129
- return [...claims]
130
- },
131
- /** The pairs held now: claimed and not released. */
132
- get claimed() {
133
- return [...held].map((key) => {
134
- const [source, eventId] = key.split("\u0000")
135
- return { source, eventId }
136
- })
137
- },
138
- /** Every `release`, in order. */
139
- get released() {
140
- return [...released]
141
- },
142
- /** Every client claim, including repeats, with the domain transaction it used. */
143
- get clientEntries() {
144
- return [...clientEntries]
145
- },
146
- /** Every retention attempt with its cutoff and domain transaction, including a scripted failure. */
147
- get sweeps() {
148
- return [...sweeps]
149
- },
150
- clear() {
151
- held.clear()
152
- claims.length = 0
153
- released.length = 0
154
- failures.clear()
155
- clients.clear()
156
- clientEntries.length = 0
157
- sweeps.length = 0
158
- },
159
- }
160
- }
161
-
162
- module.exports = { fakeInbox }