playsocketjs 2.4.2 → 2.5.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.
@@ -1679,398 +1679,386 @@ function decode(buffer, options) {
1679
1679
  return decoder.decode(buffer);
1680
1680
  }
1681
1681
 
1682
- var crdtManager;
1683
- var hasRequiredCrdtManager;
1684
-
1685
- function requireCrdtManager () {
1686
- if (hasRequiredCrdtManager) return crdtManager;
1687
- hasRequiredCrdtManager = 1;
1688
- // Custom conflict-free replicated data type system with vector clocks
1689
-
1690
- const CONSOLE_PREFIX = "PlaySocket CRDT manager: ";
1691
-
1692
- class CRDTManager {
1693
- // Storage
1694
- #replicaId;
1695
- #keyOperations = new Map();
1696
- #vectorClock = new Map();
1697
-
1698
- // Local only
1699
- #propertyStore = {}; // Current local values per key, as object
1700
- #lastPropertyStore = {}; // Last property store to compare against
1701
-
1702
- // Local Garbage Collection
1703
- #lastGCCheck = 0;
1704
- #opUuidTimestamp = new Map(); // Map every operation to a timestamp for garbage collection
1705
-
1706
- // Debug
1707
- #debug = false;
1708
-
1709
- /**
1710
- * Create a new instance
1711
- */
1712
- constructor(debug) {
1713
- if (debug) this.#debug = true;
1714
- this.#replicaId = crypto.randomUUID();
1715
- this.#vectorClock.set(this.#replicaId, 0);
1716
- }
1717
-
1718
- /**
1719
- * Import the entire state of the CRDT manager (this overwrites the old state)
1720
- * @param {Object} state
1721
- */
1722
- importState(state) {
1723
- try {
1724
- const { keyOperations, vectorClock } = state;
1725
- if (this.#debug) console.log(CONSOLE_PREFIX + "Importing state:", state);
1726
-
1727
- // Resets
1728
- this.#opUuidTimestamp.clear();
1729
- this.#propertyStore = {};
1730
- this.#lastPropertyStore = {};
1731
-
1732
- this.#keyOperations = new Map(keyOperations); // Rebuild the operations map
1733
- this.#vectorClock = new Map(vectorClock); // Rebuild the vector clock map
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
1735
-
1736
- // Map all operation uuids to current timestamp
1737
- this.#keyOperations.forEach((value, key) => {
1738
- value?.forEach((operation) => {
1739
- this.#opUuidTimestamp.set(operation?.uuid, Date.now());
1740
- });
1741
- });
1742
-
1743
- // Process each key to update local values
1744
- for (const key of this.#keyOperations.keys()) this.#processLocalProperty(key);
1745
-
1746
- } catch (error) {
1747
- console.error(CONSOLE_PREFIX + "Failed to import state:", error);
1748
- }
1749
- }
1750
-
1751
- /**
1752
- * Import property update
1753
- * @param {Object} data - Data to import
1754
- */
1755
- importPropertyUpdate(data) {
1756
- try {
1757
- const { key, operation: rawOperation, vectorClock } = data;
1758
- const operation = this.#sanitizeValue(rawOperation);
1759
- if (this.#debug) console.log(CONSOLE_PREFIX + "Importing update:", data); // Debug
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
-
1764
- // Get COPY of current ops (or empty array if none yet)
1765
- const currentOps = [...(this.#keyOperations.get(key) || [])];
1766
- // Merge vector clocks (always take max value)
1767
- for (const [id, counter] of vectorClock) {
1768
- if (!this.#vectorClock.has(id) || this.#vectorClock.get(id) < counter) {
1769
- this.#vectorClock.set(id, counter);
1770
- }
1771
- }
1772
-
1773
- // Safeguard against massive vector clocks
1774
- if (this.#vectorClock.size > 1000) this.#vectorClock = new Map([...this.#vectorClock].slice(-100));
1775
-
1776
- // Add new operation if it's not already added
1777
- const existingUuids = new Set(currentOps.map(op => op.uuid));
1778
- if (operation.uuid && !existingUuids.has(operation.uuid)) {
1779
- currentOps.push({ ...operation }); // Add operation
1780
- this.#opUuidTimestamp.set(operation.uuid, Date.now()); // Add timestamp for gc
1781
- }
1782
-
1783
- // Sort, update operations & local value
1784
- this.#keyOperations.set(key, this.#sortByVectorClock(currentOps));
1785
- this.#processLocalProperty(key);
1786
- this.#checkGarbageCollection();
1787
-
1788
- } catch (error) {
1789
- console.error(CONSOLE_PREFIX + "Failed to import property update:", error);
1790
- }
1791
- }
1792
-
1793
- /**
1794
- * Update a property
1795
- * @param {string} key
1796
- * @param {string} type
1797
- * @param {*} value
1798
- * @param {*} updateValue
1799
- * @returns {Object} - Returns the property update
1800
- */
1801
- updateProperty(key, type, value, updateValue) {
1802
- try {
1803
- // Sanitize inputs
1804
- value = this.#sanitizeValue(value);
1805
- updateValue = this.#sanitizeValue(updateValue);
1806
-
1807
- // Debug log
1808
- if (this.#debug) console.log(CONSOLE_PREFIX + `Updating property with key ${key}, type ${type}, value ${value} and updateValue ${updateValue}.`);
1809
-
1810
- // Increment vector clock
1811
- const counter = this.#vectorClock.get(this.#replicaId) || 0;
1812
- this.#vectorClock.set(this.#replicaId, counter + 1);
1813
-
1814
- // Assign shallow COPY of currennt ops or fall back to empty array
1815
- const currentOps = [...(this.#keyOperations.get(key) || [])];
1816
-
1817
- // Add operation
1818
- const newOp = this.#createOperation({ type, value, updateValue }, Array.from(this.#vectorClock.entries()));
1819
- currentOps.push(newOp);
1820
- this.#keyOperations.set(key, currentOps); // Update the operations (no need to sort via vector clock since local updates are always the latest)
1821
- this.#processLocalProperty(key); // Process local value
1822
- this.#checkGarbageCollection();
1823
-
1824
- // Return the property update with the new operation (this can be imported using importPropertyUpdate)
1825
- return {
1826
- key,
1827
- operation: { ...newOp },
1828
- vectorClock: Array.from(this.#vectorClock.entries())
1829
- };
1830
-
1831
- } catch (error) {
1832
- console.error(CONSOLE_PREFIX + `Failed to add operation for key "${key}":`, error);
1833
- }
1834
- }
1835
-
1836
- #checkGarbageCollection() {
1837
- const MIN_GC_DELAY = 1000; // Minimum 1s delay between garbage collection runs
1838
- const MIN_AGE_FOR_GC = 5000; // Garbage collect ops that are older than 5s
1839
-
1840
- try {
1841
- if (Date.now() - this.#lastGCCheck < MIN_GC_DELAY) return;
1842
- this.#lastGCCheck = Date.now();
1843
-
1844
- for (const [key, operations] of this.#keyOperations.entries()) {
1845
- if (operations?.length < 5) continue; // Min op amount per key for garbage collection to run (otherwise not worth it)
1846
-
1847
- let retainCount = operations.length;
1848
- operations.forEach((op, index) => {
1849
- // Count how many ops, from last to latest, are older than the min-age in a row
1850
- if (op.uuid && this.#opUuidTimestamp.has(op.uuid) && (Date.now() - this.#opUuidTimestamp.get(op.uuid)) > MIN_AGE_FOR_GC) {
1851
- const removeCount = index + 1;
1852
- retainCount = operations.length - removeCount;
1853
- this.#opUuidTimestamp.delete(op.uuid); // Remove from timestamps if set to be deleted
1854
- }
1855
- });
1856
-
1857
- if (retainCount < operations.length) {
1858
- if (this.#debug) console.log(CONSOLE_PREFIX + `Running garbage collection for key ${key} with current operations:`, operations);
1859
- const retainOps = operations.slice(-retainCount); // Newest ops
1860
- const removeOps = operations.slice(0, -retainCount); // Oldest ops (start = idx 0, end = retainCount counted from right side)
1861
- const baselineVectorClock = removeOps[removeOps.length - 1]?.vectorClock || []; // Use the vector clock from the last operation that we remove/overwrite
1862
-
1863
- // Calculate the value at the point where retained operations start
1864
- let baselineValue = null;
1865
- for (const op of removeOps) {
1866
- if (!op.data) continue;
1867
- baselineValue = this.#handleOperation(
1868
- baselineValue,
1869
- op.data.type,
1870
- op.data.value,
1871
- op.data.updateValue
1872
- );
1873
- }
1874
-
1875
- // Create a compact operation with baseline value and appropriate vector clock
1876
- const compactOp = this.#createOperation({ type: "set", value: baselineValue }, baselineVectorClock);
1877
-
1878
- // Debug log
1879
- if (this.#debug) console.log(CONSOLE_PREFIX + "Operations after garbage collection for this key:", [compactOp, ...retainOps]);
1880
-
1881
- // Combine compact op with retained operations
1882
- this.#keyOperations.set(key, [compactOp, ...retainOps]);
1883
- }
1884
- }
1885
- } catch (error) {
1886
- console.error(CONSOLE_PREFIX + "Error running garbage collection:", error);
1887
- }
1888
- }
1889
-
1890
- /**
1891
- * Create an operation object
1892
- * @param {Object} data
1893
- * @param {Array} vectorClock
1894
- * @returns {Object} - Operation object
1895
- */
1896
- #createOperation(data, vectorClock) {
1897
- const newOperation = {
1898
- data,
1899
- vectorClock,
1900
- source: this.#replicaId,
1901
- uuid: crypto.randomUUID()
1902
- };
1903
- this.#opUuidTimestamp.set(newOperation.uuid, Date.now());
1904
- return newOperation;
1905
- }
1906
-
1907
- /**
1908
- * Process a property's value by applying all operations and set it in the property store
1909
- * @param {string} key
1910
- */
1911
- #processLocalProperty(key) {
1912
- try {
1913
- const ops = this.#keyOperations.get(key);
1914
- if (!ops?.length) return;
1915
-
1916
- // Apply all operations in order
1917
- let value = null;
1918
-
1919
- for (const op of ops) {
1920
- if (!op.data) continue;
1921
- value = this.#handleOperation(
1922
- value,
1923
- op.data.type,
1924
- op.data.value,
1925
- op.data.updateValue
1926
- );
1927
- }
1928
-
1929
- this.#propertyStore[key] = value; // Save locally
1930
- } catch (error) {
1931
- console.error(CONSOLE_PREFIX + `Failed to process property for key ${key}:`, error);
1932
- }
1933
- }
1934
-
1935
- /**
1936
- * Sort by vector clock (causal order)
1937
- * @param {Array} operations
1938
- * @returns {Array} - Sorted operations
1939
- */
1940
- #sortByVectorClock(operations) {
1941
- return [...operations].sort((a, b) => {
1942
- const clockA = new Map(a.vectorClock || []);
1943
- const clockB = new Map(b.vectorClock || []);
1944
-
1945
- // First sort by causal relationship
1946
- let aGreater = false, bGreater = false;
1947
-
1948
- const allIds = new Set([...clockA.keys(), ...clockB.keys()]);
1949
- for (const id of allIds) {
1950
- const countA = clockA.get(id) || 0;
1951
- const countB = clockB.get(id) || 0;
1952
-
1953
- if (countA > countB) aGreater = true;
1954
- if (countA < countB) bGreater = true;
1955
- }
1956
-
1957
- // Determine causal relationship (if one is greater, and the other is smaller, it's clear)
1958
- if (aGreater && !bGreater) return 1; // a happens after b
1959
- if (!aGreater && bGreater) return -1; // a happens before b
1960
-
1961
- // Concurrent (unclear): Compare highest counter in each (sort by which one is ahead)
1962
- const maxA = Math.max(...Array.from(clockA.values()), 0);
1963
- const maxB = Math.max(...Array.from(clockB.values()), 0);
1964
- if (maxA !== maxB) return maxA - maxB;
1965
-
1966
- // Final tiebreak: Sort by ID of Replica that actually generated the operation
1967
- return a.source.localeCompare(b.source);
1968
- });
1969
- }
1970
-
1971
- /**
1972
- * Handle an operation
1973
- * @param {*} curValue
1974
- * @param {string} type
1975
- * @param {*} value
1976
- * @param {*} [updateValue]
1977
- * @returns {*} - Value after the operation
1978
- */
1979
- #handleOperation(curValue, type, value, updateValue) {
1980
- try {
1981
- // Deep copy to avoid reference issues in case value is or contains object(s)
1982
- curValue = structuredClone(curValue);
1983
-
1984
- // Set operation
1985
- if (type === "set") return value;
1986
-
1987
- // Array operations
1988
- if (type.startsWith('array')) {
1989
- if (!Array.isArray(curValue)) curValue = []; // Auto-convert to array if current value isn't one
1990
-
1991
- // Comparison function
1992
- const isObject = typeof value === 'object' && value !== null;
1993
- const compare = (item) => {
1994
- if (isObject && typeof item === 'object' && item !== null) return JSON.stringify(item) === JSON.stringify(value); // Deep comparison
1995
- return item === value; // Value comparison
1996
- };
1997
-
1998
- switch (type) {
1999
- case 'array-add':
2000
- curValue.push(value);
2001
- break;
2002
- case 'array-add-unique':
2003
- if (!curValue.some(compare)) curValue.push(value);
2004
- break;
2005
- case 'array-remove-matching':
2006
- curValue = curValue.filter(item => !compare(item));
2007
- break;
2008
- case 'array-update-matching':
2009
- const index = curValue.findIndex(compare);
2010
- if (index !== -1) curValue[index] = updateValue;
2011
- break;
2012
- }
2013
- }
2014
-
2015
- return curValue;
2016
-
2017
- } catch (error) {
2018
- console.error(CONSOLE_PREFIX + `Failed to handle operation with curValue ${curValue}:`, error);
2019
- return curValue;
2020
- }
2021
- }
2022
-
2023
- /**
2024
- * Remove HTML to prevent XSS and enforce size limits
2025
- * @param {Object} obj
2026
- * @returns {Object} - Sanitized object
2027
- */
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
-
2033
- if (typeof obj === 'string') return (obj.includes('<') || obj.includes('>')) ? obj.replace(/[<>]/g, '') : obj;
2034
- if (Array.isArray(obj)) return obj.map(item => this.#sanitizeValue(item));
2035
- if (obj && typeof obj === 'object') return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, this.#sanitizeValue(v)]));
2036
- return obj;
2037
- }
2038
-
2039
- // Get property store
2040
- get getPropertyStore() {
2041
- try {
2042
- return structuredClone(this.#propertyStore); // Return deep clone
2043
- } catch {
2044
- console.error(CONSOLE_PREFIX + "Failed to parse property store");
2045
- return {};
2046
- }
2047
- }
2048
-
2049
- // Check for changes in the local property store
2050
- get didPropertiesChange() {
2051
- try {
2052
- if (JSON.stringify(this.#propertyStore) !== JSON.stringify(this.#lastPropertyStore)) {
2053
- this.#lastPropertyStore = structuredClone(this.#propertyStore);
2054
- return true;
2055
- }
2056
- } catch { }
2057
- return false;
2058
- }
2059
-
2060
- // Get state (can be imported using importState, converts the maps to arrays for serialization)
2061
- get getState() {
2062
- return {
2063
- keyOperations: [...this.#keyOperations.entries()],
2064
- vectorClock: [...this.#vectorClock.entries()]
2065
- };
2066
- }
2067
- }
2068
-
2069
- crdtManager = { CRDTManager };
2070
- return crdtManager;
2071
- }
1682
+ // Custom conflict-free replicated data type system with vector clocks
1683
+
1684
+ const CONSOLE_PREFIX = "PlaySocket CRDT manager: ";
1685
+
1686
+ class CRDTManager {
1687
+ // Storage
1688
+ #replicaId;
1689
+ #keyOperations = new Map();
1690
+ #vectorClock = new Map();
1691
+
1692
+ // Local only
1693
+ #propertyStore = {}; // Current local values per key, as object
1694
+ #lastPropertyStore = {}; // Last property store to compare against
1695
+
1696
+ // Local Garbage Collection
1697
+ #lastGCCheck = 0;
1698
+ #opUuidTimestamp = new Map(); // Map every operation to a timestamp for garbage collection
1699
+
1700
+ // Debug
1701
+ #debug = false;
1702
+
1703
+ /**
1704
+ * Create a new instance
1705
+ */
1706
+ constructor(debug) {
1707
+ if (debug) this.#debug = true;
1708
+ this.#replicaId = crypto.randomUUID();
1709
+ this.#vectorClock.set(this.#replicaId, 0);
1710
+ }
1711
+
1712
+ /**
1713
+ * Import the entire state of the CRDT manager (this overwrites the old state)
1714
+ * @param {Object} state
1715
+ */
1716
+ importState(state) {
1717
+ try {
1718
+ const { keyOperations, vectorClock } = state;
1719
+ if (this.#debug) console.log(CONSOLE_PREFIX + "Importing state:", state);
1720
+
1721
+ // Resets
1722
+ this.#opUuidTimestamp.clear();
1723
+ this.#propertyStore = {};
1724
+ this.#lastPropertyStore = {};
1725
+
1726
+ this.#keyOperations = new Map(keyOperations); // Rebuild the operations map
1727
+ this.#vectorClock = new Map(vectorClock); // Rebuild the vector clock map
1728
+ 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
+
1730
+ // Map all operation uuids to current timestamp
1731
+ this.#keyOperations.forEach((value, key) => {
1732
+ value?.forEach((operation) => {
1733
+ this.#opUuidTimestamp.set(operation?.uuid, Date.now());
1734
+ });
1735
+ });
1736
+
1737
+ // Process each key to update local values
1738
+ for (const key of this.#keyOperations.keys()) this.#processLocalProperty(key);
1739
+
1740
+ } catch (error) {
1741
+ console.error(CONSOLE_PREFIX + "Failed to import state:", error);
1742
+ }
1743
+ }
1744
+
1745
+ /**
1746
+ * Import property update
1747
+ * @param {Object} data - Data to import
1748
+ */
1749
+ importPropertyUpdate(data) {
1750
+ try {
1751
+ const { key, operation: rawOperation, vectorClock } = data;
1752
+ const operation = this.#sanitizeValue(rawOperation);
1753
+ if (this.#debug) console.log(CONSOLE_PREFIX + "Importing update:", data); // Debug
1754
+
1755
+ // Check key limit to afeguard against too many keys
1756
+ if (this.#keyOperations.size >= 100 && !this.#keyOperations.has(key)) throw new Error('Key limit exceeded!');
1757
+
1758
+ // Get COPY of current ops (or empty array if none yet)
1759
+ const currentOps = [...(this.#keyOperations.get(key) || [])];
1760
+ // Merge vector clocks (always take max value)
1761
+ for (const [id, counter] of vectorClock) {
1762
+ if (!this.#vectorClock.has(id) || this.#vectorClock.get(id) < counter) {
1763
+ this.#vectorClock.set(id, counter);
1764
+ }
1765
+ }
1766
+
1767
+ // Safeguard against massive vector clocks
1768
+ if (this.#vectorClock.size > 1000) this.#vectorClock = new Map([...this.#vectorClock].slice(-100));
2072
1769
 
2073
- var crdtManagerExports = requireCrdtManager();
1770
+ // Add new operation if it's not already added
1771
+ const existingUuids = new Set(currentOps.map(op => op.uuid));
1772
+ if (operation.uuid && !existingUuids.has(operation.uuid)) {
1773
+ currentOps.push({ ...operation }); // Add operation
1774
+ this.#opUuidTimestamp.set(operation.uuid, Date.now()); // Add timestamp for gc
1775
+ }
1776
+
1777
+ // Sort, update operations & local value
1778
+ this.#keyOperations.set(key, this.#sortByVectorClock(currentOps));
1779
+ this.#processLocalProperty(key);
1780
+ this.#checkGarbageCollection();
1781
+
1782
+ } catch (error) {
1783
+ console.error(CONSOLE_PREFIX + "Failed to import property update:", error);
1784
+ }
1785
+ }
1786
+
1787
+ /**
1788
+ * Update a property
1789
+ * @param {string} key
1790
+ * @param {string} type
1791
+ * @param {*} value
1792
+ * @param {*} updateValue
1793
+ * @returns {Object} - Returns the property update
1794
+ */
1795
+ updateProperty(key, type, value, updateValue) {
1796
+ try {
1797
+ // Sanitize inputs
1798
+ value = this.#sanitizeValue(value);
1799
+ updateValue = this.#sanitizeValue(updateValue);
1800
+
1801
+ // Debug log
1802
+ if (this.#debug) console.log(CONSOLE_PREFIX + `Updating property with key ${key}, type ${type}, value ${value} and updateValue ${updateValue}.`);
1803
+
1804
+ // Increment vector clock
1805
+ const counter = this.#vectorClock.get(this.#replicaId) || 0;
1806
+ this.#vectorClock.set(this.#replicaId, counter + 1);
1807
+
1808
+ // Assign shallow COPY of currennt ops or fall back to empty array
1809
+ const currentOps = [...(this.#keyOperations.get(key) || [])];
1810
+
1811
+ // Add operation
1812
+ const newOp = this.#createOperation({ type, value, updateValue }, Array.from(this.#vectorClock.entries()));
1813
+ currentOps.push(newOp);
1814
+ this.#keyOperations.set(key, currentOps); // Update the operations (no need to sort via vector clock since local updates are always the latest)
1815
+ this.#processLocalProperty(key); // Process local value
1816
+ this.#checkGarbageCollection();
1817
+
1818
+ // Return the property update with the new operation (this can be imported using importPropertyUpdate)
1819
+ return {
1820
+ key,
1821
+ operation: { ...newOp },
1822
+ vectorClock: Array.from(this.#vectorClock.entries())
1823
+ };
1824
+
1825
+ } catch (error) {
1826
+ console.error(CONSOLE_PREFIX + `Failed to add operation for key "${key}":`, error);
1827
+ }
1828
+ }
1829
+
1830
+ #checkGarbageCollection() {
1831
+ const MIN_GC_DELAY = 1000; // Minimum 1s delay between garbage collection runs
1832
+ const MIN_AGE_FOR_GC = 5000; // Garbage collect ops that are older than 5s
1833
+
1834
+ try {
1835
+ if (Date.now() - this.#lastGCCheck < MIN_GC_DELAY) return;
1836
+ this.#lastGCCheck = Date.now();
1837
+
1838
+ for (const [key, operations] of this.#keyOperations.entries()) {
1839
+ if (operations?.length < 5) continue; // Min op amount per key for garbage collection to run (otherwise not worth it)
1840
+
1841
+ let retainCount = operations.length;
1842
+ operations.forEach((op, index) => {
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) {
1845
+ const removeCount = index + 1;
1846
+ retainCount = operations.length - removeCount;
1847
+ this.#opUuidTimestamp.delete(op.uuid); // Remove from timestamps if set to be deleted
1848
+ }
1849
+ });
1850
+
1851
+ if (retainCount < operations.length) {
1852
+ if (this.#debug) console.log(CONSOLE_PREFIX + `Running garbage collection for key ${key} with current operations:`, operations);
1853
+ const retainOps = operations.slice(-retainCount); // Newest ops
1854
+ 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
1856
+
1857
+ // Calculate the value at the point where retained operations start
1858
+ let baselineValue = null;
1859
+ for (const op of removeOps) {
1860
+ if (!op.data) continue;
1861
+ baselineValue = this.#handleOperation(
1862
+ baselineValue,
1863
+ op.data.type,
1864
+ op.data.value,
1865
+ op.data.updateValue
1866
+ );
1867
+ }
1868
+
1869
+ // Create a compact operation with baseline value and appropriate vector clock
1870
+ const compactOp = this.#createOperation({ type: "set", value: baselineValue }, baselineVectorClock);
1871
+
1872
+ // Debug log
1873
+ if (this.#debug) console.log(CONSOLE_PREFIX + "Operations after garbage collection for this key:", [compactOp, ...retainOps]);
1874
+
1875
+ // Combine compact op with retained operations
1876
+ this.#keyOperations.set(key, [compactOp, ...retainOps]);
1877
+ }
1878
+ }
1879
+ } catch (error) {
1880
+ console.error(CONSOLE_PREFIX + "Error running garbage collection:", error);
1881
+ }
1882
+ }
1883
+
1884
+ /**
1885
+ * Create an operation object
1886
+ * @param {Object} data
1887
+ * @param {Array} vectorClock
1888
+ * @returns {Object} - Operation object
1889
+ */
1890
+ #createOperation(data, vectorClock) {
1891
+ const newOperation = {
1892
+ data,
1893
+ vectorClock,
1894
+ source: this.#replicaId,
1895
+ uuid: crypto.randomUUID()
1896
+ };
1897
+ this.#opUuidTimestamp.set(newOperation.uuid, Date.now());
1898
+ return newOperation;
1899
+ }
1900
+
1901
+ /**
1902
+ * Process a property's value by applying all operations and set it in the property store
1903
+ * @param {string} key
1904
+ */
1905
+ #processLocalProperty(key) {
1906
+ try {
1907
+ const ops = this.#keyOperations.get(key);
1908
+ if (!ops?.length) return;
1909
+
1910
+ // Apply all operations in order
1911
+ let value = null;
1912
+
1913
+ for (const op of ops) {
1914
+ if (!op.data) continue;
1915
+ value = this.#handleOperation(
1916
+ value,
1917
+ op.data.type,
1918
+ op.data.value,
1919
+ op.data.updateValue
1920
+ );
1921
+ }
1922
+
1923
+ this.#propertyStore[key] = value; // Save locally
1924
+ } catch (error) {
1925
+ console.error(CONSOLE_PREFIX + `Failed to process property for key ${key}:`, error);
1926
+ }
1927
+ }
1928
+
1929
+ /**
1930
+ * Sort by vector clock (causal order)
1931
+ * @param {Array} operations
1932
+ * @returns {Array} - Sorted operations
1933
+ */
1934
+ #sortByVectorClock(operations) {
1935
+ return [...operations].sort((a, b) => {
1936
+ const clockA = new Map(a.vectorClock || []);
1937
+ const clockB = new Map(b.vectorClock || []);
1938
+
1939
+ // First sort by causal relationship
1940
+ let aGreater = false, bGreater = false;
1941
+
1942
+ const allIds = new Set([...clockA.keys(), ...clockB.keys()]);
1943
+ for (const id of allIds) {
1944
+ const countA = clockA.get(id) || 0;
1945
+ const countB = clockB.get(id) || 0;
1946
+
1947
+ if (countA > countB) aGreater = true;
1948
+ if (countA < countB) bGreater = true;
1949
+ }
1950
+
1951
+ // Determine causal relationship (if one is greater, and the other is smaller, it's clear)
1952
+ if (aGreater && !bGreater) return 1; // a happens after b
1953
+ if (!aGreater && bGreater) return -1; // a happens before b
1954
+
1955
+ // Concurrent (unclear): Compare highest counter in each (sort by which one is ahead)
1956
+ const maxA = Math.max(...Array.from(clockA.values()), 0);
1957
+ const maxB = Math.max(...Array.from(clockB.values()), 0);
1958
+ if (maxA !== maxB) return maxA - maxB;
1959
+
1960
+ // Final tiebreak: Sort by ID of Replica that actually generated the operation
1961
+ return a.source.localeCompare(b.source);
1962
+ });
1963
+ }
1964
+
1965
+ /**
1966
+ * Handle an operation
1967
+ * @param {*} curValue
1968
+ * @param {string} type
1969
+ * @param {*} value
1970
+ * @param {*} [updateValue]
1971
+ * @returns {*} - Value after the operation
1972
+ */
1973
+ #handleOperation(curValue, type, value, updateValue) {
1974
+ try {
1975
+ // Deep copy to avoid reference issues in case value is or contains object(s)
1976
+ curValue = structuredClone(curValue);
1977
+
1978
+ // Set operation
1979
+ if (type === "set") return value;
1980
+
1981
+ // Array operations
1982
+ if (type.startsWith('array')) {
1983
+ if (!Array.isArray(curValue)) curValue = []; // Auto-convert to array if current value isn't one
1984
+
1985
+ // Comparison function
1986
+ const isObject = typeof value === 'object' && value !== null;
1987
+ const compare = (item) => {
1988
+ if (isObject && typeof item === 'object' && item !== null) return JSON.stringify(item) === JSON.stringify(value); // Deep comparison
1989
+ return item === value; // Value comparison
1990
+ };
1991
+
1992
+ switch (type) {
1993
+ case 'array-add':
1994
+ curValue.push(value);
1995
+ break;
1996
+ case 'array-add-unique':
1997
+ if (!curValue.some(compare)) curValue.push(value);
1998
+ break;
1999
+ case 'array-remove-matching':
2000
+ curValue = curValue.filter(item => !compare(item));
2001
+ break;
2002
+ case 'array-update-matching':
2003
+ const index = curValue.findIndex(compare);
2004
+ if (index !== -1) curValue[index] = updateValue;
2005
+ break;
2006
+ }
2007
+ }
2008
+
2009
+ return curValue;
2010
+
2011
+ } catch (error) {
2012
+ console.error(CONSOLE_PREFIX + `Failed to handle operation with curValue ${curValue}:`, error);
2013
+ return curValue;
2014
+ }
2015
+ }
2016
+
2017
+ /**
2018
+ * Remove HTML to prevent XSS and enforce size limits
2019
+ * @param {Object} obj
2020
+ * @returns {Object} - Sanitized object
2021
+ */
2022
+ #sanitizeValue(obj) {
2023
+ // Check total serialized size
2024
+ const jsonString = JSON.stringify(obj);
2025
+ if (jsonString?.length > 50000) throw new Error('Value too large!'); // 50KB limit
2026
+
2027
+ if (typeof obj === 'string') return (obj.includes('<') || obj.includes('>')) ? obj.replace(/[<>]/g, '') : obj;
2028
+ if (Array.isArray(obj)) return obj.map(item => this.#sanitizeValue(item));
2029
+ if (obj && typeof obj === 'object') return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, this.#sanitizeValue(v)]));
2030
+ return obj;
2031
+ }
2032
+
2033
+ // Get property store
2034
+ get getPropertyStore() {
2035
+ try {
2036
+ return structuredClone(this.#propertyStore); // Return deep clone
2037
+ } catch {
2038
+ console.error(CONSOLE_PREFIX + "Failed to parse property store");
2039
+ return {};
2040
+ }
2041
+ }
2042
+
2043
+ // Check for changes in the local property store
2044
+ get didPropertiesChange() {
2045
+ try {
2046
+ if (JSON.stringify(this.#propertyStore) !== JSON.stringify(this.#lastPropertyStore)) {
2047
+ this.#lastPropertyStore = structuredClone(this.#propertyStore);
2048
+ return true;
2049
+ }
2050
+ } catch { }
2051
+ return false;
2052
+ }
2053
+
2054
+ // Get state (can be imported using importState, converts the maps to arrays for serialization)
2055
+ get getState() {
2056
+ return {
2057
+ keyOperations: [...this.#keyOperations.entries()],
2058
+ vectorClock: [...this.#vectorClock.entries()]
2059
+ };
2060
+ }
2061
+ }
2074
2062
 
2075
2063
  /**
2076
2064
  * PlaySocket - WebSocket-based multiplayer for games
@@ -2128,7 +2116,7 @@ class PlaySocket {
2128
2116
  if (options.endpoint) this.#endpoint = options.endpoint;
2129
2117
  if (options.customData) this.#customData = { ...options.customData };
2130
2118
  if (options.debug) this.#debug = true; // Enabling extra logging
2131
- this.#crdtManager = new crdtManagerExports.CRDTManager(this.#debug);
2119
+ this.#crdtManager = new CRDTManager(this.#debug);
2132
2120
  }
2133
2121
 
2134
2122
  /**
@@ -2301,10 +2289,10 @@ class PlaySocket {
2301
2289
  case 'room_created':
2302
2290
  if (this.#pendingCreate) {
2303
2291
  this.#inRoom = true;
2304
- this.#triggerEvent("status", `Room created${this.#pendingCreate.maxSize ? ` with max size ${this.#pendingCreate.maxSize}.` : '.'}`);
2292
+ this.#triggerEvent("status", `Room created with max. size ${message.size}.`);
2305
2293
  this.#crdtManager.importState(message.state);
2306
2294
  this.#triggerEvent("storageUpdated", this.getStorage);
2307
- this.#pendingCreate.resolve(this.#pendingCreate.id);
2295
+ this.#pendingCreate.resolve(message.roomId);
2308
2296
  }
2309
2297
  break;
2310
2298
 
@@ -2456,7 +2444,7 @@ class PlaySocket {
2456
2444
  return Promise.race([
2457
2445
  new Promise((resolve, reject) => {
2458
2446
  this.#roomHost = this.#id;
2459
- this.#pendingCreate = { maxSize, id: this.#id, resolve, reject };
2447
+ this.#pendingCreate = { resolve, reject };
2460
2448
  this.#sendToServer({
2461
2449
  type: 'create_room',
2462
2450
  initialStorage,