@signe/room 2.7.2 → 2.8.0

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.7.2",
3
+ "version": "2.8.0",
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.7.2"
20
+ "@signe/sync": "2.8.0"
21
21
  },
22
22
  "publishConfig": {
23
23
  "access": "public"
package/readme.md CHANGED
@@ -134,6 +134,73 @@ You can return:
134
134
 
135
135
  ## Advanced Features
136
136
 
137
+ ### Session Transfer
138
+
139
+ You can transfer a user's session from one room to another using `$sessionTransfer`.
140
+ This preserves the same session id (privateId) across rooms.
141
+
142
+ Server-side (inside a room or action):
143
+
144
+ ```ts
145
+ @Action("transfer")
146
+ async transfer(player: Player, data: { targetRoomId: string }, conn: Party.Connection) {
147
+ const transferToken = await this.$sessionTransfer(conn, data.targetRoomId);
148
+ return { transferToken };
149
+ }
150
+ ```
151
+
152
+ Client-side:
153
+ - Connect to the target room with the same session id (`privateId`).
154
+ - You can pass it as `id` in `connectionRoom` options from `@signe/sync/client`.
155
+ - The target room restores the session and user data.
156
+
157
+ Example (client):
158
+ ```ts
159
+ import { connectionRoom } from "@signe/sync/client";
160
+
161
+ await connectionRoom(
162
+ {
163
+ host: "https://your-host",
164
+ room: "targetRoomId",
165
+ id: "private-session-id",
166
+ },
167
+ roomInstance
168
+ );
169
+ ```
170
+
171
+ Optional: hydrate transferred snapshots before loading
172
+
173
+ If your user snapshot contains ids for complex instances (e.g. inventory items),
174
+ implement `onSessionRestore` on the room to resolve ids into instances before `load`.
175
+
176
+ ```ts
177
+ class GameRoom {
178
+ async onSessionRestore({ userSnapshot }) {
179
+ if (Array.isArray(userSnapshot.items)) {
180
+ const items = await this.itemRegistry.resolveMany(userSnapshot.items);
181
+ return { ...userSnapshot, items };
182
+ }
183
+ return userSnapshot;
184
+ }
185
+ }
186
+ ```
187
+
188
+ ### Snapshot Hydration (Ids -> Instances)
189
+
190
+ When a snapshot only contains ids for complex objects, you need to resolve them
191
+ before calling `load`. This is useful even outside session transfer.
192
+
193
+ ```ts
194
+ const snapshot = createStatesSnapshotDeep(user);
195
+
196
+ // Resolve ids to instances
197
+ const items = await itemRegistry.resolveMany(snapshot.items);
198
+
199
+ // Hydrate and load
200
+ const hydrated = { ...snapshot, items };
201
+ load(user, hydrated, true);
202
+ ```
203
+
137
204
  ### Room Configuration
138
205
 
139
206
  The `@Room` decorator accepts various configuration options:
@@ -571,4 +638,4 @@ test('test', async () => {
571
638
 
572
639
  ## License
573
640
 
574
- MIT
641
+ MIT
package/src/server.ts CHANGED
@@ -6,7 +6,8 @@ import {
6
6
  load,
7
7
  syncClass,
8
8
  DELETE_TOKEN,
9
- generateShortUUID
9
+ generateShortUUID,
10
+ createStatesSnapshotDeep
10
11
  } from "@signe/sync";
11
12
  import type * as Party from "./types/party";
12
13
  import {
@@ -248,6 +249,7 @@ export class Server implements Party.Server {
248
249
  instance.$memoryAll = {}
249
250
  instance.$autoSync = instance["autoSync"] !== false; // Default to true
250
251
  instance.$pendingSync = new Map<string, any>();
252
+ instance.$pendingInitialSync = new Map<Party.Connection, string>(); // Store connections waiting for initial sync with their publicId
251
253
  instance.$send = (conn: Party.Connection, obj: any) => {
252
254
  return this.send(conn, obj, instance)
253
255
  }
@@ -262,6 +264,7 @@ export class Server implements Party.Server {
262
264
  * @description Broadcasts all pending synchronization changes and clears the pending queue.
263
265
  * If there are pending changes, they are merged with $memoryAll and broadcast. If there are no
264
266
  * pending changes, it broadcasts the current state from $memoryAll (useful for forcing a full sync).
267
+ * Also sends initial sync to connections that were waiting for it (with their pId).
265
268
  *
266
269
  * @example
267
270
  * ```typescript
@@ -289,13 +292,28 @@ export class Server implements Party.Server {
289
292
  packet = instance.$memoryAll;
290
293
  }
291
294
 
292
- this.broadcast(
293
- {
295
+ // Send initial sync to connections that were waiting for it (with their pId)
296
+ const pendingConnections = new Set(instance.$pendingInitialSync.keys());
297
+ for (const [conn, publicId] of instance.$pendingInitialSync) {
298
+ this.send(conn, {
294
299
  type: "sync",
295
- value: packet,
296
- },
297
- instance
298
- );
300
+ value: {
301
+ pId: publicId,
302
+ ...packet,
303
+ },
304
+ }, instance);
305
+ }
306
+ instance.$pendingInitialSync.clear();
307
+
308
+ // Broadcast to all other connections (excluding those that just received initial sync)
309
+ for (const conn of this.room.getConnections()) {
310
+ if (!pendingConnections.has(conn)) {
311
+ this.send(conn, {
312
+ type: "sync",
313
+ value: packet,
314
+ }, instance);
315
+ }
316
+ }
299
317
  }
300
318
  instance.$sessionTransfer = async (conn: Party.Connection, targetRoomId: string) => {
301
319
  let user: any;
@@ -339,7 +357,7 @@ export class Server implements Party.Server {
339
357
  }
340
358
 
341
359
  // Create a snapshot of the user state
342
- const userSnapshot = createStatesSnapshot(user);
360
+ const userSnapshot = createStatesSnapshotDeep(user);
343
361
 
344
362
  const transferData = {
345
363
  privateId,
@@ -645,7 +663,7 @@ export class Server implements Party.Server {
645
663
  } else {
646
664
  user = isClass(classType) ? new classType() : classType(conn, ctx);
647
665
  signal()[publicId] = user;
648
- const snapshot = createStatesSnapshot(user);
666
+ const snapshot = createStatesSnapshotDeep(user);
649
667
  this.room.storage.put(`${usersPropName}.${publicId}`, snapshot);
650
668
  }
651
669
  }
@@ -678,8 +696,8 @@ export class Server implements Party.Server {
678
696
  await awaitReturn(subRoom["onJoin"]?.(user, conn, ctx));
679
697
 
680
698
  // Send initial sync data with both IDs to the new connection
681
- // Only send if autoSync is enabled (default behavior)
682
699
  if (subRoom.$autoSync) {
700
+ // Auto sync enabled: send immediately
683
701
  this.send(conn, {
684
702
  type: "sync",
685
703
  value: {
@@ -687,6 +705,9 @@ export class Server implements Party.Server {
687
705
  ...subRoom.$memoryAll,
688
706
  },
689
707
  }, subRoom);
708
+ } else {
709
+ // Auto sync disabled: store connection to receive sync on next $applySync()
710
+ subRoom.$pendingInitialSync.set(conn, publicId);
690
711
  }
691
712
  }
692
713
 
@@ -1039,6 +1060,11 @@ export class Server implements Party.Server {
1039
1060
  return;
1040
1061
  }
1041
1062
 
1063
+ // Clean up pending initial sync for this connection
1064
+ if (subRoom.$pendingInitialSync) {
1065
+ subRoom.$pendingInitialSync.delete(conn);
1066
+ }
1067
+
1042
1068
  const signal = this.getUsersProperty(subRoom);
1043
1069
 
1044
1070
  if (!conn.state) {
@@ -1156,15 +1182,26 @@ export class Server implements Party.Server {
1156
1182
 
1157
1183
  // Create new user instance
1158
1184
  const user = isClass(classType) ? new classType() : classType();
1185
+
1186
+ const hydratedSnapshot =
1187
+ (await awaitReturn(
1188
+ subRoom["onSessionRestore"]?.({
1189
+ userSnapshot,
1190
+ publicId,
1191
+ privateId,
1192
+ sessionState,
1193
+ room: this.room,
1194
+ })
1195
+ )) ?? userSnapshot;
1159
1196
 
1160
1197
  // Load user data from snapshot
1161
- load(user, userSnapshot, true);
1198
+ load(user, hydratedSnapshot, true);
1162
1199
 
1163
1200
  // Add user to signal
1164
1201
  signal()[publicId] = user;
1165
1202
 
1166
1203
  // Save user snapshot to storage
1167
- await this.room.storage.put(`${usersPropName}.${publicId}`, userSnapshot);
1204
+ await this.room.storage.put(`${usersPropName}.${publicId}`, hydratedSnapshot);
1168
1205
  }
1169
1206
  }
1170
1207