Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ RTC dropdown confirm
↓
end request command
↓
RtcHostLiveKitPage
RtcHostLiveKit
↓
사진 선택 → publisher 정리 → 서버 종료
↓
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ lifecycle과 stale-response 처리 의도가 명확하다.
서버 endpoint의 식별자 계약을 정확히 따르면서 비동기 응답을 현재
Viewer session에만 적용하기 위해 두 식별자를 함께 유지하는 방식을
선택했다. Host 토큰과 Viewer 토큰은 서로 다른 query와 endpoint를
사용하며 앱 전체에는 `role: "VIEWER"`인 연결 정보만 노출한다.
사용하며 Viewer entry lifecycle에는 실제 연결에 필요한 URL과 token만 노출한다.

## Trade-off

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ RTC frame sink 활성화와 역순 cleanup을 소유한다. CameraPage와 LiveKi
native track 구현을 직접 알지 않는다.

RTC session identity는 메모리 Zustand store가 소유한다. `hostSession`과
`viewerSession`은 상호배타적이며 LiveKit connection도 현재 role과 함께
보관한다. 방 access token과 LiveKit token은 앱 재시작 이후 복구하지 않고
현재 프로세스에만 유지한다. 이 런타임 책임은 서버의 방 상태와 구분하기 위해
`viewerSession`은 상호배타적이다. LiveKit connection은 현재 role feature의
화면 lifecycle에 로컬로 유지하며 앱 재시작 이후 복구하지 않는다. 이 런타임
책임은 서버의 방 상태와 구분하기 위해
`entities/rtc-session` Slice가 소유한다. 방 응답, 참가자와 이벤트는
`entities/rtc-room`이 소유한다.

Expand Down Expand Up @@ -88,7 +88,7 @@ WebRTC track을, publisher는 LiveKit publication lifecycle을 담당한다. 경
→ unpublish → raw track stop/release 순서다. track registry 소유권은 실제
release 호출 전에 소비해 cleanup 재진입에서도 두 번 해제하지 않는다.

