playsocketjs 2.1.0 → 2.2.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
@@ -72,12 +72,17 @@ 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
78
83
 
79
84
  Creates a new PlaySocket instance with a specified ID and configuration options.
80
- The id can be set to `null` to let the server pick a unique one.
85
+ The ID can be set to `null` to let the server pick a unique one.
81
86
 
82
87
  ```javascript
83
88
  new PlaySocket(id?: string, options: PlaySocketOptions)
@@ -90,11 +95,12 @@ new PlaySocket(id?: string, options: PlaySocketOptions)
90
95
 
91
96
  ### Methods
92
97
 
93
- - `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)
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 (matches the creator's ID)
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
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. 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
@@ -103,15 +109,15 @@ new PlaySocket(id?: string, options: PlaySocketOptions)
103
109
  - `error`: Error events (returns error `string`)
104
110
  - `instanceDestroyed`: Destruction event - triggered by manual .destroy() method invocation or by fatal errors and disconnects
105
111
  - `storageUpdated`: Storage state changes (returns storage `object`)
106
- - `hostMigrated`: Host changes (returns the new host's id `string`)
107
- - `clientConnected`: New client connected to the room (returns client-id `string`)
108
- - `clientDisconnected`: Client disconnected from the room (returns client-id `string`, room id (if available) `string`)
112
+ - `hostMigrated`: Host changes (returns the new host's ID `string`)
113
+ - `clientConnected`: New client connected to the room (returns client's ID `string`)
114
+ - `clientDisconnected`: Client disconnected from the room (returns client's ID `string`, room ID (if available) `string`)
109
115
 
110
116
  ### Properties (Read-only)
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
   
@@ -209,13 +215,15 @@ Creates a new PlaySocket Server instance with configuration options.
209
215
 
210
216
  #### Event types
211
217
 
212
- - `clientRegistered`: Client registered with the server (returns the client's id `string`, customData `object`)
213
- - `clientDisconnected`: Client disconnected from the server (returns the client's id `string`)
214
- - `clientJoinedRoom`: Client joined a room – note that clients can only leave by disconnecting (returns the client's id `string`, room id `string`)
215
- - `roomCreated`: Client created a room (returns room id `string`)
216
- - `roomDestroyed`: Room was destroyed, this happens when all participants leave (returns room id `string`)
217
- - `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
- - `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
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
220
+ - `clientDisconnected`: Client disconnected from the server (returns the client's ID `string`)
221
+ - `clientJoinedRoom`: Client joined a room – note that clients can only leave by disconnecting (returns the client's ID `string`, room ID `string`)
222
+ - `roomCreated`: Client created a room (returns room ID `string`)
223
+ - `roomDestroyed`: Room was destroyed, this happens when all participants leave (returns room ID `string`)
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
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
 
@@ -1687,7 +1687,7 @@ function requireCrdtManager () {
1687
1687
  hasRequiredCrdtManager = 1;
1688
1688
  // Custom conflict-free replicated data type system with vector clocks
1689
1689
 
1690
- const CONSOLE_PREFIX = "CRDT Manager: ";
1690
+ const CONSOLE_PREFIX = "PlaySocket CRDT manager: ";
1691
1691
 
1692
1692
  class CRDTManager {
1693
1693
  // Storage
@@ -1835,50 +1835,53 @@ function requireCrdtManager () {
1835
1835
  const MIN_GC_DELAY = 1000; // Minimum 1s delay between garbage collection runs
1836
1836
  const MIN_AGE_FOR_GC = 5000; // Garbage collect ops that are older than 5s
1837
1837
 
1838
- if (Date.now() - this.#lastGCCheck < MIN_GC_DELAY) return;
1839
- this.#lastGCCheck = Date.now();
1840
-
1841
- for (const [key, operations] of this.#keyOperations.entries()) {
1842
- if (operations?.length < 5) continue; // Min op amount per key for garbage collection to run (otherwise not worth it)
1843
-
1844
- let retainCount = operations.length;
1845
- operations.forEach((op, index) => {
1846
- // Count how many ops, from last to latest, are older than the min-age in a row
1847
- if (op.uuid && this.#opUuidTimestamp.has(op.uuid) && (Date.now() - this.#opUuidTimestamp.get(op.uuid)) > MIN_AGE_FOR_GC) {
1848
- const removeCount = index + 1;
1849
- retainCount = operations.length - removeCount;
1850
- this.#opUuidTimestamp.delete(op.uuid); // Remove from timestamps if set to be deleted
1851
- }
1852
- });
1838
+ try {
1839
+ if (Date.now() - this.#lastGCCheck < MIN_GC_DELAY) return;
1840
+ this.#lastGCCheck = Date.now();
1841
+
1842
+ for (const [key, operations] of this.#keyOperations.entries()) {
1843
+ if (operations?.length < 5) continue; // Min op amount per key for garbage collection to run (otherwise not worth it)
1844
+
1845
+ let retainCount = operations.length;
1846
+ operations.forEach((op, index) => {
1847
+ // Count how many ops, from last to latest, are older than the min-age in a row
1848
+ if (op.uuid && this.#opUuidTimestamp.has(op.uuid) && (Date.now() - this.#opUuidTimestamp.get(op.uuid)) > MIN_AGE_FOR_GC) {
1849
+ const removeCount = index + 1;
1850
+ retainCount = operations.length - removeCount;
1851
+ this.#opUuidTimestamp.delete(op.uuid); // Remove from timestamps if set to be deleted
1852
+ }
1853
+ });
1853
1854
 
1854
- if (retainCount < operations.length) {
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)
1858
- const baselineVectorClock = removeOps[removeOps.length - 1]?.vectorClock || []; // Use the vector clock from the last operation that we remove/overwrite
1859
-
1860
- // Calculate the value at the point where retained operations start
1861
- let baselineValue = null;
1862
- for (const op of removeOps) {
1863
- if (!op.data) continue;
1864
- if (op.data.type.startsWith('array') && !Array.isArray(baselineValue)) baselineValue = []; // Initialize array if required
1865
- baselineValue = this.#handleOperation(
1866
- baselineValue,
1867
- op.data.type,
1868
- op.data.value,
1869
- op.data.updateValue
1870
- );
1855
+ if (retainCount < operations.length) {
1856
+ if (this.#debug) console.log(CONSOLE_PREFIX + `Running garbage collection for key ${key} with current operations:`, operations);
1857
+ const retainOps = operations.slice(-retainCount); // Newest ops
1858
+ const removeOps = operations.slice(0, -retainCount); // Oldest ops (start = idx 0, end = retainCount counted from right side)
1859
+ const baselineVectorClock = removeOps[removeOps.length - 1]?.vectorClock || []; // Use the vector clock from the last operation that we remove/overwrite
1860
+
1861
+ // Calculate the value at the point where retained operations start
1862
+ let baselineValue = null;
1863
+ for (const op of removeOps) {
1864
+ if (!op.data) continue;
1865
+ baselineValue = this.#handleOperation(
1866
+ baselineValue,
1867
+ op.data.type,
1868
+ op.data.value,
1869
+ op.data.updateValue
1870
+ );
1871
+ }
1872
+
1873
+ // Create a compact operation with baseline value and appropriate vector clock
1874
+ const compactOp = this.#createOperation({ type: "set", value: baselineValue }, baselineVectorClock);
1875
+
1876
+ // Debug log
1877
+ if (this.#debug) console.log(CONSOLE_PREFIX + "Operations after garbage collection for this key:", [compactOp, ...retainOps]);
1878
+
1879
+ // Combine compact op with retained operations
1880
+ this.#keyOperations.set(key, [compactOp, ...retainOps]);
1871
1881
  }
1872
-
1873
- // Create a compact operation with baseline value and appropriate vector clock
1874
- const compactOp = this.#createOperation({ type: "set", value: baselineValue }, baselineVectorClock);
1875
-
1876
- // Debug log
1877
- if (this.#debug) console.log(CONSOLE_PREFIX + "Operations after garbage collection for this key:", [compactOp, ...retainOps]);
1878
-
1879
- // Combine compact op with retained operations
1880
- this.#keyOperations.set(key, [compactOp, ...retainOps]);
1881
1882
  }
1883
+ } catch (error) {
1884
+ console.error(CONSOLE_PREFIX + "Error running garbage collection:", error);
1882
1885
  }
1883
1886
  }
1884
1887
 
@@ -1913,7 +1916,6 @@ function requireCrdtManager () {
1913
1916
 
1914
1917
  for (const op of ops) {
1915
1918
  if (!op.data) continue;
1916
- if (op.data.type.startsWith('array') && !Array.isArray(value)) value = []; // Initialize array if required
1917
1919
  value = this.#handleOperation(
1918
1920
  value,
1919
1921
  op.data.type,
@@ -2225,6 +2227,22 @@ class PlaySocket {
2225
2227
  if (!message.type) return;
2226
2228
 
2227
2229
  switch (message.type) {
2230
+ case 'registration_failed':
2231
+ if (this.#pendingRegistration) {
2232
+ this.#pendingRegistration.reject(new Error("Failed to register: " + message.reason));
2233
+ this.#triggerEvent("error", "Failed to register: " + message.reason);
2234
+ }
2235
+ break;
2236
+
2237
+ case 'registered':
2238
+ if (this.#pendingRegistration) {
2239
+ this.#sessionToken = message.sessionToken;
2240
+ this.#initialized = true;
2241
+ this.#pendingRegistration.resolve(message.id);
2242
+ this.#triggerEvent("status", "Connected to server.");
2243
+ }
2244
+ break;
2245
+
2228
2246
  case 'join_accepted':
2229
2247
  // Connected to room
2230
2248
  if (this.#pendingJoin) {
@@ -2291,22 +2309,6 @@ class PlaySocket {
2291
2309
  }
2292
2310
  break;
2293
2311
 
2294
- case 'id_taken':
2295
- if (this.#pendingRegistration) {
2296
- this.#pendingRegistration.reject(new Error("This id is already in use"));
2297
- this.#triggerEvent("error", "This id is taken.");
2298
- }
2299
- break;
2300
-
2301
- case 'registered':
2302
- if (this.#pendingRegistration) {
2303
- this.#sessionToken = message.sessionToken;
2304
- this.#initialized = true;
2305
- this.#pendingRegistration.resolve(message.id);
2306
- this.#triggerEvent("status", "Connected to server.");
2307
- }
2308
- break;
2309
-
2310
2312
  case 'property_updated':
2311
2313
  this.#roomVersion++; // Increment room version
2312
2314
  if (this.#debug) console.log(LOG_PREFIX + "Property update received:", message.update);
@@ -1813,7 +1813,7 @@ function requireCrdtManager () {
1813
1813
  hasRequiredCrdtManager = 1;
1814
1814
  // Custom conflict-free replicated data type system with vector clocks
1815
1815
 
1816
- const CONSOLE_PREFIX = "CRDT Manager: ";
1816
+ const CONSOLE_PREFIX = "PlaySocket CRDT manager: ";
1817
1817
 
1818
1818
  class CRDTManager {
1819
1819
  // Storage
@@ -1961,50 +1961,53 @@ function requireCrdtManager () {
1961
1961
  const MIN_GC_DELAY = 1000; // Minimum 1s delay between garbage collection runs
1962
1962
  const MIN_AGE_FOR_GC = 5000; // Garbage collect ops that are older than 5s
1963
1963
 
1964
- if (Date.now() - this.#lastGCCheck < MIN_GC_DELAY) return;
1965
- this.#lastGCCheck = Date.now();
1966
-
1967
- for (const [key, operations] of this.#keyOperations.entries()) {
1968
- if (operations?.length < 5) continue; // Min op amount per key for garbage collection to run (otherwise not worth it)
1969
-
1970
- let retainCount = operations.length;
1971
- operations.forEach((op, index) => {
1972
- // Count how many ops, from last to latest, are older than the min-age in a row
1973
- if (op.uuid && this.#opUuidTimestamp.has(op.uuid) && (Date.now() - this.#opUuidTimestamp.get(op.uuid)) > MIN_AGE_FOR_GC) {
1974
- const removeCount = index + 1;
1975
- retainCount = operations.length - removeCount;
1976
- this.#opUuidTimestamp.delete(op.uuid); // Remove from timestamps if set to be deleted
1977
- }
1978
- });
1964
+ try {
1965
+ if (Date.now() - this.#lastGCCheck < MIN_GC_DELAY) return;
1966
+ this.#lastGCCheck = Date.now();
1967
+
1968
+ for (const [key, operations] of this.#keyOperations.entries()) {
1969
+ if (operations?.length < 5) continue; // Min op amount per key for garbage collection to run (otherwise not worth it)
1970
+
1971
+ let retainCount = operations.length;
1972
+ operations.forEach((op, index) => {
1973
+ // Count how many ops, from last to latest, are older than the min-age in a row
1974
+ if (op.uuid && this.#opUuidTimestamp.has(op.uuid) && (Date.now() - this.#opUuidTimestamp.get(op.uuid)) > MIN_AGE_FOR_GC) {
1975
+ const removeCount = index + 1;
1976
+ retainCount = operations.length - removeCount;
1977
+ this.#opUuidTimestamp.delete(op.uuid); // Remove from timestamps if set to be deleted
1978
+ }
1979
+ });
1979
1980
 
1980
- if (retainCount < operations.length) {
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)
1984
- const baselineVectorClock = removeOps[removeOps.length - 1]?.vectorClock || []; // Use the vector clock from the last operation that we remove/overwrite
1985
-
1986
- // Calculate the value at the point where retained operations start
1987
- let baselineValue = null;
1988
- for (const op of removeOps) {
1989
- if (!op.data) continue;
1990
- if (op.data.type.startsWith('array') && !Array.isArray(baselineValue)) baselineValue = []; // Initialize array if required
1991
- baselineValue = this.#handleOperation(
1992
- baselineValue,
1993
- op.data.type,
1994
- op.data.value,
1995
- op.data.updateValue
1996
- );
1997
- }
1981
+ if (retainCount < operations.length) {
1982
+ if (this.#debug) console.log(CONSOLE_PREFIX + `Running garbage collection for key ${key} with current operations:`, operations);
1983
+ const retainOps = operations.slice(-retainCount); // Newest ops
1984
+ const removeOps = operations.slice(0, -retainCount); // Oldest ops (start = idx 0, end = retainCount counted from right side)
1985
+ const baselineVectorClock = removeOps[removeOps.length - 1]?.vectorClock || []; // Use the vector clock from the last operation that we remove/overwrite
1986
+
1987
+ // Calculate the value at the point where retained operations start
1988
+ let baselineValue = null;
1989
+ for (const op of removeOps) {
1990
+ if (!op.data) continue;
1991
+ baselineValue = this.#handleOperation(
1992
+ baselineValue,
1993
+ op.data.type,
1994
+ op.data.value,
1995
+ op.data.updateValue
1996
+ );
1997
+ }
1998
1998
 
1999
- // Create a compact operation with baseline value and appropriate vector clock
2000
- const compactOp = this.#createOperation({ type: "set", value: baselineValue }, baselineVectorClock);
1999
+ // Create a compact operation with baseline value and appropriate vector clock
2000
+ const compactOp = this.#createOperation({ type: "set", value: baselineValue }, baselineVectorClock);
2001
2001
 
2002
- // Debug log
2003
- if (this.#debug) console.log(CONSOLE_PREFIX + "Operations after garbage collection for this key:", [compactOp, ...retainOps]);
2002
+ // Debug log
2003
+ if (this.#debug) console.log(CONSOLE_PREFIX + "Operations after garbage collection for this key:", [compactOp, ...retainOps]);
2004
2004
 
2005
- // Combine compact op with retained operations
2006
- this.#keyOperations.set(key, [compactOp, ...retainOps]);
2005
+ // Combine compact op with retained operations
2006
+ this.#keyOperations.set(key, [compactOp, ...retainOps]);
2007
+ }
2007
2008
  }
2009
+ } catch (error) {
2010
+ console.error(CONSOLE_PREFIX + "Error running garbage collection:", error);
2008
2011
  }
2009
2012
  }
2010
2013
 
@@ -2039,7 +2042,6 @@ function requireCrdtManager () {
2039
2042
 
2040
2043
  for (const op of ops) {
2041
2044
  if (!op.data) continue;
2042
- if (op.data.type.startsWith('array') && !Array.isArray(value)) value = []; // Initialize array if required
2043
2045
  value = this.#handleOperation(
2044
2046
  value,
2045
2047
  op.data.type,
@@ -2297,7 +2299,7 @@ function requireServer () {
2297
2299
  case 'register':
2298
2300
  // Register client ID if provided & check for a duplicate
2299
2301
  if (data.id && this.#clients.get(data.id)) {
2300
- ws.send(encode({ type: 'id_taken' }), { binary: true });
2302
+ ws.send(encode({ type: 'registration_failed', reason: 'ID is taken.' }), { binary: true });
2301
2303
  return;
2302
2304
  }
2303
2305
 
@@ -2313,11 +2315,20 @@ function requireServer () {
2313
2315
  }
2314
2316
  }
2315
2317
  if (!data.id) {
2316
- ws.send(encode({ type: 'id_taken' }), { binary: true });
2318
+ ws.send(encode({ type: 'registration_failed', reason: 'No available ID found.' }), { binary: true });
2317
2319
  throw new Error('Failed to generate unique ID!');
2318
2320
  }
2319
2321
  }
2320
2322
 
2323
+ // Event callback
2324
+ const registrationAllowed = await this.#triggerEvent("clientRegistrationRequested", data.id, data.customData);
2325
+ if (registrationAllowed === false || typeof registrationAllowed === 'string') {
2326
+ ws.send(encode({
2327
+ type: 'registration_failed',
2328
+ reason: typeof registrationAllowed === 'string' ? registrationAllowed : 'Denied.'
2329
+ }), { binary: true });
2330
+ return;
2331
+ }
2321
2332
 
2322
2333
  ws.clientId = data.id; // Adds the provided id to the ws object as clientId
2323
2334
  this.#clients.set(data.id, ws);
@@ -2462,8 +2473,8 @@ function requireServer () {
2462
2473
 
2463
2474
  if (updateRoom && data.update) {
2464
2475
  // 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) {
2476
+ const updateAllowed = await this.#triggerEvent("storageUpdateRequested", { roomId: updateRoomId, clientId: ws.clientId, update: structuredClone(data.update) });
2477
+ if (updateAllowed === false) {
2467
2478
  ws.send(encode({
2468
2479
  type: 'property_update_rejected',
2469
2480
  state: updateRoom.crdtManager.getState
@@ -2636,7 +2647,7 @@ function requireServer () {
2636
2647
  * @param {Function} callback - Callback function
2637
2648
  */
2638
2649
  onEvent(event, callback) {
2639
- const validEvents = ["clientRegistered", "clientDisconnected", "clientJoinedRoom", "roomCreationRequested", "requestReceived", "roomCreated", "storageUpdateRequested", "roomDestroyed"];
2650
+ const validEvents = ["clientRegistered", "clientRegistrationRequested", "clientDisconnected", "clientJoinedRoom", "roomCreationRequested", "requestReceived", "roomCreated", "storageUpdateRequested", "roomDestroyed"];
2640
2651
  if (!validEvents.includes(event)) return console.warn(`Invalid PlaySocket event type "${event}"`);
2641
2652
  if (!this.#callbacks.has(event)) this.#callbacks.set(event, []);
2642
2653
  this.#callbacks.get(event).push(callback);
@@ -2667,8 +2678,7 @@ function requireServer () {
2667
2678
  for (const callback of callbacks) {
2668
2679
  try {
2669
2680
  const result = await callback(...args);
2670
- if (result === false) return false; // False = block (in supported events)
2671
- if (typeof result === 'object') return result; // If response object is provided -> pass on
2681
+ if (result != null) return result; // Return any non-null/undefined result
2672
2682
  } catch (error) {
2673
2683
  console.error(`PlaySocket ${event} callback error:`, error);
2674
2684
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playsocketjs",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "WebSocket wrapper for creating simple multiplayer systems with ease.",
5
5
  "module": "dist/playsocket-client.js",
6
6
  "exports": {