@couch-kit/host 0.5.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -3
- package/package.json +14 -20
- package/src/buffer-utils.ts +53 -0
- package/src/declarations.d.ts +16 -0
- package/src/event-emitter.ts +4 -1
- package/src/network.ts +2 -6
- package/src/server.ts +8 -5
- package/src/websocket.ts +37 -69
package/README.md
CHANGED
|
@@ -22,7 +22,28 @@ The server-side library for React Native TV applications. This package turns you
|
|
|
22
22
|
bun add @couch-kit/host
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Then install the required peer dependencies:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx expo install expo-file-system expo-network
|
|
29
|
+
bun add react-native-tcp-socket
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
> **Note:** This library requires Expo modules (`expo-file-system`, `expo-network`) and `react-native-tcp-socket` as peer dependencies. These must be installed in your consumer app. React Native's autolinking will handle native setup automatically.
|
|
33
|
+
|
|
34
|
+
## Compatibility
|
|
35
|
+
|
|
36
|
+
| Dependency | Minimum Version |
|
|
37
|
+
| ---------------------------- | --------------- |
|
|
38
|
+
| `react` | `>= 18.2.0` |
|
|
39
|
+
| `react-native` | `>= 0.72.0` |
|
|
40
|
+
| `react-native-nitro-modules` | `>= 0.33.0` |
|
|
41
|
+
| `expo` | `>= 51.0.0` |
|
|
42
|
+
| `expo-file-system` | `>= 17.0.0` |
|
|
43
|
+
| `expo-network` | `>= 7.0.0` |
|
|
44
|
+
| `react-native-tcp-socket` | `>= 6.0.0` |
|
|
45
|
+
|
|
46
|
+
> **New Architecture:** This package supports React Native's New Architecture (Fabric/TurboModules) via React Native 0.83+.
|
|
26
47
|
|
|
27
48
|
## Usage
|
|
28
49
|
|
|
@@ -36,7 +57,7 @@ Config:
|
|
|
36
57
|
- `reducer`: `(state, action) => state` (shared reducer)
|
|
37
58
|
- `port?`: HTTP static server port (default `8080`)
|
|
38
59
|
- `wsPort?`: WebSocket game server port (default `8082`)
|
|
39
|
-
- `staticDir?`: absolute path to the directory of static files to serve.
|
|
60
|
+
- `staticDir?`: absolute path to the directory of static files to serve. **Required on Android** — APK assets live inside a zip archive and cannot be served directly, so use this to point to a writable filesystem path where you've extracted the `www/` assets at runtime. On iOS, defaults to the bundle directory + `/www`.
|
|
40
61
|
- `devMode?`: if true, do not start the TV static file server; instead point phones at `devServerUrl`
|
|
41
62
|
- `devServerUrl?`: URL of your laptop dev server (e.g. `http://192.168.1.50:5173`)
|
|
42
63
|
- `debug?`: enable verbose logs
|
|
@@ -148,6 +169,6 @@ Important: when the controller is served from the laptop, the client-side hook c
|
|
|
148
169
|
|
|
149
170
|
## Bundling / Assets
|
|
150
171
|
|
|
151
|
-
In production, the host serves static controller assets from
|
|
172
|
+
In production, the host serves static controller assets from the iOS bundle directory + `/www` by default. On Android, `staticDir` must be provided since bundle assets live inside the APK.
|
|
152
173
|
|
|
153
174
|
The CLI `couch-kit bundle` copies your web build output into `android/app/src/main/assets/www` (default). Ensure your app packaging makes those assets available under the expected `www` folder.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@couch-kit/host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "React Native host for local multiplayer party games on Android TV — WebSocket server, state management, and static file serving",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,43 +33,37 @@
|
|
|
33
33
|
"files": [
|
|
34
34
|
"src",
|
|
35
35
|
"lib",
|
|
36
|
-
"android",
|
|
37
|
-
"ios",
|
|
38
|
-
"cpp",
|
|
39
|
-
"*.podspec",
|
|
40
36
|
"!lib/typescript/example",
|
|
41
|
-
"!android/build",
|
|
42
|
-
"!ios/build",
|
|
43
37
|
"!**/__tests__",
|
|
44
38
|
"!**/__fixtures__",
|
|
45
39
|
"!**/__mocks__"
|
|
46
40
|
],
|
|
47
41
|
"scripts": {
|
|
48
|
-
"test": "
|
|
42
|
+
"test": "bun test",
|
|
49
43
|
"typecheck": "tsc --noEmit",
|
|
50
44
|
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
51
45
|
"clean": "del-cli lib"
|
|
52
46
|
},
|
|
53
47
|
"dependencies": {
|
|
54
|
-
"@couch-kit/core": "0.3.
|
|
48
|
+
"@couch-kit/core": "0.3.3",
|
|
55
49
|
"buffer": "^6.0.3",
|
|
56
50
|
"js-sha1": "^0.7.0",
|
|
57
|
-
"react-native-
|
|
58
|
-
"react-native-network-info": "^5.2.1",
|
|
59
|
-
"react-native-nitro-http-server": "^1.5.4",
|
|
60
|
-
"react-native-tcp-socket": "^6.0.6"
|
|
51
|
+
"react-native-nitro-http-server": "^1.5.4"
|
|
61
52
|
},
|
|
62
53
|
"devDependencies": {
|
|
63
|
-
"@types/react": "^
|
|
64
|
-
"
|
|
65
|
-
"react": "
|
|
66
|
-
"react-native": "0.72.6",
|
|
54
|
+
"@types/react": "^19.1.1",
|
|
55
|
+
"react": "19.0.4",
|
|
56
|
+
"react-native": "0.83.2",
|
|
67
57
|
"del-cli": "^5.1.0",
|
|
68
58
|
"jest": "^29.7.0"
|
|
69
59
|
},
|
|
70
60
|
"peerDependencies": {
|
|
71
|
-
"react": "
|
|
72
|
-
"react-native": "
|
|
73
|
-
"react-native-nitro-modules": ">=0.33.0"
|
|
61
|
+
"react": ">=18.2.0",
|
|
62
|
+
"react-native": ">=0.72.0",
|
|
63
|
+
"react-native-nitro-modules": ">=0.33.0",
|
|
64
|
+
"expo": ">=51.0.0",
|
|
65
|
+
"expo-file-system": ">=17.0.0",
|
|
66
|
+
"expo-network": ">=7.0.0",
|
|
67
|
+
"react-native-tcp-socket": ">=6.0.0"
|
|
74
68
|
}
|
|
75
69
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Buffer management utilities for WebSocket per-client receive buffers.
|
|
3
|
+
* Extracted into a standalone module so they can be tested without
|
|
4
|
+
* react-native dependencies.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Buffer } from "buffer";
|
|
8
|
+
import type { TcpSocketInstance } from "./declarations";
|
|
9
|
+
|
|
10
|
+
// Internal type for a TCP socket with our added management properties.
|
|
11
|
+
export interface ManagedSocket {
|
|
12
|
+
socket: TcpSocketInstance;
|
|
13
|
+
id: string;
|
|
14
|
+
isHandshakeComplete: boolean;
|
|
15
|
+
buffer: Buffer;
|
|
16
|
+
/** Number of valid bytes currently in `buffer` (may be less than buffer.length). */
|
|
17
|
+
bufferLength: number;
|
|
18
|
+
lastPong: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Append data to a managed socket's buffer, growing capacity geometrically
|
|
23
|
+
* to avoid re-allocation on every TCP data event.
|
|
24
|
+
*/
|
|
25
|
+
export function appendToBuffer(managed: ManagedSocket, data: Buffer): void {
|
|
26
|
+
const needed = managed.bufferLength + data.length;
|
|
27
|
+
|
|
28
|
+
if (needed > managed.buffer.length) {
|
|
29
|
+
// Grow by at least 2x or to fit the new data, whichever is larger
|
|
30
|
+
const newCapacity = Math.max(managed.buffer.length * 2, needed);
|
|
31
|
+
const grown = Buffer.alloc(newCapacity);
|
|
32
|
+
managed.buffer.copy(grown, 0, 0, managed.bufferLength);
|
|
33
|
+
managed.buffer = grown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
data.copy(managed.buffer, managed.bufferLength);
|
|
37
|
+
managed.bufferLength = needed;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Compact the buffer by discarding consumed bytes from the front.
|
|
42
|
+
* If all data has been consumed, reset the length to 0 without re-allocating.
|
|
43
|
+
*/
|
|
44
|
+
export function compactBuffer(managed: ManagedSocket, consumed: number): void {
|
|
45
|
+
const remaining = managed.bufferLength - consumed;
|
|
46
|
+
if (remaining <= 0) {
|
|
47
|
+
managed.bufferLength = 0;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// Shift remaining bytes to the front of the existing buffer
|
|
51
|
+
managed.buffer.copy(managed.buffer, 0, consumed, managed.bufferLength);
|
|
52
|
+
managed.bufferLength = remaining;
|
|
53
|
+
}
|
package/src/declarations.d.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal type definition for the raw TCP socket provided by react-native-tcp-socket.
|
|
3
|
+
* Covers only the API surface used by GameWebSocketServer.
|
|
4
|
+
*/
|
|
5
|
+
export interface TcpSocketInstance {
|
|
6
|
+
write(data: string | Buffer): void;
|
|
7
|
+
destroy(): void;
|
|
8
|
+
on(event: "data", callback: (data: Buffer | string) => void): this;
|
|
9
|
+
on(event: "error", callback: (error: Error) => void): this;
|
|
10
|
+
on(event: "close", callback: (hadError: boolean) => void): this;
|
|
11
|
+
address():
|
|
12
|
+
| { address: string; family: string; port: number }
|
|
13
|
+
| Record<string, never>;
|
|
14
|
+
readonly destroyed: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
1
17
|
declare module "react-native-nitro-http-server" {
|
|
2
18
|
export class StaticServer {
|
|
3
19
|
start(port: number, path: string, host?: string): Promise<void>;
|
package/src/event-emitter.ts
CHANGED
|
@@ -75,7 +75,10 @@ export class EventEmitter<
|
|
|
75
75
|
try {
|
|
76
76
|
listener(...args);
|
|
77
77
|
} catch (error) {
|
|
78
|
-
console.error(
|
|
78
|
+
console.error(
|
|
79
|
+
`[EventEmitter] Error in listener for "${String(event)}":`,
|
|
80
|
+
error,
|
|
81
|
+
);
|
|
79
82
|
}
|
|
80
83
|
}
|
|
81
84
|
return true;
|
package/src/network.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as Network from "expo-network";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Smart IP Discovery
|
|
@@ -7,16 +7,12 @@ import { NetworkInfo } from "react-native-network-info";
|
|
|
7
7
|
*/
|
|
8
8
|
export async function getBestIpAddress(): Promise<string | null> {
|
|
9
9
|
try {
|
|
10
|
-
|
|
11
|
-
const ip = await NetworkInfo.getIPV4Address();
|
|
10
|
+
const ip = await Network.getIpAddressAsync();
|
|
12
11
|
|
|
13
12
|
if (ip && ip !== "0.0.0.0" && ip !== "127.0.0.1") {
|
|
14
13
|
return ip;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
// Fallback logic could go here (e.g., iterating interfaces if exposed by a native module)
|
|
18
|
-
// For now, react-native-network-info is the standard abstraction.
|
|
19
|
-
|
|
20
16
|
return null;
|
|
21
17
|
} catch (error) {
|
|
22
18
|
console.warn("[CouchKit] Failed to get IP address:", error);
|
package/src/server.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
import { StaticServer } from "react-native-nitro-http-server";
|
|
3
|
-
import
|
|
3
|
+
import { Paths } from "expo-file-system";
|
|
4
4
|
import { getBestIpAddress } from "./network";
|
|
5
5
|
import { DEFAULT_HTTP_PORT, toErrorMessage } from "@couch-kit/core";
|
|
6
6
|
|
|
@@ -15,7 +15,8 @@ export interface CouchKitHostConfig {
|
|
|
15
15
|
* React hook that manages a static HTTP file server for serving the web controller.
|
|
16
16
|
*
|
|
17
17
|
* In production mode, starts a `StaticServer` bound to `0.0.0.0` on the configured port,
|
|
18
|
-
* serving files from `staticDir` (or
|
|
18
|
+
* serving files from `staticDir` (or the iOS bundle directory + `/www` by default).
|
|
19
|
+
* On Android, `staticDir` must be provided since bundle assets live inside the APK.
|
|
19
20
|
* In dev mode, skips the server and returns `devServerUrl` directly.
|
|
20
21
|
*
|
|
21
22
|
* @param config - Server configuration including port, dev mode, and static directory.
|
|
@@ -48,9 +49,11 @@ export const useStaticServer = (config: CouchKitHostConfig) => {
|
|
|
48
49
|
|
|
49
50
|
// Production Mode: Serve assets from bundle
|
|
50
51
|
try {
|
|
51
|
-
// Use staticDir if provided (required on Android where
|
|
52
|
-
// otherwise fall back to iOS
|
|
53
|
-
const path =
|
|
52
|
+
// Use staticDir if provided (required on Android where bundle path is undefined),
|
|
53
|
+
// otherwise fall back to the iOS bundle directory via expo-file-system
|
|
54
|
+
const path =
|
|
55
|
+
config.staticDir ||
|
|
56
|
+
`${Paths.bundle.uri.replace(/^file:\/\//, "")}www`;
|
|
54
57
|
const port = config.port || DEFAULT_HTTP_PORT;
|
|
55
58
|
|
|
56
59
|
server = new StaticServer();
|
package/src/websocket.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import TcpSocket from "react-native-tcp-socket";
|
|
10
|
+
import type { TcpSocketInstance } from "./declarations";
|
|
10
11
|
import { EventEmitter } from "./event-emitter";
|
|
11
12
|
import { Buffer } from "buffer";
|
|
12
13
|
import { sha1 } from "js-sha1";
|
|
@@ -16,6 +17,8 @@ import {
|
|
|
16
17
|
KEEPALIVE_INTERVAL,
|
|
17
18
|
KEEPALIVE_TIMEOUT,
|
|
18
19
|
} from "@couch-kit/core";
|
|
20
|
+
import { appendToBuffer, compactBuffer } from "./buffer-utils";
|
|
21
|
+
import type { ManagedSocket } from "./buffer-utils";
|
|
19
22
|
|
|
20
23
|
export interface WebSocketConfig {
|
|
21
24
|
port: number;
|
|
@@ -53,59 +56,12 @@ const GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
|
53
56
|
/** Initial capacity for per-client receive buffers. */
|
|
54
57
|
const INITIAL_BUFFER_CAPACITY = 4096;
|
|
55
58
|
|
|
56
|
-
/**
|
|
57
|
-
* Append data to a managed socket's buffer, growing capacity geometrically
|
|
58
|
-
* to avoid re-allocation on every TCP data event.
|
|
59
|
-
*/
|
|
60
|
-
function appendToBuffer(managed: ManagedSocket, data: Buffer): void {
|
|
61
|
-
const needed = managed.bufferLength + data.length;
|
|
62
|
-
|
|
63
|
-
if (needed > managed.buffer.length) {
|
|
64
|
-
// Grow by at least 2x or to fit the new data, whichever is larger
|
|
65
|
-
const newCapacity = Math.max(managed.buffer.length * 2, needed);
|
|
66
|
-
const grown = Buffer.alloc(newCapacity);
|
|
67
|
-
managed.buffer.copy(grown, 0, 0, managed.bufferLength);
|
|
68
|
-
managed.buffer = grown;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
data.copy(managed.buffer, managed.bufferLength);
|
|
72
|
-
managed.bufferLength = needed;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Compact the buffer by discarding consumed bytes from the front.
|
|
77
|
-
* If all data has been consumed, reset the length to 0 without re-allocating.
|
|
78
|
-
*/
|
|
79
|
-
function compactBuffer(managed: ManagedSocket, consumed: number): void {
|
|
80
|
-
const remaining = managed.bufferLength - consumed;
|
|
81
|
-
if (remaining <= 0) {
|
|
82
|
-
managed.bufferLength = 0;
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
// Shift remaining bytes to the front of the existing buffer
|
|
86
|
-
managed.buffer.copy(managed.buffer, 0, consumed, managed.bufferLength);
|
|
87
|
-
managed.bufferLength = remaining;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
59
|
interface DecodedFrame {
|
|
91
60
|
opcode: number;
|
|
92
61
|
payload: Buffer;
|
|
93
62
|
bytesConsumed: number;
|
|
94
63
|
}
|
|
95
64
|
|
|
96
|
-
// Internal type for a TCP socket with our added management properties.
|
|
97
|
-
// We use `any` for the raw socket since react-native-tcp-socket doesn't export a clean type.
|
|
98
|
-
interface ManagedSocket {
|
|
99
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
100
|
-
socket: any;
|
|
101
|
-
id: string;
|
|
102
|
-
isHandshakeComplete: boolean;
|
|
103
|
-
buffer: Buffer;
|
|
104
|
-
/** Number of valid bytes currently in `buffer` (may be less than buffer.length). */
|
|
105
|
-
bufferLength: number;
|
|
106
|
-
lastPong: number;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
65
|
export class GameWebSocketServer extends EventEmitter<WebSocketServerEvents> {
|
|
110
66
|
private server: ReturnType<typeof TcpSocket.createServer> | null = null;
|
|
111
67
|
private clients: Map<string, ManagedSocket> = new Map();
|
|
@@ -134,11 +90,11 @@ export class GameWebSocketServer extends EventEmitter<WebSocketServerEvents> {
|
|
|
134
90
|
public start() {
|
|
135
91
|
this.log(`[WebSocket] Starting server on port ${this.port}...`);
|
|
136
92
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
);
|
|
93
|
+
this.server = TcpSocket.createServer((rawSocket: TcpSocketInstance) => {
|
|
94
|
+
const addrInfo = rawSocket.address();
|
|
95
|
+
const remoteAddr =
|
|
96
|
+
addrInfo && "address" in addrInfo ? addrInfo.address : "unknown";
|
|
97
|
+
this.log(`[WebSocket] New connection from ${remoteAddr}`);
|
|
142
98
|
|
|
143
99
|
const managed: ManagedSocket = {
|
|
144
100
|
socket: rawSocket,
|
|
@@ -227,8 +183,11 @@ export class GameWebSocketServer extends EventEmitter<WebSocketServerEvents> {
|
|
|
227
183
|
this.log(`[WebSocket] Keepalive timeout for ${id}, disconnecting`);
|
|
228
184
|
try {
|
|
229
185
|
managed.socket.destroy();
|
|
230
|
-
} catch {
|
|
231
|
-
|
|
186
|
+
} catch (error) {
|
|
187
|
+
this.log(
|
|
188
|
+
"[WebSocket] Socket already destroyed during keepalive cleanup:",
|
|
189
|
+
error,
|
|
190
|
+
);
|
|
232
191
|
}
|
|
233
192
|
this.clients.delete(id);
|
|
234
193
|
this.emit("disconnect", id);
|
|
@@ -237,8 +196,8 @@ export class GameWebSocketServer extends EventEmitter<WebSocketServerEvents> {
|
|
|
237
196
|
|
|
238
197
|
try {
|
|
239
198
|
managed.socket.write(pingFrame);
|
|
240
|
-
} catch {
|
|
241
|
-
|
|
199
|
+
} catch (error) {
|
|
200
|
+
this.log("[WebSocket] Failed to send keepalive ping:", error);
|
|
242
201
|
}
|
|
243
202
|
}
|
|
244
203
|
}, this.keepaliveInterval);
|
|
@@ -258,8 +217,11 @@ export class GameWebSocketServer extends EventEmitter<WebSocketServerEvents> {
|
|
|
258
217
|
this.log(`[WebSocket] Frame error from ${managed.id}:`, error);
|
|
259
218
|
try {
|
|
260
219
|
managed.socket.destroy();
|
|
261
|
-
} catch {
|
|
262
|
-
|
|
220
|
+
} catch (destroyError) {
|
|
221
|
+
this.log(
|
|
222
|
+
"[WebSocket] Socket already destroyed after frame error:",
|
|
223
|
+
destroyError,
|
|
224
|
+
);
|
|
263
225
|
}
|
|
264
226
|
return;
|
|
265
227
|
}
|
|
@@ -278,10 +240,11 @@ export class GameWebSocketServer extends EventEmitter<WebSocketServerEvents> {
|
|
|
278
240
|
try {
|
|
279
241
|
const message = JSON.parse(frame.payload.toString("utf8"));
|
|
280
242
|
this.emit("message", managed.id, message);
|
|
281
|
-
} catch {
|
|
243
|
+
} catch (error) {
|
|
282
244
|
// Corrupt JSON in a complete frame -- discard this frame, continue processing
|
|
283
245
|
this.log(
|
|
284
|
-
`[WebSocket] Invalid JSON from ${managed.id}, discarding frame
|
|
246
|
+
`[WebSocket] Invalid JSON from ${managed.id}, discarding frame:`,
|
|
247
|
+
error,
|
|
285
248
|
);
|
|
286
249
|
}
|
|
287
250
|
break;
|
|
@@ -295,8 +258,8 @@ export class GameWebSocketServer extends EventEmitter<WebSocketServerEvents> {
|
|
|
295
258
|
closeFrame[1] = 0x00; // No payload
|
|
296
259
|
try {
|
|
297
260
|
managed.socket.write(closeFrame);
|
|
298
|
-
} catch {
|
|
299
|
-
|
|
261
|
+
} catch (error) {
|
|
262
|
+
this.log("[WebSocket] Failed to send close frame:", error);
|
|
300
263
|
}
|
|
301
264
|
managed.socket.destroy();
|
|
302
265
|
break;
|
|
@@ -308,8 +271,8 @@ export class GameWebSocketServer extends EventEmitter<WebSocketServerEvents> {
|
|
|
308
271
|
const pongFrame = this.encodeControlFrame(OPCODE.PONG, frame.payload);
|
|
309
272
|
try {
|
|
310
273
|
managed.socket.write(pongFrame);
|
|
311
|
-
} catch {
|
|
312
|
-
|
|
274
|
+
} catch (error) {
|
|
275
|
+
this.log("[WebSocket] Failed to send pong:", error);
|
|
313
276
|
}
|
|
314
277
|
break;
|
|
315
278
|
}
|
|
@@ -365,13 +328,19 @@ export class GameWebSocketServer extends EventEmitter<WebSocketServerEvents> {
|
|
|
365
328
|
this.clients.forEach((managed) => {
|
|
366
329
|
try {
|
|
367
330
|
managed.socket.write(closeFrame);
|
|
368
|
-
} catch {
|
|
369
|
-
|
|
331
|
+
} catch (error) {
|
|
332
|
+
this.log(
|
|
333
|
+
"[WebSocket] Failed to send close frame during shutdown:",
|
|
334
|
+
error,
|
|
335
|
+
);
|
|
370
336
|
}
|
|
371
337
|
try {
|
|
372
338
|
managed.socket.destroy();
|
|
373
|
-
} catch {
|
|
374
|
-
|
|
339
|
+
} catch (error) {
|
|
340
|
+
this.log(
|
|
341
|
+
"[WebSocket] Socket already destroyed during shutdown:",
|
|
342
|
+
error,
|
|
343
|
+
);
|
|
375
344
|
}
|
|
376
345
|
});
|
|
377
346
|
|
|
@@ -425,7 +394,6 @@ export class GameWebSocketServer extends EventEmitter<WebSocketServerEvents> {
|
|
|
425
394
|
|
|
426
395
|
// --- Private Helpers ---
|
|
427
396
|
|
|
428
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
429
397
|
private handleHandshake(managed: ManagedSocket, header: string) {
|
|
430
398
|
this.log("[WebSocket] Handshake request header:", JSON.stringify(header));
|
|
431
399
|
const keyMatch = header.match(/Sec-WebSocket-Key: (.+)/);
|