playsocketjs 2.6.3 → 2.7.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 CHANGED
@@ -216,6 +216,26 @@ new PlaySocket(options: PlaySocketServerOptions)
216
216
  | `server` | http.Server | - | Existing http server (optional) |
217
217
  | `rateLimit` | number | 20 | Adjust the messages/second rate limit |
218
218
  | `debug` | boolean | false | Set to true to enable extra logging |
219
+ | `verifyClient` | function | - | Optional callback to verify connections before WebSocket upgrade |
220
+
221
+ #### verifyClient callback
222
+
223
+ The `verifyClient` option allows you to implement custom connection verification logic, such as rate limiting, before the WebSocket handshake completes.
224
+
225
+ ```javascript
226
+ const server = new PlaySocketServer({
227
+ server: httpServer,
228
+ path: '/socket',
229
+ verifyClient: (info, callback) => {
230
+ // info.req - the HTTP request object, info.origin - the Origin header value
231
+ const ip = info.req.headers['x-forwarded-for'] || info.req.socket.remoteAddress;
232
+ if (isRateLimited(ip)) return callback(false, 429, 'Too Many Requests');
233
+ callback(true);
234
+ }
235
+ });
236
+ ```
237
+
238
+ The callback signature is `callback(verified, code?, message?)` where `code` and `message` are the optional HTTP response status for rejected connections.
219
239
 
220
240
  ### Methods
221
241
 
@@ -226,7 +246,7 @@ new PlaySocket(options: PlaySocketServerOptions)
226
246
  | `onEvent()` | `event: string, callback: Function` | `void` | Register a server-side event callback |
227
247
  | `getRoomStorage()` | `roomId: string` | `object` | Get a snapshot of the current room storage |
228
248
  | `updateRoomStorage()` | `roomId: string, key: string, type: 'set' \| 'array-add' \| 'array-add-unique' \| 'array-remove-matching' \| 'array-update-matching', value: any, updateValue?: any` | `void` | Update a key in the shared room storage from the server |
229
- | `createRoom()` | `initialStorage?: object, size?: number, host?: string` | `object` | Create a room (returns object containing room ID and state) – Rooms created with a non-player host like "server" (default) will not be deleted when the last participant leaves |
249
+ | `createRoom()` | `initialStorage?: object, size?: number, host?: string` | `object` | Create a room (returns object containing room ID and state)|
230
250
  | `destroyRoom()` | `roomId: string` | `void` | Destroy a room & kick all participants |
231
251
 
232
252
  ### Event types
@@ -239,7 +259,7 @@ new PlaySocket(options: PlaySocketServerOptions)
239
259
  | `clientJoinedRoom` | `clientId: string, roomId: string` | Client joined a room (clients can only leave by disconnecting) | - |
240
260
  | `clientJoinRequested` | `clientId: string, roomId: string` | Client requests to join a room | Return `false` or rejection reason `string` to block |
241
261
  | `roomCreated` | `roomId: string` | Client created a room | - |
242
- | `roomDestroyed` | `roomId: string` | Room was destroyed (happens when all participants leave) | - |
262
+ | `roomDestroyed` | `roomId: string` | Room was destroyed (happens when all participants leave, unless room host is "server") | - |
243
263
  | `roomCreationRequested` | `{clientId: string, initialStorage: object}` | Room creation requested by client | Return `object` to override initial storage, `false` to deny |
244
264
  | `storageUpdated` | `{clientId: string, roomId: string, update: object, storage: object}` | Room storage property updated | - |
245
265
  | `storageUpdateRequested` | `{clientId: string, roomId: string, update: object, storage: object}` | Room storage property update requested by client | Return `false` to block the update |
