playsocketjs 1.0.8 → 1.0.9
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 +39 -21
- package/dist/playsocket-server.js +90 -36
- package/package.json +1 -1
|
@@ -17,7 +17,7 @@ class PlaySocket {
|
|
|
17
17
|
|
|
18
18
|
// Room properties
|
|
19
19
|
#storage = {}; // Shared storage object
|
|
20
|
-
#storageTimestamps = {}; // Tracks update timestamps per key
|
|
20
|
+
#storageTimestamps = {}; // Tracks update timestamps per key to ensure that not-yet synced updates don't get overwritten
|
|
21
21
|
#roomHost;
|
|
22
22
|
#roomId; // ID of the host (client only)
|
|
23
23
|
#connectionCount = 0;
|
|
@@ -124,7 +124,9 @@ class PlaySocket {
|
|
|
124
124
|
});
|
|
125
125
|
}),
|
|
126
126
|
this.#createTimeout("Registration")
|
|
127
|
-
])
|
|
127
|
+
]).finally(() => {
|
|
128
|
+
this.#pendingRegistration = null;
|
|
129
|
+
});
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
/**
|
|
@@ -141,7 +143,9 @@ class PlaySocket {
|
|
|
141
143
|
this.#socket.onopen = resolve;
|
|
142
144
|
}),
|
|
143
145
|
this.#createTimeout("Connection attempt")
|
|
144
|
-
])
|
|
146
|
+
]).finally(() => {
|
|
147
|
+
this.#pendingConnect = null;
|
|
148
|
+
});
|
|
145
149
|
}
|
|
146
150
|
|
|
147
151
|
/**
|
|
@@ -165,16 +169,14 @@ class PlaySocket {
|
|
|
165
169
|
this.#triggerEvent("status", `Connected to room.`);
|
|
166
170
|
this.#triggerEvent("storageUpdated", { ...this.#storage });
|
|
167
171
|
this.#pendingJoin.resolve();
|
|
168
|
-
this.#pendingJoin = null;
|
|
169
172
|
}
|
|
170
173
|
break;
|
|
171
174
|
|
|
172
175
|
case 'join_rejected':
|
|
173
176
|
// Connection to room failed
|
|
174
177
|
if (this.#pendingJoin) {
|
|
175
|
-
this.#triggerEvent("status", "Connection rejected: " + message.reason
|
|
176
|
-
this.#pendingJoin.reject(new Error("Connection rejected: " + message.reason
|
|
177
|
-
this.#pendingJoin = null;
|
|
178
|
+
this.#triggerEvent("status", "Connection rejected: " + message.reason);
|
|
179
|
+
this.#pendingJoin.reject(new Error("Connection rejected: " + message.reason));
|
|
178
180
|
}
|
|
179
181
|
break;
|
|
180
182
|
|
|
@@ -197,15 +199,13 @@ class PlaySocket {
|
|
|
197
199
|
}
|
|
198
200
|
this.#triggerEvent("status", "Reconnected.");
|
|
199
201
|
this.#pendingReconnect.resolve();
|
|
200
|
-
this.#pendingReconnect = null;
|
|
201
202
|
}
|
|
202
203
|
break;
|
|
203
204
|
|
|
204
205
|
case 'reconnection_failed':
|
|
205
206
|
if (this.#pendingReconnect) {
|
|
206
207
|
this.#triggerEvent("status", `Reconnection failed: ${message.reason}`);
|
|
207
|
-
this.#pendingReconnect.
|
|
208
|
-
this.#pendingReconnect = null;
|
|
208
|
+
this.#pendingReconnect.reject(new Error("Reconnection failed: " + message.reason));
|
|
209
209
|
}
|
|
210
210
|
break;
|
|
211
211
|
|
|
@@ -214,22 +214,19 @@ class PlaySocket {
|
|
|
214
214
|
this.#pendingHost.resolve(this.#roomId);
|
|
215
215
|
this.#triggerEvent("status", `Room created${this.#pendingHost.maxSize ? ` with max size ${this.#pendingHost.maxSize}.` : '.'}`);
|
|
216
216
|
this.#triggerEvent("storageUpdated", { ...this.#storage });
|
|
217
|
-
this.#pendingHost = null;
|
|
218
217
|
}
|
|
219
218
|
break;
|
|
220
219
|
|
|
221
220
|
case 'room_creation_failed':
|
|
222
221
|
if (this.#pendingHost) {
|
|
223
|
-
this.#pendingHost.reject(new Error("Failed to create room: " + message.reason
|
|
224
|
-
this.#triggerEvent("error", "Failed to create room: " + message.reason
|
|
225
|
-
this.#pendingHost = null;
|
|
222
|
+
this.#pendingHost.reject(new Error("Failed to create room: " + message.reason));
|
|
223
|
+
this.#triggerEvent("error", "Failed to create room: " + message.reason);
|
|
226
224
|
}
|
|
227
225
|
break;
|
|
228
226
|
|
|
229
227
|
case 'id_taken':
|
|
230
228
|
if (this.#pendingRegistration) {
|
|
231
229
|
this.#pendingRegistration.reject(new Error("This id is already in use"));
|
|
232
|
-
this.#pendingRegistration = null;
|
|
233
230
|
this.#triggerEvent("error", "This id is taken.");
|
|
234
231
|
}
|
|
235
232
|
break;
|
|
@@ -242,7 +239,6 @@ class PlaySocket {
|
|
|
242
239
|
this.#sessionToken = message.sessionToken;
|
|
243
240
|
this.#initialized = true;
|
|
244
241
|
this.#pendingRegistration.resolve();
|
|
245
|
-
this.#pendingRegistration = null;
|
|
246
242
|
this.#triggerEvent("status", "Connected to server.");
|
|
247
243
|
}
|
|
248
244
|
break;
|
|
@@ -258,6 +254,17 @@ class PlaySocket {
|
|
|
258
254
|
}
|
|
259
255
|
break;
|
|
260
256
|
|
|
257
|
+
case 'storage_operation':
|
|
258
|
+
if (message.key) {
|
|
259
|
+
const updatedArray = this.#handleArrayUpdate(message.key, message.operation, message.value, message.updateValue);
|
|
260
|
+
if (JSON.stringify(this.#storage[message.key]) !== JSON.stringify(updatedArray)) {
|
|
261
|
+
this.#storage[message.key] = updatedArray;
|
|
262
|
+
this.#storageTimestamps[message.key] = message.timestamp;
|
|
263
|
+
this.#triggerEvent("storageUpdated", { ...this.#storage });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
break;
|
|
267
|
+
|
|
261
268
|
case 'client_disconnected':
|
|
262
269
|
this.#connectionCount = message.participantCount - 1; // Counted without the user themselves
|
|
263
270
|
this.#setHost(message.host);
|
|
@@ -316,7 +323,9 @@ class PlaySocket {
|
|
|
316
323
|
});
|
|
317
324
|
}),
|
|
318
325
|
this.#createTimeout("Reconnection request")
|
|
319
|
-
])
|
|
326
|
+
]).finally(() => {
|
|
327
|
+
this.#pendingReconnect = null;
|
|
328
|
+
});
|
|
320
329
|
} catch (error) {
|
|
321
330
|
this.#triggerEvent("status", "Reconnection failed: " + error.message);
|
|
322
331
|
this.#reconnectTimeout = setTimeout(() => this.#attemptReconnect(), 500);
|
|
@@ -378,7 +387,9 @@ class PlaySocket {
|
|
|
378
387
|
});
|
|
379
388
|
}),
|
|
380
389
|
this.#createTimeout("Room creation")
|
|
381
|
-
])
|
|
390
|
+
]).finally(() => {
|
|
391
|
+
this.#pendingHost = null;
|
|
392
|
+
});
|
|
382
393
|
}
|
|
383
394
|
|
|
384
395
|
/**
|
|
@@ -405,7 +416,9 @@ class PlaySocket {
|
|
|
405
416
|
});
|
|
406
417
|
}),
|
|
407
418
|
this.#createTimeout("Room join")
|
|
408
|
-
])
|
|
419
|
+
]).finally(() => {
|
|
420
|
+
this.#pendingJoin = null;
|
|
421
|
+
});
|
|
409
422
|
}
|
|
410
423
|
|
|
411
424
|
/**
|
|
@@ -437,15 +450,17 @@ class PlaySocket {
|
|
|
437
450
|
updateStorageArray(key, operation, value, updateValue) {
|
|
438
451
|
const updatedArray = this.#handleArrayUpdate(key, operation, value, updateValue);
|
|
439
452
|
if (JSON.stringify(this.#storage[key]) === JSON.stringify(updatedArray)) return; // Prevent updates without changes
|
|
453
|
+
const timestamp = this.#getTimestamp();
|
|
440
454
|
this.#sendToServer({
|
|
441
455
|
type: 'room_storage_array_update',
|
|
442
456
|
key,
|
|
443
457
|
operation,
|
|
444
458
|
value,
|
|
445
|
-
updateValue
|
|
459
|
+
updateValue,
|
|
460
|
+
timestamp
|
|
446
461
|
});
|
|
447
462
|
this.#storage[key] = updatedArray;
|
|
448
|
-
this.#storageTimestamps[key] =
|
|
463
|
+
this.#storageTimestamps[key] = timestamp;
|
|
449
464
|
this.#triggerEvent("storageUpdated", { ...this.#storage });
|
|
450
465
|
}
|
|
451
466
|
|
|
@@ -490,6 +505,8 @@ class PlaySocket {
|
|
|
490
505
|
this.#initialized = false; // Set this immediately to prevent automatic reconnection
|
|
491
506
|
|
|
492
507
|
if (this.#socket) {
|
|
508
|
+
// Signal to the server that it can immediately disconnect this user
|
|
509
|
+
if (this.#socket.readyState === WebSocket.OPEN) this.#sendToServer({ type: 'disconnect' });
|
|
493
510
|
this.#socket.close();
|
|
494
511
|
this.#socket = null;
|
|
495
512
|
|
|
@@ -503,6 +520,7 @@ class PlaySocket {
|
|
|
503
520
|
this.#roomHost = null;
|
|
504
521
|
this.#storage = {};
|
|
505
522
|
this.#storageTimestamps = {};
|
|
523
|
+
this.#serverTimeOffset = 0;
|
|
506
524
|
this.#roomId = null;
|
|
507
525
|
this.#connectionCount = 0;
|
|
508
526
|
this.#isReconnecting = false;
|
|
@@ -214,7 +214,7 @@ function requireServer () {
|
|
|
214
214
|
if (data.timestamp > existingTimestamp && JSON.stringify(updateRoom.storage[data.key]) !== JSON.stringify(data.value)) {
|
|
215
215
|
updateRoom.storage[data.key] = data.value;
|
|
216
216
|
updateRoom.storageTimestamps[data.key] = data.timestamp;
|
|
217
|
-
this.#syncRoomStorageKey(updateRoomId, data.key, data.timestamp);
|
|
217
|
+
this.#syncRoomStorageKey(updateRoomId, ws.clientId, data.key, data.timestamp);
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
break;
|
|
@@ -223,14 +223,20 @@ function requireServer () {
|
|
|
223
223
|
const arrayRoomId = this.#clientRooms.get(ws.clientId);
|
|
224
224
|
const arrayRoom = arrayRoomId ? this.#rooms[arrayRoomId] : null;
|
|
225
225
|
if (arrayRoom && data.key) {
|
|
226
|
+
// No timestamp check for array operations as they are designed to work unordered
|
|
226
227
|
const updatedArray = this.#arrayUpdate(arrayRoom.storage, data.key, data.operation, data.value, data.updateValue);
|
|
227
228
|
if (JSON.stringify(arrayRoom.storage[data.key]) !== JSON.stringify(updatedArray)) {
|
|
228
229
|
arrayRoom.storage[data.key] = updatedArray;
|
|
229
|
-
arrayRoom.storageTimestamps[data.key] =
|
|
230
|
-
this.#
|
|
230
|
+
arrayRoom.storageTimestamps[data.key] = data.timestamp;
|
|
231
|
+
this.#syncRoomStorageOperation(arrayRoomId, ws.clientId, data.key, data.operation, data.value, data.updateValue, data.timestamp);
|
|
231
232
|
}
|
|
232
233
|
}
|
|
233
234
|
break;
|
|
235
|
+
|
|
236
|
+
case 'disconnect':
|
|
237
|
+
// Client signals to server that it will will willfully disconnect soon
|
|
238
|
+
ws.willfulDisconnect = true;
|
|
239
|
+
break;
|
|
234
240
|
}
|
|
235
241
|
} catch (error) {
|
|
236
242
|
console.error('Error processing message:', error);
|
|
@@ -276,13 +282,18 @@ function requireServer () {
|
|
|
276
282
|
}
|
|
277
283
|
|
|
278
284
|
/**
|
|
279
|
-
* Sync storage key
|
|
285
|
+
* Sync storage key with all clients in room
|
|
280
286
|
* @private
|
|
287
|
+
* @param {string} roomId
|
|
288
|
+
* @param {string} requesterId - Client id of whom initiated the operation
|
|
289
|
+
* @param {string} key
|
|
290
|
+
* @param {number} timestamp - In milliseconds
|
|
281
291
|
*/
|
|
282
|
-
#syncRoomStorageKey(roomId, key, timestamp) {
|
|
292
|
+
#syncRoomStorageKey(roomId, requesterId, key, timestamp) {
|
|
283
293
|
const room = this.#rooms[roomId];
|
|
284
294
|
if (!room) return;
|
|
285
295
|
room.participants.forEach(p => {
|
|
296
|
+
if (p == requesterId) return; // Don't sync with the requester
|
|
286
297
|
const client = this.#clients.get(p);
|
|
287
298
|
if (client) {
|
|
288
299
|
client.send(JSON.stringify({
|
|
@@ -295,6 +306,36 @@ function requireServer () {
|
|
|
295
306
|
});
|
|
296
307
|
}
|
|
297
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Sync storage operations with all clients in room
|
|
311
|
+
* @private
|
|
312
|
+
* @param {string} roomId
|
|
313
|
+
* @param {string} requesterId - Client id of whom initiated the operation
|
|
314
|
+
* @param {string} key
|
|
315
|
+
* @param {string} operation
|
|
316
|
+
* @param {*} value
|
|
317
|
+
* @param {*} updateValue
|
|
318
|
+
* @param {number} timestamp - In milliseconds
|
|
319
|
+
*/
|
|
320
|
+
#syncRoomStorageOperation(roomId, requesterId, key, operation, value, updateValue, timestamp) {
|
|
321
|
+
const room = this.#rooms[roomId];
|
|
322
|
+
if (!room) return;
|
|
323
|
+
room.participants.forEach(p => {
|
|
324
|
+
if (p == requesterId) return; // Don't send the operation to the requester
|
|
325
|
+
const client = this.#clients.get(p);
|
|
326
|
+
if (client) {
|
|
327
|
+
client.send(JSON.stringify({
|
|
328
|
+
type: 'storage_operation',
|
|
329
|
+
key,
|
|
330
|
+
operation,
|
|
331
|
+
value,
|
|
332
|
+
updateValue,
|
|
333
|
+
timestamp
|
|
334
|
+
}));
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
298
339
|
/**
|
|
299
340
|
* Handle array operations
|
|
300
341
|
* @private
|
|
@@ -334,38 +375,51 @@ function requireServer () {
|
|
|
334
375
|
if (!ws.clientId) return;
|
|
335
376
|
this.#clients.delete(ws.clientId); // Immediately remove from active clients (otherwise, server would try to message this client)
|
|
336
377
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
});
|
|
363
|
-
}
|
|
364
|
-
}
|
|
378
|
+
if (ws.willfulDisconnect) {
|
|
379
|
+
this.#disconnectClient(ws); // Immediate disconnection
|
|
380
|
+
} else {
|
|
381
|
+
// Pending complete disconnection with 2s grace period to allow for reconnections
|
|
382
|
+
this.#pendingDisconnects.set(ws.clientId, {
|
|
383
|
+
timeout: setTimeout(() => {
|
|
384
|
+
this.#disconnectClient(ws);
|
|
385
|
+
}, 2000)
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
365
389
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
390
|
+
/**
|
|
391
|
+
* Disconnect a client
|
|
392
|
+
* @private
|
|
393
|
+
* @param {Object} ws
|
|
394
|
+
*/
|
|
395
|
+
#disconnectClient(ws) {
|
|
396
|
+
this.#pendingDisconnects.delete(ws.clientId);
|
|
397
|
+
this.#rateLimits.delete(ws.clientId);
|
|
398
|
+
this.#clientTokens.delete(ws.clientId);
|
|
399
|
+
const roomId = this.#clientRooms.get(ws.clientId);
|
|
400
|
+
const room = this.#rooms[roomId];
|
|
401
|
+
|
|
402
|
+
if (room) {
|
|
403
|
+
room.participants = room.participants.filter(p => p !== ws.clientId); // Remove client from room
|
|
404
|
+
this.#clientRooms.delete(ws.clientId);
|
|
405
|
+
|
|
406
|
+
if (room.participants?.length === 0) {
|
|
407
|
+
delete this.#rooms[roomId]; // Delete room if now empty
|
|
408
|
+
} else {
|
|
409
|
+
// Notify remaining participants
|
|
410
|
+
room.participants.forEach(p => {
|
|
411
|
+
const client = this.#clients.get(p);
|
|
412
|
+
if (client) client.send(JSON.stringify({
|
|
413
|
+
type: 'client_disconnected',
|
|
414
|
+
client: ws.clientId,
|
|
415
|
+
host: room.participants[0],
|
|
416
|
+
participantCount: room.participants.length
|
|
417
|
+
}));
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
this.#triggerEvent("clientDisconnected", ws.clientId);
|
|
369
423
|
}
|
|
370
424
|
|
|
371
425
|
/**
|