playsocketjs 2.5.5 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/playsocket-client.js +4 -9
- package/dist/playsocket-server.js +79 -54
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,7 +96,7 @@ new PlaySocket(id?: string, options: PlaySocketOptions)
|
|
|
96
96
|
### Methods
|
|
97
97
|
|
|
98
98
|
- `init()`: Initialize the WebSocket connection – Returns Promise (async) which resolves with the client's ID
|
|
99
|
-
- `createRoom(initialStorage?: object,
|
|
99
|
+
- `createRoom(initialStorage?: object, size?: number)`: Create a new room and become host – Returns Promise (async) which resolves with the room ID. The room participant maximum is 100
|
|
100
100
|
- `joinRoom(hostId: string)`: Join an existing room – Returns Promise (async)
|
|
101
101
|
- `destroy()`: Use this to leave a room and close the connection
|
|
102
102
|
- `updateStorage(key: string, type: 'set' | 'array-add' | 'array-add-unique' | 'array-remove-matching' | 'array-update-matching', value: any, updateValue?: any)`: Update a key in the shared storage (max. 100 keys). Array operation types allow for conflict-free simultaneous array updates. For '-matching' operations, value becomes the value to match, and updateValue the replacement.
|
|
@@ -213,6 +213,8 @@ Creates a new PlaySocket Server instance with configuration options.
|
|
|
213
213
|
- `onEvent(event: string, callback: Function)`: Register a server-side event callback
|
|
214
214
|
- `getRoomStorage(roomId: string)`: Get a snapshot of the current room storage (returns storage `object`)
|
|
215
215
|
- `updateRoomStorage(roomId: string, key: string, type: 'set' | 'array-add' | 'array-add-unique' | 'array-remove-matching' | 'array-update-matching', value: any, updateValue?: any)`: Update a key in the shared room storage from the server.
|
|
216
|
+
- `createRoom(initialStorage?: object, size?: number, host?: string)`: Create a room (returns `object` containing room ID and state) – Rooms created with a non-player host like "server" (default) will not be deleted when the last participant leaves
|
|
217
|
+
- `destroyRoom(roomId: string)`: Destroy a room & kick all participants
|
|
216
218
|
|
|
217
219
|
#### Event types
|
|
218
220
|
|
|
@@ -223,7 +225,7 @@ Creates a new PlaySocket Server instance with configuration options.
|
|
|
223
225
|
- `clientJoinRequested`: Client requests to join a room (returns the client's ID `string`, room ID `string`) – return `false` or a rejection reason `string` to block them from joining
|
|
224
226
|
- `roomCreated`: Client created a room (returns room ID `string`)
|
|
225
227
|
- `roomDestroyed`: Room was destroyed, this happens when all participants leave (returns room ID `string`)
|
|
226
|
-
- `roomCreationRequested`: Room creation requested by client (returns `object` containing the client's ID `string
|
|
228
|
+
- `roomCreationRequested`: Room creation requested by client (returns `object` containing the client's ID `string` and the initialStorage `object`) – if you return an `object` in the callback, it will take that as the initial storage instead. If you return `false`, the creation will be denied
|
|
227
229
|
- `storageUpdated`: Room storage property updated (returns `object` containing the ID of the client who requested the update `string`, room ID `string`, the update `object`, and the storage `object`)
|
|
228
230
|
- `storageUpdateRequested`: Room storage property update requested by client (returns `object` containing the client's ID `string`, room ID `string`, the update `object`, and the current storage `object` that hasn't been updated yet) – if you return `false` in the callback, the update will be blocked
|
|
229
231
|
- `requestReceived`: Request from client was received by the server (returns `object` containing client's ID `string`, if in room – room ID `string`, request name `string` and optional passed data of type `any`)
|
|
@@ -2312,7 +2312,7 @@ class PlaySocket {
|
|
|
2312
2312
|
case 'room_created':
|
|
2313
2313
|
if (this.#pendingCreate) {
|
|
2314
2314
|
this.#inRoom = true;
|
|
2315
|
-
this.#triggerEvent("status", `Room created
|
|
2315
|
+
this.#triggerEvent("status", `Room created.`);
|
|
2316
2316
|
this.#crdtManager.importState(message.state);
|
|
2317
2317
|
this.#triggerEvent("storageUpdated", this.getStorage);
|
|
2318
2318
|
this.#pendingCreate.resolve(message.roomId);
|
|
@@ -2343,11 +2343,6 @@ class PlaySocket {
|
|
|
2343
2343
|
this.#triggerEvent("storageUpdated", this.getStorage);
|
|
2344
2344
|
break;
|
|
2345
2345
|
|
|
2346
|
-
case 'server_stopped':
|
|
2347
|
-
this.#triggerEvent("error", "Server restart.");
|
|
2348
|
-
this.destroy();
|
|
2349
|
-
break;
|
|
2350
|
-
|
|
2351
2346
|
case 'kicked':
|
|
2352
2347
|
this.#triggerEvent("error", `Kicked out of room: ${message.reason || "No reason provided."}`);
|
|
2353
2348
|
this.destroy();
|
|
@@ -2458,10 +2453,10 @@ class PlaySocket {
|
|
|
2458
2453
|
/**
|
|
2459
2454
|
* Create a new room and become host
|
|
2460
2455
|
* @param {object} initialStorage - Initial state
|
|
2461
|
-
* @param {number}
|
|
2456
|
+
* @param {number} size - Max number of participants
|
|
2462
2457
|
* @returns {Promise} Resolves with room ID
|
|
2463
2458
|
*/
|
|
2464
|
-
async createRoom(initialStorage = {},
|
|
2459
|
+
async createRoom(initialStorage = {}, size) {
|
|
2465
2460
|
if (!this.#initialized) {
|
|
2466
2461
|
this.#triggerEvent("error", "Cannot create room - not initialized");
|
|
2467
2462
|
return Promise.reject(new Error("Not initialized"));
|
|
@@ -2474,7 +2469,7 @@ class PlaySocket {
|
|
|
2474
2469
|
this.#sendToServer({
|
|
2475
2470
|
type: 'create_room',
|
|
2476
2471
|
initialStorage,
|
|
2477
|
-
size
|
|
2472
|
+
size
|
|
2478
2473
|
});
|
|
2479
2474
|
}),
|
|
2480
2475
|
this.#createTimeout("Room creation")
|
|
@@ -2083,6 +2083,8 @@ class CRDTManager {
|
|
|
2083
2083
|
|
|
2084
2084
|
/* eslint-disable jsdoc/require-returns */
|
|
2085
2085
|
|
|
2086
|
+
const MAX_ROOM_SIZE = 500;
|
|
2087
|
+
|
|
2086
2088
|
/**
|
|
2087
2089
|
* @typedef {import('node:http').Server} HttpServer
|
|
2088
2090
|
* @typedef {import('ws').WebSocket} WebSocket
|
|
@@ -2185,7 +2187,7 @@ class PlaySocketServer {
|
|
|
2185
2187
|
if (!ws.isTerminating) {
|
|
2186
2188
|
ws.isTerminating = true; // Prevent multiple terminate calls (it is async)
|
|
2187
2189
|
ws.terminate();
|
|
2188
|
-
console.error(`
|
|
2190
|
+
console.error(`PlaySocket connection ${ws.connectionId} terminated due to rate limit violations.`);
|
|
2189
2191
|
return;
|
|
2190
2192
|
}
|
|
2191
2193
|
}
|
|
@@ -2193,7 +2195,7 @@ class PlaySocketServer {
|
|
|
2193
2195
|
switch (data.type) {
|
|
2194
2196
|
case 'register': {
|
|
2195
2197
|
// Register client ID if provided & check for a duplicate
|
|
2196
|
-
if (data.id && this.#clients.get(data.id)) {
|
|
2198
|
+
if (data.id && (this.#clients.get(data.id) || data.id === "server")) {
|
|
2197
2199
|
ws.send(encode({ type: 'registration_failed', reason: 'ID is taken.' }), { binary: true });
|
|
2198
2200
|
return;
|
|
2199
2201
|
}
|
|
@@ -2274,8 +2276,6 @@ class PlaySocketServer {
|
|
|
2274
2276
|
case 'create_room': {
|
|
2275
2277
|
if (!ws.clientId) return;
|
|
2276
2278
|
|
|
2277
|
-
let newRoomId;
|
|
2278
|
-
|
|
2279
2279
|
if (this.#clientRooms.get(ws.clientId)) {
|
|
2280
2280
|
ws.send(encode({
|
|
2281
2281
|
type: 'room_creation_failed',
|
|
@@ -2284,24 +2284,8 @@ class PlaySocketServer {
|
|
|
2284
2284
|
return;
|
|
2285
2285
|
}
|
|
2286
2286
|
|
|
2287
|
-
// Generate room ID
|
|
2288
|
-
for (let i = 0; i < 50; i++) {
|
|
2289
|
-
const id = this.#generateId();
|
|
2290
|
-
if (!this.#rooms[id]) {
|
|
2291
|
-
newRoomId = id;
|
|
2292
|
-
break;
|
|
2293
|
-
}
|
|
2294
|
-
}
|
|
2295
|
-
|
|
2296
|
-
if (!newRoomId) {
|
|
2297
|
-
ws.send(encode({ type: 'room_creation_failed', reason: 'No available ID found.' }), { binary: true });
|
|
2298
|
-
throw new Error('Failed to generate unique room ID!');
|
|
2299
|
-
}
|
|
2300
|
-
|
|
2301
|
-
const roomCrdtManager = new CRDTManager(this.#debug); // Create the room's crdt manager
|
|
2302
|
-
|
|
2303
2287
|
// Event callback with potential initial storage modifications
|
|
2304
|
-
const reviewedStorage = await this.#triggerEvent("roomCreationRequested", {
|
|
2288
|
+
const reviewedStorage = await this.#triggerEvent("roomCreationRequested", { clientId: ws.clientId, initialStorage: structuredClone({ ...data.initialStorage }) });
|
|
2305
2289
|
if (typeof reviewedStorage === 'object') data.initialStorage = reviewedStorage;
|
|
2306
2290
|
if (reviewedStorage === false) {
|
|
2307
2291
|
ws.send(encode({
|
|
@@ -2311,33 +2295,21 @@ class PlaySocketServer {
|
|
|
2311
2295
|
return;
|
|
2312
2296
|
}
|
|
2313
2297
|
|
|
2314
|
-
// Check if client/creator is still connected
|
|
2298
|
+
// Check if client/creator is still connected - abort if not
|
|
2315
2299
|
if (!this.#clients.has(ws.clientId)) {
|
|
2316
2300
|
if (this.#debug) console.log(`Room creation cancelled - client ${ws.clientId} disconnected during event callback.`);
|
|
2317
|
-
return;
|
|
2301
|
+
return;
|
|
2318
2302
|
}
|
|
2319
2303
|
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
});
|
|
2304
|
+
try {
|
|
2305
|
+
const newRoom = this.createRoom(data.initialStorage, data.size, ws.clientId);
|
|
2306
|
+
this.#rooms[newRoom.id].participants.push(ws.clientId); // Add client to the room
|
|
2307
|
+
this.#clientRooms.set(ws.clientId, newRoom.id); // Add client to the client-room map
|
|
2308
|
+
ws.send(encode({ type: 'room_created', state: newRoom.state, roomId: newRoom.id }), { binary: true });
|
|
2309
|
+
} catch (error) {
|
|
2310
|
+
console.error("PlaySocket error creating room:", error);
|
|
2311
|
+
ws.send(encode({ type: 'room_creation_failed', reason: error.message }), { binary: true });
|
|
2325
2312
|
}
|
|
2326
|
-
|
|
2327
|
-
// Create room
|
|
2328
|
-
const maxSize = Math.min(Number(data.size), 100) || 100; // Max. limit is 100 clients / room
|
|
2329
|
-
this.#roomVersions.set(newRoomId, 0); // Start with version 0
|
|
2330
|
-
this.#rooms[newRoomId] = {
|
|
2331
|
-
participants: [ws.clientId],
|
|
2332
|
-
host: ws.clientId,
|
|
2333
|
-
maxSize,
|
|
2334
|
-
crdtManager: roomCrdtManager
|
|
2335
|
-
};
|
|
2336
|
-
this.#clientRooms.set(ws.clientId, newRoomId); // Add client to the room
|
|
2337
|
-
|
|
2338
|
-
ws.send(encode({ type: 'room_created', state: roomCrdtManager.getState, roomId: newRoomId, size: maxSize }), { binary: true });
|
|
2339
|
-
this.#triggerEvent("roomCreated", newRoomId);
|
|
2340
|
-
if (this.#debug) console.log(`Room ${newRoomId} created with initial storage:`, data.initialStorage);
|
|
2341
2313
|
break;
|
|
2342
2314
|
}
|
|
2343
2315
|
|
|
@@ -2356,7 +2328,7 @@ class PlaySocketServer {
|
|
|
2356
2328
|
|
|
2357
2329
|
if (!room) return rejectJoin("Room not found.");
|
|
2358
2330
|
if (this.#clientRooms.get(ws.clientId)) return rejectJoin("Already in a room.");
|
|
2359
|
-
if (room.participants.length >= room.
|
|
2331
|
+
if (room.participants.length >= room.size) return rejectJoin("Room full.");
|
|
2360
2332
|
|
|
2361
2333
|
room.participants.push(ws.clientId);
|
|
2362
2334
|
this.#clientRooms.set(ws.clientId, roomId);
|
|
@@ -2438,7 +2410,7 @@ class PlaySocketServer {
|
|
|
2438
2410
|
break;
|
|
2439
2411
|
}
|
|
2440
2412
|
} catch (error) {
|
|
2441
|
-
console.error('
|
|
2413
|
+
console.error('PlaySocket error in message handler:', error);
|
|
2442
2414
|
}
|
|
2443
2415
|
}
|
|
2444
2416
|
|
|
@@ -2557,11 +2529,8 @@ class PlaySocketServer {
|
|
|
2557
2529
|
room.participants = room.participants.filter(p => p !== ws.clientId); // Remove client from room
|
|
2558
2530
|
this.#clientRooms.delete(ws.clientId);
|
|
2559
2531
|
|
|
2560
|
-
if (room.participants?.length === 0) {
|
|
2561
|
-
|
|
2562
|
-
this.#roomVersions.delete(roomId); // Delete room version
|
|
2563
|
-
this.#triggerEvent("roomDestroyed", roomId);
|
|
2564
|
-
if (this.#debug) console.log("Deleted room with id " + roomId + ".");
|
|
2532
|
+
if (room.participants?.length === 0 && room.host !== "server") {
|
|
2533
|
+
this.destroyRoom(roomId); // Destroy room if now empty & not created by server
|
|
2565
2534
|
roomId = null; // If room was destroyed, set roomId to null
|
|
2566
2535
|
} else {
|
|
2567
2536
|
// Notify remaining participants
|
|
@@ -2669,16 +2638,72 @@ class PlaySocketServer {
|
|
|
2669
2638
|
}
|
|
2670
2639
|
}
|
|
2671
2640
|
|
|
2641
|
+
/**
|
|
2642
|
+
* Create a room from the server
|
|
2643
|
+
* @param {object} [initialStorage] - Optional initial storage object
|
|
2644
|
+
* @param {number} [size] - Max. room size, up to 500
|
|
2645
|
+
* @param {string} [host] - Host ID, defaults to "server" (when set to "server", room will not be deleted if all clients leave)
|
|
2646
|
+
* @returns {object} Object containing room state and room ID
|
|
2647
|
+
*/
|
|
2648
|
+
createRoom(initialStorage, size, host = "server") {
|
|
2649
|
+
let newRoomId;
|
|
2650
|
+
|
|
2651
|
+
for (let i = 0; i < 100; i++) {
|
|
2652
|
+
const id = this.#generateId();
|
|
2653
|
+
if (!this.#rooms[id]) {
|
|
2654
|
+
newRoomId = id;
|
|
2655
|
+
break;
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
if (!newRoomId) throw new Error("No available ID found.")
|
|
2660
|
+
const roomCrdtManager = new CRDTManager(this.#debug);
|
|
2661
|
+
|
|
2662
|
+
if (initialStorage) Object.entries(initialStorage)?.forEach(([key, value]) => {
|
|
2663
|
+
roomCrdtManager.updateProperty(key, "set", value);
|
|
2664
|
+
});
|
|
2665
|
+
|
|
2666
|
+
this.#roomVersions.set(newRoomId, 0);
|
|
2667
|
+
this.#rooms[newRoomId] = {
|
|
2668
|
+
participants: [],
|
|
2669
|
+
host,
|
|
2670
|
+
size: Math.min(Number(size), MAX_ROOM_SIZE) || MAX_ROOM_SIZE,
|
|
2671
|
+
crdtManager: roomCrdtManager
|
|
2672
|
+
};
|
|
2673
|
+
|
|
2674
|
+
this.#triggerEvent("roomCreated", newRoomId);
|
|
2675
|
+
if (this.#debug) console.log(`Room ${newRoomId} created with initial storage:`, initialStorage);
|
|
2676
|
+
return { state: roomCrdtManager.getState, id: newRoomId };
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
/**
|
|
2680
|
+
* Destroy a room
|
|
2681
|
+
* @param {string} roomId - Room ID
|
|
2682
|
+
*/
|
|
2683
|
+
destroyRoom(roomId) {
|
|
2684
|
+
const room = this.#rooms[roomId];
|
|
2685
|
+
if (!room) return; // Return if room does not exist
|
|
2686
|
+
|
|
2687
|
+
// Disconnect participants if still in room
|
|
2688
|
+
const participants = room.participants;
|
|
2689
|
+
participants.forEach((clientId) => {
|
|
2690
|
+
this.kick(clientId, "Room destroyed by server.");
|
|
2691
|
+
});
|
|
2692
|
+
|
|
2693
|
+
// Delete the room
|
|
2694
|
+
delete this.#rooms[roomId];
|
|
2695
|
+
this.#roomVersions.delete(roomId); // Delete room version (used to ensure all clients are up-2-date)
|
|
2696
|
+
this.#triggerEvent("roomDestroyed", roomId);
|
|
2697
|
+
if (this.#debug) console.log("Deleted room with id " + roomId + ".");
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2672
2700
|
/**
|
|
2673
2701
|
* Close all client connections, then close the websocket and http server
|
|
2674
2702
|
*/
|
|
2675
2703
|
stop() {
|
|
2676
2704
|
clearInterval(this.#heartbeatInterval);
|
|
2677
2705
|
if (this.#wss) {
|
|
2678
|
-
this.#clients.forEach(client =>
|
|
2679
|
-
client.send(encode({ type: 'server_stopped' }), { binary: true });
|
|
2680
|
-
client.close();
|
|
2681
|
-
});
|
|
2706
|
+
this.#clients.forEach((client, clientId) => this.kick(clientId, "Server restart."));
|
|
2682
2707
|
this.#wss.close();
|
|
2683
2708
|
}
|
|
2684
2709
|
if (this.#server && this.#ownsServer) this.#server.close(() => { console.log('PlaySocket server stopped.'); });
|