playsocketjs 5.0.0 → 5.2.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 CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  An optimistic-first WebSocket synchronization library. Built for creating multiplayer games & collaborative experiences with reactive web frameworks.
4
4
 
5
+ For an introduction to PlaySocket, visit the [PlaySocket website](https://therealpaulplay.github.io/PlaySocketJS/).
6
+
5
7
  ## Why use PlaySocket?
6
8
 
7
9
  PlaySocket makes developing shared experiences a breeze:
@@ -140,9 +142,9 @@ new PlaySocket(id?: string, options: PlaySocketOptions)
140
142
  | `moved` | `roomId: string` | Moved to different room. |
141
143
  | `instanceDestroyed` | `reason: string` | Instance destroyed through `destroy()` or error. |
142
144
  | `storageUpdated` | `storage: object` | Storage state changed. Does not trigger on no-op updates. |
143
- | `hostMigrated` | `roomId: string` | Host was changed. |
145
+ | `hostMigrated` | `hostId: string` | Host was changed. |
144
146
  | `clientJoined` | `clientId: string` | New client joined the room. |
145
- | `clientLeft` | `clientId: string, roomId?: string` | A client left the room. |
147
+ | `clientLeft` | `clientId: string` | A client left the room. |
146
148
 
147
149
  #### Properties
148
150
 
@@ -313,7 +315,7 @@ The callback signature is `callback(verified, code?, message?)` where `code` ref
313
315
  | `kick()` | `clientId: string, reason?: string` | `void` | Kick a client by their client ID. |
314
316
  | `move()` | `clientId: string, roomId: string` | `void` | Move a client that is already in a room to a different room. |
315
317
  | `onEvent()` | `event: string, callback: Function` | `() => void` | Register a server-side event callback. Returns unsubscribe function. |
316
- | `getRoomStorage()` | `roomId: string` | `object` | Get a snapshot of the current room storage. |
318
+ | `getRoomStorage()` | `roomId: string, options?: { dangerouslySkipCloning?: boolean }` | `object` | Get a snapshot of the current room storage. Pass `dangerouslySkipCloning: true` to get the uncloned store for performance reasons. |
317
319
  | `getUpdateDetails()` | `update: object` | `object` | Get the details (`key`, `type`, `value` and `secondValue`) of a storage update for building validation logic. |
318
320
  | `updateRoomStorage()` | `roomId: string, key: string, type: string, value: any, secondValue?: any` | `void` | Update a key in the shared storage of a room. |
319
321
  | `createRoom()` | `initialStorage?: object, size?: number` | `object` | Create a server-owned room (returns object containing room ID and state).|
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playsocketjs",
3
- "version": "5.0.0",
3
+ "version": "5.2.0",
4
4
  "description": "WebSocket wrapper for creating simple multiplayer systems with ease.",
5
5
  "type": "module",
6
6
  "module": "./src/client/client.js",
@@ -20,6 +20,18 @@ const TIMEOUT_MS = 3000; // 3 second timeout for WS messages
20
20
 
21
21
  /** @typedef {{ resolve: (value?: any) => void, reject: (reason?: any) => void }} PendingPromise */
22
22
 
23
+ /**
24
+ * Event callback types
25
+ * @typedef {object} ClientEventMap
26
+ * @property {(message: string) => void} status
27
+ * @property {(roomId: string) => void} moved
28
+ * @property {(reason: string | undefined) => void} instanceDestroyed
29
+ * @property {(storage: Record<string, any>) => void} storageUpdated
30
+ * @property {(hostId: string) => void} hostMigrated
31
+ * @property {(clientId: string) => void} clientJoined
32
+ * @property {(clientId: string) => void} clientLeft
33
+ */
34
+
23
35
  /**
24
36
  * PlaySocket Client
25
37
  */
@@ -75,11 +87,11 @@ export default class PlaySocket {
75
87
 
76
88
  /**
77
89
  * Create a new PlaySocket instance
78
- * @param {string} [id] - Unique identifier for this client (assigned by the server if omitted)
90
+ * @param {string | null} [id] - Unique identifier for this client (assigned by the server if omitted or null)
79
91
  * @param {PlaySocketOptions} options - Connection options
80
92
  */
81
93
  constructor(id, options = /** @type {PlaySocketOptions} */ ({})) {
82
- this.#id = id;
94
+ this.#id = id ?? undefined;
83
95
  if (options.endpoint) this.#endpoint = options.endpoint;
84
96
  if (options.customData) this.#customData = { ...options.customData };
85
97
  if (options.debug) this.#debug = true; // Enabling extra logging
@@ -89,7 +101,7 @@ export default class PlaySocket {
89
101
  /**
90
102
  * Helper to create timeout promises for create room, join room etc.
91
103
  * @param {string} name - Name of the timeout
92
- * @returns {Promise<never>} - Promise that rejects after the timeout
104
+ * @returns {Promise<never>} Promise that rejects after the timeout
93
105
  */
94
106
  #createTimeout(name) {
95
107
  return new Promise((_, reject) =>
@@ -99,9 +111,10 @@ export default class PlaySocket {
99
111
 
100
112
  /**
101
113
  * Register an event callback
102
- * @param {string} event - Event name
103
- * @param {Function} callback - Callback function
104
- * @returns {Function} - Unsubscribe
114
+ * @template {keyof ClientEventMap} E
115
+ * @param {E} event - Event name
116
+ * @param {ClientEventMap[E]} callback - Callback function
117
+ * @returns {Function} Unsubscribe
105
118
  */
106
119
  onEvent(event, callback) {
107
120
  const validEvents = ["status", "moved", "instanceDestroyed", "storageUpdated", "hostMigrated", "clientJoined", "clientLeft"];
@@ -396,11 +409,11 @@ export default class PlaySocket {
396
409
 
397
410
  /**
398
411
  * Create a new room and become host
399
- * @param {object} [initialStorage] - Initial state
412
+ * @param {object | null} [initialStorage] - Initial state
400
413
  * @param {number} [size] - Max number of participants
401
414
  * @returns {Promise<string>} Resolves with room ID
402
415
  */
403
- async createRoom(initialStorage = {}, size) {
416
+ async createRoom(initialStorage, size) {
404
417
  if (!this.#initialized) throw new Error("Not initialized");
405
418
 
406
419
  return /** @type {Promise<string>} */ (Promise.race([
@@ -422,7 +435,7 @@ export default class PlaySocket {
422
435
  /**
423
436
  * Join an existing room
424
437
  * @param {string} roomId - ID of the room
425
- * @returns {Promise<void>} - Resolves when connected
438
+ * @returns {Promise<void>} Resolves when connected
426
439
  */
427
440
  async joinRoom(roomId) {
428
441
  if (!this.#initialized) throw new Error("Not initialized");
@@ -469,7 +482,7 @@ export default class PlaySocket {
469
482
  * Send a custom request to the server
470
483
  * @param {string} name - Name of the request
471
484
  * @param {*} [data] - Custom data
472
- * @returns {Promise<void>} - Request promise
485
+ * @returns {Promise<void>} Request promise
473
486
  */
474
487
  async sendRequest(name, data) {
475
488
  if (!this.#initialized) throw new Error("Not initialized");
@@ -31,6 +31,25 @@ export const RECONNECT_GRACE_PERIOD = 5000; // Exported for use in tests
31
31
  * @property {CRDTManager} crdtManager - CRDT manager holding the room storage
32
32
  */
33
33
 
34
+ /** @typedef {boolean | string | void} RejectViaStringOrBool */
35
+
36
+ /**
37
+ * Event callback types
38
+ * @typedef {object} ServerEventMap
39
+ * @property {(clientId: string, customData: any) => RejectViaStringOrBool | Promise<RejectViaStringOrBool>} clientRegistrationRequested
40
+ * @property {(clientId: string, customData: any) => void} clientRegistered
41
+ * @property {(clientId: string) => void} clientDisconnected
42
+ * @property {(clientId: string, roomId: string) => void} clientJoinedRoom
43
+ * @property {(clientId: string, roomId: string) => void} clientLeftRoom
44
+ * @property {(clientId: string, roomId: string) => RejectViaStringOrBool | Promise<RejectViaStringOrBool>} clientJoinRequested
45
+ * @property {(roomId: string) => void} roomCreated
46
+ * @property {(roomId: string) => void} roomDestroyed
47
+ * @property {(payload: { clientId: string, initialStorage: Record<string, any> }) => Record<string, any> | RejectViaStringOrBool | Promise<Record<string, any> | RejectViaStringOrBool>} roomCreationRequested
48
+ * @property {(payload: { roomId: string | null, clientId: string, name: string, data: unknown }) => RejectViaStringOrBool | Promise<RejectViaStringOrBool>} requestReceived
49
+ * @property {(payload: { roomId: string, clientId: string, update: PropertyUpdate, storage: Record<string, any> | undefined }) => RejectViaStringOrBool} storageUpdateRequested
50
+ * @property {(payload: { roomId: string, clientId: string | null, update: PropertyUpdate, storage: Record<string, any> | undefined }) => void} storageUpdated
51
+ */
52
+
34
53
  /**
35
54
  * PlaySocket Server
36
55
  */
@@ -358,7 +377,7 @@ export default class PlaySocketServer {
358
377
 
359
378
  /**
360
379
  * Generate a readable, 6 digit ID
361
- * @returns {string} - Id
380
+ * @returns {string} Id
362
381
  */
363
382
  #generateId() {
364
383
  const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789";
@@ -392,7 +411,7 @@ export default class PlaySocketServer {
392
411
 
393
412
  /**
394
413
  * Generate a random token to prevent malicious reconnect attempts
395
- * @returns {string} - Token
414
+ * @returns {string} Token
396
415
  */
397
416
  #generateSessionToken() {
398
417
  let token = "";
@@ -404,7 +423,7 @@ export default class PlaySocketServer {
404
423
  * Check rate limit using token bucket algorithm
405
424
  * @param {string} connUuid - Connection UUID (which is different from the client ID)
406
425
  * @param {string} actionType - A string describing the action
407
- * @returns {boolean} - Whether or not the action can be allowed, true means allowed, false means limited
426
+ * @returns {boolean} Whether or not the action can be allowed, true means allowed, false means limited
408
427
  */
409
428
  #checkRateLimit(connUuid, actionType) {
410
429
  const now = Date.now();
@@ -469,9 +488,10 @@ export default class PlaySocketServer {
469
488
 
470
489
  /**
471
490
  * Register an event callback
472
- * @param {string} event - Event name
473
- * @param {Function} callback - Callback function
474
- * @returns {Function} - Unsubscribe
491
+ * @template {keyof ServerEventMap} E
492
+ * @param {E} event - Event name
493
+ * @param {ServerEventMap[E]} callback - Callback function
494
+ * @returns {Function} Unsubscribe
475
495
  */
476
496
  onEvent(event, callback) {
477
497
  const validEvents = ["clientRegistered", "clientRegistrationRequested", "clientDisconnected", "clientJoinedRoom", "clientLeftRoom", "clientJoinRequested", "roomCreated", "roomCreationRequested", "requestReceived", "storageUpdated", "storageUpdateRequested", "roomDestroyed"];
@@ -494,7 +514,7 @@ export default class PlaySocketServer {
494
514
  * Trigger an event to registered callbacks
495
515
  * @param {string} event - Event name
496
516
  * @param {...*} args - Arguments
497
- * @returns {Promise<any>} - The first non-null callback return value if present (all callbacks run, a throwing callback counts as returning false), otherwise true
517
+ * @returns {Promise<any>} The first non-null callback return value if present (all callbacks run, a throwing callback counts as returning false), otherwise true
498
518
  */
499
519
  async #triggerEvent(event, ...args) {
500
520
  const syncOnlyEvents = ["storageUpdateRequested"]; // Async storage validation could mess up GC and would lead to potentially poor UX (slow sync)
@@ -556,19 +576,21 @@ export default class PlaySocketServer {
556
576
  }
557
577
 
558
578
  /**
559
- * Get snapshot of a room's storage
579
+ * Get a snapshot of a room's storage
560
580
  * @param {string} roomId - ID of the room to get the storage from
561
- * @returns {Record<string, any> | undefined} - Storage object or undefined if the room doesn't exist
581
+ * @param {object} [options] - Options
582
+ * @param {boolean} [options.dangerouslySkipCloning=false] - Return uncloned store (faster, but allows illegal mutation)
583
+ * @returns {Record<string, any> | undefined} Storage object or undefined if the room doesn't exist
562
584
  */
563
- getRoomStorage(roomId) {
585
+ getRoomStorage(roomId, { dangerouslySkipCloning = false } = {}) {
564
586
  const room = this.#rooms[roomId];
565
- if (room) return room.crdtManager.propertyStore;
587
+ if (room) return dangerouslySkipCloning ? room.crdtManager.rawPropertyStore : room.crdtManager.propertyStore;
566
588
  }
567
589
 
568
590
  /**
569
591
  * Get the operation details from a storage update (e.g. in the "storageUpdateRequested" event)
570
592
  * @param {PropertyUpdate} update - Property update
571
- * @returns {{key: string | undefined, type: PropertyUpdateType | undefined, value: *, secondValue: *}} - Operation details
593
+ * @returns {{key: string | undefined, type: PropertyUpdateType | undefined, value: *, secondValue: *}} Operation details
572
594
  */
573
595
  getUpdateDetails(update) {
574
596
  return getUpdateDetails(update);
@@ -606,7 +628,7 @@ export default class PlaySocketServer {
606
628
 
607
629
  /**
608
630
  * Create a server-owned room
609
- * @param {object} [initialStorage] - Optional initial storage object
631
+ * @param {object | null} [initialStorage] - Optional initial storage object
610
632
  * @param {number} [size] - Max. room size, up to 500
611
633
  * @returns {{ state: CRDTState, id: string }} Object containing room state and room ID
612
634
  */
@@ -617,7 +639,7 @@ export default class PlaySocketServer {
617
639
  /**
618
640
  * Create a room
619
641
  * @param {string} host - Host ID (when set to "server", room will not be deleted if all clients leave)
620
- * @param {object} [initialStorage] - Optional initial storage object
642
+ * @param {object | null} [initialStorage] - Optional initial storage object
621
643
  * @param {number} [size] - Max. room size, up to 500
622
644
  * @returns {{ state: CRDTState, id: string }} Object containing room state and room ID
623
645
  */
@@ -9,7 +9,7 @@ export const HEARTBEAT_INTERVAL = 5000;
9
9
  /**
10
10
  * Library version, must match package.json
11
11
  */
12
- export const VERSION = "5.0.0";
12
+ export const VERSION = "5.2.0";
13
13
 
14
14
  /**
15
15
  * Logging prefixes
@@ -179,7 +179,7 @@ export default class CRDTManager {
179
179
  * @param {PropertyUpdateType} type - Operation type
180
180
  * @param {*} value - Value
181
181
  * @param {*} [secondValue] - Second value (needed for some operations)
182
- * @returns {PropertyUpdate | undefined} - Returns the property update
182
+ * @returns {PropertyUpdate | undefined} Returns the property update
183
183
  */
184
184
  updateProperty(key, type, value, secondValue) {
185
185
  try {
@@ -276,7 +276,7 @@ export default class CRDTManager {
276
276
  * Create an operation object
277
277
  * @param {OperationData} data - Operation data, consists of type, value and secondValue
278
278
  * @param {VectorClockEntries} vectorClock - Vector clock of the replica/client
279
- * @returns {Operation} - Operation object
279
+ * @returns {Operation} Operation object
280
280
  */
281
281
  #createOperation(data, vectorClock) {
282
282
  const newOperation = {
@@ -323,7 +323,7 @@ export default class CRDTManager {
323
323
  /**
324
324
  * Sort operations by vector clock (causal order)
325
325
  * @param {Operation[]} operations - Full operations that include vector clocks, data and a source
326
- * @returns {Operation[]} - Sorted operations
326
+ * @returns {Operation[]} Sorted operations
327
327
  */
328
328
  #sortByVectorClock(operations) {
329
329
  return [...operations].sort((a, b) => {
@@ -362,16 +362,16 @@ export default class CRDTManager {
362
362
  * @param {PropertyUpdateType} type - Operation type
363
363
  * @param {*} value - Value
364
364
  * @param {*} [secondValue] - Second value (needed for some operations)
365
- * @returns {*} - Value after the operation
365
+ * @returns {*} Value after the operation
366
366
  */
367
367
  #handleOperation(curValue, type, value, secondValue) {
368
368
  try {
369
+ // Set operation (before the structured clone since for set we don't care about curValue anyway)
370
+ if (type === "set") return value;
371
+
369
372
  // Deep copy to avoid reference issues in case value is or contains object(s)
370
373
  curValue = structuredClone(curValue);
371
374
 
372
- // Set operation
373
- if (type === "set") return value;
374
-
375
375
  // Number increment operation
376
376
  if (type === "number-increment") {
377
377
  if (!Number.isFinite(curValue)) curValue = 0; // Auto-convert to number if current value isn't a finite number
@@ -433,7 +433,7 @@ export default class CRDTManager {
433
433
  /**
434
434
  * Remove HTML to prevent XSS and enforce size limits
435
435
  * @param {*} value - Value to sanitize
436
- * @returns {*} - Sanitized value
436
+ * @returns {*} Sanitized value
437
437
  */
438
438
  #sanitizeValue(value) {
439
439
  if (value === undefined) return null; // Normalize undefined to null to avoid divergence in transport
@@ -450,7 +450,7 @@ export default class CRDTManager {
450
450
 
451
451
  /**
452
452
  * Get property store
453
- * @returns {Record<string, any>} - Object with key value pairs
453
+ * @returns {Record<string, any>} Object with key value pairs
454
454
  */
455
455
  get propertyStore() {
456
456
  try {
@@ -461,9 +461,17 @@ export default class CRDTManager {
461
461
  }
462
462
  }
463
463
 
464
+ /**
465
+ * Get uncloned property store
466
+ * @returns {Record<string, any>} Object with key value pairs
467
+ */
468
+ get rawPropertyStore() {
469
+ return this.#propertyStore;
470
+ }
471
+
464
472
  /**
465
473
  * Check for changes in the local property store
466
- * @returns {boolean} - Check if any values changed going from the last to the current property update
474
+ * @returns {boolean} Check if any values changed going from the last to the current property update
467
475
  */
468
476
  get didPropertiesChange() {
469
477
  try {
@@ -479,7 +487,7 @@ export default class CRDTManager {
479
487
 
480
488
  /**
481
489
  * Export state (can be imported using importState, converts the maps to arrays for serialization)
482
- * @returns {CRDTState} - State
490
+ * @returns {CRDTState} State
483
491
  */
484
492
  get state() {
485
493
  return {
@@ -492,7 +500,7 @@ export default class CRDTManager {
492
500
  /**
493
501
  * Get the operation details from a property update
494
502
  * @param {PropertyUpdate} update - Property update
495
- * @returns {{key: string | undefined, type: PropertyUpdateType | undefined, value: *, secondValue: *}} - Operation details
503
+ * @returns {{key: string | undefined, type: PropertyUpdateType | undefined, value: *, secondValue: *}} Operation details
496
504
  */
497
505
  export function getUpdateDetails(update) {
498
506
  const data = update?.operation?.data;
@@ -23,6 +23,15 @@ export type PendingPromise = {
23
23
  resolve: (value?: any) => void;
24
24
  reject: (reason?: any) => void;
25
25
  };
26
+ export type ClientEventMap = {
27
+ status: (message: string) => void;
28
+ moved: (roomId: string) => void;
29
+ instanceDestroyed: (reason: string | undefined) => void;
30
+ storageUpdated: (storage: Record<string, any>) => void;
31
+ hostMigrated: (hostId: string) => void;
32
+ clientJoined: (clientId: string) => void;
33
+ clientLeft: (clientId: string) => void;
34
+ };
26
35
  /** @typedef {import("../universal/crdtManager.js").PropertyUpdateType} PropertyUpdateType */
27
36
  /**
28
37
  * @typedef {object} PlaySocketOptions
@@ -35,6 +44,17 @@ export type PendingPromise = {
35
44
  * @property {string} type - Message type
36
45
  */
37
46
  /** @typedef {{ resolve: (value?: any) => void, reject: (reason?: any) => void }} PendingPromise */
47
+ /**
48
+ * Event callback types
49
+ * @typedef {object} ClientEventMap
50
+ * @property {(message: string) => void} status
51
+ * @property {(roomId: string) => void} moved
52
+ * @property {(reason: string | undefined) => void} instanceDestroyed
53
+ * @property {(storage: Record<string, any>) => void} storageUpdated
54
+ * @property {(hostId: string) => void} hostMigrated
55
+ * @property {(clientId: string) => void} clientJoined
56
+ * @property {(clientId: string) => void} clientLeft
57
+ */
38
58
  /**
39
59
  * PlaySocket Client
40
60
  */
@@ -42,17 +62,18 @@ export default class PlaySocket {
42
62
  #private;
43
63
  /**
44
64
  * Create a new PlaySocket instance
45
- * @param {string} [id] - Unique identifier for this client (assigned by the server if omitted)
65
+ * @param {string | null} [id] - Unique identifier for this client (assigned by the server if omitted or null)
46
66
  * @param {PlaySocketOptions} options - Connection options
47
67
  */
48
- constructor(id?: string, options?: PlaySocketOptions);
68
+ constructor(id?: string | null, options?: PlaySocketOptions);
49
69
  /**
50
70
  * Register an event callback
51
- * @param {string} event - Event name
52
- * @param {Function} callback - Callback function
53
- * @returns {Function} - Unsubscribe
71
+ * @template {keyof ClientEventMap} E
72
+ * @param {E} event - Event name
73
+ * @param {ClientEventMap[E]} callback - Callback function
74
+ * @returns {Function} Unsubscribe
54
75
  */
55
- onEvent(event: string, callback: Function): Function;
76
+ onEvent<E extends keyof ClientEventMap>(event: E, callback: ClientEventMap[E]): Function;
56
77
  /**
57
78
  * Initialize the PlaySocket instance by connecting to the WS server
58
79
  * @returns {Promise<string>} Resolves with the client ID when the connection is established
@@ -60,15 +81,15 @@ export default class PlaySocket {
60
81
  init(): Promise<string>;
61
82
  /**
62
83
  * Create a new room and become host
63
- * @param {object} [initialStorage] - Initial state
84
+ * @param {object | null} [initialStorage] - Initial state
64
85
  * @param {number} [size] - Max number of participants
65
86
  * @returns {Promise<string>} Resolves with room ID
66
87
  */
67
- createRoom(initialStorage?: object, size?: number): Promise<string>;
88
+ createRoom(initialStorage?: object | null, size?: number): Promise<string>;
68
89
  /**
69
90
  * Join an existing room
70
91
  * @param {string} roomId - ID of the room
71
- * @returns {Promise<void>} - Resolves when connected
92
+ * @returns {Promise<void>} Resolves when connected
72
93
  */
73
94
  joinRoom(roomId: string): Promise<void>;
74
95
  /**
@@ -83,7 +104,7 @@ export default class PlaySocket {
83
104
  * Send a custom request to the server
84
105
  * @param {string} name - Name of the request
85
106
  * @param {*} [data] - Custom data
86
- * @returns {Promise<void>} - Request promise
107
+ * @returns {Promise<void>} Request promise
87
108
  */
88
109
  sendRequest(name: string, data?: any): Promise<void>;
89
110
  /**
@@ -33,6 +33,39 @@ export type Room = {
33
33
  */
34
34
  crdtManager: CRDTManager;
35
35
  };
36
+ export type RejectViaStringOrBool = boolean | string | void;
37
+ export type ServerEventMap = {
38
+ clientRegistrationRequested: (clientId: string, customData: any) => RejectViaStringOrBool | Promise<RejectViaStringOrBool>;
39
+ clientRegistered: (clientId: string, customData: any) => void;
40
+ clientDisconnected: (clientId: string) => void;
41
+ clientJoinedRoom: (clientId: string, roomId: string) => void;
42
+ clientLeftRoom: (clientId: string, roomId: string) => void;
43
+ clientJoinRequested: (clientId: string, roomId: string) => RejectViaStringOrBool | Promise<RejectViaStringOrBool>;
44
+ roomCreated: (roomId: string) => void;
45
+ roomDestroyed: (roomId: string) => void;
46
+ roomCreationRequested: (payload: {
47
+ clientId: string;
48
+ initialStorage: Record<string, any>;
49
+ }) => Record<string, any> | RejectViaStringOrBool | Promise<Record<string, any> | RejectViaStringOrBool>;
50
+ requestReceived: (payload: {
51
+ roomId: string | null;
52
+ clientId: string;
53
+ name: string;
54
+ data: unknown;
55
+ }) => RejectViaStringOrBool | Promise<RejectViaStringOrBool>;
56
+ storageUpdateRequested: (payload: {
57
+ roomId: string;
58
+ clientId: string;
59
+ update: PropertyUpdate;
60
+ storage: Record<string, any> | undefined;
61
+ }) => RejectViaStringOrBool;
62
+ storageUpdated: (payload: {
63
+ roomId: string;
64
+ clientId: string | null;
65
+ update: PropertyUpdate;
66
+ storage: Record<string, any> | undefined;
67
+ }) => void;
68
+ };
36
69
  /**
37
70
  * @typedef {import("node:http").Server} HttpServer
38
71
  * @typedef {import("../universal/crdtManager.js").PropertyUpdateType} PropertyUpdateType
@@ -51,6 +84,23 @@ export type Room = {
51
84
  * @property {number} size - Max. number of participants
52
85
  * @property {CRDTManager} crdtManager - CRDT manager holding the room storage
53
86
  */
87
+ /** @typedef {boolean | string | void} RejectViaStringOrBool */
88
+ /**
89
+ * Event callback types
90
+ * @typedef {object} ServerEventMap
91
+ * @property {(clientId: string, customData: any) => RejectViaStringOrBool | Promise<RejectViaStringOrBool>} clientRegistrationRequested
92
+ * @property {(clientId: string, customData: any) => void} clientRegistered
93
+ * @property {(clientId: string) => void} clientDisconnected
94
+ * @property {(clientId: string, roomId: string) => void} clientJoinedRoom
95
+ * @property {(clientId: string, roomId: string) => void} clientLeftRoom
96
+ * @property {(clientId: string, roomId: string) => RejectViaStringOrBool | Promise<RejectViaStringOrBool>} clientJoinRequested
97
+ * @property {(roomId: string) => void} roomCreated
98
+ * @property {(roomId: string) => void} roomDestroyed
99
+ * @property {(payload: { clientId: string, initialStorage: Record<string, any> }) => Record<string, any> | RejectViaStringOrBool | Promise<Record<string, any> | RejectViaStringOrBool>} roomCreationRequested
100
+ * @property {(payload: { roomId: string | null, clientId: string, name: string, data: unknown }) => RejectViaStringOrBool | Promise<RejectViaStringOrBool>} requestReceived
101
+ * @property {(payload: { roomId: string, clientId: string, update: PropertyUpdate, storage: Record<string, any> | undefined }) => RejectViaStringOrBool} storageUpdateRequested
102
+ * @property {(payload: { roomId: string, clientId: string | null, update: PropertyUpdate, storage: Record<string, any> | undefined }) => void} storageUpdated
103
+ */
54
104
  /**
55
105
  * PlaySocket Server
56
106
  */
@@ -76,11 +126,12 @@ export default class PlaySocketServer {
76
126
  });
77
127
  /**
78
128
  * Register an event callback
79
- * @param {string} event - Event name
80
- * @param {Function} callback - Callback function
81
- * @returns {Function} - Unsubscribe
129
+ * @template {keyof ServerEventMap} E
130
+ * @param {E} event - Event name
131
+ * @param {ServerEventMap[E]} callback - Callback function
132
+ * @returns {Function} Unsubscribe
82
133
  */
83
- onEvent(event: string, callback: Function): Function;
134
+ onEvent<E extends keyof ServerEventMap>(event: E, callback: ServerEventMap[E]): Function;
84
135
  /**
85
136
  * Kick a client from the server
86
137
  * @param {string} clientId - Client ID
@@ -94,15 +145,19 @@ export default class PlaySocketServer {
94
145
  */
95
146
  move(clientId: string, roomId: string): void;
96
147
  /**
97
- * Get snapshot of a room's storage
148
+ * Get a snapshot of a room's storage
98
149
  * @param {string} roomId - ID of the room to get the storage from
99
- * @returns {Record<string, any> | undefined} - Storage object or undefined if the room doesn't exist
150
+ * @param {object} [options] - Options
151
+ * @param {boolean} [options.dangerouslySkipCloning=false] - Return uncloned store (faster, but allows illegal mutation)
152
+ * @returns {Record<string, any> | undefined} Storage object or undefined if the room doesn't exist
100
153
  */
101
- getRoomStorage(roomId: string): Record<string, any> | undefined;
154
+ getRoomStorage(roomId: string, { dangerouslySkipCloning }?: {
155
+ dangerouslySkipCloning?: boolean;
156
+ }): Record<string, any> | undefined;
102
157
  /**
103
158
  * Get the operation details from a storage update (e.g. in the "storageUpdateRequested" event)
104
159
  * @param {PropertyUpdate} update - Property update
105
- * @returns {{key: string | undefined, type: PropertyUpdateType | undefined, value: *, secondValue: *}} - Operation details
160
+ * @returns {{key: string | undefined, type: PropertyUpdateType | undefined, value: *, secondValue: *}} Operation details
106
161
  */
107
162
  getUpdateDetails(update: PropertyUpdate): {
108
163
  key: string | undefined;
@@ -121,11 +176,11 @@ export default class PlaySocketServer {
121
176
  updateRoomStorage(roomId: string, key: string, type: PropertyUpdateType, value: any, secondValue?: any): void;
122
177
  /**
123
178
  * Create a server-owned room
124
- * @param {object} [initialStorage] - Optional initial storage object
179
+ * @param {object | null} [initialStorage] - Optional initial storage object
125
180
  * @param {number} [size] - Max. room size, up to 500
126
181
  * @returns {{ state: CRDTState, id: string }} Object containing room state and room ID
127
182
  */
128
- createRoom(initialStorage?: object, size?: number): {
183
+ createRoom(initialStorage?: object | null, size?: number): {
129
184
  state: CRDTState;
130
185
  id: string;
131
186
  };
@@ -8,7 +8,7 @@ export declare const HEARTBEAT_INTERVAL = 5000;
8
8
  /**
9
9
  * Library version, must match package.json
10
10
  */
11
- export declare const VERSION = "5.0.0";
11
+ export declare const VERSION = "5.2.0";
12
12
  /**
13
13
  * Logging prefixes
14
14
  */
@@ -115,29 +115,34 @@ export default class CRDTManager {
115
115
  * @param {PropertyUpdateType} type - Operation type
116
116
  * @param {*} value - Value
117
117
  * @param {*} [secondValue] - Second value (needed for some operations)
118
- * @returns {PropertyUpdate | undefined} - Returns the property update
118
+ * @returns {PropertyUpdate | undefined} Returns the property update
119
119
  */
120
120
  updateProperty(key: string, type: PropertyUpdateType, value: any, secondValue?: any): PropertyUpdate | undefined;
121
121
  /**
122
122
  * Get property store
123
- * @returns {Record<string, any>} - Object with key value pairs
123
+ * @returns {Record<string, any>} Object with key value pairs
124
124
  */
125
125
  get propertyStore(): Record<string, any>;
126
+ /**
127
+ * Get uncloned property store
128
+ * @returns {Record<string, any>} Object with key value pairs
129
+ */
130
+ get rawPropertyStore(): Record<string, any>;
126
131
  /**
127
132
  * Check for changes in the local property store
128
- * @returns {boolean} - Check if any values changed going from the last to the current property update
133
+ * @returns {boolean} Check if any values changed going from the last to the current property update
129
134
  */
130
135
  get didPropertiesChange(): boolean;
131
136
  /**
132
137
  * Export state (can be imported using importState, converts the maps to arrays for serialization)
133
- * @returns {CRDTState} - State
138
+ * @returns {CRDTState} State
134
139
  */
135
140
  get state(): CRDTState;
136
141
  }
137
142
  /**
138
143
  * Get the operation details from a property update
139
144
  * @param {PropertyUpdate} update - Property update
140
- * @returns {{key: string | undefined, type: PropertyUpdateType | undefined, value: *, secondValue: *}} - Operation details
145
+ * @returns {{key: string | undefined, type: PropertyUpdateType | undefined, value: *, secondValue: *}} Operation details
141
146
  */
142
147
  export declare function getUpdateDetails(update: PropertyUpdate): {
143
148
  key: string | undefined;