playsocketjs 1.1.8 → 1.1.10

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.
@@ -2287,9 +2287,17 @@ class PlaySocket {
2287
2287
  }
2288
2288
  break;
2289
2289
 
2290
+ case 'server_stopped':
2291
+ this.#triggerEvent("error", "Server restart.");
2292
+ this.destroy();
2293
+ break;
2294
+
2295
+ case 'host_migrated':
2296
+ this.#setHost(message.newHost);
2297
+ break;
2298
+
2290
2299
  case 'client_disconnected':
2291
2300
  this.#connectionCount = message.participantCount - 1; // Counted without the user themselves
2292
- this.#setHost(message.host);
2293
2301
  this.#triggerEvent("clientDisconnected", message.client);
2294
2302
  this.#triggerEvent("status", `Client ${message.client} disconnected.`);
2295
2303
  break;
@@ -2325,7 +2333,8 @@ class PlaySocket {
2325
2333
  * @private
2326
2334
  */
2327
2335
  async #attemptReconnect() {
2328
- if (this.#reconnectCount++ >= 3) {
2336
+ this.#reconnectCount++;
2337
+ if (this.#reconnectCount > 9) {
2329
2338
  this.#triggerEvent("error", "Disconnected from server.");
2330
2339
  return this.destroy();
2331
2340
  }
@@ -2226,6 +2226,7 @@ function requireServer () {
2226
2226
 
2227
2227
  // Set up ws event handlers
2228
2228
  this.#wss.on('connection', ws => {
2229
+ ws.connectionId = crypto.randomUUID();
2229
2230
  ws.isAlive = true;
2230
2231
  ws.on('pong', () => { ws.isAlive = true; });
2231
2232
  ws.on('message', msg => this.#handleMessage(ws, msg));
@@ -2258,9 +2259,14 @@ function requireServer () {
2258
2259
  try {
2259
2260
  const data = decode(message);
2260
2261
 
2261
- // Apply rate limiting after client is registered
2262
- if (ws.clientId && !this.#checkRateLimit(ws.clientId, data.type)) {
2263
- return console.error(`Client ${ws.clientId} rate limit exceeded.`);
2262
+ // Apply rate limiting to all connections (including unregistered)
2263
+ if (!this.#checkRateLimit(ws.connectionId, data.type)) {
2264
+ ws.rateLimitViolations = (ws.rateLimitViolations || 0) + 1;
2265
+ if (ws.rateLimitViolations > 5) {
2266
+ ws.terminate();
2267
+ return console.error(`Connection ${ws.connectionId} terminated due to repeated rate limit violations.`);
2268
+ }
2269
+ return console.error(`Connection ${ws.connectionId} rate limit exceeded.`);
2264
2270
  }
2265
2271
 
2266
2272
  switch (data.type) {
@@ -2306,7 +2312,7 @@ function requireServer () {
2306
2312
  roomData = {
2307
2313
  state: formerRoom.crdtManager.getState,
2308
2314
  participantCount: formerRoom.participants.length,
2309
- host: formerRoom.participants[0],
2315
+ host: formerRoom.host,
2310
2316
  version: this.#roomVersions.get(formerRoomId)
2311
2317
  };
2312
2318
  }
@@ -2332,6 +2338,7 @@ function requireServer () {
2332
2338
  this.#roomVersions.set(newRoomId, 0); // Start with version 0
2333
2339
  this.#rooms[newRoomId] = {
2334
2340
  participants: [ws.clientId],
2341
+ host: ws.clientId,
2335
2342
  maxSize: data.size || null,
2336
2343
  crdtManager: new CRDTManager(crypto.randomUUID(), this.#debug)
2337
2344
  };
@@ -2373,7 +2380,7 @@ function requireServer () {
2373
2380
  type: 'join_accepted',
2374
2381
  state: room.crdtManager.getState,
2375
2382
  participantCount: room.participants.length,
2376
- host: room.participants[0],
2383
+ host: room.host,
2377
2384
  version: this.#roomVersions.get(roomId)
2378
2385
  }), { binary: true });
2379
2386
 
@@ -2427,6 +2434,29 @@ function requireServer () {
2427
2434
  }
2428
2435
  }
2429
2436
 
2437
+ /**
2438
+ * If the client that disconnected (and is now in the reconnection-phase) was the room host,
2439
+ * pick a new room host immediately to avoid host-less phase (in case important logic is attached to the host)
2440
+ * @param {string} roomId
2441
+ * @param {string} clientId
2442
+ */
2443
+ #changeHostIfDisconnected(roomId, clientId) {
2444
+ const room = this.#rooms[roomId];
2445
+ if (room && room.host === clientId && room.participants.length > 1) {
2446
+ const participantsWithoutClient = room.participants.filter((e) => e !== clientId);
2447
+ room.host = participantsWithoutClient[0]; // Set new host
2448
+
2449
+ // Inform all participants about the new host
2450
+ participantsWithoutClient.forEach(p => {
2451
+ const client = this.#clients.get(p);
2452
+ if (client) client.send(encode({
2453
+ type: 'host_migrated',
2454
+ newHost: room.host,
2455
+ }), { binary: true });
2456
+ });
2457
+ }
2458
+ }
2459
+
2430
2460
  /**
2431
2461
  * Generate a random token to prevent malicious reconnect attempts
2432
2462
  * @returns {string} - Token
@@ -2441,24 +2471,24 @@ function requireServer () {
2441
2471
  * Check rate limit using token bucket algorithm
2442
2472
  * @private
2443
2473
  */
2444
- #checkRateLimit(clientId, operationType) {
2445
- const MAX_POINTS = 100;
2474
+ #checkRateLimit(connectionId, operationType) {
2475
+ const MAX_POINTS = 20;
2446
2476
  const now = Date.now();
2447
2477
 
2448
- if (!this.#rateLimits.has(clientId)) {
2449
- this.#rateLimits.set(clientId, { points: MAX_POINTS, lastReset: now });
2478
+ if (!this.#rateLimits.has(connectionId)) {
2479
+ this.#rateLimits.set(connectionId, { points: MAX_POINTS, lastReset: now });
2450
2480
  return true;
2451
2481
  }
2452
2482
 
2453
- const limit = this.#rateLimits.get(clientId);
2483
+ const limit = this.#rateLimits.get(connectionId);
2454
2484
 
2455
- // Reset points if interval has passed (5s)
2456
- if (now - limit.lastReset > 5000) {
2485
+ // Reset points if interval has passed (1s)
2486
+ if (now - limit.lastReset > 1000) {
2457
2487
  limit.points = MAX_POINTS;
2458
2488
  limit.lastReset = now;
2459
2489
  }
2460
2490
 
2461
- const pointCost = ['create_room', 'join_room'].includes(operationType) ? 20 : 1;
2491
+ const pointCost = ['create_room', 'join_room'].includes(operationType) ? 5 : 1;
2462
2492
  if (limit.points < pointCost) return false;
2463
2493
 
2464
2494
  limit.points -= pointCost;
@@ -2470,18 +2500,25 @@ function requireServer () {
2470
2500
  * @private
2471
2501
  */
2472
2502
  #handleDisconnection(ws) {
2473
- if (!ws.clientId) return;
2474
- this.#clients.delete(ws.clientId); // Immediately remove from active clients (otherwise, server would try to message this client)
2503
+ this.#rateLimits.delete(ws.connectionId);
2475
2504
 
2476
- if (ws.willfulDisconnect) {
2477
- this.#disconnectClient(ws); // Immediate disconnection
2478
- } else {
2479
- // Pending complete disconnection with 2s grace period to allow for reconnections
2480
- this.#pendingDisconnects.set(ws.clientId, {
2481
- timeout: setTimeout(() => {
2482
- this.#disconnectClient(ws);
2483
- }, 2000)
2484
- });
2505
+ if (ws.clientId) {
2506
+ this.#clients.delete(ws.clientId); // Immediately remove from active clients (otherwise, server would try to message this client)
2507
+
2508
+ // If client was in a room, check if they were the host & migrate
2509
+ const roomId = this.#clientRooms.get(ws.clientId);
2510
+ if (roomId != null) this.#changeHostIfDisconnected(roomId, ws.clientId);
2511
+
2512
+ if (ws.willfulDisconnect) {
2513
+ this.#disconnectClient(ws); // Immediate disconnection
2514
+ } else {
2515
+ // Pending complete disconnection with 5s grace period to allow for reconnections
2516
+ this.#pendingDisconnects.set(ws.clientId, {
2517
+ timeout: setTimeout(() => {
2518
+ this.#disconnectClient(ws);
2519
+ }, 5000)
2520
+ });
2521
+ }
2485
2522
  }
2486
2523
  }
2487
2524
 
@@ -2492,7 +2529,6 @@ function requireServer () {
2492
2529
  */
2493
2530
  #disconnectClient(ws) {
2494
2531
  this.#pendingDisconnects.delete(ws.clientId);
2495
- this.#rateLimits.delete(ws.clientId);
2496
2532
  this.#clientTokens.delete(ws.clientId);
2497
2533
  const roomId = this.#clientRooms.get(ws.clientId);
2498
2534
  const room = this.#rooms[roomId];
@@ -2512,7 +2548,6 @@ function requireServer () {
2512
2548
  if (client) client.send(encode({
2513
2549
  type: 'client_disconnected',
2514
2550
  client: ws.clientId,
2515
- host: room.participants[0],
2516
2551
  participantCount: room.participants.length
2517
2552
  }), { binary: true });
2518
2553
  });
@@ -2551,7 +2586,10 @@ function requireServer () {
2551
2586
  stop() {
2552
2587
  clearInterval(this.#heartbeatInterval);
2553
2588
  if (this.#wss) {
2554
- this.#wss.clients.forEach(client => client.close());
2589
+ this.#wss.clients.forEach(client => {
2590
+ client.send(encode({ type: 'server_stopped' }), { binary: true });
2591
+ client.close();
2592
+ });
2555
2593
  this.#wss.close();
2556
2594
  }
2557
2595
  if (this.#server && this.#ownsServer) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playsocketjs",
3
- "version": "1.1.8",
3
+ "version": "1.1.10",
4
4
  "description": "WebSocket wrapper for creating simple multiplayer systems with ease.",
5
5
  "module": "dist/playsocket-client.js",
6
6
  "exports": {