From c384516b47c96b889920eab61dc0e7d97fb18430 Mon Sep 17 00:00:00 2001 From: toki Date: Sun, 12 Apr 2026 07:53:31 +0900 Subject: [PATCH] feat: implement new communication patterns and add cross-test support - Add Transport class for unified communication layer - Implement new communicator patterns - Add VERSIONING.md for version management - Add crosstest files for Dart/Go integration testing - Update protocol documentation - Add new skills and templates for AI-assisted development - Various bug fixes and improvements to Dart implementation --- .claude/settings.json | 3 +- PORTING_GUIDE.md | 17 +- PROTOCOL.md | 10 +- README.md | 26 +- VERSIONING.md | 62 +++ dart/lib/src/communicator.dart | 35 +- dart/lib/src/protobuf_client.dart | 27 +- dart/lib/src/transport.dart | 6 + dart/lib/src/ws_protobuf_client.dart | 20 +- dart/lib/toki_socket.dart | 1 + dart/test/communicator_test.dart | 48 ++- dart/test/socket_test.dart | 23 ++ tasks/ai_first_improvements/code_review_0.log | 124 ++++++ tasks/ai_first_improvements/plan_0.log | 383 ++++++++++++++++++ templates/language/CROSSTEST_CHECKLIST.md | 44 ++ .../language/IMPLEMENTATION_CHECKLIST.md | 43 ++ templates/language/README.md | 41 ++ tools/check_proto_sync.sh | 38 ++ tools/generate_proto.sh | 30 ++ 19 files changed, 949 insertions(+), 32 deletions(-) create mode 100644 VERSIONING.md create mode 100644 dart/lib/src/transport.dart create mode 100644 tasks/ai_first_improvements/code_review_0.log create mode 100644 tasks/ai_first_improvements/plan_0.log create mode 100644 templates/language/CROSSTEST_CHECKLIST.md create mode 100644 templates/language/IMPLEMENTATION_CHECKLIST.md create mode 100644 templates/language/README.md create mode 100755 tools/check_proto_sync.sh create mode 100755 tools/generate_proto.sh diff --git a/.claude/settings.json b/.claude/settings.json index 73bb7c0..0cf2045 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -5,7 +5,8 @@ "Bash(apt-get install:*)", "Bash(sudo apt-get:*)", "Bash(protoc --version)", - "Bash(dart analyze:*)" + "Bash(dart analyze:*)", + "Bash(ls *.log)" ], "additionalDirectories": [ "/config/workspace/toki_socket/tasks" diff --git a/PORTING_GUIDE.md b/PORTING_GUIDE.md index df5801f..8196a69 100644 --- a/PORTING_GUIDE.md +++ b/PORTING_GUIDE.md @@ -2,8 +2,8 @@ ## 공통 원칙 -- **레퍼런스 구현체: `go/`** -- Dart 구현체는 레거시 패턴이 있으므로 참고하지 않는다 +- **레퍼런스 구현체: `go/`, `dart/`** +- 동시성·close-once 구조는 Go 구현체를 우선 참고하고, Dart 구현체는 public API와 parser map 사용 예시를 함께 참고한다 - 프로토콜 정의는 `PROTOCOL.md`가 단일 기준이다 - 각 언어의 관용적 패턴을 따르되, 아래 핵심 구조는 반드시 유지한다 @@ -20,6 +20,19 @@ --- +## 새 언어 추가 절차 + +1. 새 언어 디렉터리를 만들기 전에 `templates/language/`의 문서를 복사해 패키지 README와 체크리스트 초안으로 사용한다. +2. `PROTOCOL.md`와 `VERSIONING.md`의 현재 protocol version을 기준으로 지원 범위를 정한다. +3. canonical proto인 `dart/lib/src/packets/message_common.proto`에서 언어별 protobuf binding을 생성한다. Go처럼 generator 전용 option이 필요하면 언어 패키지 안에 copy를 둘 수 있지만, message schema는 `tools/check_proto_sync.sh`로 검증 가능해야 한다. +4. `Transport`와 `Communicator`를 먼저 구현하고, 그 위에 TCP/TLS+TCP/WS/WSS client/server를 올린다. +5. 같은 언어 단위 테스트를 작성한 뒤 `skills/add-crosstest-language/SKILL.md`의 runner 배치 규칙에 맞춰 양방향 crosstest를 추가한다. +6. README의 Implementations 표를 `Available`로 바꾸기 전에 formatter/linter, 단위 테스트, cross-language tests를 모두 통과시킨다. + +템플릿은 시작점과 완료 조건만 고정한다. 실제 소스 구조, 패키지 매니저, async runtime은 각 언어의 관용적 선택을 따른다. + +--- + ## C# (Unity / .NET) ### 핵심 매핑 diff --git a/PROTOCOL.md b/PROTOCOL.md index 8f5e674..89ee213 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -3,6 +3,8 @@ Binary TCP and WebSocket protocol using Protocol Buffers for message framing and type-based routing. Designed for bidirectional, heterogeneous communication across languages and platforms. +Current protocol version: `0.1` + --- ## Scope @@ -31,7 +33,7 @@ This protocol intentionally defines only the transport-level contract shared acr - 4 bytes, big-endian positive integer. Dart reads/writes it as `int32`; Go reads/writes the same 4 bytes as `uint32`. - Value: byte length of the following `PacketBase` protobuf payload - Value of `0`: reserved / no-op, receiver clears buffer -- Implementations may apply safety limits to payload size. The current Go implementation rejects TCP packets larger than 64 MiB. +- Implementations may apply safety limits to payload size. The current Dart and Go implementations reject TCP packets larger than 64 MiB. ### WebSocket / WSS @@ -95,6 +97,8 @@ Side A Side B - If no response within `heartbeatWaitTime` seconds → `onDisconnected()` → `dispose()` - A side that is already waiting for a heartbeat response consumes the echo and does not send another heartbeat. A side that is not waiting replies with `HeartBeat`. +Changing heartbeat timing semantics, response behavior, or disconnect rules is a breaking protocol candidate. Document the compatibility plan in [VERSIONING.md](VERSIONING.md) before changing it. + --- ## typeName Convention @@ -116,6 +120,8 @@ The current packet proto has no `package` declaration, so Dart and Go both use s **Important**: Verify typeName consistency across languages before connecting heterogeneous clients. +Changing the `typeName` derivation rule is a breaking protocol change unless all released implementations keep a compatibility path. See [VERSIONING.md](VERSIONING.md). + --- ## nonce @@ -124,6 +130,8 @@ The current packet proto has no `package` declaration, so Dart and Go both use s - Increments by `1` on every send call (including requests and responses) - Used for request-response correlation via `responseNonce` +Changing `nonce` or `responseNonce` semantics is a breaking protocol change unless older peers can continue to correlate requests and responses correctly. + --- ## Request-Response Pattern diff --git a/README.md b/README.md index 3ccf8ca..09a7d94 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ See [PROTOCOL.md](PROTOCOL.md) for the full wire format specification. For WebSocket/WSS transports, each binary frame contains one `PacketBase` protobuf payload without the TCP length header. +Protocol compatibility is tracked separately from language package versions. See [VERSIONING.md](VERSIONING.md). Even while packages are not published, the checked-in implementations must preserve the documented protocol contract. + --- ## Implementations @@ -40,6 +42,8 @@ For WebSocket/WSS transports, each binary frame contains one `PacketBase` protob | Python | Planned | `python/` | Server, tooling, scripting | | Rust | Planned | `rust/` | High-performance server, embedded | +New language implementations should start from [PORTING_GUIDE.md](PORTING_GUIDE.md) and the templates in [templates/language/](templates/language/). Mark an implementation available only after its same-language tests and cross-language tests pass. + --- ## Quick Start (Dart) @@ -80,20 +84,14 @@ await client.send(MyMessage()..text = 'hello'); ## Adding Message Types -Edit `dart/lib/src/packets/message_common.proto` and regenerate: +Edit the canonical proto at `dart/lib/src/packets/message_common.proto`, then regenerate all checked-in bindings: ```bash -cd dart -dart pub global activate protoc_plugin -protoc --dart_out=lib/src/packets lib/src/packets/message_common.proto +tools/generate_proto.sh +tools/check_proto_sync.sh ``` -For Go, keep `go/packets/message_common.proto` in sync with the canonical Dart proto. The Go copy includes `option go_package`, then regenerate from the Go module root: - -```bash -cd go -protoc --go_out=. --go_opt=paths=source_relative packets/message_common.proto -``` +The Go proto copy is allowed to keep only its Go-specific `option go_package` difference. `tools/check_proto_sync.sh` fails with a diff when the message schema drifts. --- @@ -169,6 +167,8 @@ Go also provides: ## Running Tests +Local commands are documented here for development and troubleshooting. Continuous verification is expected to run in an external tool, with Jenkins planned to execute the full Dart, Go, and cross-language test suite. + ```bash cd dart dart pub get @@ -179,3 +179,9 @@ dart test cd go go test ./... ``` + +When proto files change, also run: + +```bash +tools/check_proto_sync.sh +``` diff --git a/VERSIONING.md b/VERSIONING.md new file mode 100644 index 0000000..fc7b93e --- /dev/null +++ b/VERSIONING.md @@ -0,0 +1,62 @@ +# Versioning + +Toki Socket tracks protocol compatibility separately from language package versions. + +## Current Versions + +- Protocol version: `0.1` +- Dart package version: see `dart/pubspec.yaml` +- Go module version: repository tag based + +`0.1` is the current compatibility contract for the checked-in Dart and Go implementations. The wire format does not carry a protocol version field yet; compatibility is verified by shared proto definitions, unit tests, and cross-language tests. + +## Protocol Version + +The protocol version describes the wire-format and behavior contract that every language implementation must satisfy: + +- TCP uses a 4-byte big-endian payload length followed by one `PacketBase` protobuf payload. +- WebSocket and WSS use one binary frame per `PacketBase` protobuf payload. +- `PacketBase.typeName` routes the inner message. +- `nonce` increments for every outbound packet on a connection. +- `responseNonce` links a response packet to the request nonce. +- Heartbeat behavior follows `PROTOCOL.md`. + +Breaking protocol changes require a new major protocol version. Backward-compatible additions keep the same major protocol version but must include updated tests before release. + +## Package Version + +Each language implementation may publish on its own package cadence. Package versions communicate implementation releases, bug fixes, and language-specific API changes. + +A package release must state which protocol version it implements. A package version bump does not imply a protocol version bump unless the wire-format or required behavior changes. + +## Breaking Protocol Changes + +Examples of breaking protocol changes: + +- Renaming or removing `PacketBase` fields. +- Changing `typeName` from simple proto names to fully-qualified names without a compatibility path. +- Changing `nonce` or `responseNonce` semantics. +- Changing heartbeat request/response timing or echo behavior in a way that disconnects older peers. +- Changing TCP framing, byte order, max packet handling, or WebSocket binary-frame rules. + +## Non-breaking Protocol Changes + +Examples of non-breaking changes: + +- Adding a new protobuf message type. +- Adding optional fields to application messages when older peers can ignore them. +- Adding a new language implementation that passes the existing protocol and cross-language tests. +- Tightening tests or documentation without changing wire behavior. + +Backward-compatible message additions require updated parser maps and cross-language tests before release. + +## New Language Compatibility + +A new language implementation must target the current protocol version unless its README explicitly says otherwise. Before it is listed as available, it must: + +- Generate protobuf bindings from the canonical schema. +- Use the same `typeName` values as Dart and Go. +- Implement TCP, TLS+TCP, WS, and WSS framing consistently where the language runtime supports them. +- Implement request-response correlation with `nonce` and `responseNonce`. +- Handle heartbeat automatically inside the client/server core. +- Pass same-language tests and cross-language tests against at least one available implementation. diff --git a/dart/lib/src/communicator.dart b/dart/lib/src/communicator.dart index 495d436..0c287f0 100644 --- a/dart/lib/src/communicator.dart +++ b/dart/lib/src/communicator.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; import 'package:protobuf/protobuf.dart'; import 'packets/message_common.pb.dart'; +import 'transport.dart'; abstract class Communicator { Map _handlerDic = {}; @@ -11,6 +12,7 @@ abstract class Communicator { Map _pendingRequests = {}; late Map)> _instanceGenerator; Future _outboundWrite = Future.value(); + Transport? _transport; /// Monotonically increasing nonce. Shared by send / sendRequest / response. int _nonce = 0; @@ -29,8 +31,10 @@ abstract class Communicator { Communicator(); void initialize( - Map)> instanceGenerator) { + Map)> instanceGenerator, + {Transport? transport}) { _instanceGenerator = instanceGenerator; + _transport = transport ?? _LegacyCommunicatorTransport(this); } T Function(List) getGenerator(String type) { @@ -41,12 +45,21 @@ abstract class Communicator { return _instanceGenerator[type] as T Function(List); } - /// Transport-specific framing and write. Implemented by each client subclass. - Future transmitPacket(PacketBase base); + /// Deprecated compatibility path for subclasses that still override writes. + /// + /// New clients should pass a [Transport] to [initialize] instead. + @Deprecated('Pass a Transport to initialize instead.') + Future transmitPacket(PacketBase base) { + return Future.error(StateError('transport is not initialized')); + } /// Serializes writes so stream transports do not interleave packets. Future queuePacket(PacketBase base) { - final write = _outboundWrite.then((_) => transmitPacket(base)); + final transport = _transport; + if (transport == null) { + return Future.error(StateError('transport is not initialized')); + } + final write = _outboundWrite.then((_) => transport.writePacket(base)); _outboundWrite = write.catchError((_) {}); return write; } @@ -175,6 +188,20 @@ abstract class Communicator { } } +class _LegacyCommunicatorTransport implements Transport { + final Communicator _communicator; + + _LegacyCommunicatorTransport(this._communicator); + + @override + Future writePacket(PacketBase base) { + return _communicator.transmitPacket(base); + } + + @override + Future close() async {} +} + abstract class IDataHandler { void onMessage(List data); } diff --git a/dart/lib/src/protobuf_client.dart b/dart/lib/src/protobuf_client.dart index 6d7ff4a..d24f5ad 100644 --- a/dart/lib/src/protobuf_client.dart +++ b/dart/lib/src/protobuf_client.dart @@ -7,10 +7,14 @@ import 'dart:typed_data'; import 'package:protobuf/protobuf.dart'; import 'base_client.dart'; import 'packets/message_common.pb.dart'; +import 'transport.dart'; + +const int maxPacketSize = 64 << 20; abstract class ProtobufClient extends BaseClient { final int _headerSize = 4; final Socket _socket; + late final _TcpSocketTransport _transport; /// Plain TCP connection. static Future connect(String host, int port) => @@ -29,11 +33,12 @@ abstract class ProtobufClient extends BaseClient { Map)> parserMap) { initSelf(this); initHeartbeat(heartbeatIntervalTime, heartbeatWaitTime); + _transport = _TcpSocketTransport(_socket, _headerSize); isAlive = true; parserMap.addAll({ (HeartBeat).toString(): HeartBeat.fromBuffer, }); - super.initialize(parserMap); + super.initialize(parserMap, transport: _transport); _socket.listen(onData, onError: onError).asFuture().then((_) { close(); }); @@ -67,6 +72,10 @@ abstract class ProtobufClient extends BaseClient { _arrivedData.clear(); return; } + if (length < 0 || length > maxPacketSize) { + unawaited(close()); + return; + } _length = length; } @@ -86,7 +95,19 @@ abstract class ProtobufClient extends BaseClient { } @override - Future transmitPacket(PacketBase base) async { + Future closeTransport() async { + await _transport.close(); + } +} + +class _TcpSocketTransport implements Transport { + final Socket _socket; + final int _headerSize; + + _TcpSocketTransport(this._socket, this._headerSize); + + @override + Future writePacket(PacketBase base) async { final baseBytes = base.writeToBuffer(); final header = Uint8List(_headerSize) ..buffer.asByteData().setInt32(0, baseBytes.length); @@ -95,7 +116,7 @@ abstract class ProtobufClient extends BaseClient { } @override - Future closeTransport() async { + Future close() async { try { await _socket.close(); _socket.destroy(); diff --git a/dart/lib/src/transport.dart b/dart/lib/src/transport.dart new file mode 100644 index 0000000..ddd062b --- /dev/null +++ b/dart/lib/src/transport.dart @@ -0,0 +1,6 @@ +import 'packets/message_common.pb.dart'; + +abstract interface class Transport { + Future writePacket(PacketBase base); + Future close(); +} diff --git a/dart/lib/src/ws_protobuf_client.dart b/dart/lib/src/ws_protobuf_client.dart index 1877e9a..2a57f7d 100644 --- a/dart/lib/src/ws_protobuf_client.dart +++ b/dart/lib/src/ws_protobuf_client.dart @@ -7,9 +7,11 @@ import 'dart:typed_data'; import 'package:protobuf/protobuf.dart'; import 'base_client.dart'; import 'packets/message_common.pb.dart'; +import 'transport.dart'; abstract class WsProtobufClient extends BaseClient { final WebSocket _ws; + late final _WebSocketTransport _transport; /// Plain WebSocket connection. static Future connect(String host, int port, @@ -27,9 +29,10 @@ abstract class WsProtobufClient extends BaseClient { Map)> parserMap) { initSelf(this); initHeartbeat(heartbeatIntervalTime, heartbeatWaitTime); + _transport = _WebSocketTransport(_ws); isAlive = true; parserMap.addAll({(HeartBeat).toString(): HeartBeat.fromBuffer}); - super.initialize(parserMap); + super.initialize(parserMap, transport: _transport); _ws.listen(_onMessage, onError: onError, onDone: close); addListener(onHeartBeat); sendHeartBeat(); @@ -46,12 +49,23 @@ abstract class WsProtobufClient extends BaseClient { } @override - Future transmitPacket(PacketBase base) async { + Future closeTransport() async { + await _transport.close(); + } +} + +class _WebSocketTransport implements Transport { + final WebSocket _ws; + + _WebSocketTransport(this._ws); + + @override + Future writePacket(PacketBase base) async { _ws.add(base.writeToBuffer()); } @override - Future closeTransport() async { + Future close() async { try { await _ws.close(); } catch (_) { diff --git a/dart/lib/toki_socket.dart b/dart/lib/toki_socket.dart index cab695f..9e3a450 100644 --- a/dart/lib/toki_socket.dart +++ b/dart/lib/toki_socket.dart @@ -1,6 +1,7 @@ library toki_socket; export 'src/communicator.dart'; +export 'src/transport.dart'; export 'src/base_client.dart'; export 'src/protobuf_client.dart'; export 'src/protobuf_server.dart'; diff --git a/dart/test/communicator_test.dart b/dart/test/communicator_test.dart index 698b78a..b9f3eab 100644 --- a/dart/test/communicator_test.dart +++ b/dart/test/communicator_test.dart @@ -3,19 +3,14 @@ import 'package:test/test.dart'; import 'package:toki_socket/toki_socket.dart'; class _FakeCommunicator extends Communicator { - final sentPackets = []; + final _FakeTransport transport = _FakeTransport(); _FakeCommunicator() { isAlive = true; initialize({ TestData.getDefault().info_.qualifiedMessageName: TestData.fromBuffer, HeartBeat.getDefault().info_.qualifiedMessageName: HeartBeat.fromBuffer, - }); - } - - @override - Future transmitPacket(PacketBase base) async { - sentPackets.add(base); + }, transport: transport); } void closeForTest() { @@ -35,6 +30,23 @@ class _FakeCommunicator extends Communicator { } } +class _FakeTransport implements Transport { + final sentPackets = []; + Object? error; + + @override + Future writePacket(PacketBase base) async { + final error = this.error; + if (error != null) { + throw error; + } + sentPackets.add(base); + } + + @override + Future close() async {} +} + void main() { group('Communicator protocol guards', () { test('response typeName mismatch completes sendRequest with error', @@ -47,7 +59,7 @@ void main() { ); await Future.delayed(Duration.zero); - final requestNonce = communicator.sentPackets.single.nonce; + final requestNonce = communicator.transport.sentPackets.single.nonce; communicator.onReceivedData( HeartBeat.getDefault().info_.qualifiedMessageName, HeartBeat().writeToBuffer(), @@ -113,5 +125,25 @@ void main() { throwsA(isA()), ); }); + + test('queuePacket uses injected transport', () async { + final communicator = _FakeCommunicator(); + final packet = PacketBase() + ..typeName = TestData.getDefault().info_.qualifiedMessageName + ..nonce = 1 + ..data = (TestData()..index = 7).writeToBuffer(); + + await communicator.queuePacket(packet); + + expect(communicator.transport.sentPackets, hasLength(1)); + expect(communicator.transport.sentPackets.single, same(packet)); + + final error = StateError('write failed'); + communicator.transport.error = error; + await expectLater( + communicator.queuePacket(PacketBase()..typeName = 'TestData'), + throwsA(same(error)), + ); + }); }); } diff --git a/dart/test/socket_test.dart b/dart/test/socket_test.dart index c18b4bc..9c9d4b9 100644 --- a/dart/test/socket_test.dart +++ b/dart/test/socket_test.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:typed_data'; import 'package:test/test.dart'; import 'package:toki_socket/toki_socket.dart'; @@ -330,6 +331,28 @@ void main() { ); expect(client.isAlive, isFalse); }); + + test('TCP closes on oversized packet length', () async { + final rawSocket = await Socket.connect(_host, _testPort); + try { + await Future.delayed(const Duration(milliseconds: 100)); + final serverClient = server.connectedClients.last; + final disconnected = Completer(); + serverClient.addDisconnectListener((_) { + if (!disconnected.isCompleted) disconnected.complete(); + }); + + final header = Uint8List(4) + ..buffer.asByteData().setInt32(0, maxPacketSize + 1); + rawSocket.add(header); + await rawSocket.flush(); + + await disconnected.future.timeout(const Duration(seconds: 2)); + expect(serverClient.isAlive, isFalse); + } finally { + rawSocket.destroy(); + } + }); }); group('Heartbeat timeout', () { diff --git a/tasks/ai_first_improvements/code_review_0.log b/tasks/ai_first_improvements/code_review_0.log new file mode 100644 index 0000000..b5a6d73 --- /dev/null +++ b/tasks/ai_first_improvements/code_review_0.log @@ -0,0 +1,124 @@ + + +# Code Review Reference — AI_FIRST + +## 개요 + +task=ai_first_improvements, plan=0, tag=AI_FIRST + +## 구현 항목별 완료 여부 + +| 항목 | 완료 | 비고 | +|------|------|------| +| [AI_FIRST-1] Dart 전송 계층을 Go 기준으로 정렬 | [x] | `Transport` 추가, TCP/WS adapter 주입, TCP max packet guard 및 회귀 테스트 추가 | +| [AI_FIRST-2] Proto sync와 codegen 검증 도구 추가 | [x] | `tools/check_proto_sync.sh`, `tools/generate_proto.sh` 추가 및 README 갱신 | +| [AI_FIRST-3] 프로토콜/패키지 버전 정책 문서화 | [x] | `VERSIONING.md` 추가, `PROTOCOL.md`/README에 protocol version과 breaking 후보 명시 | +| [AI_FIRST-4] 새 언어 구현 scaffolding과 검증 체크리스트 추가 | [x] | `templates/language/` 3개 템플릿 추가, 포팅 절차 문서화 | + +## 계획 대비 변경 사항 + +- 계획서대로 구현. +- Dart `Communicator.transmitPacket`은 즉시 제거하지 않고 deprecated fallback으로 유지했다. 기존 `initialize(parserMap)`만 호출하는 subclass 호환을 보존하면서 새 구현은 `Transport` 주입 경로를 사용한다. +- `tools/check_proto_sync.sh`는 `option go_package`뿐 아니라 빈 줄 차이도 정규화한다. schema drift 검사가 목적이라 포맷 공백만으로 실패하지 않게 했다. + +## 주요 설계 결정 + +- Dart 전송 계층은 `dart/lib/src/transport.dart`의 최소 `Transport.writePacket`/`Transport.close` 인터페이스로 분리했다. +- TCP/WS client public 생성자는 유지하고 내부에서 `_TcpSocketTransport`, `_WebSocketTransport`를 만들어 `Communicator.initialize(..., transport: ...)`에 주입한다. +- TCP parser는 Go의 `MaxPacketSize = 64 << 20`와 맞춰 `maxPacketSize`를 추가했고, 음수 또는 초과 length를 받으면 `close()`를 예약한다. +- Proto sync 검사는 Dart canonical proto와 Go proto copy를 비교하되 Go 전용 `option go_package`는 허용한다. +- 새 언어 템플릿은 실제 코드를 생성하지 않고 README, 구현 체크리스트, crosstest 체크리스트만 제공한다. + +## 리뷰어를 위한 체크포인트 + +- `dart/lib/src/communicator.dart`: `initialize`의 `transport` 선택 인자, deprecated fallback, `queuePacket`의 serialized write 경로. +- `dart/lib/src/protobuf_client.dart`: TCP transport adapter write/close 분리와 oversized/negative packet length close 처리. +- `dart/lib/src/ws_protobuf_client.dart`: WebSocket binary write/close adapter 분리. +- `tools/check_proto_sync.sh`: Go 전용 proto option 허용 범위와 diff 출력. +- `templates/language/`: 새 언어 구현에 과한 코드 구조를 강제하지 않고 필수 검증 조건만 담았는지 확인. + +## 검증 결과 + +``` +$ tools/check_proto_sync.sh +Proto schemas are in sync. + +$ cd dart && dart analyze +Analyzing dart... +No issues found! + +$ cd dart && dart test +00:15 +42: All tests passed! + +$ cd go && PATH=/config/go-sdk/go/bin:/config/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod go test ./... +? toki-labs.com/toki_socket/go [no test files] +? toki-labs.com/toki_socket/go/crosstest [no test files] +? toki-labs.com/toki_socket/go/crosstest/dart_go_client [no test files] +? toki-labs.com/toki_socket/go/examples/tcp_echo [no test files] +? toki-labs.com/toki_socket/go/examples/ws_echo [no test files] +? toki-labs.com/toki_socket/go/packets [no test files] +ok toki-labs.com/toki_socket/go/test (cached) + +$ find templates/language -maxdepth 1 -type f +templates/language/IMPLEMENTATION_CHECKLIST.md +templates/language/CROSSTEST_CHECKLIST.md +templates/language/README.md + +$ cd dart && PATH=/config/go-sdk/go/bin:/config/go/bin:$PATH dart run crosstest/dart_go.dart +INFO typeName dart=TestData +INFO typeName go=TestData +PASS scenario=1 detail=fire-and-forget sent +PASS scenario=2 detail=received push from dart server +PASS scenario=3 detail=single request response matched +PASS scenario=4 detail=concurrent request responses matched +PASS scenario=1 detail=fire-and-forget sent +PASS scenario=2 detail=received push from dart server +PASS scenario=3 detail=single request response matched +PASS scenario=4 detail=concurrent request responses matched +PASS all dart-server/go-client crosstests passed + +$ cd go && PATH=/config/go-sdk/go/bin:/config/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod go run ./crosstest +INFO typeName go=TestData +INFO typeName dart=TestData +PASS scenario=1 detail=fire-and-forget sent +PASS scenario=2 detail=received push from go server +PASS scenario=3 detail=single request response matched +PASS scenario=4 detail=concurrent request responses matched +PASS scenario=1 detail=fire-and-forget sent +PASS scenario=2 detail=received push from go server +PASS scenario=3 detail=single request response matched +PASS scenario=4 detail=concurrent request responses matched +PASS all go-server/dart-client crosstests passed +``` + +참고: crosstest는 기본 sandbox에서 SDK cache 쓰기 또는 localhost bind가 차단되어 승인된 외부 실행으로 재시도했다. + +--- + +## 코드리뷰 결과 + +### 종합 판정 + +**PASS** + +> 4개 항목 모두 계획대로 구현됐고, 테스트 수(42개)와 검증 출력이 실제 코드와 일치한다. 전송 계층 분리, proto sync 도구, 버전 정책 문서, 언어 추가 템플릿 모두 품질 이슈 없음. + +### 차원별 평가 + +| 차원 | 판정 | 비고 | +|------|------|------| +| 정확성 (Correctness) | Pass | maxPacketSize 가드, queuePacket 에러 전파, _LegacyCommunicatorTransport 위임 경로 모두 정확 | +| 완성도 (Completeness) | Pass | 계획서의 모든 체크리스트 항목 구현 확인 | +| 테스트 커버리지 | Pass | `queuePacket uses injected transport`(에러 전파 포함), `TCP closes on oversized packet length` 두 신규 테스트 모두 의미 있는 assertion | +| API 계약 | Pass | `toki_socket.dart`에 `transport.dart` export 추가, 기존 `ProtobufClient`/`WsProtobufClient` public 생성자 유지 | +| 코드 품질 | Pass | 디버그 출력·dead import·leftover TODO 없음 | +| 계획 대비 이탈 | Pass | `transmitPacket` deprecated 유지 결정이 CODE_REVIEW.md에 명시됨 | +| 검증 신뢰도 | Pass | 보고된 출력(42개 통과, crosstest PASS)이 실제 코드 로직과 일치 | + +### 발견된 문제 + +- **[Nit]** `dart/lib/src/communicator.dart:59` — `if (transport == null)` 분기는 `initialize()` 호출 후에는 실행되지 않는 방어 코드다. `initialize()` 가 항상 non-null 값을 `_transport`에 할당하므로 실질적으로 dead branch다. 제거하거나 `assert(false, 'queuePacket called before initialize')` 로 교체 가능하지만 동작에 영향 없음. + +### 다음 단계 + +**[PASS]** 추가 작업 불필요. 아카이브 완료. diff --git a/tasks/ai_first_improvements/plan_0.log b/tasks/ai_first_improvements/plan_0.log new file mode 100644 index 0000000..da8eb69 --- /dev/null +++ b/tasks/ai_first_improvements/plan_0.log @@ -0,0 +1,383 @@ + +# AI-first 개선 계획 + +## 이 파일을 읽는 구현 에이전트에게 + +- 각 항목의 체크리스트를 순서대로 완료한다. +- 항목 완료마다 **중간 검증** 커맨드를 실행하고 통과를 확인한다. +- 모든 항목 완료 후 **최종 검증** 커맨드를 실행한다. +- 작업이 끝나면 `CODE_REVIEW.md`의 각 섹션을 실제 구현 내용으로 채운다: + - 구현 항목별 완료 여부 체크 + - 계획 대비 변경 사항 (없으면 "계획서 그대로 구현") + - 주요 설계 결정 + - 리뷰어를 위한 체크포인트 + - 검증 결과 (실제 실행한 명령과 출력 붙여넣기) + +## 배경 + +현재 프로젝트는 Go와 Dart 양방향 crosstest가 통과하는 좋은 기반을 갖고 있다. 다음 단계는 AI 에이전트가 새 언어, 새 메시지 타입, 새 릴리스 규칙을 안전하게 따라갈 수 있도록 반복 작업을 명문화하고 자동화하는 것이다. CI 실행 자체는 README에 명시한 대로 Jenkins가 담당하므로, 이 계획은 저장소 내부의 구현 정리와 검증 도구, 버전 정책, 언어 추가 scaffolding에 집중한다. + +--- + +### [AI_FIRST-1] Dart 전송 계층을 Go 기준으로 정렬 + +**문제** + +Go는 `Transport` 인터페이스를 통해 `Communicator`가 TCP/WS 세부 구현을 모르도록 분리되어 있다. 반면 Dart는 `Communicator`가 `transmitPacket` 추상 메서드를 직접 요구하고, TCP/WS 클라이언트가 각각 heartbeat와 전송 세부를 갖는다. + +```dart +// Before — dart/lib/src/communicator.dart:44 +/// Transport-specific framing and write. Implemented by each client subclass. +Future transmitPacket(PacketBase base); + +/// Serializes writes so stream transports do not interleave packets. +Future queuePacket(PacketBase base) { + final write = _outboundWrite.then((_) => transmitPacket(base)); + _outboundWrite = write.catchError((_) {}); + return write; +} +``` + +`PORTING_GUIDE.md:14`는 새 언어 구현에서 `Transport` 분리를 반드시 유지해야 한다고 말한다. Dart가 현재 레거시 패턴으로 남아 있으면 이후 AI 포팅 작업이 Go와 Dart 사이에서 서로 다른 기준을 학습하게 된다. + +**해결 방법** + +`dart/lib/src/transport.dart`를 새로 만들고 Dart에도 Go와 같은 최소 전송 인터페이스를 추가한다. `Communicator`는 `Transport? _transport`를 보유하고, `initialize`에서 parser map과 transport를 함께 받는 새 경로를 제공한다. 기존 `ProtobufClient`/`WsProtobufClient` public 생성자는 유지하되 내부에서 각각 TCP/WS transport adapter를 만들어 `Communicator`에 주입한다. + +```dart +// After +abstract interface class Transport { + Future writePacket(PacketBase base); + Future close(); +} + +Future queuePacket(PacketBase base) { + final transport = _transport; + if (transport == null) { + return Future.error(StateError('transport is not initialized')); + } + final write = _outboundWrite.then((_) => transport.writePacket(base)); + _outboundWrite = write.catchError((_) {}); + return write; +} +``` + +TCP 파서는 Go와 같은 최대 패킷 제한도 갖는다. Go는 `go/tcp_client.go:17`의 `MaxPacketSize = 64 << 20`과 `go/tcp_client.go:73`의 초과 close 처리가 있다. Dart도 같은 상수를 두고 음수/초과 길이는 즉시 `close()`로 처리한다. + +```dart +// Before — dart/lib/src/protobuf_client.dart:61 +final length = Uint8List.fromList(_arrivedData.sublist(0, _headerSize)) + .buffer + .asByteData() + .getInt32(0); +if (length == 0) { + _length = null; + _arrivedData.clear(); + return; +} +_length = length; + +// After +final length = Uint8List.fromList(_arrivedData.sublist(0, _headerSize)) + .buffer + .asByteData() + .getInt32(0); +if (length == 0) { + _length = null; + _arrivedData.clear(); + return; +} +if (length < 0 || length > maxPacketSize) { + unawaited(close()); + return; +} +_length = length; +``` + +**수정 파일 및 체크리스트** + +- [ ] `dart/lib/src/transport.dart` — Dart 전송 인터페이스 추가 + - [ ] `Transport.writePacket(PacketBase base)` 정의 + - [ ] `Transport.close()` 정의 +- [ ] `dart/lib/src/communicator.dart` — transport 주입 경로 추가 + - [ ] 기존 `initialize(Map<...>)` 호출부와 호환되는 migration 경로 설계 + - [ ] `queuePacket`이 `transport.writePacket`을 사용하도록 변경 + - [ ] 기존 `transmitPacket` 추상 메서드 제거 또는 deprecated 호환 경로 유지 여부를 `CODE_REVIEW.md`에 기록 +- [ ] `dart/lib/src/protobuf_client.dart` — TCP transport adapter 적용 + - [ ] `maxPacketSize = 64 << 20` 추가 + - [ ] 음수 길이와 최대 크기 초과 시 `close()` 호출 + - [ ] `_socket.add`/`flush` 로직을 TCP transport adapter로 이동 +- [ ] `dart/lib/src/ws_protobuf_client.dart` — WS transport adapter 적용 + - [ ] `_ws.add(base.writeToBuffer())` 로직을 WS transport adapter로 이동 + - [ ] close 로직을 adapter와 client close 순서가 충돌하지 않게 정리 +- [ ] `dart/lib/toki_socket.dart` — 필요한 경우 `transport.dart` export + +**테스트 작성** + +| 파일 | 테스트 이름 | 검증 목표 | +|------|-------------|-----------| +| `dart/test/communicator_test.dart` | `queuePacket uses injected transport` | fake transport가 받은 `PacketBase`와 에러 전파 확인 | +| `dart/test/socket_test.dart` | `TCP closes on oversized packet length` | 수동 socket으로 64 MiB 초과 header 전송 후 disconnect 확인 | + +내부 리팩터링이지만 전송 계층과 TCP 파서 동작이 바뀌므로 회귀 테스트를 추가한다. + +**중간 검증** + +```bash +# 정적 분석 +cd dart && dart analyze +# 테스트 +cd dart && dart test +``` + +Expected: 기존 40개 + 신규 2개 = 총 42개 통과 + +--- + +### [AI_FIRST-2] Proto sync와 codegen 검증 도구 추가 + +**문제** + +`PROTOCOL.md:182`는 Dart proto를 canonical source로 지정하고, `PROTOCOL.md:185`는 Go copy를 수동으로 sync해야 한다고 설명한다. README도 같은 수동 절차를 반복한다. + +```markdown +// Before — README.md:81 +## Adding Message Types + +Edit `dart/lib/src/packets/message_common.proto` and regenerate: +``` + +이 상태에서는 AI 에이전트나 사람이 proto를 바꾼 뒤 Go copy, generated Dart, generated Go 중 하나를 빠뜨리기 쉽다. + +**해결 방법** + +`tools/` 아래에 proto 검증과 재생성 스크립트를 추가한다. Go proto에는 `option go_package`만 허용되는 차이로 남기고, 나머지 message body가 Dart canonical proto와 일치하는지 검사한다. 재생성 스크립트는 Dart와 Go 생성 명령을 한 곳에 모은다. + +```bash +# After +tools/check_proto_sync.sh +tools/generate_proto.sh +``` + +`README.md`의 Adding Message Types 섹션은 수동 명령 나열에서 도구 기반 절차로 바꾼다. + +````markdown +// After +## Adding Message Types + +Edit `dart/lib/src/packets/message_common.proto`, then run: + +```bash +tools/generate_proto.sh +tools/check_proto_sync.sh +``` +```` + +**수정 파일 및 체크리스트** + +- [ ] `tools/check_proto_sync.sh` — Dart/Go proto sync 검사 + - [ ] Dart canonical proto와 Go proto를 읽는다 + - [ ] Go 전용 `option go_package` 라인을 제외한 schema body 비교 + - [ ] mismatch 시 diff와 함께 non-zero exit +- [ ] `tools/generate_proto.sh` — Dart/Go protobuf 재생성 + - [ ] Dart `protoc --dart_out=lib/src/packets ...` 실행 + - [ ] Go `protoc --go_out=. --go_opt=paths=source_relative ...` 실행 + - [ ] 필요한 binary가 없을 때 명확한 에러 메시지 출력 +- [ ] `README.md` — Adding Message Types와 Running Tests 갱신 + - [ ] proto 변경 절차를 `tools/generate_proto.sh` 중심으로 정리 + - [ ] Jenkins가 외부 도구에서 전체 검증을 실행한다는 문구 유지 +- [ ] `.gitignore` — 필요 시 generated temp/output 제외 + +**테스트 작성** + +스크립트 추가이므로 별도 Dart/Go 단위 테스트는 불필요하다. 대신 `tools/check_proto_sync.sh`를 직접 실행하고, 최종 검증 명령에 포함한다. + +**중간 검증** + +```bash +# proto sync +tools/check_proto_sync.sh +# 기존 테스트 +cd dart && dart test +cd go && go test ./... +``` + +Expected: proto sync 성공, Dart 40개 이상 통과, Go 전체 테스트 통과 + +--- + +### [AI_FIRST-3] 프로토콜/패키지 버전 정책 문서화 + +**문제** + +Dart 패키지는 `version: 0.1.0`을 갖지만 `publish_to: none`이라 배포 정책이 닫혀 있고, 프로토콜 자체의 호환성 버전은 별도로 없다. + +```yaml +// Before — dart/pubspec.yaml:1 +name: toki_socket +description: Multi-language standard socket protocol library. Dart implementation. +version: 0.1.0 +publish_to: none +``` + +프로토콜 필드 추가, `typeName` 규칙 변경, heartbeat 의미 변경 같은 변화가 언어별 패키지 버전과 어떻게 연결되는지 기준이 없다. AI-first 포팅에서는 이 기준이 없으면 새 언어 구현체가 어느 프로토콜 버전을 만족해야 하는지 판단하기 어렵다. + +**해결 방법** + +`VERSIONING.md`를 추가하고 protocol compatibility, package version, breaking change 기준을 분리한다. `PROTOCOL.md` 상단에는 현재 protocol version을 명시하고, README에서는 버전 정책 문서로 연결한다. 당장은 wire format을 바꾸지 않으므로 proto message에 version field를 추가하지 않는다. + +```markdown +// After +# Versioning + +- Protocol version: wire-format compatibility contract. +- Package version: language implementation release version. +- Breaking protocol changes require a new major protocol version. +- Backward-compatible message additions require crosstest updates before release. +``` + +**수정 파일 및 체크리스트** + +- [ ] `VERSIONING.md` — 새 버전 정책 문서 + - [ ] protocol version과 package version의 차이 정의 + - [ ] breaking/non-breaking 예시 작성 + - [ ] 새 언어 구현체가 따라야 할 최소 호환성 기준 작성 +- [ ] `PROTOCOL.md` — protocol version 명시 + - [ ] 문서 상단에 `Current protocol version: 0.1` 추가 + - [ ] `typeName`, `nonce`, `responseNonce`, heartbeat 변경은 breaking 후보임을 연결 +- [ ] `README.md` — Versioning 섹션 추가 + - [ ] `VERSIONING.md` 링크 + - [ ] 패키지 배포 전에도 프로토콜 호환성 기준은 유지한다는 문구 추가 +- [ ] `dart/pubspec.yaml` — 변경 없음 + - [ ] 실제 패키지 배포를 하지 않으므로 `publish_to: none`은 유지 + +**테스트 작성** + +문서 정책 추가라 단위 테스트는 불필요하다. 다만 최종 검증에서 기존 테스트와 crosstest를 실행해 문서화한 현재 호환성 상태가 실제로 유지되는지 확인한다. + +**중간 검증** + +```bash +# 문서 변경 후 기본 검증 +cd dart && dart analyze +cd go && go test ./... +``` + +Expected: analyzer 이슈 없음, Go 전체 테스트 통과 + +--- + +### [AI_FIRST-4] 새 언어 구현 scaffolding과 검증 체크리스트 추가 + +**문제** + +`PORTING_GUIDE.md`는 C#, Kotlin, Swift, Python, Rust 구현 지침을 잘 설명하지만 구현 시작점과 완료 조건은 문서 안에 흩어져 있다. `skills/add-crosstest-language/SKILL.md`에는 crosstest 규칙이 있으나, 일반 구현자가 어디서 어떤 파일을 만들어 시작해야 하는지 scaffold가 없다. + +```markdown +// Before — PORTING_GUIDE.md:23 +## C# (Unity / .NET) + +### 핵심 매핑 +``` + +AI 에이전트가 새 언어를 추가할 때 매번 디렉터리 구조, README, crosstest runner, parser map 예제를 새로 설계하게 된다. + +**해결 방법** + +`templates/language/` 아래에 새 언어 구현체가 채워 넣을 README, protocol checklist, crosstest checklist 템플릿을 둔다. 실제 코드는 언어별로 달라 템플릿이 과하게 코드를 생성하지 않게 하고, 반드시 만족해야 하는 파일 구조와 검증 명령만 고정한다. + +```text +// After +templates/language/README.md +templates/language/IMPLEMENTATION_CHECKLIST.md +templates/language/CROSSTEST_CHECKLIST.md +``` + +`PORTING_GUIDE.md`에는 "새 언어 추가 절차" 섹션을 추가해서 템플릿, proto generation, crosstest 작성 순서를 연결한다. + +**수정 파일 및 체크리스트** + +- [ ] `templates/language/README.md` — 새 언어 package README 템플릿 + - [ ] transport, parser map, client/server, tests 항목 placeholder 포함 +- [ ] `templates/language/IMPLEMENTATION_CHECKLIST.md` — 구현 완료 조건 + - [ ] `Transport`, `Communicator`, TCP, WS, TLS/WSS, heartbeat, request-response 항목 포함 + - [ ] typeName compatibility 확인 항목 포함 +- [ ] `templates/language/CROSSTEST_CHECKLIST.md` — 크로스언어 테스트 완료 조건 + - [ ] send-push, single request, concurrent request, TCP, WS 항목 포함 + - [ ] 양방향 테스트와 PASS/FAIL line format 요구 +- [ ] `PORTING_GUIDE.md` — 새 언어 추가 절차 추가 + - [ ] templates 사용 방법 링크 + - [ ] `skills/add-crosstest-language/SKILL.md`의 runner 배치 규칙과 충돌하지 않게 설명 +- [ ] `README.md` — Multi-language 확장 안내 추가 + - [ ] 새 언어는 `PORTING_GUIDE.md`와 templates를 먼저 따르도록 안내 + +**테스트 작성** + +템플릿과 문서 추가이므로 단위 테스트는 불필요하다. 하지만 Markdown 링크와 경로가 실제로 존재하는지 `find`/`rg`로 확인하고, 기존 Dart/Go 테스트를 유지 검증으로 실행한다. + +**중간 검증** + +```bash +# 템플릿 경로 확인 +find templates/language -maxdepth 1 -type f +# 기존 테스트 +cd dart && dart test +cd go && go test ./... +``` + +Expected: 템플릿 3개 존재, Dart 40개 이상 통과, Go 전체 테스트 통과 + +--- + +## 의존 관계 및 구현 순서 + +```text +[AI_FIRST-1] → [AI_FIRST-2] → [AI_FIRST-3] → [AI_FIRST-4] +``` + +`AI_FIRST-1`은 Dart 구현을 기준 구조에 맞추는 작업이라 이후 문서와 템플릿의 예시가 흔들리지 않게 먼저 끝낸다. `AI_FIRST-2`는 proto 변경 루프를 안정화하고, `AI_FIRST-3`은 안정화된 현재 wire contract를 버전 정책으로 고정한다. `AI_FIRST-4`는 앞의 결정들을 새 언어 구현 템플릿에 반영한다. + +--- + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `dart/lib/src/transport.dart` | AI_FIRST-1 | +| `dart/lib/src/communicator.dart` | AI_FIRST-1 | +| `dart/lib/src/protobuf_client.dart` | AI_FIRST-1 | +| `dart/lib/src/ws_protobuf_client.dart` | AI_FIRST-1 | +| `dart/lib/toki_socket.dart` | AI_FIRST-1 | +| `dart/test/communicator_test.dart` | AI_FIRST-1 | +| `dart/test/socket_test.dart` | AI_FIRST-1 | +| `tools/check_proto_sync.sh` | AI_FIRST-2 | +| `tools/generate_proto.sh` | AI_FIRST-2 | +| `README.md` | AI_FIRST-2, AI_FIRST-3, AI_FIRST-4 | +| `VERSIONING.md` | AI_FIRST-3 | +| `PROTOCOL.md` | AI_FIRST-3 | +| `templates/language/README.md` | AI_FIRST-4 | +| `templates/language/IMPLEMENTATION_CHECKLIST.md` | AI_FIRST-4 | +| `templates/language/CROSSTEST_CHECKLIST.md` | AI_FIRST-4 | +| `PORTING_GUIDE.md` | AI_FIRST-4 | + +--- + +## 최종 검증 + +```bash +# 1. Proto sync +tools/check_proto_sync.sh + +# 2. Dart 정적 분석과 테스트 +cd dart && dart analyze +cd dart && dart test + +# 3. Go 테스트 +cd go && go test ./... + +# 4. Cross-language tests +cd dart && PATH=/config/go-sdk/go/bin:/config/go/bin:$PATH dart run crosstest/dart_go.dart +cd go && PATH=/config/go-sdk/go/bin:/config/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod go run ./crosstest +``` + +Expected outcome: proto sync 성공, Dart 기존 40개 + 신규 2개 이상 통과, Go 전체 테스트 통과, Dart-server/Go-client 및 Go-server/Dart-client crosstest 통과 diff --git a/templates/language/CROSSTEST_CHECKLIST.md b/templates/language/CROSSTEST_CHECKLIST.md new file mode 100644 index 0000000..1a5b830 --- /dev/null +++ b/templates/language/CROSSTEST_CHECKLIST.md @@ -0,0 +1,44 @@ +# Cross-language Test Checklist + +Use the runner layout from `skills/add-crosstest-language/SKILL.md`. + +## Runner Layout + +- [ ] Server-language orchestrator lives under `/crosstest/`. +- [ ] Client-language subprocess helper lives under `/crosstest/`. +- [ ] No root-level package metadata is added only for crosstests. +- [ ] Subprocess paths resolve from the package root or runner source location. + +## Required Output + +The client helper prints its protocol type name before scenarios: + +```text +INFO typeName =TestData +``` + +Each scenario prints exactly one result line: + +```text +PASS scenario=N detail=... +FAIL scenario=N error=... +``` + +The orchestrator fails when an expected scenario is missing, any `FAIL` line appears, or the subprocess exits non-zero. + +## Required Scenarios + +Run each scenario for TCP and WebSocket: + +- [ ] Scenario 1: client sends fire-and-forget `TestData`; server validates `index` and `message`. +- [ ] Scenario 2: server pushes `TestData(index=200, message=push from ... server)` to the client. +- [ ] Scenario 3: client `sendRequest` receives `index=req.index*2` and `message=echo: req.message`. +- [ ] Scenario 4: multiple concurrent `sendRequest` calls verify nonce and response routing. + +Split protocols into separate send-push and request phases if a communicator forbids normal listener and request listener registration for the same message type. + +## Review Notes + +- [ ] Record the exact commands used to run both directions. +- [ ] Record fixed ports and why they do not conflict with existing tests. +- [ ] Record any unsupported transport or platform caveat. diff --git a/templates/language/IMPLEMENTATION_CHECKLIST.md b/templates/language/IMPLEMENTATION_CHECKLIST.md new file mode 100644 index 0000000..50fc5a7 --- /dev/null +++ b/templates/language/IMPLEMENTATION_CHECKLIST.md @@ -0,0 +1,43 @@ +# Implementation Checklist + +Target protocol version: `0.1` + +## Protocol Core + +- [ ] Generated protobuf bindings from the canonical schema. +- [ ] Parser map registers application messages by protocol-compatible `typeName`. +- [ ] Built-in `HeartBeat` registration is owned by the framework. +- [ ] `PacketBase.typeName`, `nonce`, `data`, and `responseNonce` semantics match `PROTOCOL.md`. +- [ ] Same `typeName` cannot be registered for both normal listener and request listener on one connection. + +## Transport + +- [ ] `Transport` abstraction exposes only packet write and close operations. +- [ ] `Communicator` does not know whether the connection is TCP, TLS+TCP, WS, or WSS. +- [ ] TCP framing writes a 4-byte big-endian length followed by one `PacketBase` protobuf payload. +- [ ] TCP reader rejects zero-length packets as no-op and closes on invalid or oversized lengths. +- [ ] WebSocket reader/writer uses one binary frame per `PacketBase` payload. +- [ ] TLS+TCP and WSS are supported or explicitly marked unsupported with a reason. + +## Lifecycle + +- [ ] Close is idempotent. +- [ ] Pending requests complete with an error when the connection closes. +- [ ] Writes are serialized so packet bytes cannot interleave. +- [ ] Read, write, heartbeat, and close errors converge on the same disconnect path. + +## Features + +- [ ] Fire-and-forget send. +- [ ] Request-response send with timeout or cancellation. +- [ ] Concurrent requests route by `responseNonce`. +- [ ] Heartbeat sends after inactivity and closes after missing response. +- [ ] Server broadcast, if the language package exposes server helpers. + +## Verification + +- [ ] Formatter/linter passes. +- [ ] Same-language TCP tests pass. +- [ ] Same-language WS tests pass. +- [ ] TLS/WSS tests pass where supported. +- [ ] Cross-language tests pass in both server/client directions. diff --git a/templates/language/README.md b/templates/language/README.md new file mode 100644 index 0000000..f66310a --- /dev/null +++ b/templates/language/README.md @@ -0,0 +1,41 @@ +# Toki Socket + +Status: planned + +This package implements Toki Socket protocol version `0.1` for . + +## Required Structure + +Use this package README to document the concrete files for: + +- Transport abstraction: TCP, TLS+TCP, WS, and WSS implementations. +- Communicator core: parser map, listener routing, request-response correlation, and serialized writes. +- Client/server layer: connection lifecycle, close behavior, and heartbeat ownership. +- Generated protobuf bindings from `dart/lib/src/packets/message_common.proto`. +- Unit tests and cross-language tests. + +## Proto Generation + +Document the package-local proto generation command here. The generated schema must match the canonical Dart proto except for language-specific generator options. + +```bash +# +``` + +After generation, run from the repository root: + +```bash +tools/check_proto_sync.sh +``` + +## Validation + +Before marking this implementation available, document and run: + +```bash +# +# +# +``` + +The implementation must pass same-language tests and cross-language tests against at least one available implementation. diff --git a/tools/check_proto_sync.sh b/tools/check_proto_sync.sh new file mode 100755 index 0000000..b9ce646 --- /dev/null +++ b/tools/check_proto_sync.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +dart_proto="$repo_root/dart/lib/src/packets/message_common.proto" +go_proto="$repo_root/go/packets/message_common.proto" + +if [[ ! -f "$dart_proto" ]]; then + echo "Missing canonical Dart proto: $dart_proto" >&2 + exit 1 +fi + +if [[ ! -f "$go_proto" ]]; then + echo "Missing Go proto copy: $go_proto" >&2 + exit 1 +fi + +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT + +normalize_proto() { + awk ' + /^[[:space:]]*option go_package[[:space:]]*=.*;[[:space:]]*$/ { next } + /^[[:space:]]*$/ { next } + { print } + ' "$1" +} + +normalize_proto "$dart_proto" >"$tmp_dir/dart.proto" +normalize_proto "$go_proto" >"$tmp_dir/go.proto" + +if ! cmp -s "$tmp_dir/dart.proto" "$tmp_dir/go.proto"; then + echo "Proto schema mismatch: go/packets/message_common.proto must match the Dart canonical proto except option go_package." >&2 + diff -u "$tmp_dir/dart.proto" "$tmp_dir/go.proto" >&2 + exit 1 +fi + +echo "Proto schemas are in sync." diff --git a/tools/generate_proto.sh b/tools/generate_proto.sh new file mode 100755 index 0000000..3073d42 --- /dev/null +++ b/tools/generate_proto.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +require_binary() { + local binary="$1" + local hint="$2" + if ! command -v "$binary" >/dev/null 2>&1; then + echo "Missing required binary: $binary" >&2 + echo "$hint" >&2 + exit 1 + fi +} + +require_binary protoc "Install Protocol Buffers compiler and ensure protoc is on PATH." +require_binary protoc-gen-dart "Install Dart plugin: dart pub global activate protoc_plugin, then add pub-cache/bin to PATH." +require_binary protoc-gen-go "Install Go plugin: go install google.golang.org/protobuf/cmd/protoc-gen-go@latest, then add GOPATH/bin to PATH." + +( + cd "$repo_root/dart" + protoc --dart_out=lib/src/packets lib/src/packets/message_common.proto +) + +( + cd "$repo_root/go" + protoc --go_out=. --go_opt=paths=source_relative packets/message_common.proto +) + +"$repo_root/tools/check_proto_sync.sh"