@nestjs-transactional/outbox 1.0.0-alpha.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/LICENSE +21 -0
- package/README.md +396 -0
- package/dist/api/completed-event-publications.d.ts +31 -0
- package/dist/api/completed-event-publications.js +58 -0
- package/dist/api/completed-event-publications.js.map +1 -0
- package/dist/api/failed-event-publications.d.ts +36 -0
- package/dist/api/failed-event-publications.js +75 -0
- package/dist/api/failed-event-publications.js.map +1 -0
- package/dist/api/incomplete-event-publications.d.ts +28 -0
- package/dist/api/incomplete-event-publications.js +72 -0
- package/dist/api/incomplete-event-publications.js.map +1 -0
- package/dist/decorators/inject-decorators.d.ts +85 -0
- package/dist/decorators/inject-decorators.js +100 -0
- package/dist/decorators/inject-decorators.js.map +1 -0
- package/dist/decorators/outbox-events-handler.decorator.d.ts +97 -0
- package/dist/decorators/outbox-events-handler.decorator.js +58 -0
- package/dist/decorators/outbox-events-handler.decorator.js.map +1 -0
- package/dist/dispatcher/data-source-outbox-publisher.d.ts +69 -0
- package/dist/dispatcher/data-source-outbox-publisher.js +127 -0
- package/dist/dispatcher/data-source-outbox-publisher.js.map +1 -0
- package/dist/dispatcher/event-publication-processor.d.ts +69 -0
- package/dist/dispatcher/event-publication-processor.js +192 -0
- package/dist/dispatcher/event-publication-processor.js.map +1 -0
- package/dist/dispatcher/outbox-event-publisher.d.ts +129 -0
- package/dist/dispatcher/outbox-event-publisher.js +172 -0
- package/dist/dispatcher/outbox-event-publisher.js.map +1 -0
- package/dist/dispatcher/processor-options.d.ts +24 -0
- package/dist/dispatcher/processor-options.js +15 -0
- package/dist/dispatcher/processor-options.js.map +1 -0
- package/dist/externalization/errors.d.ts +21 -0
- package/dist/externalization/errors.js +30 -0
- package/dist/externalization/errors.js.map +1 -0
- package/dist/externalization/event-externalizer.d.ts +49 -0
- package/dist/externalization/event-externalizer.js +14 -0
- package/dist/externalization/event-externalizer.js.map +1 -0
- package/dist/externalization/externalization-registry.d.ts +56 -0
- package/dist/externalization/externalization-registry.js +103 -0
- package/dist/externalization/externalization-registry.js.map +1 -0
- package/dist/externalization/externalized.decorator.d.ts +121 -0
- package/dist/externalization/externalized.decorator.js +87 -0
- package/dist/externalization/externalized.decorator.js.map +1 -0
- package/dist/externalization/types.d.ts +39 -0
- package/dist/externalization/types.js +3 -0
- package/dist/externalization/types.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/outbox-event-handler.interface.d.ts +32 -0
- package/dist/interfaces/outbox-event-handler.interface.js +3 -0
- package/dist/interfaces/outbox-event-handler.interface.js.map +1 -0
- package/dist/module/outbox-processing.module.d.ts +29 -0
- package/dist/module/outbox-processing.module.js +66 -0
- package/dist/module/outbox-processing.module.js.map +1 -0
- package/dist/module/outbox.module.d.ts +196 -0
- package/dist/module/outbox.module.js +604 -0
- package/dist/module/outbox.module.js.map +1 -0
- package/dist/recovery/staleness-monitor.d.ts +39 -0
- package/dist/recovery/staleness-monitor.js +118 -0
- package/dist/recovery/staleness-monitor.js.map +1 -0
- package/dist/recovery/startup-recovery.d.ts +37 -0
- package/dist/recovery/startup-recovery.js +55 -0
- package/dist/recovery/startup-recovery.js.map +1 -0
- package/dist/registry/event-publication-registry.d.ts +70 -0
- package/dist/registry/event-publication-registry.js +138 -0
- package/dist/registry/event-publication-registry.js.map +1 -0
- package/dist/registry/listener-registry.d.ts +52 -0
- package/dist/registry/listener-registry.js +67 -0
- package/dist/registry/listener-registry.js.map +1 -0
- package/dist/registry/multi-ds-listener-registrar.d.ts +78 -0
- package/dist/registry/multi-ds-listener-registrar.js +112 -0
- package/dist/registry/multi-ds-listener-registrar.js.map +1 -0
- package/dist/registry/outbox-listener-scanner.d.ts +66 -0
- package/dist/registry/outbox-listener-scanner.js +192 -0
- package/dist/registry/outbox-listener-scanner.js.map +1 -0
- package/dist/repository/event-publication-repository.d.ts +112 -0
- package/dist/repository/event-publication-repository.js +6 -0
- package/dist/repository/event-publication-repository.js.map +1 -0
- package/dist/serialization/event-serializer.d.ts +28 -0
- package/dist/serialization/event-serializer.js +6 -0
- package/dist/serialization/event-serializer.js.map +1 -0
- package/dist/serialization/event-type-registry.d.ts +41 -0
- package/dist/serialization/event-type-registry.js +79 -0
- package/dist/serialization/event-type-registry.js.map +1 -0
- package/dist/serialization/event-type-resolver.d.ts +43 -0
- package/dist/serialization/event-type-resolver.js +66 -0
- package/dist/serialization/event-type-resolver.js.map +1 -0
- package/dist/serialization/json-event-serializer.d.ts +20 -0
- package/dist/serialization/json-event-serializer.js +64 -0
- package/dist/serialization/json-event-serializer.js.map +1 -0
- package/dist/testing/assertable-published-events.d.ts +71 -0
- package/dist/testing/assertable-published-events.js +124 -0
- package/dist/testing/assertable-published-events.js.map +1 -0
- package/dist/testing/in-memory-repository.d.ts +52 -0
- package/dist/testing/in-memory-repository.js +230 -0
- package/dist/testing/in-memory-repository.js.map +1 -0
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.js +20 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/published-events.d.ts +62 -0
- package/dist/testing/published-events.js +110 -0
- package/dist/testing/published-events.js.map +1 -0
- package/dist/tokens/index.d.ts +2 -0
- package/dist/tokens/index.js +14 -0
- package/dist/tokens/index.js.map +1 -0
- package/dist/tokens/token-utils.d.ts +68 -0
- package/dist/tokens/token-utils.js +98 -0
- package/dist/tokens/token-utils.js.map +1 -0
- package/dist/types/completion-mode.d.ts +16 -0
- package/dist/types/completion-mode.js +20 -0
- package/dist/types/completion-mode.js.map +1 -0
- package/dist/types/errors.d.ts +50 -0
- package/dist/types/errors.js +64 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/event-publication.d.ts +47 -0
- package/dist/types/event-publication.js +3 -0
- package/dist/types/event-publication.js.map +1 -0
- package/dist/types/publication-status.d.ts +26 -0
- package/dist/types/publication-status.js +30 -0
- package/dist/types/publication-status.js.map +1 -0
- package/dist/types/resubmission-options.d.ts +37 -0
- package/dist/types/resubmission-options.js +62 -0
- package/dist/types/resubmission-options.js.map +1 -0
- package/dist/types/staleness-config.d.ts +18 -0
- package/dist/types/staleness-config.js +10 -0
- package/dist/types/staleness-config.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,72 @@
|
|
|
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.IncompleteEventPublications = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const event_publication_repository_1 = require("../repository/event-publication-repository");
|
|
18
|
+
const publication_status_1 = require("../types/publication-status");
|
|
19
|
+
const resubmission_options_1 = require("../types/resubmission-options");
|
|
20
|
+
/**
|
|
21
|
+
* Operator-facing query + resubmit API for every publication that has
|
|
22
|
+
* not yet reached the terminal `COMPLETED` state. Equivalent to Spring
|
|
23
|
+
* Modulith's `IncompleteEventPublications`.
|
|
24
|
+
*
|
|
25
|
+
* Typical use case: startup / restart recovery — query the backlog,
|
|
26
|
+
* bulk-resubmit retriable rows, let the processor drain.
|
|
27
|
+
*/
|
|
28
|
+
let IncompleteEventPublications = class IncompleteEventPublications {
|
|
29
|
+
repository;
|
|
30
|
+
constructor(repository) {
|
|
31
|
+
this.repository = repository;
|
|
32
|
+
}
|
|
33
|
+
/** Return every publication whose status is not `COMPLETED`. */
|
|
34
|
+
async findAll() {
|
|
35
|
+
return this.repository.findIncomplete();
|
|
36
|
+
}
|
|
37
|
+
/** How many publications are currently not `COMPLETED`. */
|
|
38
|
+
async count() {
|
|
39
|
+
const incomplete = await this.repository.findIncomplete();
|
|
40
|
+
return incomplete.length;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Scan every non-completed publication and transition retriable
|
|
44
|
+
* ones (`FAILED`, `PUBLISHED`) to `RESUBMITTED`. Publications
|
|
45
|
+
* currently `PROCESSING` or `RESUBMITTED` are left untouched —
|
|
46
|
+
* `PROCESSING` is owned by a worker and `RESUBMITTED` is already in
|
|
47
|
+
* the retry queue. Returns the number of rows transitioned.
|
|
48
|
+
*/
|
|
49
|
+
async resubmitIncompletePublications(options = resubmission_options_1.ResubmissionOptions.defaults()) {
|
|
50
|
+
const incomplete = await this.repository.findIncomplete();
|
|
51
|
+
const filtered = options.filter !== null ? incomplete.filter(options.filter) : incomplete;
|
|
52
|
+
const toProcess = filtered.slice(0, options.batchSize);
|
|
53
|
+
let resubmitted = 0;
|
|
54
|
+
for (const pub of toProcess) {
|
|
55
|
+
if (pub.status === publication_status_1.PublicationStatus.FAILED ||
|
|
56
|
+
pub.status === publication_status_1.PublicationStatus.PUBLISHED) {
|
|
57
|
+
await this.repository.updateStatus(pub.id, publication_status_1.PublicationStatus.RESUBMITTED, {
|
|
58
|
+
lastResubmissionDate: new Date(),
|
|
59
|
+
});
|
|
60
|
+
resubmitted++;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return resubmitted;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
exports.IncompleteEventPublications = IncompleteEventPublications;
|
|
67
|
+
exports.IncompleteEventPublications = IncompleteEventPublications = __decorate([
|
|
68
|
+
(0, common_1.Injectable)(),
|
|
69
|
+
__param(0, (0, common_1.Inject)(event_publication_repository_1.EVENT_PUBLICATION_REPOSITORY)),
|
|
70
|
+
__metadata("design:paramtypes", [Object])
|
|
71
|
+
], IncompleteEventPublications);
|
|
72
|
+
//# sourceMappingURL=incomplete-event-publications.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"incomplete-event-publications.js","sourceRoot":"","sources":["../../src/api/incomplete-event-publications.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAAoD;AAEpD,6FAGoD;AAEpD,oEAAgE;AAChE,wEAAoE;AAGpE;;;;;;;GAOG;AAEI,IAAM,2BAA2B,GAAjC,MAAM,2BAA2B;IAGnB;IAFnB,YAEmB,UAAsC;QAAtC,eAAU,GAAV,UAAU,CAA4B;IACtD,CAAC;IAEJ,gEAAgE;IAChE,KAAK,CAAC,OAAO;QACX,OAAO,IAAI,CAAC,UAAU,CAAC,cAAc,EAAE,CAAC;IAC1C,CAAC;IAED,2DAA2D;IAC3D,KAAK,CAAC,KAAK;QACT,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,cAAc,EAAE,CAAC;QAC1D,OAAO,UAAU,CAAC,MAAM,CAAC;IAC3B,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,8BAA8B,CAClC,UAA+B,0CAAmB,CAAC,QAAQ,EAAE;QAE7D,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,cAAc,EAAE,CAAC;QAE1D,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC;QAC1F,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;QAEvD,IAAI,WAAW,GAAG,CAAC,CAAC;QACpB,KAAK,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;YAC5B,IACE,GAAG,CAAC,MAAM,KAAK,sCAAiB,CAAC,MAAM;gBACvC,GAAG,CAAC,MAAM,KAAK,sCAAiB,CAAC,SAAS,EAC1C,CAAC;gBACD,MAAM,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,EAAE,sCAAiB,CAAC,WAAW,EAAE;oBACxE,oBAAoB,EAAE,IAAI,IAAI,EAAE;iBACjC,CAAC,CAAC;gBACH,WAAW,EAAE,CAAC;YAChB,CAAC;QACH,CAAC;QAED,OAAO,WAAW,CAAC;IACrB,CAAC;CACF,CAAA;AA/CY,kEAA2B;sCAA3B,2BAA2B;IADvC,IAAA,mBAAU,GAAE;IAGR,WAAA,IAAA,eAAM,EAAC,2DAA4B,CAAC,CAAA;;GAF5B,2BAA2B,CA+CvC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inject decorators for the outbox-side per-dataSource components
|
|
3
|
+
* (DD-022). Each decorator below is sugar over
|
|
4
|
+
* `@Inject(getXxxToken(dataSource))` with a default dataSource of
|
|
5
|
+
* `'default'` — matches the `@InjectRepository(Entity, dataSource?)`
|
|
6
|
+
* ergonomics from `@nestjs/typeorm`.
|
|
7
|
+
*
|
|
8
|
+
* Single-adapter consumers omit the argument; multi-adapter
|
|
9
|
+
* consumers pass the dataSource name they registered with
|
|
10
|
+
* `OutboxModule.forRoot({ dataSource })`.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Inject the per-dataSource `OutboxEventPublisher`. The
|
|
14
|
+
* default-injected publisher is the smart facade (DD-024) that
|
|
15
|
+
* detects the active transaction context and routes accordingly;
|
|
16
|
+
* this decorator binds the underlying per-dataSource publisher
|
|
17
|
+
* directly when explicit selection is needed.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* class BillingService {
|
|
22
|
+
* constructor(
|
|
23
|
+
* @InjectOutboxPublisher('billing')
|
|
24
|
+
* private readonly publisher: OutboxEventPublisher,
|
|
25
|
+
* ) {}
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare const InjectOutboxPublisher: (dataSource?: string) => ParameterDecorator;
|
|
30
|
+
/**
|
|
31
|
+
* Inject the per-dataSource `EventTypeRegistry`. The registry is
|
|
32
|
+
* populated by the `OutboxModule.forFeature([...])` calls made
|
|
33
|
+
* against the same dataSource — registrations do not bleed across
|
|
34
|
+
* dataSources.
|
|
35
|
+
*/
|
|
36
|
+
export declare const InjectEventTypeRegistry: (dataSource?: string) => ParameterDecorator;
|
|
37
|
+
/**
|
|
38
|
+
* Inject the per-dataSource `EventPublicationRegistry` — the
|
|
39
|
+
* lifecycle coordinator that owns publication state transitions
|
|
40
|
+
* (`PUBLISHED` → `PROCESSING` → `COMPLETED` / `FAILED`).
|
|
41
|
+
*/
|
|
42
|
+
export declare const InjectEventPublicationRegistry: (dataSource?: string) => ParameterDecorator;
|
|
43
|
+
/**
|
|
44
|
+
* Inject the per-dataSource `EventPublicationProcessor` — the async
|
|
45
|
+
* worker that drains publications, dispatches them to listeners,
|
|
46
|
+
* and invokes the externalizer (when bound). Typically only needed
|
|
47
|
+
* by tests that drive the loop manually with `processBatch()`.
|
|
48
|
+
*/
|
|
49
|
+
export declare const InjectEventPublicationProcessor: (dataSource?: string) => ParameterDecorator;
|
|
50
|
+
/**
|
|
51
|
+
* Inject the per-dataSource `OutboxListenerRegistry` — registry of
|
|
52
|
+
* listener entries scanned from `@OutboxEventsHandler` classes (and,
|
|
53
|
+
* when wired, `@IntegrationEventsHandler` classes routed via the
|
|
54
|
+
* outbox).
|
|
55
|
+
*/
|
|
56
|
+
export declare const InjectOutboxListenerRegistry: (dataSource?: string) => ParameterDecorator;
|
|
57
|
+
/**
|
|
58
|
+
* Inject the per-dataSource `ExternalizationRegistry` — maps event
|
|
59
|
+
* type names to their `@Externalized` metadata. Consumed by the
|
|
60
|
+
* processor at dispatch time; rarely needed by application code.
|
|
61
|
+
*/
|
|
62
|
+
export declare const InjectExternalizationRegistry: (dataSource?: string) => ParameterDecorator;
|
|
63
|
+
/**
|
|
64
|
+
* Inject the per-dataSource `EventPublicationRepository`. Adapter
|
|
65
|
+
* packages (`outbox-typeorm`, future `outbox-prisma`, ...) register
|
|
66
|
+
* their concrete repository under this token. Most application
|
|
67
|
+
* code injects the higher-level `OutboxEventPublisher` instead.
|
|
68
|
+
*/
|
|
69
|
+
export declare const InjectEventPublicationRepository: (dataSource?: string) => ParameterDecorator;
|
|
70
|
+
/**
|
|
71
|
+
* Inject the per-dataSource `EventExternalizer`. Multi-adapter
|
|
72
|
+
* processes can wire externalization independently per outbox stack
|
|
73
|
+
* — e.g. the `'billing'` outbox emits to Kafka, the `'audit'` outbox
|
|
74
|
+
* is internal-only and binds no externalizer.
|
|
75
|
+
*/
|
|
76
|
+
export declare const InjectEventExternalizer: (dataSource?: string) => ParameterDecorator;
|
|
77
|
+
/**
|
|
78
|
+
* Inject the per-dataSource `EventSerializer` used by the outbox to
|
|
79
|
+
* marshal events into the stored JSON payload and revive them at
|
|
80
|
+
* dispatch time. Distinct dataSources may use distinct
|
|
81
|
+
* serialization strategies (e.g. one JSON, another schema-validated)
|
|
82
|
+
* without forcing one strategy process-wide.
|
|
83
|
+
*/
|
|
84
|
+
export declare const InjectOutboxEventSerializer: (dataSource?: string) => ParameterDecorator;
|
|
85
|
+
//# sourceMappingURL=inject-decorators.d.ts.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InjectOutboxEventSerializer = exports.InjectEventExternalizer = exports.InjectEventPublicationRepository = exports.InjectExternalizationRegistry = exports.InjectOutboxListenerRegistry = exports.InjectEventPublicationProcessor = exports.InjectEventPublicationRegistry = exports.InjectEventTypeRegistry = exports.InjectOutboxPublisher = void 0;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
const core_1 = require("@nestjs-transactional/core");
|
|
6
|
+
const token_utils_1 = require("../tokens/token-utils");
|
|
7
|
+
/**
|
|
8
|
+
* Inject decorators for the outbox-side per-dataSource components
|
|
9
|
+
* (DD-022). Each decorator below is sugar over
|
|
10
|
+
* `@Inject(getXxxToken(dataSource))` with a default dataSource of
|
|
11
|
+
* `'default'` — matches the `@InjectRepository(Entity, dataSource?)`
|
|
12
|
+
* ergonomics from `@nestjs/typeorm`.
|
|
13
|
+
*
|
|
14
|
+
* Single-adapter consumers omit the argument; multi-adapter
|
|
15
|
+
* consumers pass the dataSource name they registered with
|
|
16
|
+
* `OutboxModule.forRoot({ dataSource })`.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Inject the per-dataSource `OutboxEventPublisher`. The
|
|
20
|
+
* default-injected publisher is the smart facade (DD-024) that
|
|
21
|
+
* detects the active transaction context and routes accordingly;
|
|
22
|
+
* this decorator binds the underlying per-dataSource publisher
|
|
23
|
+
* directly when explicit selection is needed.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* class BillingService {
|
|
28
|
+
* constructor(
|
|
29
|
+
* @InjectOutboxPublisher('billing')
|
|
30
|
+
* private readonly publisher: OutboxEventPublisher,
|
|
31
|
+
* ) {}
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
const InjectOutboxPublisher = (dataSource = core_1.DEFAULT_DATA_SOURCE_NAME) => (0, common_1.Inject)((0, token_utils_1.getOutboxPublisherToken)(dataSource));
|
|
36
|
+
exports.InjectOutboxPublisher = InjectOutboxPublisher;
|
|
37
|
+
/**
|
|
38
|
+
* Inject the per-dataSource `EventTypeRegistry`. The registry is
|
|
39
|
+
* populated by the `OutboxModule.forFeature([...])` calls made
|
|
40
|
+
* against the same dataSource — registrations do not bleed across
|
|
41
|
+
* dataSources.
|
|
42
|
+
*/
|
|
43
|
+
const InjectEventTypeRegistry = (dataSource = core_1.DEFAULT_DATA_SOURCE_NAME) => (0, common_1.Inject)((0, token_utils_1.getEventTypeRegistryToken)(dataSource));
|
|
44
|
+
exports.InjectEventTypeRegistry = InjectEventTypeRegistry;
|
|
45
|
+
/**
|
|
46
|
+
* Inject the per-dataSource `EventPublicationRegistry` — the
|
|
47
|
+
* lifecycle coordinator that owns publication state transitions
|
|
48
|
+
* (`PUBLISHED` → `PROCESSING` → `COMPLETED` / `FAILED`).
|
|
49
|
+
*/
|
|
50
|
+
const InjectEventPublicationRegistry = (dataSource = core_1.DEFAULT_DATA_SOURCE_NAME) => (0, common_1.Inject)((0, token_utils_1.getEventPublicationRegistryToken)(dataSource));
|
|
51
|
+
exports.InjectEventPublicationRegistry = InjectEventPublicationRegistry;
|
|
52
|
+
/**
|
|
53
|
+
* Inject the per-dataSource `EventPublicationProcessor` — the async
|
|
54
|
+
* worker that drains publications, dispatches them to listeners,
|
|
55
|
+
* and invokes the externalizer (when bound). Typically only needed
|
|
56
|
+
* by tests that drive the loop manually with `processBatch()`.
|
|
57
|
+
*/
|
|
58
|
+
const InjectEventPublicationProcessor = (dataSource = core_1.DEFAULT_DATA_SOURCE_NAME) => (0, common_1.Inject)((0, token_utils_1.getEventPublicationProcessorToken)(dataSource));
|
|
59
|
+
exports.InjectEventPublicationProcessor = InjectEventPublicationProcessor;
|
|
60
|
+
/**
|
|
61
|
+
* Inject the per-dataSource `OutboxListenerRegistry` — registry of
|
|
62
|
+
* listener entries scanned from `@OutboxEventsHandler` classes (and,
|
|
63
|
+
* when wired, `@IntegrationEventsHandler` classes routed via the
|
|
64
|
+
* outbox).
|
|
65
|
+
*/
|
|
66
|
+
const InjectOutboxListenerRegistry = (dataSource = core_1.DEFAULT_DATA_SOURCE_NAME) => (0, common_1.Inject)((0, token_utils_1.getOutboxListenerRegistryToken)(dataSource));
|
|
67
|
+
exports.InjectOutboxListenerRegistry = InjectOutboxListenerRegistry;
|
|
68
|
+
/**
|
|
69
|
+
* Inject the per-dataSource `ExternalizationRegistry` — maps event
|
|
70
|
+
* type names to their `@Externalized` metadata. Consumed by the
|
|
71
|
+
* processor at dispatch time; rarely needed by application code.
|
|
72
|
+
*/
|
|
73
|
+
const InjectExternalizationRegistry = (dataSource = core_1.DEFAULT_DATA_SOURCE_NAME) => (0, common_1.Inject)((0, token_utils_1.getExternalizationRegistryToken)(dataSource));
|
|
74
|
+
exports.InjectExternalizationRegistry = InjectExternalizationRegistry;
|
|
75
|
+
/**
|
|
76
|
+
* Inject the per-dataSource `EventPublicationRepository`. Adapter
|
|
77
|
+
* packages (`outbox-typeorm`, future `outbox-prisma`, ...) register
|
|
78
|
+
* their concrete repository under this token. Most application
|
|
79
|
+
* code injects the higher-level `OutboxEventPublisher` instead.
|
|
80
|
+
*/
|
|
81
|
+
const InjectEventPublicationRepository = (dataSource = core_1.DEFAULT_DATA_SOURCE_NAME) => (0, common_1.Inject)((0, token_utils_1.getEventPublicationRepositoryToken)(dataSource));
|
|
82
|
+
exports.InjectEventPublicationRepository = InjectEventPublicationRepository;
|
|
83
|
+
/**
|
|
84
|
+
* Inject the per-dataSource `EventExternalizer`. Multi-adapter
|
|
85
|
+
* processes can wire externalization independently per outbox stack
|
|
86
|
+
* — e.g. the `'billing'` outbox emits to Kafka, the `'audit'` outbox
|
|
87
|
+
* is internal-only and binds no externalizer.
|
|
88
|
+
*/
|
|
89
|
+
const InjectEventExternalizer = (dataSource = core_1.DEFAULT_DATA_SOURCE_NAME) => (0, common_1.Inject)((0, token_utils_1.getEventExternalizerToken)(dataSource));
|
|
90
|
+
exports.InjectEventExternalizer = InjectEventExternalizer;
|
|
91
|
+
/**
|
|
92
|
+
* Inject the per-dataSource `EventSerializer` used by the outbox to
|
|
93
|
+
* marshal events into the stored JSON payload and revive them at
|
|
94
|
+
* dispatch time. Distinct dataSources may use distinct
|
|
95
|
+
* serialization strategies (e.g. one JSON, another schema-validated)
|
|
96
|
+
* without forcing one strategy process-wide.
|
|
97
|
+
*/
|
|
98
|
+
const InjectOutboxEventSerializer = (dataSource = core_1.DEFAULT_DATA_SOURCE_NAME) => (0, common_1.Inject)((0, token_utils_1.getOutboxEventSerializerToken)(dataSource));
|
|
99
|
+
exports.InjectOutboxEventSerializer = InjectOutboxEventSerializer;
|
|
100
|
+
//# sourceMappingURL=inject-decorators.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inject-decorators.js","sourceRoot":"","sources":["../../src/decorators/inject-decorators.ts"],"names":[],"mappings":";;;AAAA,2CAAwC;AACxC,qDAAsE;AAEtE,uDAU+B;AAE/B;;;;;;;;;;GAUG;AAEH;;;;;;;;;;;;;;;;GAgBG;AACI,MAAM,qBAAqB,GAAG,CACnC,aAAqB,+BAAwB,EACzB,EAAE,CAAC,IAAA,eAAM,EAAC,IAAA,qCAAuB,EAAC,UAAU,CAAC,CAAC,CAAC;AAFxD,QAAA,qBAAqB,yBAEmC;AAErE;;;;;GAKG;AACI,MAAM,uBAAuB,GAAG,CACrC,aAAqB,+BAAwB,EACzB,EAAE,CAAC,IAAA,eAAM,EAAC,IAAA,uCAAyB,EAAC,UAAU,CAAC,CAAC,CAAC;AAF1D,QAAA,uBAAuB,2BAEmC;AAEvE;;;;GAIG;AACI,MAAM,8BAA8B,GAAG,CAC5C,aAAqB,+BAAwB,EACzB,EAAE,CAAC,IAAA,eAAM,EAAC,IAAA,8CAAgC,EAAC,UAAU,CAAC,CAAC,CAAC;AAFjE,QAAA,8BAA8B,kCAEmC;AAE9E;;;;;GAKG;AACI,MAAM,+BAA+B,GAAG,CAC7C,aAAqB,+BAAwB,EACzB,EAAE,CAAC,IAAA,eAAM,EAAC,IAAA,+CAAiC,EAAC,UAAU,CAAC,CAAC,CAAC;AAFlE,QAAA,+BAA+B,mCAEmC;AAE/E;;;;;GAKG;AACI,MAAM,4BAA4B,GAAG,CAC1C,aAAqB,+BAAwB,EACzB,EAAE,CAAC,IAAA,eAAM,EAAC,IAAA,4CAA8B,EAAC,UAAU,CAAC,CAAC,CAAC;AAF/D,QAAA,4BAA4B,gCAEmC;AAE5E;;;;GAIG;AACI,MAAM,6BAA6B,GAAG,CAC3C,aAAqB,+BAAwB,EACzB,EAAE,CAAC,IAAA,eAAM,EAAC,IAAA,6CAA+B,EAAC,UAAU,CAAC,CAAC,CAAC;AAFhE,QAAA,6BAA6B,iCAEmC;AAE7E;;;;;GAKG;AACI,MAAM,gCAAgC,GAAG,CAC9C,aAAqB,+BAAwB,EACzB,EAAE,CAAC,IAAA,eAAM,EAAC,IAAA,gDAAkC,EAAC,UAAU,CAAC,CAAC,CAAC;AAFnE,QAAA,gCAAgC,oCAEmC;AAEhF;;;;;GAKG;AACI,MAAM,uBAAuB,GAAG,CACrC,aAAqB,+BAAwB,EACzB,EAAE,CAAC,IAAA,eAAM,EAAC,IAAA,uCAAyB,EAAC,UAAU,CAAC,CAAC,CAAC;AAF1D,QAAA,uBAAuB,2BAEmC;AAEvE;;;;;;GAMG;AACI,MAAM,2BAA2B,GAAG,CACzC,aAAqB,+BAAwB,EACzB,EAAE,CAAC,IAAA,eAAM,EAAC,IAAA,2CAA6B,EAAC,UAAU,CAAC,CAAC,CAAC;AAF9D,QAAA,2BAA2B,+BAEmC"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { type Type } from '@nestjs/common';
|
|
3
|
+
/**
|
|
4
|
+
* Metadata key under which {@link OutboxEventsHandlerMetadata} is
|
|
5
|
+
* stored on classes decorated with {@link OutboxEventsHandler}.
|
|
6
|
+
*
|
|
7
|
+
* Fresh `Symbol` (not `Symbol.for`) — this metadata is read only by
|
|
8
|
+
* outbox's own `OutboxListenerScanner`. Cross-package sharing is
|
|
9
|
+
* not required because `@IntegrationEventsHandler` in the cqrs
|
|
10
|
+
* package uses the `OUTBOX_LISTENER_REGISTRAR` structural port, not
|
|
11
|
+
* metadata introspection, to route handlers to the outbox.
|
|
12
|
+
*/
|
|
13
|
+
export declare const OUTBOX_EVENTS_HANDLER_METADATA: unique symbol;
|
|
14
|
+
/**
|
|
15
|
+
* Options accepted by the long form of {@link OutboxEventsHandler}.
|
|
16
|
+
*/
|
|
17
|
+
export interface OutboxEventsHandlerOptions {
|
|
18
|
+
/** Domain event classes the handler subscribes to. Must be non-empty. */
|
|
19
|
+
readonly events: Type[];
|
|
20
|
+
/**
|
|
21
|
+
* Stable, globally-unique listener id base. When set, the scanner
|
|
22
|
+
* derives the per-event-type id as `${id}#${EventName}`. When
|
|
23
|
+
* omitted, the base id is `${ClassName}` — a rename of the class
|
|
24
|
+
* therefore breaks resume of already-stored publications.
|
|
25
|
+
*
|
|
26
|
+
* The id (after the suffix is appended) is persisted on every
|
|
27
|
+
* `event_publication` row — supply an explicit id to protect
|
|
28
|
+
* against renames in production.
|
|
29
|
+
*/
|
|
30
|
+
readonly id?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Run the handler in its own new transaction (`REQUIRES_NEW`
|
|
33
|
+
* semantics). Default: `true` — matches Spring Modulith's
|
|
34
|
+
* `@ApplicationModuleListener` behaviour. Set to `false` to run
|
|
35
|
+
* the handler without opening a transaction (e.g. the handler
|
|
36
|
+
* only calls an idempotent external API).
|
|
37
|
+
*/
|
|
38
|
+
readonly newTransaction?: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolved metadata attached to a handler class.
|
|
42
|
+
*/
|
|
43
|
+
export interface OutboxEventsHandlerMetadata {
|
|
44
|
+
readonly eventTypes: Type[];
|
|
45
|
+
readonly id?: string;
|
|
46
|
+
readonly newTransaction: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Mark a class as a persistent outbox handler. The class must
|
|
50
|
+
* implement {@link IOutboxEventHandler} — expose a
|
|
51
|
+
* `handle(event): Promise<void>` method.
|
|
52
|
+
*
|
|
53
|
+
* Distinct from `@TransactionalEventsHandler` (cqrs package), which
|
|
54
|
+
* is in-memory and phase-based: `@OutboxEventsHandler` is
|
|
55
|
+
* persistent, always after-commit, and supports retry / recovery on
|
|
56
|
+
* restart. Also distinct from `@IntegrationEventsHandler`, which
|
|
57
|
+
* switches between durable and in-memory delivery based on module
|
|
58
|
+
* wiring — this decorator ALWAYS routes to the outbox (and will
|
|
59
|
+
* fail bootstrap without an outbox registry).
|
|
60
|
+
*
|
|
61
|
+
* Two forms:
|
|
62
|
+
*
|
|
63
|
+
* ```ts
|
|
64
|
+
* // Short form — defaults (newTransaction: true):
|
|
65
|
+
* @OutboxEventsHandler(OrderPlacedEvent, OrderCancelledEvent)
|
|
66
|
+
*
|
|
67
|
+
* // Long form — explicit options:
|
|
68
|
+
* @OutboxEventsHandler({
|
|
69
|
+
* events: [OrderPlacedEvent],
|
|
70
|
+
* id: 'inventory.reservation',
|
|
71
|
+
* newTransaction: false,
|
|
72
|
+
* })
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* Class-level only; the metadata is written by
|
|
76
|
+
* `Reflect.defineMetadata`. The actual registration happens at
|
|
77
|
+
* application bootstrap via `OutboxListenerScanner`.
|
|
78
|
+
*
|
|
79
|
+
* **Multi-dataSource (Phase 14.3.1).** The scanner walks every
|
|
80
|
+
* per-dataSource `EventTypeRegistry` and routes the handler to the
|
|
81
|
+
* `OutboxListenerRegistry` whose dataSource owns the decorated
|
|
82
|
+
* events — automatic, no decorator option required. A handler
|
|
83
|
+
* subscribing to events spanning multiple dataSources is rejected
|
|
84
|
+
* at bootstrap with an actionable message; handlers must be
|
|
85
|
+
* dataSource-scoped.
|
|
86
|
+
*
|
|
87
|
+
* @throws {Error} If no event types are supplied.
|
|
88
|
+
*/
|
|
89
|
+
export declare function OutboxEventsHandler(...events: Type[]): ClassDecorator;
|
|
90
|
+
export declare function OutboxEventsHandler(options: OutboxEventsHandlerOptions): ClassDecorator;
|
|
91
|
+
/**
|
|
92
|
+
* Read the {@link OutboxEventsHandlerMetadata} attached to `target`
|
|
93
|
+
* by {@link OutboxEventsHandler}. Returns `undefined` when the
|
|
94
|
+
* class was not decorated.
|
|
95
|
+
*/
|
|
96
|
+
export declare function getOutboxEventsHandlerMetadata(target: object): OutboxEventsHandlerMetadata | undefined;
|
|
97
|
+
//# sourceMappingURL=outbox-events-handler.decorator.d.ts.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OUTBOX_EVENTS_HANDLER_METADATA = void 0;
|
|
4
|
+
exports.OutboxEventsHandler = OutboxEventsHandler;
|
|
5
|
+
exports.getOutboxEventsHandlerMetadata = getOutboxEventsHandlerMetadata;
|
|
6
|
+
require("reflect-metadata");
|
|
7
|
+
/**
|
|
8
|
+
* Metadata key under which {@link OutboxEventsHandlerMetadata} is
|
|
9
|
+
* stored on classes decorated with {@link OutboxEventsHandler}.
|
|
10
|
+
*
|
|
11
|
+
* Fresh `Symbol` (not `Symbol.for`) — this metadata is read only by
|
|
12
|
+
* outbox's own `OutboxListenerScanner`. Cross-package sharing is
|
|
13
|
+
* not required because `@IntegrationEventsHandler` in the cqrs
|
|
14
|
+
* package uses the `OUTBOX_LISTENER_REGISTRAR` structural port, not
|
|
15
|
+
* metadata introspection, to route handlers to the outbox.
|
|
16
|
+
*/
|
|
17
|
+
exports.OUTBOX_EVENTS_HANDLER_METADATA = Symbol('OUTBOX_EVENTS_HANDLER_METADATA');
|
|
18
|
+
function OutboxEventsHandler(...args) {
|
|
19
|
+
const metadata = resolveMetadata(args);
|
|
20
|
+
if (metadata.eventTypes.length === 0) {
|
|
21
|
+
throw new Error('@OutboxEventsHandler requires at least one event type. ' +
|
|
22
|
+
'Pass class constructors as rest arguments or via the `events` option.');
|
|
23
|
+
}
|
|
24
|
+
return (target) => {
|
|
25
|
+
Reflect.defineMetadata(exports.OUTBOX_EVENTS_HANDLER_METADATA, metadata, target);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function resolveMetadata(args) {
|
|
29
|
+
if (args.length === 1 && isOptionsObject(args[0])) {
|
|
30
|
+
const options = args[0];
|
|
31
|
+
return {
|
|
32
|
+
eventTypes: [...options.events],
|
|
33
|
+
id: options.id,
|
|
34
|
+
newTransaction: options.newTransaction ?? true,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
eventTypes: args,
|
|
39
|
+
newTransaction: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function isOptionsObject(candidate) {
|
|
43
|
+
return (candidate !== null &&
|
|
44
|
+
typeof candidate === 'object' &&
|
|
45
|
+
!Array.isArray(candidate) &&
|
|
46
|
+
typeof candidate !== 'function' &&
|
|
47
|
+
'events' in candidate);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Read the {@link OutboxEventsHandlerMetadata} attached to `target`
|
|
51
|
+
* by {@link OutboxEventsHandler}. Returns `undefined` when the
|
|
52
|
+
* class was not decorated.
|
|
53
|
+
*/
|
|
54
|
+
function getOutboxEventsHandlerMetadata(target) {
|
|
55
|
+
const value = Reflect.getMetadata(exports.OUTBOX_EVENTS_HANDLER_METADATA, target);
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=outbox-events-handler.decorator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbox-events-handler.decorator.js","sourceRoot":"","sources":["../../src/decorators/outbox-events-handler.decorator.ts"],"names":[],"mappings":";;;AA+FA,kDAeC;AAmCD,wEAKC;AAtJD,4BAA0B;AAI1B;;;;;;;;;GASG;AACU,QAAA,8BAA8B,GAAG,MAAM,CAAC,gCAAgC,CAAC,CAAC;AAiFvF,SAAgB,mBAAmB,CACjC,GAAG,IAA2C;IAE9C,MAAM,QAAQ,GAAgC,eAAe,CAAC,IAAI,CAAC,CAAC;IAEpE,IAAI,QAAQ,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CACb,yDAAyD;YACvD,uEAAuE,CAC1E,CAAC;IACJ,CAAC;IAED,OAAO,CAAC,MAAc,EAAQ,EAAE;QAC9B,OAAO,CAAC,cAAc,CAAC,sCAA8B,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC3E,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CACtB,IAA2C;IAE3C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAClD,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACxB,OAAO;YACL,UAAU,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;YAC/B,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,cAAc,EAAE,OAAO,CAAC,cAAc,IAAI,IAAI;SAC/C,CAAC;IACJ,CAAC;IAED,OAAO;QACL,UAAU,EAAE,IAAc;QAC1B,cAAc,EAAE,IAAI;KACrB,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,SAAkB;IACzC,OAAO,CACL,SAAS,KAAK,IAAI;QAClB,OAAO,SAAS,KAAK,QAAQ;QAC7B,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC;QACzB,OAAO,SAAS,KAAK,UAAU;QAC/B,QAAQ,IAAI,SAAS,CACtB,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAgB,8BAA8B,CAC5C,MAAc;IAEd,MAAM,KAAK,GAAY,OAAO,CAAC,WAAW,CAAC,sCAA8B,EAAE,MAAM,CAAC,CAAC;IACnF,OAAO,KAAgD,CAAC;AAC1D,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { EventPublicationRegistry } from '../registry/event-publication-registry';
|
|
2
|
+
import type { OutboxListenerRegistry } from '../registry/listener-registry';
|
|
3
|
+
/**
|
|
4
|
+
* Per-dataSource publisher (Phase 14.3). Owns the per-transaction
|
|
5
|
+
* buffer, lifecycle hook registration, and delegate calls into the
|
|
6
|
+
* `EventPublicationRegistry` bound to a single dataSource.
|
|
7
|
+
*
|
|
8
|
+
* Not user-facing — application code injects the smart facade
|
|
9
|
+
* {@link OutboxEventPublisher}, which routes events to the
|
|
10
|
+
* `DataSourceOutboxPublisher` whose dataSource owns the event class.
|
|
11
|
+
* Exported nonetheless because:
|
|
12
|
+
* - tests may want to drive a per-DS publisher directly
|
|
13
|
+
* - advanced consumers (e.g. cross-DS bridges) may need the
|
|
14
|
+
* dataSource-bound surface
|
|
15
|
+
*
|
|
16
|
+
* Single-DS deployments still resolve `OutboxEventPublisher` from DI
|
|
17
|
+
* — the facade transparently delegates to the only registered
|
|
18
|
+
* `DataSourceOutboxPublisher`.
|
|
19
|
+
*/
|
|
20
|
+
export declare class DataSourceOutboxPublisher {
|
|
21
|
+
readonly dataSource: string;
|
|
22
|
+
private readonly registry;
|
|
23
|
+
private readonly listenerRegistry;
|
|
24
|
+
private readonly logger;
|
|
25
|
+
private readonly pending;
|
|
26
|
+
/**
|
|
27
|
+
* @param dataSource Public dataSource name this publisher binds to.
|
|
28
|
+
* Used as the lookup key for the active transaction (DD-023) and
|
|
29
|
+
* surfaced in error messages.
|
|
30
|
+
* @param registry Per-DS {@link EventPublicationRegistry}.
|
|
31
|
+
* @param listenerRegistry Per-DS {@link OutboxListenerRegistry}.
|
|
32
|
+
*/
|
|
33
|
+
constructor(dataSource: string, registry: EventPublicationRegistry, listenerRegistry: OutboxListenerRegistry);
|
|
34
|
+
/**
|
|
35
|
+
* Publish a single event into this dataSource's outbox. Must be
|
|
36
|
+
* called inside an active transaction *for this dataSource*. Throws
|
|
37
|
+
* {@link IllegalTransactionStateError} otherwise — the publication
|
|
38
|
+
* row must commit atomically with the business write, and the
|
|
39
|
+
* specific dataSource's transaction is the only context that can
|
|
40
|
+
* provide that atomicity.
|
|
41
|
+
*
|
|
42
|
+
* Creates one publication entry per listener registered for the
|
|
43
|
+
* event type. Zero listeners is a silent no-op (but the
|
|
44
|
+
* active-transaction check still applies).
|
|
45
|
+
*/
|
|
46
|
+
publish(event: unknown): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Publish a batch of events through this dataSource — same semantics
|
|
49
|
+
* and same active-transaction check applied per event.
|
|
50
|
+
*/
|
|
51
|
+
publishAll(events: readonly unknown[]): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Synchronous scheduling for sync callers (e.g. `@nestjs/cqrs`'s
|
|
54
|
+
* `AggregateRoot.commit()`). Inside a transaction *for this
|
|
55
|
+
* dataSource*: events are buffered and flushed in a `beforeCommit`
|
|
56
|
+
* hook. Outside any transaction (or no transaction for this DS):
|
|
57
|
+
* fire-and-forget {@link publish}, errors logged.
|
|
58
|
+
*
|
|
59
|
+
* The hook is attached directly to this dataSource's `ActiveTransaction.beforeCommitHooks`
|
|
60
|
+
* — `transactionManager.registerBeforeCommit` is NOT used because it
|
|
61
|
+
* always targets "the first active transaction on the context",
|
|
62
|
+
* which is the wrong target when multiple dataSources have live
|
|
63
|
+
* transactions in the same async stack (Phase 14.2 cross-DS
|
|
64
|
+
* simultaneous scenario).
|
|
65
|
+
*/
|
|
66
|
+
scheduleForPublication(event: unknown): void;
|
|
67
|
+
private ensureActiveTransaction;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=data-source-outbox-publisher.d.ts.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DataSourceOutboxPublisher = void 0;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
const core_1 = require("@nestjs-transactional/core");
|
|
6
|
+
/**
|
|
7
|
+
* Per-dataSource publisher (Phase 14.3). Owns the per-transaction
|
|
8
|
+
* buffer, lifecycle hook registration, and delegate calls into the
|
|
9
|
+
* `EventPublicationRegistry` bound to a single dataSource.
|
|
10
|
+
*
|
|
11
|
+
* Not user-facing — application code injects the smart facade
|
|
12
|
+
* {@link OutboxEventPublisher}, which routes events to the
|
|
13
|
+
* `DataSourceOutboxPublisher` whose dataSource owns the event class.
|
|
14
|
+
* Exported nonetheless because:
|
|
15
|
+
* - tests may want to drive a per-DS publisher directly
|
|
16
|
+
* - advanced consumers (e.g. cross-DS bridges) may need the
|
|
17
|
+
* dataSource-bound surface
|
|
18
|
+
*
|
|
19
|
+
* Single-DS deployments still resolve `OutboxEventPublisher` from DI
|
|
20
|
+
* — the facade transparently delegates to the only registered
|
|
21
|
+
* `DataSourceOutboxPublisher`.
|
|
22
|
+
*/
|
|
23
|
+
class DataSourceOutboxPublisher {
|
|
24
|
+
dataSource;
|
|
25
|
+
registry;
|
|
26
|
+
listenerRegistry;
|
|
27
|
+
logger;
|
|
28
|
+
// Per-transaction buffer of events awaiting flush. Keyed by the
|
|
29
|
+
// ActiveTransaction object itself so the entry is GC'd with the
|
|
30
|
+
// transaction — no cleanup code to maintain, and no risk of
|
|
31
|
+
// cross-transaction leakage.
|
|
32
|
+
pending = new WeakMap();
|
|
33
|
+
/**
|
|
34
|
+
* @param dataSource Public dataSource name this publisher binds to.
|
|
35
|
+
* Used as the lookup key for the active transaction (DD-023) and
|
|
36
|
+
* surfaced in error messages.
|
|
37
|
+
* @param registry Per-DS {@link EventPublicationRegistry}.
|
|
38
|
+
* @param listenerRegistry Per-DS {@link OutboxListenerRegistry}.
|
|
39
|
+
*/
|
|
40
|
+
constructor(dataSource, registry, listenerRegistry) {
|
|
41
|
+
this.dataSource = dataSource;
|
|
42
|
+
this.registry = registry;
|
|
43
|
+
this.listenerRegistry = listenerRegistry;
|
|
44
|
+
this.logger = new common_1.Logger(`DataSourceOutboxPublisher[${dataSource}]`);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Publish a single event into this dataSource's outbox. Must be
|
|
48
|
+
* called inside an active transaction *for this dataSource*. Throws
|
|
49
|
+
* {@link IllegalTransactionStateError} otherwise — the publication
|
|
50
|
+
* row must commit atomically with the business write, and the
|
|
51
|
+
* specific dataSource's transaction is the only context that can
|
|
52
|
+
* provide that atomicity.
|
|
53
|
+
*
|
|
54
|
+
* Creates one publication entry per listener registered for the
|
|
55
|
+
* event type. Zero listeners is a silent no-op (but the
|
|
56
|
+
* active-transaction check still applies).
|
|
57
|
+
*/
|
|
58
|
+
async publish(event) {
|
|
59
|
+
this.ensureActiveTransaction();
|
|
60
|
+
const eventType = event.constructor.name;
|
|
61
|
+
const listeners = this.listenerRegistry.getByEventType(eventType);
|
|
62
|
+
if (listeners.length === 0) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const listenerIds = listeners.map((l) => l.id);
|
|
66
|
+
await this.registry.publish(event, listenerIds);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Publish a batch of events through this dataSource — same semantics
|
|
70
|
+
* and same active-transaction check applied per event.
|
|
71
|
+
*/
|
|
72
|
+
async publishAll(events) {
|
|
73
|
+
for (const event of events) {
|
|
74
|
+
await this.publish(event);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Synchronous scheduling for sync callers (e.g. `@nestjs/cqrs`'s
|
|
79
|
+
* `AggregateRoot.commit()`). Inside a transaction *for this
|
|
80
|
+
* dataSource*: events are buffered and flushed in a `beforeCommit`
|
|
81
|
+
* hook. Outside any transaction (or no transaction for this DS):
|
|
82
|
+
* fire-and-forget {@link publish}, errors logged.
|
|
83
|
+
*
|
|
84
|
+
* The hook is attached directly to this dataSource's `ActiveTransaction.beforeCommitHooks`
|
|
85
|
+
* — `transactionManager.registerBeforeCommit` is NOT used because it
|
|
86
|
+
* always targets "the first active transaction on the context",
|
|
87
|
+
* which is the wrong target when multiple dataSources have live
|
|
88
|
+
* transactions in the same async stack (Phase 14.2 cross-DS
|
|
89
|
+
* simultaneous scenario).
|
|
90
|
+
*/
|
|
91
|
+
scheduleForPublication(event) {
|
|
92
|
+
const tx = core_1.TransactionContext.getActiveTransactionByDataSource(this.dataSource);
|
|
93
|
+
if (tx === undefined) {
|
|
94
|
+
void this.publish(event).catch((err) => {
|
|
95
|
+
this.logger.error(`scheduleForPublication outside an active '${this.dataSource}' transaction failed: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err.stack : undefined);
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
let buffer = this.pending.get(tx);
|
|
100
|
+
if (buffer === undefined) {
|
|
101
|
+
buffer = [];
|
|
102
|
+
this.pending.set(tx, buffer);
|
|
103
|
+
// Push the hook onto THIS specific transaction's hook list —
|
|
104
|
+
// not via transactionManager.registerBeforeCommit which targets
|
|
105
|
+
// the first-active transaction (wrong with multi-DS).
|
|
106
|
+
tx.beforeCommitHooks.push(async () => {
|
|
107
|
+
const toFlush = this.pending.get(tx);
|
|
108
|
+
this.pending.delete(tx);
|
|
109
|
+
if (toFlush !== undefined && toFlush.length > 0) {
|
|
110
|
+
await this.publishAll(toFlush);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
buffer.push(event);
|
|
115
|
+
}
|
|
116
|
+
ensureActiveTransaction() {
|
|
117
|
+
const tx = core_1.TransactionContext.getActiveTransactionByDataSource(this.dataSource);
|
|
118
|
+
if (tx === undefined) {
|
|
119
|
+
throw new core_1.IllegalTransactionStateError(`OutboxEventPublisher.publish for dataSource '${this.dataSource}' must be ` +
|
|
120
|
+
`called inside an active transaction for that dataSource. Wrap the call ` +
|
|
121
|
+
`in @Transactional({ dataSource: '${this.dataSource}' }) so the publication ` +
|
|
122
|
+
`row commits atomically with the business write.`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
exports.DataSourceOutboxPublisher = DataSourceOutboxPublisher;
|
|
127
|
+
//# sourceMappingURL=data-source-outbox-publisher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-source-outbox-publisher.js","sourceRoot":"","sources":["../../src/dispatcher/data-source-outbox-publisher.ts"],"names":[],"mappings":"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"}
|