playsocketjs 2.5.4 → 2.6.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 +4 -2
- package/dist/playsocket-client.js +4 -9
- package/dist/playsocket-server.js +77 -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")
|
|
@@ -2185,7 +2185,7 @@ class PlaySocketServer {
|
|
|
2185
2185
|
if (!ws.isTerminating) {
|
|
2186
2186
|
ws.isTerminating = true; // Prevent multiple terminate calls (it is async)
|
|
2187
2187
|
ws.terminate();
|
|
2188
|
-
console.error(`
|
|
2188
|
+
console.error(`PlaySocket connection ${ws.connectionId} terminated due to rate limit violations.`);
|
|
2189
2189
|
return;
|
|
2190
2190
|
}
|
|
2191
2191
|
}
|
|
@@ -2193,7 +2193,7 @@ class PlaySocketServer {
|
|
|
2193
2193
|
switch (data.type) {
|
|
2194
2194
|
case 'register': {
|
|
2195
2195
|
// Register client ID if provided & check for a duplicate
|
|
2196
|
-
if (data.id && this.#clients.get(data.id)) {
|
|
2196
|
+
if (data.id && (this.#clients.get(data.id) || data.id === "server")) {
|
|
2197
2197
|
ws.send(encode({ type: 'registration_failed', reason: 'ID is taken.' }), { binary: true });
|
|
2198
2198
|
return;
|
|
2199
2199
|
}
|
|
@@ -2274,8 +2274,6 @@ class PlaySocketServer {
|
|
|
2274
2274
|
case 'create_room': {
|
|
2275
2275
|
if (!ws.clientId) return;
|
|
2276
2276
|
|
|
2277
|
-
let newRoomId;
|
|
2278
|
-
|
|
2279
2277
|
if (this.#clientRooms.get(ws.clientId)) {
|
|
2280
2278
|
ws.send(encode({
|
|
2281
2279
|
type: 'room_creation_failed',
|
|
@@ -2284,24 +2282,8 @@ class PlaySocketServer {
|
|
|
2284
2282
|
return;
|
|
2285
2283
|
}
|
|
2286
2284
|
|
|
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
2285
|
// Event callback with potential initial storage modifications
|
|
2304
|
-
const reviewedStorage = await this.#triggerEvent("roomCreationRequested", {
|
|
2286
|
+
const reviewedStorage = await this.#triggerEvent("roomCreationRequested", { clientId: ws.clientId, initialStorage: structuredClone({ ...data.initialStorage }) });
|
|
2305
2287
|
if (typeof reviewedStorage === 'object') data.initialStorage = reviewedStorage;
|
|
2306
2288
|
if (reviewedStorage === false) {
|
|
2307
2289
|
ws.send(encode({
|
|
@@ -2311,33 +2293,21 @@ class PlaySocketServer {
|
|
|
2311
2293
|
return;
|
|
2312
2294
|
}
|
|
2313
2295
|
|
|
2314
|
-
// Check if client/creator is still connected
|
|
2296
|
+
// Check if client/creator is still connected - abort if not
|
|
2315
2297
|
if (!this.#clients.has(ws.clientId)) {
|
|
2316
2298
|
if (this.#debug) console.log(`Room creation cancelled - client ${ws.clientId} disconnected during event callback.`);
|
|
2317
|
-
return;
|
|
2299
|
+
return;
|
|
2318
2300
|
}
|
|
2319
2301
|
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
});
|
|
2302
|
+
try {
|
|
2303
|
+
const newRoom = this.createRoom(data.initialStorage, data.size, ws.clientId);
|
|
2304
|
+
this.#rooms[newRoom.id].participants.push(ws.clientId); // Add client to the room
|
|
2305
|
+
this.#clientRooms.set(ws.clientId, newRoom.id); // Add client to the client-room map
|
|
2306
|
+
ws.send(encode({ type: 'room_created', state: newRoom.state, roomId: newRoom.id }), { binary: true });
|
|
2307
|
+
} catch (error) {
|
|
2308
|
+
console.error("PlaySocket error creating room:", error);
|
|
2309
|
+
ws.send(encode({ type: 'room_creation_failed', reason: error.message }), { binary: true });
|
|
2325
2310
|
}
|
|
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
2311
|
break;
|
|
2342
2312
|
}
|
|
2343
2313
|
|
|
@@ -2356,7 +2326,7 @@ class PlaySocketServer {
|
|
|
2356
2326
|
|
|
2357
2327
|
if (!room) return rejectJoin("Room not found.");
|
|
2358
2328
|
if (this.#clientRooms.get(ws.clientId)) return rejectJoin("Already in a room.");
|
|
2359
|
-
if (room.participants.length >= room.
|
|
2329
|
+
if (room.participants.length >= room.size) return rejectJoin("Room full.");
|
|
2360
2330
|
|
|
2361
2331
|
room.participants.push(ws.clientId);
|
|
2362
2332
|
this.#clientRooms.set(ws.clientId, roomId);
|
|
@@ -2438,7 +2408,7 @@ class PlaySocketServer {
|
|
|
2438
2408
|
break;
|
|
2439
2409
|
}
|
|
2440
2410
|
} catch (error) {
|
|
2441
|
-
console.error('
|
|
2411
|
+
console.error('PlaySocket error in message handler:', error);
|
|
2442
2412
|
}
|
|
2443
2413
|
}
|
|
2444
2414
|
|
|
@@ -2557,11 +2527,8 @@ class PlaySocketServer {
|
|
|
2557
2527
|
room.participants = room.participants.filter(p => p !== ws.clientId); // Remove client from room
|
|
2558
2528
|
this.#clientRooms.delete(ws.clientId);
|
|
2559
2529
|
|
|
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 + ".");
|
|
2530
|
+
if (room.participants?.length === 0 && room.host !== "server") {
|
|
2531
|
+
this.destroyRoom(roomId); // Destroy room if now empty & not created by server
|
|
2565
2532
|
roomId = null; // If room was destroyed, set roomId to null
|
|
2566
2533
|
} else {
|
|
2567
2534
|
// Notify remaining participants
|
|
@@ -2669,16 +2636,72 @@ class PlaySocketServer {
|
|
|
2669
2636
|
}
|
|
2670
2637
|
}
|
|
2671
2638
|
|
|
2639
|
+
/**
|
|
2640
|
+
* Create a room from the server
|
|
2641
|
+
* @param {object} [initialStorage] - Optional initial storage object
|
|
2642
|
+
* @param {number} [size] - Max. room size, up to 100 (defaults to 100)
|
|
2643
|
+
* @param {string} [host] - Host ID, defaults to "server" (when set to "server", room will not be deleted if all clients leave)
|
|
2644
|
+
* @returns {object} - Object containing room state and room ID
|
|
2645
|
+
*/
|
|
2646
|
+
createRoom(initialStorage, size, host = "server") {
|
|
2647
|
+
let newRoomId;
|
|
2648
|
+
|
|
2649
|
+
for (let i = 0; i < 100; i++) {
|
|
2650
|
+
const id = this.#generateId();
|
|
2651
|
+
if (!this.#rooms[id]) {
|
|
2652
|
+
newRoomId = id;
|
|
2653
|
+
break;
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
if (!newRoomId) throw new Error("No available ID found.")
|
|
2658
|
+
const roomCrdtManager = new CRDTManager(this.#debug);
|
|
2659
|
+
|
|
2660
|
+
if (initialStorage) Object.entries(initialStorage)?.forEach(([key, value]) => {
|
|
2661
|
+
roomCrdtManager.updateProperty(key, "set", value);
|
|
2662
|
+
});
|
|
2663
|
+
|
|
2664
|
+
this.#roomVersions.set(newRoomId, 0);
|
|
2665
|
+
this.#rooms[newRoomId] = {
|
|
2666
|
+
participants: [],
|
|
2667
|
+
host,
|
|
2668
|
+
size: Math.min(Number(size), 100) || 100,
|
|
2669
|
+
crdtManager: roomCrdtManager
|
|
2670
|
+
};
|
|
2671
|
+
|
|
2672
|
+
this.#triggerEvent("roomCreated", newRoomId);
|
|
2673
|
+
if (this.#debug) console.log(`Room ${newRoomId} created with initial storage:`, initialStorage);
|
|
2674
|
+
return { state: roomCrdtManager.getState, id: newRoomId };
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
/**
|
|
2678
|
+
* Destroy a room
|
|
2679
|
+
* @param {string} roomId - Room ID
|
|
2680
|
+
*/
|
|
2681
|
+
destroyRoom(roomId) {
|
|
2682
|
+
const room = this.#rooms[roomId];
|
|
2683
|
+
if (!room) return; // Return if room does not exist
|
|
2684
|
+
|
|
2685
|
+
// Disconnect participants if still in room
|
|
2686
|
+
const participants = room.participants;
|
|
2687
|
+
participants.forEach((clientId) => {
|
|
2688
|
+
this.kick(clientId, "Room destroyed by server.");
|
|
2689
|
+
});
|
|
2690
|
+
|
|
2691
|
+
// Delete the room
|
|
2692
|
+
delete this.#rooms[roomId];
|
|
2693
|
+
this.#roomVersions.delete(roomId); // Delete room version (used to ensure all clients are up-2-date)
|
|
2694
|
+
this.#triggerEvent("roomDestroyed", roomId);
|
|
2695
|
+
if (this.#debug) console.log("Deleted room with id " + roomId + ".");
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2672
2698
|
/**
|
|
2673
2699
|
* Close all client connections, then close the websocket and http server
|
|
2674
2700
|
*/
|
|
2675
2701
|
stop() {
|
|
2676
2702
|
clearInterval(this.#heartbeatInterval);
|
|
2677
2703
|
if (this.#wss) {
|
|
2678
|
-
this.#clients.forEach(client =>
|
|
2679
|
-
client.send(encode({ type: 'server_stopped' }), { binary: true });
|
|
2680
|
-
client.close();
|
|
2681
|
-
});
|
|
2704
|
+
this.#clients.forEach((client, clientId) => this.kick(clientId, "Server restart."));
|
|
2682
2705
|
this.#wss.close();
|
|
2683
2706
|
}
|
|
2684
2707
|
if (this.#server && this.#ownsServer) this.#server.close(() => { console.log('PlaySocket server stopped.'); });
|