@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.
- package/CHANGELOG.md +9 -0
- package/LICENSE +1 -1
- package/README.md +6 -6
- package/{builders.cjs → dist/builders.d.ts} +2 -7
- package/dist/builders.js +15 -0
- package/dist/cache.d.ts +48 -0
- package/dist/cache.js +68 -0
- package/dist/clock.d.ts +20 -0
- package/dist/clock.js +32 -0
- package/dist/entity-manager.d.ts +114 -0
- package/dist/entity-manager.js +138 -0
- package/dist/event-bus.d.ts +52 -0
- package/dist/event-bus.js +106 -0
- package/dist/ids.d.ts +21 -0
- package/dist/ids.js +42 -0
- package/dist/inbox.d.ts +65 -0
- package/dist/inbox.js +173 -0
- package/dist/index.d.ts +114 -0
- package/dist/index.js +289 -0
- package/dist/lcov.d.ts +16 -0
- package/dist/lcov.js +25 -0
- package/dist/lock.d.ts +37 -0
- package/dist/lock.js +57 -0
- package/dist/matchers.d.ts +27 -0
- package/dist/matchers.js +36 -0
- package/dist/merge.d.ts +14 -0
- package/dist/merge.js +55 -0
- package/dist/mock.d.ts +21 -0
- package/dist/mock.js +52 -0
- package/dist/queue.d.ts +30 -0
- package/dist/queue.js +50 -0
- package/dist/sonar-exclusions.d.ts +7 -0
- package/dist/sonar-exclusions.js +8 -0
- package/dist/unit-metadata.d.ts +23 -0
- package/dist/unit-metadata.js +71 -0
- package/dist/world-runner.d.ts +42 -0
- package/dist/world-runner.js +142 -0
- package/package.json +52 -32
- package/builders.d.ts +0 -2
- package/cache.cjs +0 -67
- package/cache.d.ts +0 -31
- package/clock.cjs +0 -34
- package/clock.d.ts +0 -13
- package/entity-manager.cjs +0 -146
- package/entity-manager.d.ts +0 -103
- package/event-bus.cjs +0 -100
- package/event-bus.d.ts +0 -54
- package/ids.cjs +0 -41
- package/ids.d.ts +0 -13
- package/inbox.cjs +0 -162
- package/inbox.d.ts +0 -58
- package/index.cjs +0 -295
- package/index.d.ts +0 -38
- package/lcov.cjs +0 -25
- package/lock.cjs +0 -56
- package/lock.d.ts +0 -26
- package/matchers.cjs +0 -45
- package/matchers.d.ts +0 -17
- package/merge.cjs +0 -45
- package/mock.cjs +0 -53
- package/mock.d.ts +0 -12
- package/queue.cjs +0 -51
- package/queue.d.ts +0 -26
- package/sonar-exclusions.cjs +0 -8
- package/unit-metadata.cjs +0 -63
- package/world-runner.cjs +0 -142
- /package/{unit-metadata.generated.cjs → dist/unit-metadata.generated.js} +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.recordingEventBus = recordingEventBus;
|
|
4
|
+
/**
|
|
5
|
+
* `recordingEventBus()` -- an `EventBus` double that records what a domain service publishes, so a spec asserts on `bus.events`.
|
|
6
|
+
*
|
|
7
|
+
* `publish(event, tx)` records the event with the manager it was written through. `events` is what the outbox would hold: one
|
|
8
|
+
* event per (event name, event id), like the real unique key, so a duplicate publication keeps one. `writes` is every call, duplicates
|
|
9
|
+
* included, and `entries` adds `inTransaction`: whether `tx` was the view a `fakeTransaction` body received, so a spec proves an event
|
|
10
|
+
* is published in the transaction of the change that caused it. The name of an event is the static `eventName` of its class.
|
|
11
|
+
*
|
|
12
|
+
* The read side is scripted: `setPendingRetries(count)` sets what `pendingRetries(eventClass)` answers, `deadLetters(eventClass)`
|
|
13
|
+
* returns what `queueDeadLetters(...letters)` filled, `requeue(id)` records the id, and `failNext(operation, error)` makes the next
|
|
14
|
+
* call of that operation reject once, so a spec drives the failure path.
|
|
15
|
+
*/
|
|
16
|
+
const entity_manager_ts_1 = require("./entity-manager.js");
|
|
17
|
+
/** The name of an event: the static `eventName` of its class. */
|
|
18
|
+
const nameOf = (event) => {
|
|
19
|
+
const eventClass = event.constructor;
|
|
20
|
+
return typeof eventClass === "function" && "eventName" in eventClass ? eventClass.eventName : undefined;
|
|
21
|
+
};
|
|
22
|
+
/** The event bus double. */
|
|
23
|
+
function recordingEventBus() {
|
|
24
|
+
const written = [];
|
|
25
|
+
const letters = [];
|
|
26
|
+
const requeued = [];
|
|
27
|
+
const failures = new Map();
|
|
28
|
+
let pending = 0;
|
|
29
|
+
const failIfScripted = (operation) => {
|
|
30
|
+
const error = failures.get(operation);
|
|
31
|
+
if (error === undefined)
|
|
32
|
+
return;
|
|
33
|
+
failures.delete(operation);
|
|
34
|
+
throw error;
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
async publish(event, tx) {
|
|
38
|
+
failIfScripted("publish");
|
|
39
|
+
written.push({ event, tx, inTransaction: (0, entity_manager_ts_1.isTransactionManager)(tx) });
|
|
40
|
+
},
|
|
41
|
+
async pendingRetries() {
|
|
42
|
+
failIfScripted("pendingRetries");
|
|
43
|
+
return pending;
|
|
44
|
+
},
|
|
45
|
+
async deadLetters(eventClass) {
|
|
46
|
+
failIfScripted("deadLetters");
|
|
47
|
+
return letters.filter((letter) => letter.eventName === eventClass.eventName);
|
|
48
|
+
},
|
|
49
|
+
async requeue(id) {
|
|
50
|
+
failIfScripted("requeue");
|
|
51
|
+
requeued.push(id);
|
|
52
|
+
},
|
|
53
|
+
/** The events the outbox would hold: the first publication of each (event name, event id). */
|
|
54
|
+
get events() {
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
return written
|
|
57
|
+
.filter(({ event }) => {
|
|
58
|
+
const key = `${String(nameOf(event))}\u0000${event.eventId}`;
|
|
59
|
+
if (seen.has(key))
|
|
60
|
+
return false;
|
|
61
|
+
seen.add(key);
|
|
62
|
+
return true;
|
|
63
|
+
})
|
|
64
|
+
.map(({ event }) => event);
|
|
65
|
+
},
|
|
66
|
+
/** Every publication, in order: `{ event, tx, inTransaction }`. */
|
|
67
|
+
get entries() {
|
|
68
|
+
return [...written];
|
|
69
|
+
},
|
|
70
|
+
/** Every event published, duplicates included. */
|
|
71
|
+
get writes() {
|
|
72
|
+
return written.map(({ event }) => event);
|
|
73
|
+
},
|
|
74
|
+
/** True when something was published and every publication went through a transaction manager. */
|
|
75
|
+
get allInTransaction() {
|
|
76
|
+
return written.length > 0 && written.every((entry) => entry.inTransaction);
|
|
77
|
+
},
|
|
78
|
+
/** The stored events of one class. */
|
|
79
|
+
eventsOf(eventClass) {
|
|
80
|
+
return this.events.filter((event) => nameOf(event) === eventClass.eventName);
|
|
81
|
+
},
|
|
82
|
+
/** Sets the answer of `pendingRetries`. */
|
|
83
|
+
setPendingRetries(count) {
|
|
84
|
+
pending = count;
|
|
85
|
+
},
|
|
86
|
+
/** Adds dead letters `deadLetters` hands out. */
|
|
87
|
+
queueDeadLetters(...items) {
|
|
88
|
+
letters.push(...items);
|
|
89
|
+
},
|
|
90
|
+
/** The ids passed to `requeue`, in order. */
|
|
91
|
+
get requeued() {
|
|
92
|
+
return [...requeued];
|
|
93
|
+
},
|
|
94
|
+
/** Makes the next call of `operation` reject with `error`, once. */
|
|
95
|
+
failNext(operation, error) {
|
|
96
|
+
failures.set(operation, error);
|
|
97
|
+
},
|
|
98
|
+
clear() {
|
|
99
|
+
written.length = 0;
|
|
100
|
+
letters.length = 0;
|
|
101
|
+
requeued.length = 0;
|
|
102
|
+
pending = 0;
|
|
103
|
+
failures.clear();
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
package/dist/ids.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fakeIds()` -- the test double for the platform ids port (`{ next(): string }`; the production adapter is
|
|
3
|
+
* `randomUUID()` behind the port). Ids are deterministic and UUID-shaped, so a spec can assert on the exact persisted id:
|
|
4
|
+
* the first is `00000000-0000-4000-8000-000000000001`, the second `...0002`, and so on. A `prefix` (1 to 8 hex digits)
|
|
5
|
+
* changes the leading group so two generators in one spec never collide.
|
|
6
|
+
*/
|
|
7
|
+
/** The platform ids port a spec injects `fakeIds()` for: `{ next(): string }`. */
|
|
8
|
+
export declare class FakeIds {
|
|
9
|
+
private readonly _prefix;
|
|
10
|
+
private _issued;
|
|
11
|
+
/** @param prefix - 1 to 8 hex digits for the first group; default "00000000". */
|
|
12
|
+
constructor(prefix?: string);
|
|
13
|
+
/** The next deterministic UUID-shaped id. */
|
|
14
|
+
next(): string;
|
|
15
|
+
/** Every id handed out so far, in order. */
|
|
16
|
+
get issued(): ReadonlyArray<string>;
|
|
17
|
+
/** Starts the sequence over. */
|
|
18
|
+
reset(): void;
|
|
19
|
+
}
|
|
20
|
+
/** A deterministic id generator: `00000000-0000-4000-8000-000000000001`, `...0002`, ... */
|
|
21
|
+
export declare function fakeIds(prefix?: string): FakeIds;
|
package/dist/ids.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `fakeIds()` -- the test double for the platform ids port (`{ next(): string }`; the production adapter is
|
|
4
|
+
* `randomUUID()` behind the port). Ids are deterministic and UUID-shaped, so a spec can assert on the exact persisted id:
|
|
5
|
+
* the first is `00000000-0000-4000-8000-000000000001`, the second `...0002`, and so on. A `prefix` (1 to 8 hex digits)
|
|
6
|
+
* changes the leading group so two generators in one spec never collide.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.FakeIds = void 0;
|
|
10
|
+
exports.fakeIds = fakeIds;
|
|
11
|
+
/** The platform ids port a spec injects `fakeIds()` for: `{ next(): string }`. */
|
|
12
|
+
class FakeIds {
|
|
13
|
+
_prefix;
|
|
14
|
+
_issued;
|
|
15
|
+
/** @param prefix - 1 to 8 hex digits for the first group; default "00000000". */
|
|
16
|
+
constructor(prefix = "00000000") {
|
|
17
|
+
if (!/^[0-9a-f]{1,8}$/i.test(prefix))
|
|
18
|
+
throw new Error("fakeIds: the prefix is 1 to 8 hex digits");
|
|
19
|
+
this._prefix = prefix.padStart(8, "0").toLowerCase();
|
|
20
|
+
this._issued = [];
|
|
21
|
+
}
|
|
22
|
+
/** The next deterministic UUID-shaped id. */
|
|
23
|
+
next() {
|
|
24
|
+
const n = String(this._issued.length + 1).padStart(12, "0");
|
|
25
|
+
const id = `${this._prefix}-0000-4000-8000-${n}`;
|
|
26
|
+
this._issued.push(id);
|
|
27
|
+
return id;
|
|
28
|
+
}
|
|
29
|
+
/** Every id handed out so far, in order. */
|
|
30
|
+
get issued() {
|
|
31
|
+
return [...this._issued];
|
|
32
|
+
}
|
|
33
|
+
/** Starts the sequence over. */
|
|
34
|
+
reset() {
|
|
35
|
+
this._issued = [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.FakeIds = FakeIds;
|
|
39
|
+
/** A deterministic id generator: `00000000-0000-4000-8000-000000000001`, `...0002`, ... */
|
|
40
|
+
function fakeIds(prefix) {
|
|
41
|
+
return new FakeIds(prefix);
|
|
42
|
+
}
|
package/dist/inbox.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** One claim or release of a delivery. */
|
|
2
|
+
export interface InboxEntry {
|
|
3
|
+
readonly source: string;
|
|
4
|
+
readonly eventId: string;
|
|
5
|
+
}
|
|
6
|
+
/** The inbox operations `failNext` can make reject. */
|
|
7
|
+
export type InboxOperation = "claim" | "release" | "runClient" | "sweepClients";
|
|
8
|
+
/** The client operation namespace, exact key and JSON request whose outcome the inbox stores. */
|
|
9
|
+
export interface InboxClientOperation {
|
|
10
|
+
readonly principalId: string;
|
|
11
|
+
readonly operation: string;
|
|
12
|
+
readonly idempotencyKey: string;
|
|
13
|
+
readonly request: unknown;
|
|
14
|
+
}
|
|
15
|
+
/** A client claim with the manager passed by its caller. */
|
|
16
|
+
export interface InboxClientEntry extends InboxEntry {
|
|
17
|
+
readonly tx: unknown;
|
|
18
|
+
readonly inTransaction: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** One retention sweep with the cutoff instant and manager passed by its caller. */
|
|
21
|
+
export interface InboxSweep {
|
|
22
|
+
readonly cutoff: Date;
|
|
23
|
+
readonly tx: unknown;
|
|
24
|
+
readonly inTransaction: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** An `Inbox` double with the behaviour of the real claim. */
|
|
27
|
+
export interface FakeInbox {
|
|
28
|
+
/** True for the first claim of a (source, eventId) pair, false for every later one until it is released. */
|
|
29
|
+
claim(source: string, eventId: string): Promise<boolean>;
|
|
30
|
+
/** Gives an ordinary delivery claim back while preserving client receipts and their outcomes. */
|
|
31
|
+
release(source: string, eventId: string): Promise<void>;
|
|
32
|
+
/** Runs once for the client namespace and key, replays the JSON outcome and refuses a different body. */
|
|
33
|
+
runClient<T>(request: InboxClientOperation, tx: unknown, effect: () => Promise<T>): Promise<T>;
|
|
34
|
+
/** Deletes at most one batch of completed client claims older than the cutoff. */
|
|
35
|
+
sweepClients(cutoff: Date, tx: unknown): Promise<number>;
|
|
36
|
+
/** Marks a pair as already claimed (a redelivery). */
|
|
37
|
+
seen(source: string, eventId: string): void;
|
|
38
|
+
/** Makes the next call of `operation` reject with `error`, once. */
|
|
39
|
+
failNext(operation: InboxOperation, error: Error): void;
|
|
40
|
+
/** Every claim, in order, duplicates included. */
|
|
41
|
+
readonly claims: ReadonlyArray<InboxEntry>;
|
|
42
|
+
/** The pairs held now: claimed and not released. */
|
|
43
|
+
readonly claimed: ReadonlyArray<InboxEntry>;
|
|
44
|
+
/** Every release, in order. */
|
|
45
|
+
readonly released: ReadonlyArray<InboxEntry>;
|
|
46
|
+
/** Every client claim with its manager; this double records transaction ownership, not database commit semantics. */
|
|
47
|
+
readonly clientEntries: ReadonlyArray<InboxClientEntry>;
|
|
48
|
+
/** Every sweep attempt with its cutoff and transaction, including a scripted failure. */
|
|
49
|
+
readonly sweeps: ReadonlyArray<InboxSweep>;
|
|
50
|
+
clear(): void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* `fakeInbox(clock?)` records delivery and client claims, so a spec asserts deduplication and the transaction passed by
|
|
54
|
+
* its service. Client calls replay JSON outcomes; `clock` gives retention tests a driven claim timestamp.
|
|
55
|
+
*
|
|
56
|
+
* `claim(source, eventId)` answers `true` for the first call of a (source, eventId) pair and `false` for every later one, like the
|
|
57
|
+
* real unique key; `release(source, eventId)` gives an ordinary delivery claim back while preserving client outcomes.
|
|
58
|
+
* `claims` is every claim in order, `claimed` the pairs that are held now, `released` every release, `seen(source, eventId)` marks a
|
|
59
|
+
* pair as already delivered (a redelivery), and `failNext(operation, error)` makes the next call of that operation reject once, so
|
|
60
|
+
* a spec drives the failure path.
|
|
61
|
+
*/
|
|
62
|
+
/** The inbox double. Pass a driven clock to test retention; omitted clocks stamp client claims at the Unix epoch. */
|
|
63
|
+
export declare function fakeInbox(clock?: {
|
|
64
|
+
now(): Date;
|
|
65
|
+
}): FakeInbox;
|
package/dist/inbox.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fakeInbox = fakeInbox;
|
|
4
|
+
const node_util_1 = require("node:util");
|
|
5
|
+
const entity_manager_ts_1 = require("./entity-manager.js");
|
|
6
|
+
const CLIENT_SWEEP_BATCH = 500;
|
|
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
|
+
/** Clone JSON values directly so toJSON cannot change the request or the stored outcome. */
|
|
10
|
+
const snapshot = (value, ancestors = new Set()) => {
|
|
11
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
12
|
+
return value;
|
|
13
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
14
|
+
return value === 0 ? 0 : value;
|
|
15
|
+
if (typeof value !== "object" || ancestors.has(value))
|
|
16
|
+
throw inboxError("IDEMPOTENCY_REQUEST_INVALID");
|
|
17
|
+
ancestors.add(value);
|
|
18
|
+
let result;
|
|
19
|
+
if (Array.isArray(value))
|
|
20
|
+
result = Array.from(value, (member) => snapshot(member, ancestors));
|
|
21
|
+
else {
|
|
22
|
+
const prototype = Object.getPrototypeOf(value);
|
|
23
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
24
|
+
throw inboxError("IDEMPOTENCY_REQUEST_INVALID");
|
|
25
|
+
result = Object.fromEntries(Object.entries(value).map(([key, member]) => [key, snapshot(member, ancestors)]));
|
|
26
|
+
}
|
|
27
|
+
ancestors.delete(value);
|
|
28
|
+
return result;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* `fakeInbox(clock?)` records delivery and client claims, so a spec asserts deduplication and the transaction passed by
|
|
32
|
+
* its service. Client calls replay JSON outcomes; `clock` gives retention tests a driven claim timestamp.
|
|
33
|
+
*
|
|
34
|
+
* `claim(source, eventId)` answers `true` for the first call of a (source, eventId) pair and `false` for every later one, like the
|
|
35
|
+
* real unique key; `release(source, eventId)` gives an ordinary delivery claim back while preserving client outcomes.
|
|
36
|
+
* `claims` is every claim in order, `claimed` the pairs that are held now, `released` every release, `seen(source, eventId)` marks a
|
|
37
|
+
* pair as already delivered (a redelivery), and `failNext(operation, error)` makes the next call of that operation reject once, so
|
|
38
|
+
* a spec drives the failure path.
|
|
39
|
+
*/
|
|
40
|
+
/** The inbox double. Pass a driven clock to test retention; omitted clocks stamp client claims at the Unix epoch. */
|
|
41
|
+
function fakeInbox(clock) {
|
|
42
|
+
const held = new Set();
|
|
43
|
+
const claims = [];
|
|
44
|
+
const released = [];
|
|
45
|
+
const clients = new Map();
|
|
46
|
+
const clientEntries = [];
|
|
47
|
+
const sweeps = [];
|
|
48
|
+
const failures = new Map();
|
|
49
|
+
const keyOf = (source, eventId) => `${source}\u0000${eventId}`;
|
|
50
|
+
const failIfScripted = (operation) => {
|
|
51
|
+
const error = failures.get(operation);
|
|
52
|
+
if (error === undefined)
|
|
53
|
+
return;
|
|
54
|
+
failures.delete(operation);
|
|
55
|
+
throw error;
|
|
56
|
+
};
|
|
57
|
+
async function runClient(request, tx, effect) {
|
|
58
|
+
failIfScripted("runClient");
|
|
59
|
+
const { principalId, operation, idempotencyKey } = request;
|
|
60
|
+
if (!principalId.trim() || principalId.includes(":") || !operation.trim() || operation.includes(":")
|
|
61
|
+
|| !idempotencyKey.trim() || idempotencyKey.length > 64) {
|
|
62
|
+
throw inboxError("IDEMPOTENCY_REQUEST_INVALID");
|
|
63
|
+
}
|
|
64
|
+
const source = `client:${principalId}:${operation}`;
|
|
65
|
+
const key = keyOf(source, idempotencyKey);
|
|
66
|
+
const body = snapshot(request.request);
|
|
67
|
+
claims.push({ source, eventId: idempotencyKey });
|
|
68
|
+
clientEntries.push({ source, eventId: idempotencyKey, tx, inTransaction: (0, entity_manager_ts_1.isTransactionManager)(tx) });
|
|
69
|
+
const previous = clients.get(key);
|
|
70
|
+
if (previous !== undefined) {
|
|
71
|
+
if (!(0, node_util_1.isDeepStrictEqual)(previous.request, body))
|
|
72
|
+
throw inboxError("IDEMPOTENCY_KEY_REUSED");
|
|
73
|
+
return snapshot(await previous.result);
|
|
74
|
+
}
|
|
75
|
+
if (held.has(key))
|
|
76
|
+
throw inboxError("IDEMPOTENCY_KEY_REUSED");
|
|
77
|
+
const entry = { request: body, claimedAt: clock === undefined ? new Date(0) : clock.now(), completed: false };
|
|
78
|
+
held.add(key);
|
|
79
|
+
clients.set(key, entry);
|
|
80
|
+
entry.result = Promise.resolve().then(effect).then((value) => {
|
|
81
|
+
let stored;
|
|
82
|
+
try {
|
|
83
|
+
stored = snapshot(value);
|
|
84
|
+
}
|
|
85
|
+
catch (cause) {
|
|
86
|
+
throw Object.assign(inboxError("INBOX_OUTCOME_INVALID"), { cause });
|
|
87
|
+
}
|
|
88
|
+
entry.completed = true;
|
|
89
|
+
return stored;
|
|
90
|
+
}).catch((error) => {
|
|
91
|
+
if (clients.get(key) === entry) {
|
|
92
|
+
clients.delete(key);
|
|
93
|
+
held.delete(key);
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
});
|
|
97
|
+
return snapshot(await entry.result);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
async claim(source, eventId) {
|
|
101
|
+
failIfScripted("claim");
|
|
102
|
+
claims.push({ source, eventId });
|
|
103
|
+
const key = keyOf(source, eventId);
|
|
104
|
+
if (held.has(key))
|
|
105
|
+
return false;
|
|
106
|
+
held.add(key);
|
|
107
|
+
return true;
|
|
108
|
+
},
|
|
109
|
+
async release(source, eventId) {
|
|
110
|
+
failIfScripted("release");
|
|
111
|
+
released.push({ source, eventId });
|
|
112
|
+
const key = keyOf(source, eventId);
|
|
113
|
+
if (!clients.has(key))
|
|
114
|
+
held.delete(key);
|
|
115
|
+
},
|
|
116
|
+
runClient,
|
|
117
|
+
async sweepClients(cutoff, tx) {
|
|
118
|
+
sweeps.push({ cutoff: new Date(cutoff), tx, inTransaction: (0, entity_manager_ts_1.isTransactionManager)(tx) });
|
|
119
|
+
failIfScripted("sweepClients");
|
|
120
|
+
let swept = 0;
|
|
121
|
+
for (const [key, entry] of clients) {
|
|
122
|
+
if (swept === CLIENT_SWEEP_BATCH)
|
|
123
|
+
break;
|
|
124
|
+
if (!entry.completed || entry.claimedAt >= cutoff)
|
|
125
|
+
continue;
|
|
126
|
+
clients.delete(key);
|
|
127
|
+
held.delete(key);
|
|
128
|
+
swept += 1;
|
|
129
|
+
}
|
|
130
|
+
return swept;
|
|
131
|
+
},
|
|
132
|
+
/** Marks a pair as already claimed, so the next `claim` of it answers `false` (a redelivery). */
|
|
133
|
+
seen(source, eventId) {
|
|
134
|
+
held.add(keyOf(source, eventId));
|
|
135
|
+
},
|
|
136
|
+
/** Makes the next call of `operation` reject with `error`, once. */
|
|
137
|
+
failNext(operation, error) {
|
|
138
|
+
failures.set(operation, error);
|
|
139
|
+
},
|
|
140
|
+
/** Every `claim`, in order, duplicates included. */
|
|
141
|
+
get claims() {
|
|
142
|
+
return [...claims];
|
|
143
|
+
},
|
|
144
|
+
/** The pairs held now: claimed and not released. */
|
|
145
|
+
get claimed() {
|
|
146
|
+
return [...held].map((key) => {
|
|
147
|
+
const [source = "", eventId = ""] = key.split("\u0000");
|
|
148
|
+
return { source, eventId };
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
/** Every `release`, in order. */
|
|
152
|
+
get released() {
|
|
153
|
+
return [...released];
|
|
154
|
+
},
|
|
155
|
+
/** Every client claim, including repeats, with the domain transaction it used. */
|
|
156
|
+
get clientEntries() {
|
|
157
|
+
return [...clientEntries];
|
|
158
|
+
},
|
|
159
|
+
/** Every retention attempt with its cutoff and domain transaction, including a scripted failure. */
|
|
160
|
+
get sweeps() {
|
|
161
|
+
return [...sweeps];
|
|
162
|
+
},
|
|
163
|
+
clear() {
|
|
164
|
+
held.clear();
|
|
165
|
+
claims.length = 0;
|
|
166
|
+
released.length = 0;
|
|
167
|
+
failures.clear();
|
|
168
|
+
clients.clear();
|
|
169
|
+
clientEntries.length = 0;
|
|
170
|
+
sweeps.length = 0;
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { Config } from "jest";
|
|
2
|
+
import { mock, createMock } from "./mock.ts";
|
|
3
|
+
import { mockEntityManager, fakeTransaction } from "./entity-manager.ts";
|
|
4
|
+
import { FakeClock } from "./clock.ts";
|
|
5
|
+
import { fakeCache } from "./cache.ts";
|
|
6
|
+
import { fakeLock } from "./lock.ts";
|
|
7
|
+
import { recordingEventBus } from "./event-bus.ts";
|
|
8
|
+
import { recordingQueueOutbox } from "./queue.ts";
|
|
9
|
+
import { fakeInbox } from "./inbox.ts";
|
|
10
|
+
import { builder } from "./builders.ts";
|
|
11
|
+
import { fakeIds, FakeIds } from "./ids.ts";
|
|
12
|
+
export type { MockOf } from "./mock.ts";
|
|
13
|
+
export type { MockEntityManager, FakeTransaction, EntityClass } from "./entity-manager.ts";
|
|
14
|
+
export type { FakeCache } from "./cache.ts";
|
|
15
|
+
export type { FakeLock } from "./lock.ts";
|
|
16
|
+
export type { RecordingEventBus } from "./event-bus.ts";
|
|
17
|
+
export type { RecordingQueueOutbox } from "./queue.ts";
|
|
18
|
+
export type { FakeInbox } from "./inbox.ts";
|
|
19
|
+
export type { RefusalReason } from "./matchers.ts";
|
|
20
|
+
/** The coverage scope `starci app sync` renders from the slot manifest: the measured roots (a directory pattern per measured slot, a placeholder folder written as a star), the logic roles measured in them and the directories inside a root that are never measured (each ending in a double star). */
|
|
21
|
+
export interface StarciCoverageScope {
|
|
22
|
+
readonly roots: readonly string[];
|
|
23
|
+
readonly roles: readonly string[];
|
|
24
|
+
readonly excludes: readonly string[];
|
|
25
|
+
}
|
|
26
|
+
export interface StarciJestOptions {
|
|
27
|
+
readonly coverage: StarciCoverageScope;
|
|
28
|
+
}
|
|
29
|
+
/** One project of the config (`unit`, `integration`, `e2e`, `contract`). */
|
|
30
|
+
export type StarciJestProject = Exclude<NonNullable<Config["projects"]>[number], string>;
|
|
31
|
+
/**
|
|
32
|
+
* The jest config `starciJestConfig` returns: jest's `Config` with its `projects` all objects, its `coverageReporters` allowing the path of a custom reporter (the lcov reporter of this preset is one)
|
|
33
|
+
* and its `coverageThreshold` keyed by glob only: jest accepts both, its own `Config` typing requires a `global` threshold and leaves out the reporter path.
|
|
34
|
+
*/
|
|
35
|
+
export type StarciJestConfig = Omit<Config, "projects" | "coverageReporters" | "coverageThreshold"> & {
|
|
36
|
+
projects: StarciJestProject[];
|
|
37
|
+
coverageThreshold?: Record<string, {
|
|
38
|
+
lines: number;
|
|
39
|
+
branches: number;
|
|
40
|
+
functions: number;
|
|
41
|
+
statements: number;
|
|
42
|
+
}>;
|
|
43
|
+
coverageReporters: Array<string | [string, Record<string, unknown>]>;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Coverage is measured where logic runs: every file `<name>.<role>.ts` of a LOGIC role (service, policy, projection, guard, mapper, client, ...)
|
|
47
|
+
* inside the roots the slot manifest marks `coverage: required` (the modules of the back end: domain, integrations, platform, projections).
|
|
48
|
+
* The features (handlers, resolvers, controllers, consumers, processors, steps, sagas, cli commands) are thin by rule (BE_FEATURE_THIN) and are
|
|
49
|
+
* never measured. The scope is NOT written here: `starci app sync` renders it from the manifest (scripts/hfs/coverage-scope.mjs) into the repository's
|
|
50
|
+
* jest.config.js as `starciJestConfig({ coverage: { roots, roles, excludes } })`, and the same derivation renders Sonar's
|
|
51
|
+
* `sonar.coverage.exclusions` and the Codecov paths and components, so the three can never disagree. Every measured file must reach 100 on every metric.
|
|
52
|
+
* The unit run also writes `coverage/lcov.info`: Sonar and Codecov import it.
|
|
53
|
+
*/
|
|
54
|
+
declare const COVERAGE_EXCLUDES: string[];
|
|
55
|
+
/** The four metrics, each held at 100 per file. */
|
|
56
|
+
declare const COVERAGE_THRESHOLD: Readonly<{
|
|
57
|
+
lines: 100;
|
|
58
|
+
branches: 100;
|
|
59
|
+
functions: 100;
|
|
60
|
+
statements: 100;
|
|
61
|
+
}>;
|
|
62
|
+
/** The glob one measured root stands for: every file of a logic role below it. */
|
|
63
|
+
declare function rootGlob(root: string, roles: readonly string[]): string;
|
|
64
|
+
/**
|
|
65
|
+
* jest's `collectCoverageFrom` of a coverage scope `{ roots, roles, excludes }`: the glob of every root, then the none directories inside a
|
|
66
|
+
* root (`excludes`, each a glob that ends in `**`) and the generated folders as negations.
|
|
67
|
+
*/
|
|
68
|
+
declare function collectCoverageFrom(coverage: StarciCoverageScope): string[];
|
|
69
|
+
/**
|
|
70
|
+
* Sonar `sonar.exclusions`: specs, e2e specs, dist and coverage output are not analysed at all. The list lives in sonar-exclusions.ts, a
|
|
71
|
+
* require-free module whose built file @starci/hfs also ships (packages/hfs/runtime, scripts/hfs/sync-runtime.mjs), so `starci app scaffold`
|
|
72
|
+
* renders the same exclusions before any app dependency is installed.
|
|
73
|
+
*/
|
|
74
|
+
declare function sonarExclusions(): string;
|
|
75
|
+
/** The unit project transpiles each file independently; helpers stay in tslib, source maps distinguish generated metadata, and type checks run separately. */
|
|
76
|
+
declare const UNIT_COMPILER_OPTIONS: Readonly<{
|
|
77
|
+
isolatedModules: true;
|
|
78
|
+
importHelpers: true;
|
|
79
|
+
sourceMap: true;
|
|
80
|
+
}>;
|
|
81
|
+
/**
|
|
82
|
+
* The path aliases of a StarCi back end: the same three `paths` the managed `tsconfig.json` declares, so jest resolves what
|
|
83
|
+
* `tsc` resolves. They are part of the preset, not an option: a repository has no other alias.
|
|
84
|
+
*/
|
|
85
|
+
declare const MODULE_NAME_MAPPER: Readonly<Record<string, string>>;
|
|
86
|
+
/** The test folders under `src/tests/` that a project other than `unit` owns (owner test layout 2026-09-30). */
|
|
87
|
+
declare const TEST_KIND_FOLDERS: readonly string[];
|
|
88
|
+
/**
|
|
89
|
+
* The whole jest config of a Nest repository: projects `unit` (colocated `<name>.spec.ts`), `integration`
|
|
90
|
+
* (`src/tests/integration/<capability>/<name>.integration-spec.ts`), `e2e` (`src/tests/e2e/<area>/<name>.e2e-spec.ts`) and
|
|
91
|
+
* `contract` (`src/tests/contract/<provider>/<name>.contract-spec.ts`). It takes ONE option, the coverage scope: the repository's `jest.config.js` is a managed file
|
|
92
|
+
* (R05), rendered by `starci app sync` as `module.exports = require("@starci/jest-preset").starciJestConfig({ coverage: { roots, roles, excludes } })`, the scope derived from the slot manifest.
|
|
93
|
+
*
|
|
94
|
+
* The managed scripts select one project each (`test` = unit, `test:integration`, `test:e2e`, `test:contract`); the
|
|
95
|
+
* contract project is never part of `test` or `test:e2e`, and a contract spec skips itself without sandbox config. The
|
|
96
|
+
* test tree compiles against `src/tests/tsconfig.json`, the nearest config of every file under `src/tests/`. The
|
|
97
|
+
* integration, e2e and contract projects share the world's `global-setup.ts`/`global-teardown.ts` (`src/tests/world/`) and
|
|
98
|
+
* run their spec files up to `min(--maxWorkers, slots)` at once, each in a worker process of its own bound to one data slot
|
|
99
|
+
* of the test world (`world-runner.js`; a slot's data is reset when a file boots, so no two files ever share a slot). A declared lite app instead runs its existing integration and contract specs without a world, on jest's stock runner. The unit project alone has a `setupFilesAfterEnv` (the Outcome matchers).
|
|
100
|
+
* Coverage is collected from the logic roles of the measured roots only (`coverage` option), with a per-file threshold of 100 on lines, branches, functions and
|
|
101
|
+
* statements: the `test` script runs the unit project with `--coverage`, fails below it and writes `coverage/lcov.info` for Sonar. It uses v8: istanbul instruments the helpers TypeScript emits (`__decorate`, `__param`, `__awaiter`, interop wrappers)
|
|
102
|
+
* as thousands of branches no spec can cover, while v8 measures the real source.
|
|
103
|
+
*/
|
|
104
|
+
/** The runner of the integration, e2e and contract projects (`world-runner.js`): every spec file in a worker process of its own. */
|
|
105
|
+
declare const WORLD_RUNNER: string;
|
|
106
|
+
/**
|
|
107
|
+
* True when the repository at `root` holds a file the coverage glob matches: jest refuses a threshold key that matches no covered file,
|
|
108
|
+
* and a back end may have no file of a measured root yet (no projection, no integration), so a root without files gets no key (its files,
|
|
109
|
+
* once added, are measured and held at 100 like every other).
|
|
110
|
+
*/
|
|
111
|
+
declare function hasCoverageSubjects(root: string, glob: string): boolean;
|
|
112
|
+
/** The whole jest config: projects `unit`, `integration`, `e2e` and `contract`. It takes the coverage scope the managed repository config renders; without it it refuses. */
|
|
113
|
+
declare function starciJestConfig(options: StarciJestOptions): StarciJestConfig;
|
|
114
|
+
export { starciJestConfig, TEST_KIND_FOLDERS, MODULE_NAME_MAPPER, mock, createMock, mockEntityManager, fakeTransaction, FakeClock, fakeCache, fakeLock, recordingEventBus, recordingQueueOutbox, fakeInbox, builder, fakeIds, FakeIds, collectCoverageFrom, rootGlob, sonarExclusions, hasCoverageSubjects, COVERAGE_EXCLUDES, COVERAGE_THRESHOLD, UNIT_COMPILER_OPTIONS, WORLD_RUNNER, };
|