playsocketjs 1.1.0 → 1.1.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 +2 -0
- package/dist/playsocket-client.js +143 -82
- package/dist/playsocket-server.js +101 -79
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -71,6 +71,7 @@ Creates a new PlaySocket instance with a specified ID and configuration options.
|
|
|
71
71
|
#### Configuration options
|
|
72
72
|
- `endpoint`: WebSocket server endpoint (e.g., 'wss://example.com/socket')
|
|
73
73
|
- `customData`: You can pass arbitrary data to the "clientRegistered" server event (optional)
|
|
74
|
+
- `debug`: Set this property to true to enable extra logging
|
|
74
75
|
|
|
75
76
|
### Methods
|
|
76
77
|
|
|
@@ -192,6 +193,7 @@ Creates a new PlaySocket Server instance with configuration options.
|
|
|
192
193
|
- `port`: Port to listen on (default: 3000, used only if no server provided)
|
|
193
194
|
- `path`: WebSocket endpoint path (default: '/')
|
|
194
195
|
- `server`: Existing http server (optional)
|
|
196
|
+
- `debug`: Set this property to true to enable extra logging
|
|
195
197
|
|
|
196
198
|
### Methods
|
|
197
199
|
|
|
@@ -1693,8 +1693,6 @@ function requireCrdtManager () {
|
|
|
1693
1693
|
// Storage
|
|
1694
1694
|
#replicaId;
|
|
1695
1695
|
#keyOperations = new Map();
|
|
1696
|
-
|
|
1697
|
-
// Vector clock
|
|
1698
1696
|
#vectorClock = new Map();
|
|
1699
1697
|
|
|
1700
1698
|
// Local only
|
|
@@ -1703,14 +1701,19 @@ function requireCrdtManager () {
|
|
|
1703
1701
|
|
|
1704
1702
|
// Local Garbage Collection
|
|
1705
1703
|
#lastGCCheck = 0;
|
|
1706
|
-
#
|
|
1704
|
+
#opUuidTimestamp = new Map(); // Map every operation to a timestamp for garbage collection
|
|
1705
|
+
|
|
1706
|
+
// Debug
|
|
1707
|
+
#debug = false;
|
|
1707
1708
|
|
|
1708
1709
|
/**
|
|
1709
1710
|
* Create a new instance
|
|
1710
|
-
* @param {string}
|
|
1711
|
+
* @param {string} replicaId - Choose a uuid (falls back to random uuid)
|
|
1711
1712
|
*/
|
|
1712
|
-
constructor(replicaId) {
|
|
1713
|
-
|
|
1713
|
+
constructor(replicaId, debug) {
|
|
1714
|
+
if (!replicaId) return console.error("No replicaId provided!");
|
|
1715
|
+
if (debug) this.#debug = true;
|
|
1716
|
+
this.#replicaId = replicaId;
|
|
1714
1717
|
this.#vectorClock.set(this.#replicaId, 0);
|
|
1715
1718
|
}
|
|
1716
1719
|
|
|
@@ -1721,8 +1724,13 @@ function requireCrdtManager () {
|
|
|
1721
1724
|
importState(state) {
|
|
1722
1725
|
try {
|
|
1723
1726
|
const { keyOperations, vectorClock } = state;
|
|
1727
|
+
if (this.#debug) console.log(CONSOLE_PREFIX + "Importing state:", state);
|
|
1728
|
+
|
|
1729
|
+
// Resets
|
|
1730
|
+
this.#opUuidTimestamp.clear();
|
|
1731
|
+
this.#propertyStore = {};
|
|
1732
|
+
this.#lastPropertyStore = {};
|
|
1724
1733
|
|
|
1725
|
-
this.#opUuuidTimestamp.clear(); // Reset the uuid timestamps since it might contain uuid's that no longer exist in the new state
|
|
1726
1734
|
this.#keyOperations = new Map(keyOperations); // Rebuild the map
|
|
1727
1735
|
this.#vectorClock = new Map(vectorClock); // Also rebuild the map here
|
|
1728
1736
|
if (!this.#vectorClock.has(this.#replicaId)) this.#vectorClock.set(this.#replicaId, 0); // Reset own vector clock if it wasn't present in the imported state
|
|
@@ -1730,7 +1738,7 @@ function requireCrdtManager () {
|
|
|
1730
1738
|
// Map operation uuids to timestamp
|
|
1731
1739
|
this.#keyOperations.forEach((value, key) => {
|
|
1732
1740
|
value?.forEach((operation) => {
|
|
1733
|
-
this.#
|
|
1741
|
+
this.#opUuidTimestamp.set(operation?.uuid, Date.now());
|
|
1734
1742
|
});
|
|
1735
1743
|
});
|
|
1736
1744
|
|
|
@@ -1743,72 +1751,38 @@ function requireCrdtManager () {
|
|
|
1743
1751
|
}
|
|
1744
1752
|
|
|
1745
1753
|
/**
|
|
1746
|
-
*
|
|
1747
|
-
* This can still be imported using importProperty
|
|
1748
|
-
* @param {string} key
|
|
1749
|
-
* @returns {Object} - Data export
|
|
1750
|
-
*/
|
|
1751
|
-
exportPropertyLastOpOnly(key) {
|
|
1752
|
-
const lastOp = this.#keyOperations.get(key)?.[this.#keyOperations.get(key)?.length - 1];
|
|
1753
|
-
return {
|
|
1754
|
-
key,
|
|
1755
|
-
operations: lastOp ? [lastOp] : [],
|
|
1756
|
-
vectorClock: Array.from(this.#vectorClock.entries())
|
|
1757
|
-
};
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
/**
|
|
1761
|
-
* Export property for a single key
|
|
1762
|
-
* @param {string} key
|
|
1763
|
-
* @returns {Object} - Data export
|
|
1764
|
-
*/
|
|
1765
|
-
exportProperty(key) {
|
|
1766
|
-
return {
|
|
1767
|
-
key,
|
|
1768
|
-
operations: [...(this.#keyOperations.get(key) || [])],
|
|
1769
|
-
vectorClock: Array.from(this.#vectorClock.entries())
|
|
1770
|
-
};
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
/**
|
|
1774
|
-
* Import property for a single key and merge changes
|
|
1754
|
+
* Import property update
|
|
1775
1755
|
* @param {Object} data - Data to import
|
|
1776
1756
|
*/
|
|
1777
|
-
|
|
1757
|
+
importPropertyUpdate(data) {
|
|
1778
1758
|
try {
|
|
1779
|
-
const { key,
|
|
1759
|
+
const { key, operation, vectorClock } = data;
|
|
1760
|
+
if (this.#debug) console.log(CONSOLE_PREFIX + "Importing update:", data); // Debug
|
|
1780
1761
|
|
|
1781
1762
|
// Get COPY of current ops (or empty array if none yet)
|
|
1782
1763
|
const currentOps = [...(this.#keyOperations.get(key) || [])];
|
|
1783
1764
|
|
|
1784
1765
|
// Merge vector clocks (always take max value)
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
this.#vectorClock.set(id, counter);
|
|
1789
|
-
}
|
|
1766
|
+
for (const [id, counter] of vectorClock) {
|
|
1767
|
+
if (!this.#vectorClock.has(id) || this.#vectorClock.get(id) < counter) {
|
|
1768
|
+
this.#vectorClock.set(id, counter);
|
|
1790
1769
|
}
|
|
1791
1770
|
}
|
|
1792
1771
|
|
|
1793
|
-
//
|
|
1772
|
+
// Saveguard
|
|
1794
1773
|
if (this.#vectorClock.size > 1000) this.#vectorClock = new Map([...this.#vectorClock].slice(-100));
|
|
1795
1774
|
|
|
1796
|
-
// Add new
|
|
1775
|
+
// Add new operation if it's not already added
|
|
1797
1776
|
const existingUuids = new Set(currentOps.map(op => op.uuid));
|
|
1798
|
-
if (
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
currentOps.push({ ...op }); // Add operation
|
|
1802
|
-
this.#opUuuidTimestamp.set(op?.uuid, Date.now()); // Add timestamp for gc
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1777
|
+
if (operation?.uuid && !existingUuids.has(operation.uuid)) {
|
|
1778
|
+
currentOps.push({ ...operation }); // Add operation
|
|
1779
|
+
this.#opUuidTimestamp.set(operation?.uuid, Date.now()); // Add timestamp for gc
|
|
1805
1780
|
}
|
|
1806
1781
|
|
|
1807
1782
|
// Sort, update operations & local value
|
|
1808
1783
|
this.#keyOperations.set(key, this.#sortByVectorClock(currentOps));
|
|
1809
1784
|
this.#processLocalProperty(key);
|
|
1810
|
-
|
|
1811
|
-
this.#checkGarbageCollection(); // Check if GC should run
|
|
1785
|
+
this.#checkGarbageCollection();
|
|
1812
1786
|
|
|
1813
1787
|
} catch (error) {
|
|
1814
1788
|
console.error(CONSOLE_PREFIX + "Failed to import property:", error);
|
|
@@ -1821,9 +1795,13 @@ function requireCrdtManager () {
|
|
|
1821
1795
|
* @param {string} operation
|
|
1822
1796
|
* @param {*} value
|
|
1823
1797
|
* @param {*} updateValue
|
|
1798
|
+
* @returns {Object} - Returns the property update
|
|
1824
1799
|
*/
|
|
1825
1800
|
updateProperty(key, operation, value, updateValue) {
|
|
1826
1801
|
try {
|
|
1802
|
+
// Debug log
|
|
1803
|
+
if (this.#debug) console.log(CONSOLE_PREFIX + `Updating property with key ${key}, operation ${operation}, value ${value} and updateValue ${updateValue}.`);
|
|
1804
|
+
|
|
1827
1805
|
// Increment vector clock
|
|
1828
1806
|
const counter = this.#vectorClock.get(this.#replicaId) || 0;
|
|
1829
1807
|
this.#vectorClock.set(this.#replicaId, counter + 1);
|
|
@@ -1834,13 +1812,16 @@ function requireCrdtManager () {
|
|
|
1834
1812
|
// Add operation
|
|
1835
1813
|
const newOp = this.#createOperation({ operation, value, updateValue }, Array.from(this.#vectorClock.entries()));
|
|
1836
1814
|
currentOps.push(newOp);
|
|
1815
|
+
this.#keyOperations.set(key, currentOps); // Update the operations (no need to sort via vector clock since local updates are always the latest)
|
|
1816
|
+
this.#processLocalProperty(key); // Process local value
|
|
1817
|
+
this.#checkGarbageCollection();
|
|
1837
1818
|
|
|
1838
|
-
//
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1819
|
+
// Return the property update with the new operation (this can be imported using importPropertyUpdate)
|
|
1820
|
+
return {
|
|
1821
|
+
key,
|
|
1822
|
+
operation: { ...newOp },
|
|
1823
|
+
vectorClock: Array.from(this.#vectorClock.entries())
|
|
1824
|
+
};
|
|
1844
1825
|
|
|
1845
1826
|
} catch (error) {
|
|
1846
1827
|
console.error(CONSOLE_PREFIX + `Failed to add operation for key "${key}":`, error);
|
|
@@ -1859,15 +1840,16 @@ function requireCrdtManager () {
|
|
|
1859
1840
|
|
|
1860
1841
|
let retainCount = operations.length;
|
|
1861
1842
|
operations.forEach((op, index) => {
|
|
1862
|
-
// Count how many ops, from last to latest, are
|
|
1863
|
-
if (op.uuid && this.#
|
|
1843
|
+
// Count how many ops, from last to latest, are older than the min-age in a row
|
|
1844
|
+
if (op.uuid && this.#opUuidTimestamp.has(op.uuid) && (Date.now() - this.#opUuidTimestamp.get(op.uuid)) > MIN_AGE_FOR_GC) {
|
|
1864
1845
|
const removeCount = index + 1;
|
|
1865
1846
|
retainCount = operations.length - removeCount;
|
|
1866
|
-
this.#
|
|
1847
|
+
this.#opUuidTimestamp.delete(op.uuid); // Remove from timestamps if set to be deleted
|
|
1867
1848
|
}
|
|
1868
1849
|
});
|
|
1869
1850
|
|
|
1870
1851
|
if (retainCount < operations.length) {
|
|
1852
|
+
if (this.#debug) console.log(CONSOLE_PREFIX + `Running garbage collection for key ${key} with current operations:`, operations);
|
|
1871
1853
|
const retainOps = operations.slice(-retainCount); // newest ops
|
|
1872
1854
|
const removeOps = operations.slice(0, -retainCount); // oldest ops (start = idx 0, end = retainCount counted from right side)
|
|
1873
1855
|
const baselineVectorClock = removeOps[removeOps.length - 1]?.vectorClock || []; // Use the vector clock from the last operation that we remove/overwrite
|
|
@@ -1888,6 +1870,9 @@ function requireCrdtManager () {
|
|
|
1888
1870
|
// Create a compact operation with baseline value and appropriate vector clock
|
|
1889
1871
|
const compactOp = this.#createOperation({ operation: "set", value: baselineValue }, baselineVectorClock);
|
|
1890
1872
|
|
|
1873
|
+
// Debug log
|
|
1874
|
+
if (this.#debug) console.log(CONSOLE_PREFIX + "Operations after garbage collection for this key:", [compactOp, ...retainOps]);
|
|
1875
|
+
|
|
1891
1876
|
// Combine compact op with retained operations
|
|
1892
1877
|
this.#keyOperations.set(key, [compactOp, ...retainOps]);
|
|
1893
1878
|
}
|
|
@@ -1907,7 +1892,7 @@ function requireCrdtManager () {
|
|
|
1907
1892
|
source: this.#replicaId,
|
|
1908
1893
|
uuid: crypto.randomUUID()
|
|
1909
1894
|
};
|
|
1910
|
-
this.#
|
|
1895
|
+
this.#opUuidTimestamp.set(newOperation.uuid, Date.now());
|
|
1911
1896
|
return newOperation;
|
|
1912
1897
|
}
|
|
1913
1898
|
|
|
@@ -2062,7 +2047,8 @@ var crdtManagerExports = requireCrdtManager();
|
|
|
2062
2047
|
|
|
2063
2048
|
const ERROR_PREFIX = "PlaySocket error: ";
|
|
2064
2049
|
const WARNING_PREFIX = "PlaySocket warning: ";
|
|
2065
|
-
const
|
|
2050
|
+
const LOG_PREFIX = "PlaySocket log: ";
|
|
2051
|
+
const TIMEOUT_MS = 3000; // 3 second timeout for WS messages
|
|
2066
2052
|
|
|
2067
2053
|
class PlaySocket {
|
|
2068
2054
|
// Core properties
|
|
@@ -2078,22 +2064,28 @@ class PlaySocket {
|
|
|
2078
2064
|
#roomId; // ID of the host (client only)
|
|
2079
2065
|
#connectionCount = 0;
|
|
2080
2066
|
#crdtManager;
|
|
2067
|
+
#roomVersion = 0; // Update version (used to compare local vs. remote state to detect package loss)
|
|
2068
|
+
#sentUpdates = new Set(); // Contains update uuids
|
|
2081
2069
|
|
|
2082
2070
|
// Event handling
|
|
2083
2071
|
#callbacks = new Map(); // Event callbacks
|
|
2084
2072
|
|
|
2085
2073
|
// Async server operations
|
|
2086
2074
|
#pendingJoin;
|
|
2087
|
-
#
|
|
2075
|
+
#pendingCreate;
|
|
2088
2076
|
#pendingRegistration;
|
|
2089
2077
|
#pendingConnect;
|
|
2090
2078
|
#pendingReconnect;
|
|
2079
|
+
#pendingStateRequest;
|
|
2091
2080
|
|
|
2092
2081
|
// Timeouts or intervals
|
|
2093
2082
|
#reconnectTimeout;
|
|
2094
2083
|
#reconnectCount = 0;
|
|
2095
2084
|
#isReconnecting = false;
|
|
2096
2085
|
|
|
2086
|
+
// Debug
|
|
2087
|
+
#debug = false;
|
|
2088
|
+
|
|
2097
2089
|
/**
|
|
2098
2090
|
* Create a new PlaySocket instance
|
|
2099
2091
|
* @param {string} id - Unique identifier for this client
|
|
@@ -2105,7 +2097,8 @@ class PlaySocket {
|
|
|
2105
2097
|
this.#id = id;
|
|
2106
2098
|
if (options.endpoint) this.#endpoint = options.endpoint;
|
|
2107
2099
|
if (options.customData) this.#customData = { ...options.customData };
|
|
2108
|
-
this.#
|
|
2100
|
+
if (options.debug) this.#debug = true; // Enabling extra logging
|
|
2101
|
+
this.#crdtManager = new crdtManagerExports.CRDTManager(this.#id, this.#debug);
|
|
2109
2102
|
}
|
|
2110
2103
|
|
|
2111
2104
|
/**
|
|
@@ -2210,9 +2203,11 @@ class PlaySocket {
|
|
|
2210
2203
|
case 'join_accepted':
|
|
2211
2204
|
// Connected to room
|
|
2212
2205
|
if (this.#pendingJoin) {
|
|
2206
|
+
if (this.#debug) console.log(LOG_PREFIX + "State received for join:", message.state);
|
|
2213
2207
|
this.#crdtManager.importState(message.state);
|
|
2214
2208
|
this.#connectionCount = message.participantCount - 1; // Counted without the user themselves
|
|
2215
2209
|
this.#roomHost = message.host;
|
|
2210
|
+
this.#roomVersion = message.version;
|
|
2216
2211
|
this.#triggerEvent("storageUpdated", this.getStorage);
|
|
2217
2212
|
this.#triggerEvent("status", `Connected to room.`);
|
|
2218
2213
|
this.#pendingJoin.resolve();
|
|
@@ -2233,7 +2228,9 @@ class PlaySocket {
|
|
|
2233
2228
|
this.#isReconnecting = false;
|
|
2234
2229
|
this.#reconnectCount = 0;
|
|
2235
2230
|
if (message.roomData) {
|
|
2231
|
+
if (this.#debug) console.log(LOG_PREFIX + "State received for reconnect:", message.roomData.state);
|
|
2236
2232
|
this.#crdtManager.importState(message.roomData.state);
|
|
2233
|
+
this.#roomVersion = message.roomData.version;
|
|
2237
2234
|
this.#connectionCount = message.roomData.participantCount - 1; // Counted without the user themselves
|
|
2238
2235
|
this.#setHost(message.roomData.host); // Set host before in case there are .isHost checks in the storageUpdate fallback
|
|
2239
2236
|
this.#triggerEvent("storageUpdated", this.getStorage);
|
|
@@ -2255,16 +2252,16 @@ class PlaySocket {
|
|
|
2255
2252
|
break;
|
|
2256
2253
|
|
|
2257
2254
|
case 'room_created':
|
|
2258
|
-
if (this.#
|
|
2259
|
-
this.#
|
|
2260
|
-
this.#
|
|
2255
|
+
if (this.#pendingCreate) {
|
|
2256
|
+
this.#triggerEvent("status", `Room created${this.#pendingCreate.maxSize ? ` with max size ${this.#pendingCreate.maxSize}.` : '.'}`);
|
|
2257
|
+
this.#pendingCreate.resolve(this.#roomId);
|
|
2261
2258
|
}
|
|
2262
2259
|
break;
|
|
2263
2260
|
|
|
2264
2261
|
case 'room_creation_failed':
|
|
2265
|
-
if (this.#
|
|
2266
|
-
this.#pendingHost.reject(new Error("Failed to create room: " + message.reason));
|
|
2262
|
+
if (this.#pendingCreate) {
|
|
2267
2263
|
this.#triggerEvent("error", "Failed to create room: " + message.reason);
|
|
2264
|
+
this.#pendingCreate.reject(new Error("Failed to create room: " + message.reason));
|
|
2268
2265
|
}
|
|
2269
2266
|
break;
|
|
2270
2267
|
|
|
@@ -2284,9 +2281,27 @@ class PlaySocket {
|
|
|
2284
2281
|
}
|
|
2285
2282
|
break;
|
|
2286
2283
|
|
|
2287
|
-
case '
|
|
2288
|
-
if (
|
|
2284
|
+
case 'state_sync':
|
|
2285
|
+
if (this.#pendingStateRequest) {
|
|
2286
|
+
if (this.#debug) console.log(LOG_PREFIX + "State received following state request:", message.state);
|
|
2287
|
+
this.#roomVersion = message.version;
|
|
2288
|
+
this.#crdtManager.importState(message.state);
|
|
2289
|
+
this.#triggerEvent("storageUpdated", this.getStorage);
|
|
2290
|
+
this.#triggerEvent("status", "State synchronized.");
|
|
2291
|
+
this.#pendingStateRequest.resolve();
|
|
2292
|
+
}
|
|
2293
|
+
break;
|
|
2294
|
+
|
|
2295
|
+
case 'property_update':
|
|
2296
|
+
this.#roomVersion++; // Increment room version
|
|
2297
|
+
if (this.#debug) console.log(LOG_PREFIX + "Property update received:", message.update);
|
|
2298
|
+
this.#sentUpdates.delete(message.uuid); // If this is our sent update, delete it so that timeout check knows it was received
|
|
2299
|
+
this.#crdtManager.importPropertyUpdate(message.update);
|
|
2289
2300
|
if (this.#crdtManager.didPropertiesChange) this.#triggerEvent("storageUpdated", this.getStorage);
|
|
2301
|
+
if (this.#roomVersion != message.version) {
|
|
2302
|
+
this.#triggerEvent("error", "Detected missing update – requesting full state.");
|
|
2303
|
+
this.#requestStateSync();
|
|
2304
|
+
}
|
|
2290
2305
|
break;
|
|
2291
2306
|
|
|
2292
2307
|
case 'client_disconnected':
|
|
@@ -2350,11 +2365,34 @@ class PlaySocket {
|
|
|
2350
2365
|
this.#pendingReconnect = null;
|
|
2351
2366
|
});
|
|
2352
2367
|
} catch (error) {
|
|
2368
|
+
if (!this.#initialized) return;
|
|
2353
2369
|
this.#triggerEvent("status", "Reconnection failed: " + error.message);
|
|
2354
2370
|
this.#reconnectTimeout = setTimeout(() => this.#attemptReconnect(), 500);
|
|
2355
2371
|
}
|
|
2356
2372
|
}
|
|
2357
2373
|
|
|
2374
|
+
/**
|
|
2375
|
+
* If the local state is out of sync, request the room state
|
|
2376
|
+
*/
|
|
2377
|
+
async #requestStateSync() {
|
|
2378
|
+
if (this.#pendingStateRequest) return; // Return if already requested
|
|
2379
|
+
try {
|
|
2380
|
+
Promise.race([
|
|
2381
|
+
new Promise(async (resolve) => {
|
|
2382
|
+
this.#pendingStateRequest = { resolve };
|
|
2383
|
+
this.#sendToServer({ type: 'request_state' });
|
|
2384
|
+
}),
|
|
2385
|
+
this.#createTimeout("State request")
|
|
2386
|
+
]).finally(() => {
|
|
2387
|
+
this.#pendingStateRequest = null;
|
|
2388
|
+
});
|
|
2389
|
+
} catch (error) {
|
|
2390
|
+
if (!this.#initialized) return;
|
|
2391
|
+
this.#triggerEvent("error", "Failed to re-sync state: " + error.message);
|
|
2392
|
+
this.destroy();
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2358
2396
|
/**
|
|
2359
2397
|
* Update the host in case a new one was chosen
|
|
2360
2398
|
* @param {string} hostId - Client id of new host
|
|
@@ -2402,7 +2440,7 @@ class PlaySocket {
|
|
|
2402
2440
|
});
|
|
2403
2441
|
this.#roomId = this.#id; // Create room with your own ID as the room ID to mimic p2p
|
|
2404
2442
|
this.#roomHost = this.#id;
|
|
2405
|
-
this.#
|
|
2443
|
+
this.#pendingCreate = { maxSize, resolve, reject };
|
|
2406
2444
|
this.#sendToServer({
|
|
2407
2445
|
type: 'create_room',
|
|
2408
2446
|
state: this.#crdtManager.getState,
|
|
@@ -2412,7 +2450,7 @@ class PlaySocket {
|
|
|
2412
2450
|
}),
|
|
2413
2451
|
this.#createTimeout("Room creation")
|
|
2414
2452
|
]).finally(() => {
|
|
2415
|
-
this.#
|
|
2453
|
+
this.#pendingCreate = null;
|
|
2416
2454
|
});
|
|
2417
2455
|
}
|
|
2418
2456
|
|
|
@@ -2451,12 +2489,22 @@ class PlaySocket {
|
|
|
2451
2489
|
* @param {*} value - New value
|
|
2452
2490
|
*/
|
|
2453
2491
|
updateStorage(key, value) {
|
|
2454
|
-
this.#
|
|
2492
|
+
if (this.#debug) console.log(LOG_PREFIX + "Property set update for key '" + key + "':", value);
|
|
2493
|
+
const propUpdate = this.#crdtManager.updateProperty(key, "set", value);
|
|
2494
|
+
const updateUuid = crypto.randomUUID();
|
|
2495
|
+
this.#sentUpdates.add(updateUuid);
|
|
2455
2496
|
this.#sendToServer({
|
|
2456
2497
|
type: 'property_update',
|
|
2457
2498
|
key,
|
|
2458
|
-
|
|
2499
|
+
update: propUpdate,
|
|
2500
|
+
uuid: updateUuid
|
|
2459
2501
|
});
|
|
2502
|
+
setTimeout(() => {
|
|
2503
|
+
if (this.#initialized && this.#sentUpdates.has(updateUuid)) {
|
|
2504
|
+
this.#triggerEvent("error", "Sent update did not go through – requesting state.");
|
|
2505
|
+
this.#requestStateSync();
|
|
2506
|
+
}
|
|
2507
|
+
}, TIMEOUT_MS);
|
|
2460
2508
|
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)
|
|
2461
2509
|
}
|
|
2462
2510
|
|
|
@@ -2468,12 +2516,22 @@ class PlaySocket {
|
|
|
2468
2516
|
* @param {*} updateValue - New value for update-matching
|
|
2469
2517
|
*/
|
|
2470
2518
|
updateStorageArray(key, operation, value, updateValue) {
|
|
2471
|
-
this.#
|
|
2519
|
+
if (this.#debug) console.log(LOG_PREFIX + `Property array update for key '${key}', operation '${operation}', value '${value}' and updateValue '${updateValue}'.`);
|
|
2520
|
+
const propUpdate = this.#crdtManager.updateProperty(key, "array-" + operation, value, updateValue);
|
|
2521
|
+
const updateUuid = crypto.randomUUID();
|
|
2522
|
+
this.#sentUpdates.add(updateUuid);
|
|
2472
2523
|
this.#sendToServer({
|
|
2473
2524
|
type: 'property_update',
|
|
2474
2525
|
key,
|
|
2475
|
-
|
|
2526
|
+
update: propUpdate,
|
|
2527
|
+
uuid: updateUuid
|
|
2476
2528
|
});
|
|
2529
|
+
setTimeout(() => {
|
|
2530
|
+
if (this.#initialized && this.#sentUpdates.has(updateUuid)) {
|
|
2531
|
+
this.#triggerEvent("error", "Sent update did not go through – requesting state.");
|
|
2532
|
+
this.#requestStateSync();
|
|
2533
|
+
}
|
|
2534
|
+
}, TIMEOUT_MS);
|
|
2477
2535
|
if (this.#crdtManager.didPropertiesChange) this.#triggerEvent("storageUpdated", this.getStorage);
|
|
2478
2536
|
}
|
|
2479
2537
|
|
|
@@ -2501,13 +2559,16 @@ class PlaySocket {
|
|
|
2501
2559
|
this.#connectionCount = 0;
|
|
2502
2560
|
this.#isReconnecting = false;
|
|
2503
2561
|
this.#reconnectCount = 0;
|
|
2562
|
+
this.#roomVersion = 0;
|
|
2563
|
+
this.#sentUpdates.clear();
|
|
2504
2564
|
|
|
2505
2565
|
// Reject timeouts if currently active
|
|
2506
2566
|
if (this.#pendingJoin) this.#pendingJoin.reject();
|
|
2507
|
-
if (this.#
|
|
2567
|
+
if (this.#pendingCreate) this.#pendingCreate.reject();
|
|
2508
2568
|
if (this.#pendingRegistration) this.#pendingRegistration.reject();
|
|
2509
2569
|
if (this.#pendingConnect) this.#pendingConnect.reject();
|
|
2510
2570
|
if (this.#pendingReconnect) this.#pendingReconnect.reject();
|
|
2571
|
+
if (this.#pendingStateRequest) this.#pendingStateRequest.reject();
|
|
2511
2572
|
}
|
|
2512
2573
|
|
|
2513
2574
|
// Public getters
|
|
@@ -1819,8 +1819,6 @@ function requireCrdtManager () {
|
|
|
1819
1819
|
// Storage
|
|
1820
1820
|
#replicaId;
|
|
1821
1821
|
#keyOperations = new Map();
|
|
1822
|
-
|
|
1823
|
-
// Vector clock
|
|
1824
1822
|
#vectorClock = new Map();
|
|
1825
1823
|
|
|
1826
1824
|
// Local only
|
|
@@ -1829,14 +1827,19 @@ function requireCrdtManager () {
|
|
|
1829
1827
|
|
|
1830
1828
|
// Local Garbage Collection
|
|
1831
1829
|
#lastGCCheck = 0;
|
|
1832
|
-
#
|
|
1830
|
+
#opUuidTimestamp = new Map(); // Map every operation to a timestamp for garbage collection
|
|
1831
|
+
|
|
1832
|
+
// Debug
|
|
1833
|
+
#debug = false;
|
|
1833
1834
|
|
|
1834
1835
|
/**
|
|
1835
1836
|
* Create a new instance
|
|
1836
|
-
* @param {string}
|
|
1837
|
+
* @param {string} replicaId - Choose a uuid (falls back to random uuid)
|
|
1837
1838
|
*/
|
|
1838
|
-
constructor(replicaId) {
|
|
1839
|
-
|
|
1839
|
+
constructor(replicaId, debug) {
|
|
1840
|
+
if (!replicaId) return console.error("No replicaId provided!");
|
|
1841
|
+
if (debug) this.#debug = true;
|
|
1842
|
+
this.#replicaId = replicaId;
|
|
1840
1843
|
this.#vectorClock.set(this.#replicaId, 0);
|
|
1841
1844
|
}
|
|
1842
1845
|
|
|
@@ -1847,8 +1850,13 @@ function requireCrdtManager () {
|
|
|
1847
1850
|
importState(state) {
|
|
1848
1851
|
try {
|
|
1849
1852
|
const { keyOperations, vectorClock } = state;
|
|
1853
|
+
if (this.#debug) console.log(CONSOLE_PREFIX + "Importing state:", state);
|
|
1854
|
+
|
|
1855
|
+
// Resets
|
|
1856
|
+
this.#opUuidTimestamp.clear();
|
|
1857
|
+
this.#propertyStore = {};
|
|
1858
|
+
this.#lastPropertyStore = {};
|
|
1850
1859
|
|
|
1851
|
-
this.#opUuuidTimestamp.clear(); // Reset the uuid timestamps since it might contain uuid's that no longer exist in the new state
|
|
1852
1860
|
this.#keyOperations = new Map(keyOperations); // Rebuild the map
|
|
1853
1861
|
this.#vectorClock = new Map(vectorClock); // Also rebuild the map here
|
|
1854
1862
|
if (!this.#vectorClock.has(this.#replicaId)) this.#vectorClock.set(this.#replicaId, 0); // Reset own vector clock if it wasn't present in the imported state
|
|
@@ -1856,7 +1864,7 @@ function requireCrdtManager () {
|
|
|
1856
1864
|
// Map operation uuids to timestamp
|
|
1857
1865
|
this.#keyOperations.forEach((value, key) => {
|
|
1858
1866
|
value?.forEach((operation) => {
|
|
1859
|
-
this.#
|
|
1867
|
+
this.#opUuidTimestamp.set(operation?.uuid, Date.now());
|
|
1860
1868
|
});
|
|
1861
1869
|
});
|
|
1862
1870
|
|
|
@@ -1869,72 +1877,38 @@ function requireCrdtManager () {
|
|
|
1869
1877
|
}
|
|
1870
1878
|
|
|
1871
1879
|
/**
|
|
1872
|
-
*
|
|
1873
|
-
* This can still be imported using importProperty
|
|
1874
|
-
* @param {string} key
|
|
1875
|
-
* @returns {Object} - Data export
|
|
1876
|
-
*/
|
|
1877
|
-
exportPropertyLastOpOnly(key) {
|
|
1878
|
-
const lastOp = this.#keyOperations.get(key)?.[this.#keyOperations.get(key)?.length - 1];
|
|
1879
|
-
return {
|
|
1880
|
-
key,
|
|
1881
|
-
operations: lastOp ? [lastOp] : [],
|
|
1882
|
-
vectorClock: Array.from(this.#vectorClock.entries())
|
|
1883
|
-
};
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
/**
|
|
1887
|
-
* Export property for a single key
|
|
1888
|
-
* @param {string} key
|
|
1889
|
-
* @returns {Object} - Data export
|
|
1890
|
-
*/
|
|
1891
|
-
exportProperty(key) {
|
|
1892
|
-
return {
|
|
1893
|
-
key,
|
|
1894
|
-
operations: [...(this.#keyOperations.get(key) || [])],
|
|
1895
|
-
vectorClock: Array.from(this.#vectorClock.entries())
|
|
1896
|
-
};
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
/**
|
|
1900
|
-
* Import property for a single key and merge changes
|
|
1880
|
+
* Import property update
|
|
1901
1881
|
* @param {Object} data - Data to import
|
|
1902
1882
|
*/
|
|
1903
|
-
|
|
1883
|
+
importPropertyUpdate(data) {
|
|
1904
1884
|
try {
|
|
1905
|
-
const { key,
|
|
1885
|
+
const { key, operation, vectorClock } = data;
|
|
1886
|
+
if (this.#debug) console.log(CONSOLE_PREFIX + "Importing update:", data); // Debug
|
|
1906
1887
|
|
|
1907
1888
|
// Get COPY of current ops (or empty array if none yet)
|
|
1908
1889
|
const currentOps = [...(this.#keyOperations.get(key) || [])];
|
|
1909
1890
|
|
|
1910
1891
|
// Merge vector clocks (always take max value)
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
this.#vectorClock.set(id, counter);
|
|
1915
|
-
}
|
|
1892
|
+
for (const [id, counter] of vectorClock) {
|
|
1893
|
+
if (!this.#vectorClock.has(id) || this.#vectorClock.get(id) < counter) {
|
|
1894
|
+
this.#vectorClock.set(id, counter);
|
|
1916
1895
|
}
|
|
1917
1896
|
}
|
|
1918
1897
|
|
|
1919
|
-
//
|
|
1898
|
+
// Saveguard
|
|
1920
1899
|
if (this.#vectorClock.size > 1000) this.#vectorClock = new Map([...this.#vectorClock].slice(-100));
|
|
1921
1900
|
|
|
1922
|
-
// Add new
|
|
1901
|
+
// Add new operation if it's not already added
|
|
1923
1902
|
const existingUuids = new Set(currentOps.map(op => op.uuid));
|
|
1924
|
-
if (
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
currentOps.push({ ...op }); // Add operation
|
|
1928
|
-
this.#opUuuidTimestamp.set(op?.uuid, Date.now()); // Add timestamp for gc
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1903
|
+
if (operation?.uuid && !existingUuids.has(operation.uuid)) {
|
|
1904
|
+
currentOps.push({ ...operation }); // Add operation
|
|
1905
|
+
this.#opUuidTimestamp.set(operation?.uuid, Date.now()); // Add timestamp for gc
|
|
1931
1906
|
}
|
|
1932
1907
|
|
|
1933
1908
|
// Sort, update operations & local value
|
|
1934
1909
|
this.#keyOperations.set(key, this.#sortByVectorClock(currentOps));
|
|
1935
1910
|
this.#processLocalProperty(key);
|
|
1936
|
-
|
|
1937
|
-
this.#checkGarbageCollection(); // Check if GC should run
|
|
1911
|
+
this.#checkGarbageCollection();
|
|
1938
1912
|
|
|
1939
1913
|
} catch (error) {
|
|
1940
1914
|
console.error(CONSOLE_PREFIX + "Failed to import property:", error);
|
|
@@ -1947,9 +1921,13 @@ function requireCrdtManager () {
|
|
|
1947
1921
|
* @param {string} operation
|
|
1948
1922
|
* @param {*} value
|
|
1949
1923
|
* @param {*} updateValue
|
|
1924
|
+
* @returns {Object} - Returns the property update
|
|
1950
1925
|
*/
|
|
1951
1926
|
updateProperty(key, operation, value, updateValue) {
|
|
1952
1927
|
try {
|
|
1928
|
+
// Debug log
|
|
1929
|
+
if (this.#debug) console.log(CONSOLE_PREFIX + `Updating property with key ${key}, operation ${operation}, value ${value} and updateValue ${updateValue}.`);
|
|
1930
|
+
|
|
1953
1931
|
// Increment vector clock
|
|
1954
1932
|
const counter = this.#vectorClock.get(this.#replicaId) || 0;
|
|
1955
1933
|
this.#vectorClock.set(this.#replicaId, counter + 1);
|
|
@@ -1960,13 +1938,16 @@ function requireCrdtManager () {
|
|
|
1960
1938
|
// Add operation
|
|
1961
1939
|
const newOp = this.#createOperation({ operation, value, updateValue }, Array.from(this.#vectorClock.entries()));
|
|
1962
1940
|
currentOps.push(newOp);
|
|
1941
|
+
this.#keyOperations.set(key, currentOps); // Update the operations (no need to sort via vector clock since local updates are always the latest)
|
|
1942
|
+
this.#processLocalProperty(key); // Process local value
|
|
1943
|
+
this.#checkGarbageCollection();
|
|
1963
1944
|
|
|
1964
|
-
//
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1945
|
+
// Return the property update with the new operation (this can be imported using importPropertyUpdate)
|
|
1946
|
+
return {
|
|
1947
|
+
key,
|
|
1948
|
+
operation: { ...newOp },
|
|
1949
|
+
vectorClock: Array.from(this.#vectorClock.entries())
|
|
1950
|
+
};
|
|
1970
1951
|
|
|
1971
1952
|
} catch (error) {
|
|
1972
1953
|
console.error(CONSOLE_PREFIX + `Failed to add operation for key "${key}":`, error);
|
|
@@ -1985,15 +1966,16 @@ function requireCrdtManager () {
|
|
|
1985
1966
|
|
|
1986
1967
|
let retainCount = operations.length;
|
|
1987
1968
|
operations.forEach((op, index) => {
|
|
1988
|
-
// Count how many ops, from last to latest, are
|
|
1989
|
-
if (op.uuid && this.#
|
|
1969
|
+
// Count how many ops, from last to latest, are older than the min-age in a row
|
|
1970
|
+
if (op.uuid && this.#opUuidTimestamp.has(op.uuid) && (Date.now() - this.#opUuidTimestamp.get(op.uuid)) > MIN_AGE_FOR_GC) {
|
|
1990
1971
|
const removeCount = index + 1;
|
|
1991
1972
|
retainCount = operations.length - removeCount;
|
|
1992
|
-
this.#
|
|
1973
|
+
this.#opUuidTimestamp.delete(op.uuid); // Remove from timestamps if set to be deleted
|
|
1993
1974
|
}
|
|
1994
1975
|
});
|
|
1995
1976
|
|
|
1996
1977
|
if (retainCount < operations.length) {
|
|
1978
|
+
if (this.#debug) console.log(CONSOLE_PREFIX + `Running garbage collection for key ${key} with current operations:`, operations);
|
|
1997
1979
|
const retainOps = operations.slice(-retainCount); // newest ops
|
|
1998
1980
|
const removeOps = operations.slice(0, -retainCount); // oldest ops (start = idx 0, end = retainCount counted from right side)
|
|
1999
1981
|
const baselineVectorClock = removeOps[removeOps.length - 1]?.vectorClock || []; // Use the vector clock from the last operation that we remove/overwrite
|
|
@@ -2014,6 +1996,9 @@ function requireCrdtManager () {
|
|
|
2014
1996
|
// Create a compact operation with baseline value and appropriate vector clock
|
|
2015
1997
|
const compactOp = this.#createOperation({ operation: "set", value: baselineValue }, baselineVectorClock);
|
|
2016
1998
|
|
|
1999
|
+
// Debug log
|
|
2000
|
+
if (this.#debug) console.log(CONSOLE_PREFIX + "Operations after garbage collection for this key:", [compactOp, ...retainOps]);
|
|
2001
|
+
|
|
2017
2002
|
// Combine compact op with retained operations
|
|
2018
2003
|
this.#keyOperations.set(key, [compactOp, ...retainOps]);
|
|
2019
2004
|
}
|
|
@@ -2033,7 +2018,7 @@ function requireCrdtManager () {
|
|
|
2033
2018
|
source: this.#replicaId,
|
|
2034
2019
|
uuid: crypto.randomUUID()
|
|
2035
2020
|
};
|
|
2036
|
-
this.#
|
|
2021
|
+
this.#opUuidTimestamp.set(newOperation.uuid, Date.now());
|
|
2037
2022
|
return newOperation;
|
|
2038
2023
|
}
|
|
2039
2024
|
|
|
@@ -2205,6 +2190,10 @@ function requireServer () {
|
|
|
2205
2190
|
#heartbeatInterval;
|
|
2206
2191
|
#pendingDisconnects = new Map(); // ClientId -> {timeout, roomId}
|
|
2207
2192
|
#clientTokens = new Map(); // ClientId -> Token
|
|
2193
|
+
#roomVersions = new Map(); // RoomId -> Version
|
|
2194
|
+
|
|
2195
|
+
// Debug
|
|
2196
|
+
#debug = false;
|
|
2208
2197
|
|
|
2209
2198
|
/**
|
|
2210
2199
|
* Create a new PlaySocketServer instance
|
|
@@ -2214,7 +2203,9 @@ function requireServer () {
|
|
|
2214
2203
|
* @param {string} [options.path='/socket'] - WebSocket endpoint path
|
|
2215
2204
|
*/
|
|
2216
2205
|
constructor(options = {}) {
|
|
2217
|
-
const { server, port = 3000, path = '/' } = options;
|
|
2206
|
+
const { server, port = 3000, path = '/', debug = false } = options;
|
|
2207
|
+
|
|
2208
|
+
if (debug) this.#debug = true; // Enable extra logging
|
|
2218
2209
|
|
|
2219
2210
|
// Handle server creation / usage
|
|
2220
2211
|
if (server) {
|
|
@@ -2223,7 +2214,7 @@ function requireServer () {
|
|
|
2223
2214
|
this.#ownsServer = true;
|
|
2224
2215
|
this.#server = http.createServer(); // Create HTTP server and start it
|
|
2225
2216
|
this.#server.listen(port, () => {
|
|
2226
|
-
console.log(`PlaySocket server running on port ${port}
|
|
2217
|
+
console.log(`PlaySocket server running on port ${port}.`);
|
|
2227
2218
|
});
|
|
2228
2219
|
}
|
|
2229
2220
|
|
|
@@ -2300,12 +2291,15 @@ function requireServer () {
|
|
|
2300
2291
|
|
|
2301
2292
|
// If they were in a room, provide updated room data
|
|
2302
2293
|
let roomData;
|
|
2303
|
-
const
|
|
2294
|
+
const formerRoomId = this.#clientRooms.get(data.id);
|
|
2295
|
+
const formerRoom = this.#rooms[formerRoomId];
|
|
2304
2296
|
if (formerRoom) {
|
|
2297
|
+
if (this.#debug) console.log(`State sent for reconnection for room ${formerRoomId}:`, formerRoom.crdtManager.getState);
|
|
2305
2298
|
roomData = {
|
|
2306
2299
|
state: formerRoom.crdtManager.getState,
|
|
2307
2300
|
participantCount: formerRoom.participants.length,
|
|
2308
|
-
host: formerRoom.participants[0]
|
|
2301
|
+
host: formerRoom.participants[0],
|
|
2302
|
+
version: this.#roomVersions.get(formerRoomId)
|
|
2309
2303
|
};
|
|
2310
2304
|
}
|
|
2311
2305
|
|
|
@@ -2315,6 +2309,18 @@ function requireServer () {
|
|
|
2315
2309
|
}
|
|
2316
2310
|
break;
|
|
2317
2311
|
|
|
2312
|
+
case 'request_state':
|
|
2313
|
+
const stateRoomId = this.#clientRooms.get(ws.clientId);
|
|
2314
|
+
const stateRoom = stateRoomId ? this.#rooms[stateRoomId] : null;
|
|
2315
|
+
if (stateRoom) {
|
|
2316
|
+
ws.send(encode({
|
|
2317
|
+
type: 'state_sync',
|
|
2318
|
+
state: stateRoom.crdtManager.getState,
|
|
2319
|
+
version: this.#roomVersions.get(stateRoomId)
|
|
2320
|
+
}), { binary: true });
|
|
2321
|
+
}
|
|
2322
|
+
break;
|
|
2323
|
+
|
|
2318
2324
|
case 'create_room':
|
|
2319
2325
|
if (!ws.clientId) return;
|
|
2320
2326
|
const newRoomId = ws.clientId;
|
|
@@ -2326,15 +2332,19 @@ function requireServer () {
|
|
|
2326
2332
|
return;
|
|
2327
2333
|
}
|
|
2328
2334
|
|
|
2329
|
-
// Create room
|
|
2335
|
+
// Create room
|
|
2336
|
+
this.#roomVersions.set(newRoomId, 0); // Start with version 0
|
|
2330
2337
|
this.#rooms[newRoomId] = {
|
|
2331
2338
|
participants: [ws.clientId],
|
|
2332
2339
|
maxSize: data.size || null,
|
|
2333
|
-
crdtManager: new CRDTManager()
|
|
2340
|
+
crdtManager: new CRDTManager(crypto.randomUUID(), this.#debug)
|
|
2334
2341
|
};
|
|
2335
2342
|
|
|
2336
2343
|
// Load state if provided
|
|
2337
|
-
if (data.state)
|
|
2344
|
+
if (data.state) {
|
|
2345
|
+
this.#rooms[newRoomId].crdtManager.importState(data.state);
|
|
2346
|
+
if (this.#debug) console.log("Room created with initial state:", data.state);
|
|
2347
|
+
}
|
|
2338
2348
|
|
|
2339
2349
|
this.#clientRooms.set(ws.clientId, newRoomId); // Add client to their room
|
|
2340
2350
|
ws.send(encode({ type: 'room_created' }), { binary: true });
|
|
@@ -2362,11 +2372,13 @@ function requireServer () {
|
|
|
2362
2372
|
this.#clientRooms.set(ws.clientId, roomId);
|
|
2363
2373
|
|
|
2364
2374
|
// Notify joiner with initial storage
|
|
2375
|
+
if (this.#debug) console.log("Room state sent for join:", room.crdtManager.getState);
|
|
2365
2376
|
ws.send(encode({
|
|
2366
2377
|
type: 'join_accepted',
|
|
2367
2378
|
state: room.crdtManager.getState,
|
|
2368
2379
|
participantCount: room.participants.length,
|
|
2369
|
-
host: room.participants[0]
|
|
2380
|
+
host: room.participants[0],
|
|
2381
|
+
version: this.#roomVersions.get(roomId)
|
|
2370
2382
|
}), { binary: true });
|
|
2371
2383
|
|
|
2372
2384
|
// Notify existing participants
|
|
@@ -2387,15 +2399,23 @@ function requireServer () {
|
|
|
2387
2399
|
case 'property_update':
|
|
2388
2400
|
const updateRoomId = this.#clientRooms.get(ws.clientId);
|
|
2389
2401
|
const updateRoom = updateRoomId ? this.#rooms[updateRoomId] : null;
|
|
2390
|
-
|
|
2391
|
-
if (updateRoom && data.key && data.
|
|
2392
|
-
updateRoom.crdtManager.
|
|
2402
|
+
|
|
2403
|
+
if (updateRoom && data.key && data.update && data.uuid) {
|
|
2404
|
+
updateRoom.crdtManager.importPropertyUpdate(data.update);
|
|
2405
|
+
|
|
2406
|
+
// Increment version for this room
|
|
2407
|
+
const currentVersion = this.#roomVersions.get(updateRoomId) + 1;
|
|
2408
|
+
this.#roomVersions.set(updateRoomId, currentVersion);
|
|
2409
|
+
|
|
2410
|
+
if (this.#debug) console.log("Property update received and imported:", data.update);
|
|
2393
2411
|
updateRoom.participants?.forEach(p => {
|
|
2394
2412
|
const client = this.#clients.get(p);
|
|
2395
2413
|
if (client) {
|
|
2396
2414
|
client.send(encode({
|
|
2397
|
-
type: '
|
|
2398
|
-
|
|
2415
|
+
type: 'property_update',
|
|
2416
|
+
update: data.update,
|
|
2417
|
+
version: currentVersion,
|
|
2418
|
+
uuid: data.uuid
|
|
2399
2419
|
}), { binary: true });
|
|
2400
2420
|
}
|
|
2401
2421
|
});
|
|
@@ -2488,6 +2508,8 @@ function requireServer () {
|
|
|
2488
2508
|
|
|
2489
2509
|
if (room.participants?.length === 0) {
|
|
2490
2510
|
delete this.#rooms[roomId]; // Delete room if now empty
|
|
2511
|
+
this.#roomVersions.delete(roomId); // Delete room version
|
|
2512
|
+
if (this.#debug) console.log("Deleted room with id " + roomId + ".");
|
|
2491
2513
|
} else {
|
|
2492
2514
|
// Notify remaining participants
|
|
2493
2515
|
room.participants.forEach(p => {
|
|
@@ -2539,7 +2561,7 @@ function requireServer () {
|
|
|
2539
2561
|
}
|
|
2540
2562
|
if (this.#server && this.#ownsServer) {
|
|
2541
2563
|
this.#server.close(() => {
|
|
2542
|
-
console.log('PlaySocket server stopped');
|
|
2564
|
+
console.log('PlaySocket server stopped.');
|
|
2543
2565
|
});
|
|
2544
2566
|
}
|
|
2545
2567
|
this.#pendingDisconnects.forEach((data) => {
|