<!-- task=typescript_impl plan=0 tag=API -->

# TypeScript 구현체 추가

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

각 항목의 체크리스트를 완료하면서 구현하라. 각 중간 검증 명령을 실행하고 그 출력을 `CODE_REVIEW.md`의 `검증 결과` 섹션에 붙여넣어라. 계획과 다르게 구현한 부분은 `계획 대비 변경 사항`에 이유와 함께 기록하라.

---

## 배경

Toki Socket 프로토콜 라이브러리는 현재 Dart, Go, Kotlin 구현체가 완료됐고 Python 구현체가 추가된 상태다. TypeScript는 브라우저(WebSocket)와 Node.js(TCP + WebSocket) 양쪽에서 사용되는 중요 플랫폼이다. `PORTING_GUIDE.md`와 `PROTOCOL.md`를 기준으로 TypeScript 구현체를 추가하고, Go 서버와의 크로스테스트까지 통과해야 한다.

---

## 의존 관계 및 구현 순서

API-1 → API-2 → API-3 → API-4 → API-5 → API-6 → API-7 → API-8

각 항목은 이전 항목의 완료를 전제한다.

---

### [API-1] 패키지 스캐폴드 및 Protobuf 바인딩 생성

#### 문제

`typescript/` 디렉터리가 존재하지 않는다. TypeScript 소스, 패키지 메타데이터, protobuf 바인딩이 모두 없다.

#### 해결 방법

1. `typescript/` 디렉터리를 만들고 `package.json`, `tsconfig.json`을 생성한다.
2. canonical proto(`dart/lib/src/packets/message_common.proto`)에서 protobuf-es 바인딩을 생성한다. `@bufbuild/buf` CLI 또는 `protoc`를 사용한다.
3. 생성된 파일을 `typescript/src/packets/`에 둔다.

**package.json 주요 의존성:**
```json
{
  "name": "toki-socket",
  "version": "0.1.0",
  "type": "module",
  "dependencies": {
    "@bufbuild/protobuf": "^2.0.0"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "@types/ws": "^8.0.0",
    "typescript": "^5.5.0",
    "vitest": "^2.0.0",
    "ws": "^8.0.0"
  }
}
```

**tsconfig.json 핵심 옵션:**
```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "outDir": "dist",
    "rootDir": "src"
  }
}
```

**protobuf-es 생성 명령:**
```bash
# buf CLI 사용 시
cd typescript
npx buf generate ../dart/lib/src/packets/message_common.proto
# 또는 직접 복사 후 protoc-gen-es 사용
```

protobuf-es가 환경에서 사용 불가능한 경우 `@bufbuild/protobuf`로 수동 타입 정의를 작성하는 대안도 허용한다. 단, typeName은 `"TestData"`, `"HeartBeat"`, `"PacketBase"` 그대로 유지해야 한다.

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

- [ ] `typescript/package.json` 신규 생성
- [ ] `typescript/tsconfig.json` 신규 생성
- [ ] `typescript/src/packets/` 디렉터리 + 생성/수동 작성 파일
  - [ ] `message_common_pb.ts` (또는 protobuf-es 생성 파일)
- [ ] `typescript/.gitignore` (`node_modules/`, `dist/`)

#### 테스트 작성

스킵. 패키지 구성 자체는 컴파일 성공 여부로 검증한다.

#### 중간 검증

```bash
cd typescript && npm install && npx tsc --noEmit
```
기대 결과: 오류 없이 종료.

---

### [API-2] Communicator 구현

#### 문제

핵심 라우팅·직렬화 클래스가 없다. Go의 `communicator.go`와 Python의 `communicator.py`에 해당하는 TypeScript 구현이 필요하다.

#### 해결 방법

`typescript/src/communicator.ts` 파일을 새로 생성한다.

