playsocketjs 2.1.1 → 2.2.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
@@ -72,6 +72,11 @@ socket.updateStorage('players', 'array-add-unique', { username: 'Player4', level
72
72
  socket.updateStorage('latestPlayer', 'set', 'Player4'); // Regular synced storage update
73
73
  ```
74
74
 
75
+ Sending traditional requests to the server:
76
+ ```javascript
77
+ socket.sendRequest('chosen-request-name', { fact: "You can build server-authoritative logic using this!" })
78
+ ```
79
+
75
80
  ## API Reference
76
81
 
77
82
  ### Constructor
@@ -91,10 +96,11 @@ new PlaySocket(id?: string, options: PlaySocketOptions)
91
96
  ### Methods
92
97
 
93
98
  - `init()`: Initialize the WebSocket connection – Returns Promise (async) which resolves with the client's ID
94
- - `createRoom(initialStorage?: object, maxSize?: number)`: Create a new room and become host – Returns Promise (async) which resolves with the room ID (matches the creator's ID)
99
+ - `createRoom(initialStorage?: object, maxSize?: number)`: Create a new room and become host – Returns Promise (async) which resolves with the room ID (matches the creator's ID). The absolute client maximum is 100
95
100
  - `joinRoom(hostId: string)`: Join an existing room – Returns Promise (async)
96
101
  - `destroy()`: Use this to leave a room and close the connection
97
- - `updateStorage(key: string, type: 'set' | 'array-add' | 'array-add-unique' | 'array-remove-matching' | 'array-update-matching', value: any, updateValue?: any)`: Update the shared storage. Safely update arrays in storage by adding, removing, or updating items. UpdateValue is only required for the 'array-update-matching' operation type
102
+ - `updateStorage(key: string, type: 'set' | 'array-add' | 'array-add-unique' | 'array-remove-matching' | 'array-update-matching', value: any, updateValue?: any)`: Update the shared storage (max. 100 keys). Safely update arrays in storage by adding, removing, or updating items. UpdateValue is only required for the 'array-update-matching' operation type
103
+ - `sendRequest(name: string, data?: any)`: Send requests to the server with optional custom data (handle these in the `requestReceived` server event)
98
104
  - `onEvent(event: string, callback: Function)`: Register an event callback
99
105
 
100
106
  #### Event types
@@ -111,7 +117,7 @@ new PlaySocket(id?: string, options: PlaySocketOptions)
111
117
 
112
118
  - `id`: Client's unique identifier on the WebSocket server
113
119
  - `isHost`: If this user is currently assigned the host role
114
- - `connectionCount`: Number of active client connections in room (without you)
120
+ - `connectionCount`: Number of active client connections in room (without yourself)
115
121
  - `getStorage`: Retrieve storage object
116
122
 
117
123
   
@@ -210,12 +216,14 @@ Creates a new PlaySocket Server instance with configuration options.
210
216
  #### Event types
211
217
 
212
218
  - `clientRegistered`: Client registered with the server (returns the client's ID `string`, customData `object`)
219
+ - `clientRegistrationRequested`: Client requests to register (returns the client's ID `string`, customData `object`) – return `false` or a rejection reason `string` to block the registration
213
220
  - `clientDisconnected`: Client disconnected from the server (returns the client's ID `string`)
214
221
  - `clientJoinedRoom`: Client joined a room – note that clients can only leave by disconnecting (returns the client's ID `string`, room ID `string`)
215
222
  - `roomCreated`: Client created a room (returns room ID `string`)
216
223
  - `roomDestroyed`: Room was destroyed, this happens when all participants leave (returns room ID `string`)
217
224
  - `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
218
225
  - `storageUpdateRequested`: Room storage property update requested by client (returns `object` containing the client's ID `string`, room ID `string` and the update `object`) – if you return `false` in the callback, the update will be blocked
226
+ - `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`)
219
227
 
220
228
  ### Properties (Read-only)
221
229
 
@@ -1758,9 +1758,11 @@ function requireCrdtManager () {
1758
1758
  const operation = this.#sanitizeValue(rawOperation);
1759
1759
  if (this.#debug) console.log(CONSOLE_PREFIX + "Importing update:", data); // Debug
1760
1760
 
1761
+ // Check key limit to afeguard against too many keys
1762
+ if (this.#keyOperations.size >= 100 && !this.#keyOperations.has(key)) throw new Error('Key limit exceeded!');
1763
+
1761
1764
  // Get COPY of current ops (or empty array if none yet)
1762
1765
  const currentOps = [...(this.#keyOperations.get(key) || [])];
1763
-
1764
1766
  // Merge vector clocks (always take max value)
1765
1767
  for (const [id, counter] of vectorClock) {
1766
1768
  if (!this.#vectorClock.has(id) || this.#vectorClock.get(id) < counter) {
@@ -1768,7 +1770,7 @@ function requireCrdtManager () {
1768
1770
  }
1769
1771
  }
1770
1772
 
1771
- // Saveguard
1773
+ // Safeguard against massive vector clocks
1772
1774
  if (this.#vectorClock.size > 1000) this.#vectorClock = new Map([...this.#vectorClock].slice(-100));
1773
1775
 
1774
1776
  // Add new operation if it's not already added
@@ -2019,11 +2021,15 @@ function requireCrdtManager () {
2019
2021
  }
2020
2022
 
2021
2023
  /**
2022
- * Remove HTML to prevent XSS
2023
- * @param {Object} obj
2024
+ * Remove HTML to prevent XSS and enforce size limits
2025
+ * @param {Object} obj
2024
2026
  * @returns {Object} - Sanitized object
2025
2027
  */
2026
2028
  #sanitizeValue(obj) {
2029
+ // Check total serialized size
2030
+ const jsonString = JSON.stringify(obj);
2031
+ if (jsonString?.length > 50000) throw new Error('Value too large!'); // 50KB limit
2032
+
2027
2033
  if (typeof obj === 'string') return (obj.includes('<') || obj.includes('>')) ? obj.replace(/[<>]/g, '') : obj;
2028
2034
  if (Array.isArray(obj)) return obj.map(item => this.#sanitizeValue(item));
2029
2035
  if (obj && typeof obj === 'object') return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, this.#sanitizeValue(v)]));
@@ -2227,6 +2233,22 @@ class PlaySocket {
2227
2233
  if (!message.type) return;
2228
2234
 
2229
2235
  switch (message.type) {
2236
+ case 'registration_failed':
2237
+ if (this.#pendingRegistration) {
2238
+ this.#pendingRegistration.reject(new Error("Failed to register: " + message.reason));
2239
+ this.#triggerEvent("error", "Failed to register: " + message.reason);
2240
+ }
2241
+ break;
2242
+
2243
+ case 'registered':
2244
+ if (this.#pendingRegistration) {
2245
+ this.#sessionToken = message.sessionToken;
2246
+ this.#initialized = true;
2247
+ this.#pendingRegistration.resolve(message.id);
2248
+ this.#triggerEvent("status", "Connected to server.");
2249
+ }
2250
+ break;
2251
+
2230
2252
  case 'join_accepted':
2231
2253
  // Connected to room
2232
2254
  if (this.#pendingJoin) {
@@ -2293,22 +2315,6 @@ class PlaySocket {
2293
2315
  }
2294
2316
  break;
2295
2317
 
2296
- case 'id_taken':
2297
- if (this.#pendingRegistration) {
2298
- this.#pendingRegistration.reject(new Error("This id is already in use"));
2299
- this.#triggerEvent("error", "This id is taken.");
2300
- }
2301
- break;
2302
-
2303
- case 'registered':
2304
- if (this.#pendingRegistration) {
2305
- this.#sessionToken = message.sessionToken;
2306
- this.#initialized = true;
2307
- this.#pendingRegistration.resolve(message.id);
2308
- this.#triggerEvent("status", "Connected to server.");
2309
- }
2310
- break;
2311
-
2312
2318
  case 'property_updated':
2313
2319
  this.#roomVersion++; // Increment room version
2314
2320
  if (this.#debug) console.log(LOG_PREFIX + "Property update received:", message.update);
@@ -1884,9 +1884,11 @@ function requireCrdtManager () {
1884
1884
  const operation = this.#sanitizeValue(rawOperation);
1885
1885
  if (this.#debug) console.log(CONSOLE_PREFIX + "Importing update:", data); // Debug
1886
1886
 
1887
+ // Check key limit to afeguard against too many keys
1888
+ if (this.#keyOperations.size >= 100 && !this.#keyOperations.has(key)) throw new Error('Key limit exceeded!');
1889
+
1887
1890
  // Get COPY of current ops (or empty array if none yet)
1888
1891
  const currentOps = [...(this.#keyOperations.get(key) || [])];
1889
-
1890
1892
  // Merge vector clocks (always take max value)
1891
1893
  for (const [id, counter] of vectorClock) {
1892
1894
  if (!this.#vectorClock.has(id) || this.#vectorClock.get(id) < counter) {
@@ -1894,7 +1896,7 @@ function requireCrdtManager () {
1894
1896
  }
1895
1897
  }
1896
1898
 
1897
- // Saveguard
1899
+ // Safeguard against massive vector clocks
1898
1900
  if (this.#vectorClock.size > 1000) this.#vectorClock = new Map([...this.#vectorClock].slice(-100));
1899
1901
 
1900
1902
  // Add new operation if it's not already added
@@ -2145,11 +2147,15 @@ function requireCrdtManager () {
2145
2147
  }
2146
2148
 
2147
2149
  /**
2148
- * Remove HTML to prevent XSS
2149
- * @param {Object} obj
2150
+ * Remove HTML to prevent XSS and enforce size limits
2151
+ * @param {Object} obj
2150
2152
  * @returns {Object} - Sanitized object
2151
2153
  */
2152
2154
  #sanitizeValue(obj) {
2155
+ // Check total serialized size
2156
+ const jsonString = JSON.stringify(obj);
2157
+ if (jsonString?.length > 50000) throw new Error('Value too large!'); // 50KB limit
2158
+
2153
2159
  if (typeof obj === 'string') return (obj.includes('<') || obj.includes('>')) ? obj.replace(/[<>]/g, '') : obj;
2154
2160
  if (Array.isArray(obj)) return obj.map(item => this.#sanitizeValue(item));
2155
2161
  if (obj && typeof obj === 'object') return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, this.#sanitizeValue(v)]));
@@ -2299,7 +2305,7 @@ function requireServer () {
2299
2305
  case 'register':
2300
2306
  // Register client ID if provided & check for a duplicate
2301
2307
  if (data.id && this.#clients.get(data.id)) {
2302
- ws.send(encode({ type: 'id_taken' }), { binary: true });
2308
+ ws.send(encode({ type: 'registration_failed', reason: 'ID is taken.' }), { binary: true });
2303
2309
  return;
2304
2310
  }
2305
2311
 
@@ -2315,11 +2321,20 @@ function requireServer () {
2315
2321
  }
2316
2322
  }
2317
2323
  if (!data.id) {
2318
- ws.send(encode({ type: 'id_taken' }), { binary: true });
2324
+ ws.send(encode({ type: 'registration_failed', reason: 'No available ID found.' }), { binary: true });
2319
2325
  throw new Error('Failed to generate unique ID!');
2320
2326
  }
2321
2327
  }
2322
2328
 
2329
+ // Event callback
2330
+ const registrationAllowed = await this.#triggerEvent("clientRegistrationRequested", data.id, data.customData);
2331
+ if (registrationAllowed === false || typeof registrationAllowed === 'string') {
2332
+ ws.send(encode({
2333
+ type: 'registration_failed',
2334
+ reason: typeof registrationAllowed === 'string' ? registrationAllowed : 'Denied.'
2335
+ }), { binary: true });
2336
+ return;
2337
+ }
2323
2338
 
2324
2339
  ws.clientId = data.id; // Adds the provided id to the ws object as clientId
2325
2340
  this.#clients.set(data.id, ws);
@@ -2403,7 +2418,7 @@ function requireServer () {
2403
2418
  this.#rooms[newRoomId] = {
2404
2419
  participants: [ws.clientId],
2405
2420
  host: ws.clientId,
2406
- maxSize: data.size || null,
2421
+ maxSize: Math.min(Number(data.size), 100) || 100, // Max. limit is 100 clients / room
2407
2422
  crdtManager: roomCrdtManager
2408
2423
  };
2409
2424
  this.#clientRooms.set(ws.clientId, newRoomId); // Add client to the room
@@ -2419,12 +2434,12 @@ function requireServer () {
2419
2434
  const room = this.#rooms[roomId];
2420
2435
 
2421
2436
  if (!room ||
2422
- (room.maxSize && room.participants.length >= room.maxSize) ||
2437
+ (room.participants.length >= room.maxSize) ||
2423
2438
  this.#clientRooms.get(ws.clientId)) {
2424
2439
  ws.send(encode({
2425
2440
  type: 'join_rejected',
2426
2441
  reason: !room ? 'Room not found' :
2427
- room.maxSize && room.participants.length >= room.maxSize ? 'Room full' :
2442
+ room.participants.length >= room.maxSize ? 'Room full' :
2428
2443
  'Already in a room'
2429
2444
  }), { binary: true });
2430
2445
  return;
@@ -2464,8 +2479,8 @@ function requireServer () {
2464
2479
 
2465
2480
  if (updateRoom && data.update) {
2466
2481
  // Check if update is allowed via event callback (provide clone to ensure update integrity)
2467
- const allowed = await this.#triggerEvent("storageUpdateRequested", { roomId: updateRoomId, clientId: ws.clientId, update: structuredClone(data.update) });
2468
- if (allowed === false) {
2482
+ const updateAllowed = await this.#triggerEvent("storageUpdateRequested", { roomId: updateRoomId, clientId: ws.clientId, update: structuredClone(data.update) });
2483
+ if (updateAllowed === false) {
2469
2484
  ws.send(encode({
2470
2485
  type: 'property_update_rejected',
2471
2486
  state: updateRoom.crdtManager.getState
@@ -2638,7 +2653,7 @@ function requireServer () {
2638
2653
  * @param {Function} callback - Callback function
2639
2654
  */
2640
2655
  onEvent(event, callback) {
2641
- const validEvents = ["clientRegistered", "clientDisconnected", "clientJoinedRoom", "roomCreationRequested", "requestReceived", "roomCreated", "storageUpdateRequested", "roomDestroyed"];
2656
+ const validEvents = ["clientRegistered", "clientRegistrationRequested", "clientDisconnected", "clientJoinedRoom", "roomCreationRequested", "requestReceived", "roomCreated", "storageUpdateRequested", "roomDestroyed"];
2642
2657
  if (!validEvents.includes(event)) return console.warn(`Invalid PlaySocket event type "${event}"`);
2643
2658
  if (!this.#callbacks.has(event)) this.#callbacks.set(event, []);
2644
2659
  this.#callbacks.get(event).push(callback);
@@ -2669,8 +2684,7 @@ function requireServer () {
2669
2684
  for (const callback of callbacks) {
2670
2685
  try {
2671
2686
  const result = await callback(...args);
2672
- if (result === false) return false; // False = block (in supported events)
2673
- if (typeof result === 'object') return result; // If response object is provided -> pass on
2687
+ if (result != null) return result; // Return any non-null/undefined result
2674
2688
  } catch (error) {
2675
2689
  console.error(`PlaySocket ${event} callback error:`, error);
2676
2690
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playsocketjs",
3
- "version": "2.1.1",
3
+ "version": "2.2.1",
4
4
  "description": "WebSocket wrapper for creating simple multiplayer systems with ease.",
5
5
  "module": "dist/playsocket-client.js",
6
6
  "exports": {