@shirudo/ddd-kit 3.0.0-rc.4 → 3.0.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -20
- package/dist/chunks/{errors.d.ts → kit-errors.d.ts} +272 -48
- package/dist/chunks/{errors.js → kit-errors.js} +275 -57
- package/dist/chunks/kit-errors.js.map +1 -0
- package/dist/chunks/ports.js +910 -110
- package/dist/chunks/ports.js.map +1 -1
- package/dist/chunks/snapshot-store.d.ts +925 -1338
- package/dist/http.d.ts +2 -3
- package/dist/http.js +1 -1
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +3469 -2432
- package/dist/index.js +5663 -4869
- package/dist/index.js.map +1 -1
- package/dist/money.d.ts +11 -11
- package/dist/money.js +9 -9
- package/dist/money.js.map +1 -1
- package/dist/{presentation.d.ts → public-errors.d.ts} +5 -6
- package/dist/{presentation.js → public-errors.js} +5 -5
- package/dist/public-errors.js.map +1 -0
- package/dist/testing.d.ts +61 -11
- package/dist/testing.js +526 -56
- package/dist/testing.js.map +1 -1
- package/package.json +18 -18
- package/dist/chunks/deep-equal-except.js +0 -639
- package/dist/chunks/deep-equal-except.js.map +0 -1
- package/dist/chunks/errors.js.map +0 -1
- package/dist/chunks/utils.d.ts +0 -110
- package/dist/presentation.js.map +0 -1
- package/dist/utils.d.ts +0 -2
- package/dist/utils.js +0 -3
|
@@ -1,64 +1,194 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { l as DomainError } from "./kit-errors.js";
|
|
2
2
|
import { Result } from "@shirudo/result";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
//#region src/internal/json-value.d.ts
|
|
4
|
+
/** A primitive value represented without loss by JSON. */
|
|
5
|
+
type JsonPrimitive = boolean | null | number | string;
|
|
6
|
+
/** A recursively JSON-safe value. Runtime validation rejects lossy shapes. */
|
|
7
|
+
type JsonValue = JsonPrimitive | ReadonlyArray<JsonValue> | {
|
|
8
|
+
readonly [key: string]: JsonValue;
|
|
9
|
+
};
|
|
10
|
+
/** A JSON-safe object. */
|
|
11
|
+
type JsonObject = {
|
|
12
|
+
readonly [key: string]: JsonValue;
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/application/cqrs/command/command.d.ts
|
|
5
16
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
17
|
+
* Marker interface for Commands.
|
|
18
|
+
* Commands represent write operations that change system state.
|
|
19
|
+
* They should be immutable and contain all data needed to perform the operation.
|
|
20
|
+
*
|
|
21
|
+
* This interface can be used as a type marker even when using external frameworks
|
|
22
|
+
* (e.g., RabbitMQ, AWS SQS) to ensure type safety across different bus implementations.
|
|
9
23
|
*
|
|
10
24
|
* @example
|
|
11
|
-
* ```
|
|
12
|
-
* type
|
|
13
|
-
*
|
|
25
|
+
* ```typescript
|
|
26
|
+
* type CreateOrderCommand = Command & {
|
|
27
|
+
* type: "CreateOrder";
|
|
28
|
+
* customerId: string;
|
|
29
|
+
* items: OrderItem[];
|
|
30
|
+
* };
|
|
31
|
+
* ```
|
|
14
32
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
33
|
+
* @example Using with external frameworks (RabbitMQ, etc.)
|
|
34
|
+
* ```typescript
|
|
35
|
+
* // Define command using Command marker
|
|
36
|
+
* type CreateOrderCommand = Command & {
|
|
37
|
+
* type: "CreateOrder";
|
|
38
|
+
* customerId: string;
|
|
39
|
+
* };
|
|
40
|
+
*
|
|
41
|
+
* // Handler can be typed with CommandHandler even for external frameworks
|
|
42
|
+
* const handler: CommandHandler<CreateOrderCommand, OrderId> = async (cmd) => {
|
|
43
|
+
* // ... handler logic
|
|
44
|
+
* return ok(orderId);
|
|
45
|
+
* };
|
|
46
|
+
*
|
|
47
|
+
* // The consumer owns this runtime decoder. It checks byte and collection
|
|
48
|
+
* // ceilings, parses to unknown, allow-lists fields, and constructs domain types.
|
|
49
|
+
* declare function decodeCreateOrderCommand(
|
|
50
|
+
* body: Uint8Array,
|
|
51
|
+
* principal: AuthenticatedPrincipal,
|
|
52
|
+
* ): Result<CreateOrderCommand, InvalidCommand>;
|
|
53
|
+
* declare function decodeMessageId(
|
|
54
|
+
* value: unknown,
|
|
55
|
+
* ): Result<string, InvalidTransportMetadata>;
|
|
56
|
+
* declare function createOrderDeliveryKey(messageId: string): string;
|
|
57
|
+
*
|
|
58
|
+
* // This application service invokes the handler through withIdempotentCommit.
|
|
59
|
+
* // createOrderDeliveryKey scopes the message id by consumer. The service
|
|
60
|
+
* // fingerprints the complete CreateOrder intention and commits that claim,
|
|
61
|
+
* // the aggregate write, outbox entries, and outcome together.
|
|
62
|
+
* declare function executeIdempotentCreateOrder(
|
|
63
|
+
* deliveryKey: string,
|
|
64
|
+
* command: CreateOrderCommand,
|
|
65
|
+
* ): Promise<Result<OrderId, string>>;
|
|
66
|
+
*
|
|
67
|
+
* // Register with RabbitMQ or another external bus.
|
|
68
|
+
* rabbitMQChannel.consume("order.commands", async (message) => {
|
|
69
|
+
* const messageId = decodeMessageId(message.properties.messageId);
|
|
70
|
+
* if (messageId.isErr()) {
|
|
71
|
+
* rabbitMQChannel.reject(message, false); // missing identity: dead-letter
|
|
72
|
+
* return;
|
|
73
|
+
* }
|
|
74
|
+
* const deliveryKey = createOrderDeliveryKey(messageId.value);
|
|
75
|
+
* const principal = authenticateProducer(message.properties.headers);
|
|
76
|
+
* const decoded = decodeCreateOrderCommand(message.content, principal);
|
|
77
|
+
* if (decoded.isErr()) {
|
|
78
|
+
* rabbitMQChannel.reject(message, false); // invalid input: dead-letter, do not retry
|
|
79
|
+
* return;
|
|
80
|
+
* }
|
|
81
|
+
* const outcome = await executeIdempotentCreateOrder(
|
|
82
|
+
* deliveryKey,
|
|
83
|
+
* decoded.value,
|
|
84
|
+
* );
|
|
85
|
+
* await recordCommandOutcome(deliveryKey, outcome);
|
|
86
|
+
* rabbitMQChannel.ack(message);
|
|
87
|
+
* });
|
|
17
88
|
* ```
|
|
18
89
|
*/
|
|
19
|
-
|
|
20
|
-
readonly
|
|
21
|
-
}
|
|
90
|
+
interface Command {
|
|
91
|
+
readonly type: string;
|
|
92
|
+
}
|
|
22
93
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
94
|
+
* Versioned Published Language for a command that crosses a process or
|
|
95
|
+
* Bounded-Context boundary. Unlike a local {@link Command}, its payload is
|
|
96
|
+
* JSON-safe data rather than a domain object graph. Map value objects to their
|
|
97
|
+
* wire DTOs at this boundary; for example, use `MoneyDto` instead of `Money`.
|
|
98
|
+
*/
|
|
99
|
+
interface PublishedCommand<TType extends string = string, TPayload extends JsonValue = JsonValue> extends Command {
|
|
100
|
+
readonly type: TType;
|
|
101
|
+
readonly version: number;
|
|
102
|
+
readonly payload: TPayload;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Handler for executing commands.
|
|
106
|
+
* Commands return Result for explicit error handling.
|
|
107
|
+
* Commands may modify system state. When a caller can retry or a broker can
|
|
108
|
+
* redeliver, the application service must enforce idempotency; this handler
|
|
109
|
+
* type alone does not provide it.
|
|
26
110
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* counter without persistence (resets to 1 on restart, collides with
|
|
35
|
-
* prior runs), a sequential id derived from non-atomic state.
|
|
111
|
+
* This type can be used to mark handlers even when using external frameworks
|
|
112
|
+
* (e.g., RabbitMQ, AWS SQS, Kafka) to ensure type safety and consistency.
|
|
113
|
+
*
|
|
114
|
+
* @template C - The command type (must extend Command)
|
|
115
|
+
* @template R - The result type
|
|
116
|
+
* @template E - The error channel type. Defaults to `string`; widen it (e.g.
|
|
117
|
+
* to a `DomainError` union) to carry typed failures through the bus.
|
|
36
118
|
*
|
|
37
119
|
* @example
|
|
38
|
-
* ```
|
|
39
|
-
*
|
|
120
|
+
* ```typescript
|
|
121
|
+
* const handler: CommandHandler<CreateOrderCommand, OrderId> = async (cmd) => {
|
|
122
|
+
* const order = Order.create(cmd.customerId, cmd.items);
|
|
123
|
+
* repository.add(order);
|
|
124
|
+
* return ok(order.id);
|
|
125
|
+
* };
|
|
126
|
+
* ```
|
|
40
127
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
128
|
+
* @example Using with external frameworks
|
|
129
|
+
* ```typescript
|
|
130
|
+
* // Handler typed with CommandHandler for type safety
|
|
131
|
+
* const createOrderHandler: CommandHandler<CreateOrderCommand, OrderId> = async (cmd) => {
|
|
132
|
+
* // ... handler logic
|
|
133
|
+
* return ok(orderId);
|
|
134
|
+
* };
|
|
135
|
+
*
|
|
136
|
+
* // The broker adapter validates before calling the application handler.
|
|
137
|
+
* rabbitMQChannel.consume("commands", async (msg) => {
|
|
138
|
+
* const messageId = decodeMessageId(msg.properties.messageId);
|
|
139
|
+
* if (messageId.isErr()) {
|
|
140
|
+
* rabbitMQChannel.reject(msg, false);
|
|
141
|
+
* return;
|
|
142
|
+
* }
|
|
143
|
+
* const deliveryKey = createOrderDeliveryKey(messageId.value);
|
|
144
|
+
* const principal = authenticateProducer(msg.properties.headers);
|
|
145
|
+
* const decoded = decodeCreateOrderCommand(msg.content, principal);
|
|
146
|
+
* if (decoded.isErr()) {
|
|
147
|
+
* rabbitMQChannel.reject(msg, false); // malformed or over limit
|
|
148
|
+
* return;
|
|
149
|
+
* }
|
|
150
|
+
* // executeIdempotentCreateOrder invokes createOrderHandler through the same
|
|
151
|
+
* // atomic withIdempotentCommit boundary described in the first example.
|
|
152
|
+
* const outcome = await executeIdempotentCreateOrder(
|
|
153
|
+
* deliveryKey,
|
|
154
|
+
* decoded.value,
|
|
155
|
+
* );
|
|
156
|
+
* await recordCommandOutcome(deliveryKey, outcome);
|
|
157
|
+
* rabbitMQChannel.ack(msg);
|
|
158
|
+
* });
|
|
43
159
|
* ```
|
|
160
|
+
*/
|
|
161
|
+
type CommandHandler<C extends Command, R, E = string> = (cmd: C) => Promise<Result<R, E>>;
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/domain/aggregate/aggregate-address.d.ts
|
|
164
|
+
/**
|
|
165
|
+
* Stable value address of one aggregate instance.
|
|
44
166
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
167
|
+
* Aggregate ids are type-scoped, so the raw id alone is not globally unique:
|
|
168
|
+
* `SalesOrder 1` and `FulfillmentOrder 1` are different aggregates. Event
|
|
169
|
+
* streams, snapshots, committed-event sources, and projection checkpoints
|
|
170
|
+
* therefore carry both fields instead of defining boundary-specific variants.
|
|
171
|
+
*
|
|
172
|
+
* `aggregateType` is a stable technical stream category. Renaming it changes
|
|
173
|
+
* persistence keys and orphans checkpoints unless the stored addresses are
|
|
174
|
+
* migrated. When bounded contexts share infrastructure and reuse a domain
|
|
175
|
+
* name, qualify it at the source (`sales.order`, `fulfillment.order`). The kit
|
|
176
|
+
* deliberately adds no separate `boundedContext` field: qualification remains
|
|
177
|
+
* the consumer's naming decision.
|
|
49
178
|
*/
|
|
50
|
-
interface
|
|
51
|
-
|
|
179
|
+
interface AggregateAddress<TAggregateId extends string = string> {
|
|
180
|
+
readonly aggregateType: string;
|
|
181
|
+
readonly aggregateId: TAggregateId;
|
|
52
182
|
}
|
|
53
183
|
//#endregion
|
|
54
|
-
//#region src/
|
|
184
|
+
//#region src/domain/event/clock.d.ts
|
|
55
185
|
/**
|
|
56
186
|
* Clock function producing a valid `Date` for the current instant.
|
|
57
187
|
* Event-clock reads throw `TypeError` when the result is invalid.
|
|
58
188
|
*/
|
|
59
189
|
type ClockFactory = () => Date;
|
|
60
190
|
//#endregion
|
|
61
|
-
//#region src/
|
|
191
|
+
//#region src/domain/event/domain-event.d.ts
|
|
62
192
|
/**
|
|
63
193
|
* Factory function producing a fresh, unique event identifier for each call.
|
|
64
194
|
*
|
|
@@ -170,9 +300,11 @@ interface DomainEvent<T extends string, P = void> {
|
|
|
170
300
|
* Use 1 for the initial schema version.
|
|
171
301
|
*
|
|
172
302
|
* This is the event PAYLOAD schema version, not a persisted aggregate
|
|
173
|
-
* position. Commit positions live on `CommittedDomainEvent`.
|
|
303
|
+
* position. Commit positions live on `CommittedDomainEvent`. It is
|
|
304
|
+
* also not `AggregateSnapshot.schemaVersion`: that field versions the
|
|
305
|
+
* stored snapshot state shape. The two evolve independently.
|
|
174
306
|
*/
|
|
175
|
-
readonly
|
|
307
|
+
readonly schemaVersion: number;
|
|
176
308
|
/**
|
|
177
309
|
* Optional metadata for traceability, correlation, and auditing.
|
|
178
310
|
* Includes correlationId, conversationId, causationId, userId, source, and
|
|
@@ -200,7 +332,7 @@ interface UncommittedDomainEvent<T extends string, P = void> {
|
|
|
200
332
|
readonly aggregateId?: string;
|
|
201
333
|
readonly aggregateType?: string;
|
|
202
334
|
readonly payload: P;
|
|
203
|
-
readonly
|
|
335
|
+
readonly schemaVersion: number;
|
|
204
336
|
}
|
|
205
337
|
/** Upper-bound alias for any uncommitted domain-event shape. */
|
|
206
338
|
type AnyUncommittedDomainEvent = UncommittedDomainEvent<string, unknown>;
|
|
@@ -212,7 +344,7 @@ type PendingDomainEvent<TEvent extends AnyDomainEvent> = TEvent | UncommittedDom
|
|
|
212
344
|
interface CreateUncommittedDomainEventOptions {
|
|
213
345
|
readonly aggregateId?: string;
|
|
214
346
|
readonly aggregateType?: string;
|
|
215
|
-
readonly
|
|
347
|
+
readonly schemaVersion?: number;
|
|
216
348
|
}
|
|
217
349
|
/**
|
|
218
350
|
* Shared option bag for the `createDomainEvent*` factories.
|
|
@@ -239,7 +371,7 @@ interface CreateDomainEventOptions {
|
|
|
239
371
|
/**
|
|
240
372
|
* Override for the default schema version (1).
|
|
241
373
|
*/
|
|
242
|
-
|
|
374
|
+
schemaVersion?: number;
|
|
243
375
|
/**
|
|
244
376
|
* Event metadata: correlation, causation, user, source, custom fields.
|
|
245
377
|
*/
|
|
@@ -258,7 +390,7 @@ interface DomainEventStamp {
|
|
|
258
390
|
interface CreateDomainEventFromFactsOptions extends DomainEventStamp {
|
|
259
391
|
readonly aggregateId?: string;
|
|
260
392
|
readonly aggregateType?: string;
|
|
261
|
-
readonly
|
|
393
|
+
readonly schemaVersion?: number;
|
|
262
394
|
}
|
|
263
395
|
/** Overrides accepted when an application-shell factory creates a stamp. */
|
|
264
396
|
interface CreateDomainEventStampOptions {
|
|
@@ -272,6 +404,15 @@ interface DomainEventFactoryOptions {
|
|
|
272
404
|
readonly eventIdFactory?: EventIdFactory;
|
|
273
405
|
/** Event-recording clock. Defaults to `() => new Date()`. */
|
|
274
406
|
readonly clock?: ClockFactory;
|
|
407
|
+
/**
|
|
408
|
+
* Origin stamped on every event this factory mints, unless the call site
|
|
409
|
+
* names one itself.
|
|
410
|
+
*
|
|
411
|
+
* A plain value, not a factory like the two above. Those produce a new
|
|
412
|
+
* value for each event. An origin identifies the system that mints them
|
|
413
|
+
* and does not change between two of them.
|
|
414
|
+
*/
|
|
415
|
+
readonly source?: string;
|
|
275
416
|
}
|
|
276
417
|
/**
|
|
277
418
|
* Instance-bound event constructor. Each factory permanently captures its
|
|
@@ -293,27 +434,6 @@ interface DomainEventFactory {
|
|
|
293
434
|
*/
|
|
294
435
|
readonly now: () => Date;
|
|
295
436
|
}
|
|
296
|
-
/**
|
|
297
|
-
* Creates an immutable, instance-bound domain-event factory.
|
|
298
|
-
*
|
|
299
|
-
* The supplied functions are read once and captured by value. The returned
|
|
300
|
-
* object is frozen, so another request, test, or library cannot replace its
|
|
301
|
-
* policy. Its {@link DomainEventFactory.createStamp} method is the
|
|
302
|
-
* application-shell bridge that records an accepted aggregate decision.
|
|
303
|
-
* Passing the factory through `AggregateConfig`
|
|
304
|
-
* enables the explicitly named convenience methods, whose defaults read time
|
|
305
|
-
* and randomness.
|
|
306
|
-
*
|
|
307
|
-
* @example
|
|
308
|
-
* ```ts
|
|
309
|
-
* const domainEvents = createDomainEventFactory({
|
|
310
|
-
* eventIdFactory: () => uuidv7(),
|
|
311
|
-
* clock: () => new Date(),
|
|
312
|
-
* });
|
|
313
|
-
* order.confirm();
|
|
314
|
-
* recordPendingEvents(order, domainEvents);
|
|
315
|
-
* ```
|
|
316
|
-
*/
|
|
317
437
|
declare function createDomainEventFactory(options?: DomainEventFactoryOptions): DomainEventFactory;
|
|
318
438
|
/**
|
|
319
439
|
* Immutable UUID-v4/platform-clock factory used by the top-level
|
|
@@ -354,7 +474,7 @@ declare function createDomainEventFromFacts<T extends string, P>(type: T, payloa
|
|
|
354
474
|
* const newEvent = createDomainEvent(
|
|
355
475
|
* "OrderShipped",
|
|
356
476
|
* { orderId: "123" },
|
|
357
|
-
* { metadata: copyMetadata(previousEvent, { causationId: previousEvent.
|
|
477
|
+
* { metadata: copyMetadata(previousEvent, { causationId: previousEvent.eventId }) }
|
|
358
478
|
* );
|
|
359
479
|
* ```
|
|
360
480
|
*/
|
|
@@ -374,809 +494,502 @@ declare function copyMetadata(sourceEvent: AnyDomainEvent, additionalMetadata?:
|
|
|
374
494
|
*/
|
|
375
495
|
declare function mergeMetadata(...metadataObjects: Array<EventMetadata | undefined>): EventMetadata;
|
|
376
496
|
//#endregion
|
|
377
|
-
//#region src/
|
|
378
|
-
type Version = number & {
|
|
379
|
-
readonly __v: true;
|
|
380
|
-
};
|
|
497
|
+
//#region src/messaging/committed-event.d.ts
|
|
381
498
|
/**
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
* @template TState - The type of the aggregate state
|
|
499
|
+
* Gap-proof position finalized by the event source at the persistence
|
|
500
|
+
* boundary. It is deliberately separate from `DomainEvent`: these values
|
|
501
|
+
* describe a stored commit, not the business fact itself.
|
|
387
502
|
*/
|
|
388
|
-
interface
|
|
389
|
-
/**
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
readonly
|
|
393
|
-
/**
|
|
394
|
-
|
|
395
|
-
*/
|
|
396
|
-
readonly version: Version;
|
|
397
|
-
/**
|
|
398
|
-
* Timestamp when the snapshot was created.
|
|
399
|
-
*/
|
|
400
|
-
readonly snapshotAt: Date;
|
|
503
|
+
interface CommitPosition {
|
|
504
|
+
/** Aggregate OCC version reached by this eventful commit. */
|
|
505
|
+
readonly aggregateVersion: number;
|
|
506
|
+
/** Zero-based event index inside this aggregate commit. */
|
|
507
|
+
readonly commitSequence: number;
|
|
508
|
+
/** Total number of events emitted by this aggregate commit. */
|
|
509
|
+
readonly commitSize: number;
|
|
401
510
|
/**
|
|
402
|
-
*
|
|
403
|
-
* `
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
511
|
+
* Aggregate version of the immediately preceding EVENTFUL commit for this
|
|
512
|
+
* qualified aggregate source, or `null` when this is its first eventful
|
|
513
|
+
* commit. State-only persistence is intentionally absent from this chain.
|
|
514
|
+
*
|
|
515
|
+
* The outbox/event-store adapter owns this value. It must read and advance
|
|
516
|
+
* the source head atomically with inserting the committed event envelope;
|
|
517
|
+
* application orchestration cannot derive it from the Unit of Work's OCC
|
|
518
|
+
* receipt because state-only commits are intentionally absent here.
|
|
409
519
|
*/
|
|
410
|
-
readonly
|
|
520
|
+
readonly previousEventfulAggregateVersion: number | null;
|
|
411
521
|
}
|
|
412
522
|
/**
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
* `EventSourcedAggregate`. Repository ports use this interface as their
|
|
416
|
-
* aggregate type rather than depending on concrete base classes, so persistence
|
|
417
|
-
* orchestration does not take a compile-time
|
|
418
|
-
* dependency on the aggregate hierarchy.
|
|
419
|
-
*
|
|
420
|
-
* Full per-member documentation lives on the concrete `BaseAggregate`
|
|
421
|
-
* class; the interface is intentionally terse to avoid drift. Persistence
|
|
422
|
-
* facts are readable, but acknowledgement and pending-event disposal are not
|
|
423
|
-
* part of this surface. `withCommit` and `UnitOfWork` hold that authority.
|
|
424
|
-
*
|
|
425
|
-
* @template TId - The aggregate root identifier (branded via `Id<Tag>`)
|
|
426
|
-
* @template TEvent - The domain-event union, defaults to `never`
|
|
523
|
+
* Commit information known by the application transaction before the outbox
|
|
524
|
+
* source has linked this eventful commit to its predecessor.
|
|
427
525
|
*/
|
|
428
|
-
|
|
429
|
-
readonly id: TId;
|
|
430
|
-
readonly version: Version;
|
|
431
|
-
readonly pendingEvents: ReadonlyArray<PendingDomainEvent<TEvent>>;
|
|
432
|
-
}
|
|
526
|
+
type EventCommitCandidatePosition = Omit<CommitPosition, "previousEventfulAggregateVersion">;
|
|
433
527
|
/**
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
* @template TId - The aggregate root identifier
|
|
438
|
-
* @template TEvent - The union type of all domain events
|
|
528
|
+
* A bare domain event prepared for the transactional outbox. The outbox source
|
|
529
|
+
* owns the predecessor link and turns this candidate into a
|
|
530
|
+
* {@link CommittedDomainEvent} when it persists the record.
|
|
439
531
|
*/
|
|
440
|
-
interface
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
* recoverable failure at the infrastructure boundary.
|
|
445
|
-
*/
|
|
446
|
-
loadFromHistory(history: ReadonlyArray<TEvent>): Result<void, DomainError>;
|
|
532
|
+
interface EventCommitCandidate<Evt extends AnyDomainEvent> {
|
|
533
|
+
readonly event: Evt;
|
|
534
|
+
readonly source: AggregateAddress;
|
|
535
|
+
readonly position: EventCommitCandidatePosition;
|
|
447
536
|
}
|
|
448
537
|
/**
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
* Note: Two aggregates with the same ID ARE the same aggregate (identity).
|
|
453
|
-
* This function checks if they are at the same version: i.e., no concurrent modification.
|
|
454
|
-
*
|
|
455
|
-
* @example
|
|
456
|
-
* ```typescript
|
|
457
|
-
* const before = await repository.findById(id);
|
|
458
|
-
* // ... some operations ...
|
|
459
|
-
* const after = await repository.findById(id);
|
|
460
|
-
*
|
|
461
|
-
* if (!sameVersion(before, after)) {
|
|
462
|
-
* throw new Error("Aggregate was modified by another process");
|
|
463
|
-
* }
|
|
464
|
-
* ```
|
|
538
|
+
* A domain event enriched after persistence has established its source and
|
|
539
|
+
* commit position. Outboxes and projectors consume this envelope; in-process
|
|
540
|
+
* domain handlers continue to consume the bare {@link DomainEvent} value.
|
|
465
541
|
*/
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
version: Version;
|
|
472
|
-
}): boolean;
|
|
542
|
+
interface CommittedDomainEvent<Evt extends AnyDomainEvent> {
|
|
543
|
+
readonly event: Evt;
|
|
544
|
+
readonly source: AggregateAddress;
|
|
545
|
+
readonly position: CommitPosition;
|
|
546
|
+
}
|
|
473
547
|
//#endregion
|
|
474
|
-
//#region src/
|
|
475
|
-
/**
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
* therefore carry both fields instead of defining boundary-specific variants.
|
|
482
|
-
*
|
|
483
|
-
* `aggregateType` is a stable technical stream category. Renaming it changes
|
|
484
|
-
* persistence keys and orphans checkpoints unless the stored addresses are
|
|
485
|
-
* migrated. When bounded contexts share infrastructure and reuse a domain
|
|
486
|
-
* name, qualify it at the source (`sales.order`, `fulfillment.order`). The kit
|
|
487
|
-
* deliberately adds no separate `boundedContext` field: qualification remains
|
|
488
|
-
* the consumer's naming decision.
|
|
489
|
-
*/
|
|
490
|
-
interface AggregateAddress<TAggregateId extends string = string> {
|
|
491
|
-
readonly aggregateType: string;
|
|
492
|
-
readonly aggregateId: TAggregateId;
|
|
548
|
+
//#region src/internal/async/execution.d.ts
|
|
549
|
+
/** Cancellation and deadline controls for one bounded shell operation. */
|
|
550
|
+
interface ExecutionContext {
|
|
551
|
+
/** Cooperative cancellation for the in-flight operation. */
|
|
552
|
+
readonly signal: AbortSignal;
|
|
553
|
+
/** Absolute Unix epoch millisecond at which the shell stops waiting. */
|
|
554
|
+
readonly deadlineAt: number;
|
|
493
555
|
}
|
|
494
556
|
//#endregion
|
|
495
|
-
//#region src/
|
|
496
|
-
/** A pure invariant check that throws when a candidate state is invalid. */
|
|
497
|
-
type StateValidator<TState> = (state: TState) => void;
|
|
557
|
+
//#region src/messaging/outbox/ports.d.ts
|
|
498
558
|
/**
|
|
499
|
-
*
|
|
500
|
-
*
|
|
559
|
+
* One pending event in the outbox plus the opaque id the implementation
|
|
560
|
+
* needs to ack it via `markDispatched`. The library does not prescribe
|
|
561
|
+
* what `dispatchId` looks like: an implementation can reuse the event's
|
|
562
|
+
* own `eventId`, generate its own UUID, use the row's auto-increment
|
|
563
|
+
* primary key, or whatever the storage layer prefers.
|
|
501
564
|
*/
|
|
502
|
-
interface
|
|
565
|
+
interface OutboxRecord<Evt extends AnyDomainEvent> extends CommittedDomainEvent<Evt> {
|
|
566
|
+
dispatchId: string;
|
|
503
567
|
/**
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
* Passing validation as data avoids virtual dispatch from the base
|
|
509
|
-
* constructor: the function cannot observe partly initialised subclass
|
|
510
|
-
* fields through `this`. Close over immutable policy supplied to the
|
|
511
|
-
* concrete constructor when validation needs instance-specific inputs.
|
|
568
|
+
* Failed delivery attempts so far. Populated by implementations that
|
|
569
|
+
* track dispatch failures (see {@link DispatchTrackingOutbox});
|
|
570
|
+
* plain `Outbox` implementations may omit it.
|
|
512
571
|
*/
|
|
513
|
-
|
|
572
|
+
attempts?: number;
|
|
573
|
+
}
|
|
574
|
+
/** A record that exhausted its delivery attempts; see {@link DispatchTrackingOutbox.deadLetters}. */
|
|
575
|
+
interface DeadLetterRecord<Evt extends AnyDomainEvent> extends CommittedDomainEvent<Evt> {
|
|
576
|
+
dispatchId: string;
|
|
577
|
+
/** Failed delivery attempts when the record was dead-lettered. */
|
|
578
|
+
attempts: number;
|
|
579
|
+
/** Human-readable rendering of the last delivery error, if recorded. */
|
|
580
|
+
lastError?: string;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Write half of the transactional outbox: the only outbox capability the
|
|
584
|
+
* write side (`withCommit`, `UnitOfWork`) depends on. Persisting the
|
|
585
|
+
* events atomically with the aggregate state is the kit's guarantee;
|
|
586
|
+
* DELIVERY is a separate, replaceable concern.
|
|
587
|
+
*
|
|
588
|
+
* Implement ONLY this interface to plug in an external delivery
|
|
589
|
+
* solution: `add()` writes into that solution's outbox storage inside
|
|
590
|
+
* the ambient transaction, and its own listener (polling or
|
|
591
|
+
* WAL/CDC-based, such as a Debezium-style connector, a delivery
|
|
592
|
+
* library, or a broker-native outbox) owns delivery entirely. The
|
|
593
|
+
* kit-side poll surface ({@link Outbox}) is then never involved. See
|
|
594
|
+
* the outbox guide, "External dispatchers".
|
|
595
|
+
*/
|
|
596
|
+
interface OutboxWriter<Evt extends AnyDomainEvent> {
|
|
514
597
|
/**
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
598
|
+
* Finalizes and persists event commit candidates. Called from inside
|
|
599
|
+
* `withCommit`'s transactional callback, atomically with the aggregate
|
|
600
|
+
* write.
|
|
601
|
+
*
|
|
602
|
+
* For every qualified aggregate source, the adapter must serialize source
|
|
603
|
+
* advancement, read its last eventful aggregate version, write that value as
|
|
604
|
+
* `previousEventfulAggregateVersion` on every event in the candidate's
|
|
605
|
+
* commit, and advance the source head to `aggregateVersion` in the SAME
|
|
606
|
+
* transaction. A state-only aggregate commit does not call `add()` and must
|
|
607
|
+
* therefore not advance this event-source head.
|
|
519
608
|
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
609
|
+
* A qualified source position `(aggregateType, aggregateId,
|
|
610
|
+
* aggregateVersion, commitSequence)` MUST identify one immutable event. All
|
|
611
|
+
* candidates for the same aggregate commit MUST also agree on `commitSize`.
|
|
612
|
+
* Enforce both constraints before advancing the source head; a conflicting
|
|
613
|
+
* retry must reject without replacing the stored event or changing the head.
|
|
523
614
|
*
|
|
524
|
-
* **
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
|
|
532
|
-
|
|
615
|
+
* **Idempotency:** implementations should dedupe on
|
|
616
|
+
* `candidate.event.eventId`. `withCommit` itself does not retry, but the
|
|
617
|
+
* surrounding use case (a queue consumer, an HTTP retry, a transactional
|
|
618
|
+
* outbox-dispatcher loop) may legitimately invoke the same write more than
|
|
619
|
+
* once. A unique-key constraint on `(eventId)` in the outbox table is the
|
|
620
|
+
* standard implementation; the source-head update and dedupe decision must
|
|
621
|
+
* share the transaction. Idempotency applies only to an exact candidate
|
|
622
|
+
* retry: the same event ID, qualified source, aggregate version, commit
|
|
623
|
+
* sequence, and commit size. Reusing an `eventId` for another source or
|
|
624
|
+
* position is a caller bug: adapters that retain the conflicting record
|
|
625
|
+
* should reject it rather than replace or silently reinterpret it as a retry.
|
|
626
|
+
*/
|
|
627
|
+
add: (events: ReadonlyArray<EventCommitCandidate<Evt>>) => Promise<void>;
|
|
533
628
|
}
|
|
534
629
|
/**
|
|
535
|
-
*
|
|
536
|
-
*
|
|
630
|
+
* Transactional outbox port: the bridge between the write-side
|
|
631
|
+
* transaction and the (out-of-band) event dispatcher.
|
|
537
632
|
*
|
|
538
|
-
*
|
|
539
|
-
* `
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
* Interface for Entities with state.
|
|
633
|
+
* Lifecycle:
|
|
634
|
+
* 1. `add()` inside the write transaction (`withCommit` calls this) so
|
|
635
|
+
* events persist atomically with the aggregate state
|
|
636
|
+
* ({@link OutboxWriter}, the only part the write side needs).
|
|
637
|
+
* 2. An outbox dispatcher (the kit's `OutboxDispatcher` or your own)
|
|
638
|
+
* polls `getPending()` and forwards the events to subscribers /
|
|
639
|
+
* external brokers.
|
|
640
|
+
* 3. After successful dispatch, the dispatcher calls `markDispatched()`
|
|
641
|
+
* with the records' `dispatchId`s so they don't come back next poll.
|
|
548
642
|
*
|
|
549
|
-
*
|
|
550
|
-
* -
|
|
551
|
-
*
|
|
643
|
+
* `markDispatched` is required to be idempotent: calling it with an id
|
|
644
|
+
* that's already marked is a no-op, not an error. This lets the
|
|
645
|
+
* dispatcher safely retry on partial-failure.
|
|
552
646
|
*
|
|
553
|
-
*
|
|
554
|
-
*
|
|
647
|
+
* **Competing dispatcher instances** are an adapter contract, not a
|
|
648
|
+
* dispatcher feature: a transactional implementation that should
|
|
649
|
+
* support several concurrent pollers must make `getPending` claim the
|
|
650
|
+
* returned records (`FOR UPDATE SKIP LOCKED` or equivalent). Without
|
|
651
|
+
* claiming, run one logical dispatcher per outbox.
|
|
555
652
|
*
|
|
556
|
-
*
|
|
653
|
+
* The bundled dispatcher supplies an {@link ExecutionContext} to every poll-side
|
|
654
|
+
* operation. Production adapters MUST pass its signal to native I/O or enforce
|
|
655
|
+
* a native timeout no later than `deadlineAt`; the shell can bound its wait but
|
|
656
|
+
* cannot terminate a promise that ignores cancellation. A timed-out write has
|
|
657
|
+
* an unknown outcome. Acknowledgements must remain idempotent when they complete
|
|
658
|
+
* late; a late failure update may count its original delivery attempt and must
|
|
659
|
+
* still no-op after the record was dispatched.
|
|
557
660
|
*/
|
|
558
|
-
interface
|
|
661
|
+
interface Outbox<Evt extends AnyDomainEvent> extends OutboxWriter<Evt> {
|
|
559
662
|
/**
|
|
560
|
-
*
|
|
663
|
+
* Returns up to `limit` outbox records that have not yet been
|
|
664
|
+
* dispatched, **in the order `add()` persisted them** (commit order).
|
|
665
|
+
* The ordering is part of the port contract: `withCommit` promises
|
|
666
|
+
* subscribers per-aggregate causal order, and a sequential dispatcher
|
|
667
|
+
* can only honor that promise when this read is ordered. SQL-backed
|
|
668
|
+
* implementations need a monotonic position column (an auto-increment
|
|
669
|
+
* primary key works) and an `ORDER BY` on it; a bare `SELECT` returns
|
|
670
|
+
* rows in storage order, not insertion order. The dispatcher polls
|
|
671
|
+
* this on a schedule. When `limit` is omitted, the implementation
|
|
672
|
+
* decides on a default page size. The bundled dispatcher always supplies
|
|
673
|
+
* `context`; it is optional only so existing adapters remain assignable.
|
|
561
674
|
*/
|
|
562
|
-
|
|
675
|
+
getPending: (limit?: number, context?: ExecutionContext) => Promise<ReadonlyArray<OutboxRecord<Evt>>>;
|
|
676
|
+
/**
|
|
677
|
+
* Marks the given dispatch records as delivered so subsequent
|
|
678
|
+
* `getPending` calls don't return them. Must be idempotent on
|
|
679
|
+
* already-marked ids, including a late completion after the caller's
|
|
680
|
+
* storage deadline. The bundled dispatcher always supplies `context`.
|
|
681
|
+
*/
|
|
682
|
+
markDispatched: (dispatchIds: ReadonlyArray<string>, context?: ExecutionContext) => Promise<void>;
|
|
563
683
|
}
|
|
564
684
|
/**
|
|
565
|
-
*
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
577
|
-
* @template TState - The type of the entity state
|
|
578
|
-
* @template TId - The type of the entity identifier
|
|
579
|
-
*
|
|
580
|
-
* @example
|
|
581
|
-
* ```typescript
|
|
582
|
-
* // Child Entity within an aggregate
|
|
583
|
-
* const validateOrderItemState = (state: OrderItemState): void => {
|
|
584
|
-
* if (state.quantity < 1) throw new Error("quantity must be positive");
|
|
585
|
-
* };
|
|
586
|
-
*
|
|
587
|
-
* class OrderItem extends Entity<OrderItemState, ItemId> {
|
|
588
|
-
* constructor(id: ItemId, initialState: OrderItemState) {
|
|
589
|
-
* super(id, initialState, { validateState: validateOrderItemState });
|
|
590
|
-
* }
|
|
685
|
+
* Optional extension of {@link Outbox} for dispatchers that track
|
|
686
|
+
* delivery failures. Without failure tracking, a poison message (an
|
|
687
|
+
* event whose delivery always throws) is redelivered forever: it comes
|
|
688
|
+
* back from every `getPending` poll, blocks per-aggregate ordering
|
|
689
|
+
* behind it, and burns the dispatcher's cycles. This extension gives
|
|
690
|
+
* the dispatcher a bounded-retry story: report each failed delivery via
|
|
691
|
+
* {@link markFailed}; the implementation moves records past its
|
|
692
|
+
* attempt ceiling to a dead-letter set that `getPending` no longer
|
|
693
|
+
* returns, and {@link deadLetters} exposes them for alerting, manual
|
|
694
|
+
* inspection, and redelivery (deliver by hand, then ack via
|
|
695
|
+
* `markDispatched`, which also clears dead-lettered records).
|
|
591
696
|
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
* // `this._state = ...` assignment would skip both.
|
|
595
|
-
* this.setState({ ...this.state, quantity });
|
|
596
|
-
* }
|
|
597
|
-
* }
|
|
598
|
-
* ```
|
|
697
|
+
* See the outbox guide's dispatcher recipe for the retry-then-dead-letter
|
|
698
|
+
* loop this port shape supports.
|
|
599
699
|
*/
|
|
600
|
-
|
|
601
|
-
readonly id: TId;
|
|
700
|
+
interface DispatchTrackingOutbox<Evt extends AnyDomainEvent> extends Outbox<Evt> {
|
|
602
701
|
/**
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
611
|
-
* `
|
|
612
|
-
*
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
private readonly _stateFreezeMode;
|
|
624
|
-
private readonly validateState;
|
|
625
|
-
/**
|
|
626
|
-
* **State ownership.** Plain-object and array states are shallow-copied
|
|
627
|
-
* before the freeze, so the caller's own object stays mutable. A CLASS
|
|
628
|
-
* INSTANCE passed as state is an ownership transfer: it is frozen
|
|
629
|
-
* in place (a copy would strip its prototype). Do not keep mutating
|
|
630
|
-
* the instance after handing it to the entity. The same contract
|
|
631
|
-
* applies to {@link setState}. With
|
|
632
|
-
* {@link EntityConfig.deepFreezeState} enabled, the ownership transfer
|
|
633
|
-
* widens to the whole graph: NESTED objects are frozen in place too.
|
|
634
|
-
*
|
|
635
|
-
* @throws HostileStateKeyError when a plain-object, null-prototype,
|
|
636
|
-
* or array state carries an own `"__proto__"` data key; validate and
|
|
637
|
-
* strip untrusted input at the boundary.
|
|
638
|
-
*/
|
|
639
|
-
protected constructor(id: TId, initialState: TState, config?: EntityConfig<TState>);
|
|
640
|
-
/**
|
|
641
|
-
* Freezes a state value according to this entity's configured freeze
|
|
642
|
-
* mode: the default shallow freeze, or `deepFreeze` when
|
|
643
|
-
* {@link EntityConfig.deepFreezeState} was enabled at construction.
|
|
644
|
-
* Infrastructure-style subclass code that deliberately assigns
|
|
645
|
-
* `this._state` directly must freeze through this method, not
|
|
646
|
-
* `freezeShallow`, or the opt-in silently degrades to shallow for that
|
|
647
|
-
* path. Ordinary domain behavior should use {@link setState} instead.
|
|
648
|
-
*/
|
|
649
|
-
protected freezeState(value: TState): TState;
|
|
650
|
-
/**
|
|
651
|
-
* Sets the state of the entity.
|
|
652
|
-
* This is a convenience method for state mutations.
|
|
653
|
-
* Automatically validates `newState` with the instance-bound
|
|
654
|
-
* {@link EntityConfig.validateState} function.
|
|
655
|
-
*
|
|
656
|
-
* Plain-object and array states are shallow-copied before the freeze
|
|
657
|
-
* (the caller's object stays mutable); a class-instance state is an
|
|
658
|
-
* ownership transfer and is frozen in place; see the constructor.
|
|
659
|
-
*
|
|
660
|
-
* @param newState - The new state
|
|
661
|
-
* @throws HostileStateKeyError when the state carries an own
|
|
662
|
-
* `"__proto__"` data key; the previous state is kept.
|
|
702
|
+
* Records one failed delivery attempt for the given record:
|
|
703
|
+
* increments its attempt count (surfaced as
|
|
704
|
+
* {@link OutboxRecord.attempts}) and, once the implementation's
|
|
705
|
+
* ceiling is reached, moves the record to the dead-letter set.
|
|
706
|
+
* A no-op for unknown or already-dispatched ids (a late failure
|
|
707
|
+
* report after a successful retry must not resurrect the record).
|
|
708
|
+
* Returns the exact dead-letter record only on the call that performs
|
|
709
|
+
* that transition; retries below the ceiling and no-ops return
|
|
710
|
+
* `undefined`. This lets productive pollers emit an immediate signal
|
|
711
|
+
* without scanning the durable dead-letter set after every failure. A late
|
|
712
|
+
* completion may count that original delivery attempt; it must still no-op if
|
|
713
|
+
* the record was dispatched in the meantime. The bundled dispatcher never
|
|
714
|
+
* reissues the same store call and always supplies `context`.
|
|
715
|
+
*/
|
|
716
|
+
markFailed: (dispatchId: string, error?: unknown, context?: ExecutionContext) => Promise<DeadLetterRecord<Evt> | undefined>;
|
|
717
|
+
/**
|
|
718
|
+
* Records that exhausted their delivery attempts. They no longer
|
|
719
|
+
* come back from `getPending`; wire this to durable alerting and
|
|
720
|
+
* reconciliation so poison messages surface even if the poller stops
|
|
721
|
+
* between this store transition and its immediate observer callback.
|
|
663
722
|
*/
|
|
664
|
-
|
|
723
|
+
deadLetters: () => Promise<ReadonlyArray<DeadLetterRecord<Evt>>>;
|
|
665
724
|
}
|
|
725
|
+
//#endregion
|
|
726
|
+
//#region src/application/cqrs/command/command-outbox.d.ts
|
|
666
727
|
/**
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
* shallow or deep per the `deepFreezeState` config) to prevent outside
|
|
671
|
-
* mutation of state read through the `state` getter without paying the
|
|
672
|
-
* cost of a deep clone on every read.
|
|
673
|
-
*
|
|
674
|
-
* Subclass code that assigns `this._state` directly should freeze through
|
|
675
|
-
* the protected `freezeState(value)` method rather than calling this
|
|
676
|
-
* helper, so the configured freeze mode is honored. The export remains
|
|
677
|
-
* for consumers using it as a standalone utility.
|
|
728
|
+
* Business relationships and technical trace context selected explicitly for
|
|
729
|
+
* an outgoing command. Correlation/conversation explain the business flow;
|
|
730
|
+
* W3C Trace Context connects technical spans.
|
|
678
731
|
*/
|
|
679
|
-
|
|
732
|
+
interface CommandMessageRelationships {
|
|
733
|
+
/** Groups messages that belong to one operation or trace. */
|
|
734
|
+
readonly correlationId?: string;
|
|
735
|
+
/** Groups every message in one long-running business interaction. */
|
|
736
|
+
readonly conversationId?: string;
|
|
737
|
+
/** W3C Trace Context parent for technical distributed tracing. */
|
|
738
|
+
readonly traceparent?: string;
|
|
739
|
+
/** Optional vendor trace state associated with `traceparent`. */
|
|
740
|
+
readonly tracestate?: string;
|
|
741
|
+
}
|
|
680
742
|
/**
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
* @param a - First entity
|
|
685
|
-
* @param b - Second entity
|
|
686
|
-
* @returns true if both entities have the same ID, false otherwise
|
|
687
|
-
*
|
|
688
|
-
* @example
|
|
689
|
-
* ```typescript
|
|
690
|
-
* const item1: OrderItem = { id: itemId1, productId: "prod-1", quantity: 2 };
|
|
691
|
-
* const item2: OrderItem = { id: itemId2, productId: "prod-2", quantity: 1 };
|
|
743
|
+
* Application-owned Published Language produced from one private domain or
|
|
744
|
+
* process event. `destination` names one receiver contract; it is deliberately
|
|
745
|
+
* required because a command is an instruction, not a broadcast fact.
|
|
692
746
|
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
747
|
+
* The command carries a stable schema `version` and JSON-safe `payload`.
|
|
748
|
+
* Domain value objects are translated to wire DTOs by the mapper before this
|
|
749
|
+
* boundary.
|
|
696
750
|
*/
|
|
697
|
-
|
|
751
|
+
interface CommandMessageContent<C extends PublishedCommand> extends CommandMessageRelationships {
|
|
752
|
+
readonly destination: string;
|
|
753
|
+
readonly command: C;
|
|
754
|
+
}
|
|
698
755
|
/**
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
* @param entities - Array of entities to search
|
|
703
|
-
* @param id - The ID to search for
|
|
704
|
-
* @returns The entity if found, undefined otherwise
|
|
705
|
-
*
|
|
706
|
-
* @example
|
|
707
|
-
* ```typescript
|
|
708
|
-
* const items: OrderItem[] = [
|
|
709
|
-
* { id: itemId1, productId: "prod-1", quantity: 2 },
|
|
710
|
-
* { id: itemId2, productId: "prod-2", quantity: 1 }
|
|
711
|
-
* ];
|
|
756
|
+
* Immutable, JSON-safe command envelope stored for later at-least-once
|
|
757
|
+
* delivery.
|
|
712
758
|
*
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
759
|
+
* `causationId` always identifies the private event whose accepted decision
|
|
760
|
+
* requested this command. The mapper cannot replace it with a weaker
|
|
761
|
+
* correlation. Consumer-produced events should in turn use `messageId` as
|
|
762
|
+
* their causation id.
|
|
716
763
|
*/
|
|
717
|
-
|
|
764
|
+
interface DurableCommandMessage<C extends PublishedCommand> extends CommandMessageContent<C> {
|
|
765
|
+
readonly messageId: string;
|
|
766
|
+
readonly recordedAt: string;
|
|
767
|
+
readonly causationId: string;
|
|
768
|
+
}
|
|
718
769
|
/**
|
|
719
|
-
*
|
|
720
|
-
*
|
|
721
|
-
*
|
|
722
|
-
* @param id - The ID to check for
|
|
723
|
-
* @returns true if an entity with the ID exists, false otherwise
|
|
724
|
-
*
|
|
725
|
-
* @example
|
|
726
|
-
* ```typescript
|
|
727
|
-
* const items: OrderItem[] = [
|
|
728
|
-
* { id: itemId1, productId: "prod-1", quantity: 2 }
|
|
729
|
-
* ];
|
|
730
|
-
*
|
|
731
|
-
* hasEntityId(items, itemId1); // true
|
|
732
|
-
* hasEntityId(items, itemId2); // false
|
|
733
|
-
* ```
|
|
770
|
+
* Receipt for the private event that requested one command batch. It retains
|
|
771
|
+
* commit identity and ordering without putting the private event or its
|
|
772
|
+
* payload into the command outbox.
|
|
734
773
|
*/
|
|
735
|
-
|
|
774
|
+
interface CommandCommitOriginCandidate {
|
|
775
|
+
readonly eventId: string;
|
|
776
|
+
readonly source: AggregateAddress;
|
|
777
|
+
readonly position: EventCommitCandidatePosition;
|
|
778
|
+
}
|
|
736
779
|
/**
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
780
|
+
* One private process-event commit and the exact commands it requested.
|
|
781
|
+
* `messages` may be empty: the receipt still advances the originating source
|
|
782
|
+
* and makes an exact retry distinguishable from a missing commit.
|
|
783
|
+
*/
|
|
784
|
+
interface CommandOutboxCommitCandidate<C extends PublishedCommand> {
|
|
785
|
+
readonly origin: CommandCommitOriginCandidate;
|
|
786
|
+
readonly messages: ReadonlyArray<DurableCommandMessage<C>>;
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Write port for a dedicated transactional command outbox.
|
|
745
790
|
*
|
|
746
|
-
*
|
|
747
|
-
*
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
791
|
+
* The adapter is bound to the same ambient transaction as the aggregate or
|
|
792
|
+
* event-stream repository. It must persist the complete input atomically,
|
|
793
|
+
* retain input order, deduplicate exact retries by `origin.eventId`, and reject
|
|
794
|
+
* a reused origin id whose source, position, or messages differ. It also owns
|
|
795
|
+
* the durable source cursor represented by `origin.position`; an empty command
|
|
796
|
+
* batch still advances that cursor.
|
|
752
797
|
*
|
|
753
|
-
*
|
|
754
|
-
*
|
|
755
|
-
*
|
|
798
|
+
* Delivery is out of band and at least once. A consumer therefore uses
|
|
799
|
+
* `message.messageId` as its idempotency key and acknowledges only after the
|
|
800
|
+
* command result has been stored.
|
|
756
801
|
*/
|
|
757
|
-
|
|
802
|
+
interface CommandOutboxWriter<C extends PublishedCommand> {
|
|
803
|
+
add(commits: ReadonlyArray<CommandOutboxCommitCandidate<C>>): Promise<void>;
|
|
804
|
+
}
|
|
805
|
+
/** Maps one private accepted event to zero or more addressed commands. */
|
|
806
|
+
type CommandOutboxMapper<Evt extends AnyDomainEvent, C extends PublishedCommand> = (event: Evt) => ReadonlyArray<CommandMessageContent<C>>;
|
|
758
807
|
/**
|
|
759
|
-
*
|
|
760
|
-
*
|
|
761
|
-
*
|
|
762
|
-
*
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
*
|
|
770
|
-
* @param id - The ID of the entity to update
|
|
771
|
-
* @param updater - Function that takes the entity and returns the updated entity
|
|
772
|
-
* @returns A new array with the updated entity
|
|
808
|
+
* Adapts a dedicated command outbox to the event-candidate write port consumed
|
|
809
|
+
* by `withCommit`.
|
|
810
|
+
*
|
|
811
|
+
* Mapping happens inside the transaction, before the command outbox write.
|
|
812
|
+
* The private event is used only at this boundary and is reduced to an origin
|
|
813
|
+
* receipt. The helper never publishes it or copies its payload implicitly; the
|
|
814
|
+
* application mapper selects and translates the data that belongs in the
|
|
815
|
+
* versioned Published Language. The route rejects values JSON would lose or
|
|
816
|
+
* change before it calls the adapter.
|
|
817
|
+
* Every command gets a stable id derived from the event id and its zero-based
|
|
818
|
+
* order, so an exact transaction retry produces the same rows.
|
|
773
819
|
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
777
|
-
* { id: itemId1, productId: "prod-1", quantity: 2 }
|
|
778
|
-
* ];
|
|
779
|
-
*
|
|
780
|
-
* const updated = updateEntityById(items, itemId1, (item) => ({
|
|
781
|
-
* ...item,
|
|
782
|
-
* quantity: item.quantity + 1
|
|
783
|
-
* }));
|
|
784
|
-
* // updated is [{ id: itemId1, productId: "prod-1", quantity: 3 }]
|
|
785
|
-
* ```
|
|
820
|
+
* Omit `withCommit`'s in-process `bus` for private process events. Participants
|
|
821
|
+
* consume the durable command messages from their explicitly named
|
|
822
|
+
* destinations, while event-stream replay only rebuilds process state.
|
|
786
823
|
*/
|
|
787
|
-
declare function
|
|
824
|
+
declare function routeEventsToCommandOutbox<C extends PublishedCommand, Evt extends AnyDomainEvent = AnyDomainEvent>(outbox: CommandOutboxWriter<C>, mapper: CommandOutboxMapper<Evt, C>): OutboxWriter<Evt>;
|
|
825
|
+
//#endregion
|
|
826
|
+
//#region src/domain/identity/id.d.ts
|
|
788
827
|
/**
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
* the ORIGINAL array when nothing changed (no match, or the element kept
|
|
793
|
-
* its reference), so a partial-write adapter can skip the untouched
|
|
794
|
-
* collection; a new array only when an
|
|
795
|
-
* element reference actually changed. The result is `ReadonlyArray<T>`:
|
|
796
|
-
* it may BE the (possibly frozen) input; spread it if you need a mutable
|
|
797
|
-
* copy.
|
|
798
|
-
*
|
|
799
|
-
* @param entities - Array of entities
|
|
800
|
-
* @param id - The ID of the entity to replace
|
|
801
|
-
* @param replacement - The replacement entity
|
|
802
|
-
* @returns A new array with the replaced entity
|
|
828
|
+
* Branded string ID. `Tag` carries the aggregate / entity name so two ids
|
|
829
|
+
* with different tags are not assignable to each other even though both
|
|
830
|
+
* are strings at runtime.
|
|
803
831
|
*
|
|
804
832
|
* @example
|
|
805
|
-
* ```
|
|
806
|
-
*
|
|
807
|
-
*
|
|
808
|
-
*
|
|
809
|
-
*
|
|
810
|
-
* const
|
|
811
|
-
* id: itemId1,
|
|
812
|
-
* productId: "prod-1",
|
|
813
|
-
* quantity: 5
|
|
814
|
-
* });
|
|
833
|
+
* ```ts
|
|
834
|
+
* type UserId = Id<"UserId">;
|
|
835
|
+
* type OrderId = Id<"OrderId">;
|
|
836
|
+
*
|
|
837
|
+
* const u = "user-1" as UserId;
|
|
838
|
+
* const o: OrderId = u; // ❌ compile error
|
|
815
839
|
* ```
|
|
816
840
|
*/
|
|
817
|
-
|
|
841
|
+
type Id<Tag extends string> = string & {
|
|
842
|
+
readonly __brand: Tag;
|
|
843
|
+
};
|
|
818
844
|
/**
|
|
819
|
-
*
|
|
845
|
+
* Produces fresh ids of a single, fixed tag. The tag is bound at the
|
|
846
|
+
* generator type: `IdGenerator<"UserId">.next()` returns `Id<"UserId">`
|
|
847
|
+
* with no caller-side generic to abuse.
|
|
820
848
|
*
|
|
821
|
-
*
|
|
822
|
-
*
|
|
849
|
+
* **Your factory must produce unique ids under concurrent calls.**
|
|
850
|
+
* The kit makes no attempt to dedupe or detect collisions: a collision
|
|
851
|
+
* silently overwrites earlier rows (under unique-key constraints) or
|
|
852
|
+
* silently aliases two different entities (without them). Safe choices:
|
|
853
|
+
* `crypto.randomUUID()` (UUIDv4, the default for events), ULID, UUIDv7,
|
|
854
|
+
* KSUID: all collision-resistant by design. Unsafe choices: `Date.now()`
|
|
855
|
+
* alone (duplicates within the same millisecond), a process-local
|
|
856
|
+
* counter without persistence (resets to 1 on restart, collides with
|
|
857
|
+
* prior runs), a sequential id derived from non-atomic state.
|
|
823
858
|
*
|
|
824
859
|
* @example
|
|
825
|
-
* ```
|
|
826
|
-
*
|
|
827
|
-
* { id: itemId1, productId: "prod-1", quantity: 2 },
|
|
828
|
-
* { id: itemId2, productId: "prod-2", quantity: 1 }
|
|
829
|
-
* ];
|
|
860
|
+
* ```ts
|
|
861
|
+
* import { ulid } from "ulid";
|
|
830
862
|
*
|
|
831
|
-
* const
|
|
832
|
-
*
|
|
863
|
+
* const userIds: IdGenerator<"UserId"> = { next: () => ulid() as Id<"UserId"> };
|
|
864
|
+
* const id = userIds.next(); // Id<"UserId">
|
|
833
865
|
* ```
|
|
866
|
+
*
|
|
867
|
+
* The previous shape (`IdGenerator { next<T extends string>(): Id<T> }`)
|
|
868
|
+
* let callers pick `T` themselves: `gen.next<"AnyTag">()` typechecked
|
|
869
|
+
* even when the generator produced different-tag ids, silently defeating
|
|
870
|
+
* the brand.
|
|
834
871
|
*/
|
|
835
|
-
|
|
872
|
+
interface IdGenerator<Tag extends string> {
|
|
873
|
+
next: () => Id<Tag>;
|
|
874
|
+
}
|
|
836
875
|
//#endregion
|
|
837
|
-
//#region src/aggregate/
|
|
838
|
-
|
|
839
|
-
|
|
876
|
+
//#region src/domain/aggregate/aggregate.d.ts
|
|
877
|
+
type Version = number & {
|
|
878
|
+
readonly __v: true;
|
|
879
|
+
};
|
|
880
|
+
/**
|
|
881
|
+
* Brands a stored number as an aggregate {@link Version}. A version is a
|
|
882
|
+
* safe integer of at least zero. Use it in repository adapters instead of
|
|
883
|
+
* a cast, so a corrupt row value fails here with {@link InvalidVersionError}
|
|
884
|
+
* and never reaches the optimistic-concurrency cursor.
|
|
885
|
+
*/
|
|
886
|
+
declare function toVersion(value: number): Version;
|
|
840
887
|
/**
|
|
841
|
-
*
|
|
842
|
-
*
|
|
843
|
-
*
|
|
844
|
-
* tracking, the kit-internal post-commit acknowledgement capability,
|
|
845
|
-
* the `markRestored` post-load marker, and the `createEvent` helper
|
|
846
|
-
* that auto-injects `aggregateId` + `aggregateType` on every event the
|
|
847
|
-
* aggregate emits. The application shell records the pending decisions
|
|
848
|
-
* with `recordPendingEvents` before persistence.
|
|
849
|
-
*
|
|
850
|
-
* Consumers do NOT extend this class directly; extend
|
|
851
|
-
* `AggregateRoot` for state-stored aggregates or
|
|
852
|
-
* `EventSourcedAggregate` for event-sourced ones. The split between
|
|
853
|
-
* those two reflects the canonical Vernon §8 (state-stored) /
|
|
854
|
-
* Vernon §11 + Greg Young (event-sourced) distinction in how state
|
|
855
|
-
* is represented; the lifecycle machinery is the same for both.
|
|
888
|
+
* Snapshot of an aggregate state at a specific point in time.
|
|
889
|
+
* Used for optimizing event replay by starting from a snapshot
|
|
890
|
+
* instead of replaying all events from the beginning.
|
|
856
891
|
*
|
|
857
892
|
* @template TState - The type of the aggregate state
|
|
858
|
-
* @template TId - The aggregate root identifier
|
|
859
|
-
* @template TEvent - The domain-event union. Defaults to `never` so
|
|
860
|
-
* aggregates without a declared event type cannot emit events
|
|
861
|
-
* (emitting any event becomes a compile error).
|
|
862
893
|
*/
|
|
863
|
-
|
|
864
|
-
/**
|
|
865
|
-
* The aggregate's domain type as a string, used to populate
|
|
866
|
-
* `aggregateType` on events created via {@link createEvent}.
|
|
867
|
-
*
|
|
868
|
-
* Subclasses MUST declare this as a string literal:
|
|
869
|
-
*
|
|
870
|
-
* ```ts
|
|
871
|
-
* class Order extends AggregateRoot<OrderState, OrderId, OrderEvent> {
|
|
872
|
-
* protected readonly aggregateType = "Order";
|
|
873
|
-
* }
|
|
874
|
-
* ```
|
|
875
|
-
*
|
|
876
|
-
* The string is *the* identifier downstream consumers (outbox
|
|
877
|
-
* dispatchers, projection handlers, audit logs) use to route by
|
|
878
|
-
* aggregate kind. Use the same canonical name across your system;
|
|
879
|
-
* matching the class name is the obvious choice, but the value
|
|
880
|
-
* comes from this explicit declaration, not `constructor.name`
|
|
881
|
-
* (which is fragile under minification, bundler transforms, and
|
|
882
|
-
* subclass renaming).
|
|
883
|
-
*/
|
|
884
|
-
protected abstract readonly aggregateType: string;
|
|
885
|
-
private _version;
|
|
886
|
-
/**
|
|
887
|
-
* Version the persistence layer last confirmed for this instance:
|
|
888
|
-
* `undefined` until the aggregate is reconstituted (`markRestored`) or a
|
|
889
|
-
* commit is acknowledged. Kit-internal via the lifecycle capability; it
|
|
890
|
-
* grounds the `withCommit` unique-cursor guard so an eventful commit that
|
|
891
|
-
* did not advance beyond the persisted row is rejected deterministically.
|
|
892
|
-
*/
|
|
893
|
-
private _persistedVersion;
|
|
894
|
-
private _pendingEvents;
|
|
895
|
-
protected constructor(id: TId, initialState: TState, config?: AggregateConfig<TState>);
|
|
896
|
-
private acknowledgePendingEvents;
|
|
897
|
-
/**
|
|
898
|
-
* Post-commit cleanup for the deleted disposition. The row is gone, so
|
|
899
|
-
* there is no persisted version to advance: stamping the marker from the
|
|
900
|
-
* live instance would make a later legitimate re-enrollment of this
|
|
901
|
-
* instance trip the unique-cursor guard for a row that does not exist.
|
|
902
|
-
*/
|
|
903
|
-
private discardPendingEventsAfterDeletion;
|
|
904
|
-
private stripAcknowledgedPrefix;
|
|
905
|
-
get version(): Version;
|
|
906
|
-
/**
|
|
907
|
-
* Read-only list of domain events recorded on this aggregate that
|
|
908
|
-
* have not yet been flushed to the outbox / persistence layer.
|
|
909
|
-
*/
|
|
910
|
-
get pendingEvents(): ReadonlyArray<PendingDomainEvent<TEvent>>;
|
|
911
|
-
/**
|
|
912
|
-
* Count-only accessor for internal aggregate paths: the public
|
|
913
|
-
* {@link pendingEvents} getter allocates and freezes
|
|
914
|
-
* a defensive copy per read, which a length check does not need.
|
|
915
|
-
*/
|
|
916
|
-
protected get pendingEventCount(): number;
|
|
917
|
-
protected setVersion(version: Version): void;
|
|
918
|
-
/**
|
|
919
|
-
* Manually bumps the aggregate version. Used by state-stored
|
|
920
|
-
* aggregates' `setState()` / `commit()` paths and by the
|
|
921
|
-
* event-sourced replay path after each applied event.
|
|
922
|
-
*/
|
|
923
|
-
protected bumpVersion(): void;
|
|
924
|
-
/**
|
|
925
|
-
* **Lifecycle marker, Post-Load.** Syncs both `_version` and
|
|
926
|
-
* the current version to the stored version. Used by
|
|
927
|
-
* `reconstitute(...)` factories to assemble an in-memory aggregate
|
|
928
|
-
* from a persisted row.
|
|
929
|
-
*
|
|
930
|
-
* The Factory-vs-Reconstitution distinction (Vernon §11) is honoured
|
|
931
|
-
* structurally: reconstitution stays inside the aggregate factory while
|
|
932
|
-
* post-commit acknowledgement belongs to application commit orchestration.
|
|
933
|
-
*
|
|
934
|
-
* If you override this, call `super.markRestored(version)` so the current
|
|
935
|
-
* domain version remains aligned with the reconstituted facts.
|
|
936
|
-
*
|
|
937
|
-
* @param version - The version the row currently holds in the DB
|
|
938
|
-
*
|
|
939
|
-
* @example
|
|
940
|
-
* ```ts
|
|
941
|
-
* static reconstitute(id: OrderId, state: OrderState, version: Version): Order {
|
|
942
|
-
* const order = new Order(id, state);
|
|
943
|
-
* order.markRestored(version);
|
|
944
|
-
* return order;
|
|
945
|
-
* }
|
|
946
|
-
* ```
|
|
947
|
-
*/
|
|
948
|
-
protected markRestored(version: Version): void;
|
|
949
|
-
/**
|
|
950
|
-
* Appends a domain event to the pending list. Prefer the higher-level
|
|
951
|
-
* `AggregateRoot.commit()` (state-stored) or `EventSourcedAggregate.apply()`
|
|
952
|
-
* (event-sourced) call sites, both of which wrap `addDomainEvent` in the
|
|
953
|
-
* canonical record-AFTER-mutation order (Vernon §8). Calling
|
|
954
|
-
* `addDomainEvent` directly is appropriate only after a version-advancing
|
|
955
|
-
* state mutation, or while constructing a never-persisted aggregate.
|
|
956
|
-
* An event-only commit on an already-persisted aggregate has no unique
|
|
957
|
-
* cursor and `withCommit` rejects it; use `commit(currentState, event)`.
|
|
958
|
-
*/
|
|
959
|
-
protected addDomainEvent(event: PendingDomainEvent<TEvent>): void;
|
|
894
|
+
interface AggregateSnapshot<TState> {
|
|
960
895
|
/**
|
|
961
|
-
*
|
|
962
|
-
* the kit's constructors (`createDomainEvent`,
|
|
963
|
-
* `createDomainEventFromFacts`, `createEvent`) pass,
|
|
964
|
-
* checked against the constructor's internal, unforgeable mint
|
|
965
|
-
* marker. Minted implies deeply frozen with defensively copied
|
|
966
|
-
* payload and metadata, a guarantee no frozen-ness probe can
|
|
967
|
-
* establish (a shallow-frozen literal with mutable nested data
|
|
968
|
-
* would fool it). O(1): one WeakSet lookup.
|
|
896
|
+
* The state of the aggregate at the time of the snapshot.
|
|
969
897
|
*/
|
|
970
|
-
|
|
898
|
+
readonly state: TState;
|
|
971
899
|
/**
|
|
972
|
-
*
|
|
973
|
-
* reading a clock, generating an id, or attaching tracing metadata.
|
|
974
|
-
*
|
|
975
|
-
* The application shell records pending events after the domain operation
|
|
976
|
-
* and before persistence. Payload schema version stays here, next to the
|
|
977
|
-
* concrete event producer, rather than in shell-owned recording data.
|
|
900
|
+
* The version of the aggregate when the snapshot was taken.
|
|
978
901
|
*/
|
|
979
|
-
|
|
980
|
-
}
|
|
981
|
-
//#endregion
|
|
982
|
-
//#region src/aggregate/aggregate-root.d.ts
|
|
983
|
-
/**
|
|
984
|
-
* OO-first Aggregate Root for state-stored domain models.
|
|
985
|
-
*
|
|
986
|
-
* The aggregate owns identity, valid domain state, behavior, its current
|
|
987
|
-
* domain version, and pending domain events. It deliberately does not own a
|
|
988
|
-
* database baseline or dirty-key bookkeeping. A repository adapter defines
|
|
989
|
-
* its persistence projection through `PersistenceModel`; the Unit of Work
|
|
990
|
-
* retains that opaque baseline and derives the adapter's change set at flush.
|
|
991
|
-
*/
|
|
992
|
-
declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent extends AnyDomainEvent = never> extends BaseAggregate<TState, TId, TEvent> {
|
|
902
|
+
readonly version: Version;
|
|
993
903
|
/**
|
|
994
|
-
*
|
|
995
|
-
* order. Validation and event mint checks run before the transition becomes
|
|
996
|
-
* observable, so a rejected decision records nothing.
|
|
904
|
+
* Timestamp when the snapshot was created.
|
|
997
905
|
*/
|
|
998
|
-
|
|
999
|
-
/** Every normal domain-state transition advances the OCC version. */
|
|
1000
|
-
protected setState(newState: TState): void;
|
|
906
|
+
readonly snapshotAt: Date;
|
|
1001
907
|
/**
|
|
1002
|
-
*
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
1005
|
-
*
|
|
908
|
+
* Schema version of the stored `state` shape, declared and stamped by
|
|
909
|
+
* the persistence adapter that captures the snapshot. Distinct from
|
|
910
|
+
* {@link version}, which counts mutations: this field says "which
|
|
911
|
+
* shape does the stored state have", so a restore can detect a
|
|
912
|
+
* snapshot written against an older DTO shape and migrate or
|
|
913
|
+
* discard it instead of crashing later. Optional: a snapshot without
|
|
914
|
+
* this field restores as schema `1`. Distinct also from
|
|
915
|
+
* `DomainEvent.schemaVersion`, which versions one event payload shape.
|
|
916
|
+
* A payload change and a snapshot state change bump their own field.
|
|
1006
917
|
*/
|
|
1007
|
-
|
|
918
|
+
readonly schemaVersion?: number;
|
|
1008
919
|
}
|
|
1009
|
-
//#endregion
|
|
1010
|
-
//#region src/events/json-value.d.ts
|
|
1011
|
-
/** A primitive value represented without loss by JSON. */
|
|
1012
|
-
type JsonPrimitive = boolean | null | number | string;
|
|
1013
|
-
/** A recursively JSON-safe value. Runtime validation rejects lossy shapes. */
|
|
1014
|
-
type JsonValue = JsonPrimitive | ReadonlyArray<JsonValue> | {
|
|
1015
|
-
readonly [key: string]: JsonValue;
|
|
1016
|
-
};
|
|
1017
|
-
/** A JSON-safe object. */
|
|
1018
|
-
type JsonObject = {
|
|
1019
|
-
readonly [key: string]: JsonValue;
|
|
1020
|
-
};
|
|
1021
|
-
//#endregion
|
|
1022
|
-
//#region src/app/command.d.ts
|
|
1023
920
|
/**
|
|
1024
|
-
*
|
|
1025
|
-
*
|
|
1026
|
-
*
|
|
1027
|
-
*
|
|
1028
|
-
*
|
|
1029
|
-
*
|
|
1030
|
-
*
|
|
1031
|
-
* @example
|
|
1032
|
-
* ```typescript
|
|
1033
|
-
* type CreateOrderCommand = Command & {
|
|
1034
|
-
* type: "CreateOrder";
|
|
1035
|
-
* customerId: string;
|
|
1036
|
-
* items: OrderItem[];
|
|
1037
|
-
* };
|
|
1038
|
-
* ```
|
|
1039
|
-
*
|
|
1040
|
-
* @example Using with external frameworks (RabbitMQ, etc.)
|
|
1041
|
-
* ```typescript
|
|
1042
|
-
* // Define command using Command marker
|
|
1043
|
-
* type CreateOrderCommand = Command & {
|
|
1044
|
-
* type: "CreateOrder";
|
|
1045
|
-
* customerId: string;
|
|
1046
|
-
* };
|
|
1047
|
-
*
|
|
1048
|
-
* // Handler can be typed with CommandHandler even for external frameworks
|
|
1049
|
-
* const handler: CommandHandler<CreateOrderCommand, OrderId> = async (cmd) => {
|
|
1050
|
-
* // ... handler logic
|
|
1051
|
-
* return ok(orderId);
|
|
1052
|
-
* };
|
|
1053
|
-
*
|
|
1054
|
-
* // The consumer owns this runtime decoder. It checks byte and collection
|
|
1055
|
-
* // ceilings, parses to unknown, allow-lists fields, and constructs domain types.
|
|
1056
|
-
* declare function decodeCreateOrderCommand(
|
|
1057
|
-
* body: Uint8Array,
|
|
1058
|
-
* principal: AuthenticatedPrincipal,
|
|
1059
|
-
* ): Result<CreateOrderCommand, InvalidCommand>;
|
|
1060
|
-
* declare function decodeMessageId(
|
|
1061
|
-
* value: unknown,
|
|
1062
|
-
* ): Result<string, InvalidTransportMetadata>;
|
|
1063
|
-
* declare function createOrderDeliveryKey(messageId: string): string;
|
|
921
|
+
* Public contract every Aggregate Root satisfies. Implemented by
|
|
922
|
+
* `BaseAggregate` and inherited by both `StateStoredAggregate` and
|
|
923
|
+
* `EventSourcedAggregate`. Repository ports use this interface as their
|
|
924
|
+
* aggregate type rather than depending on concrete base classes, so persistence
|
|
925
|
+
* orchestration does not take a compile-time
|
|
926
|
+
* dependency on the aggregate hierarchy.
|
|
1064
927
|
*
|
|
1065
|
-
*
|
|
1066
|
-
*
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1069
|
-
* declare function executeIdempotentCreateOrder(
|
|
1070
|
-
* deliveryKey: string,
|
|
1071
|
-
* command: CreateOrderCommand,
|
|
1072
|
-
* ): Promise<Result<OrderId, string>>;
|
|
928
|
+
* Full per-member documentation lives on the concrete `BaseAggregate`
|
|
929
|
+
* class; the interface is intentionally terse to avoid drift. Persistence
|
|
930
|
+
* facts are readable, but acknowledgement and pending-event disposal are not
|
|
931
|
+
* part of this surface. The application shell holds that authority.
|
|
1073
932
|
*
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
1076
|
-
* const messageId = decodeMessageId(message.properties.messageId);
|
|
1077
|
-
* if (messageId.isErr()) {
|
|
1078
|
-
* rabbitMQChannel.reject(message, false); // missing identity: dead-letter
|
|
1079
|
-
* return;
|
|
1080
|
-
* }
|
|
1081
|
-
* const deliveryKey = createOrderDeliveryKey(messageId.value);
|
|
1082
|
-
* const principal = authenticateProducer(message.properties.headers);
|
|
1083
|
-
* const decoded = decodeCreateOrderCommand(message.content, principal);
|
|
1084
|
-
* if (decoded.isErr()) {
|
|
1085
|
-
* rabbitMQChannel.reject(message, false); // invalid input: dead-letter, do not retry
|
|
1086
|
-
* return;
|
|
1087
|
-
* }
|
|
1088
|
-
* const outcome = await executeIdempotentCreateOrder(
|
|
1089
|
-
* deliveryKey,
|
|
1090
|
-
* decoded.value,
|
|
1091
|
-
* );
|
|
1092
|
-
* await recordCommandOutcome(deliveryKey, outcome);
|
|
1093
|
-
* rabbitMQChannel.ack(message);
|
|
1094
|
-
* });
|
|
1095
|
-
* ```
|
|
1096
|
-
*/
|
|
1097
|
-
interface Command {
|
|
1098
|
-
readonly type: string;
|
|
1099
|
-
}
|
|
1100
|
-
/**
|
|
1101
|
-
* Versioned Published Language for a command that crosses a process or
|
|
1102
|
-
* Bounded-Context boundary. Unlike a local {@link Command}, its payload is
|
|
1103
|
-
* JSON-safe data rather than a domain object graph. Map value objects to their
|
|
1104
|
-
* wire DTOs at this boundary; for example, use `MoneyDto` instead of `Money`.
|
|
933
|
+
* @template TId - The aggregate root identifier (branded via `Id<Tag>`)
|
|
934
|
+
* @template TEvent - The domain-event union, defaults to `never`
|
|
1105
935
|
*/
|
|
1106
|
-
interface
|
|
1107
|
-
readonly
|
|
1108
|
-
readonly version:
|
|
1109
|
-
readonly
|
|
936
|
+
interface Aggregate<TId extends Id<string>, TEvent extends AnyDomainEvent = never> {
|
|
937
|
+
readonly id: TId;
|
|
938
|
+
readonly version: Version;
|
|
939
|
+
readonly pendingEvents: ReadonlyArray<PendingDomainEvent<TEvent>>;
|
|
1110
940
|
}
|
|
1111
941
|
/**
|
|
1112
|
-
*
|
|
1113
|
-
*
|
|
1114
|
-
* Commands may modify system state. When a caller can retry or a broker can
|
|
1115
|
-
* redeliver, the application service must enforce idempotency; this handler
|
|
1116
|
-
* type alone does not provide it.
|
|
1117
|
-
*
|
|
1118
|
-
* This type can be used to mark handlers even when using external frameworks
|
|
1119
|
-
* (e.g., RabbitMQ, AWS SQS, Kafka) to ensure type safety and consistency.
|
|
1120
|
-
*
|
|
1121
|
-
* @template C - The command type (must extend Command)
|
|
1122
|
-
* @template R - The result type
|
|
1123
|
-
* @template E - The error channel type. Defaults to `string`; widen it (e.g.
|
|
1124
|
-
* to a `DomainError` union) to carry typed failures through the bus.
|
|
1125
|
-
*
|
|
1126
|
-
* @example
|
|
1127
|
-
* ```typescript
|
|
1128
|
-
* const handler: CommandHandler<CreateOrderCommand, OrderId> = async (cmd) => {
|
|
1129
|
-
* const order = Order.create(cmd.customerId, cmd.items);
|
|
1130
|
-
* repository.add(order);
|
|
1131
|
-
* return ok(order.id);
|
|
1132
|
-
* };
|
|
1133
|
-
* ```
|
|
1134
|
-
*
|
|
1135
|
-
* @example Using with external frameworks
|
|
1136
|
-
* ```typescript
|
|
1137
|
-
* // Handler typed with CommandHandler for type safety
|
|
1138
|
-
* const createOrderHandler: CommandHandler<CreateOrderCommand, OrderId> = async (cmd) => {
|
|
1139
|
-
* // ... handler logic
|
|
1140
|
-
* return ok(orderId);
|
|
1141
|
-
* };
|
|
942
|
+
* Public contract for Event-Sourced Aggregate Roots. Extends
|
|
943
|
+
* `Aggregate` with the replay-from-history boundary.
|
|
1142
944
|
*
|
|
1143
|
-
*
|
|
1144
|
-
*
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
*
|
|
945
|
+
* @template TId - The aggregate root identifier
|
|
946
|
+
* @template TEvent - The union type of all domain events
|
|
947
|
+
*/
|
|
948
|
+
interface ReplayableAggregate<TId extends Id<string>, TEvent extends AnyDomainEvent> extends Aggregate<TId, TEvent> {
|
|
949
|
+
/**
|
|
950
|
+
* Reconstitutes the aggregate from an event history. Returns
|
|
951
|
+
* `Result` because event-stream corruption is an expected
|
|
952
|
+
* recoverable failure at the infrastructure boundary: a `DomainError`
|
|
953
|
+
* thrown by a fold arrives as `Err`. Every other failure propagates
|
|
954
|
+
* after the all-or-nothing rollback.
|
|
955
|
+
*
|
|
956
|
+
* @throws ForeignEventError when a history event names another aggregate
|
|
957
|
+
* @throws UnreplayableAggregateError when the target carries pending
|
|
958
|
+
* decisions, or a fold records one
|
|
959
|
+
* @throws MissingFoldError when no fold is declared for an event type
|
|
960
|
+
* @throws FoldReturnedNoStateError when a fold returns `undefined`
|
|
961
|
+
* @throws HostileStateKeyError when the folded state carries an own
|
|
962
|
+
* `__proto__` key
|
|
963
|
+
*/
|
|
964
|
+
replayHistory(history: ReadonlyArray<TEvent>): Result<void, DomainError>;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Checks if two aggregates are at the same version (same ID and version).
|
|
968
|
+
* Useful for optimistic concurrency control checks.
|
|
969
|
+
*
|
|
970
|
+
* Note: Two aggregates with the same ID ARE the same aggregate (identity).
|
|
971
|
+
* This function checks if they are at the same version: i.e., no concurrent modification.
|
|
972
|
+
*
|
|
973
|
+
* @example
|
|
974
|
+
* ```typescript
|
|
975
|
+
* const before = await repository.findById(id);
|
|
976
|
+
* // ... some operations ...
|
|
977
|
+
* const after = await repository.findById(id);
|
|
978
|
+
*
|
|
979
|
+
* if (!sameVersion(before, after)) {
|
|
980
|
+
* throw new Error("Aggregate was modified by another process");
|
|
981
|
+
* }
|
|
1166
982
|
* ```
|
|
1167
983
|
*/
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
/** Absolute Unix epoch millisecond at which the shell stops waiting. */
|
|
1176
|
-
readonly deadlineAt: number;
|
|
1177
|
-
}
|
|
984
|
+
declare function sameVersion<TId extends Id<string>>(a: {
|
|
985
|
+
id: TId;
|
|
986
|
+
version: Version;
|
|
987
|
+
}, b: {
|
|
988
|
+
id: TId;
|
|
989
|
+
version: Version;
|
|
990
|
+
}): boolean;
|
|
1178
991
|
//#endregion
|
|
1179
|
-
//#region src/
|
|
992
|
+
//#region src/messaging/event-bus/ports.d.ts
|
|
1180
993
|
/**
|
|
1181
994
|
* Event handler function type for subscribing to domain events. The execution
|
|
1182
995
|
* context carries the publication's cooperative cancellation and deadline;
|
|
@@ -1190,7 +1003,14 @@ type EventHandler<Evt> = (event: Evt, context: ExecutionContext) => Promise<void
|
|
|
1190
1003
|
interface PublishOptions {
|
|
1191
1004
|
/** Owner/request cancellation propagated to every event handler. */
|
|
1192
1005
|
readonly signal?: AbortSignal;
|
|
1193
|
-
/**
|
|
1006
|
+
/**
|
|
1007
|
+
* Maximum time to await the complete publication. Default `30000`ms.
|
|
1008
|
+
*
|
|
1009
|
+
* This bounds the WAIT, not the handler. JavaScript cannot terminate a
|
|
1010
|
+
* running promise, so a handler that ignores `context.signal` keeps
|
|
1011
|
+
* running after `publish` rejects, and its side effects still land.
|
|
1012
|
+
* Pass `context.signal` into every I/O call a handler makes.
|
|
1013
|
+
*/
|
|
1194
1014
|
readonly timeoutMs?: number;
|
|
1195
1015
|
}
|
|
1196
1016
|
/**
|
|
@@ -1222,25 +1042,36 @@ interface EventBus<Evt extends AnyDomainEvent> {
|
|
|
1222
1042
|
*
|
|
1223
1043
|
* **Ordering & parallelism contract:**
|
|
1224
1044
|
*
|
|
1225
|
-
* 1. **Events run in input order.** `publish([a, b, c])` dispatches `a
|
|
1226
|
-
* awaits
|
|
1227
|
-
*
|
|
1228
|
-
*
|
|
1229
|
-
*
|
|
1230
|
-
*
|
|
1231
|
-
*
|
|
1232
|
-
*
|
|
1233
|
-
*
|
|
1234
|
-
*
|
|
1235
|
-
*
|
|
1236
|
-
*
|
|
1237
|
-
*
|
|
1238
|
-
*
|
|
1239
|
-
*
|
|
1045
|
+
* 1. **Events run in input order.** `publish([a, b, c])` dispatches `a`
|
|
1046
|
+
* and awaits every handler of `a`. Then it dispatches `b`, and so
|
|
1047
|
+
* on. The bus never changes that order. It never dispatches two
|
|
1048
|
+
* events at the same time.
|
|
1049
|
+
* 2. **The handlers of one event run in parallel.** The bus awaits
|
|
1050
|
+
* every handler of `event.type` through `Promise.allSettled`. One
|
|
1051
|
+
* handler never sees the error of another handler. The bus skips no
|
|
1052
|
+
* handler when a peer fails. The bus applies no limit here: twenty
|
|
1053
|
+
* handlers that each open a connection open twenty connections.
|
|
1054
|
+
* Backpressure belongs to the client that the handler calls.
|
|
1055
|
+
* 3. **The bus collects the errors and throws them after the batch.**
|
|
1056
|
+
* If one handler throws, the other handlers of that event still
|
|
1057
|
+
* run, and the remaining events still publish. At the end of the
|
|
1058
|
+
* batch `publish` throws. One failure throws that error directly.
|
|
1059
|
+
* Two or more failures throw an `AggregateError` with the message
|
|
1060
|
+
* "Multiple event handlers failed", which carries every collected
|
|
1061
|
+
* error. For fail-fast behavior, publish one event for each call.
|
|
1062
|
+
* A batch is not atomic.
|
|
1063
|
+
*
|
|
1064
|
+
* The contract is intentionally simple and in-process. For delivery
|
|
1065
|
+
* across processes, for example through RabbitMQ or Kafka, use the
|
|
1066
|
+
* `Outbox` port and a dedicated dispatcher.
|
|
1067
|
+
*
|
|
1068
|
+
* **Delivery guarantee.** The port does not promise persistence, retry, or
|
|
1069
|
+
* a dead-letter path. Each implementation states its own guarantee. Work
|
|
1070
|
+
* that must survive a crash belongs behind the `Outbox` port.
|
|
1240
1071
|
*
|
|
1241
|
-
*
|
|
1242
|
-
*
|
|
1243
|
-
*
|
|
1072
|
+
* **Handlers must tolerate a second run.** The port never redelivers. A
|
|
1073
|
+
* caller that retries does redeliver, and the handlers of the first
|
|
1074
|
+
* attempt can still run. Make a handler idempotent, or do not retry.
|
|
1244
1075
|
*
|
|
1245
1076
|
* @param events - Array of events to publish
|
|
1246
1077
|
* @param options - Owner cancellation and publication timeout
|
|
@@ -1267,6 +1098,38 @@ interface EventBus<Evt extends AnyDomainEvent> {
|
|
|
1267
1098
|
subscribe: <K extends Evt["type"]>(eventType: K, handler: EventHandler<Extract<Evt, {
|
|
1268
1099
|
type: K;
|
|
1269
1100
|
}>>) => () => void;
|
|
1101
|
+
/**
|
|
1102
|
+
* Subscribes one handler to a set of event types.
|
|
1103
|
+
*
|
|
1104
|
+
* The returned function releases every subscription it made, so a
|
|
1105
|
+
* consumer that reacts to several types keeps one release instead of
|
|
1106
|
+
* one for each type. Losing one of several releases is how a partial
|
|
1107
|
+
* leak starts.
|
|
1108
|
+
*
|
|
1109
|
+
* A type that appears twice subscribes once: the argument is a set of
|
|
1110
|
+
* types, and delivering the same event twice to one handler would be a
|
|
1111
|
+
* surprise, not a feature. An empty set subscribes nothing and returns
|
|
1112
|
+
* a release that does nothing.
|
|
1113
|
+
*
|
|
1114
|
+
* @param eventTypes - The event types to subscribe to
|
|
1115
|
+
* @param handler - Called with every event of those types, narrowed to
|
|
1116
|
+
* their union
|
|
1117
|
+
* @returns A function that releases all of them, and does nothing when
|
|
1118
|
+
* called again
|
|
1119
|
+
*
|
|
1120
|
+
* @example
|
|
1121
|
+
* ```typescript
|
|
1122
|
+
* const release = bus.subscribeMany(
|
|
1123
|
+
* ["OrderCreated", "OrderShipped"],
|
|
1124
|
+
* async (event) => {
|
|
1125
|
+
* await touchReadModel(event.payload.orderId);
|
|
1126
|
+
* },
|
|
1127
|
+
* );
|
|
1128
|
+
* ```
|
|
1129
|
+
*/
|
|
1130
|
+
subscribeMany: <K extends Evt["type"]>(eventTypes: readonly K[], handler: EventHandler<Extract<Evt, {
|
|
1131
|
+
type: K;
|
|
1132
|
+
}>>) => () => void;
|
|
1270
1133
|
/**
|
|
1271
1134
|
* Subscribes a handler to EVERY event type: the subscription for
|
|
1272
1135
|
* cross-cutting consumers (audit log, metrics, dev logging,
|
|
@@ -1295,359 +1158,71 @@ interface EventBus<Evt extends AnyDomainEvent> {
|
|
|
1295
1158
|
*/
|
|
1296
1159
|
subscribeAll: (handler: EventHandler<Evt>) => () => void;
|
|
1297
1160
|
/**
|
|
1298
|
-
*
|
|
1299
|
-
* Returns a Promise that resolves with the event data.
|
|
1300
|
-
* Automatically unsubscribes after the first event.
|
|
1301
|
-
*
|
|
1302
|
-
* @param eventType - The event type to wait for
|
|
1303
|
-
* @returns A Promise that resolves with the event
|
|
1161
|
+
* Releases every subscription and settles every waiter.
|
|
1304
1162
|
*
|
|
1305
|
-
*
|
|
1306
|
-
*
|
|
1307
|
-
*
|
|
1308
|
-
* console.log("Order created:", event.payload.orderId);
|
|
1309
|
-
* ```
|
|
1310
|
-
*/
|
|
1311
|
-
once: <K extends Evt["type"]>(eventType: K, options?: OnceOptions) => Promise<Extract<Evt, {
|
|
1312
|
-
type: K;
|
|
1313
|
-
}>>;
|
|
1314
|
-
}
|
|
1315
|
-
/**
|
|
1316
|
-
* Options for `EventBus.once()`. Both fields are optional; without them
|
|
1317
|
-
* `once()` waits forever (the historical behaviour).
|
|
1318
|
-
*/
|
|
1319
|
-
interface OnceOptions {
|
|
1320
|
-
/**
|
|
1321
|
-
* Aborts the wait. When `signal` fires, `once()` rejects with
|
|
1322
|
-
* `signal.reason` (or a generic abort error if none was supplied) and
|
|
1323
|
-
* the internal subscription is removed.
|
|
1324
|
-
*/
|
|
1325
|
-
signal?: AbortSignal;
|
|
1326
|
-
/**
|
|
1327
|
-
* Rejects with a timeout error after this many milliseconds if no event
|
|
1328
|
-
* has arrived. The internal subscription and timer are cleaned up
|
|
1329
|
-
* regardless of which path settles the promise.
|
|
1330
|
-
*/
|
|
1331
|
-
timeoutMs?: number;
|
|
1332
|
-
}
|
|
1333
|
-
/**
|
|
1334
|
-
* Gap-proof position finalized by the event source at the persistence
|
|
1335
|
-
* boundary. It is deliberately separate from `DomainEvent`: these values
|
|
1336
|
-
* describe a stored commit, not the business fact itself.
|
|
1337
|
-
*/
|
|
1338
|
-
interface CommitPosition {
|
|
1339
|
-
/** Aggregate OCC version reached by this eventful commit. */
|
|
1340
|
-
readonly aggregateVersion: number;
|
|
1341
|
-
/** Zero-based event index inside this aggregate commit. */
|
|
1342
|
-
readonly commitSequence: number;
|
|
1343
|
-
/** Total number of events emitted by this aggregate commit. */
|
|
1344
|
-
readonly commitSize: number;
|
|
1345
|
-
/**
|
|
1346
|
-
* Aggregate version of the immediately preceding EVENTFUL commit for this
|
|
1347
|
-
* qualified aggregate source, or `null` when this is its first eventful
|
|
1348
|
-
* commit. State-only persistence is intentionally absent from this chain.
|
|
1163
|
+
* A bus that outlives its scope keeps its handlers alive with it. A
|
|
1164
|
+
* worker that shuts down, a test that tears down, and a request scope
|
|
1165
|
+
* that ends all need one call that leaves the bus holding nothing.
|
|
1349
1166
|
*
|
|
1350
|
-
*
|
|
1351
|
-
*
|
|
1352
|
-
*
|
|
1353
|
-
*
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
}
|
|
1357
|
-
/**
|
|
1358
|
-
* Commit information known by the application transaction before the outbox
|
|
1359
|
-
* source has linked this eventful commit to its predecessor.
|
|
1360
|
-
*/
|
|
1361
|
-
type EventCommitCandidatePosition = Omit<CommitPosition, "previousEventfulAggregateVersion">;
|
|
1362
|
-
/**
|
|
1363
|
-
* A bare domain event prepared for the transactional outbox. The outbox source
|
|
1364
|
-
* owns the predecessor link and turns this candidate into a
|
|
1365
|
-
* {@link CommittedDomainEvent} when it persists the record.
|
|
1366
|
-
*/
|
|
1367
|
-
interface EventCommitCandidate<Evt extends AnyDomainEvent> {
|
|
1368
|
-
readonly event: Evt;
|
|
1369
|
-
readonly source: AggregateAddress;
|
|
1370
|
-
readonly position: EventCommitCandidatePosition;
|
|
1371
|
-
}
|
|
1372
|
-
/**
|
|
1373
|
-
* A domain event enriched after persistence has established its source and
|
|
1374
|
-
* commit position. Outboxes and projectors consume this envelope; in-process
|
|
1375
|
-
* domain handlers continue to consume the bare {@link DomainEvent} value.
|
|
1376
|
-
*/
|
|
1377
|
-
interface CommittedDomainEvent<Evt extends AnyDomainEvent> {
|
|
1378
|
-
readonly event: Evt;
|
|
1379
|
-
readonly source: AggregateAddress;
|
|
1380
|
-
readonly position: CommitPosition;
|
|
1381
|
-
}
|
|
1382
|
-
/**
|
|
1383
|
-
* One pending event in the outbox plus the opaque id the implementation
|
|
1384
|
-
* needs to ack it via `markDispatched`. The library does not prescribe
|
|
1385
|
-
* what `dispatchId` looks like: an implementation can reuse the event's
|
|
1386
|
-
* own `eventId`, generate its own UUID, use the row's auto-increment
|
|
1387
|
-
* primary key, or whatever the storage layer prefers.
|
|
1388
|
-
*/
|
|
1389
|
-
interface OutboxRecord<Evt extends AnyDomainEvent> extends CommittedDomainEvent<Evt> {
|
|
1390
|
-
dispatchId: string;
|
|
1391
|
-
/**
|
|
1392
|
-
* Failed delivery attempts so far. Populated by implementations that
|
|
1393
|
-
* track dispatch failures (see {@link DispatchTrackingOutbox});
|
|
1394
|
-
* plain `Outbox` implementations may omit it.
|
|
1395
|
-
*/
|
|
1396
|
-
attempts?: number;
|
|
1397
|
-
}
|
|
1398
|
-
/** A record that exhausted its delivery attempts; see {@link DispatchTrackingOutbox.deadLetters}. */
|
|
1399
|
-
interface DeadLetterRecord<Evt extends AnyDomainEvent> extends CommittedDomainEvent<Evt> {
|
|
1400
|
-
dispatchId: string;
|
|
1401
|
-
/** Failed delivery attempts when the record was dead-lettered. */
|
|
1402
|
-
attempts: number;
|
|
1403
|
-
/** Human-readable rendering of the last delivery error, if recorded. */
|
|
1404
|
-
lastError?: string;
|
|
1405
|
-
}
|
|
1406
|
-
/**
|
|
1407
|
-
* Write half of the transactional outbox: the only outbox capability the
|
|
1408
|
-
* write side (`withCommit`, `UnitOfWork`) depends on. Persisting the
|
|
1409
|
-
* events atomically with the aggregate state is the kit's guarantee;
|
|
1410
|
-
* DELIVERY is a separate, replaceable concern.
|
|
1411
|
-
*
|
|
1412
|
-
* Implement ONLY this interface to plug in an external delivery
|
|
1413
|
-
* solution: `add()` writes into that solution's outbox storage inside
|
|
1414
|
-
* the ambient transaction, and its own listener (polling or
|
|
1415
|
-
* WAL/CDC-based, such as a Debezium-style connector, a delivery
|
|
1416
|
-
* library, or a broker-native outbox) owns delivery entirely. The
|
|
1417
|
-
* kit-side poll surface ({@link Outbox}) is then never involved. See
|
|
1418
|
-
* the outbox guide, "External dispatchers".
|
|
1419
|
-
*/
|
|
1420
|
-
interface OutboxWriter<Evt extends AnyDomainEvent> {
|
|
1421
|
-
/**
|
|
1422
|
-
* Finalizes and persists event commit candidates. Called from inside
|
|
1423
|
-
* `withCommit`'s transactional callback, atomically with the aggregate
|
|
1424
|
-
* write.
|
|
1167
|
+
* After this call, `publish`, `subscribe`, `subscribeAll` and `once`
|
|
1168
|
+
* throw. Use after close is a programming bug, and a silent no-op would
|
|
1169
|
+
* look like a delivery that did not happen. A pending `once()` rejects
|
|
1170
|
+
* rather than waiting forever, which is the only waiter the port can
|
|
1171
|
+
* settle: a handler is a callback and learns that no event follows by
|
|
1172
|
+
* not being called again.
|
|
1425
1173
|
*
|
|
1426
|
-
*
|
|
1427
|
-
* advancement, read its last eventful aggregate version, write that value as
|
|
1428
|
-
* `previousEventfulAggregateVersion` on every event in the candidate's
|
|
1429
|
-
* commit, and advance the source head to `aggregateVersion` in the SAME
|
|
1430
|
-
* transaction. A state-only aggregate commit does not call `add()` and must
|
|
1431
|
-
* therefore not advance this event-source head.
|
|
1174
|
+
* Calling it again does nothing.
|
|
1432
1175
|
*
|
|
1433
|
-
* A
|
|
1434
|
-
*
|
|
1435
|
-
*
|
|
1436
|
-
*
|
|
1437
|
-
*
|
|
1176
|
+
* A plain method, not `Symbol.dispose`. A port that declared the symbol
|
|
1177
|
+
* would need `esnext.disposable` in the `lib` of every consumer, only to
|
|
1178
|
+
* typecheck the types of this kit, and that requirement cannot be
|
|
1179
|
+
* declined. Group your own releases instead, as the common mistakes
|
|
1180
|
+
* guide shows.
|
|
1438
1181
|
*
|
|
1439
|
-
*
|
|
1440
|
-
*
|
|
1441
|
-
*
|
|
1442
|
-
*
|
|
1443
|
-
* once. A unique-key constraint on `(eventId)` in the outbox table is the
|
|
1444
|
-
* standard implementation; the source-head update and dedupe decision must
|
|
1445
|
-
* share the transaction. Idempotency applies only to an exact candidate
|
|
1446
|
-
* retry: the same event ID, qualified source, aggregate version, commit
|
|
1447
|
-
* sequence, and commit size. Reusing an `eventId` for another source or
|
|
1448
|
-
* position is a caller bug: adapters that retain the conflicting record
|
|
1449
|
-
* should reject it rather than replace or silently reinterpret it as a retry.
|
|
1450
|
-
*/
|
|
1451
|
-
add: (events: ReadonlyArray<EventCommitCandidate<Evt>>) => Promise<void>;
|
|
1452
|
-
}
|
|
1453
|
-
/**
|
|
1454
|
-
* Transactional outbox port: the bridge between the write-side
|
|
1455
|
-
* transaction and the (out-of-band) event dispatcher.
|
|
1456
|
-
*
|
|
1457
|
-
* Lifecycle:
|
|
1458
|
-
* 1. `add()` inside the write transaction (`withCommit` calls this) so
|
|
1459
|
-
* events persist atomically with the aggregate state
|
|
1460
|
-
* ({@link OutboxWriter}, the only part the write side needs).
|
|
1461
|
-
* 2. An outbox dispatcher (the kit's `OutboxDispatcher` or your own)
|
|
1462
|
-
* polls `getPending()` and forwards the events to subscribers /
|
|
1463
|
-
* external brokers.
|
|
1464
|
-
* 3. After successful dispatch, the dispatcher calls `markDispatched()`
|
|
1465
|
-
* with the records' `dispatchId`s so they don't come back next poll.
|
|
1466
|
-
*
|
|
1467
|
-
* `markDispatched` is required to be idempotent: calling it with an id
|
|
1468
|
-
* that's already marked is a no-op, not an error. This lets the
|
|
1469
|
-
* dispatcher safely retry on partial-failure.
|
|
1470
|
-
*
|
|
1471
|
-
* **Competing dispatcher instances** are an adapter contract, not a
|
|
1472
|
-
* dispatcher feature: a transactional implementation that should
|
|
1473
|
-
* support several concurrent pollers must make `getPending` claim the
|
|
1474
|
-
* returned records (`FOR UPDATE SKIP LOCKED` or equivalent). Without
|
|
1475
|
-
* claiming, run one logical dispatcher per outbox.
|
|
1476
|
-
*
|
|
1477
|
-
* The bundled dispatcher supplies an {@link ExecutionContext} to every poll-side
|
|
1478
|
-
* operation. Production adapters MUST pass its signal to native I/O or enforce
|
|
1479
|
-
* a native timeout no later than `deadlineAt`; the shell can bound its wait but
|
|
1480
|
-
* cannot terminate a promise that ignores cancellation. A timed-out write has
|
|
1481
|
-
* an unknown outcome. Acknowledgements must remain idempotent when they complete
|
|
1482
|
-
* late; a late failure update may count its original delivery attempt and must
|
|
1483
|
-
* still no-op after the record was dispatched.
|
|
1484
|
-
*/
|
|
1485
|
-
interface Outbox<Evt extends AnyDomainEvent> extends OutboxWriter<Evt> {
|
|
1486
|
-
/**
|
|
1487
|
-
* Returns up to `limit` outbox records that have not yet been
|
|
1488
|
-
* dispatched, **in the order `add()` persisted them** (commit order).
|
|
1489
|
-
* The ordering is part of the port contract: `withCommit` promises
|
|
1490
|
-
* subscribers per-aggregate causal order, and a sequential dispatcher
|
|
1491
|
-
* can only honor that promise when this read is ordered. SQL-backed
|
|
1492
|
-
* implementations need a monotonic position column (an auto-increment
|
|
1493
|
-
* primary key works) and an `ORDER BY` on it; a bare `SELECT` returns
|
|
1494
|
-
* rows in storage order, not insertion order. The dispatcher polls
|
|
1495
|
-
* this on a schedule. When `limit` is omitted, the implementation
|
|
1496
|
-
* decides on a default page size. The bundled dispatcher always supplies
|
|
1497
|
-
* `context`; it is optional only so existing adapters remain assignable.
|
|
1182
|
+
* This releases the subscriptions. It does not stop a handler that is
|
|
1183
|
+
* already running, because JavaScript cannot terminate a running
|
|
1184
|
+
* promise. Pass `context.signal` into every call a handler makes, and a
|
|
1185
|
+
* publication in flight ends with the handler that honours it.
|
|
1498
1186
|
*/
|
|
1499
|
-
|
|
1187
|
+
close: () => void;
|
|
1500
1188
|
/**
|
|
1501
|
-
*
|
|
1502
|
-
*
|
|
1503
|
-
*
|
|
1504
|
-
*
|
|
1189
|
+
* Subscribes to the next occurrence of an event type.
|
|
1190
|
+
* Returns a Promise that resolves with the event data.
|
|
1191
|
+
* Automatically unsubscribes after the first event.
|
|
1192
|
+
*
|
|
1193
|
+
* @param eventType - The event type to wait for
|
|
1194
|
+
* @returns A Promise that resolves with the event
|
|
1195
|
+
*
|
|
1196
|
+
* @example
|
|
1197
|
+
* ```typescript
|
|
1198
|
+
* const event = await bus.once("OrderCreated");
|
|
1199
|
+
* console.log("Order created:", event.payload.orderId);
|
|
1200
|
+
* ```
|
|
1505
1201
|
*/
|
|
1506
|
-
|
|
1202
|
+
once: <K extends Evt["type"]>(eventType: K, options?: OnceOptions) => Promise<Extract<Evt, {
|
|
1203
|
+
type: K;
|
|
1204
|
+
}>>;
|
|
1507
1205
|
}
|
|
1508
1206
|
/**
|
|
1509
|
-
*
|
|
1510
|
-
*
|
|
1511
|
-
* event whose delivery always throws) is redelivered forever: it comes
|
|
1512
|
-
* back from every `getPending` poll, blocks per-aggregate ordering
|
|
1513
|
-
* behind it, and burns the dispatcher's cycles. This extension gives
|
|
1514
|
-
* the dispatcher a bounded-retry story: report each failed delivery via
|
|
1515
|
-
* {@link markFailed}; the implementation moves records past its
|
|
1516
|
-
* attempt ceiling to a dead-letter set that `getPending` no longer
|
|
1517
|
-
* returns, and {@link deadLetters} exposes them for alerting, manual
|
|
1518
|
-
* inspection, and redelivery (deliver by hand, then ack via
|
|
1519
|
-
* `markDispatched`, which also clears dead-lettered records).
|
|
1520
|
-
*
|
|
1521
|
-
* See the outbox guide's dispatcher recipe for the retry-then-dead-letter
|
|
1522
|
-
* loop this port shape supports.
|
|
1207
|
+
* Options for `EventBus.once()`. Both fields are optional; without them
|
|
1208
|
+
* `once()` waits forever.
|
|
1523
1209
|
*/
|
|
1524
|
-
interface
|
|
1210
|
+
interface OnceOptions {
|
|
1525
1211
|
/**
|
|
1526
|
-
*
|
|
1527
|
-
*
|
|
1528
|
-
*
|
|
1529
|
-
* ceiling is reached, moves the record to the dead-letter set.
|
|
1530
|
-
* A no-op for unknown or already-dispatched ids (a late failure
|
|
1531
|
-
* report after a successful retry must not resurrect the record).
|
|
1532
|
-
* Returns the exact dead-letter record only on the call that performs
|
|
1533
|
-
* that transition; retries below the ceiling and no-ops return
|
|
1534
|
-
* `undefined`. This lets productive pollers emit an immediate signal
|
|
1535
|
-
* without scanning the durable dead-letter set after every failure. A late
|
|
1536
|
-
* completion may count that original delivery attempt; it must still no-op if
|
|
1537
|
-
* the record was dispatched in the meantime. The bundled dispatcher never
|
|
1538
|
-
* reissues the same store call and always supplies `context`.
|
|
1212
|
+
* Aborts the wait. When `signal` fires, `once()` rejects with
|
|
1213
|
+
* `signal.reason` (or a generic abort error if none was supplied) and
|
|
1214
|
+
* the internal subscription is removed.
|
|
1539
1215
|
*/
|
|
1540
|
-
|
|
1216
|
+
signal?: AbortSignal;
|
|
1541
1217
|
/**
|
|
1542
|
-
*
|
|
1543
|
-
*
|
|
1544
|
-
*
|
|
1545
|
-
* between this store transition and its immediate observer callback.
|
|
1218
|
+
* Rejects with a timeout error after this many milliseconds if no event
|
|
1219
|
+
* has arrived. The internal subscription and timer are cleaned up
|
|
1220
|
+
* regardless of which path settles the promise.
|
|
1546
1221
|
*/
|
|
1547
|
-
|
|
1548
|
-
}
|
|
1549
|
-
//#endregion
|
|
1550
|
-
//#region src/app/command-outbox.d.ts
|
|
1551
|
-
/**
|
|
1552
|
-
* Business relationships and technical trace context selected explicitly for
|
|
1553
|
-
* an outgoing command. Correlation/conversation explain the business flow;
|
|
1554
|
-
* W3C Trace Context connects technical spans.
|
|
1555
|
-
*/
|
|
1556
|
-
interface CommandMessageRelationships {
|
|
1557
|
-
/** Groups messages that belong to one operation or trace. */
|
|
1558
|
-
readonly correlationId?: string;
|
|
1559
|
-
/** Groups every message in one long-running business interaction. */
|
|
1560
|
-
readonly conversationId?: string;
|
|
1561
|
-
/** W3C Trace Context parent for technical distributed tracing. */
|
|
1562
|
-
readonly traceparent?: string;
|
|
1563
|
-
/** Optional vendor trace state associated with `traceparent`. */
|
|
1564
|
-
readonly tracestate?: string;
|
|
1565
|
-
}
|
|
1566
|
-
/**
|
|
1567
|
-
* Application-owned Published Language produced from one private domain or
|
|
1568
|
-
* process event. `destination` names one receiver contract; it is deliberately
|
|
1569
|
-
* required because a command is an instruction, not a broadcast fact.
|
|
1570
|
-
*
|
|
1571
|
-
* The command carries a stable schema `version` and JSON-safe `payload`.
|
|
1572
|
-
* Domain value objects are translated to wire DTOs by the mapper before this
|
|
1573
|
-
* boundary.
|
|
1574
|
-
*/
|
|
1575
|
-
interface CommandMessageContent<C extends PublishedCommand> extends CommandMessageRelationships {
|
|
1576
|
-
readonly destination: string;
|
|
1577
|
-
readonly command: C;
|
|
1578
|
-
}
|
|
1579
|
-
/**
|
|
1580
|
-
* Immutable, JSON-safe command envelope stored for later at-least-once
|
|
1581
|
-
* delivery.
|
|
1582
|
-
*
|
|
1583
|
-
* `causationId` always identifies the private event whose accepted decision
|
|
1584
|
-
* requested this command. The mapper cannot replace it with a weaker
|
|
1585
|
-
* correlation. Consumer-produced events should in turn use `messageId` as
|
|
1586
|
-
* their causation id.
|
|
1587
|
-
*/
|
|
1588
|
-
interface DurableCommandMessage<C extends PublishedCommand> extends CommandMessageContent<C> {
|
|
1589
|
-
readonly messageId: string;
|
|
1590
|
-
readonly recordedAt: string;
|
|
1591
|
-
readonly causationId: string;
|
|
1592
|
-
}
|
|
1593
|
-
/**
|
|
1594
|
-
* Receipt for the private event that requested one command batch. It retains
|
|
1595
|
-
* commit identity and ordering without putting the private event or its
|
|
1596
|
-
* payload into the command outbox.
|
|
1597
|
-
*/
|
|
1598
|
-
interface CommandCommitOriginCandidate {
|
|
1599
|
-
readonly eventId: string;
|
|
1600
|
-
readonly source: AggregateAddress;
|
|
1601
|
-
readonly position: EventCommitCandidatePosition;
|
|
1602
|
-
}
|
|
1603
|
-
/**
|
|
1604
|
-
* One private process-event commit and the exact commands it requested.
|
|
1605
|
-
* `messages` may be empty: the receipt still advances the originating source
|
|
1606
|
-
* and makes an exact retry distinguishable from a missing commit.
|
|
1607
|
-
*/
|
|
1608
|
-
interface CommandOutboxCommitCandidate<C extends PublishedCommand> {
|
|
1609
|
-
readonly origin: CommandCommitOriginCandidate;
|
|
1610
|
-
readonly messages: ReadonlyArray<DurableCommandMessage<C>>;
|
|
1611
|
-
}
|
|
1612
|
-
/**
|
|
1613
|
-
* Write port for a dedicated transactional command outbox.
|
|
1614
|
-
*
|
|
1615
|
-
* The adapter is bound to the same ambient transaction as the aggregate or
|
|
1616
|
-
* event-stream repository. It must persist the complete input atomically,
|
|
1617
|
-
* retain input order, deduplicate exact retries by `origin.eventId`, and reject
|
|
1618
|
-
* a reused origin id whose source, position, or messages differ. It also owns
|
|
1619
|
-
* the durable source cursor represented by `origin.position`; an empty command
|
|
1620
|
-
* batch still advances that cursor.
|
|
1621
|
-
*
|
|
1622
|
-
* Delivery is out of band and at least once. A consumer therefore uses
|
|
1623
|
-
* `message.messageId` as its idempotency key and acknowledges only after the
|
|
1624
|
-
* command result has been stored.
|
|
1625
|
-
*/
|
|
1626
|
-
interface CommandOutboxWriter<C extends PublishedCommand> {
|
|
1627
|
-
add(commits: ReadonlyArray<CommandOutboxCommitCandidate<C>>): Promise<void>;
|
|
1222
|
+
timeoutMs?: number;
|
|
1628
1223
|
}
|
|
1629
|
-
/** Maps one private accepted event to zero or more addressed commands. */
|
|
1630
|
-
type CommandOutboxMapper<Evt extends AnyDomainEvent, C extends PublishedCommand> = (event: Evt) => ReadonlyArray<CommandMessageContent<C>>;
|
|
1631
|
-
/**
|
|
1632
|
-
* Adapts a dedicated command outbox to the event-candidate write port consumed
|
|
1633
|
-
* by `withCommit`.
|
|
1634
|
-
*
|
|
1635
|
-
* Mapping happens inside the transaction, before the command outbox write.
|
|
1636
|
-
* The private event is used only at this boundary and is reduced to an origin
|
|
1637
|
-
* receipt. The helper never publishes it or copies its payload implicitly; the
|
|
1638
|
-
* application mapper selects and translates the data that belongs in the
|
|
1639
|
-
* versioned Published Language. The route rejects values JSON would lose or
|
|
1640
|
-
* change before it calls the adapter.
|
|
1641
|
-
* Every command gets a stable id derived from the event id and its zero-based
|
|
1642
|
-
* order, so an exact transaction retry produces the same rows.
|
|
1643
|
-
*
|
|
1644
|
-
* Omit `withCommit`'s in-process `bus` for private process events. Participants
|
|
1645
|
-
* consume the durable command messages from their explicitly named
|
|
1646
|
-
* destinations, while event-stream replay only rebuilds process state.
|
|
1647
|
-
*/
|
|
1648
|
-
declare function routeEventsToCommandOutbox<C extends PublishedCommand, Evt extends AnyDomainEvent = AnyDomainEvent>(outbox: CommandOutboxWriter<C>, mapper: CommandOutboxMapper<Evt, C>): OutboxWriter<Evt>;
|
|
1649
1224
|
//#endregion
|
|
1650
|
-
//#region src/
|
|
1225
|
+
//#region src/persistence/repository/scope.d.ts
|
|
1651
1226
|
/** Options passed to {@link TransactionScope.transactional}. */
|
|
1652
1227
|
interface TransactionalOptions {
|
|
1653
1228
|
/**
|
|
@@ -1720,7 +1295,7 @@ interface TransactionScope<TCtx> {
|
|
|
1720
1295
|
transactional<T>(fn: (ctx: TCtx) => Promise<T>, options?: TransactionalOptions): Promise<T>;
|
|
1721
1296
|
}
|
|
1722
1297
|
//#endregion
|
|
1723
|
-
//#region src/
|
|
1298
|
+
//#region src/application/cqrs/handler.d.ts
|
|
1724
1299
|
/** Dependencies for {@link withCommit}. */
|
|
1725
1300
|
interface WithCommitDeps<Evt extends AnyDomainEvent, TCtx> {
|
|
1726
1301
|
/**
|
|
@@ -1752,7 +1327,7 @@ interface WithCommitDeps<Evt extends AnyDomainEvent, TCtx> {
|
|
|
1752
1327
|
* committed write into an apparent failure. The execution context carries
|
|
1753
1328
|
* owner cancellation and the configured post-commit deadline.
|
|
1754
1329
|
*/
|
|
1755
|
-
onPersisted?: (aggregate:
|
|
1330
|
+
onPersisted?: (aggregate: Aggregate<Id<string>, Evt>, version: Version, context: ExecutionContext) => void | Promise<void>;
|
|
1756
1331
|
/**
|
|
1757
1332
|
* Observer for post-commit persistence failures: either the internal
|
|
1758
1333
|
* acknowledgement/disposal step or the application-shell `onPersisted`
|
|
@@ -1764,7 +1339,7 @@ interface WithCommitDeps<Evt extends AnyDomainEvent, TCtx> {
|
|
|
1764
1339
|
* post-commit invariant holds, and the loop continues the remaining
|
|
1765
1340
|
* post-commit work.
|
|
1766
1341
|
*/
|
|
1767
|
-
onPersistError?: (error: unknown, aggregate:
|
|
1342
|
+
onPersistError?: (error: unknown, aggregate: Aggregate<Id<string>, Evt>) => void;
|
|
1768
1343
|
/**
|
|
1769
1344
|
* Total time allotted to the complete post-commit application phase:
|
|
1770
1345
|
* every application observer followed by in-process bus publication shares
|
|
@@ -1801,18 +1376,18 @@ interface AggregateCommitToken<Evt extends AnyDomainEvent = AnyDomainEvent> {
|
|
|
1801
1376
|
* repository write, and return every resulting token in `commits`. Omitting
|
|
1802
1377
|
* any token rejects the transaction: an enrolled write may not commit without
|
|
1803
1378
|
* its event harvest and post-commit acknowledgement. Enrollable instances
|
|
1804
|
-
* must extend `
|
|
1805
|
-
* `
|
|
1379
|
+
* must extend `StateStoredAggregate` or `EventSourcedAggregate`; structural
|
|
1380
|
+
* `Aggregate` lookalikes have no internal lifecycle capability and fail
|
|
1806
1381
|
* before commit.
|
|
1807
1382
|
*/
|
|
1808
1383
|
interface CommitEnrollment<Evt extends AnyDomainEvent> {
|
|
1809
|
-
enrollSaved(aggregate:
|
|
1384
|
+
enrollSaved(aggregate: Aggregate<Id<string>, Evt>, options?: CommitEnrollmentOptions): AggregateCommitToken<Evt>;
|
|
1810
1385
|
/**
|
|
1811
1386
|
* Enroll an aggregate whose row is deleted by the current transaction.
|
|
1812
1387
|
* Its events are harvested and discarded after commit, but the saved-only
|
|
1813
1388
|
* application `onPersisted` observer is not called.
|
|
1814
1389
|
*/
|
|
1815
|
-
enrollDeleted(aggregate:
|
|
1390
|
+
enrollDeleted(aggregate: Aggregate<Id<string>, Evt>, options?: CommitEnrollmentOptions): AggregateCommitToken<Evt>;
|
|
1816
1391
|
}
|
|
1817
1392
|
/** OCC baseline associated with one exact commit enrollment. */
|
|
1818
1393
|
interface CommitEnrollmentOptions {
|
|
@@ -1924,34 +1499,189 @@ interface WithCommitWorkResult<Evt extends AnyDomainEvent, R> {
|
|
|
1924
1499
|
* adapter persistence projection and rejects later mutation before flush. For
|
|
1925
1500
|
* direct `withCommit` use, make domain decisions first, write, and enroll last.
|
|
1926
1501
|
*
|
|
1927
|
-
* **Duplicate enrollment is idempotent by reference.** Enrolling the same
|
|
1928
|
-
* instance repeatedly returns the same token, and a repeated token in
|
|
1929
|
-
* `commits` is harvested once. A repeat call that omits `expectedVersion`
|
|
1930
|
-
* makes no OCC assertion; only a supplied value that contradicts the
|
|
1931
|
-
* enrollment-time baseline rejects. Each event lands in the outbox exactly once
|
|
1932
|
-
* and post-commit acknowledgement runs exactly once. Two
|
|
1933
|
-
* *different* instances with the same logical id cannot be detected
|
|
1934
|
-
* at this layer; that is a Repository contract violation (failure to
|
|
1935
|
-
* maintain Fowler's Identity Map per Unit of Work). See
|
|
1936
|
-
* `docs/guide/repository.md` → "Identity Map: one instance per
|
|
1937
|
-
* aggregate per Unit of Work" for the requirement on repository
|
|
1938
|
-
* implementations that makes this dedupe sound.
|
|
1502
|
+
* **Duplicate enrollment is idempotent by reference.** Enrolling the same
|
|
1503
|
+
* instance repeatedly returns the same token, and a repeated token in
|
|
1504
|
+
* `commits` is harvested once. A repeat call that omits `expectedVersion`
|
|
1505
|
+
* makes no OCC assertion; only a supplied value that contradicts the
|
|
1506
|
+
* enrollment-time baseline rejects. Each event lands in the outbox exactly once
|
|
1507
|
+
* and post-commit acknowledgement runs exactly once. Two
|
|
1508
|
+
* *different* instances with the same logical id cannot be detected
|
|
1509
|
+
* at this layer; that is a Repository contract violation (failure to
|
|
1510
|
+
* maintain Fowler's Identity Map per Unit of Work). See
|
|
1511
|
+
* `docs/guide/repository.md` → "Identity Map: one instance per
|
|
1512
|
+
* aggregate per Unit of Work" for the requirement on repository
|
|
1513
|
+
* implementations that makes this dedupe sound.
|
|
1514
|
+
*
|
|
1515
|
+
* @example Tx-bound repos (Drizzle, Prisma, Mongo, …)
|
|
1516
|
+
* ```typescript
|
|
1517
|
+
* const result = await withCommit({ outbox, bus, scope }, async (tx, enrollment) => {
|
|
1518
|
+
* const orderRepository = makeOrderRepository(tx); // your factory binds tx to the repo
|
|
1519
|
+
* const order = await orderRepository.getById(orderId);
|
|
1520
|
+
* order.confirm();
|
|
1521
|
+
* await persistOrder(tx, order); // low-level adapter write
|
|
1522
|
+
* const commit = enrollment.enrollSaved(order); // attest the repository write
|
|
1523
|
+
* return { result: order.id, commits: [commit] };
|
|
1524
|
+
* });
|
|
1525
|
+
* ```
|
|
1526
|
+
*/
|
|
1527
|
+
declare function withCommit<Evt extends AnyDomainEvent, R, TCtx>(deps: WithCommitDeps<Evt, TCtx>, fn: (ctx: TCtx, enrollment: CommitEnrollment<Evt>) => Promise<WithCommitWorkResult<Evt, R>>): Promise<R>;
|
|
1528
|
+
//#endregion
|
|
1529
|
+
//#region src/application/deadlines/deadline-store.d.ts
|
|
1530
|
+
/**
|
|
1531
|
+
* One deadline due for delivery, as returned by
|
|
1532
|
+
* {@link DeadlineStore.due}. `deliveryId` identifies this scheduled
|
|
1533
|
+
* INCARNATION of the deadline, not the `(scope, key)` address: a
|
|
1534
|
+
* reschedule replaces the incarnation, and acknowledging a stale
|
|
1535
|
+
* incarnation must never consume the new one (see
|
|
1536
|
+
* {@link DeadlineStore.markDelivered}).
|
|
1537
|
+
*/
|
|
1538
|
+
interface DueDeadline<TPayload = unknown> {
|
|
1539
|
+
/** Opaque per-incarnation id used for `markDelivered`/`markFailed`. */
|
|
1540
|
+
readonly deliveryId: string;
|
|
1541
|
+
/** The namespace half of the address, e.g. a process or policy name. */
|
|
1542
|
+
readonly scope: string;
|
|
1543
|
+
/** The instance half of the address, e.g. a saga or reservation id. */
|
|
1544
|
+
readonly key: string;
|
|
1545
|
+
/** When the deadline was due. */
|
|
1546
|
+
readonly dueAt: Date;
|
|
1547
|
+
/** The payload handed back as the input; plain data only. */
|
|
1548
|
+
readonly payload: TPayload;
|
|
1549
|
+
/** Failed delivery attempts so far (see `markFailed`). */
|
|
1550
|
+
readonly attempts: number;
|
|
1551
|
+
}
|
|
1552
|
+
/** A deadline that exhausted its delivery attempts; see {@link DeadlineStore.deadLetters}. */
|
|
1553
|
+
interface DeadLetterDeadline<TPayload = unknown> extends DueDeadline<TPayload> {
|
|
1554
|
+
/** Human-readable rendering of the last delivery error, if recorded. */
|
|
1555
|
+
readonly lastError?: string;
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Driven port for durable deadlines: timeout-as-input. A process that
|
|
1559
|
+
* waits ("if PaymentReceived has not arrived in 30 minutes,
|
|
1560
|
+
* compensate"; "release the reservation hold after 15 minutes";
|
|
1561
|
+
* "expire the offer at month's end") schedules a deadline, and a poll
|
|
1562
|
+
* loop later DELIVERS it as an input to whatever owns the decision, a
|
|
1563
|
+
* saga aggregate, a use case, a policy. The store never executes
|
|
1564
|
+
* consumer code; firing a deadline means handing back a record.
|
|
1565
|
+
*
|
|
1566
|
+
* Deliberately general-purpose and deliberately small. This is not a
|
|
1567
|
+
* scheduler framework and not a cron abstraction: there is no
|
|
1568
|
+
* recurrence, no execution engine, and the poll loop belongs to the
|
|
1569
|
+
* consumer (the outbox guide's `drainOnce` pattern fits; the deadlines
|
|
1570
|
+
* guide shows the wiring).
|
|
1571
|
+
*
|
|
1572
|
+
* Addressing is the `(scope, key)` pair, so one table serves every
|
|
1573
|
+
* waiting process in an application: `scope` names the policy
|
|
1574
|
+
* ("checkout-saga", "reservation-hold"), `key` the instance. There is
|
|
1575
|
+
* at most ONE pending deadline per address; `schedule` on an existing
|
|
1576
|
+
* address replaces it (that IS the reschedule operation), and each
|
|
1577
|
+
* scheduling gets a fresh `deliveryId`, so acknowledgements of a
|
|
1578
|
+
* replaced incarnation cannot consume its successor.
|
|
1579
|
+
*
|
|
1580
|
+
* Two sides, two transactional postures, the same split as the outbox:
|
|
1581
|
+
*
|
|
1582
|
+
* - **`schedule` and `cancel` are write-side calls** and must join the
|
|
1583
|
+
* ambient write transaction (use a tx-bound store instance inside
|
|
1584
|
+
* `withCommit`'s callback, exactly like an outbox adapter). This is
|
|
1585
|
+
* a correctness rule, not a preference: state that says "waiting for
|
|
1586
|
+
* payment" committed without its deadline is a process that never
|
|
1587
|
+
* wakes up, and a deadline scheduled for a rolled-back state change
|
|
1588
|
+
* is a ghost input.
|
|
1589
|
+
* - **`due`, `markDelivered`, `markFailed`, and `deadLetters` are the
|
|
1590
|
+
* poll surface** and run out of band, in the consumer's loop.
|
|
1591
|
+
*
|
|
1592
|
+
* Delivery is at-least-once: a crash between processing and
|
|
1593
|
+
* `markDelivered` redelivers, so consumers make deadline handling
|
|
1594
|
+
* idempotent (the idempotency store with the `deliveryId` as key is
|
|
1595
|
+
* the ready-made answer). Deadlines have no cross-key ordering
|
|
1596
|
+
* obligations, so unlike the outbox a poison deadline blocks only
|
|
1597
|
+
* itself; bounded retries still matter, which is why failure tracking
|
|
1598
|
+
* is part of the port rather than an extension: report failed
|
|
1599
|
+
* deliveries via `markFailed`, and the store dead-letters a deadline
|
|
1600
|
+
* past its attempt ceiling.
|
|
1601
|
+
*
|
|
1602
|
+
* Run one logical poller per store unless your adapter's `due` claims
|
|
1603
|
+
* records for competing pollers; the same rule as the outbox
|
|
1604
|
+
* dispatcher.
|
|
1605
|
+
*
|
|
1606
|
+
* The bundled processor supplies an `ExecutionContext` to every poll-side
|
|
1607
|
+
* operation. Production adapters MUST pass its signal to native I/O or enforce
|
|
1608
|
+
* a native timeout no later than `deadlineAt`; the shell can bound its wait but
|
|
1609
|
+
* cannot terminate a promise that ignores cancellation. A timed-out write has
|
|
1610
|
+
* an unknown outcome. Acknowledgements must remain idempotent when they complete
|
|
1611
|
+
* late; a late failure update may count its original delivery attempt and must
|
|
1612
|
+
* still no-op after the incarnation was delivered or replaced.
|
|
1939
1613
|
*
|
|
1940
|
-
*
|
|
1941
|
-
*
|
|
1942
|
-
*
|
|
1943
|
-
*
|
|
1944
|
-
*
|
|
1945
|
-
*
|
|
1946
|
-
*
|
|
1947
|
-
* const commit = enrollment.enrollSaved(order); // attest the repository write
|
|
1948
|
-
* return { result: order.id, commits: [commit] };
|
|
1949
|
-
* });
|
|
1950
|
-
* ```
|
|
1614
|
+
* Verify an adapter with `createDeadlineStoreContractTests` from
|
|
1615
|
+
* `@shirudo/ddd-kit/testing`; `InMemoryDeadlineStore` is the
|
|
1616
|
+
* reference.
|
|
1617
|
+
*
|
|
1618
|
+
* @template TPayload - The payload shape carried from `schedule` to
|
|
1619
|
+
* delivery; plain, serializable data (the same discipline as event
|
|
1620
|
+
* payloads and snapshots)
|
|
1951
1621
|
*/
|
|
1952
|
-
|
|
1622
|
+
interface DeadlineStore<TPayload = unknown> {
|
|
1623
|
+
/**
|
|
1624
|
+
* Schedules (or reschedules) the deadline at `(scope, key)`: at most
|
|
1625
|
+
* one pending deadline exists per address, and scheduling an
|
|
1626
|
+
* occupied address replaces its due time, payload, attempt count,
|
|
1627
|
+
* and incarnation. Called inside the write transaction.
|
|
1628
|
+
*/
|
|
1629
|
+
schedule(deadline: {
|
|
1630
|
+
scope: string;
|
|
1631
|
+
key: string;
|
|
1632
|
+
dueAt: Date;
|
|
1633
|
+
payload: TPayload;
|
|
1634
|
+
}): Promise<void>;
|
|
1635
|
+
/**
|
|
1636
|
+
* Removes the pending deadline at `(scope, key)`; a no-op when none
|
|
1637
|
+
* exists (the awaited input arrived in time and the wait is over).
|
|
1638
|
+
* Called inside the write transaction.
|
|
1639
|
+
*/
|
|
1640
|
+
cancel(scope: string, key: string): Promise<void>;
|
|
1641
|
+
/**
|
|
1642
|
+
* Up to `limit` deadlines with `dueAt <= now` that are neither
|
|
1643
|
+
* delivered nor dead-lettered, ordered by `dueAt` (earliest first;
|
|
1644
|
+
* ties in scheduling order). A `limit` of `0` is legal and yields an
|
|
1645
|
+
* empty page (poll loops computing a remaining capacity may pass
|
|
1646
|
+
* it). `now` is a parameter on purpose: the poll loop owns the
|
|
1647
|
+
* clock, which keeps adapters deterministic and tests free of real
|
|
1648
|
+
* time. The bundled processor always supplies `context`; it is optional only
|
|
1649
|
+
* so existing adapters remain assignable.
|
|
1650
|
+
*/
|
|
1651
|
+
due(now: Date, limit: number, context?: ExecutionContext): Promise<ReadonlyArray<DueDeadline<TPayload>>>;
|
|
1652
|
+
/**
|
|
1653
|
+
* Acknowledges delivered incarnations so they stop coming back.
|
|
1654
|
+
* Idempotent on already-acknowledged and unknown ids, and a no-op
|
|
1655
|
+
* for ids of REPLACED incarnations (a late ack after a reschedule
|
|
1656
|
+
* must not consume the successor). Also clears a dead-lettered
|
|
1657
|
+
* incarnation (manual redelivery, then ack). It remains idempotent if the
|
|
1658
|
+
* operation completes after the caller timed out. The bundled processor
|
|
1659
|
+
* always supplies `context`.
|
|
1660
|
+
*/
|
|
1661
|
+
markDelivered(deliveryIds: ReadonlyArray<string>, context?: ExecutionContext): Promise<void>;
|
|
1662
|
+
/**
|
|
1663
|
+
* Records one failed delivery attempt for the incarnation:
|
|
1664
|
+
* increments its `attempts` and, once the store's ceiling is
|
|
1665
|
+
* reached, moves it to the dead-letter set that `due` no longer
|
|
1666
|
+
* returns. A no-op for unknown, delivered, or replaced ids.
|
|
1667
|
+
* Returns the exact dead-letter record only on the call that performs
|
|
1668
|
+
* that transition; retries below the ceiling and no-ops return
|
|
1669
|
+
* `undefined`. A late completion may count that original delivery attempt; it
|
|
1670
|
+
* must still no-op if the incarnation was delivered or replaced in the
|
|
1671
|
+
* meantime. The bundled processor never reissues the same store call and
|
|
1672
|
+
* always supplies `context`.
|
|
1673
|
+
*/
|
|
1674
|
+
markFailed(deliveryId: string, error?: unknown, context?: ExecutionContext): Promise<DeadLetterDeadline<TPayload> | undefined>;
|
|
1675
|
+
/**
|
|
1676
|
+
* Deadlines that exhausted their delivery attempts. Wire this to durable
|
|
1677
|
+
* alerting and reconciliation: a growing set means processes that stopped
|
|
1678
|
+
* waking up, and the poller can stop between the store transition and its
|
|
1679
|
+
* immediate observer callback.
|
|
1680
|
+
*/
|
|
1681
|
+
deadLetters(): Promise<ReadonlyArray<DeadLetterDeadline<TPayload>>>;
|
|
1682
|
+
}
|
|
1953
1683
|
//#endregion
|
|
1954
|
-
//#region src/
|
|
1684
|
+
//#region src/application/idempotency/idempotency.d.ts
|
|
1955
1685
|
/**
|
|
1956
1686
|
* Result of `IdempotencyStore.claim()`: this execution owns the key and must
|
|
1957
1687
|
* run the command (`claimed`), a previous execution completed and its outcome
|
|
@@ -2219,162 +1949,7 @@ interface WithIdempotentCommitDeps<Evt extends AnyDomainEvent, TCtx> extends Wit
|
|
|
2219
1949
|
*/
|
|
2220
1950
|
declare function withIdempotentCommit<Evt extends AnyDomainEvent, R, TCtx>(deps: WithIdempotentCommitDeps<Evt, TCtx>, request: IdempotentCommitRequest, fn: (ctx: TCtx, enrollment: CommitEnrollment<Evt>, execution: IdempotentExecution) => Promise<WithCommitWorkResult<Evt, R>>): Promise<IdempotentCommitResult<R>>;
|
|
2221
1951
|
//#endregion
|
|
2222
|
-
//#region src/
|
|
2223
|
-
/**
|
|
2224
|
-
* One deadline due for delivery, as returned by
|
|
2225
|
-
* {@link DeadlineStore.due}. `deliveryId` identifies this scheduled
|
|
2226
|
-
* INCARNATION of the deadline, not the `(scope, key)` address: a
|
|
2227
|
-
* reschedule replaces the incarnation, and acknowledging a stale
|
|
2228
|
-
* incarnation must never consume the new one (see
|
|
2229
|
-
* {@link DeadlineStore.markDelivered}).
|
|
2230
|
-
*/
|
|
2231
|
-
interface DueDeadline<TPayload = unknown> {
|
|
2232
|
-
/** Opaque per-incarnation id used for `markDelivered`/`markFailed`. */
|
|
2233
|
-
readonly deliveryId: string;
|
|
2234
|
-
/** The namespace half of the address, e.g. a process or policy name. */
|
|
2235
|
-
readonly scope: string;
|
|
2236
|
-
/** The instance half of the address, e.g. a saga or reservation id. */
|
|
2237
|
-
readonly key: string;
|
|
2238
|
-
/** When the deadline was due. */
|
|
2239
|
-
readonly dueAt: Date;
|
|
2240
|
-
/** The payload handed back as the input; plain data only. */
|
|
2241
|
-
readonly payload: TPayload;
|
|
2242
|
-
/** Failed delivery attempts so far (see `markFailed`). */
|
|
2243
|
-
readonly attempts: number;
|
|
2244
|
-
}
|
|
2245
|
-
/** A deadline that exhausted its delivery attempts; see {@link DeadlineStore.deadLetters}. */
|
|
2246
|
-
interface DeadLetterDeadline<TPayload = unknown> extends DueDeadline<TPayload> {
|
|
2247
|
-
/** Human-readable rendering of the last delivery error, if recorded. */
|
|
2248
|
-
readonly lastError?: string;
|
|
2249
|
-
}
|
|
2250
|
-
/**
|
|
2251
|
-
* Driven port for durable deadlines: timeout-as-input. A process that
|
|
2252
|
-
* waits ("if PaymentReceived has not arrived in 30 minutes,
|
|
2253
|
-
* compensate"; "release the reservation hold after 15 minutes";
|
|
2254
|
-
* "expire the offer at month's end") schedules a deadline, and a poll
|
|
2255
|
-
* loop later DELIVERS it as an input to whatever owns the decision, a
|
|
2256
|
-
* saga aggregate, a use case, a policy. The store never executes
|
|
2257
|
-
* consumer code; firing a deadline means handing back a record.
|
|
2258
|
-
*
|
|
2259
|
-
* Deliberately general-purpose and deliberately small. This is not a
|
|
2260
|
-
* scheduler framework and not a cron abstraction: there is no
|
|
2261
|
-
* recurrence, no execution engine, and the poll loop belongs to the
|
|
2262
|
-
* consumer (the outbox guide's `drainOnce` pattern fits; the deadlines
|
|
2263
|
-
* guide shows the wiring).
|
|
2264
|
-
*
|
|
2265
|
-
* Addressing is the `(scope, key)` pair, so one table serves every
|
|
2266
|
-
* waiting process in an application: `scope` names the policy
|
|
2267
|
-
* ("checkout-saga", "reservation-hold"), `key` the instance. There is
|
|
2268
|
-
* at most ONE pending deadline per address; `schedule` on an existing
|
|
2269
|
-
* address replaces it (that IS the reschedule operation), and each
|
|
2270
|
-
* scheduling gets a fresh `deliveryId`, so acknowledgements of a
|
|
2271
|
-
* replaced incarnation cannot consume its successor.
|
|
2272
|
-
*
|
|
2273
|
-
* Two sides, two transactional postures, the same split as the outbox:
|
|
2274
|
-
*
|
|
2275
|
-
* - **`schedule` and `cancel` are write-side calls** and must join the
|
|
2276
|
-
* ambient write transaction (use a tx-bound store instance inside
|
|
2277
|
-
* `withCommit`'s callback, exactly like an outbox adapter). This is
|
|
2278
|
-
* a correctness rule, not a preference: state that says "waiting for
|
|
2279
|
-
* payment" committed without its deadline is a process that never
|
|
2280
|
-
* wakes up, and a deadline scheduled for a rolled-back state change
|
|
2281
|
-
* is a ghost input.
|
|
2282
|
-
* - **`due`, `markDelivered`, `markFailed`, and `deadLetters` are the
|
|
2283
|
-
* poll surface** and run out of band, in the consumer's loop.
|
|
2284
|
-
*
|
|
2285
|
-
* Delivery is at-least-once: a crash between processing and
|
|
2286
|
-
* `markDelivered` redelivers, so consumers make deadline handling
|
|
2287
|
-
* idempotent (the idempotency store with the `deliveryId` as key is
|
|
2288
|
-
* the ready-made answer). Deadlines have no cross-key ordering
|
|
2289
|
-
* obligations, so unlike the outbox a poison deadline blocks only
|
|
2290
|
-
* itself; bounded retries still matter, which is why failure tracking
|
|
2291
|
-
* is part of the port rather than an extension: report failed
|
|
2292
|
-
* deliveries via `markFailed`, and the store dead-letters a deadline
|
|
2293
|
-
* past its attempt ceiling.
|
|
2294
|
-
*
|
|
2295
|
-
* Run one logical poller per store unless your adapter's `due` claims
|
|
2296
|
-
* records for competing pollers; the same rule as the outbox
|
|
2297
|
-
* dispatcher.
|
|
2298
|
-
*
|
|
2299
|
-
* The bundled processor supplies an `ExecutionContext` to every poll-side
|
|
2300
|
-
* operation. Production adapters MUST pass its signal to native I/O or enforce
|
|
2301
|
-
* a native timeout no later than `deadlineAt`; the shell can bound its wait but
|
|
2302
|
-
* cannot terminate a promise that ignores cancellation. A timed-out write has
|
|
2303
|
-
* an unknown outcome. Acknowledgements must remain idempotent when they complete
|
|
2304
|
-
* late; a late failure update may count its original delivery attempt and must
|
|
2305
|
-
* still no-op after the incarnation was delivered or replaced.
|
|
2306
|
-
*
|
|
2307
|
-
* Verify an adapter with `createDeadlineStoreContractTests` from
|
|
2308
|
-
* `@shirudo/ddd-kit/testing`; `InMemoryDeadlineStore` is the
|
|
2309
|
-
* reference.
|
|
2310
|
-
*
|
|
2311
|
-
* @template TPayload - The payload shape carried from `schedule` to
|
|
2312
|
-
* delivery; plain, serializable data (the same discipline as event
|
|
2313
|
-
* payloads and snapshots)
|
|
2314
|
-
*/
|
|
2315
|
-
interface DeadlineStore<TPayload = unknown> {
|
|
2316
|
-
/**
|
|
2317
|
-
* Schedules (or reschedules) the deadline at `(scope, key)`: at most
|
|
2318
|
-
* one pending deadline exists per address, and scheduling an
|
|
2319
|
-
* occupied address replaces its due time, payload, attempt count,
|
|
2320
|
-
* and incarnation. Called inside the write transaction.
|
|
2321
|
-
*/
|
|
2322
|
-
schedule(deadline: {
|
|
2323
|
-
scope: string;
|
|
2324
|
-
key: string;
|
|
2325
|
-
dueAt: Date;
|
|
2326
|
-
payload: TPayload;
|
|
2327
|
-
}): Promise<void>;
|
|
2328
|
-
/**
|
|
2329
|
-
* Removes the pending deadline at `(scope, key)`; a no-op when none
|
|
2330
|
-
* exists (the awaited input arrived in time and the wait is over).
|
|
2331
|
-
* Called inside the write transaction.
|
|
2332
|
-
*/
|
|
2333
|
-
cancel(scope: string, key: string): Promise<void>;
|
|
2334
|
-
/**
|
|
2335
|
-
* Up to `limit` deadlines with `dueAt <= now` that are neither
|
|
2336
|
-
* delivered nor dead-lettered, ordered by `dueAt` (earliest first;
|
|
2337
|
-
* ties in scheduling order). A `limit` of `0` is legal and yields an
|
|
2338
|
-
* empty page (poll loops computing a remaining capacity may pass
|
|
2339
|
-
* it). `now` is a parameter on purpose: the poll loop owns the
|
|
2340
|
-
* clock, which keeps adapters deterministic and tests free of real
|
|
2341
|
-
* time. The bundled processor always supplies `context`; it is optional only
|
|
2342
|
-
* so existing adapters remain assignable.
|
|
2343
|
-
*/
|
|
2344
|
-
due(now: Date, limit: number, context?: ExecutionContext): Promise<ReadonlyArray<DueDeadline<TPayload>>>;
|
|
2345
|
-
/**
|
|
2346
|
-
* Acknowledges delivered incarnations so they stop coming back.
|
|
2347
|
-
* Idempotent on already-acknowledged and unknown ids, and a no-op
|
|
2348
|
-
* for ids of REPLACED incarnations (a late ack after a reschedule
|
|
2349
|
-
* must not consume the successor). Also clears a dead-lettered
|
|
2350
|
-
* incarnation (manual redelivery, then ack). It remains idempotent if the
|
|
2351
|
-
* operation completes after the caller timed out. The bundled processor
|
|
2352
|
-
* always supplies `context`.
|
|
2353
|
-
*/
|
|
2354
|
-
markDelivered(deliveryIds: ReadonlyArray<string>, context?: ExecutionContext): Promise<void>;
|
|
2355
|
-
/**
|
|
2356
|
-
* Records one failed delivery attempt for the incarnation:
|
|
2357
|
-
* increments its `attempts` and, once the store's ceiling is
|
|
2358
|
-
* reached, moves it to the dead-letter set that `due` no longer
|
|
2359
|
-
* returns. A no-op for unknown, delivered, or replaced ids.
|
|
2360
|
-
* Returns the exact dead-letter record only on the call that performs
|
|
2361
|
-
* that transition; retries below the ceiling and no-ops return
|
|
2362
|
-
* `undefined`. A late completion may count that original delivery attempt; it
|
|
2363
|
-
* must still no-op if the incarnation was delivered or replaced in the
|
|
2364
|
-
* meantime. The bundled processor never reissues the same store call and
|
|
2365
|
-
* always supplies `context`.
|
|
2366
|
-
*/
|
|
2367
|
-
markFailed(deliveryId: string, error?: unknown, context?: ExecutionContext): Promise<DeadLetterDeadline<TPayload> | undefined>;
|
|
2368
|
-
/**
|
|
2369
|
-
* Deadlines that exhausted their delivery attempts. Wire this to durable
|
|
2370
|
-
* alerting and reconciliation: a growing set means processes that stopped
|
|
2371
|
-
* waking up, and the poller can stop between the store transition and its
|
|
2372
|
-
* immediate observer callback.
|
|
2373
|
-
*/
|
|
2374
|
-
deadLetters(): Promise<ReadonlyArray<DeadLetterDeadline<TPayload>>>;
|
|
2375
|
-
}
|
|
2376
|
-
//#endregion
|
|
2377
|
-
//#region src/projections/ports.d.ts
|
|
1952
|
+
//#region src/application/projections/ports.d.ts
|
|
2378
1953
|
/**
|
|
2379
1954
|
* A projection's gap-proof cursor into one aggregate's commit chain.
|
|
2380
1955
|
* `aggregateVersion` plus `commitSequence` orders events; `commitSize`
|
|
@@ -2529,7 +2104,7 @@ interface Projection<Evt extends AnyDomainEvent, TCtx = unknown> {
|
|
|
2529
2104
|
truncate?(ctx: TCtx): Promise<void>;
|
|
2530
2105
|
}
|
|
2531
2106
|
//#endregion
|
|
2532
|
-
//#region src/
|
|
2107
|
+
//#region src/persistence/event-store/event-store.d.ts
|
|
2533
2108
|
/** Options for {@link EventStore.append}. */
|
|
2534
2109
|
interface EventStoreAppendOptions {
|
|
2535
2110
|
/**
|
|
@@ -2554,8 +2129,9 @@ interface ReadStreamOptions {
|
|
|
2554
2129
|
* Return only events AFTER this stream position (1-based event count),
|
|
2555
2130
|
* the snapshot catch-up read: `readStream(stream, { fromVersion:
|
|
2556
2131
|
* snapshot.version, limit: 256 })` yields the next page passed to
|
|
2557
|
-
* `aggregate.
|
|
2558
|
-
*
|
|
2132
|
+
* `aggregate.replayHistory`; the caller checks that the aggregate
|
|
2133
|
+
* ends at the pinned head ({@link ReplayHeadMismatchError}). Defaults
|
|
2134
|
+
* to `0` (the first stream page).
|
|
2559
2135
|
* Must be a non-negative safe integer when present.
|
|
2560
2136
|
*/
|
|
2561
2137
|
readonly fromVersion?: number;
|
|
@@ -2617,29 +2193,40 @@ type StreamReadResult<Evt extends AnyDomainEvent> = {
|
|
|
2617
2193
|
* const cached = this.tracking.identityMap.get(Order, id);
|
|
2618
2194
|
* if (cached) return cached;
|
|
2619
2195
|
* const address = this.stream(id);
|
|
2620
|
-
* const
|
|
2621
|
-
*
|
|
2622
|
-
*
|
|
2623
|
-
*
|
|
2196
|
+
* const first = await this.eventStore.readStream(address, { limit: 256 });
|
|
2197
|
+
* if (!first.exists) return undefined;
|
|
2198
|
+
* const targetVersion = first.lastVersion; // pin the first observed head
|
|
2199
|
+
* const reconstituted = reconstituteAggregateFromHistory(
|
|
2200
|
+
* () => Order.reconstitute(id), // bare instance, no events
|
|
2201
|
+
* first.events,
|
|
2202
|
+
* );
|
|
2203
|
+
* if (reconstituted.isErr()) throw reconstituted.error; // corrupt stream
|
|
2204
|
+
* const order = reconstituted.value;
|
|
2205
|
+
* let fromVersion = first.events.length;
|
|
2206
|
+
* while (fromVersion < targetVersion) {
|
|
2624
2207
|
* const page = await this.eventStore.readStream(address, {
|
|
2625
2208
|
* fromVersion,
|
|
2626
2209
|
* toVersion: targetVersion,
|
|
2627
2210
|
* limit: 256,
|
|
2628
2211
|
* });
|
|
2629
|
-
* if (!page.exists)
|
|
2630
|
-
* targetVersion ??= page.lastVersion; // pin the first observed head
|
|
2631
|
-
* if (fromVersion === targetVersion) break;
|
|
2632
|
-
* if (page.events.length === 0) {
|
|
2212
|
+
* if (!page.exists || page.events.length === 0) {
|
|
2633
2213
|
* throw new NonProgressingEventStreamPageError({
|
|
2634
2214
|
* ...address,
|
|
2635
2215
|
* fromVersion,
|
|
2636
2216
|
* targetVersion,
|
|
2637
2217
|
* });
|
|
2638
2218
|
* }
|
|
2639
|
-
* const
|
|
2640
|
-
* if (
|
|
2219
|
+
* const catchUp = order.replayHistory(page.events);
|
|
2220
|
+
* if (catchUp.isErr()) throw catchUp.error; // corrupt stream
|
|
2641
2221
|
* fromVersion += page.events.length;
|
|
2642
2222
|
* }
|
|
2223
|
+
* if (order.version !== targetVersion) {
|
|
2224
|
+
* throw new ReplayHeadMismatchError({
|
|
2225
|
+
* ...address,
|
|
2226
|
+
* targetVersion,
|
|
2227
|
+
* actualVersion: order.version,
|
|
2228
|
+
* });
|
|
2229
|
+
* }
|
|
2643
2230
|
* return this.tracking.trackLoaded(order);
|
|
2644
2231
|
* }
|
|
2645
2232
|
*
|
|
@@ -2696,7 +2283,7 @@ interface EventStore<Evt extends AnyDomainEvent> {
|
|
|
2696
2283
|
* append order and slicing; because the port cannot inject malformed
|
|
2697
2284
|
* physical rows, adapters add a store-specific corruption fixture that
|
|
2698
2285
|
* proves the duplicate/gap rejection. The repository then calls
|
|
2699
|
-
* `
|
|
2286
|
+
* `replayHistory`, whose replay guard rejects any event carrying an
|
|
2700
2287
|
* aggregate type or id that contradicts this stream key.
|
|
2701
2288
|
*
|
|
2702
2289
|
* An empty `events` array is a no-op; implementations resolve without
|
|
@@ -2742,12 +2329,12 @@ interface EventStore<Evt extends AnyDomainEvent> {
|
|
|
2742
2329
|
readStream(stream: AggregateAddress, options: ReadStreamOptions): Promise<StreamReadResult<Evt>>;
|
|
2743
2330
|
}
|
|
2744
2331
|
//#endregion
|
|
2745
|
-
//#region src/
|
|
2332
|
+
//#region src/persistence/snapshot-store/snapshot-store.d.ts
|
|
2746
2333
|
/**
|
|
2747
2334
|
* Driven port for aggregate snapshot persistence: the storage half of
|
|
2748
2335
|
* the snapshot-plus-recent-events load path for event-sourced aggregates.
|
|
2749
2336
|
* `SnapshotModel` owns projection, migration, and reconstitution;
|
|
2750
|
-
* `EventStore.readStream` supplies the catch-up tail to `
|
|
2337
|
+
* `EventStore.readStream` supplies the catch-up tail to `replayHistory`.
|
|
2751
2338
|
*
|
|
2752
2339
|
* **A snapshot is derived data, never authority.** The stream remains
|
|
2753
2340
|
* the source of truth; a snapshot only shortens replay. That shapes
|
|
@@ -2804,5 +2391,5 @@ interface SnapshotStore<TState = unknown> {
|
|
|
2804
2391
|
delete(address: AggregateAddress): Promise<void>;
|
|
2805
2392
|
}
|
|
2806
2393
|
//#endregion
|
|
2807
|
-
export {
|
|
2394
|
+
export { routeEventsToCommandOutbox as $, WithCommitWorkResult as A, defaultDomainEventFactory as At, ReplayableAggregate as B, JsonValue as Bt, DeadLetterDeadline as C, UncommittedDomainEvent as Ct, CommitEnrollment as D, createDomainEventFactory as Dt, AggregateCommitToken as E, createDomainEvent as Et, EventHandler as F, Command as Ft, IdGenerator as G, sameVersion as H, OnceOptions as I, CommandHandler as It, CommandMessageRelationships as J, CommandCommitOriginCandidate as K, PublishOptions as L, PublishedCommand as Lt, TransactionScope as M, recordDomainEvent as Mt, TransactionalOptions as N, ClockFactory as Nt, CommitEnrollmentOptions as O, createDomainEventFromFacts as Ot, EventBus as P, AggregateAddress as Pt, DurableCommandMessage as Q, Aggregate as R, JsonObject as Rt, withIdempotentCommit as S, PendingDomainEvent as St, DueDeadline as T, copyMetadata as Tt, toVersion as U, Version as V, Id as W, CommandOutboxMapper as X, CommandOutboxCommitCandidate as Y, CommandOutboxWriter as Z, IdempotencyStore as _, DomainEventFactory as _t, StreamReadResult as a, ExecutionContext as at, IdempotentExecution as b, EventIdFactory as bt, ProjectionCheckpointStore as c, EventCommitCandidate as ct, IdempotencyClaim as d, AnyUncommittedDomainEvent as dt, DeadLetterRecord as et, IdempotencyClaimHandle as f, CreateDomainEventFromFactsOptions as ft, IdempotencyReconciliationDecision as g, DomainEvent as gt, IdempotencyReconciliation as h, CreateUncommittedDomainEventOptions as ht, ReadStreamOptions as i, OutboxWriter as it, withCommit as j, mergeMetadata as jt, WithCommitDeps as k, createUncommittedDomainEvent as kt, ProjectionPosition as l, EventCommitCandidatePosition as lt, IdempotencyOperationErrorContext as m, CreateDomainEventStampOptions as mt, EventStore as n, Outbox as nt, Projection as o, CommitPosition as ot, IdempotencyLease as p, CreateDomainEventOptions as pt, CommandMessageContent as q, EventStoreAppendOptions as r, OutboxRecord as rt, ProjectionCheckpoint as s, CommittedDomainEvent as st, SnapshotStore as t, DispatchTrackingOutbox as tt, isPositionAfter as u, AnyDomainEvent as ut, IdempotentCommitRequest as v, DomainEventFactoryOptions as vt, DeadlineStore as w, UncommittedDomainEventOf as wt, WithIdempotentCommitDeps as x, EventMetadata as xt, IdempotentCommitResult as y, DomainEventStamp as yt, AggregateSnapshot as z, JsonPrimitive as zt };
|
|
2808
2395
|
//# sourceMappingURL=snapshot-store.d.ts.map
|