**핵심 타입 및 인터페이스:**
```typescript
import { Message, MessageType } from "@bufbuild/protobuf";
import { PacketBase } from "./packets/message_common_pb.js";

export type ParserMap = Map<string, (data: Uint8Array) => Message>;

export interface Transport {
  writePacket(base: PacketBase): Promise<void>;
  close(): Promise<void>;
}

interface PendingRequest {
  expectedTypeName: string;
  resolve: (msg: Message) => void;
  reject: (err: Error) => void;
}

interface QueueItem {
  base: PacketBase;
  resolve: () => void;
  reject: (err: Error) => void;
}
```

**Communicator 클래스 구조:**
```typescript
export class Communicator {
  private nonce = 0;
  private isAliveFlag = false;
  private parserMap: ParserMap = new Map();
  private handlers = new Map<string, Array<(msg: Message) => void>>();
  private reqHandlers = new Map<string, (msg: Message, nonce: number) => void>();
  private pendingRequests = new Map<number, PendingRequest>();
  private writeQueue: QueueItem[] = [];
  private writeQueueRunning = false;
  private transport: Transport | null = null;
  private closedResolvers: Array<() => void> = [];
  private writeErrorHandler: ((err: Error) => void) | null = null;

  initialize(transport: Transport, parserMap: ParserMap): void { ... }
  isAlive(): boolean { return this.isAliveFlag; }
  setWriteErrorHandler(fn: (err: Error) => void): void { ... }
  private nextNonce(): number { return ++this.nonce; }
  shutdown(): void { ... }
  async close(): Promise<void> { ... }
  async queuePacket(base: PacketBase): Promise<void> { ... }
  async send(msg: Message): Promise<void> { ... }
  async sendRequest<TRes extends Message>(req: Message, resType: MessageType<TRes>, timeoutMs?: number): Promise<TRes> { ... }
  addListener(typeName: string, fn: (msg: Message) => void): void { ... }
  removeListeners(typeName: string): void { ... }
  addRequestListener(typeName: string, fn: (msg: Message, nonce: number) => void): void { ... }
  onReceivedData(typeName: string, data: Uint8Array, nonce: number, responseNonce: number): void { ... }
  private handleResponse(typeName: string, data: Uint8Array, responseNonce: number): void { ... }
  private parse(typeName: string, data: Uint8Array): Message { ... }
  private removePending(nonce: number): PendingRequest | undefined { ... }
  private runWriteQueue(): void { ... }
}

export function typeNameOf(msgOrType: Message | MessageType): string { ... }

export function addListenerTyped<T extends Message>(
  comm: Communicator,
  msgType: MessageType<T>,
  fn: (msg: T) => void
): void { ... }

export function addRequestListenerTyped<TReq extends Message, TRes extends Message>(
  comm: Communicator,
  reqType: MessageType<TReq>,
  fn: (req: TReq) => TRes | Promise<TRes | null> | null
): void { ... }

export async function sendRequestTyped<TReq extends Message, TRes extends Message>(
  comm: Communicator,
  req: TReq,
  resType: MessageType<TRes>,
  timeoutMs?: number
): Promise<TRes> { ... }
```

**writeQueue 구현 방식**: Go/Python의 channel/Queue와 달리 JS는 단일 스레드이므로 배열 기반 큐 + `runWriteQueue()` 패턴을 사용한다. `await` 구간마다 큐에서 한 항목씩 꺼내 처리하고, 처리 완료 후 다음 항목을 처리한다.

**typeNameOf**: `@bufbuild/protobuf`에서 `Message` 인스턴스의 경우 `msg.$typeName`으로, `MessageType` 객체의 경우 `msgType.typeName`으로 가져온다.

```typescript
export function typeNameOf(msgOrType: Message | MessageType): string {
  if (typeof (msgOrType as MessageType).typeName === "string") {
    return (msgOrType as MessageType).typeName;
  }
  return (msgOrType as Message).$typeName;
}
```

