playsocketjs 2.1.1 → 2.2.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 +11 -3
- package/dist/playsocket-client.js +26 -20
- package/dist/playsocket-server.js +28 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,6 +72,11 @@ 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
|
|
@@ -91,10 +96,11 @@ new PlaySocket(id?: string, options: PlaySocketOptions)
|
|
|
91
96
|
### Methods
|
|
92
97
|
|
|
93
98
|
- `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)
|
|
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). The absolute client maximum is 100
|
|
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
|
-
- `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
|
|
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
|
|
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
|
|
@@ -111,7 +117,7 @@ new PlaySocket(id?: string, options: PlaySocketOptions)
|
|
|
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
|
|
120
|
+
- `connectionCount`: Number of active client connections in room (without yourself)
|
|
115
121
|
- `getStorage`: Retrieve storage object
|
|
116
122
|
|
|
117
123
|
|
|
@@ -210,12 +216,14 @@ Creates a new PlaySocket Server instance with configuration options.
|
|
|
210
216
|
#### Event types
|
|
211
217
|
|
|
212
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
|
|
213
220
|
- `clientDisconnected`: Client disconnected from the server (returns the client's ID `string`)
|
|
214
221
|
- `clientJoinedRoom`: Client joined a room – note that clients can only leave by disconnecting (returns the client's ID `string`, room ID `string`)
|
|
215
222
|
- `roomCreated`: Client created a room (returns room ID `string`)
|
|
216
223
|
- `roomDestroyed`: Room was destroyed, this happens when all participants leave (returns room ID `string`)
|
|
217
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
|
|
218
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
|
|
|
@@ -1758,9 +1758,11 @@ function requireCrdtManager () {
|
|
|
1758
1758
|
const operation = this.#sanitizeValue(rawOperation);
|
|
1759
1759
|
if (this.#debug) console.log(CONSOLE_PREFIX + "Importing update:", data); // Debug
|
|
1760
1760
|
|
|
1761
|
+
// Check key limit to afeguard against too many keys
|
|
1762
|
+
if (this.#keyOperations.size >= 100 && !this.#keyOperations.has(key)) throw new Error('Key limit exceeded!');
|
|
1763
|
+
|
|
1761
1764
|
// Get COPY of current ops (or empty array if none yet)
|
|
1762
1765
|
const currentOps = [...(this.#keyOperations.get(key) || [])];
|
|
1763
|
-
|
|
1764
1766
|
// Merge vector clocks (always take max value)
|
|
1765
1767
|
for (const [id, counter] of vectorClock) {
|
|
1766
1768
|
if (!this.#vectorClock.has(id) || this.#vectorClock.get(id) < counter) {
|
|
@@ -1768,7 +1770,7 @@ function requireCrdtManager () {
|
|
|
1768
1770
|
}
|
|
1769
1771
|
}
|
|
1770
1772
|
|
|
1771
|
-
//
|
|
1773
|
+
// Safeguard against massive vector clocks
|
|
1772
1774
|
if (this.#vectorClock.size > 1000) this.#vectorClock = new Map([...this.#vectorClock].slice(-100));
|
|
1773
1775
|
|
|
1774
1776
|
// Add new operation if it's not already added
|
|
@@ -2019,11 +2021,15 @@ function requireCrdtManager () {
|
|
|
2019
2021
|
}
|
|
2020
2022
|
|
|
2021
2023
|
/**
|
|
2022
|
-
* Remove HTML to prevent XSS
|
|
2023
|
-
* @param {Object} obj
|
|
2024
|
+
* Remove HTML to prevent XSS and enforce size limits
|
|
2025
|
+
* @param {Object} obj
|
|
2024
2026
|
* @returns {Object} - Sanitized object
|
|
2025
2027
|
*/
|
|
2026
2028
|
#sanitizeValue(obj) {
|
|
2029
|
+
// Check total serialized size
|
|
2030
|
+
const jsonString = JSON.stringify(obj);
|
|
2031
|
+
if (jsonString?.length > 50000) throw new Error('Value too large!'); // 50KB limit
|
|
2032
|
+
|
|
2027
2033
|
if (typeof obj === 'string') return (obj.includes('<') || obj.includes('>')) ? obj.replace(/[<>]/g, '') : obj;
|
|
2028
2034
|
if (Array.isArray(obj)) return obj.map(item => this.#sanitizeValue(item));
|
|
2029
2035
|
if (obj && typeof obj === 'object') return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, this.#sanitizeValue(v)]));
|
|
@@ -2227,6 +2233,22 @@ class PlaySocket {
|
|
|
2227
2233
|
if (!message.type) return;
|
|
2228
2234
|
|
|
2229
2235
|
switch (message.type) {
|
|
2236
|
+
case 'registration_failed':
|
|
2237
|
+
if (this.#pendingRegistration) {
|
|
2238
|
+
this.#pendingRegistration.reject(new Error("Failed to register: " + message.reason));
|
|
2239
|
+
this.#triggerEvent("error", "Failed to register: " + message.reason);
|
|
2240
|
+
}
|
|
2241
|
+
break;
|
|
2242
|
+
|
|
2243
|
+
case 'registered':
|
|
2244
|
+
if (this.#pendingRegistration) {
|
|
2245
|
+
this.#sessionToken = message.sessionToken;
|
|
2246
|
+
this.#initialized = true;
|
|
2247
|
+
this.#pendingRegistration.resolve(message.id);
|
|
2248
|
+
this.#triggerEvent("status", "Connected to server.");
|
|
2249
|
+
}
|
|
2250
|
+
break;
|
|
2251
|
+
|
|
2230
2252
|
case 'join_accepted':
|
|
2231
2253
|
// Connected to room
|
|
2232
2254
|
if (this.#pendingJoin) {
|
|
@@ -2293,22 +2315,6 @@ class PlaySocket {
|
|
|
2293
2315
|
}
|
|
2294
2316
|
break;
|
|
2295
2317
|
|
|
2296
|
-
case 'id_taken':
|
|
2297
|
-
if (this.#pendingRegistration) {
|
|
2298
|
-
this.#pendingRegistration.reject(new Error("This id is already in use"));
|
|
2299
|
-
this.#triggerEvent("error", "This id is taken.");
|
|
2300
|
-
}
|
|
2301
|
-
break;
|
|
2302
|
-
|
|
2303
|
-
case 'registered':
|
|
2304
|
-
if (this.#pendingRegistration) {
|
|
2305
|
-
this.#sessionToken = message.sessionToken;
|
|
2306
|
-
this.#initialized = true;
|
|
2307
|
-
this.#pendingRegistration.resolve(message.id);
|
|
2308
|
-
this.#triggerEvent("status", "Connected to server.");
|
|
2309
|
-
}
|
|
2310
|
-
break;
|
|
2311
|
-
|
|
2312
2318
|
case 'property_updated':
|
|
2313
2319
|
this.#roomVersion++; // Increment room version
|
|
2314
2320
|
if (this.#debug) console.log(LOG_PREFIX + "Property update received:", message.update);
|
|
@@ -1884,9 +1884,11 @@ function requireCrdtManager () {
|
|
|
1884
1884
|
const operation = this.#sanitizeValue(rawOperation);
|
|
1885
1885
|
if (this.#debug) console.log(CONSOLE_PREFIX + "Importing update:", data); // Debug
|
|
1886
1886
|
|
|
1887
|
+
// Check key limit to afeguard against too many keys
|
|
1888
|
+
if (this.#keyOperations.size >= 100 && !this.#keyOperations.has(key)) throw new Error('Key limit exceeded!');
|
|
1889
|
+
|
|
1887
1890
|
// Get COPY of current ops (or empty array if none yet)
|
|
1888
1891
|
const currentOps = [...(this.#keyOperations.get(key) || [])];
|
|
1889
|
-
|
|
1890
1892
|
// Merge vector clocks (always take max value)
|
|
1891
1893
|
for (const [id, counter] of vectorClock) {
|
|
1892
1894
|
if (!this.#vectorClock.has(id) || this.#vectorClock.get(id) < counter) {
|
|
@@ -1894,7 +1896,7 @@ function requireCrdtManager () {
|
|
|
1894
1896
|
}
|
|
1895
1897
|
}
|
|
1896
1898
|
|
|
1897
|
-
//
|
|
1899
|
+
// Safeguard against massive vector clocks
|
|
1898
1900
|
if (this.#vectorClock.size > 1000) this.#vectorClock = new Map([...this.#vectorClock].slice(-100));
|
|
1899
1901
|
|
|
1900
1902
|
// Add new operation if it's not already added
|
|
@@ -2145,11 +2147,15 @@ function requireCrdtManager () {
|
|
|
2145
2147
|
}
|
|
2146
2148
|
|
|
2147
2149
|
/**
|
|
2148
|
-
* Remove HTML to prevent XSS
|
|
2149
|
-
* @param {Object} obj
|
|
2150
|
+
* Remove HTML to prevent XSS and enforce size limits
|
|
2151
|
+
* @param {Object} obj
|
|
2150
2152
|
* @returns {Object} - Sanitized object
|
|
2151
2153
|
*/
|
|
2152
2154
|
#sanitizeValue(obj) {
|
|
2155
|
+
// Check total serialized size
|
|
2156
|
+
const jsonString = JSON.stringify(obj);
|
|
2157
|
+
if (jsonString?.length > 50000) throw new Error('Value too large!'); // 50KB limit
|
|
2158
|
+
|
|
2153
2159
|
if (typeof obj === 'string') return (obj.includes('<') || obj.includes('>')) ? obj.replace(/[<>]/g, '') : obj;
|
|
2154
2160
|
if (Array.isArray(obj)) return obj.map(item => this.#sanitizeValue(item));
|
|
2155
2161
|
if (obj && typeof obj === 'object') return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, this.#sanitizeValue(v)]));
|
|
@@ -2299,7 +2305,7 @@ function requireServer () {
|
|
|
2299
2305
|
case 'register':
|
|
2300
2306
|
// Register client ID if provided & check for a duplicate
|
|
2301
2307
|
if (data.id && this.#clients.get(data.id)) {
|
|
2302
|
-
ws.send(encode({ type: '
|
|
2308
|
+
ws.send(encode({ type: 'registration_failed', reason: 'ID is taken.' }), { binary: true });
|
|
2303
2309
|
return;
|
|
2304
2310
|
}
|
|
2305
2311
|
|
|
@@ -2315,11 +2321,20 @@ function requireServer () {
|
|
|
2315
2321
|
}
|
|
2316
2322
|
}
|
|
2317
2323
|
if (!data.id) {
|
|
2318
|
-
ws.send(encode({ type: '
|
|
2324
|
+
ws.send(encode({ type: 'registration_failed', reason: 'No available ID found.' }), { binary: true });
|
|
2319
2325
|
throw new Error('Failed to generate unique ID!');
|
|
2320
2326
|
}
|
|
2321
2327
|
}
|
|
2322
2328
|
|
|
2329
|
+
// Event callback
|
|
2330
|
+
const registrationAllowed = await this.#triggerEvent("clientRegistrationRequested", data.id, data.customData);
|
|
2331
|
+
if (registrationAllowed === false || typeof registrationAllowed === 'string') {
|
|
2332
|
+
ws.send(encode({
|
|
2333
|
+
type: 'registration_failed',
|
|
2334
|
+
reason: typeof registrationAllowed === 'string' ? registrationAllowed : 'Denied.'
|
|
2335
|
+
}), { binary: true });
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2323
2338
|
|
|
2324
2339
|
ws.clientId = data.id; // Adds the provided id to the ws object as clientId
|
|
2325
2340
|
this.#clients.set(data.id, ws);
|
|
@@ -2403,7 +2418,7 @@ function requireServer () {
|
|
|
2403
2418
|
this.#rooms[newRoomId] = {
|
|
2404
2419
|
participants: [ws.clientId],
|
|
2405
2420
|
host: ws.clientId,
|
|
2406
|
-
maxSize: data.size ||
|
|
2421
|
+
maxSize: Math.min(Number(data.size), 100) || 100, // Max. limit is 100 clients / room
|
|
2407
2422
|
crdtManager: roomCrdtManager
|
|
2408
2423
|
};
|
|
2409
2424
|
this.#clientRooms.set(ws.clientId, newRoomId); // Add client to the room
|
|
@@ -2419,12 +2434,12 @@ function requireServer () {
|
|
|
2419
2434
|
const room = this.#rooms[roomId];
|
|
2420
2435
|
|
|
2421
2436
|
if (!room ||
|
|
2422
|
-
(room.
|
|
2437
|
+
(room.participants.length >= room.maxSize) ||
|
|
2423
2438
|
this.#clientRooms.get(ws.clientId)) {
|
|
2424
2439
|
ws.send(encode({
|
|
2425
2440
|
type: 'join_rejected',
|
|
2426
2441
|
reason: !room ? 'Room not found' :
|
|
2427
|
-
room.
|
|
2442
|
+
room.participants.length >= room.maxSize ? 'Room full' :
|
|
2428
2443
|
'Already in a room'
|
|
2429
2444
|
}), { binary: true });
|
|
2430
2445
|
return;
|
|
@@ -2464,8 +2479,8 @@ function requireServer () {
|
|
|
2464
2479
|
|
|
2465
2480
|
if (updateRoom && data.update) {
|
|
2466
2481
|
// Check if update is allowed via event callback (provide clone to ensure update integrity)
|
|
2467
|
-
const
|
|
2468
|
-
if (
|
|
2482
|
+
const updateAllowed = await this.#triggerEvent("storageUpdateRequested", { roomId: updateRoomId, clientId: ws.clientId, update: structuredClone(data.update) });
|
|
2483
|
+
if (updateAllowed === false) {
|
|
2469
2484
|
ws.send(encode({
|
|
2470
2485
|
type: 'property_update_rejected',
|
|
2471
2486
|
state: updateRoom.crdtManager.getState
|
|
@@ -2638,7 +2653,7 @@ function requireServer () {
|
|
|
2638
2653
|
* @param {Function} callback - Callback function
|
|
2639
2654
|
*/
|
|
2640
2655
|
onEvent(event, callback) {
|
|
2641
|
-
const validEvents = ["clientRegistered", "clientDisconnected", "clientJoinedRoom", "roomCreationRequested", "requestReceived", "roomCreated", "storageUpdateRequested", "roomDestroyed"];
|
|
2656
|
+
const validEvents = ["clientRegistered", "clientRegistrationRequested", "clientDisconnected", "clientJoinedRoom", "roomCreationRequested", "requestReceived", "roomCreated", "storageUpdateRequested", "roomDestroyed"];
|
|
2642
2657
|
if (!validEvents.includes(event)) return console.warn(`Invalid PlaySocket event type "${event}"`);
|
|
2643
2658
|
if (!this.#callbacks.has(event)) this.#callbacks.set(event, []);
|
|
2644
2659
|
this.#callbacks.get(event).push(callback);
|
|
@@ -2669,8 +2684,7 @@ function requireServer () {
|
|
|
2669
2684
|
for (const callback of callbacks) {
|
|
2670
2685
|
try {
|
|
2671
2686
|
const result = await callback(...args);
|
|
2672
|
-
if (result
|
|
2673
|
-
if (typeof result === 'object') return result; // If response object is provided -> pass on
|
|
2687
|
+
if (result != null) return result; // Return any non-null/undefined result
|
|
2674
2688
|
} catch (error) {
|
|
2675
2689
|
console.error(`PlaySocket ${event} callback error:`, error);
|
|
2676
2690
|
}
|