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.
@@ -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 || 'Unknown reason');
176
- this.#pendingJoin.reject(new Error("Connection rejected: " + message.reason || 'Unknown 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.resolve();
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 || 'Unknown reason'));
224
- this.#triggerEvent("error", "Failed to create room: " + message.reason || 'Unknown 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] = this.#getTimestamp();
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] = Date.now(); // New date to ensure that they are applied regardless of age (they don't overwrite – they transform)
230
- this.#syncRoomStorageKey(arrayRoomId, data.key, Date.now());
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 to all clients in room
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
- // 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
- });
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
- this.#triggerEvent("clientDisconnected", ws.clientId);
367
- }, 2000)
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playsocketjs",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "WebSocket wrapper for creating simple multiplayer systems with ease.",
5
5
  "module": "dist/playsocket-client.js",
6
6
  "exports": {