playsocketjs 1.0.8 → 1.0.10
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 +32 -61
- package/dist/playsocket-server.js +60 -40
- 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;
|
|
@@ -316,7 +312,9 @@ class PlaySocket {
|
|
|
316
312
|
});
|
|
317
313
|
}),
|
|
318
314
|
this.#createTimeout("Reconnection request")
|
|
319
|
-
])
|
|
315
|
+
]).finally(() => {
|
|
316
|
+
this.#pendingReconnect = null;
|
|
317
|
+
});
|
|
320
318
|
} catch (error) {
|
|
321
319
|
this.#triggerEvent("status", "Reconnection failed: " + error.message);
|
|
322
320
|
this.#reconnectTimeout = setTimeout(() => this.#attemptReconnect(), 500);
|
|
@@ -357,7 +355,7 @@ class PlaySocket {
|
|
|
357
355
|
* @param {number} maxSize - Max number of participants
|
|
358
356
|
* @returns {Promise} Resolves with room ID
|
|
359
357
|
*/
|
|
360
|
-
createRoom(initialStorage = {}, maxSize) {
|
|
358
|
+
async createRoom(initialStorage = {}, maxSize) {
|
|
361
359
|
if (!this.#initialized) {
|
|
362
360
|
this.#triggerEvent("error", "Cannot create room - not initialized");
|
|
363
361
|
return Promise.reject(new Error("Not initialized"));
|
|
@@ -378,7 +376,9 @@ class PlaySocket {
|
|
|
378
376
|
});
|
|
379
377
|
}),
|
|
380
378
|
this.#createTimeout("Room creation")
|
|
381
|
-
])
|
|
379
|
+
]).finally(() => {
|
|
380
|
+
this.#pendingHost = null;
|
|
381
|
+
});
|
|
382
382
|
}
|
|
383
383
|
|
|
384
384
|
/**
|
|
@@ -386,7 +386,7 @@ class PlaySocket {
|
|
|
386
386
|
* @param {string} roomId - ID of the room
|
|
387
387
|
* @returns {Promise} Resolves when connected
|
|
388
388
|
*/
|
|
389
|
-
joinRoom(roomId) {
|
|
389
|
+
async joinRoom(roomId) {
|
|
390
390
|
if (!this.#initialized) {
|
|
391
391
|
this.#triggerEvent("error", "Cannot join room - not initialized");
|
|
392
392
|
return Promise.reject(new Error("Not initialized"));
|
|
@@ -405,7 +405,9 @@ class PlaySocket {
|
|
|
405
405
|
});
|
|
406
406
|
}),
|
|
407
407
|
this.#createTimeout("Room join")
|
|
408
|
-
])
|
|
408
|
+
]).finally(() => {
|
|
409
|
+
this.#pendingJoin = null;
|
|
410
|
+
});
|
|
409
411
|
}
|
|
410
412
|
|
|
411
413
|
/**
|
|
@@ -422,6 +424,7 @@ class PlaySocket {
|
|
|
422
424
|
value,
|
|
423
425
|
timestamp
|
|
424
426
|
});
|
|
427
|
+
// Optimistic update for regular storage updates
|
|
425
428
|
this.#storage[key] = value;
|
|
426
429
|
this.#storageTimestamps[key] = timestamp;
|
|
427
430
|
this.#triggerEvent("storageUpdated", { ...this.#storage });
|
|
@@ -435,52 +438,17 @@ class PlaySocket {
|
|
|
435
438
|
* @param {*} updateValue - New value for update-matching
|
|
436
439
|
*/
|
|
437
440
|
updateStorageArray(key, operation, value, updateValue) {
|
|
438
|
-
|
|
439
|
-
|
|
441
|
+
// Send operation to server (no optimistic update – we can't guarantee that it is correct)
|
|
442
|
+
// If another client has sent an update to the server that is processed first, the end result will differ.
|
|
443
|
+
const timestamp = this.#getTimestamp();
|
|
440
444
|
this.#sendToServer({
|
|
441
445
|
type: 'room_storage_array_update',
|
|
442
446
|
key,
|
|
443
447
|
operation,
|
|
444
448
|
value,
|
|
445
|
-
updateValue
|
|
449
|
+
updateValue,
|
|
450
|
+
timestamp
|
|
446
451
|
});
|
|
447
|
-
this.#storage[key] = updatedArray;
|
|
448
|
-
this.#storageTimestamps[key] = this.#getTimestamp();
|
|
449
|
-
this.#triggerEvent("storageUpdated", { ...this.#storage });
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
* Handle array operations client-side
|
|
454
|
-
* @private
|
|
455
|
-
*/
|
|
456
|
-
#handleArrayUpdate(key, operation, value, updateValue) {
|
|
457
|
-
let array = (!this.#storage[key] || !Array.isArray(this.#storage[key])) ? [] : [...this.#storage[key]];
|
|
458
|
-
const isObject = typeof value === 'object' && value !== null;
|
|
459
|
-
const compare = (item) => isObject ? JSON.stringify(item) === JSON.stringify(value) : item === value;
|
|
460
|
-
|
|
461
|
-
switch (operation) {
|
|
462
|
-
case 'add':
|
|
463
|
-
array.push(value);
|
|
464
|
-
break;
|
|
465
|
-
|
|
466
|
-
case 'add-unique':
|
|
467
|
-
if (!array.some(compare)) array.push(value);
|
|
468
|
-
break;
|
|
469
|
-
|
|
470
|
-
case 'remove-matching':
|
|
471
|
-
array = array.filter(item => !compare(item));
|
|
472
|
-
break;
|
|
473
|
-
|
|
474
|
-
case 'update-matching':
|
|
475
|
-
const index = array.findIndex(compare);
|
|
476
|
-
if (index !== -1) array[index] = updateValue;
|
|
477
|
-
break;
|
|
478
|
-
|
|
479
|
-
default:
|
|
480
|
-
console.error(ERROR_PREFIX + `Unknown array operation: ${operation}`);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
return array;
|
|
484
452
|
}
|
|
485
453
|
|
|
486
454
|
/**
|
|
@@ -490,6 +458,8 @@ class PlaySocket {
|
|
|
490
458
|
this.#initialized = false; // Set this immediately to prevent automatic reconnection
|
|
491
459
|
|
|
492
460
|
if (this.#socket) {
|
|
461
|
+
// Signal to the server that it can immediately disconnect this user
|
|
462
|
+
if (this.#socket.readyState === WebSocket.OPEN) this.#sendToServer({ type: 'disconnect' });
|
|
493
463
|
this.#socket.close();
|
|
494
464
|
this.#socket = null;
|
|
495
465
|
|
|
@@ -503,6 +473,7 @@ class PlaySocket {
|
|
|
503
473
|
this.#roomHost = null;
|
|
504
474
|
this.#storage = {};
|
|
505
475
|
this.#storageTimestamps = {};
|
|
476
|
+
this.#serverTimeOffset = 0;
|
|
506
477
|
this.#roomId = null;
|
|
507
478
|
this.#connectionCount = 0;
|
|
508
479
|
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(
|
|
217
|
+
this.#syncRoomStorageKey(updateRoom, 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.#syncRoomStorageKey(
|
|
230
|
+
arrayRoom.storageTimestamps[data.key] = data.timestamp;
|
|
231
|
+
this.#syncRoomStorageKey(arrayRoom, data.key, 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,19 +282,20 @@ 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 {Object} room
|
|
288
|
+
* @param {string} key
|
|
289
|
+
* @param {number} timestamp - In milliseconds
|
|
281
290
|
*/
|
|
282
|
-
#syncRoomStorageKey(
|
|
283
|
-
|
|
284
|
-
if (!room) return;
|
|
285
|
-
room.participants.forEach(p => {
|
|
291
|
+
#syncRoomStorageKey(room, key, timestamp) {
|
|
292
|
+
room?.participants?.forEach(p => {
|
|
286
293
|
const client = this.#clients.get(p);
|
|
287
294
|
if (client) {
|
|
288
295
|
client.send(JSON.stringify({
|
|
289
296
|
type: 'storage_sync',
|
|
290
297
|
key,
|
|
291
|
-
value: room
|
|
298
|
+
value: room?.storage?.[key],
|
|
292
299
|
timestamp
|
|
293
300
|
}));
|
|
294
301
|
}
|
|
@@ -334,38 +341,51 @@ function requireServer () {
|
|
|
334
341
|
if (!ws.clientId) return;
|
|
335
342
|
this.#clients.delete(ws.clientId); // Immediately remove from active clients (otherwise, server would try to message this client)
|
|
336
343
|
|
|
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
|
-
}
|
|
344
|
+
if (ws.willfulDisconnect) {
|
|
345
|
+
this.#disconnectClient(ws); // Immediate disconnection
|
|
346
|
+
} else {
|
|
347
|
+
// Pending complete disconnection with 2s grace period to allow for reconnections
|
|
348
|
+
this.#pendingDisconnects.set(ws.clientId, {
|
|
349
|
+
timeout: setTimeout(() => {
|
|
350
|
+
this.#disconnectClient(ws);
|
|
351
|
+
}, 2000)
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
365
355
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
356
|
+
/**
|
|
357
|
+
* Disconnect a client
|
|
358
|
+
* @private
|
|
359
|
+
* @param {Object} ws
|
|
360
|
+
*/
|
|
361
|
+
#disconnectClient(ws) {
|
|
362
|
+
this.#pendingDisconnects.delete(ws.clientId);
|
|
363
|
+
this.#rateLimits.delete(ws.clientId);
|
|
364
|
+
this.#clientTokens.delete(ws.clientId);
|
|
365
|
+
const roomId = this.#clientRooms.get(ws.clientId);
|
|
366
|
+
const room = this.#rooms[roomId];
|
|
367
|
+
|
|
368
|
+
if (room) {
|
|
369
|
+
room.participants = room.participants.filter(p => p !== ws.clientId); // Remove client from room
|
|
370
|
+
this.#clientRooms.delete(ws.clientId);
|
|
371
|
+
|
|
372
|
+
if (room.participants?.length === 0) {
|
|
373
|
+
delete this.#rooms[roomId]; // Delete room if now empty
|
|
374
|
+
} else {
|
|
375
|
+
// Notify remaining participants
|
|
376
|
+
room.participants.forEach(p => {
|
|
377
|
+
const client = this.#clients.get(p);
|
|
378
|
+
if (client) client.send(JSON.stringify({
|
|
379
|
+
type: 'client_disconnected',
|
|
380
|
+
client: ws.clientId,
|
|
381
|
+
host: room.participants[0],
|
|
382
|
+
participantCount: room.participants.length
|
|
383
|
+
}));
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
this.#triggerEvent("clientDisconnected", ws.clientId);
|
|
369
389
|
}
|
|
370
390
|
|
|
371
391
|
/**
|