feat: add notification microservices and transactional outbox
هذا الالتزام موجود في:
3
apps/api-gateway/src/main.ts
Normal file
3
apps/api-gateway/src/main.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { bootstrap } from '../../../src/main';
|
||||
|
||||
void bootstrap();
|
||||
9
apps/api-gateway/tsconfig.app.json
Normal file
9
apps/api-gateway/tsconfig.app.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": false,
|
||||
"outDir": "../../dist/apps/api-gateway"
|
||||
},
|
||||
"include": ["src/**/*.ts", "../../src/**/*.ts", "../../libs/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist", "test", "**/*.spec.ts"]
|
||||
}
|
||||
14
apps/notification-service/src/actor.schema.ts
Normal file
14
apps/notification-service/src/actor.schema.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Schema } from 'mongoose';
|
||||
|
||||
// A deliberately small read model. Notifications only populate these public actor fields.
|
||||
export const ActorSchema = new Schema(
|
||||
{
|
||||
name: String,
|
||||
username: String,
|
||||
stageName: String,
|
||||
avatar: Schema.Types.Mixed,
|
||||
isVerified: Boolean,
|
||||
isDisabled: Boolean,
|
||||
},
|
||||
{ collection: 'users', strict: false, versionKey: false },
|
||||
);
|
||||
47
apps/notification-service/src/app.module.ts
Normal file
47
apps/notification-service/src/app.module.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { NOTIFICATION_RMQ_QUEUE } from '@app/contracts';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { NotificationRealtimeSink } from './notification-realtime.sink';
|
||||
import { NotificationConsumer } from './notification.consumer';
|
||||
import { HealthController } from './health.controller';
|
||||
import { NOTIFICATION_SERVICE_STATE, NotificationServiceState } from './service-state';
|
||||
import { ActorSchema } from './actor.schema';
|
||||
import { NotificationsGateway } from '../../../src/modules/notifications/notifications.gateway';
|
||||
import { NotificationsRepository } from '../../../src/modules/notifications/notifications.repository';
|
||||
import { NotificationsService } from '../../../src/modules/notifications/notifications.service';
|
||||
import { Notification, NotificationSchema } from '../../../src/modules/notifications/schemas/notification.schema';
|
||||
|
||||
export const notificationServiceState: NotificationServiceState = {
|
||||
rabbitmqReady: false,
|
||||
draining: false,
|
||||
};
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
MongooseModule.forRootAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
uri: config.get<string>('NOTIFICATION_MONGODB_URI')
|
||||
?? config.get<string>('MONGODB_URI')
|
||||
?? 'mongodb://127.0.0.1:27017/oudelaa',
|
||||
autoIndex: config.get<string>('MONGODB_AUTO_INDEX', 'true') === 'true',
|
||||
}),
|
||||
}),
|
||||
MongooseModule.forFeature([
|
||||
{ name: Notification.name, schema: NotificationSchema, collection: 'notifications' },
|
||||
{ name: 'User', schema: ActorSchema, collection: 'users' },
|
||||
]),
|
||||
],
|
||||
controllers: [HealthController, NotificationConsumer],
|
||||
providers: [
|
||||
NotificationsRepository,
|
||||
NotificationsService,
|
||||
NotificationRealtimeSink,
|
||||
{ provide: NotificationsGateway, useExisting: NotificationRealtimeSink },
|
||||
{ provide: NOTIFICATION_SERVICE_STATE, useValue: notificationServiceState },
|
||||
{ provide: 'NOTIFICATION_QUEUE_NAME', useValue: NOTIFICATION_RMQ_QUEUE },
|
||||
],
|
||||
})
|
||||
export class NotificationServiceModule {}
|
||||
31
apps/notification-service/src/health.controller.ts
Normal file
31
apps/notification-service/src/health.controller.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Controller, Get, Inject, ServiceUnavailableException } from '@nestjs/common';
|
||||
import { InjectConnection } from '@nestjs/mongoose';
|
||||
import { Connection } from 'mongoose';
|
||||
import { NOTIFICATION_SERVICE_STATE, NotificationServiceState } from './service-state';
|
||||
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
constructor(
|
||||
@InjectConnection() private readonly connection: Connection,
|
||||
@Inject(NOTIFICATION_SERVICE_STATE) private readonly state: NotificationServiceState,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@Get('live')
|
||||
live() {
|
||||
return { status: this.state.draining ? 'draining' : 'ok', service: 'notification-service' };
|
||||
}
|
||||
|
||||
@Get('ready')
|
||||
async ready() {
|
||||
const mongodbReady = this.connection.readyState === 1;
|
||||
const ready = !this.state.draining && mongodbReady && this.state.rabbitmqReady;
|
||||
const response = {
|
||||
status: ready ? 'ready' : 'not-ready',
|
||||
service: 'notification-service',
|
||||
checks: { mongodb: mongodbReady, rabbitmq: this.state.rabbitmqReady },
|
||||
};
|
||||
if (!ready) throw new ServiceUnavailableException(response);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
44
apps/notification-service/src/main.ts
Normal file
44
apps/notification-service/src/main.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { NOTIFICATION_RMQ_QUEUE } from '@app/contracts';
|
||||
import { processEnvSource, resolveRabbitMqConfig } from '@app/common';
|
||||
import { Logger, ShutdownSignal, ValidationPipe } from '@nestjs/common';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
|
||||
import { NotificationServiceModule, notificationServiceState } from './app.module';
|
||||
import { assertNotificationTopology, notificationQueueArguments } from './rabbitmq-topology';
|
||||
|
||||
export async function bootstrapNotificationService(): Promise<void> {
|
||||
const app = await NestFactory.create(NotificationServiceModule, { bufferLogs: true });
|
||||
const logger = new Logger('NotificationServiceBootstrap');
|
||||
const rabbit = resolveRabbitMqConfig(processEnvSource(), 'notification-service', true);
|
||||
const port = Number(process.env.NOTIFICATION_SERVICE_PORT ?? 4001);
|
||||
|
||||
app.enableShutdownHooks([ShutdownSignal.SIGINT, ShutdownSignal.SIGTERM]);
|
||||
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
|
||||
const microservice = app.connectMicroservice<MicroserviceOptions>({
|
||||
transport: Transport.RMQ,
|
||||
options: {
|
||||
urls: [rabbit.url],
|
||||
socketOptions: rabbit.socketOptions,
|
||||
queue: NOTIFICATION_RMQ_QUEUE,
|
||||
noAck: false,
|
||||
prefetchCount: Number(process.env.NOTIFICATION_RMQ_PREFETCH ?? 10),
|
||||
queueOptions: { durable: true, autoDelete: false, exclusive: false, arguments: notificationQueueArguments },
|
||||
},
|
||||
});
|
||||
const status = (microservice as unknown as { status: { subscribe: (callback: (value: string) => void) => void } }).status;
|
||||
status.subscribe((value) => {
|
||||
notificationServiceState.rabbitmqReady = value === 'connected' || value === 'unblocked';
|
||||
});
|
||||
|
||||
const markDraining = () => { notificationServiceState.draining = true; };
|
||||
process.once('SIGTERM', markDraining);
|
||||
process.once('SIGINT', markDraining);
|
||||
|
||||
await assertNotificationTopology(rabbit);
|
||||
await app.startAllMicroservices();
|
||||
notificationServiceState.rabbitmqReady = true;
|
||||
await app.listen(port, process.env.NOTIFICATION_SERVICE_HOST ?? '0.0.0.0');
|
||||
logger.log(`HTTP health endpoint listening on ${port}; consuming ${NOTIFICATION_RMQ_QUEUE}; broker=${rabbit.sanitizedEndpoint}`);
|
||||
}
|
||||
|
||||
if (require.main === module) void bootstrapNotificationService();
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
// Socket.IO remains owned by the gateway. The RMQ response carries realtime data back to it.
|
||||
@Injectable()
|
||||
export class NotificationRealtimeSink {
|
||||
emitCreated(): void {}
|
||||
emitUnreadCount(): void {}
|
||||
}
|
||||
100
apps/notification-service/src/notification.consumer.spec.ts
Normal file
100
apps/notification-service/src/notification.consumer.spec.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { FOLLOW_REQUEST_APPROVED_EVENT, FollowRequestApprovedEvent } from '@app/events';
|
||||
import { NotificationConsumer } from './notification.consumer';
|
||||
|
||||
const event: FollowRequestApprovedEvent = {
|
||||
eventId: 'event-1',
|
||||
eventType: FOLLOW_REQUEST_APPROVED_EVENT,
|
||||
occurredAt: '2026-08-03T10:00:00.000Z',
|
||||
payload: { requestId: 'request-1', actorId: 'actor-1', recipientId: 'recipient-1' },
|
||||
};
|
||||
|
||||
function rmqContext() {
|
||||
const channel = { ack: jest.fn(), nack: jest.fn() };
|
||||
const message = {
|
||||
properties: { headers: { correlationId: 'correlation-1', requestId: 'request-1' } },
|
||||
};
|
||||
return {
|
||||
channel,
|
||||
message,
|
||||
context: {
|
||||
getChannelRef: () => channel,
|
||||
getMessage: () => message,
|
||||
} as never,
|
||||
};
|
||||
}
|
||||
|
||||
describe('NotificationConsumer', () => {
|
||||
const notification = { toJSON: () => ({ _id: 'notification-1' }) };
|
||||
let notifications: Record<string, jest.Mock>;
|
||||
let consumer: NotificationConsumer;
|
||||
|
||||
beforeEach(() => {
|
||||
notifications = {
|
||||
createFollowRequestApprovedNotification: jest.fn().mockResolvedValue(notification),
|
||||
getUnreadCounts: jest.fn().mockResolvedValue({ total: 1, follows: 0, followRequests: 1 }),
|
||||
};
|
||||
consumer = new NotificationConsumer(notifications as never);
|
||||
jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||
jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||
});
|
||||
|
||||
afterEach(() => jest.restoreAllMocks());
|
||||
|
||||
it('consumes follow.request.approved and acknowledges only after persistence', async () => {
|
||||
const rmq = rmqContext();
|
||||
const result = await consumer.followRequestApproved(event, rmq.context);
|
||||
expect(notifications.createFollowRequestApprovedNotification).toHaveBeenCalledWith({
|
||||
...event.payload,
|
||||
eventId: event.eventId,
|
||||
});
|
||||
expect(result).toEqual(expect.objectContaining({
|
||||
result: notification,
|
||||
realtime: expect.objectContaining({ recipientId: 'recipient-1', unreadCount: 1 }),
|
||||
}));
|
||||
expect(rmq.channel.ack).toHaveBeenCalledWith(rmq.message);
|
||||
expect(rmq.channel.nack).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('retries transient failures with a finite limit', async () => {
|
||||
notifications.createFollowRequestApprovedNotification
|
||||
.mockRejectedValueOnce(new Error('temporary-1'))
|
||||
.mockRejectedValueOnce(new Error('temporary-2'))
|
||||
.mockResolvedValueOnce(notification);
|
||||
const rmq = rmqContext();
|
||||
await consumer.followRequestApproved(event, rmq.context);
|
||||
expect(notifications.createFollowRequestApprovedNotification).toHaveBeenCalledTimes(3);
|
||||
expect(rmq.channel.ack).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('rejects to the DLQ after retry exhaustion', async () => {
|
||||
notifications.createFollowRequestApprovedNotification.mockRejectedValue(new Error('mongo down'));
|
||||
const rmq = rmqContext();
|
||||
await expect(consumer.followRequestApproved(event, rmq.context)).rejects.toThrow('mongo down');
|
||||
expect(notifications.createFollowRequestApprovedNotification).toHaveBeenCalledTimes(3);
|
||||
expect(rmq.channel.nack).toHaveBeenCalledWith(rmq.message, false, false);
|
||||
expect(rmq.channel.ack).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects an invalid contract without persistence', async () => {
|
||||
const rmq = rmqContext();
|
||||
await expect(consumer.followRequestApproved({ ...event, eventId: '' }, rmq.context)).rejects.toThrow(
|
||||
'Invalid follow.request.approved contract',
|
||||
);
|
||||
expect(notifications.createFollowRequestApprovedNotification).not.toHaveBeenCalled();
|
||||
expect(rmq.channel.nack).toHaveBeenCalledWith(rmq.message, false, false);
|
||||
});
|
||||
|
||||
it('requires non-empty string IDs and a canonical ISO occurredAt value', async () => {
|
||||
for (const invalid of [
|
||||
{ ...event, occurredAt: 'August 3, 2026' },
|
||||
{ ...event, payload: { ...event.payload, actorId: '' } },
|
||||
]) {
|
||||
const rmq = rmqContext();
|
||||
await expect(consumer.followRequestApproved(invalid, rmq.context)).rejects.toThrow(
|
||||
'Invalid follow.request.approved contract',
|
||||
);
|
||||
expect(rmq.channel.nack).toHaveBeenCalledWith(rmq.message, false, false);
|
||||
}
|
||||
expect(notifications.createFollowRequestApprovedNotification).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
155
apps/notification-service/src/notification.consumer.ts
Normal file
155
apps/notification-service/src/notification.consumer.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { withLimitedRetry } from '@app/common';
|
||||
import {
|
||||
MessageContext,
|
||||
NOTIFICATION_RPC,
|
||||
NotificationListRequest,
|
||||
NotificationMarkAllReadRequest,
|
||||
NotificationMarkReadRequest,
|
||||
NotificationRpcResponse,
|
||||
NotificationUnreadCountRequest,
|
||||
} from '@app/contracts';
|
||||
import {
|
||||
FOLLOW_REQUEST_APPROVED_EVENT,
|
||||
FollowRequestApprovedEvent,
|
||||
isFollowRequestApprovedEvent,
|
||||
} from '@app/events';
|
||||
import { Controller } from '@nestjs/common';
|
||||
import { Ctx, MessagePattern, Payload, RmqContext, RpcException } from '@nestjs/microservices';
|
||||
import { NotificationCategory, NotificationQueryDto } from '../../../src/modules/notifications/dto/notification-query.dto';
|
||||
import { NotificationsService } from '../../../src/modules/notifications/notifications.service';
|
||||
|
||||
@Controller()
|
||||
export class NotificationConsumer {
|
||||
constructor(private readonly notifications: NotificationsService) {}
|
||||
|
||||
@MessagePattern(NOTIFICATION_RPC.list)
|
||||
async list(@Payload() input: NotificationListRequest, @Ctx() context: RmqContext) {
|
||||
return this.rpc(context, () => this.notifications.getMine(
|
||||
input.recipientId,
|
||||
input.query as unknown as NotificationQueryDto,
|
||||
));
|
||||
}
|
||||
|
||||
@MessagePattern(NOTIFICATION_RPC.unreadCount)
|
||||
async unreadCount(@Payload() input: NotificationUnreadCountRequest, @Ctx() context: RmqContext) {
|
||||
return this.rpc(context, () => this.notifications.getUnreadCountByCategory(
|
||||
input.recipientId,
|
||||
input.category as NotificationCategory | undefined,
|
||||
));
|
||||
}
|
||||
|
||||
@MessagePattern(NOTIFICATION_RPC.markRead)
|
||||
async markRead(@Payload() input: NotificationMarkReadRequest, @Ctx() context: RmqContext) {
|
||||
return this.rpc(context, async () => {
|
||||
const result = await this.notifications.markRead(input.recipientId, input.notificationId);
|
||||
const unreadCounts = await this.notifications.getUnreadCounts(input.recipientId);
|
||||
return { result, realtime: {
|
||||
recipientId: input.recipientId,
|
||||
unreadCount: unreadCounts.total,
|
||||
unreadCounts,
|
||||
} };
|
||||
}, true);
|
||||
}
|
||||
|
||||
@MessagePattern(NOTIFICATION_RPC.markAllRead)
|
||||
async markAllRead(@Payload() input: NotificationMarkAllReadRequest, @Ctx() context: RmqContext) {
|
||||
return this.rpc(context, async () => {
|
||||
const result = await this.notifications.markAllRead(input.recipientId);
|
||||
const unreadCounts = await this.notifications.getUnreadCounts(input.recipientId);
|
||||
return { result, realtime: {
|
||||
recipientId: input.recipientId,
|
||||
unreadCount: unreadCounts.total,
|
||||
unreadCounts,
|
||||
} };
|
||||
}, true);
|
||||
}
|
||||
|
||||
@MessagePattern(FOLLOW_REQUEST_APPROVED_EVENT)
|
||||
async followRequestApproved(
|
||||
@Payload() event: FollowRequestApprovedEvent,
|
||||
@Ctx() context: RmqContext,
|
||||
) {
|
||||
if (!isFollowRequestApprovedEvent(event)) {
|
||||
return this.reject(context, new Error('Invalid follow.request.approved contract'));
|
||||
}
|
||||
|
||||
const metadata = this.metadata(context);
|
||||
try {
|
||||
const notification = await withLimitedRetry(
|
||||
() => this.notifications.createFollowRequestApprovedNotification({
|
||||
...event.payload,
|
||||
eventId: event.eventId,
|
||||
}),
|
||||
3,
|
||||
25,
|
||||
);
|
||||
const unreadCounts = await this.notifications.getUnreadCounts(event.payload.recipientId);
|
||||
this.ack(context);
|
||||
this.log('info', 'follow.request.approved consumed', metadata, { eventId: event.eventId });
|
||||
return {
|
||||
result: notification,
|
||||
realtime: {
|
||||
recipientId: event.payload.recipientId,
|
||||
notification,
|
||||
unreadCount: unreadCounts.total,
|
||||
unreadCounts,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return this.reject(context, error, metadata, event.eventId);
|
||||
}
|
||||
}
|
||||
|
||||
private async rpc<T>(
|
||||
context: RmqContext,
|
||||
operation: () => Promise<T | NotificationRpcResponse<T>>,
|
||||
alreadyWrapped = false,
|
||||
): Promise<NotificationRpcResponse<T>> {
|
||||
try {
|
||||
const value = await operation();
|
||||
this.ack(context);
|
||||
return (alreadyWrapped ? value : { result: value }) as NotificationRpcResponse<T>;
|
||||
} catch (error) {
|
||||
this.ack(context);
|
||||
throw new RpcException(error instanceof Error ? error.message : 'Notification operation failed');
|
||||
}
|
||||
}
|
||||
|
||||
private ack(context: RmqContext): void {
|
||||
context.getChannelRef().ack(context.getMessage());
|
||||
}
|
||||
|
||||
private reject(
|
||||
context: RmqContext,
|
||||
error: unknown,
|
||||
metadata = this.metadata(context),
|
||||
eventId?: string,
|
||||
): never {
|
||||
context.getChannelRef().nack(context.getMessage(), false, false);
|
||||
const message = error instanceof Error ? error.message : 'Notification event failed';
|
||||
this.log('error', message, metadata, { eventId });
|
||||
throw new RpcException(message);
|
||||
}
|
||||
|
||||
private metadata(context: RmqContext): MessageContext {
|
||||
const message = context.getMessage() as { properties?: { headers?: Record<string, unknown>; correlationId?: string } };
|
||||
const headers = message.properties?.headers ?? {};
|
||||
return {
|
||||
correlationId: String(headers.correlationId ?? message.properties?.correlationId ?? ''),
|
||||
requestId: String(headers.requestId ?? ''),
|
||||
};
|
||||
}
|
||||
|
||||
private log(
|
||||
level: 'info' | 'error',
|
||||
message: string,
|
||||
context: MessageContext,
|
||||
extra: Record<string, unknown>,
|
||||
): void {
|
||||
const entry = JSON.stringify({
|
||||
timestamp: new Date().toISOString(), level, service: 'notification-service', message, ...context, ...extra,
|
||||
});
|
||||
if (level === 'error') console.error(entry);
|
||||
else console.log(entry);
|
||||
}
|
||||
}
|
||||
29
apps/notification-service/src/rabbitmq-topology.spec.ts
Normal file
29
apps/notification-service/src/rabbitmq-topology.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { NOTIFICATION_RMQ_DLQ, NOTIFICATION_RMQ_QUEUE } from '@app/contracts';
|
||||
import { assertNotificationTopology, notificationQueueArguments } from './rabbitmq-topology';
|
||||
|
||||
describe('RabbitMQ notification adapter', () => {
|
||||
it('asserts durable main and dead-letter queues', async () => {
|
||||
const channel = {
|
||||
assertQueue: jest.fn().mockResolvedValue({}),
|
||||
close: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const connection = {
|
||||
createChannel: jest.fn().mockResolvedValue(channel),
|
||||
close: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
await assertNotificationTopology('amqp://rabbit', jest.fn().mockResolvedValue(connection) as never);
|
||||
|
||||
expect(channel.assertQueue).toHaveBeenNthCalledWith(1, NOTIFICATION_RMQ_DLQ, {
|
||||
durable: true, autoDelete: false, exclusive: false,
|
||||
});
|
||||
expect(channel.assertQueue).toHaveBeenNthCalledWith(2, NOTIFICATION_RMQ_QUEUE, {
|
||||
durable: true,
|
||||
autoDelete: false,
|
||||
exclusive: false,
|
||||
arguments: notificationQueueArguments,
|
||||
});
|
||||
expect(channel.close).toHaveBeenCalled();
|
||||
expect(connection.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
35
apps/notification-service/src/rabbitmq-topology.ts
Normal file
35
apps/notification-service/src/rabbitmq-topology.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { NOTIFICATION_RMQ_DLQ, NOTIFICATION_RMQ_QUEUE } from '@app/contracts';
|
||||
import { ChannelModel, connect } from 'amqplib';
|
||||
import { ResolvedRabbitMqConfig } from '@app/common';
|
||||
|
||||
type Connect = (url: string, options?: Record<string, unknown>) => Promise<ChannelModel>;
|
||||
|
||||
export const notificationQueueArguments = {
|
||||
'x-dead-letter-exchange': '',
|
||||
'x-dead-letter-routing-key': NOTIFICATION_RMQ_DLQ,
|
||||
};
|
||||
|
||||
export async function assertNotificationTopology(
|
||||
config: string | ResolvedRabbitMqConfig,
|
||||
connectToRabbit: Connect = connect as unknown as Connect,
|
||||
): Promise<void> {
|
||||
const url = typeof config === 'string' ? config : config.url;
|
||||
const options = typeof config === 'string' ? undefined : config.socketOptions.connectionOptions;
|
||||
const connection = await connectToRabbit(url, options);
|
||||
try {
|
||||
const channel = await connection.createChannel();
|
||||
try {
|
||||
await channel.assertQueue(NOTIFICATION_RMQ_DLQ, { durable: true, autoDelete: false, exclusive: false });
|
||||
await channel.assertQueue(NOTIFICATION_RMQ_QUEUE, {
|
||||
durable: true,
|
||||
autoDelete: false,
|
||||
exclusive: false,
|
||||
arguments: notificationQueueArguments,
|
||||
});
|
||||
} finally {
|
||||
await channel.close();
|
||||
}
|
||||
} finally {
|
||||
await connection.close();
|
||||
}
|
||||
}
|
||||
6
apps/notification-service/src/service-state.ts
Normal file
6
apps/notification-service/src/service-state.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const NOTIFICATION_SERVICE_STATE = Symbol('NOTIFICATION_SERVICE_STATE');
|
||||
|
||||
export interface NotificationServiceState {
|
||||
rabbitmqReady: boolean;
|
||||
draining: boolean;
|
||||
}
|
||||
9
apps/notification-service/tsconfig.app.json
Normal file
9
apps/notification-service/tsconfig.app.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": false,
|
||||
"outDir": "../../dist/apps/notification-service"
|
||||
},
|
||||
"include": ["src/**/*.ts", "../../src/modules/notifications/**/*.ts", "../../src/common/**/*.ts", "../../libs/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist", "test", "**/*.spec.ts"]
|
||||
}
|
||||
المرجع في مشكلة جديدة
حظر مستخدم