playsocketjs 2.5.5 → 2.6.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/README.md CHANGED
@@ -96,7 +96,7 @@ new PlaySocket(id?: string, options: PlaySocketOptions)
96
96
  ### Methods
97
97
 
98
98
  - `init()`: Initialize the WebSocket connection – Returns Promise (async) which resolves with the client's ID
99
- - `createRoom(initialStorage?: object, maxSize?: number)`: Create a new room and become host – Returns Promise (async) which resolves with the room ID. The room participant maximum is 100
99
+ - `createRoom(initialStorage?: object, size?: number)`: Create a new room and become host – Returns Promise (async) which resolves with the room ID. The room participant maximum is 100
100
100
  - `joinRoom(hostId: string)`: Join an existing room – Returns Promise (async)
101
101
  - `destroy()`: Use this to leave a room and close the connection
102
102
  - `updateStorage(key: string, type: 'set' | 'array-add' | 'array-add-unique' | 'array-remove-matching' | 'array-update-matching', value: any, updateValue?: any)`: Update a key in the shared storage (max. 100 keys). Array operation types allow for conflict-free simultaneous array updates. For '-matching' operations, value becomes the value to match, and updateValue the replacement.
@@ -213,6 +213,8 @@ Creates a new PlaySocket Server instance with configuration options.
213
213
  - `onEvent(event: string, callback: Function)`: Register a server-side event callback
214
214
  - `getRoomStorage(roomId: string)`: Get a snapshot of the current room storage (returns storage `object`)
215
215
  - `updateRoomStorage(roomId: string, key: string, type: 'set' | 'array-add' | 'array-add-unique' | 'array-remove-matching' | 'array-update-matching', value: any, updateValue?: any)`: Update a key in the shared room storage from the server.
216
+ - `createRoom(initialStorage?: object, size?: number, host?: string)`: 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
217
+ - `destroyRoom(roomId: string)`: Destroy a room & kick all participants
216
218
 
217
219
  #### Event types
218
220
 