주의: protobuf-es에서 typeName은 `package.MessageName` 형태다. 현재 proto에 package 선언이 없으면 단순 `TestData`가 된다. 실행 시 반드시 확인해야 한다.

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

- [ ] `typescript/src/communicator.ts` 신규 생성
  - [ ] `ParserMap`, `Transport` 타입 정의
  - [ ] `Communicator` 클래스 전체 구현
  - [ ] `typeNameOf` 함수
  - [ ] `addListenerTyped`, `addRequestListenerTyped`, `sendRequestTyped` 제네릭 헬퍼

#### 테스트 작성

`typescript/test/communicator.test.ts`를 작성한다.

| 테스트명 | 검증 목표 |
|----------|-----------|
| `addListener/addRequestListener mutual exclusion` | 동일 typeName에 양쪽 등록 시 `Error` 발생 |
| `send increments nonce` | 두 번 send 시 nonce가 1, 2로 증가 |
| `sendRequest resolves on response` | responseNonce로 올바른 Future 완료 |
| `sendRequest rejects on timeout` | 타임아웃 시 Error 발생 |
| `shutdown cancels pending requests` | shutdown 시 모든 pendingRequests에 NotConnectedError |

테스트용 Mock Transport:
```typescript
class MockTransport implements Transport {
  packets: PacketBase[] = [];
  async writePacket(base: PacketBase) { this.packets.push(base); }
  async close() {}
}
```

#### 중간 검증

```bash
cd typescript && npx tsc --noEmit && npx vitest run test/communicator.test.ts
```
기대 결과: 모든 테스트 PASS.

---

### [API-3] BaseClient 구현 (하트비트 + disconnect 리스너)

#### 문제

하트비트 타이머 로직과 disconnect 리스너 관리가 없다. Go의 `base_client.go`, Python의 `base_client.py`에 해당한다.

#### 해결 방법

`typescript/src/base_client.ts`를 작성한다.

```typescript
export abstract class BaseClient {
  readonly communicator: Communicator;
  private intervalSec: number;
  private waitSec: number;
  private doClose: () => Promise<void>;
  private isClosed = false;
  private hbTimer: ReturnType<typeof setTimeout> | null = null;
  private waitingHbResponse = false;
  private disconnectListeners: Array<(self: this) => void | Promise<void>> = [];

  constructor(intervalSec: number, waitSec: number, doClose: () => Promise<void>) { ... }
  protected initBase(parserMap: ParserMap): void { ... }

  async close(): Promise<void> {
    if (this.isClosed) return;
    this.isClosed = true;
    this.communicator.shutdown();
    this.stopHeartbeat();
    await this.doClose();
    await this.notifyDisconnected();
  }

  addDisconnectListener(fn: (self: this) => void | Promise<void>): void { ... }
  removeDisconnectListeners(): void { ... }

  protected async sendHeartbeat(): Promise<void> { ... }
  protected async onHeartbeat(): Promise<void> { ... }
  protected async onDisconnected(): Promise<void> { await this.close(); }
  private stopHeartbeat(): void { ... }
  private async notifyDisconnected(): Promise<void> { ... }
}
```

하트비트 타이머는 `setTimeout` + `clearTimeout`으로 구현한다. Python의 `_HbTimer`와 동일한 구조다.

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

- [ ] `typescript/src/base_client.ts` 신규 생성
  - [ ] `BaseClient` abstract class
  - [ ] `close()` 멱등성 (`isClosed` 플래그)
  - [ ] 하트비트 타이머 (`setTimeout`/`clearTimeout`)
  - [ ] `addDisconnectListener`, `removeDisconnectListeners`

#### 테스트 작성

`typescript/test/base_client.test.ts`를 작성한다.

