@signe/room 2.9.4 → 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 +13 -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 +65 -188
- package/dist/index.js +742 -146
- 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 +377 -11
- package/src/cloudflare/index.ts +474 -0
- package/src/mock.ts +29 -7
- package/src/node/index.ts +1112 -0
- package/src/server.ts +626 -90
- 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 +170 -79
- 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) {
|
|
@@ -76,11 +69,10 @@ function Guard(guards) {
|
|
|
76
69
|
// ../sync/src/utils.ts
|
|
77
70
|
function generateShortUUID() {
|
|
78
71
|
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
79
|
-
const randomBytes = typeof globalThis.crypto?.getRandomValues === "function" ? globalThis.crypto.getRandomValues(new Uint8Array(8)) : null;
|
|
80
72
|
let uuid = "";
|
|
81
73
|
for (let i = 0; i < 8; i++) {
|
|
82
|
-
const
|
|
83
|
-
uuid += chars[
|
|
74
|
+
const randomIndex = Math.floor(Math.random() * chars.length);
|
|
75
|
+
uuid += chars[randomIndex];
|
|
84
76
|
}
|
|
85
77
|
return uuid;
|
|
86
78
|
}
|
|
@@ -91,16 +83,38 @@ var Storage = class {
|
|
|
91
83
|
this.memory = /* @__PURE__ */ new Map();
|
|
92
84
|
}
|
|
93
85
|
async put(key, value) {
|
|
94
|
-
|
|
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
|
+
}
|
|
95
93
|
}
|
|
96
94
|
async get(key) {
|
|
97
95
|
return this.memory.get(key);
|
|
98
96
|
}
|
|
99
97
|
async delete(key) {
|
|
100
|
-
|
|
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);
|
|
101
108
|
}
|
|
102
|
-
async list() {
|
|
103
|
-
|
|
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
|
+
);
|
|
104
118
|
}
|
|
105
119
|
};
|
|
106
120
|
|
|
@@ -439,6 +453,14 @@ var Message = z.object({
|
|
|
439
453
|
action: z.string(),
|
|
440
454
|
value: z.any()
|
|
441
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;
|
|
442
464
|
var Server = class {
|
|
443
465
|
/**
|
|
444
466
|
* @constructor
|
|
@@ -471,6 +493,88 @@ var Server = class {
|
|
|
471
493
|
get roomStorage() {
|
|
472
494
|
return this.room.storage;
|
|
473
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
|
+
}
|
|
474
578
|
async send(conn, obj, subRoom) {
|
|
475
579
|
obj = structuredClone(obj);
|
|
476
580
|
if (subRoom.interceptorPacket) {
|
|
@@ -512,33 +616,44 @@ var Server = class {
|
|
|
512
616
|
async garbageCollector(options) {
|
|
513
617
|
const subRoom = await this.getSubRoom();
|
|
514
618
|
if (!subRoom) return;
|
|
619
|
+
const SESSION_EXPIRY_TIME = Number(options.sessionExpiryTime);
|
|
620
|
+
if (!Number.isFinite(SESSION_EXPIRY_TIME)) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
515
623
|
const activeConnections = [...this.room.getConnections()];
|
|
516
|
-
const activePrivateIds = new Set(activeConnections.map((conn) => conn
|
|
624
|
+
const activePrivateIds = new Set(activeConnections.map((conn) => this.getPrivateId(conn)));
|
|
517
625
|
try {
|
|
518
|
-
const sessions = await this.
|
|
626
|
+
const sessions = await this.listStorage(SESSION_PREFIX);
|
|
519
627
|
const users = this.getUsersProperty(subRoom);
|
|
520
628
|
const usersPropName = this.getUsersPropName(subRoom);
|
|
629
|
+
const metrics = subRoom.$storageMetrics;
|
|
630
|
+
if (metrics) {
|
|
631
|
+
metrics.sessionGcRuns += 1;
|
|
632
|
+
metrics.sessionGcScanned += sessions.size;
|
|
633
|
+
}
|
|
521
634
|
const validPublicIds = /* @__PURE__ */ new Set();
|
|
522
635
|
const expiredPublicIds = /* @__PURE__ */ new Set();
|
|
523
|
-
const SESSION_EXPIRY_TIME = options.sessionExpiryTime;
|
|
524
636
|
const now = Date.now();
|
|
525
637
|
for (const [key, session] of sessions) {
|
|
526
|
-
if (!key.startsWith(
|
|
527
|
-
const privateId = key.
|
|
638
|
+
if (!key.startsWith(SESSION_PREFIX)) continue;
|
|
639
|
+
const privateId = key.slice(SESSION_PREFIX.length);
|
|
528
640
|
const typedSession = session;
|
|
529
|
-
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) {
|
|
530
642
|
await this.deleteSession(privateId);
|
|
531
643
|
expiredPublicIds.add(typedSession.publicId);
|
|
644
|
+
if (metrics) {
|
|
645
|
+
metrics.sessionGcExpired += 1;
|
|
646
|
+
}
|
|
532
647
|
} else if (typedSession && typedSession.publicId) {
|
|
533
648
|
validPublicIds.add(typedSession.publicId);
|
|
534
649
|
}
|
|
535
650
|
}
|
|
651
|
+
await this.repairSessionPublicIndexes(sessions, metrics);
|
|
536
652
|
if (users && usersPropName) {
|
|
537
653
|
const currentUsers = users();
|
|
538
654
|
for (const publicId in currentUsers) {
|
|
539
655
|
if (expiredPublicIds.has(publicId) && !validPublicIds.has(publicId)) {
|
|
540
656
|
delete currentUsers[publicId];
|
|
541
|
-
await this.room.storage.delete(`${usersPropName}.${publicId}`);
|
|
542
657
|
}
|
|
543
658
|
}
|
|
544
659
|
}
|
|
@@ -546,6 +661,145 @@ var Server = class {
|
|
|
546
661
|
console.error("Error in garbage collector:", error);
|
|
547
662
|
}
|
|
548
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
|
+
}
|
|
549
803
|
/**
|
|
550
804
|
* @method createRoom
|
|
551
805
|
* @private
|
|
@@ -577,21 +831,55 @@ var Server = class {
|
|
|
577
831
|
return null;
|
|
578
832
|
}
|
|
579
833
|
const loadMemory = async () => {
|
|
580
|
-
const
|
|
581
|
-
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;
|
|
582
839
|
const tmpObject = root || {};
|
|
583
|
-
for (let [
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
}
|
|
587
|
-
if (key == ".") {
|
|
840
|
+
for (let [storageKey, value] of memory) {
|
|
841
|
+
const key = storageKey.slice(STATE_PREFIX.length);
|
|
842
|
+
if (key === ".") {
|
|
588
843
|
continue;
|
|
589
844
|
}
|
|
590
845
|
dset2(tmpObject, key, value);
|
|
591
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
|
+
}
|
|
592
878
|
load(instance, tmpObject, true);
|
|
879
|
+
metrics.loadMs = Date.now() - startedAt;
|
|
593
880
|
};
|
|
594
881
|
instance.$memoryAll = {};
|
|
882
|
+
instance.$storageMetrics = this.createStorageMetrics();
|
|
595
883
|
instance.$autoSync = instance["autoSync"] !== false;
|
|
596
884
|
instance.$pendingSync = /* @__PURE__ */ new Map();
|
|
597
885
|
instance.$pendingInitialSync = /* @__PURE__ */ new Map();
|
|
@@ -645,16 +933,9 @@ var Server = class {
|
|
|
645
933
|
console.error(`[sessionTransfer] User with publicId ${publicId} not found.`);
|
|
646
934
|
return null;
|
|
647
935
|
}
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
for (const [key, session] of sessions) {
|
|
652
|
-
if (key.startsWith("session:") && session.publicId === publicId) {
|
|
653
|
-
userSession = session;
|
|
654
|
-
privateId = key.replace("session:", "");
|
|
655
|
-
break;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
936
|
+
const sessionEntry = await this.getSessionEntryByPublicId(publicId);
|
|
937
|
+
const userSession = sessionEntry?.session;
|
|
938
|
+
const privateId = sessionEntry?.privateId ?? null;
|
|
658
939
|
if (!userSession || !privateId) {
|
|
659
940
|
console.error(`[sessionTransfer] Session for publicId ${publicId} not found.`);
|
|
660
941
|
return null;
|
|
@@ -720,20 +1001,79 @@ var Server = class {
|
|
|
720
1001
|
values.clear();
|
|
721
1002
|
return;
|
|
722
1003
|
}
|
|
1004
|
+
const startedAt = Date.now();
|
|
1005
|
+
const stateWrites = {};
|
|
1006
|
+
const deleteTasks = [];
|
|
723
1007
|
for (let [path, value] of values) {
|
|
724
1008
|
const _instance = path == "." ? instance : getByPath(instance, path);
|
|
725
|
-
const itemValue = createStatesSnapshot(_instance);
|
|
726
1009
|
if (value == DELETE_TOKEN) {
|
|
727
|
-
|
|
1010
|
+
deleteTasks.push(this.deleteStatePath(path));
|
|
728
1011
|
} else {
|
|
729
|
-
|
|
1012
|
+
const itemValue = _instance?.$snapshot ? createStatesSnapshot(_instance) : value;
|
|
1013
|
+
stateWrites[this.stateKey(path)] = itemValue;
|
|
730
1014
|
}
|
|
731
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
|
+
}
|
|
732
1027
|
values.clear();
|
|
733
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
|
+
};
|
|
734
1074
|
syncClass(instance, {
|
|
735
1075
|
onSync: instance["throttleSync"] ? throttle(syncCb, instance["throttleSync"]) : syncCb,
|
|
736
|
-
onPersist: instance["throttleStorage"] ?
|
|
1076
|
+
onPersist: instance["throttleStorage"] ? debouncePersist(instance["throttleStorage"]) : persistCb
|
|
737
1077
|
});
|
|
738
1078
|
await loadMemory();
|
|
739
1079
|
initPersist = false;
|
|
@@ -840,24 +1180,93 @@ var Server = class {
|
|
|
840
1180
|
async getSession(privateId) {
|
|
841
1181
|
if (!privateId) return null;
|
|
842
1182
|
try {
|
|
843
|
-
const session = await this.room.storage.get(
|
|
1183
|
+
const session = await this.room.storage.get(this.sessionKey(privateId));
|
|
844
1184
|
return session;
|
|
845
1185
|
} catch (e) {
|
|
846
1186
|
return null;
|
|
847
1187
|
}
|
|
848
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
|
+
}
|
|
849
1244
|
async saveSession(privateId, data) {
|
|
1245
|
+
const existingSession = await this.getSession(privateId);
|
|
850
1246
|
const sessionData = {
|
|
851
1247
|
...data,
|
|
852
1248
|
created: data.created || Date.now(),
|
|
853
1249
|
connected: data.connected !== void 0 ? data.connected : true
|
|
854
1250
|
};
|
|
855
|
-
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);
|
|
856
1256
|
}
|
|
857
1257
|
async updateSessionConnection(privateId, connected) {
|
|
858
1258
|
const session = await this.getSession(privateId);
|
|
859
1259
|
if (session) {
|
|
860
|
-
|
|
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);
|
|
861
1270
|
}
|
|
862
1271
|
}
|
|
863
1272
|
/**
|
|
@@ -872,7 +1281,11 @@ var Server = class {
|
|
|
872
1281
|
* ```
|
|
873
1282
|
*/
|
|
874
1283
|
async deleteSession(privateId) {
|
|
875
|
-
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
|
+
}
|
|
876
1289
|
}
|
|
877
1290
|
async onConnectClient(conn, ctx) {
|
|
878
1291
|
const subRoom = await this.getSubRoom({
|
|
@@ -882,8 +1295,17 @@ var Server = class {
|
|
|
882
1295
|
conn.close();
|
|
883
1296
|
return;
|
|
884
1297
|
}
|
|
885
|
-
const sessionExpiryTime =
|
|
886
|
-
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
|
+
}
|
|
887
1309
|
const roomGuards = subRoom.constructor["_roomGuards"] || [];
|
|
888
1310
|
for (const guard of roomGuards) {
|
|
889
1311
|
const isAuthorized = await guard(conn, ctx, this.room);
|
|
@@ -899,12 +1321,18 @@ var Server = class {
|
|
|
899
1321
|
}
|
|
900
1322
|
let transferData = null;
|
|
901
1323
|
if (transferToken) {
|
|
902
|
-
|
|
1324
|
+
const transferKey = this.transferKey(transferToken);
|
|
1325
|
+
transferData = await this.room.storage.get(transferKey);
|
|
903
1326
|
if (transferData) {
|
|
904
|
-
|
|
1327
|
+
if (this.isTransferExpired(transferData, transferExpiryTime)) {
|
|
1328
|
+
transferData = null;
|
|
1329
|
+
}
|
|
1330
|
+
await this.room.storage.delete(transferKey);
|
|
905
1331
|
}
|
|
906
1332
|
}
|
|
907
|
-
const
|
|
1333
|
+
const requestedPrivateId = this.getPrivateId(conn);
|
|
1334
|
+
const privateId = transferData?.privateId || requestedPrivateId;
|
|
1335
|
+
const existingSession = await this.getSession(privateId);
|
|
908
1336
|
const publicId = existingSession?.publicId || transferData?.publicId || generateShortUUID2();
|
|
909
1337
|
let user = null;
|
|
910
1338
|
const signal2 = this.getUsersProperty(subRoom);
|
|
@@ -918,24 +1346,24 @@ var Server = class {
|
|
|
918
1346
|
user = isClass(classType) ? new classType() : classType(conn, ctx);
|
|
919
1347
|
signal2()[publicId] = user;
|
|
920
1348
|
const snapshot = createStatesSnapshotDeep(user);
|
|
921
|
-
this.
|
|
1349
|
+
await this.saveStatePath(`${usersPropName}.${publicId}`, snapshot);
|
|
922
1350
|
}
|
|
923
1351
|
} else {
|
|
924
1352
|
user = signal2()[existingSession.publicId];
|
|
925
1353
|
}
|
|
926
1354
|
if (!existingSession) {
|
|
927
|
-
|
|
928
|
-
await this.saveSession(sessionPrivateId, {
|
|
1355
|
+
await this.saveSession(privateId, {
|
|
929
1356
|
publicId
|
|
930
1357
|
});
|
|
931
1358
|
} else {
|
|
932
|
-
await this.updateSessionConnection(
|
|
1359
|
+
await this.updateSessionConnection(privateId, true);
|
|
933
1360
|
}
|
|
934
1361
|
}
|
|
935
1362
|
this.updateUserConnectionStatus(user, true);
|
|
936
1363
|
conn.setState({
|
|
937
1364
|
...conn.state,
|
|
938
|
-
publicId
|
|
1365
|
+
publicId,
|
|
1366
|
+
privateId
|
|
939
1367
|
});
|
|
940
1368
|
await awaitReturn(subRoom["onJoin"]?.(user, conn, ctx));
|
|
941
1369
|
if (subRoom.$autoSync) {
|
|
@@ -968,11 +1396,19 @@ var Server = class {
|
|
|
968
1396
|
*/
|
|
969
1397
|
async onConnect(conn, ctx) {
|
|
970
1398
|
if (ctx.request?.headers.has("x-shard-id")) {
|
|
1399
|
+
if (!this.isAuthorizedShardRequest(ctx.request)) {
|
|
1400
|
+
conn.close();
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
971
1403
|
this.onConnectShard(conn, ctx);
|
|
972
1404
|
} else {
|
|
973
1405
|
await this.onConnectClient(conn, ctx);
|
|
974
1406
|
}
|
|
975
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
|
+
}
|
|
976
1412
|
/**
|
|
977
1413
|
* @method onConnectShard
|
|
978
1414
|
* @private
|
|
@@ -983,9 +1419,11 @@ var Server = class {
|
|
|
983
1419
|
*/
|
|
984
1420
|
onConnectShard(conn, ctx) {
|
|
985
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";
|
|
986
1423
|
conn.setState({
|
|
987
1424
|
shard: true,
|
|
988
1425
|
shardId,
|
|
1426
|
+
worldId,
|
|
989
1427
|
clients: /* @__PURE__ */ new Map()
|
|
990
1428
|
// Track clients connected through this shard
|
|
991
1429
|
});
|
|
@@ -1255,11 +1693,15 @@ var Server = class {
|
|
|
1255
1693
|
if (!conn.state) {
|
|
1256
1694
|
return;
|
|
1257
1695
|
}
|
|
1258
|
-
const privateId = conn
|
|
1696
|
+
const privateId = this.getPrivateId(conn);
|
|
1259
1697
|
const { publicId } = conn.state;
|
|
1260
1698
|
const user = signal2?.()[publicId];
|
|
1261
1699
|
if (!user) return;
|
|
1700
|
+
if (this.hasActiveSessionConnection(privateId)) {
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1262
1703
|
await this.updateSessionConnection(privateId, false);
|
|
1704
|
+
this.scheduleSessionGarbageCollector(this.getSessionExpiryTime(subRoom), privateId);
|
|
1263
1705
|
const connectionUpdated = this.updateUserConnectionStatus(user, false);
|
|
1264
1706
|
await awaitReturn(subRoom["onLeave"]?.(user, conn));
|
|
1265
1707
|
if (!connectionUpdated) {
|
|
@@ -1294,6 +1736,9 @@ var Server = class {
|
|
|
1294
1736
|
return res.status(200).send({});
|
|
1295
1737
|
}
|
|
1296
1738
|
if (isFromShard) {
|
|
1739
|
+
if (!this.isAuthorizedShardRequest(req)) {
|
|
1740
|
+
return res.unauthorized("Invalid shard credentials");
|
|
1741
|
+
}
|
|
1297
1742
|
return this.handleShardRequest(req, res, shardId);
|
|
1298
1743
|
}
|
|
1299
1744
|
return this.handleDirectRequest(req, res);
|
|
@@ -1343,14 +1788,15 @@ var Server = class {
|
|
|
1343
1788
|
) ?? userSnapshot;
|
|
1344
1789
|
signal2()[publicId] = user;
|
|
1345
1790
|
load(user, hydratedSnapshot, true);
|
|
1346
|
-
await this.
|
|
1791
|
+
await this.saveStatePath(`${usersPropName}.${publicId}`, userSnapshot);
|
|
1347
1792
|
}
|
|
1348
1793
|
}
|
|
1349
1794
|
const transferToken = generateShortUUID2();
|
|
1350
|
-
await this.room.storage.put(
|
|
1795
|
+
await this.room.storage.put(this.transferKey(transferToken), {
|
|
1351
1796
|
privateId,
|
|
1352
1797
|
publicId,
|
|
1353
|
-
restored: true
|
|
1798
|
+
restored: true,
|
|
1799
|
+
created: Date.now()
|
|
1354
1800
|
});
|
|
1355
1801
|
return res.success({ transferToken });
|
|
1356
1802
|
} catch (error) {
|
|
@@ -1404,7 +1850,8 @@ var Server = class {
|
|
|
1404
1850
|
}
|
|
1405
1851
|
const url = new URL(req.url);
|
|
1406
1852
|
const method = req.method;
|
|
1407
|
-
|
|
1853
|
+
let pathname = url.pathname;
|
|
1854
|
+
pathname = "/" + pathname.split("/").slice(4).join("/");
|
|
1408
1855
|
for (const [routeKey, handler] of requestHandlers.entries()) {
|
|
1409
1856
|
const firstColonIndex = routeKey.indexOf(":");
|
|
1410
1857
|
const handlerMethod = routeKey.substring(0, firstColonIndex);
|
|
@@ -1428,17 +1875,16 @@ var Server = class {
|
|
|
1428
1875
|
if (handler.bodyValidation && ["POST", "PUT", "PATCH"].includes(method)) {
|
|
1429
1876
|
try {
|
|
1430
1877
|
const contentType = req.headers.get("content-type") || "";
|
|
1431
|
-
if (
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1878
|
+
if (contentType.includes("application/json")) {
|
|
1879
|
+
const body = await req.json();
|
|
1880
|
+
const validation = handler.bodyValidation.safeParse(body);
|
|
1881
|
+
if (!validation.success) {
|
|
1882
|
+
return res.badRequest("Invalid request body", {
|
|
1883
|
+
details: validation.error
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
bodyData = validation.data;
|
|
1440
1887
|
}
|
|
1441
|
-
bodyData = validation.data;
|
|
1442
1888
|
} catch (error) {
|
|
1443
1889
|
return res.badRequest("Failed to parse request body");
|
|
1444
1890
|
}
|
|
@@ -1470,7 +1916,9 @@ var Server = class {
|
|
|
1470
1916
|
* @returns {boolean} True if the paths match
|
|
1471
1917
|
*/
|
|
1472
1918
|
pathMatches(requestPath, handlerPath) {
|
|
1473
|
-
|
|
1919
|
+
const pathRegexString = handlerPath.replace(/\//g, "\\/").replace(/:([^\/]+)/g, "([^/]+)");
|
|
1920
|
+
const pathRegex = new RegExp(`^${pathRegexString}`);
|
|
1921
|
+
return pathRegex.test(requestPath);
|
|
1474
1922
|
}
|
|
1475
1923
|
/**
|
|
1476
1924
|
* @method extractPathParams
|
|
@@ -1488,7 +1936,8 @@ var Server = class {
|
|
|
1488
1936
|
paramNames.push(segment.substring(1));
|
|
1489
1937
|
}
|
|
1490
1938
|
});
|
|
1491
|
-
const
|
|
1939
|
+
const pathRegexString = handlerPath.replace(/\//g, "\\/").replace(/:([^\/]+)/g, "([^/]+)");
|
|
1940
|
+
const pathRegex = new RegExp(`^${pathRegexString}`);
|
|
1492
1941
|
const matches = requestPath.match(pathRegex);
|
|
1493
1942
|
if (matches && matches.length > 1) {
|
|
1494
1943
|
for (let i = 0; i < paramNames.length; i++) {
|
|
@@ -1497,23 +1946,6 @@ var Server = class {
|
|
|
1497
1946
|
}
|
|
1498
1947
|
return params;
|
|
1499
1948
|
}
|
|
1500
|
-
normalizeRequestPath(pathname) {
|
|
1501
|
-
const parts = pathname.split("/").filter(Boolean);
|
|
1502
|
-
if (parts[0] === "parties" && parts.length >= 3) {
|
|
1503
|
-
const routePath = parts.slice(3).join("/");
|
|
1504
|
-
return routePath ? `/${routePath}` : "/";
|
|
1505
|
-
}
|
|
1506
|
-
return pathname || "/";
|
|
1507
|
-
}
|
|
1508
|
-
pathPatternToRegex(handlerPath) {
|
|
1509
|
-
const segments = handlerPath.split("/").map((segment) => {
|
|
1510
|
-
if (segment.startsWith(":")) {
|
|
1511
|
-
return "([^/]+)";
|
|
1512
|
-
}
|
|
1513
|
-
return segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1514
|
-
});
|
|
1515
|
-
return new RegExp(`^${segments.join("/")}$`);
|
|
1516
|
-
}
|
|
1517
1949
|
/**
|
|
1518
1950
|
* @method handleShardRequest
|
|
1519
1951
|
* @private
|
|
@@ -1568,17 +2000,33 @@ var Server = class {
|
|
|
1568
2000
|
|
|
1569
2001
|
// src/shard.ts
|
|
1570
2002
|
var Shard = class {
|
|
1571
|
-
constructor(room) {
|
|
2003
|
+
constructor(room, options = {}) {
|
|
1572
2004
|
this.room = room;
|
|
1573
2005
|
this.connectionMap = /* @__PURE__ */ new Map();
|
|
1574
2006
|
this.worldUrl = null;
|
|
1575
|
-
this.worldId = "default";
|
|
1576
2007
|
this.lastReportedConnections = 0;
|
|
1577
2008
|
this.statsInterval = 3e4;
|
|
1578
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;
|
|
1579
2027
|
}
|
|
1580
2028
|
async onStart() {
|
|
1581
|
-
const roomId = this.room.id
|
|
2029
|
+
const roomId = this.getRoomIdFromShardId(this.room.id);
|
|
1582
2030
|
const roomStub = this.room.context.parties.main.get(roomId);
|
|
1583
2031
|
if (!roomStub) {
|
|
1584
2032
|
console.warn("No room room stub found in main party context");
|
|
@@ -1587,17 +2035,30 @@ var Shard = class {
|
|
|
1587
2035
|
this.mainServerStub = roomStub;
|
|
1588
2036
|
this.ws = await roomStub.socket({
|
|
1589
2037
|
headers: {
|
|
1590
|
-
"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
|
|
1591
2041
|
}
|
|
1592
2042
|
});
|
|
1593
2043
|
this.ws.addEventListener("message", (event) => {
|
|
1594
2044
|
try {
|
|
1595
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
|
+
}
|
|
1596
2055
|
if (message.targetClientId) {
|
|
1597
|
-
const
|
|
1598
|
-
if (
|
|
2056
|
+
const clientConnections = this.connectionMap.get(message.targetClientId);
|
|
2057
|
+
if (clientConnections?.size) {
|
|
1599
2058
|
delete message.targetClientId;
|
|
1600
|
-
clientConn
|
|
2059
|
+
for (const clientConn of clientConnections) {
|
|
2060
|
+
clientConn.send(message.data);
|
|
2061
|
+
}
|
|
1601
2062
|
}
|
|
1602
2063
|
} else {
|
|
1603
2064
|
this.room.broadcast(event.data);
|
|
@@ -1606,21 +2067,22 @@ var Shard = class {
|
|
|
1606
2067
|
console.error("Error processing message from main server:", error);
|
|
1607
2068
|
}
|
|
1608
2069
|
});
|
|
1609
|
-
await this.updateWorldStats();
|
|
2070
|
+
await this.updateWorldStats(true);
|
|
1610
2071
|
this.startPeriodicStatsUpdates();
|
|
1611
2072
|
}
|
|
1612
2073
|
startPeriodicStatsUpdates() {
|
|
1613
|
-
if (!this.
|
|
2074
|
+
if (this.statsInterval <= 0 || !this.room.context.parties.world) {
|
|
1614
2075
|
return;
|
|
1615
2076
|
}
|
|
1616
2077
|
if (this.statsIntervalId) {
|
|
1617
2078
|
clearInterval(this.statsIntervalId);
|
|
1618
2079
|
}
|
|
1619
2080
|
this.statsIntervalId = setInterval(() => {
|
|
1620
|
-
this.updateWorldStats().catch((error) => {
|
|
2081
|
+
this.updateWorldStats(true).catch((error) => {
|
|
1621
2082
|
console.error("Error in periodic stats update:", error);
|
|
1622
2083
|
});
|
|
1623
2084
|
}, this.statsInterval);
|
|
2085
|
+
this.statsIntervalId?.unref?.();
|
|
1624
2086
|
}
|
|
1625
2087
|
stopPeriodicStatsUpdates() {
|
|
1626
2088
|
if (this.statsIntervalId) {
|
|
@@ -1629,7 +2091,10 @@ var Shard = class {
|
|
|
1629
2091
|
}
|
|
1630
2092
|
}
|
|
1631
2093
|
onConnect(conn, ctx) {
|
|
1632
|
-
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);
|
|
1633
2098
|
const headers = {};
|
|
1634
2099
|
if (ctx.request?.headers) {
|
|
1635
2100
|
ctx.request.headers.forEach((value, key) => {
|
|
@@ -1643,7 +2108,7 @@ var Shard = class {
|
|
|
1643
2108
|
} : null;
|
|
1644
2109
|
this.ws.send(JSON.stringify({
|
|
1645
2110
|
type: "shard.clientConnected",
|
|
1646
|
-
privateId
|
|
2111
|
+
privateId,
|
|
1647
2112
|
requestInfo
|
|
1648
2113
|
}));
|
|
1649
2114
|
this.updateWorldStats();
|
|
@@ -1653,7 +2118,7 @@ var Shard = class {
|
|
|
1653
2118
|
const parsedMessage = typeof message === "string" ? JSON.parse(message) : message;
|
|
1654
2119
|
const wrappedMessage = JSON.stringify({
|
|
1655
2120
|
type: "shard.clientMessage",
|
|
1656
|
-
privateId: sender
|
|
2121
|
+
privateId: this.getPrivateId(sender),
|
|
1657
2122
|
publicId: sender.state?.publicId,
|
|
1658
2123
|
payload: parsedMessage
|
|
1659
2124
|
});
|
|
@@ -1663,21 +2128,35 @@ var Shard = class {
|
|
|
1663
2128
|
}
|
|
1664
2129
|
}
|
|
1665
2130
|
onClose(conn) {
|
|
1666
|
-
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);
|
|
1667
2139
|
this.ws.send(JSON.stringify({
|
|
1668
2140
|
type: "shard.clientDisconnected",
|
|
1669
|
-
privateId
|
|
2141
|
+
privateId,
|
|
1670
2142
|
publicId: conn.state?.publicId
|
|
1671
2143
|
}));
|
|
1672
2144
|
this.updateWorldStats();
|
|
1673
2145
|
}
|
|
1674
|
-
async updateWorldStats() {
|
|
1675
|
-
const currentConnections = this.connectionMap.size;
|
|
1676
|
-
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) {
|
|
1677
2149
|
return true;
|
|
1678
2150
|
}
|
|
1679
2151
|
try {
|
|
1680
|
-
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
|
+
}
|
|
1681
2160
|
const response2 = await worldRoom.fetch("/update-shard", {
|
|
1682
2161
|
method: "POST",
|
|
1683
2162
|
headers: {
|
|
@@ -1686,6 +2165,7 @@ var Shard = class {
|
|
|
1686
2165
|
},
|
|
1687
2166
|
body: JSON.stringify({
|
|
1688
2167
|
shardId: this.room.id,
|
|
2168
|
+
worldId: this.worldId,
|
|
1689
2169
|
connections: currentConnections
|
|
1690
2170
|
})
|
|
1691
2171
|
});
|
|
@@ -1725,7 +2205,9 @@ var Shard = class {
|
|
|
1725
2205
|
headers.set(key, value);
|
|
1726
2206
|
});
|
|
1727
2207
|
headers.set("x-shard-id", this.room.id);
|
|
2208
|
+
headers.set("x-shard-world-id", this.worldId);
|
|
1728
2209
|
headers.set("x-forwarded-by-shard", "true");
|
|
2210
|
+
headers.set("x-access-shard", this.room.env.SHARD_SECRET);
|
|
1729
2211
|
const clientIp = req.headers.get("x-forwarded-for") || "unknown";
|
|
1730
2212
|
if (clientIp) {
|
|
1731
2213
|
headers.set("x-original-client-ip", clientIp);
|
|
@@ -1735,7 +2217,7 @@ var Shard = class {
|
|
|
1735
2217
|
headers,
|
|
1736
2218
|
body
|
|
1737
2219
|
};
|
|
1738
|
-
const response2 = await this.mainServerStub.fetch(path, requestInit);
|
|
2220
|
+
const response2 = await this.mainServerStub.fetch(path + url.search, requestInit);
|
|
1739
2221
|
return response2;
|
|
1740
2222
|
} catch (error) {
|
|
1741
2223
|
return response(500, { error: "Error forwarding request" });
|
|
@@ -1747,7 +2229,7 @@ var Shard = class {
|
|
|
1747
2229
|
* @description Executed periodically, used to perform maintenance tasks
|
|
1748
2230
|
*/
|
|
1749
2231
|
async onAlarm() {
|
|
1750
|
-
await this.updateWorldStats();
|
|
2232
|
+
await this.updateWorldStats(true);
|
|
1751
2233
|
}
|
|
1752
2234
|
};
|
|
1753
2235
|
|
|
@@ -1759,7 +2241,7 @@ async function testRoom(Room2, options = {}) {
|
|
|
1759
2241
|
return server2;
|
|
1760
2242
|
};
|
|
1761
2243
|
const isShard = options.shard || false;
|
|
1762
|
-
const io = new ServerIo(Room2.path, isShard ? {
|
|
2244
|
+
const io = new ServerIo(options.id ?? Room2.path, isShard ? {
|
|
1763
2245
|
parties: {
|
|
1764
2246
|
game: createServer,
|
|
1765
2247
|
...options.parties || {}
|
|
@@ -1803,7 +2285,7 @@ async function testRoom(Room2, options = {}) {
|
|
|
1803
2285
|
return client;
|
|
1804
2286
|
},
|
|
1805
2287
|
getServerUser: async (client, prop = "users") => {
|
|
1806
|
-
const privateId = client.conn.id;
|
|
2288
|
+
const privateId = client.conn.sessionId || client.conn.id;
|
|
1807
2289
|
const session = await server.getSession(privateId);
|
|
1808
2290
|
return server.subRoom[prop]()[session?.publicId];
|
|
1809
2291
|
}
|
|
@@ -1823,11 +2305,11 @@ function tick(ms = 0) {
|
|
|
1823
2305
|
|
|
1824
2306
|
// src/mock.ts
|
|
1825
2307
|
var MockPartyClient = class {
|
|
1826
|
-
constructor(server,
|
|
2308
|
+
constructor(server, sessionId) {
|
|
1827
2309
|
this.server = server;
|
|
1828
2310
|
this.events = /* @__PURE__ */ new Map();
|
|
1829
|
-
this.id =
|
|
1830
|
-
this.conn = new MockConnection(this);
|
|
2311
|
+
this.id = generateShortUUID();
|
|
2312
|
+
this.conn = new MockConnection(this, sessionId || this.id);
|
|
1831
2313
|
}
|
|
1832
2314
|
addEventListener(event, cb) {
|
|
1833
2315
|
if (!this.events.has(event)) {
|
|
@@ -1863,8 +2345,8 @@ var MockLobby = class {
|
|
|
1863
2345
|
this.server = server;
|
|
1864
2346
|
this.lobbyId = lobbyId;
|
|
1865
2347
|
}
|
|
1866
|
-
socket(
|
|
1867
|
-
return new MockPartyClient(this.server);
|
|
2348
|
+
socket(init) {
|
|
2349
|
+
return new MockPartyClient(this.server, init?.id);
|
|
1868
2350
|
}
|
|
1869
2351
|
async connection(idOrOptions, maybeOptions) {
|
|
1870
2352
|
const id2 = typeof idOrOptions === "string" ? idOrOptions : idOrOptions?.id;
|
|
@@ -1940,7 +2422,13 @@ var MockPartyRoom = class {
|
|
|
1940
2422
|
});
|
|
1941
2423
|
}
|
|
1942
2424
|
getConnection(id2) {
|
|
1943
|
-
|
|
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;
|
|
1944
2432
|
}
|
|
1945
2433
|
getConnections() {
|
|
1946
2434
|
return Array.from(this.clients.values()).map((client) => client.conn);
|
|
@@ -1948,13 +2436,25 @@ var MockPartyRoom = class {
|
|
|
1948
2436
|
clear() {
|
|
1949
2437
|
this.clients.clear();
|
|
1950
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
|
+
}
|
|
1951
2450
|
};
|
|
1952
2451
|
var MockConnection = class {
|
|
1953
|
-
constructor(client) {
|
|
2452
|
+
constructor(client, sessionId) {
|
|
1954
2453
|
this.client = client;
|
|
1955
2454
|
this.state = {};
|
|
1956
2455
|
this.server = client.server;
|
|
1957
2456
|
this.id = client.id;
|
|
2457
|
+
this.sessionId = sessionId;
|
|
1958
2458
|
}
|
|
1959
2459
|
setState(value) {
|
|
1960
2460
|
this.state = value;
|
|
@@ -1963,6 +2463,7 @@ var MockConnection = class {
|
|
|
1963
2463
|
this.client._trigger("message", data);
|
|
1964
2464
|
}
|
|
1965
2465
|
close() {
|
|
2466
|
+
this.server.room.deleteConnection?.(this.id, this);
|
|
1966
2467
|
this.server.onClose(this);
|
|
1967
2468
|
}
|
|
1968
2469
|
};
|
|
@@ -2142,7 +2643,7 @@ var guardManageWorld = async (_, req, room) => {
|
|
|
2142
2643
|
return true;
|
|
2143
2644
|
}
|
|
2144
2645
|
const url = new URL(req.url);
|
|
2145
|
-
const token = req
|
|
2646
|
+
const token = getAuthToken(req, url);
|
|
2146
2647
|
if (!token) {
|
|
2147
2648
|
return false;
|
|
2148
2649
|
}
|
|
@@ -2152,11 +2653,28 @@ var guardManageWorld = async (_, req, room) => {
|
|
|
2152
2653
|
if (!payload) {
|
|
2153
2654
|
return false;
|
|
2154
2655
|
}
|
|
2656
|
+
if (!canAccessWorld(payload, room.id)) {
|
|
2657
|
+
return false;
|
|
2658
|
+
}
|
|
2155
2659
|
} catch (error) {
|
|
2156
2660
|
return false;
|
|
2157
2661
|
}
|
|
2158
2662
|
return true;
|
|
2159
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
|
+
}
|
|
2160
2678
|
|
|
2161
2679
|
// src/world.ts
|
|
2162
2680
|
var MAX_PLAYERS_PER_SHARD = 75;
|
|
@@ -2168,8 +2686,16 @@ var RoomConfigSchema = z2.object({
|
|
|
2168
2686
|
minShards: z2.number().int().min(0),
|
|
2169
2687
|
maxShards: z2.number().int().positive().optional()
|
|
2170
2688
|
});
|
|
2689
|
+
var RegisterShardSchema = z2.object({
|
|
2690
|
+
shardId: z2.string(),
|
|
2691
|
+
roomId: z2.string(),
|
|
2692
|
+
worldId: z2.string().optional(),
|
|
2693
|
+
url: z2.string().url(),
|
|
2694
|
+
maxConnections: z2.number().int().positive()
|
|
2695
|
+
});
|
|
2171
2696
|
var UpdateShardStatsSchema = z2.object({
|
|
2172
2697
|
shardId: z2.string(),
|
|
2698
|
+
worldId: z2.string().optional(),
|
|
2173
2699
|
connections: z2.number().int().min(0),
|
|
2174
2700
|
status: z2.enum(["active", "maintenance", "draining"]).optional()
|
|
2175
2701
|
});
|
|
@@ -2215,6 +2741,7 @@ __decorateClass([
|
|
|
2215
2741
|
var ShardInfo = class {
|
|
2216
2742
|
constructor() {
|
|
2217
2743
|
this.roomId = signal("");
|
|
2744
|
+
this.worldId = signal("");
|
|
2218
2745
|
this.url = signal("");
|
|
2219
2746
|
this.currentConnections = signal(0);
|
|
2220
2747
|
this.maxConnections = signal(MAX_PLAYERS_PER_SHARD);
|
|
@@ -2228,6 +2755,9 @@ __decorateClass([
|
|
|
2228
2755
|
__decorateClass([
|
|
2229
2756
|
sync()
|
|
2230
2757
|
], ShardInfo.prototype, "roomId", 2);
|
|
2758
|
+
__decorateClass([
|
|
2759
|
+
sync()
|
|
2760
|
+
], ShardInfo.prototype, "worldId", 2);
|
|
2231
2761
|
__decorateClass([
|
|
2232
2762
|
sync()
|
|
2233
2763
|
], ShardInfo.prototype, "url", 2);
|
|
@@ -2261,6 +2791,7 @@ var WorldRoom = class {
|
|
|
2261
2791
|
if (!SHARD_SECRET) {
|
|
2262
2792
|
throw new Error("SHARD_SECRET env variable is not set");
|
|
2263
2793
|
}
|
|
2794
|
+
this.scheduleInactiveShardCleanup();
|
|
2264
2795
|
}
|
|
2265
2796
|
async onJoin(user, conn, ctx) {
|
|
2266
2797
|
const canConnect = await guardManageWorld(user, ctx.request, this.room);
|
|
@@ -2276,6 +2807,13 @@ var WorldRoom = class {
|
|
|
2276
2807
|
return obj;
|
|
2277
2808
|
}
|
|
2278
2809
|
// Helper methods
|
|
2810
|
+
getWorldId() {
|
|
2811
|
+
return this.room.id;
|
|
2812
|
+
}
|
|
2813
|
+
scheduleInactiveShardCleanup() {
|
|
2814
|
+
const timeoutId = setTimeout(() => this.cleanupInactiveShards(), 6e4);
|
|
2815
|
+
timeoutId?.unref?.();
|
|
2816
|
+
}
|
|
2279
2817
|
cleanupInactiveShards() {
|
|
2280
2818
|
const now = Date.now();
|
|
2281
2819
|
const timeout = 5 * 60 * 1e3;
|
|
@@ -2287,16 +2825,25 @@ var WorldRoom = class {
|
|
|
2287
2825
|
hasChanges = true;
|
|
2288
2826
|
}
|
|
2289
2827
|
});
|
|
2290
|
-
|
|
2828
|
+
this.scheduleInactiveShardCleanup();
|
|
2829
|
+
}
|
|
2830
|
+
removeShard(shardId) {
|
|
2831
|
+
delete this.shards()[shardId];
|
|
2832
|
+
}
|
|
2833
|
+
shouldCompleteDrain(shard) {
|
|
2834
|
+
return shard.status() === "draining" && shard.currentConnections() === 0;
|
|
2291
2835
|
}
|
|
2292
2836
|
async registerRoom(req, res) {
|
|
2293
|
-
const
|
|
2294
|
-
if (!
|
|
2837
|
+
const parseResult = RoomConfigSchema.safeParse(await req.json());
|
|
2838
|
+
if (!parseResult.success) {
|
|
2295
2839
|
return res?.badRequest("Invalid room configuration", {
|
|
2296
|
-
details:
|
|
2840
|
+
details: parseResult.error
|
|
2297
2841
|
});
|
|
2298
2842
|
}
|
|
2299
|
-
const roomConfig =
|
|
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
|
+
}
|
|
2300
2847
|
const roomId = roomConfig.name;
|
|
2301
2848
|
if (!this.rooms()[roomId]) {
|
|
2302
2849
|
const newRoom = new RoomConfig();
|
|
@@ -2321,34 +2868,41 @@ var WorldRoom = class {
|
|
|
2321
2868
|
room.minShards.set(roomConfig.minShards);
|
|
2322
2869
|
room.maxShards.set(roomConfig.maxShards);
|
|
2323
2870
|
}
|
|
2871
|
+
await this.ensureMinShards(roomId);
|
|
2324
2872
|
}
|
|
2325
2873
|
async updateShardStats(req, res) {
|
|
2326
|
-
const
|
|
2327
|
-
if (!
|
|
2328
|
-
return res.badRequest("Invalid shard
|
|
2329
|
-
details:
|
|
2874
|
+
const parseResult = UpdateShardStatsSchema.safeParse(await req.json());
|
|
2875
|
+
if (!parseResult.success) {
|
|
2876
|
+
return res.badRequest("Invalid shard stats", {
|
|
2877
|
+
details: parseResult.error
|
|
2330
2878
|
});
|
|
2331
2879
|
}
|
|
2332
|
-
const body =
|
|
2880
|
+
const body = parseResult.data;
|
|
2333
2881
|
const { shardId, connections, status } = body;
|
|
2334
2882
|
const shard = this.shards()[shardId];
|
|
2335
2883
|
if (!shard) {
|
|
2336
2884
|
return res.notFound(`Shard ${shardId} not found`);
|
|
2337
2885
|
}
|
|
2886
|
+
if (body.worldId && body.worldId !== this.getWorldId()) {
|
|
2887
|
+
return res.badRequest(`Shard ${shardId} belongs to world ${body.worldId}, not ${this.getWorldId()}`);
|
|
2888
|
+
}
|
|
2338
2889
|
shard.currentConnections.set(connections);
|
|
2339
2890
|
if (status) {
|
|
2340
2891
|
shard.status.set(status);
|
|
2341
2892
|
}
|
|
2342
2893
|
shard.lastHeartbeat.set(Date.now());
|
|
2894
|
+
if (this.shouldCompleteDrain(shard)) {
|
|
2895
|
+
this.removeShard(shard.id);
|
|
2896
|
+
}
|
|
2343
2897
|
}
|
|
2344
2898
|
async scaleRoom(req, res) {
|
|
2345
|
-
const
|
|
2346
|
-
if (!
|
|
2347
|
-
return res.badRequest("Invalid scale request", {
|
|
2348
|
-
details:
|
|
2899
|
+
const parseResult = ScaleRoomSchema.safeParse(await req.json());
|
|
2900
|
+
if (!parseResult.success) {
|
|
2901
|
+
return res.badRequest("Invalid scale room request", {
|
|
2902
|
+
details: parseResult.error
|
|
2349
2903
|
});
|
|
2350
2904
|
}
|
|
2351
|
-
const data =
|
|
2905
|
+
const data = parseResult.data;
|
|
2352
2906
|
const { targetShardCount, shardTemplate, roomId } = data;
|
|
2353
2907
|
const room = this.rooms()[roomId];
|
|
2354
2908
|
if (!room) {
|
|
@@ -2363,13 +2917,16 @@ var WorldRoom = class {
|
|
|
2363
2917
|
});
|
|
2364
2918
|
}
|
|
2365
2919
|
if (targetShardCount < previousShardCount) {
|
|
2366
|
-
const
|
|
2920
|
+
const shardsToDrain = [...roomShards].sort((a, b) => {
|
|
2367
2921
|
if (a.status() === "draining" && b.status() !== "draining") return -1;
|
|
2368
2922
|
if (a.status() !== "draining" && b.status() === "draining") return 1;
|
|
2369
2923
|
return a.currentConnections() - b.currentConnections();
|
|
2370
2924
|
}).slice(0, previousShardCount - targetShardCount);
|
|
2371
|
-
for (const shard of
|
|
2372
|
-
|
|
2925
|
+
for (const shard of shardsToDrain) {
|
|
2926
|
+
shard.status.set("draining");
|
|
2927
|
+
if (this.shouldCompleteDrain(shard)) {
|
|
2928
|
+
this.removeShard(shard.id);
|
|
2929
|
+
}
|
|
2373
2930
|
}
|
|
2374
2931
|
return;
|
|
2375
2932
|
}
|
|
@@ -2456,8 +3013,20 @@ var WorldRoom = class {
|
|
|
2456
3013
|
return { error: `No shards available for room ${roomId}` };
|
|
2457
3014
|
}
|
|
2458
3015
|
}
|
|
2459
|
-
|
|
3016
|
+
let activeShards = this.getAvailableShards(roomShards);
|
|
2460
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
|
+
}
|
|
2461
3030
|
return { error: `No active shards available for room ${roomId}` };
|
|
2462
3031
|
}
|
|
2463
3032
|
const balancingStrategy = room.balancingStrategy();
|
|
@@ -2485,6 +3054,14 @@ var WorldRoom = class {
|
|
|
2485
3054
|
url: selectedShard.url()
|
|
2486
3055
|
};
|
|
2487
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
|
+
}
|
|
2488
3065
|
// Private methods
|
|
2489
3066
|
async createShard(roomId, urlTemplate, maxConnections) {
|
|
2490
3067
|
const room = this.rooms()[roomId];
|
|
@@ -2492,13 +3069,15 @@ var WorldRoom = class {
|
|
|
2492
3069
|
console.error(`Cannot create shard for non-existent room: ${roomId}`);
|
|
2493
3070
|
return null;
|
|
2494
3071
|
}
|
|
2495
|
-
const
|
|
3072
|
+
const worldId = this.getWorldId();
|
|
3073
|
+
const shardId = `${roomId}:${worldId}:${Date.now()}-${Math.floor(Math.random() * 1e4)}`;
|
|
2496
3074
|
const template = urlTemplate || this.defaultShardUrlTemplate();
|
|
2497
3075
|
const url = template.replace("{shardId}", shardId).replace("{roomId}", roomId);
|
|
2498
3076
|
const max = maxConnections || room.maxPlayersPerShard();
|
|
2499
3077
|
const newShard = new ShardInfo();
|
|
2500
3078
|
newShard.id = shardId;
|
|
2501
3079
|
newShard.roomId.set(roomId);
|
|
3080
|
+
newShard.worldId.set(worldId);
|
|
2502
3081
|
newShard.url.set(url);
|
|
2503
3082
|
newShard.maxConnections.set(max);
|
|
2504
3083
|
newShard.currentConnections.set(0);
|
|
@@ -2507,6 +3086,20 @@ var WorldRoom = class {
|
|
|
2507
3086
|
this.shards()[shardId] = newShard;
|
|
2508
3087
|
return newShard;
|
|
2509
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
|
+
}
|
|
2510
3103
|
};
|
|
2511
3104
|
__decorateClass([
|
|
2512
3105
|
sync(RoomConfig)
|
|
@@ -2557,13 +3150,16 @@ WorldRoom = __decorateClass([
|
|
|
2557
3150
|
], WorldRoom);
|
|
2558
3151
|
|
|
2559
3152
|
// src/session.guard.ts
|
|
3153
|
+
function getPrivateId(sender) {
|
|
3154
|
+
return sender.sessionId || sender.id;
|
|
3155
|
+
}
|
|
2560
3156
|
function createRequireSessionGuard(storage) {
|
|
2561
3157
|
return async (sender, value) => {
|
|
2562
3158
|
if (!sender || !sender.id) {
|
|
2563
3159
|
return false;
|
|
2564
3160
|
}
|
|
2565
3161
|
try {
|
|
2566
|
-
const session = await storage.get(`session:${sender
|
|
3162
|
+
const session = await storage.get(`session:${getPrivateId(sender)}`);
|
|
2567
3163
|
if (!session) {
|
|
2568
3164
|
return false;
|
|
2569
3165
|
}
|
|
@@ -2583,7 +3179,7 @@ var requireSession = async (sender, value, room) => {
|
|
|
2583
3179
|
return false;
|
|
2584
3180
|
}
|
|
2585
3181
|
try {
|
|
2586
|
-
const session = await room.storage.get(`session:${sender
|
|
3182
|
+
const session = await room.storage.get(`session:${getPrivateId(sender)}`);
|
|
2587
3183
|
if (!session) {
|
|
2588
3184
|
return false;
|
|
2589
3185
|
}
|