fix: install bounded Browser RPC stream leases

R-04: install the RPC contract bindings as exact immutable snapshots. Registry
and row data are copied from own data descriptors into frozen null-prototype
maps before validation, so a getter is never invoked, extra and symbol keys and
malformed descriptors are composition-time TypeErrors, and the runtime reads
only the snapshot. A post-validation mutation can no longer change replay
policy, deadlines, byte ceilings or transport selection.

R-01: bound transport stream cleanup. The generation is fenced and listeners
released immediately, and iterator.return() is awaited only within a cleanup
bound, so a non-cooperative iterator cannot keep the application generator, its
listeners or the total deadline alive. Unresolved cleanup stays observed.

R-05: reject oversized WebSocket text frames before allocating an encoded copy
and count UTF-8 bytes incrementally with an early exit, matching TextEncoder for
surrogate pairs and lone surrogates.

R-06: canonicalise clock and generation-fence failures into the closed Result
taxonomy instead of letting them escape as native rejections, with listener and
timer cleanup on every exit path.

Browser RPC remains AVAILABLE_NOT_COMPOSED; R-07 transport evidence is still
required before composition.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-14 00:00:15 +09:00
co-authored by Claude Opus 5
parent 2f29ccbf1a
commit 8f67974f68
7 changed files with 585 additions and 38 deletions
@@ -19,6 +19,37 @@
- 운영 절차:
[API contract와 server-state recovery](../operations/api-contract-and-server-state-recovery.md)
## Installed binding snapshot과 stream cleanup bound (R-01, R-04, R-05, R-06)
- `installBrowserRpcContractBindings()`가 registry를 **parse → validate →
install** 순서로 처리한다. own data descriptor만 읽어 exact key set으로
null-prototype frozen snapshot을 만들고, 그 snapshot을 검증한 뒤 설치한다.
getter/accessor, extra key, symbol key, malformed descriptor, revoked proxy는
composition-time `TypeError`이며 getter는 호출조차 되지 않는다. runtime과
transport call은 이후 snapshot만 읽으므로 validation 이후 registry mutation이
replay policy·deadline·byte ceiling·transport selection을 바꿀 수 없다.
- server stream 종료는 transport iterator에 lifecycle authority를 위임하지
않는다. commit/admission generation은 즉시 fence하고 listener는 바로 해제하며,
`iterator.return()`은 cleanup **요청**으로서 bound 안에서만 기다린다. 끝나지
않은 cleanup은 관찰만 유지되고(unhandled rejection 없음) application generator는
bound 안에 종료된다. cleanup rejection은 이미 선택된 application failure를
덮지 않는다.
- WebSocket text frame은 allocation 전에 admission한다. UTF-16 code unit 길이가
이미 cap을 넘으면 encoder를 만들지 않고 거절하고, 나머지는 early exit하는
code-point 누적으로 센다. valid surrogate pair는 4 bytes, lone surrogate는
`TextEncoder`와 동일하게 replacement 3 bytes다.
- clock/fence collaborator 예외는 Result 경계를 벗어나지 않는다. clock 실패는
`SERVER_FAILURE/RPC_RUNTIME_DEPENDENCY_FAILED`, capture 실패는
`SCOPE_GENERATION_CHANGED/RPC_SCOPE_GENERATION_UNAVAILABLE`, `isCurrent` 실패는
fail-closed로 canonicalize하며 listener/timer는 단일 exit path에서 정확히 한 번
해제한다.
Browser RPC는 여전히 `AVAILABLE_NOT_COMPOSED`다. 선택된 Connect/gRPC-Web
transport는 enqueue-time `maxBufferedBytes`, raw/decompressed ceiling,
cancel/closed receipt, terminal framing, target browser와 load behavior를
별도로 증명해야 조립할 수 있다 (R-07).
## 1. 먼저 축을 분리한다
네 이름은 같은 종류의 대안이 아니다.
@@ -104,12 +104,12 @@ Rollout state starts at `NOT_STARTED`; documented-unimplemented items start at
| ID | Activation | Red test command | Fix commit/PR | Rollout state | Rollback trigger | Evidence |
| --- | --- | --- | --- | --- | --- | --- |
| R-01 | Browser RPC `AVAILABLE_NOT_COMPOSED` | `corepack pnpm exec vitest run tests/unit/browser-rpc/browser-rpc-runtime.test.ts` | — | `NOT_STARTED` | stream lease deadlock | — |
| R-01 | Browser RPC `AVAILABLE_NOT_COMPOSED` | `corepack pnpm exec vitest run tests/unit/browser-rpc/browser-rpc-runtime.test.ts` | `fix: install bounded Browser RPC stream leases` | `FIXED_NOT_RELEASED` | stream lease deadlock | Stream cleanup is bounded; the generator no longer waits indefinitely on a non-cooperative `iterator.return()` |
| R-02 | Realtime `NOT_SELECTED` | `corepack pnpm exec vitest run tests/unit/realtime/stream-coordinator.test.ts` | `fix: retain realtime work through draining` | `FIXED_NOT_RELEASED` | stream stuck in `DRAINING` | Red never-settling effect and recovery → green 27/27; `close()` returns `IDLE_TIMEOUT` while a task is retained and success only after actual settlement |
| R-03 | Realtime `NOT_SELECTED` | `corepack pnpm exec vitest run tests/unit/realtime/live-poll-handoff-coordinator.test.ts` | `fix: retain realtime work through draining` | `FIXED_NOT_RELEASED` | retired-writer set growth | Red overflow fail-close then `close()` → green 11/11; retired writers are waited on and only removed once actually quiesced |
| R-04 | Browser RPC `AVAILABLE_NOT_COMPOSED` | `corepack pnpm exec vitest run tests/unit/browser-rpc/browser-rpc-contract.test.ts` | — | `NOT_STARTED` | binding install rejection | — |
| R-05 | WebSocket protocol codec | `corepack pnpm exec vitest run tests/unit/realtime/websocket-protocol.test.ts` | — | `NOT_STARTED` | frame rejection regression | — |
| R-06 | Browser RPC `AVAILABLE_NOT_COMPOSED` | `corepack pnpm exec vitest run tests/unit/browser-rpc/browser-rpc-runtime.test.ts` | — | `NOT_STARTED` | closed-failure taxonomy drift | — |
| R-04 | Browser RPC `AVAILABLE_NOT_COMPOSED` | `corepack pnpm exec vitest run tests/unit/browser-rpc/browser-rpc-contract.test.ts` | `fix: install bounded Browser RPC stream leases` | `FIXED_NOT_RELEASED` | binding install rejection | Red post-validation mutation, accessor and symbol cases → green 19/19; getters are never invoked |
| R-05 | WebSocket protocol codec | `corepack pnpm exec vitest run tests/unit/realtime/websocket-protocol.test.ts` | `fix: install bounded Browser RPC stream leases` | `FIXED_NOT_RELEASED` | frame rejection regression | Red oversize frame allocated an encoder copy → green 8/8; byte counts match `TextEncoder` including lone surrogates |
| R-06 | Browser RPC `AVAILABLE_NOT_COMPOSED` | `corepack pnpm exec vitest run tests/unit/browser-rpc/browser-rpc-runtime.test.ts` | `fix: install bounded Browser RPC stream leases` | `FIXED_NOT_RELEASED` | closed-failure taxonomy drift | Clock and fence reads are canonicalised into the closed Result taxonomy with single-exit cleanup |
| R-07 | Promotion blocker | concrete transport conformance evidence | — | `PROMOTION_BLOCKED` | n/a | — |
### Browser transfer (`docs/reviews/adapters/04-browser-transfer.md`)