playsocketjs 5.3.2 → 6.0.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 +20 -6
- package/package.json +1 -1
- package/src/client/client.js +15 -22
- package/src/server/server.js +138 -140
- package/src/server/ws-extensions.d.ts +1 -1
- package/src/universal/constants.js +1 -1
- package/types/client/client.d.ts +1 -2
- package/types/server/server.d.ts +5 -10
- package/types/universal/constants.d.ts +1 -1
package/README.md
CHANGED
|
@@ -127,7 +127,7 @@ new PlaySocket(id?: string, options: PlaySocketOptions)
|
|
|
127
127
|
| Name | Parameters | Return type | Description |
|
|
128
128
|
|--------|------------|-------------|-------------|
|
|
129
129
|
| `init()` | - | `Promise<string>` | Initialize the WebSocket connection, resolves with the client ID. |
|
|
130
|
-
| `createRoom()` | `initialStorage?: object
|
|
130
|
+
| `createRoom()` | `initialStorage?: object` | `Promise<string>` | Create a new room, resolves with the room ID. |
|
|
131
131
|
| `joinRoom()` | `roomId: string` | `Promise<void>` | Join an existing room. |
|
|
132
132
|
| `destroy()` | `reason?: string` | `void` | Leave room, close the connection, and destroy the instance. |
|
|
133
133
|
| `updateStorage()` | `key: string, type: string, value: any, secondValue?: any` | `void` | Update a key in the shared storage. |
|
|
@@ -243,6 +243,16 @@ server.onEvent("requestReceived", async ({ roomId, clientId, name, data }) => {
|
|
|
243
243
|
});
|
|
244
244
|
```
|
|
245
245
|
|
|
246
|
+
Limiting the room size:
|
|
247
|
+
|
|
248
|
+
```javascript
|
|
249
|
+
const server = new PlaySocketServer();
|
|
250
|
+
|
|
251
|
+
server.onEvent("clientJoinRequested", (clientId, roomId) => {
|
|
252
|
+
if (server.rooms[roomId].participants.length >= 10) return "Room full";
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
246
256
|
Combining PlaySocket with a custom authentication system:
|
|
247
257
|
|
|
248
258
|
```javascript
|
|
@@ -253,12 +263,16 @@ server.onEvent("clientRegistrationRequested", async (clientId, data) => {
|
|
|
253
263
|
try {
|
|
254
264
|
// Your custom auth logic...
|
|
255
265
|
// For example, data could contain a token
|
|
256
|
-
authedClients.push(clientId);
|
|
257
266
|
} catch (error) {
|
|
258
267
|
return "Error occurred during auth"; // Blocks the registration
|
|
259
268
|
}
|
|
260
269
|
});
|
|
261
270
|
|
|
271
|
+
// On successful registration, mark the client as authed
|
|
272
|
+
server.onEvent("clientRegistered", (clientId, data) => {
|
|
273
|
+
authedClients.push(clientId);
|
|
274
|
+
});
|
|
275
|
+
|
|
262
276
|
server.onEvent("clientDisconnected", async (clientId) => {
|
|
263
277
|
const removeIndex = authedClients.indexOf(clientId);
|
|
264
278
|
if (removeIndex !== -1) authedClients.splice(removeIndex, 1);
|
|
@@ -308,18 +322,18 @@ The callback signature is `callback(verified, code?, message?)` where `code` ref
|
|
|
308
322
|
#### Methods
|
|
309
323
|
|
|
310
324
|
> [!IMPORTANT]
|
|
311
|
-
> Rooms created by the server
|
|
325
|
+
> Rooms created by the server have no host (`host` is null). Such rooms are not auto-deleted when all participants have left, so you need to take care of their lifecycle.
|
|
312
326
|
|
|
313
327
|
| Name | Parameters | Return type | Description |
|
|
314
328
|
|--------|------------|-------------|-------------|
|
|
315
|
-
| `stop()` | - | `void` | Closes active client connections and the underlying http server (if it's standalone). |
|
|
329
|
+
| `stop()` | - | `void` | Closes active client connections (without triggering events) and the underlying http server (if it's standalone). |
|
|
316
330
|
| `kick()` | `clientId: string, reason?: string` | `void` | Kick a client by their client ID. |
|
|
317
331
|
| `move()` | `clientId: string, roomId: string` | `void` | Move a client that is already in a room to a different room. |
|
|
318
332
|
| `onEvent()` | `event: string, callback: Function` | `() => void` | Register a server-side event callback. Returns unsubscribe function. |
|
|
319
333
|
| `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. |
|
|
320
334
|
| `getUpdateDetails()` | `update: object` | `object` | Get the details (`key`, `type`, `value` and `secondValue`) of a storage update for building validation logic. |
|
|
321
335
|
| `updateRoomStorage()` | `roomId: string, key: string, type: string, value: any, secondValue?: any` | `void` | Update a key in the shared storage of a room. |
|
|
322
|
-
| `createRoom()` | `initialStorage?: object
|
|
336
|
+
| `createRoom()` | `initialStorage?: object` | `object` | Create a server-owned room (returns object containing room ID and state).|
|
|
323
337
|
| `destroyRoom()` | `roomId: string` | `void` | Destroy a room & kick all participants. |
|
|
324
338
|
|
|
325
339
|
#### Events
|
|
@@ -335,7 +349,7 @@ The callback signature is `callback(verified, code?, message?)` where `code` ref
|
|
|
335
349
|
| `roomCreated` | `roomId: string` | Room was created. | - |
|
|
336
350
|
| `roomDestroyed` | `roomId: string` | Room was destroyed. | - |
|
|
337
351
|
| `roomCreationRequested` | `{clientId: string, initialStorage: object}` | Client requested to create room. | Return `object` to override initial storage, `false` or rejection reason `string` to block. |
|
|
338
|
-
| `storageUpdated` | `{clientId: string, roomId: string, update: object, storage: object}` | Room storage updated. | - |
|
|
352
|
+
| `storageUpdated` | `{clientId: string \| null, roomId: string, update: object, storage: object}` | Room storage updated (`null` as the clientId for server-authored updates). | - |
|
|
339
353
|
| `storageUpdateRequested` | `{clientId: string, roomId: string, update: object, storage: object}` | Client requested storage update. | Return `false` or rejection reason `string` to block the update. Callback must be synchronous. |
|
|
340
354
|
| `requestReceived` | `{clientId: string, roomId?: string, name: string, data?: any}` | Request from client. | Return `false` or rejection reason `string` to block. |
|
|
341
355
|
|
package/package.json
CHANGED
package/src/client/client.js
CHANGED
|
@@ -52,11 +52,10 @@ export default class PlaySocket {
|
|
|
52
52
|
// Room properties
|
|
53
53
|
/** @type {string | null | undefined} */
|
|
54
54
|
#roomHost;
|
|
55
|
-
/** @type {
|
|
56
|
-
#
|
|
55
|
+
/** @type {string | null} */
|
|
56
|
+
#roomId = null; // ID of the room the client is currently in
|
|
57
57
|
#participantCount = 0;
|
|
58
58
|
#crdtManager;
|
|
59
|
-
#roomVersion = 0; // Update version (used to compare local vs. remote state to detect package loss)
|
|
60
59
|
|
|
61
60
|
// Event handling
|
|
62
61
|
/** @type {Map<string, Function[]>} */
|
|
@@ -227,11 +226,10 @@ export default class PlaySocket {
|
|
|
227
226
|
|
|
228
227
|
case "join_accepted":
|
|
229
228
|
if (this.#debug) console.log(LOG_PREFIX + "State received for join:", message.state);
|
|
230
|
-
this.#
|
|
229
|
+
this.#roomId = message.roomId;
|
|
231
230
|
this.#crdtManager.importState(message.state);
|
|
232
231
|
this.#participantCount = message.participantCount;
|
|
233
232
|
this.#roomHost = message.host;
|
|
234
|
-
this.#roomVersion = message.version;
|
|
235
233
|
this.#triggerEvent("storageUpdated", this.storage);
|
|
236
234
|
if (this.#pendingJoin) {
|
|
237
235
|
this.#triggerEvent("status", "Connected to room.");
|
|
@@ -253,13 +251,16 @@ export default class PlaySocket {
|
|
|
253
251
|
if (message.roomData) {
|
|
254
252
|
if (this.#debug) console.log(LOG_PREFIX + "State received for reconnect:", message.roomData.state);
|
|
255
253
|
this.#crdtManager.importState(message.roomData.state);
|
|
256
|
-
this.#roomVersion = message.roomData.version;
|
|
257
254
|
this.#participantCount = message.roomData.participantCount;
|
|
258
255
|
this.#setHost(message.roomData.host); // Set host before in case there are .isHost checks in the storageUpdate fallback
|
|
259
256
|
this.#triggerEvent("storageUpdated", this.storage);
|
|
260
|
-
|
|
261
|
-
// If
|
|
262
|
-
|
|
257
|
+
|
|
258
|
+
// If the client was moved to another room while it was disconnected
|
|
259
|
+
if (this.#roomId && message.roomData.roomId !== this.#roomId) {
|
|
260
|
+
this.#triggerEvent("status", "Moved to room.");
|
|
261
|
+
this.#triggerEvent("moved", message.roomData.roomId);
|
|
262
|
+
}
|
|
263
|
+
this.#roomId = message.roomData.roomId;
|
|
263
264
|
}
|
|
264
265
|
this.#triggerEvent("status", "Reconnected.");
|
|
265
266
|
if (this.#pendingReconnect) this.#pendingReconnect.resolve();
|
|
@@ -270,7 +271,7 @@ export default class PlaySocket {
|
|
|
270
271
|
break;
|
|
271
272
|
|
|
272
273
|
case "room_created":
|
|
273
|
-
this.#
|
|
274
|
+
this.#roomId = message.roomId;
|
|
274
275
|
this.#participantCount = message.participantCount;
|
|
275
276
|
this.#triggerEvent("status", "Room created.");
|
|
276
277
|
this.#crdtManager.importState(message.state);
|
|
@@ -283,14 +284,9 @@ export default class PlaySocket {
|
|
|
283
284
|
break;
|
|
284
285
|
|
|
285
286
|
case "property_updated":
|
|
286
|
-
this.#roomVersion++; // Increment room version
|
|
287
287
|
if (this.#debug) console.log(LOG_PREFIX + "Property update received:", message.update);
|
|
288
288
|
this.#crdtManager.importPropertyUpdate(message.update);
|
|
289
289
|
if (this.#crdtManager.didPropertiesChange) this.#triggerEvent("storageUpdated", this.storage);
|
|
290
|
-
if (this.#roomVersion != message.version && this.#initialized && this.#socket?.readyState === WebSocket.OPEN) {
|
|
291
|
-
console.error(ERROR_PREFIX + "Detected skipped property update, forcing reconnect");
|
|
292
|
-
this.#socket?.close();
|
|
293
|
-
}
|
|
294
290
|
break;
|
|
295
291
|
|
|
296
292
|
case "property_update_rejected":
|
|
@@ -410,10 +406,9 @@ export default class PlaySocket {
|
|
|
410
406
|
/**
|
|
411
407
|
* Create a new room and become host
|
|
412
408
|
* @param {object | null} [initialStorage] - Initial state
|
|
413
|
-
* @param {number} [size] - Max number of participants
|
|
414
409
|
* @returns {Promise<string>} Resolves with room ID
|
|
415
410
|
*/
|
|
416
|
-
async createRoom(initialStorage
|
|
411
|
+
async createRoom(initialStorage) {
|
|
417
412
|
if (!this.#initialized) throw new Error("Not initialized");
|
|
418
413
|
|
|
419
414
|
return /** @type {Promise<string>} */ (Promise.race([
|
|
@@ -422,8 +417,7 @@ export default class PlaySocket {
|
|
|
422
417
|
this.#pendingCreate = { resolve, reject };
|
|
423
418
|
this.#sendToServer({
|
|
424
419
|
type: "create_room",
|
|
425
|
-
initialStorage
|
|
426
|
-
size
|
|
420
|
+
initialStorage
|
|
427
421
|
});
|
|
428
422
|
}),
|
|
429
423
|
this.#createTimeout("Room creation")
|
|
@@ -465,7 +459,7 @@ export default class PlaySocket {
|
|
|
465
459
|
* @param {*} [secondValue] - Second value (needed for some operations)
|
|
466
460
|
*/
|
|
467
461
|
updateStorage(key, type, value, secondValue) {
|
|
468
|
-
if (!this.#
|
|
462
|
+
if (!this.#roomId) {
|
|
469
463
|
console.error(ERROR_PREFIX + "Failed to update storage, not in a room")
|
|
470
464
|
return;
|
|
471
465
|
};
|
|
@@ -528,11 +522,10 @@ export default class PlaySocket {
|
|
|
528
522
|
// Reset state
|
|
529
523
|
clearTimeout(this.#reconnectTimeout);
|
|
530
524
|
this.#roomHost = null;
|
|
531
|
-
this.#
|
|
525
|
+
this.#roomId = null;
|
|
532
526
|
this.#participantCount = 0;
|
|
533
527
|
this.#isReconnecting = false;
|
|
534
528
|
this.#reconnectCount = 0;
|
|
535
|
-
this.#roomVersion = 0;
|
|
536
529
|
|
|
537
530
|
// Reject pending promises if currently active
|
|
538
531
|
if (this.#pendingJoin) this.#pendingJoin.reject(new Error("Destroyed"));
|
package/src/server/server.js
CHANGED
|
@@ -4,7 +4,6 @@ import { encode, decode } from "@msgpack/msgpack";
|
|
|
4
4
|
import CRDTManager, { getUpdateDetails } from "../universal/crdtManager.js";
|
|
5
5
|
import { HEARTBEAT_INTERVAL, VERSION, ERROR_PREFIX, WARNING_PREFIX, LOG_PREFIX } from "../universal/constants.js";
|
|
6
6
|
|
|
7
|
-
const MAX_ROOM_SIZE = 500;
|
|
8
7
|
export const RECONNECT_GRACE_PERIOD = 5000; // Exported for use in tests
|
|
9
8
|
|
|
10
9
|
/**
|
|
@@ -26,8 +25,7 @@ export const RECONNECT_GRACE_PERIOD = 5000; // Exported for use in tests
|
|
|
26
25
|
/**
|
|
27
26
|
* @typedef {object} Room
|
|
28
27
|
* @property {string[]} participants - Client IDs of the room participants
|
|
29
|
-
* @property {string | null} host - Client ID of the room host (
|
|
30
|
-
* @property {number} size - Max. number of participants
|
|
28
|
+
* @property {string | null} host - Client ID of the room host (null for server-created rooms)
|
|
31
29
|
* @property {CRDTManager} crdtManager - CRDT manager holding the room storage
|
|
32
30
|
*/
|
|
33
31
|
|
|
@@ -70,12 +68,10 @@ export default class PlaySocketServer {
|
|
|
70
68
|
/** @type {Map<string, Function[]>} */
|
|
71
69
|
#callbacks = new Map(); // Event -> [callback functions]
|
|
72
70
|
#heartbeatInterval;
|
|
73
|
-
/** @type {Map<string, { timeout: ReturnType<typeof setTimeout
|
|
74
|
-
#pendingDisconnects = new Map(); // ClientId -> {timeout}
|
|
71
|
+
/** @type {Map<string, { timeout: ReturnType<typeof setTimeout>, onTimeout: () => void }>} */
|
|
72
|
+
#pendingDisconnects = new Map(); // ClientId -> {timeout, onTimeout}
|
|
75
73
|
/** @type {Map<string, string>} */
|
|
76
74
|
#clientTokens = new Map(); // ClientId -> Token
|
|
77
|
-
/** @type {Map<string, number>} */
|
|
78
|
-
#roomVersions = new Map(); // RoomId -> Version
|
|
79
75
|
|
|
80
76
|
// Debug
|
|
81
77
|
#debug = false;
|
|
@@ -172,12 +168,6 @@ export default class PlaySocketServer {
|
|
|
172
168
|
return;
|
|
173
169
|
}
|
|
174
170
|
|
|
175
|
-
// Register client ID if provided & check for a duplicate
|
|
176
|
-
if (data.id && (this.#clients.get(data.id) || this.#pendingDisconnects.has(data.id) || data.id === "server")) {
|
|
177
|
-
ws.send(encode({ type: "registration_failed", reason: "ID is taken" }), { binary: true });
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
171
|
// Generate client ID if none provided
|
|
182
172
|
if (!data.id) {
|
|
183
173
|
for (let i = 0; i < 50; i++) {
|
|
@@ -203,6 +193,15 @@ export default class PlaySocketServer {
|
|
|
203
193
|
return;
|
|
204
194
|
}
|
|
205
195
|
|
|
196
|
+
// Silently abort if the socket closed or if that same socket has already registered (e.g. duplicate registration message)
|
|
197
|
+
if (ws.readyState !== ws.OPEN || ws.clientId) return;
|
|
198
|
+
|
|
199
|
+
// Abort if the ID is taken (or was taken during the async event callback)
|
|
200
|
+
if (this.#clients.has(data.id) || this.#pendingDisconnects.has(data.id)) {
|
|
201
|
+
ws.send(encode({ type: "registration_failed", reason: "ID is taken" }), { binary: true });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
206
205
|
ws.clientId = data.id; // Adds the provided id to the ws object as clientId
|
|
207
206
|
this.#clients.set(data.id, ws);
|
|
208
207
|
const sessionToken = this.#generateSessionToken();
|
|
@@ -213,6 +212,8 @@ export default class PlaySocketServer {
|
|
|
213
212
|
}
|
|
214
213
|
|
|
215
214
|
case "reconnect": {
|
|
215
|
+
if (ws.clientId) return; // Clients must reconnect with a fresh, unregistered socket
|
|
216
|
+
|
|
216
217
|
// If user is pending disconnect or still on a dropped connection, respond (otherwise it's too late)
|
|
217
218
|
const pendingDisconnect = this.#pendingDisconnects.get(data.id);
|
|
218
219
|
const staleSocket = this.#clients.get(data.id); // Client requests reconnect even though the server thinks they are still connected
|
|
@@ -228,9 +229,8 @@ export default class PlaySocketServer {
|
|
|
228
229
|
clearTimeout(pendingDisconnect?.timeout);
|
|
229
230
|
this.#pendingDisconnects.delete(data.id);
|
|
230
231
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
staleSocket.clientId = null; // So the close skips disconnection (handleDisconnection exits if the client ID is falsy)
|
|
232
|
+
if (staleSocket) {
|
|
233
|
+
staleSocket.clientId = null; // To skip handleDisconnection() on socket "close"
|
|
234
234
|
staleSocket.terminate();
|
|
235
235
|
}
|
|
236
236
|
|
|
@@ -243,18 +243,19 @@ export default class PlaySocketServer {
|
|
|
243
243
|
const formerRoomId = this.#clientRooms.get(data.id);
|
|
244
244
|
const formerRoom = this.#rooms[formerRoomId];
|
|
245
245
|
if (formerRoom) {
|
|
246
|
-
// If the room has no host (they were the only one and disconnected), restore them as host
|
|
247
|
-
if (formerRoom.host === null) formerRoom.host = data.id;
|
|
248
246
|
if (this.#debug) console.log(LOG_PREFIX + `State sent for reconnection for room ${formerRoomId}:`, formerRoom.crdtManager.state);
|
|
249
247
|
roomData = {
|
|
248
|
+
roomId: formerRoomId,
|
|
250
249
|
state: formerRoom.crdtManager.state,
|
|
251
250
|
participantCount: formerRoom.participants.length,
|
|
252
|
-
host: formerRoom.host
|
|
253
|
-
version: this.#roomVersions.get(formerRoomId)
|
|
251
|
+
host: formerRoom.host
|
|
254
252
|
}
|
|
255
253
|
}
|
|
256
254
|
|
|
257
255
|
ws.send(encode({ type: "reconnected", roomData }), { binary: true });
|
|
256
|
+
|
|
257
|
+
// If the host has disconnected or is pending disconnect, migrate host (after reconnected so hostMigrated event fires correctly on client)
|
|
258
|
+
if (formerRoom && formerRoom.host !== null && !this.#clients.has(formerRoom.host)) this.#migrateHost(formerRoomId, formerRoom.host);
|
|
258
259
|
} else {
|
|
259
260
|
ws.send(encode({ type: "reconnection_failed", reason: "Client unknown to server" }), { binary: true });
|
|
260
261
|
}
|
|
@@ -264,37 +265,39 @@ export default class PlaySocketServer {
|
|
|
264
265
|
case "create_room": {
|
|
265
266
|
if (!ws.clientId) return;
|
|
266
267
|
|
|
267
|
-
if (this.#clientRooms.get(ws.clientId)) {
|
|
268
|
-
ws.send(encode({
|
|
269
|
-
type: "room_creation_failed",
|
|
270
|
-
reason: "Already in a room"
|
|
271
|
-
}), { binary: true });
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
268
|
// Event callback with potential initial storage modifications
|
|
276
269
|
const reviewedStorage = await this.#triggerEvent("roomCreationRequested", { clientId: ws.clientId, initialStorage: structuredClone({ ...data.initialStorage }) });
|
|
277
270
|
if (typeof reviewedStorage === "object") data.initialStorage = reviewedStorage;
|
|
271
|
+
|
|
272
|
+
// Verify client is still connected and abort if not
|
|
273
|
+
if (!this.#getClient(ws.clientId)) {
|
|
274
|
+
if (this.#debug) console.log(LOG_PREFIX + `Room creation cancelled, client ${ws.clientId} disconnected during event callback`);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
278
|
if (reviewedStorage === false || typeof reviewedStorage === "string") {
|
|
279
|
-
ws.send(encode({ type: "room_creation_failed", reason: typeof reviewedStorage === "string" ? reviewedStorage : null }), { binary: true });
|
|
279
|
+
this.#getClient(ws.clientId)?.send(encode({ type: "room_creation_failed", reason: typeof reviewedStorage === "string" ? reviewedStorage : null }), { binary: true });
|
|
280
280
|
return;
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
try {
|
|
284
|
-
//
|
|
285
|
-
if (
|
|
286
|
-
|
|
284
|
+
// Ensure client isn't already in another room (after the async event callback)
|
|
285
|
+
if (this.#clientRooms.get(ws.clientId)) {
|
|
286
|
+
this.#getClient(ws.clientId)?.send(encode({
|
|
287
|
+
type: "room_creation_failed",
|
|
288
|
+
reason: "Already in a room"
|
|
289
|
+
}), { binary: true });
|
|
287
290
|
return;
|
|
288
291
|
}
|
|
289
292
|
|
|
290
|
-
const newRoom = this.#createRoom(ws.clientId, data.initialStorage
|
|
293
|
+
const newRoom = this.#createRoom(ws.clientId, data.initialStorage); // Create room
|
|
291
294
|
|
|
292
295
|
this.#rooms[newRoom.id].participants.push(ws.clientId) // Add client to the room
|
|
293
296
|
this.#clientRooms.set(ws.clientId, newRoom.id); // Add client to the client-room map
|
|
294
|
-
ws.send(encode({ type: "room_created", state: newRoom.state, roomId: newRoom.id, participantCount: this.#rooms[newRoom.id].participants.length }), { binary: true });
|
|
297
|
+
this.#getClient(ws.clientId)?.send(encode({ type: "room_created", state: newRoom.state, roomId: newRoom.id, participantCount: this.#rooms[newRoom.id].participants.length }), { binary: true });
|
|
295
298
|
} catch (error) {
|
|
296
299
|
console.error(ERROR_PREFIX + "Error creating room:", error);
|
|
297
|
-
ws.send(encode({ type: "room_creation_failed", reason: error.message }), { binary: true });
|
|
300
|
+
this.#getClient(ws.clientId)?.send(encode({ type: "room_creation_failed", reason: error.message }), { binary: true });
|
|
298
301
|
}
|
|
299
302
|
break;
|
|
300
303
|
}
|
|
@@ -305,15 +308,17 @@ export default class PlaySocketServer {
|
|
|
305
308
|
|
|
306
309
|
/** @param {string} reason */
|
|
307
310
|
const rejectJoin = (reason) => {
|
|
308
|
-
ws.send(encode({ type: "join_rejected", reason }), { binary: true });
|
|
311
|
+
this.#getClient(ws.clientId)?.send(encode({ type: "join_rejected", reason }), { binary: true });
|
|
309
312
|
};
|
|
310
313
|
|
|
314
|
+
if (!this.#rooms[roomId]) return rejectJoin("Invalid room ID");
|
|
315
|
+
|
|
311
316
|
// Event callback
|
|
312
317
|
const joinAllowed = await this.#triggerEvent("clientJoinRequested", ws.clientId, roomId);
|
|
313
318
|
if (joinAllowed === false || typeof joinAllowed === "string") return rejectJoin(typeof joinAllowed === "string" ? joinAllowed : null);
|
|
314
319
|
|
|
315
320
|
// Verify client is still connected and abort if not
|
|
316
|
-
if (!this.#
|
|
321
|
+
if (!this.#getClient(ws.clientId)) {
|
|
317
322
|
if (this.#debug) console.log(LOG_PREFIX + `Room join cancelled, client ${ws.clientId} disconnected during event callback`);
|
|
318
323
|
return;
|
|
319
324
|
}
|
|
@@ -329,14 +334,14 @@ export default class PlaySocketServer {
|
|
|
329
334
|
|
|
330
335
|
case "update_property": {
|
|
331
336
|
const roomId = this.#clientRooms.get(ws.clientId);
|
|
332
|
-
const room =
|
|
337
|
+
const room = this.#rooms[roomId];
|
|
333
338
|
|
|
334
339
|
if (room && data.update) {
|
|
335
340
|
// Check if update is allowed via event callback (provide clone to ensure update integrity)
|
|
336
341
|
// Must be synchronous so that messages that follow in the same read from the socket (which can flush multiple at once) get processed reliably in order (await would schedule to a new microtask, so the order can fall apart)
|
|
337
342
|
const updateAllowed = this.#triggerEventSync("storageUpdateRequested", { roomId, clientId: ws.clientId, update: structuredClone(data.update), storage: this.getRoomStorage(roomId) });
|
|
338
343
|
if (updateAllowed === false || typeof updateAllowed === "string") {
|
|
339
|
-
ws.send(encode({
|
|
344
|
+
this.#getClient(ws.clientId)?.send(encode({
|
|
340
345
|
type: "property_update_rejected",
|
|
341
346
|
reason: typeof updateAllowed === "string" ? updateAllowed : null,
|
|
342
347
|
update: data.update
|
|
@@ -346,17 +351,12 @@ export default class PlaySocketServer {
|
|
|
346
351
|
|
|
347
352
|
room.crdtManager.importPropertyUpdate(data.update); // Import update into server state
|
|
348
353
|
|
|
349
|
-
|
|
350
|
-
const currentVersion = this.#roomVersions.get(roomId) + 1;
|
|
351
|
-
this.#roomVersions.set(roomId, currentVersion);
|
|
352
|
-
|
|
353
|
-
room.participants?.forEach(p => {
|
|
354
|
+
room.participants.forEach(p => {
|
|
354
355
|
const client = this.#clients.get(p);
|
|
355
356
|
if (client) {
|
|
356
357
|
client.send(encode({
|
|
357
358
|
type: "property_updated",
|
|
358
|
-
update: data.update
|
|
359
|
-
version: currentVersion
|
|
359
|
+
update: data.update
|
|
360
360
|
}), { binary: true });
|
|
361
361
|
}
|
|
362
362
|
});
|
|
@@ -371,8 +371,8 @@ export default class PlaySocketServer {
|
|
|
371
371
|
if (!ws.clientId) return;
|
|
372
372
|
const roomId = this.#clientRooms.get(ws.clientId) || null;
|
|
373
373
|
const requestSuccess = await this.#triggerEvent("requestReceived", { roomId, clientId: ws.clientId, name: data.request.name, data: data.request.data });
|
|
374
|
-
if (requestSuccess === false || typeof requestSuccess === "string") ws.send(encode({ type: "request_failed", request: data.request, reason: typeof requestSuccess === "string" ? requestSuccess : null }), { binary: true });
|
|
375
|
-
else ws.send(encode({ type: "request_succeeded", request: data.request }), { binary: true });
|
|
374
|
+
if (requestSuccess === false || typeof requestSuccess === "string") this.#getClient(ws.clientId)?.send(encode({ type: "request_failed", request: data.request, reason: typeof requestSuccess === "string" ? requestSuccess : null }), { binary: true });
|
|
375
|
+
else this.#getClient(ws.clientId)?.send(encode({ type: "request_succeeded", request: data.request }), { binary: true });
|
|
376
376
|
break;
|
|
377
377
|
}
|
|
378
378
|
|
|
@@ -395,31 +395,6 @@ export default class PlaySocketServer {
|
|
|
395
395
|
return Array.from({ length: 6 }, () => chars[Math.floor(Math.random() * chars.length)]).join("");
|
|
396
396
|
}
|
|
397
397
|
|
|
398
|
-
/**
|
|
399
|
-
* Migrate the host role to another participant
|
|
400
|
-
* @param {string} roomId - ID of the room
|
|
401
|
-
* @param {string} clientId - ID of the current host
|
|
402
|
-
*/
|
|
403
|
-
#migrateHost(roomId, clientId) {
|
|
404
|
-
const room = this.#rooms[roomId];
|
|
405
|
-
if (!room) throw new Error("Room not found");
|
|
406
|
-
if (room.host !== clientId) throw new Error("Client is not the host");
|
|
407
|
-
|
|
408
|
-
const remainingParticipants = room.participants.filter(p => p !== clientId);
|
|
409
|
-
|
|
410
|
-
if (remainingParticipants.length >= 1) {
|
|
411
|
-
// Inform all participants about the new host
|
|
412
|
-
room.host = remainingParticipants[0];
|
|
413
|
-
remainingParticipants.forEach(p => {
|
|
414
|
-
const client = this.#clients.get(p);
|
|
415
|
-
if (client) client.send(encode({
|
|
416
|
-
type: "host_migrated",
|
|
417
|
-
newHost: room.host,
|
|
418
|
-
}), { binary: true });
|
|
419
|
-
});
|
|
420
|
-
} else room.host = null; // The next person to join will become the host
|
|
421
|
-
}
|
|
422
|
-
|
|
423
398
|
/**
|
|
424
399
|
* Generate a random token to prevent malicious reconnect attempts
|
|
425
400
|
* @returns {string} Token
|
|
@@ -428,6 +403,15 @@ export default class PlaySocketServer {
|
|
|
428
403
|
return crypto.randomUUID();
|
|
429
404
|
}
|
|
430
405
|
|
|
406
|
+
/**
|
|
407
|
+
* Get the latest socket (It can change through reconnect, relevant when event callbacks are awaited in handlers)
|
|
408
|
+
* @param {string} clientId - Client ID
|
|
409
|
+
* @returns {WebSocket | undefined} Socket
|
|
410
|
+
*/
|
|
411
|
+
#getClient(clientId) {
|
|
412
|
+
return this.#clients.get(clientId);
|
|
413
|
+
}
|
|
414
|
+
|
|
431
415
|
/**
|
|
432
416
|
* Check rate limit using token bucket algorithm
|
|
433
417
|
* @param {string} connUuid - Connection UUID (which is different from the client ID)
|
|
@@ -457,6 +441,33 @@ export default class PlaySocketServer {
|
|
|
457
441
|
return true;
|
|
458
442
|
}
|
|
459
443
|
|
|
444
|
+
/**
|
|
445
|
+
* Migrate the host role to another participant
|
|
446
|
+
* @param {string} roomId - ID of the room
|
|
447
|
+
* @param {string} clientId - ID of the current host
|
|
448
|
+
*/
|
|
449
|
+
#migrateHost(roomId, clientId) {
|
|
450
|
+
const room = this.#rooms[roomId];
|
|
451
|
+
if (!room) throw new Error("Room not found");
|
|
452
|
+
if (room.host !== clientId) throw new Error("Client is not the host");
|
|
453
|
+
|
|
454
|
+
const remainingParticipants = room.participants.filter(p => p !== clientId);
|
|
455
|
+
|
|
456
|
+
if (remainingParticipants.length >= 1) {
|
|
457
|
+
// Change host (prefer connected participants over ones pending disconnect)
|
|
458
|
+
room.host = remainingParticipants.find(p => this.#clients.has(p)) ?? remainingParticipants[0];
|
|
459
|
+
|
|
460
|
+
// Inform all participants about the new host
|
|
461
|
+
remainingParticipants.forEach(p => {
|
|
462
|
+
const client = this.#clients.get(p);
|
|
463
|
+
if (client) client.send(encode({
|
|
464
|
+
type: "host_migrated",
|
|
465
|
+
newHost: room.host,
|
|
466
|
+
}), { binary: true });
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
460
471
|
/**
|
|
461
472
|
* Handle client disconnection
|
|
462
473
|
* @param {WebSocket} ws - WebSocket client
|
|
@@ -469,30 +480,27 @@ export default class PlaySocketServer {
|
|
|
469
480
|
|
|
470
481
|
// If client was in a room, check if they were the host & migrate
|
|
471
482
|
const roomId = this.#clientRooms.get(ws.clientId);
|
|
472
|
-
if (roomId
|
|
483
|
+
if (roomId && this.#rooms[roomId].host === ws.clientId) this.#migrateHost(roomId, ws.clientId);
|
|
473
484
|
|
|
474
|
-
if (ws.willfulDisconnect) this.#disconnectClient(ws); // Immediate disconnection
|
|
485
|
+
if (ws.willfulDisconnect) this.#disconnectClient(ws.clientId); // Immediate disconnection
|
|
475
486
|
else {
|
|
476
487
|
// Pending complete disconnection with 5s grace period to allow for reconnections
|
|
477
|
-
this.#
|
|
478
|
-
|
|
479
|
-
this.#disconnectClient(ws);
|
|
480
|
-
}, RECONNECT_GRACE_PERIOD)
|
|
481
|
-
});
|
|
488
|
+
const onTimeout = () => this.#disconnectClient(ws.clientId);
|
|
489
|
+
this.#pendingDisconnects.set(ws.clientId, { timeout: setTimeout(onTimeout, RECONNECT_GRACE_PERIOD), onTimeout });
|
|
482
490
|
}
|
|
483
491
|
}
|
|
484
492
|
}
|
|
485
493
|
|
|
486
494
|
/**
|
|
487
495
|
* Disconnect a client
|
|
488
|
-
* @param {
|
|
496
|
+
* @param {string} clientId - Client ID
|
|
489
497
|
*/
|
|
490
|
-
#disconnectClient(
|
|
491
|
-
this.#pendingDisconnects.delete(
|
|
492
|
-
this.#clientTokens.delete(
|
|
493
|
-
const roomId = this.#clientRooms.get(
|
|
494
|
-
if (roomId
|
|
495
|
-
this.#triggerEvent("clientDisconnected",
|
|
498
|
+
#disconnectClient(clientId) {
|
|
499
|
+
this.#pendingDisconnects.delete(clientId);
|
|
500
|
+
this.#clientTokens.delete(clientId);
|
|
501
|
+
const roomId = this.#clientRooms.get(clientId);
|
|
502
|
+
if (roomId) this.#leaveRoom(clientId, roomId);
|
|
503
|
+
this.#triggerEvent("clientDisconnected", clientId);
|
|
496
504
|
}
|
|
497
505
|
|
|
498
506
|
/**
|
|
@@ -575,13 +583,23 @@ export default class PlaySocketServer {
|
|
|
575
583
|
* Kick a client from the server
|
|
576
584
|
* @param {string} clientId - Client ID
|
|
577
585
|
* @param {string} [reason] - Optional reason
|
|
586
|
+
* @param {boolean} [shutdown=false] - Skip the disconnection and events
|
|
578
587
|
*/
|
|
579
|
-
kick(clientId, reason = null) {
|
|
588
|
+
kick(clientId, reason = null, shutdown = false) {
|
|
580
589
|
const client = this.#clients.get(clientId);
|
|
581
|
-
|
|
582
|
-
client
|
|
583
|
-
|
|
584
|
-
|
|
590
|
+
|
|
591
|
+
if (client) {
|
|
592
|
+
client.send(encode({ type: "kicked", reason }), { binary: true });
|
|
593
|
+
client.willfulDisconnect = true;
|
|
594
|
+
if (!shutdown) this.#handleDisconnection(client);
|
|
595
|
+
client.clientId = null; // Prevent handleDisconnection() from running on socket "close"
|
|
596
|
+
client.close();
|
|
597
|
+
} else if (this.#pendingDisconnects.has(clientId)) {
|
|
598
|
+
// Client is pending disconnect, fast-forward the grace period and fully disconnect
|
|
599
|
+
const { timeout, onTimeout } = this.#pendingDisconnects.get(clientId);
|
|
600
|
+
clearTimeout(timeout);
|
|
601
|
+
if (!shutdown) onTimeout();
|
|
602
|
+
}
|
|
585
603
|
}
|
|
586
604
|
|
|
587
605
|
/**
|
|
@@ -592,15 +610,12 @@ export default class PlaySocketServer {
|
|
|
592
610
|
move(clientId, roomId) {
|
|
593
611
|
const client = this.#clients.get(clientId);
|
|
594
612
|
const oldRoomId = this.#clientRooms.get(clientId);
|
|
595
|
-
const oldRoom = this.#rooms[oldRoomId];
|
|
596
613
|
const targetRoom = this.#rooms[roomId];
|
|
597
614
|
if (!client && !this.#pendingDisconnects.has(clientId)) throw new Error("Client not found");
|
|
598
615
|
if (!oldRoomId) throw new Error("Client is not in a room");
|
|
599
|
-
if (!oldRoom) throw new Error("Client room not found");
|
|
600
616
|
if (oldRoomId === roomId) throw new Error("Client is already in target room");
|
|
601
617
|
if (!targetRoom) throw new Error("Target room not found");
|
|
602
|
-
if (
|
|
603
|
-
if (oldRoom.host === clientId) this.#migrateHost(oldRoomId, clientId);
|
|
618
|
+
if (this.#rooms[oldRoomId].host === clientId) this.#migrateHost(oldRoomId, clientId);
|
|
604
619
|
this.#leaveRoom(clientId, oldRoomId);
|
|
605
620
|
this.#joinRoom(clientId, roomId);
|
|
606
621
|
}
|
|
@@ -639,16 +654,13 @@ export default class PlaySocketServer {
|
|
|
639
654
|
const room = this.#rooms[roomId];
|
|
640
655
|
if (room) {
|
|
641
656
|
const propertyUpdate = room.crdtManager.updateProperty(key, type, value, secondValue);
|
|
642
|
-
const currentVersion = this.#roomVersions.get(roomId) + 1;
|
|
643
|
-
this.#roomVersions.set(roomId, currentVersion); // Increment version for this room
|
|
644
657
|
|
|
645
|
-
room.participants
|
|
658
|
+
room.participants.forEach(p => {
|
|
646
659
|
const client = this.#clients.get(p);
|
|
647
660
|
if (client) {
|
|
648
661
|
client.send(encode({
|
|
649
662
|
type: "property_updated",
|
|
650
|
-
update: propertyUpdate
|
|
651
|
-
version: currentVersion
|
|
663
|
+
update: propertyUpdate
|
|
652
664
|
}), { binary: true });
|
|
653
665
|
}
|
|
654
666
|
});
|
|
@@ -659,21 +671,19 @@ export default class PlaySocketServer {
|
|
|
659
671
|
/**
|
|
660
672
|
* Create a server-owned room
|
|
661
673
|
* @param {object | null} [initialStorage] - Optional initial storage object
|
|
662
|
-
* @param {number} [size] - Max. room size, up to 500
|
|
663
674
|
* @returns {{ state: CRDTState, id: string }} Object containing room state and room ID
|
|
664
675
|
*/
|
|
665
|
-
createRoom(initialStorage
|
|
666
|
-
return this.#createRoom(
|
|
676
|
+
createRoom(initialStorage) {
|
|
677
|
+
return this.#createRoom(null, initialStorage);
|
|
667
678
|
}
|
|
668
679
|
|
|
669
680
|
/**
|
|
670
681
|
* Create a room
|
|
671
|
-
* @param {string} host - Host ID (when set to
|
|
682
|
+
* @param {string | null} host - Host ID (when set to null, the room was created by the server & will not be deleted if all clients leave)
|
|
672
683
|
* @param {object | null} [initialStorage] - Optional initial storage object
|
|
673
|
-
* @param {number} [size] - Max. room size, up to 500
|
|
674
684
|
* @returns {{ state: CRDTState, id: string }} Object containing room state and room ID
|
|
675
685
|
*/
|
|
676
|
-
#createRoom(host, initialStorage
|
|
686
|
+
#createRoom(host, initialStorage) {
|
|
677
687
|
let newRoomId;
|
|
678
688
|
|
|
679
689
|
for (let i = 0; i < 100; i++) {
|
|
@@ -691,11 +701,9 @@ export default class PlaySocketServer {
|
|
|
691
701
|
roomCrdtManager.updateProperty(key, "set", value);
|
|
692
702
|
});
|
|
693
703
|
|
|
694
|
-
this.#roomVersions.set(newRoomId, 0);
|
|
695
704
|
this.#rooms[newRoomId] = {
|
|
696
705
|
participants: [],
|
|
697
706
|
host,
|
|
698
|
-
size: Math.min(Number(size), MAX_ROOM_SIZE) || MAX_ROOM_SIZE,
|
|
699
707
|
crdtManager: roomCrdtManager
|
|
700
708
|
};
|
|
701
709
|
|
|
@@ -715,7 +723,6 @@ export default class PlaySocketServer {
|
|
|
715
723
|
if (!room) throw new Error("Room not found");
|
|
716
724
|
if (!client && !this.#pendingDisconnects.has(clientId)) throw new Error("Client not found");
|
|
717
725
|
if (this.#clientRooms.get(clientId)) throw new Error("Already in a room");
|
|
718
|
-
if (room.participants.length >= room.size) throw new Error("Room full");
|
|
719
726
|
|
|
720
727
|
room.participants.push(clientId);
|
|
721
728
|
this.#clientRooms.set(clientId, roomId);
|
|
@@ -728,23 +735,12 @@ export default class PlaySocketServer {
|
|
|
728
735
|
roomId,
|
|
729
736
|
state: room.crdtManager.state,
|
|
730
737
|
participantCount: room.participants.length,
|
|
731
|
-
host: room.host
|
|
732
|
-
version: this.#roomVersions.get(roomId)
|
|
738
|
+
host: room.host
|
|
733
739
|
}), { binary: true });
|
|
734
740
|
}
|
|
735
741
|
|
|
736
|
-
// If the
|
|
737
|
-
|
|
738
|
-
const becameHost = room.host === null;
|
|
739
|
-
if (becameHost) {
|
|
740
|
-
room.host = clientId; // Change host
|
|
741
|
-
if (client) {
|
|
742
|
-
client.send(encode({
|
|
743
|
-
type: "host_migrated",
|
|
744
|
-
newHost: room.host,
|
|
745
|
-
}), { binary: true });
|
|
746
|
-
}
|
|
747
|
-
}
|
|
742
|
+
// If the host has disconnected or is pending disconnect, migrate host (after join_accepted so hostMigrated event fires correctly on client)
|
|
743
|
+
if (room.host !== null && !this.#clients.has(room.host)) this.#migrateHost(roomId, room.host);
|
|
748
744
|
|
|
749
745
|
// Notify existing participants
|
|
750
746
|
room.participants.forEach(p => {
|
|
@@ -774,8 +770,8 @@ export default class PlaySocketServer {
|
|
|
774
770
|
this.#clientRooms.delete(clientId);
|
|
775
771
|
this.#triggerEvent("clientLeftRoom", clientId, roomId); // Before potential room destruction to allow for accessing its data in callback
|
|
776
772
|
|
|
777
|
-
if (room.participants.length === 0 && room.host !==
|
|
778
|
-
this
|
|
773
|
+
if (room.participants.length === 0 && room.host !== null) {
|
|
774
|
+
this.#deleteRoom(roomId); // Delete empty room that isn't server-created
|
|
779
775
|
} else {
|
|
780
776
|
// Notify remaining participants
|
|
781
777
|
room.participants.forEach(p => {
|
|
@@ -797,15 +793,22 @@ export default class PlaySocketServer {
|
|
|
797
793
|
const room = this.#rooms[roomId];
|
|
798
794
|
if (!room) throw new Error("Room not found");
|
|
799
795
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
796
|
+
room.participants.forEach((clientId) => this.kick(clientId, "Room destroyed by server"));
|
|
797
|
+
|
|
798
|
+
// Server-created rooms aren't auto-deleted when all clients leave
|
|
799
|
+
if (room.host === null) this.#deleteRoom(roomId);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Delete a room
|
|
804
|
+
* @param {string} roomId - Room ID
|
|
805
|
+
*/
|
|
806
|
+
#deleteRoom(roomId) {
|
|
807
|
+
const room = this.#rooms[roomId];
|
|
808
|
+
if (!room) throw new Error("Room not found");
|
|
809
|
+
if (room.participants.length > 0) throw new Error("Cannot delete non-empty room");
|
|
805
810
|
|
|
806
|
-
// Delete the room
|
|
807
811
|
delete this.#rooms[roomId];
|
|
808
|
-
this.#roomVersions.delete(roomId); // Delete room version (used to ensure all clients are up-2-date)
|
|
809
812
|
this.#triggerEvent("roomDestroyed", roomId);
|
|
810
813
|
if (this.#debug) console.log(LOG_PREFIX + "Deleted room with id " + roomId);
|
|
811
814
|
}
|
|
@@ -815,14 +818,9 @@ export default class PlaySocketServer {
|
|
|
815
818
|
*/
|
|
816
819
|
stop() {
|
|
817
820
|
clearInterval(this.#heartbeatInterval);
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
this.#wss.close();
|
|
821
|
-
}
|
|
821
|
+
[...this.#clients.keys(), ...this.#pendingDisconnects.keys()].forEach((clientId) => this.kick(clientId, "Server restart", true));
|
|
822
|
+
this.#wss.close();
|
|
822
823
|
if (this.#server && this.#ownsServer) this.#server.close(() => { console.log(LOG_PREFIX + "Server stopped"); });
|
|
823
|
-
this.#pendingDisconnects.forEach((data) => {
|
|
824
|
-
clearTimeout(data.timeout);
|
|
825
|
-
});
|
|
826
824
|
}
|
|
827
825
|
|
|
828
826
|
get rooms() { return structuredClone(this.#rooms) }
|
package/types/client/client.d.ts
CHANGED
|
@@ -82,10 +82,9 @@ export default class PlaySocket {
|
|
|
82
82
|
/**
|
|
83
83
|
* Create a new room and become host
|
|
84
84
|
* @param {object | null} [initialStorage] - Initial state
|
|
85
|
-
* @param {number} [size] - Max number of participants
|
|
86
85
|
* @returns {Promise<string>} Resolves with room ID
|
|
87
86
|
*/
|
|
88
|
-
createRoom(initialStorage?: object | null
|
|
87
|
+
createRoom(initialStorage?: object | null): Promise<string>;
|
|
89
88
|
/**
|
|
90
89
|
* Join an existing room
|
|
91
90
|
* @param {string} roomId - ID of the room
|
package/types/server/server.d.ts
CHANGED
|
@@ -21,13 +21,9 @@ export type Room = {
|
|
|
21
21
|
*/
|
|
22
22
|
participants: string[];
|
|
23
23
|
/**
|
|
24
|
-
* - Client ID of the room host (
|
|
24
|
+
* - Client ID of the room host (null for server-created rooms)
|
|
25
25
|
*/
|
|
26
26
|
host: string | null;
|
|
27
|
-
/**
|
|
28
|
-
* - Max. number of participants
|
|
29
|
-
*/
|
|
30
|
-
size: number;
|
|
31
27
|
/**
|
|
32
28
|
* - CRDT manager holding the room storage
|
|
33
29
|
*/
|
|
@@ -80,8 +76,7 @@ export type ServerEventMap = {
|
|
|
80
76
|
/**
|
|
81
77
|
* @typedef {object} Room
|
|
82
78
|
* @property {string[]} participants - Client IDs of the room participants
|
|
83
|
-
* @property {string | null} host - Client ID of the room host (
|
|
84
|
-
* @property {number} size - Max. number of participants
|
|
79
|
+
* @property {string | null} host - Client ID of the room host (null for server-created rooms)
|
|
85
80
|
* @property {CRDTManager} crdtManager - CRDT manager holding the room storage
|
|
86
81
|
*/
|
|
87
82
|
/** @typedef {boolean | string | void} RejectViaStringOrBool */
|
|
@@ -138,8 +133,9 @@ export default class PlaySocketServer {
|
|
|
138
133
|
* Kick a client from the server
|
|
139
134
|
* @param {string} clientId - Client ID
|
|
140
135
|
* @param {string} [reason] - Optional reason
|
|
136
|
+
* @param {boolean} [shutdown=false] - Skip the disconnection and events
|
|
141
137
|
*/
|
|
142
|
-
kick(clientId: string, reason?: string): void;
|
|
138
|
+
kick(clientId: string, reason?: string, shutdown?: boolean): void;
|
|
143
139
|
/**
|
|
144
140
|
* Move client into a different room
|
|
145
141
|
* @param {string} clientId - Client ID
|
|
@@ -179,10 +175,9 @@ export default class PlaySocketServer {
|
|
|
179
175
|
/**
|
|
180
176
|
* Create a server-owned room
|
|
181
177
|
* @param {object | null} [initialStorage] - Optional initial storage object
|
|
182
|
-
* @param {number} [size] - Max. room size, up to 500
|
|
183
178
|
* @returns {{ state: CRDTState, id: string }} Object containing room state and room ID
|
|
184
179
|
*/
|
|
185
|
-
createRoom(initialStorage?: object | null
|
|
180
|
+
createRoom(initialStorage?: object | null): {
|
|
186
181
|
state: CRDTState;
|
|
187
182
|
id: string;
|
|
188
183
|
};
|