@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.
- package/dist/auth/auth.controller.d.ts +4 -4
- package/dist/auth/auth.service.d.ts +4 -4
- package/dist/challenge/challenge.service.d.ts +2 -2
- package/dist/core.module.d.ts.map +1 -1
- package/dist/core.module.js +4 -1
- package/dist/core.module.js.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +2 -2
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -1
- package/dist/integration/index.d.ts +4 -0
- package/dist/integration/index.d.ts.map +1 -0
- package/dist/integration/index.js +20 -0
- package/dist/integration/index.js.map +1 -0
- package/dist/integration/integration-api.validation.d.ts +2 -0
- package/dist/integration/integration-api.validation.d.ts.map +1 -0
- package/dist/integration/integration-api.validation.js +126 -0
- package/dist/integration/integration-api.validation.js.map +1 -0
- package/dist/integration/integration.module.d.ts +3 -0
- package/dist/integration/integration.module.d.ts.map +1 -0
- package/dist/integration/integration.module.js +54 -0
- package/dist/integration/integration.module.js.map +1 -0
- package/dist/integration/services/domain-event.publisher.d.ts +31 -0
- package/dist/integration/services/domain-event.publisher.d.ts.map +1 -0
- package/dist/integration/services/domain-event.publisher.js +79 -0
- package/dist/integration/services/domain-event.publisher.js.map +1 -0
- package/dist/integration/services/event-subscriber.registry.d.ts +37 -0
- package/dist/integration/services/event-subscriber.registry.d.ts.map +1 -0
- package/dist/integration/services/event-subscriber.registry.js +86 -0
- package/dist/integration/services/event-subscriber.registry.js.map +1 -0
- package/dist/integration/services/inbox.service.d.ts +55 -0
- package/dist/integration/services/inbox.service.d.ts.map +1 -0
- package/dist/integration/services/inbox.service.js +173 -0
- package/dist/integration/services/inbox.service.js.map +1 -0
- package/dist/integration/services/index.d.ts +12 -0
- package/dist/integration/services/index.d.ts.map +1 -0
- package/dist/integration/services/index.js +28 -0
- package/dist/integration/services/index.js.map +1 -0
- package/dist/integration/services/integration-developer-api.service.d.ts +30 -0
- package/dist/integration/services/integration-developer-api.service.d.ts.map +1 -0
- package/dist/integration/services/integration-developer-api.service.js +55 -0
- package/dist/integration/services/integration-developer-api.service.js.map +1 -0
- package/dist/integration/services/integration-link.service.d.ts +52 -0
- package/dist/integration/services/integration-link.service.d.ts.map +1 -0
- package/dist/integration/services/integration-link.service.js +128 -0
- package/dist/integration/services/integration-link.service.js.map +1 -0
- package/dist/integration/services/integration-settings.service.d.ts +23 -0
- package/dist/integration/services/integration-settings.service.d.ts.map +1 -0
- package/dist/integration/services/integration-settings.service.js +81 -0
- package/dist/integration/services/integration-settings.service.js.map +1 -0
- package/dist/integration/services/outbox-polling.coordinator.d.ts +45 -0
- package/dist/integration/services/outbox-polling.coordinator.d.ts.map +1 -0
- package/dist/integration/services/outbox-polling.coordinator.js +143 -0
- package/dist/integration/services/outbox-polling.coordinator.js.map +1 -0
- package/dist/integration/services/outbox.notifier.d.ts +30 -0
- package/dist/integration/services/outbox.notifier.d.ts.map +1 -0
- package/dist/integration/services/outbox.notifier.js +57 -0
- package/dist/integration/services/outbox.notifier.js.map +1 -0
- package/dist/integration/services/outbox.processor.d.ts +42 -0
- package/dist/integration/services/outbox.processor.d.ts.map +1 -0
- package/dist/integration/services/outbox.processor.job.d.ts +43 -0
- package/dist/integration/services/outbox.processor.job.d.ts.map +1 -0
- package/dist/integration/services/outbox.processor.job.js +100 -0
- package/dist/integration/services/outbox.processor.job.js.map +1 -0
- package/dist/integration/services/outbox.processor.js +208 -0
- package/dist/integration/services/outbox.processor.js.map +1 -0
- package/dist/integration/services/outbox.service.d.ts +53 -0
- package/dist/integration/services/outbox.service.d.ts.map +1 -0
- package/dist/integration/services/outbox.service.js +149 -0
- package/dist/integration/services/outbox.service.js.map +1 -0
- package/dist/integration/types/event.types.d.ts +88 -0
- package/dist/integration/types/event.types.d.ts.map +1 -0
- package/dist/integration/types/event.types.js +35 -0
- package/dist/integration/types/event.types.js.map +1 -0
- package/dist/integration/types/index.d.ts +3 -0
- package/dist/integration/types/index.d.ts.map +1 -0
- package/dist/integration/types/index.js +19 -0
- package/dist/integration/types/index.js.map +1 -0
- package/dist/integration/types/subscriber.types.d.ts +31 -0
- package/dist/integration/types/subscriber.types.d.ts.map +1 -0
- package/dist/integration/types/subscriber.types.js +3 -0
- package/dist/integration/types/subscriber.types.js.map +1 -0
- package/dist/oauth/oauth.controller.js.map +1 -1
- package/dist/oauth/oauth.service.d.ts +3 -3
- package/dist/oauth/oauth.service.d.ts.map +1 -1
- package/dist/oauth/oauth.service.js.map +1 -1
- package/dist/profile/profile.controller.d.ts +3 -3
- package/dist/profile/profile.service.d.ts +3 -3
- package/dist/setting/setting.controller.d.ts +12 -8
- package/dist/setting/setting.controller.d.ts.map +1 -1
- package/dist/setting/setting.service.d.ts +12 -8
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +21 -1
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/user/user.controller.d.ts +4 -4
- package/dist/user/user.service.d.ts +9 -9
- package/hedhog/data/dashboard_component_role.yaml +223 -223
- package/hedhog/data/dashboard_role.yaml +18 -18
- package/hedhog/data/route.yaml +2 -0
- package/hedhog/data/setting_group.yaml +955 -470
- package/hedhog/data/setting_subgroup.yaml +303 -0
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +44 -18
- package/hedhog/frontend/app/configurations/[slug]/page.tsx.ejs +134 -27
- package/hedhog/frontend/app/configurations/layout.tsx.ejs +84 -23
- package/hedhog/frontend/app/dashboard/components/widgets/permissions-chart.tsx.ejs +62 -62
- package/hedhog/frontend/app/dashboard/page.tsx.ejs +29 -29
- package/hedhog/frontend/app/preferences/page.tsx.ejs +2 -5
- package/hedhog/table/inbox_event.yaml +40 -0
- package/hedhog/table/integration_link.yaml +33 -0
- package/hedhog/table/outbox_event.yaml +45 -0
- package/hedhog/table/setting.yaml +7 -0
- package/hedhog/table/setting_subgroup.yaml +19 -0
- package/package.json +8 -8
- package/src/ai/ai.service.ts +3 -3
- package/src/auth/auth.controller.ts +11 -11
- package/src/auth/auth.service.ts +8 -8
- package/src/core.module.ts +4 -1
- package/src/index.ts +15 -0
- package/src/integration/README.md +397 -0
- package/src/integration/USAGE_EXAMPLE.md +279 -0
- package/src/integration/index.ts +4 -0
- package/src/integration/integration-api.validation.ts +154 -0
- package/src/integration/integration.module.ts +53 -0
- package/src/integration/services/domain-event.publisher.ts +136 -0
- package/src/integration/services/event-subscriber.registry.ts +89 -0
- package/src/integration/services/inbox.service.ts +218 -0
- package/src/integration/services/index.ts +12 -0
- package/src/integration/services/integration-developer-api.service.ts +96 -0
- package/src/integration/services/integration-link.service.ts +154 -0
- package/src/integration/services/integration-settings.service.ts +128 -0
- package/src/integration/services/outbox-polling.coordinator.ts +146 -0
- package/src/integration/services/outbox.notifier.ts +48 -0
- package/src/integration/services/outbox.processor.job.ts +97 -0
- package/src/integration/services/outbox.processor.ts +266 -0
- package/src/integration/services/outbox.service.ts +209 -0
- package/src/integration/types/event.types.ts +93 -0
- package/src/integration/types/index.ts +3 -0
- package/src/integration/types/subscriber.types.ts +37 -0
- package/src/oauth/oauth.controller.ts +17 -17
- package/src/oauth/oauth.service.ts +20 -20
- package/src/setting/setting.service.ts +27 -2
- 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
|
+
}
|