@hed-hog/core 0.0.295 → 0.0.296

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/dist/auth/auth.controller.d.ts +4 -4
  2. package/dist/auth/auth.service.d.ts +4 -4
  3. package/dist/challenge/challenge.service.d.ts +2 -2
  4. package/dist/core.module.d.ts.map +1 -1
  5. package/dist/core.module.js +4 -1
  6. package/dist/core.module.js.map +1 -1
  7. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +1 -1
  8. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +2 -2
  9. package/dist/index.d.ts +13 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +14 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/integration/index.d.ts +4 -0
  14. package/dist/integration/index.d.ts.map +1 -0
  15. package/dist/integration/index.js +20 -0
  16. package/dist/integration/index.js.map +1 -0
  17. package/dist/integration/integration-api.validation.d.ts +2 -0
  18. package/dist/integration/integration-api.validation.d.ts.map +1 -0
  19. package/dist/integration/integration-api.validation.js +126 -0
  20. package/dist/integration/integration-api.validation.js.map +1 -0
  21. package/dist/integration/integration.module.d.ts +3 -0
  22. package/dist/integration/integration.module.d.ts.map +1 -0
  23. package/dist/integration/integration.module.js +54 -0
  24. package/dist/integration/integration.module.js.map +1 -0
  25. package/dist/integration/services/domain-event.publisher.d.ts +31 -0
  26. package/dist/integration/services/domain-event.publisher.d.ts.map +1 -0
  27. package/dist/integration/services/domain-event.publisher.js +79 -0
  28. package/dist/integration/services/domain-event.publisher.js.map +1 -0
  29. package/dist/integration/services/event-subscriber.registry.d.ts +37 -0
  30. package/dist/integration/services/event-subscriber.registry.d.ts.map +1 -0
  31. package/dist/integration/services/event-subscriber.registry.js +86 -0
  32. package/dist/integration/services/event-subscriber.registry.js.map +1 -0
  33. package/dist/integration/services/inbox.service.d.ts +55 -0
  34. package/dist/integration/services/inbox.service.d.ts.map +1 -0
  35. package/dist/integration/services/inbox.service.js +173 -0
  36. package/dist/integration/services/inbox.service.js.map +1 -0
  37. package/dist/integration/services/index.d.ts +12 -0
  38. package/dist/integration/services/index.d.ts.map +1 -0
  39. package/dist/integration/services/index.js +28 -0
  40. package/dist/integration/services/index.js.map +1 -0
  41. package/dist/integration/services/integration-developer-api.service.d.ts +30 -0
  42. package/dist/integration/services/integration-developer-api.service.d.ts.map +1 -0
  43. package/dist/integration/services/integration-developer-api.service.js +55 -0
  44. package/dist/integration/services/integration-developer-api.service.js.map +1 -0
  45. package/dist/integration/services/integration-link.service.d.ts +52 -0
  46. package/dist/integration/services/integration-link.service.d.ts.map +1 -0
  47. package/dist/integration/services/integration-link.service.js +128 -0
  48. package/dist/integration/services/integration-link.service.js.map +1 -0
  49. package/dist/integration/services/integration-settings.service.d.ts +23 -0
  50. package/dist/integration/services/integration-settings.service.d.ts.map +1 -0
  51. package/dist/integration/services/integration-settings.service.js +81 -0
  52. package/dist/integration/services/integration-settings.service.js.map +1 -0
  53. package/dist/integration/services/outbox-polling.coordinator.d.ts +45 -0
  54. package/dist/integration/services/outbox-polling.coordinator.d.ts.map +1 -0
  55. package/dist/integration/services/outbox-polling.coordinator.js +143 -0
  56. package/dist/integration/services/outbox-polling.coordinator.js.map +1 -0
  57. package/dist/integration/services/outbox.notifier.d.ts +30 -0
  58. package/dist/integration/services/outbox.notifier.d.ts.map +1 -0
  59. package/dist/integration/services/outbox.notifier.js +57 -0
  60. package/dist/integration/services/outbox.notifier.js.map +1 -0
  61. package/dist/integration/services/outbox.processor.d.ts +42 -0
  62. package/dist/integration/services/outbox.processor.d.ts.map +1 -0
  63. package/dist/integration/services/outbox.processor.job.d.ts +43 -0
  64. package/dist/integration/services/outbox.processor.job.d.ts.map +1 -0
  65. package/dist/integration/services/outbox.processor.job.js +100 -0
  66. package/dist/integration/services/outbox.processor.job.js.map +1 -0
  67. package/dist/integration/services/outbox.processor.js +208 -0
  68. package/dist/integration/services/outbox.processor.js.map +1 -0
  69. package/dist/integration/services/outbox.service.d.ts +53 -0
  70. package/dist/integration/services/outbox.service.d.ts.map +1 -0
  71. package/dist/integration/services/outbox.service.js +149 -0
  72. package/dist/integration/services/outbox.service.js.map +1 -0
  73. package/dist/integration/types/event.types.d.ts +88 -0
  74. package/dist/integration/types/event.types.d.ts.map +1 -0
  75. package/dist/integration/types/event.types.js +35 -0
  76. package/dist/integration/types/event.types.js.map +1 -0
  77. package/dist/integration/types/index.d.ts +3 -0
  78. package/dist/integration/types/index.d.ts.map +1 -0
  79. package/dist/integration/types/index.js +19 -0
  80. package/dist/integration/types/index.js.map +1 -0
  81. package/dist/integration/types/subscriber.types.d.ts +31 -0
  82. package/dist/integration/types/subscriber.types.d.ts.map +1 -0
  83. package/dist/integration/types/subscriber.types.js +3 -0
  84. package/dist/integration/types/subscriber.types.js.map +1 -0
  85. package/dist/oauth/oauth.controller.js.map +1 -1
  86. package/dist/oauth/oauth.service.d.ts +3 -3
  87. package/dist/oauth/oauth.service.d.ts.map +1 -1
  88. package/dist/oauth/oauth.service.js.map +1 -1
  89. package/dist/profile/profile.controller.d.ts +3 -3
  90. package/dist/profile/profile.service.d.ts +3 -3
  91. package/dist/setting/setting.controller.d.ts +12 -8
  92. package/dist/setting/setting.controller.d.ts.map +1 -1
  93. package/dist/setting/setting.service.d.ts +12 -8
  94. package/dist/setting/setting.service.d.ts.map +1 -1
  95. package/dist/setting/setting.service.js +21 -1
  96. package/dist/setting/setting.service.js.map +1 -1
  97. package/dist/user/user.controller.d.ts +4 -4
  98. package/dist/user/user.service.d.ts +9 -9
  99. package/hedhog/data/dashboard_component_role.yaml +223 -223
  100. package/hedhog/data/dashboard_role.yaml +18 -18
  101. package/hedhog/data/route.yaml +2 -0
  102. package/hedhog/data/setting_group.yaml +955 -470
  103. package/hedhog/data/setting_subgroup.yaml +303 -0
  104. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +44 -18
  105. package/hedhog/frontend/app/configurations/[slug]/page.tsx.ejs +134 -27
  106. package/hedhog/frontend/app/configurations/layout.tsx.ejs +84 -23
  107. package/hedhog/frontend/app/dashboard/components/widgets/permissions-chart.tsx.ejs +62 -62
  108. package/hedhog/frontend/app/dashboard/page.tsx.ejs +29 -29
  109. package/hedhog/frontend/app/preferences/page.tsx.ejs +2 -5
  110. package/hedhog/table/inbox_event.yaml +40 -0
  111. package/hedhog/table/integration_link.yaml +33 -0
  112. package/hedhog/table/outbox_event.yaml +45 -0
  113. package/hedhog/table/setting.yaml +7 -0
  114. package/hedhog/table/setting_subgroup.yaml +19 -0
  115. package/package.json +8 -8
  116. package/src/ai/ai.service.ts +3 -3
  117. package/src/auth/auth.controller.ts +11 -11
  118. package/src/auth/auth.service.ts +8 -8
  119. package/src/core.module.ts +4 -1
  120. package/src/index.ts +15 -0
  121. package/src/integration/README.md +397 -0
  122. package/src/integration/USAGE_EXAMPLE.md +279 -0
  123. package/src/integration/index.ts +4 -0
  124. package/src/integration/integration-api.validation.ts +154 -0
  125. package/src/integration/integration.module.ts +53 -0
  126. package/src/integration/services/domain-event.publisher.ts +136 -0
  127. package/src/integration/services/event-subscriber.registry.ts +89 -0
  128. package/src/integration/services/inbox.service.ts +218 -0
  129. package/src/integration/services/index.ts +12 -0
  130. package/src/integration/services/integration-developer-api.service.ts +96 -0
  131. package/src/integration/services/integration-link.service.ts +154 -0
  132. package/src/integration/services/integration-settings.service.ts +128 -0
  133. package/src/integration/services/outbox-polling.coordinator.ts +146 -0
  134. package/src/integration/services/outbox.notifier.ts +48 -0
  135. package/src/integration/services/outbox.processor.job.ts +97 -0
  136. package/src/integration/services/outbox.processor.ts +266 -0
  137. package/src/integration/services/outbox.service.ts +209 -0
  138. package/src/integration/types/event.types.ts +93 -0
  139. package/src/integration/types/index.ts +3 -0
  140. package/src/integration/types/subscriber.types.ts +37 -0
  141. package/src/oauth/oauth.controller.ts +17 -17
  142. package/src/oauth/oauth.service.ts +20 -20
  143. package/src/setting/setting.service.ts +27 -2
  144. package/src/task/task.service.ts +5 -5
