@apso/domain-events 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -0
- package/dist/destinations/delivery-destination.d.ts +25 -0
- package/dist/destinations/delivery-destination.d.ts.map +1 -0
- package/dist/destinations/delivery-destination.js +10 -0
- package/dist/destinations/delivery-destination.js.map +1 -0
- package/dist/destinations/eventbridge.destination.d.ts +23 -0
- package/dist/destinations/eventbridge.destination.d.ts.map +1 -0
- package/dist/destinations/eventbridge.destination.js +55 -0
- package/dist/destinations/eventbridge.destination.js.map +1 -0
- package/dist/destinations/index.d.ts +22 -0
- package/dist/destinations/index.d.ts.map +1 -0
- package/dist/destinations/index.js +60 -0
- package/dist/destinations/index.js.map +1 -0
- package/dist/destinations/kafka.destination.d.ts +24 -0
- package/dist/destinations/kafka.destination.d.ts.map +1 -0
- package/dist/destinations/kafka.destination.js +66 -0
- package/dist/destinations/kafka.destination.js.map +1 -0
- package/dist/destinations/sqs.destination.d.ts +20 -0
- package/dist/destinations/sqs.destination.d.ts.map +1 -0
- package/dist/destinations/sqs.destination.js +46 -0
- package/dist/destinations/sqs.destination.js.map +1 -0
- package/dist/destinations/webhook.destination.d.ts +34 -0
- package/dist/destinations/webhook.destination.d.ts.map +1 -0
- package/dist/destinations/webhook.destination.js +73 -0
- package/dist/destinations/webhook.destination.js.map +1 -0
- package/dist/domain-event.entity.d.ts +28 -0
- package/dist/domain-event.entity.d.ts.map +1 -0
- package/dist/domain-event.entity.js +62 -0
- package/dist/domain-event.entity.js.map +1 -0
- package/dist/domain-event.mapper.d.ts +41 -0
- package/dist/domain-event.mapper.d.ts.map +1 -0
- package/dist/domain-event.mapper.js +42 -0
- package/dist/domain-event.mapper.js.map +1 -0
- package/dist/domain-event.relay.d.ts +80 -0
- package/dist/domain-event.relay.d.ts.map +1 -0
- package/dist/domain-event.relay.js +167 -0
- package/dist/domain-event.relay.js.map +1 -0
- package/dist/domain-event.subscriber.d.ts +33 -0
- package/dist/domain-event.subscriber.d.ts.map +1 -0
- package/dist/domain-event.subscriber.js +98 -0
- package/dist/domain-event.subscriber.js.map +1 -0
- package/dist/domain-events.module.d.ts +43 -0
- package/dist/domain-events.module.d.ts.map +1 -0
- package/dist/domain-events.module.js +74 -0
- package/dist/domain-events.module.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.DomainEvent = void 0;
|
|
13
|
+
const typeorm_1 = require("typeorm");
|
|
14
|
+
/**
|
|
15
|
+
* DomainEvent is a durable, append-only log of domain state changes.
|
|
16
|
+
*
|
|
17
|
+
* Rows are written in the SAME database transaction as the state change that
|
|
18
|
+
* produced them (see {@link DomainEventSubscriber}), which is what makes
|
|
19
|
+
* delivery durable. This implements the standard "transactional outbox"
|
|
20
|
+
* pattern, but is surfaced with generic domain-event naming — no public
|
|
21
|
+
* artifact is named "outbox".
|
|
22
|
+
*
|
|
23
|
+
* A separate {@link DomainEventRelay} polls pending rows and delivers them.
|
|
24
|
+
* Because the stable `id` is preserved across delivery attempts, consumers can
|
|
25
|
+
* dedupe on it for at-least-once delivery semantics.
|
|
26
|
+
*/
|
|
27
|
+
let DomainEvent = class DomainEvent {
|
|
28
|
+
};
|
|
29
|
+
exports.DomainEvent = DomainEvent;
|
|
30
|
+
__decorate([
|
|
31
|
+
(0, typeorm_1.PrimaryGeneratedColumn)('uuid'),
|
|
32
|
+
__metadata("design:type", String)
|
|
33
|
+
], DomainEvent.prototype, "id", void 0);
|
|
34
|
+
__decorate([
|
|
35
|
+
(0, typeorm_1.Column)({ type: 'varchar' }),
|
|
36
|
+
__metadata("design:type", String)
|
|
37
|
+
], DomainEvent.prototype, "type", void 0);
|
|
38
|
+
__decorate([
|
|
39
|
+
(0, typeorm_1.Column)({ type: 'jsonb' }),
|
|
40
|
+
__metadata("design:type", Object)
|
|
41
|
+
], DomainEvent.prototype, "payload", void 0);
|
|
42
|
+
__decorate([
|
|
43
|
+
(0, typeorm_1.Column)({ type: 'varchar', default: 'pending' }),
|
|
44
|
+
__metadata("design:type", String)
|
|
45
|
+
], DomainEvent.prototype, "status", void 0);
|
|
46
|
+
__decorate([
|
|
47
|
+
(0, typeorm_1.Column)({ type: 'int', default: 0 }),
|
|
48
|
+
__metadata("design:type", Number)
|
|
49
|
+
], DomainEvent.prototype, "attempts", void 0);
|
|
50
|
+
__decorate([
|
|
51
|
+
(0, typeorm_1.CreateDateColumn)({ type: 'timestamptz' }),
|
|
52
|
+
__metadata("design:type", Date)
|
|
53
|
+
], DomainEvent.prototype, "created_at", void 0);
|
|
54
|
+
__decorate([
|
|
55
|
+
(0, typeorm_1.Column)({ type: 'timestamptz', nullable: true }),
|
|
56
|
+
__metadata("design:type", Object)
|
|
57
|
+
], DomainEvent.prototype, "publishedAt", void 0);
|
|
58
|
+
exports.DomainEvent = DomainEvent = __decorate([
|
|
59
|
+
(0, typeorm_1.Index)(['status', 'created_at']),
|
|
60
|
+
(0, typeorm_1.Entity)('events')
|
|
61
|
+
], DomainEvent);
|
|
62
|
+
//# sourceMappingURL=domain-event.entity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-event.entity.js","sourceRoot":"","sources":["../src/domain-event.entity.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,qCAMiB;AAEjB;;;;;;;;;;;;GAYG;AAGI,IAAM,WAAW,GAAjB,MAAM,WAAW;CA0BvB,CAAA;AA1BY,kCAAW;AAEtB;IADC,IAAA,gCAAsB,EAAC,MAAM,CAAC;;uCACnB;AAIZ;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;yCACd;AAId;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;;4CACQ;AAIlC;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;;2CACJ;AAI5C;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;;6CAClB;AAGlB;IADC,IAAA,0BAAgB,EAAC,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;8BAC7B,IAAI;+CAAC;AAIlB;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;gDACtB;sBAzBf,WAAW;IAFvB,IAAA,eAAK,EAAC,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;IAC/B,IAAA,gBAAM,EAAC,QAAQ,CAAC;GACJ,WAAW,CA0BvB"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action that produced a domain event.
|
|
3
|
+
*/
|
|
4
|
+
export type DomainEventAction = 'created' | 'updated' | 'removed';
|
|
5
|
+
/**
|
|
6
|
+
* DI token for the {@link DomainEventMapper}.
|
|
7
|
+
*
|
|
8
|
+
* {@link DomainEventsModule} binds this token to {@link DefaultDomainEventMapper}.
|
|
9
|
+
* Your application can override it by providing its own implementation:
|
|
10
|
+
*
|
|
11
|
+
* { provide: DOMAIN_EVENT_MAPPER, useClass: MyDomainEventMapper }
|
|
12
|
+
*/
|
|
13
|
+
export declare const DOMAIN_EVENT_MAPPER = "DOMAIN_EVENT_MAPPER";
|
|
14
|
+
/**
|
|
15
|
+
* DomainEventMapper is the extension point that keeps application semantics
|
|
16
|
+
* (event-type taxonomy, payload shape) OUT of the library. Provide your own
|
|
17
|
+
* implementation under {@link DOMAIN_EVENT_MAPPER} to customize.
|
|
18
|
+
*/
|
|
19
|
+
export interface DomainEventMapper {
|
|
20
|
+
/**
|
|
21
|
+
* Maps an entity name + action to an event type string, e.g.
|
|
22
|
+
* ("Product", "created") -> "product.created".
|
|
23
|
+
*/
|
|
24
|
+
eventType(entityName: string, action: DomainEventAction): string;
|
|
25
|
+
/**
|
|
26
|
+
* Builds the serializable payload for an event from the changed entity.
|
|
27
|
+
*/
|
|
28
|
+
toPayload(entity: unknown, action: DomainEventAction): Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Default mapper. Uses an `entity.action` type taxonomy (entity name
|
|
32
|
+
* lower-camel-cased) and returns the entity as-is (shallow) as the payload.
|
|
33
|
+
*
|
|
34
|
+
* Override by providing your own {@link DomainEventMapper} under
|
|
35
|
+
* {@link DOMAIN_EVENT_MAPPER}.
|
|
36
|
+
*/
|
|
37
|
+
export declare class DefaultDomainEventMapper implements DomainEventMapper {
|
|
38
|
+
eventType(entityName: string, action: DomainEventAction): string;
|
|
39
|
+
toPayload(entity: unknown, _action: DomainEventAction): Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=domain-event.mapper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-event.mapper.d.ts","sourceRoot":"","sources":["../src/domain-event.mapper.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAElE;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,wBAAwB,CAAC;AAEzD;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAAC;IAEjE;;OAEG;IACH,SAAS,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChF;AAED;;;;;;GAMG;AACH,qBACa,wBAAyB,YAAW,iBAAiB;IAChE,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,MAAM;IAOhE,SAAS,CACP,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,iBAAiB,GACzB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAG3B"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.DefaultDomainEventMapper = exports.DOMAIN_EVENT_MAPPER = void 0;
|
|
10
|
+
const common_1 = require("@nestjs/common");
|
|
11
|
+
/**
|
|
12
|
+
* DI token for the {@link DomainEventMapper}.
|
|
13
|
+
*
|
|
14
|
+
* {@link DomainEventsModule} binds this token to {@link DefaultDomainEventMapper}.
|
|
15
|
+
* Your application can override it by providing its own implementation:
|
|
16
|
+
*
|
|
17
|
+
* { provide: DOMAIN_EVENT_MAPPER, useClass: MyDomainEventMapper }
|
|
18
|
+
*/
|
|
19
|
+
exports.DOMAIN_EVENT_MAPPER = 'DOMAIN_EVENT_MAPPER';
|
|
20
|
+
/**
|
|
21
|
+
* Default mapper. Uses an `entity.action` type taxonomy (entity name
|
|
22
|
+
* lower-camel-cased) and returns the entity as-is (shallow) as the payload.
|
|
23
|
+
*
|
|
24
|
+
* Override by providing your own {@link DomainEventMapper} under
|
|
25
|
+
* {@link DOMAIN_EVENT_MAPPER}.
|
|
26
|
+
*/
|
|
27
|
+
let DefaultDomainEventMapper = class DefaultDomainEventMapper {
|
|
28
|
+
eventType(entityName, action) {
|
|
29
|
+
const normalized = entityName
|
|
30
|
+
? entityName.charAt(0).toLowerCase() + entityName.slice(1)
|
|
31
|
+
: entityName;
|
|
32
|
+
return `${normalized}.${action}`;
|
|
33
|
+
}
|
|
34
|
+
toPayload(entity, _action) {
|
|
35
|
+
return { ...entity };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
exports.DefaultDomainEventMapper = DefaultDomainEventMapper;
|
|
39
|
+
exports.DefaultDomainEventMapper = DefaultDomainEventMapper = __decorate([
|
|
40
|
+
(0, common_1.Injectable)()
|
|
41
|
+
], DefaultDomainEventMapper);
|
|
42
|
+
//# sourceMappingURL=domain-event.mapper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-event.mapper.js","sourceRoot":"","sources":["../src/domain-event.mapper.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAA4C;AAO5C;;;;;;;GAOG;AACU,QAAA,mBAAmB,GAAG,qBAAqB,CAAC;AAoBzD;;;;;;GAMG;AAEI,IAAM,wBAAwB,GAA9B,MAAM,wBAAwB;IACnC,SAAS,CAAC,UAAkB,EAAE,MAAyB;QACrD,MAAM,UAAU,GAAG,UAAU;YAC3B,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;YAC1D,CAAC,CAAC,UAAU,CAAC;QACf,OAAO,GAAG,UAAU,IAAI,MAAM,EAAE,CAAC;IACnC,CAAC;IAED,SAAS,CACP,MAAe,EACf,OAA0B;QAE1B,OAAO,EAAE,GAAI,MAAkC,EAAE,CAAC;IACpD,CAAC;CACF,CAAA;AAdY,4DAAwB;mCAAxB,wBAAwB;IADpC,IAAA,mBAAU,GAAE;GACA,wBAAwB,CAcpC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { OnApplicationBootstrap, OnModuleDestroy } from '@nestjs/common';
|
|
2
|
+
import { Repository } from 'typeorm';
|
|
3
|
+
import { DomainEvent } from './domain-event.entity';
|
|
4
|
+
import { DeliveryDestination } from './destinations';
|
|
5
|
+
/**
|
|
6
|
+
* Maximum number of delivery attempts before an event is marked 'failed'.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MAX_ATTEMPTS = 5;
|
|
9
|
+
/**
|
|
10
|
+
* Default poll interval (ms) for the self-contained drain loop.
|
|
11
|
+
*/
|
|
12
|
+
export declare const DEFAULT_POLL_INTERVAL_MS = 5000;
|
|
13
|
+
/**
|
|
14
|
+
* DI token carrying the poll interval (ms) for the relay's self-contained
|
|
15
|
+
* poller. Provided by {@link DomainEventsModule.forRoot}.
|
|
16
|
+
*/
|
|
17
|
+
export declare const DOMAIN_EVENT_POLL_INTERVAL = "DOMAIN_EVENT_POLL_INTERVAL";
|
|
18
|
+
/**
|
|
19
|
+
* DomainEventRelay drains the durable `events` table and delivers pending
|
|
20
|
+
* events. It is the consumer side of the transactional-outbox pattern: the
|
|
21
|
+
* subscriber writes events transactionally, the relay publishes them
|
|
22
|
+
* asynchronously with at-least-once semantics.
|
|
23
|
+
*
|
|
24
|
+
* SELF-CONTAINED POLLER: on application bootstrap the relay starts its own
|
|
25
|
+
* `setInterval` drain loop and clears it on shutdown. It depends on NO external
|
|
26
|
+
* scheduler package (no `@nestjs/schedule`). The interval defaults to
|
|
27
|
+
* {@link DEFAULT_POLL_INTERVAL_MS} and is configurable via `forRoot`.
|
|
28
|
+
*
|
|
29
|
+
* DELIVERY: `publish()` fans each event out to every ACTIVE
|
|
30
|
+
* {@link DeliveryDestination} (chosen at runtime via the `EVENTS_DESTINATION`
|
|
31
|
+
* env var). Any thrown error bubbles so the relay retries.
|
|
32
|
+
*
|
|
33
|
+
* DUPLICATES — IMPORTANT: delivery is tracked at the event grain (a single
|
|
34
|
+
* `events.status`), NOT per (event × destination). With MORE THAN ONE active
|
|
35
|
+
* destination, a failure in any one destination retries the WHOLE event, so the
|
|
36
|
+
* healthy destinations receive the event AGAIN on every retry. Consumer-side
|
|
37
|
+
* dedupe on `event.id` is therefore MANDATORY, not optional, whenever you run
|
|
38
|
+
* multiple destinations. (A single destination — the common case — never
|
|
39
|
+
* duplicates beyond ordinary at-least-once.) You can override `publish()` to
|
|
40
|
+
* change this behavior.
|
|
41
|
+
*/
|
|
42
|
+
export declare class DomainEventRelay implements OnApplicationBootstrap, OnModuleDestroy {
|
|
43
|
+
private readonly events;
|
|
44
|
+
private readonly destinations;
|
|
45
|
+
private readonly pollIntervalMs;
|
|
46
|
+
private readonly logger;
|
|
47
|
+
private timer?;
|
|
48
|
+
private draining;
|
|
49
|
+
constructor(events: Repository<DomainEvent>, destinations?: DeliveryDestination[], pollIntervalMs?: number);
|
|
50
|
+
/**
|
|
51
|
+
* Starts the self-contained periodic drain. Called by Nest on app bootstrap.
|
|
52
|
+
*/
|
|
53
|
+
onApplicationBootstrap(): void;
|
|
54
|
+
/**
|
|
55
|
+
* Stops the periodic drain. Called by Nest on shutdown.
|
|
56
|
+
*/
|
|
57
|
+
onModuleDestroy(): void;
|
|
58
|
+
/**
|
|
59
|
+
* One non-overlapping drain pass. Errors are swallowed (logged) so a single
|
|
60
|
+
* bad tick never kills the interval.
|
|
61
|
+
*/
|
|
62
|
+
private tick;
|
|
63
|
+
/**
|
|
64
|
+
* Load pending events oldest-first and deliver each. On success the event is
|
|
65
|
+
* marked `published` with `publishedAt`; on failure `attempts` increments and
|
|
66
|
+
* the event is marked `failed` once `attempts >= MAX_ATTEMPTS`.
|
|
67
|
+
*/
|
|
68
|
+
processPending(limit?: number): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Deliver a single domain event.
|
|
71
|
+
*
|
|
72
|
+
* Fans the event out to every ACTIVE {@link DeliveryDestination}. Any rejection
|
|
73
|
+
* bubbles so the relay retries (and, with multiple destinations, re-sends to
|
|
74
|
+
* ALL of them — see the class doc on mandatory consumer-side dedupe). Override
|
|
75
|
+
* to change delivery behavior. When no destination is active (empty
|
|
76
|
+
* EVENTS_DESTINATION), this throws so the misconfiguration surfaces.
|
|
77
|
+
*/
|
|
78
|
+
publish(event: DomainEvent): Promise<void>;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=domain-event.relay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-event.relay.d.ts","sourceRoot":"","sources":["../src/domain-event.relay.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,sBAAsB,EACtB,eAAe,EAChB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EACL,mBAAmB,EAEpB,MAAM,gBAAgB,CAAC;AAExB;;GAEG;AACH,eAAO,MAAM,YAAY,IAAI,CAAC;AAE9B;;GAEG;AACH,eAAO,MAAM,wBAAwB,OAAO,CAAC;AAE7C;;;GAGG;AACH,eAAO,MAAM,0BAA0B,+BAA+B,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBACa,gBACX,YAAW,sBAAsB,EAAE,eAAe;IAQhD,OAAO,CAAC,QAAQ,CAAC,MAAM;IAGvB,OAAO,CAAC,QAAQ,CAAC,YAAY;IAG7B,OAAO,CAAC,QAAQ,CAAC,cAAc;IAZjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqC;IAC5D,OAAO,CAAC,KAAK,CAAC,CAAiC;IAC/C,OAAO,CAAC,QAAQ,CAAS;gBAIN,MAAM,EAAE,UAAU,CAAC,WAAW,CAAC,EAG/B,YAAY,GAAE,mBAAmB,EAAO,EAGxC,cAAc,GAAE,MAAiC;IAGpE;;OAEG;IACH,sBAAsB,IAAI,IAAI;IAW9B;;OAEG;IACH,eAAe,IAAI,IAAI;IAOvB;;;OAGG;YACW,IAAI;IAelB;;;;OAIG;IACG,cAAc,CAAC,KAAK,SAAK,GAAG,OAAO,CAAC,IAAI,CAAC;IA8B/C;;;;;;;;OAQG;IACG,OAAO,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;CAUjD"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
var DomainEventRelay_1;
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.DomainEventRelay = exports.DOMAIN_EVENT_POLL_INTERVAL = exports.DEFAULT_POLL_INTERVAL_MS = exports.MAX_ATTEMPTS = void 0;
|
|
17
|
+
const common_1 = require("@nestjs/common");
|
|
18
|
+
const typeorm_1 = require("@nestjs/typeorm");
|
|
19
|
+
const typeorm_2 = require("typeorm");
|
|
20
|
+
const domain_event_entity_1 = require("./domain-event.entity");
|
|
21
|
+
const destinations_1 = require("./destinations");
|
|
22
|
+
/**
|
|
23
|
+
* Maximum number of delivery attempts before an event is marked 'failed'.
|
|
24
|
+
*/
|
|
25
|
+
exports.MAX_ATTEMPTS = 5;
|
|
26
|
+
/**
|
|
27
|
+
* Default poll interval (ms) for the self-contained drain loop.
|
|
28
|
+
*/
|
|
29
|
+
exports.DEFAULT_POLL_INTERVAL_MS = 5000;
|
|
30
|
+
/**
|
|
31
|
+
* DI token carrying the poll interval (ms) for the relay's self-contained
|
|
32
|
+
* poller. Provided by {@link DomainEventsModule.forRoot}.
|
|
33
|
+
*/
|
|
34
|
+
exports.DOMAIN_EVENT_POLL_INTERVAL = 'DOMAIN_EVENT_POLL_INTERVAL';
|
|
35
|
+
/**
|
|
36
|
+
* DomainEventRelay drains the durable `events` table and delivers pending
|
|
37
|
+
* events. It is the consumer side of the transactional-outbox pattern: the
|
|
38
|
+
* subscriber writes events transactionally, the relay publishes them
|
|
39
|
+
* asynchronously with at-least-once semantics.
|
|
40
|
+
*
|
|
41
|
+
* SELF-CONTAINED POLLER: on application bootstrap the relay starts its own
|
|
42
|
+
* `setInterval` drain loop and clears it on shutdown. It depends on NO external
|
|
43
|
+
* scheduler package (no `@nestjs/schedule`). The interval defaults to
|
|
44
|
+
* {@link DEFAULT_POLL_INTERVAL_MS} and is configurable via `forRoot`.
|
|
45
|
+
*
|
|
46
|
+
* DELIVERY: `publish()` fans each event out to every ACTIVE
|
|
47
|
+
* {@link DeliveryDestination} (chosen at runtime via the `EVENTS_DESTINATION`
|
|
48
|
+
* env var). Any thrown error bubbles so the relay retries.
|
|
49
|
+
*
|
|
50
|
+
* DUPLICATES — IMPORTANT: delivery is tracked at the event grain (a single
|
|
51
|
+
* `events.status`), NOT per (event × destination). With MORE THAN ONE active
|
|
52
|
+
* destination, a failure in any one destination retries the WHOLE event, so the
|
|
53
|
+
* healthy destinations receive the event AGAIN on every retry. Consumer-side
|
|
54
|
+
* dedupe on `event.id` is therefore MANDATORY, not optional, whenever you run
|
|
55
|
+
* multiple destinations. (A single destination — the common case — never
|
|
56
|
+
* duplicates beyond ordinary at-least-once.) You can override `publish()` to
|
|
57
|
+
* change this behavior.
|
|
58
|
+
*/
|
|
59
|
+
let DomainEventRelay = DomainEventRelay_1 = class DomainEventRelay {
|
|
60
|
+
constructor(events, destinations = [], pollIntervalMs = exports.DEFAULT_POLL_INTERVAL_MS) {
|
|
61
|
+
this.events = events;
|
|
62
|
+
this.destinations = destinations;
|
|
63
|
+
this.pollIntervalMs = pollIntervalMs;
|
|
64
|
+
this.logger = new common_1.Logger(DomainEventRelay_1.name);
|
|
65
|
+
this.draining = false;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Starts the self-contained periodic drain. Called by Nest on app bootstrap.
|
|
69
|
+
*/
|
|
70
|
+
onApplicationBootstrap() {
|
|
71
|
+
if (this.timer)
|
|
72
|
+
return;
|
|
73
|
+
this.timer = setInterval(() => {
|
|
74
|
+
void this.tick();
|
|
75
|
+
}, this.pollIntervalMs);
|
|
76
|
+
// Don't keep the event loop alive solely for the poller.
|
|
77
|
+
if (typeof this.timer.unref === 'function') {
|
|
78
|
+
this.timer.unref();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Stops the periodic drain. Called by Nest on shutdown.
|
|
83
|
+
*/
|
|
84
|
+
onModuleDestroy() {
|
|
85
|
+
if (this.timer) {
|
|
86
|
+
clearInterval(this.timer);
|
|
87
|
+
this.timer = undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* One non-overlapping drain pass. Errors are swallowed (logged) so a single
|
|
92
|
+
* bad tick never kills the interval.
|
|
93
|
+
*/
|
|
94
|
+
async tick() {
|
|
95
|
+
if (this.draining)
|
|
96
|
+
return;
|
|
97
|
+
this.draining = true;
|
|
98
|
+
try {
|
|
99
|
+
await this.processPending();
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
this.logger.error('DomainEventRelay drain tick failed', error instanceof Error ? error.stack : undefined);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
this.draining = false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Load pending events oldest-first and deliver each. On success the event is
|
|
110
|
+
* marked `published` with `publishedAt`; on failure `attempts` increments and
|
|
111
|
+
* the event is marked `failed` once `attempts >= MAX_ATTEMPTS`.
|
|
112
|
+
*/
|
|
113
|
+
async processPending(limit = 50) {
|
|
114
|
+
const pending = await this.events.find({
|
|
115
|
+
where: { status: 'pending' },
|
|
116
|
+
order: { created_at: 'ASC' },
|
|
117
|
+
take: limit,
|
|
118
|
+
});
|
|
119
|
+
for (const event of pending) {
|
|
120
|
+
try {
|
|
121
|
+
// eslint-disable-next-line no-await-in-loop
|
|
122
|
+
await this.publish(event);
|
|
123
|
+
event.status = 'published';
|
|
124
|
+
event.publishedAt = new Date();
|
|
125
|
+
// eslint-disable-next-line no-await-in-loop
|
|
126
|
+
await this.events.save(event);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
event.attempts += 1;
|
|
130
|
+
if (event.attempts >= exports.MAX_ATTEMPTS) {
|
|
131
|
+
event.status = 'failed';
|
|
132
|
+
this.logger.error(`DomainEvent ${event.id} (${event.type}) failed after ${event.attempts} attempts`, error instanceof Error ? error.stack : undefined);
|
|
133
|
+
}
|
|
134
|
+
// eslint-disable-next-line no-await-in-loop
|
|
135
|
+
await this.events.save(event);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Deliver a single domain event.
|
|
141
|
+
*
|
|
142
|
+
* Fans the event out to every ACTIVE {@link DeliveryDestination}. Any rejection
|
|
143
|
+
* bubbles so the relay retries (and, with multiple destinations, re-sends to
|
|
144
|
+
* ALL of them — see the class doc on mandatory consumer-side dedupe). Override
|
|
145
|
+
* to change delivery behavior. When no destination is active (empty
|
|
146
|
+
* EVENTS_DESTINATION), this throws so the misconfiguration surfaces.
|
|
147
|
+
*/
|
|
148
|
+
async publish(event) {
|
|
149
|
+
if (this.destinations?.length) {
|
|
150
|
+
await Promise.all(this.destinations.map((d) => d.send(event)));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
throw new Error('DomainEventRelay.publish() has no active destinations — set ' +
|
|
154
|
+
'EVENTS_DESTINATION (comma-separated) or override publish()');
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
exports.DomainEventRelay = DomainEventRelay;
|
|
158
|
+
exports.DomainEventRelay = DomainEventRelay = DomainEventRelay_1 = __decorate([
|
|
159
|
+
(0, common_1.Injectable)(),
|
|
160
|
+
__param(0, (0, typeorm_1.InjectRepository)(domain_event_entity_1.DomainEvent)),
|
|
161
|
+
__param(1, (0, common_1.Optional)()),
|
|
162
|
+
__param(1, (0, common_1.Inject)(destinations_1.DOMAIN_EVENT_DESTINATIONS)),
|
|
163
|
+
__param(2, (0, common_1.Optional)()),
|
|
164
|
+
__param(2, (0, common_1.Inject)(exports.DOMAIN_EVENT_POLL_INTERVAL)),
|
|
165
|
+
__metadata("design:paramtypes", [typeorm_2.Repository, Array, Number])
|
|
166
|
+
], DomainEventRelay);
|
|
167
|
+
//# sourceMappingURL=domain-event.relay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-event.relay.js","sourceRoot":"","sources":["../src/domain-event.relay.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DataSource, EntitySubscriberInterface, InsertEvent, UpdateEvent, RemoveEvent } from 'typeorm';
|
|
2
|
+
import { DomainEventMapper } from './domain-event.mapper';
|
|
3
|
+
/**
|
|
4
|
+
* DI token carrying the set of entity classes that have opted in to
|
|
5
|
+
* domain-event emission. Provided by {@link DomainEventsModule.forRoot} from the
|
|
6
|
+
* `entities` it is given. The library NEVER hardcodes this set — it is the
|
|
7
|
+
* CLI-emitted manifest of opted-in entities.
|
|
8
|
+
*/
|
|
9
|
+
export declare const DOMAIN_EVENT_ENTITIES = "DOMAIN_EVENT_ENTITIES";
|
|
10
|
+
/**
|
|
11
|
+
* DomainEventSubscriber writes a {@link DomainEvent} row for every
|
|
12
|
+
* insert/update/remove of an opted-in entity.
|
|
13
|
+
*
|
|
14
|
+
* DURABILITY: each row is written via `event.manager` (the EntityManager of the
|
|
15
|
+
* in-flight transaction), so the event is committed atomically WITH the state
|
|
16
|
+
* change. This is the durability lever behind the transactional-outbox pattern.
|
|
17
|
+
*
|
|
18
|
+
* RECURSION GUARD: we never emit events for {@link DomainEvent} itself, otherwise
|
|
19
|
+
* each emitted row would itself trigger another emission. Only entities in the
|
|
20
|
+
* opted-in set (passed to {@link DomainEventsModule.forRoot}) emit; everything
|
|
21
|
+
* else is skipped.
|
|
22
|
+
*/
|
|
23
|
+
export declare class DomainEventSubscriber implements EntitySubscriberInterface {
|
|
24
|
+
private readonly mapper;
|
|
25
|
+
private readonly emittingEntities;
|
|
26
|
+
constructor(mapper: DomainEventMapper, emittingEntities: Function[], dataSource?: DataSource);
|
|
27
|
+
private isEmitting;
|
|
28
|
+
private emit;
|
|
29
|
+
afterInsert(event: InsertEvent<unknown>): Promise<void>;
|
|
30
|
+
afterUpdate(event: UpdateEvent<unknown>): Promise<void>;
|
|
31
|
+
afterRemove(event: RemoveEvent<unknown>): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=domain-event.subscriber.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-event.subscriber.d.ts","sourceRoot":"","sources":["../src/domain-event.subscriber.ts"],"names":[],"mappings":"AACA,OAAO,EACL,UAAU,EAEV,yBAAyB,EAEzB,WAAW,EACX,WAAW,EACX,WAAW,EACZ,MAAM,SAAS,CAAC;AAEjB,OAAO,EAGL,iBAAiB,EAClB,MAAM,uBAAuB,CAAC;AAE/B;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,0BAA0B,CAAC;AAE7D;;;;;;;;;;;;GAYG;AACH,qBAEa,qBAAsB,YAAW,yBAAyB;IAEtC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAErB,OAAO,CAAC,QAAQ,CAAC,gBAAgB;gBAFlB,MAAM,EAAE,iBAAiB,EAEvB,gBAAgB,EAAE,QAAQ,EAAE,EAKhE,UAAU,CAAC,EAAE,UAAU;IAMrC,OAAO,CAAC,UAAU;YAUJ,IAAI;IAoBZ,WAAW,CAAC,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAWvD,WAAW,CAAC,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAWvD,WAAW,CAAC,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;CAU9D"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.DomainEventSubscriber = exports.DOMAIN_EVENT_ENTITIES = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const typeorm_1 = require("typeorm");
|
|
18
|
+
const domain_event_entity_1 = require("./domain-event.entity");
|
|
19
|
+
const domain_event_mapper_1 = require("./domain-event.mapper");
|
|
20
|
+
/**
|
|
21
|
+
* DI token carrying the set of entity classes that have opted in to
|
|
22
|
+
* domain-event emission. Provided by {@link DomainEventsModule.forRoot} from the
|
|
23
|
+
* `entities` it is given. The library NEVER hardcodes this set — it is the
|
|
24
|
+
* CLI-emitted manifest of opted-in entities.
|
|
25
|
+
*/
|
|
26
|
+
exports.DOMAIN_EVENT_ENTITIES = 'DOMAIN_EVENT_ENTITIES';
|
|
27
|
+
/**
|
|
28
|
+
* DomainEventSubscriber writes a {@link DomainEvent} row for every
|
|
29
|
+
* insert/update/remove of an opted-in entity.
|
|
30
|
+
*
|
|
31
|
+
* DURABILITY: each row is written via `event.manager` (the EntityManager of the
|
|
32
|
+
* in-flight transaction), so the event is committed atomically WITH the state
|
|
33
|
+
* change. This is the durability lever behind the transactional-outbox pattern.
|
|
34
|
+
*
|
|
35
|
+
* RECURSION GUARD: we never emit events for {@link DomainEvent} itself, otherwise
|
|
36
|
+
* each emitted row would itself trigger another emission. Only entities in the
|
|
37
|
+
* opted-in set (passed to {@link DomainEventsModule.forRoot}) emit; everything
|
|
38
|
+
* else is skipped.
|
|
39
|
+
*/
|
|
40
|
+
let DomainEventSubscriber = class DomainEventSubscriber {
|
|
41
|
+
constructor(mapper, emittingEntities, dataSource) {
|
|
42
|
+
this.mapper = mapper;
|
|
43
|
+
this.emittingEntities = emittingEntities;
|
|
44
|
+
dataSource?.subscribers?.push(this);
|
|
45
|
+
}
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
47
|
+
isEmitting(target) {
|
|
48
|
+
// RECURSION GUARD: never emit for DomainEvent itself.
|
|
49
|
+
if (target === domain_event_entity_1.DomainEvent || target === 'DomainEvent') {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return this.emittingEntities.some((cls) => target === cls || target === cls.name);
|
|
53
|
+
}
|
|
54
|
+
async emit(manager,
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
56
|
+
entityClass, entity, action) {
|
|
57
|
+
const entityName = entityClass.name;
|
|
58
|
+
const repo = manager.getRepository(domain_event_entity_1.DomainEvent);
|
|
59
|
+
// manager keeps us inside the active transaction.
|
|
60
|
+
await repo.save(repo.create({
|
|
61
|
+
type: this.mapper.eventType(entityName, action),
|
|
62
|
+
payload: this.mapper.toPayload(entity, action),
|
|
63
|
+
status: 'pending',
|
|
64
|
+
attempts: 0,
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
async afterInsert(event) {
|
|
68
|
+
if (!this.isEmitting(event.metadata.target))
|
|
69
|
+
return;
|
|
70
|
+
await this.emit(event.manager,
|
|
71
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
72
|
+
event.metadata.target, event.entity, 'created');
|
|
73
|
+
}
|
|
74
|
+
async afterUpdate(event) {
|
|
75
|
+
if (!this.isEmitting(event.metadata.target))
|
|
76
|
+
return;
|
|
77
|
+
await this.emit(event.manager,
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
79
|
+
event.metadata.target, event.entity ?? event.databaseEntity, 'updated');
|
|
80
|
+
}
|
|
81
|
+
async afterRemove(event) {
|
|
82
|
+
if (!this.isEmitting(event.metadata.target))
|
|
83
|
+
return;
|
|
84
|
+
await this.emit(event.manager,
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
86
|
+
event.metadata.target, event.entity ?? event.databaseEntity, 'removed');
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
exports.DomainEventSubscriber = DomainEventSubscriber;
|
|
90
|
+
exports.DomainEventSubscriber = DomainEventSubscriber = __decorate([
|
|
91
|
+
(0, common_1.Injectable)(),
|
|
92
|
+
(0, typeorm_1.EventSubscriber)(),
|
|
93
|
+
__param(0, (0, common_1.Inject)(domain_event_mapper_1.DOMAIN_EVENT_MAPPER)),
|
|
94
|
+
__param(1, (0, common_1.Inject)(exports.DOMAIN_EVENT_ENTITIES)),
|
|
95
|
+
__param(2, (0, common_1.Optional)()),
|
|
96
|
+
__metadata("design:paramtypes", [Object, Array, typeorm_1.DataSource])
|
|
97
|
+
], DomainEventSubscriber);
|
|
98
|
+
//# sourceMappingURL=domain-event.subscriber.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-event.subscriber.js","sourceRoot":"","sources":["../src/domain-event.subscriber.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAA8D;AAC9D,qCAQiB;AACjB,+DAAoD;AACpD,+DAI+B;AAE/B;;;;;GAKG;AACU,QAAA,qBAAqB,GAAG,uBAAuB,CAAC;AAE7D;;;;;;;;;;;;GAYG;AAGI,IAAM,qBAAqB,GAA3B,MAAM,qBAAqB;IAChC,YACgD,MAAyB,EAEvB,gBAA4B,EAKhE,UAAuB;QAPW,WAAM,GAAN,MAAM,CAAmB;QAEvB,qBAAgB,GAAhB,gBAAgB,CAAY;QAO5E,UAAU,EAAE,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;IAED,wDAAwD;IAChD,UAAU,CAAC,MAAyB;QAC1C,sDAAsD;QACtD,IAAI,MAAM,KAAK,iCAAW,IAAI,MAAM,KAAK,aAAa,EAAE,CAAC;YACvD,OAAO,KAAK,CAAC;QACf,CAAC;QACD,OAAO,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAC/B,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,GAAG,CAAC,IAAI,CAC/C,CAAC;IACJ,CAAC;IAEO,KAAK,CAAC,IAAI,CAChB,OAAsB;IACtB,wDAAwD;IACxD,WAAqB,EACrB,MAAe,EACf,MAAyB;QAEzB,MAAM,UAAU,GAAG,WAAW,CAAC,IAAI,CAAC;QACpC,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,iCAAW,CAAC,CAAC;QAChD,kDAAkD;QAClD,MAAM,IAAI,CAAC,IAAI,CACb,IAAI,CAAC,MAAM,CAAC;YACV,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,EAAE,MAAM,CAAC;YAC/C,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC;YAC9C,MAAM,EAAE,SAAS;YACjB,QAAQ,EAAE,CAAC;SACZ,CAAC,CACH,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,KAA2B;QAC3C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,OAAO;QACpD,MAAM,IAAI,CAAC,IAAI,CACb,KAAK,CAAC,OAAO;QACb,wDAAwD;QACxD,KAAK,CAAC,QAAQ,CAAC,MAAkB,EACjC,KAAK,CAAC,MAAM,EACZ,SAAS,CACV,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,KAA2B;QAC3C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,OAAO;QACpD,MAAM,IAAI,CAAC,IAAI,CACb,KAAK,CAAC,OAAO;QACb,wDAAwD;QACxD,KAAK,CAAC,QAAQ,CAAC,MAAkB,EACjC,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,cAAc,EACpC,SAAS,CACV,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,KAA2B;QAC3C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,OAAO;QACpD,MAAM,IAAI,CAAC,IAAI,CACb,KAAK,CAAC,OAAO;QACb,wDAAwD;QACxD,KAAK,CAAC,QAAQ,CAAC,MAAkB,EACjC,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,cAAc,EACpC,SAAS,CACV,CAAC;IACJ,CAAC;CACF,CAAA;AA7EY,sDAAqB;gCAArB,qBAAqB;IAFjC,IAAA,mBAAU,GAAE;IACZ,IAAA,yBAAe,GAAE;IAGb,WAAA,IAAA,eAAM,EAAC,yCAAmB,CAAC,CAAA;IAE3B,WAAA,IAAA,eAAM,EAAC,6BAAqB,CAAC,CAAA;IAK7B,WAAA,IAAA,iBAAQ,GAAE,CAAA;oDAAc,oBAAU;GAT1B,qBAAqB,CA6EjC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { DynamicModule, Type } from '@nestjs/common';
|
|
2
|
+
import { DomainEventMapper } from './domain-event.mapper';
|
|
3
|
+
/**
|
|
4
|
+
* Options for {@link DomainEventsModule.forRoot}.
|
|
5
|
+
*/
|
|
6
|
+
export interface DomainEventsModuleOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Entity classes opted in to domain-event emission (the CLI-emitted
|
|
9
|
+
* manifest). Only state changes to these emit events; the library never
|
|
10
|
+
* hardcodes this set.
|
|
11
|
+
*/
|
|
12
|
+
entities: Function[];
|
|
13
|
+
/**
|
|
14
|
+
* Optional custom mapper class to bind under {@link DOMAIN_EVENT_MAPPER}.
|
|
15
|
+
* Defaults to {@link DefaultDomainEventMapper}.
|
|
16
|
+
*/
|
|
17
|
+
mapper?: Type<DomainEventMapper>;
|
|
18
|
+
/**
|
|
19
|
+
* Poll interval (ms) for the relay's self-contained drain loop. Defaults to
|
|
20
|
+
* {@link DEFAULT_POLL_INTERVAL_MS} (5000).
|
|
21
|
+
*/
|
|
22
|
+
pollIntervalMs?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* DomainEventsModule wires the durable domain-event spine (transactional-outbox
|
|
26
|
+
* pattern, surfaced as generic domain events).
|
|
27
|
+
*
|
|
28
|
+
* - {@link DomainEventSubscriber} writes events in-transaction with state changes.
|
|
29
|
+
* - {@link DomainEventRelay} drains and delivers pending events, with its own
|
|
30
|
+
* self-contained poller (no external scheduler dependency).
|
|
31
|
+
* - {@link DOMAIN_EVENT_MAPPER} controls event-type/payload semantics; pass a
|
|
32
|
+
* `mapper` to override.
|
|
33
|
+
* - {@link DOMAIN_EVENT_DESTINATIONS} is the set of ACTIVE delivery adapters,
|
|
34
|
+
* built from the `EVENTS_DESTINATION` env var at runtime.
|
|
35
|
+
*
|
|
36
|
+
* MULTI-DATASOURCE: `@EventSubscriber()` auto-registers on the default
|
|
37
|
+
* DataSource; for additional named DataSources, register this module (or the
|
|
38
|
+
* subscriber) per DataSource.
|
|
39
|
+
*/
|
|
40
|
+
export declare class DomainEventsModule {
|
|
41
|
+
static forRoot(options: DomainEventsModuleOptions): DynamicModule;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=domain-events.module.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-events.module.d.ts","sourceRoot":"","sources":["../src/domain-events.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAA4B,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAY/E,OAAO,EAGL,iBAAiB,EAClB,MAAM,uBAAuB,CAAC;AAG/B;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;;OAIG;IAEH,QAAQ,EAAE,QAAQ,EAAE,CAAC;IAErB;;;OAGG;IACH,MAAM,CAAC,EAAE,IAAI,CAAC,iBAAiB,CAAC,CAAC;IAEjC;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAEa,kBAAkB;IAC7B,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,yBAAyB,GAAG,aAAa;CAmClE"}
|