playsocketjs 5.3.0 → 5.3.2
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/package.json
CHANGED
package/src/server/server.js
CHANGED
|
@@ -213,9 +213,11 @@ export default class PlaySocketServer {
|
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
case "reconnect": {
|
|
216
|
-
// If user is pending disconnect, respond (otherwise it's too late)
|
|
217
|
-
const
|
|
218
|
-
|
|
216
|
+
// If user is pending disconnect or still on a dropped connection, respond (otherwise it's too late)
|
|
217
|
+
const pendingDisconnect = this.#pendingDisconnects.get(data.id);
|
|
218
|
+
const staleSocket = this.#clients.get(data.id); // Client requests reconnect even though the server thinks they are still connected
|
|
219
|
+
|
|
220
|
+
if ((pendingDisconnect || staleSocket) && data.sessionToken) {
|
|
219
221
|
if (data.sessionToken !== this.#clientTokens.get(data.id)) {
|
|
220
222
|
ws.send(encode({
|
|
221
223
|
type: "reconnection_failed",
|
|
@@ -223,9 +225,15 @@ export default class PlaySocketServer {
|
|
|
223
225
|
}), { binary: true });
|
|
224
226
|
return;
|
|
225
227
|
}
|
|
226
|
-
clearTimeout(
|
|
228
|
+
clearTimeout(pendingDisconnect?.timeout);
|
|
227
229
|
this.#pendingDisconnects.delete(data.id);
|
|
228
230
|
|
|
231
|
+
// Only terminate if it's a different connection sending the reconnect
|
|
232
|
+
if (staleSocket && staleSocket !== ws) {
|
|
233
|
+
staleSocket.clientId = null; // So the close skips disconnection (handleDisconnection exits if the client ID is falsy)
|
|
234
|
+
staleSocket.terminate();
|
|
235
|
+
}
|
|
236
|
+
|
|
229
237
|
// Re-assign old client ID to the new ws connection
|
|
230
238
|
ws.clientId = data.id;
|
|
231
239
|
this.#clients.set(data.id, ws);
|
|
@@ -325,7 +333,8 @@ export default class PlaySocketServer {
|
|
|
325
333
|
|
|
326
334
|
if (room && data.update) {
|
|
327
335
|
// Check if update is allowed via event callback (provide clone to ensure update integrity)
|
|
328
|
-
|
|
336
|
+
// Must be synchronous so that messages that follow in the same read from the socket (which can flush multiple at once) get processed reliably in order (await would schedule to a new microtask, so the order can fall apart)
|
|
337
|
+
const updateAllowed = this.#triggerEventSync("storageUpdateRequested", { roomId, clientId: ws.clientId, update: structuredClone(data.update), storage: this.getRoomStorage(roomId) });
|
|
329
338
|
if (updateAllowed === false || typeof updateAllowed === "string") {
|
|
330
339
|
ws.send(encode({
|
|
331
340
|
type: "property_update_rejected",
|
|
@@ -416,9 +425,7 @@ export default class PlaySocketServer {
|
|
|
416
425
|
* @returns {string} Token
|
|
417
426
|
*/
|
|
418
427
|
#generateSessionToken() {
|
|
419
|
-
|
|
420
|
-
for (let i = 0; i < 16; i++) token += Math.floor(Math.random() * 16).toString(16);
|
|
421
|
-
return token;
|
|
428
|
+
return crypto.randomUUID();
|
|
422
429
|
}
|
|
423
430
|
|
|
424
431
|
/**
|
|
@@ -519,20 +526,41 @@ export default class PlaySocketServer {
|
|
|
519
526
|
* @returns {Promise<any>} The first non-null callback return value if present (all callbacks run, a throwing callback counts as returning false), otherwise true
|
|
520
527
|
*/
|
|
521
528
|
async #triggerEvent(event, ...args) {
|
|
522
|
-
const syncOnlyEvents = ["storageUpdateRequested"]; // Async storage validation could mess up GC and would lead to potentially poor UX (slow sync)
|
|
523
529
|
const callbacks = this.#callbacks.get(event);
|
|
524
|
-
if (!callbacks) return;
|
|
530
|
+
if (!callbacks) return true;
|
|
531
|
+
|
|
532
|
+
let firstResult;
|
|
533
|
+
for (const callback of [...callbacks]) {
|
|
534
|
+
try {
|
|
535
|
+
let result = callback(...args);
|
|
536
|
+
if (typeof result?.then === "function") result = await result;
|
|
537
|
+
if (firstResult == null && result != null) firstResult = result;
|
|
538
|
+
} catch (error) {
|
|
539
|
+
console.error(ERROR_PREFIX + `${event} callback error:`, error);
|
|
540
|
+
if (firstResult == null) firstResult = false; // Fail closed (false blocks for most events)
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return firstResult ?? true;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Trigger an event to registered synchronous callbacks (e.g. storage validation, async validation could mess up GC and lead to poor UX)
|
|
548
|
+
* @param {string} event - Event name
|
|
549
|
+
* @param {...*} args - Arguments
|
|
550
|
+
* @returns {any} The first non-null callback return value if present (all callbacks run, a throwing callback counts as returning false, an async one as a rejection), otherwise true
|
|
551
|
+
*/
|
|
552
|
+
#triggerEventSync(event, ...args) {
|
|
553
|
+
const callbacks = this.#callbacks.get(event);
|
|
554
|
+
if (!callbacks) return true;
|
|
525
555
|
|
|
526
556
|
let firstResult;
|
|
527
557
|
for (const callback of [...callbacks]) {
|
|
528
558
|
try {
|
|
529
559
|
let result = callback(...args);
|
|
530
560
|
if (typeof result?.then === "function") {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
result = `${event} callback must be synchronous.`; // Rejection string to explicitly block this practice
|
|
535
|
-
} else result = await result;
|
|
561
|
+
result.catch(() => { });
|
|
562
|
+
console.error(ERROR_PREFIX + `${event} callbacks must be synchronous`);
|
|
563
|
+
result = `${event} callback must be synchronous`; // Rejection string to explicitly block this practice
|
|
536
564
|
}
|
|
537
565
|
if (firstResult == null && result != null) firstResult = result;
|
|
538
566
|
} catch (error) {
|
|
@@ -451,8 +451,8 @@ export default class CRDTManager {
|
|
|
451
451
|
try {
|
|
452
452
|
return structuredClone(this.#propertyStore); // Return deep clone
|
|
453
453
|
} catch {
|
|
454
|
-
console.error(CONSOLE_PREFIX + "Failed to parse property store")
|
|
455
|
-
return {}
|
|
454
|
+
console.error(CONSOLE_PREFIX + "Failed to parse property store");
|
|
455
|
+
return {};
|
|
456
456
|
}
|
|
457
457
|
}
|
|
458
458
|
|
|
@@ -475,7 +475,7 @@ export default class CRDTManager {
|
|
|
475
475
|
return true;
|
|
476
476
|
}
|
|
477
477
|
} catch {
|
|
478
|
-
console.error(CONSOLE_PREFIX + "Failed to stringify and compare property stores")
|
|
478
|
+
console.error(CONSOLE_PREFIX + "Failed to stringify and compare property stores");
|
|
479
479
|
}
|
|
480
480
|
return false;
|
|
481
481
|
}
|