Skip to content

Owner·Auth·Connectivity 수명주기 계약 (이슈 #1335, A07)

한 문장 — 계정(owner)이 바뀌거나 떠날 때 "누가 세대를 세고, 어떤 순서로 정리하고, 늦은 응답을 누가 거르는가"를 runtime 세 개와 계약 하나로 고정한다. 유저 A 에서 시작한 결과는 B 에게도, A 의 새 로그인에도 적용되지 않는다.

  • 이슈: #1335 (계획 ID A07, Phase 2-2). 선행 A01 §5(취소 신호)·B01 §5(보존 정책)·§6(오류 분류)·S08 "Lifecycle and Ordering"(초안 보존 순서). 계약 타입 = G03 §8 OwnerScope{userId, epoch}.
  • 코드: src/react/app/ownerRuntime.ts(정본) · app/ownerStoreSetHost.ts · app/authSessionRuntime.ts · app/connectivityRuntimeHost.ts · 변환 어댑터 controllers/authSessionRevision.ts · 조립 appController.tsx·controllers/remoteDataController.ts.
  • 검사: tests/react/ownerRuntime·ownerStoreSetHost·authSessionRuntime·ownerSwitchFixture·ownerStores·authSessionFailurePolicy·authDegradedBootstrap·authConnectivityCoordination·connectivityRuntime. 픽스처 tests/support/ownerSwitch.mjs·authSessionRuntime.mjs.

1. 유저 A 의 로그아웃 한 건

A 가 홈에서 PR 대시보드를 불러오는 중에 로그아웃하고, 같은 폰에서 B 가 로그인한다.

  1. 로그아웃 순간 owner runtime 이 떠나기를 돈다: ① A 세트의 작성 중 초안을 기기에 저장하기 시작하고(S08) ② A 에 등록된 자원(요청 취소 신호·연결 runtime·구독)을 등록 역순으로 정리하고 ③ 세대(epoch)를 올리며 A 세대의 취소 신호를 발화하고 ④ 구독자에게 알린다 — store 세트 host 가 여기서 빈 세트를 만든다. 전부 동기이고 React 렌더보다 먼저다.
  2. A 의 PR 대시보드 요청은 ③ 에서 transport 까지 끊긴다. 끊기기 전에 이미 서버가 답을 보냈어도, 답을 받는 쪽은 "시작할 때 잡아 둔 scope 가 아직 현재인가"(isCurrent)를 보고 버린다.
  3. B 가 로그인하면 다시 ①~④ 가 돈다(비어 있던 owner 에서 B 로). B 의 세트는 초기 상태 리터럴이다 — A 의 토스트·초안·조회 결과가 남을 자리가 없다.
  4. A 가 곧바로 다시 로그인하면 세대가 또 오른다(로그아웃 1, 로그인 1). 로그아웃 전 비행 결과는 A 의 새 세대에도 적용되지 않는다. A 의 초안 복원은 ① 의 저장이 끝난 뒤에 읽는다(awaitLeave(A), S08 보존 꼬리).

2. runtime 세 개와 책임

runtime파일책임하지 않는 것
OwnerRuntimeapp/ownerRuntime.ts세대(epoch)의 유일한 카운터 · 떠나는 순서(보존 → 폐기 → 무효화 → 알림) · owner 별 AbortSignal · 자원 등록/폐기 · awaitLeave인증 증거를 모른다. 기기 저장물을 지우지 않는다
AuthSessionRuntimeapp/authSessionRuntime.ts부팅(기기 세션 흔적 → 로컬 채택 → 서버 확인 → 셸 적재) · SDK 이벤트(SIGNED_IN/SIGNED_OUT/TOKEN_REFRESHED/USER_UPDATED) · degraded 재시도 타이머 · 세 사건에 이유 부여feature 조회 성공/실패를 인증 상태로 합치지 않는다. 데이터 5xx 는 degraded(연결)이지 로그아웃이 아니다
ConnectivityRuntime hostapp/connectivityRuntimeHost.ts (본체 controllers/connectivityRuntime.ts 불변)owner scope 하나에 묶어 leader/follower·probe·rehydrate 조립 · offline listener · 폐기 반복 안전인증 상태를 바꾸지 않는다. 로그아웃 정리(snapshot purge)와 분리
store 세트 hostapp/ownerStoreSetHost.tsowner 전환 때 세트 교체를 runtime 순서 안에서(① preserveOwnerStoresresetOwnerStores ④ 새 세트)React 를 모른다. 조립 루트는 current 를 읽기만

세 runtime 은 앱 셸(appController)이 인스턴스당 하나씩 만들어 주입한다 — 전역 mutable singleton 이 아니다.

3. 세대(epoch)와 변환 경계

  • 정본은 OwnerRuntime.capture()OwnerScope{userId, epoch}. 명령·조회·캐시는 시작할 때 scope 를 잡고, 결과를 적용하기 전에 isCurrent(scope) 로 비교한다. 같은 사용자라도 epoch 가 다르면 다른 scope 다.
  • 변환 어댑터 controllers/authSessionRevision.ts: 기존 호출부가 쓰는 AuthRequestScope{revision, userId}revision = epoch 인 이름 바꾸기다. 숫자·의미가 같아 두 값을 섞어 비교해도 된다. ownerScopeControllerremoteUserRevisionRef 도 runtime epoch 를 읽는다(렌더 중 카운팅 삭제).
  • 제거 조건: authSessionRevision.ts 를 import 하는 파일이 0 이 되면 삭제한다. 그 전까지 새 카운터를 만들지 않는다. 지금 남은 것: remoteDataController·connectivityController(state.scope)·connectivityRuntime·authBootstrapPolicy·screenLoadPolicyController 등이 AuthRequestScope 타입을 쓴다 — A08(resource cache)이 OwnerScope 로 키를 바꾸면서 줄인다.
  • 남은 별도 카운터 둘은 세대가 아니다: authBootstrapRevisionRef(부팅 비행 번호 — 같은 세대 안에서 재시도를 구분)·authSessionLifecycleRevision(인증 이벤트 순번 — PR 대시보드 조회가 이벤트 사이 재진입을 막는 용도). 둘 다 owner 가 바뀌면 함께 오르지만 owner 판정에는 쓰지 않는다.

4. 떠나는 순서 계약

OwnerRuntime.adopt(next) / leave(reason) 안에서 동기로:

단계누가무엇
① preserveregister(hook, {phase:"preserve"}) 등록 순서대로이전 세트 초안 보존 시작(prepareWorkoutDraftForOwnerChange). 돌려준 Promise 는 awaitLeave(previousUserId) 에 이어진다. 실패해도 전환은 막지 않는다(마지막으로 끝난 기기 저장물이 복원 기준)
② disposeregister(hook) 등록 역순연결 runtime host 폐기·세트 리셋(resetOwnerStores)·기타 등록 자원. 돌고 나면 자동 해제
③ 무효화runtimeepoch +1, 이전 세대 AbortSignal 발화, 새 signal 발급
④ 알림subscribe 리스너store 세트 host 가 새 세트 생성. 조립 루트는 다음 렌더에서 읽기만

React 쪽(appController layoutEffect)은 자기 소유 React 상태만 리셋하고 hydrateAppShellSnapshotForAdoptedOwner 를 시작한다. 첫 로그인('' → A)도 세트를 바꾼다(로그인 전 화면 상태가 첫 owner 로 새지 않게).

5. 취소 신호 규칙

  • OwnerRuntime.signal() 은 현재 세대의 AbortSignal 이다. 셸 hydration(loadAppShellData)·PR 대시보드·볼륨·세션 상세 요청은 이 신호를 transport 까지 전달한다. 리스너는 요청이 끝나면 해제한다(세대 동안 누적 금지).
  • 취소는 서버 미커밋의 증거가 아니다. 늦은 write receipt 의 서버 확정 여부와 화면 적용 권한은 다른 질문이다: durable 행(기기 대기열)은 userId 기준으로 다음 세션까지 남고(isSameOwnerUser), 화면·cache·toast 에 적용할 권한만 epoch 로 제한한다.
  • 구독 해제·자원 해제·stop()·dispose() 는 몇 번 불러도 안전하다(테스트가 잠근다).

6. 세 사건과 보존 정책 (B01 §5 표를 소비)

사건진입OwnerLeaveReason초안·대기열화면·기기 사본
명시 로그아웃SIGNED_OUT 이벤트 / profileController.runSignOutsigned_out보존purgeAuthenticatedOwnerData(profileController)
세션 만료종료 오류(isAuthSessionError: 폐기 refresh token 등)expired (만료 문구와 함께)보존셸 전환만
계정 삭제profileController.deleteAccount — 서버 200/410 deleted삭제(초안·보관함·대기열, controller 가 아니라 profileController 가 성공 확인 뒤)정리
계정 전환다른 사용자 채택switched보존세트 교체

runtime 은 어느 경우에도 기기 저장물을 지우지 않는다. 인증 오류 분류는 B01 services/auth/authErrors.ts 를 API port 로 소비한다 — provider 별 인증 구현을 복제하지 않는다.

7. 데이터 오류 ≠ 인증 오류

  • 셸·프로필 서버의 5xx·조회 실패 → dataStatus/연결 runtime(degraded, 재시도)로. 인증 상태·owner 는 그대로.
  • 401 → resolveScopedUnauthorizedSession 이 같은 scope 의 세션을 다시 확인한 뒤에만 로그아웃(한 번의 빈 세션 읽기는 토큰 회전 유예 350ms 뒤 재확인).
  • 행동 테스트: authSessionRuntime.test.mjs("5xx 는 degraded 이지 로그아웃 아님")·authSessionFailurePolicy.test.mjs·authDegradedBootstrap.test.mjs.

8. 남은 손 리셋과 A08 인계

  • remoteDataController.resetRemoteUserData 는 아직 feature 조회 ref 약 50개를 손으로 비운다(PR 상세·피드·검색·카탈로그·세션 상세 캐시). 연결 runtime 폐기는 owner 자원 등록이 정본이고, 이 함수 안의 disposeConnectivityRuntime() 은 안전망(이미 비어 있어 no-op)이다. A08 이 이 ref 들을 ResourceCachePort.invalidateOwner(owner) 로 흡수하면서 목록을 지운다 — 새 화면 조회는 이 목록에 줄을 더하지 말고 owner 자원으로 등록하거나 resource cache 를 쓴다.
  • A08 이 가져갈 지점:
    • scope 캡처/비교: ownerRuntime.capture()isCurrent(scope)(또는 isSameOwnerScope). 요청 시작과 결과 채택 두 곳.
    • owner 종료 신호: ownerRuntime.signal()(transport 취소) · subscribe(transition)(cache 의 invalidateOwner(previous.userId)) · transition.reason(deleted 면 durable 까지, 그 밖은 화면 cache 만).
    • 자원 등록/해제: ownerRuntime.register(dispose) → 해제 함수. 세대가 끝나면 자동으로 돈다. 반복 해제 안전.
    • 픽스처: tests/support/ownerSwitch.mjs(harness.runtime·deferred().scope/signal·isStale) 가 제품 runtime 으로 세대를 센다.
  • 이 이슈에서 옮기지 않은 것: 피드·검색·카탈로그·연도 활동·PR 상세 조회는 scope 비교만 하고 취소 신호는 받지 않는다(A08 resource 화 대상). screenLoadPolicyController(ownerId, ownerRevision) 쌍은 어댑터 경유로 동작한다.
  • A08 반영(2026-09-08, 이슈 #1338): resources/resourceStore.ts 가 위 세 지점(scope 캡처/비교·전환 구독·요청 취소)을 store 하나로 흡수했다 — Owner 범위 조회 캐시 계약. 첫 소비자 = 완료 세션 상세: resetRemoteUserData 의 세션 상세 줄 5개가 사라졌고, 남은 ref 목록은 그 문서 §10 표가 화면 담당(A09~A11)별로 든다.