@signe/room 2.6.0 → 2.7.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signe/room",
3
- "version": "2.6.0",
3
+ "version": "2.7.1",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "keywords": [],
@@ -17,7 +17,7 @@
17
17
  "dset": "^3.1.3",
18
18
  "partysocket": "^1.0.1",
19
19
  "zod": "^3.23.8",
20
- "@signe/sync": "2.6.0"
20
+ "@signe/sync": "2.7.1"
21
21
  },
22
22
  "publishConfig": {
23
23
  "access": "public"
package/readme.md CHANGED
@@ -246,6 +246,76 @@ class GameRoom {
246
246
  }
247
247
  ```
248
248
 
249
+ ### Manual Synchronization
250
+
251
+ By default, state changes are automatically synchronized to all clients. However, you can disable automatic synchronization and manually control when to broadcast changes using the `autoSync` option and the `$applySync` method.
252
+
253
+ This is useful when you want to:
254
+ - Batch multiple state changes before broadcasting
255
+ - Control synchronization timing for performance optimization
256
+ - Implement custom synchronization logic
257
+
258
+ ```ts
259
+ @Room({
260
+ path: "game"
261
+ })
262
+ class GameRoom {
263
+ autoSync = false; // Disable automatic synchronization
264
+
265
+ @sync() count = signal(0);
266
+ @sync() score = signal(0);
267
+ @sync() level = signal(1);
268
+
269
+ @Action("updateGameState")
270
+ updateGameState(player: Player, data: { count: number, score: number, level: number }) {
271
+ // Make multiple changes without triggering sync
272
+ this.count.set(data.count);
273
+ this.score.set(data.score);
274
+ this.level.set(data.level);
275
+
276
+ // Manually trigger synchronization when ready
277
+ this.$applySync();
278
+ }
279
+
280
+ @Action("batchUpdate")
281
+ batchUpdate(player: Player, updates: any[]) {
282
+ // Apply multiple updates
283
+ updates.forEach(update => {
284
+ // ... apply updates
285
+ });
286
+
287
+ // Sync all changes at once
288
+ this.$applySync();
289
+ }
290
+ }
291
+ ```
292
+
293
+ You can also toggle `autoSync` at runtime:
294
+
295
+ ```ts
296
+ class GameRoom {
297
+ @sync() count = signal(0);
298
+
299
+ @Action("startBatchMode")
300
+ startBatchMode() {
301
+ this.$autoSync = false; // Disable auto sync
302
+ }
303
+
304
+ @Action("endBatchMode")
305
+ endBatchMode() {
306
+ this.$applySync(); // Sync pending changes
307
+ this.$autoSync = true; // Re-enable auto sync
308
+ }
309
+ }
310
+ ```
311
+
312
+ **Instance Properties:**
313
+ - `$autoSync` (boolean): Controls whether synchronization happens automatically (default: `true`)
314
+ - `$pendingSync` (Map): Stores pending synchronization changes when `autoSync` is disabled
315
+ - `$applySync()` (method): Manually broadcasts all pending changes to all clients
316
+
317
+ **Note:** When `autoSync` is disabled, changes are stored in `$pendingSync` until you call `$applySync()`. If you call `$applySync()` with no pending changes, it will broadcast the current state from `$memoryAll`, which is useful for forcing a full state synchronization.
318
+
249
319
  ### Connecting to World Service
250
320
 
251
321
  The World Service provides optimal room and shard assignment for distributed applications. It handles load balancing and allows clients to connect to the most appropriate server.
package/src/interfaces.ts CHANGED
@@ -15,4 +15,9 @@ export interface RoomOnLeave {
15
15
  export interface RoomMethods {
16
16
  $send: (conn: Party.Connection, obj: any) => void;
17
17
  $broadcast: (obj: any) => void;
18
+ $applySync: () => void;
19
+ $sessionTransfer: (conn: Party.Connection, targetRoomId: string) => Promise<string | null>;
20
+ $pendingSync: Map<string, any>;
21
+ $memoryAll: Map<string, any>;
22
+ $autoSync: boolean;
18
23
  }
package/src/server.ts CHANGED
@@ -246,12 +246,57 @@ export class Server implements Party.Server {
246
246
  };
247
247
 
248
248
  instance.$memoryAll = {}
249
+ instance.$autoSync = instance["autoSync"] !== false; // Default to true
250
+ instance.$pendingSync = new Map<string, any>();
249
251
  instance.$send = (conn: Party.Connection, obj: any) => {
250
252
  return this.send(conn, obj, instance)
251
253
  }
252
254
  instance.$broadcast = (obj: any) => {
253
255
  return this.broadcast(obj, instance)
254
256
  }
257
+ /**
258
+ * Applies pending synchronization changes by broadcasting them to all clients.
259
+ * This method is useful when autoSync is disabled and you want to manually trigger synchronization.
260
+ *
261
+ * @method $applySync
262
+ * @description Broadcasts all pending synchronization changes and clears the pending queue.
263
+ * If there are pending changes, they are merged with $memoryAll and broadcast. If there are no
264
+ * pending changes, it broadcasts the current state from $memoryAll (useful for forcing a full sync).
265
+ *
266
+ * @example
267
+ * ```typescript
268
+ * // Disable auto sync
269
+ * instance.$autoSync = false;
270
+ *
271
+ * // Make some changes
272
+ * instance.count.set(10);
273
+ * instance.text.set('hello');
274
+ *
275
+ * // Manually apply sync when ready
276
+ * instance.$applySync();
277
+ * ```
278
+ */
279
+ instance.$applySync = () => {
280
+ let packet: any;
281
+ if (instance.$pendingSync.size > 0) {
282
+ if (options.getMemoryAll) {
283
+ buildObject(instance.$pendingSync, instance.$memoryAll);
284
+ }
285
+ packet = buildObject(instance.$pendingSync, instance.$memoryAll);
286
+ instance.$pendingSync.clear();
287
+ } else {
288
+ // No pending changes, broadcast current state from memory
289
+ packet = instance.$memoryAll;
290
+ }
291
+
292
+ this.broadcast(
293
+ {
294
+ type: "sync",
295
+ value: packet,
296
+ },
297
+ instance
298
+ );
299
+ }
255
300
  instance.$sessionTransfer = async (conn: Party.Connection, targetRoomId: string) => {
256
301
  let user: any;
257
302
 
@@ -326,15 +371,28 @@ export class Server implements Party.Server {
326
371
  }
327
372
  };
328
373
 
329
- // Sync callback: Broadcast changes to all clients
374
+ // Sync callback: Broadcast changes to all clients or store them for manual sync
330
375
  const syncCb = (values) => {
331
376
  if (options.getMemoryAll) {
332
377
  buildObject(values, instance.$memoryAll);
333
378
  }
379
+ // During initialization in hibernate mode, skip entirely
334
380
  if (init && this.isHibernate) {
335
381
  init = false;
336
382
  return;
337
383
  }
384
+
385
+ // If autoSync is disabled, store changes in pendingSync instead of broadcasting
386
+ if (!instance.$autoSync) {
387
+ // Merge pending changes into $pendingSync
388
+ for (const [path, value] of values) {
389
+ instance.$pendingSync.set(path, value);
390
+ }
391
+ values.clear();
392
+ return;
393
+ }
394
+
395
+ // Auto sync: broadcast immediately (even during init if autoSync is enabled)
338
396
  const packet = buildObject(values, instance.$memoryAll);
339
397
  this.broadcast(
340
398
  {
@@ -367,13 +425,14 @@ export class Server implements Party.Server {
367
425
 
368
426
  // Set up syncing and persistence with throttling to optimize performance
369
427
  syncClass(instance, {
370
- onSync: throttle(syncCb, instance["throttleSync"] ?? 500),
371
- onPersist: throttle(persistCb, instance["throttleStorage"] ?? 2000),
428
+ onSync: instance["throttleSync"] ? throttle(syncCb, instance["throttleSync"]) : syncCb,
429
+ onPersist: instance["throttleStorage"] ? throttle(persistCb, instance["throttleStorage"]) : persistCb,
372
430
  });
373
431
 
374
432
  await loadMemory();
375
433
 
376
434
  initPersist = false
435
+ init = false; // Allow syncs after initialization is complete
377
436
 
378
437
  return instance
379
438
  }