playsocketjs 1.0.12 → 1.0.14
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 +79 -7
- package/dist/playsocket-server.js +46 -10
- package/package.json +1 -1
|
@@ -17,9 +17,11 @@ class PlaySocket {
|
|
|
17
17
|
|
|
18
18
|
// Room properties
|
|
19
19
|
#storage = {}; // Shared storage object
|
|
20
|
+
#storageTimestamps = {}; // Tracks update timestamps per key to ensure that not-yet synced updates don't get overwritten
|
|
20
21
|
#roomHost;
|
|
21
22
|
#roomId; // ID of the host (client only)
|
|
22
23
|
#connectionCount = 0;
|
|
24
|
+
#serverTimeOffset = 0; // Offset between server time and local time
|
|
23
25
|
|
|
24
26
|
// Event handling
|
|
25
27
|
#callbacks = new Map(); // Event callbacks
|
|
@@ -59,6 +61,14 @@ class PlaySocket {
|
|
|
59
61
|
);
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Get synchronized timestamp
|
|
66
|
+
* @private
|
|
67
|
+
*/
|
|
68
|
+
#getTimestamp() {
|
|
69
|
+
return Date.now() + this.#serverTimeOffset;
|
|
70
|
+
}
|
|
71
|
+
|
|
62
72
|
/**
|
|
63
73
|
* Register an event callback
|
|
64
74
|
* @param {string} event - Event name
|
|
@@ -109,7 +119,8 @@ class PlaySocket {
|
|
|
109
119
|
this.#sendToServer({
|
|
110
120
|
type: 'register',
|
|
111
121
|
id: this.#id,
|
|
112
|
-
customData: this.#customData || undefined
|
|
122
|
+
customData: this.#customData || undefined,
|
|
123
|
+
clientTime: Date.now()
|
|
113
124
|
});
|
|
114
125
|
}),
|
|
115
126
|
this.#createTimeout("Registration")
|
|
@@ -152,6 +163,7 @@ class PlaySocket {
|
|
|
152
163
|
// Connected to room
|
|
153
164
|
if (this.#pendingJoin) {
|
|
154
165
|
this.#storage = message.storage;
|
|
166
|
+
this.#storageTimestamps = message.storageTimestamps;
|
|
155
167
|
this.#connectionCount = message.participantCount - 1; // Counted without the user themselves
|
|
156
168
|
this.#roomHost = message.host;
|
|
157
169
|
this.#triggerEvent("status", `Connected to room.`);
|
|
@@ -175,6 +187,7 @@ class PlaySocket {
|
|
|
175
187
|
this.#reconnectCount = 0;
|
|
176
188
|
if (message.roomData) {
|
|
177
189
|
this.#storage = message.roomData.storage;
|
|
190
|
+
this.#storageTimestamps = message.roomData.storageTimestamps;
|
|
178
191
|
this.#connectionCount = message.roomData.participantCount - 1; // Counted without the user themselves
|
|
179
192
|
this.#setHost(message.roomData.host);
|
|
180
193
|
this.#triggerEvent("storageUpdated", { ...this.#storage });
|
|
@@ -220,6 +233,9 @@ class PlaySocket {
|
|
|
220
233
|
|
|
221
234
|
case 'registered':
|
|
222
235
|
if (this.#pendingRegistration) {
|
|
236
|
+
const roundTripTime = Date.now() - message.clientTime;
|
|
237
|
+
const serverTimeAtClientNow = message.serverTime + (roundTripTime / 2);
|
|
238
|
+
this.#serverTimeOffset = serverTimeAtClientNow - Date.now();
|
|
223
239
|
this.#sessionToken = message.sessionToken;
|
|
224
240
|
this.#initialized = true;
|
|
225
241
|
this.#pendingRegistration.resolve();
|
|
@@ -229,8 +245,21 @@ class PlaySocket {
|
|
|
229
245
|
|
|
230
246
|
case 'storage_sync':
|
|
231
247
|
if (message.key) {
|
|
232
|
-
|
|
248
|
+
const localTimestamp = this.#storageTimestamps[message.key] || 0;
|
|
249
|
+
if (message.timestamp > localTimestamp && JSON.stringify(this.#storage[message.key]) !== JSON.stringify(message.value)) {
|
|
233
250
|
this.#storage[message.key] = message.value;
|
|
251
|
+
this.#storageTimestamps[message.key] = message.timestamp;
|
|
252
|
+
this.#triggerEvent("storageUpdated", { ...this.#storage });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
break;
|
|
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;
|
|
234
263
|
this.#triggerEvent("storageUpdated", { ...this.#storage });
|
|
235
264
|
}
|
|
236
265
|
}
|
|
@@ -399,13 +428,16 @@ class PlaySocket {
|
|
|
399
428
|
*/
|
|
400
429
|
updateStorage(key, value) {
|
|
401
430
|
if (JSON.stringify(this.#storage[key]) === JSON.stringify(value)) return; // Prevent updates without changes
|
|
431
|
+
const timestamp = this.#getTimestamp();
|
|
402
432
|
this.#sendToServer({
|
|
403
433
|
type: 'room_storage_update',
|
|
404
434
|
key,
|
|
405
|
-
value
|
|
435
|
+
value,
|
|
436
|
+
timestamp
|
|
406
437
|
});
|
|
407
|
-
// Optimistic update
|
|
438
|
+
// Optimistic update
|
|
408
439
|
this.#storage[key] = value;
|
|
440
|
+
this.#storageTimestamps[key] = timestamp;
|
|
409
441
|
this.#triggerEvent("storageUpdated", { ...this.#storage });
|
|
410
442
|
}
|
|
411
443
|
|
|
@@ -417,8 +449,8 @@ class PlaySocket {
|
|
|
417
449
|
* @param {*} updateValue - New value for update-matching
|
|
418
450
|
*/
|
|
419
451
|
updateStorageArray(key, operation, value, updateValue) {
|
|
420
|
-
|
|
421
|
-
|
|
452
|
+
const updatedArray = this.#handleArrayUpdate(key, operation, value, updateValue);
|
|
453
|
+
if (JSON.stringify(this.#storage[key]) === JSON.stringify(updatedArray)) return; // Prevent updates without changes
|
|
422
454
|
this.#sendToServer({
|
|
423
455
|
type: 'room_storage_array_update',
|
|
424
456
|
key,
|
|
@@ -426,6 +458,44 @@ class PlaySocket {
|
|
|
426
458
|
value,
|
|
427
459
|
updateValue
|
|
428
460
|
});
|
|
461
|
+
// Optimistic update
|
|
462
|
+
this.#storage[key] = updatedArray;
|
|
463
|
+
this.#storageTimestamps[key] = this.#getTimestamp();
|
|
464
|
+
this.#triggerEvent("storageUpdated", { ...this.#storage });
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Handle array operations client-side
|
|
469
|
+
* @private
|
|
470
|
+
*/
|
|
471
|
+
#handleArrayUpdate(key, operation, value, updateValue) {
|
|
472
|
+
let array = (!this.#storage[key] || !Array.isArray(this.#storage[key])) ? [] : [...this.#storage[key]];
|
|
473
|
+
const isObject = typeof value === 'object' && value !== null;
|
|
474
|
+
const compare = (item) => isObject ? JSON.stringify(item) === JSON.stringify(value) : item === value;
|
|
475
|
+
|
|
476
|
+
switch (operation) {
|
|
477
|
+
case 'add':
|
|
478
|
+
array.push(value);
|
|
479
|
+
break;
|
|
480
|
+
|
|
481
|
+
case 'add-unique':
|
|
482
|
+
if (!array.some(compare)) array.push(value);
|
|
483
|
+
break;
|
|
484
|
+
|
|
485
|
+
case 'remove-matching':
|
|
486
|
+
array = array.filter(item => !compare(item));
|
|
487
|
+
break;
|
|
488
|
+
|
|
489
|
+
case 'update-matching':
|
|
490
|
+
const index = array.findIndex(compare);
|
|
491
|
+
if (index !== -1) array[index] = updateValue;
|
|
492
|
+
break;
|
|
493
|
+
|
|
494
|
+
default:
|
|
495
|
+
console.error(ERROR_PREFIX + `Unknown array operation: ${operation}`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return array;
|
|
429
499
|
}
|
|
430
500
|
|
|
431
501
|
/**
|
|
@@ -449,6 +519,8 @@ class PlaySocket {
|
|
|
449
519
|
clearTimeout(this.#reconnectTimeout);
|
|
450
520
|
this.#roomHost = null;
|
|
451
521
|
this.#storage = {};
|
|
522
|
+
this.#storageTimestamps = {};
|
|
523
|
+
this.#serverTimeOffset = 0;
|
|
452
524
|
this.#roomId = null;
|
|
453
525
|
this.#connectionCount = 0;
|
|
454
526
|
this.#isReconnecting = false;
|
|
@@ -457,7 +529,7 @@ class PlaySocket {
|
|
|
457
529
|
|
|
458
530
|
// Public getters
|
|
459
531
|
get connectionCount() { return this.#connectionCount; }
|
|
460
|
-
get getStorage() { return
|
|
532
|
+
get getStorage() { return JSON.parse(JSON.stringify(this.#storage)); }
|
|
461
533
|
get isHost() { return this.#id == this.#roomHost; }
|
|
462
534
|
get id() { return this.#id; }
|
|
463
535
|
}
|
|
@@ -102,7 +102,7 @@ function requireServer () {
|
|
|
102
102
|
this.#clients.set(data.id, ws);
|
|
103
103
|
const sessionToken = this.#generateSessionToken();
|
|
104
104
|
this.#clientTokens.set(data.id, sessionToken); // Token is used when reconnecting to prevent impersonation attacks
|
|
105
|
-
ws.send(JSON.stringify({ type: 'registered', sessionToken }));
|
|
105
|
+
ws.send(JSON.stringify({ type: 'registered', sessionToken, serverTime: Date.now(), clientTime: data.clientTime }));
|
|
106
106
|
this.#triggerEvent("clientRegistered", data.id, data.customData);
|
|
107
107
|
break;
|
|
108
108
|
|
|
@@ -127,6 +127,7 @@ function requireServer () {
|
|
|
127
127
|
if (formerRoom) {
|
|
128
128
|
roomData = {
|
|
129
129
|
storage: formerRoom.storage,
|
|
130
|
+
storageTimestamps: formerRoom.storageTimestamps,
|
|
130
131
|
participantCount: formerRoom.participants.length,
|
|
131
132
|
host: formerRoom.participants[0]
|
|
132
133
|
};
|
|
@@ -152,7 +153,8 @@ function requireServer () {
|
|
|
152
153
|
this.#rooms[newRoomId] = {
|
|
153
154
|
participants: [ws.clientId],
|
|
154
155
|
maxSize: data.size || null,
|
|
155
|
-
storage: data.storage || {}
|
|
156
|
+
storage: data.storage || {},
|
|
157
|
+
storageTimestamps: {},
|
|
156
158
|
};
|
|
157
159
|
|
|
158
160
|
this.#clientRooms.set(ws.clientId, newRoomId);
|
|
@@ -184,6 +186,7 @@ function requireServer () {
|
|
|
184
186
|
ws.send(JSON.stringify({
|
|
185
187
|
type: 'join_accepted',
|
|
186
188
|
storage: room.storage,
|
|
189
|
+
storageTimestamps: room.storageTimestamps,
|
|
187
190
|
participantCount: room.participants.length,
|
|
188
191
|
host: room.participants[0]
|
|
189
192
|
}));
|
|
@@ -207,9 +210,11 @@ function requireServer () {
|
|
|
207
210
|
const updateRoomId = this.#clientRooms.get(ws.clientId);
|
|
208
211
|
const updateRoom = updateRoomId ? this.#rooms[updateRoomId] : null;
|
|
209
212
|
if (updateRoom && data.key) {
|
|
210
|
-
|
|
213
|
+
const existingTimestamp = updateRoom.storageTimestamps[data.key] || 0;
|
|
214
|
+
if (data.timestamp > existingTimestamp && JSON.stringify(updateRoom.storage[data.key]) !== JSON.stringify(data.value)) {
|
|
211
215
|
updateRoom.storage[data.key] = data.value;
|
|
212
|
-
|
|
216
|
+
updateRoom.storageTimestamps[data.key] = data.timestamp;
|
|
217
|
+
this.#syncRoomStorageKey(updateRoom, data.key, data.timestamp);
|
|
213
218
|
}
|
|
214
219
|
}
|
|
215
220
|
break;
|
|
@@ -218,10 +223,12 @@ function requireServer () {
|
|
|
218
223
|
const arrayRoomId = this.#clientRooms.get(ws.clientId);
|
|
219
224
|
const arrayRoom = arrayRoomId ? this.#rooms[arrayRoomId] : null;
|
|
220
225
|
if (arrayRoom && data.key) {
|
|
226
|
+
// No timestamp check for array operations as they are designed to work unordered
|
|
221
227
|
const updatedArray = this.#arrayUpdate(arrayRoom.storage, data.key, data.operation, data.value, data.updateValue);
|
|
222
228
|
if (JSON.stringify(arrayRoom.storage[data.key]) !== JSON.stringify(updatedArray)) {
|
|
223
229
|
arrayRoom.storage[data.key] = updatedArray;
|
|
224
|
-
|
|
230
|
+
arrayRoom.storageTimestamps[data.key] = Date.now(); // Assign new date to ensure that this update is always being applied
|
|
231
|
+
this.#syncRoomStorageOperation(arrayRoom, ws.clientId, data.key, data.operation, data.value, data.updateValue, Date.now());
|
|
225
232
|
}
|
|
226
233
|
}
|
|
227
234
|
break;
|
|
@@ -277,19 +284,48 @@ function requireServer () {
|
|
|
277
284
|
/**
|
|
278
285
|
* Sync storage key with all clients in room
|
|
279
286
|
* @private
|
|
280
|
-
* @param {
|
|
287
|
+
* @param {string} room
|
|
288
|
+
* @param {string} requesterId - Client id of whom initiated the operation
|
|
281
289
|
* @param {string} key
|
|
282
|
-
* @param {
|
|
290
|
+
* @param {number} timestamp - In milliseconds
|
|
283
291
|
*/
|
|
284
|
-
#syncRoomStorageKey(room, key,
|
|
292
|
+
#syncRoomStorageKey(room, key, timestamp) {
|
|
285
293
|
room?.participants?.forEach(p => {
|
|
286
|
-
if (p === exclude) return;
|
|
287
294
|
const client = this.#clients.get(p);
|
|
288
295
|
if (client) {
|
|
289
296
|
client.send(JSON.stringify({
|
|
290
297
|
type: 'storage_sync',
|
|
291
298
|
key,
|
|
292
|
-
value: room?.storage
|
|
299
|
+
value: room?.storage[key],
|
|
300
|
+
timestamp
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Sync storage operations with all clients in room
|
|
308
|
+
* @private
|
|
309
|
+
* @param {string} room
|
|
310
|
+
* @param {string} requesterId - Client id of whom initiated the operation
|
|
311
|
+
* @param {string} key
|
|
312
|
+
* @param {string} operation
|
|
313
|
+
* @param {*} value
|
|
314
|
+
* @param {*} updateValue
|
|
315
|
+
* @param {number} timestamp - In milliseconds
|
|
316
|
+
*/
|
|
317
|
+
#syncRoomStorageOperation(room, requesterId, key, operation, value, updateValue, timestamp) {
|
|
318
|
+
room?.participants?.forEach(p => {
|
|
319
|
+
if (p == requesterId) return; // Don't send the operation to the requester
|
|
320
|
+
const client = this.#clients.get(p);
|
|
321
|
+
if (client) {
|
|
322
|
+
client.send(JSON.stringify({
|
|
323
|
+
type: 'storage_operation',
|
|
324
|
+
key,
|
|
325
|
+
operation,
|
|
326
|
+
value,
|
|
327
|
+
updateValue,
|
|
328
|
+
timestamp
|
|
293
329
|
}));
|
|
294
330
|
}
|
|
295
331
|
});
|