playsocketjs 2.5.2 → 2.5.4

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
@@ -99,7 +99,7 @@ new PlaySocket(id?: string, options: PlaySocketOptions)
99
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
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
- - `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
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.
103
103
  - `sendRequest(name: string, data?: any)`: Send requests to the server with optional custom data (handle these in the `requestReceived` server event)
104
104
  - `onEvent(event: string, callback: Function)`: Register an event callback
105
105
 
@@ -212,7 +212,7 @@ Creates a new PlaySocket Server instance with configuration options.
212
212
  - `kick(clientId: string, reason?: string)`: Kick a client by their clientID – this will close their connection and set an error message
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
- - `updateRoomStorage(roomId: string, key: string, type: 'set' | 'array-add' | 'array-add-unique' | 'array-remove-matching' | 'array-update-matching', value: any, updateValue?: any)`: Update the shared room storage from the server.
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
216
 
217
217
  #### Event types
218
218
 
@@ -225,7 +225,7 @@ Creates a new PlaySocket Server instance with configuration options.
225
225
  - `roomDestroyed`: Room was destroyed, this happens when all participants leave (returns room ID `string`)
226
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
227
227
  - `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
- - `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
228
+ - `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
229
  - `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`)
230
230
 
231
231
  ### Properties (Read-only)