| 테스트명 | 검증 목표 |
|----------|-----------|
| `close is idempotent` | 여러 번 close 호출 시 doClose 1회만 실행 |
| `disconnect listeners called on close` | close 시 리스너 호출 |
| `heartbeat sends HeartBeat after interval` | intervalSec 후 HeartBeat 전송 확인 (vitest fake timers) |
| `heartbeat timeout triggers disconnect` | 응답 없이 waitSec 경과 시 onDisconnected 호출 |
| `onHeartbeat resets waiting flag` | 응답 HeartBeat 수신 시 waitingHbResponse=false |

#### 중간 검증

```bash
cd typescript && npx tsc --noEmit && npx vitest run test/base_client.test.ts
```
기대 결과: 모든 테스트 PASS.

---

### [API-4] TcpClient 구현 (Node.js)

#### 문제

Node.js TCP 소켓 클라이언트가 없다. Go의 `tcp_client.go`, Python의 `tcp_client.py`에 해당한다.

#### 해결 방법

`typescript/src/tcp_client.ts`를 작성한다. Node.js `net` 모듈을 사용하며, 브라우저에서는 사용 불가이므로 파일 상단에 주석으로 명시한다.

```typescript
import * as net from "node:net";
import { BaseClient } from "./base_client.js";

const MAX_PACKET_SIZE = 64 << 20;

export class TcpClient extends BaseClient {
  private socket: net.Socket;
  private readBuf = Buffer.alloc(0);

  constructor(socket: net.Socket, intervalSec: number, waitSec: number, parserMap: ParserMap) {
    super(intervalSec, waitSec, async () => {
      socket.destroy();
    });
    this.socket = socket;
    this.initBase(parserMap);
    this.communicator.setWriteErrorHandler(() => this.onDisconnected());
    addListenerTyped(this.communicator, HeartBeat, () => this.onHeartbeat());
    socket.on("data", (chunk) => this.onData(chunk));
    socket.on("error", () => this.onDisconnected());
    socket.on("close", () => this.onDisconnected());
    this.sendHeartbeat();
  }

  async writePacket(base: PacketBase): Promise<void> {
    const data = base.toBinary();
    const header = Buffer.allocUnsafe(4);
    header.writeUInt32BE(data.length, 0);
    await new Promise<void>((resolve, reject) => {
      this.socket.write(Buffer.concat([header, data]), (err) => {
        if (err) reject(err); else resolve();
      });
    });
  }

  private onData(chunk: Buffer): void {
    this.readBuf = Buffer.concat([this.readBuf, chunk]);
    while (this.readBuf.length >= 4) {
      const length = this.readBuf.readUInt32BE(0);
      if (length === 0) { this.readBuf = this.readBuf.subarray(4); continue; }
      if (length > MAX_PACKET_SIZE) { this.onDisconnected(); return; }
      if (this.readBuf.length < 4 + length) break;
      const packetBytes = this.readBuf.subarray(4, 4 + length);
      this.readBuf = this.readBuf.subarray(4 + length);
      try {
        const base = PacketBase.fromBinary(packetBytes);
        this.communicator.onReceivedData(base.typeName, base.data, base.nonce, base.responseNonce);
        this.sendHeartbeat();
      } catch {
        this.onDisconnected();
        return;
      }
    }
  }
}

export async function connectTcp(
  host: string, port: number,
  intervalSec: number, waitSec: number,
  parserMap: ParserMap
): Promise<TcpClient> {
  return new Promise((resolve, reject) => {
    const socket = net.createConnection({ host, port }, () => {
      resolve(new TcpClient(socket, intervalSec, waitSec, parserMap));
    });
    socket.once("error", reject);
  });
}
```

`readBuf` 방식으로 TCP 스트림 버퍼링을 처리한다. 4바이트 빅엔디안 헤더 파싱 후 전체 패킷 도착을 기다린다.

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

- [ ] `typescript/src/tcp_client.ts` 신규 생성
  - [ ] `TcpClient extends BaseClient` 구현
  - [ ] `writePacket`: 4바이트 빅엔디안 헤더 + protobuf 직렬화
  - [ ] `onData`: 스트림 버퍼링 + 길이 프리픽스 파싱
  - [ ] `connectTcp` 팩토리 함수

