# 언어별 구현 포팅 가이드

## 공통 원칙

- **레퍼런스 구현체: `go/`, `dart/`**
- 동시성·close-once 구조는 Go 구현체를 우선 참고하고, Dart 구현체는 public API와 parser map 사용 예시를 함께 참고한다
- 프로토콜 정의는 `PROTOCOL.md`가 단일 기준이다
- 각 언어의 관용적 패턴을 따르되, 아래 핵심 구조는 반드시 유지한다
- Protocol Buffers는 이 프로젝트의 핵심 프로토콜 의존성으로 취급한다. 그 외 런타임 구현은 가능한 한 native platform API와 standard library를 우선한다
- protobuf 외 외부 의존성은 구현하려는 기능에 정확히 대응하는 작고 focused된 라이브러리일 때만 허용한다. 넓은 프레임워크, 대형 런타임 계층, 부가 기능이 과도한 패키지를 좁은 기능 하나 때문에 추가하지 않는다
- protobuf 외 외부 의존성을 추가해야 한다면 해당 언어에서 실용적인 native 대안이 없는지 먼저 검토하고, README에 이유와 범위를 명시한다
- TCP/TLS/WebSocket, 바이너리 버퍼, 타이머, 동시성/비동기 처리처럼 플랫폼이 기본 제공하는 기능은 외부 패키지보다 기본 API를 우선한다

### 반드시 유지해야 하는 구조

| 요소 | Go 기준 | 설명 |
|------|---------|------|
| Transport 분리 | `Transport` interface | WritePacket과 Close만 있으면 됨. 소켓 종류(TCP/WS)에 무관하게 Communicator가 동작 |
| typeName 라우팅 | `TypeNameOf()` | protobuf full message name 기반. 언어마다 추출 방식이 다르므로 PROTOCOL.md의 언어별 표 확인 |
| nonce 단조 증가 + 충돌 회피 | `atomic.Int32` + pending-correlation 조회 | 연결당 1에서 시작, 송신마다 +1. 송신 전 candidate nonce가 active pending request의 key(`sendRequest` 대기 중 nonce)와 충돌하면 건너뛴다. candidate 선택과 pending 조회는 같은 임계구역 또는 동등한 동시성 안전성을 가져야 한다. 충돌 없는 값이 없으면 오류로 실패 |
| connCloseOnce | `sync.Once` | Close가 여러 번 불려도 한 번만 수행되어야 함 |
| addListener / addRequestListener 상호 배타 | panic or throw | 같은 typeName에 두 가지 동시 등록 금지. 프로토콜 계약 |
| heartbeat 자동 처리 | `baseClient` 내부 | 앱 코드에서 HeartBeat를 직접 등록하거나 처리하지 않음 |
| inbound queue | bounded FIFO (권장 capacity 64) | 프레임 파서와 dispatch 사이에 연결당 하나의 수신 큐를 둔다. 큐가 가득 차면 읽기를 일시 중단해 backpressure를 적용한다. 단, Browser WS, OkHttp WS처럼 transport pause가 불가능한 환경에서는 64 capacity의 Bounded Input Gate를 두며, 이를 초과할 시 강제로 disconnect(connection close/destroy)를 수행해 memory exhaustion을 차단한다. response 패킷(responseNonce > 0)은 FIFO 순서 보장이 불필요하므로 큐를 거치지 않고 직접 handleResponse를 실행해 비동기 대기를 즉시 완료시킨다 |
| receive coordinator | 단일 루프 | 수신 큐에서 패킷을 순서대로 꺼내 request handler 호출 → listener 호출 순서로 처리한다. 자동 응답은 같은 루프 이터레이션에서 write queue에 enqueue한다 |
| close/drain/cancel 정책 | `doClose` + `sync.Once` | Close 호출 시 이미 큐에 들어간 패킷을 drain한 뒤 coordinator를 종료한다. 전송 오류 발생 시에는 drain 없이 즉시 폐기한다. 미완료 sendRequest는 오류로 cancel한다. 상세 계약은 PROTOCOL.md의 `Receive Ordering and Backpressure` 섹션 참조 |

### Worker gateway boundary

대량 raw packet 처리나 CPU 비용이 큰 decode/전처리를 병렬화할 때만 선택적으로 worker gateway를 둔다. gateway는 stateful dispatch를 소유하지 않으며, 순서 복원과 상태 접근은 receive coordinator가 단독으로 책임진다.