@@ -223,7 +225,7 @@ Creates a new PlaySocket Server instance with configuration options.
223
225
  - `clientJoinRequested`: Client requests to join a room (returns the client's ID `string`, room ID `string`) – return `false` or a rejection reason `string` to block them from joining
224
226
  - `roomCreated`: Client created a room (returns room ID `string`)
225
227
  - `roomDestroyed`: Room was destroyed, this happens when all participants leave (returns room ID `string`)
226
- - `roomCreationRequested`: Room creation requested by client (returns `object` containing the client's ID `string`, room ID `string` and the initialStorage `object`) – if you return an `object` in the callback, it will take that as the initial storage instead. If you return `false`, the creation will be denied
228
+ - `roomCreationRequested`: Room creation requested by client (returns `object` containing the client's ID `string` and the initialStorage `object`) – if you return an `object` in the callback, it will take that as the initial storage instead. If you return `false`, the creation will be denied
227
229
  - `storageUpdated`: Room storage property updated (returns `object` containing the ID of the client who requested the update `string`, room ID `string`, the update `object`, and the storage `object`)
228
230
  - `storageUpdateRequested`: Room storage property update requested by client (returns `object` containing the client's ID `string`, room ID `string`, the update `object`, and the current storage `object` that hasn't been updated yet) – if you return `false` in the callback, the update will be blocked
229
231
  - `requestReceived`: Request from client was received by the server (returns `object` containing client's ID `string`, if in room – room ID `string`, request name `string` and optional passed data of type `any`)
@@ -2312,7 +2312,7 @@ class PlaySocket {
2312
2312
  case 'room_created':
2313
2313
  if (this.#pendingCreate) {
2314
2314
  this.#inRoom = true;
2315
- this.#triggerEvent("status", `Room created with max. size ${message.size}.`);
2315
+ this.#triggerEvent("status", `Room created.`);
2316
2316
  this.#crdtManager.importState(message.state);
2317
2317
  this.#triggerEvent("storageUpdated", this.getStorage);
2318
2318
  this.#pendingCreate.resolve(message.roomId);
@@ -2343,11 +2343,6 @@ class PlaySocket {
2343
2343
  this.#triggerEvent("storageUpdated", this.getStorage);
2344
2344
  break;
2345
2345
 
2346
- case 'server_stopped':
2347
- this.#triggerEvent("error", "Server restart.");
2348
- this.destroy();
2349
- break;
2350
-
2351
2346
  case 'kicked':
2352
2347
  this.#triggerEvent("error", `Kicked out of room: ${message.reason || "No reason provided."}`);
2353
2348
  this.destroy();
@@ -2458,10 +2453,10 @@ class PlaySocket {
2458
2453
  /**
2459
2454
  * Create a new room and become host
2460
2455
  * @param {object} initialStorage - Initial state
2461
- * @param {number} maxSize - Max number of participants
2456
+ * @param {number} size - Max number of participants
2462
2457
  * @returns {Promise} Resolves with room ID
2463
2458
  */
2464
- async createRoom(initialStorage = {}, maxSize) {
2459
+ async createRoom(initialStorage = {}, size) {
2465
2460
  if (!this.#initialized) {
2466
2461
  this.#triggerEvent("error", "Cannot create room - not initialized");
2467
2462
  return Promise.reject(new Error("Not initialized"));
@@ -2474,7 +2469,7 @@ class PlaySocket {
2474
2469
  this.#sendToServer({
2475
2470
  type: 'create_room',
2476
2471
  initialStorage,
2477
- size: maxSize
2472
+ size
2478
2473
  });
2479
2474
  }),
2480
2475
  this.#createTimeout("Room creation")
@@ -2083,6 +2083,8 @@ class CRDTManager {
2083
2083
 
2084
2084
  /* eslint-disable jsdoc/require-returns */
2085
2085
 
2086
+ const MAX_ROOM_SIZE = 500;
2087
+
2086
2088
  /**
2087
2089
  * @typedef {import('node:http').Server} HttpServer
2088
2090
  * @typedef {import('ws').WebSocket} WebSocket
@@ -2185,7 +2187,7 @@ class PlaySocketServer {
2185
2187
  if (!ws.isTerminating) {
2186
2188
  ws.isTerminating = true; // Prevent multiple terminate calls (it is async)
2187
2189
  ws.terminate();
2188
- console.error(`Connection ${ws.connectionId} terminated due to rate limit violations.`);
2190
+ console.error(`PlaySocket connection ${ws.connectionId} terminated due to rate limit violations.`);
2189
2191
  return;
2190
2192
  }
2191
2193
  }
@@ -2193,7 +2195,7 @@ class PlaySocketServer {
2193
2195
  switch (data.type) {
2194
2196
  case 'register': {
2195
2197
  // Register client ID if provided & check for a duplicate
2196
- if (data.id && this.#clients.get(data.id)) {
2198
+ if (data.id && (this.#clients.get(data.id) || data.id === "server")) {
2197
2199
  ws.send(encode({ type: 'registration_failed', reason: 'ID is taken.' }), { binary: true });
2198
2200
  return;
2199
2201
  }
@@ -2274,8 +2276,6 @@ class PlaySocketServer {
2274
2276
  case 'create_room': {
2275
2277
  if (!ws.clientId) return;
2276
2278
 
2277
- let newRoomId;
2278
-
2279
2279
  if (this.#clientRooms.get(ws.clientId)) {
2280
2280
  ws.send(encode({
2281
2281
  type: 'room_creation_failed',
@@ -2284,24 +2284,8 @@ class PlaySocketServer {
2284
2284
  return;
2285
2285
  }
2286
2286
 
2287
- // Generate room ID
2288
- for (let i = 0; i < 50; i++) {
2289
- const id = this.#generateId();
2290
- if (!this.#rooms[id]) {
2291
- newRoomId = id;
2292
- break;
2293
- }
2294
- }
2295
-
2296
- if (!newRoomId) {
2297
- ws.send(encode({ type: 'room_creation_failed', reason: 'No available ID found.' }), { binary: true });
2298
- throw new Error('Failed to generate unique room ID!');
2299
- }
2300
-
2301
- const roomCrdtManager = new CRDTManager(this.#debug); // Create the room's crdt manager
2302
-
2303
2287
  // Event callback with potential initial storage modifications
2304
- const reviewedStorage = await this.#triggerEvent("roomCreationRequested", { roomId: newRoomId, clientId: ws.clientId, initialStorage: structuredClone({ ...data.initialStorage }) });
2288
+ const reviewedStorage = await this.#triggerEvent("roomCreationRequested", { clientId: ws.clientId, initialStorage: structuredClone({ ...data.initialStorage }) });
2305
2289
  if (typeof reviewedStorage === 'object') data.initialStorage = reviewedStorage;
2306
2290
  if (reviewedStorage === false) {
2307
2291
  ws.send(encode({
@@ -2311,33 +2295,21 @@ class PlaySocketServer {
2311
2295
  return;
2312
2296
  }
2313
2297
 
2314
- // Check if client/creator is still connected
2298
+ // Check if client/creator is still connected - abort if not
2315
2299
  if (!this.#clients.has(ws.clientId)) {
2316
2300
  if (this.#debug) console.log(`Room creation cancelled - client ${ws.clientId} disconnected during event callback.`);
2317
- return; // Abort room creation if client is no longer connected
2301
+ return;
2318
2302
  }
2319
2303
 
2320
- // Load initial storage if provided
2321
- if (data.initialStorage) {
2322
- Object.entries(data.initialStorage)?.forEach(([key, value]) => {
2323
- roomCrdtManager.updateProperty(key, "set", value);
2324
- });
2304
+ try {
2305
+ const newRoom = this.createRoom(data.initialStorage, data.size, ws.clientId);
2306
+ this.#rooms[newRoom.id].participants.push(ws.clientId); // Add client to the room
2307
+ this.#clientRooms.set(ws.clientId, newRoom.id); // Add client to the client-room map
2308
+ ws.send(encode({ type: 'room_created', state: newRoom.state, roomId: newRoom.id }), { binary: true });
2309
+ } catch (error) {
2310
+ console.error("PlaySocket error creating room:", error);
2311
+ ws.send(encode({ type: 'room_creation_failed', reason: error.message }), { binary: true });
2325
2312
  }
2326
-
2327
- // Create room
2328
- const maxSize = Math.min(Number(data.size), 100) || 100; // Max. limit is 100 clients / room
2329
- this.#roomVersions.set(newRoomId, 0); // Start with version 0
2330
- this.#rooms[newRoomId] = {
2331
- participants: [ws.clientId],
2332
- host: ws.clientId,
2333
- maxSize,
2334
- crdtManager: roomCrdtManager
2335
- };
2336
- this.#clientRooms.set(ws.clientId, newRoomId); // Add client to the room
2337
-
2338
- ws.send(encode({ type: 'room_created', state: roomCrdtManager.getState, roomId: newRoomId, size: maxSize }), { binary: true });
2339
- this.#triggerEvent("roomCreated", newRoomId);
2340
- if (this.#debug) console.log(`Room ${newRoomId} created with initial storage:`, data.initialStorage);
2341
2313
  break;
2342
2314
  }
2343
2315
 
@@ -2356,7 +2328,7 @@ class PlaySocketServer {
2356
2328
 
2357
2329
  if (!room) return rejectJoin("Room not found.");
2358
2330
  if (this.#clientRooms.get(ws.clientId)) return rejectJoin("Already in a room.");
2359
- if (room.participants.length >= room.maxSize) return rejectJoin("Room full.");
2331
+ if (room.participants.length >= room.size) return rejectJoin("Room full.");
2360
2332
 
2361
2333
  room.participants.push(ws.clientId);
2362
2334
  this.#clientRooms.set(ws.clientId, roomId);
@@ -2438,7 +2410,7 @@ class PlaySocketServer {
2438
2410
  break;
2439
2411
  }
2440
2412
  } catch (error) {
2441
- console.error('Error in message handler:', error);
2413
+ console.error('PlaySocket error in message handler:', error);
2442
2414
  }
2443
2415
  }
2444
2416
 
@@ -2557,11 +2529,8 @@ class PlaySocketServer {
2557
2529
  room.participants = room.participants.filter(p => p !== ws.clientId); // Remove client from room
2558
2530
  this.#clientRooms.delete(ws.clientId);
2559
2531
 
2560
- if (room.participants?.length === 0) {
2561
- delete this.#rooms[roomId]; // Delete room if now empty
2562
- this.#roomVersions.delete(roomId); // Delete room version
2563
- this.#triggerEvent("roomDestroyed", roomId);
2564
- if (this.#debug) console.log("Deleted room with id " + roomId + ".");
2532
+ if (room.participants?.length === 0 && room.host !== "server") {
2533
+ this.destroyRoom(roomId); // Destroy room if now empty & not created by server
2565
2534
  roomId = null; // If room was destroyed, set roomId to null
2566
2535
  } else {
2567
2536
  // Notify remaining participants
@@ -2669,16 +2638,72 @@ class PlaySocketServer {
2669
2638
  }
2670
2639
  }
2671
2640
 
2641
+ /**
2642
+ * Create a room from the server
2643
+ * @param {object} [initialStorage] - Optional initial storage object
2644
+ * @param {number} [size] - Max. room size, up to 500
2645
+ * @param {string} [host] - Host ID, defaults to "server" (when set to "server", room will not be deleted if all clients leave)
2646
+ * @returns {object} Object containing room state and room ID
2647
+ */
2648
+ createRoom(initialStorage, size, host = "server") {
2649
+ let newRoomId;
2650
+
2651
+ for (let i = 0; i < 100; i++) {
2652
+ const id = this.#generateId();
2653
+ if (!this.#rooms[id]) {
2654
+ newRoomId = id;
2655
+ break;
2656
+ }
2657
+ }
2658
+
2659
+ if (!newRoomId) throw new Error("No available ID found.")
2660
+ const roomCrdtManager = new CRDTManager(this.#debug);
2661
+
2662
+ if (initialStorage) Object.entries(initialStorage)?.forEach(([key, value]) => {
2663
+ roomCrdtManager.updateProperty(key, "set", value);
2664
+ });
2665
+
2666
+ this.#roomVersions.set(newRoomId, 0);
2667
+ this.#rooms[newRoomId] = {
2668
+ participants: [],
2669
+ host,
2670
+ size: Math.min(Number(size), MAX_ROOM_SIZE) || MAX_ROOM_SIZE,
2671
+ crdtManager: roomCrdtManager
2672
+ };
2673
+
2674
+ this.#triggerEvent("roomCreated", newRoomId);
2675
+ if (this.#debug) console.log(`Room ${newRoomId} created with initial storage:`, initialStorage);
2676
+ return { state: roomCrdtManager.getState, id: newRoomId };
2677
+ }
2678
+
2679
+ /**
2680
+ * Destroy a room
2681
+ * @param {string} roomId - Room ID
2682
+ */
2683
+ destroyRoom(roomId) {
2684
+ const room = this.#rooms[roomId];
2685
+ if (!room) return; // Return if room does not exist
2686
+
2687
+ // Disconnect participants if still in room
2688
+ const participants = room.participants;
2689
+ participants.forEach((clientId) => {
2690
+ this.kick(clientId, "Room destroyed by server.");
2691
+ });
2692
+
2693
+ // Delete the room
2694
+ delete this.#rooms[roomId];
2695
+ this.#roomVersions.delete(roomId); // Delete room version (used to ensure all clients are up-2-date)
2696
+ this.#triggerEvent("roomDestroyed", roomId);
2697
+ if (this.#debug) console.log("Deleted room with id " + roomId + ".");
2698
+ }
2699
+
2672
2700
  /**
2673
2701
  * Close all client connections, then close the websocket and http server
2674
2702
  */
2675
2703
  stop() {
2676
2704
  clearInterval(this.#heartbeatInterval);
2677
2705
  if (this.#wss) {
2678
- this.#clients.forEach(client => {
2679
- client.send(encode({ type: 'server_stopped' }), { binary: true });
2680
- client.close();
2681
- });
2706
+ this.#clients.forEach((client, clientId) => this.kick(clientId, "Server restart."));
2682
2707
  this.#wss.close();
2683
2708
  }
2684
2709
  if (this.#server && this.#ownsServer) this.#server.close(() => { console.log('PlaySocket server stopped.'); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playsocketjs",
3
- "version": "2.5.5",
3
+ "version": "2.6.1",
4
4
  "description": "WebSocket wrapper for creating simple multiplayer systems with ease.",
5
5
  "type": "module",
6
6
  "module": "dist/playsocket-client.js",