#### 테스트 작성

`typescript/test/tcp.test.ts`를 작성한다. Node.js `net` 모듈을 사용하는 실제 TCP 서버를 띄워 검증한다 (vitest에서 실제 소켓 테스트 가능).

| 테스트명 | 검증 목표 |
|----------|-----------|
| `connectTcp sends and receives TestData` | send → 서버 echo → client 수신 |
| `sendRequest/response roundtrip` | sendRequest 완료 + 값 검증 |

테스트 서버는 `TcpServer`(API-5 이후) 또는 직접 `net.createServer`로 구성한다.

#### 중간 검증

```bash
cd typescript && npx tsc --noEmit && npx vitest run test/tcp.test.ts
```
기대 결과: 모든 테스트 PASS.

---

### [API-5] TcpServer 구현 (Node.js)

#### 문제

TCP 서버가 없다. Go의 `tcp_server.go`, Python의 `tcp_server.py`에 해당한다.

#### 해결 방법

`typescript/src/tcp_server.ts`를 작성한다.

```typescript
import * as net from "node:net";

export class TcpServer {
  private server: net.Server;
  private clients: TcpClient[] = [];
  onClientConnected: (client: TcpClient) => void = () => {};

  constructor(
    private host: string,
    private port: number,
    private newClient: (socket: net.Socket) => TcpClient
  ) {
    this.server = net.createServer((socket) => {
      const client = this.newClient(socket);
      this.clients.push(client);
      client.addDisconnectListener(() => this.removeClient(client));
      this.onClientConnected(client);
    });
  }

  start(): Promise<void> { ... }
  stop(): Promise<void> { ... }
  async broadcast(msg: Message): Promise<void> { ... }
  private removeClient(client: TcpClient): void { ... }
}
```

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

- [ ] `typescript/src/tcp_server.ts` 신규 생성
  - [ ] `TcpServer` 클래스
  - [ ] `start()`, `stop()`, `broadcast()`

#### 테스트 작성

API-4 테스트에서 TcpServer를 함께 사용하므로 별도 파일은 작성하지 않는다.

#### 중간 검증

```bash
cd typescript && npx tsc --noEmit
```
기대 결과: 타입 오류 없음.

---

### [API-6] WsClient 구현 (Node.js / Browser)

#### 문제

WebSocket 클라이언트가 없다. Go의 `ws_client.go`, Python의 `ws_client.py`에 해당한다.

#### 해결 방법

`typescript/src/ws_client.ts`를 작성한다. Node.js에서는 `ws` 패키지, 브라우저에서는 내장 `WebSocket`을 사용한다. Transport를 주입받는 설계로 환경 독립성을 유지한다.

실제 구현은 Node.js `ws` 패키지를 기본으로 한다:

```typescript
import WebSocket from "ws";

export class WsClient extends BaseClient {
  private ws: WebSocket;

  constructor(ws: WebSocket, intervalSec: number, waitSec: number, parserMap: ParserMap) {
    super(intervalSec, waitSec, async () => {
      ws.close();
    });
    this.ws = ws;
    this.initBase(parserMap);
    this.communicator.setWriteErrorHandler(() => this.onDisconnected());
    addListenerTyped(this.communicator, HeartBeat, () => this.onHeartbeat());
    ws.on("message", (data: Buffer) => {
      try {
        const base = PacketBase.fromBinary(new Uint8Array(data));
        this.communicator.onReceivedData(base.typeName, base.data, base.nonce, base.responseNonce);
        this.sendHeartbeat();
      } catch { this.onDisconnected(); }
    });
    ws.on("close", () => this.onDisconnected());
    ws.on("error", () => this.onDisconnected());
    this.sendHeartbeat();
  }

  async writePacket(base: PacketBase): Promise<void> {
    const data = base.toBinary();
    return new Promise((resolve, reject) => {
      this.ws.send(data, { binary: true }, (err) => {
        if (err) reject(err); else resolve();
      });
    });
  }
}

export async function connectWs(
  host: string, port: number, path: string,
  intervalSec: number, waitSec: number,
  parserMap: ParserMap
): Promise<WsClient> { ... }
```

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

