playsocketjs 4.2.0 → 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 +38 -55
- package/src/server/server.js +44 -45
- package/src/universal/constants.js +12 -0
- package/types/client/client.d.ts +2 -1
- package/types/universal/constants.d.ts +10 -0
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
|
|
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();
|
|
@@ -161,7 +158,7 @@ export default class PlaySocket {
|
|
|
161
158
|
this.#sendToServer({
|
|
162
159
|
type: "register",
|
|
163
160
|
id: this.#id,
|
|
164
|
-
version:
|
|
161
|
+
version: VERSION,
|
|
165
162
|
customData: this.#customData || undefined
|
|
166
163
|
});
|
|
167
164
|
}),
|
|
@@ -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,8 +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
|
|
6
|
-
import { HEARTBEAT_INTERVAL } from "../universal/constants.js";
|
|
5
|
+
import { HEARTBEAT_INTERVAL, VERSION, ERROR_PREFIX, WARNING_PREFIX, LOG_PREFIX } from "../universal/constants.js";
|
|
7
6
|
|
|
8
7
|
const MAX_ROOM_SIZE = 500;
|
|
9
8
|
export const RECONNECT_GRACE_PERIOD = 5000; // Exported for use in tests
|
|
@@ -86,7 +85,7 @@ export default class PlaySocketServer {
|
|
|
86
85
|
this.#ownsServer = true;
|
|
87
86
|
this.#server = createServer(); // Create HTTP server and start it
|
|
88
87
|
this.#server.listen(port, () => {
|
|
89
|
-
console.log(`
|
|
88
|
+
console.log(LOG_PREFIX + `Server running on port ${port}`);
|
|
90
89
|
});
|
|
91
90
|
}
|
|
92
91
|
|
|
@@ -105,13 +104,13 @@ export default class PlaySocketServer {
|
|
|
105
104
|
ws.on("message", msg => this.#handleMessage(ws, msg));
|
|
106
105
|
ws.on("close", () => this.#handleDisconnection(ws));
|
|
107
106
|
ws.on("error", (error) => {
|
|
108
|
-
console.error(`
|
|
107
|
+
console.error(ERROR_PREFIX + `WebSocket connection error for client ${ws.clientId || "unknown"}:`, error); // Catch conn errors
|
|
109
108
|
});
|
|
110
109
|
});
|
|
111
110
|
|
|
112
111
|
// Log & catch server-level errors
|
|
113
112
|
this.#wss.on("error", (error) => {
|
|
114
|
-
console.error("
|
|
113
|
+
console.error(ERROR_PREFIX + "WebSocket server error:", error);
|
|
115
114
|
});
|
|
116
115
|
|
|
117
116
|
// Start heartbeat
|
|
@@ -139,7 +138,7 @@ export default class PlaySocketServer {
|
|
|
139
138
|
if (!ws.isTerminating) {
|
|
140
139
|
ws.isTerminating = true; // Prevent multiple terminate calls (it is async)
|
|
141
140
|
ws.terminate();
|
|
142
|
-
console.error(`
|
|
141
|
+
console.error(ERROR_PREFIX + `Connection ${ws.uuid} terminated due to rate limit violations`);
|
|
143
142
|
return;
|
|
144
143
|
}
|
|
145
144
|
}
|
|
@@ -147,14 +146,14 @@ export default class PlaySocketServer {
|
|
|
147
146
|
switch (data.type) {
|
|
148
147
|
case "register": {
|
|
149
148
|
// Ensure client uses the same version as server
|
|
150
|
-
if (data.version !==
|
|
151
|
-
ws.send(encode({ type: "registration_failed", reason: `Version mismatch (client ${data.version ? "v" + data.version : "legacy"}, server v${
|
|
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}), please reload the page` }), { binary: true });
|
|
152
151
|
return;
|
|
153
152
|
}
|
|
154
153
|
|
|
155
154
|
// Register client ID if provided & check for a duplicate
|
|
156
155
|
if (data.id && (this.#clients.get(data.id) || data.id === "server")) {
|
|
157
|
-
ws.send(encode({ type: "registration_failed", reason: "ID is taken
|
|
156
|
+
ws.send(encode({ type: "registration_failed", reason: "ID is taken" }), { binary: true });
|
|
158
157
|
return;
|
|
159
158
|
}
|
|
160
159
|
|
|
@@ -168,8 +167,8 @@ export default class PlaySocketServer {
|
|
|
168
167
|
}
|
|
169
168
|
}
|
|
170
169
|
if (!data.id) {
|
|
171
|
-
ws.send(encode({ type: "registration_failed", reason: "No available ID found
|
|
172
|
-
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");
|
|
173
172
|
}
|
|
174
173
|
}
|
|
175
174
|
|
|
@@ -199,7 +198,7 @@ export default class PlaySocketServer {
|
|
|
199
198
|
if (data.sessionToken !== this.#clientTokens.get(data.id)) {
|
|
200
199
|
ws.send(encode({
|
|
201
200
|
type: "reconnection_failed",
|
|
202
|
-
reason: "Session token does not match
|
|
201
|
+
reason: "Session token does not match"
|
|
203
202
|
}), { binary: true });
|
|
204
203
|
return;
|
|
205
204
|
}
|
|
@@ -217,7 +216,7 @@ export default class PlaySocketServer {
|
|
|
217
216
|
if (formerRoom) {
|
|
218
217
|
// If the room has no host (they were the only one and disconnected), restore them as host
|
|
219
218
|
if (formerRoom.host === null) formerRoom.host = data.id;
|
|
220
|
-
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);
|
|
221
220
|
roomData = {
|
|
222
221
|
state: formerRoom.crdtManager.state,
|
|
223
222
|
participantCount: formerRoom.participants.length,
|
|
@@ -228,7 +227,7 @@ export default class PlaySocketServer {
|
|
|
228
227
|
|
|
229
228
|
ws.send(encode({ type: "reconnected", roomData }), { binary: true });
|
|
230
229
|
} else {
|
|
231
|
-
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 });
|
|
232
231
|
}
|
|
233
232
|
break;
|
|
234
233
|
}
|
|
@@ -239,7 +238,7 @@ export default class PlaySocketServer {
|
|
|
239
238
|
if (this.#clientRooms.get(ws.clientId)) {
|
|
240
239
|
ws.send(encode({
|
|
241
240
|
type: "room_creation_failed",
|
|
242
|
-
reason: "Already in a room
|
|
241
|
+
reason: "Already in a room"
|
|
243
242
|
}), { binary: true });
|
|
244
243
|
return;
|
|
245
244
|
}
|
|
@@ -255,7 +254,7 @@ export default class PlaySocketServer {
|
|
|
255
254
|
try {
|
|
256
255
|
// Verify client is still connected and abort if not
|
|
257
256
|
if (!this.#clients.has(ws.clientId)) {
|
|
258
|
-
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`);
|
|
259
258
|
return;
|
|
260
259
|
}
|
|
261
260
|
|
|
@@ -265,7 +264,7 @@ export default class PlaySocketServer {
|
|
|
265
264
|
this.#clientRooms.set(ws.clientId, newRoom.id); // Add client to the client-room map
|
|
266
265
|
ws.send(encode({ type: "room_created", state: newRoom.state, roomId: newRoom.id, participantCount: this.#rooms[newRoom.id].participants.length }), { binary: true });
|
|
267
266
|
} catch (error) {
|
|
268
|
-
console.error("
|
|
267
|
+
console.error(ERROR_PREFIX + "Error creating room:", error);
|
|
269
268
|
ws.send(encode({ type: "room_creation_failed", reason: error.message }), { binary: true });
|
|
270
269
|
}
|
|
271
270
|
break;
|
|
@@ -286,7 +285,7 @@ export default class PlaySocketServer {
|
|
|
286
285
|
|
|
287
286
|
// Verify client is still connected and abort if not
|
|
288
287
|
if (!this.#clients.has(ws.clientId)) {
|
|
289
|
-
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`);
|
|
290
289
|
return;
|
|
291
290
|
}
|
|
292
291
|
|
|
@@ -333,7 +332,7 @@ export default class PlaySocketServer {
|
|
|
333
332
|
});
|
|
334
333
|
|
|
335
334
|
this.#triggerEvent("storageUpdated", { roomId, clientId: ws.clientId, update: structuredClone(data.update), storage: this.getRoomStorage(roomId) });
|
|
336
|
-
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);
|
|
337
336
|
}
|
|
338
337
|
break;
|
|
339
338
|
}
|
|
@@ -353,7 +352,7 @@ export default class PlaySocketServer {
|
|
|
353
352
|
break;
|
|
354
353
|
}
|
|
355
354
|
} catch (error) {
|
|
356
|
-
console.error("
|
|
355
|
+
console.error(ERROR_PREFIX + "Error in message handler:", error);
|
|
357
356
|
}
|
|
358
357
|
}
|
|
359
358
|
|
|
@@ -373,8 +372,8 @@ export default class PlaySocketServer {
|
|
|
373
372
|
*/
|
|
374
373
|
#migrateHost(roomId, clientId) {
|
|
375
374
|
const room = this.#rooms[roomId];
|
|
376
|
-
if (!room) throw new Error("Room not found
|
|
377
|
-
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");
|
|
378
377
|
|
|
379
378
|
const remainingParticipants = room.participants.filter(p => p !== clientId);
|
|
380
379
|
|
|
@@ -477,7 +476,7 @@ export default class PlaySocketServer {
|
|
|
477
476
|
onEvent(event, callback) {
|
|
478
477
|
const validEvents = ["clientRegistered", "clientRegistrationRequested", "clientDisconnected", "clientJoinedRoom", "clientLeftRoom", "clientJoinRequested", "roomCreated", "roomCreationRequested", "requestReceived", "storageUpdated", "storageUpdateRequested", "roomDestroyed"];
|
|
479
478
|
if (!validEvents.includes(event)) {
|
|
480
|
-
console.warn(`Invalid
|
|
479
|
+
console.warn(WARNING_PREFIX + `Invalid event type ${event}`);
|
|
481
480
|
return () => { };
|
|
482
481
|
};
|
|
483
482
|
if (!this.#callbacks.has(event)) this.#callbacks.set(event, []);
|
|
@@ -509,13 +508,13 @@ export default class PlaySocketServer {
|
|
|
509
508
|
if (typeof result?.then === "function") {
|
|
510
509
|
if (syncOnlyEvents.includes(event)) {
|
|
511
510
|
result.catch(() => { });
|
|
512
|
-
console.error(
|
|
511
|
+
console.error(ERROR_PREFIX + `${event} callbacks must be synchronous`);
|
|
513
512
|
result = `${event} callback must be synchronous.`; // Rejection string to explicitly block this practice
|
|
514
513
|
} else result = await result;
|
|
515
514
|
}
|
|
516
515
|
if (firstResult == null && result != null) firstResult = result;
|
|
517
516
|
} catch (error) {
|
|
518
|
-
console.error(
|
|
517
|
+
console.error(ERROR_PREFIX + `${event} callback error:`, error);
|
|
519
518
|
if (firstResult == null) firstResult = false; // Fail closed (false blocks for most events)
|
|
520
519
|
}
|
|
521
520
|
}
|
|
@@ -545,12 +544,12 @@ export default class PlaySocketServer {
|
|
|
545
544
|
const oldRoomId = this.#clientRooms.get(clientId);
|
|
546
545
|
const oldRoom = this.#rooms[oldRoomId];
|
|
547
546
|
const targetRoom = this.#rooms[roomId];
|
|
548
|
-
if (!client && !this.#pendingDisconnects.has(clientId)) throw new Error("Client not found
|
|
549
|
-
if (!oldRoomId) throw new Error("Client is not in a room
|
|
550
|
-
if (!oldRoom) throw new Error("Client room not found
|
|
551
|
-
if (oldRoomId === roomId) throw new Error("Client is already in target room
|
|
552
|
-
if (!targetRoom) throw new Error("Target room not found
|
|
553
|
-
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");
|
|
554
553
|
if (oldRoom.host === clientId) this.#migrateHost(oldRoomId, clientId);
|
|
555
554
|
this.#leaveRoom(clientId, oldRoomId);
|
|
556
555
|
this.#joinRoom(clientId, roomId);
|
|
@@ -584,7 +583,7 @@ export default class PlaySocketServer {
|
|
|
584
583
|
* @param {*} [secondValue] - Second value (needed for some operations)
|
|
585
584
|
*/
|
|
586
585
|
updateRoomStorage(roomId, key, type, value, secondValue) {
|
|
587
|
-
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}`);
|
|
588
587
|
const room = this.#rooms[roomId];
|
|
589
588
|
if (room) {
|
|
590
589
|
const propertyUpdate = room.crdtManager.updateProperty(key, type, value, secondValue);
|
|
@@ -623,7 +622,7 @@ export default class PlaySocketServer {
|
|
|
623
622
|
}
|
|
624
623
|
}
|
|
625
624
|
|
|
626
|
-
if (!newRoomId) throw new Error("No available ID found
|
|
625
|
+
if (!newRoomId) throw new Error("No available ID found");
|
|
627
626
|
const roomCrdtManager = new CRDTManager(this.#debug);
|
|
628
627
|
|
|
629
628
|
if (initialStorage) Object.entries(initialStorage)?.forEach(([key, value]) => {
|
|
@@ -639,7 +638,7 @@ export default class PlaySocketServer {
|
|
|
639
638
|
};
|
|
640
639
|
|
|
641
640
|
this.#triggerEvent("roomCreated", newRoomId);
|
|
642
|
-
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);
|
|
643
642
|
return { state: roomCrdtManager.state, id: newRoomId };
|
|
644
643
|
}
|
|
645
644
|
|
|
@@ -651,17 +650,17 @@ export default class PlaySocketServer {
|
|
|
651
650
|
#joinRoom(clientId, roomId) {
|
|
652
651
|
const room = this.#rooms[roomId];
|
|
653
652
|
const client = this.#clients.get(clientId);
|
|
654
|
-
if (!room) throw new Error("Room not found
|
|
655
|
-
if (!client && !this.#pendingDisconnects.has(clientId)) throw new Error("Client not found
|
|
656
|
-
if (this.#clientRooms.get(clientId)) throw new Error("Already in a room
|
|
657
|
-
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");
|
|
658
657
|
|
|
659
658
|
room.participants.push(clientId);
|
|
660
659
|
this.#clientRooms.set(clientId, roomId);
|
|
661
660
|
|
|
662
661
|
// Notify joiner with initial storage (only if connected - pending disconnect clients get state on reconnect)
|
|
663
662
|
if (client) {
|
|
664
|
-
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);
|
|
665
664
|
client.send(encode({
|
|
666
665
|
type: "join_accepted",
|
|
667
666
|
roomId,
|
|
@@ -707,7 +706,7 @@ export default class PlaySocketServer {
|
|
|
707
706
|
*/
|
|
708
707
|
#leaveRoom(clientId, roomId) {
|
|
709
708
|
const room = this.#rooms[roomId];
|
|
710
|
-
if (!room) throw new Error("Room not found
|
|
709
|
+
if (!room) throw new Error("Room not found");
|
|
711
710
|
|
|
712
711
|
room.participants = room.participants.filter(p => p !== clientId); // Remove client from room
|
|
713
712
|
this.#clientRooms.delete(clientId);
|
|
@@ -734,19 +733,19 @@ export default class PlaySocketServer {
|
|
|
734
733
|
*/
|
|
735
734
|
destroyRoom(roomId) {
|
|
736
735
|
const room = this.#rooms[roomId];
|
|
737
|
-
if (!room) throw new Error("Room not found
|
|
736
|
+
if (!room) throw new Error("Room not found");
|
|
738
737
|
|
|
739
738
|
// Disconnect clients if still in room
|
|
740
739
|
const participants = room.participants;
|
|
741
740
|
participants.forEach((clientId) => {
|
|
742
|
-
this.kick(clientId, "Room destroyed by server
|
|
741
|
+
this.kick(clientId, "Room destroyed by server");
|
|
743
742
|
});
|
|
744
743
|
|
|
745
744
|
// Delete the room
|
|
746
745
|
delete this.#rooms[roomId];
|
|
747
746
|
this.#roomVersions.delete(roomId); // Delete room version (used to ensure all clients are up-2-date)
|
|
748
747
|
this.#triggerEvent("roomDestroyed", roomId);
|
|
749
|
-
if (this.#debug) console.log("Deleted room with id " + roomId
|
|
748
|
+
if (this.#debug) console.log(LOG_PREFIX + "Deleted room with id " + roomId);
|
|
750
749
|
}
|
|
751
750
|
|
|
752
751
|
/**
|
|
@@ -755,10 +754,10 @@ export default class PlaySocketServer {
|
|
|
755
754
|
stop() {
|
|
756
755
|
clearInterval(this.#heartbeatInterval);
|
|
757
756
|
if (this.#wss) {
|
|
758
|
-
this.#clients.forEach((client, clientId) => this.kick(clientId, "Server restart
|
|
757
|
+
this.#clients.forEach((client, clientId) => this.kick(clientId, "Server restart"));
|
|
759
758
|
this.#wss.close();
|
|
760
759
|
}
|
|
761
|
-
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"); });
|
|
762
761
|
this.#pendingDisconnects.forEach((data) => {
|
|
763
762
|
clearTimeout(data.timeout);
|
|
764
763
|
});
|
|
@@ -5,3 +5,15 @@
|
|
|
5
5
|
* be desynced as a result of gc
|
|
6
6
|
*/
|
|
7
7
|
export const HEARTBEAT_INTERVAL = 5000;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Library version, must match package.json
|
|
11
|
+
*/
|
|
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;
|
|
@@ -5,3 +5,13 @@
|
|
|
5
5
|
* be desynced as a result of gc
|
|
6
6
|
*/
|
|
7
7
|
export declare const HEARTBEAT_INTERVAL = 5000;
|
|
8
|
+
/**
|
|
9
|
+
* Library version, must match package.json
|
|
10
|
+
*/
|
|
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: ";
|