playsocketjs 4.2.1 → 4.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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("error", error => console.error(error));
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()` | - | `void` | Leave room, close the connection, and destroy the instance. |
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 updated. |
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` | - | Instance destroyed through `destroy()` or fatal error. |
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."; // Blocks the registration
256
+ return "Error occurred during auth"; // Blocks the registration
259
257
  }
260
258
  });
261
259
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playsocketjs",
3
- "version": "4.2.1",
3
+ "version": "4.3.1",
4
4
  "description": "WebSocket wrapper for creating simple multiplayer systems with ease.",
5
5
  "type": "module",
6
6
  "module": "./src/client/client.js",
@@ -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.`)), TIMEOUT_MS)
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", "error", "moved", "instanceDestroyed", "storageUpdated", "hostMigrated", "clientJoined", "clientLeft"];
107
+ const validEvents = ["status", "moved", "instanceDestroyed", "storageUpdated", "hostMigrated", "clientJoined", "clientLeft"];
111
108
  if (!validEvents.includes(event)) {
112
- console.warn(WARNING_PREFIX + `Invalid event type "${event}".`);
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.#triggerEvent("error", "Failed to register: " + (message.reason || "No reason provided."));
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 were in a room...
253
- this.#triggerEvent("error", "Reconnected, but room no longer exists.");
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.#triggerEvent("error", "Failed to create room: " + (message.reason || "No reason provided."));
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
- this.#triggerEvent("error", "Detected skipped update – forcing reconnect.");
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
- this.#triggerEvent("error", "Property update rejected, reverting: " + (message.reason || "No reason provided."));
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.#triggerEvent("error", `Kicked out of room: ${message.reason || "No reason provided."}`);
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.#triggerEvent("error", "WebSocket error.");
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.#triggerEvent("error", "Disconnected from server.");
351
- this.destroy();
342
+ this.destroy("Disconnected, reconnection failed.");
352
343
  return;
353
344
  }
354
345
 
355
- this.#triggerEvent("status", `Attempting to reconnect... (${this.#reconnectCount})`);
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 - not connected.");
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
- this.#triggerEvent("error", "Failed to update storage - not in a room.");
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
- this.#triggerEvent("error", "Failed to send request - not initialized");
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
 
@@ -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(`PlaySocket server running on port ${port}.`);
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(`PlaySocket WebSocket connection error for client ${ws.clientId || "unknown"}:`, error); // Catch conn errors
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("PlaySocket WebSocket server error:", 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(`PlaySocket connection ${ws.uuid} terminated due to rate limit violations.`);
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}). Please reload the page, an update may have been released.` }), { binary: true });
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." }), { binary: true });
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." }), { binary: true });
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." }), { binary: true });
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 - client ${ws.clientId} disconnected during event callback.`);
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("PlaySocket error creating room:", 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 - client ${ws.clientId} disconnected during event callback.`);
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("PlaySocket error in message handler:", 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 PlaySocket event type "${event}"`);
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(`PlaySocket ${event} callbacks must be synchronous.`);
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(`PlaySocket ${event} callback error:`, 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(`Playsocket server property update for room ${roomId}, key ${key}, operation ${type}, value ${value} and secondValue ${secondValue}.`);
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("PlaySocket server stopped."); });
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.2.1";
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: ";
@@ -114,7 +114,7 @@ export default class CRDTManager {
114
114
  if (this.#debug) console.log(CONSOLE_PREFIX + "Importing update:", update); // Debug
115
115
 
116
116
  // Check key limit to safeguard against too many keys
117
- if (this.#keyOperations.size >= 100 && !this.#keyOperations.has(key)) throw new Error("Key limit exceeded!");
117
+ if (this.#keyOperations.size >= 100 && !this.#keyOperations.has(key)) throw new Error("Key limit exceeded");
118
118
 
119
119
  // Get COPY of current ops (or empty array if none yet)
120
120
  const currentOps = [...(this.#keyOperations.get(key) || [])];
@@ -157,7 +157,7 @@ export default class CRDTManager {
157
157
 
158
158
  const currentOps = this.#keyOperations.get(key);
159
159
  if (!currentOps?.some(op => op.uuid === opUuid)) return; // Operation not found
160
- if (this.#debug) console.log(CONSOLE_PREFIX + `Removing operation ${opUuid} for key ${key} as part of property update reversion.`);
160
+ if (this.#debug) console.log(CONSOLE_PREFIX + `Removing operation ${opUuid} for key ${key} as part of property update reversion`);
161
161
 
162
162
  // The own vector clock entry isn't decremented because later ops may already embed the incremented counter
163
163
  // and reusing a counter value would corrupt operation ordering (while a gap is harmless)
@@ -188,7 +188,7 @@ export default class CRDTManager {
188
188
  secondValue = this.#sanitizeValue(secondValue);
189
189
 
190
190
  // Debug log
191
- if (this.#debug) console.log(CONSOLE_PREFIX + `Updating property with key ${key}, type ${type}, value ${value} and secondValue ${secondValue}.`);
191
+ if (this.#debug) console.log(CONSOLE_PREFIX + `Updating property with key ${key}, type ${type}, value ${value} and secondValue ${secondValue}`);
192
192
 
193
193
  // Increment vector clock
194
194
  const counter = this.#vectorClock.get(this.#replicaId) || 0;
@@ -440,7 +440,7 @@ export default class CRDTManager {
440
440
 
441
441
  // Check total serialized size
442
442
  const jsonString = JSON.stringify(value);
443
- if (jsonString?.length > 50000) throw new Error("Value too large!"); // 50KB limit
443
+ if (jsonString?.length > 50000) throw new Error("Value too large"); // 50KB limit
444
444
 
445
445
  if (typeof value === "string") return (value.includes("<") || value.includes(">")) ? value.replace(/[<>]/g, "") : value;
446
446
  if (Array.isArray(value)) return Array.from(value, item => this.#sanitizeValue(item));
@@ -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.2.1";
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: ";