playsocketjs 1.0.7 → 1.0.8
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/dist/playsocket-client.js +134 -70
- package/dist/playsocket-server.js +88 -34
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ const TIMEOUT_MS = 5000; // 5 second timeout for operations
|
|
|
9
9
|
class PlaySocket {
|
|
10
10
|
// Core properties
|
|
11
11
|
#id; // Unique client ID
|
|
12
|
+
#sessionToken; // Unique session token
|
|
12
13
|
#endpoint;
|
|
13
14
|
#socket; // WebSocket connection
|
|
14
15
|
#initialized = false; // Initialization status
|
|
@@ -17,7 +18,6 @@ class PlaySocket {
|
|
|
17
18
|
// Room properties
|
|
18
19
|
#storage = {}; // Shared storage object
|
|
19
20
|
#storageTimestamps = {}; // Tracks update timestamps per key
|
|
20
|
-
#isHost = false; // Whether this client is host
|
|
21
21
|
#roomHost;
|
|
22
22
|
#roomId; // ID of the host (client only)
|
|
23
23
|
#connectionCount = 0;
|
|
@@ -29,10 +29,14 @@ class PlaySocket {
|
|
|
29
29
|
// Async server operations
|
|
30
30
|
#pendingJoin;
|
|
31
31
|
#pendingHost;
|
|
32
|
-
#
|
|
32
|
+
#pendingRegistration;
|
|
33
|
+
#pendingConnect;
|
|
34
|
+
#pendingReconnect;
|
|
33
35
|
|
|
34
36
|
// Timeouts or intervals
|
|
35
37
|
#reconnectTimeout;
|
|
38
|
+
#reconnectCount = 0;
|
|
39
|
+
#isReconnecting = false;
|
|
36
40
|
|
|
37
41
|
/**
|
|
38
42
|
* Create a new PlaySocket instance
|
|
@@ -72,7 +76,7 @@ class PlaySocket {
|
|
|
72
76
|
*/
|
|
73
77
|
onEvent(event, callback) {
|
|
74
78
|
const validEvents = [
|
|
75
|
-
"status", "error", "instanceDestroyed", "storageUpdated", "hostMigrated", "clientConnected", "clientDisconnected"
|
|
79
|
+
"status", "error", "instanceDestroyed", "storageUpdated", "hostMigrated", "clientConnected", "clientDisconnected"
|
|
76
80
|
];
|
|
77
81
|
if (!validEvents.includes(event)) return console.warn(WARNING_PREFIX + `Invalid event type "${event}".`);
|
|
78
82
|
if (!this.#callbacks.has(event)) this.#callbacks.set(event, []);
|
|
@@ -99,41 +103,45 @@ class PlaySocket {
|
|
|
99
103
|
* @returns {Promise} Resolves when connection is established
|
|
100
104
|
*/
|
|
101
105
|
async init() {
|
|
102
|
-
if (this.#initialized) return Promise.reject(new Error("Already initialized
|
|
103
|
-
if (!this.#endpoint) return Promise.reject(new Error("No websocket endpoint provided
|
|
106
|
+
if (this.#initialized) return Promise.reject(new Error("Already initialized"));
|
|
107
|
+
if (!this.#endpoint) return Promise.reject(new Error("No websocket endpoint provided"));
|
|
104
108
|
this.#triggerEvent("status", "Initializing...");
|
|
105
109
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
this.#createTimeout("Initialization")
|
|
109
|
-
]);
|
|
110
|
-
}
|
|
110
|
+
// Connect to WS server
|
|
111
|
+
await this.#connect();
|
|
111
112
|
|
|
113
|
+
// Register with server
|
|
114
|
+
await Promise.race([
|
|
115
|
+
new Promise(async (resolve, reject) => {
|
|
116
|
+
this.#pendingRegistration = { resolve, reject }; // Resolve or reject depending on answer to registration msg
|
|
112
117
|
|
|
113
|
-
|
|
114
|
-
* Connect to the WS server and register
|
|
115
|
-
* @returns {Promise} Resolves when user is registered on the WS server
|
|
116
|
-
* @private
|
|
117
|
-
*/
|
|
118
|
-
async #connect() {
|
|
119
|
-
return new Promise((resolve, reject) => {
|
|
120
|
-
this.#socket = new WebSocket(this.#endpoint);
|
|
121
|
-
this.#setupSocketHandlers(); // Message & close events
|
|
122
|
-
|
|
123
|
-
// Resolve or reject depending on answer to registration msg
|
|
124
|
-
this.#pendingInit = { resolve, reject };
|
|
125
|
-
|
|
126
|
-
// Register with server
|
|
127
|
-
this.#socket.onopen = () => {
|
|
128
|
-
this.#initialized = true;
|
|
118
|
+
// Register with server
|
|
129
119
|
this.#sendToServer({
|
|
130
120
|
type: 'register',
|
|
131
121
|
id: this.#id,
|
|
132
122
|
customData: this.#customData || undefined,
|
|
133
123
|
clientTime: Date.now()
|
|
134
124
|
});
|
|
135
|
-
}
|
|
136
|
-
|
|
125
|
+
}),
|
|
126
|
+
this.#createTimeout("Registration")
|
|
127
|
+
]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Connect to the WS server and
|
|
132
|
+
* @returns {Promise} Resolves when the connection was established
|
|
133
|
+
* @private
|
|
134
|
+
*/
|
|
135
|
+
async #connect() {
|
|
136
|
+
return Promise.race([
|
|
137
|
+
new Promise((resolve, reject) => {
|
|
138
|
+
this.#pendingConnect = { reject };
|
|
139
|
+
this.#socket = new WebSocket(this.#endpoint);
|
|
140
|
+
this.#setupSocketHandlers(); // Message & close events
|
|
141
|
+
this.#socket.onopen = resolve;
|
|
142
|
+
}),
|
|
143
|
+
this.#createTimeout("Connection attempt")
|
|
144
|
+
]);
|
|
137
145
|
}
|
|
138
146
|
|
|
139
147
|
/**
|
|
@@ -147,12 +155,13 @@ class PlaySocket {
|
|
|
147
155
|
if (!message.type) return;
|
|
148
156
|
|
|
149
157
|
switch (message.type) {
|
|
150
|
-
case '
|
|
158
|
+
case 'join_accepted':
|
|
151
159
|
// Connected to room
|
|
152
160
|
if (this.#pendingJoin) {
|
|
153
161
|
this.#storage = message.storage;
|
|
154
162
|
this.#storageTimestamps = message.storageTimestamps;
|
|
155
|
-
this.#connectionCount = message.participantCount - 1;
|
|
163
|
+
this.#connectionCount = message.participantCount - 1; // Counted without the user themselves
|
|
164
|
+
this.#roomHost = message.host;
|
|
156
165
|
this.#triggerEvent("status", `Connected to room.`);
|
|
157
166
|
this.#triggerEvent("storageUpdated", { ...this.#storage });
|
|
158
167
|
this.#pendingJoin.resolve();
|
|
@@ -160,7 +169,7 @@ class PlaySocket {
|
|
|
160
169
|
}
|
|
161
170
|
break;
|
|
162
171
|
|
|
163
|
-
case '
|
|
172
|
+
case 'join_rejected':
|
|
164
173
|
// Connection to room failed
|
|
165
174
|
if (this.#pendingJoin) {
|
|
166
175
|
this.#triggerEvent("status", "Connection rejected: " + message.reason || 'Unknown reason');
|
|
@@ -169,6 +178,37 @@ class PlaySocket {
|
|
|
169
178
|
}
|
|
170
179
|
break;
|
|
171
180
|
|
|
181
|
+
case 'reconnected':
|
|
182
|
+
// Successfully reconnected
|
|
183
|
+
if (this.#pendingReconnect) {
|
|
184
|
+
this.#isReconnecting = false;
|
|
185
|
+
this.#reconnectCount = 0;
|
|
186
|
+
if (message.roomData) {
|
|
187
|
+
this.#storage = message.roomData.storage;
|
|
188
|
+
this.#storageTimestamps = message.roomData.storageTimestamps;
|
|
189
|
+
this.#connectionCount = message.roomData.participantCount - 1; // Counted without the user themselves
|
|
190
|
+
this.#setHost(message.roomData.host);
|
|
191
|
+
this.#triggerEvent("storageUpdated", { ...this.#storage });
|
|
192
|
+
} else {
|
|
193
|
+
if (this.#roomId) {
|
|
194
|
+
this.#triggerEvent("error", "Reconnected, but room no longer exists.");
|
|
195
|
+
return this.destroy();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
this.#triggerEvent("status", "Reconnected.");
|
|
199
|
+
this.#pendingReconnect.resolve();
|
|
200
|
+
this.#pendingReconnect = null;
|
|
201
|
+
}
|
|
202
|
+
break;
|
|
203
|
+
|
|
204
|
+
case 'reconnection_failed':
|
|
205
|
+
if (this.#pendingReconnect) {
|
|
206
|
+
this.#triggerEvent("status", `Reconnection failed: ${message.reason}`);
|
|
207
|
+
this.#pendingReconnect.resolve();
|
|
208
|
+
this.#pendingReconnect = null;
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
|
|
172
212
|
case 'room_created':
|
|
173
213
|
if (this.#pendingHost) {
|
|
174
214
|
this.#pendingHost.resolve(this.#roomId);
|
|
@@ -187,21 +227,23 @@ class PlaySocket {
|
|
|
187
227
|
break;
|
|
188
228
|
|
|
189
229
|
case 'id_taken':
|
|
190
|
-
if (this.#
|
|
191
|
-
this.#
|
|
230
|
+
if (this.#pendingRegistration) {
|
|
231
|
+
this.#pendingRegistration.reject(new Error("This id is already in use"));
|
|
232
|
+
this.#pendingRegistration = null;
|
|
192
233
|
this.#triggerEvent("error", "This id is taken.");
|
|
193
|
-
this.#pendingInit = null;
|
|
194
234
|
}
|
|
195
235
|
break;
|
|
196
236
|
|
|
197
237
|
case 'registered':
|
|
198
|
-
if (this.#
|
|
238
|
+
if (this.#pendingRegistration) {
|
|
199
239
|
const roundTripTime = Date.now() - message.clientTime;
|
|
200
240
|
const serverTimeAtClientNow = message.serverTime + (roundTripTime / 2);
|
|
201
241
|
this.#serverTimeOffset = serverTimeAtClientNow - Date.now();
|
|
202
|
-
this.#
|
|
242
|
+
this.#sessionToken = message.sessionToken;
|
|
243
|
+
this.#initialized = true;
|
|
244
|
+
this.#pendingRegistration.resolve();
|
|
245
|
+
this.#pendingRegistration = null;
|
|
203
246
|
this.#triggerEvent("status", "Connected to server.");
|
|
204
|
-
this.#pendingInit = null;
|
|
205
247
|
}
|
|
206
248
|
break;
|
|
207
249
|
|
|
@@ -217,21 +259,15 @@ class PlaySocket {
|
|
|
217
259
|
break;
|
|
218
260
|
|
|
219
261
|
case 'client_disconnected':
|
|
220
|
-
this.#isHost = this.#id == message.updatedHost;
|
|
221
262
|
this.#connectionCount = message.participantCount - 1; // Counted without the user themselves
|
|
222
|
-
|
|
223
|
-
// If host has changed...
|
|
224
|
-
if (this.#roomHost != message.updatedHost) {
|
|
225
|
-
this.#roomHost = message.updatedHost;
|
|
226
|
-
this.#triggerEvent("hostMigrated", this.#roomHost);
|
|
227
|
-
}
|
|
263
|
+
this.#setHost(message.host);
|
|
228
264
|
|
|
229
265
|
this.#triggerEvent("clientDisconnected", message.client);
|
|
230
266
|
this.#triggerEvent("status", `Client ${message.client} disconnected.`);
|
|
231
267
|
break;
|
|
232
268
|
|
|
233
269
|
case 'client_connected':
|
|
234
|
-
this.#connectionCount = message.participantCount - 1;
|
|
270
|
+
this.#connectionCount = message.participantCount - 1; // Counted without the user themselves
|
|
235
271
|
this.#triggerEvent("clientConnected", message.client);
|
|
236
272
|
this.#triggerEvent("status", `Client ${message.client} connected.`);
|
|
237
273
|
break;
|
|
@@ -244,37 +280,67 @@ class PlaySocket {
|
|
|
244
280
|
// Handle socket errors
|
|
245
281
|
this.#socket.onerror = () => {
|
|
246
282
|
this.#triggerEvent("error", "WebSocket error.");
|
|
247
|
-
if (this.#
|
|
283
|
+
if (this.#pendingConnect) this.#pendingConnect.reject(new Error("WebSocket error"));
|
|
248
284
|
};
|
|
249
285
|
|
|
250
286
|
// Handle socket close & attempt reconnect
|
|
251
287
|
this.#socket.onclose = () => {
|
|
252
|
-
if (!this.#initialized) return;
|
|
253
|
-
this.#triggerEvent("status", "Disconnected
|
|
254
|
-
|
|
255
|
-
this.#
|
|
256
|
-
try {
|
|
257
|
-
await this.#connect(); // Will set status to connected if successful
|
|
258
|
-
|
|
259
|
-
// Rejoin room if needed
|
|
260
|
-
if (this.#roomId) {
|
|
261
|
-
this.#triggerEvent("status", "Rejoining room...");
|
|
262
|
-
await this.joinRoom(this.#roomId); // If this fails, it will be caught by the outer catch
|
|
263
|
-
}
|
|
264
|
-
} catch (error) {
|
|
265
|
-
this.#triggerEvent("error", "WebSocket connection permanently closed: " + error);
|
|
266
|
-
this.destroy();
|
|
267
|
-
}
|
|
268
|
-
}, 1000);
|
|
288
|
+
if (!this.#initialized || this.#isReconnecting) return;
|
|
289
|
+
this.#triggerEvent("status", "Disconnected.");
|
|
290
|
+
this.#reconnectCount = 0;
|
|
291
|
+
this.#attemptReconnect();
|
|
269
292
|
};
|
|
270
293
|
};
|
|
271
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Attempt to reconnect with a fixed number of retries
|
|
297
|
+
* @private
|
|
298
|
+
*/
|
|
299
|
+
async #attemptReconnect() {
|
|
300
|
+
if (this.#reconnectCount++ >= 3) {
|
|
301
|
+
this.#triggerEvent("error", "Disconnected from server.");
|
|
302
|
+
return this.destroy();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
this.#triggerEvent("status", `Attempting to reconnect... (${this.#reconnectCount})`);
|
|
306
|
+
this.#isReconnecting = true;
|
|
307
|
+
try {
|
|
308
|
+
await this.#connect();
|
|
309
|
+
await Promise.race([
|
|
310
|
+
new Promise(async (resolve, reject) => {
|
|
311
|
+
this.#pendingReconnect = { resolve, reject };
|
|
312
|
+
this.#sendToServer({
|
|
313
|
+
type: 'reconnect',
|
|
314
|
+
id: this.#id,
|
|
315
|
+
sessionToken: this.#sessionToken
|
|
316
|
+
});
|
|
317
|
+
}),
|
|
318
|
+
this.#createTimeout("Reconnection request")
|
|
319
|
+
]);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
this.#triggerEvent("status", "Reconnection failed: " + error.message);
|
|
322
|
+
this.#reconnectTimeout = setTimeout(() => this.#attemptReconnect(), 500);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Update the host in case a new one was chosen
|
|
328
|
+
* @param {string} hostId - Client id of new host
|
|
329
|
+
* @private
|
|
330
|
+
*/
|
|
331
|
+
#setHost(hostId) {
|
|
332
|
+
if (this.#roomHost != hostId) {
|
|
333
|
+
this.#roomHost = hostId;
|
|
334
|
+
this.#triggerEvent("hostMigrated", hostId);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
272
338
|
/**
|
|
273
339
|
* Send a message to the server
|
|
274
340
|
* @private
|
|
275
341
|
*/
|
|
276
342
|
#sendToServer(data) {
|
|
277
|
-
if (!this.#
|
|
343
|
+
if (!this.#socket || this.#socket.readyState !== WebSocket.OPEN) {
|
|
278
344
|
return console.error(ERROR_PREFIX + "Cannot send message - not connected.");
|
|
279
345
|
}
|
|
280
346
|
try {
|
|
@@ -299,7 +365,6 @@ class PlaySocket {
|
|
|
299
365
|
|
|
300
366
|
return Promise.race([
|
|
301
367
|
new Promise((resolve, reject) => {
|
|
302
|
-
this.#isHost = true;
|
|
303
368
|
this.#storage = initialStorage;
|
|
304
369
|
this.#roomId = this.#id; // Create room with your own ID as the room ID to mimic p2p
|
|
305
370
|
this.#roomHost = this.#id;
|
|
@@ -329,9 +394,7 @@ class PlaySocket {
|
|
|
329
394
|
|
|
330
395
|
return Promise.race([
|
|
331
396
|
new Promise((resolve, reject) => {
|
|
332
|
-
this.#isHost = false;
|
|
333
397
|
this.#roomId = roomId;
|
|
334
|
-
this.#roomHost = roomId;
|
|
335
398
|
this.#pendingJoin = { resolve, reject };
|
|
336
399
|
|
|
337
400
|
// Send connection request
|
|
@@ -436,19 +499,20 @@ class PlaySocket {
|
|
|
436
499
|
}
|
|
437
500
|
|
|
438
501
|
// Reset state
|
|
439
|
-
this.#
|
|
502
|
+
clearTimeout(this.#reconnectTimeout);
|
|
440
503
|
this.#roomHost = null;
|
|
441
504
|
this.#storage = {};
|
|
442
505
|
this.#storageTimestamps = {};
|
|
443
506
|
this.#roomId = null;
|
|
444
507
|
this.#connectionCount = 0;
|
|
445
|
-
|
|
508
|
+
this.#isReconnecting = false;
|
|
509
|
+
this.#reconnectCount = 0;
|
|
446
510
|
}
|
|
447
511
|
|
|
448
512
|
// Public getters
|
|
449
513
|
get connectionCount() { return this.#connectionCount; }
|
|
450
514
|
get getStorage() { return { ...this.#storage }; }
|
|
451
|
-
get isHost() { return this.#
|
|
515
|
+
get isHost() { return this.#id == this.#roomHost; }
|
|
452
516
|
get id() { return this.#id; }
|
|
453
517
|
}
|
|
454
518
|
|
|
@@ -27,8 +27,10 @@ function requireServer () {
|
|
|
27
27
|
#rooms = {};
|
|
28
28
|
#clientRooms = new Map(); // ClientId -> RoomId
|
|
29
29
|
#rateLimits = new Map(); // Rate limiting storage
|
|
30
|
-
#callbacks = new Map();
|
|
30
|
+
#callbacks = new Map(); // Event -> [callback functions]
|
|
31
31
|
#heartbeatInterval;
|
|
32
|
+
#pendingDisconnects = new Map(); // ClientId -> {timeout, roomId}
|
|
33
|
+
#clientTokens = new Map(); // ClientId -> Token
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
36
|
* Create a new PlaySocketServer instance
|
|
@@ -98,10 +100,45 @@ function requireServer () {
|
|
|
98
100
|
}
|
|
99
101
|
ws.clientId = data.id; // Adds the provided id to the ws object as clientId
|
|
100
102
|
this.#clients.set(data.id, ws);
|
|
101
|
-
|
|
103
|
+
const sessionToken = this.#generateSessionToken();
|
|
104
|
+
this.#clientTokens.set(data.id, sessionToken); // Token is used when reconnecting to prevent impersonation attacks
|
|
105
|
+
ws.send(JSON.stringify({ type: 'registered', sessionToken, serverTime: Date.now(), clientTime: data.clientTime }));
|
|
102
106
|
this.#triggerEvent("clientRegistered", data.id, data.customData);
|
|
103
107
|
break;
|
|
104
108
|
|
|
109
|
+
case 'reconnect':
|
|
110
|
+
// If user is pending disconnect, respond (otherwise it's too late)
|
|
111
|
+
const pd = this.#pendingDisconnects.get(data.id);
|
|
112
|
+
if (pd && data.sessionToken) {
|
|
113
|
+
if (data.sessionToken !== this.#clientTokens.get(data.id)) {
|
|
114
|
+
ws.send(JSON.stringify({ type: 'reconnection_failed', reason: "Session token does not match" }));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
clearTimeout(pd.timeout);
|
|
118
|
+
this.#pendingDisconnects.delete(data.id);
|
|
119
|
+
|
|
120
|
+
// Re-assign old client id to the new ws connection
|
|
121
|
+
ws.clientId = data.id;
|
|
122
|
+
this.#clients.set(data.id, ws);
|
|
123
|
+
|
|
124
|
+
// If they were in a room, provide updated room data
|
|
125
|
+
let roomData;
|
|
126
|
+
const formerRoom = this.#rooms[this.#clientRooms.get(data.id)];
|
|
127
|
+
if (formerRoom) {
|
|
128
|
+
roomData = {
|
|
129
|
+
storage: formerRoom.storage,
|
|
130
|
+
storageTimestamps: formerRoom.storageTimestamps,
|
|
131
|
+
participantCount: formerRoom.participants.length,
|
|
132
|
+
host: formerRoom.participants[0]
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
ws.send(JSON.stringify({ type: 'reconnected', roomData }));
|
|
137
|
+
} else {
|
|
138
|
+
ws.send(JSON.stringify({ type: 'reconnection_failed', reason: "Client unknown to server" }));
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
|
|
105
142
|
case 'create_room':
|
|
106
143
|
if (!ws.clientId) return;
|
|
107
144
|
const newRoomId = ws.clientId;
|
|
@@ -126,7 +163,7 @@ function requireServer () {
|
|
|
126
163
|
break;
|
|
127
164
|
|
|
128
165
|
case 'join_room':
|
|
129
|
-
if (!data.roomId) return;
|
|
166
|
+
if (!data.roomId || !ws.clientId) return;
|
|
130
167
|
const roomId = data.roomId;
|
|
131
168
|
const room = this.#rooms[roomId];
|
|
132
169
|
|
|
@@ -134,7 +171,7 @@ function requireServer () {
|
|
|
134
171
|
(room.maxSize && room.participants.length >= room.maxSize) ||
|
|
135
172
|
this.#clientRooms.get(ws.clientId)) {
|
|
136
173
|
ws.send(JSON.stringify({
|
|
137
|
-
type: '
|
|
174
|
+
type: 'join_rejected',
|
|
138
175
|
reason: !room ? 'Room not found' :
|
|
139
176
|
room.maxSize && room.participants.length >= room.maxSize ? 'Room full' :
|
|
140
177
|
'Already in a room'
|
|
@@ -147,10 +184,11 @@ function requireServer () {
|
|
|
147
184
|
|
|
148
185
|
// Notify joiner with initial storage
|
|
149
186
|
ws.send(JSON.stringify({
|
|
150
|
-
type: '
|
|
187
|
+
type: 'join_accepted',
|
|
151
188
|
storage: room.storage,
|
|
152
189
|
storageTimestamps: room.storageTimestamps,
|
|
153
|
-
participantCount: room.participants.length
|
|
190
|
+
participantCount: room.participants.length,
|
|
191
|
+
host: room.participants[0]
|
|
154
192
|
}));
|
|
155
193
|
|
|
156
194
|
// Notify existing participants
|
|
@@ -199,6 +237,16 @@ function requireServer () {
|
|
|
199
237
|
}
|
|
200
238
|
}
|
|
201
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Generate a random token to prevent malicious reconnect attempts
|
|
242
|
+
* @returns {string} - Token
|
|
243
|
+
*/
|
|
244
|
+
#generateSessionToken() {
|
|
245
|
+
let token = '';
|
|
246
|
+
for (let i = 0; i < 16; i++) token += Math.floor(Math.random() * 16).toString(16);
|
|
247
|
+
return token;
|
|
248
|
+
}
|
|
249
|
+
|
|
202
250
|
/**
|
|
203
251
|
* Check rate limit using token bucket algorithm
|
|
204
252
|
* @private
|
|
@@ -284,37 +332,40 @@ function requireServer () {
|
|
|
284
332
|
*/
|
|
285
333
|
#handleDisconnection(ws) {
|
|
286
334
|
if (!ws.clientId) return;
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
335
|
+
this.#clients.delete(ws.clientId); // Immediately remove from active clients (otherwise, server would try to message this client)
|
|
336
|
+
|
|
337
|
+
// Pending disconnection with 2s grace period to allow for reconnections
|
|
338
|
+
this.#pendingDisconnects.set(ws.clientId, {
|
|
339
|
+
timeout: setTimeout(() => {
|
|
340
|
+
this.#pendingDisconnects.delete(ws.clientId);
|
|
341
|
+
this.#rateLimits.delete(ws.clientId);
|
|
342
|
+
this.#clientTokens.delete(ws.clientId);
|
|
343
|
+
const roomId = this.#clientRooms.get(ws.clientId);
|
|
344
|
+
const room = this.#rooms[roomId];
|
|
345
|
+
|
|
346
|
+
if (room) {
|
|
347
|
+
room.participants = room.participants.filter(p => p !== ws.clientId); // Remove client from room
|
|
348
|
+
this.#clientRooms.delete(ws.clientId);
|
|
349
|
+
|
|
350
|
+
if (room.participants?.length === 0) {
|
|
351
|
+
delete this.#rooms[roomId]; // Delete room if now empty
|
|
352
|
+
} else {
|
|
353
|
+
// Notify remaining participants
|
|
354
|
+
room.participants.forEach(p => {
|
|
355
|
+
const client = this.#clients.get(p);
|
|
356
|
+
if (client) client.send(JSON.stringify({
|
|
357
|
+
type: 'client_disconnected',
|
|
358
|
+
client: ws.clientId,
|
|
359
|
+
host: room.participants[0],
|
|
360
|
+
participantCount: room.participants.length
|
|
361
|
+
}));
|
|
362
|
+
});
|
|
311
363
|
}
|
|
312
364
|
}
|
|
313
|
-
});
|
|
314
|
-
if (room.participants?.length === 0) delete this.#rooms[roomId]; // Delete room if now empty
|
|
315
|
-
}
|
|
316
365
|
|
|
317
|
-
|
|
366
|
+
this.#triggerEvent("clientDisconnected", ws.clientId);
|
|
367
|
+
}, 2000)
|
|
368
|
+
});
|
|
318
369
|
}
|
|
319
370
|
|
|
320
371
|
/**
|
|
@@ -354,6 +405,9 @@ function requireServer () {
|
|
|
354
405
|
console.log('PlaySocket server stopped');
|
|
355
406
|
});
|
|
356
407
|
}
|
|
408
|
+
this.#pendingDisconnects.forEach((data) => {
|
|
409
|
+
clearTimeout(data.timeout);
|
|
410
|
+
});
|
|
357
411
|
}
|
|
358
412
|
|
|
359
413
|
get getRooms() { return { ...this.#rooms } }
|