@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/dist/index.js +37 -9
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/readme.md +68 -1
- package/src/server.ts +49 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signe/room",
|
|
3
|
-
"version": "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.
|
|
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
|
-
|
|
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:
|
|
296
|
-
|
|
297
|
-
|
|
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 =
|
|
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 =
|
|
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,
|
|
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}`,
|
|
1204
|
+
await this.room.storage.put(`${usersPropName}.${publicId}`, hydratedSnapshot);
|
|
1168
1205
|
}
|
|
1169
1206
|
}
|
|
1170
1207
|
|