기능: 브라우저 WebSocket 진입점을 추가한다

브라우저 번들에서 Node 전용 ws 의존성을 분리하고 Go 모듈 경로를 현재 저장소명과 맞추기 위해 진입점과 import 경로를 정리한다.
This commit is contained in:
toki 2026-05-20 07:30:53 +09:00
parent 9872ebac87
commit 8daf53003d
49 changed files with 574 additions and 133 deletions

View file

@ -107,8 +107,8 @@ import (
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
protoSocket "git.toki-labs.com/toki/common-proto-socket/go" protoSocket "git.toki-labs.com/toki/proto-socket/go"
"git.toki-labs.com/toki/common-proto-socket/go/packets" "git.toki-labs.com/toki/proto-socket/go/packets"
) )
func parserMap() protoSocket.ParserMap { func parserMap() protoSocket.ParserMap {

View file

@ -26,9 +26,9 @@ const PacketBase$json = {
/// Descriptor for `PacketBase`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `PacketBase`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List packetBaseDescriptor = $convert.base64Decode( final $typed_data.Uint8List packetBaseDescriptor = $convert.base64Decode(
'CgpQYWNrZXRCYXNlEhoKCHR5cGVOYW1lGAEgASgJUgh0eXBlTmFtZRIUCgVub25jZRgCIAEoBVIF' 'CgpQYWNrZXRCYXNlEhoKCHR5cGVOYW1lGAEgASgJUgh0eXBlTmFtZRIUCgVub25jZRgCIAEoBV'
'bm9uY2USEgoEZGF0YRgDIAEoDFIEZGF0YRIkCg1yZXNwb25zZU5vbmNlGAQgASgFUg1yZXNwb25z' 'IFbm9uY2USEgoEZGF0YRgDIAEoDFIEZGF0YRIkCg1yZXNwb25zZU5vbmNlGAQgASgFUg1yZXNw'
'ZU5vbmNl'); 'b25zZU5vbmNl');
@$core.Deprecated('Use heartBeatDescriptor instead') @$core.Deprecated('Use heartBeatDescriptor instead')
const HeartBeat$json = { const HeartBeat$json = {

View file

@ -28,7 +28,7 @@ git config --global url."https://toki@git.toki-labs.com/".insteadOf "https://git
## Install Or Update ## Install Or Update
```bash ```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 go mod tidy
``` ```

View file

@ -2,7 +2,7 @@ module example.com/proto-socket-go-consumer
go 1.22 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 ( require (
google.golang.org/protobuf v1.36.5 // indirect google.golang.org/protobuf v1.36.5 // indirect

View file

@ -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 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=

View file

@ -3,7 +3,7 @@ package main
import ( import (
"fmt" "fmt"
protoSocket "git.toki-labs.com/toki/common-proto-socket/go" protoSocket "git.toki-labs.com/toki/proto-socket/go"
) )
func main() { func main() {

View file

@ -5,7 +5,7 @@ import (
"sync" "sync"
"time" "time"
"git.toki-labs.com/toki/common-proto-socket/go/packets" "git.toki-labs.com/toki/proto-socket/go/packets"
) )
const ( const (

View file

@ -10,7 +10,7 @@ import (
"google.golang.org/protobuf/proto" "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") var ErrNotConnected = errors.New("not connected")

View file

@ -7,7 +7,7 @@ import (
"google.golang.org/protobuf/proto" "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 { type nonceTestTransport struct {

View file

@ -12,8 +12,8 @@ import (
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
toki "git.toki-labs.com/toki/common-proto-socket/go" toki "git.toki-labs.com/toki/proto-socket/go"
"git.toki-labs.com/toki/common-proto-socket/go/packets" "git.toki-labs.com/toki/proto-socket/go/packets"
) )
const ( const (

View file

@ -21,8 +21,8 @@ import (
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
"nhooyr.io/websocket" "nhooyr.io/websocket"
toki "git.toki-labs.com/toki/common-proto-socket/go" toki "git.toki-labs.com/toki/proto-socket/go"
"git.toki-labs.com/toki/common-proto-socket/go/packets" "git.toki-labs.com/toki/proto-socket/go/packets"
) )
const ( const (

View file

@ -21,8 +21,8 @@ import (
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
"nhooyr.io/websocket" "nhooyr.io/websocket"
toki "git.toki-labs.com/toki/common-proto-socket/go" toki "git.toki-labs.com/toki/proto-socket/go"
"git.toki-labs.com/toki/common-proto-socket/go/packets" "git.toki-labs.com/toki/proto-socket/go/packets"
) )
const ( const (

View file

@ -21,8 +21,8 @@ import (
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
"nhooyr.io/websocket" "nhooyr.io/websocket"
toki "git.toki-labs.com/toki/common-proto-socket/go" toki "git.toki-labs.com/toki/proto-socket/go"
"git.toki-labs.com/toki/common-proto-socket/go/packets" "git.toki-labs.com/toki/proto-socket/go/packets"
) )
const ( const (

View file

@ -21,8 +21,8 @@ import (
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
"nhooyr.io/websocket" "nhooyr.io/websocket"
toki "git.toki-labs.com/toki/common-proto-socket/go" toki "git.toki-labs.com/toki/proto-socket/go"
"git.toki-labs.com/toki/common-proto-socket/go/packets" "git.toki-labs.com/toki/proto-socket/go/packets"
) )
const ( const (

View file

@ -12,8 +12,8 @@ import (
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
toki "git.toki-labs.com/toki/common-proto-socket/go" toki "git.toki-labs.com/toki/proto-socket/go"
"git.toki-labs.com/toki/common-proto-socket/go/packets" "git.toki-labs.com/toki/proto-socket/go/packets"
) )
const ( const (

View file

@ -12,8 +12,8 @@ import (
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
toki "git.toki-labs.com/toki/common-proto-socket/go" toki "git.toki-labs.com/toki/proto-socket/go"
"git.toki-labs.com/toki/common-proto-socket/go/packets" "git.toki-labs.com/toki/proto-socket/go/packets"
) )
const ( const (

View file

@ -12,8 +12,8 @@ import (
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
toki "git.toki-labs.com/toki/common-proto-socket/go" toki "git.toki-labs.com/toki/proto-socket/go"
"git.toki-labs.com/toki/common-proto-socket/go/packets" "git.toki-labs.com/toki/proto-socket/go/packets"
) )
const ( const (

View file

@ -7,8 +7,8 @@ import (
"net" "net"
"time" "time"
toki "git.toki-labs.com/toki/common-proto-socket/go" toki "git.toki-labs.com/toki/proto-socket/go"
"git.toki-labs.com/toki/common-proto-socket/go/packets" "git.toki-labs.com/toki/proto-socket/go/packets"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )

View file

@ -6,8 +6,8 @@ import (
"log" "log"
"time" "time"
toki "git.toki-labs.com/toki/common-proto-socket/go" toki "git.toki-labs.com/toki/proto-socket/go"
"git.toki-labs.com/toki/common-proto-socket/go/packets" "git.toki-labs.com/toki/proto-socket/go/packets"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
"nhooyr.io/websocket" "nhooyr.io/websocket"

View file

@ -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 go 1.22

View file

@ -191,7 +191,7 @@ const file_packets_message_common_proto_rawDesc = "" +
"\tHeartBeat\":\n" + "\tHeartBeat\":\n" +
"\bTestData\x12\x14\n" + "\bTestData\x12\x14\n" +
"\x05index\x18\x01 \x01(\x05R\x05index\x12\x18\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 ( var (
file_packets_message_common_proto_rawDescOnce sync.Once file_packets_message_common_proto_rawDescOnce sync.Once

View file

@ -1,6 +1,6 @@
syntax = "proto3"; 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 { message PacketBase {
string typeName = 1; string typeName = 1;

View file

@ -12,7 +12,7 @@ import (
"google.golang.org/protobuf/proto" "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 const MaxPacketSize = 64 << 20

View file

@ -8,8 +8,8 @@ import (
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
toki "git.toki-labs.com/toki/common-proto-socket/go" toki "git.toki-labs.com/toki/proto-socket/go"
"git.toki-labs.com/toki/common-proto-socket/go/packets" "git.toki-labs.com/toki/proto-socket/go/packets"
) )
type fakeTransport struct { type fakeTransport struct {

View file

@ -12,7 +12,7 @@ import (
"nhooyr.io/websocket" "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) { func TestHeartbeatDisconnectsWithoutResponse(t *testing.T) {

View file

@ -8,8 +8,8 @@ import (
"testing" "testing"
"time" "time"
toki "git.toki-labs.com/toki/common-proto-socket/go" toki "git.toki-labs.com/toki/proto-socket/go"
"git.toki-labs.com/toki/common-proto-socket/go/packets" "git.toki-labs.com/toki/proto-socket/go/packets"
) )
func TestTcpRequestResponse(t *testing.T) { func TestTcpRequestResponse(t *testing.T) {

View file

@ -13,8 +13,8 @@ import (
"testing" "testing"
"time" "time"
toki "git.toki-labs.com/toki/common-proto-socket/go" toki "git.toki-labs.com/toki/proto-socket/go"
"git.toki-labs.com/toki/common-proto-socket/go/packets" "git.toki-labs.com/toki/proto-socket/go/packets"
) )
func TestTypeNameMatchesDartConvention(t *testing.T) { func TestTypeNameMatchesDartConvention(t *testing.T) {

View file

@ -8,8 +8,8 @@ import (
"nhooyr.io/websocket" "nhooyr.io/websocket"
toki "git.toki-labs.com/toki/common-proto-socket/go" toki "git.toki-labs.com/toki/proto-socket/go"
"git.toki-labs.com/toki/common-proto-socket/go/packets" "git.toki-labs.com/toki/proto-socket/go/packets"
) )
func TestTLSTcp(t *testing.T) { func TestTLSTcp(t *testing.T) {

View file

@ -9,8 +9,8 @@ import (
"nhooyr.io/websocket" "nhooyr.io/websocket"
toki "git.toki-labs.com/toki/common-proto-socket/go" toki "git.toki-labs.com/toki/proto-socket/go"
"git.toki-labs.com/toki/common-proto-socket/go/packets" "git.toki-labs.com/toki/proto-socket/go/packets"
) )
func TestWsRequestResponse(t *testing.T) { func TestWsRequestResponse(t *testing.T) {

View file

@ -10,7 +10,7 @@ import (
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
"nhooyr.io/websocket" "nhooyr.io/websocket"
"git.toki-labs.com/toki/common-proto-socket/go/packets" "git.toki-labs.com/toki/proto-socket/go/packets"
) )
const ( const (

View file

@ -6,9 +6,29 @@ This package implements Proto Socket protocol version `0.1` for TypeScript.
## Scope ## Scope
- Runtime targets: Node.js TCP, Node.js WebSocket, browser-friendly core types. - Runtime targets: Node.js TCP, Node.js WebSocket, browser native WebSocket.
- Currently in scope: `Communicator`, `BaseClient`, TCP/WS client and server helpers, Go cross-language tests. - Browser entrypoint (`proto-socket`): `Communicator`, `BaseClient`, `BrowserWsClient`, `connectBrowserWs`. Has no `ws`, `node:*`, or `Buffer` dependency.
- Out of scope for this iteration: TLS+TCP, WSS, browser WebSocket transport wrapper. - 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 ## Proto Generation

View file

@ -10,7 +10,7 @@ import {
} from "../src/communicator.js"; } from "../src/communicator.js";
import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js"; import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
import { connectTcp, connectTcpTls } from "../src/tcp_client.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 HOST = "127.0.0.1";
const WS_PATH = "/"; const WS_PATH = "/";
@ -101,7 +101,7 @@ async function dial(mode: Mode, port: number, cert?: string): Promise<BaseClient
return connectTcp(HOST, port, 0, 0, parserMap); return connectTcp(HOST, port, 0, 0, parserMap);
} }
if (mode === "ws") { if (mode === "ws") {
return connectWs(HOST, port, WS_PATH, 0, 0, parserMap); return connectNodeWs(HOST, port, WS_PATH, 0, 0, parserMap);
} }
if (cert === undefined || cert.length === 0) { if (cert === undefined || cert.length === 0) {
throw new Error(`--cert is required for ${mode} mode`); throw new Error(`--cert is required for ${mode} mode`);
@ -110,7 +110,7 @@ async function dial(mode: Mode, port: number, cert?: string): Promise<BaseClient
if (mode === "tls") { if (mode === "tls") {
return connectTcpTls(HOST, port, { ca }, 0, 0, parserMap); return connectTcpTls(HOST, port, { ca }, 0, 0, parserMap);
} }
return connectWss(HOST, port, WS_PATH, { ca }, 0, 0, parserMap); return connectNodeWss(HOST, port, WS_PATH, { ca }, 0, 0, parserMap);
} }
async function runSendPush(client: BaseClient): Promise<boolean> { async function runSendPush(client: BaseClient): Promise<boolean> {

View file

@ -10,7 +10,7 @@ import {
} from "../src/communicator.js"; } from "../src/communicator.js";
import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js"; import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
import { connectTcp, connectTcpTls } from "../src/tcp_client.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 HOST = "127.0.0.1";
const WS_PATH = "/"; const WS_PATH = "/";
@ -101,7 +101,7 @@ async function dial(mode: Mode, port: number, cert?: string): Promise<BaseClient
return connectTcp(HOST, port, 0, 0, parserMap); return connectTcp(HOST, port, 0, 0, parserMap);
} }
if (mode === "ws") { if (mode === "ws") {
return connectWs(HOST, port, WS_PATH, 0, 0, parserMap); return connectNodeWs(HOST, port, WS_PATH, 0, 0, parserMap);
} }
if (cert === undefined || cert.length === 0) { if (cert === undefined || cert.length === 0) {
throw new Error(`--cert is required for ${mode} mode`); throw new Error(`--cert is required for ${mode} mode`);
@ -110,7 +110,7 @@ async function dial(mode: Mode, port: number, cert?: string): Promise<BaseClient
if (mode === "tls") { if (mode === "tls") {
return connectTcpTls(HOST, port, { ca }, 0, 0, parserMap); return connectTcpTls(HOST, port, { ca }, 0, 0, parserMap);
} }
return connectWss(HOST, port, WS_PATH, { ca }, 0, 0, parserMap); return connectNodeWss(HOST, port, WS_PATH, { ca }, 0, 0, parserMap);
} }
async function runSendPush(client: BaseClient): Promise<boolean> { async function runSendPush(client: BaseClient): Promise<boolean> {

View file

@ -10,7 +10,7 @@ import {
} from "../src/communicator.js"; } from "../src/communicator.js";
import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js"; import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
import { connectTcp, connectTcpTls } from "../src/tcp_client.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 HOST = "127.0.0.1";
const WS_PATH = "/"; const WS_PATH = "/";
@ -103,7 +103,7 @@ async function dial(mode: Mode, port: number, cert?: string): Promise<BaseClient
return connectTcp(HOST, port, 0, 0, parserMap); return connectTcp(HOST, port, 0, 0, parserMap);
} }
if (mode === "ws") { if (mode === "ws") {
return connectWs(HOST, port, WS_PATH, 0, 0, parserMap); return connectNodeWs(HOST, port, WS_PATH, 0, 0, parserMap);
} }
if (cert === undefined || cert.length === 0) { if (cert === undefined || cert.length === 0) {
throw new Error(`--cert is required for ${mode} mode`); throw new Error(`--cert is required for ${mode} mode`);
@ -112,7 +112,7 @@ async function dial(mode: Mode, port: number, cert?: string): Promise<BaseClient
if (mode === "tls") { if (mode === "tls") {
return connectTcpTls(HOST, port, { ca }, 0, 0, parserMap); return connectTcpTls(HOST, port, { ca }, 0, 0, parserMap);
} }
return connectWss(HOST, port, WS_PATH, { ca }, 0, 0, parserMap); return connectNodeWss(HOST, port, WS_PATH, { ca }, 0, 0, parserMap);
} }
async function runSendPush(client: BaseClient): Promise<boolean> { async function runSendPush(client: BaseClient): Promise<boolean> {

View file

@ -10,7 +10,7 @@ import {
} from "../src/communicator.js"; } from "../src/communicator.js";
import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js"; import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
import { connectTcp, connectTcpTls } from "../src/tcp_client.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 HOST = "127.0.0.1";
const WS_PATH = "/"; const WS_PATH = "/";
@ -101,7 +101,7 @@ async function dial(mode: Mode, port: number, cert?: string): Promise<BaseClient
return connectTcp(HOST, port, 0, 0, parserMap); return connectTcp(HOST, port, 0, 0, parserMap);
} }
if (mode === "ws") { if (mode === "ws") {
return connectWs(HOST, port, WS_PATH, 0, 0, parserMap); return connectNodeWs(HOST, port, WS_PATH, 0, 0, parserMap);
} }
if (cert === undefined || cert.length === 0) { if (cert === undefined || cert.length === 0) {
throw new Error(`--cert is required for ${mode} mode`); throw new Error(`--cert is required for ${mode} mode`);
@ -110,7 +110,7 @@ async function dial(mode: Mode, port: number, cert?: string): Promise<BaseClient
if (mode === "tls") { if (mode === "tls") {
return connectTcpTls(HOST, port, { ca }, 0, 0, parserMap); return connectTcpTls(HOST, port, { ca }, 0, 0, parserMap);
} }
return connectWss(HOST, port, WS_PATH, { ca }, 0, 0, parserMap); return connectNodeWss(HOST, port, WS_PATH, { ca }, 0, 0, parserMap);
} }
async function runSendPush(client: BaseClient): Promise<boolean> { async function runSendPush(client: BaseClient): Promise<boolean> {

View file

@ -14,8 +14,8 @@ import {
import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js"; import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
import { TcpClient } from "../src/tcp_client.js"; import { TcpClient } from "../src/tcp_client.js";
import { TcpServer } from "../src/tcp_server.js"; import { TcpServer } from "../src/tcp_server.js";
import { WsClient } from "../src/ws_client.js"; import { NodeWsClient } from "../src/node_ws_client.js";
import { WsServer } from "../src/ws_server.js"; import { NodeWsServer } from "../src/node_ws_server.js";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const repoRoot = path.resolve(path.dirname(__filename), "../.."); const repoRoot = path.resolve(path.dirname(__filename), "../..");
@ -121,7 +121,7 @@ async function runWsSendPush(): Promise<void> {
const received = new Promise<boolean>((resolve) => { const received = new Promise<boolean>((resolve) => {
resolveReceived = 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) => { server.onClientConnected = (client) => {
addListenerTyped(client.communicator, TestDataSchema, (data) => { addListenerTyped(client.communicator, TestDataSchema, (data) => {
console.log(`SERVER_RECEIVED index=${data.index} message=${data.message}`); console.log(`SERVER_RECEIVED index=${data.index} message=${data.message}`);
@ -150,7 +150,7 @@ async function runWsSendPush(): Promise<void> {
} }
async function runWsRequests(): Promise<void> { async function runWsRequests(): Promise<void> {
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) => { server.onClientConnected = (client) => {
addRequestListenerTyped(client.communicator, TestDataSchema, (req) => addRequestListenerTyped(client.communicator, TestDataSchema, (req) =>
create(TestDataSchema, { create(TestDataSchema, {
@ -223,11 +223,11 @@ async function runWssSendPush(): Promise<void> {
const received = new Promise<boolean>((resolve) => { const received = new Promise<boolean>((resolve) => {
resolveReceived = resolve; resolveReceived = resolve;
}); });
const server = new WsServer( const server = new NodeWsServer(
HOST, HOST,
WSS_PORT, WSS_PORT,
WS_PATH, WS_PATH,
(ws) => new WsClient(ws, 0, 0, parserMap()), (ws) => new NodeWsClient(ws, 0, 0, parserMap()),
serverTlsOptions(), serverTlsOptions(),
); );
server.onClientConnected = (client) => { server.onClientConnected = (client) => {
@ -258,11 +258,11 @@ async function runWssSendPush(): Promise<void> {
} }
async function runWssRequests(): Promise<void> { async function runWssRequests(): Promise<void> {
const server = new WsServer( const server = new NodeWsServer(
HOST, HOST,
WSS_PORT, WSS_PORT,
WS_PATH, WS_PATH,
(ws) => new WsClient(ws, 0, 0, parserMap()), (ws) => new NodeWsClient(ws, 0, 0, parserMap()),
serverTlsOptions(), serverTlsOptions(),
); );
server.onClientConnected = (client) => { server.onClientConnected = (client) => {

View file

@ -14,8 +14,8 @@ import {
import { TestDataSchema } from "../src/packets/message_common_pb.js"; import { TestDataSchema } from "../src/packets/message_common_pb.js";
import { TcpClient } from "../src/tcp_client.js"; import { TcpClient } from "../src/tcp_client.js";
import { TcpServer } from "../src/tcp_server.js"; import { TcpServer } from "../src/tcp_server.js";
import { WsClient } from "../src/ws_client.js"; import { NodeWsClient } from "../src/node_ws_client.js";
import { WsServer } from "../src/ws_server.js"; import { NodeWsServer } from "../src/node_ws_server.js";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const repoRoot = path.resolve(path.dirname(__filename), "../.."); const repoRoot = path.resolve(path.dirname(__filename), "../..");
@ -121,7 +121,7 @@ async function runWsSendPush(): Promise<void> {
const received = new Promise<boolean>((resolve) => { const received = new Promise<boolean>((resolve) => {
resolveReceived = 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) => { server.onClientConnected = (client) => {
addListenerTyped(client.communicator, TestDataSchema, (data) => { addListenerTyped(client.communicator, TestDataSchema, (data) => {
console.log(`SERVER_RECEIVED index=${data.index} message=${data.message}`); console.log(`SERVER_RECEIVED index=${data.index} message=${data.message}`);
@ -150,7 +150,7 @@ async function runWsSendPush(): Promise<void> {
} }
async function runWsRequests(): Promise<void> { async function runWsRequests(): Promise<void> {
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) => { server.onClientConnected = (client) => {
addRequestListenerTyped(client.communicator, TestDataSchema, (req) => addRequestListenerTyped(client.communicator, TestDataSchema, (req) =>
create(TestDataSchema, { create(TestDataSchema, {
@ -223,11 +223,11 @@ async function runWssSendPush(): Promise<void> {
const received = new Promise<boolean>((resolve) => { const received = new Promise<boolean>((resolve) => {
resolveReceived = resolve; resolveReceived = resolve;
}); });
const server = new WsServer( const server = new NodeWsServer(
HOST, HOST,
WSS_PORT, WSS_PORT,
WS_PATH, WS_PATH,
(ws) => new WsClient(ws, 0, 0, parserMap()), (ws) => new NodeWsClient(ws, 0, 0, parserMap()),
serverTlsOptions(), serverTlsOptions(),
); );
server.onClientConnected = (client) => { server.onClientConnected = (client) => {
@ -258,11 +258,11 @@ async function runWssSendPush(): Promise<void> {
} }
async function runWssRequests(): Promise<void> { async function runWssRequests(): Promise<void> {
const server = new WsServer( const server = new NodeWsServer(
HOST, HOST,
WSS_PORT, WSS_PORT,
WS_PATH, WS_PATH,
(ws) => new WsClient(ws, 0, 0, parserMap()), (ws) => new NodeWsClient(ws, 0, 0, parserMap()),
serverTlsOptions(), serverTlsOptions(),
); );
server.onClientConnected = (client) => { server.onClientConnected = (client) => {

View file

@ -14,8 +14,8 @@ import {
import { TestDataSchema } from "../src/packets/message_common_pb.js"; import { TestDataSchema } from "../src/packets/message_common_pb.js";
import { TcpClient } from "../src/tcp_client.js"; import { TcpClient } from "../src/tcp_client.js";
import { TcpServer } from "../src/tcp_server.js"; import { TcpServer } from "../src/tcp_server.js";
import { WsClient } from "../src/ws_client.js"; import { NodeWsClient } from "../src/node_ws_client.js";
import { WsServer } from "../src/ws_server.js"; import { NodeWsServer } from "../src/node_ws_server.js";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const repoRoot = path.resolve(path.dirname(__filename), "../.."); const repoRoot = path.resolve(path.dirname(__filename), "../..");
@ -125,7 +125,7 @@ async function runWsSendPush(): Promise<void> {
const received = new Promise<boolean>((resolve) => { const received = new Promise<boolean>((resolve) => {
resolveReceived = 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) => { server.onClientConnected = (client) => {
addListenerTyped(client.communicator, TestDataSchema, (data) => { addListenerTyped(client.communicator, TestDataSchema, (data) => {
console.log(`SERVER_RECEIVED index=${data.index} message=${data.message}`); console.log(`SERVER_RECEIVED index=${data.index} message=${data.message}`);
@ -154,7 +154,7 @@ async function runWsSendPush(): Promise<void> {
} }
async function runWsRequests(): Promise<void> { async function runWsRequests(): Promise<void> {
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) => { server.onClientConnected = (client) => {
addRequestListenerTyped(client.communicator, TestDataSchema, (req) => addRequestListenerTyped(client.communicator, TestDataSchema, (req) =>
create(TestDataSchema, { create(TestDataSchema, {
@ -227,11 +227,11 @@ async function runWssSendPush(): Promise<void> {
const received = new Promise<boolean>((resolve) => { const received = new Promise<boolean>((resolve) => {
resolveReceived = resolve; resolveReceived = resolve;
}); });
const server = new WsServer( const server = new NodeWsServer(
HOST, HOST,
WSS_PORT, WSS_PORT,
WS_PATH, WS_PATH,
(ws) => new WsClient(ws, 0, 0, parserMap()), (ws) => new NodeWsClient(ws, 0, 0, parserMap()),
serverTlsOptions(), serverTlsOptions(),
); );
server.onClientConnected = (client) => { server.onClientConnected = (client) => {
@ -262,11 +262,11 @@ async function runWssSendPush(): Promise<void> {
} }
async function runWssRequests(): Promise<void> { async function runWssRequests(): Promise<void> {
const server = new WsServer( const server = new NodeWsServer(
HOST, HOST,
WSS_PORT, WSS_PORT,
WS_PATH, WS_PATH,
(ws) => new WsClient(ws, 0, 0, parserMap()), (ws) => new NodeWsClient(ws, 0, 0, parserMap()),
serverTlsOptions(), serverTlsOptions(),
); );
server.onClientConnected = (client) => { server.onClientConnected = (client) => {

View file

@ -14,8 +14,8 @@ import {
import { TestDataSchema } from "../src/packets/message_common_pb.js"; import { TestDataSchema } from "../src/packets/message_common_pb.js";
import { TcpClient } from "../src/tcp_client.js"; import { TcpClient } from "../src/tcp_client.js";
import { TcpServer } from "../src/tcp_server.js"; import { TcpServer } from "../src/tcp_server.js";
import { WsClient } from "../src/ws_client.js"; import { NodeWsClient } from "../src/node_ws_client.js";
import { WsServer } from "../src/ws_server.js"; import { NodeWsServer } from "../src/node_ws_server.js";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const repoRoot = path.resolve(path.dirname(__filename), "../.."); const repoRoot = path.resolve(path.dirname(__filename), "../..");
@ -121,7 +121,7 @@ async function runWsSendPush(): Promise<void> {
const received = new Promise<boolean>((resolve) => { const received = new Promise<boolean>((resolve) => {
resolveReceived = 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) => { server.onClientConnected = (client) => {
addListenerTyped(client.communicator, TestDataSchema, (data) => { addListenerTyped(client.communicator, TestDataSchema, (data) => {
console.log(`SERVER_RECEIVED index=${data.index} message=${data.message}`); console.log(`SERVER_RECEIVED index=${data.index} message=${data.message}`);
@ -150,7 +150,7 @@ async function runWsSendPush(): Promise<void> {
} }
async function runWsRequests(): Promise<void> { async function runWsRequests(): Promise<void> {
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) => { server.onClientConnected = (client) => {
addRequestListenerTyped(client.communicator, TestDataSchema, (req) => addRequestListenerTyped(client.communicator, TestDataSchema, (req) =>
create(TestDataSchema, { create(TestDataSchema, {
@ -223,11 +223,11 @@ async function runWssSendPush(): Promise<void> {
const received = new Promise<boolean>((resolve) => { const received = new Promise<boolean>((resolve) => {
resolveReceived = resolve; resolveReceived = resolve;
}); });
const server = new WsServer( const server = new NodeWsServer(
HOST, HOST,
WSS_PORT, WSS_PORT,
WS_PATH, WS_PATH,
(ws) => new WsClient(ws, 0, 0, parserMap()), (ws) => new NodeWsClient(ws, 0, 0, parserMap()),
serverTlsOptions(), serverTlsOptions(),
); );
server.onClientConnected = (client) => { server.onClientConnected = (client) => {
@ -258,11 +258,11 @@ async function runWssSendPush(): Promise<void> {
} }
async function runWssRequests(): Promise<void> { async function runWssRequests(): Promise<void> {
const server = new WsServer( const server = new NodeWsServer(
HOST, HOST,
WSS_PORT, WSS_PORT,
WS_PATH, WS_PATH,
(ws) => new WsClient(ws, 0, 0, parserMap()), (ws) => new NodeWsClient(ws, 0, 0, parserMap()),
serverTlsOptions(), serverTlsOptions(),
); );
server.onClientConnected = (client) => { server.onClientConnected = (client) => {

View file

@ -8,8 +8,7 @@
"name": "proto-socket", "name": "proto-socket",
"version": "1.0.5", "version": "1.0.5",
"dependencies": { "dependencies": {
"@bufbuild/protobuf": "^2.2.5", "@bufbuild/protobuf": "^2.2.5"
"ws": "^8.18.1"
}, },
"devDependencies": { "devDependencies": {
"@bufbuild/protoc-gen-es": "^2.2.5", "@bufbuild/protoc-gen-es": "^2.2.5",
@ -17,7 +16,16 @@
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"tsx": "^4.19.3", "tsx": "^4.19.3",
"typescript": "^5.8.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": { "node_modules/@bufbuild/protobuf": {
@ -1748,6 +1756,7 @@
"version": "8.20.0", "version": "8.20.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"

View file

@ -3,21 +3,39 @@
"version": "1.0.5", "version": "1.0.5",
"private": true, "private": true,
"type": "module", "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": { "scripts": {
"build": "tsc -p tsconfig.json", "build": "tsc -p tsconfig.json",
"test": "vitest run", "test": "vitest run",
"check": "tsc --noEmit" "check": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@bufbuild/protobuf": "^2.2.5", "@bufbuild/protobuf": "^2.2.5"
},
"peerDependencies": {
"ws": "^8.18.1" "ws": "^8.18.1"
}, },
"peerDependenciesMeta": {
"ws": {
"optional": true
}
},
"devDependencies": { "devDependencies": {
"@bufbuild/protoc-gen-es": "^2.2.5", "@bufbuild/protoc-gen-es": "^2.2.5",
"@types/node": "^22.15.21", "@types/node": "^22.15.21",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"tsx": "^4.19.3", "tsx": "^4.19.3",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vitest": "^3.1.3" "vitest": "^3.1.3",
"ws": "^8.18.1"
} }
} }

View file

@ -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<void> {
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<void> {
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<BrowserWsClient> {
const Ctor = options?.WebSocketCtor ?? globalThis.WebSocket;
if (Ctor === undefined) {
throw new Error("global WebSocket is not available; provide WebSocketCtor option");
}
return new Promise<BrowserWsClient>((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<Uint8Array> {
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<void> {
if (ws.readyState === ws.CLOSED) {
return;
}
await new Promise<void>((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();
}
});
}

View file

@ -1,7 +1,4 @@
export * from "./base_client.js"; export * from "./base_client.js";
export * from "./communicator.js"; export * from "./communicator.js";
export * from "./packets/message_common_pb.js"; export * from "./packets/message_common_pb.js";
export * from "./tcp_client.js"; export * from "./browser_ws_client.js";
export * from "./tcp_server.js";
export * from "./ws_client.js";
export * from "./ws_server.js";

7
typescript/src/node.ts Normal file
View file

@ -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";

View file

@ -6,7 +6,7 @@ import { BaseClient } from "./base_client.js";
import { type ParserMap } from "./communicator.js"; import { type ParserMap } from "./communicator.js";
import { PacketBaseSchema, type PacketBase } from "./packets/message_common_pb.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; private readonly ws: WebSocket;
constructor(ws: WebSocket, intervalSec: number, waitSec: number, parserMap: ParserMap) { 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, host: string,
port: number, port: number,
path: string, path: string,
intervalSec: number, intervalSec: number,
waitSec: number, waitSec: number,
parserMap: ParserMap, parserMap: ParserMap,
): Promise<WsClient> { ): Promise<NodeWsClient> {
return new Promise<WsClient>((resolve, reject) => { return new Promise<NodeWsClient>((resolve, reject) => {
const ws = new WebSocket(`ws://${host}:${port}${path}`); const ws = new WebSocket(`ws://${host}:${port}${path}`);
const onOpen = () => { const onOpen = () => {
cleanup(); cleanup();
resolve(new WsClient(ws, intervalSec, waitSec, parserMap)); resolve(new NodeWsClient(ws, intervalSec, waitSec, parserMap));
}; };
const onError = (err: Error) => { const onError = (err: Error) => {
cleanup(); cleanup();
@ -77,7 +77,7 @@ export async function connectWs(
}); });
} }
export async function connectWss( export async function connectNodeWss(
host: string, host: string,
port: number, port: number,
path: string, path: string,
@ -85,12 +85,12 @@ export async function connectWss(
intervalSec: number, intervalSec: number,
waitSec: number, waitSec: number,
parserMap: ParserMap, parserMap: ParserMap,
): Promise<WsClient> { ): Promise<NodeWsClient> {
return new Promise<WsClient>((resolve, reject) => { return new Promise<NodeWsClient>((resolve, reject) => {
const ws = new WebSocket(`wss://${host}:${port}${path}`, wsOptions); const ws = new WebSocket(`wss://${host}:${port}${path}`, wsOptions);
const onOpen = () => { const onOpen = () => {
cleanup(); cleanup();
resolve(new WsClient(ws, intervalSec, waitSec, parserMap)); resolve(new NodeWsClient(ws, intervalSec, waitSec, parserMap));
}; };
const onError = (err: Error) => { const onError = (err: Error) => {
cleanup(); cleanup();

View file

@ -3,20 +3,20 @@ import * as https from "node:https";
import { type Message } from "@bufbuild/protobuf"; import { type Message } from "@bufbuild/protobuf";
import WebSocket, { WebSocketServer } from "ws"; 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 wss: WebSocketServer | null = null;
private httpsServer: https.Server | null = null; private httpsServer: https.Server | null = null;
private readonly clients = new Set<WsClient>(); private readonly clients = new Set<NodeWsClient>();
onClientConnected: (client: WsClient) => void = () => {}; onClientConnected: (client: NodeWsClient) => void = () => {};
constructor( constructor(
private readonly host: string, private readonly host: string,
private readonly listenPort: number, private readonly listenPort: number,
private readonly path: string, private readonly path: string,
private readonly newClient: (ws: WebSocket) => WsClient, private readonly newClient: (ws: WebSocket) => NodeWsClient,
private readonly tlsOptions?: https.ServerOptions, private readonly tlsOptions?: https.ServerOptions,
) {} ) {}
@ -113,7 +113,7 @@ export class WsServer {
await Promise.all([...this.clients].map(async (client) => client.communicator.send(msg))); 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); this.clients.delete(client);
} }

View file

@ -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<string, Set<Listener>>();
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<void>> = [];
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<TestData>((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<TestData>((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<void>((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<void>((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<void>((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();
});
});

View file

@ -12,8 +12,8 @@ import {
sendRequestTyped, sendRequestTyped,
} from "../src/communicator.js"; } from "../src/communicator.js";
import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js"; import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
import { connectWs, connectWss, WsClient } from "../src/ws_client.js"; import { connectNodeWs, connectNodeWss, NodeWsClient } from "../src/node_ws_client.js";
import { WsServer } from "../src/ws_server.js"; import { NodeWsServer } from "../src/node_ws_server.js";
const certsDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "certs"); const certsDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "certs");
const cert = fs.readFileSync(path.join(certsDir, "server.crt")); const cert = fs.readFileSync(path.join(certsDir, "server.crt"));
@ -23,7 +23,7 @@ function parserMap() {
return new Map([[TestDataSchema.typeName, parserFromSchema(TestDataSchema)]]); return new Map([[TestDataSchema.typeName, parserFromSchema(TestDataSchema)]]);
} }
async function waitForClientMessage(client: WsClient): Promise<TestData> { async function waitForClientMessage(client: NodeWsClient): Promise<TestData> {
return new Promise<TestData>((resolve) => { return new Promise<TestData>((resolve) => {
addListenerTyped(client.communicator, TestDataSchema, (msg) => resolve(msg)); addListenerTyped(client.communicator, TestDataSchema, (msg) => resolve(msg));
}); });
@ -42,7 +42,7 @@ describe("WS", () => {
}); });
test("connectWs sends and receives TestData", async () => { 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()); cleanup.push(async () => server.stop());
await server.start(); 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()); cleanup.push(async () => client.close());
const pushed = waitForClientMessage(client); const pushed = waitForClientMessage(client);
@ -69,7 +69,7 @@ describe("WS", () => {
}); });
test("sendRequest/response roundtrip over WS", async () => { 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()); cleanup.push(async () => server.stop());
await server.start(); 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()); cleanup.push(async () => client.close());
await expect( await expect(
@ -99,7 +99,7 @@ describe("WS", () => {
}); });
test("connectWss sends and receives TestData", async () => { 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, cert,
key, key,
}); });
@ -116,7 +116,7 @@ describe("WS", () => {
}); });
}; };
const client = await connectWss( const client = await connectNodeWss(
"127.0.0.1", "127.0.0.1",
server.port, server.port,
"/", "/",
@ -137,7 +137,7 @@ describe("WS", () => {
}); });
test("WSS sendRequest/response roundtrip", async () => { 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, cert,
key, key,
}); });
@ -153,7 +153,7 @@ describe("WS", () => {
); );
}; };
const client = await connectWss( const client = await connectNodeWss(
"127.0.0.1", "127.0.0.1",
server.port, server.port,
"/", "/",
@ -178,7 +178,7 @@ describe("WS", () => {
}); });
test("concurrent sendRequest/response roundtrip over WS", async () => { 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()); cleanup.push(async () => server.stop());
await server.start(); 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()); cleanup.push(async () => client.close());
const results = await Promise.all( const results = await Promise.all(

View file

@ -10,6 +10,7 @@
"skipLibCheck": true, "skipLibCheck": true,
"rootDir": ".", "rootDir": ".",
"outDir": "dist", "outDir": "dist",
"lib": ["ES2022", "DOM"],
"types": ["node"] "types": ["node"]
}, },
"include": ["src/**/*.ts", "test/**/*.ts", "crosstest/**/*.ts"] "include": ["src/**/*.ts", "test/**/*.ts", "crosstest/**/*.ts"]