playsocketjs 1.3.0 → 1.4.0
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 +4 -1
- package/dist/playsocket-client.js +32 -30
- package/dist/playsocket-server.js +80 -48
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -206,8 +206,11 @@ Creates a new PlaySocket Server instance with configuration options.
|
|
|
206
206
|
|
|
207
207
|
- `clientRegistered`: Client registered with the server (returns the client's id `string`, customData `object`)
|
|
208
208
|
- `clientDisconnected`: Client disconnected from the server (returns the client's id `string`)
|
|
209
|
+
- `clientJoinedRoom`: Client joined a room – note that clients can only leave by disconnecting (returns the client's id `string`, room id `string`)
|
|
209
210
|
- `roomCreated`: Client created a room (returns room id `string`)
|
|
210
|
-
- `
|
|
211
|
+
- `roomDestroyed`: Room was destroyed, this happens when all participants leave (returns room id `string`)
|
|
212
|
+
- `roomCreationRequested`: Room creation requested by client (returns `object` containing the client's id `string`, room id `string` and the initialStorage `object`) – if you return an object, it will take that as the initial storage instead
|
|
213
|
+
- `storageUpdateRequested`: Room storage property update requested by client (returns `object` containing the client's id `string`, room id `string` and the update `object`) – if you return `false` in the callback, the update will be blocked
|
|
211
214
|
|
|
212
215
|
### Properties (Read-only)
|
|
213
216
|
|
|
@@ -1791,19 +1791,19 @@ function requireCrdtManager () {
|
|
|
1791
1791
|
/**
|
|
1792
1792
|
* Update a property
|
|
1793
1793
|
* @param {string} key
|
|
1794
|
-
* @param {string}
|
|
1794
|
+
* @param {string} type
|
|
1795
1795
|
* @param {*} value
|
|
1796
1796
|
* @param {*} updateValue
|
|
1797
1797
|
* @returns {Object} - Returns the property update
|
|
1798
1798
|
*/
|
|
1799
|
-
updateProperty(key,
|
|
1799
|
+
updateProperty(key, type, value, updateValue) {
|
|
1800
1800
|
try {
|
|
1801
1801
|
// Sanitize inputs
|
|
1802
1802
|
value = this.#sanitizeValue(value);
|
|
1803
1803
|
updateValue = this.#sanitizeValue(updateValue);
|
|
1804
1804
|
|
|
1805
1805
|
// Debug log
|
|
1806
|
-
if (this.#debug) console.log(CONSOLE_PREFIX + `Updating property with key ${key},
|
|
1806
|
+
if (this.#debug) console.log(CONSOLE_PREFIX + `Updating property with key ${key}, type ${type}, value ${value} and updateValue ${updateValue}.`);
|
|
1807
1807
|
|
|
1808
1808
|
// Increment vector clock
|
|
1809
1809
|
const counter = this.#vectorClock.get(this.#replicaId) || 0;
|
|
@@ -1813,7 +1813,7 @@ function requireCrdtManager () {
|
|
|
1813
1813
|
const currentOps = [...(this.#keyOperations.get(key) || [])];
|
|
1814
1814
|
|
|
1815
1815
|
// Add operation
|
|
1816
|
-
const newOp = this.#createOperation({
|
|
1816
|
+
const newOp = this.#createOperation({ type, value, updateValue }, Array.from(this.#vectorClock.entries()));
|
|
1817
1817
|
currentOps.push(newOp);
|
|
1818
1818
|
this.#keyOperations.set(key, currentOps); // Update the operations (no need to sort via vector clock since local updates are always the latest)
|
|
1819
1819
|
this.#processLocalProperty(key); // Process local value
|
|
@@ -1853,25 +1853,25 @@ function requireCrdtManager () {
|
|
|
1853
1853
|
|
|
1854
1854
|
if (retainCount < operations.length) {
|
|
1855
1855
|
if (this.#debug) console.log(CONSOLE_PREFIX + `Running garbage collection for key ${key} with current operations:`, operations);
|
|
1856
|
-
const retainOps = operations.slice(-retainCount); //
|
|
1857
|
-
const removeOps = operations.slice(0, -retainCount); //
|
|
1856
|
+
const retainOps = operations.slice(-retainCount); // Newest ops
|
|
1857
|
+
const removeOps = operations.slice(0, -retainCount); // Oldest ops (start = idx 0, end = retainCount counted from right side)
|
|
1858
1858
|
const baselineVectorClock = removeOps[removeOps.length - 1]?.vectorClock || []; // Use the vector clock from the last operation that we remove/overwrite
|
|
1859
1859
|
|
|
1860
1860
|
// Calculate the value at the point where retained operations start
|
|
1861
1861
|
let baselineValue = null;
|
|
1862
1862
|
for (const op of removeOps) {
|
|
1863
1863
|
if (!op.data) continue;
|
|
1864
|
-
if (op.data.
|
|
1864
|
+
if (op.data.type.startsWith('array') && !Array.isArray(baselineValue)) baselineValue = []; // Initialize array if required
|
|
1865
1865
|
baselineValue = this.#handleOperation(
|
|
1866
1866
|
baselineValue,
|
|
1867
|
-
op.data.
|
|
1867
|
+
op.data.type,
|
|
1868
1868
|
op.data.value,
|
|
1869
1869
|
op.data.updateValue
|
|
1870
1870
|
);
|
|
1871
1871
|
}
|
|
1872
1872
|
|
|
1873
1873
|
// Create a compact operation with baseline value and appropriate vector clock
|
|
1874
|
-
const compactOp = this.#createOperation({
|
|
1874
|
+
const compactOp = this.#createOperation({ type: "set", value: baselineValue }, baselineVectorClock);
|
|
1875
1875
|
|
|
1876
1876
|
// Debug log
|
|
1877
1877
|
if (this.#debug) console.log(CONSOLE_PREFIX + "Operations after garbage collection for this key:", [compactOp, ...retainOps]);
|
|
@@ -1913,10 +1913,10 @@ function requireCrdtManager () {
|
|
|
1913
1913
|
|
|
1914
1914
|
for (const op of ops) {
|
|
1915
1915
|
if (!op.data) continue;
|
|
1916
|
-
if (op.data.
|
|
1916
|
+
if (op.data.type.startsWith('array') && !Array.isArray(value)) value = []; // Initialize array if required
|
|
1917
1917
|
value = this.#handleOperation(
|
|
1918
1918
|
value,
|
|
1919
|
-
op.data.
|
|
1919
|
+
op.data.type,
|
|
1920
1920
|
op.data.value,
|
|
1921
1921
|
op.data.updateValue
|
|
1922
1922
|
);
|
|
@@ -1924,7 +1924,7 @@ function requireCrdtManager () {
|
|
|
1924
1924
|
|
|
1925
1925
|
this.#propertyStore[key] = value; // Save locally
|
|
1926
1926
|
} catch (error) {
|
|
1927
|
-
console.error(CONSOLE_PREFIX + `Failed to process property for
|
|
1927
|
+
console.error(CONSOLE_PREFIX + `Failed to process property for key ${key}:`, error);
|
|
1928
1928
|
}
|
|
1929
1929
|
}
|
|
1930
1930
|
|
|
@@ -1967,33 +1967,31 @@ function requireCrdtManager () {
|
|
|
1967
1967
|
/**
|
|
1968
1968
|
* Handle an operation
|
|
1969
1969
|
* @param {*} curValue
|
|
1970
|
-
* @param {string}
|
|
1970
|
+
* @param {string} type
|
|
1971
1971
|
* @param {*} value
|
|
1972
1972
|
* @param {*} [updateValue]
|
|
1973
1973
|
* @returns {*} - Value after the operation
|
|
1974
1974
|
*/
|
|
1975
|
-
#handleOperation(curValue,
|
|
1975
|
+
#handleOperation(curValue, type, value, updateValue) {
|
|
1976
1976
|
try {
|
|
1977
|
-
//
|
|
1978
|
-
curValue =
|
|
1977
|
+
// Deep copy to avoid reference issues in case value is or contains object(s)
|
|
1978
|
+
curValue = structuredClone(curValue);
|
|
1979
1979
|
|
|
1980
1980
|
// Set operation
|
|
1981
|
-
if (
|
|
1981
|
+
if (type === "set") return value;
|
|
1982
1982
|
|
|
1983
1983
|
// Array operations
|
|
1984
|
-
if (
|
|
1984
|
+
if (type.startsWith('array')) {
|
|
1985
1985
|
if (!Array.isArray(curValue)) curValue = []; // Auto-convert to array if current value isn't one
|
|
1986
1986
|
|
|
1987
1987
|
// Comparison function
|
|
1988
1988
|
const isObject = typeof value === 'object' && value !== null;
|
|
1989
1989
|
const compare = (item) => {
|
|
1990
|
-
if (isObject && typeof item === 'object' && item !== null)
|
|
1991
|
-
return JSON.stringify(item) === JSON.stringify(value); // Deep comparison
|
|
1992
|
-
}
|
|
1990
|
+
if (isObject && typeof item === 'object' && item !== null) return JSON.stringify(item) === JSON.stringify(value); // Deep comparison
|
|
1993
1991
|
return item === value; // Value comparison
|
|
1994
1992
|
};
|
|
1995
1993
|
|
|
1996
|
-
switch (
|
|
1994
|
+
switch (type) {
|
|
1997
1995
|
case 'array-add':
|
|
1998
1996
|
curValue.push(value);
|
|
1999
1997
|
break;
|
|
@@ -2013,7 +2011,7 @@ function requireCrdtManager () {
|
|
|
2013
2011
|
return curValue;
|
|
2014
2012
|
|
|
2015
2013
|
} catch (error) {
|
|
2016
|
-
console.error(CONSOLE_PREFIX + `Failed to
|
|
2014
|
+
console.error(CONSOLE_PREFIX + `Failed to handle operation with curValue ${curValue}:`, error);
|
|
2017
2015
|
return curValue;
|
|
2018
2016
|
}
|
|
2019
2017
|
}
|
|
@@ -2033,7 +2031,7 @@ function requireCrdtManager () {
|
|
|
2033
2031
|
// Get property store
|
|
2034
2032
|
get getPropertyStore() {
|
|
2035
2033
|
try {
|
|
2036
|
-
return
|
|
2034
|
+
return structuredClone(this.#propertyStore); // Return deep clone
|
|
2037
2035
|
} catch {
|
|
2038
2036
|
console.error(CONSOLE_PREFIX + "Failed to parse property store");
|
|
2039
2037
|
return {};
|
|
@@ -2044,7 +2042,7 @@ function requireCrdtManager () {
|
|
|
2044
2042
|
get didPropertiesChange() {
|
|
2045
2043
|
try {
|
|
2046
2044
|
if (JSON.stringify(this.#propertyStore) !== JSON.stringify(this.#lastPropertyStore)) {
|
|
2047
|
-
this.#lastPropertyStore =
|
|
2045
|
+
this.#lastPropertyStore = structuredClone(this.#propertyStore);
|
|
2048
2046
|
return true;
|
|
2049
2047
|
}
|
|
2050
2048
|
} catch { }
|
|
@@ -2280,6 +2278,8 @@ class PlaySocket {
|
|
|
2280
2278
|
if (this.#pendingCreate) {
|
|
2281
2279
|
this.#inRoom = true;
|
|
2282
2280
|
this.#triggerEvent("status", `Room created${this.#pendingCreate.maxSize ? ` with max size ${this.#pendingCreate.maxSize}.` : '.'}`);
|
|
2281
|
+
this.#crdtManager.importState(message.state);
|
|
2282
|
+
this.#triggerEvent("storageUpdated", this.getStorage);
|
|
2283
2283
|
this.#pendingCreate.resolve(this.#pendingCreate.id);
|
|
2284
2284
|
}
|
|
2285
2285
|
break;
|
|
@@ -2318,6 +2318,12 @@ class PlaySocket {
|
|
|
2318
2318
|
}
|
|
2319
2319
|
break;
|
|
2320
2320
|
|
|
2321
|
+
case 'property_update_rejected':
|
|
2322
|
+
this.#triggerEvent("error", "Property update rejected. Re-syncing state.");
|
|
2323
|
+
this.#crdtManager.importState(message.state);
|
|
2324
|
+
this.#triggerEvent("storageUpdated", this.getStorage);
|
|
2325
|
+
break;
|
|
2326
|
+
|
|
2321
2327
|
case 'server_stopped':
|
|
2322
2328
|
this.#triggerEvent("error", "Server restart.");
|
|
2323
2329
|
this.destroy();
|
|
@@ -2441,17 +2447,13 @@ class PlaySocket {
|
|
|
2441
2447
|
|
|
2442
2448
|
return Promise.race([
|
|
2443
2449
|
new Promise((resolve, reject) => {
|
|
2444
|
-
Object.entries(initialStorage)?.forEach(([key, value]) => {
|
|
2445
|
-
this.#crdtManager.updateProperty(key, "set", value);
|
|
2446
|
-
});
|
|
2447
2450
|
this.#roomHost = this.#id;
|
|
2448
2451
|
this.#pendingCreate = { maxSize, id: this.#id, resolve, reject };
|
|
2449
2452
|
this.#sendToServer({
|
|
2450
2453
|
type: 'create_room',
|
|
2451
|
-
|
|
2454
|
+
initialStorage,
|
|
2452
2455
|
size: maxSize
|
|
2453
2456
|
});
|
|
2454
|
-
this.#triggerEvent("storageUpdated", this.getStorage);
|
|
2455
2457
|
}),
|
|
2456
2458
|
this.#createTimeout("Room creation")
|
|
2457
2459
|
]).finally(() => {
|
|
@@ -1917,19 +1917,19 @@ function requireCrdtManager () {
|
|
|
1917
1917
|
/**
|
|
1918
1918
|
* Update a property
|
|
1919
1919
|
* @param {string} key
|
|
1920
|
-
* @param {string}
|
|
1920
|
+
* @param {string} type
|
|
1921
1921
|
* @param {*} value
|
|
1922
1922
|
* @param {*} updateValue
|
|
1923
1923
|
* @returns {Object} - Returns the property update
|
|
1924
1924
|
*/
|
|
1925
|
-
updateProperty(key,
|
|
1925
|
+
updateProperty(key, type, value, updateValue) {
|
|
1926
1926
|
try {
|
|
1927
1927
|
// Sanitize inputs
|
|
1928
1928
|
value = this.#sanitizeValue(value);
|
|
1929
1929
|
updateValue = this.#sanitizeValue(updateValue);
|
|
1930
1930
|
|
|
1931
1931
|
// Debug log
|
|
1932
|
-
if (this.#debug) console.log(CONSOLE_PREFIX + `Updating property with key ${key},
|
|
1932
|
+
if (this.#debug) console.log(CONSOLE_PREFIX + `Updating property with key ${key}, type ${type}, value ${value} and updateValue ${updateValue}.`);
|
|
1933
1933
|
|
|
1934
1934
|
// Increment vector clock
|
|
1935
1935
|
const counter = this.#vectorClock.get(this.#replicaId) || 0;
|
|
@@ -1939,7 +1939,7 @@ function requireCrdtManager () {
|
|
|
1939
1939
|
const currentOps = [...(this.#keyOperations.get(key) || [])];
|
|
1940
1940
|
|
|
1941
1941
|
// Add operation
|
|
1942
|
-
const newOp = this.#createOperation({
|
|
1942
|
+
const newOp = this.#createOperation({ type, value, updateValue }, Array.from(this.#vectorClock.entries()));
|
|
1943
1943
|
currentOps.push(newOp);
|
|
1944
1944
|
this.#keyOperations.set(key, currentOps); // Update the operations (no need to sort via vector clock since local updates are always the latest)
|
|
1945
1945
|
this.#processLocalProperty(key); // Process local value
|
|
@@ -1979,25 +1979,25 @@ function requireCrdtManager () {
|
|
|
1979
1979
|
|
|
1980
1980
|
if (retainCount < operations.length) {
|
|
1981
1981
|
if (this.#debug) console.log(CONSOLE_PREFIX + `Running garbage collection for key ${key} with current operations:`, operations);
|
|
1982
|
-
const retainOps = operations.slice(-retainCount); //
|
|
1983
|
-
const removeOps = operations.slice(0, -retainCount); //
|
|
1982
|
+
const retainOps = operations.slice(-retainCount); // Newest ops
|
|
1983
|
+
const removeOps = operations.slice(0, -retainCount); // Oldest ops (start = idx 0, end = retainCount counted from right side)
|
|
1984
1984
|
const baselineVectorClock = removeOps[removeOps.length - 1]?.vectorClock || []; // Use the vector clock from the last operation that we remove/overwrite
|
|
1985
1985
|
|
|
1986
1986
|
// Calculate the value at the point where retained operations start
|
|
1987
1987
|
let baselineValue = null;
|
|
1988
1988
|
for (const op of removeOps) {
|
|
1989
1989
|
if (!op.data) continue;
|
|
1990
|
-
if (op.data.
|
|
1990
|
+
if (op.data.type.startsWith('array') && !Array.isArray(baselineValue)) baselineValue = []; // Initialize array if required
|
|
1991
1991
|
baselineValue = this.#handleOperation(
|
|
1992
1992
|
baselineValue,
|
|
1993
|
-
op.data.
|
|
1993
|
+
op.data.type,
|
|
1994
1994
|
op.data.value,
|
|
1995
1995
|
op.data.updateValue
|
|
1996
1996
|
);
|
|
1997
1997
|
}
|
|
1998
1998
|
|
|
1999
1999
|
// Create a compact operation with baseline value and appropriate vector clock
|
|
2000
|
-
const compactOp = this.#createOperation({
|
|
2000
|
+
const compactOp = this.#createOperation({ type: "set", value: baselineValue }, baselineVectorClock);
|
|
2001
2001
|
|
|
2002
2002
|
// Debug log
|
|
2003
2003
|
if (this.#debug) console.log(CONSOLE_PREFIX + "Operations after garbage collection for this key:", [compactOp, ...retainOps]);
|
|
@@ -2039,10 +2039,10 @@ function requireCrdtManager () {
|
|
|
2039
2039
|
|
|
2040
2040
|
for (const op of ops) {
|
|
2041
2041
|
if (!op.data) continue;
|
|
2042
|
-
if (op.data.
|
|
2042
|
+
if (op.data.type.startsWith('array') && !Array.isArray(value)) value = []; // Initialize array if required
|
|
2043
2043
|
value = this.#handleOperation(
|
|
2044
2044
|
value,
|
|
2045
|
-
op.data.
|
|
2045
|
+
op.data.type,
|
|
2046
2046
|
op.data.value,
|
|
2047
2047
|
op.data.updateValue
|
|
2048
2048
|
);
|
|
@@ -2050,7 +2050,7 @@ function requireCrdtManager () {
|
|
|
2050
2050
|
|
|
2051
2051
|
this.#propertyStore[key] = value; // Save locally
|
|
2052
2052
|
} catch (error) {
|
|
2053
|
-
console.error(CONSOLE_PREFIX + `Failed to process property for
|
|
2053
|
+
console.error(CONSOLE_PREFIX + `Failed to process property for key ${key}:`, error);
|
|
2054
2054
|
}
|
|
2055
2055
|
}
|
|
2056
2056
|
|
|
@@ -2093,33 +2093,31 @@ function requireCrdtManager () {
|
|
|
2093
2093
|
/**
|
|
2094
2094
|
* Handle an operation
|
|
2095
2095
|
* @param {*} curValue
|
|
2096
|
-
* @param {string}
|
|
2096
|
+
* @param {string} type
|
|
2097
2097
|
* @param {*} value
|
|
2098
2098
|
* @param {*} [updateValue]
|
|
2099
2099
|
* @returns {*} - Value after the operation
|
|
2100
2100
|
*/
|
|
2101
|
-
#handleOperation(curValue,
|
|
2101
|
+
#handleOperation(curValue, type, value, updateValue) {
|
|
2102
2102
|
try {
|
|
2103
|
-
//
|
|
2104
|
-
curValue =
|
|
2103
|
+
// Deep copy to avoid reference issues in case value is or contains object(s)
|
|
2104
|
+
curValue = structuredClone(curValue);
|
|
2105
2105
|
|
|
2106
2106
|
// Set operation
|
|
2107
|
-
if (
|
|
2107
|
+
if (type === "set") return value;
|
|
2108
2108
|
|
|
2109
2109
|
// Array operations
|
|
2110
|
-
if (
|
|
2110
|
+
if (type.startsWith('array')) {
|
|
2111
2111
|
if (!Array.isArray(curValue)) curValue = []; // Auto-convert to array if current value isn't one
|
|
2112
2112
|
|
|
2113
2113
|
// Comparison function
|
|
2114
2114
|
const isObject = typeof value === 'object' && value !== null;
|
|
2115
2115
|
const compare = (item) => {
|
|
2116
|
-
if (isObject && typeof item === 'object' && item !== null)
|
|
2117
|
-
return JSON.stringify(item) === JSON.stringify(value); // Deep comparison
|
|
2118
|
-
}
|
|
2116
|
+
if (isObject && typeof item === 'object' && item !== null) return JSON.stringify(item) === JSON.stringify(value); // Deep comparison
|
|
2119
2117
|
return item === value; // Value comparison
|
|
2120
2118
|
};
|
|
2121
2119
|
|
|
2122
|
-
switch (
|
|
2120
|
+
switch (type) {
|
|
2123
2121
|
case 'array-add':
|
|
2124
2122
|
curValue.push(value);
|
|
2125
2123
|
break;
|
|
@@ -2139,7 +2137,7 @@ function requireCrdtManager () {
|
|
|
2139
2137
|
return curValue;
|
|
2140
2138
|
|
|
2141
2139
|
} catch (error) {
|
|
2142
|
-
console.error(CONSOLE_PREFIX + `Failed to
|
|
2140
|
+
console.error(CONSOLE_PREFIX + `Failed to handle operation with curValue ${curValue}:`, error);
|
|
2143
2141
|
return curValue;
|
|
2144
2142
|
}
|
|
2145
2143
|
}
|
|
@@ -2159,7 +2157,7 @@ function requireCrdtManager () {
|
|
|
2159
2157
|
// Get property store
|
|
2160
2158
|
get getPropertyStore() {
|
|
2161
2159
|
try {
|
|
2162
|
-
return
|
|
2160
|
+
return structuredClone(this.#propertyStore); // Return deep clone
|
|
2163
2161
|
} catch {
|
|
2164
2162
|
console.error(CONSOLE_PREFIX + "Failed to parse property store");
|
|
2165
2163
|
return {};
|
|
@@ -2170,7 +2168,7 @@ function requireCrdtManager () {
|
|
|
2170
2168
|
get didPropertiesChange() {
|
|
2171
2169
|
try {
|
|
2172
2170
|
if (JSON.stringify(this.#propertyStore) !== JSON.stringify(this.#lastPropertyStore)) {
|
|
2173
|
-
this.#lastPropertyStore =
|
|
2171
|
+
this.#lastPropertyStore = structuredClone(this.#propertyStore);
|
|
2174
2172
|
return true;
|
|
2175
2173
|
}
|
|
2176
2174
|
} catch { }
|
|
@@ -2281,20 +2279,18 @@ function requireServer () {
|
|
|
2281
2279
|
* Handle WebSocket message
|
|
2282
2280
|
* @private
|
|
2283
2281
|
*/
|
|
2284
|
-
#handleMessage(ws, message) {
|
|
2282
|
+
async #handleMessage(ws, message) {
|
|
2285
2283
|
if (ws.isTerminating) return;
|
|
2286
2284
|
try {
|
|
2287
2285
|
const data = decode(message);
|
|
2288
2286
|
|
|
2289
2287
|
// Apply rate limiting to all connections (including unregistered)
|
|
2290
2288
|
if (!this.#checkRateLimit(ws.connectionId, data.type)) {
|
|
2291
|
-
|
|
2292
|
-
if (ws.rateLimitViolations > 5 && !ws.isTerminating) {
|
|
2289
|
+
if (!ws.isTerminating) {
|
|
2293
2290
|
ws.isTerminating = true; // Prevent multiple terminate calls (it is async)
|
|
2294
2291
|
ws.terminate();
|
|
2295
|
-
return console.error(`Connection ${ws.connectionId} terminated due to
|
|
2292
|
+
return console.error(`Connection ${ws.connectionId} terminated due to rate limit violations.`);
|
|
2296
2293
|
}
|
|
2297
|
-
return console.error(`Connection ${ws.connectionId} rate limit exceeded.`);
|
|
2298
2294
|
}
|
|
2299
2295
|
|
|
2300
2296
|
switch (data.type) {
|
|
@@ -2372,6 +2368,7 @@ function requireServer () {
|
|
|
2372
2368
|
case 'create_room':
|
|
2373
2369
|
if (!ws.clientId) return;
|
|
2374
2370
|
const newRoomId = ws.clientId;
|
|
2371
|
+
|
|
2375
2372
|
if (this.#clientRooms.get(ws.clientId) || this.#rooms[newRoomId]) {
|
|
2376
2373
|
ws.send(encode({
|
|
2377
2374
|
type: 'room_creation_failed',
|
|
@@ -2380,24 +2377,38 @@ function requireServer () {
|
|
|
2380
2377
|
return;
|
|
2381
2378
|
}
|
|
2382
2379
|
|
|
2380
|
+
const roomCrdtManager = new CRDTManager(this.#debug); // Create the room's crdt manager
|
|
2381
|
+
|
|
2382
|
+
// Event callback with potential initial storage modifications
|
|
2383
|
+
const reviewedStorage = await this.#triggerEvent("roomCreationRequested", { roomId: newRoomId, clientId: ws.clientId, initialStorage: structuredClone(data.initialStorage || {}) });
|
|
2384
|
+
if (typeof reviewedStorage === 'object') data.initialStorage = reviewedStorage;
|
|
2385
|
+
|
|
2386
|
+
// Check if client/creator is still connected
|
|
2387
|
+
if (!this.#clients.has(ws.clientId)) {
|
|
2388
|
+
if (this.#debug) console.log(`Room creation cancelled - client ${ws.clientId} disconnected during event callback.`);
|
|
2389
|
+
return; // Abort room creation if client is no longer connected
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
// Load initial storage if provided
|
|
2393
|
+
if (data.initialStorage) {
|
|
2394
|
+
Object.entries(data.initialStorage)?.forEach(([key, value]) => {
|
|
2395
|
+
roomCrdtManager.updateProperty(key, "set", value);
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2383
2399
|
// Create room
|
|
2384
2400
|
this.#roomVersions.set(newRoomId, 0); // Start with version 0
|
|
2385
2401
|
this.#rooms[newRoomId] = {
|
|
2386
2402
|
participants: [ws.clientId],
|
|
2387
2403
|
host: ws.clientId,
|
|
2388
2404
|
maxSize: data.size || null,
|
|
2389
|
-
crdtManager:
|
|
2405
|
+
crdtManager: roomCrdtManager
|
|
2390
2406
|
};
|
|
2391
|
-
|
|
2392
|
-
// Load state if provided
|
|
2393
|
-
if (data.state) {
|
|
2394
|
-
this.#rooms[newRoomId].crdtManager.importState(data.state);
|
|
2395
|
-
if (this.#debug) console.log("Room created with initial state:", data.state);
|
|
2396
|
-
}
|
|
2397
|
-
|
|
2398
2407
|
this.#clientRooms.set(ws.clientId, newRoomId); // Add client to their room
|
|
2399
|
-
|
|
2400
|
-
|
|
2408
|
+
|
|
2409
|
+
ws.send(encode({ type: 'room_created', state: roomCrdtManager.getState }), { binary: true });
|
|
2410
|
+
this.#triggerEvent("roomCreated", newRoomId);
|
|
2411
|
+
if (this.#debug) console.log("Room created with initial storage:", data.initialStorage);
|
|
2401
2412
|
break;
|
|
2402
2413
|
|
|
2403
2414
|
case 'join_room':
|
|
@@ -2442,7 +2453,7 @@ function requireServer () {
|
|
|
2442
2453
|
}), { binary: true });
|
|
2443
2454
|
}
|
|
2444
2455
|
});
|
|
2445
|
-
this.#triggerEvent("
|
|
2456
|
+
this.#triggerEvent("clientJoinedRoom", ws.clientId, roomId);
|
|
2446
2457
|
break;
|
|
2447
2458
|
|
|
2448
2459
|
case 'update_property':
|
|
@@ -2450,13 +2461,23 @@ function requireServer () {
|
|
|
2450
2461
|
const updateRoom = updateRoomId ? this.#rooms[updateRoomId] : null;
|
|
2451
2462
|
|
|
2452
2463
|
if (updateRoom && data.update) {
|
|
2453
|
-
|
|
2464
|
+
// Check if update is allowed via event callback (provide clone to ensure update integrity)
|
|
2465
|
+
const allowed = await this.#triggerEvent("storageUpdateRequested", { roomId: updateRoomId, clientId: ws.clientId, update: structuredClone(data.update) });
|
|
2466
|
+
if (allowed === false) {
|
|
2467
|
+
ws.send(encode({
|
|
2468
|
+
type: 'property_update_rejected',
|
|
2469
|
+
state: updateRoom.crdtManager.getState
|
|
2470
|
+
}), { binary: true });
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
updateRoom.crdtManager.importPropertyUpdate(data.update); // Import update into server state
|
|
2454
2475
|
|
|
2455
2476
|
// Increment version for this room
|
|
2456
2477
|
const currentVersion = this.#roomVersions.get(updateRoomId) + 1;
|
|
2457
2478
|
this.#roomVersions.set(updateRoomId, currentVersion);
|
|
2458
|
-
|
|
2459
2479
|
if (this.#debug) console.log("Property update received and imported:", data.update);
|
|
2480
|
+
|
|
2460
2481
|
updateRoom.participants?.forEach(p => {
|
|
2461
2482
|
const client = this.#clients.get(p);
|
|
2462
2483
|
if (client) {
|
|
@@ -2586,6 +2607,7 @@ function requireServer () {
|
|
|
2586
2607
|
if (room.participants?.length === 0) {
|
|
2587
2608
|
delete this.#rooms[roomId]; // Delete room if now empty
|
|
2588
2609
|
this.#roomVersions.delete(roomId); // Delete room version
|
|
2610
|
+
this.#triggerEvent("roomDestroyed", roomId);
|
|
2589
2611
|
if (this.#debug) console.log("Deleted room with id " + roomId + ".");
|
|
2590
2612
|
} else {
|
|
2591
2613
|
// Notify remaining participants
|
|
@@ -2609,7 +2631,7 @@ function requireServer () {
|
|
|
2609
2631
|
* @param {Function} callback - Callback function
|
|
2610
2632
|
*/
|
|
2611
2633
|
onEvent(event, callback) {
|
|
2612
|
-
const validEvents = ["clientRegistered", "clientDisconnected", "roomCreated", "
|
|
2634
|
+
const validEvents = ["clientRegistered", "clientDisconnected", "clientJoinedRoom", "roomCreationRequested", "roomCreated", "storageUpdateRequested", "roomDestroyed"];
|
|
2613
2635
|
if (!validEvents.includes(event)) return console.warn(`Invalid PlaySocket event type "${event}"`);
|
|
2614
2636
|
if (!this.#callbacks.has(event)) this.#callbacks.set(event, []);
|
|
2615
2637
|
this.#callbacks.get(event).push(callback);
|
|
@@ -2623,6 +2645,7 @@ function requireServer () {
|
|
|
2623
2645
|
kick(clientId, reason) {
|
|
2624
2646
|
const client = this.#clients.get(clientId);
|
|
2625
2647
|
if (client) {
|
|
2648
|
+
client.willfulDisconnect = true;
|
|
2626
2649
|
client.send(encode({ type: 'kicked', reason }), { binary: true });
|
|
2627
2650
|
client.close();
|
|
2628
2651
|
}
|
|
@@ -2632,11 +2655,20 @@ function requireServer () {
|
|
|
2632
2655
|
* Trigger an event to registered callbacks
|
|
2633
2656
|
* @private
|
|
2634
2657
|
*/
|
|
2635
|
-
#triggerEvent(event, ...args) {
|
|
2658
|
+
async #triggerEvent(event, ...args) {
|
|
2636
2659
|
const callbacks = this.#callbacks.get(event);
|
|
2637
|
-
callbacks
|
|
2638
|
-
|
|
2639
|
-
|
|
2660
|
+
if (!callbacks) return;
|
|
2661
|
+
|
|
2662
|
+
for (const callback of callbacks) {
|
|
2663
|
+
try {
|
|
2664
|
+
const result = await callback(...args);
|
|
2665
|
+
if (result === false) return false; // False = block (in supported events)
|
|
2666
|
+
if (typeof result === 'object') return result; // If response object is provided -> pass on
|
|
2667
|
+
} catch (error) {
|
|
2668
|
+
console.error(`PlaySocket ${event} callback error:`, error);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
return true;
|
|
2640
2672
|
}
|
|
2641
2673
|
|
|
2642
2674
|
/**
|