기능: TypeScript 패키지를 devDependencies로 통합하고 WebSocket 기능을 개선했다
- @bufbuild/protobuf를 dependencies에서 devDependencies로 이동 - ws를 peerDependencies에서 devDependencies로 이동 - node_ws_server.ts에 TLS 및 라우팅 기능 추가 - node_ws_client.ts에 TCP 클라이언트 통합 기능 추가 - message_common_pb.ts 프로토콜 버퍼 코드 재생성 - 크로스테스트 클라이언트 파일들 경로 업데이트 - 테스트 파일들 설정 업데이트 - 문서(PORTING_GUIDE, README) 업데이트
This commit is contained in:
parent
002d612e7b
commit
5a2a21e75a
28 changed files with 891 additions and 367 deletions
|
|
@ -6,6 +6,10 @@
|
|||
- 동시성·close-once 구조는 Go 구현체를 우선 참고하고, Dart 구현체는 public API와 parser map 사용 예시를 함께 참고한다
|
||||
- 프로토콜 정의는 `PROTOCOL.md`가 단일 기준이다
|
||||
- 각 언어의 관용적 패턴을 따르되, 아래 핵심 구조는 반드시 유지한다
|
||||
- Protocol Buffers는 이 프로젝트의 핵심 프로토콜 의존성으로 취급한다. 그 외 런타임 구현은 가능한 한 native platform API와 standard library를 우선한다
|
||||
- protobuf 외 외부 의존성은 구현하려는 기능에 정확히 대응하는 작고 focused된 라이브러리일 때만 허용한다. 넓은 프레임워크, 대형 런타임 계층, 부가 기능이 과도한 패키지를 좁은 기능 하나 때문에 추가하지 않는다
|
||||
- protobuf 외 외부 의존성을 추가해야 한다면 해당 언어에서 실용적인 native 대안이 없는지 먼저 검토하고, README에 이유와 범위를 명시한다
|
||||
- TCP/TLS/WebSocket, 바이너리 버퍼, 타이머, 동시성/비동기 처리처럼 플랫폼이 기본 제공하는 기능은 외부 패키지보다 기본 API를 우선한다
|
||||
|
||||
### 반드시 유지해야 하는 구조
|
||||
|
||||
|
|
@ -27,7 +31,7 @@
|
|||
3. canonical proto인 `proto/message_common.proto`에서 언어별 protobuf binding을 생성한다. Go처럼 generator 전용 option이 필요하면 언어 패키지 안에 copy를 둘 수 있지만, message schema는 `tools/check_proto_sync.sh`로 검증 가능해야 한다.
|
||||
4. `Transport`와 `Communicator`를 먼저 구현하고, 그 위에 TCP/TLS+TCP/WS/WSS client/server를 올린다.
|
||||
5. 같은 언어 단위 테스트를 작성한 뒤 `agent-ops/skills/project/add-proto-socket-crosstest-language/SKILL.md`의 runner 배치 규칙에 맞춰 양방향 crosstest를 추가한다.
|
||||
6. README의 Implementations 표를 `Available`로 바꾸기 전에 formatter/linter, 단위 테스트, cross-language tests를 모두 통과시킨다.
|
||||
6. README의 Implementations 표를 `Available`로 바꾸기 전에 formatter/linter, 단위 테스트, cross-language tests를 모두 통과시키고, protobuf 외 런타임 의존성이 있다면 native 대안 검토 결과, 기능 범위와 라이브러리 범위가 맞는 이유, 필요성을 문서화한다.
|
||||
|
||||
템플릿은 시작점과 완료 조건만 고정한다. 실제 소스 구조, 패키지 매니저, async runtime은 각 언어의 관용적 선택을 따른다.
|
||||
|
||||
|
|
@ -149,8 +153,8 @@
|
|||
|
||||
### 주의사항
|
||||
|
||||
- **크로스 환경 (Browser & Node.js)**: 코어 로직(`Communicator`, `BaseClient`)은 런타임 환경에 독립적으로 작성한다. 브라우저에서는 내장 `WebSocket`을, Node.js에서는 `net.Socket`(TCP)이나 `ws` 패키지를 사용하는 Transport 구현체를 주입받도록 설계한다
|
||||
- **protobuf**: `@bufbuild/protobuf` (protobuf-es) 패키지 사용을 권장한다. `typeName`은 `MessageType.typeName`에서 추출한다. `PROTOCOL.md` 표에서 TypeScript 행을 확인한다
|
||||
- **크로스 환경 (Browser & Node.js)**: 코어 로직(`Communicator`, `BaseClient`)은 런타임 환경에 독립적으로 작성한다. 브라우저에서는 내장 `WebSocket`을 사용하고, Node.js에서는 `net`, `tls`, `http`, `https` 등 Node.js 내장 모듈 기반 Transport를 사용한다. TypeScript 런타임에 외부 WebSocket 패키지를 추가하지 않는다
|
||||
- **protobuf**: protobuf는 핵심 프로토콜 계층이므로 예외로 취급한다. 현재 TypeScript 구현은 canonical schema에 맞춘 native codec을 사용하지만, protobuf binding/runtime을 도입해야 한다면 프로토콜 계층 용도로만 제한한다. `typeName`은 로컬 `MessageType.typeName` 값이 `PROTOCOL.md` 표와 일치해야 한다
|
||||
- **단일 스레드 (Event Loop)**: JS/TS는 단일 스레드 기반이므로 메모리 동시 접근에 대한 Mutex/Lock(`sync.RWMutex`)은 불필요하다. 단, 비동기 컨텍스트(await) 간의 논리적 상태 오염은 주의한다
|
||||
- **바이너리 처리**: Node.js의 `Buffer` 대신 표준 웹 API인 `Uint8Array`를 기준으로 작성하여 브라우저 환경 호환성을 확보한다
|
||||
- **`connCloseOnce` 패턴**: 단순 `isClosed: boolean` 플래그로 멱등성을 보장한다. 만약 `Close()` 내에 `await`가 포함될 경우 중복 실행(re-entrancy) 방지에 유의한다
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ Built on Protocol Buffers with TCP length-prefixed framing, WebSocket binary fra
|
|||
- Keep the core transport layer thin and stable
|
||||
- Provide only the minimum common foundation for cross-language communication
|
||||
- Standardize framing, serialization, routing, request-response correlation, and heartbeat
|
||||
- Treat Protocol Buffers as the core protocol dependency. Other runtime dependencies should be native-first, narrowly scoped, and proportional to the feature they implement
|
||||
- Do not embed application semantics into the protocol core
|
||||
- Domain-specific concerns such as auth, session, agent workflow, chat features, and game logic belong in upper-layer implementations
|
||||
|
||||
|
|
@ -44,6 +45,8 @@ Protocol compatibility is tracked separately from language package versions. See
|
|||
|
||||
New language implementations should start from [PORTING_GUIDE.md](PORTING_GUIDE.md) and the templates in [agent-ops/skills/project/add-proto-socket-crosstest-language/templates/](agent-ops/skills/project/add-proto-socket-crosstest-language/templates/). Mark an implementation available only after its same-language tests and cross-language tests pass.
|
||||
|
||||
Roadmap direction: every implementation should stay as close to native platform behavior as possible outside the protobuf layer. Prefer built-in TCP/TLS/WebSocket, concurrency, timer, and binary APIs over external packages. A focused library that directly matches a protocol need is acceptable when native support is missing or impractical, but broad frameworks or runtime layers must not be introduced just to cover a narrow feature such as WebSocket transport. Runtime dependencies beyond protobuf must be explicitly justified, protocol-relevant, and kept minimal.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start (Dart)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ Target protocol version: `0.1`
|
|||
- [ ] Built-in `HeartBeat` registration is owned by the framework.
|
||||
- [ ] `PacketBase.typeName`, `nonce`, `data`, and `responseNonce` semantics match `PROTOCOL.md`.
|
||||
- [ ] Same `typeName` cannot be registered for both normal listener and request listener on one connection.
|
||||
- [ ] Runtime dependencies beyond protobuf are avoided where practical; any required non-protobuf dependency is focused, proportional to the implemented feature, and documented with its purpose and native alternative review.
|
||||
|
||||
## Transport
|
||||
|
||||
|
|
@ -17,6 +18,8 @@ Target protocol version: `0.1`
|
|||
- [ ] TCP framing writes a 4-byte big-endian length followed by one `PacketBase` protobuf payload.
|
||||
- [ ] TCP reader rejects zero-length packets as no-op and closes on invalid or oversized lengths.
|
||||
- [ ] WebSocket reader/writer uses one binary frame per `PacketBase` payload.
|
||||
- [ ] TCP/TLS/WebSocket implementations prefer native platform APIs or the standard library over external packages.
|
||||
- [ ] No broad framework or runtime layer is introduced solely to cover TCP/TLS/WebSocket transport.
|
||||
- [ ] TLS+TCP and WSS are supported or explicitly marked unsupported with a reason.
|
||||
|
||||
## Lifecycle
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ Use this package README to document the concrete files for:
|
|||
- Generated protobuf bindings from `proto/message_common.proto`.
|
||||
- Unit tests and cross-language tests.
|
||||
|
||||
## Runtime Dependencies
|
||||
|
||||
Protocol Buffers is the core protocol dependency. For everything else, prefer native platform APIs and the standard library for transport, binary handling, timers, and concurrency. A non-protobuf runtime dependency must be focused and proportional to the feature it implements. Do not introduce a broad framework or runtime layer solely to cover a narrow feature such as WebSocket transport. If a non-protobuf dependency is required, document why the native alternative is insufficient and keep the dependency surface minimal.
|
||||
|
||||
## Proto Generation
|
||||
|
||||
Document the package-local proto generation command here. The generated schema must match `proto/message_common.proto` except for language-specific generator options.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ This package implements Proto Socket protocol version `0.1` for TypeScript.
|
|||
|
||||
- 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`.
|
||||
- Node entrypoint (`proto-socket/node`): all of the above plus `TcpClient`/`TcpServer`, `NodeWsClient`/`NodeWsServer`, `connectNodeWs`/`connectNodeWss` using Node.js built-ins.
|
||||
|
||||
## Import
|
||||
|
||||
|
|
@ -22,28 +22,15 @@ 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`):
|
||||
The TypeScript runtime has no non-protobuf package dependencies. The current canonical message codec
|
||||
and Node.js TCP/TLS/WebSocket transports are implemented with native platform APIs, so this package
|
||||
has no runtime dependencies at all.
|
||||
|
||||
```bash
|
||||
npm install ws
|
||||
```
|
||||
## Proto Sync
|
||||
|
||||
The browser entrypoint and Node TCP-only consumers do not need `ws`.
|
||||
|
||||
## Proto Generation
|
||||
|
||||
Generated bindings live in `src/packets/`.
|
||||
|
||||
```bash
|
||||
cd typescript
|
||||
PATH="$PWD/node_modules/.bin:$PATH" protoc \
|
||||
--proto_path ../proto \
|
||||
--es_out src/packets \
|
||||
--es_opt target=ts \
|
||||
message_common.proto
|
||||
```
|
||||
|
||||
After generation, run from the repository root:
|
||||
The canonical schema lives in `../proto/message_common.proto`. The TypeScript packet codec in
|
||||
`src/packets/` must stay wire-compatible with that schema. After proto changes, run from the
|
||||
repository root:
|
||||
|
||||
```bash
|
||||
tools/check_proto_sync.sh
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
import * as fs from "node:fs";
|
||||
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
|
||||
import type { BaseClient } from "../src/base_client.js";
|
||||
import {
|
||||
addListenerTyped,
|
||||
parserFromSchema,
|
||||
sendRequestTyped,
|
||||
} from "../src/communicator.js";
|
||||
import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
|
||||
import { create, TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
|
||||
import { connectTcp, connectTcpTls } from "../src/tcp_client.js";
|
||||
import { connectNodeWs, connectNodeWss } from "../src/node_ws_client.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
import * as fs from "node:fs";
|
||||
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
|
||||
import type { BaseClient } from "../src/base_client.js";
|
||||
import {
|
||||
addListenerTyped,
|
||||
parserFromSchema,
|
||||
sendRequestTyped,
|
||||
} from "../src/communicator.js";
|
||||
import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
|
||||
import { create, TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
|
||||
import { connectTcp, connectTcpTls } from "../src/tcp_client.js";
|
||||
import { connectNodeWs, connectNodeWss } from "../src/node_ws_client.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
import * as fs from "node:fs";
|
||||
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
|
||||
import type { BaseClient } from "../src/base_client.js";
|
||||
import {
|
||||
addListenerTyped,
|
||||
parserFromSchema,
|
||||
sendRequestTyped,
|
||||
} from "../src/communicator.js";
|
||||
import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
|
||||
import { create, TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
|
||||
import { connectTcp, connectTcpTls } from "../src/tcp_client.js";
|
||||
import { connectNodeWs, connectNodeWss } from "../src/node_ws_client.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
import * as fs from "node:fs";
|
||||
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
|
||||
import type { BaseClient } from "../src/base_client.js";
|
||||
import {
|
||||
addListenerTyped,
|
||||
parserFromSchema,
|
||||
sendRequestTyped,
|
||||
} from "../src/communicator.js";
|
||||
import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
|
||||
import { create, TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
|
||||
import { connectTcp, connectTcpTls } from "../src/tcp_client.js";
|
||||
import { connectNodeWs, connectNodeWss } from "../src/node_ws_client.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -4,14 +4,12 @@ import * as path from "node:path";
|
|||
import * as readline from "node:readline";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
|
||||
import {
|
||||
addListenerTyped,
|
||||
addRequestListenerTyped,
|
||||
parserFromSchema,
|
||||
} from "../src/communicator.js";
|
||||
import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
|
||||
import { create, 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 { NodeWsClient } from "../src/node_ws_client.js";
|
||||
|
|
|
|||
|
|
@ -4,14 +4,12 @@ import * as path from "node:path";
|
|||
import * as readline from "node:readline";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
|
||||
import {
|
||||
addListenerTyped,
|
||||
addRequestListenerTyped,
|
||||
parserFromSchema,
|
||||
} from "../src/communicator.js";
|
||||
import { TestDataSchema } from "../src/packets/message_common_pb.js";
|
||||
import { create, TestDataSchema } from "../src/packets/message_common_pb.js";
|
||||
import { TcpClient } from "../src/tcp_client.js";
|
||||
import { TcpServer } from "../src/tcp_server.js";
|
||||
import { NodeWsClient } from "../src/node_ws_client.js";
|
||||
|
|
|
|||
|
|
@ -4,14 +4,12 @@ import * as path from "node:path";
|
|||
import * as readline from "node:readline";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
|
||||
import {
|
||||
addListenerTyped,
|
||||
addRequestListenerTyped,
|
||||
parserFromSchema,
|
||||
} from "../src/communicator.js";
|
||||
import { TestDataSchema } from "../src/packets/message_common_pb.js";
|
||||
import { create, TestDataSchema } from "../src/packets/message_common_pb.js";
|
||||
import { TcpClient } from "../src/tcp_client.js";
|
||||
import { TcpServer } from "../src/tcp_server.js";
|
||||
import { NodeWsClient } from "../src/node_ws_client.js";
|
||||
|
|
|
|||
|
|
@ -4,14 +4,12 @@ import * as path from "node:path";
|
|||
import * as readline from "node:readline";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
|
||||
import {
|
||||
addListenerTyped,
|
||||
addRequestListenerTyped,
|
||||
parserFromSchema,
|
||||
} from "../src/communicator.js";
|
||||
import { TestDataSchema } from "../src/packets/message_common_pb.js";
|
||||
import { create, TestDataSchema } from "../src/packets/message_common_pb.js";
|
||||
import { TcpClient } from "../src/tcp_client.js";
|
||||
import { TcpServer } from "../src/tcp_server.js";
|
||||
import { NodeWsClient } from "../src/node_ws_client.js";
|
||||
|
|
|
|||
118
typescript/package-lock.json
generated
118
typescript/package-lock.json
generated
|
|
@ -7,82 +7,11 @@
|
|||
"": {
|
||||
"name": "proto-socket",
|
||||
"version": "1.0.5",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.2.5"
|
||||
},
|
||||
"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",
|
||||
"ws": "^8.18.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ws": "^8.18.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ws": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/protobuf": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.12.0.tgz",
|
||||
"integrity": "sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA==",
|
||||
"license": "(Apache-2.0 AND BSD-3-Clause)"
|
||||
},
|
||||
"node_modules/@bufbuild/protoc-gen-es": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protoc-gen-es/-/protoc-gen-es-2.12.0.tgz",
|
||||
"integrity": "sha512-d9htF6jEkSwPbp9d/vSmZOBF7eeG18AvTMKmVg4I23afnrQOxL2w3WOXa9TaufMCyu24QakEUb4vux8apI5e7A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "2.12.0",
|
||||
"@bufbuild/protoplugin": "2.12.0"
|
||||
},
|
||||
"bin": {
|
||||
"protoc-gen-es": "bin/protoc-gen-es"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@bufbuild/protobuf": "2.12.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@bufbuild/protobuf": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/protoplugin": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protoplugin/-/protoplugin-2.12.0.tgz",
|
||||
"integrity": "sha512-ORlDITp8AFUXzIhLRoMCG+ud+D3MPKWb5HQXBoskMMnjeyEjE1H1qLonVNPyOr8lkx3xSfYUo8a0dvOZJVAzow==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "2.12.0",
|
||||
"@typescript/vfs": "^1.6.2",
|
||||
"typescript": "5.4.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/protoplugin/node_modules/typescript": {
|
||||
"version": "5.4.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
||||
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
"vitest": "^3.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
|
|
@ -919,29 +848,6 @@
|
|||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/vfs": {
|
||||
"version": "1.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.4.tgz",
|
||||
"integrity": "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
|
||||
|
|
@ -1751,28 +1657,6 @@
|
|||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"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"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,24 +18,10 @@
|
|||
"test": "vitest run",
|
||||
"check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"ws": "^8.18.1"
|
||||
"vitest": "^3.1.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { create } from "@bufbuild/protobuf";
|
||||
|
||||
import { addListenerTyped, Communicator, type ParserMap, type Transport } from "./communicator.js";
|
||||
import { HeartBeatSchema, type PacketBase } from "./packets/message_common_pb.js";
|
||||
import { create, HeartBeatSchema, type PacketBase } from "./packets/message_common_pb.js";
|
||||
|
||||
export abstract class BaseClient implements Transport {
|
||||
readonly communicator = new Communicator();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
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";
|
||||
import { fromBinary, PacketBaseSchema, toBinary, type PacketBase } from "./packets/message_common_pb.js";
|
||||
|
||||
export interface BrowserWsConnectOptions {
|
||||
WebSocketCtor?: typeof WebSocket;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
import { create, fromBinary, toBinary, type Message } from "@bufbuild/protobuf";
|
||||
import type { GenMessage } from "@bufbuild/protobuf/codegenv2";
|
||||
|
||||
import {
|
||||
create,
|
||||
fromBinary,
|
||||
HeartBeatSchema,
|
||||
type Message,
|
||||
type MessageType,
|
||||
PacketBaseSchema,
|
||||
toBinary,
|
||||
type PacketBase,
|
||||
} from "./packets/message_common_pb.js";
|
||||
|
||||
export type { Message, MessageType } from "./packets/message_common_pb.js";
|
||||
|
||||
export class NotConnectedError extends Error {
|
||||
constructor() {
|
||||
super("not connected");
|
||||
|
|
@ -14,8 +18,6 @@ export class NotConnectedError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
export type MessageType<T extends Message = Message> = GenMessage<T>;
|
||||
|
||||
export type MessageParser<T extends Message = Message> = ((data: Uint8Array) => T) & {
|
||||
schema?: MessageType<T>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,15 +1,258 @@
|
|||
import WebSocket, { type ClientOptions, type RawData } from "ws";
|
||||
|
||||
import { fromBinary, toBinary } from "@bufbuild/protobuf";
|
||||
import * as crypto from "node:crypto";
|
||||
import { EventEmitter } from "node:events";
|
||||
import * as net from "node:net";
|
||||
import * as tls from "node:tls";
|
||||
|
||||
import { BaseClient } from "./base_client.js";
|
||||
import { type ParserMap } from "./communicator.js";
|
||||
import { PacketBaseSchema, type PacketBase } from "./packets/message_common_pb.js";
|
||||
import { fromBinary, PacketBaseSchema, toBinary, type PacketBase } from "./packets/message_common_pb.js";
|
||||
|
||||
const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
const MAX_WS_PAYLOAD_SIZE = 64 << 20;
|
||||
|
||||
export type NodeWssConnectOptions = tls.ConnectionOptions;
|
||||
|
||||
type NodeSocket = net.Socket | tls.TLSSocket;
|
||||
|
||||
export class NodeWebSocket {
|
||||
static readonly CONNECTING = 0;
|
||||
static readonly OPEN = 1;
|
||||
static readonly CLOSING = 2;
|
||||
static readonly CLOSED = 3;
|
||||
|
||||
readyState = NodeWebSocket.OPEN;
|
||||
|
||||
private readonly events = new EventEmitter();
|
||||
private readBuf = Buffer.alloc(0);
|
||||
private fragments: Buffer[] = [];
|
||||
private fragmentOpcode: number | null = null;
|
||||
private closeEmitted = false;
|
||||
|
||||
constructor(
|
||||
private readonly socket: NodeSocket,
|
||||
private readonly maskOutgoing: boolean,
|
||||
initialData: Buffer = Buffer.alloc(0),
|
||||
) {
|
||||
this.events.on("error", () => {});
|
||||
|
||||
socket.on("data", (chunk) => {
|
||||
this.onData(chunk);
|
||||
});
|
||||
socket.on("error", (err) => {
|
||||
this.events.emit("error", err);
|
||||
});
|
||||
socket.once("close", () => {
|
||||
this.emitClose();
|
||||
});
|
||||
|
||||
if (initialData.byteLength > 0) {
|
||||
queueMicrotask(() => this.onData(initialData));
|
||||
}
|
||||
}
|
||||
|
||||
on(event: "message", listener: (data: Uint8Array) => void): this;
|
||||
on(event: "close", listener: () => void): this;
|
||||
on(event: "error", listener: (err: Error) => void): this;
|
||||
on(event: string, listener: (...args: any[]) => void): this {
|
||||
this.events.on(event, listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
once(event: "close", listener: () => void): this;
|
||||
once(event: "error", listener: (err: Error) => void): this;
|
||||
once(event: string, listener: (...args: any[]) => void): this {
|
||||
this.events.once(event, listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
off(event: "message", listener: (data: Uint8Array) => void): this;
|
||||
off(event: "close", listener: () => void): this;
|
||||
off(event: "error", listener: (err: Error) => void): this;
|
||||
off(event: string, listener: (...args: any[]) => void): this {
|
||||
this.events.off(event, listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
send(data: Uint8Array, callback?: (err?: Error | null) => void): void {
|
||||
if (this.readyState !== NodeWebSocket.OPEN) {
|
||||
callback?.(new Error("WebSocket is not open"));
|
||||
return;
|
||||
}
|
||||
this.writeFrame(0x2, Buffer.from(data), callback);
|
||||
}
|
||||
|
||||
close(): void {
|
||||
if (this.readyState === NodeWebSocket.CLOSED || this.readyState === NodeWebSocket.CLOSING) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.readyState = NodeWebSocket.CLOSING;
|
||||
this.writeFrame(0x8, Buffer.alloc(0), () => {
|
||||
this.socket.end();
|
||||
});
|
||||
}
|
||||
|
||||
terminate(): void {
|
||||
this.readyState = NodeWebSocket.CLOSED;
|
||||
this.socket.destroy();
|
||||
this.emitClose();
|
||||
}
|
||||
|
||||
private onData(chunk: Buffer): void {
|
||||
if (this.readyState === NodeWebSocket.CLOSED) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.readBuf = Buffer.concat([this.readBuf, chunk]);
|
||||
try {
|
||||
while (this.tryReadFrame()) {
|
||||
continue;
|
||||
}
|
||||
} catch (err) {
|
||||
this.events.emit("error", err instanceof Error ? err : new Error(String(err)));
|
||||
this.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
private tryReadFrame(): boolean {
|
||||
if (this.readBuf.byteLength < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const first = this.readBuf[0]!;
|
||||
const second = this.readBuf[1]!;
|
||||
const fin = (first & 0x80) !== 0;
|
||||
const opcode = first & 0x0f;
|
||||
const masked = (second & 0x80) !== 0;
|
||||
let payloadLength = second & 0x7f;
|
||||
let offset = 2;
|
||||
|
||||
if (payloadLength === 126) {
|
||||
if (this.readBuf.byteLength < offset + 2) {
|
||||
return false;
|
||||
}
|
||||
payloadLength = this.readBuf.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
} else if (payloadLength === 127) {
|
||||
if (this.readBuf.byteLength < offset + 8) {
|
||||
return false;
|
||||
}
|
||||
const longLength = this.readBuf.readBigUInt64BE(offset);
|
||||
if (longLength > BigInt(MAX_WS_PAYLOAD_SIZE)) {
|
||||
throw new Error("WebSocket payload exceeds maximum size");
|
||||
}
|
||||
payloadLength = Number(longLength);
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
const expectedMasked = !this.maskOutgoing;
|
||||
if (masked !== expectedMasked) {
|
||||
throw new Error("invalid WebSocket frame masking");
|
||||
}
|
||||
if (payloadLength > MAX_WS_PAYLOAD_SIZE) {
|
||||
throw new Error("WebSocket payload exceeds maximum size");
|
||||
}
|
||||
|
||||
const maskOffset = masked ? 4 : 0;
|
||||
const frameLength = offset + maskOffset + payloadLength;
|
||||
if (this.readBuf.byteLength < frameLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const mask = masked ? this.readBuf.subarray(offset, offset + 4) : null;
|
||||
offset += maskOffset;
|
||||
const rawPayload = this.readBuf.subarray(offset, offset + payloadLength);
|
||||
const payload = mask === null ? Buffer.from(rawPayload) : unmask(rawPayload, mask);
|
||||
this.readBuf = this.readBuf.subarray(frameLength);
|
||||
|
||||
this.handleFrame(opcode, fin, payload);
|
||||
return true;
|
||||
}
|
||||
|
||||
private handleFrame(opcode: number, fin: boolean, payload: Buffer): void {
|
||||
if (opcode >= 0x8) {
|
||||
if (!fin || payload.byteLength > 125) {
|
||||
throw new Error("invalid WebSocket control frame");
|
||||
}
|
||||
this.handleControlFrame(opcode, payload);
|
||||
return;
|
||||
}
|
||||
|
||||
if (opcode === 0x0) {
|
||||
if (this.fragmentOpcode === null) {
|
||||
throw new Error("unexpected WebSocket continuation frame");
|
||||
}
|
||||
this.fragments.push(payload);
|
||||
if (fin) {
|
||||
const data = Buffer.concat(this.fragments);
|
||||
const fragmentOpcode = this.fragmentOpcode;
|
||||
this.fragments = [];
|
||||
this.fragmentOpcode = null;
|
||||
this.handleMessageFrame(fragmentOpcode, data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (opcode !== 0x1 && opcode !== 0x2) {
|
||||
throw new Error(`unsupported WebSocket opcode: ${opcode}`);
|
||||
}
|
||||
|
||||
if (!fin) {
|
||||
this.fragmentOpcode = opcode;
|
||||
this.fragments = [payload];
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleMessageFrame(opcode, payload);
|
||||
}
|
||||
|
||||
private handleControlFrame(opcode: number, payload: Buffer): void {
|
||||
if (opcode === 0x8) {
|
||||
if (this.readyState === NodeWebSocket.OPEN) {
|
||||
this.readyState = NodeWebSocket.CLOSING;
|
||||
this.writeFrame(0x8, payload, () => {
|
||||
this.socket.end();
|
||||
});
|
||||
} else {
|
||||
this.socket.end();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (opcode === 0x9) {
|
||||
this.writeFrame(0xA, payload);
|
||||
}
|
||||
}
|
||||
|
||||
private handleMessageFrame(opcode: number, payload: Buffer): void {
|
||||
if (opcode !== 0x2) {
|
||||
throw new Error("unsupported WebSocket text frame");
|
||||
}
|
||||
this.events.emit("message", new Uint8Array(payload));
|
||||
}
|
||||
|
||||
private writeFrame(opcode: number, payload: Buffer, callback?: (err?: Error | null) => void): void {
|
||||
if (this.socket.destroyed) {
|
||||
callback?.(new Error("socket is closed"));
|
||||
return;
|
||||
}
|
||||
const frame = encodeFrame(opcode, payload, this.maskOutgoing);
|
||||
this.socket.write(frame, callback);
|
||||
}
|
||||
|
||||
private emitClose(): void {
|
||||
if (this.closeEmitted) {
|
||||
return;
|
||||
}
|
||||
this.closeEmitted = true;
|
||||
this.readyState = NodeWebSocket.CLOSED;
|
||||
this.events.emit("close");
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeWsClient extends BaseClient {
|
||||
private readonly ws: WebSocket;
|
||||
private readonly ws: NodeWebSocket;
|
||||
|
||||
constructor(ws: WebSocket, intervalSec: number, waitSec: number, parserMap: ParserMap) {
|
||||
constructor(ws: NodeWebSocket, intervalSec: number, waitSec: number, parserMap: ParserMap) {
|
||||
super(intervalSec, waitSec, () => closeWebSocket(ws));
|
||||
this.ws = ws;
|
||||
this.initBase(parserMap);
|
||||
|
|
@ -28,7 +271,7 @@ export class NodeWsClient extends BaseClient {
|
|||
async writePacket(base: PacketBase): Promise<void> {
|
||||
const data = toBinary(PacketBaseSchema, base);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
this.ws.send(data, { binary: true }, (err) => {
|
||||
this.ws.send(data, (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
|
|
@ -38,9 +281,9 @@ export class NodeWsClient extends BaseClient {
|
|||
});
|
||||
}
|
||||
|
||||
private onMessage(data: RawData): void {
|
||||
private onMessage(data: Uint8Array): void {
|
||||
try {
|
||||
const base = fromBinary(PacketBaseSchema, toUint8Array(data));
|
||||
const base = fromBinary(PacketBaseSchema, data);
|
||||
this.communicator.onReceivedData(base.typeName, base.data, base.nonce, base.responseNonce);
|
||||
void this.sendHeartbeat();
|
||||
} catch {
|
||||
|
|
@ -57,67 +300,217 @@ export async function connectNodeWs(
|
|||
waitSec: number,
|
||||
parserMap: ParserMap,
|
||||
): Promise<NodeWsClient> {
|
||||
return new Promise<NodeWsClient>((resolve, reject) => {
|
||||
const ws = new WebSocket(`ws://${host}:${port}${path}`);
|
||||
const onOpen = () => {
|
||||
cleanup();
|
||||
resolve(new NodeWsClient(ws, intervalSec, waitSec, parserMap));
|
||||
};
|
||||
const onError = (err: Error) => {
|
||||
cleanup();
|
||||
reject(err);
|
||||
};
|
||||
const cleanup = () => {
|
||||
ws.off("open", onOpen);
|
||||
ws.off("error", onError);
|
||||
};
|
||||
|
||||
ws.once("open", onOpen);
|
||||
ws.once("error", onError);
|
||||
});
|
||||
const socket = await connectPlainSocket(host, port);
|
||||
return connectNodeWebSocket(socket, host, port, path, intervalSec, waitSec, parserMap);
|
||||
}
|
||||
|
||||
export async function connectNodeWss(
|
||||
host: string,
|
||||
port: number,
|
||||
path: string,
|
||||
wsOptions: ClientOptions,
|
||||
wsOptions: NodeWssConnectOptions,
|
||||
intervalSec: number,
|
||||
waitSec: number,
|
||||
parserMap: ParserMap,
|
||||
): Promise<NodeWsClient> {
|
||||
return new Promise<NodeWsClient>((resolve, reject) => {
|
||||
const ws = new WebSocket(`wss://${host}:${port}${path}`, wsOptions);
|
||||
const onOpen = () => {
|
||||
const socket = await connectTlsSocket(host, port, wsOptions);
|
||||
return connectNodeWebSocket(socket, host, port, path, intervalSec, waitSec, parserMap);
|
||||
}
|
||||
|
||||
export function createWebSocketAccept(key: string): string {
|
||||
return crypto.createHash("sha1").update(`${key}${WS_GUID}`).digest("base64");
|
||||
}
|
||||
|
||||
async function connectPlainSocket(host: string, port: number): Promise<net.Socket> {
|
||||
return new Promise<net.Socket>((resolve, reject) => {
|
||||
const socket = net.createConnection({ host, port });
|
||||
const cleanup = () => {
|
||||
socket.off("connect", onConnect);
|
||||
socket.off("error", onError);
|
||||
};
|
||||
const onConnect = () => {
|
||||
cleanup();
|
||||
resolve(new NodeWsClient(ws, intervalSec, waitSec, parserMap));
|
||||
resolve(socket);
|
||||
};
|
||||
const onError = (err: Error) => {
|
||||
cleanup();
|
||||
reject(err);
|
||||
};
|
||||
const cleanup = () => {
|
||||
ws.off("open", onOpen);
|
||||
ws.off("error", onError);
|
||||
};
|
||||
|
||||
ws.once("open", onOpen);
|
||||
ws.once("error", onError);
|
||||
socket.once("connect", onConnect);
|
||||
socket.once("error", onError);
|
||||
});
|
||||
}
|
||||
|
||||
function toUint8Array(data: RawData): Uint8Array {
|
||||
if (Array.isArray(data)) {
|
||||
return Buffer.concat(data.map((chunk) => Buffer.from(chunk)));
|
||||
}
|
||||
if (data instanceof ArrayBuffer) {
|
||||
return new Uint8Array(data);
|
||||
}
|
||||
return new Uint8Array(data);
|
||||
async function connectTlsSocket(
|
||||
host: string,
|
||||
port: number,
|
||||
options: tls.ConnectionOptions,
|
||||
): Promise<tls.TLSSocket> {
|
||||
return new Promise<tls.TLSSocket>((resolve, reject) => {
|
||||
const servername = options.servername ?? (net.isIP(host) === 0 ? host : undefined);
|
||||
const socket = tls.connect({
|
||||
...options,
|
||||
host,
|
||||
port,
|
||||
servername,
|
||||
});
|
||||
const cleanup = () => {
|
||||
socket.off("secureConnect", onSecureConnect);
|
||||
socket.off("error", onError);
|
||||
};
|
||||
const onSecureConnect = () => {
|
||||
cleanup();
|
||||
resolve(socket);
|
||||
};
|
||||
const onError = (err: Error) => {
|
||||
cleanup();
|
||||
reject(err);
|
||||
};
|
||||
|
||||
socket.once("secureConnect", onSecureConnect);
|
||||
socket.once("error", onError);
|
||||
});
|
||||
}
|
||||
|
||||
async function closeWebSocket(ws: WebSocket): Promise<void> {
|
||||
if (ws.readyState === WebSocket.CLOSED) {
|
||||
async function connectNodeWebSocket(
|
||||
socket: NodeSocket,
|
||||
host: string,
|
||||
port: number,
|
||||
path: string,
|
||||
intervalSec: number,
|
||||
waitSec: number,
|
||||
parserMap: ParserMap,
|
||||
): Promise<NodeWsClient> {
|
||||
return new Promise<NodeWsClient>((resolve, reject) => {
|
||||
const key = crypto.randomBytes(16).toString("base64");
|
||||
let response = Buffer.alloc(0);
|
||||
|
||||
const cleanup = () => {
|
||||
socket.off("data", onData);
|
||||
socket.off("error", onError);
|
||||
socket.off("close", onClose);
|
||||
};
|
||||
const fail = (err: Error) => {
|
||||
cleanup();
|
||||
socket.destroy();
|
||||
reject(err);
|
||||
};
|
||||
const onError = (err: Error) => {
|
||||
fail(err);
|
||||
};
|
||||
const onClose = () => {
|
||||
fail(new Error("socket closed during WebSocket handshake"));
|
||||
};
|
||||
const onData = (chunk: Buffer) => {
|
||||
response = Buffer.concat([response, chunk]);
|
||||
const headerEnd = response.indexOf("\r\n\r\n");
|
||||
if (headerEnd < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const header = response.subarray(0, headerEnd).toString("latin1");
|
||||
const remaining = response.subarray(headerEnd + 4);
|
||||
try {
|
||||
verifyHandshakeResponse(header, key);
|
||||
} catch (err) {
|
||||
fail(err instanceof Error ? err : new Error(String(err)));
|
||||
return;
|
||||
}
|
||||
|
||||
cleanup();
|
||||
resolve(new NodeWsClient(new NodeWebSocket(socket, true, remaining), intervalSec, waitSec, parserMap));
|
||||
};
|
||||
|
||||
socket.on("data", onData);
|
||||
socket.once("error", onError);
|
||||
socket.once("close", onClose);
|
||||
socket.write(buildHandshakeRequest(host, port, path, key));
|
||||
});
|
||||
}
|
||||
|
||||
function buildHandshakeRequest(host: string, port: number, path: string, key: string): string {
|
||||
const requestPath = path.startsWith("/") ? path : `/${path}`;
|
||||
return [
|
||||
`GET ${requestPath} HTTP/1.1`,
|
||||
`Host: ${host}:${port}`,
|
||||
"Upgrade: websocket",
|
||||
"Connection: Upgrade",
|
||||
`Sec-WebSocket-Key: ${key}`,
|
||||
"Sec-WebSocket-Version: 13",
|
||||
"\r\n",
|
||||
].join("\r\n");
|
||||
}
|
||||
|
||||
function verifyHandshakeResponse(header: string, key: string): void {
|
||||
const lines = header.split("\r\n");
|
||||
const statusLine = lines.shift() ?? "";
|
||||
if (!/^HTTP\/1\.[01] 101(?: |$)/.test(statusLine)) {
|
||||
throw new Error(`invalid WebSocket handshake status: ${statusLine}`);
|
||||
}
|
||||
|
||||
const headers = new Map<string, string>();
|
||||
for (const line of lines) {
|
||||
const index = line.indexOf(":");
|
||||
if (index <= 0) {
|
||||
continue;
|
||||
}
|
||||
headers.set(line.slice(0, index).trim().toLowerCase(), line.slice(index + 1).trim());
|
||||
}
|
||||
|
||||
const accept = headers.get("sec-websocket-accept");
|
||||
if (accept !== createWebSocketAccept(key)) {
|
||||
throw new Error("invalid WebSocket handshake accept header");
|
||||
}
|
||||
}
|
||||
|
||||
function encodeFrame(opcode: number, payload: Buffer, mask: boolean): Buffer {
|
||||
const payloadLength = payload.byteLength;
|
||||
const lengthBytes = payloadLength < 126 ? 0 : payloadLength <= 0xffff ? 2 : 8;
|
||||
const maskBytes = mask ? 4 : 0;
|
||||
const header = Buffer.allocUnsafe(2 + lengthBytes + maskBytes);
|
||||
let offset = 0;
|
||||
|
||||
header[offset] = 0x80 | opcode;
|
||||
offset += 1;
|
||||
if (payloadLength < 126) {
|
||||
header[offset] = (mask ? 0x80 : 0) | payloadLength;
|
||||
offset += 1;
|
||||
} else if (payloadLength <= 0xffff) {
|
||||
header[offset] = (mask ? 0x80 : 0) | 126;
|
||||
offset += 1;
|
||||
header.writeUInt16BE(payloadLength, offset);
|
||||
offset += 2;
|
||||
} else {
|
||||
header[offset] = (mask ? 0x80 : 0) | 127;
|
||||
offset += 1;
|
||||
header.writeBigUInt64BE(BigInt(payloadLength), offset);
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
if (!mask) {
|
||||
return Buffer.concat([header, payload]);
|
||||
}
|
||||
|
||||
const maskingKey = crypto.randomBytes(4);
|
||||
maskingKey.copy(header, offset);
|
||||
return Buffer.concat([header, maskPayload(payload, maskingKey)]);
|
||||
}
|
||||
|
||||
function unmask(payload: Buffer, mask: Buffer): Buffer {
|
||||
return maskPayload(payload, mask);
|
||||
}
|
||||
|
||||
function maskPayload(payload: Buffer, mask: Buffer): Buffer {
|
||||
const result = Buffer.allocUnsafe(payload.byteLength);
|
||||
for (let i = 0; i < payload.byteLength; i += 1) {
|
||||
result[i] = payload[i]! ^ mask[i % 4]!;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function closeWebSocket(ws: NodeWebSocket): Promise<void> {
|
||||
if (ws.readyState === NodeWebSocket.CLOSED) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -139,7 +532,7 @@ async function closeWebSocket(ws: WebSocket): Promise<void> {
|
|||
ws.once("close", onDone);
|
||||
ws.once("error", onDone);
|
||||
|
||||
if (ws.readyState === WebSocket.CLOSING) {
|
||||
if (ws.readyState === NodeWebSocket.CLOSING) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import * as http from "node:http";
|
||||
import * as https from "node:https";
|
||||
import * as net from "node:net";
|
||||
|
||||
import { type Message } from "@bufbuild/protobuf";
|
||||
import WebSocket, { WebSocketServer } from "ws";
|
||||
|
||||
import { NodeWsClient } from "./node_ws_client.js";
|
||||
import { NodeWebSocket, NodeWsClient, createWebSocketAccept } from "./node_ws_client.js";
|
||||
import { type Message } from "./packets/message_common_pb.js";
|
||||
|
||||
export class NodeWsServer {
|
||||
private wss: WebSocketServer | null = null;
|
||||
private httpsServer: https.Server | null = null;
|
||||
private server: http.Server | https.Server | null = null;
|
||||
private readonly clients = new Set<NodeWsClient>();
|
||||
|
||||
onClientConnected: (client: NodeWsClient) => void = () => {};
|
||||
|
|
@ -16,12 +15,12 @@ export class NodeWsServer {
|
|||
private readonly host: string,
|
||||
private readonly listenPort: number,
|
||||
private readonly path: string,
|
||||
private readonly newClient: (ws: WebSocket) => NodeWsClient,
|
||||
private readonly newClient: (ws: NodeWebSocket) => NodeWsClient,
|
||||
private readonly tlsOptions?: https.ServerOptions,
|
||||
) {}
|
||||
|
||||
get port(): number {
|
||||
const address = this.httpsServer?.address() ?? this.wss?.address();
|
||||
const address = this.server?.address();
|
||||
if (address !== undefined && address !== null && typeof address === "object") {
|
||||
return address.port;
|
||||
}
|
||||
|
|
@ -29,73 +28,46 @@ export class NodeWsServer {
|
|||
}
|
||||
|
||||
start(): Promise<void> {
|
||||
if (this.wss !== null) {
|
||||
if (this.server !== null) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const httpsServer = this.tlsOptions ? https.createServer(this.tlsOptions) : null;
|
||||
const wss = httpsServer
|
||||
? new WebSocketServer({ server: httpsServer, path: this.path })
|
||||
: new WebSocketServer({
|
||||
host: this.host,
|
||||
path: this.path,
|
||||
port: this.listenPort,
|
||||
});
|
||||
const server = this.tlsOptions ? https.createServer(this.tlsOptions) : http.createServer();
|
||||
const onError = (err: Error) => {
|
||||
cleanup();
|
||||
reject(err);
|
||||
};
|
||||
const onListening = () => {
|
||||
cleanup();
|
||||
this.wss = wss;
|
||||
this.httpsServer = httpsServer;
|
||||
this.server = server;
|
||||
resolve();
|
||||
};
|
||||
const cleanup = () => {
|
||||
wss.off("error", onError);
|
||||
wss.off("listening", onListening);
|
||||
httpsServer?.off("error", onError);
|
||||
httpsServer?.off("listening", onListening);
|
||||
server.off("error", onError);
|
||||
server.off("listening", onListening);
|
||||
};
|
||||
|
||||
wss.once("error", onError);
|
||||
wss.on("connection", (ws) => this.handleConnection(ws));
|
||||
if (httpsServer !== null) {
|
||||
httpsServer.once("error", onError);
|
||||
httpsServer.once("listening", onListening);
|
||||
httpsServer.listen(this.listenPort, this.host);
|
||||
} else {
|
||||
wss.once("listening", onListening);
|
||||
}
|
||||
server.on("upgrade", (req, socket, head) => {
|
||||
this.handleUpgrade(req, socket as net.Socket, head);
|
||||
});
|
||||
server.once("error", onError);
|
||||
server.once("listening", onListening);
|
||||
server.listen(this.listenPort, this.host);
|
||||
});
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (this.wss === null) {
|
||||
if (this.server === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wss = this.wss;
|
||||
const httpsServer = this.httpsServer;
|
||||
this.wss = null;
|
||||
this.httpsServer = null;
|
||||
const server = this.server;
|
||||
this.server = null;
|
||||
const clients = [...this.clients];
|
||||
this.clients.clear();
|
||||
const closeServer = new Promise<void>((resolve, reject) => {
|
||||
wss.close((err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
const closeHttpsServer =
|
||||
httpsServer === null
|
||||
? Promise.resolve()
|
||||
: new Promise<void>((resolve, reject) => {
|
||||
httpsServer.close((err) => {
|
||||
server.close((err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
|
|
@ -106,7 +78,6 @@ export class NodeWsServer {
|
|||
|
||||
await Promise.allSettled(clients.map(async (client) => client.close()));
|
||||
await closeServer;
|
||||
await closeHttpsServer;
|
||||
}
|
||||
|
||||
async broadcast(msg: Message): Promise<void> {
|
||||
|
|
@ -117,7 +88,53 @@ export class NodeWsServer {
|
|||
this.clients.delete(client);
|
||||
}
|
||||
|
||||
private handleConnection(ws: WebSocket): void {
|
||||
private handleUpgrade(req: http.IncomingMessage, socket: net.Socket, head: Buffer): void {
|
||||
const key = this.validateUpgrade(req);
|
||||
if (key === null) {
|
||||
socket.write("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
socket.write(
|
||||
[
|
||||
"HTTP/1.1 101 Switching Protocols",
|
||||
"Upgrade: websocket",
|
||||
"Connection: Upgrade",
|
||||
`Sec-WebSocket-Accept: ${createWebSocketAccept(key)}`,
|
||||
"\r\n",
|
||||
].join("\r\n"),
|
||||
);
|
||||
this.handleConnection(new NodeWebSocket(socket, false, head));
|
||||
}
|
||||
|
||||
private validateUpgrade(req: http.IncomingMessage): string | null {
|
||||
if (!this.pathMatches(req)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const upgrade = String(req.headers.upgrade ?? "").toLowerCase();
|
||||
const connection = String(req.headers.connection ?? "").toLowerCase();
|
||||
const version = String(req.headers["sec-websocket-version"] ?? "");
|
||||
const keyHeader = req.headers["sec-websocket-key"];
|
||||
const key = Array.isArray(keyHeader) ? keyHeader[0] : keyHeader;
|
||||
|
||||
if (upgrade !== "websocket" || !connection.split(",").map((part) => part.trim()).includes("upgrade")) {
|
||||
return null;
|
||||
}
|
||||
if (version !== "13" || typeof key !== "string" || key.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
private pathMatches(req: http.IncomingMessage): boolean {
|
||||
const requestUrl = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
||||
return requestUrl.pathname === this.path;
|
||||
}
|
||||
|
||||
private handleConnection(ws: NodeWebSocket): void {
|
||||
const client = this.newClient(ws);
|
||||
this.clients.add(client);
|
||||
client.addDisconnectListener(() => {
|
||||
|
|
|
|||
|
|
@ -1,81 +1,343 @@
|
|||
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts"
|
||||
// @generated from file message_common.proto (syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2";
|
||||
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
export interface Message<TypeName extends string = string> {
|
||||
readonly $typeName: TypeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the file message_common.proto.
|
||||
*/
|
||||
export const file_message_common: GenFile = /*@__PURE__*/
|
||||
fileDesc("ChRtZXNzYWdlX2NvbW1vbi5wcm90byJSCgpQYWNrZXRCYXNlEhAKCHR5cGVOYW1lGAEgASgJEg0KBW5vbmNlGAIgASgFEgwKBGRhdGEYAyABKAwSFQoNcmVzcG9uc2VOb25jZRgEIAEoBSILCglIZWFydEJlYXQiKgoIVGVzdERhdGESDQoFaW5kZXgYASABKAUSDwoHbWVzc2FnZRgCIAEoCWIGcHJvdG8z");
|
||||
export type MessageInit<T extends Message> = Partial<Omit<T, "$typeName">>;
|
||||
|
||||
export interface MessageType<T extends Message = Message> {
|
||||
readonly typeName: T["$typeName"];
|
||||
create(value?: MessageInit<T>): T;
|
||||
fromBinary(data: Uint8Array): T;
|
||||
toBinary(message: T): Uint8Array;
|
||||
}
|
||||
|
||||
export const file_message_common = {
|
||||
name: "message_common.proto",
|
||||
messages: ["PacketBase", "HeartBeat", "TestData"],
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* @generated from message PacketBase
|
||||
*/
|
||||
export type PacketBase = Message<"PacketBase"> & {
|
||||
/**
|
||||
* @generated from field: string typeName = 1;
|
||||
*/
|
||||
typeName: string;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 nonce = 2;
|
||||
*/
|
||||
nonce: number;
|
||||
|
||||
/**
|
||||
* @generated from field: bytes data = 3;
|
||||
*/
|
||||
data: Uint8Array;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 responseNonce = 4;
|
||||
*/
|
||||
responseNonce: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message PacketBase.
|
||||
* Use `create(PacketBaseSchema)` to create a new message.
|
||||
*/
|
||||
export const PacketBaseSchema: GenMessage<PacketBase> = /*@__PURE__*/
|
||||
messageDesc(file_message_common, 0);
|
||||
export type HeartBeat = Message<"HeartBeat">;
|
||||
|
||||
/**
|
||||
* @generated from message HeartBeat
|
||||
*/
|
||||
export type HeartBeat = Message<"HeartBeat"> & {
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message HeartBeat.
|
||||
* Use `create(HeartBeatSchema)` to create a new message.
|
||||
*/
|
||||
export const HeartBeatSchema: GenMessage<HeartBeat> = /*@__PURE__*/
|
||||
messageDesc(file_message_common, 1);
|
||||
|
||||
/**
|
||||
* @generated from message TestData
|
||||
*/
|
||||
export type TestData = Message<"TestData"> & {
|
||||
/**
|
||||
* @generated from field: int32 index = 1;
|
||||
*/
|
||||
index: number;
|
||||
|
||||
/**
|
||||
* @generated from field: string message = 2;
|
||||
*/
|
||||
message: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message TestData.
|
||||
* Use `create(TestDataSchema)` to create a new message.
|
||||
*/
|
||||
export const TestDataSchema: GenMessage<TestData> = /*@__PURE__*/
|
||||
messageDesc(file_message_common, 2);
|
||||
export const PacketBaseSchema: MessageType<PacketBase> = {
|
||||
typeName: "PacketBase",
|
||||
create(value = {}) {
|
||||
return {
|
||||
$typeName: "PacketBase",
|
||||
typeName: value.typeName ?? "",
|
||||
nonce: value.nonce ?? 0,
|
||||
data: cloneBytes(value.data),
|
||||
responseNonce: value.responseNonce ?? 0,
|
||||
};
|
||||
},
|
||||
fromBinary(data) {
|
||||
return decodePacketBase(data);
|
||||
},
|
||||
toBinary(message) {
|
||||
return encodePacketBase(message);
|
||||
},
|
||||
};
|
||||
|
||||
export const HeartBeatSchema: MessageType<HeartBeat> = {
|
||||
typeName: "HeartBeat",
|
||||
create() {
|
||||
return { $typeName: "HeartBeat" };
|
||||
},
|
||||
fromBinary(data) {
|
||||
skipUnknownMessage(data);
|
||||
return this.create();
|
||||
},
|
||||
toBinary() {
|
||||
return new Uint8Array();
|
||||
},
|
||||
};
|
||||
|
||||
export const TestDataSchema: MessageType<TestData> = {
|
||||
typeName: "TestData",
|
||||
create(value = {}) {
|
||||
return {
|
||||
$typeName: "TestData",
|
||||
index: value.index ?? 0,
|
||||
message: value.message ?? "",
|
||||
};
|
||||
},
|
||||
fromBinary(data) {
|
||||
return decodeTestData(data);
|
||||
},
|
||||
toBinary(message) {
|
||||
return encodeTestData(message);
|
||||
},
|
||||
};
|
||||
|
||||
export function create<T extends Message>(schema: MessageType<T>, value?: MessageInit<T>): T {
|
||||
return schema.create(value);
|
||||
}
|
||||
|
||||
export function fromBinary<T extends Message>(schema: MessageType<T>, data: Uint8Array): T {
|
||||
return schema.fromBinary(data);
|
||||
}
|
||||
|
||||
export function toBinary<T extends Message>(schema: MessageType<T>, message: T): Uint8Array {
|
||||
return schema.toBinary(message);
|
||||
}
|
||||
|
||||
function encodePacketBase(message: PacketBase): Uint8Array {
|
||||
const chunks: Uint8Array[] = [];
|
||||
if (message.typeName !== "") {
|
||||
writeString(chunks, 1, message.typeName);
|
||||
}
|
||||
if (message.nonce !== 0) {
|
||||
writeInt32(chunks, 2, message.nonce);
|
||||
}
|
||||
if (message.data.byteLength > 0) {
|
||||
writeBytes(chunks, 3, message.data);
|
||||
}
|
||||
if (message.responseNonce !== 0) {
|
||||
writeInt32(chunks, 4, message.responseNonce);
|
||||
}
|
||||
return concat(chunks);
|
||||
}
|
||||
|
||||
function decodePacketBase(data: Uint8Array): PacketBase {
|
||||
const reader = new BinaryReader(data);
|
||||
const message = PacketBaseSchema.create();
|
||||
|
||||
while (!reader.done()) {
|
||||
const tag = reader.readVarintNumber();
|
||||
const fieldNumber = tag >>> 3;
|
||||
const wireType = tag & 7;
|
||||
switch (fieldNumber) {
|
||||
case 1:
|
||||
assertWireType(wireType, 2);
|
||||
message.typeName = reader.readString();
|
||||
break;
|
||||
case 2:
|
||||
assertWireType(wireType, 0);
|
||||
message.nonce = reader.readInt32();
|
||||
break;
|
||||
case 3:
|
||||
assertWireType(wireType, 2);
|
||||
message.data = reader.readBytes();
|
||||
break;
|
||||
case 4:
|
||||
assertWireType(wireType, 0);
|
||||
message.responseNonce = reader.readInt32();
|
||||
break;
|
||||
default:
|
||||
reader.skip(wireType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
function encodeTestData(message: TestData): Uint8Array {
|
||||
const chunks: Uint8Array[] = [];
|
||||
if (message.index !== 0) {
|
||||
writeInt32(chunks, 1, message.index);
|
||||
}
|
||||
if (message.message !== "") {
|
||||
writeString(chunks, 2, message.message);
|
||||
}
|
||||
return concat(chunks);
|
||||
}
|
||||
|
||||
function decodeTestData(data: Uint8Array): TestData {
|
||||
const reader = new BinaryReader(data);
|
||||
const message = TestDataSchema.create();
|
||||
|
||||
while (!reader.done()) {
|
||||
const tag = reader.readVarintNumber();
|
||||
const fieldNumber = tag >>> 3;
|
||||
const wireType = tag & 7;
|
||||
switch (fieldNumber) {
|
||||
case 1:
|
||||
assertWireType(wireType, 0);
|
||||
message.index = reader.readInt32();
|
||||
break;
|
||||
case 2:
|
||||
assertWireType(wireType, 2);
|
||||
message.message = reader.readString();
|
||||
break;
|
||||
default:
|
||||
reader.skip(wireType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
function skipUnknownMessage(data: Uint8Array): void {
|
||||
const reader = new BinaryReader(data);
|
||||
while (!reader.done()) {
|
||||
const tag = reader.readVarintNumber();
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
}
|
||||
|
||||
function writeInt32(chunks: Uint8Array[], fieldNumber: number, value: number): void {
|
||||
chunks.push(encodeTag(fieldNumber, 0));
|
||||
chunks.push(encodeInt32(value));
|
||||
}
|
||||
|
||||
function writeString(chunks: Uint8Array[], fieldNumber: number, value: string): void {
|
||||
writeLengthDelimited(chunks, fieldNumber, textEncoder.encode(value));
|
||||
}
|
||||
|
||||
function writeBytes(chunks: Uint8Array[], fieldNumber: number, value: Uint8Array): void {
|
||||
writeLengthDelimited(chunks, fieldNumber, value);
|
||||
}
|
||||
|
||||
function writeLengthDelimited(chunks: Uint8Array[], fieldNumber: number, value: Uint8Array): void {
|
||||
chunks.push(encodeTag(fieldNumber, 2));
|
||||
chunks.push(encodeVarint(BigInt(value.byteLength)));
|
||||
chunks.push(value);
|
||||
}
|
||||
|
||||
function encodeTag(fieldNumber: number, wireType: number): Uint8Array {
|
||||
return encodeVarint(BigInt((fieldNumber << 3) | wireType));
|
||||
}
|
||||
|
||||
function encodeInt32(value: number): Uint8Array {
|
||||
if (!Number.isInteger(value) || value < -2147483648 || value > 2147483647) {
|
||||
throw new Error(`int32 out of range: ${value}`);
|
||||
}
|
||||
const normalized = value | 0;
|
||||
const varint = normalized < 0 ? BigInt.asUintN(64, BigInt(normalized)) : BigInt(normalized);
|
||||
return encodeVarint(varint);
|
||||
}
|
||||
|
||||
function encodeVarint(value: bigint): Uint8Array {
|
||||
if (value < 0n) {
|
||||
throw new Error("varint cannot be negative");
|
||||
}
|
||||
const bytes: number[] = [];
|
||||
let current = value;
|
||||
while (current > 0x7fn) {
|
||||
bytes.push(Number((current & 0x7fn) | 0x80n));
|
||||
current >>= 7n;
|
||||
}
|
||||
bytes.push(Number(current));
|
||||
return new Uint8Array(bytes);
|
||||
}
|
||||
|
||||
class BinaryReader {
|
||||
private offset = 0;
|
||||
|
||||
constructor(private readonly data: Uint8Array) {}
|
||||
|
||||
done(): boolean {
|
||||
return this.offset >= this.data.byteLength;
|
||||
}
|
||||
|
||||
readInt32(): number {
|
||||
return Number(BigInt.asIntN(32, this.readVarintBig()));
|
||||
}
|
||||
|
||||
readVarintNumber(): number {
|
||||
const value = this.readVarintBig();
|
||||
if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
|
||||
throw new Error("varint exceeds safe integer range");
|
||||
}
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
readString(): string {
|
||||
return textDecoder.decode(this.readBytes());
|
||||
}
|
||||
|
||||
readBytes(): Uint8Array {
|
||||
const length = this.readVarintNumber();
|
||||
this.require(length);
|
||||
const bytes = this.data.subarray(this.offset, this.offset + length);
|
||||
this.offset += length;
|
||||
return new Uint8Array(bytes);
|
||||
}
|
||||
|
||||
skip(wireType: number): void {
|
||||
switch (wireType) {
|
||||
case 0:
|
||||
this.readVarintBig();
|
||||
return;
|
||||
case 1:
|
||||
this.require(8);
|
||||
this.offset += 8;
|
||||
return;
|
||||
case 2: {
|
||||
const length = this.readVarintNumber();
|
||||
this.require(length);
|
||||
this.offset += length;
|
||||
return;
|
||||
}
|
||||
case 5:
|
||||
this.require(4);
|
||||
this.offset += 4;
|
||||
return;
|
||||
default:
|
||||
throw new Error(`unsupported protobuf wire type: ${wireType}`);
|
||||
}
|
||||
}
|
||||
|
||||
private readVarintBig(): bigint {
|
||||
let shift = 0n;
|
||||
let result = 0n;
|
||||
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
this.require(1);
|
||||
const byte = this.data[this.offset]!;
|
||||
this.offset += 1;
|
||||
result |= BigInt(byte & 0x7f) << shift;
|
||||
if ((byte & 0x80) === 0) {
|
||||
return result;
|
||||
}
|
||||
shift += 7n;
|
||||
}
|
||||
|
||||
throw new Error("invalid protobuf varint");
|
||||
}
|
||||
|
||||
private require(length: number): void {
|
||||
if (length < 0 || this.offset + length > this.data.byteLength) {
|
||||
throw new Error("truncated protobuf message");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertWireType(actual: number, expected: number): void {
|
||||
if (actual !== expected) {
|
||||
throw new Error(`unexpected protobuf wire type ${actual}, expected ${expected}`);
|
||||
}
|
||||
}
|
||||
|
||||
function concat(chunks: Uint8Array[]): Uint8Array {
|
||||
const length = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
|
||||
const result = new Uint8Array(length);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
result.set(chunk, offset);
|
||||
offset += chunk.byteLength;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function cloneBytes(value: Uint8Array | undefined): Uint8Array {
|
||||
return value === undefined ? new Uint8Array() : new Uint8Array(value);
|
||||
}
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
const textDecoder = new TextDecoder();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import * as net from "node:net";
|
||||
import * as tls from "node:tls";
|
||||
|
||||
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";
|
||||
import { fromBinary, PacketBaseSchema, toBinary, type PacketBase } from "./packets/message_common_pb.js";
|
||||
|
||||
export const MAX_PACKET_SIZE = 64 << 20;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import * as net from "node:net";
|
||||
import * as tls from "node:tls";
|
||||
|
||||
import { type Message } from "@bufbuild/protobuf";
|
||||
|
||||
import { type Message } from "./packets/message_common_pb.js";
|
||||
import { TcpClient } from "./tcp_client.js";
|
||||
|
||||
export class TcpServer {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { create, toBinary } from "@bufbuild/protobuf";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { BaseClient } from "../src/base_client.js";
|
||||
import { parserFromSchema } from "../src/communicator.js";
|
||||
import {
|
||||
create,
|
||||
HeartBeatSchema,
|
||||
TestDataSchema,
|
||||
toBinary,
|
||||
type PacketBase,
|
||||
} from "../src/packets/message_common_pb.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
|
||||
import {
|
||||
|
|
@ -8,8 +7,11 @@ import {
|
|||
sendRequestTyped,
|
||||
} from "../src/communicator.js";
|
||||
import {
|
||||
create,
|
||||
fromBinary,
|
||||
PacketBaseSchema,
|
||||
TestDataSchema,
|
||||
toBinary,
|
||||
type TestData,
|
||||
} from "../src/packets/message_common_pb.js";
|
||||
import { BrowserWsClient, connectBrowserWs } from "../src/browser_ws_client.js";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { create, toBinary } from "@bufbuild/protobuf";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import {
|
||||
|
|
@ -8,8 +7,10 @@ import {
|
|||
parserFromSchema,
|
||||
} from "../src/communicator.js";
|
||||
import {
|
||||
create,
|
||||
PacketBaseSchema,
|
||||
TestDataSchema,
|
||||
toBinary,
|
||||
type PacketBase,
|
||||
} from "../src/packets/message_common_pb.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import * as fs from "node:fs";
|
|||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
|
||||
import {
|
||||
|
|
@ -11,7 +10,7 @@ import {
|
|||
parserFromSchema,
|
||||
sendRequestTyped,
|
||||
} from "../src/communicator.js";
|
||||
import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
|
||||
import { create, TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
|
||||
import { connectTcp, connectTcpTls, TcpClient } from "../src/tcp_client.js";
|
||||
import { TcpServer } from "../src/tcp_server.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import * as fs from "node:fs";
|
|||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
|
||||
import {
|
||||
|
|
@ -11,7 +10,7 @@ import {
|
|||
parserFromSchema,
|
||||
sendRequestTyped,
|
||||
} from "../src/communicator.js";
|
||||
import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
|
||||
import { create, TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
|
||||
import { connectNodeWs, connectNodeWss, NodeWsClient } from "../src/node_ws_client.js";
|
||||
import { NodeWsServer } from "../src/node_ws_server.js";
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue