From 8daf53003d4eff8b86fbf5eaa73d6c428458c3cf Mon Sep 17 00:00:00 2001 From: toki Date: Wed, 20 May 2026 07:30:53 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B8=B0=EB=8A=A5:=20=EB=B8=8C=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=EC=A0=80=20WebSocket=20=EC=A7=84=EC=9E=85=EC=A0=90?= =?UTF-8?q?=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 브라우저 번들에서 Node 전용 ws 의존성을 분리하고 Go 모듈 경로를 현재 저장소명과 맞추기 위해 진입점과 import 경로를 정리한다. --- README.md | 4 +- .../src/packets/message_common.pbjson.dart | 6 +- examples/go-module-consumer/README.md | 2 +- examples/go-module-consumer/go.mod | 2 +- examples/go-module-consumer/go.sum | 2 - examples/go-module-consumer/main.go | 2 +- go/base_client.go | 2 +- go/communicator.go | 2 +- go/communicator_nonce_test.go | 2 +- go/crosstest/dart_go_client/main.go | 4 +- go/crosstest/go_dart.go | 4 +- go/crosstest/go_kotlin.go | 4 +- go/crosstest/go_python.go | 4 +- go/crosstest/go_typescript.go | 4 +- go/crosstest/kotlin_go_client/main.go | 4 +- go/crosstest/python_go_client/main.go | 4 +- go/crosstest/typescript_go_client/main.go | 4 +- go/examples/tcp_echo/main.go | 4 +- go/examples/ws_echo/main.go | 4 +- go/go.mod | 2 +- go/packets/message_common.pb.go | 2 +- go/packets/message_common.proto | 2 +- go/tcp_client.go | 2 +- go/test/communicator_test.go | 4 +- go/test/heartbeat_test.go | 2 +- go/test/tcp_test.go | 4 +- go/test/test_helpers_test.go | 4 +- go/test/tls_test.go | 4 +- go/test/ws_test.go | 4 +- go/ws_client.go | 2 +- typescript/README.md | 26 +- .../crosstest/dart_typescript_client.ts | 6 +- typescript/crosstest/go_typescript_client.ts | 6 +- .../crosstest/kotlin_typescript_client.ts | 6 +- .../crosstest/python_typescript_client.ts | 6 +- typescript/crosstest/typescript_dart.ts | 16 +- typescript/crosstest/typescript_go.ts | 16 +- typescript/crosstest/typescript_kotlin.ts | 16 +- typescript/crosstest/typescript_python.ts | 16 +- typescript/package-lock.json | 15 +- typescript/package.json | 22 +- typescript/src/browser_ws_client.ts | 131 +++++++++ typescript/src/index.ts | 5 +- typescript/src/node.ts | 7 + .../src/{ws_client.ts => node_ws_client.ts} | 18 +- .../src/{ws_server.ts => node_ws_server.ts} | 12 +- typescript/test/browser_ws_client.test.ts | 260 ++++++++++++++++++ typescript/test/ws.test.ts | 26 +- typescript/tsconfig.json | 1 + 49 files changed, 574 insertions(+), 133 deletions(-) create mode 100644 typescript/src/browser_ws_client.ts create mode 100644 typescript/src/node.ts rename typescript/src/{ws_client.ts => node_ws_client.ts} (88%) rename typescript/src/{ws_server.ts => node_ws_server.ts} (91%) create mode 100644 typescript/test/browser_ws_client.test.ts diff --git a/README.md b/README.md index c7cec48..564d0de 100644 --- a/README.md +++ b/README.md @@ -107,8 +107,8 @@ import ( "google.golang.org/protobuf/proto" - protoSocket "git.toki-labs.com/toki/common-proto-socket/go" - "git.toki-labs.com/toki/common-proto-socket/go/packets" + protoSocket "git.toki-labs.com/toki/proto-socket/go" + "git.toki-labs.com/toki/proto-socket/go/packets" ) func parserMap() protoSocket.ParserMap { diff --git a/dart/lib/src/packets/message_common.pbjson.dart b/dart/lib/src/packets/message_common.pbjson.dart index 61fcc84..6acb17b 100644 --- a/dart/lib/src/packets/message_common.pbjson.dart +++ b/dart/lib/src/packets/message_common.pbjson.dart @@ -26,9 +26,9 @@ const PacketBase$json = { /// Descriptor for `PacketBase`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List packetBaseDescriptor = $convert.base64Decode( - 'CgpQYWNrZXRCYXNlEhoKCHR5cGVOYW1lGAEgASgJUgh0eXBlTmFtZRIUCgVub25jZRgCIAEoBVIF' - 'bm9uY2USEgoEZGF0YRgDIAEoDFIEZGF0YRIkCg1yZXNwb25zZU5vbmNlGAQgASgFUg1yZXNwb25z' - 'ZU5vbmNl'); + 'CgpQYWNrZXRCYXNlEhoKCHR5cGVOYW1lGAEgASgJUgh0eXBlTmFtZRIUCgVub25jZRgCIAEoBV' + 'IFbm9uY2USEgoEZGF0YRgDIAEoDFIEZGF0YRIkCg1yZXNwb25zZU5vbmNlGAQgASgFUg1yZXNw' + 'b25zZU5vbmNl'); @$core.Deprecated('Use heartBeatDescriptor instead') const HeartBeat$json = { diff --git a/examples/go-module-consumer/README.md b/examples/go-module-consumer/README.md index 811f12b..24467f9 100644 --- a/examples/go-module-consumer/README.md +++ b/examples/go-module-consumer/README.md @@ -28,7 +28,7 @@ git config --global url."https://toki@git.toki-labs.com/".insteadOf "https://git ## Install Or Update ```bash -go get git.toki-labs.com/toki/common-proto-socket/go@latest +go get git.toki-labs.com/toki/proto-socket/go@latest go mod tidy ``` diff --git a/examples/go-module-consumer/go.mod b/examples/go-module-consumer/go.mod index eaf51b3..dc7a4dc 100644 --- a/examples/go-module-consumer/go.mod +++ b/examples/go-module-consumer/go.mod @@ -2,7 +2,7 @@ module example.com/proto-socket-go-consumer go 1.22 -require git.toki-labs.com/toki/common-proto-socket/go v0.0.0-20260501220005-284b66a22300 +require git.toki-labs.com/toki/proto-socket/go v0.0.0-20260501220005-284b66a22300 require ( google.golang.org/protobuf v1.36.5 // indirect diff --git a/examples/go-module-consumer/go.sum b/examples/go-module-consumer/go.sum index 0c9fda8..f714725 100644 --- a/examples/go-module-consumer/go.sum +++ b/examples/go-module-consumer/go.sum @@ -1,5 +1,3 @@ -git.toki-labs.com/toki/common-proto-socket/go v0.0.0-20260501220005-284b66a22300 h1:wvESR2WEz1yPqWRGZB2ORUewbI2K71++zOKyfpHcAkc= -git.toki-labs.com/toki/common-proto-socket/go v0.0.0-20260501220005-284b66a22300/go.mod h1:rEQJuwkoWQgOHXU4ijUinhhNzq92f+xzAkHti3CD3pA= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= diff --git a/examples/go-module-consumer/main.go b/examples/go-module-consumer/main.go index 8a41795..a22e633 100644 --- a/examples/go-module-consumer/main.go +++ b/examples/go-module-consumer/main.go @@ -3,7 +3,7 @@ package main import ( "fmt" - protoSocket "git.toki-labs.com/toki/common-proto-socket/go" + protoSocket "git.toki-labs.com/toki/proto-socket/go" ) func main() { diff --git a/go/base_client.go b/go/base_client.go index 024989f..a09fdc9 100644 --- a/go/base_client.go +++ b/go/base_client.go @@ -5,7 +5,7 @@ import ( "sync" "time" - "git.toki-labs.com/toki/common-proto-socket/go/packets" + "git.toki-labs.com/toki/proto-socket/go/packets" ) const ( diff --git a/go/communicator.go b/go/communicator.go index 24011b7..bcec048 100644 --- a/go/communicator.go +++ b/go/communicator.go @@ -10,7 +10,7 @@ import ( "google.golang.org/protobuf/proto" - "git.toki-labs.com/toki/common-proto-socket/go/packets" + "git.toki-labs.com/toki/proto-socket/go/packets" ) var ErrNotConnected = errors.New("not connected") diff --git a/go/communicator_nonce_test.go b/go/communicator_nonce_test.go index 027a394..66a70d3 100644 --- a/go/communicator_nonce_test.go +++ b/go/communicator_nonce_test.go @@ -7,7 +7,7 @@ import ( "google.golang.org/protobuf/proto" - "git.toki-labs.com/toki/common-proto-socket/go/packets" + "git.toki-labs.com/toki/proto-socket/go/packets" ) type nonceTestTransport struct { diff --git a/go/crosstest/dart_go_client/main.go b/go/crosstest/dart_go_client/main.go index 0414eb6..0c1ee53 100644 --- a/go/crosstest/dart_go_client/main.go +++ b/go/crosstest/dart_go_client/main.go @@ -12,8 +12,8 @@ import ( "google.golang.org/protobuf/proto" - toki "git.toki-labs.com/toki/common-proto-socket/go" - "git.toki-labs.com/toki/common-proto-socket/go/packets" + toki "git.toki-labs.com/toki/proto-socket/go" + "git.toki-labs.com/toki/proto-socket/go/packets" ) const ( diff --git a/go/crosstest/go_dart.go b/go/crosstest/go_dart.go index f1ffa00..5950036 100644 --- a/go/crosstest/go_dart.go +++ b/go/crosstest/go_dart.go @@ -21,8 +21,8 @@ import ( "google.golang.org/protobuf/proto" "nhooyr.io/websocket" - toki "git.toki-labs.com/toki/common-proto-socket/go" - "git.toki-labs.com/toki/common-proto-socket/go/packets" + toki "git.toki-labs.com/toki/proto-socket/go" + "git.toki-labs.com/toki/proto-socket/go/packets" ) const ( diff --git a/go/crosstest/go_kotlin.go b/go/crosstest/go_kotlin.go index 48e57cc..6dab323 100644 --- a/go/crosstest/go_kotlin.go +++ b/go/crosstest/go_kotlin.go @@ -21,8 +21,8 @@ import ( "google.golang.org/protobuf/proto" "nhooyr.io/websocket" - toki "git.toki-labs.com/toki/common-proto-socket/go" - "git.toki-labs.com/toki/common-proto-socket/go/packets" + toki "git.toki-labs.com/toki/proto-socket/go" + "git.toki-labs.com/toki/proto-socket/go/packets" ) const ( diff --git a/go/crosstest/go_python.go b/go/crosstest/go_python.go index 3a733ec..42ece1a 100644 --- a/go/crosstest/go_python.go +++ b/go/crosstest/go_python.go @@ -21,8 +21,8 @@ import ( "google.golang.org/protobuf/proto" "nhooyr.io/websocket" - toki "git.toki-labs.com/toki/common-proto-socket/go" - "git.toki-labs.com/toki/common-proto-socket/go/packets" + toki "git.toki-labs.com/toki/proto-socket/go" + "git.toki-labs.com/toki/proto-socket/go/packets" ) const ( diff --git a/go/crosstest/go_typescript.go b/go/crosstest/go_typescript.go index fc3b9b3..9da2c2b 100644 --- a/go/crosstest/go_typescript.go +++ b/go/crosstest/go_typescript.go @@ -21,8 +21,8 @@ import ( "google.golang.org/protobuf/proto" "nhooyr.io/websocket" - toki "git.toki-labs.com/toki/common-proto-socket/go" - "git.toki-labs.com/toki/common-proto-socket/go/packets" + toki "git.toki-labs.com/toki/proto-socket/go" + "git.toki-labs.com/toki/proto-socket/go/packets" ) const ( diff --git a/go/crosstest/kotlin_go_client/main.go b/go/crosstest/kotlin_go_client/main.go index 922710f..75d2e6f 100644 --- a/go/crosstest/kotlin_go_client/main.go +++ b/go/crosstest/kotlin_go_client/main.go @@ -12,8 +12,8 @@ import ( "google.golang.org/protobuf/proto" - toki "git.toki-labs.com/toki/common-proto-socket/go" - "git.toki-labs.com/toki/common-proto-socket/go/packets" + toki "git.toki-labs.com/toki/proto-socket/go" + "git.toki-labs.com/toki/proto-socket/go/packets" ) const ( diff --git a/go/crosstest/python_go_client/main.go b/go/crosstest/python_go_client/main.go index 3a8744a..e6555b5 100644 --- a/go/crosstest/python_go_client/main.go +++ b/go/crosstest/python_go_client/main.go @@ -12,8 +12,8 @@ import ( "google.golang.org/protobuf/proto" - toki "git.toki-labs.com/toki/common-proto-socket/go" - "git.toki-labs.com/toki/common-proto-socket/go/packets" + toki "git.toki-labs.com/toki/proto-socket/go" + "git.toki-labs.com/toki/proto-socket/go/packets" ) const ( diff --git a/go/crosstest/typescript_go_client/main.go b/go/crosstest/typescript_go_client/main.go index 3227734..337e5eb 100644 --- a/go/crosstest/typescript_go_client/main.go +++ b/go/crosstest/typescript_go_client/main.go @@ -12,8 +12,8 @@ import ( "google.golang.org/protobuf/proto" - toki "git.toki-labs.com/toki/common-proto-socket/go" - "git.toki-labs.com/toki/common-proto-socket/go/packets" + toki "git.toki-labs.com/toki/proto-socket/go" + "git.toki-labs.com/toki/proto-socket/go/packets" ) const ( diff --git a/go/examples/tcp_echo/main.go b/go/examples/tcp_echo/main.go index da23917..5e848ae 100644 --- a/go/examples/tcp_echo/main.go +++ b/go/examples/tcp_echo/main.go @@ -7,8 +7,8 @@ import ( "net" "time" - toki "git.toki-labs.com/toki/common-proto-socket/go" - "git.toki-labs.com/toki/common-proto-socket/go/packets" + toki "git.toki-labs.com/toki/proto-socket/go" + "git.toki-labs.com/toki/proto-socket/go/packets" "google.golang.org/protobuf/proto" ) diff --git a/go/examples/ws_echo/main.go b/go/examples/ws_echo/main.go index ee306f9..8545279 100644 --- a/go/examples/ws_echo/main.go +++ b/go/examples/ws_echo/main.go @@ -6,8 +6,8 @@ import ( "log" "time" - toki "git.toki-labs.com/toki/common-proto-socket/go" - "git.toki-labs.com/toki/common-proto-socket/go/packets" + toki "git.toki-labs.com/toki/proto-socket/go" + "git.toki-labs.com/toki/proto-socket/go/packets" "google.golang.org/protobuf/proto" "nhooyr.io/websocket" diff --git a/go/go.mod b/go/go.mod index f2a0cc4..4e4c08d 100644 --- a/go/go.mod +++ b/go/go.mod @@ -1,4 +1,4 @@ -module git.toki-labs.com/toki/common-proto-socket/go +module git.toki-labs.com/toki/proto-socket/go go 1.22 diff --git a/go/packets/message_common.pb.go b/go/packets/message_common.pb.go index 1231e99..95c9a53 100644 --- a/go/packets/message_common.pb.go +++ b/go/packets/message_common.pb.go @@ -191,7 +191,7 @@ const file_packets_message_common_proto_rawDesc = "" + "\tHeartBeat\":\n" + "\bTestData\x12\x14\n" + "\x05index\x18\x01 \x01(\x05R\x05index\x12\x18\n" + - "\amessage\x18\x02 \x01(\tR\amessageB7Z5git.toki-labs.com/toki/common-proto-socket/go/packetsb\x06proto3" + "\amessage\x18\x02 \x01(\tR\amessageB0Z.git.toki-labs.com/toki/proto-socket/go/packetsb\x06proto3" var ( file_packets_message_common_proto_rawDescOnce sync.Once diff --git a/go/packets/message_common.proto b/go/packets/message_common.proto index 3911653..b9e02ca 100644 --- a/go/packets/message_common.proto +++ b/go/packets/message_common.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -option go_package = "git.toki-labs.com/toki/common-proto-socket/go/packets"; +option go_package = "git.toki-labs.com/toki/proto-socket/go/packets"; message PacketBase { string typeName = 1; diff --git a/go/tcp_client.go b/go/tcp_client.go index 9f19a02..026f9a2 100644 --- a/go/tcp_client.go +++ b/go/tcp_client.go @@ -12,7 +12,7 @@ import ( "google.golang.org/protobuf/proto" - "git.toki-labs.com/toki/common-proto-socket/go/packets" + "git.toki-labs.com/toki/proto-socket/go/packets" ) const MaxPacketSize = 64 << 20 diff --git a/go/test/communicator_test.go b/go/test/communicator_test.go index a452cba..35fd705 100644 --- a/go/test/communicator_test.go +++ b/go/test/communicator_test.go @@ -8,8 +8,8 @@ import ( "google.golang.org/protobuf/proto" - toki "git.toki-labs.com/toki/common-proto-socket/go" - "git.toki-labs.com/toki/common-proto-socket/go/packets" + toki "git.toki-labs.com/toki/proto-socket/go" + "git.toki-labs.com/toki/proto-socket/go/packets" ) type fakeTransport struct { diff --git a/go/test/heartbeat_test.go b/go/test/heartbeat_test.go index 2fb5811..edd7e0d 100644 --- a/go/test/heartbeat_test.go +++ b/go/test/heartbeat_test.go @@ -12,7 +12,7 @@ import ( "nhooyr.io/websocket" - toki "git.toki-labs.com/toki/common-proto-socket/go" + toki "git.toki-labs.com/toki/proto-socket/go" ) func TestHeartbeatDisconnectsWithoutResponse(t *testing.T) { diff --git a/go/test/tcp_test.go b/go/test/tcp_test.go index 9ea05b2..076c6ad 100644 --- a/go/test/tcp_test.go +++ b/go/test/tcp_test.go @@ -8,8 +8,8 @@ import ( "testing" "time" - toki "git.toki-labs.com/toki/common-proto-socket/go" - "git.toki-labs.com/toki/common-proto-socket/go/packets" + toki "git.toki-labs.com/toki/proto-socket/go" + "git.toki-labs.com/toki/proto-socket/go/packets" ) func TestTcpRequestResponse(t *testing.T) { diff --git a/go/test/test_helpers_test.go b/go/test/test_helpers_test.go index 2984e91..7c23fca 100644 --- a/go/test/test_helpers_test.go +++ b/go/test/test_helpers_test.go @@ -13,8 +13,8 @@ import ( "testing" "time" - toki "git.toki-labs.com/toki/common-proto-socket/go" - "git.toki-labs.com/toki/common-proto-socket/go/packets" + toki "git.toki-labs.com/toki/proto-socket/go" + "git.toki-labs.com/toki/proto-socket/go/packets" ) func TestTypeNameMatchesDartConvention(t *testing.T) { diff --git a/go/test/tls_test.go b/go/test/tls_test.go index 8b03b0e..b11cb89 100644 --- a/go/test/tls_test.go +++ b/go/test/tls_test.go @@ -8,8 +8,8 @@ import ( "nhooyr.io/websocket" - toki "git.toki-labs.com/toki/common-proto-socket/go" - "git.toki-labs.com/toki/common-proto-socket/go/packets" + toki "git.toki-labs.com/toki/proto-socket/go" + "git.toki-labs.com/toki/proto-socket/go/packets" ) func TestTLSTcp(t *testing.T) { diff --git a/go/test/ws_test.go b/go/test/ws_test.go index d2e56fd..b0625fb 100644 --- a/go/test/ws_test.go +++ b/go/test/ws_test.go @@ -9,8 +9,8 @@ import ( "nhooyr.io/websocket" - toki "git.toki-labs.com/toki/common-proto-socket/go" - "git.toki-labs.com/toki/common-proto-socket/go/packets" + toki "git.toki-labs.com/toki/proto-socket/go" + "git.toki-labs.com/toki/proto-socket/go/packets" ) func TestWsRequestResponse(t *testing.T) { diff --git a/go/ws_client.go b/go/ws_client.go index f2dd48a..e4786cf 100644 --- a/go/ws_client.go +++ b/go/ws_client.go @@ -10,7 +10,7 @@ import ( "google.golang.org/protobuf/proto" "nhooyr.io/websocket" - "git.toki-labs.com/toki/common-proto-socket/go/packets" + "git.toki-labs.com/toki/proto-socket/go/packets" ) const ( diff --git a/typescript/README.md b/typescript/README.md index 04ac065..e323913 100644 --- a/typescript/README.md +++ b/typescript/README.md @@ -6,9 +6,29 @@ This package implements Proto Socket protocol version `0.1` for TypeScript. ## Scope -- Runtime targets: Node.js TCP, Node.js WebSocket, browser-friendly core types. -- Currently in scope: `Communicator`, `BaseClient`, TCP/WS client and server helpers, Go cross-language tests. -- Out of scope for this iteration: TLS+TCP, WSS, browser WebSocket transport wrapper. +- Runtime targets: Node.js TCP, Node.js WebSocket, browser native WebSocket. +- Browser entrypoint (`proto-socket`): `Communicator`, `BaseClient`, `BrowserWsClient`, `connectBrowserWs`. Has no `ws`, `node:*`, or `Buffer` dependency. +- Node entrypoint (`proto-socket/node`): all of the above plus `TcpClient`/`TcpServer`, `NodeWsClient`/`NodeWsServer`, `connectNodeWs`/`connectNodeWss`. + +## Import + +```ts +// browser +import { connectBrowserWs } from "proto-socket"; + +// node +import { connectNodeWs, NodeWsServer, TcpClient } from "proto-socket/node"; +``` + +## Runtime Dependencies + +`ws` is declared as an optional peer dependency. Install it only when using the Node WebSocket entrypoint (`NodeWsClient`/`NodeWsServer`/`connectNodeWs`/`connectNodeWss`): + +```bash +npm install ws +``` + +The browser entrypoint and Node TCP-only consumers do not need `ws`. ## Proto Generation diff --git a/typescript/crosstest/dart_typescript_client.ts b/typescript/crosstest/dart_typescript_client.ts index 240c410..4a19ebe 100644 --- a/typescript/crosstest/dart_typescript_client.ts +++ b/typescript/crosstest/dart_typescript_client.ts @@ -10,7 +10,7 @@ import { } from "../src/communicator.js"; import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js"; import { connectTcp, connectTcpTls } from "../src/tcp_client.js"; -import { connectWs, connectWss } from "../src/ws_client.js"; +import { connectNodeWs, connectNodeWss } from "../src/node_ws_client.js"; const HOST = "127.0.0.1"; const WS_PATH = "/"; @@ -101,7 +101,7 @@ async function dial(mode: Mode, port: number, cert?: string): Promise { diff --git a/typescript/crosstest/go_typescript_client.ts b/typescript/crosstest/go_typescript_client.ts index 9e3e682..b4267ec 100644 --- a/typescript/crosstest/go_typescript_client.ts +++ b/typescript/crosstest/go_typescript_client.ts @@ -10,7 +10,7 @@ import { } from "../src/communicator.js"; import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js"; import { connectTcp, connectTcpTls } from "../src/tcp_client.js"; -import { connectWs, connectWss } from "../src/ws_client.js"; +import { connectNodeWs, connectNodeWss } from "../src/node_ws_client.js"; const HOST = "127.0.0.1"; const WS_PATH = "/"; @@ -101,7 +101,7 @@ async function dial(mode: Mode, port: number, cert?: string): Promise { diff --git a/typescript/crosstest/kotlin_typescript_client.ts b/typescript/crosstest/kotlin_typescript_client.ts index 4bdde89..4b7b743 100644 --- a/typescript/crosstest/kotlin_typescript_client.ts +++ b/typescript/crosstest/kotlin_typescript_client.ts @@ -10,7 +10,7 @@ import { } from "../src/communicator.js"; import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js"; import { connectTcp, connectTcpTls } from "../src/tcp_client.js"; -import { connectWs, connectWss } from "../src/ws_client.js"; +import { connectNodeWs, connectNodeWss } from "../src/node_ws_client.js"; const HOST = "127.0.0.1"; const WS_PATH = "/"; @@ -103,7 +103,7 @@ async function dial(mode: Mode, port: number, cert?: string): Promise { diff --git a/typescript/crosstest/python_typescript_client.ts b/typescript/crosstest/python_typescript_client.ts index 9491867..736f7d4 100644 --- a/typescript/crosstest/python_typescript_client.ts +++ b/typescript/crosstest/python_typescript_client.ts @@ -10,7 +10,7 @@ import { } from "../src/communicator.js"; import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js"; import { connectTcp, connectTcpTls } from "../src/tcp_client.js"; -import { connectWs, connectWss } from "../src/ws_client.js"; +import { connectNodeWs, connectNodeWss } from "../src/node_ws_client.js"; const HOST = "127.0.0.1"; const WS_PATH = "/"; @@ -101,7 +101,7 @@ async function dial(mode: Mode, port: number, cert?: string): Promise { diff --git a/typescript/crosstest/typescript_dart.ts b/typescript/crosstest/typescript_dart.ts index 4ca20ea..2604666 100644 --- a/typescript/crosstest/typescript_dart.ts +++ b/typescript/crosstest/typescript_dart.ts @@ -14,8 +14,8 @@ import { import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js"; import { TcpClient } from "../src/tcp_client.js"; import { TcpServer } from "../src/tcp_server.js"; -import { WsClient } from "../src/ws_client.js"; -import { WsServer } from "../src/ws_server.js"; +import { NodeWsClient } from "../src/node_ws_client.js"; +import { NodeWsServer } from "../src/node_ws_server.js"; const __filename = fileURLToPath(import.meta.url); const repoRoot = path.resolve(path.dirname(__filename), "../.."); @@ -121,7 +121,7 @@ async function runWsSendPush(): Promise { const received = new Promise((resolve) => { resolveReceived = resolve; }); - const server = new WsServer(HOST, WS_PORT, WS_PATH, (ws) => new WsClient(ws, 0, 0, parserMap())); + const server = new NodeWsServer(HOST, WS_PORT, WS_PATH, (ws) => new NodeWsClient(ws, 0, 0, parserMap())); server.onClientConnected = (client) => { addListenerTyped(client.communicator, TestDataSchema, (data) => { console.log(`SERVER_RECEIVED index=${data.index} message=${data.message}`); @@ -150,7 +150,7 @@ async function runWsSendPush(): Promise { } async function runWsRequests(): Promise { - const server = new WsServer(HOST, WS_PORT, WS_PATH, (ws) => new WsClient(ws, 0, 0, parserMap())); + const server = new NodeWsServer(HOST, WS_PORT, WS_PATH, (ws) => new NodeWsClient(ws, 0, 0, parserMap())); server.onClientConnected = (client) => { addRequestListenerTyped(client.communicator, TestDataSchema, (req) => create(TestDataSchema, { @@ -223,11 +223,11 @@ async function runWssSendPush(): Promise { const received = new Promise((resolve) => { resolveReceived = resolve; }); - const server = new WsServer( + const server = new NodeWsServer( HOST, WSS_PORT, WS_PATH, - (ws) => new WsClient(ws, 0, 0, parserMap()), + (ws) => new NodeWsClient(ws, 0, 0, parserMap()), serverTlsOptions(), ); server.onClientConnected = (client) => { @@ -258,11 +258,11 @@ async function runWssSendPush(): Promise { } async function runWssRequests(): Promise { - const server = new WsServer( + const server = new NodeWsServer( HOST, WSS_PORT, WS_PATH, - (ws) => new WsClient(ws, 0, 0, parserMap()), + (ws) => new NodeWsClient(ws, 0, 0, parserMap()), serverTlsOptions(), ); server.onClientConnected = (client) => { diff --git a/typescript/crosstest/typescript_go.ts b/typescript/crosstest/typescript_go.ts index 8bdd5b3..d15d2b4 100644 --- a/typescript/crosstest/typescript_go.ts +++ b/typescript/crosstest/typescript_go.ts @@ -14,8 +14,8 @@ import { import { TestDataSchema } from "../src/packets/message_common_pb.js"; import { TcpClient } from "../src/tcp_client.js"; import { TcpServer } from "../src/tcp_server.js"; -import { WsClient } from "../src/ws_client.js"; -import { WsServer } from "../src/ws_server.js"; +import { NodeWsClient } from "../src/node_ws_client.js"; +import { NodeWsServer } from "../src/node_ws_server.js"; const __filename = fileURLToPath(import.meta.url); const repoRoot = path.resolve(path.dirname(__filename), "../.."); @@ -121,7 +121,7 @@ async function runWsSendPush(): Promise { const received = new Promise((resolve) => { resolveReceived = resolve; }); - const server = new WsServer(HOST, WS_PORT, WS_PATH, (ws) => new WsClient(ws, 0, 0, parserMap())); + const server = new NodeWsServer(HOST, WS_PORT, WS_PATH, (ws) => new NodeWsClient(ws, 0, 0, parserMap())); server.onClientConnected = (client) => { addListenerTyped(client.communicator, TestDataSchema, (data) => { console.log(`SERVER_RECEIVED index=${data.index} message=${data.message}`); @@ -150,7 +150,7 @@ async function runWsSendPush(): Promise { } async function runWsRequests(): Promise { - const server = new WsServer(HOST, WS_PORT, WS_PATH, (ws) => new WsClient(ws, 0, 0, parserMap())); + const server = new NodeWsServer(HOST, WS_PORT, WS_PATH, (ws) => new NodeWsClient(ws, 0, 0, parserMap())); server.onClientConnected = (client) => { addRequestListenerTyped(client.communicator, TestDataSchema, (req) => create(TestDataSchema, { @@ -223,11 +223,11 @@ async function runWssSendPush(): Promise { const received = new Promise((resolve) => { resolveReceived = resolve; }); - const server = new WsServer( + const server = new NodeWsServer( HOST, WSS_PORT, WS_PATH, - (ws) => new WsClient(ws, 0, 0, parserMap()), + (ws) => new NodeWsClient(ws, 0, 0, parserMap()), serverTlsOptions(), ); server.onClientConnected = (client) => { @@ -258,11 +258,11 @@ async function runWssSendPush(): Promise { } async function runWssRequests(): Promise { - const server = new WsServer( + const server = new NodeWsServer( HOST, WSS_PORT, WS_PATH, - (ws) => new WsClient(ws, 0, 0, parserMap()), + (ws) => new NodeWsClient(ws, 0, 0, parserMap()), serverTlsOptions(), ); server.onClientConnected = (client) => { diff --git a/typescript/crosstest/typescript_kotlin.ts b/typescript/crosstest/typescript_kotlin.ts index ad8cd3d..5b649de 100644 --- a/typescript/crosstest/typescript_kotlin.ts +++ b/typescript/crosstest/typescript_kotlin.ts @@ -14,8 +14,8 @@ import { import { TestDataSchema } from "../src/packets/message_common_pb.js"; import { TcpClient } from "../src/tcp_client.js"; import { TcpServer } from "../src/tcp_server.js"; -import { WsClient } from "../src/ws_client.js"; -import { WsServer } from "../src/ws_server.js"; +import { NodeWsClient } from "../src/node_ws_client.js"; +import { NodeWsServer } from "../src/node_ws_server.js"; const __filename = fileURLToPath(import.meta.url); const repoRoot = path.resolve(path.dirname(__filename), "../.."); @@ -125,7 +125,7 @@ async function runWsSendPush(): Promise { const received = new Promise((resolve) => { resolveReceived = resolve; }); - const server = new WsServer(HOST, WS_PORT, WS_PATH, (ws) => new WsClient(ws, 0, 0, parserMap())); + const server = new NodeWsServer(HOST, WS_PORT, WS_PATH, (ws) => new NodeWsClient(ws, 0, 0, parserMap())); server.onClientConnected = (client) => { addListenerTyped(client.communicator, TestDataSchema, (data) => { console.log(`SERVER_RECEIVED index=${data.index} message=${data.message}`); @@ -154,7 +154,7 @@ async function runWsSendPush(): Promise { } async function runWsRequests(): Promise { - const server = new WsServer(HOST, WS_PORT, WS_PATH, (ws) => new WsClient(ws, 0, 0, parserMap())); + const server = new NodeWsServer(HOST, WS_PORT, WS_PATH, (ws) => new NodeWsClient(ws, 0, 0, parserMap())); server.onClientConnected = (client) => { addRequestListenerTyped(client.communicator, TestDataSchema, (req) => create(TestDataSchema, { @@ -227,11 +227,11 @@ async function runWssSendPush(): Promise { const received = new Promise((resolve) => { resolveReceived = resolve; }); - const server = new WsServer( + const server = new NodeWsServer( HOST, WSS_PORT, WS_PATH, - (ws) => new WsClient(ws, 0, 0, parserMap()), + (ws) => new NodeWsClient(ws, 0, 0, parserMap()), serverTlsOptions(), ); server.onClientConnected = (client) => { @@ -262,11 +262,11 @@ async function runWssSendPush(): Promise { } async function runWssRequests(): Promise { - const server = new WsServer( + const server = new NodeWsServer( HOST, WSS_PORT, WS_PATH, - (ws) => new WsClient(ws, 0, 0, parserMap()), + (ws) => new NodeWsClient(ws, 0, 0, parserMap()), serverTlsOptions(), ); server.onClientConnected = (client) => { diff --git a/typescript/crosstest/typescript_python.ts b/typescript/crosstest/typescript_python.ts index fc8bae5..d2af1a9 100644 --- a/typescript/crosstest/typescript_python.ts +++ b/typescript/crosstest/typescript_python.ts @@ -14,8 +14,8 @@ import { import { TestDataSchema } from "../src/packets/message_common_pb.js"; import { TcpClient } from "../src/tcp_client.js"; import { TcpServer } from "../src/tcp_server.js"; -import { WsClient } from "../src/ws_client.js"; -import { WsServer } from "../src/ws_server.js"; +import { NodeWsClient } from "../src/node_ws_client.js"; +import { NodeWsServer } from "../src/node_ws_server.js"; const __filename = fileURLToPath(import.meta.url); const repoRoot = path.resolve(path.dirname(__filename), "../.."); @@ -121,7 +121,7 @@ async function runWsSendPush(): Promise { const received = new Promise((resolve) => { resolveReceived = resolve; }); - const server = new WsServer(HOST, WS_PORT, WS_PATH, (ws) => new WsClient(ws, 0, 0, parserMap())); + const server = new NodeWsServer(HOST, WS_PORT, WS_PATH, (ws) => new NodeWsClient(ws, 0, 0, parserMap())); server.onClientConnected = (client) => { addListenerTyped(client.communicator, TestDataSchema, (data) => { console.log(`SERVER_RECEIVED index=${data.index} message=${data.message}`); @@ -150,7 +150,7 @@ async function runWsSendPush(): Promise { } async function runWsRequests(): Promise { - const server = new WsServer(HOST, WS_PORT, WS_PATH, (ws) => new WsClient(ws, 0, 0, parserMap())); + const server = new NodeWsServer(HOST, WS_PORT, WS_PATH, (ws) => new NodeWsClient(ws, 0, 0, parserMap())); server.onClientConnected = (client) => { addRequestListenerTyped(client.communicator, TestDataSchema, (req) => create(TestDataSchema, { @@ -223,11 +223,11 @@ async function runWssSendPush(): Promise { const received = new Promise((resolve) => { resolveReceived = resolve; }); - const server = new WsServer( + const server = new NodeWsServer( HOST, WSS_PORT, WS_PATH, - (ws) => new WsClient(ws, 0, 0, parserMap()), + (ws) => new NodeWsClient(ws, 0, 0, parserMap()), serverTlsOptions(), ); server.onClientConnected = (client) => { @@ -258,11 +258,11 @@ async function runWssSendPush(): Promise { } async function runWssRequests(): Promise { - const server = new WsServer( + const server = new NodeWsServer( HOST, WSS_PORT, WS_PATH, - (ws) => new WsClient(ws, 0, 0, parserMap()), + (ws) => new NodeWsClient(ws, 0, 0, parserMap()), serverTlsOptions(), ); server.onClientConnected = (client) => { diff --git a/typescript/package-lock.json b/typescript/package-lock.json index 9ac9bb0..d2c2103 100644 --- a/typescript/package-lock.json +++ b/typescript/package-lock.json @@ -8,8 +8,7 @@ "name": "proto-socket", "version": "1.0.5", "dependencies": { - "@bufbuild/protobuf": "^2.2.5", - "ws": "^8.18.1" + "@bufbuild/protobuf": "^2.2.5" }, "devDependencies": { "@bufbuild/protoc-gen-es": "^2.2.5", @@ -17,7 +16,16 @@ "@types/ws": "^8.18.1", "tsx": "^4.19.3", "typescript": "^5.8.3", - "vitest": "^3.1.3" + "vitest": "^3.1.3", + "ws": "^8.18.1" + }, + "peerDependencies": { + "ws": "^8.18.1" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + } } }, "node_modules/@bufbuild/protobuf": { @@ -1748,6 +1756,7 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/typescript/package.json b/typescript/package.json index 3591888..5c1dfbb 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -3,21 +3,39 @@ "version": "1.0.5", "private": true, "type": "module", + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "default": "./dist/src/index.js" + }, + "./node": { + "types": "./dist/src/node.d.ts", + "default": "./dist/src/node.js" + } + }, "scripts": { "build": "tsc -p tsconfig.json", "test": "vitest run", "check": "tsc --noEmit" }, "dependencies": { - "@bufbuild/protobuf": "^2.2.5", + "@bufbuild/protobuf": "^2.2.5" + }, + "peerDependencies": { "ws": "^8.18.1" }, + "peerDependenciesMeta": { + "ws": { + "optional": true + } + }, "devDependencies": { "@bufbuild/protoc-gen-es": "^2.2.5", "@types/node": "^22.15.21", "@types/ws": "^8.18.1", "tsx": "^4.19.3", "typescript": "^5.8.3", - "vitest": "^3.1.3" + "vitest": "^3.1.3", + "ws": "^8.18.1" } } diff --git a/typescript/src/browser_ws_client.ts b/typescript/src/browser_ws_client.ts new file mode 100644 index 0000000..0e247af --- /dev/null +++ b/typescript/src/browser_ws_client.ts @@ -0,0 +1,131 @@ +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; + private readonly handleMessage: (event: MessageEvent) => void; + private readonly handleClose: () => void; + private readonly handleError: () => void; + + 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.handleMessage = (event: MessageEvent) => { + void this.onMessage(event.data); + }; + this.handleClose = () => { + void this.onDisconnected(); + }; + this.handleError = () => { + void this.onDisconnected(); + }; + + this.ws.addEventListener("message", this.handleMessage); + this.ws.addEventListener("close", this.handleClose); + this.ws.addEventListener("error", this.handleError); + } + + async writePacket(base: PacketBase): Promise { + const data = toBinary(PacketBaseSchema, base); + try { + this.ws.send(data); + } catch (err) { + throw err instanceof Error ? err : new Error(String(err)); + } + } + + private async onMessage(data: unknown): Promise { + let bytes: Uint8Array; + try { + bytes = await toUint8Array(data); + } catch { + void this.onDisconnected(); + return; + } + try { + const base = fromBinary(PacketBaseSchema, bytes); + this.communicator.onReceivedData(base.typeName, base.data, base.nonce, base.responseNonce); + void this.sendHeartbeat(); + } catch { + void this.onDisconnected(); + } + } +} + +export async function connectBrowserWs( + url: string, + intervalSec: number, + waitSec: number, + parserMap: ParserMap, + options?: BrowserWsConnectOptions, +): Promise { + const Ctor = options?.WebSocketCtor ?? globalThis.WebSocket; + if (Ctor === undefined) { + throw new Error("global WebSocket is not available; provide WebSocketCtor option"); + } + return new Promise((resolve, reject) => { + const ws = new Ctor(url); + ws.binaryType = "arraybuffer"; + const onOpen = () => { + cleanup(); + resolve(new BrowserWsClient(ws, intervalSec, waitSec, parserMap)); + }; + const onError = () => { + cleanup(); + reject(new Error(`failed to connect ${url}`)); + }; + const cleanup = () => { + ws.removeEventListener("open", onOpen); + ws.removeEventListener("error", onError); + }; + ws.addEventListener("open", onOpen); + ws.addEventListener("error", onError); + }); +} + +async function toUint8Array(data: unknown): Promise { + if (data instanceof ArrayBuffer) { + return new Uint8Array(data); + } + if (data instanceof Uint8Array) { + return data; + } + if (typeof Blob !== "undefined" && data instanceof Blob) { + const buf = await data.arrayBuffer(); + return new Uint8Array(buf); + } + throw new Error("unsupported WebSocket frame: text or unknown payload"); +} + +async function closeBrowserWebSocket(ws: WebSocket): Promise { + if (ws.readyState === ws.CLOSED) { + return; + } + await new Promise((resolve) => { + const onDone = () => { + ws.removeEventListener("close", onDone); + ws.removeEventListener("error", onDone); + resolve(); + }; + ws.addEventListener("close", onDone); + ws.addEventListener("error", onDone); + if (ws.readyState === ws.CLOSING) { + return; + } + try { + ws.close(1000, ""); + } catch { + onDone(); + } + }); +} diff --git a/typescript/src/index.ts b/typescript/src/index.ts index 93ae180..9dafde3 100644 --- a/typescript/src/index.ts +++ b/typescript/src/index.ts @@ -1,7 +1,4 @@ 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 "./ws_client.js"; -export * from "./ws_server.js"; +export * from "./browser_ws_client.js"; diff --git a/typescript/src/node.ts b/typescript/src/node.ts new file mode 100644 index 0000000..07fbf81 --- /dev/null +++ b/typescript/src/node.ts @@ -0,0 +1,7 @@ +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"; diff --git a/typescript/src/ws_client.ts b/typescript/src/node_ws_client.ts similarity index 88% rename from typescript/src/ws_client.ts rename to typescript/src/node_ws_client.ts index cd7e3b2..49e733d 100644 --- a/typescript/src/ws_client.ts +++ b/typescript/src/node_ws_client.ts @@ -6,7 +6,7 @@ import { BaseClient } from "./base_client.js"; import { type ParserMap } from "./communicator.js"; import { PacketBaseSchema, type PacketBase } from "./packets/message_common_pb.js"; -export class WsClient extends BaseClient { +export class NodeWsClient extends BaseClient { private readonly ws: WebSocket; constructor(ws: WebSocket, intervalSec: number, waitSec: number, parserMap: ParserMap) { @@ -49,19 +49,19 @@ export class WsClient extends BaseClient { } } -export async function connectWs( +export async function connectNodeWs( host: string, port: number, path: string, intervalSec: number, waitSec: number, parserMap: ParserMap, -): Promise { - return new Promise((resolve, reject) => { +): Promise { + return new Promise((resolve, reject) => { const ws = new WebSocket(`ws://${host}:${port}${path}`); const onOpen = () => { cleanup(); - resolve(new WsClient(ws, intervalSec, waitSec, parserMap)); + resolve(new NodeWsClient(ws, intervalSec, waitSec, parserMap)); }; const onError = (err: Error) => { cleanup(); @@ -77,7 +77,7 @@ export async function connectWs( }); } -export async function connectWss( +export async function connectNodeWss( host: string, port: number, path: string, @@ -85,12 +85,12 @@ export async function connectWss( intervalSec: number, waitSec: number, parserMap: ParserMap, -): Promise { - return new Promise((resolve, reject) => { +): Promise { + return new Promise((resolve, reject) => { const ws = new WebSocket(`wss://${host}:${port}${path}`, wsOptions); const onOpen = () => { cleanup(); - resolve(new WsClient(ws, intervalSec, waitSec, parserMap)); + resolve(new NodeWsClient(ws, intervalSec, waitSec, parserMap)); }; const onError = (err: Error) => { cleanup(); diff --git a/typescript/src/ws_server.ts b/typescript/src/node_ws_server.ts similarity index 91% rename from typescript/src/ws_server.ts rename to typescript/src/node_ws_server.ts index e8ea125..312b107 100644 --- a/typescript/src/ws_server.ts +++ b/typescript/src/node_ws_server.ts @@ -3,20 +3,20 @@ import * as https from "node:https"; import { type Message } from "@bufbuild/protobuf"; import WebSocket, { WebSocketServer } from "ws"; -import { WsClient } from "./ws_client.js"; +import { NodeWsClient } from "./node_ws_client.js"; -export class WsServer { +export class NodeWsServer { private wss: WebSocketServer | null = null; private httpsServer: https.Server | null = null; - private readonly clients = new Set(); + private readonly clients = new Set(); - onClientConnected: (client: WsClient) => void = () => {}; + onClientConnected: (client: NodeWsClient) => void = () => {}; constructor( private readonly host: string, private readonly listenPort: number, private readonly path: string, - private readonly newClient: (ws: WebSocket) => WsClient, + private readonly newClient: (ws: WebSocket) => NodeWsClient, private readonly tlsOptions?: https.ServerOptions, ) {} @@ -113,7 +113,7 @@ export class WsServer { await Promise.all([...this.clients].map(async (client) => client.communicator.send(msg))); } - private removeClient(client: WsClient): void { + private removeClient(client: NodeWsClient): void { this.clients.delete(client); } diff --git a/typescript/test/browser_ws_client.test.ts b/typescript/test/browser_ws_client.test.ts new file mode 100644 index 0000000..cceb7dc --- /dev/null +++ b/typescript/test/browser_ws_client.test.ts @@ -0,0 +1,260 @@ +import { create, fromBinary, toBinary } from "@bufbuild/protobuf"; +import { afterEach, describe, expect, test } from "vitest"; + +import { + addListenerTyped, + addRequestListenerTyped, + parserFromSchema, + sendRequestTyped, +} from "../src/communicator.js"; +import { + PacketBaseSchema, + TestDataSchema, + type TestData, +} from "../src/packets/message_common_pb.js"; +import { BrowserWsClient, connectBrowserWs } from "../src/browser_ws_client.js"; + +type Listener = (event: { data?: unknown; code?: number; reason?: string }) => void; + +class FakeWebSocket { + static instances: FakeWebSocket[] = []; + + static readonly CONNECTING = 0 as const; + static readonly OPEN = 1 as const; + static readonly CLOSING = 2 as const; + static readonly CLOSED = 3 as const; + + readonly CONNECTING = FakeWebSocket.CONNECTING; + readonly OPEN = FakeWebSocket.OPEN; + readonly CLOSING = FakeWebSocket.CLOSING; + readonly CLOSED = FakeWebSocket.CLOSED; + + url: string; + binaryType = "blob"; + readyState: number = FakeWebSocket.CONNECTING; + sentFrames: Uint8Array[] = []; + sendShouldThrow: Error | null = null; + closedWith: { code: number; reason: string } | null = null; + + private readonly listeners = new Map>(); + + constructor(url: string) { + this.url = url; + FakeWebSocket.instances.push(this); + } + + addEventListener(type: string, fn: Listener): void { + if (!this.listeners.has(type)) { + this.listeners.set(type, new Set()); + } + this.listeners.get(type)!.add(fn); + } + removeEventListener(type: string, fn: Listener): void { + this.listeners.get(type)?.delete(fn); + } + + send(data: ArrayBuffer | Uint8Array): void { + if (this.sendShouldThrow !== null) { + throw this.sendShouldThrow; + } + const buf = + data instanceof Uint8Array ? new Uint8Array(data) : new Uint8Array(data as ArrayBuffer); + this.sentFrames.push(buf); + } + + close(code = 1000, reason = ""): void { + if (this.readyState === FakeWebSocket.CLOSED) { + return; + } + this.closedWith = { code, reason }; + this.readyState = FakeWebSocket.CLOSED; + this.dispatch("close", { code, reason }); + } + + fireOpen(): void { + this.readyState = FakeWebSocket.OPEN; + this.dispatch("open", {}); + } + fireMessage(data: unknown): void { + this.dispatch("message", { data }); + } + fireError(): void { + this.dispatch("error", {}); + } + + private dispatch(type: string, event: { data?: unknown; code?: number; reason?: string }): void { + const set = this.listeners.get(type); + if (set === undefined) { + return; + } + for (const fn of [...set]) { + fn(event); + } + } +} + +function parserMap() { + return new Map([[TestDataSchema.typeName, parserFromSchema(TestDataSchema)]]); +} + +function decodeFrame(buf: Uint8Array): { typeName: string; nonce: number; responseNonce: number; payload: Uint8Array } { + const base = fromBinary(PacketBaseSchema, buf); + return { + typeName: base.typeName, + nonce: base.nonce, + responseNonce: base.responseNonce, + payload: base.data, + }; +} + +function encodeTestData(msg: TestData, nonce: number, responseNonce = 0): Uint8Array { + return toBinary( + PacketBaseSchema, + create(PacketBaseSchema, { + typeName: TestDataSchema.typeName, + nonce, + responseNonce, + data: toBinary(TestDataSchema, msg), + }), + ); +} + +describe("BrowserWsClient", () => { + const cleanup: Array<() => Promise> = []; + + afterEach(async () => { + FakeWebSocket.instances.length = 0; + while (cleanup.length > 0) { + const fn = cleanup.pop(); + if (fn) { + await fn(); + } + } + }); + + test("connectBrowserWs resolves after open and sends binary frames", async () => { + const pending = connectBrowserWs("ws://test/", 0, 0, parserMap(), { + WebSocketCtor: FakeWebSocket as unknown as typeof WebSocket, + }); + expect(FakeWebSocket.instances).toHaveLength(1); + const ws = FakeWebSocket.instances[0]!; + expect(ws.binaryType).toBe("arraybuffer"); + ws.fireOpen(); + const client = await pending; + cleanup.push(async () => client.close()); + expect(client).toBeInstanceOf(BrowserWsClient); + + await client.communicator.send(create(TestDataSchema, { index: 11, message: "hi" })); + expect(ws.sentFrames).toHaveLength(1); + const decoded = decodeFrame(ws.sentFrames[0]!); + expect(decoded.typeName).toBe(TestDataSchema.typeName); + }); + + test("routes ArrayBuffer message to communicator listener", async () => { + const pending = connectBrowserWs("ws://test/", 0, 0, parserMap(), { + WebSocketCtor: FakeWebSocket as unknown as typeof WebSocket, + }); + const ws = FakeWebSocket.instances[0]!; + ws.fireOpen(); + const client = await pending; + cleanup.push(async () => client.close()); + + const received = new Promise((resolve) => { + addListenerTyped(client.communicator, TestDataSchema, resolve); + }); + + const frame = encodeTestData(create(TestDataSchema, { index: 7, message: "push" }), 1); + ws.fireMessage(frame.buffer.slice(frame.byteOffset, frame.byteOffset + frame.byteLength)); + + await expect(received).resolves.toMatchObject({ index: 7, message: "push" }); + }); + + test("routes Blob message", async () => { + const pending = connectBrowserWs("ws://test/", 0, 0, parserMap(), { + WebSocketCtor: FakeWebSocket as unknown as typeof WebSocket, + }); + const ws = FakeWebSocket.instances[0]!; + ws.fireOpen(); + const client = await pending; + cleanup.push(async () => client.close()); + + const received = new Promise((resolve) => { + addListenerTyped(client.communicator, TestDataSchema, resolve); + }); + + const frame = encodeTestData(create(TestDataSchema, { index: 8, message: "blob" }), 1); + ws.fireMessage(new Blob([new Uint8Array(frame).slice().buffer])); + + await expect(received).resolves.toMatchObject({ index: 8, message: "blob" }); + }); + + test("sendRequest resolves on response frame", async () => { + const pending = connectBrowserWs("ws://test/", 0, 0, parserMap(), { + WebSocketCtor: FakeWebSocket as unknown as typeof WebSocket, + }); + const ws = FakeWebSocket.instances[0]!; + ws.fireOpen(); + const client = await pending; + cleanup.push(async () => client.close()); + + const promise = sendRequestTyped( + client.communicator, + create(TestDataSchema, { index: 21, message: "req" }), + TestDataSchema, + 500, + ); + + // wait a microtask for queued send + await new Promise((r) => setTimeout(r, 0)); + expect(ws.sentFrames).toHaveLength(1); + const sent = decodeFrame(ws.sentFrames[0]!); + const responseFrame = encodeTestData( + create(TestDataSchema, { index: 42, message: "echo: req" }), + 99, + sent.nonce, + ); + ws.fireMessage( + responseFrame.buffer.slice(responseFrame.byteOffset, responseFrame.byteOffset + responseFrame.byteLength), + ); + + await expect(promise).resolves.toMatchObject({ index: 42, message: "echo: req" }); + }); + + test("disconnects on string frame", async () => { + const pending = connectBrowserWs("ws://test/", 0, 0, parserMap(), { + WebSocketCtor: FakeWebSocket as unknown as typeof WebSocket, + }); + const ws = FakeWebSocket.instances[0]!; + ws.fireOpen(); + const client = await pending; + cleanup.push(async () => client.close()); + + expect(client.communicator.isAlive()).toBe(true); + ws.fireMessage("not binary"); + await new Promise((r) => setTimeout(r, 0)); + expect(client.communicator.isAlive()).toBe(false); + }); + + test("server registers request handlers via communicator", async () => { + const pending = connectBrowserWs("ws://test/", 0, 0, parserMap(), { + WebSocketCtor: FakeWebSocket as unknown as typeof WebSocket, + }); + const ws = FakeWebSocket.instances[0]!; + ws.fireOpen(); + const client = await pending; + cleanup.push(async () => client.close()); + + addRequestListenerTyped(client.communicator, TestDataSchema, (req) => + create(TestDataSchema, { index: req.index * 2, message: `echo: ${req.message}` }), + ); + + const reqFrame = encodeTestData(create(TestDataSchema, { index: 3, message: "ping" }), 77); + ws.fireMessage(reqFrame.buffer.slice(reqFrame.byteOffset, reqFrame.byteOffset + reqFrame.byteLength)); + + await new Promise((r) => setTimeout(r, 5)); + expect(ws.sentFrames.length).toBeGreaterThanOrEqual(1); + const responses = ws.sentFrames.map((f) => decodeFrame(f)); + const match = responses.find((r) => r.responseNonce === 77); + expect(match).toBeDefined(); + }); +}); diff --git a/typescript/test/ws.test.ts b/typescript/test/ws.test.ts index d0bc01a..acac6b6 100644 --- a/typescript/test/ws.test.ts +++ b/typescript/test/ws.test.ts @@ -12,8 +12,8 @@ import { sendRequestTyped, } from "../src/communicator.js"; import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js"; -import { connectWs, connectWss, WsClient } from "../src/ws_client.js"; -import { WsServer } from "../src/ws_server.js"; +import { connectNodeWs, connectNodeWss, NodeWsClient } from "../src/node_ws_client.js"; +import { NodeWsServer } from "../src/node_ws_server.js"; const certsDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "certs"); const cert = fs.readFileSync(path.join(certsDir, "server.crt")); @@ -23,7 +23,7 @@ function parserMap() { return new Map([[TestDataSchema.typeName, parserFromSchema(TestDataSchema)]]); } -async function waitForClientMessage(client: WsClient): Promise { +async function waitForClientMessage(client: NodeWsClient): Promise { return new Promise((resolve) => { addListenerTyped(client.communicator, TestDataSchema, (msg) => resolve(msg)); }); @@ -42,7 +42,7 @@ describe("WS", () => { }); test("connectWs sends and receives TestData", async () => { - const server = new WsServer("127.0.0.1", 0, "/", (ws) => new WsClient(ws, 0, 0, parserMap())); + const server = new NodeWsServer("127.0.0.1", 0, "/", (ws) => new NodeWsClient(ws, 0, 0, parserMap())); cleanup.push(async () => server.stop()); await server.start(); @@ -56,7 +56,7 @@ describe("WS", () => { }); }; - const client = await connectWs("127.0.0.1", server.port, "/", 0, 0, parserMap()); + const client = await connectNodeWs("127.0.0.1", server.port, "/", 0, 0, parserMap()); cleanup.push(async () => client.close()); const pushed = waitForClientMessage(client); @@ -69,7 +69,7 @@ describe("WS", () => { }); test("sendRequest/response roundtrip over WS", async () => { - const server = new WsServer("127.0.0.1", 0, "/", (ws) => new WsClient(ws, 0, 0, parserMap())); + const server = new NodeWsServer("127.0.0.1", 0, "/", (ws) => new NodeWsClient(ws, 0, 0, parserMap())); cleanup.push(async () => server.stop()); await server.start(); @@ -82,7 +82,7 @@ describe("WS", () => { ); }; - const client = await connectWs("127.0.0.1", server.port, "/", 0, 0, parserMap()); + const client = await connectNodeWs("127.0.0.1", server.port, "/", 0, 0, parserMap()); cleanup.push(async () => client.close()); await expect( @@ -99,7 +99,7 @@ describe("WS", () => { }); test("connectWss sends and receives TestData", async () => { - const server = new WsServer("127.0.0.1", 0, "/", (ws) => new WsClient(ws, 0, 0, parserMap()), { + const server = new NodeWsServer("127.0.0.1", 0, "/", (ws) => new NodeWsClient(ws, 0, 0, parserMap()), { cert, key, }); @@ -116,7 +116,7 @@ describe("WS", () => { }); }; - const client = await connectWss( + const client = await connectNodeWss( "127.0.0.1", server.port, "/", @@ -137,7 +137,7 @@ describe("WS", () => { }); test("WSS sendRequest/response roundtrip", async () => { - const server = new WsServer("127.0.0.1", 0, "/", (ws) => new WsClient(ws, 0, 0, parserMap()), { + const server = new NodeWsServer("127.0.0.1", 0, "/", (ws) => new NodeWsClient(ws, 0, 0, parserMap()), { cert, key, }); @@ -153,7 +153,7 @@ describe("WS", () => { ); }; - const client = await connectWss( + const client = await connectNodeWss( "127.0.0.1", server.port, "/", @@ -178,7 +178,7 @@ describe("WS", () => { }); test("concurrent sendRequest/response roundtrip over WS", async () => { - const server = new WsServer("127.0.0.1", 0, "/", (ws) => new WsClient(ws, 0, 0, parserMap())); + const server = new NodeWsServer("127.0.0.1", 0, "/", (ws) => new NodeWsClient(ws, 0, 0, parserMap())); cleanup.push(async () => server.stop()); await server.start(); @@ -191,7 +191,7 @@ describe("WS", () => { ); }; - const client = await connectWs("127.0.0.1", server.port, "/", 0, 0, parserMap()); + const client = await connectNodeWs("127.0.0.1", server.port, "/", 0, 0, parserMap()); cleanup.push(async () => client.close()); const results = await Promise.all( diff --git a/typescript/tsconfig.json b/typescript/tsconfig.json index edf4487..7f8a835 100644 --- a/typescript/tsconfig.json +++ b/typescript/tsconfig.json @@ -10,6 +10,7 @@ "skipLibCheck": true, "rootDir": ".", "outDir": "dist", + "lib": ["ES2022", "DOM"], "types": ["node"] }, "include": ["src/**/*.ts", "test/**/*.ts", "crosstest/**/*.ts"]