@@ -2119,9 +2119,10 @@ class PlaySocketServer {
2119
2119
  * @param {string} [options.path='/'] - WebSocket endpoint path
2120
2120
  * @param {boolean} [options.debug=false] - Enable debug logging
2121
2121
  * @param {number} [options.rateLimit=20] - Maximum number of operations per second per client
2122
+ * @param {Function} [options.verifyClient] - Optional callback to verify client connections before upgrade. Receives (info, callback) where info contains { req, origin } and callback is (verified, code?, message?) => void
2122
2123
  */
2123
2124
  constructor(options = {}) {
2124
- const { server, port = 3000, path = '/', debug = false, rateLimit = 20 } = options;
2125
+ const { server, port = 3000, path = '/', debug = false, rateLimit = 20, verifyClient } = options;
2125
2126
 
2126
2127
  if (debug) this.#debug = true; // Enable extra logging
2127
2128
 
@@ -2141,7 +2142,8 @@ class PlaySocketServer {
2141
2142
  // Create WebSocket server
2142
2143
  this.#wss = new WebSocketServer({
2143
2144
  server: this.#server,
2144
- path
2145
+ path,
2146
+ ...(verifyClient && { verifyClient })
2145
2147
  });
2146
2148
 
2147
2149
  // Set up ws event handlers
@@ -2257,6 +2259,9 @@ class PlaySocketServer {
2257
2259
  const formerRoomId = this.#clientRooms.get(data.id);
2258
2260
  const formerRoom = this.#rooms[formerRoomId];
2259
2261
  if (formerRoom) {
2262
+ // If the room has no host (they were the only one and disconnected), restore them as host
2263
+ if (formerRoom.host === null) formerRoom.host = data.id;
2264
+
2260
2265
  if (this.#debug) console.log(`State sent for reconnection for room ${formerRoomId}:`, formerRoom.crdtManager.getState);
2261
2266
  roomData = {
2262
2267
  state: formerRoom.crdtManager.getState,
@@ -2339,6 +2344,17 @@ class PlaySocketServer {
2339
2344
  version: this.#roomVersions.get(roomId)
2340
2345
  }), { binary: true });
2341
2346
 
2347
+ // If the room has no host (previous host disconnected and was the only participant), make joiner the host
2348
+ // Since the room had to be empty, only the joiner needs to be notified
2349
+ const becameHost = room.host === null;
2350
+ if (becameHost) {
2351
+ room.host = ws.clientId;
2352
+ ws.send(encode({
2353
+ type: 'host_migrated',
2354
+ newHost: room.host,
2355
+ }), { binary: true });
2356
+ }
2357
+
2342
2358
  // Notify existing participants
2343
2359
  room.participants.forEach(p => {
2344
2360
  if (p === ws.clientId) return;
@@ -2427,18 +2443,21 @@ class PlaySocketServer {
2427
2443
  */
2428
2444
  #changeHostIfDisconnected(roomId, clientId) {
2429
2445
  const room = this.#rooms[roomId];
2430
- if (room && room.host === clientId && room.participants.length > 1) {
2446
+ if (room && room.host === clientId) {
2431
2447
  const participantsWithoutClient = room.participants.filter((e) => e !== clientId);
2432
- room.host = participantsWithoutClient[0]; // Set new host
2433
2448
 
2434
- // Inform all participants about the new host
2435
- participantsWithoutClient.forEach(p => {
2436
- const client = this.#clients.get(p);
2437
- if (client) client.send(encode({
2438
- type: 'host_migrated',
2439
- newHost: room.host,
2440
- }), { binary: true });
2441
- });
2449
+ if (participantsWithoutClient.length >= 1) {
2450
+ room.host = participantsWithoutClient[0]; // Set new host
2451
+
2452
+ // Inform all participants about the new host
2453
+ participantsWithoutClient.forEach(p => {
2454
+ const client = this.#clients.get(p);
2455
+ if (client) client.send(encode({
2456
+ type: 'host_migrated',
2457
+ newHost: room.host,
2458
+ }), { binary: true });
2459
+ });
2460
+ } else room.host = null; // The next person to join will become the host
2442
2461
  }
2443
2462
  }
2444
2463
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playsocketjs",
3
- "version": "2.6.3",
3
+ "version": "2.7.0",
4
4
  "description": "WebSocket wrapper for creating simple multiplayer systems with ease.",
5
5
  "type": "module",
6
6
  "module": "dist/playsocket-client.js",