- [ ] `typescript/src/ws_client.ts` 신규 생성
  - [ ] `WsClient extends BaseClient` 구현
  - [ ] `writePacket`: protobuf 바이너리 직렬화 후 binary 프레임 전송
  - [ ] `connectWs` 팩토리 함수

#### 테스트 작성

`typescript/test/ws.test.ts`를 작성한다. `ws` 패키지의 `WebSocket.Server`로 로컬 서버를 구성한다.

| 테스트명 | 검증 목표 |
|----------|-----------|
| `connectWs sends and receives TestData` | send → echo → client 수신 |
| `sendRequest/response roundtrip over WS` | sendRequest 완료 + 값 검증 |

#### 중간 검증

```bash
cd typescript && npx tsc --noEmit && npx vitest run test/ws.test.ts
```
기대 결과: 모든 테스트 PASS.

---

### [API-7] WsServer 구현 (Node.js)

#### 문제

WebSocket 서버가 없다. Go의 `ws_server.go`, Python의 `ws_server.py`에 해당한다.

#### 해결 방법

`typescript/src/ws_server.ts`를 작성한다. `ws` 패키지의 `WebSocket.Server`를 사용한다.

```typescript
import { WebSocketServer, WebSocket } from "ws";

export class WsServer {
  private wss: WebSocketServer | null = null;
  private clients: WsClient[] = [];
  onClientConnected: (client: WsClient) => void = () => {};

  constructor(
    private host: string,
    private port: number,
    private path: string,
    private newClient: (ws: WebSocket) => WsClient
  ) {}

  start(): Promise<void> { ... }
  stop(): Promise<void> { ... }
  async broadcast(msg: Message): Promise<void> { ... }
  private removeClient(client: WsClient): void { ... }
}
```

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

- [ ] `typescript/src/ws_server.ts` 신규 생성
  - [ ] `WsServer` 클래스
  - [ ] `start()`, `stop()`, `broadcast()`

#### 테스트 작성

API-6 테스트에서 WsServer를 함께 사용하므로 별도 파일은 작성하지 않는다.

#### 중간 검증

```bash
cd typescript && npx tsc --noEmit
```
기대 결과: 타입 오류 없음.

---

### [API-8] 크로스테스트 추가 (Go 서버 / TypeScript 클라이언트)

#### 문제

TypeScript 클라이언트와 Go 서버 간 프로토콜 호환성을 검증하는 크로스테스트가 없다.

#### 해결 방법

Go 서버 orchestrator + TypeScript 클라이언트 방식으로 구성한다.

**포트 배정** (기존 할당과 충돌하지 않는 값 사용):
- Go server / TypeScript client TCP: `29490`
- Go server / TypeScript client WS: `29492`

**파일:**
1. `go/crosstest/go_typescript.go` — Go 오케스트레이터 (Go 서버 시작 + TypeScript 클라이언트 서브프로세스 실행)
2. `typescript/crosstest/go_typescript_client.ts` — TypeScript 클라이언트 (4개 시나리오 실행)

**go_typescript.go 구조** (go_python.go 참조):
```go
//go:build ignore
package main

const (
  goPythonTCPPort = 29490
  goPythonWSPort  = 29492
)

func runTypescriptClient(mode, port, phase, ...) error {
  cmd := exec.CommandContext(ctx, "node", "--input-type=module",
    // 또는 tsx, ts-node 등 TypeScript 실행 방법
  )
  // ...
}
```

