playsocketjs 2.5.2 → 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/dist/playsocket-client.js +76 -49
- package/dist/{playsocket-server.mjs → playsocket-server.js} +110 -65
- package/package.json +12 -12
- package/dist/playsocket-server.cjs +0 -2669
|
@@ -1683,13 +1683,15 @@ function decode(buffer, options) {
|
|
|
1683
1683
|
|
|
1684
1684
|
const CONSOLE_PREFIX = "PlaySocket CRDT manager: ";
|
|
1685
1685
|
|
|
1686
|
+
/**
|
|
1687
|
+
* CRDT Manager
|
|
1688
|
+
*/
|
|
1686
1689
|
class CRDTManager {
|
|
1687
1690
|
// Storage
|
|
1688
1691
|
#replicaId;
|
|
1689
1692
|
#keyOperations = new Map();
|
|
1690
1693
|
#vectorClock = new Map();
|
|
1691
1694
|
|
|
1692
|
-
// Local only
|
|
1693
1695
|
#propertyStore = {}; // Current local values per key, as object
|
|
1694
1696
|
#lastPropertyStore = {}; // Last property store to compare against
|
|
1695
1697
|
|
|
@@ -1702,6 +1704,7 @@ class CRDTManager {
|
|
|
1702
1704
|
|
|
1703
1705
|
/**
|
|
1704
1706
|
* Create a new instance
|
|
1707
|
+
* @param {boolean} [debug=false] - Enable debug logging
|
|
1705
1708
|
*/
|
|
1706
1709
|
constructor(debug) {
|
|
1707
1710
|
if (debug) this.#debug = true;
|
|
@@ -1711,7 +1714,7 @@ class CRDTManager {
|
|
|
1711
1714
|
|
|
1712
1715
|
/**
|
|
1713
1716
|
* Import the entire state of the CRDT manager (this overwrites the old state)
|
|
1714
|
-
* @param {
|
|
1717
|
+
* @param {object} state - Full state of the CRDT manager
|
|
1715
1718
|
*/
|
|
1716
1719
|
importState(state) {
|
|
1717
1720
|
try {
|
|
@@ -1728,7 +1731,7 @@ class CRDTManager {
|
|
|
1728
1731
|
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
|
|
1729
1732
|
|
|
1730
1733
|
// Map all operation uuids to current timestamp
|
|
1731
|
-
this.#keyOperations.forEach((value,
|
|
1734
|
+
this.#keyOperations.forEach((value, _key) => {
|
|
1732
1735
|
value?.forEach((operation) => {
|
|
1733
1736
|
this.#opUuidTimestamp.set(operation?.uuid, Date.now());
|
|
1734
1737
|
});
|
|
@@ -1744,7 +1747,7 @@ class CRDTManager {
|
|
|
1744
1747
|
|
|
1745
1748
|
/**
|
|
1746
1749
|
* Import property update
|
|
1747
|
-
* @param {
|
|
1750
|
+
* @param {object} data - Property update data
|
|
1748
1751
|
*/
|
|
1749
1752
|
importPropertyUpdate(data) {
|
|
1750
1753
|
try {
|
|
@@ -1786,11 +1789,11 @@ class CRDTManager {
|
|
|
1786
1789
|
|
|
1787
1790
|
/**
|
|
1788
1791
|
* Update a property
|
|
1789
|
-
* @param {string} key
|
|
1790
|
-
* @param {string} type
|
|
1791
|
-
* @param {*} value
|
|
1792
|
-
* @param {*} updateValue
|
|
1793
|
-
* @returns {
|
|
1792
|
+
* @param {string} key - Storage key
|
|
1793
|
+
* @param {string} type - Operation type
|
|
1794
|
+
* @param {*} value - New value, or value to match
|
|
1795
|
+
* @param {*} updateValue - New value for 'matching' operations
|
|
1796
|
+
* @returns {object | undefined} - Returns the property update
|
|
1794
1797
|
*/
|
|
1795
1798
|
updateProperty(key, type, value, updateValue) {
|
|
1796
1799
|
try {
|
|
@@ -1827,6 +1830,9 @@ class CRDTManager {
|
|
|
1827
1830
|
}
|
|
1828
1831
|
}
|
|
1829
1832
|
|
|
1833
|
+
/**
|
|
1834
|
+
* Check if garbage collected can be performed & run it
|
|
1835
|
+
*/
|
|
1830
1836
|
#checkGarbageCollection() {
|
|
1831
1837
|
const MIN_GC_DELAY = 1000; // Minimum 1s delay between garbage collection runs
|
|
1832
1838
|
const MIN_AGE_FOR_GC = 5000; // Garbage collect ops that are older than 5s
|
|
@@ -1852,7 +1858,7 @@ class CRDTManager {
|
|
|
1852
1858
|
if (this.#debug) console.log(CONSOLE_PREFIX + `Running garbage collection for key ${key} with current operations:`, operations);
|
|
1853
1859
|
const retainOps = operations.slice(-retainCount); // Newest ops
|
|
1854
1860
|
const removeOps = operations.slice(0, -retainCount); // Oldest ops (start = idx 0, end = retainCount counted from right side)
|
|
1855
|
-
const baselineVectorClock = removeOps[removeOps.length - 1]?.vectorClock || []; // Use the vector clock from the last operation that we remove/overwrite
|
|
1861
|
+
const baselineVectorClock = removeOps[removeOps.length - 1]?.vectorClock || []; // Use the vector clock from the last operation that we remove/overwrite
|
|
1856
1862
|
|
|
1857
1863
|
// Calculate the value at the point where retained operations start
|
|
1858
1864
|
let baselineValue = null;
|
|
@@ -1883,9 +1889,9 @@ class CRDTManager {
|
|
|
1883
1889
|
|
|
1884
1890
|
/**
|
|
1885
1891
|
* Create an operation object
|
|
1886
|
-
* @param {
|
|
1887
|
-
* @param {Array} vectorClock
|
|
1888
|
-
* @returns {
|
|
1892
|
+
* @param {object} data - Operation data, consists of type, value and updateValue
|
|
1893
|
+
* @param {Array} vectorClock - Vector clock of the replica/client
|
|
1894
|
+
* @returns {object} - Operation object
|
|
1889
1895
|
*/
|
|
1890
1896
|
#createOperation(data, vectorClock) {
|
|
1891
1897
|
const newOperation = {
|
|
@@ -1900,7 +1906,7 @@ class CRDTManager {
|
|
|
1900
1906
|
|
|
1901
1907
|
/**
|
|
1902
1908
|
* Process a property's value by applying all operations and set it in the property store
|
|
1903
|
-
* @param {string} key
|
|
1909
|
+
* @param {string} key - Storage key
|
|
1904
1910
|
*/
|
|
1905
1911
|
#processLocalProperty(key) {
|
|
1906
1912
|
try {
|
|
@@ -1927,8 +1933,8 @@ class CRDTManager {
|
|
|
1927
1933
|
}
|
|
1928
1934
|
|
|
1929
1935
|
/**
|
|
1930
|
-
* Sort by vector clock (causal order)
|
|
1931
|
-
* @param {Array} operations
|
|
1936
|
+
* Sort operations by vector clock (causal order)
|
|
1937
|
+
* @param {Array} operations - Full operations that include vector clocks, data and a source
|
|
1932
1938
|
* @returns {Array} - Sorted operations
|
|
1933
1939
|
*/
|
|
1934
1940
|
#sortByVectorClock(operations) {
|
|
@@ -1964,10 +1970,10 @@ class CRDTManager {
|
|
|
1964
1970
|
|
|
1965
1971
|
/**
|
|
1966
1972
|
* Handle an operation
|
|
1967
|
-
* @param {*} curValue
|
|
1968
|
-
* @param {string} type
|
|
1969
|
-
* @param {*} value
|
|
1970
|
-
* @param {*} [updateValue]
|
|
1973
|
+
* @param {*} curValue - Current value of the storage key
|
|
1974
|
+
* @param {string} type - Operation type
|
|
1975
|
+
* @param {*} value - New value, or value to match
|
|
1976
|
+
* @param {*} [updateValue] - New value for 'matching' operations
|
|
1971
1977
|
* @returns {*} - Value after the operation
|
|
1972
1978
|
*/
|
|
1973
1979
|
#handleOperation(curValue, type, value, updateValue) {
|
|
@@ -1999,10 +2005,11 @@ class CRDTManager {
|
|
|
1999
2005
|
case 'array-remove-matching':
|
|
2000
2006
|
curValue = curValue.filter(item => !compare(item));
|
|
2001
2007
|
break;
|
|
2002
|
-
case 'array-update-matching':
|
|
2008
|
+
case 'array-update-matching': {
|
|
2003
2009
|
const index = curValue.findIndex(compare);
|
|
2004
2010
|
if (index !== -1) curValue[index] = updateValue;
|
|
2005
2011
|
break;
|
|
2012
|
+
}
|
|
2006
2013
|
}
|
|
2007
2014
|
}
|
|
2008
2015
|
|
|
@@ -2016,42 +2023,53 @@ class CRDTManager {
|
|
|
2016
2023
|
|
|
2017
2024
|
/**
|
|
2018
2025
|
* Remove HTML to prevent XSS and enforce size limits
|
|
2019
|
-
* @param {
|
|
2020
|
-
* @returns {
|
|
2026
|
+
* @param {*} value - Value to sanitize
|
|
2027
|
+
* @returns {*} - Sanitized value
|
|
2021
2028
|
*/
|
|
2022
|
-
#sanitizeValue(
|
|
2029
|
+
#sanitizeValue(value) {
|
|
2023
2030
|
// Check total serialized size
|
|
2024
|
-
const jsonString = JSON.stringify(
|
|
2031
|
+
const jsonString = JSON.stringify(value);
|
|
2025
2032
|
if (jsonString?.length > 50000) throw new Error('Value too large!'); // 50KB limit
|
|
2026
2033
|
|
|
2027
|
-
if (typeof
|
|
2028
|
-
if (Array.isArray(
|
|
2029
|
-
if (
|
|
2030
|
-
return
|
|
2034
|
+
if (typeof value === 'string') return (value.includes('<') || value.includes('>')) ? value.replace(/[<>]/g, '') : value;
|
|
2035
|
+
if (Array.isArray(value)) return value.map(item => this.#sanitizeValue(item));
|
|
2036
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, this.#sanitizeValue(v)]));
|
|
2037
|
+
return value;
|
|
2031
2038
|
}
|
|
2032
2039
|
|
|
2033
|
-
|
|
2040
|
+
/**
|
|
2041
|
+
* Get property store
|
|
2042
|
+
* @returns {Record<string, any>} - Object with key value pairs
|
|
2043
|
+
*/
|
|
2034
2044
|
get getPropertyStore() {
|
|
2035
2045
|
try {
|
|
2036
2046
|
return structuredClone(this.#propertyStore); // Return deep clone
|
|
2037
2047
|
} catch {
|
|
2038
2048
|
console.error(CONSOLE_PREFIX + "Failed to parse property store");
|
|
2039
|
-
return {}
|
|
2049
|
+
return {}
|
|
2040
2050
|
}
|
|
2041
2051
|
}
|
|
2042
2052
|
|
|
2043
|
-
|
|
2053
|
+
/**
|
|
2054
|
+
* Check for changes in the local property store
|
|
2055
|
+
* @returns {boolean} - Check if any values changed going from the last to the current property update
|
|
2056
|
+
*/
|
|
2044
2057
|
get didPropertiesChange() {
|
|
2045
2058
|
try {
|
|
2046
2059
|
if (JSON.stringify(this.#propertyStore) !== JSON.stringify(this.#lastPropertyStore)) {
|
|
2047
2060
|
this.#lastPropertyStore = structuredClone(this.#propertyStore);
|
|
2048
2061
|
return true;
|
|
2049
2062
|
}
|
|
2050
|
-
} catch {
|
|
2063
|
+
} catch {
|
|
2064
|
+
console.error(CONSOLE_PREFIX + "Failed to stringify and compare property stores");
|
|
2065
|
+
}
|
|
2051
2066
|
return false;
|
|
2052
2067
|
}
|
|
2053
2068
|
|
|
2054
|
-
|
|
2069
|
+
/**
|
|
2070
|
+
* Export state (can be imported using importState, converts the maps to arrays for serialization)
|
|
2071
|
+
* @returns {object} - State
|
|
2072
|
+
*/
|
|
2055
2073
|
get getState() {
|
|
2056
2074
|
return {
|
|
2057
2075
|
keyOperations: [...this.#keyOperations.entries()],
|
|
@@ -2060,16 +2078,14 @@ class CRDTManager {
|
|
|
2060
2078
|
}
|
|
2061
2079
|
}
|
|
2062
2080
|
|
|
2063
|
-
/**
|
|
2064
|
-
* PlaySocket - WebSocket-based multiplayer for games
|
|
2065
|
-
*/
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
2081
|
const ERROR_PREFIX = "PlaySocket error: ";
|
|
2069
2082
|
const WARNING_PREFIX = "PlaySocket warning: ";
|
|
2070
2083
|
const LOG_PREFIX = "PlaySocket log: ";
|
|
2071
2084
|
const TIMEOUT_MS = 3000; // 3 second timeout for WS messages
|
|
2072
2085
|
|
|
2086
|
+
/**
|
|
2087
|
+
* PlaySocket Client
|
|
2088
|
+
*/
|
|
2073
2089
|
class PlaySocket {
|
|
2074
2090
|
// Core properties
|
|
2075
2091
|
#id; // Unique client ID
|
|
@@ -2122,11 +2138,13 @@ class PlaySocket {
|
|
|
2122
2138
|
|
|
2123
2139
|
/**
|
|
2124
2140
|
* Helper to create timeout promises for create room, join room etc.
|
|
2141
|
+
* @param {string} name - Name of the timeout
|
|
2142
|
+
* @returns {Promise} - Promise that rejects after the timeout
|
|
2125
2143
|
* @private
|
|
2126
2144
|
*/
|
|
2127
|
-
#createTimeout(
|
|
2145
|
+
#createTimeout(name) {
|
|
2128
2146
|
return new Promise((_, reject) =>
|
|
2129
|
-
setTimeout(() => reject(new Error(`${
|
|
2147
|
+
setTimeout(() => reject(new Error(`${name} timed out`)), TIMEOUT_MS)
|
|
2130
2148
|
);
|
|
2131
2149
|
}
|
|
2132
2150
|
|
|
@@ -2137,13 +2155,17 @@ class PlaySocket {
|
|
|
2137
2155
|
*/
|
|
2138
2156
|
onEvent(event, callback) {
|
|
2139
2157
|
const validEvents = ["status", "error", "instanceDestroyed", "storageUpdated", "hostMigrated", "clientConnected", "clientDisconnected"];
|
|
2140
|
-
if (!validEvents.includes(event))
|
|
2141
|
-
|
|
2158
|
+
if (!validEvents.includes(event)) {
|
|
2159
|
+
console.warn(WARNING_PREFIX + `Invalid event type "${event}".`);
|
|
2160
|
+
return;
|
|
2161
|
+
} if (!this.#callbacks.has(event)) this.#callbacks.set(event, []);
|
|
2142
2162
|
this.#callbacks.get(event).push(callback);
|
|
2143
2163
|
}
|
|
2144
2164
|
|
|
2145
2165
|
/**
|
|
2146
2166
|
* Trigger an event to registered callbacks
|
|
2167
|
+
* @param {string} event - Event name
|
|
2168
|
+
* @param {...*} args - Arguments
|
|
2147
2169
|
* @private
|
|
2148
2170
|
*/
|
|
2149
2171
|
#triggerEvent(event, ...args) {
|
|
@@ -2375,7 +2397,8 @@ class PlaySocket {
|
|
|
2375
2397
|
this.#reconnectCount++;
|
|
2376
2398
|
if (this.#reconnectCount > 9) {
|
|
2377
2399
|
this.#triggerEvent("error", "Disconnected from server.");
|
|
2378
|
-
|
|
2400
|
+
this.destroy();
|
|
2401
|
+
return;
|
|
2379
2402
|
}
|
|
2380
2403
|
|
|
2381
2404
|
this.#triggerEvent("status", `Attempting to reconnect... (${this.#reconnectCount})`);
|
|
@@ -2383,7 +2406,7 @@ class PlaySocket {
|
|
|
2383
2406
|
try {
|
|
2384
2407
|
await this.#connect();
|
|
2385
2408
|
await Promise.race([
|
|
2386
|
-
new Promise(
|
|
2409
|
+
new Promise((resolve, reject) => {
|
|
2387
2410
|
this.#pendingReconnect = { resolve, reject };
|
|
2388
2411
|
this.#sendToServer({
|
|
2389
2412
|
type: 'reconnect',
|
|
@@ -2416,11 +2439,13 @@ class PlaySocket {
|
|
|
2416
2439
|
|
|
2417
2440
|
/**
|
|
2418
2441
|
* Send a message to the server
|
|
2442
|
+
* @param {object} data - The data to send to the server
|
|
2419
2443
|
* @private
|
|
2420
2444
|
*/
|
|
2421
2445
|
#sendToServer(data) {
|
|
2422
2446
|
if (!this.#socket || this.#socket?.readyState !== WebSocket.OPEN) {
|
|
2423
|
-
|
|
2447
|
+
console.warn(WARNING_PREFIX + "Cannot send message - not connected.");
|
|
2448
|
+
return;
|
|
2424
2449
|
}
|
|
2425
2450
|
try {
|
|
2426
2451
|
this.#socket.send(encode(data));
|
|
@@ -2461,7 +2486,7 @@ class PlaySocket {
|
|
|
2461
2486
|
/**
|
|
2462
2487
|
* Join an existing room
|
|
2463
2488
|
* @param {string} roomId - ID of the room
|
|
2464
|
-
* @returns {Promise} Resolves when connected
|
|
2489
|
+
* @returns {Promise} - Resolves when connected
|
|
2465
2490
|
*/
|
|
2466
2491
|
async joinRoom(roomId) {
|
|
2467
2492
|
if (!this.#initialized) {
|
|
@@ -2494,8 +2519,10 @@ class PlaySocket {
|
|
|
2494
2519
|
* @param {*} updateValue - New value for update-matching
|
|
2495
2520
|
*/
|
|
2496
2521
|
updateStorage(key, type, value, updateValue) {
|
|
2497
|
-
if (!this.#inRoom)
|
|
2498
|
-
|
|
2522
|
+
if (!this.#inRoom) {
|
|
2523
|
+
this.#triggerEvent("error", "Cannot update storage when not in a room.");
|
|
2524
|
+
return;
|
|
2525
|
+
} if (this.#debug) console.log(LOG_PREFIX + `Property update for key ${key}, operation ${type}, value ${value} and updateValue ${updateValue}.`);
|
|
2499
2526
|
const propUpdate = this.#crdtManager.updateProperty(key, type, value, updateValue);
|
|
2500
2527
|
this.#sendToServer({
|
|
2501
2528
|
type: 'update_property',
|