playsocketjs 1.3.1 → 1.4.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
@@ -206,8 +206,11 @@ Creates a new PlaySocket Server instance with configuration options.
206
206
 
207
207
  - `clientRegistered`: Client registered with the server (returns the client's id `string`, customData `object`)
208
208
  - `clientDisconnected`: Client disconnected from the server (returns the client's id `string`)
209
+ - `clientJoinedRoom`: Client joined a room – note that clients can only leave by disconnecting (returns the client's id `string`, room id `string`)
209
210
  - `roomCreated`: Client created a room (returns room id `string`)
210
- - `roomJoined`: Client joined a room (returns the client's id `string`, room id `string`)
211
+ - `roomDestroyed`: Room was destroyed, this happens when all participants leave (returns room id `string`)
212
+ - `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, it will take that as the initial storage instead
213
+ - `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
211
214
 
212
215
  ### Properties (Read-only)
213
216
 
@@ -1791,19 +1791,19 @@ function requireCrdtManager () {
1791
1791
  /**
1792
1792
  * Update a property
1793
1793
  * @param {string} key
1794
- * @param {string} operation
1794
+ * @param {string} type
1795
1795
  * @param {*} value
1796
1796
  * @param {*} updateValue
1797
1797
  * @returns {Object} - Returns the property update
1798
1798
  */
1799
- updateProperty(key, operation, value, updateValue) {
1799
+ updateProperty(key, type, value, updateValue) {
1800
1800
  try {
1801
1801
  // Sanitize inputs
1802
1802
  value = this.#sanitizeValue(value);
1803
1803
  updateValue = this.#sanitizeValue(updateValue);
1804
1804
 
1805
1805
  // Debug log
1806
- if (this.#debug) console.log(CONSOLE_PREFIX + `Updating property with key ${key}, operation ${operation}, value ${value} and updateValue ${updateValue}.`);
1806
+ if (this.#debug) console.log(CONSOLE_PREFIX + `Updating property with key ${key}, type ${type}, value ${value} and updateValue ${updateValue}.`);
1807
1807
 
1808
1808
  // Increment vector clock
1809
1809
  const counter = this.#vectorClock.get(this.#replicaId) || 0;
@@ -1813,7 +1813,7 @@ function requireCrdtManager () {
1813
1813
  const currentOps = [...(this.#keyOperations.get(key) || [])];
1814
1814
 
1815
1815
  // Add operation
1816
- const newOp = this.#createOperation({ operation, value, updateValue }, Array.from(this.#vectorClock.entries()));
1816
+ const newOp = this.#createOperation({ type, value, updateValue }, Array.from(this.#vectorClock.entries()));
1817
1817
  currentOps.push(newOp);
1818
1818
  this.#keyOperations.set(key, currentOps); // Update the operations (no need to sort via vector clock since local updates are always the latest)
1819
1819
  this.#processLocalProperty(key); // Process local value
@@ -1853,25 +1853,25 @@ function requireCrdtManager () {
1853
1853
 
1854
1854
  if (retainCount < operations.length) {
1855
1855
  if (this.#debug) console.log(CONSOLE_PREFIX + `Running garbage collection for key ${key} with current operations:`, operations);
1856
- const retainOps = operations.slice(-retainCount); // newest ops
1857
- const removeOps = operations.slice(0, -retainCount); // oldest ops (start = idx 0, end = retainCount counted from right side)
1856
+ const retainOps = operations.slice(-retainCount); // Newest ops
1857
+ const removeOps = operations.slice(0, -retainCount); // Oldest ops (start = idx 0, end = retainCount counted from right side)
1858
1858
  const baselineVectorClock = removeOps[removeOps.length - 1]?.vectorClock || []; // Use the vector clock from the last operation that we remove/overwrite
1859
1859
 
1860
1860
  // Calculate the value at the point where retained operations start
1861
1861
  let baselineValue = null;
1862
1862
  for (const op of removeOps) {
1863
1863
  if (!op.data) continue;
1864
- if (op.data.operation.startsWith('array') && !Array.isArray(baselineValue)) baselineValue = []; // Initialize array if required
1864
+ if (op.data.type.startsWith('array') && !Array.isArray(baselineValue)) baselineValue = []; // Initialize array if required
1865
1865
  baselineValue = this.#handleOperation(
1866
1866
  baselineValue,
1867
- op.data.operation,
1867
+ op.data.type,
1868
1868
  op.data.value,
1869
1869
  op.data.updateValue
1870
1870
  );
1871
1871
  }
1872
1872
 
1873
1873
  // Create a compact operation with baseline value and appropriate vector clock
1874
- const compactOp = this.#createOperation({ operation: "set", value: baselineValue }, baselineVectorClock);
1874
+ const compactOp = this.#createOperation({ type: "set", value: baselineValue }, baselineVectorClock);
1875
1875
 
1876
1876
  // Debug log
1877
1877
  if (this.#debug) console.log(CONSOLE_PREFIX + "Operations after garbage collection for this key:", [compactOp, ...retainOps]);
@@ -1913,10 +1913,10 @@ function requireCrdtManager () {
1913
1913
 
1914
1914
  for (const op of ops) {
1915
1915
  if (!op.data) continue;
1916
- if (op.data.operation.startsWith('array') && !Array.isArray(value)) value = []; // Initialize array if required
1916
+ if (op.data.type.startsWith('array') && !Array.isArray(value)) value = []; // Initialize array if required
1917
1917
  value = this.#handleOperation(
1918
1918
  value,
1919
- op.data.operation,
1919
+ op.data.type,
1920
1920
  op.data.value,
1921
1921
  op.data.updateValue
1922
1922
  );
@@ -1924,7 +1924,7 @@ function requireCrdtManager () {
1924
1924
 
1925
1925
  this.#propertyStore[key] = value; // Save locally
1926
1926
  } catch (error) {
1927
- console.error(CONSOLE_PREFIX + `Failed to process property for "${key}":`, error);
1927
+ console.error(CONSOLE_PREFIX + `Failed to process property for key ${key}:`, error);
1928
1928
  }
1929
1929
  }
1930
1930
 
@@ -1967,33 +1967,31 @@ function requireCrdtManager () {
1967
1967
  /**
1968
1968
  * Handle an operation
1969
1969
  * @param {*} curValue
1970
- * @param {string} operation
1970
+ * @param {string} type
1971
1971
  * @param {*} value
1972
1972
  * @param {*} [updateValue]
1973
1973
  * @returns {*} - Value after the operation
1974
1974
  */
1975
- #handleOperation(curValue, operation, value, updateValue) {
1975
+ #handleOperation(curValue, type, value, updateValue) {
1976
1976
  try {
1977
- // Clone to avoid reference issues
1978
- curValue = JSON.parse(JSON.stringify(curValue || null));
1977
+ // Deep copy to avoid reference issues in case value is or contains object(s)
1978
+ curValue = structuredClone(curValue);
1979
1979
 
1980
1980
  // Set operation
1981
- if (operation === "set") return value;
1981
+ if (type === "set") return value;
1982
1982
 
1983
1983
  // Array operations
1984
- if (operation.startsWith('array')) {
1984
+ if (type.startsWith('array')) {
1985
1985
  if (!Array.isArray(curValue)) curValue = []; // Auto-convert to array if current value isn't one
1986
1986
 
1987
1987
  // Comparison function
1988
1988
  const isObject = typeof value === 'object' && value !== null;
1989
1989
  const compare = (item) => {
1990
- if (isObject && typeof item === 'object' && item !== null) {
1991
- return JSON.stringify(item) === JSON.stringify(value); // Deep comparison
1992
- }
1990
+ if (isObject && typeof item === 'object' && item !== null) return JSON.stringify(item) === JSON.stringify(value); // Deep comparison
1993
1991
  return item === value; // Value comparison
1994
1992
  };
1995
1993
 
1996
- switch (operation) {
1994
+ switch (type) {
1997
1995
  case 'array-add':
1998
1996
  curValue.push(value);
1999
1997
  break;
@@ -2013,7 +2011,7 @@ function requireCrdtManager () {
2013
2011
  return curValue;
2014
2012
 
2015
2013
  } catch (error) {
2016
- console.error(CONSOLE_PREFIX + `Failed to process operation with curValue ${curValue}:`, error);
2014
+ console.error(CONSOLE_PREFIX + `Failed to handle operation with curValue ${curValue}:`, error);
2017
2015
  return curValue;
2018
2016
  }
2019
2017
  }
@@ -2033,7 +2031,7 @@ function requireCrdtManager () {
2033
2031
  // Get property store
2034
2032
  get getPropertyStore() {
2035
2033
  try {
2036
- return JSON.parse(JSON.stringify(this.#propertyStore)); // Return full copy
2034
+ return structuredClone(this.#propertyStore); // Return deep clone
2037
2035
  } catch {
2038
2036
  console.error(CONSOLE_PREFIX + "Failed to parse property store");
2039
2037
  return {};
@@ -2044,7 +2042,7 @@ function requireCrdtManager () {
2044
2042
  get didPropertiesChange() {
2045
2043
  try {
2046
2044
  if (JSON.stringify(this.#propertyStore) !== JSON.stringify(this.#lastPropertyStore)) {
2047
- this.#lastPropertyStore = JSON.parse(JSON.stringify(this.#propertyStore));
2045
+ this.#lastPropertyStore = structuredClone(this.#propertyStore);
2048
2046
  return true;
2049
2047
  }
2050
2048
  } catch { }
@@ -2280,6 +2278,8 @@ class PlaySocket {
2280
2278
  if (this.#pendingCreate) {
2281
2279
  this.#inRoom = true;
2282
2280
  this.#triggerEvent("status", `Room created${this.#pendingCreate.maxSize ? ` with max size ${this.#pendingCreate.maxSize}.` : '.'}`);
2281
+ this.#crdtManager.importState(message.state);
2282
+ this.#triggerEvent("storageUpdated", this.getStorage);
2283
2283
  this.#pendingCreate.resolve(this.#pendingCreate.id);
2284
2284
  }
2285
2285
  break;
@@ -2318,6 +2318,12 @@ class PlaySocket {
2318
2318
  }
2319
2319
  break;
2320
2320
 
2321
+ case 'property_update_rejected':
2322
+ this.#triggerEvent("error", "Property update rejected. Re-syncing state.");
2323
+ this.#crdtManager.importState(message.state);
2324
+ this.#triggerEvent("storageUpdated", this.getStorage);
2325
+ break;
2326
+
2321
2327
  case 'server_stopped':
2322
2328
  this.#triggerEvent("error", "Server restart.");
2323
2329
  this.destroy();
@@ -2441,17 +2447,13 @@ class PlaySocket {
2441
2447
 
2442
2448
  return Promise.race([
2443
2449
  new Promise((resolve, reject) => {
2444
- Object.entries(initialStorage)?.forEach(([key, value]) => {
2445
- this.#crdtManager.updateProperty(key, "set", value);
2446
- });
2447
2450
  this.#roomHost = this.#id;
2448
2451
  this.#pendingCreate = { maxSize, id: this.#id, resolve, reject };
2449
2452
  this.#sendToServer({
2450
2453
  type: 'create_room',
2451
- state: this.#crdtManager.getState,
2454
+ initialStorage,
2452
2455
  size: maxSize
2453
2456
  });
2454
- this.#triggerEvent("storageUpdated", this.getStorage);
2455
2457
  }),
2456
2458
  this.#createTimeout("Room creation")
2457
2459
  ]).finally(() => {
@@ -1917,19 +1917,19 @@ function requireCrdtManager () {
1917
1917
  /**
1918
1918
  * Update a property
1919
1919
  * @param {string} key
1920
- * @param {string} operation
1920
+ * @param {string} type
1921
1921
  * @param {*} value
1922
1922
  * @param {*} updateValue
1923
1923
  * @returns {Object} - Returns the property update
1924
1924
  */
1925
- updateProperty(key, operation, value, updateValue) {
1925
+ updateProperty(key, type, value, updateValue) {
1926
1926
  try {
1927
1927
  // Sanitize inputs
1928
1928
  value = this.#sanitizeValue(value);
1929
1929
  updateValue = this.#sanitizeValue(updateValue);
1930
1930
 
1931
1931
  // Debug log
1932
- if (this.#debug) console.log(CONSOLE_PREFIX + `Updating property with key ${key}, operation ${operation}, value ${value} and updateValue ${updateValue}.`);
1932
+ if (this.#debug) console.log(CONSOLE_PREFIX + `Updating property with key ${key}, type ${type}, value ${value} and updateValue ${updateValue}.`);
1933
1933
 
1934
1934
  // Increment vector clock
1935
1935
  const counter = this.#vectorClock.get(this.#replicaId) || 0;
@@ -1939,7 +1939,7 @@ function requireCrdtManager () {
1939
1939
  const currentOps = [...(this.#keyOperations.get(key) || [])];
1940
1940
 
1941
1941
  // Add operation
1942
- const newOp = this.#createOperation({ operation, value, updateValue }, Array.from(this.#vectorClock.entries()));
1942
+ const newOp = this.#createOperation({ type, value, updateValue }, Array.from(this.#vectorClock.entries()));
1943
1943
  currentOps.push(newOp);
1944
1944
  this.#keyOperations.set(key, currentOps); // Update the operations (no need to sort via vector clock since local updates are always the latest)
1945
1945
  this.#processLocalProperty(key); // Process local value
@@ -1979,25 +1979,25 @@ function requireCrdtManager () {
1979
1979
 
1980
1980
  if (retainCount < operations.length) {
1981
1981
  if (this.#debug) console.log(CONSOLE_PREFIX + `Running garbage collection for key ${key} with current operations:`, operations);
1982
- const retainOps = operations.slice(-retainCount); // newest ops
1983
- const removeOps = operations.slice(0, -retainCount); // oldest ops (start = idx 0, end = retainCount counted from right side)
1982
+ const retainOps = operations.slice(-retainCount); // Newest ops
1983
+ const removeOps = operations.slice(0, -retainCount); // Oldest ops (start = idx 0, end = retainCount counted from right side)
1984
1984
  const baselineVectorClock = removeOps[removeOps.length - 1]?.vectorClock || []; // Use the vector clock from the last operation that we remove/overwrite
1985
1985
 
1986
1986
  // Calculate the value at the point where retained operations start
1987
1987
  let baselineValue = null;
1988
1988
  for (const op of removeOps) {
1989
1989
  if (!op.data) continue;
1990
- if (op.data.operation.startsWith('array') && !Array.isArray(baselineValue)) baselineValue = []; // Initialize array if required
1990
+ if (op.data.type.startsWith('array') && !Array.isArray(baselineValue)) baselineValue = []; // Initialize array if required
1991
1991
  baselineValue = this.#handleOperation(
1992
1992
  baselineValue,
1993
- op.data.operation,
1993
+ op.data.type,
1994
1994
  op.data.value,
1995
1995
  op.data.updateValue
1996
1996
  );
1997
1997
  }
1998
1998
 
1999
1999
  // Create a compact operation with baseline value and appropriate vector clock
2000
- const compactOp = this.#createOperation({ operation: "set", value: baselineValue }, baselineVectorClock);
2000
+ const compactOp = this.#createOperation({ type: "set", value: baselineValue }, baselineVectorClock);
2001
2001
 
2002
2002
  // Debug log
2003
2003
  if (this.#debug) console.log(CONSOLE_PREFIX + "Operations after garbage collection for this key:", [compactOp, ...retainOps]);
@@ -2039,10 +2039,10 @@ function requireCrdtManager () {
2039
2039
 
2040
2040
  for (const op of ops) {
2041
2041
  if (!op.data) continue;
2042
- if (op.data.operation.startsWith('array') && !Array.isArray(value)) value = []; // Initialize array if required
2042
+ if (op.data.type.startsWith('array') && !Array.isArray(value)) value = []; // Initialize array if required
2043
2043
  value = this.#handleOperation(
2044
2044
  value,
2045
- op.data.operation,
2045
+ op.data.type,
2046
2046
  op.data.value,
2047
2047
  op.data.updateValue
2048
2048
  );
@@ -2050,7 +2050,7 @@ function requireCrdtManager () {
2050
2050
 
2051
2051
  this.#propertyStore[key] = value; // Save locally
2052
2052
  } catch (error) {
2053
- console.error(CONSOLE_PREFIX + `Failed to process property for "${key}":`, error);
2053
+ console.error(CONSOLE_PREFIX + `Failed to process property for key ${key}:`, error);
2054
2054
  }
2055
2055
  }
2056
2056
 
@@ -2093,33 +2093,31 @@ function requireCrdtManager () {
2093
2093
  /**
2094
2094
  * Handle an operation
2095
2095
  * @param {*} curValue
2096
- * @param {string} operation
2096
+ * @param {string} type
2097
2097
  * @param {*} value
2098
2098
  * @param {*} [updateValue]
2099
2099
  * @returns {*} - Value after the operation
2100
2100
  */
2101
- #handleOperation(curValue, operation, value, updateValue) {
2101
+ #handleOperation(curValue, type, value, updateValue) {
2102
2102
  try {
2103
- // Clone to avoid reference issues
2104
- curValue = JSON.parse(JSON.stringify(curValue || null));
2103
+ // Deep copy to avoid reference issues in case value is or contains object(s)
2104
+ curValue = structuredClone(curValue);
2105
2105
 
2106
2106
  // Set operation
2107
- if (operation === "set") return value;
2107
+ if (type === "set") return value;
2108
2108
 
2109
2109
  // Array operations
2110
- if (operation.startsWith('array')) {
2110
+ if (type.startsWith('array')) {
2111
2111
  if (!Array.isArray(curValue)) curValue = []; // Auto-convert to array if current value isn't one
2112
2112
 
2113
2113
  // Comparison function
2114
2114
  const isObject = typeof value === 'object' && value !== null;
2115
2115
  const compare = (item) => {
2116
- if (isObject && typeof item === 'object' && item !== null) {
2117
- return JSON.stringify(item) === JSON.stringify(value); // Deep comparison
2118
- }
2116
+ if (isObject && typeof item === 'object' && item !== null) return JSON.stringify(item) === JSON.stringify(value); // Deep comparison
2119
2117
  return item === value; // Value comparison
2120
2118
  };
2121
2119
 
2122
- switch (operation) {
2120
+ switch (type) {
2123
2121
  case 'array-add':
2124
2122
  curValue.push(value);
2125
2123
  break;
@@ -2139,7 +2137,7 @@ function requireCrdtManager () {
2139
2137
  return curValue;
2140
2138
 
2141
2139
  } catch (error) {
2142
- console.error(CONSOLE_PREFIX + `Failed to process operation with curValue ${curValue}:`, error);
2140
+ console.error(CONSOLE_PREFIX + `Failed to handle operation with curValue ${curValue}:`, error);
2143
2141
  return curValue;
2144
2142
  }
2145
2143
  }
@@ -2159,7 +2157,7 @@ function requireCrdtManager () {
2159
2157
  // Get property store
2160
2158
  get getPropertyStore() {
2161
2159
  try {
2162
- return JSON.parse(JSON.stringify(this.#propertyStore)); // Return full copy
2160
+ return structuredClone(this.#propertyStore); // Return deep clone
2163
2161
  } catch {
2164
2162
  console.error(CONSOLE_PREFIX + "Failed to parse property store");
2165
2163
  return {};
@@ -2170,7 +2168,7 @@ function requireCrdtManager () {
2170
2168
  get didPropertiesChange() {
2171
2169
  try {
2172
2170
  if (JSON.stringify(this.#propertyStore) !== JSON.stringify(this.#lastPropertyStore)) {
2173
- this.#lastPropertyStore = JSON.parse(JSON.stringify(this.#propertyStore));
2171
+ this.#lastPropertyStore = structuredClone(this.#propertyStore);
2174
2172
  return true;
2175
2173
  }
2176
2174
  } catch { }
@@ -2281,20 +2279,18 @@ function requireServer () {
2281
2279
  * Handle WebSocket message
2282
2280
  * @private
2283
2281
  */
2284
- #handleMessage(ws, message) {
2282
+ async #handleMessage(ws, message) {
2285
2283
  if (ws.isTerminating) return;
2286
2284
  try {
2287
2285
  const data = decode(message);
2288
2286
 
2289
2287
  // Apply rate limiting to all connections (including unregistered)
2290
2288
  if (!this.#checkRateLimit(ws.connectionId, data.type)) {
2291
- ws.rateLimitViolations = (ws.rateLimitViolations || 0) + 1;
2292
- if (ws.rateLimitViolations > 5 && !ws.isTerminating) {
2289
+ if (!ws.isTerminating) {
2293
2290
  ws.isTerminating = true; // Prevent multiple terminate calls (it is async)
2294
2291
  ws.terminate();
2295
- return console.error(`Connection ${ws.connectionId} terminated due to repeated rate limit violations.`);
2292
+ return console.error(`Connection ${ws.connectionId} terminated due to rate limit violations.`);
2296
2293
  }
2297
- return console.error(`Connection ${ws.connectionId} rate limit exceeded.`);
2298
2294
  }
2299
2295
 
2300
2296
  switch (data.type) {
@@ -2372,6 +2368,7 @@ function requireServer () {
2372
2368
  case 'create_room':
2373
2369
  if (!ws.clientId) return;
2374
2370
  const newRoomId = ws.clientId;
2371
+
2375
2372
  if (this.#clientRooms.get(ws.clientId) || this.#rooms[newRoomId]) {
2376
2373
  ws.send(encode({
2377
2374
  type: 'room_creation_failed',
@@ -2380,24 +2377,38 @@ function requireServer () {
2380
2377
  return;
2381
2378
  }
2382
2379
 
2380
+ const roomCrdtManager = new CRDTManager(this.#debug); // Create the room's crdt manager
2381
+
2382
+ // Event callback with potential initial storage modifications
2383
+ const reviewedStorage = await this.#triggerEvent("roomCreationRequested", { roomId: newRoomId, clientId: ws.clientId, initialStorage: structuredClone({ ...data.initialStorage }) });
2384
+ if (typeof reviewedStorage === 'object') data.initialStorage = reviewedStorage;
2385
+
2386
+ // Check if client/creator is still connected
2387
+ if (!this.#clients.has(ws.clientId)) {
2388
+ if (this.#debug) console.log(`Room creation cancelled - client ${ws.clientId} disconnected during event callback.`);
2389
+ return; // Abort room creation if client is no longer connected
2390
+ }
2391
+
2392
+ // Load initial storage if provided
2393
+ if (data.initialStorage) {
2394
+ Object.entries(data.initialStorage)?.forEach(([key, value]) => {
2395
+ roomCrdtManager.updateProperty(key, "set", value);
2396
+ });
2397
+ }
2398
+
2383
2399
  // Create room
2384
2400
  this.#roomVersions.set(newRoomId, 0); // Start with version 0
2385
2401
  this.#rooms[newRoomId] = {
2386
2402
  participants: [ws.clientId],
2387
2403
  host: ws.clientId,
2388
2404
  maxSize: data.size || null,
2389
- crdtManager: new CRDTManager(this.#debug)
2405
+ crdtManager: roomCrdtManager
2390
2406
  };
2407
+ this.#clientRooms.set(ws.clientId, newRoomId); // Add client to the room
2391
2408
 
2392
- // Load state if provided
2393
- if (data.state) {
2394
- this.#rooms[newRoomId].crdtManager.importState(data.state);
2395
- if (this.#debug) console.log("Room created with initial state:", data.state);
2396
- }
2397
-
2398
- this.#clientRooms.set(ws.clientId, newRoomId); // Add client to their room
2399
- ws.send(encode({ type: 'room_created' }), { binary: true });
2400
- this.#triggerEvent("roomCreated", ws.clientId); // Client id = room id
2409
+ ws.send(encode({ type: 'room_created', state: roomCrdtManager.getState }), { binary: true });
2410
+ this.#triggerEvent("roomCreated", newRoomId);
2411
+ if (this.#debug) console.log("Room created with initial storage:", data.initialStorage);
2401
2412
  break;
2402
2413
 
2403
2414
  case 'join_room':
@@ -2442,7 +2453,7 @@ function requireServer () {
2442
2453
  }), { binary: true });
2443
2454
  }
2444
2455
  });
2445
- this.#triggerEvent("roomJoined", ws.clientId, roomId);
2456
+ this.#triggerEvent("clientJoinedRoom", ws.clientId, roomId);
2446
2457
  break;
2447
2458
 
2448
2459
  case 'update_property':
@@ -2450,13 +2461,23 @@ function requireServer () {
2450
2461
  const updateRoom = updateRoomId ? this.#rooms[updateRoomId] : null;
2451
2462
 
2452
2463
  if (updateRoom && data.update) {
2453
- updateRoom.crdtManager.importPropertyUpdate(data.update);
2464
+ // Check if update is allowed via event callback (provide clone to ensure update integrity)
2465
+ const allowed = await this.#triggerEvent("storageUpdateRequested", { roomId: updateRoomId, clientId: ws.clientId, update: structuredClone(data.update) });
2466
+ if (allowed === false) {
2467
+ ws.send(encode({
2468
+ type: 'property_update_rejected',
2469
+ state: updateRoom.crdtManager.getState
2470
+ }), { binary: true });
2471
+ return;
2472
+ }
2473
+
2474
+ updateRoom.crdtManager.importPropertyUpdate(data.update); // Import update into server state
2454
2475
 
2455
2476
  // Increment version for this room
2456
2477
  const currentVersion = this.#roomVersions.get(updateRoomId) + 1;
2457
2478
  this.#roomVersions.set(updateRoomId, currentVersion);
2458
-
2459
2479
  if (this.#debug) console.log("Property update received and imported:", data.update);
2480
+
2460
2481
  updateRoom.participants?.forEach(p => {
2461
2482
  const client = this.#clients.get(p);
2462
2483
  if (client) {
@@ -2586,6 +2607,7 @@ function requireServer () {
2586
2607
  if (room.participants?.length === 0) {
2587
2608
  delete this.#rooms[roomId]; // Delete room if now empty
2588
2609
  this.#roomVersions.delete(roomId); // Delete room version
2610
+ this.#triggerEvent("roomDestroyed", roomId);
2589
2611
  if (this.#debug) console.log("Deleted room with id " + roomId + ".");
2590
2612
  } else {
2591
2613
  // Notify remaining participants
@@ -2609,7 +2631,7 @@ function requireServer () {
2609
2631
  * @param {Function} callback - Callback function
2610
2632
  */
2611
2633
  onEvent(event, callback) {
2612
- const validEvents = ["clientRegistered", "clientDisconnected", "roomCreated", "roomJoined"];
2634
+ const validEvents = ["clientRegistered", "clientDisconnected", "clientJoinedRoom", "roomCreationRequested", "roomCreated", "storageUpdateRequested", "roomDestroyed"];
2613
2635
  if (!validEvents.includes(event)) return console.warn(`Invalid PlaySocket event type "${event}"`);
2614
2636
  if (!this.#callbacks.has(event)) this.#callbacks.set(event, []);
2615
2637
  this.#callbacks.get(event).push(callback);
@@ -2633,11 +2655,20 @@ function requireServer () {
2633
2655
  * Trigger an event to registered callbacks
2634
2656
  * @private
2635
2657
  */
2636
- #triggerEvent(event, ...args) {
2658
+ async #triggerEvent(event, ...args) {
2637
2659
  const callbacks = this.#callbacks.get(event);
2638
- callbacks?.forEach(callback => {
2639
- try { callback(...args); } catch (error) { console.error(`PlaySocket ${event} callback error:`, error); }
2640
- });
2660
+ if (!callbacks) return;
2661
+
2662
+ for (const callback of callbacks) {
2663
+ try {
2664
+ const result = await callback(...args);
2665
+ if (result === false) return false; // False = block (in supported events)
2666
+ if (typeof result === 'object') return result; // If response object is provided -> pass on
2667
+ } catch (error) {
2668
+ console.error(`PlaySocket ${event} callback error:`, error);
2669
+ }
2670
+ }
2671
+ return true;
2641
2672
  }
2642
2673
 
2643
2674
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playsocketjs",
3
- "version": "1.3.1",
3
+ "version": "1.4.1",
4
4
  "description": "WebSocket wrapper for creating simple multiplayer systems with ease.",
5
5
  "module": "dist/playsocket-client.js",
6
6
  "exports": {