TypeScript 클라이언트 실행 방법: `tsx crosstest/go_typescript_client.ts` (개발 환경) 또는 컴파일 후 `node dist/crosstest/go_typescript_client.js`를 사용한다. `tsx`를 devDependency로 추가한다.

**go_typescript_client.ts 구조** (go_python_client.py 참조):
```typescript
// 4개 시나리오: send-push (1,2), requests (3,4)
// PASS scenario=N detail=...
// FAIL scenario=N error=...
```

**크로스테스트 시나리오:**

| 시나리오 | 검증 내용 |
|----------|-----------|
| 1 | TypeScript 클라이언트 `send(TestData{index:101, message:"fire from typescript client"})` → Go 서버 수신 확인 |
| 2 | Go 서버 `Send(TestData{index:200, message:"push from go server"})` → TypeScript 수신 확인 |
| 3 | TypeScript `sendRequest(TestData{index:21})` → Go 서버 응답 `{index:42, message:"echo: ..."}` 확인 |
| 4 | 5개 concurrent `sendRequest` → 모든 nonce/responseNonce 매핑 검증 |

TCP와 WebSocket 양쪽으로 4개 시나리오를 실행한다. `send-push`와 `requests` 단계로 분리한다.

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

- [ ] `go/crosstest/go_typescript.go` 신규 생성
  - [ ] Go 서버 시작 (TCP + WS)
  - [ ] TypeScript 클라이언트 서브프로세스 실행
  - [ ] PASS/FAIL 파싱 및 검증
- [ ] `typescript/crosstest/go_typescript_client.ts` 신규 생성
  - [ ] `--mode`, `--port`, `--phase` 인수 파싱
  - [ ] `send-push` 페이즈: 시나리오 1, 2
  - [ ] `requests` 페이즈: 시나리오 3, 4
- [ ] `typescript/package.json`에 `tsx` devDependency 추가

#### 테스트 작성

크로스테스트 자체가 통합 테스트다. 별도 단위 테스트는 작성하지 않는다.

#### 중간 검증

```bash
# TypeScript 클라이언트 직접 실행 테스트 (Go 서버 먼저 수동 실행 필요)
cd typescript && npx tsx crosstest/go_typescript_client.ts --mode=tcp --port=29490 --phase=send-push

# Go 오케스트레이터 전체 실행
cd go && go run ./crosstest/go_typescript.go
```
기대 결과: `PASS all go-server/typescript-client crosstests passed`

---

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `typescript/package.json` | API-1 |
| `typescript/tsconfig.json` | API-1 |
| `typescript/.gitignore` | API-1 |
| `typescript/src/packets/message_common_pb.ts` | API-1 |
| `typescript/src/communicator.ts` | API-2 |
| `typescript/src/base_client.ts` | API-3 |
| `typescript/src/tcp_client.ts` | API-4 |
| `typescript/src/tcp_server.ts` | API-5 |
| `typescript/src/ws_client.ts` | API-6 |
| `typescript/src/ws_server.ts` | API-7 |
| `typescript/test/communicator.test.ts` | API-2 |
| `typescript/test/base_client.test.ts` | API-3 |
| `typescript/test/tcp.test.ts` | API-4 |
| `typescript/test/ws.test.ts` | API-6 |
| `typescript/crosstest/go_typescript_client.ts` | API-8 |
| `go/crosstest/go_typescript.go` | API-8 |

---

## 최종 검증

```bash
# 1. TypeScript 컴파일 검사
cd typescript && npx tsc --noEmit

# 2. 단위 테스트 전체 실행
cd typescript && npx vitest run

# 3. Go 크로스테스트 (Go 환경 필요)
cd go && go run ./crosstest/go_typescript.go
```

기대 결과:
1. 타입 오류 없음
2. 모든 vitest 테스트 PASS
3. `PASS all go-server/typescript-client crosstests passed`