`RtcHostLiveKitPage`는 connect와 cleanup promise를 single-flight로 유지하고
`RtcHostLiveKit`은 connect와 cleanup promise를 single-flight로 유지하고
lifecycle epoch로 오래된 connect 완료를 무효화한다. Camera route가 focus를
잃으면 CameraSession과 host publisher/Room cleanup을 시작하며, 다시 활성화될
때만 새 연결 세대를 허용한다.
Expand Down Expand Up @@ -126,8 +126,9 @@ Room disconnect를 재시도한다.
정확히 한 번 해제한다.
- 호스트는 VisionCamera native track만 LiveKit에 publish하며 참여자는
camera capture 없이 remote track만 구독한다.
- store setter가 host와 viewer session을 상호배타적으로 전환하고 role이
일치하는 LiveKit connection만 함께 정리한다.
- store setter가 host와 viewer session을 상호배타적으로 전환한다.
- 역할별 LiveKit connection과 종료 lifecycle은 ADR-0044에 따라 feature
내부에서 관리한다.
- 화면 blur, unmount와 방 종료가 같은 idempotent publisher cleanup 경로를
재사용한다.
- 참여자 종료 결과는 권한이 있는 host RPC와 같은 `roomId`인지 검증한 뒤에만
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ CameraPage finalizationState
└─ IDLE → 촬영 결과 화면 또는 기존 카메라
```

종료 workflow와 재시도용 완료 결과 cache는 `RtcHostLiveKitPage`가,
종료 workflow와 재시도용 완료 결과 cache는 Host termination controller가,
presentation state snapshot은 CameraPage가 소유한다. overlay는 LiveKit이나
서버 API를 알지 않고 `RtcHostFinalizationState`만 입력받는다. VisionCamera와
LiveKit lifecycle은 기존 owner가 계속 관리한다.
Expand All @@ -87,7 +87,7 @@ LiveKit lifecycle은 기존 owner가 계속 관리한다.
- `isRtcFinalizationBlocking` type guard로 차단 상태를 두 단계로 제한했다.
- CameraPage가 `usePreventRemove`, hardware BackHandler, 최상단 pointer
overlay를 같은 조건으로 적용한다.
- overlay를 추가해도 Camera와 `RtcHostLiveKitPage`는 기존 component tree에
- overlay를 추가해도 Camera와 `RtcHostLiveKit`은 기존 component tree에
계속 마운트된다.
- 상태 분류, navigation 차단, overlay pointer 계약 테스트와 iOS/Android
Expo SDK 56 export를 통과했다.
5 changes: 5 additions & 0 deletions docs/architecture/adr-0026-rtc-viewer-waiting-entry.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ LiveKit 토큰 발급 실패는 자동 반복하지 않는다. 같은 SSE 연결
이벤트가 오면 viewer session을 먼저 무효화해 늦은 토큰 응답을 차단하고,
Alert 확인 후 피드 화면으로 이동한다.

발급된 LiveKit connection은 전역 RTC store가 아니라 이 entry hook의 현재
session epoch에 로컬로 유지한다. 나가기 lifecycle은 연결 전후 모두 서버 leave를
먼저 수행하는 ADR-0044의 Viewer exit controller가 담당한다.

## Context

기존 참여 폼은 join API와 LiveKit 토큰 API를 하나의 submit 흐름으로 묶었다.
Expand Down Expand Up @@ -71,6 +75,7 @@ LIVE 판정, token single-flight와 수동 retry를 담당한다. 상태 경계
- 반복 LIVE 이벤트에 의한 자동 token 재요청 방지
- 공용 SSE adapter와 역할별 lifecycle 분리
- ended와 진행 중 token 응답 사이 race 차단
- 이전 session epoch에서 늦게 완료된 token 응답의 연결 활성화 차단

포기하거나 제한된 것:

Expand Down
7 changes: 4 additions & 3 deletions docs/architecture/adr-0040-rtc-entity-boundaries.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ entities/rtc-room

entities/rtc-session
- 메모리 기반 Host/Viewer session identity
- LiveKit connection과 token 계약
- 역할 feature가 사용하는 LiveKit connection과 token 계약
- Host 인증 header 조합
- VisionCamera → LiveKit video publisher adapter

Expand Down Expand Up @@ -125,8 +125,9 @@ LiveKit과 VisionCamera를 연결하는 publisher는 외부 라이브러리 세
## Result

- 방 관련 API, schema, SSE와 Query Key는 `entities/rtc-room`에서만 export한다.
- Host/Viewer session, LiveKit connection과 publisher는
`entities/rtc-session`에서만 export한다.
- Host/Viewer session과 LiveKit connection 계약, publisher adapter는
`entities/rtc-session`에서 export한다. 실제 connection 상태는 역할 feature가
로컬로 소유한다.
- `entities/rtc-stored-photo`의 독립 Query와 만료 lifecycle은 유지한다.
- Reaction emoji 계약과 Query Key는 `features/rtc/reactions` 내부로 이동했다.
- 기존 endpoint, 요청 payload, RTC store 전이와 Camera/LiveKit lifecycle은
Expand Down
133 changes: 133 additions & 0 deletions docs/architecture/adr-0044-rtc-role-session-termination-lifecycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# ADR-0044: RTC 역할별 연결과 Session 종료 lifecycle

## Decision

Host와 Viewer의 LiveKit 연결 정보 및 명시적 종료 절차를 각 역할 feature가
소유한다. `entities/rtc-session` store에는 현재 프로세스의 Host/Viewer session
identity만 두고, 화면 연결에만 필요한 LiveKit URL/token은 전역 상태로 올리지
않는다.

```text
Host 종료 입력
(종료 메뉴 / 화면 뒤로가기 / Android back)
↓
host termination controller (single-flight)
↓ 사진 선택·업로드 준비
↓ publisher stop
↓ 서버 room end
↓ Viewer 결과 RPC
↓ LiveKit disconnect
↓ Host session clear
↓ 결과 화면
↓ 완료: Camera 유지 또는 이전 화면 이동