- Gateway input/output carries an internal `seq` assigned by the read side.
- Gateway may parse raw bytes or perform pure preprocessing only.
- The receive coordinator owns reorder-by-seq, pending response matching, listener dispatch, request handler execution, and automatic response enqueue.
- Gateway must NOT own or manage pending response maps, listener/request handlers, or write queue logic.
- `seq`는 구현 내부 값이며 wire format(`PacketBase`/transport framing)에 추가하지 않는다. 상세는 PROTOCOL.md의 `Worker Gateway and Internal seq` 섹션을 따른다.
- gateway를 아직 구현하지 않은 언어는 gateway off fallback에서도 동일한 coordinator path로 동작해야 한다.

---

## 새 언어 추가 절차

1. 새 언어 디렉터리를 만들기 전에 `agent-ops/skills/project/add-proto-socket-crosstest-language/templates/`의 문서를 복사해 패키지 README와 체크리스트 초안으로 사용한다.
2. `PROTOCOL.md`와 `VERSIONING.md`의 현재 protocol version을 기준으로 지원 범위를 정한다.
3. canonical proto인 `proto/message_common.proto`에서 언어별 protobuf binding을 생성한다. 공통 proto package는 `proto_socket`이며, 새 application proto package는 lowercase snake_case segment를 dot으로 연결한 형태만 사용한다. Go처럼 generator 전용 option이 필요하면 언어 패키지 안에 copy를 둘 수 있지만, protobuf package와 message schema는 `tools/check_proto_sync.sh`로 검증 가능해야 한다.
4. `Transport`와 `Communicator`를 먼저 구현하고, 그 위에 TCP/TLS+TCP/WS/WSS client/server를 올린다.
5. 같은 언어 단위 테스트를 작성한 뒤 `agent-ops/skills/project/add-proto-socket-crosstest-language/SKILL.md`의 runner 배치 규칙에 맞춰 양방향 crosstest를 추가한다.
6. README의 Implementations 표를 `Available`로 바꾸기 전에 formatter/linter, 단위 테스트, cross-language tests를 모두 통과시키고, protobuf 외 런타임 의존성이 있다면 native 대안 검토 결과, 기능 범위와 라이브러리 범위가 맞는 이유, 필요성을 문서화한다.

템플릿은 시작점과 완료 조건만 고정한다. 실제 소스 구조, 패키지 매니저, async runtime은 각 언어의 관용적 선택을 따른다.

---

## C# (Unity / .NET)

### 핵심 매핑

| Go | C# |
|----|----|
| `Transport` interface | `ITransport` interface |
| `baseClient[Self]` | `BaseClient<TSelf>` generic class |
| `ParserMap` | `Dictionary<string, Func<byte[], IMessage>>` |
| goroutine | `Task` + `CancellationToken` |
| `context.Context` | `CancellationToken` |
| `sync.Once` | `Interlocked` 또는 `SemaphoreSlim(1,1)` |
| `atomic.Bool` | `volatile bool` 또는 `Interlocked.Exchange` |
| channel (writeQueue) | `Channel<T>` (`System.Threading.Channels`) |

### 주의사항

- **Unity 환경**: `System.Threading.Channels`가 Unity 버전에 따라 미지원일 수 있다. 대안으로 `ConcurrentQueue<T>` + ManualResetEvent 조합을 사용한다
- **메인 스레드 제약**: Unity는 UnityEngine API를 메인 스레드에서만 호출할 수 있다. 수신 콜백을 메인 스레드로 dispatch하는 래퍼(`SynchronizationContext`)가 필요하다. Communicator 코어는 스레드 무관하게 유지하고, 래퍼 레이어에서 처리한다
- **protobuf**: `Google.Protobuf` NuGet 패키지 사용. descriptor의 full name이 canonical proto full name과 일치하는지 반드시 검증한다. 언어 namespace는 wire identity와 분리한다. PROTOCOL.md의 typeName 표 참고
- **제네릭 헬퍼**: `AddListenerTyped<T>`, `SendRequestTyped<TReq, TRes>` 패턴은 C# 제네릭으로 그대로 구현 가능하다
- **`doClose` 패턴**: C#의 람다 캡처 동작이 Go와 동일하므로 그대로 이식 가능하다

---

## Kotlin (Android)

### 핵심 매핑

