<!-- task=01+browser_native_ws plan=0 tag=BNW -->

# Browser Native WebSocket 계획 - BNW

## 이 파일을 읽는 구현 에이전트에게

**`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것은 필수 최종 단계다. 이 파일이 채워지기 전에는 작업이 완료된 것이 아니다.**
구현 체크리스트를 기준으로 작업하고, plan과 review stub 양쪽 체크리스트를 모두 완료하며, 중간/최종 검증의 실제 stdout/stderr를 `CODE_REVIEW-*-G??.md`에 기록한다.
review 파일의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 아카이브 지시는 실행하지 않는다. `코드리뷰 전용 체크리스트`도 구현 에이전트가 수정하거나 체크하지 않는다.

## 배경

Web에서 control-plane으로 직접 연결하려면 브라우저가 제공하는 native `WebSocket` API만으로 protobuf binary frame을 송수신할 수 있어야 한다.
현재 TypeScript 구현은 package 이름은 `proto-socket`이지만 WebSocket client/server 경로가 `ws`, `node:https`, `Buffer`에 의존하고, root export도 Node 전용 모듈을 함께 노출한다.
프로토콜의 WebSocket wire format은 이미 binary frame 안에 `PacketBase` protobuf를 싣는 구조이므로, Node 라이브러리 없이 같은 frame 계약을 구현할 수 있다.

## 의존 관계 및 구현 순서

`agent-task/01_proto_socket_naming`이 PASS되어 `complete.log`까지 생성된 뒤 시작한다.
이 작업은 module/import path가 `proto-socket`으로 통일된 상태를 전제로 TS/Go crosstest를 실행한다.

## 분석 결과

### 읽은 파일

- `AGENTS.md`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/project/domain/protocol/rules.md`
- `agent-ops/rules/project/domain/tools/rules.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `PROTOCOL.md`
- `typescript/package.json`
- `typescript/tsconfig.json`
- `typescript/README.md`
- `typescript/src/base_client.ts`
- `typescript/src/communicator.ts`
- `typescript/src/index.ts`
- `typescript/src/tcp_client.ts`
- `typescript/src/tcp_server.ts`
- `typescript/src/ws_client.ts`
- `typescript/src/ws_server.ts`
- `typescript/test/base_client.test.ts`
- `typescript/test/communicator.test.ts`
- `typescript/test/tcp.test.ts`
- `typescript/test/ws.test.ts`
- `typescript/crosstest/go_typescript_client.ts`
- `typescript/crosstest/typescript_go.ts`
- `go/ws_client.go`
- `go/ws_server.go`

### 테스트 커버리지 공백

- Browser native WebSocket client: 기존 테스트 없음. `typescript/test/ws.test.ts`는 Node `ws` server/client만 검증한다.
- Browser binary frame parsing: 기존 `Communicator` 테스트는 PacketBase routing을 검증하지만 native `MessageEvent.data`의 `ArrayBuffer`, `Uint8Array`, `Blob` 입력은 검증하지 않는다.
- Browser-safe export boundary: 기존 테스트 없음. 정적 `rg` guard로 browser/root entrypoint에 `ws`, `node:*`, `Buffer`가 남지 않는지 검증한다.
- Node WebSocket/TCP 회귀: 기존 `typescript/test/ws.test.ts`, `typescript/test/tcp.test.ts`, `go run ./crosstest/go_typescript.go`가 유지되어야 한다.

### 심볼 참조

변경 또는 분리 대상 public/import symbol:

- `WsClient`, `connectWs`, `connectWss`: `typescript/src/ws_client.ts`, `typescript/test/ws.test.ts`, `typescript/crosstest/go_typescript_client.ts`, `typescript/crosstest/typescript_go.ts`
- `WsServer`: `typescript/src/ws_server.ts`, `typescript/test/ws.test.ts`, `typescript/crosstest/typescript_go.ts`
- root export: `typescript/src/index.ts:4-7`에서 `tcp_client`, `tcp_server`, `ws_client`, `ws_server`를 함께 export한다.
- Node dependency strings: `typescript/src/ws_client.ts:1`, `typescript/src/ws_server.ts:1`, `typescript/src/ws_server.ts:4`, `typescript/src/tcp_client.ts:1`, `typescript/src/tcp_server.ts:1`, `typescript/package.json:13`, `typescript/tsconfig.json:12`

### 범위 결정 근거

- TCP transport는 browser runtime에서 사용할 수 없으므로 native browser 구현 대상이 아니다. Node 전용 entrypoint로 분리만 한다.
- WebSocket server는 브라우저가 제공하지 않으므로 Node 전용 entrypoint로 유지한다.
- 프로토콜 schema와 Go WebSocket wire format은 변경하지 않는다. `PROTOCOL.md:38-48`의 binary frame 계약을 그대로 따른다.
- 실제 브라우저 E2E 자동화는 이번 계획의 필수 범위로 넣지 않는다. 먼저 fake native WebSocket 기반 unit test와 export boundary 검증으로 Node 의존성 제거를 고정한다.

### 빌드 등급

build=`cloud-G08`, review=`cloud-G08`. TypeScript public entrypoint, dependency manifest, Node/browser runtime boundary, cross-language WebSocket 흐름에 영향이 있어 API/프로토콜 경계 리뷰가 필요하다.

## 구현 체크리스트

- [ ] [BNW-1] 브라우저 native WebSocket client를 Node 라이브러리 없이 구현한다.
- [ ] [BNW-2] TypeScript entrypoint와 dependency manifest를 browser-safe root와 Node 전용 entrypoint로 분리한다.
- [ ] [BNW-3] 브라우저 WS unit test, Node WS 회귀 테스트, crosstest, 정적 guard 검증을 추가/갱신한다.
- [ ] [BNW-4] 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [BNW-1] 브라우저 native WebSocket client 구현

#### 문제

현재 WebSocket client는 Node `ws` 패키지와 Node buffer API에 묶여 있다.

```ts
// typescript/src/ws_client.ts:1
import WebSocket, { type ClientOptions, type RawData } from "ws";
```

```ts
// typescript/src/ws_client.ts:31
this.ws.send(data, { binary: true }, (err) => {
```

```ts
// typescript/src/ws_client.ts:109-116
function toUint8Array(data: RawData): Uint8Array {
  if (Array.isArray(data)) {
    return Buffer.concat(data.map((chunk) => Buffer.from(chunk)));
  }
  if (data instanceof ArrayBuffer) {
    return new Uint8Array(data);
  }
  return new Uint8Array(data);
}
```

브라우저 WebSocket에는 `send` callback, `RawData`, `off`, `once`, `terminate`, `ClientOptions`가 없다.

#### 해결 방법

`typescript/src/browser_ws_client.ts`를 추가한다. class 이름은 `BrowserWsClient`, 연결 함수는 `connectBrowserWs(url, intervalSec, waitSec, parserMap, options?)`로 둔다.
`options`에는 테스트 주입용 `WebSocketCtor?: typeof WebSocket`만 둔다. WSS는 별도 함수가 아니라 `wss://` URL로 처리한다.
구현은 `globalThis.WebSocket` 또는 주입된 constructor를 사용하고, `binaryType = "arraybuffer"`를 설정한다.

```ts
// typescript/src/browser_ws_client.ts
import { fromBinary, toBinary } from "@bufbuild/protobuf";

import { BaseClient } from "./base_client.js";
import { type ParserMap } from "./communicator.js";
import { PacketBaseSchema, type PacketBase } from "./packets/message_common_pb.js";

export interface BrowserWsConnectOptions {
  WebSocketCtor?: typeof WebSocket;
}

export class BrowserWsClient extends BaseClient {
  private readonly ws: WebSocket;

  constructor(ws: WebSocket, intervalSec: number, waitSec: number, parserMap: ParserMap) {
    super(intervalSec, waitSec, () => closeBrowserWebSocket(ws));
    this.ws = ws;
    this.ws.binaryType = "arraybuffer";
    this.initBase(parserMap);
    this.ws.addEventListener("message", (event) => {
      void this.onMessage(event.data);
    });
    this.ws.addEventListener("close", () => {
      void this.onDisconnected();
    });
    this.ws.addEventListener("error", () => {
      void this.onDisconnected();
    });
  }

  async writePacket(base: PacketBase): Promise<void> {
    this.ws.send(toBinary(PacketBaseSchema, base));
  }
}
```

`onMessage`는 `ArrayBuffer`, `Uint8Array`, `Blob`을 `Uint8Array`로 변환한다. 문자열 frame은 protocol 위반으로 처리해 disconnect한다.

#### 수정 파일 및 체크리스트

- [ ] `typescript/src/browser_ws_client.ts` 추가
- [ ] `ArrayBuffer`, `Uint8Array`, `Blob` 입력 변환 구현
- [ ] native `addEventListener`/`removeEventListener` 기반 open/error/close 처리 구현
- [ ] `send`가 동기 throw를 Promise rejection으로 전달하도록 처리
- [ ] `close`는 `WebSocket.CLOSED`/`CLOSING` 상태를 고려하고 `close(1000, "")`만 사용한다. `terminate`는 사용하지 않는다.

#### 테스트 작성

`typescript/test/browser_ws_client.test.ts`를 새로 작성한다.
테스트는 fake native WebSocket constructor를 사용하며, 다음을 검증한다.

- `connectBrowserWs`가 open event 뒤 `BrowserWsClient`를 resolve한다.
- `communicator.send(TestData)`가 binary `PacketBase` frame을 native `send`로 전달한다.
- `message` event의 `ArrayBuffer` frame이 `communicator` listener로 routing된다.
- `sendRequestTyped` pending request가 response frame으로 resolve된다.
- non-binary string frame 또는 protobuf parse 실패가 disconnect를 발생시킨다.

#### 중간 검증

```bash
cd typescript && npm test -- browser_ws_client
```

예상 결과: browser native WebSocket unit test 성공.

### [BNW-2] Browser-safe root export와 Node 전용 entrypoint 분리

#### 문제

root export가 Node 전용 transport를 모두 노출한다.

```ts
// typescript/src/index.ts:4-7
export * from "./tcp_client.js";
export * from "./tcp_server.js";
export * from "./ws_client.js";
export * from "./ws_server.js";
```

`typescript/src/ws_server.ts`는 Node server API와 `ws`를 import한다.

```ts
// typescript/src/ws_server.ts:1-4
import * as https from "node:https";
import WebSocket, { WebSocketServer } from "ws";
```

`typescript/package.json:13`은 `ws`를 runtime dependency로 강제한다.

```json
// typescript/package.json:11-14
"dependencies": {
  "@bufbuild/protobuf": "^2.2.5",
  "ws": "^8.18.1"
}
```

#### 해결 방법

root `typescript/src/index.ts`는 browser-safe API만 export한다.

```ts
// typescript/src/index.ts
export * from "./base_client.js";
export * from "./communicator.js";
export * from "./packets/message_common_pb.js";
export * from "./browser_ws_client.js";
```

Node 전용 API는 `typescript/src/node.ts`를 추가해 export한다.

```ts
// typescript/src/node.ts
export * from "./base_client.js";
export * from "./communicator.js";
export * from "./packets/message_common_pb.js";
export * from "./tcp_client.js";
export * from "./tcp_server.js";
export * from "./node_ws_client.js";
export * from "./node_ws_server.js";
```

현재 `ws_client.ts`/`ws_server.ts`의 Node 구현은 `node_ws_client.ts`/`node_ws_server.ts`로 이동하거나 같은 내용을 새 파일에 보존한다. Node API 이름은 명확히 `NodeWsClient`, `connectNodeWs`, `connectNodeWss`, `NodeWsServer`를 사용한다. 기존 TS 테스트와 crosstest import는 Node entrypoint 또는 Node 파일명으로 갱신한다.

`package.json`에는 root와 Node subpath export를 명시한다.

```json
"exports": {
  ".": {
    "types": "./dist/src/index.d.ts",
    "default": "./dist/src/index.js"
  },
  "./node": {
    "types": "./dist/src/node.d.ts",
    "default": "./dist/src/node.js"
  }
}
```

`ws`와 `@types/ws`는 Node test/build용 devDependency로 둔다. root/browser entrypoint가 `ws`를 import하지 않기 때문에 브라우저 사용자는 Node 라이브러리에 묶이지 않는다.
`tsconfig.json`에는 DOM WebSocket 타입을 위해 `lib: ["ES2022", "DOM"]`를 추가한다. Node 전용 파일 compile을 위해 기존 `types: ["node"]`는 유지한다.

#### 수정 파일 및 체크리스트

- [ ] `typescript/src/index.ts`를 browser-safe export로 축소
- [ ] `typescript/src/node.ts` 추가
- [ ] `typescript/src/ws_client.ts`의 Node 구현을 `typescript/src/node_ws_client.ts`로 이동 또는 복사 후 명확한 Node API명 적용
- [ ] `typescript/src/ws_server.ts`의 Node 구현을 `typescript/src/node_ws_server.ts`로 이동 또는 복사 후 명확한 Node API명 적용
- [ ] 기존 Node-only `ws_client.ts`/`ws_server.ts`가 root export로 노출되지 않도록 정리
- [ ] `typescript/package.json` exports와 dependency/devDependency 정리
- [ ] `typescript/package-lock.json` 동기화
- [ ] `typescript/tsconfig.json` DOM lib 추가
- [ ] `typescript/README.md`에서 browser native WebSocket이 지원됨을 문서화하고 Node entrypoint import를 구분

#### 테스트 작성

신규 테스트는 BNW-1에서 작성한다. 이 항목은 기존 Node WS 테스트와 crosstest import 갱신으로 검증한다.

#### 중간 검증

```bash
rg --sort path -n 'from "ws"|node:|Buffer|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
```

예상 결과: 출력 없음.

```bash
cd typescript && npm run check
```

예상 결과: TypeScript typecheck 성공.

### [BNW-3] 테스트와 crosstest 갱신

#### 문제

기존 TS WebSocket 테스트는 Node `ws` 기반 이름을 직접 import한다.

```ts
// typescript/test/ws.test.ts
import { connectWs, connectWss, WsClient } from "../src/ws_client.js";
import { WsServer } from "../src/ws_server.js";
```

TS/Go crosstest도 Node WS 구현을 전제로 한다.

```ts
// typescript/crosstest/go_typescript_client.ts
import { connectWs, connectWss } from "../src/ws_client.js";
```

#### 해결 방법

Node WS 테스트와 crosstest는 `../src/node.js` 또는 `../src/node_ws_client.js`, `../src/node_ws_server.js`를 import하도록 바꾼다.
Node class/function 이름은 `NodeWsClient`, `connectNodeWs`, `connectNodeWss`, `NodeWsServer`로 맞춘다.
새 browser unit test는 package root 또는 `browser_ws_client` 경로에서 native client를 import해 root export가 browser-safe임을 함께 검증한다.

#### 수정 파일 및 체크리스트

- [ ] `typescript/test/ws.test.ts` Node API import/name 갱신
- [ ] `typescript/crosstest/go_typescript_client.ts` Node API import/name 갱신
- [ ] `typescript/crosstest/typescript_go.ts` Node API import/name 갱신
- [ ] `typescript/test/browser_ws_client.test.ts` 추가
- [ ] `typescript/README.md` validation/import 예시 갱신

#### 테스트 작성

`typescript/test/browser_ws_client.test.ts` 신규 작성은 필수다. 기존 `typescript/test/ws.test.ts`는 삭제하지 않고 Node WebSocket 회귀 테스트로 유지한다.

#### 중간 검증

```bash
cd typescript && npm test
```

예상 결과: TypeScript 전체 테스트 성공.

```bash
cd go && go run ./crosstest/go_typescript.go
```

예상 결과: Go/TypeScript crosstest 성공.

### [BNW-4] Browser boundary 최종 guard

#### 문제

구현 후에도 root/browser entrypoint에 Node import가 다시 섞이면 web-control-plane 연결에서 bundling/runtime 문제가 재발한다.

#### 해결 방법

정적 검색으로 browser-safe 파일의 금지 문자열을 검증한다. Node 전용 파일은 검색 범위에 넣지 않는다.

#### 수정 파일 및 체크리스트

- [ ] root/browser 파일에 `from "ws"`, `node:`, `Buffer`, `ClientOptions`, `RawData`가 없는지 확인
- [ ] Node 전용 파일은 `src/node.ts`, `src/node_ws_client.ts`, `src/node_ws_server.ts`, `src/tcp_client.ts`, `src/tcp_server.ts`에만 모은다

#### 테스트 작성

정적 guard 검증만 수행한다. 별도 테스트 파일은 BNW-1에서 작성한 browser WS 테스트로 충분하다.

#### 중간 검증

```bash
rg --sort path -n 'from "ws"|node:|Buffer|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
```

예상 결과: 출력 없음.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `typescript/src/browser_ws_client.ts` | BNW-1 |
| `typescript/src/index.ts` | BNW-2, BNW-4 |
| `typescript/src/node.ts` | BNW-2 |
| `typescript/src/node_ws_client.ts` | BNW-2, BNW-3 |
| `typescript/src/node_ws_server.ts` | BNW-2, BNW-3 |
| `typescript/src/ws_client.ts` | BNW-2 |
| `typescript/src/ws_server.ts` | BNW-2 |
| `typescript/package.json` | BNW-2 |
| `typescript/package-lock.json` | BNW-2 |
| `typescript/tsconfig.json` | BNW-2 |
| `typescript/README.md` | BNW-2, BNW-3 |
| `typescript/test/browser_ws_client.test.ts` | BNW-1, BNW-3 |
| `typescript/test/ws.test.ts` | BNW-3 |
| `typescript/crosstest/go_typescript_client.ts` | BNW-3 |
| `typescript/crosstest/typescript_go.ts` | BNW-3 |

## 최종 검증

```bash
cd typescript && npm run check
```

예상 결과: TypeScript typecheck 성공.

```bash
cd typescript && npm test
```

예상 결과: TypeScript 전체 테스트 성공.

```bash
cd go && go run ./crosstest/go_typescript.go
```

예상 결과: Go/TypeScript crosstest 성공.

```bash
rg --sort path -n 'from "ws"|node:|Buffer|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
```

예상 결과: 출력 없음.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