Viewer 나가기 입력
(대기 취소 / LiveKit 화면 닫기 / Android back)
↓
viewer exit controller (single-flight)
↓ 서버 participant leave
↓ LiveKit disconnect (연결된 경우)
↓ Viewer session clear
↓ Feed 이동
```

Host 종료 controller는 사진 준비, publisher 정리, 서버 종료, 결과 전달과 Room
정리의 실행 순서를 조정한다. Camera Page는 사진 선택 UI와 종료 API callback을
주입하고, LiveKit UI는 publisher/Room adapter를 주입한다. 종료 버튼과 화면
이탈은 같은 요청을 사용한다. 화면 이탈에서 시작된 종료도 사진 선택과 결과
전달을 생략하지 않으며, 결과 화면에서 `완료`를 선택한 뒤 이전 화면으로
이동한다.

Viewer exit controller는 LiveKit 연결 전후에 관계없이 서버 leave를 먼저
수행한다. 서버 leave가 실패하면 LiveKit 연결과 Viewer session을 유지해 같은
화면에서 재시도할 수 있다. 서버 leave 성공 후 로컬 disconnect가 실패한 경우는
서버 session을 되돌릴 수 없으므로 session 정리와 navigation을 계속하고,
component unmount cleanup에 마지막 disconnect를 맡긴다.

역할별 lifecycle UI도 사용하는 feature로 이동한다.

- `host-controls`: Host LiveKit publisher/Room, 공유 준비 Sheet
- `join-room`: Viewer SSE/token entry, LiveKit 구독, 대기 UI, exit controller
- `finalize-session`: 종료 사진 선택 UI와 업로드 파일 준비

Camera/Viewer/Result 화면 전체의 Widget 재설계는 이 결정에 포함하지 않는다.

## Context

기존 store는 Host/Viewer session과 `liveKitConnection`을 함께 보관했다. 하지만
Host는 Camera Page가 token 응답으로 별도의 `broadcastConnection`을 다시
만들었고, Viewer만 전역 connection을 실제 렌더링에 사용했다. 한 store의
필드가 역할별로 다르게 사용되어 연결 상태의 실제 소유자를 알기 어려웠고,
늦은 token 응답이 runtime 전역 상태를 변경할 수 있었다.

종료 흐름도 진입 위치에 따라 달랐다. Host의 종료 메뉴는 사진 저장, 서버 방
종료와 결과 RPC를 수행했지만 화면 뒤로가기는 component unmount cleanup만
실행했다. Viewer는 LiveKit 연결 전 취소에서는 서버 leave를 호출했지만 연결 후
나가기에서는 Room disconnect와 로컬 session clear만 수행했다. 그 결과 UI는
사라졌지만 서버에는 활성 Room 또는 participant가 남을 수 있었다.

## Alternatives

### 전역 RTC store에 모든 connection과 종료 상태 유지

페이지 간 연결 전달은 단순하지만 session identity, 일회성 token, Room 객체와
종료 진행 상태가 하나의 전역 수명으로 섞인다. Host/Viewer의 다른 연결 방식과
화면 unmount 시점을 store action이 알아야 한다.

### 각 UI event handler에서 필요한 cleanup 직접 수행

추가 abstraction은 적지만 종료 버튼, back handler와 연결 전후 화면마다 호출
순서가 다시 갈라진다. 새 종료 진입점이 추가될 때 서버 요청이나 cleanup 단계가
누락되기 쉽다.

### 역할별 local connection과 lifecycle controller 사용

연결 정보는 실제 연결 화면의 수명에 묶고, 여러 UI 진입점은 역할별 controller의
한 종료 명령으로 합친다. callback 경계가 추가되지만 서버 상태와 media cleanup의
순서를 한 곳에서 보장할 수 있다.

## Reason

세 번째 안을 선택했다. LiveKit token은 session identity가 아니라 특정 화면
연결을 위한 단기 자격이므로 소비하는 역할 feature가 소유하는 편이 수명과
일치한다. 반면 `roomId`, Host access token과 `participantId`는 여러 역할
use case가 공유하는 runtime session identity이므로 Entity store에 남긴다.

종료는 단순 UI callback이 아니라 서버와 media 상태를 함께 바꾸는 use case다.
역할별 controller가 single-flight와 순서를 소유하면 어떤 UI에서 시작하더라도
동일한 계약을 지킬 수 있고, API 실패 시 session을 지운 뒤 복구할 수 없는
상태를 피할 수 있다.

## Trade-off

얻는 것:

- Host/Viewer LiveKit token의 실제 소비자와 상태 소유자가 일치한다.
- 종료 버튼과 화면 이탈이 같은 사진 저장·서버 종료·cleanup 경로를 사용한다.
- Viewer 연결 전후의 나가기가 같은 서버 leave 정책을 사용한다.
- 중복 종료 입력은 controller의 진행 중 Promise를 재사용한다.
- 서버 종료 실패 시 runtime session을 유지해 재시도할 수 있다.
- lifecycle UI가 역할 feature public API를 통해 Pages에 제공된다.

포기하거나 제한된 것:

- 앱 프로세스가 종료되면 LiveKit 연결 정보와 RTC session은 복원되지 않는다.
- Host 서버 종료 후 Viewer RPC가 실패하면 재시도 시 새 Room 연결이 필요하다.
- LiveKit disconnect 자체가 실패해도 서버 종료가 확정된 뒤에는 session 종료를
되돌리지 않는다.
- Camera/Viewer/Result의 큰 화면 조합과 표시 전용 UI 이동은 후속 구조 개선
범위로 남는다.

## Result

- `useRtcStore`는 Host/Viewer session identity만 관리한다.
- 역할별 local connection은 공통 `url`, `token`만 표현하며 상태 소유 위치가
Host/Viewer 역할을 결정한다.
- Host token 응답은 Camera 흐름의 local `broadcastConnection`으로만 전달된다.
- Viewer token 응답은 `useRtcViewerEntry`의 local connection이 되며 session
epoch가 바뀐 뒤 완료된 응답은 무시한다.
- Host 종료 controller가 사진 선택, publisher stop, 서버 end, 결과 RPC와 Room
disconnect를 직렬화한다.
- Host 뒤로가기는 종료 완료와 결과 확인 전에는 route를 제거하지 않는다.
- Viewer exit controller는 서버 leave 성공 뒤에만 LiveKit과 session을 정리한다.
- 서버 leave/end 실패 시 현재 화면과 session identity를 유지한다.
1 change: 0 additions & 1 deletion src/entities/rtc-session/model/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from "./models";
export * from "./rtc-store";
export * from "./rtc-viewer-session";
4 changes: 0 additions & 4 deletions src/entities/rtc-session/model/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,7 @@ export interface RtcViewerSession {
participantId: string;
}

export type RtcLiveKitRole = "HOST" | "VIEWER";

export interface RtcLiveKitConnection {
role: RtcLiveKitRole;
url: string;
token: string;
expiresAt?: string;
}
40 changes: 7 additions & 33 deletions src/entities/rtc-session/model/rtc-store.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import { create } from "zustand";
import {
RtcHostSession,
RtcLiveKitConnection,
RtcViewerSession,
} from "./models";
import { RtcHostSession, RtcViewerSession } from "./models";

export interface RtcStore {
hostSession: RtcHostSession | null;
viewerSession: RtcViewerSession | null;
liveKitConnection: RtcLiveKitConnection | null;

setHostSession: (session: RtcHostSession) => void;
updateHostSessionExpiresAt: (roomId: string, expiresAt: string) => void;
Expand All @@ -17,25 +12,21 @@ export interface RtcStore {
setViewerSession: (session: RtcViewerSession) => void;
clearViewerSession: () => void;

setLiveKitConnection: (connection: RtcLiveKitConnection) => void;
clearLiveKitConnection: () => void;
clearRtcSession: () => void;
}

/**
* RTC 토큰은 SecureStore 등에 영속화하지 않고 현재 앱 프로세스의
* 메모리에만 보관합니다.
* RTC Host/Viewer session identity는 SecureStore 등에 영속화하지 않고
* 현재 앱 프로세스의 메모리에만 보관합니다.
*/
export const useRtcStore = create<RtcStore>()((set) => ({
hostSession: null,
viewerSession: null,
liveKitConnection: null,

setHostSession: (hostSession) => {
set({
hostSession,
viewerSession: null,
liveKitConnection: null,
});
},
updateHostSessionExpiresAt: (roomId, expiresAt) => {
Expand All @@ -51,43 +42,26 @@ export const useRtcStore = create<RtcStore>()((set) => ({
});
},
clearHostSession: () => {
set((state) => ({
set({
hostSession: null,
liveKitConnection:
state.liveKitConnection?.role === "HOST"
? null
: state.liveKitConnection,
}));
});
},

setViewerSession: (viewerSession) => {
set({
hostSession: null,
viewerSession,
liveKitConnection: null,
});
},
clearViewerSession: () => {
set((state) => ({
set({
viewerSession: null,
liveKitConnection:
state.liveKitConnection?.role === "VIEWER"
? null
: state.liveKitConnection,
}));
},

setLiveKitConnection: (liveKitConnection) => {
set({ liveKitConnection });
},
clearLiveKitConnection: () => {
set({ liveKitConnection: null });
});
},
clearRtcSession: () => {
set({
hostSession: null,
viewerSession: null,
liveKitConnection: null,
});
},
}));
11 changes: 0 additions & 11 deletions src/entities/rtc-session/model/rtc-viewer-session.ts

This file was deleted.

1 change: 1 addition & 0 deletions src/features/rtc/finalize-session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export {
} from "./lib/prepare-rtc-end-images";
export { resetMyRtcStoredPhotos } from "./lib/reset-my-rtc-stored-photos";
export { useResetMyRtcStoredPhotos } from "./model/use-reset-my-rtc-stored-photos";
export { RtcEndPhotoSelection } from "./ui/rtc-end-photo-selection";
Loading
Loading