@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
@@ -0,0 +1,146 @@
1
+ import { Injectable, Logger } from '@nestjs/common';
2
+ import { IntegrationSettingsService } from './integration-settings.service';
3
+ import { OutboxNotifier } from './outbox.notifier';
4
+ import { OutboxProcessor } from './outbox.processor';
5
+
6
+ /**
7
+ * Hybrid polling coordinator
8
+ * Manages the adaptive polling strategy: immediate reactions + periodic checks
9
+ * Uses in-memory notifications for immediate wakeup + configurable polling intervals
10
+ */
11
+ @Injectable()
12
+ export class OutboxPollingCoordinator {
13
+ private readonly logger = new Logger(OutboxPollingCoordinator.name);
14
+ private processingLoopRunning = false;
15
+ private processingLoopPromise: Promise<void> | null = null;
16
+ private immediateWakeupTriggered = false;
17
+ private shouldStopLoop = false;
18
+
19
+ constructor(
20
+ private readonly processor: OutboxProcessor,
21
+ private readonly notifier: OutboxNotifier,
22
+ private readonly integrationSettingsService: IntegrationSettingsService,
23
+ ) {}
24
+
25
+ /**
26
+ * Start the hybrid processing loop
27
+ * Reacts to notifications immediately, polls periodically
28
+ */
29
+ async startProcessingLoop(): Promise<void> {
30
+ if (this.processingLoopRunning) {
31
+ this.logger.warn('Processing loop already running');
32
+ return;
33
+ }
34
+
35
+ this.processingLoopRunning = true;
36
+ this.shouldStopLoop = false;
37
+ this.logger.log('Starting outbox hybrid processing loop');
38
+
39
+ this.processingLoopPromise = this.runProcessingLoop();
40
+ }
41
+
42
+ /**
43
+ * Stop the processing loop gracefully
44
+ */
45
+ async stopProcessingLoop(): Promise<void> {
46
+ if (!this.processingLoopRunning) {
47
+ return;
48
+ }
49
+
50
+ this.logger.log('Stopping outbox processing loop');
51
+ this.shouldStopLoop = true;
52
+
53
+ if (this.processingLoopPromise) {
54
+ await this.processingLoopPromise;
55
+ }
56
+
57
+ this.processingLoopRunning = false;
58
+ }
59
+
60
+ /**
61
+ * Trigger immediate processing from notifier
62
+ */
63
+ triggerImmediateProcessing(): void {
64
+ this.immediateWakeupTriggered = true;
65
+ }
66
+
67
+ /**
68
+ * Check if loop is currently running
69
+ */
70
+ isRunning(): boolean {
71
+ return this.processingLoopRunning;
72
+ }
73
+
74
+ /**
75
+ * Main processing loop - runs continuously
76
+ */
77
+ private async runProcessingLoop(): Promise<void> {
78
+ // Subscribe to notifications
79
+ const immediateWakeupListener = () => {
80
+ this.triggerImmediateProcessing();
81
+ };
82
+ this.notifier.subscribe(immediateWakeupListener);
83
+
84
+ try {
85
+ while (!this.shouldStopLoop) {
86
+ const settings = await this.integrationSettingsService.getRuntimeSettings();
87
+ const isEnabled = settings.outboxEnabled;
88
+ const processorEnabled = settings.outboxProcessorEnabled;
89
+
90
+ if (!isEnabled || !processorEnabled) {
91
+ // If disabled, wait before checking again
92
+ await this.delay(settings.idlePollingIntervalMs);
93
+ continue;
94
+ }
95
+
96
+ // Check if we should process immediately (woken by notification)
97
+ const shouldProcessImmediately = this.immediateWakeupTriggered;
98
+ this.immediateWakeupTriggered = false;
99
+
100
+ // Process batch
101
+ const processedCount = await this.processor.processBatch();
102
+
103
+ // Determine next polling interval
104
+ let nextDelayMs: number;
105
+
106
+ if (shouldProcessImmediately && processedCount > 0) {
107
+ // We processed items from immediate wakeup - use active polling interval
108
+ nextDelayMs = settings.pollingIntervalMs;
109
+ this.logger.debug(
110
+ `Processed ${processedCount} events from immediate wakeup, next poll in ${nextDelayMs}ms`,
111
+ );
112
+ } else if (processedCount > 0) {
113
+ // Processed items from periodic polling - use active interval
114
+ nextDelayMs = settings.pollingIntervalMs;
115
+ this.logger.debug(
116
+ `Processed ${processedCount} events, next poll in ${nextDelayMs}ms`,
117
+ );
118
+ } else {
119
+ // No items processed - use idle interval
120
+ nextDelayMs = settings.idlePollingIntervalMs;
121
+ this.logger.debug(
122
+ `No events processed, idle polling for ${nextDelayMs}ms`,
123
+ );
124
+ }
125
+
126
+ // Wait for next poll, but be ready for immediate wakeup
127
+ await this.delay(nextDelayMs);
128
+ }
129
+ } finally {
130
+ // Cleanup: unsubscribe from notifications
131
+ this.notifier.unsubscribe(immediateWakeupListener);
132
+ this.logger.log('Processing loop stopped');
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Utility: delay with cancellation support
138
+ */
139
+ private delay(ms: number): Promise<void> {
140
+ return new Promise((resolve) => {
141
+ const timeout = setTimeout(resolve, ms);
142
+ // Store timeout for potential cancellation (not used yet, but available)
143
+ (this as any)._currentTimeout = timeout;
144
+ });
145
+ }
146
+ }
@@ -0,0 +1,48 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { EventEmitter } from 'events';
3
+
4
+ /**
5
+ * In-memory notifier for signaling new outbox events
6
+ * Provides immediate wakeup to the processor without database polling
7
+ * NOT the source of truth; database outbox is the authoritative queue
8
+ */
9
+ @Injectable()
10
+ export class OutboxNotifier {
11
+ private readonly emitter = new EventEmitter();
12
+ private readonly EVENT_WRITTEN = 'outbox:event:written';
13
+
14
+ /**
15
+ * Subscribe to outbox event notifications
16
+ */
17
+ subscribe(listener: () => void): void {
18
+ this.emitter.on(this.EVENT_WRITTEN, listener);
19
+ }
20
+
21
+ /**
22
+ * Unsubscribe from outbox event notifications
23
+ */
24
+ unsubscribe(listener: () => void): void {
25
+ this.emitter.removeListener(this.EVENT_WRITTEN, listener);
26
+ }
27
+
28
+ /**
29
+ * Emit notification that a new event was written to outbox
30
+ */
31
+ notifyEventWritten(): void {
32
+ this.emitter.emit(this.EVENT_WRITTEN);
33
+ }
34
+
35
+ /**
36
+ * Get current number of subscribers
37
+ */
38
+ getSubscriberCount(): number {
39
+ return this.emitter.listenerCount(this.EVENT_WRITTEN);
40
+ }
41
+
42
+ /**
43
+ * Remove all listeners
44
+ */
45
+ removeAllListeners(): void {
46
+ this.emitter.removeAllListeners(this.EVENT_WRITTEN);
47
+ }
48
+ }
@@ -0,0 +1,97 @@
1
+ import {
2
+ Injectable,
3
+ Logger,
4
+ OnModuleDestroy,
5
+ OnModuleInit,
6
+ } from '@nestjs/common';
7
+ import { IntegrationSettingsService } from './integration-settings.service';
8
+ import { OutboxPollingCoordinator } from './outbox-polling.coordinator';
9
+ import { OutboxNotifier } from './outbox.notifier';
10
+ import { OutboxProcessor } from './outbox.processor';
11
+
12
+ /**
13
+ * Hybrid background job for outbox processing
14
+ * Combines startup recovery, immediate notification reactions, and periodic polling
15
+ */
16
+ @Injectable()
17
+ export class OutboxProcessorJob implements OnModuleInit, OnModuleDestroy {
18
+ private readonly logger = new Logger(OutboxProcessorJob.name);
19
+
20
+ constructor(
21
+ private readonly processor: OutboxProcessor,
22
+ private readonly notifier: OutboxNotifier,
23
+ private readonly coordinator: OutboxPollingCoordinator,
24
+ private readonly integrationSettingsService: IntegrationSettingsService,
25
+ ) {}
26
+
27
+ /**
28
+ * Called when module initializes
29
+ * 1. Drain pending events from startup
30
+ * 2. Start hybrid processing loop
31
+ */
32
+ async onModuleInit(): Promise<void> {
33
+ const settings = await this.integrationSettingsService.getRuntimeSettings();
34
+ const isEnabled = settings.outboxEnabled;
35
+ if (!isEnabled) {
36
+ this.logger.debug('Outbox integration disabled in settings');
37
+ return;
38
+ }
39
+
40
+ // Step 1: Startup drain
41
+ const startupDrainEnabled = settings.startupDrainEnabled;
42
+
43
+ if (startupDrainEnabled) {
44
+ this.logger.log('Starting outbox startup drain...');
45
+ try {
46
+ const processed = await this.processor.startupDrain();
47
+ this.logger.log(
48
+ `Outbox startup drain completed: ${processed} events`,
49
+ );
50
+ } catch (error) {
51
+ this.logger.error('Outbox startup drain failed:', error);
52
+ }
53
+ }
54
+
55
+ // Step 2: Start hybrid processing loop
56
+ const processorEnabled = settings.outboxProcessorEnabled;
57
+
58
+ if (processorEnabled) {
59
+ this.logger.log('Starting outbox hybrid processing loop');
60
+ try {
61
+ await this.coordinator.startProcessingLoop();
62
+ } catch (error) {
63
+ this.logger.error('Failed to start outbox processing loop:', error);
64
+ }
65
+ } else {
66
+ this.logger.debug('Outbox processor disabled in settings');
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Called when module is destroyed
72
+ * Gracefully stop the processing loop
73
+ */
74
+ async onModuleDestroy(): Promise<void> {
75
+ this.logger.log('Shutting down outbox processing');
76
+ try {
77
+ await this.coordinator.stopProcessingLoop();
78
+ } catch (error) {
79
+ this.logger.error(
80
+ 'Error stopping outbox processing loop:',
81
+ error,
82
+ );
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Get current processing statistics
88
+ * Useful for health checks and monitoring
89
+ */
90
+ async getStats() {
91
+ const stats = await this.processor.getStats();
92
+ return {
93
+ ...stats,
94
+ pollingLoopRunning: this.coordinator.isRunning(),
95
+ };
96
+ }
97
+ }
@@ -0,0 +1,266 @@
1
+ import { Injectable, Logger } from '@nestjs/common';
2
+ import { InboxEventStatus, OutboxEventStatus } from '../types';
3
+ import { EventSubscriberRegistry } from './event-subscriber.registry';
4
+ import { InboxService } from './inbox.service';
5
+ import { IntegrationLinkService } from './integration-link.service';
6
+ import { IntegrationSettingsService } from './integration-settings.service';
7
+ import { OutboxService } from './outbox.service';
8
+
9
+ @Injectable()
10
+ export class OutboxProcessor {
11
+ private readonly logger = new Logger(OutboxProcessor.name);
12
+
13
+ constructor(
14
+ private readonly outboxService: OutboxService,
15
+ private readonly inboxService: InboxService,
16
+ private readonly linkService: IntegrationLinkService,
17
+ private readonly registry: EventSubscriberRegistry,
18
+ private readonly integrationSettingsService: IntegrationSettingsService,
19
+ ) {}
20
+
21
+ /**
22
+ * Process a batch of pending events
23
+ * Called in a loop or by scheduled job
24
+ */
25
+ async processBatch(options?: { batchSizeOverride?: number }): Promise<number> {
26
+ const settings = await this.integrationSettingsService.getRuntimeSettings();
27
+ const batchSize = options?.batchSizeOverride ?? settings.batchSize;
28
+ const leaseMs = settings.processingLeaseMs;
29
+
30
+ // Find pending events
31
+ const events = await this.outboxService.findPending(batchSize, leaseMs);
32
+
33
+ if (events.length === 0) {
34
+ return 0;
35
+ }
36
+
37
+ this.logger.debug(`Processing ${events.length} outbox events`);
38
+
39
+ let processedCount = 0;
40
+
41
+ for (const event of events) {
42
+ try {
43
+ // Mark as processing with lease
44
+ await this.outboxService.markProcessing(event.id, leaseMs);
45
+
46
+ // Get handlers for this event
47
+ const handlers = this.registry.getHandlers(event.eventName);
48
+
49
+ if (handlers.length === 0) {
50
+ this.logger.warn(
51
+ `No handlers registered for event: ${event.eventName}`,
52
+ );
53
+ // Mark as processed even though no handlers
54
+ await this.outboxService.updateStatus(
55
+ event.id,
56
+ OutboxEventStatus.PROCESSED,
57
+ {
58
+ processedAt: new Date(),
59
+ },
60
+ );
61
+ processedCount++;
62
+ continue;
63
+ }
64
+
65
+ // Execute each handler
66
+ let anyFailed = false;
67
+ for (const handlerDef of handlers) {
68
+ const inboxEvent = await this.inboxService.getOrCreate(
69
+ event.id,
70
+ handlerDef.consumerName,
71
+ );
72
+
73
+ // Skip if already processed
74
+ if (inboxEvent.status === InboxEventStatus.PROCESSED) {
75
+ this.logger.debug(
76
+ `Event ${event.id} already processed by ${handlerDef.consumerName}`,
77
+ );
78
+ continue;
79
+ }
80
+
81
+ await this.inboxService.markProcessing(inboxEvent.id);
82
+
83
+ try {
84
+ // Execute handler
85
+ await handlerDef.handler(
86
+ {
87
+ eventName: event.eventName,
88
+ sourceModule: event.sourceModule,
89
+ aggregateType: event.aggregateType,
90
+ aggregateId: event.aggregateId,
91
+ payload: event.payload,
92
+ timestamp: event.createdAt,
93
+ },
94
+ {
95
+ logger: this.logger,
96
+ inboxService: this.inboxService,
97
+ linkService: this.linkService,
98
+ },
99
+ );
100
+
101
+ // Mark as processed
102
+ await this.inboxService.markProcessed(inboxEvent.id);
103
+ this.logger.debug(
104
+ `Handler ${handlerDef.consumerName} processed event ${event.id}`,
105
+ );
106
+ } catch (error) {
107
+ anyFailed = true;
108
+ this.logger.error(
109
+ `Handler ${handlerDef.consumerName} failed for event ${event.id}:`,
110
+ error,
111
+ );
112
+ await this.inboxService.markFailed(
113
+ inboxEvent.id,
114
+ error instanceof Error ? error.message : String(error),
115
+ );
116
+ }
117
+ }
118
+
119
+ // Update outbox event status based on handler results
120
+ if (!anyFailed) {
121
+ await this.outboxService.updateStatus(
122
+ event.id,
123
+ OutboxEventStatus.PROCESSED,
124
+ {
125
+ processedAt: new Date(),
126
+ },
127
+ );
128
+ processedCount++;
129
+ } else {
130
+ // At least one handler failed, retry logic applies
131
+ await this.handleEventFailure(event.id);
132
+ }
133
+ } catch (error) {
134
+ this.logger.error(`Error processing event ${event.id}:`, error);
135
+ await this.handleEventFailure(event.id);
136
+ }
137
+ }
138
+
139
+ return processedCount;
140
+ }
141
+
142
+ /**
143
+ * Handle event that failed to process
144
+ */
145
+ private async handleEventFailure(eventId: string): Promise<void> {
146
+ const settings = await this.integrationSettingsService.getRuntimeSettings();
147
+ const maxAttempts = settings.maxAttempts;
148
+ const retryBaseDelayMs = settings.retryBaseDelayMs;
149
+ const deadLetterEnabled = settings.deadLetterEnabled;
150
+
151
+ const event = await this.outboxService.getById(eventId);
152
+ if (!event) {
153
+ return;
154
+ }
155
+
156
+ const nextAttemptCount = event.attemptCount + 1;
157
+
158
+ if (nextAttemptCount >= maxAttempts) {
159
+ if (deadLetterEnabled) {
160
+ this.logger.warn(
161
+ `Event ${eventId} moved to dead letter after ${maxAttempts} attempts`,
162
+ );
163
+ await this.outboxService.updateStatus(
164
+ eventId,
165
+ OutboxEventStatus.DEAD_LETTER,
166
+ {
167
+ attemptCount: nextAttemptCount,
168
+ },
169
+ );
170
+ } else {
171
+ this.logger.warn(
172
+ `Event ${eventId} failed after ${maxAttempts} attempts (dead letter disabled)`,
173
+ );
174
+ await this.outboxService.updateStatus(
175
+ eventId,
176
+ OutboxEventStatus.FAILED,
177
+ {
178
+ attemptCount: nextAttemptCount,
179
+ },
180
+ );
181
+ }
182
+ } else {
183
+ // Calculate exponential backoff
184
+ const delayMs = retryBaseDelayMs * Math.pow(2, event.attemptCount);
185
+ const availableAt = new Date(Date.now() + delayMs);
186
+
187
+ this.logger.debug(
188
+ `Retrying event ${eventId} in ${delayMs}ms (attempt ${nextAttemptCount}/${maxAttempts})`,
189
+ );
190
+
191
+ await this.outboxService.updateStatus(
192
+ eventId,
193
+ OutboxEventStatus.PENDING,
194
+ {
195
+ attemptCount: nextAttemptCount,
196
+ availableAt,
197
+ },
198
+ );
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Process all pending events at startup (drain)
204
+ */
205
+ async startupDrain(): Promise<number> {
206
+ const settings = await this.integrationSettingsService.getRuntimeSettings();
207
+ const drainBatchSize = settings.startupDrainBatchSize;
208
+
209
+ this.logger.log(`Starting outbox drain with batch size: ${drainBatchSize}`);
210
+
211
+ let totalProcessed = 0;
212
+
213
+ // Keep processing until no more events
214
+ let processed = 1;
215
+ while (processed > 0) {
216
+ processed = await this.processBatch({
217
+ batchSizeOverride: drainBatchSize,
218
+ });
219
+ totalProcessed += processed;
220
+
221
+ // Small delay to prevent overwhelming the system
222
+ if (processed > 0) {
223
+ await new Promise((resolve) => setTimeout(resolve, 100));
224
+ }
225
+ }
226
+
227
+ this.logger.log(`Startup drain completed. Processed ${totalProcessed} events`);
228
+ return totalProcessed;
229
+ }
230
+
231
+ /**
232
+ * Get processor statistics
233
+ */
234
+ async getStats() {
235
+ const pendingCount = await this.outboxService.countByStatus(
236
+ OutboxEventStatus.PENDING,
237
+ );
238
+ const processingCount = await this.outboxService.countByStatus(
239
+ OutboxEventStatus.PROCESSING,
240
+ );
241
+ const processedCount = await this.outboxService.countByStatus(
242
+ OutboxEventStatus.PROCESSED,
243
+ );
244
+ const failedCount = await this.outboxService.countByStatus(
245
+ OutboxEventStatus.FAILED,
246
+ );
247
+ const deadLetterCount = await this.outboxService.countByStatus(
248
+ OutboxEventStatus.DEAD_LETTER,
249
+ );
250
+
251
+ return {
252
+ pending: pendingCount,
253
+ processing: processingCount,
254
+ processed: processedCount,
255
+ failed: failedCount,
256
+ dead_letter: deadLetterCount,
257
+ total:
258
+ pendingCount +
259
+ processingCount +
260
+ processedCount +
261
+ failedCount +
262
+ deadLetterCount,
263
+ handlerCount: this.registry.getHandlerCount(),
264
+ };
265
+ }
266
+ }