Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion share/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@grimity/dto",
"version": "1.0.31",
"version": "1.0.32",
"types": "dist/share/index.d.ts",
"exports": {
".": {
Expand Down
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { ChatModule } from './module/chat/chat.module';
import { AppController } from './app.controller';
import { ImageModule } from './module/image/image.module';
import { PushModule } from './module/push/push.module';
import { EventModule } from './infrastructure/event/event.module';

@Module({
imports: [
Expand Down Expand Up @@ -56,6 +57,7 @@ import { PushModule } from './module/push/push.module';
delimiter: ':',
wildcard: true,
}),
EventModule,
ClsModule.forRoot({
global: true,
middleware: { mount: true },
Expand Down
43 changes: 43 additions & 0 deletions src/database/redis/redis-event.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
export interface RedisEventPayloadMap {
newChatMessage: {
chatId: string;
senderId: string;
chatUsers: {
id: string;
name: string;
image: string | null;
url: string;
unreadCount: number;
}[];
messages: {
id: string;
content: string | null;
image: string | null;
createdAt: Date;
replyTo: {
id: string;
content: string | null;
image: string | null;
createdAt: Date;
} | null;
}[];
};

likeChatMessage: {
messageId: string;
};

unlikeChatMessage: {
messageId: string;
};

deleteChat: {
chatIds: string[];
};
}

export type RedisEventName = keyof RedisEventPayloadMap;

// Redis -> EventEmitter로 전달될 때 targetUserId가 추가됨
export type RedisToEventPayload<K extends RedisEventName> =
RedisEventPayloadMap[K] & { targetUserId: string };
5 changes: 3 additions & 2 deletions src/database/redis/redis.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Module, Global } from '@nestjs/common';
import { RedisService } from 'src/database/redis/redis.service';
import { TypedRedisPublisher } from './typed-redis-publisher';

@Global()
@Module({
providers: [RedisService],
exports: [RedisService],
providers: [RedisService, TypedRedisPublisher],
exports: [RedisService, TypedRedisPublisher],
})
export class RedisModule {}
11 changes: 7 additions & 4 deletions src/database/redis/redis.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Injectable, OnModuleDestroy } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import Redis from 'ioredis';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { TypedEventEmitter } from 'src/infrastructure/event/typed-event-emitter';
import { RedisEventName, RedisEventPayloadMap } from './redis-event.types';

@Injectable()
export class RedisService implements OnModuleDestroy {
Expand All @@ -10,20 +11,22 @@ export class RedisService implements OnModuleDestroy {

constructor(
private configService: ConfigService,
private eventEmitter: EventEmitter2,
private eventEmitter: TypedEventEmitter,
) {
this.redis = new Redis({
host: this.configService.get('REDIS_HOST'),
});
this.subRedis = this.redis.duplicate();

this.subRedis.on('message', (channel: string, message: string) => {
const { event, ...payload } = JSON.parse(message);
const { event, ...payload } = JSON.parse(message) as {
event: RedisEventName;
} & RedisEventPayloadMap[RedisEventName];

this.eventEmitter.emit(event, {
...payload,
targetUserId: channel.split(':')[1],
});
} as never);
});
}

Expand Down
16 changes: 16 additions & 0 deletions src/database/redis/typed-redis-publisher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Injectable } from '@nestjs/common';
import { RedisService } from './redis.service';
import { RedisEventName, RedisEventPayloadMap } from './redis-event.types';

@Injectable()
export class TypedRedisPublisher {
constructor(private readonly redisService: RedisService) {}

async publish<K extends RedisEventName>(
channel: string,
event: K,
payload: RedisEventPayloadMap[K],
) {
await this.redisService.publish(channel, { event, ...payload });
}
}
51 changes: 51 additions & 0 deletions src/infrastructure/event/event-payload.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { RedisToEventPayload } from 'src/database/redis/redis-event.types';

export interface EventPayloadMap {
'notification:FEED_LIKE': { feedId: string; likeCount: number };
'notification:FEED_MENTION': {
actorId: string;
feedId: string;
mentionedUserId: string;
};
'notification:FEED_REPLY': {
actorId: string;
feedId: string;
parentId: string;
};
'notification:FEED_COMMENT': { actorId: string; feedId: string };

'notification:POST_MENTION': {
actorId: string;
postId: string;
mentionedUserId: string;
};
'notification:POST_REPLY': {
actorId: string;
postId: string;
parentId: string;
};
'notification:POST_COMMENT': { actorId: string; postId: string };

'notification:FOLLOW': { actorId: string; userId: string };

push: PushPayload;

// Redis Pub/Sub -> EventEmitter로 전달되는 채팅 이벤트
newChatMessage: RedisToEventPayload<'newChatMessage'>;
likeChatMessage: RedisToEventPayload<'likeChatMessage'>;
unlikeChatMessage: RedisToEventPayload<'unlikeChatMessage'>;
deleteChat: RedisToEventPayload<'deleteChat'>;
}

export interface PushPayload {
userId: string;
title: string;
text: string;
imageUrl?: string | null;
data?: Record<string, string>;
silent?: boolean;
key?: string;
badge?: number;
}

export type EventName = keyof EventPayloadMap;
9 changes: 9 additions & 0 deletions src/infrastructure/event/event.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { TypedEventEmitter } from './typed-event-emitter';

@Global()
@Module({
providers: [TypedEventEmitter],
exports: [TypedEventEmitter],
})
export class EventModule {}
12 changes: 12 additions & 0 deletions src/infrastructure/event/typed-event-emitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { EventName, EventPayloadMap } from './event-payload.types';

@Injectable()
export class TypedEventEmitter {
constructor(private readonly eventEmitter: EventEmitter2) {}

emit<K extends EventName>(event: K, payload: EventPayloadMap[K]) {
return this.eventEmitter.emit(event, payload);
}
}
57 changes: 30 additions & 27 deletions src/module/chat/chat-message.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { ChatReader } from './repository/chat.reader';
import { getImageUrl } from 'src/shared/util/get-image-url';
import { UserReader } from '../user/repository/user.reader';
import { RedisService } from 'src/database/redis/redis.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { TypedEventEmitter } from 'src/infrastructure/event/typed-event-emitter';
import { TypedRedisPublisher } from 'src/database/redis/typed-redis-publisher';

@Injectable()
export class ChatMessageService {
Expand All @@ -14,7 +15,8 @@ export class ChatMessageService {
private readonly chatReader: ChatReader,
private readonly userReader: UserReader,
private readonly redisService: RedisService,
private readonly eventEmitter: EventEmitter2,
private readonly eventEmitter: TypedEventEmitter,
private readonly redisPublisher: TypedRedisPublisher,
) {
this.redisClient = this.redisService.pubClient;
}
Expand Down Expand Up @@ -116,7 +118,7 @@ export class ChatMessageService {

const chatUsers = await this.chatReader.findUsersByChatId(chatId);

const newMessageEvent = {
const newMessagePayload = {
chatId,
senderId: userId,
chatUsers: chatUsers.map((user) => ({
Expand Down Expand Up @@ -144,22 +146,24 @@ export class ChatMessageService {
})),
};

await this.redisService.publish(`user:${userId}`, {
...newMessageEvent,
event: 'newChatMessage',
});
await this.redisPublisher.publish(
`user:${userId}`,
'newChatMessage',
newMessagePayload,
);

const myInfo = chatUsers.find((user) => user.id === userId);
if (!myInfo) throw new HttpException('USER', 404);

if (targetUserIsOnline) {
await this.redisService.publish(`user:${targetUserStatus.userId}`, {
...newMessageEvent,
event: 'newChatMessage',
});
await this.redisPublisher.publish(
`user:${targetUserStatus.userId}`,
'newChatMessage',
newMessagePayload,
);
}
if (!targetUserJoinedChat) {
const pushPayload: PushPayload = {
this.eventEmitter.emit(`push`, {
userId: targetUserStatus.userId,
title: `${myInfo.name}`,
text:
Expand All @@ -170,12 +174,11 @@ export class ChatMessageService {
data: {
event: 'newChatMessage',
deepLink: `/chats/${chatId}`,
data: JSON.stringify(newMessageEvent),
data: JSON.stringify(newMessagePayload),
},
key: `chat-message-${chatId}`,
badge: targetUserStatus.unreadCount + toCreateMessages.length,
};
this.eventEmitter.emit(`push`, pushPayload);
});
}

return;
Expand All @@ -193,14 +196,14 @@ export class ChatMessageService {

await this.chatWriter.updateMessageLike(messageId, true);

await this.redisService.publish(`user:${userId}`, {
await this.redisPublisher.publish(`user:${userId}`, 'likeChatMessage', {
messageId,
event: 'likeChatMessage',
});
await this.redisService.publish(`user:${targetUser.id}`, {
messageId,
event: 'likeChatMessage',
});
await this.redisPublisher.publish(
`user:${targetUser.id}`,
'likeChatMessage',
{ messageId },
);
return;
}

Expand All @@ -215,14 +218,14 @@ export class ChatMessageService {

await this.chatWriter.updateMessageLike(messageId, false);

await this.redisService.publish(`user:${userId}`, {
await this.redisPublisher.publish(`user:${userId}`, 'unlikeChatMessage', {
messageId,
event: 'unlikeChatMessage',
});
await this.redisService.publish(`user:${targetUser.id}`, {
messageId,
event: 'unlikeChatMessage',
});
await this.redisPublisher.publish(
`user:${targetUser.id}`,
'unlikeChatMessage',
{ messageId },
);
return;
}

Expand Down
22 changes: 7 additions & 15 deletions src/module/chat/chat.listener.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,32 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { GlobalGateway } from '../websocket/global.gateway';
import type { NewChatMessageEventResponse } from './dto';
import type { EventPayloadMap } from 'src/infrastructure/event/event-payload.types';

@Injectable()
export class ChatListener {
constructor(private readonly gateway: GlobalGateway) {}

@OnEvent('newChatMessage')
handleNewChatMessageEvent({
targetUserId,
...payload
}: NewChatMessageEventResponse & { targetUserId: string }) {
this.gateway.emitMessageEventToUser(targetUserId, payload);
handleNewChatMessageEvent(payload: EventPayloadMap['newChatMessage']) {
const { targetUserId, ...rest } = payload;
this.gateway.emitMessageEventToUser(targetUserId, rest);
}

@OnEvent('likeChatMessage')
handleLikeChatMessageEvent(payload: {
targetUserId: string;
messageId: string;
}) {
handleLikeChatMessageEvent(payload: EventPayloadMap['likeChatMessage']) {
const { targetUserId, messageId } = payload;
this.gateway.emitLikeChatMessageEventToUser(targetUserId, messageId);
}

@OnEvent('unlikeChatMessage')
handleUnlikeChatMessageEvent(payload: {
targetUserId: string;
messageId: string;
}) {
handleUnlikeChatMessageEvent(payload: EventPayloadMap['unlikeChatMessage']) {
const { targetUserId, messageId } = payload;
this.gateway.emitUnlikeChatMessageEventToUser(targetUserId, messageId);
}

@OnEvent('deleteChat')
handleDeleteChatEvent(payload: { targetUserId: string; chatIds: string[] }) {
handleDeleteChatEvent(payload: EventPayloadMap['deleteChat']) {
const { targetUserId, chatIds } = payload;
this.gateway.emitDeleteChatEventToUser(targetUserId, chatIds);
}
Expand Down
Loading
Loading