| Go | Kotlin |
|----|--------|
| `Transport` interface | `interface Transport` |
| `baseClient[Self]` | `abstract class BaseClient<Self>` 또는 generic open class |
| goroutine | `CoroutineScope` + `launch` |
| `context.Context` | `CoroutineContext` + `Job` |
| channel (writeQueue) | `Channel<T>` (kotlinx.coroutines) |
| `sync.Once` | `AtomicBoolean` + `compareAndSet` |
| `atomic.Bool` | `AtomicBoolean` |
| `sync.RWMutex` | `ReentrantReadWriteLock` 또는 `Mutex` (coroutines) |

### 주의사항

- **Coroutine Scope 관리**: 클라이언트 생성 시 `CoroutineScope(SupervisorJob() + Dispatchers.IO)`를 만들고, `Close()` 시 `scope.cancel()`로 정리한다. SupervisorJob을 사용해야 하위 Job 하나 실패가 전체를 취소하지 않는다
- **protobuf**: `com.google.protobuf:protobuf-kotlin` 사용. typeName은 `descriptor.fullName`으로 추출. Java/Kotlin package option은 generated code namespace일 뿐 wire identity를 바꾸지 않는다. PROTOCOL.md 표에서 Kotlin 행 확인
- **Android 메인 스레드**: Unity와 동일하게 UI 콜백은 `withContext(Dispatchers.Main)`으로 전환한다. Communicator 코어는 IO Dispatcher에서 동작
- **직렬화된 쓰기**: Go의 writeQueue(buffered channel 64) 패턴을 `Channel(capacity = 64)` + 별도 write coroutine으로 구현한다
- **`Self` 타입 전달**: Kotlin 제네릭의 타입 소거(type erasure) 때문에 런타임에 `Self` 인스턴스를 직접 넘겨야 한다. Go의 `self Self` 필드 패턴을 그대로 사용한다

---

## Swift (iOS / macOS)

### 핵심 매핑

| Go | Swift |
|----|-------|
| `Transport` interface | `protocol Transport` |
| `baseClient[Self]` | `class BaseClient<Self>` 또는 `protocol` + associated type |
| goroutine | `Task` (Swift Concurrency) |
| `context.Context` | `Task` 취소 (cooperative cancellation) |
| channel (writeQueue) | `AsyncStream` 또는 `actor` 기반 queue |
| `sync.Once` | `DispatchOnce` 또는 `actor` 격리 |
| `atomic.Bool` | `OSAllocatedUnfairLock` 또는 `actor` |

### 주의사항

- **Actor 활용**: Swift 5.5+ actor를 사용하면 mutex/lock 없이 상태 보호가 가능하다. `Communicator`를 actor로 구현하면 `sync.RWMutex` 없이 동일한 안전성을 확보할 수 있다
- **protobuf**: SwiftProtobuf 패키지 사용. typeName은 `Message.protoMessageName`으로 추출. Swift module/type namespace는 wire identity와 분리한다. PROTOCOL.md 표에서 Swift 행 확인
- **`connCloseOnce` 패턴**: Swift에는 `DispatchOnce`가 deprecated됐다. actor의 상태 변수(`var isClosed = false`) + actor 격리로 대체한다
- **WebSocket**: `URLSessionWebSocketTask` (iOS 13+) 또는 `Network.framework`의 `NWConnection` 사용
- **메모리 관리**: 클로저에서 `[weak self]` 캡처를 빠뜨리면 retain cycle이 발생한다. `doClose`, heartbeat 타이머 클로저 모두 `[weak self]`를 사용한다
- **`Self` 제약**: Swift에서 제네릭 `Self`는 프로토콜 associated type으로 처리하거나, 구체 타입을 생성자에서 전달하는 Go의 `self Self` 필드 패턴을 그대로 사용한다

---

## Python

### 핵심 매핑

| Go | Python |
|----|--------|
| `Transport` interface | `Protocol` (typing) 또는 ABC |
| `baseClient[Self]` | `Generic[Self]` + `TypeVar` |
| goroutine | `asyncio.Task` |
| `context.Context` | `asyncio.Event` 또는 `CancelScope` (anyio) |
| channel (writeQueue) | `asyncio.Queue(maxsize=64)` |
| `sync.Once` | `asyncio.Lock` + bool flag |
| `atomic.Bool` | `asyncio` 단일 스레드 내에서는 plain bool 가능 |

### 주의사항

