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 +1 -1
- package/dist/playsocket-client.js +78 -50
- package/dist/{playsocket-server.mjs → playsocket-server.js} +114 -67
- package/package.json +12 -12
- package/dist/playsocket-server.cjs +0 -2667
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { WebSocketServer } from 'ws';
|
|
2
|
-
import
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
3
|
|
|
4
4
|
function utf8Count(str) {
|
|
5
5
|
const strLength = str.length;
|
|
@@ -1686,13 +1686,15 @@ function decode(buffer, options) {
|
|
|
1686
1686
|
|
|
1687
1687
|
const CONSOLE_PREFIX = "PlaySocket CRDT manager: ";
|
|
1688
1688
|
|
|
1689
|
+
/**
|
|
1690
|
+
* CRDT Manager
|
|
1691
|
+
*/
|
|
1689
1692
|
class CRDTManager {
|
|
1690
1693
|
// Storage
|
|
1691
1694
|
#replicaId;
|
|
1692
1695
|
#keyOperations = new Map();
|
|
1693
1696
|
#vectorClock = new Map();
|
|
1694
1697
|
|
|
1695
|
-
// Local only
|
|
1696
1698
|
#propertyStore = {}; // Current local values per key, as object
|
|
1697
1699
|
#lastPropertyStore = {}; // Last property store to compare against
|
|
1698
1700
|
|
|
@@ -1705,6 +1707,7 @@ class CRDTManager {
|
|
|
1705
1707
|
|
|
1706
1708
|
/**
|
|
1707
1709
|
* Create a new instance
|
|
1710
|
+
* @param {boolean} [debug=false] - Enable debug logging
|
|
1708
1711
|
*/
|
|
1709
1712
|
constructor(debug) {
|
|
1710
1713
|
if (debug) this.#debug = true;
|
|
@@ -1714,7 +1717,7 @@ class CRDTManager {
|
|
|
1714
1717
|
|
|
1715
1718
|
/**
|
|
1716
1719
|
* Import the entire state of the CRDT manager (this overwrites the old state)
|
|
1717
|
-
* @param {
|
|
1720
|
+
* @param {object} state - Full state of the CRDT manager
|
|
1718
1721
|
*/
|
|
1719
1722
|
importState(state) {
|
|
1720
1723
|
try {
|
|
@@ -1731,7 +1734,7 @@ class CRDTManager {
|
|
|
1731
1734
|
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
|
|
1732
1735
|
|
|
1733
1736
|
// Map all operation uuids to current timestamp
|
|
1734
|
-
this.#keyOperations.forEach((value,
|
|
1737
|
+
this.#keyOperations.forEach((value, _key) => {
|
|
1735
1738
|
value?.forEach((operation) => {
|
|
1736
1739
|
this.#opUuidTimestamp.set(operation?.uuid, Date.now());
|
|
1737
1740
|
});
|
|
@@ -1747,7 +1750,7 @@ class CRDTManager {
|
|
|
1747
1750
|
|
|
1748
1751
|
/**
|
|
1749
1752
|
* Import property update
|
|
1750
|
-
* @param {
|
|
1753
|
+
* @param {object} data - Property update data
|
|
1751
1754
|
*/
|
|
1752
1755
|
importPropertyUpdate(data) {
|
|
1753
1756
|
try {
|
|
@@ -1789,11 +1792,11 @@ class CRDTManager {
|
|
|
1789
1792
|
|
|
1790
1793
|
/**
|
|
1791
1794
|
* Update a property
|
|
1792
|
-
* @param {string} key
|
|
1793
|
-
* @param {string} type
|
|
1794
|
-
* @param {*} value
|
|
1795
|
-
* @param {*} updateValue
|
|
1796
|
-
* @returns {
|
|
1795
|
+
* @param {string} key - Storage key
|
|
1796
|
+
* @param {string} type - Operation type
|
|
1797
|
+
* @param {*} value - New value, or value to match
|
|
1798
|
+
* @param {*} updateValue - New value for 'matching' operations
|
|
1799
|
+
* @returns {object | undefined} - Returns the property update
|
|
1797
1800
|
*/
|
|
1798
1801
|
updateProperty(key, type, value, updateValue) {
|
|
1799
1802
|
try {
|
|
@@ -1830,6 +1833,9 @@ class CRDTManager {
|
|
|
1830
1833
|
}
|
|
1831
1834
|
}
|
|
1832
1835
|
|
|
1836
|
+
/**
|
|
1837
|
+
* Check if garbage collected can be performed & run it
|
|
1838
|
+
*/
|
|
1833
1839
|
#checkGarbageCollection() {
|
|
1834
1840
|
const MIN_GC_DELAY = 1000; // Minimum 1s delay between garbage collection runs
|
|
1835
1841
|
const MIN_AGE_FOR_GC = 5000; // Garbage collect ops that are older than 5s
|
|
@@ -1855,7 +1861,7 @@ class CRDTManager {
|
|
|
1855
1861
|
if (this.#debug) console.log(CONSOLE_PREFIX + `Running garbage collection for key ${key} with current operations:`, operations);
|
|
1856
1862
|
const retainOps = operations.slice(-retainCount); // Newest ops
|
|
1857
1863
|
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
|
|
1864
|
+
const baselineVectorClock = removeOps[removeOps.length - 1]?.vectorClock || []; // Use the vector clock from the last operation that we remove/overwrite
|
|
1859
1865
|
|
|
1860
1866
|
// Calculate the value at the point where retained operations start
|
|
1861
1867
|
let baselineValue = null;
|
|
@@ -1886,9 +1892,9 @@ class CRDTManager {
|
|
|
1886
1892
|
|
|
1887
1893
|
/**
|
|
1888
1894
|
* Create an operation object
|
|
1889
|
-
* @param {
|
|
1890
|
-
* @param {Array} vectorClock
|
|
1891
|
-
* @returns {
|
|
1895
|
+
* @param {object} data - Operation data, consists of type, value and updateValue
|
|
1896
|
+
* @param {Array} vectorClock - Vector clock of the replica/client
|
|
1897
|
+
* @returns {object} - Operation object
|
|
1892
1898
|
*/
|
|
1893
1899
|
#createOperation(data, vectorClock) {
|
|
1894
1900
|
const newOperation = {
|
|
@@ -1903,7 +1909,7 @@ class CRDTManager {
|
|
|
1903
1909
|
|
|
1904
1910
|
/**
|
|
1905
1911
|
* Process a property's value by applying all operations and set it in the property store
|
|
1906
|
-
* @param {string} key
|
|
1912
|
+
* @param {string} key - Storage key
|
|
1907
1913
|
*/
|
|
1908
1914
|
#processLocalProperty(key) {
|
|
1909
1915
|
try {
|
|
@@ -1930,8 +1936,8 @@ class CRDTManager {
|
|
|
1930
1936
|
}
|
|
1931
1937
|
|
|
1932
1938
|
/**
|
|
1933
|
-
* Sort by vector clock (causal order)
|
|
1934
|
-
* @param {Array} operations
|
|
1939
|
+
* Sort operations by vector clock (causal order)
|
|
1940
|
+
* @param {Array} operations - Full operations that include vector clocks, data and a source
|
|
1935
1941
|
* @returns {Array} - Sorted operations
|
|
1936
1942
|
*/
|
|
1937
1943
|
#sortByVectorClock(operations) {
|
|
@@ -1967,10 +1973,10 @@ class CRDTManager {
|
|
|
1967
1973
|
|
|
1968
1974
|
/**
|
|
1969
1975
|
* Handle an operation
|
|
1970
|
-
* @param {*} curValue
|
|
1971
|
-
* @param {string} type
|
|
1972
|
-
* @param {*} value
|
|
1973
|
-
* @param {*} [updateValue]
|
|
1976
|
+
* @param {*} curValue - Current value of the storage key
|
|
1977
|
+
* @param {string} type - Operation type
|
|
1978
|
+
* @param {*} value - New value, or value to match
|
|
1979
|
+
* @param {*} [updateValue] - New value for 'matching' operations
|
|
1974
1980
|
* @returns {*} - Value after the operation
|
|
1975
1981
|
*/
|
|
1976
1982
|
#handleOperation(curValue, type, value, updateValue) {
|
|
@@ -2002,10 +2008,11 @@ class CRDTManager {
|
|
|
2002
2008
|
case 'array-remove-matching':
|
|
2003
2009
|
curValue = curValue.filter(item => !compare(item));
|
|
2004
2010
|
break;
|
|
2005
|
-
case 'array-update-matching':
|
|
2011
|
+
case 'array-update-matching': {
|
|
2006
2012
|
const index = curValue.findIndex(compare);
|
|
2007
2013
|
if (index !== -1) curValue[index] = updateValue;
|
|
2008
2014
|
break;
|
|
2015
|
+
}
|
|
2009
2016
|
}
|
|
2010
2017
|
}
|
|
2011
2018
|
|
|
@@ -2019,42 +2026,53 @@ class CRDTManager {
|
|
|
2019
2026
|
|
|
2020
2027
|
/**
|
|
2021
2028
|
* Remove HTML to prevent XSS and enforce size limits
|
|
2022
|
-
* @param {
|
|
2023
|
-
* @returns {
|
|
2029
|
+
* @param {*} value - Value to sanitize
|
|
2030
|
+
* @returns {*} - Sanitized value
|
|
2024
2031
|
*/
|
|
2025
|
-
#sanitizeValue(
|
|
2032
|
+
#sanitizeValue(value) {
|
|
2026
2033
|
// Check total serialized size
|
|
2027
|
-
const jsonString = JSON.stringify(
|
|
2034
|
+
const jsonString = JSON.stringify(value);
|
|
2028
2035
|
if (jsonString?.length > 50000) throw new Error('Value too large!'); // 50KB limit
|
|
2029
2036
|
|
|
2030
|
-
if (typeof
|
|
2031
|
-
if (Array.isArray(
|
|
2032
|
-
if (
|
|
2033
|
-
return
|
|
2037
|
+
if (typeof value === 'string') return (value.includes('<') || value.includes('>')) ? value.replace(/[<>]/g, '') : value;
|
|
2038
|
+
if (Array.isArray(value)) return value.map(item => this.#sanitizeValue(item));
|
|
2039
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, this.#sanitizeValue(v)]));
|
|
2040
|
+
return value;
|
|
2034
2041
|
}
|
|
2035
2042
|
|
|
2036
|
-
|
|
2043
|
+
/**
|
|
2044
|
+
* Get property store
|
|
2045
|
+
* @returns {Record<string, any>} - Object with key value pairs
|
|
2046
|
+
*/
|
|
2037
2047
|
get getPropertyStore() {
|
|
2038
2048
|
try {
|
|
2039
2049
|
return structuredClone(this.#propertyStore); // Return deep clone
|
|
2040
2050
|
} catch {
|
|
2041
2051
|
console.error(CONSOLE_PREFIX + "Failed to parse property store");
|
|
2042
|
-
return {}
|
|
2052
|
+
return {}
|
|
2043
2053
|
}
|
|
2044
2054
|
}
|
|
2045
2055
|
|
|
2046
|
-
|
|
2056
|
+
/**
|
|
2057
|
+
* Check for changes in the local property store
|
|
2058
|
+
* @returns {boolean} - Check if any values changed going from the last to the current property update
|
|
2059
|
+
*/
|
|
2047
2060
|
get didPropertiesChange() {
|
|
2048
2061
|
try {
|
|
2049
2062
|
if (JSON.stringify(this.#propertyStore) !== JSON.stringify(this.#lastPropertyStore)) {
|
|
2050
2063
|
this.#lastPropertyStore = structuredClone(this.#propertyStore);
|
|
2051
2064
|
return true;
|
|
2052
2065
|
}
|
|
2053
|
-
} catch {
|
|
2066
|
+
} catch {
|
|
2067
|
+
console.error(CONSOLE_PREFIX + "Failed to stringify and compare property stores");
|
|
2068
|
+
}
|
|
2054
2069
|
return false;
|
|
2055
2070
|
}
|
|
2056
2071
|
|
|
2057
|
-
|
|
2072
|
+
/**
|
|
2073
|
+
* Export state (can be imported using importState, converts the maps to arrays for serialization)
|
|
2074
|
+
* @returns {object} - State
|
|
2075
|
+
*/
|
|
2058
2076
|
get getState() {
|
|
2059
2077
|
return {
|
|
2060
2078
|
keyOperations: [...this.#keyOperations.entries()],
|
|
@@ -2063,8 +2081,15 @@ class CRDTManager {
|
|
|
2063
2081
|
}
|
|
2064
2082
|
}
|
|
2065
2083
|
|
|
2084
|
+
/* eslint-disable jsdoc/require-returns */
|
|
2085
|
+
|
|
2086
|
+
/**
|
|
2087
|
+
* @typedef {import('node:http').Server} HttpServer
|
|
2088
|
+
* @typedef {import('ws').WebSocket} WebSocket
|
|
2089
|
+
*/
|
|
2090
|
+
|
|
2066
2091
|
/**
|
|
2067
|
-
*
|
|
2092
|
+
* PlaySocket Server
|
|
2068
2093
|
*/
|
|
2069
2094
|
class PlaySocketServer {
|
|
2070
2095
|
#server;
|
|
@@ -2086,10 +2111,12 @@ class PlaySocketServer {
|
|
|
2086
2111
|
|
|
2087
2112
|
/**
|
|
2088
2113
|
* Create a new PlaySocketServer instance
|
|
2089
|
-
* @param {
|
|
2114
|
+
* @param {object} options - Server configuration options
|
|
2090
2115
|
* @param {HttpServer} [options.server] - Existing http server
|
|
2091
2116
|
* @param {number} [options.port=3000] - Port to listen on (if no server provided)
|
|
2092
|
-
* @param {string} [options.path='/
|
|
2117
|
+
* @param {string} [options.path='/'] - WebSocket endpoint path
|
|
2118
|
+
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
2119
|
+
* @param {number} [options.rateLimit=20] - Maximum number of operations per second per client
|
|
2093
2120
|
*/
|
|
2094
2121
|
constructor(options = {}) {
|
|
2095
2122
|
const { server, port = 3000, path = '/', debug = false, rateLimit = 20 } = options;
|
|
@@ -2103,7 +2130,7 @@ class PlaySocketServer {
|
|
|
2103
2130
|
this.#server = server; // Use provided HTTP server
|
|
2104
2131
|
} else {
|
|
2105
2132
|
this.#ownsServer = true;
|
|
2106
|
-
this.#server =
|
|
2133
|
+
this.#server = createServer(); // Create HTTP server and start it
|
|
2107
2134
|
this.#server.listen(port, () => {
|
|
2108
2135
|
console.log(`PlaySocket server running on port ${port}.`);
|
|
2109
2136
|
});
|
|
@@ -2144,6 +2171,8 @@ class PlaySocketServer {
|
|
|
2144
2171
|
|
|
2145
2172
|
/**
|
|
2146
2173
|
* Handle WebSocket message
|
|
2174
|
+
* @param {WebSocket} ws - WebSocket client
|
|
2175
|
+
* @param {Buffer} message - Message encoded with messagepack
|
|
2147
2176
|
* @private
|
|
2148
2177
|
*/
|
|
2149
2178
|
async #handleMessage(ws, message) {
|
|
@@ -2156,12 +2185,13 @@ class PlaySocketServer {
|
|
|
2156
2185
|
if (!ws.isTerminating) {
|
|
2157
2186
|
ws.isTerminating = true; // Prevent multiple terminate calls (it is async)
|
|
2158
2187
|
ws.terminate();
|
|
2159
|
-
|
|
2188
|
+
console.error(`Connection ${ws.connectionId} terminated due to rate limit violations.`);
|
|
2189
|
+
return;
|
|
2160
2190
|
}
|
|
2161
2191
|
}
|
|
2162
2192
|
|
|
2163
2193
|
switch (data.type) {
|
|
2164
|
-
case 'register':
|
|
2194
|
+
case 'register': {
|
|
2165
2195
|
// Register client ID if provided & check for a duplicate
|
|
2166
2196
|
if (data.id && this.#clients.get(data.id)) {
|
|
2167
2197
|
ws.send(encode({ type: 'registration_failed', reason: 'ID is taken.' }), { binary: true });
|
|
@@ -2200,8 +2230,9 @@ class PlaySocketServer {
|
|
|
2200
2230
|
ws.send(encode({ type: 'registered', id: data.id, sessionToken }), { binary: true });
|
|
2201
2231
|
this.#triggerEvent("clientRegistered", data.id, data.customData);
|
|
2202
2232
|
break;
|
|
2233
|
+
}
|
|
2203
2234
|
|
|
2204
|
-
case 'reconnect':
|
|
2235
|
+
case 'reconnect': {
|
|
2205
2236
|
// If user is pending disconnect, respond (otherwise it's too late)
|
|
2206
2237
|
const pd = this.#pendingDisconnects.get(data.id);
|
|
2207
2238
|
if (pd && data.sessionToken) {
|
|
@@ -2238,8 +2269,9 @@ class PlaySocketServer {
|
|
|
2238
2269
|
ws.send(encode({ type: 'reconnection_failed', reason: "Client unknown to server." }), { binary: true });
|
|
2239
2270
|
}
|
|
2240
2271
|
break;
|
|
2272
|
+
}
|
|
2241
2273
|
|
|
2242
|
-
case 'create_room':
|
|
2274
|
+
case 'create_room': {
|
|
2243
2275
|
if (!ws.clientId) return;
|
|
2244
2276
|
|
|
2245
2277
|
let newRoomId;
|
|
@@ -2307,8 +2339,9 @@ class PlaySocketServer {
|
|
|
2307
2339
|
this.#triggerEvent("roomCreated", newRoomId);
|
|
2308
2340
|
if (this.#debug) console.log(`Room ${newRoomId} created with initial storage:`, data.initialStorage);
|
|
2309
2341
|
break;
|
|
2342
|
+
}
|
|
2310
2343
|
|
|
2311
|
-
case 'join_room':
|
|
2344
|
+
case 'join_room': {
|
|
2312
2345
|
if (!data.roomId || !ws.clientId) return;
|
|
2313
2346
|
const roomId = data.roomId;
|
|
2314
2347
|
const room = this.#rooms[roomId];
|
|
@@ -2352,29 +2385,30 @@ class PlaySocketServer {
|
|
|
2352
2385
|
});
|
|
2353
2386
|
this.#triggerEvent("clientJoinedRoom", ws.clientId, roomId);
|
|
2354
2387
|
break;
|
|
2388
|
+
}
|
|
2355
2389
|
|
|
2356
|
-
case 'update_property':
|
|
2357
|
-
const
|
|
2358
|
-
const
|
|
2390
|
+
case 'update_property': {
|
|
2391
|
+
const roomId = this.#clientRooms.get(ws.clientId);
|
|
2392
|
+
const room = roomId ? this.#rooms[roomId] : null;
|
|
2359
2393
|
|
|
2360
|
-
if (
|
|
2394
|
+
if (room && data.update) {
|
|
2361
2395
|
// Check if update is allowed via event callback (provide clone to ensure update integrity)
|
|
2362
|
-
const updateAllowed = await this.#triggerEvent("storageUpdateRequested", { roomId
|
|
2396
|
+
const updateAllowed = await this.#triggerEvent("storageUpdateRequested", { roomId, clientId: ws.clientId, update: structuredClone(data.update) });
|
|
2363
2397
|
if (updateAllowed === false) {
|
|
2364
2398
|
ws.send(encode({
|
|
2365
2399
|
type: 'property_update_rejected',
|
|
2366
|
-
state:
|
|
2400
|
+
state: room.crdtManager.getState
|
|
2367
2401
|
}), { binary: true });
|
|
2368
2402
|
return;
|
|
2369
2403
|
}
|
|
2370
2404
|
|
|
2371
|
-
|
|
2405
|
+
room.crdtManager.importPropertyUpdate(data.update); // Import update into server state
|
|
2372
2406
|
|
|
2373
2407
|
// Increment version for this room
|
|
2374
|
-
const currentVersion = this.#roomVersions.get(
|
|
2375
|
-
this.#roomVersions.set(
|
|
2408
|
+
const currentVersion = this.#roomVersions.get(roomId) + 1;
|
|
2409
|
+
this.#roomVersions.set(roomId, currentVersion);
|
|
2376
2410
|
|
|
2377
|
-
|
|
2411
|
+
room.participants?.forEach(p => {
|
|
2378
2412
|
const client = this.#clients.get(p);
|
|
2379
2413
|
if (client) {
|
|
2380
2414
|
client.send(encode({
|
|
@@ -2385,16 +2419,18 @@ class PlaySocketServer {
|
|
|
2385
2419
|
}
|
|
2386
2420
|
});
|
|
2387
2421
|
|
|
2388
|
-
this.#triggerEvent("storageUpdated", { roomId
|
|
2422
|
+
this.#triggerEvent("storageUpdated", { roomId, clientId: ws.clientId, update: structuredClone(data.update), storage: this.getRoomStorage(roomId) });
|
|
2389
2423
|
if (this.#debug) console.log("Property update received and imported:", data.update);
|
|
2390
2424
|
}
|
|
2391
2425
|
break;
|
|
2426
|
+
}
|
|
2392
2427
|
|
|
2393
|
-
case 'request':
|
|
2428
|
+
case 'request': {
|
|
2394
2429
|
if (!ws.clientId) return;
|
|
2395
|
-
const
|
|
2396
|
-
this.#triggerEvent("requestReceived", { roomId
|
|
2430
|
+
const roomId = this.#clientRooms.get(ws.clientId) || null;
|
|
2431
|
+
this.#triggerEvent("requestReceived", { roomId, clientId: ws.clientId, name: data.request.name, data: data.request.data });
|
|
2397
2432
|
break;
|
|
2433
|
+
}
|
|
2398
2434
|
|
|
2399
2435
|
case 'disconnect':
|
|
2400
2436
|
// Client signals to server that it will will willfully disconnect soon
|
|
@@ -2408,6 +2444,7 @@ class PlaySocketServer {
|
|
|
2408
2444
|
|
|
2409
2445
|
/**
|
|
2410
2446
|
* Generate a readable, 6 digit ID
|
|
2447
|
+
* @returns {string} - Id
|
|
2411
2448
|
*/
|
|
2412
2449
|
#generateId() {
|
|
2413
2450
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789';
|
|
@@ -2417,8 +2454,8 @@ class PlaySocketServer {
|
|
|
2417
2454
|
/**
|
|
2418
2455
|
* If the client that disconnected (and is now in the reconnection-phase) was the room host,
|
|
2419
2456
|
* pick a new room host immediately to avoid host-less phase (in case important logic is attached to the host)
|
|
2420
|
-
* @param {string} roomId
|
|
2421
|
-
* @param {string} clientId
|
|
2457
|
+
* @param {string} roomId - ID of the room that the disconnected client was in
|
|
2458
|
+
* @param {string} clientId - ID of the disconnected client
|
|
2422
2459
|
*/
|
|
2423
2460
|
#changeHostIfDisconnected(roomId, clientId) {
|
|
2424
2461
|
const room = this.#rooms[roomId];
|
|
@@ -2449,9 +2486,12 @@ class PlaySocketServer {
|
|
|
2449
2486
|
|
|
2450
2487
|
/**
|
|
2451
2488
|
* Check rate limit using token bucket algorithm
|
|
2489
|
+
* @param {string} connectionId - Random connection UUID which is different from the client ID
|
|
2490
|
+
* @param {string} actionType - A string describing the action
|
|
2491
|
+
* @returns {boolean} - Whether or not the action can be allowed, true means allowed, false means limited
|
|
2452
2492
|
* @private
|
|
2453
2493
|
*/
|
|
2454
|
-
#checkRateLimit(connectionId,
|
|
2494
|
+
#checkRateLimit(connectionId, actionType) {
|
|
2455
2495
|
const now = Date.now();
|
|
2456
2496
|
|
|
2457
2497
|
if (!this.#rateLimits.has(connectionId)) {
|
|
@@ -2467,7 +2507,7 @@ class PlaySocketServer {
|
|
|
2467
2507
|
limit.lastReset = now;
|
|
2468
2508
|
}
|
|
2469
2509
|
|
|
2470
|
-
const pointCost =
|
|
2510
|
+
const pointCost = actionType == "create_room" ? 5 : 1;
|
|
2471
2511
|
if (limit.points < pointCost) return false;
|
|
2472
2512
|
|
|
2473
2513
|
limit.points -= pointCost;
|
|
@@ -2476,6 +2516,7 @@ class PlaySocketServer {
|
|
|
2476
2516
|
|
|
2477
2517
|
/**
|
|
2478
2518
|
* Handle client disconnection
|
|
2519
|
+
* @param {WebSocket} ws - WebSocket client
|
|
2479
2520
|
* @private
|
|
2480
2521
|
*/
|
|
2481
2522
|
#handleDisconnection(ws) {
|
|
@@ -2504,7 +2545,7 @@ class PlaySocketServer {
|
|
|
2504
2545
|
/**
|
|
2505
2546
|
* Disconnect a client
|
|
2506
2547
|
* @private
|
|
2507
|
-
* @param {
|
|
2548
|
+
* @param {WebSocket} ws - WebSocket client
|
|
2508
2549
|
*/
|
|
2509
2550
|
#disconnectClient(ws) {
|
|
2510
2551
|
this.#pendingDisconnects.delete(ws.clientId);
|
|
@@ -2545,8 +2586,10 @@ class PlaySocketServer {
|
|
|
2545
2586
|
*/
|
|
2546
2587
|
onEvent(event, callback) {
|
|
2547
2588
|
const validEvents = ["clientRegistered", "clientRegistrationRequested", "clientDisconnected", "clientJoinedRoom", "clientJoinRequested", "roomCreated", "roomCreationRequested", "requestReceived", "storageUpdated", "storageUpdateRequested", "roomDestroyed"];
|
|
2548
|
-
if (!validEvents.includes(event))
|
|
2549
|
-
|
|
2589
|
+
if (!validEvents.includes(event)) {
|
|
2590
|
+
console.warn(`Invalid PlaySocket event type "${event}"`);
|
|
2591
|
+
return;
|
|
2592
|
+
} if (!this.#callbacks.has(event)) this.#callbacks.set(event, []);
|
|
2550
2593
|
this.#callbacks.get(event).push(callback);
|
|
2551
2594
|
}
|
|
2552
2595
|
|
|
@@ -2566,6 +2609,9 @@ class PlaySocketServer {
|
|
|
2566
2609
|
|
|
2567
2610
|
/**
|
|
2568
2611
|
* Trigger an event to registered callbacks
|
|
2612
|
+
* @param {string} event - Event name
|
|
2613
|
+
* @param {...*} args - Arguments
|
|
2614
|
+
* @returns {any} - Undefined if no callbacks, true if all callbacks ran, what the first callback with a return statement returns if present
|
|
2569
2615
|
* @private
|
|
2570
2616
|
*/
|
|
2571
2617
|
async #triggerEvent(event, ...args) {
|
|
@@ -2585,7 +2631,8 @@ class PlaySocketServer {
|
|
|
2585
2631
|
|
|
2586
2632
|
/**
|
|
2587
2633
|
* Get snapshot of a room's storage
|
|
2588
|
-
* @
|
|
2634
|
+
* @param {string} roomId - ID of the room to get the storage from
|
|
2635
|
+
* @returns {object|undefined} - Storage object or undefined if the room doesn't exist
|
|
2589
2636
|
*/
|
|
2590
2637
|
getRoomStorage(roomId) {
|
|
2591
2638
|
const room = this.#rooms[roomId];
|
|
@@ -2596,7 +2643,7 @@ class PlaySocketServer {
|
|
|
2596
2643
|
* Update a value in a room's storage
|
|
2597
2644
|
* @param {string} roomId - Room ID
|
|
2598
2645
|
* @param {string} key - Storage key
|
|
2599
|
-
* @param {
|
|
2646
|
+
* @param {'set' | 'array-add' | 'array-add-unique' | 'array-remove-matching' | 'array-update-matching'} type - Operation type
|
|
2600
2647
|
* @param {*} value - New value or value to operate on
|
|
2601
2648
|
* @param {*} updateValue - New value for update-matching
|
|
2602
2649
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playsocketjs",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.3",
|
|
4
4
|
"description": "WebSocket wrapper for creating simple multiplayer systems with ease.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "dist/playsocket-client.js",
|
|
@@ -9,18 +9,17 @@
|
|
|
9
9
|
"import": "./dist/playsocket-client.js"
|
|
10
10
|
},
|
|
11
11
|
"./server": {
|
|
12
|
-
"import": "./dist/playsocket-server.
|
|
13
|
-
"require": "./dist/playsocket-server.cjs"
|
|
12
|
+
"import": "./dist/playsocket-server.js"
|
|
14
13
|
}
|
|
15
14
|
},
|
|
16
15
|
"files": [
|
|
17
16
|
"dist"
|
|
18
17
|
],
|
|
19
18
|
"scripts": {
|
|
20
|
-
"start": "node example/example-server.
|
|
21
|
-
"start-module": "node example/example-server.mjs",
|
|
19
|
+
"start": "node example/example-server.js",
|
|
22
20
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
23
|
-
"build": "rollup -c rollup.config.
|
|
21
|
+
"build": "rollup -c rollup.config.js",
|
|
22
|
+
"lint": "eslint ./src"
|
|
24
23
|
},
|
|
25
24
|
"repository": {
|
|
26
25
|
"type": "git",
|
|
@@ -28,11 +27,10 @@
|
|
|
28
27
|
},
|
|
29
28
|
"keywords": [
|
|
30
29
|
"WebSocket",
|
|
31
|
-
"
|
|
30
|
+
"Data channel",
|
|
32
31
|
"Socket",
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"game"
|
|
32
|
+
"Multiplayer",
|
|
33
|
+
"Game"
|
|
36
34
|
],
|
|
37
35
|
"author": "PaulPlay",
|
|
38
36
|
"license": "MIT",
|
|
@@ -41,9 +39,11 @@
|
|
|
41
39
|
},
|
|
42
40
|
"homepage": "https://github.com/therealPaulPlay/PlaySocketJS#readme",
|
|
43
41
|
"devDependencies": {
|
|
44
|
-
"@rollup/plugin-commonjs": "^28.0.2",
|
|
45
42
|
"@rollup/plugin-node-resolve": "^16.0.0",
|
|
46
|
-
"
|
|
43
|
+
"eslint": "^9.35.0",
|
|
44
|
+
"eslint-plugin-jsdoc": "^54.4.0",
|
|
45
|
+
"rollup": "^4.31.0",
|
|
46
|
+
"globals": "^16.3.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"ws": "^8.18.1"
|