Files
tech-log-frontend/docs/reviews/adapters/02-realtime-and-browser-rpc.md
DongHyeonkaandClaude Opus 5 4bff9ca151 chore: sync the frontend template from 4dc033c to 8157ad4
The product was materialized from the template at `4dc033c` and has stayed
on it through 43 template commits, so it was missing all three rounds of
adapter remediation — including files it never had, such as the shared
`abortable-operation` primitive and the `exact-snapshot` decoder that
later fixes are written against. Taking only the newest round was not
possible for that reason: the delta is coherent only as a whole.

The product had not touched `src/adapters` at all since materialization,
so the 140-file delta applied with a three-way merge and no conflicts.
`package.json` was the single overlap and merged cleanly: the product owns
`name`, the template contributed `check:adapter-inventory`,
`check:remediation-ledger` and the image-resolve-signal type fixture.
All 24 product-owned files — README, index.html, CI workflow, i18n
catalog, home page, generated schemas, evidence scripts, component and
visual snapshots — are byte-identical to `main`.

`template.lock.json` now pins the synced revision and tree.

Verified in this repository, not inherited from the template: six type
projects, lint, nine gates (adapter inventory, remediation ledger,
registries, diagnostics, realtime boundaries, architecture, browser
file/storage boundaries, optional recipes, documentation), the production
build, and 2,054 of 2,073 tests. The 19 failures are all in
`tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing
sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template
records; four suites that failed once under parallel load pass in
isolation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 12:04:58 +09:00

40 KiB

Realtime / Browser RPC adapter 구현 리뷰

  • 리뷰 기준: 4dc033c (2026-08-13, Asia/Seoul)
  • 구현 범위: src/adapters/realtime/**, src/adapters/browser-rpc/**
  • 추적 범위: 대응 contracts, application ports, bootstrap 조립, unit/boundary tests, architecture docs
  • 방식: 코드 리뷰만 수행했다. 이 문서 외 구현 파일은 수정하지 않았다.
  • 결론: Critical 0, High 4, Medium 3이다. R-01~R-06은 코드상 확정된 lifecycle/immutability/resource 문제이고, R-07은 문서에도 미완료라고 명시된 production promotion blocker다. 두 runtime 모두 현재 AVAILABLE_NOT_COMPOSED이므로 production traffic 사고로 과장하지 않는다.

1. 21/21 파일 inventory와 책임

아래 경로는 모두 저장소 루트 기준 full path이며, 범위의 구현 파일 21개를 모두 읽었다.

# full path 책임 주요 의존성 / downstream 판정
1 src/adapters/browser-rpc/browser-rpc-runtime.ts operation을 unary/server-stream application port로 bind하고 request schema/encoder, deadline/retry, transport, response schema/mapper, generation fence를 순서대로 집행 application/ports/browser-rpc, ClockPort, Browser RPC contract, schema/mapper registry, transport.ts, AppFailure R-01, R-04, R-06, R-07
2 src/adapters/browser-rpc/index.ts Browser RPC public adapter export surface runtime, transport, unavailable adapter 새 lease/install type export 필요
3 src/adapters/browser-rpc/transport.ts provider-neutral unary/stream transport result와 runtime identity 계약 src/contracts/browser-rpc.ts R-01, R-07
4 src/adapters/browser-rpc/unavailable-browser-rpc-transport.ts 선택되지 않은 runtime의 명시적 fail-closed Null Object transport.ts 유지; 새 stream lease shape만 맞춤
5 src/adapters/realtime/event-codec.ts raw JSON byte/shape/registry/schema 검증, immutable DTO와 semantic fingerprint 생성 realtime contracts, schema registry, JSON scanner, result codec 유지
6 src/adapters/realtime/event-consumer.ts SSE/WS cursor 규칙을 codec 결과와 결합하고 common stream coordinator outcome으로 투영 realtime ports/contracts, event codec, stream coordinator 유지
7 src/adapters/realtime/index.ts common realtime adapter public export surface codec, consumer, reconnect, handoff, stream, sub-index R-02/R-03 lifecycle type export 필요
8 src/adapters/realtime/json-member-scanner.ts JSON.parse 전 duplicate member와 structure budget을 비재귀적으로 검사 독립 utility 유지
9 src/adapters/realtime/live-poll-handoff-coordinator.ts LIVE/POLL 단일 effect writer, generation fence, quiescence/checkpoint, probe buffer와 전환 ClockPort, realtime result/ports R-03
10 src/adapters/realtime/polling/bounded-poll-coordinator.ts visible/online finite lease, single-flight poll, retry hint, response/apply deadline, non-cooperative task drain bounded polling policy, ClockPort, realtime result 유지
11 src/adapters/realtime/polling/index.ts polling public exports bounded poll coordinator 유지
12 src/adapters/realtime/reconnect-coordinator.ts 단일 reconnect owner, online gate, retry budget, session close authority, exact recovery proof, post-abort DRAINING reconnect policy, ClockPort, realtime ports/result 유지
13 src/adapters/realtime/reconnect-policy.ts immutable reconnect policy, full jitter, elapsed budget, Retry-After 계산/검증 독립 policy 유지
14 src/adapters/realtime/result.ts hostile/mutable collaborator result를 exact own-data snapshot으로 canonicalize realtime ports/contracts 유지; R-04의 기준 패턴
15 src/adapters/realtime/sse/fetch-sse-connection.ts fixed same-origin fetch-stream SSE, response/media/open gate, read/event deadline, cursor rule, bounded reader cancel ClockPort, event authority/result, parser, reconnect policy 유지; R-02 common drain과 함께 검증
16 src/adapters/realtime/sse/index.ts SSE public exports fetch connection, parser 유지
17 src/adapters/realtime/sse/sse-parser.ts strict incremental UTF-8 SSE parser, BOM/line ending/id/retry/event buffer ceiling realtime contracts/result 유지
18 src/adapters/realtime/stream-coordinator.ts per-stream sequential effect, dedupe/order, recovery, checkpoint/barrier, scope generation fence event authority port, realtime contracts, mapper registry, codec/result R-02
19 src/adapters/realtime/websocket/index.ts WebSocket connection/protocol public exports connection, protocol 유지
20 src/adapters/realtime/websocket/websocket-connection.ts 한 physical WS의 handshake/subscription/tombstone/FIFO/heartbeat/apply gate/recovery close ClockPort, realtime contracts/result, WS protocol 유지; R-02 upstream timeout과 함께 검증
21 src/adapters/realtime/websocket/websocket-protocol.ts exact closed JSON frame decode/encode, duplicate key/structure/sequence/frame byte 검증 realtime contracts, JSON scanner R-05

2. 추적한 contracts, ports, bootstrap, tests, docs

계층 읽은 파일과 근거 대조 결과
Realtime contracts src/contracts/realtime-streams.ts, src/contracts/realtime-events.ts registry가 stream/event/recovery/queue ceiling을 닫고 cursor/sequence/scope 문법을 소유한다. adapter가 이를 우회하지 않는다.
Realtime ports src/application/ports/realtime/shared.ts:1-66, src/application/ports/realtime/event-authority.ts:15-202, src/application/ports/realtime/index.ts:1-31 native error/payload/cursor 없는 closed result, exact recovery checkpoint identity, effect/recovery commit authority를 확인했다. R-02 lifecycle inspection 확장이 필요하다.
Browser RPC contract src/contracts/browser-rpc.ts:66-179,256-469,472-591 wire/profile/operation join과 hard limit은 풍부하지만 validate-only mutable binding이다(R-04). maxBufferedBytes는 선언/검증만 된다(R-07).
Browser RPC port src/application/ports/browser-rpc/browser-rpc.ts:4-35, src/application/ports/browser-rpc/index.ts application에는 typed unary/stream Result만 보이고 generated type/frame/endpoint는 노출되지 않는다. 변경 불필요.
Clock / Result src/application/ports/clock-port.ts, src/adapters/platform/system-clock.ts, src/application/result.ts, src/contracts/errors.ts injected clock/fence failure도 port Result 의미로 닫아야 한다(R-06).
Bootstrap src/bootstrap/optional-runtime-host.ts:21-29,65-70,87-92,142-150 realtime: null, health UNAVAILABLE, 제품 contribution 전 미조립은 의도다. Browser RPC 조립도 없다. 미조립 자체는 결함이 아니다.
Boundary gates scripts/check-realtime-boundaries.ts, scripts/lib/realtime-boundaries.ts, scripts/check-realtime-boundary-fixtures.ts, scripts/test-realtime-runtime-removal.ts; tests/fixtures/realtime-boundaries/allowed/**, forbidden/** native realtime API 소유권과 unselected composition을 정적 검사한다. Browser RPC에는 아직 같은 별도 boundary gate가 없다.
Realtime docs docs/architecture/decisions/VD-28-realtime-events-web-push-and-bounded-polling.md, docs/architecture/realtime-events-web-push-and-bounded-polling.md, docs/architecture/optional-adapter-recipes.md fixed endpoint, exact barrier, single writer, overflow fail-close, bounded cleanup/DRAINING, 미조립 상태를 코드와 대조했다.
Browser RPC docs docs/architecture/protobuf-browser-transport-and-rest-gateway.md, docs/architecture/decisions/VD-27-grpc-web-unary-and-server-stream.md, docs/architecture/decisions/VD-29-connect-web-and-browser-protobuf-runtime.md common lifecycle만 구현됐고 concrete framing/raw-byte/provider/browser conformance는 pending이라고 명시한다.

대조한 16개 테스트 파일:

  • tests/unit/browser-rpc/browser-rpc-contract.test.ts
  • tests/unit/browser-rpc/browser-rpc-runtime.test.ts
  • tests/unit/realtime/bounded-poll-coordinator.test.ts
  • tests/unit/realtime/bounded-polling-policy.test.ts
  • tests/unit/realtime/event-codec.test.ts
  • tests/unit/realtime/event-consumer.test.ts
  • tests/unit/realtime/fetch-sse-connection.test.ts
  • tests/unit/realtime/live-poll-handoff-coordinator.test.ts
  • tests/unit/realtime/realtime-reconnect-coordinator.test.ts
  • tests/unit/realtime/realtime-reconnect-policy.test.ts
  • tests/unit/realtime/realtime-stream-registry.test.ts
  • tests/unit/realtime/result.test.ts
  • tests/unit/realtime/sse-parser.test.ts
  • tests/unit/realtime/stream-coordinator.test.ts
  • tests/unit/realtime/websocket-connection.test.ts
  • tests/unit/realtime/websocket-protocol.test.ts

3. 분류

확정 결함

ID 심각도 확신도 요약
R-01 High High Browser RPC server-stream 종료가 non-cooperative iterator에서 무기한 멈춘다.
R-02 High High common stream coordinator가 non-cooperative effect/recovery 하나로 영구 wedge된다.
R-03 High High LIVE↔POLL overflow fail-close가 active lease를 잃어 이후 close가 거짓 성공한다.
R-04 High High Browser RPC bindings는 validate-then-use TOCTOU이며 exact immutable install이 아니다.
R-05 Medium High WS frame byte ceiling 전에 입력 전체 UTF-8 copy를 추가 할당한다.
R-06 Medium High Browser RPC clock/fence 예외가 Result 경계를 탈출하고 cleanup을 건너뛴다.

미조립 단계 promotion blocker / 선택 개선

ID 심각도 확신도 요약
R-07 Medium, promotion blocker High maxBufferedBytes의 concrete transport 집행 및 provider/browser conformance가 아직 없다. 문서에도 pending으로 명시되어 현재 common runtime bug로 세지 않는다.

4. 확정 결함 상세

R-01 — Browser RPC server-stream 종료가 non-cooperative iterator에서 무기한 멈춘다

  • 심각도: High
  • 확신도: High
  • 근거:
    • src/adapters/browser-rpc/transport.ts:53-61은 stream을 AsyncIterable 하나로 표현한다. 명시적 cancel/waitClosed/cleanup bound가 없다.
    • src/adapters/browser-rpc/browser-rpc-runtime.ts:488-512iterator.next()를 deadline과 race하지만, timeout 뒤 원래 next() task는 남을 수 있다.
    • src/adapters/browser-rpc/browser-rpc-runtime.ts:630-640finally에서 await iterator.return()을 deadline 없이 기다린다. pending next()가 signal을 무시하면 async generator의 queued return()도 완료되지 않는다.
  • 영향: caller abort, idle/total timeout, response limit, consumer break 뒤 application iterator completion이 무기한 pending이다. 외부 abort listener 수명도 finally 완료 전까지 닫히지 않는다. timeout Result를 선택했어도 iterator가 끝나지 않아 total deadline 의미가 깨진다.
  • 기존 증거와 gap: tests/unit/browser-rpc/browser-rpc-runtime.test.ts:343-369은 cooperative generator가 abort를 보고 finally로 끝나는 경우만 확인한다. docs/architecture/protobuf-browser-transport-and-rest-gateway.md:381-399는 reader cancel/release, bounded consumer queue, terminal envelope, EOF non-success를 요구한다.
  • 적용 패턴: Explicit Stream Lease + structured concurrency + retained DRAINING task. 암묵적인 AsyncIterable.return()에 transport lifecycle authority를 숨기지 않는다.
  • 결정:
    1. app-facing generator는 idle/total/limit/caller abort 후 cleanup bound 안에 끝난다.
    2. commit/admission generation은 즉시 fence한다.
    3. underlying task가 bound 안에 끝나지 않으면 transport lease는 DRAINING에 남고 실제 waitClosed() settlement까지 추적한다.
    4. return()/waitClosed() rejection은 이미 선택한 application failure를 덮지 않는다.

R-02 — common stream coordinator가 non-cooperative authority 하나로 영구 wedge된다

  • 심각도: High
  • 확신도: High
  • 근거:
    • src/adapters/realtime/stream-coordinator.ts:231-247은 event를 state.tail에 직렬 연결한다.
    • src/adapters/realtime/stream-coordinator.ts:373-408은 effect authority를 직접 await한다. AbortSignal을 무시하는 Promise에 deadline/drain state가 없다.
    • recovery는 src/adapters/realtime/stream-coordinator.ts:497-529에서 기존 tail을 기다리고 :543-560에서 recovery authority를 다시 무기한 기다린다.
    • close():809-834는 controller만 abort하고 즉시 void로 끝나 실제 settlement/DRAINING을 나타내지 않는다.
  • 영향: WS maxApplyMs(websocket-connection.ts:1060-1080)나 SSE event timeout(fetch-sse-connection.ts:321-355)은 transport caller만 끝낸다. common tail은 pending이라 새 generation event와 queue-overflow recovery까지 영구 대기한다. generation fence는 late commit을 막지만 liveness/resource convergence는 보장하지 않는다.
  • 기존 증거와 gap:
    • tests/unit/realtime/stream-coordinator.test.ts:383-466의 in-flight effect는 결국 resolve되고 :791-821의 non-cooperative recovery도 테스트 끝에서 settle한다. never-settling authority와 bounded close는 없다.
    • VD-28...md:218-224,250-256,437-446은 terminal/idempotent close와 bound를 넘긴 task가 실제 settle할 때까지 DRAINING을 유지하도록 정한다.
  • 적용 패턴: per-stream State Machine + Task Lease Registry + generation capability.
  • 결정:
    1. freshness와 별도로 lifecycle OPEN | DRAINING | CLOSED를 둔다.
    2. effect/recovery deadline에 commit capability를 영구 false로 만들고 abort한다.
    3. caller에는 IDLE_TIMEOUT (operation: APPLY | RECOVER, non-retryable)을 bounded하게 반환하고 실제 task는 retain한다.
    4. DRAINING 중 새 event/recovery를 허용하지 않는다. actual settle 뒤 STALE에서 authoritative recovery를 요구하거나 close 요청이면 CLOSED로 간다.
    5. close()Promise<RealtimeResult<void>>로 bounded quiescence 결과를 반환한다.

R-03 — LIVE↔POLL overflow 뒤 active writer reference를 잃는다

  • 심각도: High
  • 확신도: High
  • 근거:
    • src/adapters/realtime/live-poll-handoff-coordinator.ts:274-286은 active tail overflow 시 failClosed()를 호출한다.
    • failClosed():774-788은 controller를 abort한 뒤 active = null로 지우지만 해당 lease/tail을 retired set에 보존하지 않는다.
    • performClose():672-700은 현재 active/probe/quiescing/transitionCandidate만 모으므로 이미 버린 non-cooperative active writer를 기다리지 않고 success할 수 있다.
  • 영향: 256건/4MiB overflow로 generation 전체를 닫았지만 effect는 계속 실행 중이고 lifecycle owner가 추적하지 않는다. teardown success가 quiescence를 뜻하지 않아 새 runtime과 old task가 겹칠 수 있다. isCurrent()는 commit만 fence한다.
  • 기존 증거와 gap:
    • tests/unit/realtime/live-poll-handoff-coordinator.test.ts:166-215는 non-cooperative overflow를 만들지만 이후 close()를 호출하지 않는다.
    • :466-493의 close test는 reference를 잃기 전 active writer만 다룬다.
    • ADR VD-28...md:422-427,443-446은 overflow full-generation fail-close와 actual settlement까지 DRAINING을 요구한다.
  • 적용 패턴: Retired Lease Registry + two-phase close.
  • 결정: failClosed()는 모든 lease를 abort하고 retiredWriters에 옮겨 admission을 닫는다. close()는 current+retired를 dedupe해 bounded하게 기다리고, timeout에는 IDLE_TIMEOUT/CLOSE를 반환하되 마지막 tail settlement까지 DRAINING을 유지한다.

R-04 — Browser RPC bindings가 validate-then-use TOCTOU이다

  • 심각도: High
  • 확신도: High
  • 근거:
    • src/contracts/browser-rpc.ts:256-285define*는 shallow spread/freeze만 하고 exact own key/data descriptor를 검사하지 않는다. extra/accessor property가 남는다.
    • validateBrowserRpcContractBindings():330-469은 원본 registry/row를 읽어 true만 반환하며 installed snapshot을 만들지 않는다.
    • src/adapters/browser-rpc/browser-rpc-runtime.ts:102-128은 factory에서 검증한 뒤 bind() 때 원본 dependencies.*를 다시 읽는다.
    • src/adapters/browser-rpc/browser-rpc-runtime.ts:644-687도 validation용 runtime identity만 복사하며 operations/profiles/schema/mappers/encoders/transports 원본을 계속 사용한다.
  • 영향: TypeScript Readonly는 runtime 보호가 아니다. factory 이후 mutation으로 replay policy, attempt/deadline, byte ceiling, mapper/transport selection을 validation과 다르게 만들 수 있다. operation/profile 객체의 extra property도 transport가 해석할 수 있다.
  • 기존 증거와 gap: tests/unit/browser-rpc/browser-rpc-contract.test.ts:173-202는 raw invalid row를 재검증하지만 검증 후 mutation, accessor non-invocation, extra/symbol key 거절은 없다. realtime result.test.ts:15-151과 reconnect policy tests에는 exact descriptor snapshot 패턴이 이미 있다.
  • 적용 패턴: Parse/Validate/Install anti-corruption layer + immutable exact registry snapshot.
  • 결정:
    1. factory 시작 시 registry own descriptors를 한 번 캡처하고 null-prototype exact map으로 복사/freeze한다.
    2. operation/profile/encoder/schema/mapper/transport row를 허용 key의 own data property로 snapshot한다. getter, extra, symbol, revoked proxy는 composition-time TypeError다.
    3. runtime과 transport call은 installed snapshot만 사용한다.
    4. parse/map/encode/invoke function identity는 snapshot하되 row/registry를 재독하지 않는다.

R-05 — WS byte cap 전에 전체 UTF-8 copy를 할당한다

  • 심각도: Medium
  • 확신도: High
  • 근거: src/adapters/realtime/websocket/websocket-protocol.ts:215-228은 먼저 utf8ByteLength(input)을 호출하고 :469-470new TextEncoder().encode(input)으로 전체 크기의 두 번째 buffer를 만든다.
  • 영향: hostile/buggy server가 큰 text frame을 보냈을 때 negotiated cap으로 즉시 거절하지 못하고 cap 확인 전에 전체 UTF-8 copy를 추가 할당한다. browser가 원본 string을 materialize했다는 사실과 adapter의 추가 peak allocation은 별개다.
  • 기존 증거와 gap: tests/unit/realtime/websocket-protocol.test.ts:130-166은 결과 코드와 multibyte bytes는 확인하지만 pre-allocation reject는 확인하지 않는다. event-codec.ts:102-115raw.length > maxBytes 선검사를 이미 사용한다.
  • 적용 패턴: admission before allocation + bounded incremental accounting.
  • 결정: input.length > maxFrameBytes를 먼저 거절한다. 남은 입력은 allocation 없는 code-point loop로 UTF-8 bytes를 누적해 초과 즉시 중단하며 lone surrogate는 TextEncoder와 동일하게 replacement 3 bytes로 센다.

R-06 — Browser RPC collaborator exception이 Result 경계를 탈출한다

  • 심각도: Medium
  • 확신도: High
  • 근거:
    • unary src/adapters/browser-rpc/browser-rpc-runtime.ts:188-191,219-221,307-323clock.now()를 safe wrapper 없이 호출한다.
    • mapResponse():778-786,835-845generationFence.isCurrent()clock.now()도 throw를 잡지 않는다.
    • raceWithin():1094-1120은 abort listener를 붙인 뒤 clock.sleep() synchronous throw 또는 race 예외를 감싸는 finally가 없다.
    • unary 전체에 outer try/finally가 없어 linked.cleanup():198은 정상 finish() 경로에서만 보장된다.
  • 영향: application port가 Promise<Result<...>>/AsyncIterable<Result<...>> 대신 native rejection을 노출한다. clock/scope owner 실패 시 listener/timer cleanup과 observation도 빠질 수 있다.
  • 기존 증거와 gap: standard systemClock, 정상 fence, generation change는 테스트하지만 throwing clock/fence와 listener balance는 없다. bounded poll/reconnect는 safeNow, safeIsCurrent, finally cleanup을 이미 사용한다.
  • 적용 패턴: Result boundary guard + RAII-style finally.
  • 결정: clock failure는 SERVER_FAILURE/RPC_RUNTIME_DEPENDENCY_FAILED, capture/isCurrent 실패는 fail-closed SCOPE_GENERATION_CHANGED/RPC_SCOPE_GENERATION_UNAVAILABLE로 canonicalize한다. linked listener/timer는 단일 outer finally에서 정확히 한 번 해제한다.

5. 미조립/promotion blocker

R-07 — maxBufferedBytes 집행 증거가 없다

  • 심각도: Medium, production promotion blocker
  • 확신도: High
  • 확정 사실:
    • src/contracts/browser-rpc.ts:114-120,519-528maxBufferedBytes를 선언/검증한다.
    • common runtime은 src/adapters/browser-rpc/browser-rpc-runtime.ts:587-593에서 yielded message count/per-message/aggregate만 센다.
    • src/adapters/browser-rpc/transport.ts:58-60의 bare AsyncIterable에는 buffer admission/inspection contract가 없다.
  • 문서로 확인한 현재 상태: docs/architecture/protobuf-browser-transport-and-rest-gateway.md:42-61,363-366,381-399는 selected transport/raw-byte cap/provider-browser conformance가 pending이라고 명시한다. 따라서 common runtime이 wire framing/internal buffer를 직접 집행하지 않는 것 자체는 현재 결함이 아니다.
  • promotion 위험: callback/stock client가 consumer보다 빨리 frame을 쌓으면 common runtime이 item을 받기 전에 heap cap이 깨질 수 있다. maxTotalResponseBytes는 aggregate이고 maxBufferedBytes와 다른 backpressure 축이다.
  • 적용 패턴: transport conformance contract + enqueue-time backpressure admission.
  • 결정: concrete Connect/gRPC-Web transport가 enqueue 전에 operation.maxBufferedBytes, raw/decompressed ceiling을 집행하고 overflow 시 lease cancel + RESPONSE_BODY_LIMIT을 낸다는 conformance suite를 통과하기 전 bootstrap/product traffic을 금지한다. common runtime의 message/aggregate guard는 second line으로 유지한다.

6. 상태머신, protocol, framing, backpressure와 cleanup 결정

명시 결정 이유
Common stream state freshness UNKNOWN/CURRENT/STALE/RESYNCING와 lifecycle OPEN/DRAINING/CLOSED를 직교 축으로 둔다. timeout/abort 뒤 actual task가 남으면 DRAINING이다. commit fence와 resource settlement는 다른 사실이다(R-02).
Reconnect 기존 IDLE/RUNNING/DRAINING/CLOSED, 단일 retry owner, full jitter, exact bounded server hint, exact branded recovery proof를 유지한다. offline에는 retry timer를 두지 않고 protocol 자동 downgrade를 금지한다. 구현/ADR/test가 일치한다.
Poll lease 기존 single-flight IDLE/RUNNING/DRAINING/CLOSED, visible+online finite lease, one-request HTTP retry owner를 유지한다. non-cooperative execute/apply를 이미 fence+track한다.
LIVE↔POLL handoff Poll은 probe 동안 유일 authoritative writer다. old writer fence→abort→quiesce→checkpoint→buffer drain 뒤 LIVE를 활성화한다. overflow는 generation terminal이며 retired lease actual settlement까지 DRAINING이다. silent overlap/lost update 방지(R-03).
SSE framing strict UTF-8, blank-line terminated SSE, incomplete EOF discard, CURSOR일 때만 explicit id, exact status/media/same-origin 규칙을 유지한다. tests/docs와 일치한다.
WS framing text JSON + exact frame keys + duplicate-member/structure/uint64 검증을 유지한다. byte cap은 allocation 전에 집행한다. malformed/overflow는 whole generation close + snapshot recovery다. classic WS에는 receive pause가 없고 delta drop은 안전하지 않다.
Browser RPC framing common runtime은 logical message/terminal/failure만 받는다. Connect 5-byte envelope/EndStream과 gRPC-Web trailer authority는 concrete transport가 각각 소유하며 서로 추론/혼합하지 않는다. EOF alone은 success가 아니다. provider-neutral layer와 wire semantics를 분리한다.
Backpressure WS inbound/outbound와 bufferedAmount, handoff queues, Browser RPC transport buffer를 count+bytes로 admission한다. cap 초과는 silent drop/자동 상향 없이 terminal close/failure다. state-bearing delta의 부분 유실은 복구 없이는 안전하지 않다.
Cancel/timer/listener listener를 얻은 scope의 finally에서 제거하고 모든 sleep timer controller를 abort한다. non-cooperative task의 caller wait만 bounded하고 reference는 actual settlement까지 retain한다. bounded response와 resource convergence를 함께 만족한다.
Error semantics QUEUE_OVERFLOW=admission/backpressure와 recovery 필요, IDLE_TIMEOUT=handler/quiescence cleanup deadline, APPLY_FAILED=authority reject/throw/invalid result, PROVIDER_UNAVAILABLE=clock/host dependency 실패, PROTOCOL_MISMATCH=shape/framing 위반, SCOPE_FENCED=old generation. raw/native 원인은 노출하지 않는다. retry/rollback/운영 대응을 원인별로 닫는다.

7. 제안 인터페이스와 정확한 파일 작업

7.1 새/변경 interface signature

// src/adapters/browser-rpc/transport.ts
export type BrowserRpcStreamCancelReason =
  | "CALLER_ABORT"
  | "IDLE_TIMEOUT"
  | "TOTAL_DEADLINE"
  | "LIMIT_EXCEEDED"
  | "CONTRACT_FAILURE"
  | "CONSUMER_CLOSED";

export type BrowserRpcTransportStream = Readonly<{
  frames: AsyncIterable<BrowserRpcStreamFrame>;
  cancel(reason: BrowserRpcStreamCancelReason): void;
  waitClosed(): Promise<void>;
}>;

export type BrowserRpcTransport = BrowserRpcRuntimeBindingIdentity & Readonly<{
  invokeUnary?(call: BrowserRpcTransportCall): Promise<BrowserRpcUnaryTransportResult>;
  openServerStream?(call: BrowserRpcTransportCall): BrowserRpcTransportStream;
}>;
// src/contracts/browser-rpc.ts
export type InstalledBrowserRpcContractBindings = Readonly<{
  operations: Readonly<Record<string, BrowserRpcOperationV3>>;
  profiles: Readonly<Record<string, BrowserRpcProviderProfile>>;
  schemaCodecs: Readonly<Record<string, RuntimeSchemaCodec>>;
  mappers: Readonly<Record<string, InstalledBoundaryMapper>>;
  requestEncoders: Readonly<Record<string, BrowserRpcRequestEncoder>>;
  runtimeBindings: Readonly<Record<string, BrowserRpcRuntimeBindingIdentity>>;
}>;

export function installBrowserRpcContractBindings(
  bindings: BrowserRpcContractBindings,
): InstalledBrowserRpcContractBindings;
// src/adapters/browser-rpc/browser-rpc-runtime.ts
export type BrowserRpcRuntimeDependencies = Readonly<{
  // existing registries/collaborators stay
  streamCleanupTimeoutMs?: number; // default 2_000, implementation max 30_000
}>;
// src/application/ports/realtime/event-authority.ts
export type RealtimeStreamLifecycle = "OPEN" | "DRAINING" | "CLOSED";

export type RealtimeStreamInspection = Readonly<{
  lifecycle: RealtimeStreamLifecycle;
  // existing freshness/queue/dedupe/barrier fields unchanged
}>;
// src/adapters/realtime/stream-coordinator.ts
export type RealtimeStreamTaskLimits = Readonly<{
  effectTimeoutMs: number;
  recoveryTimeoutMs: number;
  drainTimeoutMs: number;
}>;

export type RealtimeStreamCoordinatorDependencies = Readonly<{
  // existing dependencies stay
  clock?: ClockPort;
  taskLimits: RealtimeStreamTaskLimits;
}>;

export type RealtimeStreamCoordinator = Readonly<{
  // existing methods stay
  close(): Promise<RealtimeResult<void>>;
}>;
// src/adapters/realtime/live-poll-handoff-coordinator.ts
export type LivePollHandoffState =
  | "LIVE_ACTIVE"
  | "POLL_ACTIVE"
  | "LIVE_PROBING"
  | "DRAINING"
  | "CLOSED";

export type LivePollHandoffInspection = Readonly<{
  // existing fields stay
  drainingWriters: number;
}>;

7.2 정확한 생성/수정/삭제/이동 목록

생성: 없음. lifecycle/install type은 기존 owner 파일에 둔다. 이 리뷰 문서 docs/reviews/adapters/02-realtime-and-browser-rpc.md만 리뷰 산출물로 새로 생성했다.

수정:

  1. src/contracts/browser-rpc.ts — exact descriptor snapshot installer와 installed type.
  2. src/adapters/browser-rpc/transport.ts — explicit stream lease/cancel/closed receipt.
  3. src/adapters/browser-rpc/browser-rpc-runtime.ts — installed snapshot만 사용, bounded stream cleanup/DRAINING, safe clock/fence, outer cleanup.
  4. src/adapters/browser-rpc/unavailable-browser-rpc-transport.ts — unavailable stream을 즉시 closed lease로 반환.
  5. src/adapters/browser-rpc/index.ts — installed/stream lifecycle types export.
  6. src/application/ports/realtime/event-authority.ts — stream lifecycle inspection.
  7. src/application/ports/realtime/index.tsRealtimeStreamLifecycle export.
  8. src/adapters/realtime/stream-coordinator.ts — bounded task lease registry, lifecycle state, async close.
  9. src/adapters/realtime/live-poll-handoff-coordinator.ts — retired writer set과 DRAINING convergence.
  10. src/adapters/realtime/index.ts — lifecycle/limit types export.
  11. src/adapters/realtime/websocket/websocket-protocol.ts — allocation-free bounded UTF-8 counter.
  12. tests/unit/browser-rpc/browser-rpc-contract.test.ts — mutation/accessor/extra-key installer tests.
  13. tests/unit/browser-rpc/browser-rpc-runtime.test.ts — non-cooperative stream, throwing clock/fence, cleanup balance tests와 fixture lease 전환.
  14. tests/unit/realtime/stream-coordinator.test.ts — never-settling effect/recovery, DRAINING/async close tests.
  15. tests/unit/realtime/live-poll-handoff-coordinator.test.ts — overflow 뒤 retired writer close test.
  16. tests/unit/realtime/websocket-protocol.test.ts — oversize preflight/multibyte/lone-surrogate tests.
  17. docs/architecture/protobuf-browser-transport-and-rest-gateway.md — stream lease, buffer owner, promotion evidence.
  18. docs/architecture/realtime-events-web-push-and-bounded-polling.md — common stream/handoff DRAINING와 error semantics.
  19. docs/architecture/decisions/VD-28-realtime-events-web-push-and-bounded-polling.md — actual-settlement lifecycle amendment.

삭제: 없음.

이동: 없음.

의도적으로 변경하지 않음: src/bootstrap/optional-runtime-host.ts는 제품/provider 선택 전 null/UNAVAILABLE 유지가 맞다. src/application/ports/browser-rpc/browser-rpc.ts의 app-facing API도 변경할 필요가 없다.

8. TDD 테스트 계획

먼저 아래 테스트를 실패시키고(red), 최소 구현 후 개별 green, 마지막에 전체 범위를 실행한다.

테스트 이름 입력/준비 기대 결과
bounds_non_cooperative_stream_cancel_and_completes_consumer Browser RPC stream의 next()waitClosed()가 signal/cancel을 무시; idle deadline 진행 caller iterator는 cleanup bound 안에 REQUEST_TIMEOUT 후 done; cancel("IDLE_TIMEOUT") 1회; lease DRAINING
rejects_new_stream_while_prior_lease_is_draining 위 stream actual settlement 전 같은 transport에 두 번째 open network side effect 없이 SERVER_FAILURE/RPC_STREAM_DRAINING; old settle 후 새 open 가능
consumer_break_cancels_and_bounds_stream_cleanup 첫 message 뒤 consumer break; close non-cooperative cancel("CONSUMER_CLOSED"); generator return bounded; listener/timer 0
runtime_snapshots_bindings_before_later_mutation factory 뒤 원본 operation retry/deadline/profile/transport map mutation execute는 installed snapshot만 사용; mutation이 의미 변경 불가
binding_installer_rejects_extra_and_accessor_keys_without_invoking_them operation/profile/registry에 getter, symbol, extra key getter 호출 0; composition-time TypeError
returns_canonical_failure_and_cleans_listener_when_clock_throws transport 전/후 clock.now/sleep synchronous throw; listener-counting signal rejection 없음; 지정 AppFailure; listener/timer 0; observation 1회
fences_when_generation_fence_throws capture 또는 isCurrent throw SCOPE_GENERATION_CHANGED/RPC_SCOPE_GENERATION_UNAVAILABLE; mapped value 미commit
keeps_stream_draining_until_non_cooperative_effect_actually_settles effect Promise never settles; fake clock가 effect/drain deadline 진행 accept는 bounded IDLE_TIMEOUT/APPLY; isCurrent=false; DRAINING; 새 effect 0
bounds_non_cooperative_recovery_and_rejects_late_checkpoint recovery가 timeout 뒤 늦게 success checkpoint 반환 bounded IDLE_TIMEOUT/RECOVER; late checkpoint 미commit; settle 후 STALE/recovery 필요
close_waits_for_all_tracked_stream_tasks_and_times_out effect와 recovery pending 중 close controller 모두 abort; bound 뒤 IDLE_TIMEOUT/CLOSE; settlement까지 DRAINING, 이후 CLOSED
close_after_active_queue_overflow_tracks_retired_writer handoff active effect never settles, queue cap 초과 후 close overflow QUEUE_OVERFLOW; close 즉시 success 금지; bound 뒤 IDLE_TIMEOUT; late settle 시 draining 0/CLOSED
rejects_oversized_ascii_frame_before_utf8_copy "x".repeat(maxFrameBytes + 1) FRAME_TOO_LARGE; full-size byte copy 경로 없음
counts_multibyte_and_lone_surrogate_like_text_encoder ASCII/2-byte/3-byte/surrogate pair/lone surrogate 경계 기존 byte 의미와 동일한 exact accept/reject
transport_conformance_enforces_max_buffered_bytes_before_enqueue push/callback fake transport가 consumer 정지 중 cap+1 byte enqueue enqueue 거부, lease cancel, raw/message 미노출; provider suite 없이는 promotion 금지

실행 명령:

corepack pnpm exec vitest run tests/unit/browser-rpc/browser-rpc-contract.test.ts
corepack pnpm exec vitest run tests/unit/browser-rpc/browser-rpc-runtime.test.ts
corepack pnpm exec vitest run tests/unit/realtime/stream-coordinator.test.ts
corepack pnpm exec vitest run tests/unit/realtime/live-poll-handoff-coordinator.test.ts
corepack pnpm exec vitest run tests/unit/realtime/websocket-protocol.test.ts
corepack pnpm exec vitest run tests/unit/realtime tests/unit/browser-rpc --reporter=dot --maxWorkers=4
corepack pnpm run check:types:app
corepack pnpm run check:types:test
corepack pnpm run check:realtime-boundaries
corepack pnpm run check:realtime-boundaries:fixture

제품 transport 선택 시 별도 필수 evidence:

# 실제 provider contribution이 script 이름과 target browser matrix를 고정해야 한다.
corepack pnpm run test:browser-rpc-transport-conformance
corepack pnpm run test:browser-rpc-target-browsers

9. compatibility, migration, rollback

Migration 순서:

  1. 새 tests와 lifecycle inspection을 먼저 추가한다. runtime은 미조립 상태라 production traffic 영향은 없다.
  2. createBrowserRpcRuntime은 raw input을 받아 내부에서 installer를 호출해 기존 caller signature를 유지한다. mutation에 의존한 fixture는 composition-time 오류로 고친다.
  3. 한 migration release 동안 기존 AsyncIterable transport를 internal adapter로 BrowserRpcTransportStream에 감쌀 수 있다. deprecated wrapper의 waitClosediterator.return() settlement이고 common cleanup bound가 이를 감싼다. provider 선택 전 legacy branch를 제거한다.
  4. common stream close(): Promise<Result>로 바꾸고 모든 test/향후 composition owner는 await한다. 기존 fire-and-forget 호출은 typecheck로 식별한다.
  5. handoff retired set을 도입하고 DRAINING에는 writer/probe admission을 막는다.
  6. WS byte counter는 wire/error shape가 같아 독립적으로 먼저 적용할 수 있다.
  7. actual provider/browser/load evidence와 R-01~R-07 closure 전까지 AVAILABLE_NOT_COMPOSED를 유지한다. bootstrap composition은 마지막 단계다.

Compatibility 결정:

  • application-facing Browser RPC unary/stream port shape는 유지한다.
  • wire protocol, frame shape, failure kind, retry owner는 바꾸지 않는다.
  • RealtimeStreamInspection.lifecycle는 additive다. close 반환형은 source-compatible fire-and-forget일 수 있으나 lifecycle correctness를 위해 owner는 await하도록 migration한다.
  • exact installer가 과거 extra/accessor/mutable row를 거절하는 것은 의도된 fail-closed tightening이다.

Rollback 순서:

  1. traffic admission을 DISABLED로 전환한다.
  2. connection/runtime lifecycle을 DRAINING으로 만들고 actual leases settlement 또는 bounded failure를 기록한다.
  3. 살아 있는 lease를 버리고 즉시 이전 runtime을 열지 않는다.
  4. source commit을 revert하되 installed snapshot과 allocation-before-cap 수정은 보안/정확성 강화이므로 우선 유지한다.
  5. cursor/checkpoint를 합성하지 않고 authoritative snapshot recovery를 수행한다.
  6. SSE↔WS, Connect↔gRPC-Web↔REST, live↔Poll을 장애 때문에 즉석 자동 전환하지 않는다. fallback은 registry/ADR에 선언된 새 semantic operation/generation으로만 시작한다.

10. 유지할 좋은 설계

  1. RealtimeFailure/AppFailure로 native error, raw close reason, payload, cursor, provider metadata를 경계 밖에 내보내지 않는다.
  2. realtime result/registry의 exact own-data snapshot, accessor 거절, immutable recovery checkpoint object identity.
  3. fixed same-origin SSE/WS endpoint, URL/subprotocol credential 금지, exact media/subprotocol 검증.
  4. SSE, WebSocket, Browser RPC stream, Poll을 서로 다른 delivery/protocol 의미로 유지하고 자동 downgrade/replay하지 않는다.
  5. WS inbound/outbound FIFO, count+byte+bufferedAmount ceiling과 overflow whole-generation recovery.
  6. reconnect의 단일 retry owner, full jitter, hint not-before, finite budget, stable proof 뒤 reset, post-abort DRAINING.
  7. Poll의 visible/online finite single-flight lease와 non-cooperative execute/apply tracking.
  8. LIVE↔POLL의 one-writer generation, probe buffer, activation 전 quiescence/checkpoint.
  9. SSE parser의 incremental strict UTF-8, incomplete EOF discard, bounded reader cancellation.
  10. unavailable Browser RPC adapter와 optional runtime host의 null/UNAVAILABLE; 조용한 network fallback이 없다.

11. false-positive 방지 대조

의심 항목 최종 판정과 근거
Realtime/Browser RPC가 bootstrap에 조립되지 않음 결함 아님. optional-runtime-host.ts:91-92,143와 architecture docs가 제품 선택 전 미조립을 요구한다.
Common Browser RPC가 Connect/gRPC-Web raw framing을 decode하지 않음 결함 아님. protobuf...md:57-61,381-399상 concrete transport 책임이다. R-07은 이 미완료 상태를 무시한 promotion만 막는다.
Reconnect가 offline 동안 timer 없이 기다림 의도. ADR과 realtime-reconnect-coordinator.test.ts:370-406가 explicit online signal을 요구한다.
healthy session waitClosed()에 deadline 없음 의도. ADR은 abort 후 drain만 bounded하고 active close receipt는 authoritative하게 기다린다.
WS overflow에서 일부 event drop 대신 connection close 의도. ADR과 websocket-connection.test.ts:579-651은 receive pause 없는 classic WS에서 snapshot recovery를 택한다.
SSE 204와 incomplete EOF 각각 terminal/no reconnect와 incomplete discard가 맞다. fetch/parser tests가 확인한다.
exact recovery object identity 의도된 capability token이다. event-authority.ts:17-23, reconnect/stream barrier tests가 clone/forgery를 막는다.
Handoff overflow 자체 이미 fail-close한다. R-03은 overflow 판정이 아니라 그 직후 retired tail reference를 잃는 cleanup bug다.
Transport에 effect timeout이 이미 있음 transport caller는 bounded해도 common state.tail은 settle하지 않는다. R-02는 commit fence가 아니라 retained task/liveness 문제다.

12. baseline 검증

  1. corepack pnpm exec vitest run tests/unit/realtime tests/unit/browser-rpc --reporter=dot --maxWorkers=4
    • exit 0, 16 files / 185 tests passed.
  2. corepack pnpm run check:realtime-boundaries
    • exit 0, Realtime boundaries: PASS (src).
  3. check:realtime-boundaries:fixture wrapper는 이 sandbox에서 child-process 제한 때문에 진단 없이 exit 1이었다. 같은 allowed/forbidden child 명령을 직접 실행해 allowed exit 0, forbidden exit 1과 세 규칙 UNSELECTED_REALTIME_RUNTIME_COMPOSED, PRESENTATION_INTERVAL_OWNER, NATIVE_REALTIME_API_OUTSIDE_ADAPTER를 확인했다. adapter defect로 세지 않는다.
  4. test:realtime-removal의 별도 복제에서 범위 tests는 통과했으나 저장소 전체 baseline의 CI authority count drift, 누락 .npmrc, child spawnSync ... EPERM, architecture report 문제로 최종 exit 1이었다. 검토 범위 failure 증거로 사용하지 않는다.

13. 구현 우선순위

  1. R-03 retired writer tracking: 국소적이고 확정적인 cleanup bug다.
  2. R-02 common stream task lifecycle: SSE/WS 양쪽 liveness 기반을 닫는다.
  3. R-01 Browser RPC explicit stream lease와 bounded cleanup.
  4. R-04 installed immutable bindings, 이어 R-06 exception/cleanup guard.
  5. R-05 allocation-before-cap 제거.
  6. R-07 concrete transport conformance는 provider 선택과 함께 수행하되 완료 전 production composition을 금지한다.