@signe/room 2.10.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +66 -187
- package/dist/index.js +727 -106
- 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 +371 -4
- package/src/cloudflare/index.ts +474 -0
- package/src/jwt.ts +1 -5
- package/src/mock.ts +29 -7
- package/src/node/index.ts +1112 -0
- package/src/server.ts +600 -51
- 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 +4 -1
- package/src/world.guard.ts +23 -4
- package/src/world.ts +121 -21
- 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,88 @@ 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
|
+
path
|
|
548
|
+
]);
|
|
549
|
+
await this.saveStatePath(path, DELETE_TOKEN);
|
|
550
|
+
}
|
|
551
|
+
createStorageMetrics() {
|
|
552
|
+
return {
|
|
553
|
+
loadMs: 0,
|
|
554
|
+
loadStateKeys: 0,
|
|
555
|
+
loadLegacyKeys: 0,
|
|
556
|
+
persistFlushes: 0,
|
|
557
|
+
persistWrites: 0,
|
|
558
|
+
persistDeletes: 0,
|
|
559
|
+
persistLastFlushMs: 0,
|
|
560
|
+
sessionGcRuns: 0,
|
|
561
|
+
sessionGcScanned: 0,
|
|
562
|
+
sessionGcExpired: 0,
|
|
563
|
+
sessionIndexRepairs: 0,
|
|
564
|
+
transferGcRuns: 0,
|
|
565
|
+
transferGcScanned: 0,
|
|
566
|
+
transferGcExpired: 0
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
getTransferExpiryTime(subRoom) {
|
|
570
|
+
return subRoom?.transferExpiryTime ?? subRoom?.constructor?.prototype?.transferExpiryTime ?? subRoom?.constructor?.transferExpiryTime ?? DEFAULT_TRANSFER_EXPIRY_MS;
|
|
571
|
+
}
|
|
572
|
+
getPrivateId(conn) {
|
|
573
|
+
return conn.state?.privateId || conn.sessionId || conn.id;
|
|
574
|
+
}
|
|
575
|
+
hasActiveSessionConnection(privateId) {
|
|
576
|
+
return Array.from(this.room.getConnections()).some((conn) => this.getPrivateId(conn) === privateId);
|
|
577
|
+
}
|
|
473
578
|
async send(conn, obj, subRoom) {
|
|
474
579
|
obj = structuredClone(obj);
|
|
475
580
|
if (subRoom.interceptorPacket) {
|
|
@@ -511,33 +616,44 @@ var Server = class {
|
|
|
511
616
|
async garbageCollector(options) {
|
|
512
617
|
const subRoom = await this.getSubRoom();
|
|
513
618
|
if (!subRoom) return;
|
|
619
|
+
const SESSION_EXPIRY_TIME = Number(options.sessionExpiryTime);
|
|
620
|
+
if (!Number.isFinite(SESSION_EXPIRY_TIME)) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
514
623
|
const activeConnections = [...this.room.getConnections()];
|
|
515
|
-
const activePrivateIds = new Set(activeConnections.map((conn) => conn
|
|
624
|
+
const activePrivateIds = new Set(activeConnections.map((conn) => this.getPrivateId(conn)));
|
|
516
625
|
try {
|
|
517
|
-
const sessions = await this.
|
|
626
|
+
const sessions = await this.listStorage(SESSION_PREFIX);
|
|
518
627
|
const users = this.getUsersProperty(subRoom);
|
|
519
628
|
const usersPropName = this.getUsersPropName(subRoom);
|
|
629
|
+
const metrics = subRoom.$storageMetrics;
|
|
630
|
+
if (metrics) {
|
|
631
|
+
metrics.sessionGcRuns += 1;
|
|
632
|
+
metrics.sessionGcScanned += sessions.size;
|
|
633
|
+
}
|
|
520
634
|
const validPublicIds = /* @__PURE__ */ new Set();
|
|
521
635
|
const expiredPublicIds = /* @__PURE__ */ new Set();
|
|
522
|
-
const SESSION_EXPIRY_TIME = options.sessionExpiryTime;
|
|
523
636
|
const now = Date.now();
|
|
524
637
|
for (const [key, session] of sessions) {
|
|
525
|
-
if (!key.startsWith(
|
|
526
|
-
const privateId = key.
|
|
638
|
+
if (!key.startsWith(SESSION_PREFIX)) continue;
|
|
639
|
+
const privateId = key.slice(SESSION_PREFIX.length);
|
|
527
640
|
const typedSession = session;
|
|
528
|
-
if (!activePrivateIds.has(privateId) && !typedSession.connected && now - typedSession.
|
|
641
|
+
if (!activePrivateIds.has(privateId) && !typedSession.connected && typedSession.disconnectedAt !== void 0 && now - typedSession.disconnectedAt >= SESSION_EXPIRY_TIME) {
|
|
529
642
|
await this.deleteSession(privateId);
|
|
530
643
|
expiredPublicIds.add(typedSession.publicId);
|
|
644
|
+
if (metrics) {
|
|
645
|
+
metrics.sessionGcExpired += 1;
|
|
646
|
+
}
|
|
531
647
|
} else if (typedSession && typedSession.publicId) {
|
|
532
648
|
validPublicIds.add(typedSession.publicId);
|
|
533
649
|
}
|
|
534
650
|
}
|
|
651
|
+
await this.repairSessionPublicIndexes(sessions, metrics);
|
|
535
652
|
if (users && usersPropName) {
|
|
536
653
|
const currentUsers = users();
|
|
537
654
|
for (const publicId in currentUsers) {
|
|
538
655
|
if (expiredPublicIds.has(publicId) && !validPublicIds.has(publicId)) {
|
|
539
656
|
delete currentUsers[publicId];
|
|
540
|
-
await this.room.storage.delete(`${usersPropName}.${publicId}`);
|
|
541
657
|
}
|
|
542
658
|
}
|
|
543
659
|
}
|
|
@@ -545,6 +661,145 @@ var Server = class {
|
|
|
545
661
|
console.error("Error in garbage collector:", error);
|
|
546
662
|
}
|
|
547
663
|
}
|
|
664
|
+
scheduleSessionGarbageCollector(sessionExpiryTime, privateId) {
|
|
665
|
+
const normalizedSessionExpiryTime = Number(sessionExpiryTime);
|
|
666
|
+
if (!Number.isFinite(normalizedSessionExpiryTime) || normalizedSessionExpiryTime < 0) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
setTimeout(() => {
|
|
670
|
+
if (privateId) {
|
|
671
|
+
void this.expireDisconnectedSession(privateId, normalizedSessionExpiryTime);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
void this.garbageCollector({ sessionExpiryTime: normalizedSessionExpiryTime });
|
|
675
|
+
}, normalizedSessionExpiryTime);
|
|
676
|
+
}
|
|
677
|
+
getSessionExpiryTime(subRoom) {
|
|
678
|
+
return subRoom?.sessionExpiryTime ?? subRoom?.constructor?.prototype?.sessionExpiryTime ?? subRoom?.constructor?.sessionExpiryTime;
|
|
679
|
+
}
|
|
680
|
+
async shouldRunSessionGarbageCollector(sessionExpiryTime) {
|
|
681
|
+
const normalizedSessionExpiryTime = Number(sessionExpiryTime);
|
|
682
|
+
if (!Number.isFinite(normalizedSessionExpiryTime) || normalizedSessionExpiryTime < 0) {
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
const now = Date.now();
|
|
686
|
+
const lastRun = await this.room.storage.get(SESSION_GC_LAST_RUN_KEY);
|
|
687
|
+
if (lastRun && now - lastRun < normalizedSessionExpiryTime) {
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
await this.room.storage.put(SESSION_GC_LAST_RUN_KEY, now);
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
async shouldRunInterval(key, interval) {
|
|
694
|
+
const normalizedInterval = Number(interval);
|
|
695
|
+
if (!Number.isFinite(normalizedInterval) || normalizedInterval < 0) {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
const now = Date.now();
|
|
699
|
+
const lastRun = await this.room.storage.get(key);
|
|
700
|
+
if (lastRun && now - lastRun < normalizedInterval) {
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
await this.room.storage.put(key, now);
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
async repairSessionPublicIndexes(sessions, metrics) {
|
|
707
|
+
const sessionEntries = sessions ?? await this.listStorage(SESSION_PREFIX);
|
|
708
|
+
const expected = /* @__PURE__ */ new Map();
|
|
709
|
+
for (const [key, session] of sessionEntries) {
|
|
710
|
+
if (!session?.publicId) continue;
|
|
711
|
+
const privateId = key.slice(SESSION_PREFIX.length);
|
|
712
|
+
const privateIds = expected.get(session.publicId) ?? [];
|
|
713
|
+
privateIds.push(privateId);
|
|
714
|
+
expected.set(session.publicId, privateIds);
|
|
715
|
+
}
|
|
716
|
+
const publicIndexes = await this.listStorage(SESSION_PUBLIC_PREFIX);
|
|
717
|
+
const writes = {};
|
|
718
|
+
const deletes = [];
|
|
719
|
+
let repairs = 0;
|
|
720
|
+
for (const [publicId, privateIds] of expected) {
|
|
721
|
+
privateIds.sort();
|
|
722
|
+
const key = this.sessionPublicKey(publicId);
|
|
723
|
+
const existing = publicIndexes.get(key) ?? [];
|
|
724
|
+
const normalizedExisting = Array.isArray(existing) ? [...existing].sort() : [];
|
|
725
|
+
if (JSON.stringify(normalizedExisting) !== JSON.stringify(privateIds)) {
|
|
726
|
+
writes[key] = privateIds;
|
|
727
|
+
repairs += 1;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
for (const [key] of publicIndexes) {
|
|
731
|
+
const publicId = key.slice(SESSION_PUBLIC_PREFIX.length);
|
|
732
|
+
if (!expected.has(publicId)) {
|
|
733
|
+
deletes.push(key);
|
|
734
|
+
repairs += 1;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
await Promise.all([
|
|
738
|
+
this.putStorageEntries(writes),
|
|
739
|
+
this.deleteStorageKeys(deletes)
|
|
740
|
+
]);
|
|
741
|
+
if (metrics) {
|
|
742
|
+
metrics.sessionIndexRepairs += repairs;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
isTransferExpired(transfer, transferExpiryTime) {
|
|
746
|
+
if (!transfer) return true;
|
|
747
|
+
if (!Number.isFinite(transferExpiryTime) || transferExpiryTime < 0) {
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
const created = Number(transfer.created);
|
|
751
|
+
if (!Number.isFinite(created)) {
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
return Date.now() - created >= transferExpiryTime;
|
|
755
|
+
}
|
|
756
|
+
async cleanupExpiredTransfers(transferExpiryTime, metrics) {
|
|
757
|
+
if (!Number.isFinite(transferExpiryTime) || transferExpiryTime < 0) {
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
const transfers = await this.listStorage(TRANSFER_PREFIX);
|
|
761
|
+
const expiredKeys = [];
|
|
762
|
+
for (const [key, transfer] of transfers) {
|
|
763
|
+
if (this.isTransferExpired(transfer, transferExpiryTime)) {
|
|
764
|
+
expiredKeys.push(key);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
await this.deleteStorageKeys(expiredKeys);
|
|
768
|
+
if (metrics) {
|
|
769
|
+
metrics.transferGcRuns += 1;
|
|
770
|
+
metrics.transferGcScanned += transfers.size;
|
|
771
|
+
metrics.transferGcExpired += expiredKeys.length;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
async expireDisconnectedSession(privateId, sessionExpiryTime) {
|
|
775
|
+
const session = await this.getSession(privateId);
|
|
776
|
+
if (!session || session.connected || session.disconnectedAt === void 0) {
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
if (this.hasActiveSessionConnection(privateId)) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const elapsed = Date.now() - session.disconnectedAt;
|
|
783
|
+
if (elapsed < sessionExpiryTime) {
|
|
784
|
+
setTimeout(() => {
|
|
785
|
+
void this.expireDisconnectedSession(privateId, sessionExpiryTime);
|
|
786
|
+
}, sessionExpiryTime - elapsed);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
await this.deleteSession(privateId);
|
|
790
|
+
const privateIds = await this.getSessionPrivateIds(session.publicId);
|
|
791
|
+
for (const otherPrivateId of privateIds) {
|
|
792
|
+
const otherSession = await this.getSession(otherPrivateId);
|
|
793
|
+
if (otherSession?.publicId === session.publicId) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
const subRoom = await this.getSubRoom();
|
|
798
|
+
const users = this.getUsersProperty(subRoom);
|
|
799
|
+
if (users?.()[session.publicId]) {
|
|
800
|
+
delete users()[session.publicId];
|
|
801
|
+
}
|
|
802
|
+
}
|
|
548
803
|
/**
|
|
549
804
|
* @method createRoom
|
|
550
805
|
* @private
|
|
@@ -576,21 +831,55 @@ var Server = class {
|
|
|
576
831
|
return null;
|
|
577
832
|
}
|
|
578
833
|
const loadMemory = async () => {
|
|
579
|
-
const
|
|
580
|
-
const
|
|
834
|
+
const startedAt = Date.now();
|
|
835
|
+
const metrics = instance.$storageMetrics;
|
|
836
|
+
const root = await this.loadStatePath(".");
|
|
837
|
+
const memory = await this.listStorage(STATE_PREFIX);
|
|
838
|
+
metrics.loadStateKeys = memory.size;
|
|
581
839
|
const tmpObject = root || {};
|
|
582
|
-
for (let [
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
}
|
|
586
|
-
if (key == ".") {
|
|
840
|
+
for (let [storageKey, value] of memory) {
|
|
841
|
+
const key = storageKey.slice(STATE_PREFIX.length);
|
|
842
|
+
if (key === ".") {
|
|
587
843
|
continue;
|
|
588
844
|
}
|
|
589
845
|
dset2(tmpObject, key, value);
|
|
590
846
|
}
|
|
847
|
+
if (root === void 0 && memory.size === 0) {
|
|
848
|
+
const legacyRoot = await this.room.storage.get(".");
|
|
849
|
+
const legacyMemory = await this.room.storage.list();
|
|
850
|
+
const legacyObject = legacyRoot || {};
|
|
851
|
+
const migratedEntries = [];
|
|
852
|
+
const legacyDeleteKeys = [];
|
|
853
|
+
if (legacyRoot !== void 0) {
|
|
854
|
+
migratedEntries.push([".", legacyRoot]);
|
|
855
|
+
legacyDeleteKeys.push(".");
|
|
856
|
+
}
|
|
857
|
+
for (let [key, value] of legacyMemory) {
|
|
858
|
+
if (key === "." || this.isInternalStorageKey(key)) {
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
dset2(legacyObject, key, value);
|
|
862
|
+
migratedEntries.push([key, value]);
|
|
863
|
+
legacyDeleteKeys.push(key);
|
|
864
|
+
}
|
|
865
|
+
metrics.loadLegacyKeys = migratedEntries.length;
|
|
866
|
+
await this.putStorageEntries(
|
|
867
|
+
Object.fromEntries(
|
|
868
|
+
migratedEntries.map(([path, value]) => [this.stateKey(path), value])
|
|
869
|
+
)
|
|
870
|
+
);
|
|
871
|
+
if (legacyRoot !== void 0) {
|
|
872
|
+
await this.deleteStorageKeys(legacyDeleteKeys);
|
|
873
|
+
}
|
|
874
|
+
load(instance, legacyObject, true);
|
|
875
|
+
metrics.loadMs = Date.now() - startedAt;
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
591
878
|
load(instance, tmpObject, true);
|
|
879
|
+
metrics.loadMs = Date.now() - startedAt;
|
|
592
880
|
};
|
|
593
881
|
instance.$memoryAll = {};
|
|
882
|
+
instance.$storageMetrics = this.createStorageMetrics();
|
|
594
883
|
instance.$autoSync = instance["autoSync"] !== false;
|
|
595
884
|
instance.$pendingSync = /* @__PURE__ */ new Map();
|
|
596
885
|
instance.$pendingInitialSync = /* @__PURE__ */ new Map();
|
|
@@ -644,16 +933,9 @@ var Server = class {
|
|
|
644
933
|
console.error(`[sessionTransfer] User with publicId ${publicId} not found.`);
|
|
645
934
|
return null;
|
|
646
935
|
}
|
|
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
|
-
}
|
|
936
|
+
const sessionEntry = await this.getSessionEntryByPublicId(publicId);
|
|
937
|
+
const userSession = sessionEntry?.session;
|
|
938
|
+
const privateId = sessionEntry?.privateId ?? null;
|
|
657
939
|
if (!userSession || !privateId) {
|
|
658
940
|
console.error(`[sessionTransfer] Session for publicId ${publicId} not found.`);
|
|
659
941
|
return null;
|
|
@@ -719,20 +1001,79 @@ var Server = class {
|
|
|
719
1001
|
values.clear();
|
|
720
1002
|
return;
|
|
721
1003
|
}
|
|
1004
|
+
const startedAt = Date.now();
|
|
1005
|
+
const stateWrites = {};
|
|
1006
|
+
const deleteTasks = [];
|
|
722
1007
|
for (let [path, value] of values) {
|
|
723
1008
|
const _instance = path == "." ? instance : getByPath(instance, path);
|
|
724
|
-
const itemValue = createStatesSnapshot(_instance);
|
|
725
1009
|
if (value == DELETE_TOKEN) {
|
|
726
|
-
|
|
1010
|
+
deleteTasks.push(this.deleteStatePath(path));
|
|
727
1011
|
} else {
|
|
728
|
-
|
|
1012
|
+
const itemValue = _instance?.$snapshot ? createStatesSnapshot(_instance) : value;
|
|
1013
|
+
stateWrites[this.stateKey(path)] = itemValue;
|
|
729
1014
|
}
|
|
730
1015
|
}
|
|
1016
|
+
await Promise.all([
|
|
1017
|
+
this.putStorageEntries(stateWrites),
|
|
1018
|
+
...deleteTasks
|
|
1019
|
+
]);
|
|
1020
|
+
const metrics = instance.$storageMetrics;
|
|
1021
|
+
if (metrics) {
|
|
1022
|
+
metrics.persistFlushes += 1;
|
|
1023
|
+
metrics.persistWrites += Object.keys(stateWrites).length;
|
|
1024
|
+
metrics.persistDeletes += deleteTasks.length;
|
|
1025
|
+
metrics.persistLastFlushMs = Date.now() - startedAt;
|
|
1026
|
+
}
|
|
731
1027
|
values.clear();
|
|
732
1028
|
};
|
|
1029
|
+
const debouncePersist = (wait) => {
|
|
1030
|
+
let timeout = null;
|
|
1031
|
+
let flushing = false;
|
|
1032
|
+
const pending = /* @__PURE__ */ new Map();
|
|
1033
|
+
const schedule = () => {
|
|
1034
|
+
if (timeout) {
|
|
1035
|
+
clearTimeout(timeout);
|
|
1036
|
+
}
|
|
1037
|
+
timeout = setTimeout(() => {
|
|
1038
|
+
void flush();
|
|
1039
|
+
}, wait);
|
|
1040
|
+
};
|
|
1041
|
+
const flush = async () => {
|
|
1042
|
+
timeout = null;
|
|
1043
|
+
if (flushing) {
|
|
1044
|
+
schedule();
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
const values = new Map(pending);
|
|
1048
|
+
pending.clear();
|
|
1049
|
+
if (!values.size) {
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
flushing = true;
|
|
1053
|
+
try {
|
|
1054
|
+
await persistCb(values);
|
|
1055
|
+
} finally {
|
|
1056
|
+
flushing = false;
|
|
1057
|
+
if (pending.size) {
|
|
1058
|
+
schedule();
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
return (values) => {
|
|
1063
|
+
if (initPersist) {
|
|
1064
|
+
values.clear();
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
for (const [path, value] of values) {
|
|
1068
|
+
pending.set(path, value);
|
|
1069
|
+
}
|
|
1070
|
+
values.clear();
|
|
1071
|
+
schedule();
|
|
1072
|
+
};
|
|
1073
|
+
};
|
|
733
1074
|
syncClass(instance, {
|
|
734
1075
|
onSync: instance["throttleSync"] ? throttle(syncCb, instance["throttleSync"]) : syncCb,
|
|
735
|
-
onPersist: instance["throttleStorage"] ?
|
|
1076
|
+
onPersist: instance["throttleStorage"] ? debouncePersist(instance["throttleStorage"]) : persistCb
|
|
736
1077
|
});
|
|
737
1078
|
await loadMemory();
|
|
738
1079
|
initPersist = false;
|
|
@@ -839,24 +1180,93 @@ var Server = class {
|
|
|
839
1180
|
async getSession(privateId) {
|
|
840
1181
|
if (!privateId) return null;
|
|
841
1182
|
try {
|
|
842
|
-
const session = await this.room.storage.get(
|
|
1183
|
+
const session = await this.room.storage.get(this.sessionKey(privateId));
|
|
843
1184
|
return session;
|
|
844
1185
|
} catch (e) {
|
|
845
1186
|
return null;
|
|
846
1187
|
}
|
|
847
1188
|
}
|
|
1189
|
+
async getSessionPrivateIds(publicId) {
|
|
1190
|
+
if (!publicId) return [];
|
|
1191
|
+
const privateIds = await this.room.storage.get(this.sessionPublicKey(publicId));
|
|
1192
|
+
return Array.isArray(privateIds) ? privateIds : [];
|
|
1193
|
+
}
|
|
1194
|
+
async saveSessionPrivateIds(publicId, privateIds) {
|
|
1195
|
+
const key = this.sessionPublicKey(publicId);
|
|
1196
|
+
if (privateIds.length === 0) {
|
|
1197
|
+
await this.room.storage.delete(key);
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
await this.room.storage.put(key, privateIds);
|
|
1201
|
+
}
|
|
1202
|
+
async addSessionToPublicIndex(privateId, publicId) {
|
|
1203
|
+
const privateIds = await this.getSessionPrivateIds(publicId);
|
|
1204
|
+
if (privateIds.includes(privateId)) {
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
await this.saveSessionPrivateIds(publicId, [...privateIds, privateId]);
|
|
1208
|
+
}
|
|
1209
|
+
async removeSessionFromPublicIndex(privateId, publicId) {
|
|
1210
|
+
const privateIds = await this.getSessionPrivateIds(publicId);
|
|
1211
|
+
await this.saveSessionPrivateIds(
|
|
1212
|
+
publicId,
|
|
1213
|
+
privateIds.filter((id2) => id2 !== privateId)
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1216
|
+
async getSessionEntryByPublicId(publicId) {
|
|
1217
|
+
const indexedPrivateIds = await this.getSessionPrivateIds(publicId);
|
|
1218
|
+
const stalePrivateIds = [];
|
|
1219
|
+
for (const privateId of indexedPrivateIds) {
|
|
1220
|
+
const session = await this.getSession(privateId);
|
|
1221
|
+
if (session?.publicId === publicId) {
|
|
1222
|
+
return { privateId, session };
|
|
1223
|
+
}
|
|
1224
|
+
stalePrivateIds.push(privateId);
|
|
1225
|
+
}
|
|
1226
|
+
if (stalePrivateIds.length) {
|
|
1227
|
+
await this.saveSessionPrivateIds(
|
|
1228
|
+
publicId,
|
|
1229
|
+
indexedPrivateIds.filter((id2) => !stalePrivateIds.includes(id2))
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
const sessions = await this.listStorage(SESSION_PREFIX);
|
|
1233
|
+
for (const [key, session] of sessions) {
|
|
1234
|
+
const privateId = key.slice(SESSION_PREFIX.length);
|
|
1235
|
+
if (session?.publicId) {
|
|
1236
|
+
await this.addSessionToPublicIndex(privateId, session.publicId);
|
|
1237
|
+
}
|
|
1238
|
+
if (session?.publicId === publicId) {
|
|
1239
|
+
return { privateId, session };
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return null;
|
|
1243
|
+
}
|
|
848
1244
|
async saveSession(privateId, data) {
|
|
1245
|
+
const existingSession = await this.getSession(privateId);
|
|
849
1246
|
const sessionData = {
|
|
850
1247
|
...data,
|
|
851
1248
|
created: data.created || Date.now(),
|
|
852
1249
|
connected: data.connected !== void 0 ? data.connected : true
|
|
853
1250
|
};
|
|
854
|
-
await this.room.storage.put(
|
|
1251
|
+
await this.room.storage.put(this.sessionKey(privateId), sessionData);
|
|
1252
|
+
if (existingSession?.publicId && existingSession.publicId !== sessionData.publicId) {
|
|
1253
|
+
await this.removeSessionFromPublicIndex(privateId, existingSession.publicId);
|
|
1254
|
+
}
|
|
1255
|
+
await this.addSessionToPublicIndex(privateId, sessionData.publicId);
|
|
855
1256
|
}
|
|
856
1257
|
async updateSessionConnection(privateId, connected) {
|
|
857
1258
|
const session = await this.getSession(privateId);
|
|
858
1259
|
if (session) {
|
|
859
|
-
|
|
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);
|
|
860
1270
|
}
|
|
861
1271
|
}
|
|
862
1272
|
/**
|
|
@@ -871,7 +1281,11 @@ var Server = class {
|
|
|
871
1281
|
* ```
|
|
872
1282
|
*/
|
|
873
1283
|
async deleteSession(privateId) {
|
|
874
|
-
await this.
|
|
1284
|
+
const session = await this.getSession(privateId);
|
|
1285
|
+
await this.room.storage.delete(this.sessionKey(privateId));
|
|
1286
|
+
if (session?.publicId) {
|
|
1287
|
+
await this.removeSessionFromPublicIndex(privateId, session.publicId);
|
|
1288
|
+
}
|
|
875
1289
|
}
|
|
876
1290
|
async onConnectClient(conn, ctx) {
|
|
877
1291
|
const subRoom = await this.getSubRoom({
|
|
@@ -881,8 +1295,17 @@ var Server = class {
|
|
|
881
1295
|
conn.close();
|
|
882
1296
|
return;
|
|
883
1297
|
}
|
|
884
|
-
const sessionExpiryTime = subRoom
|
|
885
|
-
await this.
|
|
1298
|
+
const sessionExpiryTime = this.getSessionExpiryTime(subRoom);
|
|
1299
|
+
if (await this.shouldRunSessionGarbageCollector(sessionExpiryTime)) {
|
|
1300
|
+
await this.garbageCollector({ sessionExpiryTime });
|
|
1301
|
+
}
|
|
1302
|
+
const transferExpiryTime = this.getTransferExpiryTime(subRoom);
|
|
1303
|
+
if (await this.shouldRunInterval(TRANSFER_GC_LAST_RUN_KEY, transferExpiryTime)) {
|
|
1304
|
+
await this.cleanupExpiredTransfers(
|
|
1305
|
+
transferExpiryTime,
|
|
1306
|
+
subRoom.$storageMetrics
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
886
1309
|
const roomGuards = subRoom.constructor["_roomGuards"] || [];
|
|
887
1310
|
for (const guard of roomGuards) {
|
|
888
1311
|
const isAuthorized = await guard(conn, ctx, this.room);
|
|
@@ -898,12 +1321,18 @@ var Server = class {
|
|
|
898
1321
|
}
|
|
899
1322
|
let transferData = null;
|
|
900
1323
|
if (transferToken) {
|
|
901
|
-
|
|
1324
|
+
const transferKey = this.transferKey(transferToken);
|
|
1325
|
+
transferData = await this.room.storage.get(transferKey);
|
|
902
1326
|
if (transferData) {
|
|
903
|
-
|
|
1327
|
+
if (this.isTransferExpired(transferData, transferExpiryTime)) {
|
|
1328
|
+
transferData = null;
|
|
1329
|
+
}
|
|
1330
|
+
await this.room.storage.delete(transferKey);
|
|
904
1331
|
}
|
|
905
1332
|
}
|
|
906
|
-
const
|
|
1333
|
+
const requestedPrivateId = this.getPrivateId(conn);
|
|
1334
|
+
const privateId = transferData?.privateId || requestedPrivateId;
|
|
1335
|
+
const existingSession = await this.getSession(privateId);
|
|
907
1336
|
const publicId = existingSession?.publicId || transferData?.publicId || generateShortUUID2();
|
|
908
1337
|
let user = null;
|
|
909
1338
|
const signal2 = this.getUsersProperty(subRoom);
|
|
@@ -917,24 +1346,24 @@ var Server = class {
|
|
|
917
1346
|
user = isClass(classType) ? new classType() : classType(conn, ctx);
|
|
918
1347
|
signal2()[publicId] = user;
|
|
919
1348
|
const snapshot = createStatesSnapshotDeep(user);
|
|
920
|
-
this.
|
|
1349
|
+
await this.saveStatePath(`${usersPropName}.${publicId}`, snapshot);
|
|
921
1350
|
}
|
|
922
1351
|
} else {
|
|
923
1352
|
user = signal2()[existingSession.publicId];
|
|
924
1353
|
}
|
|
925
1354
|
if (!existingSession) {
|
|
926
|
-
|
|
927
|
-
await this.saveSession(sessionPrivateId, {
|
|
1355
|
+
await this.saveSession(privateId, {
|
|
928
1356
|
publicId
|
|
929
1357
|
});
|
|
930
1358
|
} else {
|
|
931
|
-
await this.updateSessionConnection(
|
|
1359
|
+
await this.updateSessionConnection(privateId, true);
|
|
932
1360
|
}
|
|
933
1361
|
}
|
|
934
1362
|
this.updateUserConnectionStatus(user, true);
|
|
935
1363
|
conn.setState({
|
|
936
1364
|
...conn.state,
|
|
937
|
-
publicId
|
|
1365
|
+
publicId,
|
|
1366
|
+
privateId
|
|
938
1367
|
});
|
|
939
1368
|
await awaitReturn(subRoom["onJoin"]?.(user, conn, ctx));
|
|
940
1369
|
if (subRoom.$autoSync) {
|
|
@@ -967,11 +1396,19 @@ var Server = class {
|
|
|
967
1396
|
*/
|
|
968
1397
|
async onConnect(conn, ctx) {
|
|
969
1398
|
if (ctx.request?.headers.has("x-shard-id")) {
|
|
1399
|
+
if (!this.isAuthorizedShardRequest(ctx.request)) {
|
|
1400
|
+
conn.close();
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
970
1403
|
this.onConnectShard(conn, ctx);
|
|
971
1404
|
} else {
|
|
972
1405
|
await this.onConnectClient(conn, ctx);
|
|
973
1406
|
}
|
|
974
1407
|
}
|
|
1408
|
+
isAuthorizedShardRequest(req) {
|
|
1409
|
+
const shardSecret = this.room.env.SHARD_SECRET;
|
|
1410
|
+
return typeof shardSecret === "string" && shardSecret.length > 0 && req?.headers.get("x-access-shard") === shardSecret;
|
|
1411
|
+
}
|
|
975
1412
|
/**
|
|
976
1413
|
* @method onConnectShard
|
|
977
1414
|
* @private
|
|
@@ -982,9 +1419,11 @@ var Server = class {
|
|
|
982
1419
|
*/
|
|
983
1420
|
onConnectShard(conn, ctx) {
|
|
984
1421
|
const shardId = ctx.request?.headers.get("x-shard-id") || "unknown-shard";
|
|
1422
|
+
const worldId = ctx.request?.headers.get("x-shard-world-id") || "world-default";
|
|
985
1423
|
conn.setState({
|
|
986
1424
|
shard: true,
|
|
987
1425
|
shardId,
|
|
1426
|
+
worldId,
|
|
988
1427
|
clients: /* @__PURE__ */ new Map()
|
|
989
1428
|
// Track clients connected through this shard
|
|
990
1429
|
});
|
|
@@ -1254,11 +1693,15 @@ var Server = class {
|
|
|
1254
1693
|
if (!conn.state) {
|
|
1255
1694
|
return;
|
|
1256
1695
|
}
|
|
1257
|
-
const privateId = conn
|
|
1696
|
+
const privateId = this.getPrivateId(conn);
|
|
1258
1697
|
const { publicId } = conn.state;
|
|
1259
1698
|
const user = signal2?.()[publicId];
|
|
1260
1699
|
if (!user) return;
|
|
1700
|
+
if (this.hasActiveSessionConnection(privateId)) {
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1261
1703
|
await this.updateSessionConnection(privateId, false);
|
|
1704
|
+
this.scheduleSessionGarbageCollector(this.getSessionExpiryTime(subRoom), privateId);
|
|
1262
1705
|
const connectionUpdated = this.updateUserConnectionStatus(user, false);
|
|
1263
1706
|
await awaitReturn(subRoom["onLeave"]?.(user, conn));
|
|
1264
1707
|
if (!connectionUpdated) {
|
|
@@ -1293,6 +1736,9 @@ var Server = class {
|
|
|
1293
1736
|
return res.status(200).send({});
|
|
1294
1737
|
}
|
|
1295
1738
|
if (isFromShard) {
|
|
1739
|
+
if (!this.isAuthorizedShardRequest(req)) {
|
|
1740
|
+
return res.unauthorized("Invalid shard credentials");
|
|
1741
|
+
}
|
|
1296
1742
|
return this.handleShardRequest(req, res, shardId);
|
|
1297
1743
|
}
|
|
1298
1744
|
return this.handleDirectRequest(req, res);
|
|
@@ -1342,14 +1788,15 @@ var Server = class {
|
|
|
1342
1788
|
) ?? userSnapshot;
|
|
1343
1789
|
signal2()[publicId] = user;
|
|
1344
1790
|
load(user, hydratedSnapshot, true);
|
|
1345
|
-
await this.
|
|
1791
|
+
await this.saveStatePath(`${usersPropName}.${publicId}`, userSnapshot);
|
|
1346
1792
|
}
|
|
1347
1793
|
}
|
|
1348
1794
|
const transferToken = generateShortUUID2();
|
|
1349
|
-
await this.room.storage.put(
|
|
1795
|
+
await this.room.storage.put(this.transferKey(transferToken), {
|
|
1350
1796
|
privateId,
|
|
1351
1797
|
publicId,
|
|
1352
|
-
restored: true
|
|
1798
|
+
restored: true,
|
|
1799
|
+
created: Date.now()
|
|
1353
1800
|
});
|
|
1354
1801
|
return res.success({ transferToken });
|
|
1355
1802
|
} catch (error) {
|
|
@@ -1553,17 +2000,33 @@ var Server = class {
|
|
|
1553
2000
|
|
|
1554
2001
|
// src/shard.ts
|
|
1555
2002
|
var Shard = class {
|
|
1556
|
-
constructor(room) {
|
|
2003
|
+
constructor(room, options = {}) {
|
|
1557
2004
|
this.room = room;
|
|
1558
2005
|
this.connectionMap = /* @__PURE__ */ new Map();
|
|
1559
2006
|
this.worldUrl = null;
|
|
1560
|
-
this.worldId = "default";
|
|
1561
2007
|
this.lastReportedConnections = 0;
|
|
1562
2008
|
this.statsInterval = 3e4;
|
|
1563
2009
|
this.statsIntervalId = null;
|
|
2010
|
+
this.worldUrl = options.worldUrl ?? null;
|
|
2011
|
+
this.worldId = options.worldId ?? this.getWorldIdFromShardId(room.id) ?? this.getEnvString("WORLD_ID") ?? this.getEnvString("SIGNE_WORLD_ID") ?? "world-default";
|
|
2012
|
+
this.statsInterval = options.statsInterval ?? this.statsInterval;
|
|
2013
|
+
}
|
|
2014
|
+
getPrivateId(conn) {
|
|
2015
|
+
return conn.sessionId || conn.id;
|
|
2016
|
+
}
|
|
2017
|
+
getEnvString(key) {
|
|
2018
|
+
const value = this.room.env?.[key];
|
|
2019
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2020
|
+
}
|
|
2021
|
+
getRoomIdFromShardId(shardId) {
|
|
2022
|
+
return shardId.split(":")[0];
|
|
2023
|
+
}
|
|
2024
|
+
getWorldIdFromShardId(shardId) {
|
|
2025
|
+
const parts = shardId.split(":");
|
|
2026
|
+
return parts.length >= 3 ? parts[1] : void 0;
|
|
1564
2027
|
}
|
|
1565
2028
|
async onStart() {
|
|
1566
|
-
const roomId = this.room.id
|
|
2029
|
+
const roomId = this.getRoomIdFromShardId(this.room.id);
|
|
1567
2030
|
const roomStub = this.room.context.parties.main.get(roomId);
|
|
1568
2031
|
if (!roomStub) {
|
|
1569
2032
|
console.warn("No room room stub found in main party context");
|
|
@@ -1572,17 +2035,30 @@ var Shard = class {
|
|
|
1572
2035
|
this.mainServerStub = roomStub;
|
|
1573
2036
|
this.ws = await roomStub.socket({
|
|
1574
2037
|
headers: {
|
|
1575
|
-
"x-shard-id": this.room.id
|
|
2038
|
+
"x-shard-id": this.room.id,
|
|
2039
|
+
"x-shard-world-id": this.worldId,
|
|
2040
|
+
"x-access-shard": this.room.env.SHARD_SECRET
|
|
1576
2041
|
}
|
|
1577
2042
|
});
|
|
1578
2043
|
this.ws.addEventListener("message", (event) => {
|
|
1579
2044
|
try {
|
|
1580
2045
|
const message = JSON.parse(event.data);
|
|
2046
|
+
if (message.type === "shard.closeClient" && message.privateId) {
|
|
2047
|
+
const clientConnections = this.connectionMap.get(message.privateId);
|
|
2048
|
+
if (clientConnections?.size) {
|
|
2049
|
+
for (const clientConn of [...clientConnections]) {
|
|
2050
|
+
clientConn.close();
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
1581
2055
|
if (message.targetClientId) {
|
|
1582
|
-
const
|
|
1583
|
-
if (
|
|
2056
|
+
const clientConnections = this.connectionMap.get(message.targetClientId);
|
|
2057
|
+
if (clientConnections?.size) {
|
|
1584
2058
|
delete message.targetClientId;
|
|
1585
|
-
clientConn
|
|
2059
|
+
for (const clientConn of clientConnections) {
|
|
2060
|
+
clientConn.send(message.data);
|
|
2061
|
+
}
|
|
1586
2062
|
}
|
|
1587
2063
|
} else {
|
|
1588
2064
|
this.room.broadcast(event.data);
|
|
@@ -1591,21 +2067,22 @@ var Shard = class {
|
|
|
1591
2067
|
console.error("Error processing message from main server:", error);
|
|
1592
2068
|
}
|
|
1593
2069
|
});
|
|
1594
|
-
await this.updateWorldStats();
|
|
2070
|
+
await this.updateWorldStats(true);
|
|
1595
2071
|
this.startPeriodicStatsUpdates();
|
|
1596
2072
|
}
|
|
1597
2073
|
startPeriodicStatsUpdates() {
|
|
1598
|
-
if (!this.
|
|
2074
|
+
if (this.statsInterval <= 0 || !this.room.context.parties.world) {
|
|
1599
2075
|
return;
|
|
1600
2076
|
}
|
|
1601
2077
|
if (this.statsIntervalId) {
|
|
1602
2078
|
clearInterval(this.statsIntervalId);
|
|
1603
2079
|
}
|
|
1604
2080
|
this.statsIntervalId = setInterval(() => {
|
|
1605
|
-
this.updateWorldStats().catch((error) => {
|
|
2081
|
+
this.updateWorldStats(true).catch((error) => {
|
|
1606
2082
|
console.error("Error in periodic stats update:", error);
|
|
1607
2083
|
});
|
|
1608
2084
|
}, this.statsInterval);
|
|
2085
|
+
this.statsIntervalId?.unref?.();
|
|
1609
2086
|
}
|
|
1610
2087
|
stopPeriodicStatsUpdates() {
|
|
1611
2088
|
if (this.statsIntervalId) {
|
|
@@ -1614,7 +2091,10 @@ var Shard = class {
|
|
|
1614
2091
|
}
|
|
1615
2092
|
}
|
|
1616
2093
|
onConnect(conn, ctx) {
|
|
1617
|
-
this.
|
|
2094
|
+
const privateId = this.getPrivateId(conn);
|
|
2095
|
+
const connections = this.connectionMap.get(privateId) ?? /* @__PURE__ */ new Set();
|
|
2096
|
+
connections.add(conn);
|
|
2097
|
+
this.connectionMap.set(privateId, connections);
|
|
1618
2098
|
const headers = {};
|
|
1619
2099
|
if (ctx.request?.headers) {
|
|
1620
2100
|
ctx.request.headers.forEach((value, key) => {
|
|
@@ -1628,7 +2108,7 @@ var Shard = class {
|
|
|
1628
2108
|
} : null;
|
|
1629
2109
|
this.ws.send(JSON.stringify({
|
|
1630
2110
|
type: "shard.clientConnected",
|
|
1631
|
-
privateId
|
|
2111
|
+
privateId,
|
|
1632
2112
|
requestInfo
|
|
1633
2113
|
}));
|
|
1634
2114
|
this.updateWorldStats();
|
|
@@ -1638,7 +2118,7 @@ var Shard = class {
|
|
|
1638
2118
|
const parsedMessage = typeof message === "string" ? JSON.parse(message) : message;
|
|
1639
2119
|
const wrappedMessage = JSON.stringify({
|
|
1640
2120
|
type: "shard.clientMessage",
|
|
1641
|
-
privateId: sender
|
|
2121
|
+
privateId: this.getPrivateId(sender),
|
|
1642
2122
|
publicId: sender.state?.publicId,
|
|
1643
2123
|
payload: parsedMessage
|
|
1644
2124
|
});
|
|
@@ -1648,21 +2128,35 @@ var Shard = class {
|
|
|
1648
2128
|
}
|
|
1649
2129
|
}
|
|
1650
2130
|
onClose(conn) {
|
|
1651
|
-
this.
|
|
2131
|
+
const privateId = this.getPrivateId(conn);
|
|
2132
|
+
const connections = this.connectionMap.get(privateId);
|
|
2133
|
+
connections?.delete(conn);
|
|
2134
|
+
if (connections?.size) {
|
|
2135
|
+
this.updateWorldStats();
|
|
2136
|
+
return;
|
|
2137
|
+
}
|
|
2138
|
+
this.connectionMap.delete(privateId);
|
|
1652
2139
|
this.ws.send(JSON.stringify({
|
|
1653
2140
|
type: "shard.clientDisconnected",
|
|
1654
|
-
privateId
|
|
2141
|
+
privateId,
|
|
1655
2142
|
publicId: conn.state?.publicId
|
|
1656
2143
|
}));
|
|
1657
2144
|
this.updateWorldStats();
|
|
1658
2145
|
}
|
|
1659
|
-
async updateWorldStats() {
|
|
1660
|
-
const currentConnections = this.connectionMap.size;
|
|
1661
|
-
if (currentConnections === this.lastReportedConnections) {
|
|
2146
|
+
async updateWorldStats(force = false) {
|
|
2147
|
+
const currentConnections = Array.from(this.connectionMap.values()).reduce((total, connections) => total + connections.size, 0);
|
|
2148
|
+
if (!force && currentConnections === this.lastReportedConnections) {
|
|
1662
2149
|
return true;
|
|
1663
2150
|
}
|
|
1664
2151
|
try {
|
|
1665
|
-
const
|
|
2152
|
+
const worldParty = this.room.context.parties.world;
|
|
2153
|
+
if (!worldParty) {
|
|
2154
|
+
return false;
|
|
2155
|
+
}
|
|
2156
|
+
const worldRoom = worldParty.get(this.worldId);
|
|
2157
|
+
if (!worldRoom?.fetch) {
|
|
2158
|
+
return false;
|
|
2159
|
+
}
|
|
1666
2160
|
const response2 = await worldRoom.fetch("/update-shard", {
|
|
1667
2161
|
method: "POST",
|
|
1668
2162
|
headers: {
|
|
@@ -1671,6 +2165,7 @@ var Shard = class {
|
|
|
1671
2165
|
},
|
|
1672
2166
|
body: JSON.stringify({
|
|
1673
2167
|
shardId: this.room.id,
|
|
2168
|
+
worldId: this.worldId,
|
|
1674
2169
|
connections: currentConnections
|
|
1675
2170
|
})
|
|
1676
2171
|
});
|
|
@@ -1710,7 +2205,9 @@ var Shard = class {
|
|
|
1710
2205
|
headers.set(key, value);
|
|
1711
2206
|
});
|
|
1712
2207
|
headers.set("x-shard-id", this.room.id);
|
|
2208
|
+
headers.set("x-shard-world-id", this.worldId);
|
|
1713
2209
|
headers.set("x-forwarded-by-shard", "true");
|
|
2210
|
+
headers.set("x-access-shard", this.room.env.SHARD_SECRET);
|
|
1714
2211
|
const clientIp = req.headers.get("x-forwarded-for") || "unknown";
|
|
1715
2212
|
if (clientIp) {
|
|
1716
2213
|
headers.set("x-original-client-ip", clientIp);
|
|
@@ -1720,7 +2217,7 @@ var Shard = class {
|
|
|
1720
2217
|
headers,
|
|
1721
2218
|
body
|
|
1722
2219
|
};
|
|
1723
|
-
const response2 = await this.mainServerStub.fetch(path, requestInit);
|
|
2220
|
+
const response2 = await this.mainServerStub.fetch(path + url.search, requestInit);
|
|
1724
2221
|
return response2;
|
|
1725
2222
|
} catch (error) {
|
|
1726
2223
|
return response(500, { error: "Error forwarding request" });
|
|
@@ -1732,7 +2229,7 @@ var Shard = class {
|
|
|
1732
2229
|
* @description Executed periodically, used to perform maintenance tasks
|
|
1733
2230
|
*/
|
|
1734
2231
|
async onAlarm() {
|
|
1735
|
-
await this.updateWorldStats();
|
|
2232
|
+
await this.updateWorldStats(true);
|
|
1736
2233
|
}
|
|
1737
2234
|
};
|
|
1738
2235
|
|
|
@@ -1744,7 +2241,7 @@ async function testRoom(Room2, options = {}) {
|
|
|
1744
2241
|
return server2;
|
|
1745
2242
|
};
|
|
1746
2243
|
const isShard = options.shard || false;
|
|
1747
|
-
const io = new ServerIo(Room2.path, isShard ? {
|
|
2244
|
+
const io = new ServerIo(options.id ?? Room2.path, isShard ? {
|
|
1748
2245
|
parties: {
|
|
1749
2246
|
game: createServer,
|
|
1750
2247
|
...options.parties || {}
|
|
@@ -1788,7 +2285,7 @@ async function testRoom(Room2, options = {}) {
|
|
|
1788
2285
|
return client;
|
|
1789
2286
|
},
|
|
1790
2287
|
getServerUser: async (client, prop = "users") => {
|
|
1791
|
-
const privateId = client.conn.id;
|
|
2288
|
+
const privateId = client.conn.sessionId || client.conn.id;
|
|
1792
2289
|
const session = await server.getSession(privateId);
|
|
1793
2290
|
return server.subRoom[prop]()[session?.publicId];
|
|
1794
2291
|
}
|
|
@@ -1808,11 +2305,11 @@ function tick(ms = 0) {
|
|
|
1808
2305
|
|
|
1809
2306
|
// src/mock.ts
|
|
1810
2307
|
var MockPartyClient = class {
|
|
1811
|
-
constructor(server,
|
|
2308
|
+
constructor(server, sessionId) {
|
|
1812
2309
|
this.server = server;
|
|
1813
2310
|
this.events = /* @__PURE__ */ new Map();
|
|
1814
|
-
this.id =
|
|
1815
|
-
this.conn = new MockConnection(this);
|
|
2311
|
+
this.id = generateShortUUID();
|
|
2312
|
+
this.conn = new MockConnection(this, sessionId || this.id);
|
|
1816
2313
|
}
|
|
1817
2314
|
addEventListener(event, cb) {
|
|
1818
2315
|
if (!this.events.has(event)) {
|
|
@@ -1848,8 +2345,8 @@ var MockLobby = class {
|
|
|
1848
2345
|
this.server = server;
|
|
1849
2346
|
this.lobbyId = lobbyId;
|
|
1850
2347
|
}
|
|
1851
|
-
socket(
|
|
1852
|
-
return new MockPartyClient(this.server);
|
|
2348
|
+
socket(init) {
|
|
2349
|
+
return new MockPartyClient(this.server, init?.id);
|
|
1853
2350
|
}
|
|
1854
2351
|
async connection(idOrOptions, maybeOptions) {
|
|
1855
2352
|
const id2 = typeof idOrOptions === "string" ? idOrOptions : idOrOptions?.id;
|
|
@@ -1925,7 +2422,13 @@ var MockPartyRoom = class {
|
|
|
1925
2422
|
});
|
|
1926
2423
|
}
|
|
1927
2424
|
getConnection(id2) {
|
|
1928
|
-
|
|
2425
|
+
let connection;
|
|
2426
|
+
for (const client of this.clients.values()) {
|
|
2427
|
+
if (client.conn.id === id2 || client.conn.sessionId === id2) {
|
|
2428
|
+
connection = client.conn;
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
return connection;
|
|
1929
2432
|
}
|
|
1930
2433
|
getConnections() {
|
|
1931
2434
|
return Array.from(this.clients.values()).map((client) => client.conn);
|
|
@@ -1933,13 +2436,25 @@ var MockPartyRoom = class {
|
|
|
1933
2436
|
clear() {
|
|
1934
2437
|
this.clients.clear();
|
|
1935
2438
|
}
|
|
2439
|
+
deleteConnection(id2, connection) {
|
|
2440
|
+
if (connection) {
|
|
2441
|
+
this.clients.delete(connection.id);
|
|
2442
|
+
return;
|
|
2443
|
+
}
|
|
2444
|
+
for (const [connectionKey, client] of this.clients) {
|
|
2445
|
+
if (client.conn.id === id2 || client.conn.sessionId === id2) {
|
|
2446
|
+
this.clients.delete(connectionKey);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
1936
2450
|
};
|
|
1937
2451
|
var MockConnection = class {
|
|
1938
|
-
constructor(client) {
|
|
2452
|
+
constructor(client, sessionId) {
|
|
1939
2453
|
this.client = client;
|
|
1940
2454
|
this.state = {};
|
|
1941
2455
|
this.server = client.server;
|
|
1942
2456
|
this.id = client.id;
|
|
2457
|
+
this.sessionId = sessionId;
|
|
1943
2458
|
}
|
|
1944
2459
|
setState(value) {
|
|
1945
2460
|
this.state = value;
|
|
@@ -1948,6 +2463,7 @@ var MockConnection = class {
|
|
|
1948
2463
|
this.client._trigger("message", data);
|
|
1949
2464
|
}
|
|
1950
2465
|
close() {
|
|
2466
|
+
this.server.room.deleteConnection?.(this.id, this);
|
|
1951
2467
|
this.server.onClose(this);
|
|
1952
2468
|
}
|
|
1953
2469
|
};
|
|
@@ -2127,7 +2643,7 @@ var guardManageWorld = async (_, req, room) => {
|
|
|
2127
2643
|
return true;
|
|
2128
2644
|
}
|
|
2129
2645
|
const url = new URL(req.url);
|
|
2130
|
-
const token = req
|
|
2646
|
+
const token = getAuthToken(req, url);
|
|
2131
2647
|
if (!token) {
|
|
2132
2648
|
return false;
|
|
2133
2649
|
}
|
|
@@ -2137,11 +2653,28 @@ var guardManageWorld = async (_, req, room) => {
|
|
|
2137
2653
|
if (!payload) {
|
|
2138
2654
|
return false;
|
|
2139
2655
|
}
|
|
2656
|
+
if (!canAccessWorld(payload, room.id)) {
|
|
2657
|
+
return false;
|
|
2658
|
+
}
|
|
2140
2659
|
} catch (error) {
|
|
2141
2660
|
return false;
|
|
2142
2661
|
}
|
|
2143
2662
|
return true;
|
|
2144
2663
|
};
|
|
2664
|
+
function getAuthToken(req, url) {
|
|
2665
|
+
const authorization = req.headers.get("Authorization");
|
|
2666
|
+
if (authorization?.startsWith("Bearer ")) {
|
|
2667
|
+
return authorization.slice("Bearer ".length).trim();
|
|
2668
|
+
}
|
|
2669
|
+
return authorization ?? url.searchParams.get("world-auth-token");
|
|
2670
|
+
}
|
|
2671
|
+
function canAccessWorld(payload, worldId) {
|
|
2672
|
+
const worlds = payload.worlds;
|
|
2673
|
+
if (!Array.isArray(worlds)) {
|
|
2674
|
+
return false;
|
|
2675
|
+
}
|
|
2676
|
+
return worlds.some((world) => world === "*" || world === worldId);
|
|
2677
|
+
}
|
|
2145
2678
|
|
|
2146
2679
|
// src/world.ts
|
|
2147
2680
|
var MAX_PLAYERS_PER_SHARD = 75;
|
|
@@ -2156,10 +2689,13 @@ var RoomConfigSchema = z2.object({
|
|
|
2156
2689
|
var RegisterShardSchema = z2.object({
|
|
2157
2690
|
shardId: z2.string(),
|
|
2158
2691
|
roomId: z2.string(),
|
|
2692
|
+
worldId: z2.string().optional(),
|
|
2159
2693
|
url: z2.string().url(),
|
|
2160
2694
|
maxConnections: z2.number().int().positive()
|
|
2161
2695
|
});
|
|
2162
2696
|
var UpdateShardStatsSchema = z2.object({
|
|
2697
|
+
shardId: z2.string(),
|
|
2698
|
+
worldId: z2.string().optional(),
|
|
2163
2699
|
connections: z2.number().int().min(0),
|
|
2164
2700
|
status: z2.enum(["active", "maintenance", "draining"]).optional()
|
|
2165
2701
|
});
|
|
@@ -2205,6 +2741,7 @@ __decorateClass([
|
|
|
2205
2741
|
var ShardInfo = class {
|
|
2206
2742
|
constructor() {
|
|
2207
2743
|
this.roomId = signal("");
|
|
2744
|
+
this.worldId = signal("");
|
|
2208
2745
|
this.url = signal("");
|
|
2209
2746
|
this.currentConnections = signal(0);
|
|
2210
2747
|
this.maxConnections = signal(MAX_PLAYERS_PER_SHARD);
|
|
@@ -2218,6 +2755,9 @@ __decorateClass([
|
|
|
2218
2755
|
__decorateClass([
|
|
2219
2756
|
sync()
|
|
2220
2757
|
], ShardInfo.prototype, "roomId", 2);
|
|
2758
|
+
__decorateClass([
|
|
2759
|
+
sync()
|
|
2760
|
+
], ShardInfo.prototype, "worldId", 2);
|
|
2221
2761
|
__decorateClass([
|
|
2222
2762
|
sync()
|
|
2223
2763
|
], ShardInfo.prototype, "url", 2);
|
|
@@ -2251,6 +2791,7 @@ var WorldRoom = class {
|
|
|
2251
2791
|
if (!SHARD_SECRET) {
|
|
2252
2792
|
throw new Error("SHARD_SECRET env variable is not set");
|
|
2253
2793
|
}
|
|
2794
|
+
this.scheduleInactiveShardCleanup();
|
|
2254
2795
|
}
|
|
2255
2796
|
async onJoin(user, conn, ctx) {
|
|
2256
2797
|
const canConnect = await guardManageWorld(user, ctx.request, this.room);
|
|
@@ -2266,6 +2807,13 @@ var WorldRoom = class {
|
|
|
2266
2807
|
return obj;
|
|
2267
2808
|
}
|
|
2268
2809
|
// Helper methods
|
|
2810
|
+
getWorldId() {
|
|
2811
|
+
return this.room.id;
|
|
2812
|
+
}
|
|
2813
|
+
scheduleInactiveShardCleanup() {
|
|
2814
|
+
const timeoutId = setTimeout(() => this.cleanupInactiveShards(), 6e4);
|
|
2815
|
+
timeoutId?.unref?.();
|
|
2816
|
+
}
|
|
2269
2817
|
cleanupInactiveShards() {
|
|
2270
2818
|
const now = Date.now();
|
|
2271
2819
|
const timeout = 5 * 60 * 1e3;
|
|
@@ -2277,10 +2825,25 @@ var WorldRoom = class {
|
|
|
2277
2825
|
hasChanges = true;
|
|
2278
2826
|
}
|
|
2279
2827
|
});
|
|
2280
|
-
|
|
2828
|
+
this.scheduleInactiveShardCleanup();
|
|
2281
2829
|
}
|
|
2282
|
-
|
|
2283
|
-
|
|
2830
|
+
removeShard(shardId) {
|
|
2831
|
+
delete this.shards()[shardId];
|
|
2832
|
+
}
|
|
2833
|
+
shouldCompleteDrain(shard) {
|
|
2834
|
+
return shard.status() === "draining" && shard.currentConnections() === 0;
|
|
2835
|
+
}
|
|
2836
|
+
async registerRoom(req, res) {
|
|
2837
|
+
const parseResult = RoomConfigSchema.safeParse(await req.json());
|
|
2838
|
+
if (!parseResult.success) {
|
|
2839
|
+
return res?.badRequest("Invalid room configuration", {
|
|
2840
|
+
details: parseResult.error
|
|
2841
|
+
});
|
|
2842
|
+
}
|
|
2843
|
+
const roomConfig = parseResult.data;
|
|
2844
|
+
if (roomConfig.maxShards !== void 0 && roomConfig.minShards > roomConfig.maxShards) {
|
|
2845
|
+
return res?.badRequest("minShards cannot be greater than maxShards");
|
|
2846
|
+
}
|
|
2284
2847
|
const roomId = roomConfig.name;
|
|
2285
2848
|
if (!this.rooms()[roomId]) {
|
|
2286
2849
|
const newRoom = new RoomConfig();
|
|
@@ -2305,22 +2868,41 @@ var WorldRoom = class {
|
|
|
2305
2868
|
room.minShards.set(roomConfig.minShards);
|
|
2306
2869
|
room.maxShards.set(roomConfig.maxShards);
|
|
2307
2870
|
}
|
|
2871
|
+
await this.ensureMinShards(roomId);
|
|
2308
2872
|
}
|
|
2309
2873
|
async updateShardStats(req, res) {
|
|
2310
|
-
const
|
|
2874
|
+
const parseResult = UpdateShardStatsSchema.safeParse(await req.json());
|
|
2875
|
+
if (!parseResult.success) {
|
|
2876
|
+
return res.badRequest("Invalid shard stats", {
|
|
2877
|
+
details: parseResult.error
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
const body = parseResult.data;
|
|
2311
2881
|
const { shardId, connections, status } = body;
|
|
2312
2882
|
const shard = this.shards()[shardId];
|
|
2313
2883
|
if (!shard) {
|
|
2314
2884
|
return res.notFound(`Shard ${shardId} not found`);
|
|
2315
2885
|
}
|
|
2886
|
+
if (body.worldId && body.worldId !== this.getWorldId()) {
|
|
2887
|
+
return res.badRequest(`Shard ${shardId} belongs to world ${body.worldId}, not ${this.getWorldId()}`);
|
|
2888
|
+
}
|
|
2316
2889
|
shard.currentConnections.set(connections);
|
|
2317
2890
|
if (status) {
|
|
2318
2891
|
shard.status.set(status);
|
|
2319
2892
|
}
|
|
2320
2893
|
shard.lastHeartbeat.set(Date.now());
|
|
2894
|
+
if (this.shouldCompleteDrain(shard)) {
|
|
2895
|
+
this.removeShard(shard.id);
|
|
2896
|
+
}
|
|
2321
2897
|
}
|
|
2322
2898
|
async scaleRoom(req, res) {
|
|
2323
|
-
const
|
|
2899
|
+
const parseResult = ScaleRoomSchema.safeParse(await req.json());
|
|
2900
|
+
if (!parseResult.success) {
|
|
2901
|
+
return res.badRequest("Invalid scale room request", {
|
|
2902
|
+
details: parseResult.error
|
|
2903
|
+
});
|
|
2904
|
+
}
|
|
2905
|
+
const data = parseResult.data;
|
|
2324
2906
|
const { targetShardCount, shardTemplate, roomId } = data;
|
|
2325
2907
|
const room = this.rooms()[roomId];
|
|
2326
2908
|
if (!room) {
|
|
@@ -2335,16 +2917,16 @@ var WorldRoom = class {
|
|
|
2335
2917
|
});
|
|
2336
2918
|
}
|
|
2337
2919
|
if (targetShardCount < previousShardCount) {
|
|
2338
|
-
const
|
|
2920
|
+
const shardsToDrain = [...roomShards].sort((a, b) => {
|
|
2339
2921
|
if (a.status() === "draining" && b.status() !== "draining") return -1;
|
|
2340
2922
|
if (a.status() !== "draining" && b.status() === "draining") return 1;
|
|
2341
2923
|
return a.currentConnections() - b.currentConnections();
|
|
2342
2924
|
}).slice(0, previousShardCount - targetShardCount);
|
|
2343
|
-
const
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2925
|
+
for (const shard of shardsToDrain) {
|
|
2926
|
+
shard.status.set("draining");
|
|
2927
|
+
if (this.shouldCompleteDrain(shard)) {
|
|
2928
|
+
this.removeShard(shard.id);
|
|
2929
|
+
}
|
|
2348
2930
|
}
|
|
2349
2931
|
return;
|
|
2350
2932
|
}
|
|
@@ -2431,8 +3013,20 @@ var WorldRoom = class {
|
|
|
2431
3013
|
return { error: `No shards available for room ${roomId}` };
|
|
2432
3014
|
}
|
|
2433
3015
|
}
|
|
2434
|
-
|
|
3016
|
+
let activeShards = this.getAvailableShards(roomShards);
|
|
2435
3017
|
if (activeShards.length === 0) {
|
|
3018
|
+
if (autoCreate && this.canCreateShard(room, roomShards.length)) {
|
|
3019
|
+
const newShard = await this.createShard(roomId);
|
|
3020
|
+
if (newShard) {
|
|
3021
|
+
return {
|
|
3022
|
+
shardId: newShard.id,
|
|
3023
|
+
url: newShard.url()
|
|
3024
|
+
};
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
if (roomShards.some((shard) => shard.status() === "active")) {
|
|
3028
|
+
return { error: `No shard capacity available for room ${roomId}` };
|
|
3029
|
+
}
|
|
2436
3030
|
return { error: `No active shards available for room ${roomId}` };
|
|
2437
3031
|
}
|
|
2438
3032
|
const balancingStrategy = room.balancingStrategy();
|
|
@@ -2460,6 +3054,14 @@ var WorldRoom = class {
|
|
|
2460
3054
|
url: selectedShard.url()
|
|
2461
3055
|
};
|
|
2462
3056
|
}
|
|
3057
|
+
getAvailableShards(shards) {
|
|
3058
|
+
return shards.filter(
|
|
3059
|
+
(shard) => shard && shard.status() === "active" && shard.currentConnections() < shard.maxConnections()
|
|
3060
|
+
);
|
|
3061
|
+
}
|
|
3062
|
+
canCreateShard(room, currentShardCount) {
|
|
3063
|
+
return room.maxShards() === void 0 || currentShardCount < room.maxShards();
|
|
3064
|
+
}
|
|
2463
3065
|
// Private methods
|
|
2464
3066
|
async createShard(roomId, urlTemplate, maxConnections) {
|
|
2465
3067
|
const room = this.rooms()[roomId];
|
|
@@ -2467,13 +3069,15 @@ var WorldRoom = class {
|
|
|
2467
3069
|
console.error(`Cannot create shard for non-existent room: ${roomId}`);
|
|
2468
3070
|
return null;
|
|
2469
3071
|
}
|
|
2470
|
-
const
|
|
3072
|
+
const worldId = this.getWorldId();
|
|
3073
|
+
const shardId = `${roomId}:${worldId}:${Date.now()}-${Math.floor(Math.random() * 1e4)}`;
|
|
2471
3074
|
const template = urlTemplate || this.defaultShardUrlTemplate();
|
|
2472
3075
|
const url = template.replace("{shardId}", shardId).replace("{roomId}", roomId);
|
|
2473
3076
|
const max = maxConnections || room.maxPlayersPerShard();
|
|
2474
3077
|
const newShard = new ShardInfo();
|
|
2475
3078
|
newShard.id = shardId;
|
|
2476
3079
|
newShard.roomId.set(roomId);
|
|
3080
|
+
newShard.worldId.set(worldId);
|
|
2477
3081
|
newShard.url.set(url);
|
|
2478
3082
|
newShard.maxConnections.set(max);
|
|
2479
3083
|
newShard.currentConnections.set(0);
|
|
@@ -2482,6 +3086,20 @@ var WorldRoom = class {
|
|
|
2482
3086
|
this.shards()[shardId] = newShard;
|
|
2483
3087
|
return newShard;
|
|
2484
3088
|
}
|
|
3089
|
+
async ensureMinShards(roomId) {
|
|
3090
|
+
const room = this.rooms()[roomId];
|
|
3091
|
+
if (!room) {
|
|
3092
|
+
return;
|
|
3093
|
+
}
|
|
3094
|
+
const currentShardCount = Object.values(this.shards()).filter((shard) => shard.roomId() === roomId).length;
|
|
3095
|
+
const targetShardCount = room.minShards();
|
|
3096
|
+
if (currentShardCount >= targetShardCount) {
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
for (let i = currentShardCount; i < targetShardCount; i++) {
|
|
3100
|
+
await this.createShard(roomId);
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
2485
3103
|
};
|
|
2486
3104
|
__decorateClass([
|
|
2487
3105
|
sync(RoomConfig)
|
|
@@ -2532,13 +3150,16 @@ WorldRoom = __decorateClass([
|
|
|
2532
3150
|
], WorldRoom);
|
|
2533
3151
|
|
|
2534
3152
|
// src/session.guard.ts
|
|
3153
|
+
function getPrivateId(sender) {
|
|
3154
|
+
return sender.sessionId || sender.id;
|
|
3155
|
+
}
|
|
2535
3156
|
function createRequireSessionGuard(storage) {
|
|
2536
3157
|
return async (sender, value) => {
|
|
2537
3158
|
if (!sender || !sender.id) {
|
|
2538
3159
|
return false;
|
|
2539
3160
|
}
|
|
2540
3161
|
try {
|
|
2541
|
-
const session = await storage.get(`session:${sender
|
|
3162
|
+
const session = await storage.get(`session:${getPrivateId(sender)}`);
|
|
2542
3163
|
if (!session) {
|
|
2543
3164
|
return false;
|
|
2544
3165
|
}
|
|
@@ -2558,7 +3179,7 @@ var requireSession = async (sender, value, room) => {
|
|
|
2558
3179
|
return false;
|
|
2559
3180
|
}
|
|
2560
3181
|
try {
|
|
2561
|
-
const session = await room.storage.get(`session:${sender
|
|
3182
|
+
const session = await room.storage.get(`session:${getPrivateId(sender)}`);
|
|
2562
3183
|
if (!session) {
|
|
2563
3184
|
return false;
|
|
2564
3185
|
}
|