@couch-kit/host 1.4.0 → 1.5.1
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/lib/provider.d.ts +2 -0
- package/lib/provider.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/provider.tsx +178 -65
- package/lib/buffer-utils.d.ts +0 -27
- package/lib/buffer-utils.d.ts.map +0 -1
- package/src/buffer-utils.ts +0 -53
- package/src/declarations.d.ts +0 -25
package/lib/provider.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface GameHostConfig<S extends IGameState, A extends IAction> {
|
|
|
9
9
|
devServerUrl?: string;
|
|
10
10
|
staticDir?: string;
|
|
11
11
|
debug?: boolean;
|
|
12
|
+
/** Timeout (ms) before a disconnected player is permanently removed (default: 5 minutes). */
|
|
13
|
+
disconnectTimeout?: number;
|
|
12
14
|
/** Called when a player successfully joins. */
|
|
13
15
|
onPlayerJoined?: (playerId: string, name: string) => void;
|
|
14
16
|
/** Called when a player disconnects. */
|
package/lib/provider.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AAGf,OAAO,
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AAGf,OAAO,EAUL,KAAK,UAAU,EACf,KAAK,OAAO,EAGb,MAAM,iBAAiB,CAAC;AAQzB,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO;IACrE,YAAY,EAAE,CAAC,CAAC;IAChB,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,6FAA6F;IAC7F,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,+CAA+C;IAC/C,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1D,wCAAwC;IACxC,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,yCAAyC;IACzC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAED,UAAU,oBAAoB,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO;IACpE,KAAK,EAAE,CAAC,CAAC;IACT,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,CAAC;IAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,KAAK,GAAG,IAAI,CAAC;CAC3B;AA4CD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,EAAE,EACxE,QAAQ,EACR,MAAM,GACP,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,MAAM,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;CAC9B,qBAmaA;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,KAK/C,oBAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,CAC7C"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@couch-kit/host",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"provenance": true
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"prepublishOnly": "bun run build"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@couch-kit/core": "0.
|
|
58
|
+
"@couch-kit/core": "0.7.0",
|
|
59
59
|
"buffer": "^6.0.3",
|
|
60
60
|
"js-sha1": "^0.7.0",
|
|
61
61
|
"react-native-nitro-http-server": "^1.5.4"
|
package/src/provider.tsx
CHANGED
|
@@ -14,8 +14,10 @@ import {
|
|
|
14
14
|
InternalActionTypes,
|
|
15
15
|
DEFAULT_HTTP_PORT,
|
|
16
16
|
DEFAULT_WS_PORT_OFFSET,
|
|
17
|
+
DEFAULT_DISCONNECT_TIMEOUT,
|
|
17
18
|
createGameReducer,
|
|
18
19
|
derivePlayerId,
|
|
20
|
+
derivePlayerIdLegacy,
|
|
19
21
|
isValidSecret,
|
|
20
22
|
type IGameState,
|
|
21
23
|
type IAction,
|
|
@@ -23,6 +25,12 @@ import {
|
|
|
23
25
|
type ClientMessage,
|
|
24
26
|
} from "@couch-kit/core";
|
|
25
27
|
|
|
28
|
+
/** Maximum actions per rate-limit window. */
|
|
29
|
+
const RATE_LIMIT_MAX = 60;
|
|
30
|
+
|
|
31
|
+
/** Rate-limit window duration (ms). */
|
|
32
|
+
const RATE_LIMIT_WINDOW = 1000;
|
|
33
|
+
|
|
26
34
|
export interface GameHostConfig<S extends IGameState, A extends IAction> {
|
|
27
35
|
initialState: S;
|
|
28
36
|
reducer: (state: S, action: A) => S;
|
|
@@ -32,6 +40,8 @@ export interface GameHostConfig<S extends IGameState, A extends IAction> {
|
|
|
32
40
|
devServerUrl?: string;
|
|
33
41
|
staticDir?: string; // Override the default www directory path (required on Android)
|
|
34
42
|
debug?: boolean;
|
|
43
|
+
/** Timeout (ms) before a disconnected player is permanently removed (default: 5 minutes). */
|
|
44
|
+
disconnectTimeout?: number;
|
|
35
45
|
/** Called when a player successfully joins. */
|
|
36
46
|
onPlayerJoined?: (playerId: string, name: string) => void;
|
|
37
47
|
/** Called when a player disconnects. */
|
|
@@ -125,23 +135,36 @@ export function GameHostProvider<S extends IGameState, A extends IAction>({
|
|
|
125
135
|
stateRef.current = state;
|
|
126
136
|
}, [state]);
|
|
127
137
|
|
|
128
|
-
// Send WELCOME messages after state has settled (post-render).
|
|
138
|
+
// Send WELCOME/RECONNECTED messages after state has settled (post-render).
|
|
129
139
|
// This guarantees the joining player is included in the state snapshot.
|
|
130
140
|
useEffect(() => {
|
|
131
141
|
if (pendingWelcome.current.size === 0) return;
|
|
132
142
|
if (!wsServer.current) return;
|
|
133
143
|
|
|
134
144
|
const server = wsServer.current;
|
|
135
|
-
for (const [
|
|
145
|
+
for (const [
|
|
146
|
+
socketId,
|
|
147
|
+
{ playerId, isReconnect },
|
|
148
|
+
] of pendingWelcome.current) {
|
|
136
149
|
welcomedClients.current.add(socketId);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
150
|
+
if (isReconnect) {
|
|
151
|
+
server.send(socketId, {
|
|
152
|
+
type: MessageTypes.RECONNECTED,
|
|
153
|
+
payload: {
|
|
154
|
+
playerId,
|
|
155
|
+
state,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
} else {
|
|
159
|
+
server.send(socketId, {
|
|
160
|
+
type: MessageTypes.WELCOME,
|
|
161
|
+
payload: {
|
|
162
|
+
playerId,
|
|
163
|
+
state,
|
|
164
|
+
serverTime: Date.now(),
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|
|
145
168
|
}
|
|
146
169
|
pendingWelcome.current.clear();
|
|
147
170
|
}, [state]);
|
|
@@ -178,8 +201,24 @@ export function GameHostProvider<S extends IGameState, A extends IAction>({
|
|
|
178
201
|
// Track socket IDs that have received their WELCOME message
|
|
179
202
|
const welcomedClients = useRef<Set<string>>(new Set());
|
|
180
203
|
|
|
181
|
-
// Track socket IDs that need a WELCOME message after state settles
|
|
182
|
-
const pendingWelcome = useRef<
|
|
204
|
+
// Track socket IDs that need a WELCOME/RECONNECTED message after state settles
|
|
205
|
+
const pendingWelcome = useRef<
|
|
206
|
+
Map<string, { playerId: string; isReconnect: boolean }>
|
|
207
|
+
>(new Map());
|
|
208
|
+
|
|
209
|
+
// Cache: socketId -> playerId (avoids async derivation in hot paths)
|
|
210
|
+
const socketIdToPlayerId = useRef<Map<string, string>>(new Map());
|
|
211
|
+
|
|
212
|
+
// Track which players have finished loading assets
|
|
213
|
+
const assetsLoaded = useRef<Map<string, boolean>>(new Map());
|
|
214
|
+
|
|
215
|
+
// Queue of actions dispatched since last broadcast (for STATE_UPDATE.action)
|
|
216
|
+
const actionQueue = useRef<unknown[]>([]);
|
|
217
|
+
|
|
218
|
+
// Rate limiting: track action count per socket
|
|
219
|
+
const rateLimits = useRef<
|
|
220
|
+
Map<string, { count: number; windowStart: number }>
|
|
221
|
+
>(new Map());
|
|
183
222
|
|
|
184
223
|
useEffect(() => {
|
|
185
224
|
const port = config.wsPort || httpPort + DEFAULT_WS_PORT_OFFSET;
|
|
@@ -240,39 +279,69 @@ export function GameHostProvider<S extends IGameState, A extends IAction>({
|
|
|
240
279
|
return;
|
|
241
280
|
}
|
|
242
281
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
282
|
+
// Async dual-derivation: try SHA-256 first, fall back to legacy
|
|
283
|
+
derivePlayerId(secret).then((hashedId) => {
|
|
284
|
+
let playerId = hashedId;
|
|
285
|
+
|
|
286
|
+
// Migration: if a player exists under the legacy ID, use that instead
|
|
287
|
+
const legacyId = derivePlayerIdLegacy(secret);
|
|
288
|
+
if (
|
|
289
|
+
!stateRef.current.players[playerId] &&
|
|
290
|
+
stateRef.current.players[legacyId]
|
|
291
|
+
) {
|
|
292
|
+
playerId = legacyId;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Cache the resolved playerId for this socket
|
|
296
|
+
socketIdToPlayerId.current.set(socketId, playerId);
|
|
297
|
+
|
|
298
|
+
// Update session maps
|
|
299
|
+
sessions.current.set(secret, socketId);
|
|
300
|
+
reverseMap.current.set(socketId, secret);
|
|
301
|
+
|
|
302
|
+
// Cancel any pending cleanup timer for this player
|
|
303
|
+
const existingTimer = cleanupTimers.current.get(playerId);
|
|
304
|
+
if (existingTimer) {
|
|
305
|
+
clearTimeout(existingTimer);
|
|
306
|
+
cleanupTimers.current.delete(playerId);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Check if this is a returning player
|
|
310
|
+
const existingPlayer = stateRef.current.players[playerId];
|
|
311
|
+
if (existingPlayer) {
|
|
312
|
+
// Reconnection — restore existing player
|
|
313
|
+
dispatch({
|
|
314
|
+
type: InternalActionTypes.PLAYER_RECONNECTED,
|
|
315
|
+
payload: { playerId },
|
|
316
|
+
} as InternalAction<S>);
|
|
317
|
+
|
|
318
|
+
// Reset assets loaded status on reconnect
|
|
319
|
+
assetsLoaded.current.set(playerId, false);
|
|
320
|
+
|
|
321
|
+
// Queue RECONNECTED message
|
|
322
|
+
pendingWelcome.current.set(socketId, {
|
|
323
|
+
playerId,
|
|
324
|
+
isReconnect: true,
|
|
325
|
+
});
|
|
326
|
+
} else {
|
|
327
|
+
// New player
|
|
328
|
+
dispatch({
|
|
329
|
+
type: InternalActionTypes.PLAYER_JOINED,
|
|
330
|
+
payload: { id: playerId, ...payload },
|
|
331
|
+
} as InternalAction<S>);
|
|
332
|
+
|
|
333
|
+
// Initialize assets loaded status
|
|
334
|
+
assetsLoaded.current.set(playerId, false);
|
|
335
|
+
|
|
336
|
+
// Queue WELCOME message
|
|
337
|
+
pendingWelcome.current.set(socketId, {
|
|
338
|
+
playerId,
|
|
339
|
+
isReconnect: false,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
configRef.current.onPlayerJoined?.(playerId, payload.name);
|
|
344
|
+
});
|
|
276
345
|
break;
|
|
277
346
|
}
|
|
278
347
|
|
|
@@ -302,12 +371,32 @@ export function GameHostProvider<S extends IGameState, A extends IAction>({
|
|
|
302
371
|
});
|
|
303
372
|
return;
|
|
304
373
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
374
|
+
|
|
375
|
+
// Rate limiting
|
|
376
|
+
const now = Date.now();
|
|
377
|
+
let rateInfo = rateLimits.current.get(socketId);
|
|
378
|
+
if (!rateInfo || now - rateInfo.windowStart > RATE_LIMIT_WINDOW) {
|
|
379
|
+
rateInfo = { count: 0, windowStart: now };
|
|
380
|
+
rateLimits.current.set(socketId, rateInfo);
|
|
381
|
+
}
|
|
382
|
+
rateInfo.count++;
|
|
383
|
+
if (rateInfo.count > RATE_LIMIT_MAX) {
|
|
384
|
+
if (configRef.current.debug)
|
|
385
|
+
console.warn(`[GameHost] Rate limited ${socketId}`);
|
|
386
|
+
server.send(socketId, {
|
|
387
|
+
type: MessageTypes.ERROR,
|
|
388
|
+
payload: {
|
|
389
|
+
code: "RATE_LIMITED",
|
|
390
|
+
message: "Too many actions, slow down",
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Use cached playerId (populated at JOIN time)
|
|
397
|
+
const resolvedPlayerId = socketIdToPlayerId.current.get(socketId);
|
|
310
398
|
dispatch({ ...actionPayload, playerId: resolvedPlayerId });
|
|
399
|
+
actionQueue.current.push(actionPayload);
|
|
311
400
|
break;
|
|
312
401
|
}
|
|
313
402
|
|
|
@@ -321,6 +410,16 @@ export function GameHostProvider<S extends IGameState, A extends IAction>({
|
|
|
321
410
|
},
|
|
322
411
|
});
|
|
323
412
|
break;
|
|
413
|
+
|
|
414
|
+
case MessageTypes.ASSETS_LOADED: {
|
|
415
|
+
const loadedPlayerId = socketIdToPlayerId.current.get(socketId);
|
|
416
|
+
if (loadedPlayerId) {
|
|
417
|
+
assetsLoaded.current.set(loadedPlayerId, true);
|
|
418
|
+
if (configRef.current.debug)
|
|
419
|
+
console.log(`[GameHost] Assets loaded for ${loadedPlayerId}`);
|
|
420
|
+
}
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
324
423
|
}
|
|
325
424
|
});
|
|
326
425
|
|
|
@@ -330,13 +429,21 @@ export function GameHostProvider<S extends IGameState, A extends IAction>({
|
|
|
330
429
|
|
|
331
430
|
welcomedClients.current.delete(socketId);
|
|
332
431
|
|
|
333
|
-
//
|
|
432
|
+
// Use cached playerId
|
|
433
|
+
const playerId = socketIdToPlayerId.current.get(socketId);
|
|
434
|
+
socketIdToPlayerId.current.delete(socketId);
|
|
435
|
+
|
|
436
|
+
// Clean up reverse map
|
|
334
437
|
const secret = reverseMap.current.get(socketId);
|
|
335
438
|
reverseMap.current.delete(socketId);
|
|
336
439
|
|
|
337
|
-
|
|
440
|
+
// Clean up rate limits
|
|
441
|
+
rateLimits.current.delete(socketId);
|
|
442
|
+
|
|
443
|
+
if (!playerId || !secret) return; // Unknown socket, nothing to do
|
|
338
444
|
|
|
339
|
-
|
|
445
|
+
// Clean up assets loaded tracking
|
|
446
|
+
assetsLoaded.current.delete(playerId);
|
|
340
447
|
|
|
341
448
|
// RACE GUARD: Only mark as left if this socket is still the active one for this secret
|
|
342
449
|
if (sessions.current.get(secret) !== socketId) {
|
|
@@ -352,18 +459,17 @@ export function GameHostProvider<S extends IGameState, A extends IAction>({
|
|
|
352
459
|
|
|
353
460
|
configRef.current.onPlayerLeft?.(playerId);
|
|
354
461
|
|
|
355
|
-
// Start stale player cleanup timer
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
);
|
|
462
|
+
// Start stale player cleanup timer
|
|
463
|
+
const timeout =
|
|
464
|
+
configRef.current.disconnectTimeout ?? DEFAULT_DISCONNECT_TIMEOUT;
|
|
465
|
+
const timer = setTimeout(() => {
|
|
466
|
+
cleanupTimers.current.delete(playerId);
|
|
467
|
+
sessions.current.delete(secret);
|
|
468
|
+
dispatch({
|
|
469
|
+
type: InternalActionTypes.PLAYER_REMOVED,
|
|
470
|
+
payload: { playerId },
|
|
471
|
+
} as InternalAction<S>);
|
|
472
|
+
}, timeout);
|
|
367
473
|
cleanupTimers.current.set(playerId, timer);
|
|
368
474
|
});
|
|
369
475
|
|
|
@@ -389,11 +495,18 @@ export function GameHostProvider<S extends IGameState, A extends IAction>({
|
|
|
389
495
|
|
|
390
496
|
const broadcastState = useCallback(() => {
|
|
391
497
|
if (wsServer.current) {
|
|
498
|
+
const actions = actionQueue.current;
|
|
499
|
+
actionQueue.current = [];
|
|
392
500
|
wsServer.current.broadcast({
|
|
393
501
|
type: MessageTypes.STATE_UPDATE,
|
|
394
502
|
payload: {
|
|
395
503
|
newState: stateRef.current,
|
|
396
504
|
timestamp: Date.now(),
|
|
505
|
+
...(actions.length === 1
|
|
506
|
+
? { action: actions[0] }
|
|
507
|
+
: actions.length > 1
|
|
508
|
+
? { action: actions }
|
|
509
|
+
: {}),
|
|
397
510
|
},
|
|
398
511
|
});
|
|
399
512
|
}
|
package/lib/buffer-utils.d.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
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
|
-
import { Buffer } from "buffer";
|
|
7
|
-
import type { TcpSocketInstance } from "./declarations";
|
|
8
|
-
export interface ManagedSocket {
|
|
9
|
-
socket: TcpSocketInstance;
|
|
10
|
-
id: string;
|
|
11
|
-
isHandshakeComplete: boolean;
|
|
12
|
-
buffer: Buffer;
|
|
13
|
-
/** Number of valid bytes currently in `buffer` (may be less than buffer.length). */
|
|
14
|
-
bufferLength: number;
|
|
15
|
-
lastPong: number;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Append data to a managed socket's buffer, growing capacity geometrically
|
|
19
|
-
* to avoid re-allocation on every TCP data event.
|
|
20
|
-
*/
|
|
21
|
-
export declare function appendToBuffer(managed: ManagedSocket, data: Buffer): void;
|
|
22
|
-
/**
|
|
23
|
-
* Compact the buffer by discarding consumed bytes from the front.
|
|
24
|
-
* If all data has been consumed, reset the length to 0 without re-allocating.
|
|
25
|
-
*/
|
|
26
|
-
export declare function compactBuffer(managed: ManagedSocket, consumed: number): void;
|
|
27
|
-
//# sourceMappingURL=buffer-utils.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"buffer-utils.d.ts","sourceRoot":"","sources":["../src/buffer-utils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAGxD,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,iBAAiB,CAAC;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,mBAAmB,EAAE,OAAO,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,oFAAoF;IACpF,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAazE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAS5E"}
|
package/src/buffer-utils.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
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
|
-
|
|
6
|
-
import type { Buffer } from "buffer";
|
|
7
|
-
|
|
8
|
-
export interface TcpSocketInstance {
|
|
9
|
-
write(data: string | Buffer): void;
|
|
10
|
-
destroy(): void;
|
|
11
|
-
on(event: "data", callback: (data: Buffer | string) => void): this;
|
|
12
|
-
on(event: "error", callback: (error: Error) => void): this;
|
|
13
|
-
on(event: "close", callback: (hadError: boolean) => void): this;
|
|
14
|
-
address():
|
|
15
|
-
| { address: string; family: string; port: number }
|
|
16
|
-
| Record<string, never>;
|
|
17
|
-
readonly destroyed: boolean;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
declare module "react-native-nitro-http-server" {
|
|
21
|
-
export class StaticServer {
|
|
22
|
-
start(port: number, path: string, host?: string): Promise<void>;
|
|
23
|
-
stop(): void;
|
|
24
|
-
}
|
|
25
|
-
}
|