playsocketjs 2.5.1 → 2.5.3

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
@@ -143,7 +143,7 @@ Here are usage examples for a standalone server and an Express.js application.
143
143
  ```javascript
144
144
  import PlaySocketServer from 'playsocketjs/server'; // Both ES Module & CommonJS Module syntax is supported
145
145
 
146
- const server = new PlaySocketServer(); // Create and start the server (default path is /socket)
146
+ const server = new PlaySocketServer(); // Create and start the server (default path is /)
147
147
 
148
148
  // Gracefully disconnect all clients and close the server (optional)
149
149
  function shutdown() {
@@ -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
@@ -2110,6 +2126,7 @@ class PlaySocket {
2110
2126
  * @param {object} options - Connection options
2111
2127
  * @param {string} options.endpoint - WebSocket endpoint path
2112
2128
  * @param {object} [options.customData] - Custom registration data
2129
+ * @param {boolean} [options.debug=false] - Enable debug logging
2113
2130
  */
2114
2131
  constructor(id, options = {}) {
2115
2132
  this.#id = id;
@@ -2121,11 +2138,13 @@ class PlaySocket {
2121
2138
 
2122
2139
  /**
2123
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
2124
2143
  * @private
2125
2144
  */
2126
- #createTimeout(operation) {
2145
+ #createTimeout(name) {
2127
2146
  return new Promise((_, reject) =>
2128
- setTimeout(() => reject(new Error(`${operation} timed out`)), TIMEOUT_MS)
2147
+ setTimeout(() => reject(new Error(`${name} timed out`)), TIMEOUT_MS)
2129
2148
  );
2130
2149
  }
2131
2150
 
@@ -2136,13 +2155,17 @@ class PlaySocket {
2136
2155
  */
2137
2156
  onEvent(event, callback) {
2138
2157
  const validEvents = ["status", "error", "instanceDestroyed", "storageUpdated", "hostMigrated", "clientConnected", "clientDisconnected"];
2139
- if (!validEvents.includes(event)) return console.warn(WARNING_PREFIX + `Invalid event type "${event}".`);
2140
- 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, []);
2141
2162
  this.#callbacks.get(event).push(callback);
2142
2163
  }
2143
2164
 
2144
2165
  /**
2145
2166
  * Trigger an event to registered callbacks
2167
+ * @param {string} event - Event name
2168
+ * @param {...*} args - Arguments
2146
2169
  * @private
2147
2170
  */
2148
2171
  #triggerEvent(event, ...args) {
@@ -2374,7 +2397,8 @@ class PlaySocket {
2374
2397
  this.#reconnectCount++;
2375
2398
  if (this.#reconnectCount > 9) {
2376
2399
  this.#triggerEvent("error", "Disconnected from server.");
2377
- return this.destroy();
2400
+ this.destroy();
2401
+ return;
2378
2402
  }
2379
2403
 
2380
2404
  this.#triggerEvent("status", `Attempting to reconnect... (${this.#reconnectCount})`);
@@ -2382,7 +2406,7 @@ class PlaySocket {
2382
2406
  try {
2383
2407
  await this.#connect();
2384
2408
  await Promise.race([
2385
- new Promise(async (resolve, reject) => {
2409
+ new Promise((resolve, reject) => {
2386
2410
  this.#pendingReconnect = { resolve, reject };
2387
2411
  this.#sendToServer({
2388
2412
  type: 'reconnect',
@@ -2415,11 +2439,13 @@ class PlaySocket {
2415
2439
 
2416
2440
  /**
2417
2441
  * Send a message to the server
2442
+ * @param {object} data - The data to send to the server
2418
2443
  * @private
2419
2444
  */
2420
2445
  #sendToServer(data) {
2421
2446
  if (!this.#socket || this.#socket?.readyState !== WebSocket.OPEN) {
2422
- return console.warn(WARNING_PREFIX + "Cannot send message - not connected.");
2447
+ console.warn(WARNING_PREFIX + "Cannot send message - not connected.");
2448
+ return;
2423
2449
  }
2424
2450
  try {
2425
2451
  this.#socket.send(encode(data));
@@ -2460,7 +2486,7 @@ class PlaySocket {
2460
2486
  /**
2461
2487
  * Join an existing room
2462
2488
  * @param {string} roomId - ID of the room
2463
- * @returns {Promise} Resolves when connected
2489
+ * @returns {Promise} - Resolves when connected
2464
2490
  */
2465
2491
  async joinRoom(roomId) {
2466
2492
  if (!this.#initialized) {
@@ -2488,13 +2514,15 @@ class PlaySocket {
2488
2514
  /**
2489
2515
  * Update a value in the shared storage
2490
2516
  * @param {string} key - Storage key
2491
- * @param {string} type - Operation type: add, add-unique, remove-matching, update-matching
2517
+ * @param {'set' | 'array-add' | 'array-add-unique' | 'array-remove-matching' | 'array-update-matching'} type - Operation type
2492
2518
  * @param {*} value - New value or value to operate on
2493
2519
  * @param {*} updateValue - New value for update-matching
2494
2520
  */
2495
2521
  updateStorage(key, type, value, updateValue) {
2496
- if (!this.#inRoom) return this.#triggerEvent("error", "Cannot update storage when not in a room.");
2497
- 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}.`);
2498
2526
  const propUpdate = this.#crdtManager.updateProperty(key, type, value, updateValue);
2499
2527
  this.#sendToServer({
2500
2528
  type: 'update_property',