playsocketjs 3.1.2 → 4.1.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.
@@ -0,0 +1,13 @@
1
+ // Custom properties that PlaySocketServer attaches to ws WebSocket instances,
2
+ // added to the ws module's own WebSocket type via declaration merging
3
+ import "ws";
4
+
5
+ declare module "ws" {
6
+ interface WebSocket {
7
+ uuid: string;
8
+ isAlive: boolean;
9
+ clientId?: string;
10
+ isTerminating?: boolean;
11
+ willfulDisconnect?: boolean;
12
+ }
13
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Heartbeat ping interval in milliseconds
3
+ * Also used as the minimum age for CRDT garbage collection - operations are
4
+ * retained for at least one heartbeat cycle to ensure even slow clients can never
5
+ * be desynced as a result of gc
6
+ */
7
+ export const HEARTBEAT_INTERVAL = 5000;
@@ -0,0 +1,498 @@
1
+ import { HEARTBEAT_INTERVAL } from "./constants.js";
2
+
3
+ const CONSOLE_PREFIX = "PlaySocket CRDT manager: ";
4
+
5
+ /** @typedef {"set" | "number-increment" | "array-add" | "array-add-unique" | "array-remove-matching" | "array-update-matching" | "object-set-key" | "object-remove-key"} PropertyUpdateType */
6
+
7
+ /** @typedef {[string, number][]} VectorClockEntries */
8
+
9
+ /**
10
+ * @typedef {object} OperationData
11
+ * @property {PropertyUpdateType} type - Operation type
12
+ * @property {*} value - Value
13
+ * @property {*} [secondValue] - Second value (needed for some operations)
14
+ */
15
+
16
+ /**
17
+ * @typedef {object} Operation
18
+ * @property {OperationData} data - Operation data
19
+ * @property {VectorClockEntries} vectorClock - Vector clock entries at creation time
20
+ * @property {string} source - ID of the replica that created the operation
21
+ * @property {string} uuid - Unique operation ID
22
+ */
23
+
24
+ /**
25
+ * @typedef {object} PropertyUpdate
26
+ * @property {string} key - Storage key
27
+ * @property {Operation} operation - New operation
28
+ * @property {VectorClockEntries} vectorClock - Vector clock entries of the replica
29
+ */
30
+
31
+ /**
32
+ * @typedef {object} CRDTState
33
+ * @property {[string, Operation[]][]} keyOperations - Operations per storage key
34
+ * @property {VectorClockEntries} vectorClock - Vector clock entries
35
+ */
36
+
37
+ /**
38
+ * CRDT Manager
39
+ * Custom conflict-free replicated data type system with vector clocks
40
+ */
41
+ export default class CRDTManager {
42
+ // Storage
43
+ /** @type {string} */
44
+ #replicaId;
45
+ /** @type {Map<string, Operation[]>} */
46
+ #keyOperations = new Map();
47
+ /** @type {Map<string, number>} */
48
+ #vectorClock = new Map();
49
+
50
+ /** @type {Record<string, any>} */
51
+ #propertyStore = {}; // Current local values as key/value store object
52
+ /** @type {Record<string, any>} */
53
+ #lastPropertyStore = {}; // Last property store to compare against
54
+
55
+ // Local Garbage Collection
56
+ #lastGCCheck = 0;
57
+ /** @type {Map<string, number>} */
58
+ #opUuidTimestamp = new Map(); // Map every operation to a timestamp for garbage collection
59
+
60
+ // Debug
61
+ #debug = false;
62
+
63
+ /**
64
+ * Create a new instance
65
+ * @param {boolean} [debug=false] - Enable debug logging
66
+ */
67
+ constructor(debug) {
68
+ if (debug) this.#debug = true;
69
+ this.#replicaId = crypto.randomUUID();
70
+ this.#vectorClock.set(this.#replicaId, 0);
71
+ }
72
+
73
+ /**
74
+ * Import the entire state of the CRDT manager (this overwrites the old state)
75
+ * @param {CRDTState} state - Full state of the CRDT manager
76
+ */
77
+ importState(state) {
78
+ try {
79
+ const { keyOperations, vectorClock } = state;
80
+ if (this.#debug) console.log(CONSOLE_PREFIX + "Importing state:", state);
81
+
82
+ // Resets
83
+ this.#opUuidTimestamp.clear();
84
+ this.#propertyStore = {};
85
+ this.#lastPropertyStore = {};
86
+
87
+ this.#keyOperations = new Map(keyOperations); // Rebuild the operations map
88
+ this.#vectorClock = new Map(vectorClock); // Rebuild the vector clock map
89
+ if (!this.#vectorClock.has(this.#replicaId)) this.#vectorClock.set(this.#replicaId, 0); // Add own vector clock if it wasn't present in the imported state
90
+
91
+ // Map all operation uuids to current timestamp
92
+ this.#keyOperations.forEach((value, _key) => {
93
+ value.forEach((operation) => {
94
+ this.#opUuidTimestamp.set(operation.uuid, Date.now());
95
+ })
96
+ });
97
+
98
+ // Process each key to update local values
99
+ for (const key of this.#keyOperations.keys()) this.#processLocalProperty(key);
100
+
101
+ } catch (error) {
102
+ console.error(CONSOLE_PREFIX + "Failed to import state:", error);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Import property update
108
+ * @param {PropertyUpdate} update - Property update
109
+ */
110
+ importPropertyUpdate(update) {
111
+ try {
112
+ const { key, operation: rawOperation, vectorClock } = update;
113
+ const operation = /** @type {Operation} */ (this.#sanitizeValue(rawOperation));
114
+ if (this.#debug) console.log(CONSOLE_PREFIX + "Importing update:", update); // Debug
115
+
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!");
118
+
119
+ // Get COPY of current ops (or empty array if none yet)
120
+ const currentOps = [...(this.#keyOperations.get(key) || [])];
121
+ // Merge vector clocks (always take max value)
122
+ for (const [id, counter] of vectorClock) {
123
+ if (!this.#vectorClock.has(id) || this.#vectorClock.get(id) < counter) {
124
+ this.#vectorClock.set(id, counter);
125
+ }
126
+ }
127
+
128
+ // Safeguard against massive vector clocks
129
+ if (this.#vectorClock.size > 1000) this.#vectorClock = new Map([...this.#vectorClock].slice(-100));
130
+
131
+ // Add new operation if it's not already added
132
+ const existingUuids = new Set(currentOps.map(op => op.uuid));
133
+ if (operation.uuid && !existingUuids.has(operation.uuid)) {
134
+ currentOps.push({ ...operation }); // Add operation
135
+ this.#opUuidTimestamp.set(operation.uuid, Date.now()); // Add timestamp for gc
136
+ }
137
+
138
+ // Sort, update operations & local value
139
+ this.#keyOperations.set(key, this.#sortByVectorClock(currentOps));
140
+ this.#processLocalProperty(key);
141
+ this.#checkGarbageCollection();
142
+
143
+ } catch (error) {
144
+ console.error(CONSOLE_PREFIX + "Failed to import property update:", error);
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Revert a local property update by removing the operation and recomputing the affected key
150
+ * Should NOT be used on the server or for synced ops, only for reverting local optimistic updates
151
+ * @param {PropertyUpdate} update - Property update
152
+ */
153
+ revertPropertyUpdate(update) {
154
+ try {
155
+ const key = update.key;
156
+ const opUuid = update.operation.uuid;
157
+
158
+ const currentOps = this.#keyOperations.get(key);
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.`);
161
+
162
+ // The own vector clock entry isn't decremented because later ops may already embed the incremented counter
163
+ // and reusing a counter value would corrupt operation ordering (while a gap is harmless)
164
+ const remainingOps = currentOps.filter(op => op.uuid !== opUuid);
165
+ this.#opUuidTimestamp.delete(opUuid);
166
+
167
+ if (remainingOps.length) this.#keyOperations.set(key, remainingOps);
168
+ else this.#keyOperations.delete(key); // This was the only operation for this key, remove key
169
+ this.#processLocalProperty(key);
170
+
171
+ } catch (error) {
172
+ console.error(CONSOLE_PREFIX + "Failed to revert property update:", error);
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Update a property
178
+ * @param {string} key - Storage key
179
+ * @param {PropertyUpdateType} type - Operation type
180
+ * @param {*} value - Value
181
+ * @param {*} [secondValue] - Second value (needed for some operations)
182
+ * @returns {PropertyUpdate | undefined} - Returns the property update
183
+ */
184
+ updateProperty(key, type, value, secondValue) {
185
+ try {
186
+ // Sanitize inputs
187
+ value = this.#sanitizeValue(value);
188
+ secondValue = this.#sanitizeValue(secondValue);
189
+
190
+ // Debug log
191
+ if (this.#debug) console.log(CONSOLE_PREFIX + `Updating property with key ${key}, type ${type}, value ${value} and secondValue ${secondValue}.`);
192
+
193
+ // Increment vector clock
194
+ const counter = this.#vectorClock.get(this.#replicaId) || 0;
195
+ this.#vectorClock.set(this.#replicaId, counter + 1);
196
+
197
+ // Assign shallow copy of current ops or fall back to empty array
198
+ const currentOps = [...(this.#keyOperations.get(key) || [])];
199
+
200
+ // Add operation
201
+ const newOp = this.#createOperation({ type, value, secondValue }, Array.from(this.#vectorClock.entries()));
202
+ currentOps.push(newOp);
203
+ this.#keyOperations.set(key, currentOps); // Update the operations (no need to sort via vector clock since local updates are always the latest)
204
+ this.#processLocalProperty(key); // Process local value
205
+ this.#checkGarbageCollection();
206
+
207
+ // Return the property update with the new operation (this can be imported using importPropertyUpdate)
208
+ return {
209
+ key,
210
+ operation: { ...newOp },
211
+ vectorClock: Array.from(this.#vectorClock.entries())
212
+ };
213
+
214
+ } catch (error) {
215
+ console.error(CONSOLE_PREFIX + `Failed to add operation for key ${key}:`, error);
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Check if garbage collected can be performed & run it
221
+ */
222
+ #checkGarbageCollection() {
223
+ const MIN_GC_DELAY = 1000; // Minimum 1s delay between garbage collection runs
224
+
225
+ try {
226
+ if (Date.now() - this.#lastGCCheck < MIN_GC_DELAY) return;
227
+ this.#lastGCCheck = Date.now()
228
+
229
+ for (const [key, operations] of this.#keyOperations.entries()) {
230
+ if (operations?.length < 5) continue; // Min op amount per key for garbage collection to run (otherwise not worth it)
231
+
232
+ let retainCount = operations.length;
233
+ operations.forEach((op, index) => {
234
+ // Count how many ops, from last to latest, are older than the min-age in a row
235
+ if (op.uuid && this.#opUuidTimestamp.has(op.uuid) && (Date.now() - this.#opUuidTimestamp.get(op.uuid)) > HEARTBEAT_INTERVAL) {
236
+ const removeCount = index + 1;
237
+ retainCount = operations.length - removeCount;
238
+ this.#opUuidTimestamp.delete(op.uuid); // Remove from timestamps if set to be deleted
239
+ }
240
+ });
241
+
242
+ if (retainCount < operations.length) {
243
+ if (this.#debug) console.log(CONSOLE_PREFIX + `Running garbage collection for key ${key} with current operations:`, operations);
244
+ const retainOps = operations.slice(-retainCount); // Newest ops
245
+ const removeOps = operations.slice(0, -retainCount); // Oldest ops (start = idx 0, end = retainCount counted from right side)
246
+ const baselineVectorClock = removeOps[removeOps.length - 1]?.vectorClock || []; // Use the vector clock from the last operation that we remove/overwrite
247
+
248
+ // Calculate the value at the point where retained operations start
249
+ let baselineValue = null;
250
+ for (const op of removeOps) {
251
+ if (!op.data) continue;
252
+ baselineValue = this.#handleOperation(
253
+ baselineValue,
254
+ op.data.type,
255
+ op.data.value,
256
+ op.data.secondValue
257
+ );
258
+ }
259
+
260
+ // Create a compact operation with baseline value and appropriate vector clock
261
+ const compactOp = this.#createOperation({ type: "set", value: baselineValue }, baselineVectorClock);
262
+
263
+ // Debug log
264
+ if (this.#debug) console.log(CONSOLE_PREFIX + "Operations after garbage collection for this key:", [compactOp, ...retainOps]);
265
+
266
+ // Combine compact op with retained operations
267
+ this.#keyOperations.set(key, [compactOp, ...retainOps]);
268
+ }
269
+ }
270
+ } catch (error) {
271
+ console.error(CONSOLE_PREFIX + "Error running garbage collection:", error);
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Create an operation object
277
+ * @param {OperationData} data - Operation data, consists of type, value and secondValue
278
+ * @param {VectorClockEntries} vectorClock - Vector clock of the replica/client
279
+ * @returns {Operation} - Operation object
280
+ */
281
+ #createOperation(data, vectorClock) {
282
+ const newOperation = {
283
+ data,
284
+ vectorClock,
285
+ source: this.#replicaId,
286
+ uuid: crypto.randomUUID()
287
+ }
288
+ this.#opUuidTimestamp.set(newOperation.uuid, Date.now());
289
+ return newOperation;
290
+ }
291
+
292
+ /**
293
+ * Process a property's value by applying all operations and set it in the property store
294
+ * @param {string} key - Storage key
295
+ */
296
+ #processLocalProperty(key) {
297
+ try {
298
+ const ops = this.#keyOperations.get(key);
299
+ if (!ops?.length) {
300
+ delete this.#propertyStore[key]; // Delete empty key
301
+ return;
302
+ }
303
+
304
+ // Apply all operations in order
305
+ let value = null;
306
+
307
+ for (const op of ops) {
308
+ if (!op.data) continue;
309
+ value = this.#handleOperation(
310
+ value,
311
+ op.data.type,
312
+ op.data.value,
313
+ op.data.secondValue
314
+ );
315
+ }
316
+
317
+ this.#propertyStore[key] = value; // Save locally
318
+ } catch (error) {
319
+ console.error(CONSOLE_PREFIX + `Failed to process property for key ${key}:`, error);
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Sort operations by vector clock (causal order)
325
+ * @param {Operation[]} operations - Full operations that include vector clocks, data and a source
326
+ * @returns {Operation[]} - Sorted operations
327
+ */
328
+ #sortByVectorClock(operations) {
329
+ return [...operations].sort((a, b) => {
330
+ const clockA = new Map(a.vectorClock || []);
331
+ const clockB = new Map(b.vectorClock || []);
332
+
333
+ // First sort by causal relationship
334
+ let aGreater = false, bGreater = false;
335
+
336
+ const allIds = new Set([...clockA.keys(), ...clockB.keys()]);
337
+ for (const id of allIds) {
338
+ const countA = clockA.get(id) || 0;
339
+ const countB = clockB.get(id) || 0;
340
+
341
+ if (countA > countB) aGreater = true;
342
+ if (countA < countB) bGreater = true;
343
+ }
344
+
345
+ // Determine causal relationship (if one is greater, and the other is smaller, it's clear)
346
+ if (aGreater && !bGreater) return 1; // a happens after b
347
+ if (!aGreater && bGreater) return -1; // a happens before b
348
+
349
+ // Concurrent (unclear): Compare highest counter in each (sort by which one is ahead)
350
+ const maxA = Math.max(...Array.from(clockA.values()), 0);
351
+ const maxB = Math.max(...Array.from(clockB.values()), 0);
352
+ if (maxA !== maxB) return maxA - maxB;
353
+
354
+ // Final tiebreak: Sort by ID of Replica that actually generated the operation
355
+ return a.source.localeCompare(b.source);
356
+ });
357
+ }
358
+
359
+ /**
360
+ * Handle an operation
361
+ * @param {*} curValue - Current value of the storage key
362
+ * @param {PropertyUpdateType} type - Operation type
363
+ * @param {*} value - Value
364
+ * @param {*} [secondValue] - Second value (needed for some operations)
365
+ * @returns {*} - Value after the operation
366
+ */
367
+ #handleOperation(curValue, type, value, secondValue) {
368
+ try {
369
+ // Deep copy to avoid reference issues in case value is or contains object(s)
370
+ curValue = structuredClone(curValue);
371
+
372
+ // Set operation
373
+ if (type === "set") return value;
374
+
375
+ // Number increment operation
376
+ if (type === "number-increment") {
377
+ if (!Number.isFinite(curValue)) curValue = 0; // Auto-convert to number if current value isn't a finite number
378
+ if (Number.isFinite(value)) curValue += value; // Negative values are allowed, increment can be used for decrementing (non-finite values are ignored)
379
+ }
380
+
381
+ // Array operations
382
+ if (type.startsWith("array")) {
383
+ if (!Array.isArray(curValue)) curValue = []; // Auto-convert to array if current value isn't one
384
+
385
+ // Comparison function
386
+ const isObject = typeof value === "object" && value !== null;
387
+ /** @param {*} item */
388
+ const compare = (item) => {
389
+ if (isObject && typeof item === "object" && item !== null) return JSON.stringify(item) === JSON.stringify(value); // Deep comparison
390
+ return item === value; // Value comparison
391
+ };
392
+
393
+ switch (type) {
394
+ case "array-add":
395
+ curValue.push(value);
396
+ break;
397
+ case "array-add-unique":
398
+ if (!curValue.some(compare)) curValue.push(value);
399
+ break;
400
+ case "array-remove-matching":
401
+ curValue = curValue.filter((/** @type {*} */ item) => !compare(item));
402
+ break;
403
+ case "array-update-matching": {
404
+ const index = curValue.findIndex(compare);
405
+ if (index !== -1) curValue[index] = secondValue;
406
+ break;
407
+ }
408
+ }
409
+ }
410
+
411
+ // Object operations
412
+ if (type.startsWith("object")) {
413
+ if (typeof curValue !== "object" || curValue === null || Array.isArray(curValue)) curValue = {}; // Auto-convert to object if current value isn't one
414
+
415
+ switch (type) {
416
+ case "object-set-key":
417
+ if (value !== "__proto__") curValue[value] = secondValue; // Assigning __proto__ is not allowed as it would break things
418
+ break;
419
+ case "object-remove-key":
420
+ delete curValue[value];
421
+ break;
422
+ }
423
+ }
424
+
425
+ return curValue;
426
+
427
+ } catch (error) {
428
+ console.error(CONSOLE_PREFIX + `Failed to handle operation with curValue ${curValue}:`, error);
429
+ return curValue;
430
+ }
431
+ }
432
+
433
+ /**
434
+ * Remove HTML to prevent XSS and enforce size limits
435
+ * @param {*} value - Value to sanitize
436
+ * @returns {*} - Sanitized value
437
+ */
438
+ #sanitizeValue(value) {
439
+ // Check total serialized size
440
+ const jsonString = JSON.stringify(value);
441
+ if (jsonString?.length > 50000) throw new Error("Value too large!"); // 50KB limit
442
+
443
+ if (typeof value === "string") return (value.includes("<") || value.includes(">")) ? value.replace(/[<>]/g, "") : value;
444
+ if (Array.isArray(value)) return value.map(item => this.#sanitizeValue(item));
445
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, this.#sanitizeValue(v)]));
446
+ return value;
447
+ }
448
+
449
+ /**
450
+ * Get property store
451
+ * @returns {Record<string, any>} - Object with key value pairs
452
+ */
453
+ get propertyStore() {
454
+ try {
455
+ return structuredClone(this.#propertyStore); // Return deep clone
456
+ } catch {
457
+ console.error(CONSOLE_PREFIX + "Failed to parse property store")
458
+ return {}
459
+ }
460
+ }
461
+
462
+ /**
463
+ * Check for changes in the local property store
464
+ * @returns {boolean} - Check if any values changed going from the last to the current property update
465
+ */
466
+ get didPropertiesChange() {
467
+ try {
468
+ if (JSON.stringify(this.#propertyStore) !== JSON.stringify(this.#lastPropertyStore)) {
469
+ this.#lastPropertyStore = structuredClone(this.#propertyStore);
470
+ return true;
471
+ }
472
+ } catch {
473
+ console.error(CONSOLE_PREFIX + "Failed to stringify and compare property stores")
474
+ }
475
+ return false;
476
+ }
477
+
478
+ /**
479
+ * Export state (can be imported using importState, converts the maps to arrays for serialization)
480
+ * @returns {CRDTState} - State
481
+ */
482
+ get state() {
483
+ return {
484
+ keyOperations: [...this.#keyOperations.entries()],
485
+ vectorClock: [...this.#vectorClock.entries()]
486
+ };
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Get the operation details from a property update
492
+ * @param {PropertyUpdate} update - Property update
493
+ * @returns {{key: string | undefined, type: PropertyUpdateType | undefined, value: *, secondValue: *}} - Operation details
494
+ */
495
+ export function getUpdateDetails(update) {
496
+ const data = update?.operation?.data;
497
+ return { key: update?.key, type: data?.type, value: data?.value, secondValue: data?.secondValue };
498
+ }
@@ -0,0 +1,97 @@
1
+ export type PropertyUpdateType = import("../universal/crdtManager.js").PropertyUpdateType;
2
+ export type PlaySocketOptions = {
3
+ /**
4
+ * - WebSocket server endpoint (e.g. 'wss://example.com/socket')
5
+ */
6
+ endpoint: string;
7
+ /**
8
+ * - Custom registration data
9
+ */
10
+ customData?: object;
11
+ /**
12
+ * - Enable debug logging
13
+ */
14
+ debug?: boolean;
15
+ };
16
+ export type ServerMessage = {
17
+ /**
18
+ * - Message type
19
+ */
20
+ type: string;
21
+ };
22
+ export type PendingPromise = {
23
+ resolve: (value?: any) => void;
24
+ reject: (reason?: any) => void;
25
+ };
26
+ /** @typedef {import("../universal/crdtManager.js").PropertyUpdateType} PropertyUpdateType */
27
+ /**
28
+ * @typedef {object} PlaySocketOptions
29
+ * @property {string} endpoint - WebSocket server endpoint (e.g. 'wss://example.com/socket')
30
+ * @property {object} [customData] - Custom registration data
31
+ * @property {boolean} [debug=false] - Enable debug logging
32
+ */
33
+ /**
34
+ * @typedef {object} ServerMessage
35
+ * @property {string} type - Message type
36
+ */
37
+ /** @typedef {{ resolve: (value?: any) => void, reject: (reason?: any) => void }} PendingPromise */
38
+ /**
39
+ * PlaySocket Client
40
+ */
41
+ export default class PlaySocket {
42
+ #private;
43
+ /**
44
+ * Create a new PlaySocket instance
45
+ * @param {string} [id] - Unique identifier for this client (assigned by the server if omitted)
46
+ * @param {PlaySocketOptions} options - Connection options
47
+ */
48
+ constructor(id?: string, options?: PlaySocketOptions);
49
+ /**
50
+ * Register an event callback
51
+ * @param {string} event - Event name
52
+ * @param {Function} callback - Callback function
53
+ * @returns {Function} - Unsubscribe
54
+ */
55
+ onEvent(event: string, callback: Function): Function;
56
+ /**
57
+ * Initialize the PlaySocket instance by connecting to the WS server
58
+ * @returns {Promise<string>} Resolves with the client ID when the connection is established
59
+ */
60
+ init(): Promise<string>;
61
+ /**
62
+ * Create a new room and become host
63
+ * @param {object} [initialStorage] - Initial state
64
+ * @param {number} [size] - Max number of participants
65
+ * @returns {Promise<string>} Resolves with room ID
66
+ */
67
+ createRoom(initialStorage?: object, size?: number): Promise<string>;
68
+ /**
69
+ * Join an existing room
70
+ * @param {string} roomId - ID of the room
71
+ * @returns {Promise<void>} - Resolves when connected
72
+ */
73
+ joinRoom(roomId: string): Promise<void>;
74
+ /**
75
+ * Update a value in the shared storage
76
+ * @param {string} key - Storage key
77
+ * @param {PropertyUpdateType} type - Operation type
78
+ * @param {*} value - Value
79
+ * @param {*} [secondValue] - Second value (needed for some operations)
80
+ */
81
+ updateStorage(key: string, type: PropertyUpdateType, value: any, secondValue?: any): void;
82
+ /**
83
+ * Send a custom request to the server
84
+ * @param {string} name - Name of the request
85
+ * @param {*} [data] - Custom data
86
+ * @returns {Promise<void>} - Request promise
87
+ */
88
+ sendRequest(name: string, data?: any): Promise<void>;
89
+ /**
90
+ * Destroy the PlaySocket instance and disconnect
91
+ */
92
+ destroy(): void;
93
+ get participantCount(): number;
94
+ get storage(): Record<string, any>;
95
+ get isHost(): boolean;
96
+ get id(): string;
97
+ }