playsocketjs 4.2.1 → 4.3.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 +8 -10
- package/package.json +1 -1
- package/src/client/client.js +37 -54
- package/src/server/server.js +43 -43
- package/src/universal/constants.js +8 -1
- package/types/client/client.d.ts +2 -1
- package/types/universal/constants.d.ts +7 -1
package/README.md
CHANGED
|
@@ -43,8 +43,7 @@ const socket = new PlaySocket("unique-client-id", {
|
|
|
43
43
|
|
|
44
44
|
// Set up the event handlers you need
|
|
45
45
|
socket.onEvent("status", status => console.log(status));
|
|
46
|
-
socket.onEvent("
|
|
47
|
-
...
|
|
46
|
+
socket.onEvent("storageUpdated", storage => { /* ... */ });
|
|
48
47
|
|
|
49
48
|
const clientId = await socket.init(); // Connect
|
|
50
49
|
```
|
|
@@ -128,7 +127,7 @@ new PlaySocket(id?: string, options: PlaySocketOptions)
|
|
|
128
127
|
| `init()` | - | `Promise<string>` | Initialize the WebSocket connection, resolves with the client ID. |
|
|
129
128
|
| `createRoom()` | `initialStorage?: object, size?: number` | `Promise<string>` | Create a new room, resolves with the room ID. Max. 500 participants. |
|
|
130
129
|
| `joinRoom()` | `roomId: string` | `Promise<void>` | Join an existing room. |
|
|
131
|
-
| `destroy()` |
|
|
130
|
+
| `destroy()` | `reason?: string` | `void` | Leave room, close the connection, and destroy the instance. |
|
|
132
131
|
| `updateStorage()` | `key: string, type: string, value: any, secondValue?: any` | `void` | Update a key in the shared storage. |
|
|
133
132
|
| `sendRequest()` | `name: string, data?: any` | `Promise<void>` | Send a request to the server with optional attached data. |
|
|
134
133
|
| `onEvent()` | `event: string, callback: Function` | `() => void` | Register an event callback. Returns unsubscribe function. |
|
|
@@ -137,10 +136,9 @@ new PlaySocket(id?: string, options: PlaySocketOptions)
|
|
|
137
136
|
|
|
138
137
|
| Event | Callback parameter | Description |
|
|
139
138
|
|-------|-------------------|-------------|
|
|
140
|
-
| `status` | `status: string` | Connection or room status
|
|
141
|
-
| `error` | `error: string` | Error occurred. |
|
|
139
|
+
| `status` | `status: string` | Connection or room status changed, e.g. display in UI during joining or room creation. |
|
|
142
140
|
| `moved` | `roomId: string` | Moved to different room. |
|
|
143
|
-
| `instanceDestroyed` |
|
|
141
|
+
| `instanceDestroyed` | `reason: string` | Instance destroyed through `destroy()` or error. |
|
|
144
142
|
| `storageUpdated` | `storage: object` | Storage state changed. Does not trigger on no-op updates. |
|
|
145
143
|
| `hostMigrated` | `roomId: string` | Host was changed. |
|
|
146
144
|
| `clientJoined` | `clientId: string` | New client joined the room. |
|
|
@@ -196,7 +194,7 @@ const playSocketServer = new PlaySocketServer({
|
|
|
196
194
|
|
|
197
195
|
// Start the server
|
|
198
196
|
httpServer.listen(3000, () => {
|
|
199
|
-
console.log("Server running on port 3000");
|
|
197
|
+
console.log("Server running on port 3000.");
|
|
200
198
|
});
|
|
201
199
|
|
|
202
200
|
function shutdown() {
|
|
@@ -236,7 +234,7 @@ server.onEvent("requestReceived", async ({ roomId, clientId, name, data }) => {
|
|
|
236
234
|
const players = server.getRoomStorage(roomId)?.players || [];
|
|
237
235
|
|
|
238
236
|
// Returning false or string rejects sendRequest() on the client
|
|
239
|
-
if (players.find(p => p.id === clientId)) return "Player already added
|
|
237
|
+
if (players.find(p => p.id === clientId)) return "Player already added";
|
|
240
238
|
|
|
241
239
|
server.updateRoomStorage(roomId, "players", "array-add", { id: clientId, timestamp: Date.now() });
|
|
242
240
|
}
|
|
@@ -255,7 +253,7 @@ server.onEvent("clientRegistrationRequested", async (clientId, data) => {
|
|
|
255
253
|
// For example, data could contain a token
|
|
256
254
|
authedClients.push(clientId);
|
|
257
255
|
} catch (error) {
|
|
258
|
-
return "Error occurred during auth
|
|
256
|
+
return "Error occurred during auth"; // Blocks the registration
|
|
259
257
|
}
|
|
260
258
|
});
|
|
261
259
|
|
|
@@ -295,7 +293,7 @@ const server = new PlaySocketServer({
|
|
|
295
293
|
const forwarded = info.req.headers["x-forwarded-for"];
|
|
296
294
|
const ip = forwarded ? forwarded.split(",")[0].trim() : info.req.socket.remoteAddress;
|
|
297
295
|
if (isRateLimited(ip)) {
|
|
298
|
-
return callback(false, 429, "Too
|
|
296
|
+
return callback(false, 429, "Too many requests");
|
|
299
297
|
}
|
|
300
298
|
callback(true);
|
|
301
299
|
}
|
package/package.json
CHANGED
package/src/client/client.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { encode, decode } from "@msgpack/msgpack";
|
|
2
2
|
import CRDTManager from "../universal/crdtManager.js";
|
|
3
|
-
import { VERSION } from "../universal/constants.js";
|
|
3
|
+
import { VERSION, ERROR_PREFIX, WARNING_PREFIX, LOG_PREFIX } from "../universal/constants.js";
|
|
4
4
|
|
|
5
|
-
const ERROR_PREFIX = "PlaySocket error: ";
|
|
6
|
-
const WARNING_PREFIX = "PlaySocket warning: ";
|
|
7
|
-
const LOG_PREFIX = "PlaySocket log: ";
|
|
8
5
|
const TIMEOUT_MS = 3000; // 3 second timeout for WS messages
|
|
9
6
|
|
|
10
7
|
/** @typedef {import("../universal/crdtManager.js").PropertyUpdateType} PropertyUpdateType */
|
|
@@ -96,7 +93,7 @@ export default class PlaySocket {
|
|
|
96
93
|
*/
|
|
97
94
|
#createTimeout(name) {
|
|
98
95
|
return new Promise((_, reject) =>
|
|
99
|
-
setTimeout(() => reject(new Error(`${name} timed out
|
|
96
|
+
setTimeout(() => reject(new Error(`${name} timed out`)), TIMEOUT_MS)
|
|
100
97
|
);
|
|
101
98
|
}
|
|
102
99
|
|
|
@@ -107,9 +104,9 @@ export default class PlaySocket {
|
|
|
107
104
|
* @returns {Function} - Unsubscribe
|
|
108
105
|
*/
|
|
109
106
|
onEvent(event, callback) {
|
|
110
|
-
const validEvents = ["status", "
|
|
107
|
+
const validEvents = ["status", "moved", "instanceDestroyed", "storageUpdated", "hostMigrated", "clientJoined", "clientLeft"];
|
|
111
108
|
if (!validEvents.includes(event)) {
|
|
112
|
-
console.warn(WARNING_PREFIX + `Invalid event type
|
|
109
|
+
console.warn(WARNING_PREFIX + `Invalid event type ${event}`);
|
|
113
110
|
return () => { };
|
|
114
111
|
};
|
|
115
112
|
if (!this.#callbacks.has(event)) this.#callbacks.set(event, []);
|
|
@@ -145,9 +142,9 @@ export default class PlaySocket {
|
|
|
145
142
|
* @returns {Promise<string>} Resolves with the client ID when the connection is established
|
|
146
143
|
*/
|
|
147
144
|
async init() {
|
|
148
|
-
if (this.#initialized) throw new Error("Already initialized
|
|
149
|
-
if (!this.#endpoint) throw new Error("No websocket endpoint provided
|
|
150
|
-
this.#triggerEvent("status", "Initializing
|
|
145
|
+
if (this.#initialized) throw new Error("Already initialized");
|
|
146
|
+
if (!this.#endpoint) throw new Error("No websocket endpoint provided");
|
|
147
|
+
this.#triggerEvent("status", "Initializing.");
|
|
151
148
|
|
|
152
149
|
// Connect to WS server
|
|
153
150
|
await this.#connect();
|
|
@@ -205,8 +202,7 @@ export default class PlaySocket {
|
|
|
205
202
|
|
|
206
203
|
switch (message.type) {
|
|
207
204
|
case "registration_failed":
|
|
208
|
-
this.#
|
|
209
|
-
if (this.#pendingRegistration) this.#pendingRegistration.reject(new Error("Failed to register: " + (message.reason || "No reason provided.")));
|
|
205
|
+
if (this.#pendingRegistration) this.#pendingRegistration.reject(new Error("Failed to register: " + (message.reason || "No reason provided")));
|
|
210
206
|
break;
|
|
211
207
|
|
|
212
208
|
case "registered":
|
|
@@ -234,8 +230,8 @@ export default class PlaySocket {
|
|
|
234
230
|
break;
|
|
235
231
|
|
|
236
232
|
case "join_rejected":
|
|
237
|
-
this.#triggerEvent("status", "Failed to join room: " + (message.reason || "No reason provided
|
|
238
|
-
if (this.#pendingJoin) this.#pendingJoin.reject(new Error("Failed to join room: " + (message.reason || "No reason provided
|
|
233
|
+
this.#triggerEvent("status", "Failed to join room: " + (message.reason || "No reason provided"));
|
|
234
|
+
if (this.#pendingJoin) this.#pendingJoin.reject(new Error("Failed to join room: " + (message.reason || "No reason provided")));
|
|
239
235
|
break;
|
|
240
236
|
|
|
241
237
|
case "reconnected":
|
|
@@ -249,9 +245,8 @@ export default class PlaySocket {
|
|
|
249
245
|
this.#setHost(message.roomData.host); // Set host before in case there are .isHost checks in the storageUpdate fallback
|
|
250
246
|
this.#triggerEvent("storageUpdated", this.storage);
|
|
251
247
|
} else if (this.#inRoom) {
|
|
252
|
-
// If no room data received, but client thinks they
|
|
253
|
-
this
|
|
254
|
-
return this.destroy();
|
|
248
|
+
// If no room data was received, but client thinks they are in a room
|
|
249
|
+
return this.destroy("Reconnected, but room no longer exists.");
|
|
255
250
|
}
|
|
256
251
|
this.#triggerEvent("status", "Reconnected.");
|
|
257
252
|
if (this.#pendingReconnect) this.#pendingReconnect.resolve();
|
|
@@ -271,8 +266,7 @@ export default class PlaySocket {
|
|
|
271
266
|
break;
|
|
272
267
|
|
|
273
268
|
case "room_creation_failed":
|
|
274
|
-
this.#
|
|
275
|
-
if (this.#pendingCreate) this.#pendingCreate.reject(new Error("Failed to create room: " + (message.reason || "No reason provided.")));
|
|
269
|
+
if (this.#pendingCreate) this.#pendingCreate.reject(new Error("Failed to create room: " + (message.reason || "No reason provided")));
|
|
276
270
|
break;
|
|
277
271
|
|
|
278
272
|
case "property_updated":
|
|
@@ -281,20 +275,19 @@ export default class PlaySocket {
|
|
|
281
275
|
this.#crdtManager.importPropertyUpdate(message.update);
|
|
282
276
|
if (this.#crdtManager.didPropertiesChange) this.#triggerEvent("storageUpdated", this.storage);
|
|
283
277
|
if (this.#roomVersion != message.version && this.#initialized && this.#socket?.readyState === WebSocket.OPEN) {
|
|
284
|
-
|
|
278
|
+
console.error(ERROR_PREFIX + "Detected skipped property update, forcing reconnect");
|
|
285
279
|
this.#socket?.close();
|
|
286
280
|
}
|
|
287
281
|
break;
|
|
288
282
|
|
|
289
283
|
case "property_update_rejected":
|
|
290
|
-
|
|
284
|
+
console.warn(LOG_PREFIX + "Property update rejected: " + (message.reason || "No reason provided"));
|
|
291
285
|
this.#crdtManager.revertPropertyUpdate(message.update);
|
|
292
286
|
if (this.#crdtManager.didPropertiesChange) this.#triggerEvent("storageUpdated", this.storage);
|
|
293
287
|
break;
|
|
294
288
|
|
|
295
289
|
case "kicked":
|
|
296
|
-
this
|
|
297
|
-
this.destroy();
|
|
290
|
+
this.destroy(`Kicked out of room: ${message.reason || "No reason provided"}`);
|
|
298
291
|
break;
|
|
299
292
|
|
|
300
293
|
case "host_migrated":
|
|
@@ -318,7 +311,7 @@ export default class PlaySocket {
|
|
|
318
311
|
break;
|
|
319
312
|
|
|
320
313
|
case "request_failed":
|
|
321
|
-
this.#pendingRequests.get(message.request.uuid)?.reject(new Error(message.reason || "No reason provided
|
|
314
|
+
this.#pendingRequests.get(message.request.uuid)?.reject(new Error(message.reason || "No reason provided"));
|
|
322
315
|
break;
|
|
323
316
|
}
|
|
324
317
|
} catch (error) {
|
|
@@ -328,8 +321,7 @@ export default class PlaySocket {
|
|
|
328
321
|
|
|
329
322
|
// Handle socket errors
|
|
330
323
|
this.#socket.onerror = () => {
|
|
331
|
-
this.#
|
|
332
|
-
if (this.#pendingConnect) this.#pendingConnect.reject(new Error("WebSocket error!"));
|
|
324
|
+
if (this.#pendingConnect) this.#pendingConnect.reject(new Error("WebSocket error"));
|
|
333
325
|
}
|
|
334
326
|
|
|
335
327
|
// Handle socket close & attempt reconnect
|
|
@@ -347,12 +339,11 @@ export default class PlaySocket {
|
|
|
347
339
|
async #attemptReconnect() {
|
|
348
340
|
this.#reconnectCount++;
|
|
349
341
|
if (this.#reconnectCount > 9) {
|
|
350
|
-
this
|
|
351
|
-
this.destroy();
|
|
342
|
+
this.destroy("Disconnected, reconnection failed.");
|
|
352
343
|
return;
|
|
353
344
|
}
|
|
354
345
|
|
|
355
|
-
this.#triggerEvent("status", `
|
|
346
|
+
this.#triggerEvent("status", `Trying to reconnect: Attempt ${this.#reconnectCount}`);
|
|
356
347
|
this.#isReconnecting = true;
|
|
357
348
|
try {
|
|
358
349
|
await this.#connect();
|
|
@@ -393,14 +384,13 @@ export default class PlaySocket {
|
|
|
393
384
|
*/
|
|
394
385
|
#sendToServer(data) {
|
|
395
386
|
if (!this.#socket || this.#socket?.readyState !== WebSocket.OPEN) {
|
|
396
|
-
console.warn(WARNING_PREFIX + "Failed to send message
|
|
387
|
+
console.warn(WARNING_PREFIX + "Failed to send message, not connected");
|
|
397
388
|
return;
|
|
398
389
|
}
|
|
399
390
|
try {
|
|
400
391
|
this.#socket.send(encode(data));
|
|
401
392
|
} catch (error) {
|
|
402
393
|
console.error(ERROR_PREFIX + "Error sending message:", error);
|
|
403
|
-
this.#triggerEvent("error", "Error sending message: " + error.message);
|
|
404
394
|
}
|
|
405
395
|
}
|
|
406
396
|
|
|
@@ -411,10 +401,7 @@ export default class PlaySocket {
|
|
|
411
401
|
* @returns {Promise<string>} Resolves with room ID
|
|
412
402
|
*/
|
|
413
403
|
async createRoom(initialStorage = {}, size) {
|
|
414
|
-
if (!this.#initialized)
|
|
415
|
-
this.#triggerEvent("error", "Failed to create room - not initialized");
|
|
416
|
-
throw new Error("Not initialized!");
|
|
417
|
-
}
|
|
404
|
+
if (!this.#initialized) throw new Error("Not initialized");
|
|
418
405
|
|
|
419
406
|
return /** @type {Promise<string>} */ (Promise.race([
|
|
420
407
|
new Promise((resolve, reject) => {
|
|
@@ -438,17 +425,14 @@ export default class PlaySocket {
|
|
|
438
425
|
* @returns {Promise<void>} - Resolves when connected
|
|
439
426
|
*/
|
|
440
427
|
async joinRoom(roomId) {
|
|
441
|
-
if (!this.#initialized)
|
|
442
|
-
this.#triggerEvent("error", "Failed to join room - not initialized");
|
|
443
|
-
throw new Error("Not initialized!");
|
|
444
|
-
}
|
|
428
|
+
if (!this.#initialized) throw new Error("Not initialized");
|
|
445
429
|
|
|
446
430
|
return /** @type {Promise<void>} */ (Promise.race([
|
|
447
431
|
new Promise((resolve, reject) => {
|
|
448
432
|
this.#pendingJoin = { resolve, reject };
|
|
449
433
|
|
|
450
434
|
// Send connection request
|
|
451
|
-
this.#triggerEvent("status", `Connecting to room ${roomId}
|
|
435
|
+
this.#triggerEvent("status", `Connecting to room ${roomId}.`);
|
|
452
436
|
this.#sendToServer({
|
|
453
437
|
type: "join_room",
|
|
454
438
|
roomId
|
|
@@ -469,10 +453,10 @@ export default class PlaySocket {
|
|
|
469
453
|
*/
|
|
470
454
|
updateStorage(key, type, value, secondValue) {
|
|
471
455
|
if (!this.#inRoom) {
|
|
472
|
-
|
|
456
|
+
console.error(ERROR_PREFIX + "Failed to update storage, not in a room")
|
|
473
457
|
return;
|
|
474
458
|
};
|
|
475
|
-
if (this.#debug) console.log(LOG_PREFIX + `Property update for key ${key}, operation ${type}, value ${value} and secondValue ${secondValue}
|
|
459
|
+
if (this.#debug) console.log(LOG_PREFIX + `Property update for key ${key}, operation ${type}, value ${value} and secondValue ${secondValue}`);
|
|
476
460
|
const propUpdate = this.#crdtManager.updateProperty(key, type, value, secondValue);
|
|
477
461
|
this.#sendToServer({
|
|
478
462
|
type: "update_property",
|
|
@@ -488,10 +472,8 @@ export default class PlaySocket {
|
|
|
488
472
|
* @returns {Promise<void>} - Request promise
|
|
489
473
|
*/
|
|
490
474
|
async sendRequest(name, data) {
|
|
491
|
-
if (!this.#initialized)
|
|
492
|
-
|
|
493
|
-
throw new Error("Not initialized!");
|
|
494
|
-
}
|
|
475
|
+
if (!this.#initialized) throw new Error("Not initialized");
|
|
476
|
+
|
|
495
477
|
if (this.#debug) console.log(LOG_PREFIX + `Server request with name ${name} and data:`, data);
|
|
496
478
|
const uuid = crypto.randomUUID();
|
|
497
479
|
return /** @type {Promise<void>} */ (Promise.race([
|
|
@@ -511,8 +493,9 @@ export default class PlaySocket {
|
|
|
511
493
|
|
|
512
494
|
/**
|
|
513
495
|
* Destroy the PlaySocket instance and disconnect
|
|
496
|
+
* @param {string} [reason] - Optional reason
|
|
514
497
|
*/
|
|
515
|
-
destroy() {
|
|
498
|
+
destroy(reason) {
|
|
516
499
|
this.#initialized = false; // Set this immediately to prevent automatic reconnection
|
|
517
500
|
|
|
518
501
|
if (this.#socket) {
|
|
@@ -526,7 +509,7 @@ export default class PlaySocket {
|
|
|
526
509
|
|
|
527
510
|
// Trigger events
|
|
528
511
|
this.#triggerEvent("status", "Destroyed.");
|
|
529
|
-
this.#triggerEvent("instanceDestroyed");
|
|
512
|
+
this.#triggerEvent("instanceDestroyed", reason);
|
|
530
513
|
}
|
|
531
514
|
|
|
532
515
|
// Reset state
|
|
@@ -539,13 +522,13 @@ export default class PlaySocket {
|
|
|
539
522
|
this.#roomVersion = 0;
|
|
540
523
|
|
|
541
524
|
// Reject pending promises if currently active
|
|
542
|
-
if (this.#pendingJoin) this.#pendingJoin.reject(new Error("Destroyed
|
|
543
|
-
if (this.#pendingCreate) this.#pendingCreate.reject(new Error("Destroyed
|
|
544
|
-
if (this.#pendingRegistration) this.#pendingRegistration.reject(new Error("Destroyed
|
|
545
|
-
if (this.#pendingConnect) this.#pendingConnect.reject(new Error("Destroyed
|
|
546
|
-
if (this.#pendingReconnect) this.#pendingReconnect.reject(new Error("Destroyed
|
|
525
|
+
if (this.#pendingJoin) this.#pendingJoin.reject(new Error("Destroyed"));
|
|
526
|
+
if (this.#pendingCreate) this.#pendingCreate.reject(new Error("Destroyed"));
|
|
527
|
+
if (this.#pendingRegistration) this.#pendingRegistration.reject(new Error("Destroyed"));
|
|
528
|
+
if (this.#pendingConnect) this.#pendingConnect.reject(new Error("Destroyed"));
|
|
529
|
+
if (this.#pendingReconnect) this.#pendingReconnect.reject(new Error("Destroyed"));
|
|
547
530
|
this.#pendingRequests.forEach((request) => {
|
|
548
|
-
request.reject(new Error("Destroyed
|
|
531
|
+
request.reject(new Error("Destroyed"));
|
|
549
532
|
});
|
|
550
533
|
}
|
|
551
534
|
|
package/src/server/server.js
CHANGED
|
@@ -2,7 +2,7 @@ import { WebSocketServer } from "ws";
|
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
3
|
import { encode, decode } from "@msgpack/msgpack";
|
|
4
4
|
import CRDTManager, { getUpdateDetails } from "../universal/crdtManager.js";
|
|
5
|
-
import { HEARTBEAT_INTERVAL, VERSION } from "../universal/constants.js";
|
|
5
|
+
import { HEARTBEAT_INTERVAL, VERSION, ERROR_PREFIX, WARNING_PREFIX, LOG_PREFIX } from "../universal/constants.js";
|
|
6
6
|
|
|
7
7
|
const MAX_ROOM_SIZE = 500;
|
|
8
8
|
export const RECONNECT_GRACE_PERIOD = 5000; // Exported for use in tests
|
|
@@ -85,7 +85,7 @@ export default class PlaySocketServer {
|
|
|
85
85
|
this.#ownsServer = true;
|
|
86
86
|
this.#server = createServer(); // Create HTTP server and start it
|
|
87
87
|
this.#server.listen(port, () => {
|
|
88
|
-
console.log(`
|
|
88
|
+
console.log(LOG_PREFIX + `Server running on port ${port}`);
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -104,13 +104,13 @@ export default class PlaySocketServer {
|
|
|
104
104
|
ws.on("message", msg => this.#handleMessage(ws, msg));
|
|
105
105
|
ws.on("close", () => this.#handleDisconnection(ws));
|
|
106
106
|
ws.on("error", (error) => {
|
|
107
|
-
console.error(`
|
|
107
|
+
console.error(ERROR_PREFIX + `WebSocket connection error for client ${ws.clientId || "unknown"}:`, error); // Catch conn errors
|
|
108
108
|
});
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
// Log & catch server-level errors
|
|
112
112
|
this.#wss.on("error", (error) => {
|
|
113
|
-
console.error("
|
|
113
|
+
console.error(ERROR_PREFIX + "WebSocket server error:", error);
|
|
114
114
|
});
|
|
115
115
|
|
|
116
116
|
// Start heartbeat
|
|
@@ -138,7 +138,7 @@ export default class PlaySocketServer {
|
|
|
138
138
|
if (!ws.isTerminating) {
|
|
139
139
|
ws.isTerminating = true; // Prevent multiple terminate calls (it is async)
|
|
140
140
|
ws.terminate();
|
|
141
|
-
console.error(`
|
|
141
|
+
console.error(ERROR_PREFIX + `Connection ${ws.uuid} terminated due to rate limit violations`);
|
|
142
142
|
return;
|
|
143
143
|
}
|
|
144
144
|
}
|
|
@@ -147,13 +147,13 @@ export default class PlaySocketServer {
|
|
|
147
147
|
case "register": {
|
|
148
148
|
// Ensure client uses the same version as server
|
|
149
149
|
if (data.version !== VERSION) {
|
|
150
|
-
ws.send(encode({ type: "registration_failed", reason: `Version mismatch (client ${data.version ? "v" + data.version : "legacy"}, server v${VERSION})
|
|
150
|
+
ws.send(encode({ type: "registration_failed", reason: `Version mismatch (client ${data.version ? "v" + data.version : "legacy"}, server v${VERSION}), please reload the page` }), { binary: true });
|
|
151
151
|
return;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
// Register client ID if provided & check for a duplicate
|
|
155
155
|
if (data.id && (this.#clients.get(data.id) || data.id === "server")) {
|
|
156
|
-
ws.send(encode({ type: "registration_failed", reason: "ID is taken
|
|
156
|
+
ws.send(encode({ type: "registration_failed", reason: "ID is taken" }), { binary: true });
|
|
157
157
|
return;
|
|
158
158
|
}
|
|
159
159
|
|
|
@@ -167,8 +167,8 @@ export default class PlaySocketServer {
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
if (!data.id) {
|
|
170
|
-
ws.send(encode({ type: "registration_failed", reason: "No available ID found
|
|
171
|
-
throw new Error("Failed to generate unique ID
|
|
170
|
+
ws.send(encode({ type: "registration_failed", reason: "No available ID found" }), { binary: true });
|
|
171
|
+
throw new Error("Failed to generate unique ID");
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
@@ -198,7 +198,7 @@ export default class PlaySocketServer {
|
|
|
198
198
|
if (data.sessionToken !== this.#clientTokens.get(data.id)) {
|
|
199
199
|
ws.send(encode({
|
|
200
200
|
type: "reconnection_failed",
|
|
201
|
-
reason: "Session token does not match
|
|
201
|
+
reason: "Session token does not match"
|
|
202
202
|
}), { binary: true });
|
|
203
203
|
return;
|
|
204
204
|
}
|
|
@@ -216,7 +216,7 @@ export default class PlaySocketServer {
|
|
|
216
216
|
if (formerRoom) {
|
|
217
217
|
// If the room has no host (they were the only one and disconnected), restore them as host
|
|
218
218
|
if (formerRoom.host === null) formerRoom.host = data.id;
|
|
219
|
-
if (this.#debug) console.log(`State sent for reconnection for room ${formerRoomId}:`, formerRoom.crdtManager.state);
|
|
219
|
+
if (this.#debug) console.log(LOG_PREFIX + `State sent for reconnection for room ${formerRoomId}:`, formerRoom.crdtManager.state);
|
|
220
220
|
roomData = {
|
|
221
221
|
state: formerRoom.crdtManager.state,
|
|
222
222
|
participantCount: formerRoom.participants.length,
|
|
@@ -227,7 +227,7 @@ export default class PlaySocketServer {
|
|
|
227
227
|
|
|
228
228
|
ws.send(encode({ type: "reconnected", roomData }), { binary: true });
|
|
229
229
|
} else {
|
|
230
|
-
ws.send(encode({ type: "reconnection_failed", reason: "Client unknown to server
|
|
230
|
+
ws.send(encode({ type: "reconnection_failed", reason: "Client unknown to server" }), { binary: true });
|
|
231
231
|
}
|
|
232
232
|
break;
|
|
233
233
|
}
|
|
@@ -238,7 +238,7 @@ export default class PlaySocketServer {
|
|
|
238
238
|
if (this.#clientRooms.get(ws.clientId)) {
|
|
239
239
|
ws.send(encode({
|
|
240
240
|
type: "room_creation_failed",
|
|
241
|
-
reason: "Already in a room
|
|
241
|
+
reason: "Already in a room"
|
|
242
242
|
}), { binary: true });
|
|
243
243
|
return;
|
|
244
244
|
}
|
|
@@ -254,7 +254,7 @@ export default class PlaySocketServer {
|
|
|
254
254
|
try {
|
|
255
255
|
// Verify client is still connected and abort if not
|
|
256
256
|
if (!this.#clients.has(ws.clientId)) {
|
|
257
|
-
if (this.#debug) console.log(`Room creation cancelled
|
|
257
|
+
if (this.#debug) console.log(LOG_PREFIX + `Room creation cancelled, client ${ws.clientId} disconnected during event callback`);
|
|
258
258
|
return;
|
|
259
259
|
}
|
|
260
260
|
|
|
@@ -264,7 +264,7 @@ export default class PlaySocketServer {
|
|
|
264
264
|
this.#clientRooms.set(ws.clientId, newRoom.id); // Add client to the client-room map
|
|
265
265
|
ws.send(encode({ type: "room_created", state: newRoom.state, roomId: newRoom.id, participantCount: this.#rooms[newRoom.id].participants.length }), { binary: true });
|
|
266
266
|
} catch (error) {
|
|
267
|
-
console.error("
|
|
267
|
+
console.error(ERROR_PREFIX + "Error creating room:", error);
|
|
268
268
|
ws.send(encode({ type: "room_creation_failed", reason: error.message }), { binary: true });
|
|
269
269
|
}
|
|
270
270
|
break;
|
|
@@ -285,7 +285,7 @@ export default class PlaySocketServer {
|
|
|
285
285
|
|
|
286
286
|
// Verify client is still connected and abort if not
|
|
287
287
|
if (!this.#clients.has(ws.clientId)) {
|
|
288
|
-
if (this.#debug) console.log(`Room join cancelled
|
|
288
|
+
if (this.#debug) console.log(LOG_PREFIX + `Room join cancelled, client ${ws.clientId} disconnected during event callback`);
|
|
289
289
|
return;
|
|
290
290
|
}
|
|
291
291
|
|
|
@@ -332,7 +332,7 @@ export default class PlaySocketServer {
|
|
|
332
332
|
});
|
|
333
333
|
|
|
334
334
|
this.#triggerEvent("storageUpdated", { roomId, clientId: ws.clientId, update: structuredClone(data.update), storage: this.getRoomStorage(roomId) });
|
|
335
|
-
if (this.#debug) console.log("Property update received and imported:", data.update);
|
|
335
|
+
if (this.#debug) console.log(LOG_PREFIX + "Property update received and imported:", data.update);
|
|
336
336
|
}
|
|
337
337
|
break;
|
|
338
338
|
}
|
|
@@ -352,7 +352,7 @@ export default class PlaySocketServer {
|
|
|
352
352
|
break;
|
|
353
353
|
}
|
|
354
354
|
} catch (error) {
|
|
355
|
-
console.error("
|
|
355
|
+
console.error(ERROR_PREFIX + "Error in message handler:", error);
|
|
356
356
|
}
|
|
357
357
|
}
|
|
358
358
|
|
|
@@ -372,8 +372,8 @@ export default class PlaySocketServer {
|
|
|
372
372
|
*/
|
|
373
373
|
#migrateHost(roomId, clientId) {
|
|
374
374
|
const room = this.#rooms[roomId];
|
|
375
|
-
if (!room) throw new Error("Room not found
|
|
376
|
-
if (room.host !== clientId) throw new Error("Client is not the host
|
|
375
|
+
if (!room) throw new Error("Room not found");
|
|
376
|
+
if (room.host !== clientId) throw new Error("Client is not the host");
|
|
377
377
|
|
|
378
378
|
const remainingParticipants = room.participants.filter(p => p !== clientId);
|
|
379
379
|
|
|
@@ -476,7 +476,7 @@ export default class PlaySocketServer {
|
|
|
476
476
|
onEvent(event, callback) {
|
|
477
477
|
const validEvents = ["clientRegistered", "clientRegistrationRequested", "clientDisconnected", "clientJoinedRoom", "clientLeftRoom", "clientJoinRequested", "roomCreated", "roomCreationRequested", "requestReceived", "storageUpdated", "storageUpdateRequested", "roomDestroyed"];
|
|
478
478
|
if (!validEvents.includes(event)) {
|
|
479
|
-
console.warn(`Invalid
|
|
479
|
+
console.warn(WARNING_PREFIX + `Invalid event type ${event}`);
|
|
480
480
|
return () => { };
|
|
481
481
|
};
|
|
482
482
|
if (!this.#callbacks.has(event)) this.#callbacks.set(event, []);
|
|
@@ -508,13 +508,13 @@ export default class PlaySocketServer {
|
|
|
508
508
|
if (typeof result?.then === "function") {
|
|
509
509
|
if (syncOnlyEvents.includes(event)) {
|
|
510
510
|
result.catch(() => { });
|
|
511
|
-
console.error(
|
|
511
|
+
console.error(ERROR_PREFIX + `${event} callbacks must be synchronous`);
|
|
512
512
|
result = `${event} callback must be synchronous.`; // Rejection string to explicitly block this practice
|
|
513
513
|
} else result = await result;
|
|
514
514
|
}
|
|
515
515
|
if (firstResult == null && result != null) firstResult = result;
|
|
516
516
|
} catch (error) {
|
|
517
|
-
console.error(
|
|
517
|
+
console.error(ERROR_PREFIX + `${event} callback error:`, error);
|
|
518
518
|
if (firstResult == null) firstResult = false; // Fail closed (false blocks for most events)
|
|
519
519
|
}
|
|
520
520
|
}
|
|
@@ -544,12 +544,12 @@ export default class PlaySocketServer {
|
|
|
544
544
|
const oldRoomId = this.#clientRooms.get(clientId);
|
|
545
545
|
const oldRoom = this.#rooms[oldRoomId];
|
|
546
546
|
const targetRoom = this.#rooms[roomId];
|
|
547
|
-
if (!client && !this.#pendingDisconnects.has(clientId)) throw new Error("Client not found
|
|
548
|
-
if (!oldRoomId) throw new Error("Client is not in a room
|
|
549
|
-
if (!oldRoom) throw new Error("Client room not found
|
|
550
|
-
if (oldRoomId === roomId) throw new Error("Client is already in target room
|
|
551
|
-
if (!targetRoom) throw new Error("Target room not found
|
|
552
|
-
if (targetRoom.participants.length >= targetRoom.size) throw new Error("Target room is full
|
|
547
|
+
if (!client && !this.#pendingDisconnects.has(clientId)) throw new Error("Client not found");
|
|
548
|
+
if (!oldRoomId) throw new Error("Client is not in a room");
|
|
549
|
+
if (!oldRoom) throw new Error("Client room not found");
|
|
550
|
+
if (oldRoomId === roomId) throw new Error("Client is already in target room");
|
|
551
|
+
if (!targetRoom) throw new Error("Target room not found");
|
|
552
|
+
if (targetRoom.participants.length >= targetRoom.size) throw new Error("Target room is full");
|
|
553
553
|
if (oldRoom.host === clientId) this.#migrateHost(oldRoomId, clientId);
|
|
554
554
|
this.#leaveRoom(clientId, oldRoomId);
|
|
555
555
|
this.#joinRoom(clientId, roomId);
|
|
@@ -583,7 +583,7 @@ export default class PlaySocketServer {
|
|
|
583
583
|
* @param {*} [secondValue] - Second value (needed for some operations)
|
|
584
584
|
*/
|
|
585
585
|
updateRoomStorage(roomId, key, type, value, secondValue) {
|
|
586
|
-
if (this.#debug) console.log(
|
|
586
|
+
if (this.#debug) console.log(LOG_PREFIX + `Property update for room ${roomId}, key ${key}, operation ${type}, value ${value} and secondValue ${secondValue}`);
|
|
587
587
|
const room = this.#rooms[roomId];
|
|
588
588
|
if (room) {
|
|
589
589
|
const propertyUpdate = room.crdtManager.updateProperty(key, type, value, secondValue);
|
|
@@ -622,7 +622,7 @@ export default class PlaySocketServer {
|
|
|
622
622
|
}
|
|
623
623
|
}
|
|
624
624
|
|
|
625
|
-
if (!newRoomId) throw new Error("No available ID found
|
|
625
|
+
if (!newRoomId) throw new Error("No available ID found");
|
|
626
626
|
const roomCrdtManager = new CRDTManager(this.#debug);
|
|
627
627
|
|
|
628
628
|
if (initialStorage) Object.entries(initialStorage)?.forEach(([key, value]) => {
|
|
@@ -638,7 +638,7 @@ export default class PlaySocketServer {
|
|
|
638
638
|
};
|
|
639
639
|
|
|
640
640
|
this.#triggerEvent("roomCreated", newRoomId);
|
|
641
|
-
if (this.#debug) console.log(`Room ${newRoomId} created with initial storage:`, initialStorage);
|
|
641
|
+
if (this.#debug) console.log(LOG_PREFIX + `Room ${newRoomId} created with initial storage:`, initialStorage);
|
|
642
642
|
return { state: roomCrdtManager.state, id: newRoomId };
|
|
643
643
|
}
|
|
644
644
|
|
|
@@ -650,17 +650,17 @@ export default class PlaySocketServer {
|
|
|
650
650
|
#joinRoom(clientId, roomId) {
|
|
651
651
|
const room = this.#rooms[roomId];
|
|
652
652
|
const client = this.#clients.get(clientId);
|
|
653
|
-
if (!room) throw new Error("Room not found
|
|
654
|
-
if (!client && !this.#pendingDisconnects.has(clientId)) throw new Error("Client not found
|
|
655
|
-
if (this.#clientRooms.get(clientId)) throw new Error("Already in a room
|
|
656
|
-
if (room.participants.length >= room.size) throw new Error("Room full
|
|
653
|
+
if (!room) throw new Error("Room not found");
|
|
654
|
+
if (!client && !this.#pendingDisconnects.has(clientId)) throw new Error("Client not found");
|
|
655
|
+
if (this.#clientRooms.get(clientId)) throw new Error("Already in a room");
|
|
656
|
+
if (room.participants.length >= room.size) throw new Error("Room full");
|
|
657
657
|
|
|
658
658
|
room.participants.push(clientId);
|
|
659
659
|
this.#clientRooms.set(clientId, roomId);
|
|
660
660
|
|
|
661
661
|
// Notify joiner with initial storage (only if connected - pending disconnect clients get state on reconnect)
|
|
662
662
|
if (client) {
|
|
663
|
-
if (this.#debug) console.log("Room state sent for join:", room.crdtManager.state);
|
|
663
|
+
if (this.#debug) console.log(LOG_PREFIX + "Room state sent for join:", room.crdtManager.state);
|
|
664
664
|
client.send(encode({
|
|
665
665
|
type: "join_accepted",
|
|
666
666
|
roomId,
|
|
@@ -706,7 +706,7 @@ export default class PlaySocketServer {
|
|
|
706
706
|
*/
|
|
707
707
|
#leaveRoom(clientId, roomId) {
|
|
708
708
|
const room = this.#rooms[roomId];
|
|
709
|
-
if (!room) throw new Error("Room not found
|
|
709
|
+
if (!room) throw new Error("Room not found");
|
|
710
710
|
|
|
711
711
|
room.participants = room.participants.filter(p => p !== clientId); // Remove client from room
|
|
712
712
|
this.#clientRooms.delete(clientId);
|
|
@@ -733,19 +733,19 @@ export default class PlaySocketServer {
|
|
|
733
733
|
*/
|
|
734
734
|
destroyRoom(roomId) {
|
|
735
735
|
const room = this.#rooms[roomId];
|
|
736
|
-
if (!room) throw new Error("Room not found
|
|
736
|
+
if (!room) throw new Error("Room not found");
|
|
737
737
|
|
|
738
738
|
// Disconnect clients if still in room
|
|
739
739
|
const participants = room.participants;
|
|
740
740
|
participants.forEach((clientId) => {
|
|
741
|
-
this.kick(clientId, "Room destroyed by server
|
|
741
|
+
this.kick(clientId, "Room destroyed by server");
|
|
742
742
|
});
|
|
743
743
|
|
|
744
744
|
// Delete the room
|
|
745
745
|
delete this.#rooms[roomId];
|
|
746
746
|
this.#roomVersions.delete(roomId); // Delete room version (used to ensure all clients are up-2-date)
|
|
747
747
|
this.#triggerEvent("roomDestroyed", roomId);
|
|
748
|
-
if (this.#debug) console.log("Deleted room with id " + roomId
|
|
748
|
+
if (this.#debug) console.log(LOG_PREFIX + "Deleted room with id " + roomId);
|
|
749
749
|
}
|
|
750
750
|
|
|
751
751
|
/**
|
|
@@ -754,10 +754,10 @@ export default class PlaySocketServer {
|
|
|
754
754
|
stop() {
|
|
755
755
|
clearInterval(this.#heartbeatInterval);
|
|
756
756
|
if (this.#wss) {
|
|
757
|
-
this.#clients.forEach((client, clientId) => this.kick(clientId, "Server restart
|
|
757
|
+
this.#clients.forEach((client, clientId) => this.kick(clientId, "Server restart"));
|
|
758
758
|
this.#wss.close();
|
|
759
759
|
}
|
|
760
|
-
if (this.#server && this.#ownsServer) this.#server.close(() => { console.log("
|
|
760
|
+
if (this.#server && this.#ownsServer) this.#server.close(() => { console.log(LOG_PREFIX + "Server stopped"); });
|
|
761
761
|
this.#pendingDisconnects.forEach((data) => {
|
|
762
762
|
clearTimeout(data.timeout);
|
|
763
763
|
});
|
|
@@ -9,4 +9,11 @@ export const HEARTBEAT_INTERVAL = 5000;
|
|
|
9
9
|
/**
|
|
10
10
|
* Library version, must match package.json
|
|
11
11
|
*/
|
|
12
|
-
export const VERSION = "4.
|
|
12
|
+
export const VERSION = "4.3.0";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Logging prefixes
|
|
16
|
+
*/
|
|
17
|
+
export const ERROR_PREFIX = "PlaySocket error: ";
|
|
18
|
+
export const WARNING_PREFIX = "PlaySocket warning: ";
|
|
19
|
+
export const LOG_PREFIX = "PlaySocket log: ";
|
package/types/client/client.d.ts
CHANGED
|
@@ -88,8 +88,9 @@ export default class PlaySocket {
|
|
|
88
88
|
sendRequest(name: string, data?: any): Promise<void>;
|
|
89
89
|
/**
|
|
90
90
|
* Destroy the PlaySocket instance and disconnect
|
|
91
|
+
* @param {string} [reason] - Optional reason
|
|
91
92
|
*/
|
|
92
|
-
destroy(): void;
|
|
93
|
+
destroy(reason?: string): void;
|
|
93
94
|
get participantCount(): number;
|
|
94
95
|
get storage(): Record<string, any>;
|
|
95
96
|
get isHost(): boolean;
|
|
@@ -8,4 +8,10 @@ export declare const HEARTBEAT_INTERVAL = 5000;
|
|
|
8
8
|
/**
|
|
9
9
|
* Library version, must match package.json
|
|
10
10
|
*/
|
|
11
|
-
export declare const VERSION = "4.
|
|
11
|
+
export declare const VERSION = "4.3.0";
|
|
12
|
+
/**
|
|
13
|
+
* Logging prefixes
|
|
14
|
+
*/
|
|
15
|
+
export declare const ERROR_PREFIX = "PlaySocket error: ";
|
|
16
|
+
export declare const WARNING_PREFIX = "PlaySocket warning: ";
|
|
17
|
+
export declare const LOG_PREFIX = "PlaySocket log: ";
|