@@ -13,6 +13,7 @@ import { SystemModule } from './core/system.module';
13
13
  import { DashboardModule } from './dashboard/dashboard.module';
14
14
  import { FileModule } from './file/file.module';
15
15
  import { InstallModule } from './install/install.module';
16
+ import { IntegrationModule } from './integration/integration.module';
16
17
  import { MailSentModule } from './mail-sent/mail-sent.module';
17
18
  import { MailModule } from './mail/mail.module';
18
19
  import { MenuModule } from './menu/menu.module';
@@ -60,7 +61,8 @@ import { ValidatorServiceLocator } from './validators/service-locator';
60
61
  forwardRef(() => SecurityModule),
61
62
  forwardRef(() => ProfileModule),
62
63
  forwardRef(() => SessionModule),
63
- forwardRef(() => OAuthModule)
64
+ forwardRef(() => OAuthModule),
65
+ forwardRef(() => IntegrationModule),
64
66
  ],
65
67
  exports: [
66
68
  UserModule,
@@ -75,6 +77,7 @@ import { ValidatorServiceLocator } from './validators/service-locator';
75
77
  FileModule,
76
78
  SessionModule,
77
79
  AiModule,
80
+ IntegrationModule,
78
81
  IsStrongPasswordWithSettingsConstraint,
79
82
  IsEmailWithSettingsConstraint,
80
83
  IsPinCodeWithSettingConstraint,
package/src/index.ts CHANGED
@@ -29,6 +29,21 @@ export * from './setting/setting.service';
29
29
  export * from './ai/ai.module';
30
30
  export * from './ai/ai.service';
31
31
 
32
+ // Integration Module
33
+ export * from './integration/integration.module';
34
+ export * from './integration/services/domain-event.publisher';
35
+ export * from './integration/services/event-subscriber.registry';
36
+ export * from './integration/services/inbox.service';
37
+ export * from './integration/services/integration-developer-api.service';
38
+ export * from './integration/services/integration-link.service';
39
+ export * from './integration/services/integration-settings.service';
40
+ export * from './integration/services/outbox-polling.coordinator';
41
+ export * from './integration/services/outbox.notifier';
42
+ export * from './integration/services/outbox.processor';
43
+ export * from './integration/services/outbox.processor.job';
44
+ export * from './integration/services/outbox.service';
45
+ export * from './integration/types';
46
+
32
47
  // Validators
33
48
  export * from './validators/is-email-with-settings.validator';
34
49
  export * from './validators/is-pin-code-with-setting.validator';
@@ -0,0 +1,397 @@
1
+ # Core Integration Module
2
+
3
+ A reusable, event-driven cross-module integration foundation for the HedHog platform.
4
+
5
+ ## Overview
6
+
7
+ The **integration** module provides:
8
+
9
+ - **Durable Outbox**: Database-backed event queue with automatic retry and exponential backoff
10
+ - **Idempotent Inbox**: Per-consumer tracking ensures handlers never process the same event twice
11
+ - **Entity Integration Links**: Generic mappings between entities across different modules
12
+ - **Event Subscriber Registry**: Global registry for modules to register event handlers
13
+ - **Settings-Driven Configuration**: All timeouts, retry logic, and batch sizes configurable via SettingService
14
+ - **Hybrid Processing Strategy**: Database as source of truth + in-memory notifier for immediate wakeup
15
+
16
+ ## Architecture
17
+
18
+ ```
19
+ Module A (Finance) → publishEvent() → DomainEventPublisher
20
+
21
+ OutboxService (write to DB)
22
+
23
+ OutboxNotifier (wake up processor)
24
+
25
+ OutboxProcessorJob (hybrid loop)
26
+
27
+ ┌───────────────────────────────────────────┐
28
+ ↓ ↓
29
+ Fetch pending events ExecuteHandlers
30
+ ↓ ↓
31
+ Lock with lease time Get handlers from registry
32
+ ↓ ↓
33
+ Mark as processing For each registered handler:
34
+ ↓ ↓
35
+ Track inbox per consumer 1. Get/create InboxEvent
36
+ ↓ 2. Mark as processing
37
+ Execute handler code 3. Call handler function
38
+ ↓ 4. Mark as processed (or failed)
39
+ Handle failures: 5. Create IntegrationLinks
40
+ - Retry with backoff (as side effects)
41
+ - Dead letter after max
42
+ - Update outbox status
43
+
44
+ Module B (Operations) ← Subscribes via registry, receives events, creates links
45
+ ```
46
+
47
+ ## Database Schema
48
+
49
+ ### `outbox_event` Table
50
+
51
+ Stores domain events for cross-module integration.
52
+
53
+ ```sql
54
+ CREATE TABLE outbox_event (
55
+ id UUID PRIMARY KEY,
56
+ event_name VARCHAR(255) NOT NULL,
57
+ source_module VARCHAR(127) NOT NULL,
58
+ aggregate_type VARCHAR(127) NOT NULL,
59
+ aggregate_id VARCHAR(36) NOT NULL,
60
+ payload JSONB NOT NULL,
61
+ status VARCHAR(63) NOT NULL, -- pending | processing | processed | failed | dead_letter
62
+ attempt_count INT DEFAULT 0,
63
+ last_error VARCHAR(1023),
64
+ available_at TIMESTAMP WITH TIME ZONE,
65
+ processed_at TIMESTAMP WITH TIME ZONE,
66
+ created_at TIMESTAMP WITH TIME ZONE,
67
+ updated_at TIMESTAMP WITH TIME ZONE
68
+ );
69
+
70
+ -- Indexes for efficient polling
71
+ CREATE INDEX idx_outbox_event_status_available_at
72
+ ON outbox_event(status, available_at);
73
+ CREATE INDEX idx_outbox_event_source
74
+ ON outbox_event(source_module, aggregate_type, aggregate_id);
75
+ CREATE INDEX idx_outbox_event_created_at
76
+ ON outbox_event(created_at);
77
+ ```
78
+
79
+ ### `inbox_event` Table
80
+
81
+ Per-consumer idempotency tracking to ensure exactly-once processing semantics.
82
+
83
+ ```sql
84
+ CREATE TABLE inbox_event (
85
+ id UUID PRIMARY KEY,
86
+ outbox_event_id UUID NOT NULL REFERENCES outbox_event(id) ON DELETE CASCADE,
87
+ consumer_name VARCHAR(255) NOT NULL,
88
+ status VARCHAR(63) NOT NULL, -- received | processing | processed | failed | skipped
89
+ attempt_count INT DEFAULT 0,
90
+ last_error VARCHAR(1023),
91
+ processed_at TIMESTAMP WITH TIME ZONE,
92
+ created_at TIMESTAMP WITH TIME ZONE,
93
+ updated_at TIMESTAMP WITH TIME ZONE
94
+ );
95
+
96
+ -- Unique constraint ensures idempotency
97
+ CREATE UNIQUE INDEX idx_inbox_event_idempotency
98
+ ON inbox_event(outbox_event_id, consumer_name);
99
+ CREATE INDEX idx_inbox_event_status
100
+ ON inbox_event(status);
101
+ ```
102
+
103
+ ### `integration_link` Table
104
+
105
+ Generic mappings between entities across modules (no direct foreign keys).
106
+
107
+ ```sql
108
+ CREATE TABLE integration_link (
109
+ id UUID PRIMARY KEY,
110
+ source_module VARCHAR(127) NOT NULL,
111
+ source_entity_type VARCHAR(127) NOT NULL,
112
+ source_entity_id VARCHAR(36) NOT NULL,
113
+ target_module VARCHAR(127) NOT NULL,
114
+ target_entity_type VARCHAR(127) NOT NULL,
115
+ target_entity_id VARCHAR(36) NOT NULL,
116
+ link_type VARCHAR(63) NOT NULL, -- reference | cascade | aggregate_root
117
+ metadata JSONB,
118
+ created_at TIMESTAMP WITH TIME ZONE,
119
+ updated_at TIMESTAMP WITH TIME ZONE
120
+ );
121
+
122
+ -- Efficient lookups
123
+ CREATE INDEX idx_integration_link_source
124
+ ON integration_link(source_module, source_entity_type, source_entity_id);
125
+ CREATE INDEX idx_integration_link_target
126
+ ON integration_link(target_module, target_entity_type, target_entity_id);
127
+ CREATE INDEX idx_integration_link_type
128
+ ON integration_link(link_type);
129
+ ```
130
+
131
+ ## Core Services
132
+
133
+ ### `DomainEventPublisher`
134
+
135
+ Entry point for modules to publish events.
136
+
137
+ ```typescript
138
+ @Injectable()
139
+ export class DomainEventPublisher {
140
+ async publishEvent(
141
+ eventName: string,
142
+ sourceModule: string,
143
+ aggregateType: string,
144
+ aggregateId: string,
145
+ payload?: Record<string, any>,
146
+ ): Promise<void>
147
+
148
+ async publishEvents(events: DomainEvent[]): Promise<void>
149
+ }
150
+ ```
151
+
152
+ **Usage:**
153
+ ```typescript
154
+ @Injectable()
155
+ export class InvoiceService {
156
+ constructor(
157
+ private readonly eventPublisher: DomainEventPublisher,
158
+ ) {}
159
+
160
+ async createInvoice(data: any): Promise<Invoice> {
161
+ const invoice = await this.prisma.invoice.create({ data });
162
+
163
+ await this.eventPublisher.publishEvent(
164
+ 'invoice.created',
165
+ 'finance',
166
+ 'Invoice',
167
+ invoice.id,
168
+ { amount: invoice.amount },
169
+ );
170
+
171
+ return invoice;
172
+ }
173
+ }
174
+ ```
175
+
176
+ ### `IntegrationDeveloperApiService`
177
+
178
+ Single developer-facing facade for publishing events, subscribing handlers, and managing links.
179
+
180
+ ```typescript
181
+ @Injectable()
182
+ export class OperationsBillingIntegration {
183
+ constructor(private readonly integrationApi: IntegrationDeveloperApiService) {}
184
+
185
+ async publishProjectReady(projectId: string): Promise<void> {
186
+ await this.integrationApi.publishEvent({
187
+ eventName: 'operations.project.billing_ready',
188
+ sourceModule: 'operations',
189
+ aggregateType: 'project',
190
+ aggregateId: projectId,
191
+ payload: { projectId },
192
+ metadata: { producer: 'operations' },
193
+ });
194
+ }
195
+
196
+ onModuleInit(): void {
197
+ this.integrationApi.subscribe({
198
+ eventName: 'operations.contract.payable_generated',
199
+ consumerName: 'operations-finance-handler',
200
+ priority: 10,
201
+ handler: async (event, context) => {
202
+ context.logger.log(`Received ${event.eventName} for ${event.aggregateId}`);
203
+ },
204
+ });
205
+ }
206
+ }
207
+ ```
208
+
209
+ ### `EventSubscriberRegistry`
210
+
211
+ Global registry for handler registration.
212
+
213
+ ```typescript
214
+ @Injectable()
215
+ export class EventSubscriberRegistry {
216
+ registerHandler(definition: SubscriberDefinition): void
217
+ getHandlers(eventName: string): SubscriberDefinition[]
218
+ hasHandlers(eventName: string): boolean
219
+ getEventNames(): string[]
220
+ getHandlerCount(): number
221
+ clear(): void
222
+ }
223
+ ```
224
+
225
+ **Usage:**
226
+ ```typescript
227
+ @Injectable()
228
+ export class OperationsEventHandlers implements OnModuleInit {
229
+ constructor(private readonly registry: EventSubscriberRegistry) {}
230
+
231
+ onModuleInit(): void {
232
+ this.registry.registerHandler({
233
+ eventName: 'invoice.created',
234
+ consumerName: 'operations-module',
235
+ priority: 10,
236
+ handler: async (event, context) => {
237
+ // Handle event
238
+ },
239
+ });
240
+ }
241
+ }
242
+ ```
243
+
244
+ ### `InboxService`
245
+
246
+ Tracks per-consumer processing for idempotency.
247
+
248
+ ```typescript
249
+ @Injectable()
250
+ export class InboxService {
251
+ async getOrCreate(
252
+ outboxEventId: string,
253
+ consumerName: string,
254
+ ): Promise<InboxEvent>
255
+
256
+ async markProcessing(inboxEventId: string): Promise<InboxEvent>
257
+ async markProcessed(inboxEventId: string): Promise<InboxEvent>
258
+ async markFailed(inboxEventId: string, error: string): Promise<InboxEvent>
259
+ async markSkipped(inboxEventId: string): Promise<InboxEvent>
260
+ async isProcessed(
261
+ outboxEventId: string,
262
+ consumerName: string,
263
+ ): Promise<boolean>
264
+ }
265
+ ```
266
+
267
+ ### `IntegrationLinkService`
268
+
269
+ Create and query cross-module entity relationships.
270
+
271
+ ```typescript
272
+ @Injectable()
273
+ export class IntegrationLinkService {
274
+ async createLink(dto: CreateIntegrationLinkDto): Promise<IntegrationLink>
275
+ async findOutbound(
276
+ sourceModule: string,
277
+ sourceEntityType: string,
278
+ sourceEntityId: string,
279
+ ): Promise<IntegrationLink[]>
280
+ async findInbound(
281
+ targetModule: string,
282
+ targetEntityType: string,
283
+ targetEntityId: string,
284
+ ): Promise<IntegrationLink[]>
285
+ async findByLinkType(linkType: LinkType): Promise<IntegrationLink[]>
286
+ async findBidirectional(...): Promise<IntegrationLink | null>
287
+ }
288
+ ```
289
+
290
+ The facade `IntegrationDeveloperApiService` also provides link methods:
291
+
292
+ - `createLink(dto, persistenceClient?)`
293
+ - `findLinksBySource({ module, entityType, entityId })`
294
+ - `findLinksByTarget({ module, entityType, entityId })`
295
+ - `findLinksByType(linkType)`
296
+
297
+ ### `OutboxProcessor`
298
+
299
+ Processes pending events, executes handlers, and manages retries.
300
+
301
+ ```typescript
302
+ @Injectable()
303
+ export class OutboxProcessor {
304
+ async processBatch(): Promise<number> // Process one batch
305
+ async startupDrain(): Promise<number> // Drain on startup
306
+ async getStats(): Promise<ProcessorStats>
307
+ }
308
+ ```
309
+
310
+ ## Settings Configuration
311
+
312
+ All integration settings are registered in `setting_group.yaml` under the `integration` group:
313
+
314
+ | Setting Key | Type | Default | Purpose |
315
+ |---|---|---|---|
316
+ | `core.integration.outbox.enabled` | boolean | `true` | Enable/disable outbox system |
317
+ | `core.integration.outbox.processor.enabled` | boolean | `true` | Enable/disable processor job |
318
+ | `core.integration.outbox.startupDrainEnabled` | boolean | `true` | Run drain on startup |
319
+ | `core.integration.outbox.pollingIntervalMs` | number | `5000` | Poll interval when processing |
320
+ | `core.integration.outbox.idlePollingIntervalMs` | number | `30000` | Poll interval when idle |
321
+ | `core.integration.outbox.batchSize` | number | `10` | Events per batch |
322
+ | `core.integration.outbox.maxAttempts` | number | `3` | Retry attempts |
323
+ | `core.integration.outbox.processingLeaseMs` | number | `30000` | Lock duration |
324
+ | `core.integration.outbox.retryBaseDelayMs` | number | `1000` | Base delay for backoff |
325
+ | `core.integration.outbox.deadLetterEnabled` | boolean | `true` | Move failed events to dead letter |
326
+ | `core.integration.outbox.startupDrainBatchSize` | number | `50` | Batch size for startup drain |
327
+
328
+ ## Event Handler Contract
329
+
330
+ ```typescript
331
+ export type EventHandler = (
332
+ event: DomainEvent,
333
+ context: SubscriberContext,
334
+ ) => Promise<void>
335
+
336
+ export interface DomainEvent {
337
+ eventName: string
338
+ sourceModule: string
339
+ aggregateType: string
340
+ aggregateId: string
341
+ payload: Record<string, any>
342
+ timestamp: Date
343
+ }
344
+
345
+ export interface SubscriberContext {
346
+ logger: Logger // NestJS Logger
347
+ inboxService: InboxService // Mark as processed/failed
348
+ linkService: IntegrationLinkService // Create links
349
+ }
350
+ ```
351
+
352
+ ## Failure Handling & Retry Logic
353
+
354
+ 1. Handler executes; if exception thrown → marked as failed
355
+ 2. Exponential backoff: delay = `retryBaseDelayMs` × 2^(attemptCount)
356
+ 3. After `maxAttempts`, event moves to `dead_letter` status
357
+ 4. Processing lease prevents stuck events: if not updated within `processingLeaseMs`, available for reprocessing
358
+ 5. Startup drain recovers all pending events on app restart
359
+
360
+ ## Idempotency
361
+
362
+ The inbox table with unique constraint on `(outboxEventId, consumerName)` ensures:
363
+
364
+ - Same handler never processes same event twice
365
+ - Crashes during processing don't cause duplicates
366
+ - Replay is safe (read-only handlers preferred)
367
+
368
+ **Important**: Applications must still make handlers idempotent in logic:
369
+ - Operations should be repeatable without side effects
370
+ - Or: check for existence before creating
371
+ - Or: use database-level constraints (unique, upsert)
372
+
373
+ ## Performance Considerations
374
+
375
+ - **Polling Interval**: Tune based on event volume and latency requirements
376
+ - **Batch Size**: Larger batches reduce DB queries but block longer
377
+ - **Processing Lease**: Must exceed typical handler execution time
378
+ - **Dead Letter Threshold**: Balance between retries and operational alerts
379
+ - **Indexes**: Already defined for common query patterns
380
+
381
+ ## Testing
382
+
383
+ See [USAGE_EXAMPLE.md](./USAGE_EXAMPLE.md) for examples of how to:
384
+ - Publish events from one module
385
+ - Register handlers in another module
386
+ - Query integration links
387
+ - Test idempotency
388
+
389
+ ## Future Enhancements
390
+
391
+ - Event schema registry and versioning
392
+ - Distributed tracing / correlation IDs
393
+ - Event replay / time-travel features
394
+ - Saga orchestration (choreography → orchestration)
395
+ - Webhook / external event ingestion
396
+ - Dead letter queue UI dashboard
397
+ - Metrics and observability