@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/dist/index.js
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
5
|
-
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
6
|
-
if (decorator = decorators[i])
|
|
7
|
-
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
8
|
-
if (kind && result) __defProp(target, key, result);
|
|
9
|
-
return result;
|
|
10
|
-
};
|
|
1
|
+
import {
|
|
2
|
+
__decorateClass
|
|
3
|
+
} from "./chunk-EUXUH3YW.js";
|
|
11
4
|
|
|
12
5
|
// src/decorators.ts
|
|
13
6
|
function Action(name, bodyValidation) {
|
|
@@ -90,16 +83,38 @@ var Storage = class {
|
|
|
90
83
|
this.memory = /* @__PURE__ */ new Map();
|
|
91
84
|
}
|
|
92
85
|
async put(key, value) {
|
|
93
|
-
|
|
86
|
+
if (typeof key === "string") {
|
|
87
|
+
this.memory.set(key, value);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
for (const [entryKey, entryValue] of Object.entries(key)) {
|
|
91
|
+
this.memory.set(entryKey, entryValue);
|
|
92
|
+
}
|
|
94
93
|
}
|
|
95
94
|
async get(key) {
|
|
96
95
|
return this.memory.get(key);
|
|
97
96
|
}
|
|
98
97
|
async delete(key) {
|
|
99
|
-
|
|
98
|
+
if (Array.isArray(key)) {
|
|
99
|
+
let deleted = 0;
|
|
100
|
+
for (const entryKey of key) {
|
|
101
|
+
if (this.memory.delete(entryKey)) {
|
|
102
|
+
deleted += 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return deleted;
|
|
106
|
+
}
|
|
107
|
+
return this.memory.delete(key);
|
|
100
108
|
}
|
|
101
|
-
async list() {
|
|
102
|
-
|
|
109
|
+
async list(options) {
|
|
110
|
+
if (!options?.prefix) {
|
|
111
|
+
return this.memory;
|
|
112
|
+
}
|
|
113
|
+
return new Map(
|
|
114
|
+
Array.from(this.memory.entries()).filter(
|
|
115
|
+
([key]) => String(key).startsWith(options.prefix)
|
|
116
|
+
)
|
|
117
|
+
);
|
|
103
118
|
}
|
|
104
119
|
};
|
|
105
120
|
|
|
@@ -438,6 +453,14 @@ var Message = z.object({
|
|
|
438
453
|
action: z.string(),
|
|
439
454
|
value: z.any()
|
|
440
455
|
});
|
|
456
|
+
var STATE_PREFIX = "state:";
|
|
457
|
+
var SESSION_PREFIX = "session:";
|
|
458
|
+
var SESSION_PUBLIC_PREFIX = "session-public:";
|
|
459
|
+
var TRANSFER_PREFIX = "transfer:";
|
|
460
|
+
var INTERNAL_PREFIX = "$room:";
|
|
461
|
+
var SESSION_GC_LAST_RUN_KEY = `${INTERNAL_PREFIX}session-gc:last-run`;
|
|
462
|
+
var TRANSFER_GC_LAST_RUN_KEY = `${INTERNAL_PREFIX}transfer-gc:last-run`;
|
|
463
|
+
var DEFAULT_TRANSFER_EXPIRY_MS = 5 * 60 * 1e3;
|
|
441
464
|
var Server = class {
|
|
442
465
|
/**
|
|
443
466
|
* @constructor
|
|
@@ -470,6 +493,187 @@ var Server = class {
|
|
|
470
493
|
get roomStorage() {
|
|
471
494
|
return this.room.storage;
|
|
472
495
|
}
|
|
496
|
+
stateKey(path) {
|
|
497
|
+
return `${STATE_PREFIX}${path}`;
|
|
498
|
+
}
|
|
499
|
+
sessionKey(privateId) {
|
|
500
|
+
return `${SESSION_PREFIX}${privateId}`;
|
|
501
|
+
}
|
|
502
|
+
sessionPublicKey(publicId) {
|
|
503
|
+
return `${SESSION_PUBLIC_PREFIX}${publicId}`;
|
|
504
|
+
}
|
|
505
|
+
transferKey(token) {
|
|
506
|
+
return `${TRANSFER_PREFIX}${token}`;
|
|
507
|
+
}
|
|
508
|
+
isInternalStorageKey(key) {
|
|
509
|
+
return key.startsWith(STATE_PREFIX) || key.startsWith(SESSION_PREFIX) || key.startsWith(SESSION_PUBLIC_PREFIX) || key.startsWith(TRANSFER_PREFIX) || key.startsWith(INTERNAL_PREFIX);
|
|
510
|
+
}
|
|
511
|
+
async listStorage(prefix) {
|
|
512
|
+
if (!prefix) {
|
|
513
|
+
return this.room.storage.list();
|
|
514
|
+
}
|
|
515
|
+
return this.room.storage.list({ prefix });
|
|
516
|
+
}
|
|
517
|
+
async loadStatePath(path) {
|
|
518
|
+
return this.room.storage.get(this.stateKey(path));
|
|
519
|
+
}
|
|
520
|
+
async saveStatePath(path, value) {
|
|
521
|
+
await this.room.storage.put(this.stateKey(path), value);
|
|
522
|
+
}
|
|
523
|
+
async putStorageEntries(entries) {
|
|
524
|
+
const keys = Object.keys(entries);
|
|
525
|
+
if (keys.length === 0) return;
|
|
526
|
+
if (keys.length === 1) {
|
|
527
|
+
const key = keys[0];
|
|
528
|
+
await this.room.storage.put(key, entries[key]);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
await this.room.storage.put(entries);
|
|
532
|
+
}
|
|
533
|
+
async deleteStorageKeys(keys) {
|
|
534
|
+
const uniqueKeys = Array.from(new Set(keys));
|
|
535
|
+
if (uniqueKeys.length === 0) return;
|
|
536
|
+
if (uniqueKeys.length === 1) {
|
|
537
|
+
await this.room.storage.delete(uniqueKeys[0]);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
await this.room.storage.delete(uniqueKeys);
|
|
541
|
+
}
|
|
542
|
+
async deleteStatePath(path) {
|
|
543
|
+
const stateKey = this.stateKey(path);
|
|
544
|
+
const descendantEntries = await this.listStorage(`${stateKey}.`);
|
|
545
|
+
await this.deleteStorageKeys([
|
|
546
|
+
...Array.from(descendantEntries.keys()),
|
|
547
|
+
stateKey
|
|
548
|
+
]);
|
|
549
|
+
await this.saveStatePath(path, DELETE_TOKEN);
|
|
550
|
+
}
|
|
551
|
+
containsDeleteToken(value) {
|
|
552
|
+
if (value === DELETE_TOKEN) {
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
if (!value || typeof value !== "object") {
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
if (Array.isArray(value)) {
|
|
559
|
+
return value.some((item) => this.containsDeleteToken(item));
|
|
560
|
+
}
|
|
561
|
+
return Object.values(value).some((item) => this.containsDeleteToken(item));
|
|
562
|
+
}
|
|
563
|
+
async compactStateStorage(instance) {
|
|
564
|
+
const entries = await this.listStorage(STATE_PREFIX);
|
|
565
|
+
const keys = Array.from(entries.keys());
|
|
566
|
+
if (keys.length === 0) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const snapshot = createStatesSnapshotDeep(instance);
|
|
570
|
+
await this.deleteStorageKeys(keys);
|
|
571
|
+
await this.saveStatePath(".", snapshot);
|
|
572
|
+
const metrics = instance.$storageMetrics;
|
|
573
|
+
if (metrics) {
|
|
574
|
+
metrics.stateCompactions += 1;
|
|
575
|
+
metrics.stateCompactionDeletes += keys.length;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
createUserFromClassType(classType, ...args) {
|
|
579
|
+
if (!classType) {
|
|
580
|
+
return void 0;
|
|
581
|
+
}
|
|
582
|
+
return isClass(classType) ? new classType() : classType(...args);
|
|
583
|
+
}
|
|
584
|
+
async restoreUsersStorageSnapshot(instance, snapshot, options) {
|
|
585
|
+
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) {
|
|
586
|
+
return snapshot;
|
|
587
|
+
}
|
|
588
|
+
const restoreUser = instance["onUserStorageRestore"];
|
|
589
|
+
if (typeof restoreUser !== "function") {
|
|
590
|
+
return snapshot;
|
|
591
|
+
}
|
|
592
|
+
const signal2 = this.getUsersProperty(instance);
|
|
593
|
+
const usersPropName = this.getUsersPropName(instance);
|
|
594
|
+
if (!signal2 || !usersPropName) {
|
|
595
|
+
return snapshot;
|
|
596
|
+
}
|
|
597
|
+
const usersSnapshot = snapshot[usersPropName];
|
|
598
|
+
if (!usersSnapshot || typeof usersSnapshot !== "object" || Array.isArray(usersSnapshot)) {
|
|
599
|
+
return snapshot;
|
|
600
|
+
}
|
|
601
|
+
const { classType } = signal2.options || {};
|
|
602
|
+
let nextSnapshot = snapshot;
|
|
603
|
+
let nextUsersSnapshot = usersSnapshot;
|
|
604
|
+
for (const [publicId, userSnapshot] of Object.entries(usersSnapshot)) {
|
|
605
|
+
const user = this.createUserFromClassType(classType, publicId);
|
|
606
|
+
const restoredUserSnapshot = await awaitReturn(
|
|
607
|
+
restoreUser.call(instance, {
|
|
608
|
+
userSnapshot,
|
|
609
|
+
user,
|
|
610
|
+
publicId,
|
|
611
|
+
usersPropName,
|
|
612
|
+
room: this.room,
|
|
613
|
+
server: this,
|
|
614
|
+
legacy: options.legacy
|
|
615
|
+
})
|
|
616
|
+
);
|
|
617
|
+
if (restoredUserSnapshot !== void 0 && restoredUserSnapshot !== userSnapshot) {
|
|
618
|
+
if (nextSnapshot === snapshot) {
|
|
619
|
+
nextSnapshot = { ...snapshot };
|
|
620
|
+
}
|
|
621
|
+
if (nextUsersSnapshot === usersSnapshot) {
|
|
622
|
+
nextUsersSnapshot = { ...usersSnapshot };
|
|
623
|
+
nextSnapshot[usersPropName] = nextUsersSnapshot;
|
|
624
|
+
}
|
|
625
|
+
nextUsersSnapshot[publicId] = restoredUserSnapshot;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return nextSnapshot;
|
|
629
|
+
}
|
|
630
|
+
async restoreStorageSnapshot(instance, snapshot, options) {
|
|
631
|
+
const restoreSnapshot = instance["onStorageRestore"];
|
|
632
|
+
let restoredSnapshot = snapshot;
|
|
633
|
+
if (typeof restoreSnapshot === "function") {
|
|
634
|
+
const result = await awaitReturn(
|
|
635
|
+
restoreSnapshot.call(instance, {
|
|
636
|
+
snapshot,
|
|
637
|
+
room: this.room,
|
|
638
|
+
server: this,
|
|
639
|
+
legacy: options.legacy
|
|
640
|
+
})
|
|
641
|
+
);
|
|
642
|
+
if (result !== void 0) {
|
|
643
|
+
restoredSnapshot = result;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return this.restoreUsersStorageSnapshot(instance, restoredSnapshot, options);
|
|
647
|
+
}
|
|
648
|
+
createStorageMetrics() {
|
|
649
|
+
return {
|
|
650
|
+
loadMs: 0,
|
|
651
|
+
loadStateKeys: 0,
|
|
652
|
+
loadLegacyKeys: 0,
|
|
653
|
+
stateCompactions: 0,
|
|
654
|
+
stateCompactionDeletes: 0,
|
|
655
|
+
persistFlushes: 0,
|
|
656
|
+
persistWrites: 0,
|
|
657
|
+
persistDeletes: 0,
|
|
658
|
+
persistLastFlushMs: 0,
|
|
659
|
+
sessionGcRuns: 0,
|
|
660
|
+
sessionGcScanned: 0,
|
|
661
|
+
sessionGcExpired: 0,
|
|
662
|
+
sessionIndexRepairs: 0,
|
|
663
|
+
transferGcRuns: 0,
|
|
664
|
+
transferGcScanned: 0,
|
|
665
|
+
transferGcExpired: 0
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
getTransferExpiryTime(subRoom) {
|
|
669
|
+
return subRoom?.transferExpiryTime ?? subRoom?.constructor?.prototype?.transferExpiryTime ?? subRoom?.constructor?.transferExpiryTime ?? DEFAULT_TRANSFER_EXPIRY_MS;
|
|
670
|
+
}
|
|
671
|
+
getPrivateId(conn) {
|
|
672
|
+
return conn.state?.privateId || conn.sessionId || conn.id;
|
|
673
|
+
}
|
|
674
|
+
hasActiveSessionConnection(privateId) {
|
|
675
|
+
return Array.from(this.room.getConnections()).some((conn) => this.getPrivateId(conn) === privateId);
|
|
676
|
+
}
|
|
473
677
|
async send(conn, obj, subRoom) {
|
|
474
678
|
obj = structuredClone(obj);
|
|
475
679
|
if (subRoom.interceptorPacket) {
|
|
@@ -511,33 +715,44 @@ var Server = class {
|
|
|
511
715
|
async garbageCollector(options) {
|
|
512
716
|
const subRoom = await this.getSubRoom();
|
|
513
717
|
if (!subRoom) return;
|
|
718
|
+
const SESSION_EXPIRY_TIME = Number(options.sessionExpiryTime);
|
|
719
|
+
if (!Number.isFinite(SESSION_EXPIRY_TIME)) {
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
514
722
|
const activeConnections = [...this.room.getConnections()];
|
|
515
|
-
const activePrivateIds = new Set(activeConnections.map((conn) => conn
|
|
723
|
+
const activePrivateIds = new Set(activeConnections.map((conn) => this.getPrivateId(conn)));
|
|
516
724
|
try {
|
|
517
|
-
const sessions = await this.
|
|
725
|
+
const sessions = await this.listStorage(SESSION_PREFIX);
|
|
518
726
|
const users = this.getUsersProperty(subRoom);
|
|
519
727
|
const usersPropName = this.getUsersPropName(subRoom);
|
|
728
|
+
const metrics = subRoom.$storageMetrics;
|
|
729
|
+
if (metrics) {
|
|
730
|
+
metrics.sessionGcRuns += 1;
|
|
731
|
+
metrics.sessionGcScanned += sessions.size;
|
|
732
|
+
}
|
|
520
733
|
const validPublicIds = /* @__PURE__ */ new Set();
|
|
521
734
|
const expiredPublicIds = /* @__PURE__ */ new Set();
|
|
522
|
-
const SESSION_EXPIRY_TIME = options.sessionExpiryTime;
|
|
523
735
|
const now = Date.now();
|
|
524
736
|
for (const [key, session] of sessions) {
|
|
525
|
-
if (!key.startsWith(
|
|
526
|
-
const privateId = key.
|
|
737
|
+
if (!key.startsWith(SESSION_PREFIX)) continue;
|
|
738
|
+
const privateId = key.slice(SESSION_PREFIX.length);
|
|
527
739
|
const typedSession = session;
|
|
528
|
-
if (!activePrivateIds.has(privateId) && !typedSession.connected && now - typedSession.
|
|
740
|
+
if (!activePrivateIds.has(privateId) && !typedSession.connected && typedSession.disconnectedAt !== void 0 && now - typedSession.disconnectedAt >= SESSION_EXPIRY_TIME) {
|
|
529
741
|
await this.deleteSession(privateId);
|
|
530
742
|
expiredPublicIds.add(typedSession.publicId);
|
|
743
|
+
if (metrics) {
|
|
744
|
+
metrics.sessionGcExpired += 1;
|
|
745
|
+
}
|
|
531
746
|
} else if (typedSession && typedSession.publicId) {
|
|
532
747
|
validPublicIds.add(typedSession.publicId);
|
|
533
748
|
}
|
|
534
749
|
}
|
|
750
|
+
await this.repairSessionPublicIndexes(sessions, metrics);
|
|
535
751
|
if (users && usersPropName) {
|
|
536
752
|
const currentUsers = users();
|
|
537
753
|
for (const publicId in currentUsers) {
|
|
538
754
|
if (expiredPublicIds.has(publicId) && !validPublicIds.has(publicId)) {
|
|
539
755
|
delete currentUsers[publicId];
|
|
540
|
-
await this.room.storage.delete(`${usersPropName}.${publicId}`);
|
|
541
756
|
}
|
|
542
757
|
}
|
|
543
758
|
}
|
|
@@ -545,6 +760,145 @@ var Server = class {
|
|
|
545
760
|
console.error("Error in garbage collector:", error);
|
|
546
761
|
}
|
|
547
762
|
}
|
|
763
|
+
scheduleSessionGarbageCollector(sessionExpiryTime, privateId) {
|
|
764
|
+
const normalizedSessionExpiryTime = Number(sessionExpiryTime);
|
|
765
|
+
if (!Number.isFinite(normalizedSessionExpiryTime) || normalizedSessionExpiryTime < 0) {
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
setTimeout(() => {
|
|
769
|
+
if (privateId) {
|
|
770
|
+
void this.expireDisconnectedSession(privateId, normalizedSessionExpiryTime);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
void this.garbageCollector({ sessionExpiryTime: normalizedSessionExpiryTime });
|
|
774
|
+
}, normalizedSessionExpiryTime);
|
|
775
|
+
}
|
|
776
|
+
getSessionExpiryTime(subRoom) {
|
|
777
|
+
return subRoom?.sessionExpiryTime ?? subRoom?.constructor?.prototype?.sessionExpiryTime ?? subRoom?.constructor?.sessionExpiryTime;
|
|
778
|
+
}
|
|
779
|
+
async shouldRunSessionGarbageCollector(sessionExpiryTime) {
|
|
780
|
+
const normalizedSessionExpiryTime = Number(sessionExpiryTime);
|
|
781
|
+
if (!Number.isFinite(normalizedSessionExpiryTime) || normalizedSessionExpiryTime < 0) {
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
const now = Date.now();
|
|
785
|
+
const lastRun = await this.room.storage.get(SESSION_GC_LAST_RUN_KEY);
|
|
786
|
+
if (lastRun && now - lastRun < normalizedSessionExpiryTime) {
|
|
787
|
+
return false;
|
|
788
|
+
}
|
|
789
|
+
await this.room.storage.put(SESSION_GC_LAST_RUN_KEY, now);
|
|
790
|
+
return true;
|
|
791
|
+
}
|
|
792
|
+
async shouldRunInterval(key, interval) {
|
|
793
|
+
const normalizedInterval = Number(interval);
|
|
794
|
+
if (!Number.isFinite(normalizedInterval) || normalizedInterval < 0) {
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
const now = Date.now();
|
|
798
|
+
const lastRun = await this.room.storage.get(key);
|
|
799
|
+
if (lastRun && now - lastRun < normalizedInterval) {
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
await this.room.storage.put(key, now);
|
|
803
|
+
return true;
|
|
804
|
+
}
|
|
805
|
+
async repairSessionPublicIndexes(sessions, metrics) {
|
|
806
|
+
const sessionEntries = sessions ?? await this.listStorage(SESSION_PREFIX);
|
|
807
|
+
const expected = /* @__PURE__ */ new Map();
|
|
808
|
+
for (const [key, session] of sessionEntries) {
|
|
809
|
+
if (!session?.publicId) continue;
|
|
810
|
+
const privateId = key.slice(SESSION_PREFIX.length);
|
|
811
|
+
const privateIds = expected.get(session.publicId) ?? [];
|
|
812
|
+
privateIds.push(privateId);
|
|
813
|
+
expected.set(session.publicId, privateIds);
|
|
814
|
+
}
|
|
815
|
+
const publicIndexes = await this.listStorage(SESSION_PUBLIC_PREFIX);
|
|
816
|
+
const writes = {};
|
|
817
|
+
const deletes = [];
|
|
818
|
+
let repairs = 0;
|
|
819
|
+
for (const [publicId, privateIds] of expected) {
|
|
820
|
+
privateIds.sort();
|
|
821
|
+
const key = this.sessionPublicKey(publicId);
|
|
822
|
+
const existing = publicIndexes.get(key) ?? [];
|
|
823
|
+
const normalizedExisting = Array.isArray(existing) ? [...existing].sort() : [];
|
|
824
|
+
if (JSON.stringify(normalizedExisting) !== JSON.stringify(privateIds)) {
|
|
825
|
+
writes[key] = privateIds;
|
|
826
|
+
repairs += 1;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
for (const [key] of publicIndexes) {
|
|
830
|
+
const publicId = key.slice(SESSION_PUBLIC_PREFIX.length);
|
|
831
|
+
if (!expected.has(publicId)) {
|
|
832
|
+
deletes.push(key);
|
|
833
|
+
repairs += 1;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
await Promise.all([
|
|
837
|
+
this.putStorageEntries(writes),
|
|
838
|
+
this.deleteStorageKeys(deletes)
|
|
839
|
+
]);
|
|
840
|
+
if (metrics) {
|
|
841
|
+
metrics.sessionIndexRepairs += repairs;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
isTransferExpired(transfer, transferExpiryTime) {
|
|
845
|
+
if (!transfer) return true;
|
|
846
|
+
if (!Number.isFinite(transferExpiryTime) || transferExpiryTime < 0) {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
const created = Number(transfer.created);
|
|
850
|
+
if (!Number.isFinite(created)) {
|
|
851
|
+
return false;
|
|
852
|
+
}
|
|
853
|
+
return Date.now() - created >= transferExpiryTime;
|
|
854
|
+
}
|
|
855
|
+
async cleanupExpiredTransfers(transferExpiryTime, metrics) {
|
|
856
|
+
if (!Number.isFinite(transferExpiryTime) || transferExpiryTime < 0) {
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const transfers = await this.listStorage(TRANSFER_PREFIX);
|
|
860
|
+
const expiredKeys = [];
|
|
861
|
+
for (const [key, transfer] of transfers) {
|
|
862
|
+
if (this.isTransferExpired(transfer, transferExpiryTime)) {
|
|
863
|
+
expiredKeys.push(key);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
await this.deleteStorageKeys(expiredKeys);
|
|
867
|
+
if (metrics) {
|
|
868
|
+
metrics.transferGcRuns += 1;
|
|
869
|
+
metrics.transferGcScanned += transfers.size;
|
|
870
|
+
metrics.transferGcExpired += expiredKeys.length;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
async expireDisconnectedSession(privateId, sessionExpiryTime) {
|
|
874
|
+
const session = await this.getSession(privateId);
|
|
875
|
+
if (!session || session.connected || session.disconnectedAt === void 0) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
if (this.hasActiveSessionConnection(privateId)) {
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
const elapsed = Date.now() - session.disconnectedAt;
|
|
882
|
+
if (elapsed < sessionExpiryTime) {
|
|
883
|
+
setTimeout(() => {
|
|
884
|
+
void this.expireDisconnectedSession(privateId, sessionExpiryTime);
|
|
885
|
+
}, sessionExpiryTime - elapsed);
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
await this.deleteSession(privateId);
|
|
889
|
+
const privateIds = await this.getSessionPrivateIds(session.publicId);
|
|
890
|
+
for (const otherPrivateId of privateIds) {
|
|
891
|
+
const otherSession = await this.getSession(otherPrivateId);
|
|
892
|
+
if (otherSession?.publicId === session.publicId) {
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
const subRoom = await this.getSubRoom();
|
|
897
|
+
const users = this.getUsersProperty(subRoom);
|
|
898
|
+
if (users?.()[session.publicId]) {
|
|
899
|
+
delete users()[session.publicId];
|
|
900
|
+
}
|
|
901
|
+
}
|
|
548
902
|
/**
|
|
549
903
|
* @method createRoom
|
|
550
904
|
* @private
|
|
@@ -576,21 +930,67 @@ var Server = class {
|
|
|
576
930
|
return null;
|
|
577
931
|
}
|
|
578
932
|
const loadMemory = async () => {
|
|
579
|
-
const
|
|
580
|
-
const
|
|
933
|
+
const startedAt = Date.now();
|
|
934
|
+
const metrics = instance.$storageMetrics;
|
|
935
|
+
const root = await this.loadStatePath(".");
|
|
936
|
+
const memory = await this.listStorage(STATE_PREFIX);
|
|
937
|
+
metrics.loadStateKeys = memory.size;
|
|
581
938
|
const tmpObject = root || {};
|
|
582
|
-
for (let [
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
}
|
|
586
|
-
if (key == ".") {
|
|
939
|
+
for (let [storageKey, value] of memory) {
|
|
940
|
+
const key = storageKey.slice(STATE_PREFIX.length);
|
|
941
|
+
if (key === ".") {
|
|
587
942
|
continue;
|
|
588
943
|
}
|
|
589
944
|
dset2(tmpObject, key, value);
|
|
590
945
|
}
|
|
591
|
-
|
|
946
|
+
if (root === void 0 && memory.size === 0) {
|
|
947
|
+
const legacyRoot = await this.room.storage.get(".");
|
|
948
|
+
const legacyMemory = await this.room.storage.list();
|
|
949
|
+
const legacyObject = legacyRoot || {};
|
|
950
|
+
const migratedEntries = [];
|
|
951
|
+
const legacyDeleteKeys = [];
|
|
952
|
+
if (legacyRoot !== void 0) {
|
|
953
|
+
migratedEntries.push([".", legacyRoot]);
|
|
954
|
+
legacyDeleteKeys.push(".");
|
|
955
|
+
}
|
|
956
|
+
for (let [key, value] of legacyMemory) {
|
|
957
|
+
if (key === "." || this.isInternalStorageKey(key)) {
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
dset2(legacyObject, key, value);
|
|
961
|
+
migratedEntries.push([key, value]);
|
|
962
|
+
legacyDeleteKeys.push(key);
|
|
963
|
+
}
|
|
964
|
+
metrics.loadLegacyKeys = migratedEntries.length;
|
|
965
|
+
await this.putStorageEntries(
|
|
966
|
+
Object.fromEntries(
|
|
967
|
+
migratedEntries.map(([path, value]) => [this.stateKey(path), value])
|
|
968
|
+
)
|
|
969
|
+
);
|
|
970
|
+
if (legacyDeleteKeys.length > 0) {
|
|
971
|
+
await this.deleteStorageKeys(legacyDeleteKeys);
|
|
972
|
+
}
|
|
973
|
+
const restoredLegacyObject = await this.restoreStorageSnapshot(instance, legacyObject, {
|
|
974
|
+
legacy: true
|
|
975
|
+
});
|
|
976
|
+
load(instance, restoredLegacyObject, true);
|
|
977
|
+
if (this.containsDeleteToken(restoredLegacyObject)) {
|
|
978
|
+
await this.compactStateStorage(instance);
|
|
979
|
+
}
|
|
980
|
+
metrics.loadMs = Date.now() - startedAt;
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const restoredObject = await this.restoreStorageSnapshot(instance, tmpObject, {
|
|
984
|
+
legacy: false
|
|
985
|
+
});
|
|
986
|
+
load(instance, restoredObject, true);
|
|
987
|
+
if (this.containsDeleteToken(restoredObject)) {
|
|
988
|
+
await this.compactStateStorage(instance);
|
|
989
|
+
}
|
|
990
|
+
metrics.loadMs = Date.now() - startedAt;
|
|
592
991
|
};
|
|
593
992
|
instance.$memoryAll = {};
|
|
993
|
+
instance.$storageMetrics = this.createStorageMetrics();
|
|
594
994
|
instance.$autoSync = instance["autoSync"] !== false;
|
|
595
995
|
instance.$pendingSync = /* @__PURE__ */ new Map();
|
|
596
996
|
instance.$pendingInitialSync = /* @__PURE__ */ new Map();
|
|
@@ -644,16 +1044,9 @@ var Server = class {
|
|
|
644
1044
|
console.error(`[sessionTransfer] User with publicId ${publicId} not found.`);
|
|
645
1045
|
return null;
|
|
646
1046
|
}
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
for (const [key, session] of sessions) {
|
|
651
|
-
if (key.startsWith("session:") && session.publicId === publicId) {
|
|
652
|
-
userSession = session;
|
|
653
|
-
privateId = key.replace("session:", "");
|
|
654
|
-
break;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
1047
|
+
const sessionEntry = await this.getSessionEntryByPublicId(publicId);
|
|
1048
|
+
const userSession = sessionEntry?.session;
|
|
1049
|
+
const privateId = sessionEntry?.privateId ?? null;
|
|
657
1050
|
if (!userSession || !privateId) {
|
|
658
1051
|
console.error(`[sessionTransfer] Session for publicId ${publicId} not found.`);
|
|
659
1052
|
return null;
|
|
@@ -714,25 +1107,98 @@ var Server = class {
|
|
|
714
1107
|
);
|
|
715
1108
|
values.clear();
|
|
716
1109
|
};
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
1110
|
+
const flushPersist = async (values) => {
|
|
1111
|
+
const startedAt = Date.now();
|
|
1112
|
+
const stateWrites = {};
|
|
1113
|
+
const deleteTasks = [];
|
|
1114
|
+
let hasDeletes = false;
|
|
722
1115
|
for (let [path, value] of values) {
|
|
723
1116
|
const _instance = path == "." ? instance : getByPath(instance, path);
|
|
724
|
-
const itemValue = createStatesSnapshot(_instance);
|
|
725
1117
|
if (value == DELETE_TOKEN) {
|
|
726
|
-
|
|
1118
|
+
hasDeletes = true;
|
|
1119
|
+
deleteTasks.push(this.deleteStatePath(path));
|
|
727
1120
|
} else {
|
|
728
|
-
|
|
1121
|
+
const itemValue = _instance?.$snapshot ? createStatesSnapshot(_instance) : value;
|
|
1122
|
+
stateWrites[this.stateKey(path)] = itemValue;
|
|
729
1123
|
}
|
|
730
1124
|
}
|
|
1125
|
+
await Promise.all([
|
|
1126
|
+
this.putStorageEntries(stateWrites),
|
|
1127
|
+
...deleteTasks
|
|
1128
|
+
]);
|
|
1129
|
+
if (hasDeletes) {
|
|
1130
|
+
await this.compactStateStorage(instance);
|
|
1131
|
+
}
|
|
1132
|
+
const metrics = instance.$storageMetrics;
|
|
1133
|
+
if (metrics) {
|
|
1134
|
+
metrics.persistFlushes += 1;
|
|
1135
|
+
metrics.persistWrites += Object.keys(stateWrites).length;
|
|
1136
|
+
metrics.persistDeletes += deleteTasks.length;
|
|
1137
|
+
metrics.persistLastFlushMs = Date.now() - startedAt;
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
let persistQueue = Promise.resolve();
|
|
1141
|
+
const persistCb = async (values) => {
|
|
1142
|
+
if (initPersist) {
|
|
1143
|
+
values.clear();
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
const valuesSnapshot = new Map(values);
|
|
731
1147
|
values.clear();
|
|
1148
|
+
persistQueue = persistQueue.then(
|
|
1149
|
+
() => flushPersist(valuesSnapshot),
|
|
1150
|
+
() => flushPersist(valuesSnapshot)
|
|
1151
|
+
);
|
|
1152
|
+
await persistQueue;
|
|
1153
|
+
};
|
|
1154
|
+
const debouncePersist = (wait) => {
|
|
1155
|
+
let timeout = null;
|
|
1156
|
+
let flushing = false;
|
|
1157
|
+
const pending = /* @__PURE__ */ new Map();
|
|
1158
|
+
const schedule = () => {
|
|
1159
|
+
if (timeout) {
|
|
1160
|
+
clearTimeout(timeout);
|
|
1161
|
+
}
|
|
1162
|
+
timeout = setTimeout(() => {
|
|
1163
|
+
void flush();
|
|
1164
|
+
}, wait);
|
|
1165
|
+
};
|
|
1166
|
+
const flush = async () => {
|
|
1167
|
+
timeout = null;
|
|
1168
|
+
if (flushing) {
|
|
1169
|
+
schedule();
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
const values = new Map(pending);
|
|
1173
|
+
pending.clear();
|
|
1174
|
+
if (!values.size) {
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
flushing = true;
|
|
1178
|
+
try {
|
|
1179
|
+
await persistCb(values);
|
|
1180
|
+
} finally {
|
|
1181
|
+
flushing = false;
|
|
1182
|
+
if (pending.size) {
|
|
1183
|
+
schedule();
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
return (values) => {
|
|
1188
|
+
if (initPersist) {
|
|
1189
|
+
values.clear();
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
for (const [path, value] of values) {
|
|
1193
|
+
pending.set(path, value);
|
|
1194
|
+
}
|
|
1195
|
+
values.clear();
|
|
1196
|
+
schedule();
|
|
1197
|
+
};
|
|
732
1198
|
};
|
|
733
1199
|
syncClass(instance, {
|
|
734
1200
|
onSync: instance["throttleSync"] ? throttle(syncCb, instance["throttleSync"]) : syncCb,
|
|
735
|
-
onPersist: instance["throttleStorage"] ?
|
|
1201
|
+
onPersist: instance["throttleStorage"] ? debouncePersist(instance["throttleStorage"]) : persistCb
|
|
736
1202
|
});
|
|
737
1203
|
await loadMemory();
|
|
738
1204
|
initPersist = false;
|
|
@@ -839,24 +1305,93 @@ var Server = class {
|
|
|
839
1305
|
async getSession(privateId) {
|
|
840
1306
|
if (!privateId) return null;
|
|
841
1307
|
try {
|
|
842
|
-
const session = await this.room.storage.get(
|
|
1308
|
+
const session = await this.room.storage.get(this.sessionKey(privateId));
|
|
843
1309
|
return session;
|
|
844
1310
|
} catch (e) {
|
|
845
1311
|
return null;
|
|
846
1312
|
}
|
|
847
1313
|
}
|
|
1314
|
+
async getSessionPrivateIds(publicId) {
|
|
1315
|
+
if (!publicId) return [];
|
|
1316
|
+
const privateIds = await this.room.storage.get(this.sessionPublicKey(publicId));
|
|
1317
|
+
return Array.isArray(privateIds) ? privateIds : [];
|
|
1318
|
+
}
|
|
1319
|
+
async saveSessionPrivateIds(publicId, privateIds) {
|
|
1320
|
+
const key = this.sessionPublicKey(publicId);
|
|
1321
|
+
if (privateIds.length === 0) {
|
|
1322
|
+
await this.room.storage.delete(key);
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
await this.room.storage.put(key, privateIds);
|
|
1326
|
+
}
|
|
1327
|
+
async addSessionToPublicIndex(privateId, publicId) {
|
|
1328
|
+
const privateIds = await this.getSessionPrivateIds(publicId);
|
|
1329
|
+
if (privateIds.includes(privateId)) {
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
await this.saveSessionPrivateIds(publicId, [...privateIds, privateId]);
|
|
1333
|
+
}
|
|
1334
|
+
async removeSessionFromPublicIndex(privateId, publicId) {
|
|
1335
|
+
const privateIds = await this.getSessionPrivateIds(publicId);
|
|
1336
|
+
await this.saveSessionPrivateIds(
|
|
1337
|
+
publicId,
|
|
1338
|
+
privateIds.filter((id2) => id2 !== privateId)
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
async getSessionEntryByPublicId(publicId) {
|
|
1342
|
+
const indexedPrivateIds = await this.getSessionPrivateIds(publicId);
|
|
1343
|
+
const stalePrivateIds = [];
|
|
1344
|
+
for (const privateId of indexedPrivateIds) {
|
|
1345
|
+
const session = await this.getSession(privateId);
|
|
1346
|
+
if (session?.publicId === publicId) {
|
|
1347
|
+
return { privateId, session };
|
|
1348
|
+
}
|
|
1349
|
+
stalePrivateIds.push(privateId);
|
|
1350
|
+
}
|
|
1351
|
+
if (stalePrivateIds.length) {
|
|
1352
|
+
await this.saveSessionPrivateIds(
|
|
1353
|
+
publicId,
|
|
1354
|
+
indexedPrivateIds.filter((id2) => !stalePrivateIds.includes(id2))
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
const sessions = await this.listStorage(SESSION_PREFIX);
|
|
1358
|
+
for (const [key, session] of sessions) {
|
|
1359
|
+
const privateId = key.slice(SESSION_PREFIX.length);
|
|
1360
|
+
if (session?.publicId) {
|
|
1361
|
+
await this.addSessionToPublicIndex(privateId, session.publicId);
|
|
1362
|
+
}
|
|
1363
|
+
if (session?.publicId === publicId) {
|
|
1364
|
+
return { privateId, session };
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
return null;
|
|
1368
|
+
}
|
|
848
1369
|
async saveSession(privateId, data) {
|
|
1370
|
+
const existingSession = await this.getSession(privateId);
|
|
849
1371
|
const sessionData = {
|
|
850
1372
|
...data,
|
|
851
1373
|
created: data.created || Date.now(),
|
|
852
1374
|
connected: data.connected !== void 0 ? data.connected : true
|
|
853
1375
|
};
|
|
854
|
-
await this.room.storage.put(
|
|
1376
|
+
await this.room.storage.put(this.sessionKey(privateId), sessionData);
|
|
1377
|
+
if (existingSession?.publicId && existingSession.publicId !== sessionData.publicId) {
|
|
1378
|
+
await this.removeSessionFromPublicIndex(privateId, existingSession.publicId);
|
|
1379
|
+
}
|
|
1380
|
+
await this.addSessionToPublicIndex(privateId, sessionData.publicId);
|
|
855
1381
|
}
|
|
856
1382
|
async updateSessionConnection(privateId, connected) {
|
|
857
1383
|
const session = await this.getSession(privateId);
|
|
858
1384
|
if (session) {
|
|
859
|
-
|
|
1385
|
+
const nextSession = { ...session, connected };
|
|
1386
|
+
if (connected) {
|
|
1387
|
+
delete nextSession.disconnectedAt;
|
|
1388
|
+
} else {
|
|
1389
|
+
nextSession.disconnectedAt = Date.now();
|
|
1390
|
+
}
|
|
1391
|
+
if (!await this.getSession(privateId)) {
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
await this.saveSession(privateId, nextSession);
|
|
860
1395
|
}
|
|
861
1396
|
}
|
|
862
1397
|
/**
|
|
@@ -871,7 +1406,11 @@ var Server = class {
|
|
|
871
1406
|
* ```
|
|
872
1407
|
*/
|
|
873
1408
|
async deleteSession(privateId) {
|
|
874
|
-
await this.
|
|
1409
|
+
const session = await this.getSession(privateId);
|
|
1410
|
+
await this.room.storage.delete(this.sessionKey(privateId));
|
|
1411
|
+
if (session?.publicId) {
|
|
1412
|
+
await this.removeSessionFromPublicIndex(privateId, session.publicId);
|
|
1413
|
+
}
|
|
875
1414
|
}
|
|
876
1415
|
async onConnectClient(conn, ctx) {
|
|
877
1416
|
const subRoom = await this.getSubRoom({
|
|
@@ -881,8 +1420,17 @@ var Server = class {
|
|
|
881
1420
|
conn.close();
|
|
882
1421
|
return;
|
|
883
1422
|
}
|
|
884
|
-
const sessionExpiryTime = subRoom
|
|
885
|
-
await this.
|
|
1423
|
+
const sessionExpiryTime = this.getSessionExpiryTime(subRoom);
|
|
1424
|
+
if (await this.shouldRunSessionGarbageCollector(sessionExpiryTime)) {
|
|
1425
|
+
await this.garbageCollector({ sessionExpiryTime });
|
|
1426
|
+
}
|
|
1427
|
+
const transferExpiryTime = this.getTransferExpiryTime(subRoom);
|
|
1428
|
+
if (await this.shouldRunInterval(TRANSFER_GC_LAST_RUN_KEY, transferExpiryTime)) {
|
|
1429
|
+
await this.cleanupExpiredTransfers(
|
|
1430
|
+
transferExpiryTime,
|
|
1431
|
+
subRoom.$storageMetrics
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
886
1434
|
const roomGuards = subRoom.constructor["_roomGuards"] || [];
|
|
887
1435
|
for (const guard of roomGuards) {
|
|
888
1436
|
const isAuthorized = await guard(conn, ctx, this.room);
|
|
@@ -898,12 +1446,18 @@ var Server = class {
|
|
|
898
1446
|
}
|
|
899
1447
|
let transferData = null;
|
|
900
1448
|
if (transferToken) {
|
|
901
|
-
|
|
1449
|
+
const transferKey = this.transferKey(transferToken);
|
|
1450
|
+
transferData = await this.room.storage.get(transferKey);
|
|
902
1451
|
if (transferData) {
|
|
903
|
-
|
|
1452
|
+
if (this.isTransferExpired(transferData, transferExpiryTime)) {
|
|
1453
|
+
transferData = null;
|
|
1454
|
+
}
|
|
1455
|
+
await this.room.storage.delete(transferKey);
|
|
904
1456
|
}
|
|
905
1457
|
}
|
|
906
|
-
const
|
|
1458
|
+
const requestedPrivateId = this.getPrivateId(conn);
|
|
1459
|
+
const privateId = transferData?.privateId || requestedPrivateId;
|
|
1460
|
+
const existingSession = await this.getSession(privateId);
|
|
907
1461
|
const publicId = existingSession?.publicId || transferData?.publicId || generateShortUUID2();
|
|
908
1462
|
let user = null;
|
|
909
1463
|
const signal2 = this.getUsersProperty(subRoom);
|
|
@@ -914,27 +1468,27 @@ var Server = class {
|
|
|
914
1468
|
if (transferData?.restored && signal2()[publicId]) {
|
|
915
1469
|
user = signal2()[publicId];
|
|
916
1470
|
} else {
|
|
917
|
-
user =
|
|
1471
|
+
user = this.createUserFromClassType(classType, conn, ctx);
|
|
918
1472
|
signal2()[publicId] = user;
|
|
919
1473
|
const snapshot = createStatesSnapshotDeep(user);
|
|
920
|
-
this.
|
|
1474
|
+
await this.saveStatePath(`${usersPropName}.${publicId}`, snapshot);
|
|
921
1475
|
}
|
|
922
1476
|
} else {
|
|
923
1477
|
user = signal2()[existingSession.publicId];
|
|
924
1478
|
}
|
|
925
1479
|
if (!existingSession) {
|
|
926
|
-
|
|
927
|
-
await this.saveSession(sessionPrivateId, {
|
|
1480
|
+
await this.saveSession(privateId, {
|
|
928
1481
|
publicId
|
|
929
1482
|
});
|
|
930
1483
|
} else {
|
|
931
|
-
await this.updateSessionConnection(
|
|
1484
|
+
await this.updateSessionConnection(privateId, true);
|
|
932
1485
|
}
|
|
933
1486
|
}
|
|
934
1487
|
this.updateUserConnectionStatus(user, true);
|
|
935
1488
|
conn.setState({
|
|
936
1489
|
...conn.state,
|
|
937
|
-
publicId
|
|
1490
|
+
publicId,
|
|
1491
|
+
privateId
|
|
938
1492
|
});
|
|
939
1493
|
await awaitReturn(subRoom["onJoin"]?.(user, conn, ctx));
|
|
940
1494
|
if (subRoom.$autoSync) {
|
|
@@ -967,11 +1521,19 @@ var Server = class {
|
|
|
967
1521
|
*/
|
|
968
1522
|
async onConnect(conn, ctx) {
|
|
969
1523
|
if (ctx.request?.headers.has("x-shard-id")) {
|
|
1524
|
+
if (!this.isAuthorizedShardRequest(ctx.request)) {
|
|
1525
|
+
conn.close();
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
970
1528
|
this.onConnectShard(conn, ctx);
|
|
971
1529
|
} else {
|
|
972
1530
|
await this.onConnectClient(conn, ctx);
|
|
973
1531
|
}
|
|
974
1532
|
}
|
|
1533
|
+
isAuthorizedShardRequest(req) {
|
|
1534
|
+
const shardSecret = this.room.env.SHARD_SECRET;
|
|
1535
|
+
return typeof shardSecret === "string" && shardSecret.length > 0 && req?.headers.get("x-access-shard") === shardSecret;
|
|
1536
|
+
}
|
|
975
1537
|
/**
|
|
976
1538
|
* @method onConnectShard
|
|
977
1539
|
* @private
|
|
@@ -982,9 +1544,11 @@ var Server = class {
|
|
|
982
1544
|
*/
|
|
983
1545
|
onConnectShard(conn, ctx) {
|
|
984
1546
|
const shardId = ctx.request?.headers.get("x-shard-id") || "unknown-shard";
|
|
1547
|
+
const worldId = ctx.request?.headers.get("x-shard-world-id") || "world-default";
|
|
985
1548
|
conn.setState({
|
|
986
1549
|
shard: true,
|
|
987
1550
|
shardId,
|
|
1551
|
+
worldId,
|
|
988
1552
|
clients: /* @__PURE__ */ new Map()
|
|
989
1553
|
// Track clients connected through this shard
|
|
990
1554
|
});
|
|
@@ -1254,11 +1818,15 @@ var Server = class {
|
|
|
1254
1818
|
if (!conn.state) {
|
|
1255
1819
|
return;
|
|
1256
1820
|
}
|
|
1257
|
-
const privateId = conn
|
|
1821
|
+
const privateId = this.getPrivateId(conn);
|
|
1258
1822
|
const { publicId } = conn.state;
|
|
1259
1823
|
const user = signal2?.()[publicId];
|
|
1260
1824
|
if (!user) return;
|
|
1825
|
+
if (this.hasActiveSessionConnection(privateId)) {
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1261
1828
|
await this.updateSessionConnection(privateId, false);
|
|
1829
|
+
this.scheduleSessionGarbageCollector(this.getSessionExpiryTime(subRoom), privateId);
|
|
1262
1830
|
const connectionUpdated = this.updateUserConnectionStatus(user, false);
|
|
1263
1831
|
await awaitReturn(subRoom["onLeave"]?.(user, conn));
|
|
1264
1832
|
if (!connectionUpdated) {
|
|
@@ -1293,6 +1861,9 @@ var Server = class {
|
|
|
1293
1861
|
return res.status(200).send({});
|
|
1294
1862
|
}
|
|
1295
1863
|
if (isFromShard) {
|
|
1864
|
+
if (!this.isAuthorizedShardRequest(req)) {
|
|
1865
|
+
return res.unauthorized("Invalid shard credentials");
|
|
1866
|
+
}
|
|
1296
1867
|
return this.handleShardRequest(req, res, shardId);
|
|
1297
1868
|
}
|
|
1298
1869
|
return this.handleDirectRequest(req, res);
|
|
@@ -1329,7 +1900,7 @@ var Server = class {
|
|
|
1329
1900
|
const usersPropName = this.getUsersPropName(subRoom);
|
|
1330
1901
|
if (signal2 && usersPropName) {
|
|
1331
1902
|
const { classType } = signal2.options;
|
|
1332
|
-
const user =
|
|
1903
|
+
const user = this.createUserFromClassType(classType);
|
|
1333
1904
|
const hydratedSnapshot = await awaitReturn(
|
|
1334
1905
|
subRoom["onSessionRestore"]?.({
|
|
1335
1906
|
userSnapshot,
|
|
@@ -1342,14 +1913,15 @@ var Server = class {
|
|
|
1342
1913
|
) ?? userSnapshot;
|
|
1343
1914
|
signal2()[publicId] = user;
|
|
1344
1915
|
load(user, hydratedSnapshot, true);
|
|
1345
|
-
await this.
|
|
1916
|
+
await this.saveStatePath(`${usersPropName}.${publicId}`, userSnapshot);
|
|
1346
1917
|
}
|
|
1347
1918
|
}
|
|
1348
1919
|
const transferToken = generateShortUUID2();
|
|
1349
|
-
await this.room.storage.put(
|
|
1920
|
+
await this.room.storage.put(this.transferKey(transferToken), {
|
|
1350
1921
|
privateId,
|
|
1351
1922
|
publicId,
|
|
1352
|
-
restored: true
|
|
1923
|
+
restored: true,
|
|
1924
|
+
created: Date.now()
|
|
1353
1925
|
});
|
|
1354
1926
|
return res.success({ transferToken });
|
|
1355
1927
|
} catch (error) {
|
|
@@ -1553,17 +2125,33 @@ var Server = class {
|
|
|
1553
2125
|
|
|
1554
2126
|
// src/shard.ts
|
|
1555
2127
|
var Shard = class {
|
|
1556
|
-
constructor(room) {
|
|
2128
|
+
constructor(room, options = {}) {
|
|
1557
2129
|
this.room = room;
|
|
1558
2130
|
this.connectionMap = /* @__PURE__ */ new Map();
|
|
1559
2131
|
this.worldUrl = null;
|
|
1560
|
-
this.worldId = "default";
|
|
1561
2132
|
this.lastReportedConnections = 0;
|
|
1562
2133
|
this.statsInterval = 3e4;
|
|
1563
2134
|
this.statsIntervalId = null;
|
|
2135
|
+
this.worldUrl = options.worldUrl ?? null;
|
|
2136
|
+
this.worldId = options.worldId ?? this.getWorldIdFromShardId(room.id) ?? this.getEnvString("WORLD_ID") ?? this.getEnvString("SIGNE_WORLD_ID") ?? "world-default";
|
|
2137
|
+
this.statsInterval = options.statsInterval ?? this.statsInterval;
|
|
2138
|
+
}
|
|
2139
|
+
getPrivateId(conn) {
|
|
2140
|
+
return conn.sessionId || conn.id;
|
|
2141
|
+
}
|
|
2142
|
+
getEnvString(key) {
|
|
2143
|
+
const value = this.room.env?.[key];
|
|
2144
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2145
|
+
}
|
|
2146
|
+
getRoomIdFromShardId(shardId) {
|
|
2147
|
+
return shardId.split(":")[0];
|
|
2148
|
+
}
|
|
2149
|
+
getWorldIdFromShardId(shardId) {
|
|
2150
|
+
const parts = shardId.split(":");
|
|
2151
|
+
return parts.length >= 3 ? parts[1] : void 0;
|
|
1564
2152
|
}
|
|
1565
2153
|
async onStart() {
|
|
1566
|
-
const roomId = this.room.id
|
|
2154
|
+
const roomId = this.getRoomIdFromShardId(this.room.id);
|
|
1567
2155
|
const roomStub = this.room.context.parties.main.get(roomId);
|
|
1568
2156
|
if (!roomStub) {
|
|
1569
2157
|
console.warn("No room room stub found in main party context");
|
|
@@ -1572,17 +2160,30 @@ var Shard = class {
|
|
|
1572
2160
|
this.mainServerStub = roomStub;
|
|
1573
2161
|
this.ws = await roomStub.socket({
|
|
1574
2162
|
headers: {
|
|
1575
|
-
"x-shard-id": this.room.id
|
|
2163
|
+
"x-shard-id": this.room.id,
|
|
2164
|
+
"x-shard-world-id": this.worldId,
|
|
2165
|
+
"x-access-shard": this.room.env.SHARD_SECRET
|
|
1576
2166
|
}
|
|
1577
2167
|
});
|
|
1578
2168
|
this.ws.addEventListener("message", (event) => {
|
|
1579
2169
|
try {
|
|
1580
2170
|
const message = JSON.parse(event.data);
|
|
2171
|
+
if (message.type === "shard.closeClient" && message.privateId) {
|
|
2172
|
+
const clientConnections = this.connectionMap.get(message.privateId);
|
|
2173
|
+
if (clientConnections?.size) {
|
|
2174
|
+
for (const clientConn of [...clientConnections]) {
|
|
2175
|
+
clientConn.close();
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
1581
2180
|
if (message.targetClientId) {
|
|
1582
|
-
const
|
|
1583
|
-
if (
|
|
2181
|
+
const clientConnections = this.connectionMap.get(message.targetClientId);
|
|
2182
|
+
if (clientConnections?.size) {
|
|
1584
2183
|
delete message.targetClientId;
|
|
1585
|
-
clientConn
|
|
2184
|
+
for (const clientConn of clientConnections) {
|
|
2185
|
+
clientConn.send(message.data);
|
|
2186
|
+
}
|
|
1586
2187
|
}
|
|
1587
2188
|
} else {
|
|
1588
2189
|
this.room.broadcast(event.data);
|
|
@@ -1591,21 +2192,22 @@ var Shard = class {
|
|
|
1591
2192
|
console.error("Error processing message from main server:", error);
|
|
1592
2193
|
}
|
|
1593
2194
|
});
|
|
1594
|
-
await this.updateWorldStats();
|
|
2195
|
+
await this.updateWorldStats(true);
|
|
1595
2196
|
this.startPeriodicStatsUpdates();
|
|
1596
2197
|
}
|
|
1597
2198
|
startPeriodicStatsUpdates() {
|
|
1598
|
-
if (!this.
|
|
2199
|
+
if (this.statsInterval <= 0 || !this.room.context.parties.world) {
|
|
1599
2200
|
return;
|
|
1600
2201
|
}
|
|
1601
2202
|
if (this.statsIntervalId) {
|
|
1602
2203
|
clearInterval(this.statsIntervalId);
|
|
1603
2204
|
}
|
|
1604
2205
|
this.statsIntervalId = setInterval(() => {
|
|
1605
|
-
this.updateWorldStats().catch((error) => {
|
|
2206
|
+
this.updateWorldStats(true).catch((error) => {
|
|
1606
2207
|
console.error("Error in periodic stats update:", error);
|
|
1607
2208
|
});
|
|
1608
2209
|
}, this.statsInterval);
|
|
2210
|
+
this.statsIntervalId?.unref?.();
|
|
1609
2211
|
}
|
|
1610
2212
|
stopPeriodicStatsUpdates() {
|
|
1611
2213
|
if (this.statsIntervalId) {
|
|
@@ -1614,7 +2216,10 @@ var Shard = class {
|
|
|
1614
2216
|
}
|
|
1615
2217
|
}
|
|
1616
2218
|
onConnect(conn, ctx) {
|
|
1617
|
-
this.
|
|
2219
|
+
const privateId = this.getPrivateId(conn);
|
|
2220
|
+
const connections = this.connectionMap.get(privateId) ?? /* @__PURE__ */ new Set();
|
|
2221
|
+
connections.add(conn);
|
|
2222
|
+
this.connectionMap.set(privateId, connections);
|
|
1618
2223
|
const headers = {};
|
|
1619
2224
|
if (ctx.request?.headers) {
|
|
1620
2225
|
ctx.request.headers.forEach((value, key) => {
|
|
@@ -1628,7 +2233,7 @@ var Shard = class {
|
|
|
1628
2233
|
} : null;
|
|
1629
2234
|
this.ws.send(JSON.stringify({
|
|
1630
2235
|
type: "shard.clientConnected",
|
|
1631
|
-
privateId
|
|
2236
|
+
privateId,
|
|
1632
2237
|
requestInfo
|
|
1633
2238
|
}));
|
|
1634
2239
|
this.updateWorldStats();
|
|
@@ -1638,7 +2243,7 @@ var Shard = class {
|
|
|
1638
2243
|
const parsedMessage = typeof message === "string" ? JSON.parse(message) : message;
|
|
1639
2244
|
const wrappedMessage = JSON.stringify({
|
|
1640
2245
|
type: "shard.clientMessage",
|
|
1641
|
-
privateId: sender
|
|
2246
|
+
privateId: this.getPrivateId(sender),
|
|
1642
2247
|
publicId: sender.state?.publicId,
|
|
1643
2248
|
payload: parsedMessage
|
|
1644
2249
|
});
|
|
@@ -1648,21 +2253,35 @@ var Shard = class {
|
|
|
1648
2253
|
}
|
|
1649
2254
|
}
|
|
1650
2255
|
onClose(conn) {
|
|
1651
|
-
this.
|
|
2256
|
+
const privateId = this.getPrivateId(conn);
|
|
2257
|
+
const connections = this.connectionMap.get(privateId);
|
|
2258
|
+
connections?.delete(conn);
|
|
2259
|
+
if (connections?.size) {
|
|
2260
|
+
this.updateWorldStats();
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
this.connectionMap.delete(privateId);
|
|
1652
2264
|
this.ws.send(JSON.stringify({
|
|
1653
2265
|
type: "shard.clientDisconnected",
|
|
1654
|
-
privateId
|
|
2266
|
+
privateId,
|
|
1655
2267
|
publicId: conn.state?.publicId
|
|
1656
2268
|
}));
|
|
1657
2269
|
this.updateWorldStats();
|
|
1658
2270
|
}
|
|
1659
|
-
async updateWorldStats() {
|
|
1660
|
-
const currentConnections = this.connectionMap.size;
|
|
1661
|
-
if (currentConnections === this.lastReportedConnections) {
|
|
2271
|
+
async updateWorldStats(force = false) {
|
|
2272
|
+
const currentConnections = Array.from(this.connectionMap.values()).reduce((total, connections) => total + connections.size, 0);
|
|
2273
|
+
if (!force && currentConnections === this.lastReportedConnections) {
|
|
1662
2274
|
return true;
|
|
1663
2275
|
}
|
|
1664
2276
|
try {
|
|
1665
|
-
const
|
|
2277
|
+
const worldParty = this.room.context.parties.world;
|
|
2278
|
+
if (!worldParty) {
|
|
2279
|
+
return false;
|
|
2280
|
+
}
|
|
2281
|
+
const worldRoom = worldParty.get(this.worldId);
|
|
2282
|
+
if (!worldRoom?.fetch) {
|
|
2283
|
+
return false;
|
|
2284
|
+
}
|
|
1666
2285
|
const response2 = await worldRoom.fetch("/update-shard", {
|
|
1667
2286
|
method: "POST",
|
|
1668
2287
|
headers: {
|
|
@@ -1671,6 +2290,7 @@ var Shard = class {
|
|
|
1671
2290
|
},
|
|
1672
2291
|
body: JSON.stringify({
|
|
1673
2292
|
shardId: this.room.id,
|
|
2293
|
+
worldId: this.worldId,
|
|
1674
2294
|
connections: currentConnections
|
|
1675
2295
|
})
|
|
1676
2296
|
});
|
|
@@ -1710,7 +2330,9 @@ var Shard = class {
|
|
|
1710
2330
|
headers.set(key, value);
|
|
1711
2331
|
});
|
|
1712
2332
|
headers.set("x-shard-id", this.room.id);
|
|
2333
|
+
headers.set("x-shard-world-id", this.worldId);
|
|
1713
2334
|
headers.set("x-forwarded-by-shard", "true");
|
|
2335
|
+
headers.set("x-access-shard", this.room.env.SHARD_SECRET);
|
|
1714
2336
|
const clientIp = req.headers.get("x-forwarded-for") || "unknown";
|
|
1715
2337
|
if (clientIp) {
|
|
1716
2338
|
headers.set("x-original-client-ip", clientIp);
|
|
@@ -1720,7 +2342,7 @@ var Shard = class {
|
|
|
1720
2342
|
headers,
|
|
1721
2343
|
body
|
|
1722
2344
|
};
|
|
1723
|
-
const response2 = await this.mainServerStub.fetch(path, requestInit);
|
|
2345
|
+
const response2 = await this.mainServerStub.fetch(path + url.search, requestInit);
|
|
1724
2346
|
return response2;
|
|
1725
2347
|
} catch (error) {
|
|
1726
2348
|
return response(500, { error: "Error forwarding request" });
|
|
@@ -1732,7 +2354,7 @@ var Shard = class {
|
|
|
1732
2354
|
* @description Executed periodically, used to perform maintenance tasks
|
|
1733
2355
|
*/
|
|
1734
2356
|
async onAlarm() {
|
|
1735
|
-
await this.updateWorldStats();
|
|
2357
|
+
await this.updateWorldStats(true);
|
|
1736
2358
|
}
|
|
1737
2359
|
};
|
|
1738
2360
|
|
|
@@ -1744,7 +2366,7 @@ async function testRoom(Room2, options = {}) {
|
|
|
1744
2366
|
return server2;
|
|
1745
2367
|
};
|
|
1746
2368
|
const isShard = options.shard || false;
|
|
1747
|
-
const io = new ServerIo(Room2.path, isShard ? {
|
|
2369
|
+
const io = new ServerIo(options.id ?? Room2.path, isShard ? {
|
|
1748
2370
|
parties: {
|
|
1749
2371
|
game: createServer,
|
|
1750
2372
|
...options.parties || {}
|
|
@@ -1788,7 +2410,7 @@ async function testRoom(Room2, options = {}) {
|
|
|
1788
2410
|
return client;
|
|
1789
2411
|
},
|
|
1790
2412
|
getServerUser: async (client, prop = "users") => {
|
|
1791
|
-
const privateId = client.conn.id;
|
|
2413
|
+
const privateId = client.conn.sessionId || client.conn.id;
|
|
1792
2414
|
const session = await server.getSession(privateId);
|
|
1793
2415
|
return server.subRoom[prop]()[session?.publicId];
|
|
1794
2416
|
}
|
|
@@ -1808,11 +2430,11 @@ function tick(ms = 0) {
|
|
|
1808
2430
|
|
|
1809
2431
|
// src/mock.ts
|
|
1810
2432
|
var MockPartyClient = class {
|
|
1811
|
-
constructor(server,
|
|
2433
|
+
constructor(server, sessionId) {
|
|
1812
2434
|
this.server = server;
|
|
1813
2435
|
this.events = /* @__PURE__ */ new Map();
|
|
1814
|
-
this.id =
|
|
1815
|
-
this.conn = new MockConnection(this);
|
|
2436
|
+
this.id = generateShortUUID();
|
|
2437
|
+
this.conn = new MockConnection(this, sessionId || this.id);
|
|
1816
2438
|
}
|
|
1817
2439
|
addEventListener(event, cb) {
|
|
1818
2440
|
if (!this.events.has(event)) {
|
|
@@ -1848,8 +2470,8 @@ var MockLobby = class {
|
|
|
1848
2470
|
this.server = server;
|
|
1849
2471
|
this.lobbyId = lobbyId;
|
|
1850
2472
|
}
|
|
1851
|
-
socket(
|
|
1852
|
-
return new MockPartyClient(this.server);
|
|
2473
|
+
socket(init) {
|
|
2474
|
+
return new MockPartyClient(this.server, init?.id);
|
|
1853
2475
|
}
|
|
1854
2476
|
async connection(idOrOptions, maybeOptions) {
|
|
1855
2477
|
const id2 = typeof idOrOptions === "string" ? idOrOptions : idOrOptions?.id;
|
|
@@ -1925,7 +2547,13 @@ var MockPartyRoom = class {
|
|
|
1925
2547
|
});
|
|
1926
2548
|
}
|
|
1927
2549
|
getConnection(id2) {
|
|
1928
|
-
|
|
2550
|
+
let connection;
|
|
2551
|
+
for (const client of this.clients.values()) {
|
|
2552
|
+
if (client.conn.id === id2 || client.conn.sessionId === id2) {
|
|
2553
|
+
connection = client.conn;
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
return connection;
|
|
1929
2557
|
}
|
|
1930
2558
|
getConnections() {
|
|
1931
2559
|
return Array.from(this.clients.values()).map((client) => client.conn);
|
|
@@ -1933,13 +2561,25 @@ var MockPartyRoom = class {
|
|
|
1933
2561
|
clear() {
|
|
1934
2562
|
this.clients.clear();
|
|
1935
2563
|
}
|
|
2564
|
+
deleteConnection(id2, connection) {
|
|
2565
|
+
if (connection) {
|
|
2566
|
+
this.clients.delete(connection.id);
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
for (const [connectionKey, client] of this.clients) {
|
|
2570
|
+
if (client.conn.id === id2 || client.conn.sessionId === id2) {
|
|
2571
|
+
this.clients.delete(connectionKey);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
1936
2575
|
};
|
|
1937
2576
|
var MockConnection = class {
|
|
1938
|
-
constructor(client) {
|
|
2577
|
+
constructor(client, sessionId) {
|
|
1939
2578
|
this.client = client;
|
|
1940
2579
|
this.state = {};
|
|
1941
2580
|
this.server = client.server;
|
|
1942
2581
|
this.id = client.id;
|
|
2582
|
+
this.sessionId = sessionId;
|
|
1943
2583
|
}
|
|
1944
2584
|
setState(value) {
|
|
1945
2585
|
this.state = value;
|
|
@@ -1948,6 +2588,7 @@ var MockConnection = class {
|
|
|
1948
2588
|
this.client._trigger("message", data);
|
|
1949
2589
|
}
|
|
1950
2590
|
close() {
|
|
2591
|
+
this.server.room.deleteConnection?.(this.id, this);
|
|
1951
2592
|
this.server.onClose(this);
|
|
1952
2593
|
}
|
|
1953
2594
|
};
|
|
@@ -2127,7 +2768,7 @@ var guardManageWorld = async (_, req, room) => {
|
|
|
2127
2768
|
return true;
|
|
2128
2769
|
}
|
|
2129
2770
|
const url = new URL(req.url);
|
|
2130
|
-
const token = req
|
|
2771
|
+
const token = getAuthToken(req, url);
|
|
2131
2772
|
if (!token) {
|
|
2132
2773
|
return false;
|
|
2133
2774
|
}
|
|
@@ -2137,11 +2778,28 @@ var guardManageWorld = async (_, req, room) => {
|
|
|
2137
2778
|
if (!payload) {
|
|
2138
2779
|
return false;
|
|
2139
2780
|
}
|
|
2781
|
+
if (!canAccessWorld(payload, room.id)) {
|
|
2782
|
+
return false;
|
|
2783
|
+
}
|
|
2140
2784
|
} catch (error) {
|
|
2141
2785
|
return false;
|
|
2142
2786
|
}
|
|
2143
2787
|
return true;
|
|
2144
2788
|
};
|
|
2789
|
+
function getAuthToken(req, url) {
|
|
2790
|
+
const authorization = req.headers.get("Authorization");
|
|
2791
|
+
if (authorization?.startsWith("Bearer ")) {
|
|
2792
|
+
return authorization.slice("Bearer ".length).trim();
|
|
2793
|
+
}
|
|
2794
|
+
return authorization ?? url.searchParams.get("world-auth-token");
|
|
2795
|
+
}
|
|
2796
|
+
function canAccessWorld(payload, worldId) {
|
|
2797
|
+
const worlds = payload.worlds;
|
|
2798
|
+
if (!Array.isArray(worlds)) {
|
|
2799
|
+
return false;
|
|
2800
|
+
}
|
|
2801
|
+
return worlds.some((world) => world === "*" || world === worldId);
|
|
2802
|
+
}
|
|
2145
2803
|
|
|
2146
2804
|
// src/world.ts
|
|
2147
2805
|
var MAX_PLAYERS_PER_SHARD = 75;
|
|
@@ -2156,10 +2814,13 @@ var RoomConfigSchema = z2.object({
|
|
|
2156
2814
|
var RegisterShardSchema = z2.object({
|
|
2157
2815
|
shardId: z2.string(),
|
|
2158
2816
|
roomId: z2.string(),
|
|
2817
|
+
worldId: z2.string().optional(),
|
|
2159
2818
|
url: z2.string().url(),
|
|
2160
2819
|
maxConnections: z2.number().int().positive()
|
|
2161
2820
|
});
|
|
2162
2821
|
var UpdateShardStatsSchema = z2.object({
|
|
2822
|
+
shardId: z2.string(),
|
|
2823
|
+
worldId: z2.string().optional(),
|
|
2163
2824
|
connections: z2.number().int().min(0),
|
|
2164
2825
|
status: z2.enum(["active", "maintenance", "draining"]).optional()
|
|
2165
2826
|
});
|
|
@@ -2205,6 +2866,7 @@ __decorateClass([
|
|
|
2205
2866
|
var ShardInfo = class {
|
|
2206
2867
|
constructor() {
|
|
2207
2868
|
this.roomId = signal("");
|
|
2869
|
+
this.worldId = signal("");
|
|
2208
2870
|
this.url = signal("");
|
|
2209
2871
|
this.currentConnections = signal(0);
|
|
2210
2872
|
this.maxConnections = signal(MAX_PLAYERS_PER_SHARD);
|
|
@@ -2218,6 +2880,9 @@ __decorateClass([
|
|
|
2218
2880
|
__decorateClass([
|
|
2219
2881
|
sync()
|
|
2220
2882
|
], ShardInfo.prototype, "roomId", 2);
|
|
2883
|
+
__decorateClass([
|
|
2884
|
+
sync()
|
|
2885
|
+
], ShardInfo.prototype, "worldId", 2);
|
|
2221
2886
|
__decorateClass([
|
|
2222
2887
|
sync()
|
|
2223
2888
|
], ShardInfo.prototype, "url", 2);
|
|
@@ -2251,6 +2916,7 @@ var WorldRoom = class {
|
|
|
2251
2916
|
if (!SHARD_SECRET) {
|
|
2252
2917
|
throw new Error("SHARD_SECRET env variable is not set");
|
|
2253
2918
|
}
|
|
2919
|
+
this.scheduleInactiveShardCleanup();
|
|
2254
2920
|
}
|
|
2255
2921
|
async onJoin(user, conn, ctx) {
|
|
2256
2922
|
const canConnect = await guardManageWorld(user, ctx.request, this.room);
|
|
@@ -2266,6 +2932,13 @@ var WorldRoom = class {
|
|
|
2266
2932
|
return obj;
|
|
2267
2933
|
}
|
|
2268
2934
|
// Helper methods
|
|
2935
|
+
getWorldId() {
|
|
2936
|
+
return this.room.id;
|
|
2937
|
+
}
|
|
2938
|
+
scheduleInactiveShardCleanup() {
|
|
2939
|
+
const timeoutId = setTimeout(() => this.cleanupInactiveShards(), 6e4);
|
|
2940
|
+
timeoutId?.unref?.();
|
|
2941
|
+
}
|
|
2269
2942
|
cleanupInactiveShards() {
|
|
2270
2943
|
const now = Date.now();
|
|
2271
2944
|
const timeout = 5 * 60 * 1e3;
|
|
@@ -2277,10 +2950,25 @@ var WorldRoom = class {
|
|
|
2277
2950
|
hasChanges = true;
|
|
2278
2951
|
}
|
|
2279
2952
|
});
|
|
2280
|
-
|
|
2953
|
+
this.scheduleInactiveShardCleanup();
|
|
2954
|
+
}
|
|
2955
|
+
removeShard(shardId) {
|
|
2956
|
+
delete this.shards()[shardId];
|
|
2281
2957
|
}
|
|
2282
|
-
|
|
2283
|
-
|
|
2958
|
+
shouldCompleteDrain(shard) {
|
|
2959
|
+
return shard.status() === "draining" && shard.currentConnections() === 0;
|
|
2960
|
+
}
|
|
2961
|
+
async registerRoom(req, res) {
|
|
2962
|
+
const parseResult = RoomConfigSchema.safeParse(await req.json());
|
|
2963
|
+
if (!parseResult.success) {
|
|
2964
|
+
return res?.badRequest("Invalid room configuration", {
|
|
2965
|
+
details: parseResult.error
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
2968
|
+
const roomConfig = parseResult.data;
|
|
2969
|
+
if (roomConfig.maxShards !== void 0 && roomConfig.minShards > roomConfig.maxShards) {
|
|
2970
|
+
return res?.badRequest("minShards cannot be greater than maxShards");
|
|
2971
|
+
}
|
|
2284
2972
|
const roomId = roomConfig.name;
|
|
2285
2973
|
if (!this.rooms()[roomId]) {
|
|
2286
2974
|
const newRoom = new RoomConfig();
|
|
@@ -2305,22 +2993,41 @@ var WorldRoom = class {
|
|
|
2305
2993
|
room.minShards.set(roomConfig.minShards);
|
|
2306
2994
|
room.maxShards.set(roomConfig.maxShards);
|
|
2307
2995
|
}
|
|
2996
|
+
await this.ensureMinShards(roomId);
|
|
2308
2997
|
}
|
|
2309
2998
|
async updateShardStats(req, res) {
|
|
2310
|
-
const
|
|
2999
|
+
const parseResult = UpdateShardStatsSchema.safeParse(await req.json());
|
|
3000
|
+
if (!parseResult.success) {
|
|
3001
|
+
return res.badRequest("Invalid shard stats", {
|
|
3002
|
+
details: parseResult.error
|
|
3003
|
+
});
|
|
3004
|
+
}
|
|
3005
|
+
const body = parseResult.data;
|
|
2311
3006
|
const { shardId, connections, status } = body;
|
|
2312
3007
|
const shard = this.shards()[shardId];
|
|
2313
3008
|
if (!shard) {
|
|
2314
3009
|
return res.notFound(`Shard ${shardId} not found`);
|
|
2315
3010
|
}
|
|
3011
|
+
if (body.worldId && body.worldId !== this.getWorldId()) {
|
|
3012
|
+
return res.badRequest(`Shard ${shardId} belongs to world ${body.worldId}, not ${this.getWorldId()}`);
|
|
3013
|
+
}
|
|
2316
3014
|
shard.currentConnections.set(connections);
|
|
2317
3015
|
if (status) {
|
|
2318
3016
|
shard.status.set(status);
|
|
2319
3017
|
}
|
|
2320
3018
|
shard.lastHeartbeat.set(Date.now());
|
|
3019
|
+
if (this.shouldCompleteDrain(shard)) {
|
|
3020
|
+
this.removeShard(shard.id);
|
|
3021
|
+
}
|
|
2321
3022
|
}
|
|
2322
3023
|
async scaleRoom(req, res) {
|
|
2323
|
-
const
|
|
3024
|
+
const parseResult = ScaleRoomSchema.safeParse(await req.json());
|
|
3025
|
+
if (!parseResult.success) {
|
|
3026
|
+
return res.badRequest("Invalid scale room request", {
|
|
3027
|
+
details: parseResult.error
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
const data = parseResult.data;
|
|
2324
3031
|
const { targetShardCount, shardTemplate, roomId } = data;
|
|
2325
3032
|
const room = this.rooms()[roomId];
|
|
2326
3033
|
if (!room) {
|
|
@@ -2335,16 +3042,16 @@ var WorldRoom = class {
|
|
|
2335
3042
|
});
|
|
2336
3043
|
}
|
|
2337
3044
|
if (targetShardCount < previousShardCount) {
|
|
2338
|
-
const
|
|
3045
|
+
const shardsToDrain = [...roomShards].sort((a, b) => {
|
|
2339
3046
|
if (a.status() === "draining" && b.status() !== "draining") return -1;
|
|
2340
3047
|
if (a.status() !== "draining" && b.status() === "draining") return 1;
|
|
2341
3048
|
return a.currentConnections() - b.currentConnections();
|
|
2342
3049
|
}).slice(0, previousShardCount - targetShardCount);
|
|
2343
|
-
const
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
3050
|
+
for (const shard of shardsToDrain) {
|
|
3051
|
+
shard.status.set("draining");
|
|
3052
|
+
if (this.shouldCompleteDrain(shard)) {
|
|
3053
|
+
this.removeShard(shard.id);
|
|
3054
|
+
}
|
|
2348
3055
|
}
|
|
2349
3056
|
return;
|
|
2350
3057
|
}
|
|
@@ -2431,8 +3138,20 @@ var WorldRoom = class {
|
|
|
2431
3138
|
return { error: `No shards available for room ${roomId}` };
|
|
2432
3139
|
}
|
|
2433
3140
|
}
|
|
2434
|
-
|
|
3141
|
+
let activeShards = this.getAvailableShards(roomShards);
|
|
2435
3142
|
if (activeShards.length === 0) {
|
|
3143
|
+
if (autoCreate && this.canCreateShard(room, roomShards.length)) {
|
|
3144
|
+
const newShard = await this.createShard(roomId);
|
|
3145
|
+
if (newShard) {
|
|
3146
|
+
return {
|
|
3147
|
+
shardId: newShard.id,
|
|
3148
|
+
url: newShard.url()
|
|
3149
|
+
};
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
if (roomShards.some((shard) => shard.status() === "active")) {
|
|
3153
|
+
return { error: `No shard capacity available for room ${roomId}` };
|
|
3154
|
+
}
|
|
2436
3155
|
return { error: `No active shards available for room ${roomId}` };
|
|
2437
3156
|
}
|
|
2438
3157
|
const balancingStrategy = room.balancingStrategy();
|
|
@@ -2460,6 +3179,14 @@ var WorldRoom = class {
|
|
|
2460
3179
|
url: selectedShard.url()
|
|
2461
3180
|
};
|
|
2462
3181
|
}
|
|
3182
|
+
getAvailableShards(shards) {
|
|
3183
|
+
return shards.filter(
|
|
3184
|
+
(shard) => shard && shard.status() === "active" && shard.currentConnections() < shard.maxConnections()
|
|
3185
|
+
);
|
|
3186
|
+
}
|
|
3187
|
+
canCreateShard(room, currentShardCount) {
|
|
3188
|
+
return room.maxShards() === void 0 || currentShardCount < room.maxShards();
|
|
3189
|
+
}
|
|
2463
3190
|
// Private methods
|
|
2464
3191
|
async createShard(roomId, urlTemplate, maxConnections) {
|
|
2465
3192
|
const room = this.rooms()[roomId];
|
|
@@ -2467,13 +3194,15 @@ var WorldRoom = class {
|
|
|
2467
3194
|
console.error(`Cannot create shard for non-existent room: ${roomId}`);
|
|
2468
3195
|
return null;
|
|
2469
3196
|
}
|
|
2470
|
-
const
|
|
3197
|
+
const worldId = this.getWorldId();
|
|
3198
|
+
const shardId = `${roomId}:${worldId}:${Date.now()}-${Math.floor(Math.random() * 1e4)}`;
|
|
2471
3199
|
const template = urlTemplate || this.defaultShardUrlTemplate();
|
|
2472
3200
|
const url = template.replace("{shardId}", shardId).replace("{roomId}", roomId);
|
|
2473
3201
|
const max = maxConnections || room.maxPlayersPerShard();
|
|
2474
3202
|
const newShard = new ShardInfo();
|
|
2475
3203
|
newShard.id = shardId;
|
|
2476
3204
|
newShard.roomId.set(roomId);
|
|
3205
|
+
newShard.worldId.set(worldId);
|
|
2477
3206
|
newShard.url.set(url);
|
|
2478
3207
|
newShard.maxConnections.set(max);
|
|
2479
3208
|
newShard.currentConnections.set(0);
|
|
@@ -2482,6 +3211,20 @@ var WorldRoom = class {
|
|
|
2482
3211
|
this.shards()[shardId] = newShard;
|
|
2483
3212
|
return newShard;
|
|
2484
3213
|
}
|
|
3214
|
+
async ensureMinShards(roomId) {
|
|
3215
|
+
const room = this.rooms()[roomId];
|
|
3216
|
+
if (!room) {
|
|
3217
|
+
return;
|
|
3218
|
+
}
|
|
3219
|
+
const currentShardCount = Object.values(this.shards()).filter((shard) => shard.roomId() === roomId).length;
|
|
3220
|
+
const targetShardCount = room.minShards();
|
|
3221
|
+
if (currentShardCount >= targetShardCount) {
|
|
3222
|
+
return;
|
|
3223
|
+
}
|
|
3224
|
+
for (let i = currentShardCount; i < targetShardCount; i++) {
|
|
3225
|
+
await this.createShard(roomId);
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
2485
3228
|
};
|
|
2486
3229
|
__decorateClass([
|
|
2487
3230
|
sync(RoomConfig)
|
|
@@ -2532,13 +3275,16 @@ WorldRoom = __decorateClass([
|
|
|
2532
3275
|
], WorldRoom);
|
|
2533
3276
|
|
|
2534
3277
|
// src/session.guard.ts
|
|
3278
|
+
function getPrivateId(sender) {
|
|
3279
|
+
return sender.sessionId || sender.id;
|
|
3280
|
+
}
|
|
2535
3281
|
function createRequireSessionGuard(storage) {
|
|
2536
3282
|
return async (sender, value) => {
|
|
2537
3283
|
if (!sender || !sender.id) {
|
|
2538
3284
|
return false;
|
|
2539
3285
|
}
|
|
2540
3286
|
try {
|
|
2541
|
-
const session = await storage.get(`session:${sender
|
|
3287
|
+
const session = await storage.get(`session:${getPrivateId(sender)}`);
|
|
2542
3288
|
if (!session) {
|
|
2543
3289
|
return false;
|
|
2544
3290
|
}
|
|
@@ -2558,7 +3304,7 @@ var requireSession = async (sender, value, room) => {
|
|
|
2558
3304
|
return false;
|
|
2559
3305
|
}
|
|
2560
3306
|
try {
|
|
2561
|
-
const session = await room.storage.get(`session:${sender
|
|
3307
|
+
const session = await room.storage.get(`session:${getPrivateId(sender)}`);
|
|
2562
3308
|
if (!session) {
|
|
2563
3309
|
return false;
|
|
2564
3310
|
}
|