playsocketjs 1.4.1 → 2.1.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
@@ -1,15 +1,15 @@
1
- # PlaySocket
1
+ # PlaySocket Client
2
2
 
3
- A WebSocket-based multiplayer library that simplifies game development by abstracting away the backend logic and moving it to the frontend.
3
+ A reactive, optimistic WebSocket library that simplifies game & app development by abstracting away complex sync logic.
4
4
 
5
5
  ## Why use PlaySocket?
6
6
 
7
- PlaySocket eliminates the traditional complexity of multiplayer implementations:
7
+ PlaySocket eliminates the traditional complexity of collaborative experiences:
8
8
 
9
- - **Streamlined Architecture**: No additional backend code is required
10
- - **State Synchronization**: Built-in storage system keeps game state synchronized across all users
11
- - **Resilient & Secure Connections**: Automatic reconnection handling and rate limiting
12
- - **Lightweight**: Uses WebSockets for efficient, predictable & reliable communication with little code required
9
+ - **Streamlined architecture**: No additional backend code is required, but server-authoritative behavior supported
10
+ - **State synchronization**: Built-in storage system keeps the full state synchronized across all clients, always conflict-free and in order
11
+ - **Resilient & secure connections**: Automatic reconnection handling & strict rate-limiting
12
+ - **Lightweight**: Uses WebSockets for efficient, predictable, reliable communication and has little dependencies
13
13
 
14
14
  ## Installation
15
15
 
@@ -17,58 +17,72 @@ PlaySocket eliminates the traditional complexity of multiplayer implementations:
17
17
  npm install playsocketjs
18
18
  ```
19
19
 
20
- ## Usage
20
+ ## Usage examples
21
21
 
22
- Note that in production, you should **always try...catch** these promises, such as socket.init(), to ensure your application continues to run if errors occur.
22
+ Note that in production, you should **always try...catch** promises, such as socket.init() – they can reject!
23
23
 
24
+ Initializing the client:
24
25
  ```javascript
25
26
  import PlaySocket from 'playsocketjs';
26
27
 
27
28
  // Create a new instance
