-
Notifications
You must be signed in to change notification settings - Fork 0
feat(conversation): 실시간 subscription — graphql-ws + Redis PubSub #270
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
d01ea4d
feat(conversation): 실시간 subscription — graphql-ws + Redis PubSub
chanwoo7 17884c9
fix(conversation): Redis 장애 격리·이벤트 최신 상태 발행 (PR #270 리뷰 반영)
chanwoo7 b3d3e42
fix(conversation): 목록 이벤트 스냅샷을 단일 트랜잭션으로 (PR #270 리뷰 반영)
chanwoo7 f2c5e13
fix(conversation): 커밋 후 발행 전체 격리 + 이벤트 순서 계약 명시 (PR #270 리뷰 반영)
chanwoo7 e028c61
fix(conversation): 목록 이벤트에 lastReadAt 실어 폐기 규칙 확장 + 스냅샷 매장명 (PR #270 …
chanwoo7 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { registerAs } from '@nestjs/config'; | ||
|
|
||
| /** | ||
| * Redis 설정 타입 (GraphQL subscription PubSub용) | ||
| */ | ||
| export interface RedisConfig { | ||
| url: string; | ||
| } | ||
|
|
||
| /** | ||
| * Redis 설정. | ||
| * DATABASE_URL과 달리 미설정 시 로컬 docker-compose 기본값으로 폴백한다 — | ||
| * 현재 배포 인프라가 꺼져 있고 FE도 로컬 백엔드로 테스트하는 개발 단계라, | ||
| * 필수 강제보다 로컬 DX(compose up 후 바로 동작)를 우선한다. | ||
| */ | ||
| export default registerAs('redis', (): RedisConfig => { | ||
| return { | ||
| url: process.env.REDIS_URL ?? 'redis://localhost:6379', | ||
| }; | ||
| }); |
40 changes: 40 additions & 0 deletions
40
src/features/conversation/conversation-subscription.graphql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| extend type Subscription { | ||
| """ | ||
| 대화방 신규 메시지 실시간 구독. 구매자(대화 소유자)와 판매자(해당 매장 | ||
| 소유자)만 구독할 수 있다. 인증은 graphql-ws connectionParams.authorization. | ||
| """ | ||
| conversationMessageAdded(conversationId: ID!): ConversationMessage! | ||
| """구매자 대화 목록/배지 갱신 이벤트(새 메시지 도착 시).""" | ||
| myConversationUpdated: ConversationListUpdate! | ||
| """판매자 대화 목록 갱신 이벤트(고객 메시지 도착 시).""" | ||
| sellerConversationUpdated: SellerConversationListUpdate! | ||
| } | ||
|
|
||
| """ | ||
| 구매자 대화 목록 갱신 이벤트. | ||
| 이벤트 간 도착 순서는 보장되지 않는다 — 구독자는 (lastMessageAt, lastReadAt)을 | ||
| 사전식으로 비교해 이미 표시 중인 상태보다 오래된 이벤트를 폐기해야 한다. | ||
| 읽음 처리는 lastReadAt만 전진시키므로, 읽음 이후 도착한 지연 이벤트도 이 비교로 | ||
| 걸러진다(메시지 스트림은 id 기준). | ||
| """ | ||
| type ConversationListUpdate { | ||
| conversationId: ID! | ||
| storeId: ID! | ||
| storeName: String! | ||
| """마지막 메시지 미리보기(HTML은 태그 제거)""" | ||
| lastMessagePreview: String | ||
| lastMessageAt: DateTime! | ||
| """이벤트 스냅샷 시점의 구매자 마지막 읽음 시각(폐기 규칙 비교용)""" | ||
| lastReadAt: DateTime | ||
| """이벤트 시점의 안읽은 수신 메시지 수""" | ||
| unreadCount: Int! | ||
| } | ||
|
|
||
| """판매자 대화 목록 갱신 이벤트. 도착 순서 비보장 — lastMessageAt 기준 폐기 규칙 동일.""" | ||
| type SellerConversationListUpdate { | ||
| conversationId: ID! | ||
| accountId: ID! | ||
| """마지막 메시지 미리보기(HTML은 태그 제거)""" | ||
| lastMessagePreview: String | ||
| lastMessageAt: DateTime! | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,7 @@ | ||
| // cross-feature 공개 API. 단일 구현 repo라 토큰/인터페이스 없이 구체 클래스로 주입(의도적). | ||
| export { ConversationModule } from '@/features/conversation/conversation.module'; | ||
| export { ConversationRepository } from '@/features/conversation/repositories/conversation.repository'; | ||
| // subscription 이벤트 발행/구독 어댑터 — 판매자 답장(seller feature)도 같은 토픽을 쓴다. | ||
| export { ConversationEventsService } from '@/features/conversation/services/conversation-events.service'; | ||
| export { toEventPreview } from '@/features/conversation/services/conversation-events-mappers.helper'; | ||
| export { toLastMessagePreview } from '@/features/conversation/services/conversation-center-mappers.helper'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
54 changes: 54 additions & 0 deletions
54
src/features/conversation/resolvers/conversation-subscription.resolver.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import { UseGuards } from '@nestjs/common'; | ||
| import { Args, Resolver, Subscription } from '@nestjs/graphql'; | ||
|
|
||
| import { ConversationSubscriptionService } from '@/features/conversation/services/conversation-subscription.service'; | ||
| import { | ||
| CurrentUser, | ||
| JwtAuthGuard, | ||
| parseAccountId, | ||
| type JwtUser, | ||
| } from '@/global/auth'; | ||
|
|
||
| /** | ||
| * 이벤트 payload는 발행 시 이미 GraphQL 출력 형태로 조립돼 있어 | ||
| * resolve는 payload를 그대로 통과시킨다. | ||
| */ | ||
| const passthrough = { resolve: (payload: unknown): unknown => payload }; | ||
|
|
||
| @Resolver('Subscription') | ||
| @UseGuards(JwtAuthGuard) | ||
| export class ConversationSubscriptionResolver { | ||
| constructor( | ||
| private readonly subscriptionService: ConversationSubscriptionService, | ||
| ) {} | ||
|
|
||
| @Subscription('conversationMessageAdded', passthrough) | ||
| conversationMessageAdded( | ||
| @CurrentUser() user: JwtUser, | ||
| @Args('conversationId') conversationId: string, | ||
| ): Promise<AsyncIterator<unknown>> { | ||
| const accountId = parseAccountId(user); | ||
| return this.subscriptionService.subscribeConversationMessages( | ||
| accountId, | ||
| conversationId, | ||
| ); | ||
| } | ||
|
|
||
| @Subscription('myConversationUpdated', passthrough) | ||
| myConversationUpdated( | ||
| @CurrentUser() user: JwtUser, | ||
| ): Promise<AsyncIterator<unknown>> { | ||
| const accountId = parseAccountId(user); | ||
| return this.subscriptionService.subscribeMyConversationUpdates(accountId); | ||
| } | ||
|
|
||
| @Subscription('sellerConversationUpdated', passthrough) | ||
| sellerConversationUpdated( | ||
| @CurrentUser() user: JwtUser, | ||
| ): Promise<AsyncIterator<unknown>> { | ||
| const accountId = parseAccountId(user); | ||
| return this.subscriptionService.subscribeSellerConversationUpdates( | ||
| accountId, | ||
| ); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When two sends for the same conversation are committed within one millisecond, both can have identical
lastMessageAtvalues becausenew Date()andStoreConversation.last_message_atuse millisecond precision (prisma/schema.prisma:1373); buyer sends can also assign the samelastReadAt. If their list events arrive out of order, the documented tuple is therefore identical for the old and new previews, so the subscriber cannot discard the stale event. This is fresh evidence for the ordering case: the timestamp-based mitigation is not strictly monotonic. Include the latest message ID or another sequence in both list-update payloads and comparison rules.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
미반영(한도 내 수용): PR #269의 ms 동률 엣지와 동일 계열 — DateTime(3) 동일 밀리초에 커밋된 두 전송의 목록 이벤트가 역순 도착하는 극소 확률 케이스로, 영향은 미리보기 한 건의 일시적 흔들림이며 다음 이벤트/재조회로 자가 수복됨. 엄밀한 해소는 이벤트 시퀀스 채번(outbox/버전 컬럼)이 필요해 범위와 비례하지 않음 — 필요 시 별도 이슈로. 라운드 상한 도달로 머지 진행.