@hed-hog/core 0.0.294 → 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/mail/mail.controller.d.ts +2 -2
- package/dist/mail/mail.service.d.ts +2 -2
- package/dist/mail-sent/mail-sent.controller.d.ts +3 -3
- package/dist/mail-sent/mail-sent.service.d.ts +3 -3
- 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/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/preferences/page.tsx.ejs +45 -48
- 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/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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import {
|
|
3
|
+
DomainEvent,
|
|
4
|
+
EventHandler,
|
|
5
|
+
IntegrationLink,
|
|
6
|
+
LinkType,
|
|
7
|
+
OutboxEvent,
|
|
8
|
+
SubscriberDefinition,
|
|
9
|
+
} from '../types';
|
|
10
|
+
import {
|
|
11
|
+
DomainEventPublisher,
|
|
12
|
+
PublishDomainEventInput,
|
|
13
|
+
PublishDomainEventOptions,
|
|
14
|
+
} from './domain-event.publisher';
|
|
15
|
+
import { EventSubscriberRegistry } from './event-subscriber.registry';
|
|
16
|
+
import {
|
|
17
|
+
CreateIntegrationLinkDto,
|
|
18
|
+
IntegrationLinkPersistenceClient,
|
|
19
|
+
IntegrationLinkService,
|
|
20
|
+
} from './integration-link.service';
|
|
21
|
+
|
|
22
|
+
export interface IntegrationSubscriptionInput {
|
|
23
|
+
eventName: string;
|
|
24
|
+
consumerName: string;
|
|
25
|
+
handler: EventHandler;
|
|
26
|
+
priority?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface IntegrationLinkQuery {
|
|
30
|
+
module: string;
|
|
31
|
+
entityType: string;
|
|
32
|
+
entityId: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Injectable()
|
|
36
|
+
export class IntegrationDeveloperApiService {
|
|
37
|
+
constructor(
|
|
38
|
+
private readonly publisher: DomainEventPublisher,
|
|
39
|
+
private readonly subscriberRegistry: EventSubscriberRegistry,
|
|
40
|
+
private readonly linkService: IntegrationLinkService,
|
|
41
|
+
) {}
|
|
42
|
+
|
|
43
|
+
async publishEvent(
|
|
44
|
+
input: PublishDomainEventInput,
|
|
45
|
+
options?: PublishDomainEventOptions,
|
|
46
|
+
): Promise<OutboxEvent> {
|
|
47
|
+
return this.publisher.publishEvent(input, options);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async publishEvents(
|
|
51
|
+
events: Array<DomainEvent | PublishDomainEventInput>,
|
|
52
|
+
options?: PublishDomainEventOptions,
|
|
53
|
+
): Promise<OutboxEvent[]> {
|
|
54
|
+
return this.publisher.publishEvents(events, options);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
subscribe(input: IntegrationSubscriptionInput): void {
|
|
58
|
+
this.subscriberRegistry.subscribe(
|
|
59
|
+
input.eventName,
|
|
60
|
+
input.consumerName,
|
|
61
|
+
input.handler,
|
|
62
|
+
input.priority || 0,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
subscribeMany(definitions: SubscriberDefinition[]): void {
|
|
67
|
+
this.subscriberRegistry.subscribeMany(definitions);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async createLink(
|
|
71
|
+
dto: CreateIntegrationLinkDto,
|
|
72
|
+
persistenceClient?: IntegrationLinkPersistenceClient,
|
|
73
|
+
): Promise<IntegrationLink> {
|
|
74
|
+
return this.linkService.createLink(dto, persistenceClient);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async findLinksBySource(query: IntegrationLinkQuery): Promise<IntegrationLink[]> {
|
|
78
|
+
return this.linkService.findOutbound(
|
|
79
|
+
query.module,
|
|
80
|
+
query.entityType,
|
|
81
|
+
query.entityId,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async findLinksByTarget(query: IntegrationLinkQuery): Promise<IntegrationLink[]> {
|
|
86
|
+
return this.linkService.findInbound(
|
|
87
|
+
query.module,
|
|
88
|
+
query.entityType,
|
|
89
|
+
query.entityId,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async findLinksByType(linkType: LinkType): Promise<IntegrationLink[]> {
|
|
94
|
+
return this.linkService.findByLinkType(linkType);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { PrismaService } from '@hed-hog/api-prisma';
|
|
2
|
+
import { Injectable } from '@nestjs/common';
|
|
3
|
+
import { IntegrationLink, LinkType } from '../types';
|
|
4
|
+
|
|
5
|
+
export interface CreateIntegrationLinkDto {
|
|
6
|
+
sourceModule: string;
|
|
7
|
+
sourceEntityType: string;
|
|
8
|
+
sourceEntityId: string;
|
|
9
|
+
targetModule: string;
|
|
10
|
+
targetEntityType: string;
|
|
11
|
+
targetEntityId: string;
|
|
12
|
+
linkType: LinkType;
|
|
13
|
+
metadata?: Record<string, any>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface IntegrationLinkPersistenceClient {
|
|
17
|
+
integrationLink: PrismaService['integrationLink'];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Injectable()
|
|
21
|
+
export class IntegrationLinkService {
|
|
22
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a link between entities from different modules
|
|
26
|
+
*/
|
|
27
|
+
async createLink(
|
|
28
|
+
dto: CreateIntegrationLinkDto,
|
|
29
|
+
persistenceClient?: IntegrationLinkPersistenceClient,
|
|
30
|
+
): Promise<IntegrationLink> {
|
|
31
|
+
const client = persistenceClient ?? this.prisma;
|
|
32
|
+
|
|
33
|
+
return client.integrationLink.create({
|
|
34
|
+
data: {
|
|
35
|
+
sourceModule: dto.sourceModule,
|
|
36
|
+
sourceEntityType: dto.sourceEntityType,
|
|
37
|
+
sourceEntityId: dto.sourceEntityId,
|
|
38
|
+
targetModule: dto.targetModule,
|
|
39
|
+
targetEntityType: dto.targetEntityType,
|
|
40
|
+
targetEntityId: dto.targetEntityId,
|
|
41
|
+
linkType: dto.linkType,
|
|
42
|
+
metadata: dto.metadata || null,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Find all links originating from a source entity
|
|
49
|
+
*/
|
|
50
|
+
async findOutbound(
|
|
51
|
+
sourceModule: string,
|
|
52
|
+
sourceEntityType: string,
|
|
53
|
+
sourceEntityId: string,
|
|
54
|
+
): Promise<IntegrationLink[]> {
|
|
55
|
+
return this.prisma.integrationLink.findMany({
|
|
56
|
+
where: {
|
|
57
|
+
sourceModule,
|
|
58
|
+
sourceEntityType,
|
|
59
|
+
sourceEntityId,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Find all links terminating at a target entity
|
|
66
|
+
*/
|
|
67
|
+
async findInbound(
|
|
68
|
+
targetModule: string,
|
|
69
|
+
targetEntityType: string,
|
|
70
|
+
targetEntityId: string,
|
|
71
|
+
): Promise<IntegrationLink[]> {
|
|
72
|
+
return this.prisma.integrationLink.findMany({
|
|
73
|
+
where: {
|
|
74
|
+
targetModule,
|
|
75
|
+
targetEntityType,
|
|
76
|
+
targetEntityId,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Find links of a specific type
|
|
83
|
+
*/
|
|
84
|
+
async findByLinkType(linkType: LinkType): Promise<IntegrationLink[]> {
|
|
85
|
+
return this.prisma.integrationLink.findMany({
|
|
86
|
+
where: { linkType },
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Delete a link
|
|
92
|
+
*/
|
|
93
|
+
async deleteLink(linkId: string): Promise<IntegrationLink> {
|
|
94
|
+
return this.prisma.integrationLink.delete({
|
|
95
|
+
where: { id: linkId },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get link by ID
|
|
101
|
+
*/
|
|
102
|
+
async getById(linkId: string): Promise<IntegrationLink | null> {
|
|
103
|
+
return this.prisma.integrationLink.findUnique({
|
|
104
|
+
where: { id: linkId },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Find bidirectional link between two entities
|
|
110
|
+
*/
|
|
111
|
+
async findBidirectional(
|
|
112
|
+
module1: string,
|
|
113
|
+
entity1Type: string,
|
|
114
|
+
entity1Id: string,
|
|
115
|
+
module2: string,
|
|
116
|
+
entity2Type: string,
|
|
117
|
+
entity2Id: string,
|
|
118
|
+
): Promise<IntegrationLink | null> {
|
|
119
|
+
// Look for forward or reverse direction
|
|
120
|
+
const link = await this.prisma.integrationLink.findFirst({
|
|
121
|
+
where: {
|
|
122
|
+
OR: [
|
|
123
|
+
{
|
|
124
|
+
sourceModule: module1,
|
|
125
|
+
sourceEntityType: entity1Type,
|
|
126
|
+
sourceEntityId: entity1Id,
|
|
127
|
+
targetModule: module2,
|
|
128
|
+
targetEntityType: entity2Type,
|
|
129
|
+
targetEntityId: entity2Id,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
sourceModule: module2,
|
|
133
|
+
sourceEntityType: entity2Type,
|
|
134
|
+
sourceEntityId: entity2Id,
|
|
135
|
+
targetModule: module1,
|
|
136
|
+
targetEntityType: entity1Type,
|
|
137
|
+
targetEntityId: entity1Id,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return link;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Count links from a module
|
|
148
|
+
*/
|
|
149
|
+
async countFromModule(sourceModule: string): Promise<number> {
|
|
150
|
+
return this.prisma.integrationLink.count({
|
|
151
|
+
where: { sourceModule },
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Injectable, Logger } from '@nestjs/common';
|
|
2
|
+
import { SettingService } from '../../setting/setting.service';
|
|
3
|
+
|
|
4
|
+
export interface IntegrationRuntimeSettings {
|
|
5
|
+
outboxEnabled: boolean;
|
|
6
|
+
outboxProcessorEnabled: boolean;
|
|
7
|
+
startupDrainEnabled: boolean;
|
|
8
|
+
pollingIntervalMs: number;
|
|
9
|
+
idlePollingIntervalMs: number;
|
|
10
|
+
batchSize: number;
|
|
11
|
+
startupDrainBatchSize: number;
|
|
12
|
+
maxAttempts: number;
|
|
13
|
+
processingLeaseMs: number;
|
|
14
|
+
retryBaseDelayMs: number;
|
|
15
|
+
deadLetterEnabled: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@Injectable()
|
|
19
|
+
export class IntegrationSettingsService {
|
|
20
|
+
private readonly logger = new Logger(IntegrationSettingsService.name);
|
|
21
|
+
|
|
22
|
+
constructor(private readonly settingService: SettingService) {}
|
|
23
|
+
|
|
24
|
+
async getRuntimeSettings(): Promise<IntegrationRuntimeSettings> {
|
|
25
|
+
const values = await this.settingService.getSettingValues([
|
|
26
|
+
'outbox-enabled',
|
|
27
|
+
'outbox-processor-enabled',
|
|
28
|
+
'outbox-startup-drain-enabled',
|
|
29
|
+
'outbox-polling-interval-ms',
|
|
30
|
+
'outbox-idle-polling-interval-ms',
|
|
31
|
+
'outbox-batch-size',
|
|
32
|
+
'outbox-startup-drain-batch-size',
|
|
33
|
+
'outbox-max-attempts',
|
|
34
|
+
'outbox-processing-lease-ms',
|
|
35
|
+
'outbox-retry-base-delay-ms',
|
|
36
|
+
'outbox-dead-letter-enabled',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const pollingIntervalMs = this.getPositiveInt(
|
|
40
|
+
values['outbox-polling-interval-ms'],
|
|
41
|
+
5000,
|
|
42
|
+
100,
|
|
43
|
+
'outbox-polling-interval-ms',
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
let idlePollingIntervalMs = this.getPositiveInt(
|
|
47
|
+
values['outbox-idle-polling-interval-ms'],
|
|
48
|
+
30000,
|
|
49
|
+
100,
|
|
50
|
+
'outbox-idle-polling-interval-ms',
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (idlePollingIntervalMs < pollingIntervalMs) {
|
|
54
|
+
this.logger.warn(
|
|
55
|
+
`Setting outbox-idle-polling-interval-ms (${idlePollingIntervalMs}) is lower than outbox-polling-interval-ms (${pollingIntervalMs}). Using ${pollingIntervalMs}.`,
|
|
56
|
+
);
|
|
57
|
+
idlePollingIntervalMs = pollingIntervalMs;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
outboxEnabled: this.getBoolean(values['outbox-enabled'], true),
|
|
62
|
+
outboxProcessorEnabled: this.getBoolean(
|
|
63
|
+
values['outbox-processor-enabled'],
|
|
64
|
+
true,
|
|
65
|
+
),
|
|
66
|
+
startupDrainEnabled: this.getBoolean(
|
|
67
|
+
values['outbox-startup-drain-enabled'],
|
|
68
|
+
true,
|
|
69
|
+
),
|
|
70
|
+
pollingIntervalMs,
|
|
71
|
+
idlePollingIntervalMs,
|
|
72
|
+
batchSize: this.getPositiveInt(values['outbox-batch-size'], 10, 1, 'outbox-batch-size'),
|
|
73
|
+
startupDrainBatchSize: this.getPositiveInt(
|
|
74
|
+
values['outbox-startup-drain-batch-size'],
|
|
75
|
+
50,
|
|
76
|
+
1,
|
|
77
|
+
'outbox-startup-drain-batch-size',
|
|
78
|
+
),
|
|
79
|
+
maxAttempts: this.getPositiveInt(values['outbox-max-attempts'], 3, 1, 'outbox-max-attempts'),
|
|
80
|
+
processingLeaseMs: this.getPositiveInt(
|
|
81
|
+
values['outbox-processing-lease-ms'],
|
|
82
|
+
30000,
|
|
83
|
+
1000,
|
|
84
|
+
'outbox-processing-lease-ms',
|
|
85
|
+
),
|
|
86
|
+
retryBaseDelayMs: this.getPositiveInt(
|
|
87
|
+
values['outbox-retry-base-delay-ms'],
|
|
88
|
+
1000,
|
|
89
|
+
100,
|
|
90
|
+
'outbox-retry-base-delay-ms',
|
|
91
|
+
),
|
|
92
|
+
deadLetterEnabled: this.getBoolean(values['outbox-dead-letter-enabled'], true),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private getPositiveInt(
|
|
97
|
+
value: unknown,
|
|
98
|
+
fallback: number,
|
|
99
|
+
min: number,
|
|
100
|
+
slug: string,
|
|
101
|
+
): number {
|
|
102
|
+
const parsed = Number(value);
|
|
103
|
+
if (!Number.isFinite(parsed) || parsed < min) {
|
|
104
|
+
this.logger.warn(
|
|
105
|
+
`Invalid ${slug} setting value (${String(value)}). Using fallback ${fallback}.`,
|
|
106
|
+
);
|
|
107
|
+
return fallback;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return Math.floor(parsed);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private getBoolean(value: unknown, fallback: boolean): boolean {
|
|
114
|
+
if (typeof value === 'boolean') {
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (value === 'true' || value === '1') {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (value === 'false' || value === '0') {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return fallback;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -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
|
+
}
|