28
- const socket = new PlaySocket('unique-client-id', {
29
+ const socket = new PlaySocket('unique-client-id', { // You can pass no ID to let the server pick one
29
30
  endpoint: 'wss://example.com/socket'
30
31
  });
31
32
 
32
- // Set up event handlers
33
+ // Set up event handlers (optional)
33
34
  socket.onEvent('status', status => console.log('Status:', status));
34
- socket.onEvent('storageUpdated', storage => console.log('Storage update received:', storage));
35
+ socket.onEvent('error', status => console.log('Error:', status));
35
36
 
36
- // Initialize the socket
37
- await socket.init();
37
+ const clientId = await socket.init(); // Initialize the socket
38
+ ```
38
39
 
40
+ Creating a room:
41
+ ```javascript
39
42
  // Create a new room
40
43
  const roomId = await socket.createRoom();
41
44
 
42
- // Optionally, you can create a room with an initial storage object
45
+ // Optionally, with initial storage
43
46
  const roomId = await socket.createRoom({
44
- players: [],
45
- moreThings: {},
47
+ players: ["this-player"],
46
48
  latestPlayer: null,
47
49
  });
50
+ ```
51
+
52
+ Joining a room:
53
+ ```javascript
54
+ await socket.joinRoom('room-id'); // Join an existing room
55
+ ```
48
56
 
49
- // Join an existing room
50
- await socket.joinRoom('room-id');
57
+ Leaving a room:
58
+ ```javascript
59
+ socket.destroy(); // To leave the room, destroy the instance
60
+ ```
51
61
 
52
- // Interact with the synced storage (available if in room)
53
- const currentState = socket.getStorage;
54
- socket.updateStorageArray('players', 'add-unique', { username: 'Player4', level: 2 }); // Special method to enable safe, simultaneous storage updates for arrays
55
- socket.updateStorage('latestPlayer', 'Player4'); // Regular synced storage update
62
+ Using the storage update event for reactivity:
63
+ ```javascript
64
+ const reactiveVariable = useState(); // Or $state(), reactive(), depending on your framework
65
+ socket.onEvent('storageUpdated', storage => (reactiveVariable = storage)); // Assign on update
66
+ ```
56
67
 
57
- // To leave the room, destroy the instance
58
- socket.destroy();
68
+ Interfacing with the synchronized storage (examples):
69
+ ```javascript
70
+ const currentState = socket.getStorage; // Synchronous, local access
71
+ socket.updateStorage('players', 'array-add-unique', { username: 'Player4', level: 2 }); // Special method to enable conflict-free additions for arrays
72
+ socket.updateStorage('latestPlayer', 'set', 'Player4'); // Regular synced storage update
59
73
  ```
60
74
 
61
75
  ## API Reference
62
76
 
63
77
  ### Constructor
64
78
 
79
+ 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.
81
+
65
82
  ```javascript
66
83
  new PlaySocket(id?: string, options: PlaySocketOptions)
67
84
  ```
68
85
 
69
- Creates a new PlaySocket instance with a specified ID and configuration options.
70
- Note: With PlaySocket, the id can be set to `null` to let the server pick a unique one.
71
-
72
86
  #### Configuration options
73
87
  - `endpoint`: WebSocket server endpoint (e.g., 'wss://example.com/socket')
74
88
  - `customData`: You can pass arbitrary data to the "clientRegistered" server event (optional)
@@ -76,39 +90,32 @@ Note: With PlaySocket, the id can be set to `null` to let the server pick a uniq
76
90
 
77
91
  ### Methods
78
92
 
79
- #### Core
80
-
81
93
  - `init()`: Initialize the WebSocket connection – Returns Promise (async) which resolves with the client's id
82
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)
83
95
  - `joinRoom(hostId: string)`: Join an existing room – Returns Promise (async)
84
96
  - `destroy()`: Use this to leave a room and close the connection
85
-
86
- #### State management
87
-
88
- - `updateStorage(key: string, value: any)`: Update a value in the synchronized storage
89
- - `updateStorageArray(key: string, operation: 'add' | 'add-unique' | 'remove-matching' | 'update-matching', value: any, updateValue?: any)`: Safely update arrays in storage by adding, removing, or updating items. This is necessary for when array updates might be happening simultaneously to ensure changes are being applied and not overwritten. Using add-unique instead of add ensures that this value can only be in the array once.
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
90
98
  - `onEvent(event: string, callback: Function)`: Register an event callback
91
99
 
92
- ##### Event types
100
+ #### Event types
93
101
 
94
102
  - `status`: Connection status updates (returns status `string`)
95
103
  - `error`: Error events (returns error `string`)
96
104
  - `instanceDestroyed`: Destruction event - triggered by manual .destroy() method invocation or by fatal errors and disconnects
97
105
  - `storageUpdated`: Storage state changes (returns storage `object`)
98
- - `hostMigrated`: Host changes (returns the new host's id `string`) – As compared to PlayPeerJS, the room id does NOT change when the host changes
106
+ - `hostMigrated`: Host changes (returns the new host's id `string`)
99
107
  - `clientConnected`: New client connected to the room (returns client-id `string`)
100
- - `clientDisconnected`: Client disconnected from the room (returns client-id `string`)
108
+ - `clientDisconnected`: Client disconnected from the room (returns client-id `string`, room id (if available) `string`)
101
109
 
102
110
  ### Properties (Read-only)
103
111
 
104
- The `id` is used to distinguish the client from other clients on the WebSocket server.
105
- Using a UUID is recommended, but it is also fine to use any other random string. If you're using a public WebSocket server, including your application's name in the `id` can help to prevent overlap (e.g. your-app-012345abcdef).
106
-
107
- - `id`: Client's unique identifier
112
+ - `id`: Client's unique identifier on the WebSocket server
108
113
  - `isHost`: If this user is currently assigned the host role
109
114
  - `connectionCount`: Number of active client connections in room (without you)
110
115
  - `getStorage`: Retrieve storage object
111
116
 
117
+  
118
+
112
119
  # PlaySocket Server
113
120
 
114
121
  PlaySocket includes a server implementation that can be set up in seconds.
@@ -121,18 +128,16 @@ To use the server component, you'll need to install playsocketjs and the ws pack
121
128
  npm install playsocketjs ws
122
129
  ```
123
130
 
124
- ## Usage
131
+ ## Usage examples
125
132
 
126
- Here are usage examples for a standalone server and an express application. The implementation is
127
- framework agnostic and the Express example can be adapted to any other backend solution.
133
+ Here are usage examples for a standalone server and an Express.js application.
128
134
 
129
135
  ### Standalone server
130
136
 
131
137
  ```javascript
132
138
  const PlaySocketServer = require('playsocketjs/server');
133
139
 
134
- // Create and start the server
135
- const server = new PlaySocketServer();
140
+ const server = new PlaySocketServer(); // Create and start the server (default path is /socket)
136
141
 
137
142
  // Gracefully disconnect all clients and close the server (optional)
138
143
  function shutdown() {
@@ -145,30 +150,28 @@ process.on('SIGINT', shutdown);
145
150
  process.on('SIGTERM', shutdown);
146
151
  ```
147
152
 
148
- ### With Express.js
153
+ ### Together with Express.js (or other Backend frameworks)
149
154
 
150
155
  ```javascript
151
156
  const express = require('express');
152
157
  const http = require('http');
153
158
  const PlaySocketServer = require('playsocketjs/server');
154
- const port = 3000;
155
159
 
156
160
  const app = express();
157
161
  const httpServer = http.createServer(app);
158
162
 
159
163
  // Create PlaySocket server with your HTTP server
160
- // You'll likely want to use a custom path in this scenario
161
164
  const playSocketServer = new PlaySocketServer({
162
165
  server: httpServer,
163
166
  path: '/socket'
164
167
  });
165
168
 
166
169
  // Start the server
167
- httpServer.listen(port, () => {
170
+ httpServer.listen(3000, () => {
168
171
  console.log('Server running on port 3000');
169
172
  });
170
173
 
171
- // Gracefully disconnect all clients and close the server (optional)
174
+ // Gracefully disconnect all clients and close the server (recommended)
172
175
  function shutdown() {
173
176
  server.stop();
174
177
  process.exit(0);
@@ -201,15 +204,17 @@ Creates a new PlaySocket Server instance with configuration options.
201
204
  - `stop`: Closes all active client connections, the websocket server and the underlying http server if it's standalone
202
205
  - `kick(clientId: string, reason?: string)`: Kick a client by their clientID – this will close their connection and set an error message
203
206
  - `onEvent(event: string, callback: Function)`: Register a server-side event callback
207
+ - `getRoomStorage(roomId: string)`: Get a snapshot of the current room storage (returns storage `object`)
208
+ - `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.
204
209
 
205
- ##### Event types
210
+ #### Event types
206
211
 
207
212
  - `clientRegistered`: Client registered with the server (returns the client's id `string`, customData `object`)
208
213
  - `clientDisconnected`: Client disconnected from the server (returns the client's id `string`)
209
214
  - `clientJoinedRoom`: Client joined a room – note that clients can only leave by disconnecting (returns the client's id `string`, room id `string`)
210
215
  - `roomCreated`: Client created a room (returns room id `string`)
211
216
  - `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
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
213
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
214
219
 
215
220
  ### Properties (Read-only)
@@ -218,8 +223,4 @@ Creates a new PlaySocket Server instance with configuration options.
218
223
 
219
224
  # License
220
225
 
221
- MIT
222
-
223
- # Contributing
224
-
225
- Please feel free to fork the repository and submit a Pull Request.
226
+ MIT
@@ -2407,7 +2407,7 @@ class PlaySocket {
2407
2407
 
2408
2408
  /**
2409
2409
  * Update the host in case a new one was chosen
2410
- * @param {string} hostId - Client id of new host
2410
+ * @param {string} hostId - Client ID of new host
2411
2411
  * @private
2412
2412
  */
2413
2413
  #setHost(hostId) {
@@ -2492,33 +2492,31 @@ class PlaySocket {
2492
2492
  /**
2493
2493
  * Update a value in the shared storage
2494
2494
  * @param {string} key - Storage key
2495
- * @param {*} value - New value
2495
+ * @param {string} type - Operation type: add, add-unique, remove-matching, update-matching
2496
+ * @param {*} value - New value or value to operate on
2497
+ * @param {*} updateValue - New value for update-matching
2496
2498
  */
2497
- updateStorage(key, value) {
2498
- if (this.#debug) console.log(LOG_PREFIX + "Property set update for key '" + key + "':", value);
2499
- const propUpdate = this.#crdtManager.updateProperty(key, "set", value);
2499
+ updateStorage(key, type, value, updateValue) {
2500
+ if (this.#debug) console.log(LOG_PREFIX + `Property update for key ${key}, operation ${type}, value ${value} and updateValue ${updateValue}.`);
2501
+ const propUpdate = this.#crdtManager.updateProperty(key, type, value, updateValue);
2500
2502
  this.#sendToServer({
2501
2503
  type: 'update_property',
2502
2504
  update: propUpdate
2503
2505
  });
2504
- if (this.#crdtManager.didPropertiesChange) this.#triggerEvent("storageUpdated", this.getStorage); // Always trigger callback after send in case msgs are sent in the callback (which would break the order)
2506
+ if (this.#crdtManager.didPropertiesChange) this.#triggerEvent("storageUpdated", this.getStorage); // Always trigger callback AFTER send in case msgs are sent in the callback (which would break the order)
2505
2507
  }
2506
2508
 
2507
2509
  /**
2508
- * Update an array with special operations in the shared storage
2509
- * @param {string} key - Storage key
2510
- * @param {string} operation - Operation type: add, add-unique, remove-matching, update-matching
2511
- * @param {*} value - Value to operate on
2512
- * @param {*} updateValue - New value for update-matching
2510
+ * Send a custom request to the server
2511
+ * @param {string} name - Name of the request
2512
+ * @param {*} [data] - Custom data
2513
2513
  */
2514
- updateStorageArray(key, operation, value, updateValue) {
2515
- if (this.#debug) console.log(LOG_PREFIX + `Property array update for key '${key}', operation '${operation}', value '${value}' and updateValue '${updateValue}'.`);
2516
- const propUpdate = this.#crdtManager.updateProperty(key, "array-" + operation, value, updateValue);
2514
+ sendRequest(name, data) {
2515
+ if (this.#debug) console.log(LOG_PREFIX + `Server request with name ${name} and data:`, data);
2517
2516
  this.#sendToServer({
2518
- type: 'update_property',
2519
- update: propUpdate
2517
+ type: 'request',
2518
+ request: { name, data }
2520
2519
  });
2521
- if (this.#crdtManager.didPropertiesChange) this.#triggerEvent("storageUpdated", this.getStorage);
2522
2520
  }
2523
2521
 
2524
2522
  /**
@@ -2341,7 +2341,7 @@ function requireServer () {
2341
2341
  clearTimeout(pd.timeout);
2342
2342
  this.#pendingDisconnects.delete(data.id);
2343
2343
 
2344
- // Re-assign old client id to the new ws connection
2344
+ // Re-assign old client ID to the new ws connection
2345
2345
  ws.clientId = data.id;
2346
2346
  this.#clients.set(data.id, ws);
2347
2347
 
@@ -2372,7 +2372,7 @@ function requireServer () {
2372
2372
  if (this.#clientRooms.get(ws.clientId) || this.#rooms[newRoomId]) {
2373
2373
  ws.send(encode({
2374
2374
  type: 'room_creation_failed',
2375
- reason: this.#clientRooms.get(ws.clientId) ? 'Already in a room' : 'Room id is taken'
2375
+ reason: this.#clientRooms.get(ws.clientId) ? 'Already in a room' : 'Room ID is taken'
2376
2376
  }), { binary: true });
2377
2377
  return;
2378
2378
  }
@@ -2491,6 +2491,11 @@ function requireServer () {
2491
2491
  }
2492
2492
  break;
2493
2493
 
2494
+ case 'request':
2495
+ const requestorRoomId = this.#clientRooms.get(ws.clientId) || null;
2496
+ this.#triggerEvent("requestReceived", { roomId: requestorRoomId, clientId: ws.clientId, name: data.request.name, data: data.request.data });
2497
+ break;
2498
+
2494
2499
  case 'disconnect':
2495
2500
  // Client signals to server that it will will willfully disconnect soon
2496
2501
  ws.willfulDisconnect = true;
@@ -2622,7 +2627,7 @@ function requireServer () {
2622
2627
  }
2623
2628
  }
2624
2629
 
2625
- this.#triggerEvent("clientDisconnected", ws.clientId);
2630
+ this.#triggerEvent("clientDisconnected", ws.clientId, roomId);
2626
2631
  }
2627
2632
 
2628
2633
  /**
@@ -2631,7 +2636,7 @@ function requireServer () {
2631
2636
  * @param {Function} callback - Callback function
2632
2637
  */
2633
2638
  onEvent(event, callback) {
2634
- const validEvents = ["clientRegistered", "clientDisconnected", "clientJoinedRoom", "roomCreationRequested", "roomCreated", "storageUpdateRequested", "roomDestroyed"];
2639
+ const validEvents = ["clientRegistered", "clientDisconnected", "clientJoinedRoom", "roomCreationRequested", "requestReceived", "roomCreated", "storageUpdateRequested", "roomDestroyed"];
2635
2640
  if (!validEvents.includes(event)) return console.warn(`Invalid PlaySocket event type "${event}"`);
2636
2641
  if (!this.#callbacks.has(event)) this.#callbacks.set(event, []);
2637
2642
  this.#callbacks.get(event).push(callback);
@@ -2671,6 +2676,44 @@ function requireServer () {
2671
2676
  return true;
2672
2677
  }
2673
2678
 
2679
+ /**
2680
+ * Get snapshot of a room's storage
2681
+ * @returns {Object} - Storage object
2682
+ */
2683
+ getRoomStorage(roomId) {
2684
+ const room = this.#rooms[roomId];
2685
+ if (room) return room.crdtManager.getPropertyStore;
2686
+ }
2687
+
2688
+ /**
2689
+ * Update a value in a room's storage
2690
+ * @param {string} roomId - Room ID
2691
+ * @param {string} key - Storage key
2692
+ * @param {string} type - Operation type
2693
+ * @param {*} value - New value or value to operate on
2694
+ * @param {*} updateValue - New value for update-matching
2695
+ */
2696
+ updateRoomStorage(roomId, key, type, value, updateValue) {
2697
+ if (this.#debug) console.log(`Playsocket server property update for room ${roomId}, key ${key}, operation ${type}, value ${value} and updateValue ${updateValue}.`);
2698
+ const room = this.#rooms[roomId];
2699
+ if (room) {
2700
+ const propertyUpdate = room.crdtManager.updateProperty(key, type, value, updateValue);
2701
+ const currentVersion = this.#roomVersions.get(roomId) + 1;
2702
+ this.#roomVersions.set(roomId, currentVersion); // Increment version for this room
2703
+
2704
+ room.participants?.forEach(p => {
2705
+ const client = this.#clients.get(p);
2706
+ if (client) {
2707
+ client.send(encode({
2708
+ type: 'property_updated',
2709
+ update: propertyUpdate,
2710
+ version: currentVersion
2711
+ }), { binary: true });
2712
+ }
2713
+ });
2714
+ }
2715
+ }
2716
+
2674
2717
  /**
2675
2718
  * Close all client connections, then close the websocket and http server
2676
2719
  */
@@ -2683,11 +2726,7 @@ function requireServer () {
2683
2726
  });
2684
2727
  this.#wss.close();
2685
2728
  }
2686
- if (this.#server && this.#ownsServer) {
2687
- this.#server.close(() => {
2688
- console.log('PlaySocket server stopped.');
2689
- });
2690
- }
2729
+ if (this.#server && this.#ownsServer) this.#server.close(() => { console.log('PlaySocket server stopped.'); });
2691
2730
  this.#pendingDisconnects.forEach((data) => {
2692
2731
  clearTimeout(data.timeout);
2693
2732
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playsocketjs",
3
- "version": "1.4.1",
3
+ "version": "2.1.0",
4
4
  "description": "WebSocket wrapper for creating simple multiplayer systems with ease.",
5
5
  "module": "dist/playsocket-client.js",
6
6
  "exports": {