playsocketjs 2.5.4 → 2.6.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
@@ -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")
@@ -2185,7 +2185,7 @@ class PlaySocketServer {
2185
2185
  if (!ws.isTerminating) {
2186
2186
  ws.isTerminating = true; // Prevent multiple terminate calls (it is async)
2187
2187
  ws.terminate();
2188
- console.error(`Connection ${ws.connectionId} terminated due to rate limit violations.`);
2188
+ console.error(`PlaySocket connection ${ws.connectionId} terminated due to rate limit violations.`);
2189
2189
  return;
2190
2190
  }
2191
2191
  }
@@ -2193,7 +2193,7 @@ class PlaySocketServer {
2193
2193
  switch (data.type) {
2194
2194
  case 'register': {
2195
2195
  // Register client ID if provided & check for a duplicate
2196
- if (data.id && this.#clients.get(data.id)) {
2196
+ if (data.id && (this.#clients.get(data.id) || data.id === "server")) {
2197
2197
  ws.send(encode({ type: 'registration_failed', reason: 'ID is taken.' }), { binary: true });
2198
2198
  return;
2199
2199
  }
@@ -2274,8 +2274,6 @@ class PlaySocketServer {
2274
2274
  case 'create_room': {
2275
2275
  if (!ws.clientId) return;
2276
2276
 
2277
- let newRoomId;
2278
-
2279
2277
  if (this.#clientRooms.get(ws.clientId)) {
2280
2278
  ws.send(encode({
2281
2279
  type: 'room_creation_failed',
@@ -2284,24 +2282,8 @@ class PlaySocketServer {
2284
2282
  return;
2285
2283
  }
2286
2284
 
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
2285
  // Event callback with potential initial storage modifications
2304
- const reviewedStorage = await this.#triggerEvent("roomCreationRequested", { roomId: newRoomId, clientId: ws.clientId, initialStorage: structuredClone({ ...data.initialStorage }) });
2286
+ const reviewedStorage = await this.#triggerEvent("roomCreationRequested", { clientId: ws.clientId, initialStorage: structuredClone({ ...data.initialStorage }) });
2305
2287
  if (typeof reviewedStorage === 'object') data.initialStorage = reviewedStorage;
2306
2288
  if (reviewedStorage === false) {
2307
2289
  ws.send(encode({
@@ -2311,33 +2293,21 @@ class PlaySocketServer {
2311
2293
  return;
2312
2294
  }
2313
2295
 
2314
- // Check if client/creator is still connected
2296
+ // Check if client/creator is still connected - abort if not
2315
2297
  if (!this.#clients.has(ws.clientId)) {
2316
2298
  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
2299
+ return;
2318
2300
  }
2319
2301
 
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
- });
2302
+ try {
2303
+ const newRoom = this.createRoom(data.initialStorage, data.size, ws.clientId);
2304
+ this.#rooms[newRoom.id].participants.push(ws.clientId); // Add client to the room
2305
+ this.#clientRooms.set(ws.clientId, newRoom.id); // Add client to the client-room map
2306
+ ws.send(encode({ type: 'room_created', state: newRoom.state, roomId: newRoom.id }), { binary: true });
2307
+ } catch (error) {
2308
+ console.error("PlaySocket error creating room:", error);
2309
+ ws.send(encode({ type: 'room_creation_failed', reason: error.message }), { binary: true });
2325
2310
  }
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
2311
  break;
2342
2312
  }
2343
2313
 
@@ -2356,7 +2326,7 @@ class PlaySocketServer {
2356
2326
 
2357
2327
  if (!room) return rejectJoin("Room not found.");
2358
2328
  if (this.#clientRooms.get(ws.clientId)) return rejectJoin("Already in a room.");
2359
- if (room.participants.length >= room.maxSize) return rejectJoin("Room full.");
2329
+ if (room.participants.length >= room.size) return rejectJoin("Room full.");
2360
2330
 
2361
2331
  room.participants.push(ws.clientId);
2362
2332
  this.#clientRooms.set(ws.clientId, roomId);
@@ -2438,7 +2408,7 @@ class PlaySocketServer {
2438
2408
  break;
2439
2409
  }
2440
2410
  } catch (error) {
2441
- console.error('Error in message handler:', error);
2411
+ console.error('PlaySocket error in message handler:', error);
2442
2412
  }
2443
2413
  }
2444
2414
 
@@ -2557,11 +2527,8 @@ class PlaySocketServer {
2557
2527
  room.participants = room.participants.filter(p => p !== ws.clientId); // Remove client from room
2558
2528
  this.#clientRooms.delete(ws.clientId);
2559
2529
 
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 + ".");
2530
+ if (room.participants?.length === 0 && room.host !== "server") {
2531
+ this.destroyRoom(roomId); // Destroy room if now empty & not created by server
2565
2532
  roomId = null; // If room was destroyed, set roomId to null
2566
2533
  } else {
2567
2534
  // Notify remaining participants
@@ -2669,16 +2636,72 @@ class PlaySocketServer {
2669
2636
  }
2670
2637
  }
2671
2638
 
2639
+ /**
2640
+ * Create a room from the server
2641
+ * @param {object} [initialStorage] - Optional initial storage object
2642
+ * @param {number} [size] - Max. room size, up to 100 (defaults to 100)
2643
+ * @param {string} [host] - Host ID, defaults to "server" (when set to "server", room will not be deleted if all clients leave)
2644
+ * @returns {object} - Object containing room state and room ID
2645
+ */
2646
+ createRoom(initialStorage, size, host = "server") {
2647
+ let newRoomId;
2648
+
2649
+ for (let i = 0; i < 100; i++) {
2650
+ const id = this.#generateId();
2651
+ if (!this.#rooms[id]) {
2652
+ newRoomId = id;
2653
+ break;
2654
+ }
2655
+ }
2656
+
2657
+ if (!newRoomId) throw new Error("No available ID found.")
2658
+ const roomCrdtManager = new CRDTManager(this.#debug);
2659
+
2660
+ if (initialStorage) Object.entries(initialStorage)?.forEach(([key, value]) => {
2661
+ roomCrdtManager.updateProperty(key, "set", value);
2662
+ });
2663
+
2664
+ this.#roomVersions.set(newRoomId, 0);
2665
+ this.#rooms[newRoomId] = {
2666
+ participants: [],
2667
+ host,
2668
+ size: Math.min(Number(size), 100) || 100,
2669
+ crdtManager: roomCrdtManager
2670
+ };
2671
+
2672
+ this.#triggerEvent("roomCreated", newRoomId);
2673
+ if (this.#debug) console.log(`Room ${newRoomId} created with initial storage:`, initialStorage);
2674
+ return { state: roomCrdtManager.getState, id: newRoomId };
2675
+ }
2676
+
2677
+ /**
2678
+ * Destroy a room
2679
+ * @param {string} roomId - Room ID
2680
+ */
2681
+ destroyRoom(roomId) {
2682
+ const room = this.#rooms[roomId];
2683
+ if (!room) return; // Return if room does not exist
2684
+
2685
+ // Disconnect participants if still in room
2686
+ const participants = room.participants;
2687
+ participants.forEach((clientId) => {
2688
+ this.kick(clientId, "Room destroyed by server.");
2689
+ });
2690
+
2691
+ // Delete the room
2692
+ delete this.#rooms[roomId];
2693
+ this.#roomVersions.delete(roomId); // Delete room version (used to ensure all clients are up-2-date)
2694
+ this.#triggerEvent("roomDestroyed", roomId);
2695
+ if (this.#debug) console.log("Deleted room with id " + roomId + ".");
2696
+ }
2697
+
2672
2698
  /**
2673
2699
  * Close all client connections, then close the websocket and http server
2674
2700
  */
2675
2701
  stop() {
2676
2702
  clearInterval(this.#heartbeatInterval);
2677
2703
  if (this.#wss) {
2678
- this.#clients.forEach(client => {
2679
- client.send(encode({ type: 'server_stopped' }), { binary: true });
2680
- client.close();
2681
- });
2704
+ this.#clients.forEach((client, clientId) => this.kick(clientId, "Server restart."));
2682
2705
  this.#wss.close();
2683
2706
  }
2684
2707
  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.4",
3
+ "version": "2.6.0",
4
4
  "description": "WebSocket wrapper for creating simple multiplayer systems with ease.",
5
5
  "type": "module",
6
6
  "module": "dist/playsocket-client.js",