@@ -1683,13 +1683,15 @@ function decode(buffer, options) {
1683
1683
 
1684
1684
  const CONSOLE_PREFIX = "PlaySocket CRDT manager: ";
1685
1685
 
1686
+ /**
1687
+ * CRDT Manager
1688
+ */
1686
1689
  class CRDTManager {
1687
1690
  // Storage
1688
1691
  #replicaId;
1689
1692
  #keyOperations = new Map();
1690
1693
  #vectorClock = new Map();
1691
1694
 
1692
- // Local only
1693
1695
  #propertyStore = {}; // Current local values per key, as object
1694
1696
  #lastPropertyStore = {}; // Last property store to compare against
1695
1697
 
@@ -1702,6 +1704,7 @@ class CRDTManager {
1702
1704
 
1703
1705
  /**
1704
1706
  * Create a new instance
1707
+ * @param {boolean} [debug=false] - Enable debug logging
1705
1708
  */
1706
1709
  constructor(debug) {
1707
1710
  if (debug) this.#debug = true;
@@ -1711,7 +1714,7 @@ class CRDTManager {
1711
1714
 
1712
1715
  /**
1713
1716
  * Import the entire state of the CRDT manager (this overwrites the old state)
1714
- * @param {Object} state
1717
+ * @param {object} state - Full state of the CRDT manager
1715
1718
  */
1716
1719
  importState(state) {
1717
1720
  try {
@@ -1728,7 +1731,7 @@ class CRDTManager {
1728
1731
  if (!this.#vectorClock.has(this.#replicaId)) this.#vectorClock.set(this.#replicaId, 0); // Add own vector clock if it wasn't present in the imported state
1729
1732
 
1730
1733
  // Map all operation uuids to current timestamp
1731
- this.#keyOperations.forEach((value, key) => {
1734
+ this.#keyOperations.forEach((value, _key) => {
1732
1735
  value?.forEach((operation) => {
1733
1736
  this.#opUuidTimestamp.set(operation?.uuid, Date.now());
1734
1737
  });
@@ -1744,7 +1747,7 @@ class CRDTManager {
1744
1747
 
1745
1748
  /**
1746
1749
  * Import property update
1747
- * @param {Object} data - Data to import
1750
+ * @param {object} data - Property update data
1748
1751
  */
1749
1752
  importPropertyUpdate(data) {
1750
1753
  try {
@@ -1786,11 +1789,11 @@ class CRDTManager {
1786
1789
 
1787
1790
  /**
1788
1791
  * Update a property
1789
- * @param {string} key
1790
- * @param {string} type
1791
- * @param {*} value
1792
- * @param {*} updateValue
1793
- * @returns {Object} - Returns the property update
1792
+ * @param {string} key - Storage key
1793
+ * @param {string} type - Operation type
1794
+ * @param {*} value - New value, or value to match
1795
+ * @param {*} updateValue - New value for 'matching' operations
1796
+ * @returns {object | undefined} - Returns the property update
1794
1797
  */
1795
1798
  updateProperty(key, type, value, updateValue) {
1796
1799
  try {
@@ -1827,6 +1830,9 @@ class CRDTManager {
1827
1830
  }
1828
1831
  }
1829
1832
 
1833
+ /**
1834
+ * Check if garbage collected can be performed & run it
1835
+ */
1830
1836
  #checkGarbageCollection() {
1831
1837
  const MIN_GC_DELAY = 1000; // Minimum 1s delay between garbage collection runs
1832
1838
  const MIN_AGE_FOR_GC = 5000; // Garbage collect ops that are older than 5s
@@ -1852,7 +1858,7 @@ class CRDTManager {
1852
1858
  if (this.#debug) console.log(CONSOLE_PREFIX + `Running garbage collection for key ${key} with current operations:`, operations);
1853
1859
  const retainOps = operations.slice(-retainCount); // Newest ops
1854
1860
  const removeOps = operations.slice(0, -retainCount); // Oldest ops (start = idx 0, end = retainCount counted from right side)
1855
- const baselineVectorClock = removeOps[removeOps.length - 1]?.vectorClock || []; // Use the vector clock from the last operation that we remove/overwrite
1861
+ const baselineVectorClock = removeOps[removeOps.length - 1]?.vectorClock || []; // Use the vector clock from the last operation that we remove/overwrite
1856
1862
 
1857
1863
  // Calculate the value at the point where retained operations start
1858
1864
  let baselineValue = null;
@@ -1883,9 +1889,9 @@ class CRDTManager {
1883
1889
 
1884
1890
  /**
1885
1891
  * Create an operation object
1886
- * @param {Object} data
1887
- * @param {Array} vectorClock
1888
- * @returns {Object} - Operation object
1892
+ * @param {object} data - Operation data, consists of type, value and updateValue
1893
+ * @param {Array} vectorClock - Vector clock of the replica/client
1894
+ * @returns {object} - Operation object
1889
1895
  */
1890
1896
  #createOperation(data, vectorClock) {
1891
1897
  const newOperation = {
@@ -1900,7 +1906,7 @@ class CRDTManager {
1900
1906
 
1901
1907
  /**
1902
1908
  * Process a property's value by applying all operations and set it in the property store
1903
- * @param {string} key
1909
+ * @param {string} key - Storage key
1904
1910
  */
1905
1911
  #processLocalProperty(key) {
1906
1912
  try {
@@ -1927,8 +1933,8 @@ class CRDTManager {
1927
1933
  }
1928
1934
 
1929
1935
  /**
1930
- * Sort by vector clock (causal order)
1931
- * @param {Array} operations
1936
+ * Sort operations by vector clock (causal order)
1937
+ * @param {Array} operations - Full operations that include vector clocks, data and a source
1932
1938
  * @returns {Array} - Sorted operations
1933
1939
  */
1934
1940
  #sortByVectorClock(operations) {
@@ -1964,10 +1970,10 @@ class CRDTManager {
1964
1970
 
1965
1971
  /**
1966
1972
  * Handle an operation
1967
- * @param {*} curValue
1968
- * @param {string} type
1969
- * @param {*} value
1970
- * @param {*} [updateValue]
1973
+ * @param {*} curValue - Current value of the storage key
1974
+ * @param {string} type - Operation type
1975
+ * @param {*} value - New value, or value to match
1976
+ * @param {*} [updateValue] - New value for 'matching' operations
1971
1977
  * @returns {*} - Value after the operation
1972
1978
  */
1973
1979
  #handleOperation(curValue, type, value, updateValue) {
@@ -1999,10 +2005,11 @@ class CRDTManager {
1999
2005
  case 'array-remove-matching':
2000
2006
  curValue = curValue.filter(item => !compare(item));
2001
2007
  break;
2002
- case 'array-update-matching':
2008
+ case 'array-update-matching': {
2003
2009
  const index = curValue.findIndex(compare);
2004
2010
  if (index !== -1) curValue[index] = updateValue;
2005
2011
  break;
2012
+ }
2006
2013
  }
2007
2014
  }
2008
2015
 
@@ -2016,42 +2023,53 @@ class CRDTManager {
2016
2023
 
2017
2024
  /**
2018
2025
  * Remove HTML to prevent XSS and enforce size limits
2019
- * @param {Object} obj
2020
- * @returns {Object} - Sanitized object
2026
+ * @param {*} value - Value to sanitize
2027
+ * @returns {*} - Sanitized value
2021
2028
  */
2022
- #sanitizeValue(obj) {
2029
+ #sanitizeValue(value) {
2023
2030
  // Check total serialized size
2024
- const jsonString = JSON.stringify(obj);
2031
+ const jsonString = JSON.stringify(value);
2025
2032
  if (jsonString?.length > 50000) throw new Error('Value too large!'); // 50KB limit
2026
2033
 
2027
- if (typeof obj === 'string') return (obj.includes('<') || obj.includes('>')) ? obj.replace(/[<>]/g, '') : obj;
2028
- if (Array.isArray(obj)) return obj.map(item => this.#sanitizeValue(item));
2029
- if (obj && typeof obj === 'object') return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, this.#sanitizeValue(v)]));
2030
- return obj;
2034
+ if (typeof value === 'string') return (value.includes('<') || value.includes('>')) ? value.replace(/[<>]/g, '') : value;
2035
+ if (Array.isArray(value)) return value.map(item => this.#sanitizeValue(item));
2036
+ if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, this.#sanitizeValue(v)]));
2037
+ return value;
2031
2038
  }
2032
2039
 
2033
- // Get property store
2040
+ /**
2041
+ * Get property store
2042
+ * @returns {Record<string, any>} - Object with key value pairs
2043
+ */
2034
2044
  get getPropertyStore() {
2035
2045
  try {
2036
2046
  return structuredClone(this.#propertyStore); // Return deep clone
2037
2047
  } catch {
2038
2048
  console.error(CONSOLE_PREFIX + "Failed to parse property store");
2039
- return {};
2049
+ return {}
2040
2050
  }
2041
2051
  }
2042
2052
 
2043
- // Check for changes in the local property store
2053
+ /**
2054
+ * Check for changes in the local property store
2055
+ * @returns {boolean} - Check if any values changed going from the last to the current property update
2056
+ */
2044
2057
  get didPropertiesChange() {
2045
2058
  try {
2046
2059
  if (JSON.stringify(this.#propertyStore) !== JSON.stringify(this.#lastPropertyStore)) {
2047
2060
  this.#lastPropertyStore = structuredClone(this.#propertyStore);
2048
2061
  return true;
2049
2062
  }
2050
- } catch { }
2063
+ } catch {
2064
+ console.error(CONSOLE_PREFIX + "Failed to stringify and compare property stores");
2065
+ }
2051
2066
  return false;
2052
2067
  }
2053
2068
 
2054
- // Get state (can be imported using importState, converts the maps to arrays for serialization)
2069
+ /**
2070
+ * Export state (can be imported using importState, converts the maps to arrays for serialization)
2071
+ * @returns {object} - State
2072
+ */
2055
2073
  get getState() {
2056
2074
  return {
2057
2075
  keyOperations: [...this.#keyOperations.entries()],
@@ -2060,16 +2078,14 @@ class CRDTManager {
2060
2078
  }
2061
2079
  }
2062
2080
 
2063
- /**
2064
- * PlaySocket - WebSocket-based multiplayer for games
2065
- */
2066
-
2067
-
2068
2081
  const ERROR_PREFIX = "PlaySocket error: ";
2069
2082
  const WARNING_PREFIX = "PlaySocket warning: ";
2070
2083
  const LOG_PREFIX = "PlaySocket log: ";
2071
2084
  const TIMEOUT_MS = 3000; // 3 second timeout for WS messages
2072
2085
 
2086
+ /**
2087
+ * PlaySocket Client
2088
+ */
2073
2089
  class PlaySocket {
2074
2090
  // Core properties
2075
2091
  #id; // Unique client ID
@@ -2122,11 +2138,13 @@ class PlaySocket {
2122
2138
 
2123
2139
  /**
2124
2140
  * Helper to create timeout promises for create room, join room etc.
2141
+ * @param {string} name - Name of the timeout
2142
+ * @returns {Promise} - Promise that rejects after the timeout
2125
2143
  * @private
2126
2144
  */
2127
- #createTimeout(operation) {
2145
+ #createTimeout(name) {
2128
2146
  return new Promise((_, reject) =>
2129
- setTimeout(() => reject(new Error(`${operation} timed out`)), TIMEOUT_MS)
2147
+ setTimeout(() => reject(new Error(`${name} timed out`)), TIMEOUT_MS)
2130
2148
  );
2131
2149
  }
2132
2150
 
@@ -2137,13 +2155,17 @@ class PlaySocket {
2137
2155
  */
2138
2156
  onEvent(event, callback) {
2139
2157
  const validEvents = ["status", "error", "instanceDestroyed", "storageUpdated", "hostMigrated", "clientConnected", "clientDisconnected"];
2140
- if (!validEvents.includes(event)) return console.warn(WARNING_PREFIX + `Invalid event type "${event}".`);
2141
- if (!this.#callbacks.has(event)) this.#callbacks.set(event, []);
2158
+ if (!validEvents.includes(event)) {
2159
+ console.warn(WARNING_PREFIX + `Invalid event type "${event}".`);
2160
+ return;
2161
+ } if (!this.#callbacks.has(event)) this.#callbacks.set(event, []);
2142
2162
  this.#callbacks.get(event).push(callback);
2143
2163
  }
2144
2164
 
2145
2165
  /**
2146
2166
  * Trigger an event to registered callbacks
2167
+ * @param {string} event - Event name
2168
+ * @param {...*} args - Arguments
2147
2169
  * @private
2148
2170
  */
2149
2171
  #triggerEvent(event, ...args) {
@@ -2375,7 +2397,8 @@ class PlaySocket {
2375
2397
  this.#reconnectCount++;
2376
2398
  if (this.#reconnectCount > 9) {
2377
2399
  this.#triggerEvent("error", "Disconnected from server.");
2378
- return this.destroy();
2400
+ this.destroy();
2401
+ return;
2379
2402
  }
2380
2403
 
2381
2404
  this.#triggerEvent("status", `Attempting to reconnect... (${this.#reconnectCount})`);
@@ -2383,7 +2406,7 @@ class PlaySocket {
2383
2406
  try {
2384
2407
  await this.#connect();
2385
2408
  await Promise.race([
2386
- new Promise(async (resolve, reject) => {
2409
+ new Promise((resolve, reject) => {
2387
2410
  this.#pendingReconnect = { resolve, reject };
2388
2411
  this.#sendToServer({
2389
2412
  type: 'reconnect',
@@ -2416,11 +2439,13 @@ class PlaySocket {
2416
2439
 
2417
2440
  /**
2418
2441
  * Send a message to the server
2442
+ * @param {object} data - The data to send to the server
2419
2443
  * @private
2420
2444
  */
2421
2445
  #sendToServer(data) {
2422
2446
  if (!this.#socket || this.#socket?.readyState !== WebSocket.OPEN) {
2423
- return console.warn(WARNING_PREFIX + "Cannot send message - not connected.");
2447
+ console.warn(WARNING_PREFIX + "Cannot send message - not connected.");
2448
+ return;
2424
2449
  }
2425
2450
  try {
2426
2451
  this.#socket.send(encode(data));
@@ -2461,7 +2486,7 @@ class PlaySocket {
2461
2486
  /**
2462
2487
  * Join an existing room
2463
2488
  * @param {string} roomId - ID of the room
2464
- * @returns {Promise} Resolves when connected
2489
+ * @returns {Promise} - Resolves when connected
2465
2490
  */
2466
2491
  async joinRoom(roomId) {
2467
2492
  if (!this.#initialized) {
@@ -2494,8 +2519,10 @@ class PlaySocket {
2494
2519
  * @param {*} updateValue - New value for update-matching
2495
2520
  */
2496
2521
  updateStorage(key, type, value, updateValue) {
2497
- if (!this.#inRoom) return this.#triggerEvent("error", "Cannot update storage when not in a room.");
2498
- if (this.#debug) console.log(LOG_PREFIX + `Property update for key ${key}, operation ${type}, value ${value} and updateValue ${updateValue}.`);
2522
+ if (!this.#inRoom) {
2523
+ this.#triggerEvent("error", "Cannot update storage when not in a room.");
2524
+ return;
2525
+ } if (this.#debug) console.log(LOG_PREFIX + `Property update for key ${key}, operation ${type}, value ${value} and updateValue ${updateValue}.`);
2499
2526
  const propUpdate = this.#crdtManager.updateProperty(key, type, value, updateValue);
2500
2527
  this.#sendToServer({
2501
2528
  type: 'update_property',