@couch-kit/host 1.4.0 → 1.5.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/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. */
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AAGf,OAAO,EAQL,KAAK,UAAU,EACf,KAAK,OAAO,EAGb,MAAM,iBAAiB,CAAC;AAEzB,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,+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,qBA4TA;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,KAK/C,oBAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,CAC7C"}
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.4.0",
3
+ "version": "1.5.0",
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.5.1",
58
+ "@couch-kit/core": "0.6.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 [socketId, playerId] of pendingWelcome.current) {
145
+ for (const [
146
+ socketId,
147
+ { playerId, isReconnect },
148
+ ] of pendingWelcome.current) {
136
149
  welcomedClients.current.add(socketId);
137
- server.send(socketId, {
138
- type: MessageTypes.WELCOME,
139
- payload: {
140
- playerId,
141
- state,
142
- serverTime: Date.now(),
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<Map<string, string>>(new Map()); // socketId -> playerId
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
- const playerId = derivePlayerId(secret);
244
-
245
- // Update session maps
246
- sessions.current.set(secret, socketId);
247
- reverseMap.current.set(socketId, secret);
248
-
249
- // Cancel any pending cleanup timer for this player
250
- const existingTimer = cleanupTimers.current.get(playerId);
251
- if (existingTimer) {
252
- clearTimeout(existingTimer);
253
- cleanupTimers.current.delete(playerId);
254
- }
255
-
256
- // Check if this is a returning player
257
- const existingPlayer = stateRef.current.players[playerId];
258
- if (existingPlayer) {
259
- // Reconnection restore existing player
260
- dispatch({
261
- type: InternalActionTypes.PLAYER_RECONNECTED,
262
- payload: { playerId },
263
- } as InternalAction<S>);
264
- } else {
265
- // New player
266
- dispatch({
267
- type: InternalActionTypes.PLAYER_JOINED,
268
- payload: { id: playerId, ...payload },
269
- } as InternalAction<S>);
270
- }
271
-
272
- // Queue WELCOME
273
- pendingWelcome.current.set(socketId, playerId);
274
-
275
- configRef.current.onPlayerJoined?.(playerId, payload.name);
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
- // Resolve playerId from socketId
306
- const actionSecret = reverseMap.current.get(socketId);
307
- const resolvedPlayerId = actionSecret
308
- ? derivePlayerId(actionSecret)
309
- : undefined;
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
- // Resolve socketId -> secret -> playerId
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
- if (!secret) return; // Unknown socket, nothing to do
440
+ // Clean up rate limits
441
+ rateLimits.current.delete(socketId);
442
+
443
+ if (!playerId || !secret) return; // Unknown socket, nothing to do
338
444
 
339
- const playerId = derivePlayerId(secret);
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 (5 minutes default)
356
- const timer = setTimeout(
357
- () => {
358
- cleanupTimers.current.delete(playerId);
359
- sessions.current.delete(secret);
360
- dispatch({
361
- type: InternalActionTypes.PLAYER_REMOVED,
362
- payload: { playerId },
363
- } as InternalAction<S>);
364
- },
365
- 5 * 60 * 1000,
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
  }
@@ -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"}
@@ -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
- }
@@ -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
- }