@signe/room 2.10.0 → 3.0.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/CHANGELOG.md +7 -0
- package/dist/chunk-EUXUH3YW.js +15 -0
- package/dist/chunk-EUXUH3YW.js.map +1 -0
- package/dist/cloudflare/index.d.ts +71 -0
- package/dist/cloudflare/index.js +320 -0
- package/dist/cloudflare/index.js.map +1 -0
- package/dist/index.d.ts +87 -188
- package/dist/index.js +860 -114
- package/dist/index.js.map +1 -1
- package/dist/node/index.d.ts +164 -0
- package/dist/node/index.js +786 -0
- package/dist/node/index.js.map +1 -0
- package/dist/party-dNs-hqkq.d.ts +175 -0
- package/examples/cloudflare/README.md +62 -0
- package/examples/cloudflare/node_modules/.bin/tsc +17 -0
- package/examples/cloudflare/node_modules/.bin/tsserver +17 -0
- package/examples/cloudflare/node_modules/.bin/wrangler +17 -0
- package/examples/cloudflare/node_modules/.bin/wrangler2 +17 -0
- package/examples/cloudflare/package.json +24 -0
- package/examples/cloudflare/public/index.html +443 -0
- package/examples/cloudflare/src/index.ts +28 -0
- package/examples/cloudflare/src/room.ts +44 -0
- package/examples/cloudflare/tsconfig.json +10 -0
- package/examples/cloudflare/wrangler.jsonc +25 -0
- package/examples/node/README.md +57 -0
- package/examples/node/node_modules/.bin/tsc +17 -0
- package/examples/node/node_modules/.bin/tsserver +17 -0
- package/examples/node/node_modules/.bin/tsx +17 -0
- package/examples/node/package.json +23 -0
- package/examples/node/public/index.html +443 -0
- package/examples/node/room.ts +44 -0
- package/examples/node/server.sqlite.ts +52 -0
- package/examples/node/server.ts +51 -0
- package/examples/node/tsconfig.json +10 -0
- package/examples/node-game/README.md +66 -0
- package/examples/node-game/package.json +23 -0
- package/examples/node-game/public/index.html +705 -0
- package/examples/node-game/room.ts +145 -0
- package/examples/node-game/server.sqlite.ts +54 -0
- package/examples/node-game/server.ts +53 -0
- package/examples/node-game/tsconfig.json +10 -0
- package/examples/node-shard/README.md +32 -0
- package/examples/node-shard/dev.ts +39 -0
- package/examples/node-shard/package.json +24 -0
- package/examples/node-shard/public/index.html +777 -0
- package/examples/node-shard/room-server.ts +68 -0
- package/examples/node-shard/room.ts +105 -0
- package/examples/node-shard/shared.ts +6 -0
- package/examples/node-shard/tsconfig.json +14 -0
- package/examples/node-shard/world-server.ts +169 -0
- package/package.json +14 -5
- package/readme.md +418 -4
- package/src/cloudflare/index.ts +474 -0
- package/src/index.ts +2 -2
- package/src/jwt.ts +1 -5
- package/src/mock.ts +29 -7
- package/src/node/index.ts +1112 -0
- package/src/server.ts +781 -60
- package/src/session.guard.ts +6 -2
- package/src/shard.ts +91 -23
- package/src/storage.ts +29 -5
- package/src/testing.ts +4 -3
- package/src/types/party.ts +30 -1
- package/src/world.guard.ts +23 -4
- package/src/world.ts +121 -21
- package/tests/storage-restore.spec.ts +122 -0
- package/examples/game/.vscode/launch.json +0 -11
- package/examples/game/.vscode/settings.json +0 -11
- package/examples/game/README.md +0 -40
- package/examples/game/app/client.tsx +0 -15
- package/examples/game/app/components/Admin.tsx +0 -1089
- package/examples/game/app/components/Room.tsx +0 -162
- package/examples/game/app/styles.css +0 -31
- package/examples/game/package-lock.json +0 -225
- package/examples/game/package.json +0 -20
- package/examples/game/party/game.room.ts +0 -32
- package/examples/game/party/server.ts +0 -10
- package/examples/game/party/shard.ts +0 -5
- package/examples/game/partykit.json +0 -14
- package/examples/game/public/favicon.ico +0 -0
- package/examples/game/public/index.html +0 -27
- package/examples/game/public/normalize.css +0 -351
- package/examples/game/shared/room.schema.ts +0 -14
- package/examples/game/tsconfig.json +0 -109
package/src/server.ts
CHANGED
|
@@ -33,6 +33,66 @@ type CreateRoomOptions = {
|
|
|
33
33
|
throttleStorage?: number;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
export type StorageRestoreContext<TSnapshot = any> = {
|
|
37
|
+
snapshot: TSnapshot;
|
|
38
|
+
room: Party.Room;
|
|
39
|
+
server: Server;
|
|
40
|
+
legacy: boolean;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type UserStorageRestoreContext<TUser = any, TSnapshot = any> = {
|
|
44
|
+
userSnapshot: TSnapshot;
|
|
45
|
+
user: TUser | undefined;
|
|
46
|
+
publicId: string;
|
|
47
|
+
usersPropName: string;
|
|
48
|
+
room: Party.Room;
|
|
49
|
+
server: Server;
|
|
50
|
+
legacy: boolean;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type SessionData = {
|
|
54
|
+
publicId: string;
|
|
55
|
+
state?: any;
|
|
56
|
+
created?: number;
|
|
57
|
+
connected?: boolean;
|
|
58
|
+
disconnectedAt?: number;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type TransferData = {
|
|
62
|
+
privateId: string;
|
|
63
|
+
publicId: string;
|
|
64
|
+
restored: boolean;
|
|
65
|
+
created: number;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type StorageMetrics = {
|
|
69
|
+
loadMs: number;
|
|
70
|
+
loadStateKeys: number;
|
|
71
|
+
loadLegacyKeys: number;
|
|
72
|
+
stateCompactions: number;
|
|
73
|
+
stateCompactionDeletes: number;
|
|
74
|
+
persistFlushes: number;
|
|
75
|
+
persistWrites: number;
|
|
76
|
+
persistDeletes: number;
|
|
77
|
+
persistLastFlushMs: number;
|
|
78
|
+
sessionGcRuns: number;
|
|
79
|
+
sessionGcScanned: number;
|
|
80
|
+
sessionGcExpired: number;
|
|
81
|
+
sessionIndexRepairs: number;
|
|
82
|
+
transferGcRuns: number;
|
|
83
|
+
transferGcScanned: number;
|
|
84
|
+
transferGcExpired: number;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const STATE_PREFIX = "state:";
|
|
88
|
+
const SESSION_PREFIX = "session:";
|
|
89
|
+
const SESSION_PUBLIC_PREFIX = "session-public:";
|
|
90
|
+
const TRANSFER_PREFIX = "transfer:";
|
|
91
|
+
const INTERNAL_PREFIX = "$room:";
|
|
92
|
+
const SESSION_GC_LAST_RUN_KEY = `${INTERNAL_PREFIX}session-gc:last-run`;
|
|
93
|
+
const TRANSFER_GC_LAST_RUN_KEY = `${INTERNAL_PREFIX}transfer-gc:last-run`;
|
|
94
|
+
const DEFAULT_TRANSFER_EXPIRY_MS = 5 * 60 * 1000;
|
|
95
|
+
|
|
36
96
|
/**
|
|
37
97
|
* @class Server
|
|
38
98
|
* @implements {Party.Server}
|
|
@@ -89,6 +149,234 @@ export class Server implements Party.Server {
|
|
|
89
149
|
return this.room.storage
|
|
90
150
|
}
|
|
91
151
|
|
|
152
|
+
private stateKey(path: string) {
|
|
153
|
+
return `${STATE_PREFIX}${path}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private sessionKey(privateId: string) {
|
|
157
|
+
return `${SESSION_PREFIX}${privateId}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private sessionPublicKey(publicId: string) {
|
|
161
|
+
return `${SESSION_PUBLIC_PREFIX}${publicId}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private transferKey(token: string) {
|
|
165
|
+
return `${TRANSFER_PREFIX}${token}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private isInternalStorageKey(key: string) {
|
|
169
|
+
return key.startsWith(STATE_PREFIX)
|
|
170
|
+
|| key.startsWith(SESSION_PREFIX)
|
|
171
|
+
|| key.startsWith(SESSION_PUBLIC_PREFIX)
|
|
172
|
+
|| key.startsWith(TRANSFER_PREFIX)
|
|
173
|
+
|| key.startsWith(INTERNAL_PREFIX);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private async listStorage<T = unknown>(prefix?: string) {
|
|
177
|
+
if (!prefix) {
|
|
178
|
+
return this.room.storage.list<T>();
|
|
179
|
+
}
|
|
180
|
+
return this.room.storage.list<T>({ prefix });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private async loadStatePath<T = unknown>(path: string) {
|
|
184
|
+
return this.room.storage.get<T>(this.stateKey(path));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private async saveStatePath(path: string, value: any) {
|
|
188
|
+
await this.room.storage.put(this.stateKey(path), value);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private async putStorageEntries(entries: Record<string, any>) {
|
|
192
|
+
const keys = Object.keys(entries);
|
|
193
|
+
if (keys.length === 0) return;
|
|
194
|
+
if (keys.length === 1) {
|
|
195
|
+
const key = keys[0];
|
|
196
|
+
await this.room.storage.put(key, entries[key]);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
await this.room.storage.put(entries);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private async deleteStorageKeys(keys: string[]) {
|
|
203
|
+
const uniqueKeys = Array.from(new Set(keys));
|
|
204
|
+
if (uniqueKeys.length === 0) return;
|
|
205
|
+
if (uniqueKeys.length === 1) {
|
|
206
|
+
await this.room.storage.delete(uniqueKeys[0]);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
await this.room.storage.delete(uniqueKeys);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private async deleteStatePath(path: string) {
|
|
213
|
+
const stateKey = this.stateKey(path);
|
|
214
|
+
const descendantEntries = await this.listStorage(`${stateKey}.`);
|
|
215
|
+
await this.deleteStorageKeys([
|
|
216
|
+
...Array.from(descendantEntries.keys()),
|
|
217
|
+
stateKey,
|
|
218
|
+
]);
|
|
219
|
+
await this.saveStatePath(path, DELETE_TOKEN);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private containsDeleteToken(value: any): boolean {
|
|
223
|
+
if (value === DELETE_TOKEN) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
if (!value || typeof value !== "object") {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
if (Array.isArray(value)) {
|
|
230
|
+
return value.some((item) => this.containsDeleteToken(item));
|
|
231
|
+
}
|
|
232
|
+
return Object.values(value).some((item) => this.containsDeleteToken(item));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private async compactStateStorage(instance: any) {
|
|
236
|
+
const entries = await this.listStorage(STATE_PREFIX);
|
|
237
|
+
const keys = Array.from(entries.keys());
|
|
238
|
+
if (keys.length === 0) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const snapshot = createStatesSnapshotDeep(instance);
|
|
243
|
+
await this.deleteStorageKeys(keys);
|
|
244
|
+
await this.saveStatePath(".", snapshot);
|
|
245
|
+
|
|
246
|
+
const metrics = instance.$storageMetrics as StorageMetrics | undefined;
|
|
247
|
+
if (metrics) {
|
|
248
|
+
metrics.stateCompactions += 1;
|
|
249
|
+
metrics.stateCompactionDeletes += keys.length;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private createUserFromClassType(classType: any, ...args: any[]) {
|
|
254
|
+
if (!classType) {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
return isClass(classType) ? new classType() : classType(...args);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private async restoreUsersStorageSnapshot(
|
|
261
|
+
instance: any,
|
|
262
|
+
snapshot: any,
|
|
263
|
+
options: { legacy: boolean }
|
|
264
|
+
) {
|
|
265
|
+
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) {
|
|
266
|
+
return snapshot;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const restoreUser = instance["onUserStorageRestore"];
|
|
270
|
+
if (typeof restoreUser !== "function") {
|
|
271
|
+
return snapshot;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const signal = this.getUsersProperty(instance);
|
|
275
|
+
const usersPropName = this.getUsersPropName(instance);
|
|
276
|
+
if (!signal || !usersPropName) {
|
|
277
|
+
return snapshot;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const usersSnapshot = snapshot[usersPropName];
|
|
281
|
+
if (!usersSnapshot || typeof usersSnapshot !== "object" || Array.isArray(usersSnapshot)) {
|
|
282
|
+
return snapshot;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const { classType } = signal.options || {};
|
|
286
|
+
let nextSnapshot = snapshot;
|
|
287
|
+
let nextUsersSnapshot = usersSnapshot;
|
|
288
|
+
|
|
289
|
+
for (const [publicId, userSnapshot] of Object.entries(usersSnapshot)) {
|
|
290
|
+
const user = this.createUserFromClassType(classType, publicId);
|
|
291
|
+
const restoredUserSnapshot = await awaitReturn(
|
|
292
|
+
restoreUser.call(instance, {
|
|
293
|
+
userSnapshot,
|
|
294
|
+
user,
|
|
295
|
+
publicId,
|
|
296
|
+
usersPropName,
|
|
297
|
+
room: this.room,
|
|
298
|
+
server: this,
|
|
299
|
+
legacy: options.legacy,
|
|
300
|
+
} satisfies UserStorageRestoreContext)
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
if (restoredUserSnapshot !== undefined && restoredUserSnapshot !== userSnapshot) {
|
|
304
|
+
if (nextSnapshot === snapshot) {
|
|
305
|
+
nextSnapshot = { ...snapshot };
|
|
306
|
+
}
|
|
307
|
+
if (nextUsersSnapshot === usersSnapshot) {
|
|
308
|
+
nextUsersSnapshot = { ...usersSnapshot };
|
|
309
|
+
nextSnapshot[usersPropName] = nextUsersSnapshot;
|
|
310
|
+
}
|
|
311
|
+
nextUsersSnapshot[publicId] = restoredUserSnapshot;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return nextSnapshot;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private async restoreStorageSnapshot(
|
|
319
|
+
instance: any,
|
|
320
|
+
snapshot: any,
|
|
321
|
+
options: { legacy: boolean }
|
|
322
|
+
) {
|
|
323
|
+
const restoreSnapshot = instance["onStorageRestore"];
|
|
324
|
+
let restoredSnapshot = snapshot;
|
|
325
|
+
|
|
326
|
+
if (typeof restoreSnapshot === "function") {
|
|
327
|
+
const result = await awaitReturn(
|
|
328
|
+
restoreSnapshot.call(instance, {
|
|
329
|
+
snapshot,
|
|
330
|
+
room: this.room,
|
|
331
|
+
server: this,
|
|
332
|
+
legacy: options.legacy,
|
|
333
|
+
} satisfies StorageRestoreContext)
|
|
334
|
+
);
|
|
335
|
+
if (result !== undefined) {
|
|
336
|
+
restoredSnapshot = result;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return this.restoreUsersStorageSnapshot(instance, restoredSnapshot, options);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private createStorageMetrics(): StorageMetrics {
|
|
344
|
+
return {
|
|
345
|
+
loadMs: 0,
|
|
346
|
+
loadStateKeys: 0,
|
|
347
|
+
loadLegacyKeys: 0,
|
|
348
|
+
stateCompactions: 0,
|
|
349
|
+
stateCompactionDeletes: 0,
|
|
350
|
+
persistFlushes: 0,
|
|
351
|
+
persistWrites: 0,
|
|
352
|
+
persistDeletes: 0,
|
|
353
|
+
persistLastFlushMs: 0,
|
|
354
|
+
sessionGcRuns: 0,
|
|
355
|
+
sessionGcScanned: 0,
|
|
356
|
+
sessionGcExpired: 0,
|
|
357
|
+
sessionIndexRepairs: 0,
|
|
358
|
+
transferGcRuns: 0,
|
|
359
|
+
transferGcScanned: 0,
|
|
360
|
+
transferGcExpired: 0,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private getTransferExpiryTime(subRoom: any) {
|
|
365
|
+
return subRoom?.transferExpiryTime
|
|
366
|
+
?? subRoom?.constructor?.prototype?.transferExpiryTime
|
|
367
|
+
?? subRoom?.constructor?.transferExpiryTime
|
|
368
|
+
?? DEFAULT_TRANSFER_EXPIRY_MS;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private getPrivateId(conn: Party.Connection) {
|
|
372
|
+
return (conn.state as any)?.privateId || conn.sessionId || conn.id;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private hasActiveSessionConnection(privateId: string) {
|
|
376
|
+
return Array.from(this.room.getConnections())
|
|
377
|
+
.some((conn) => this.getPrivateId(conn) === privateId);
|
|
378
|
+
}
|
|
379
|
+
|
|
92
380
|
async send(conn: Party.Connection, obj: any, subRoom: any) {
|
|
93
381
|
obj = structuredClone(obj);
|
|
94
382
|
if (subRoom.interceptorPacket) {
|
|
@@ -138,28 +426,37 @@ export class Server implements Party.Server {
|
|
|
138
426
|
const subRoom = await this.getSubRoom();
|
|
139
427
|
if (!subRoom) return;
|
|
140
428
|
|
|
429
|
+
const SESSION_EXPIRY_TIME = Number(options.sessionExpiryTime);
|
|
430
|
+
if (!Number.isFinite(SESSION_EXPIRY_TIME)) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
141
434
|
// Get active connections
|
|
142
435
|
const activeConnections = [...this.room.getConnections()];
|
|
143
|
-
const activePrivateIds = new Set(activeConnections.map(conn => conn
|
|
436
|
+
const activePrivateIds = new Set(activeConnections.map(conn => this.getPrivateId(conn)));
|
|
144
437
|
|
|
145
438
|
try {
|
|
146
439
|
// Get all sessions from storage
|
|
147
|
-
const sessions = await this.
|
|
440
|
+
const sessions = await this.listStorage<SessionData>(SESSION_PREFIX);
|
|
148
441
|
const users = this.getUsersProperty(subRoom);
|
|
149
442
|
const usersPropName = this.getUsersPropName(subRoom);
|
|
443
|
+
const metrics = subRoom.$storageMetrics as StorageMetrics | undefined;
|
|
444
|
+
if (metrics) {
|
|
445
|
+
metrics.sessionGcRuns += 1;
|
|
446
|
+
metrics.sessionGcScanned += sessions.size;
|
|
447
|
+
}
|
|
150
448
|
|
|
151
449
|
// Store valid publicIds from sessions
|
|
152
450
|
const validPublicIds = new Set<string>();
|
|
153
451
|
const expiredPublicIds = new Set<string>();
|
|
154
|
-
const SESSION_EXPIRY_TIME = options.sessionExpiryTime
|
|
155
452
|
const now = Date.now();
|
|
156
453
|
|
|
157
454
|
for (const [key, session] of sessions) {
|
|
158
455
|
// Only process session entries
|
|
159
|
-
if (!key.startsWith(
|
|
456
|
+
if (!key.startsWith(SESSION_PREFIX)) continue;
|
|
160
457
|
|
|
161
|
-
const privateId = key.
|
|
162
|
-
const typedSession = session as
|
|
458
|
+
const privateId = key.slice(SESSION_PREFIX.length);
|
|
459
|
+
const typedSession = session as SessionData;
|
|
163
460
|
|
|
164
461
|
// Check if session should be deleted based on:
|
|
165
462
|
// 1. Connection is not active
|
|
@@ -167,16 +464,22 @@ export class Server implements Party.Server {
|
|
|
167
464
|
// 3. Session is older than expiry time
|
|
168
465
|
if (!activePrivateIds.has(privateId) &&
|
|
169
466
|
!typedSession.connected &&
|
|
170
|
-
|
|
467
|
+
typedSession.disconnectedAt !== undefined &&
|
|
468
|
+
(now - typedSession.disconnectedAt) >= SESSION_EXPIRY_TIME) {
|
|
171
469
|
// Delete expired session
|
|
172
470
|
await this.deleteSession(privateId);
|
|
173
471
|
expiredPublicIds.add(typedSession.publicId);
|
|
472
|
+
if (metrics) {
|
|
473
|
+
metrics.sessionGcExpired += 1;
|
|
474
|
+
}
|
|
174
475
|
} else if (typedSession && typedSession.publicId) {
|
|
175
476
|
// Keep track of valid publicIds from active or recent sessions
|
|
176
477
|
validPublicIds.add(typedSession.publicId);
|
|
177
478
|
}
|
|
178
479
|
}
|
|
179
480
|
|
|
481
|
+
await this.repairSessionPublicIndexes(sessions, metrics);
|
|
482
|
+
|
|
180
483
|
// Clean up users only if ALL their sessions are expired
|
|
181
484
|
if (users && usersPropName) {
|
|
182
485
|
const currentUsers = users();
|
|
@@ -184,7 +487,6 @@ export class Server implements Party.Server {
|
|
|
184
487
|
// Only delete user if they have an expired session and no valid sessions
|
|
185
488
|
if (expiredPublicIds.has(publicId) && !validPublicIds.has(publicId)) {
|
|
186
489
|
delete currentUsers[publicId];
|
|
187
|
-
await this.room.storage.delete(`${usersPropName}.${publicId}`);
|
|
188
490
|
}
|
|
189
491
|
}
|
|
190
492
|
}
|
|
@@ -194,6 +496,177 @@ export class Server implements Party.Server {
|
|
|
194
496
|
}
|
|
195
497
|
}
|
|
196
498
|
|
|
499
|
+
private scheduleSessionGarbageCollector(sessionExpiryTime: number | undefined, privateId?: string) {
|
|
500
|
+
const normalizedSessionExpiryTime = Number(sessionExpiryTime);
|
|
501
|
+
if (!Number.isFinite(normalizedSessionExpiryTime) || normalizedSessionExpiryTime < 0) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
setTimeout(() => {
|
|
506
|
+
if (privateId) {
|
|
507
|
+
void this.expireDisconnectedSession(privateId, normalizedSessionExpiryTime);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
void this.garbageCollector({ sessionExpiryTime: normalizedSessionExpiryTime });
|
|
511
|
+
}, normalizedSessionExpiryTime);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private getSessionExpiryTime(subRoom: any) {
|
|
515
|
+
return subRoom?.sessionExpiryTime
|
|
516
|
+
?? subRoom?.constructor?.prototype?.sessionExpiryTime
|
|
517
|
+
?? subRoom?.constructor?.sessionExpiryTime;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private async shouldRunSessionGarbageCollector(sessionExpiryTime: number | undefined) {
|
|
521
|
+
const normalizedSessionExpiryTime = Number(sessionExpiryTime);
|
|
522
|
+
if (!Number.isFinite(normalizedSessionExpiryTime) || normalizedSessionExpiryTime < 0) {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const now = Date.now();
|
|
527
|
+
const lastRun = await this.room.storage.get<number>(SESSION_GC_LAST_RUN_KEY);
|
|
528
|
+
if (lastRun && now - lastRun < normalizedSessionExpiryTime) {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
await this.room.storage.put(SESSION_GC_LAST_RUN_KEY, now);
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private async shouldRunInterval(key: string, interval: number) {
|
|
537
|
+
const normalizedInterval = Number(interval);
|
|
538
|
+
if (!Number.isFinite(normalizedInterval) || normalizedInterval < 0) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const now = Date.now();
|
|
543
|
+
const lastRun = await this.room.storage.get<number>(key);
|
|
544
|
+
if (lastRun && now - lastRun < normalizedInterval) {
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
await this.room.storage.put(key, now);
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private async repairSessionPublicIndexes(
|
|
553
|
+
sessions?: Map<string, SessionData>,
|
|
554
|
+
metrics?: StorageMetrics
|
|
555
|
+
) {
|
|
556
|
+
const sessionEntries = sessions ?? await this.listStorage<SessionData>(SESSION_PREFIX);
|
|
557
|
+
const expected = new Map<string, string[]>();
|
|
558
|
+
|
|
559
|
+
for (const [key, session] of sessionEntries) {
|
|
560
|
+
if (!session?.publicId) continue;
|
|
561
|
+
const privateId = key.slice(SESSION_PREFIX.length);
|
|
562
|
+
const privateIds = expected.get(session.publicId) ?? [];
|
|
563
|
+
privateIds.push(privateId);
|
|
564
|
+
expected.set(session.publicId, privateIds);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const publicIndexes = await this.listStorage<string[]>(SESSION_PUBLIC_PREFIX);
|
|
568
|
+
const writes: Record<string, string[]> = {};
|
|
569
|
+
const deletes: string[] = [];
|
|
570
|
+
let repairs = 0;
|
|
571
|
+
|
|
572
|
+
for (const [publicId, privateIds] of expected) {
|
|
573
|
+
privateIds.sort();
|
|
574
|
+
const key = this.sessionPublicKey(publicId);
|
|
575
|
+
const existing = publicIndexes.get(key) ?? [];
|
|
576
|
+
const normalizedExisting = Array.isArray(existing) ? [...existing].sort() : [];
|
|
577
|
+
if (JSON.stringify(normalizedExisting) !== JSON.stringify(privateIds)) {
|
|
578
|
+
writes[key] = privateIds;
|
|
579
|
+
repairs += 1;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
for (const [key] of publicIndexes) {
|
|
584
|
+
const publicId = key.slice(SESSION_PUBLIC_PREFIX.length);
|
|
585
|
+
if (!expected.has(publicId)) {
|
|
586
|
+
deletes.push(key);
|
|
587
|
+
repairs += 1;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
await Promise.all([
|
|
592
|
+
this.putStorageEntries(writes),
|
|
593
|
+
this.deleteStorageKeys(deletes),
|
|
594
|
+
]);
|
|
595
|
+
|
|
596
|
+
if (metrics) {
|
|
597
|
+
metrics.sessionIndexRepairs += repairs;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private isTransferExpired(transfer: Partial<TransferData> | null | undefined, transferExpiryTime: number) {
|
|
602
|
+
if (!transfer) return true;
|
|
603
|
+
if (!Number.isFinite(transferExpiryTime) || transferExpiryTime < 0) {
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
const created = Number(transfer.created);
|
|
607
|
+
if (!Number.isFinite(created)) {
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
return Date.now() - created >= transferExpiryTime;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
private async cleanupExpiredTransfers(transferExpiryTime: number, metrics?: StorageMetrics) {
|
|
614
|
+
if (!Number.isFinite(transferExpiryTime) || transferExpiryTime < 0) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const transfers = await this.listStorage<TransferData>(TRANSFER_PREFIX);
|
|
619
|
+
const expiredKeys: string[] = [];
|
|
620
|
+
for (const [key, transfer] of transfers) {
|
|
621
|
+
if (this.isTransferExpired(transfer, transferExpiryTime)) {
|
|
622
|
+
expiredKeys.push(key);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
await this.deleteStorageKeys(expiredKeys);
|
|
627
|
+
|
|
628
|
+
if (metrics) {
|
|
629
|
+
metrics.transferGcRuns += 1;
|
|
630
|
+
metrics.transferGcScanned += transfers.size;
|
|
631
|
+
metrics.transferGcExpired += expiredKeys.length;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
private async expireDisconnectedSession(privateId: string, sessionExpiryTime: number) {
|
|
636
|
+
const session = await this.getSession(privateId);
|
|
637
|
+
if (!session || session.connected || session.disconnectedAt === undefined) {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (this.hasActiveSessionConnection(privateId)) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const elapsed = Date.now() - session.disconnectedAt;
|
|
646
|
+
if (elapsed < sessionExpiryTime) {
|
|
647
|
+
setTimeout(() => {
|
|
648
|
+
void this.expireDisconnectedSession(privateId, sessionExpiryTime);
|
|
649
|
+
}, sessionExpiryTime - elapsed);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
await this.deleteSession(privateId);
|
|
654
|
+
|
|
655
|
+
const privateIds = await this.getSessionPrivateIds(session.publicId);
|
|
656
|
+
for (const otherPrivateId of privateIds) {
|
|
657
|
+
const otherSession = await this.getSession(otherPrivateId);
|
|
658
|
+
if (otherSession?.publicId === session.publicId) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const subRoom = await this.getSubRoom();
|
|
664
|
+
const users = this.getUsersProperty(subRoom);
|
|
665
|
+
if (users?.()[session.publicId]) {
|
|
666
|
+
delete users()[session.publicId];
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
197
670
|
/**
|
|
198
671
|
* @method createRoom
|
|
199
672
|
* @private
|
|
@@ -231,22 +704,73 @@ export class Server implements Party.Server {
|
|
|
231
704
|
// Load the room's memory from storage
|
|
232
705
|
// This ensures persistence across server restarts
|
|
233
706
|
const loadMemory = async () => {
|
|
234
|
-
const
|
|
235
|
-
const
|
|
707
|
+
const startedAt = Date.now();
|
|
708
|
+
const metrics = instance.$storageMetrics as StorageMetrics;
|
|
709
|
+
const root = await this.loadStatePath(".");
|
|
710
|
+
const memory = await this.listStorage(STATE_PREFIX);
|
|
711
|
+
metrics.loadStateKeys = memory.size;
|
|
236
712
|
const tmpObject: any = root || {};
|
|
237
|
-
for (let [
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
if (key == ".") {
|
|
713
|
+
for (let [storageKey, value] of memory) {
|
|
714
|
+
const key = storageKey.slice(STATE_PREFIX.length);
|
|
715
|
+
if (key === ".") {
|
|
242
716
|
continue;
|
|
243
717
|
}
|
|
244
718
|
dset(tmpObject, key, value);
|
|
245
719
|
}
|
|
246
|
-
|
|
720
|
+
|
|
721
|
+
if (root === undefined && memory.size === 0) {
|
|
722
|
+
const legacyRoot = await this.room.storage.get(".");
|
|
723
|
+
const legacyMemory = await this.room.storage.list();
|
|
724
|
+
const legacyObject: any = legacyRoot || {};
|
|
725
|
+
const migratedEntries: Array<[string, any]> = [];
|
|
726
|
+
const legacyDeleteKeys: string[] = [];
|
|
727
|
+
|
|
728
|
+
if (legacyRoot !== undefined) {
|
|
729
|
+
migratedEntries.push([".", legacyRoot]);
|
|
730
|
+
legacyDeleteKeys.push(".");
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
for (let [key, value] of legacyMemory) {
|
|
734
|
+
if (key === "." || this.isInternalStorageKey(key)) {
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
dset(legacyObject, key, value);
|
|
738
|
+
migratedEntries.push([key, value]);
|
|
739
|
+
legacyDeleteKeys.push(key);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
metrics.loadLegacyKeys = migratedEntries.length;
|
|
743
|
+
await this.putStorageEntries(
|
|
744
|
+
Object.fromEntries(
|
|
745
|
+
migratedEntries.map(([path, value]) => [this.stateKey(path), value])
|
|
746
|
+
)
|
|
747
|
+
);
|
|
748
|
+
if (legacyDeleteKeys.length > 0) {
|
|
749
|
+
await this.deleteStorageKeys(legacyDeleteKeys);
|
|
750
|
+
}
|
|
751
|
+
const restoredLegacyObject = await this.restoreStorageSnapshot(instance, legacyObject, {
|
|
752
|
+
legacy: true,
|
|
753
|
+
});
|
|
754
|
+
load(instance, restoredLegacyObject, true);
|
|
755
|
+
if (this.containsDeleteToken(restoredLegacyObject)) {
|
|
756
|
+
await this.compactStateStorage(instance);
|
|
757
|
+
}
|
|
758
|
+
metrics.loadMs = Date.now() - startedAt;
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const restoredObject = await this.restoreStorageSnapshot(instance, tmpObject, {
|
|
763
|
+
legacy: false,
|
|
764
|
+
});
|
|
765
|
+
load(instance, restoredObject, true);
|
|
766
|
+
if (this.containsDeleteToken(restoredObject)) {
|
|
767
|
+
await this.compactStateStorage(instance);
|
|
768
|
+
}
|
|
769
|
+
metrics.loadMs = Date.now() - startedAt;
|
|
247
770
|
};
|
|
248
771
|
|
|
249
772
|
instance.$memoryAll = {}
|
|
773
|
+
instance.$storageMetrics = this.createStorageMetrics();
|
|
250
774
|
instance.$autoSync = instance["autoSync"] !== false; // Default to true
|
|
251
775
|
instance.$pendingSync = new Map<string, any>();
|
|
252
776
|
instance.$pendingInitialSync = new Map<Party.Connection, string>(); // Store connections waiting for initial sync with their publicId
|
|
@@ -333,17 +857,9 @@ export class Server implements Party.Server {
|
|
|
333
857
|
return null;
|
|
334
858
|
}
|
|
335
859
|
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
for (const [key, session] of sessions) {
|
|
341
|
-
if (key.startsWith('session:') && (session as any).publicId === publicId) {
|
|
342
|
-
userSession = session;
|
|
343
|
-
privateId = key.replace('session:', '');
|
|
344
|
-
break;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
860
|
+
const sessionEntry = await this.getSessionEntryByPublicId(publicId);
|
|
861
|
+
const userSession = sessionEntry?.session;
|
|
862
|
+
const privateId = sessionEntry?.privateId ?? null;
|
|
347
863
|
|
|
348
864
|
if (!userSession || !privateId) {
|
|
349
865
|
console.error(`[sessionTransfer] Session for publicId ${publicId} not found.`);
|
|
@@ -422,29 +938,115 @@ export class Server implements Party.Server {
|
|
|
422
938
|
values.clear();
|
|
423
939
|
}
|
|
424
940
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
941
|
+
const flushPersist = async (values: Map<string, any>) => {
|
|
942
|
+
const startedAt = Date.now();
|
|
943
|
+
const stateWrites: Record<string, any> = {};
|
|
944
|
+
const deleteTasks: Promise<void>[] = [];
|
|
945
|
+
let hasDeletes = false;
|
|
431
946
|
for (let [path, value] of values) {
|
|
432
947
|
const _instance =
|
|
433
948
|
path == "." ? instance : getByPath(instance, path);
|
|
434
|
-
const itemValue = createStatesSnapshot(_instance);
|
|
435
949
|
if (value == DELETE_TOKEN) {
|
|
436
|
-
|
|
950
|
+
hasDeletes = true;
|
|
951
|
+
deleteTasks.push(this.deleteStatePath(path));
|
|
437
952
|
} else {
|
|
438
|
-
|
|
953
|
+
const itemValue = _instance?.$snapshot
|
|
954
|
+
? createStatesSnapshot(_instance)
|
|
955
|
+
: value;
|
|
956
|
+
stateWrites[this.stateKey(path)] = itemValue;
|
|
439
957
|
}
|
|
440
958
|
}
|
|
959
|
+
await Promise.all([
|
|
960
|
+
this.putStorageEntries(stateWrites),
|
|
961
|
+
...deleteTasks,
|
|
962
|
+
]);
|
|
963
|
+
if (hasDeletes) {
|
|
964
|
+
await this.compactStateStorage(instance);
|
|
965
|
+
}
|
|
966
|
+
const metrics = instance.$storageMetrics as StorageMetrics | undefined;
|
|
967
|
+
if (metrics) {
|
|
968
|
+
metrics.persistFlushes += 1;
|
|
969
|
+
metrics.persistWrites += Object.keys(stateWrites).length;
|
|
970
|
+
metrics.persistDeletes += deleteTasks.length;
|
|
971
|
+
metrics.persistLastFlushMs = Date.now() - startedAt;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
let persistQueue = Promise.resolve();
|
|
976
|
+
|
|
977
|
+
// Persist callback: Save changes to storage
|
|
978
|
+
const persistCb = async (values: Map<string, any>) => {
|
|
979
|
+
if (initPersist) {
|
|
980
|
+
values.clear();
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const valuesSnapshot = new Map(values);
|
|
441
985
|
values.clear();
|
|
986
|
+
|
|
987
|
+
persistQueue = persistQueue.then(
|
|
988
|
+
() => flushPersist(valuesSnapshot),
|
|
989
|
+
() => flushPersist(valuesSnapshot)
|
|
990
|
+
);
|
|
991
|
+
await persistQueue;
|
|
442
992
|
}
|
|
443
993
|
|
|
994
|
+
const debouncePersist = (wait: number) => {
|
|
995
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
996
|
+
let flushing = false;
|
|
997
|
+
const pending = new Map<string, any>();
|
|
998
|
+
|
|
999
|
+
const schedule = () => {
|
|
1000
|
+
if (timeout) {
|
|
1001
|
+
clearTimeout(timeout);
|
|
1002
|
+
}
|
|
1003
|
+
timeout = setTimeout(() => {
|
|
1004
|
+
void flush();
|
|
1005
|
+
}, wait);
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
const flush = async () => {
|
|
1009
|
+
timeout = null;
|
|
1010
|
+
if (flushing) {
|
|
1011
|
+
schedule();
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const values = new Map(pending);
|
|
1016
|
+
pending.clear();
|
|
1017
|
+
if (!values.size) {
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
flushing = true;
|
|
1022
|
+
try {
|
|
1023
|
+
await persistCb(values);
|
|
1024
|
+
} finally {
|
|
1025
|
+
flushing = false;
|
|
1026
|
+
if (pending.size) {
|
|
1027
|
+
schedule();
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
return (values: Map<string, any>) => {
|
|
1033
|
+
if (initPersist) {
|
|
1034
|
+
values.clear();
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
for (const [path, value] of values) {
|
|
1039
|
+
pending.set(path, value);
|
|
1040
|
+
}
|
|
1041
|
+
values.clear();
|
|
1042
|
+
schedule();
|
|
1043
|
+
};
|
|
1044
|
+
};
|
|
1045
|
+
|
|
444
1046
|
// Set up syncing and persistence with throttling to optimize performance
|
|
445
1047
|
syncClass(instance, {
|
|
446
1048
|
onSync: instance["throttleSync"] ? throttle(syncCb, instance["throttleSync"]) : syncCb,
|
|
447
|
-
onPersist: instance["throttleStorage"] ?
|
|
1049
|
+
onPersist: instance["throttleStorage"] ? debouncePersist(instance["throttleStorage"]) : persistCb,
|
|
448
1050
|
});
|
|
449
1051
|
|
|
450
1052
|
await loadMemory();
|
|
@@ -564,29 +1166,107 @@ export class Server implements Party.Server {
|
|
|
564
1166
|
* console.log(session);
|
|
565
1167
|
* ```
|
|
566
1168
|
*/
|
|
567
|
-
async getSession(privateId: string): Promise<
|
|
1169
|
+
async getSession(privateId: string): Promise<SessionData | null> {
|
|
568
1170
|
if (!privateId) return null;
|
|
569
1171
|
try {
|
|
570
|
-
const session = await this.room.storage.get(
|
|
571
|
-
return session as
|
|
1172
|
+
const session = await this.room.storage.get(this.sessionKey(privateId));
|
|
1173
|
+
return session as SessionData | null;
|
|
572
1174
|
} catch (e) {
|
|
573
1175
|
return null;
|
|
574
1176
|
}
|
|
575
1177
|
}
|
|
576
1178
|
|
|
577
|
-
private async
|
|
1179
|
+
private async getSessionPrivateIds(publicId: string): Promise<string[]> {
|
|
1180
|
+
if (!publicId) return [];
|
|
1181
|
+
const privateIds = await this.room.storage.get<string[]>(this.sessionPublicKey(publicId));
|
|
1182
|
+
return Array.isArray(privateIds) ? privateIds : [];
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
private async saveSessionPrivateIds(publicId: string, privateIds: string[]) {
|
|
1186
|
+
const key = this.sessionPublicKey(publicId);
|
|
1187
|
+
if (privateIds.length === 0) {
|
|
1188
|
+
await this.room.storage.delete(key);
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
await this.room.storage.put(key, privateIds);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
private async addSessionToPublicIndex(privateId: string, publicId: string) {
|
|
1195
|
+
const privateIds = await this.getSessionPrivateIds(publicId);
|
|
1196
|
+
if (privateIds.includes(privateId)) {
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
await this.saveSessionPrivateIds(publicId, [...privateIds, privateId]);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
private async removeSessionFromPublicIndex(privateId: string, publicId: string) {
|
|
1203
|
+
const privateIds = await this.getSessionPrivateIds(publicId);
|
|
1204
|
+
await this.saveSessionPrivateIds(
|
|
1205
|
+
publicId,
|
|
1206
|
+
privateIds.filter((id) => id !== privateId)
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
private async getSessionEntryByPublicId(publicId: string): Promise<{ privateId: string; session: SessionData } | null> {
|
|
1211
|
+
const indexedPrivateIds = await this.getSessionPrivateIds(publicId);
|
|
1212
|
+
const stalePrivateIds: string[] = [];
|
|
1213
|
+
|
|
1214
|
+
for (const privateId of indexedPrivateIds) {
|
|
1215
|
+
const session = await this.getSession(privateId);
|
|
1216
|
+
if (session?.publicId === publicId) {
|
|
1217
|
+
return { privateId, session };
|
|
1218
|
+
}
|
|
1219
|
+
stalePrivateIds.push(privateId);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
if (stalePrivateIds.length) {
|
|
1223
|
+
await this.saveSessionPrivateIds(
|
|
1224
|
+
publicId,
|
|
1225
|
+
indexedPrivateIds.filter((id) => !stalePrivateIds.includes(id))
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const sessions = await this.listStorage<SessionData>(SESSION_PREFIX);
|
|
1230
|
+
for (const [key, session] of sessions) {
|
|
1231
|
+
const privateId = key.slice(SESSION_PREFIX.length);
|
|
1232
|
+
if (session?.publicId) {
|
|
1233
|
+
await this.addSessionToPublicIndex(privateId, session.publicId);
|
|
1234
|
+
}
|
|
1235
|
+
if (session?.publicId === publicId) {
|
|
1236
|
+
return { privateId, session };
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
return null;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
private async saveSession(privateId: string, data: SessionData) {
|
|
1244
|
+
const existingSession = await this.getSession(privateId);
|
|
578
1245
|
const sessionData = {
|
|
579
1246
|
...data,
|
|
580
1247
|
created: data.created || Date.now(),
|
|
581
1248
|
connected: data.connected !== undefined ? data.connected : true
|
|
582
1249
|
};
|
|
583
|
-
await this.room.storage.put(
|
|
1250
|
+
await this.room.storage.put(this.sessionKey(privateId), sessionData);
|
|
1251
|
+
if (existingSession?.publicId && existingSession.publicId !== sessionData.publicId) {
|
|
1252
|
+
await this.removeSessionFromPublicIndex(privateId, existingSession.publicId);
|
|
1253
|
+
}
|
|
1254
|
+
await this.addSessionToPublicIndex(privateId, sessionData.publicId);
|
|
584
1255
|
}
|
|
585
1256
|
|
|
586
1257
|
private async updateSessionConnection(privateId: string, connected: boolean) {
|
|
587
1258
|
const session = await this.getSession(privateId);
|
|
588
1259
|
if (session) {
|
|
589
|
-
|
|
1260
|
+
const nextSession = { ...session, connected };
|
|
1261
|
+
if (connected) {
|
|
1262
|
+
delete nextSession.disconnectedAt;
|
|
1263
|
+
} else {
|
|
1264
|
+
nextSession.disconnectedAt = Date.now();
|
|
1265
|
+
}
|
|
1266
|
+
if (!await this.getSession(privateId)) {
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
await this.saveSession(privateId, nextSession);
|
|
590
1270
|
}
|
|
591
1271
|
}
|
|
592
1272
|
|
|
@@ -602,7 +1282,11 @@ export class Server implements Party.Server {
|
|
|
602
1282
|
* ```
|
|
603
1283
|
*/
|
|
604
1284
|
async deleteSession(privateId: string) {
|
|
605
|
-
await this.
|
|
1285
|
+
const session = await this.getSession(privateId);
|
|
1286
|
+
await this.room.storage.delete(this.sessionKey(privateId));
|
|
1287
|
+
if (session?.publicId) {
|
|
1288
|
+
await this.removeSessionFromPublicIndex(privateId, session.publicId);
|
|
1289
|
+
}
|
|
606
1290
|
}
|
|
607
1291
|
|
|
608
1292
|
async onConnectClient(conn: Party.Connection, ctx: Party.ConnectionContext) {
|
|
@@ -615,8 +1299,17 @@ export class Server implements Party.Server {
|
|
|
615
1299
|
return;
|
|
616
1300
|
}
|
|
617
1301
|
|
|
618
|
-
const sessionExpiryTime = subRoom
|
|
619
|
-
await this.
|
|
1302
|
+
const sessionExpiryTime = this.getSessionExpiryTime(subRoom);
|
|
1303
|
+
if (await this.shouldRunSessionGarbageCollector(sessionExpiryTime)) {
|
|
1304
|
+
await this.garbageCollector({ sessionExpiryTime });
|
|
1305
|
+
}
|
|
1306
|
+
const transferExpiryTime = this.getTransferExpiryTime(subRoom);
|
|
1307
|
+
if (await this.shouldRunInterval(TRANSFER_GC_LAST_RUN_KEY, transferExpiryTime)) {
|
|
1308
|
+
await this.cleanupExpiredTransfers(
|
|
1309
|
+
transferExpiryTime,
|
|
1310
|
+
subRoom.$storageMetrics as StorageMetrics | undefined
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
620
1313
|
|
|
621
1314
|
// Check room guards
|
|
622
1315
|
const roomGuards = subRoom.constructor['_roomGuards'] || [];
|
|
@@ -636,14 +1329,20 @@ export class Server implements Party.Server {
|
|
|
636
1329
|
}
|
|
637
1330
|
let transferData: any = null;
|
|
638
1331
|
if (transferToken) {
|
|
639
|
-
|
|
1332
|
+
const transferKey = this.transferKey(transferToken);
|
|
1333
|
+
transferData = await this.room.storage.get<TransferData>(transferKey);
|
|
640
1334
|
if (transferData) {
|
|
641
|
-
|
|
1335
|
+
if (this.isTransferExpired(transferData, transferExpiryTime)) {
|
|
1336
|
+
transferData = null;
|
|
1337
|
+
}
|
|
1338
|
+
await this.room.storage.delete(transferKey);
|
|
642
1339
|
}
|
|
643
1340
|
}
|
|
644
1341
|
|
|
645
1342
|
// Check for existing session
|
|
646
|
-
const
|
|
1343
|
+
const requestedPrivateId = this.getPrivateId(conn);
|
|
1344
|
+
const privateId = transferData?.privateId || requestedPrivateId;
|
|
1345
|
+
const existingSession = await this.getSession(privateId)
|
|
647
1346
|
|
|
648
1347
|
// Generate IDs
|
|
649
1348
|
const publicId = existingSession?.publicId || transferData?.publicId || generateShortUUID();
|
|
@@ -661,10 +1360,10 @@ export class Server implements Party.Server {
|
|
|
661
1360
|
if (transferData?.restored && signal()[publicId]) {
|
|
662
1361
|
user = signal()[publicId];
|
|
663
1362
|
} else {
|
|
664
|
-
user =
|
|
1363
|
+
user = this.createUserFromClassType(classType, conn, ctx);
|
|
665
1364
|
signal()[publicId] = user;
|
|
666
1365
|
const snapshot = createStatesSnapshotDeep(user);
|
|
667
|
-
this.
|
|
1366
|
+
await this.saveStatePath(`${usersPropName}.${publicId}`, snapshot);
|
|
668
1367
|
}
|
|
669
1368
|
}
|
|
670
1369
|
else {
|
|
@@ -674,13 +1373,12 @@ export class Server implements Party.Server {
|
|
|
674
1373
|
// Only store new session if it doesn't exist
|
|
675
1374
|
if (!existingSession) {
|
|
676
1375
|
// Use the transferred privateId if available, otherwise use connection id
|
|
677
|
-
|
|
678
|
-
await this.saveSession(sessionPrivateId, {
|
|
1376
|
+
await this.saveSession(privateId, {
|
|
679
1377
|
publicId
|
|
680
1378
|
});
|
|
681
1379
|
}
|
|
682
1380
|
else {
|
|
683
|
-
await this.updateSessionConnection(
|
|
1381
|
+
await this.updateSessionConnection(privateId, true);
|
|
684
1382
|
}
|
|
685
1383
|
}
|
|
686
1384
|
// Update user connection status if applicable
|
|
@@ -689,7 +1387,8 @@ export class Server implements Party.Server {
|
|
|
689
1387
|
// Store both IDs in connection state
|
|
690
1388
|
conn.setState({
|
|
691
1389
|
...conn.state,
|
|
692
|
-
publicId
|
|
1390
|
+
publicId,
|
|
1391
|
+
privateId
|
|
693
1392
|
});
|
|
694
1393
|
|
|
695
1394
|
// Call the room's onJoin method if it exists
|
|
@@ -729,6 +1428,10 @@ export class Server implements Party.Server {
|
|
|
729
1428
|
*/
|
|
730
1429
|
async onConnect(conn: Party.Connection, ctx: Party.ConnectionContext) {
|
|
731
1430
|
if (ctx.request?.headers.has('x-shard-id')) {
|
|
1431
|
+
if (!this.isAuthorizedShardRequest(ctx.request)) {
|
|
1432
|
+
conn.close();
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
732
1435
|
this.onConnectShard(conn, ctx);
|
|
733
1436
|
}
|
|
734
1437
|
else {
|
|
@@ -736,6 +1439,13 @@ export class Server implements Party.Server {
|
|
|
736
1439
|
}
|
|
737
1440
|
}
|
|
738
1441
|
|
|
1442
|
+
private isAuthorizedShardRequest(req?: Party.Request) {
|
|
1443
|
+
const shardSecret = this.room.env.SHARD_SECRET;
|
|
1444
|
+
return typeof shardSecret === 'string'
|
|
1445
|
+
&& shardSecret.length > 0
|
|
1446
|
+
&& req?.headers.get('x-access-shard') === shardSecret;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
739
1449
|
/**
|
|
740
1450
|
* @method onConnectShard
|
|
741
1451
|
* @private
|
|
@@ -747,9 +1457,11 @@ export class Server implements Party.Server {
|
|
|
747
1457
|
onConnectShard(conn: Party.Connection, ctx: Party.ConnectionContext) {
|
|
748
1458
|
// Set shard metadata in connection state
|
|
749
1459
|
const shardId = ctx.request?.headers.get('x-shard-id') || 'unknown-shard';
|
|
1460
|
+
const worldId = ctx.request?.headers.get('x-shard-world-id') || 'world-default';
|
|
750
1461
|
conn.setState({
|
|
751
1462
|
shard: true,
|
|
752
1463
|
shardId,
|
|
1464
|
+
worldId,
|
|
753
1465
|
clients: new Map() // Track clients connected through this shard
|
|
754
1466
|
});
|
|
755
1467
|
}
|
|
@@ -1085,14 +1797,19 @@ export class Server implements Party.Server {
|
|
|
1085
1797
|
return;
|
|
1086
1798
|
}
|
|
1087
1799
|
|
|
1088
|
-
const privateId = conn
|
|
1800
|
+
const privateId = this.getPrivateId(conn);
|
|
1089
1801
|
const { publicId } = conn.state as any;
|
|
1090
1802
|
const user = signal?.()[publicId];
|
|
1091
1803
|
|
|
1092
1804
|
if (!user) return;
|
|
1093
1805
|
|
|
1806
|
+
if (this.hasActiveSessionConnection(privateId)) {
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1094
1810
|
// Mark session as disconnected instead of deleting it
|
|
1095
1811
|
await this.updateSessionConnection(privateId, false);
|
|
1812
|
+
this.scheduleSessionGarbageCollector(this.getSessionExpiryTime(subRoom), privateId);
|
|
1096
1813
|
|
|
1097
1814
|
// Update user connection status in the signal
|
|
1098
1815
|
const connectionUpdated = this.updateUserConnectionStatus(user, false);
|
|
@@ -1142,6 +1859,9 @@ export class Server implements Party.Server {
|
|
|
1142
1859
|
}
|
|
1143
1860
|
|
|
1144
1861
|
if (isFromShard) {
|
|
1862
|
+
if (!this.isAuthorizedShardRequest(req)) {
|
|
1863
|
+
return res.unauthorized('Invalid shard credentials');
|
|
1864
|
+
}
|
|
1145
1865
|
return this.handleShardRequest(req, res, shardId);
|
|
1146
1866
|
}
|
|
1147
1867
|
|
|
@@ -1195,7 +1915,7 @@ export class Server implements Party.Server {
|
|
|
1195
1915
|
const { classType } = signal.options;
|
|
1196
1916
|
|
|
1197
1917
|
// Create new user instance
|
|
1198
|
-
const user =
|
|
1918
|
+
const user = this.createUserFromClassType(classType);
|
|
1199
1919
|
|
|
1200
1920
|
const hydratedSnapshot =
|
|
1201
1921
|
(await awaitReturn(
|
|
@@ -1216,16 +1936,17 @@ export class Server implements Party.Server {
|
|
|
1216
1936
|
load(user, hydratedSnapshot, true);
|
|
1217
1937
|
|
|
1218
1938
|
// Save user snapshot to storage
|
|
1219
|
-
await this.
|
|
1939
|
+
await this.saveStatePath(`${usersPropName}.${publicId}`, userSnapshot);
|
|
1220
1940
|
}
|
|
1221
1941
|
}
|
|
1222
1942
|
|
|
1223
1943
|
// Generate transfer token for the client to use when connecting
|
|
1224
1944
|
const transferToken = generateShortUUID();
|
|
1225
|
-
await this.room.storage.put(
|
|
1945
|
+
await this.room.storage.put(this.transferKey(transferToken), {
|
|
1226
1946
|
privateId,
|
|
1227
1947
|
publicId,
|
|
1228
|
-
restored: true
|
|
1948
|
+
restored: true,
|
|
1949
|
+
created: Date.now(),
|
|
1229
1950
|
});
|
|
1230
1951
|
|
|
1231
1952
|
return res.success({ transferToken });
|