- **asyncio 기반**: `async/await` + `asyncio.Queue`로 Go의 goroutine + channel 구조를 근사한다. 동기 API는 제공하지 않는다
- **타입 안전성 한계**: Python에는 런타임 제네릭이 없다. `AddListenerTyped`처럼 완전한 타입 안전 헬퍼 구현이 어렵다. `TypeVar`와 `@overload`로 가능한 범위까지만 표현하고, 나머지는 docstring으로 보완한다
- **protobuf**: `protobuf` 패키지 사용. typeName은 `message.DESCRIPTOR.full_name`으로 추출. Python module/package path는 wire identity와 분리한다. PROTOCOL.md 표에서 Python 행 확인
- **스레드 안전**: asyncio는 단일 스레드이므로 대부분의 lock이 불필요하다. 단, 멀티스레드 환경에서 쓸 경우 `asyncio.Lock`을 사용한다
- **`connCloseOnce` 패턴**: bool flag + asyncio.Lock으로 구현한다. `asyncio.Event`의 `set()`은 멱등하므로 활용 가능하다
- **에러 처리**: Go의 명시적 error 반환 대신 exception을 사용하되, public API에서는 구체 exception 타입을 정의해서 문서화한다

---

## TypeScript

### 핵심 매핑

| Go | TypeScript |
|----|------------|
| `Transport` interface | `interface Transport` |
| `baseClient[Self]` | `abstract class BaseClient<T>` |
| goroutine | `Promise` + `async/await` |
| `context.Context` | `AbortSignal` (`AbortController`) |
| channel (writeQueue) | Async Queue (배열 기반 큐 또는 async iterator) |
| `sync.Once` | `boolean` 플래그 (단일 스레드이므로 충분) |
| `atomic.Bool` | `boolean` |

### 주의사항

- **크로스 환경 (Browser & Node.js)**: 코어 로직(`Communicator`, `BaseClient`)은 런타임 환경에 독립적으로 작성한다. 브라우저에서는 내장 `WebSocket`을 사용하고, Node.js에서는 `net`, `tls`, `http`, `https` 등 Node.js 내장 모듈 기반 Transport를 사용한다. TypeScript 런타임에 외부 WebSocket 패키지를 추가하지 않는다
- **protobuf**: protobuf는 핵심 프로토콜 계층이므로 예외로 취급한다. 현재 TypeScript 구현은 canonical schema에 맞춘 native codec을 사용하지만, protobuf binding/runtime을 도입해야 한다면 프로토콜 계층 용도로만 제한한다. `typeName`은 로컬 `MessageType.typeName` 값이 canonical proto full name 및 `PROTOCOL.md` 표와 일치해야 한다
- **단일 스레드 (Event Loop)**: JS/TS는 단일 스레드 기반이므로 메모리 동시 접근에 대한 Mutex/Lock(`sync.RWMutex`)은 불필요하다. 단, 비동기 컨텍스트(await) 간의 논리적 상태 오염은 주의한다
- **바이너리 처리**: Node.js의 `Buffer` 대신 표준 웹 API인 `Uint8Array`를 기준으로 작성하여 브라우저 환경 호환성을 확보한다
- **`connCloseOnce` 패턴**: 단순 `isClosed: boolean` 플래그로 멱등성을 보장한다. 만약 `Close()` 내에 `await`가 포함될 경우 중복 실행(re-entrancy) 방지에 유의한다
- **제네릭 헬퍼**: TypeScript의 강력한 타입 시스템을 활용해 `addListener<T>`, `sendRequest<TReq, TRes>` 등을 완벽히 타입 안전한 API로 구현한다

---

## 공통 금지사항

다음은 Dart 구현체의 레거시 패턴이다. 새 구현체에 복제하지 않는다.

- 생성자·에러 핸들러에서 `print()` / `console.log()` 직접 호출 — 로거 인터페이스를 두거나 생략한다
- `isAlive`, `nonce`를 public mutable 필드로 노출 — 내부 상태는 캡슐화한다
- `onDisconnected`와 `dispose`를 별도 메서드로 분리 — Go처럼 `Close()` 하나로 통일한다
- heartbeat 타이머를 재귀 호출로 구현 — 루프 또는 단순 타이머 재설정으로 구현한다
- TCP와 WebSocket 클라이언트에 heartbeat·disconnect 로직 중복 — `baseClient` 패턴으로 공유한다
