@rpgjs/client 5.0.0-alpha.43 → 5.0.0-alpha.44

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.
Files changed (137) hide show
  1. package/dist/Game/AnimationManager.js +25 -24
  2. package/dist/Game/AnimationManager.js.map +1 -1
  3. package/dist/Game/Event.js +11 -10
  4. package/dist/Game/Event.js.map +1 -1
  5. package/dist/Game/Map.js +78 -94
  6. package/dist/Game/Map.js.map +1 -1
  7. package/dist/Game/Object.js +182 -216
  8. package/dist/Game/Object.js.map +1 -1
  9. package/dist/Game/Player.js +11 -10
  10. package/dist/Game/Player.js.map +1 -1
  11. package/dist/Gui/Gui.js +440 -473
  12. package/dist/Gui/Gui.js.map +1 -1
  13. package/dist/Gui/NotificationManager.js +48 -50
  14. package/dist/Gui/NotificationManager.js.map +1 -1
  15. package/dist/Resource.js +132 -113
  16. package/dist/Resource.js.map +1 -1
  17. package/dist/RpgClientEngine.js +1333 -1486
  18. package/dist/RpgClientEngine.js.map +1 -1
  19. package/dist/Sound.js +162 -92
  20. package/dist/Sound.js.map +1 -1
  21. package/dist/_virtual/_@oxc-project_runtime@0.115.0/helpers/decorate.js +9 -0
  22. package/dist/_virtual/_@oxc-project_runtime@0.115.0/helpers/decorateMetadata.js +6 -0
  23. package/dist/components/animations/animation.ce.js +22 -19
  24. package/dist/components/animations/animation.ce.js.map +1 -1
  25. package/dist/components/animations/hit.ce.js +67 -68
  26. package/dist/components/animations/hit.ce.js.map +1 -1
  27. package/dist/components/animations/index.js +9 -8
  28. package/dist/components/animations/index.js.map +1 -1
  29. package/dist/components/character.ce.js +390 -314
  30. package/dist/components/character.ce.js.map +1 -1
  31. package/dist/components/dynamics/parse-value.js +42 -52
  32. package/dist/components/dynamics/parse-value.js.map +1 -1
  33. package/dist/components/dynamics/text.ce.js +71 -139
  34. package/dist/components/dynamics/text.ce.js.map +1 -1
  35. package/dist/components/gui/box.ce.js +26 -25
  36. package/dist/components/gui/box.ce.js.map +1 -1
  37. package/dist/components/gui/dialogbox/index.ce.js +202 -149
  38. package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
  39. package/dist/components/gui/gameover.ce.js +190 -138
  40. package/dist/components/gui/gameover.ce.js.map +1 -1
  41. package/dist/components/gui/hud/hud.ce.js +90 -33
  42. package/dist/components/gui/hud/hud.ce.js.map +1 -1
  43. package/dist/components/gui/index.js +14 -0
  44. package/dist/components/gui/menu/equip-menu.ce.js +478 -346
  45. package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
  46. package/dist/components/gui/menu/exit-menu.ce.js +52 -33
  47. package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
  48. package/dist/components/gui/menu/items-menu.ce.js +341 -226
  49. package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
  50. package/dist/components/gui/menu/main-menu.ce.js +414 -205
  51. package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
  52. package/dist/components/gui/menu/options-menu.ce.js +46 -26
  53. package/dist/components/gui/menu/options-menu.ce.js.map +1 -1
  54. package/dist/components/gui/menu/skills-menu.ce.js +104 -50
  55. package/dist/components/gui/menu/skills-menu.ce.js.map +1 -1
  56. package/dist/components/gui/mobile/index.js +18 -21
  57. package/dist/components/gui/mobile/index.js.map +1 -1
  58. package/dist/components/gui/mobile/mobile.ce.js +76 -15
  59. package/dist/components/gui/mobile/mobile.ce.js.map +1 -1
  60. package/dist/components/gui/notification/notification.ce.js +62 -36
  61. package/dist/components/gui/notification/notification.ce.js.map +1 -1
  62. package/dist/components/gui/save-load.ce.js +386 -239
  63. package/dist/components/gui/save-load.ce.js.map +1 -1
  64. package/dist/components/gui/shop/shop.ce.js +649 -319
  65. package/dist/components/gui/shop/shop.ce.js.map +1 -1
  66. package/dist/components/gui/title-screen.ce.js +187 -145
  67. package/dist/components/gui/title-screen.ce.js.map +1 -1
  68. package/dist/components/index.js +4 -0
  69. package/dist/components/prebuilt/hp-bar.ce.js +114 -104
  70. package/dist/components/prebuilt/hp-bar.ce.js.map +1 -1
  71. package/dist/components/prebuilt/index.js +2 -0
  72. package/dist/components/prebuilt/light-halo.ce.js +92 -74
  73. package/dist/components/prebuilt/light-halo.ce.js.map +1 -1
  74. package/dist/components/scenes/canvas.ce.js +58 -42
  75. package/dist/components/scenes/canvas.ce.js.map +1 -1
  76. package/dist/components/scenes/draw-map.ce.js +87 -64
  77. package/dist/components/scenes/draw-map.ce.js.map +1 -1
  78. package/dist/components/scenes/event-layer.ce.js +26 -16
  79. package/dist/components/scenes/event-layer.ce.js.map +1 -1
  80. package/dist/core/inject.js +11 -10
  81. package/dist/core/inject.js.map +1 -1
  82. package/dist/core/setup.js +13 -13
  83. package/dist/core/setup.js.map +1 -1
  84. package/dist/index.js +44 -41
  85. package/dist/module.js +169 -168
  86. package/dist/module.js.map +1 -1
  87. package/dist/node_modules/.pnpm/@signe_di@2.8.3/node_modules/@signe/di/dist/index.js +209 -298
  88. package/dist/node_modules/.pnpm/@signe_di@2.8.3/node_modules/@signe/di/dist/index.js.map +1 -1
  89. package/dist/node_modules/.pnpm/@signe_reactive@2.8.3/node_modules/@signe/reactive/dist/index.js +430 -507
  90. package/dist/node_modules/.pnpm/@signe_reactive@2.8.3/node_modules/@signe/reactive/dist/index.js.map +1 -1
  91. package/dist/node_modules/.pnpm/@signe_room@2.8.3/node_modules/@signe/room/dist/index.js +2062 -2571
  92. package/dist/node_modules/.pnpm/@signe_room@2.8.3/node_modules/@signe/room/dist/index.js.map +1 -1
  93. package/dist/node_modules/.pnpm/@signe_sync@2.8.3/node_modules/@signe/sync/dist/chunk-7QVYU63E.js +8 -3
  94. package/dist/node_modules/.pnpm/@signe_sync@2.8.3/node_modules/@signe/sync/dist/chunk-7QVYU63E.js.map +1 -1
  95. package/dist/node_modules/.pnpm/@signe_sync@2.8.3/node_modules/@signe/sync/dist/client/index.js +78 -101
  96. package/dist/node_modules/.pnpm/@signe_sync@2.8.3/node_modules/@signe/sync/dist/client/index.js.map +1 -1
  97. package/dist/node_modules/.pnpm/@signe_sync@2.8.3/node_modules/@signe/sync/dist/index.js +264 -401
  98. package/dist/node_modules/.pnpm/@signe_sync@2.8.3/node_modules/@signe/sync/dist/index.js.map +1 -1
  99. package/dist/node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js +9 -7
  100. package/dist/node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js.map +1 -1
  101. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-HAC622V3.js +106 -165
  102. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-HAC622V3.js.map +1 -1
  103. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-S74YV6PU.js +361 -461
  104. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-S74YV6PU.js.map +1 -1
  105. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/index.js +2 -0
  106. package/dist/node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js +3633 -4280
  107. package/dist/node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js.map +1 -1
  108. package/dist/presets/animation.js +37 -25
  109. package/dist/presets/animation.js.map +1 -1
  110. package/dist/presets/faceset.js +49 -22
  111. package/dist/presets/faceset.js.map +1 -1
  112. package/dist/presets/icon.js +13 -13
  113. package/dist/presets/icon.js.map +1 -1
  114. package/dist/presets/index.js +15 -14
  115. package/dist/presets/index.js.map +1 -1
  116. package/dist/presets/lpc.js +96 -93
  117. package/dist/presets/lpc.js.map +1 -1
  118. package/dist/presets/rmspritesheet.js +40 -39
  119. package/dist/presets/rmspritesheet.js.map +1 -1
  120. package/dist/services/AbstractSocket.js +10 -8
  121. package/dist/services/AbstractSocket.js.map +1 -1
  122. package/dist/services/keyboardControls.js +20 -18
  123. package/dist/services/keyboardControls.js.map +1 -1
  124. package/dist/services/loadMap.js +120 -36
  125. package/dist/services/loadMap.js.map +1 -1
  126. package/dist/services/mmorpg.js +128 -136
  127. package/dist/services/mmorpg.js.map +1 -1
  128. package/dist/services/save.js +74 -66
  129. package/dist/services/save.js.map +1 -1
  130. package/dist/services/standalone.js +165 -167
  131. package/dist/services/standalone.js.map +1 -1
  132. package/dist/utils/getEntityProp.js +49 -51
  133. package/dist/utils/getEntityProp.js.map +1 -1
  134. package/package.json +8 -8
  135. package/src/components/character.ce +1 -1
  136. package/src/components/scenes/draw-map.ce +3 -1
  137. package/dist/index.js.map +0 -1
@@ -1,2689 +1,2180 @@
1
- import { dset } from '../../../../../dset@3.1.4/node_modules/dset/dist/index.js';
2
- import z from '../../../../../zod@3.24.2/node_modules/zod/lib/index.js';
3
- import { createStatesSnapshotDeep, syncClass, generateShortUUID as generateShortUUID$1, load, id, sync, persist, getByPath, createStatesSnapshot, DELETE_TOKEN } from '../../../../../@signe_sync@2.8.3/node_modules/@signe/sync/dist/index.js';
4
- import { signal } from '../../../../../@signe_reactive@2.8.3/node_modules/@signe/reactive/dist/index.js';
5
-
1
+ import { signal } from "../../../../../@signe_reactive@2.8.3/node_modules/@signe/reactive/dist/index.js";
2
+ import { createStatesSnapshot, createStatesSnapshotDeep, generateShortUUID as generateShortUUID$1, getByPath, id, load, persist, sync, syncClass } from "../../../../../@signe_sync@2.8.3/node_modules/@signe/sync/dist/index.js";
3
+ import { dset } from "../../../../../dset@3.1.4/node_modules/dset/dist/index.js";
4
+ import { z } from "../../../../../zod@3.24.2/node_modules/zod/lib/index.js";
5
+ //#region ../../node_modules/.pnpm/@signe+room@2.8.3/node_modules/@signe/room/dist/index.js
6
6
  var __defProp = Object.defineProperty;
7
- var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
8
-
9
- // src/decorators.ts
7
+ var __name = (target, value) => __defProp(target, "name", {
8
+ value,
9
+ configurable: true
10
+ });
10
11
  function Action(name, bodyValidation) {
11
- return function(target, propertyKey) {
12
- if (!target.constructor._actionMetadata) {
13
- target.constructor._actionMetadata = /* @__PURE__ */ new Map();
14
- }
15
- target.constructor._actionMetadata.set(name, {
16
- key: propertyKey,
17
- bodyValidation
18
- });
19
- };
12
+ return function(target, propertyKey) {
13
+ if (!target.constructor._actionMetadata) target.constructor._actionMetadata = /* @__PURE__ */ new Map();
14
+ target.constructor._actionMetadata.set(name, {
15
+ key: propertyKey,
16
+ bodyValidation
17
+ });
18
+ };
20
19
  }
21
20
  __name(Action, "Action");
22
21
  function Request2(options, bodyValidation) {
23
- return function(target, propertyKey) {
24
- if (!target.constructor._requestMetadata) {
25
- target.constructor._requestMetadata = /* @__PURE__ */ new Map();
26
- }
27
- const path = options.path.startsWith("/") ? options.path : `/${options.path}`;
28
- const method = options.method || "GET";
29
- const routeKey = `${method}:${path}`;
30
- target.constructor._requestMetadata.set(routeKey, {
31
- key: propertyKey,
32
- path,
33
- method,
34
- bodyValidation
35
- });
36
- };
22
+ return function(target, propertyKey) {
23
+ if (!target.constructor._requestMetadata) target.constructor._requestMetadata = /* @__PURE__ */ new Map();
24
+ const path = options.path.startsWith("/") ? options.path : `/${options.path}`;
25
+ const method = options.method || "GET";
26
+ const routeKey = `${method}:${path}`;
27
+ target.constructor._requestMetadata.set(routeKey, {
28
+ key: propertyKey,
29
+ path,
30
+ method,
31
+ bodyValidation
32
+ });
33
+ };
37
34
  }
38
35
  __name(Request2, "Request");
39
36
  function Room(options) {
40
- return function(target) {
41
- target.path = options.path;
42
- target.prototype.maxUsers = options.maxUsers;
43
- target.prototype.throttleStorage = options.throttleStorage;
44
- target.prototype.throttleSync = options.throttleSync;
45
- target.prototype.sessionExpiryTime = options.sessionExpiryTime ?? 5 * 60 * 1e3;
46
- if (options.guards) {
47
- target["_roomGuards"] = options.guards;
48
- }
49
- };
37
+ return function(target) {
38
+ target.path = options.path;
39
+ target.prototype.maxUsers = options.maxUsers;
40
+ target.prototype.throttleStorage = options.throttleStorage;
41
+ target.prototype.throttleSync = options.throttleSync;
42
+ target.prototype.sessionExpiryTime = options.sessionExpiryTime ?? 300 * 1e3;
43
+ if (options.guards) target["_roomGuards"] = options.guards;
44
+ };
50
45
  }
51
46
  __name(Room, "Room");
52
47
  function RoomGuard(guards) {
53
- return function(target) {
54
- target["_roomGuards"] = guards;
55
- };
48
+ return function(target) {
49
+ target["_roomGuards"] = guards;
50
+ };
56
51
  }
57
52
  __name(RoomGuard, "RoomGuard");
58
53
  function Guard(guards) {
59
- return function(target, propertyKey, descriptor) {
60
- if (!target.constructor["_actionGuards"]) {
61
- target.constructor["_actionGuards"] = /* @__PURE__ */ new Map();
62
- }
63
- if (!Array.isArray(guards)) {
64
- guards = [
65
- guards
66
- ];
67
- }
68
- target.constructor["_actionGuards"].set(propertyKey, guards);
69
- };
54
+ return function(target, propertyKey, descriptor) {
55
+ if (!target.constructor["_actionGuards"]) target.constructor["_actionGuards"] = /* @__PURE__ */ new Map();
56
+ if (!Array.isArray(guards)) guards = [guards];
57
+ target.constructor["_actionGuards"].set(propertyKey, guards);
58
+ };
70
59
  }
71
60
  __name(Guard, "Guard");
72
-
73
- // ../sync/src/utils.ts
74
61
  function generateShortUUID() {
75
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
76
- let uuid = "";
77
- for (let i = 0; i < 8; i++) {
78
- const randomIndex = Math.floor(Math.random() * chars.length);
79
- uuid += chars[randomIndex];
80
- }
81
- return uuid;
62
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
63
+ let uuid = "";
64
+ for (let i = 0; i < 8; i++) {
65
+ const randomIndex = Math.floor(Math.random() * 62);
66
+ uuid += chars[randomIndex];
67
+ }
68
+ return uuid;
82
69
  }
83
70
  __name(generateShortUUID, "generateShortUUID");
84
-
85
- // src/storage.ts
86
71
  var Storage = class {
87
- static {
88
- __name(this, "Storage");
89
- }
90
- memory = /* @__PURE__ */ new Map();
91
- async put(key, value) {
92
- this.memory.set(key, value);
93
- }
94
- async get(key) {
95
- return this.memory.get(key);
96
- }
97
- async delete(key) {
98
- this.memory.delete(key);
99
- }
100
- async list() {
101
- return this.memory;
102
- }
72
+ static {
73
+ __name(this, "Storage");
74
+ }
75
+ memory = /* @__PURE__ */ new Map();
76
+ async put(key, value) {
77
+ this.memory.set(key, value);
78
+ }
79
+ async get(key) {
80
+ return this.memory.get(key);
81
+ }
82
+ async delete(key) {
83
+ this.memory.delete(key);
84
+ }
85
+ async list() {
86
+ return this.memory;
87
+ }
103
88
  };
104
89
  function isPromise(value) {
105
- return value instanceof Promise;
90
+ return value instanceof Promise;
106
91
  }
107
92
  __name(isPromise, "isPromise");
108
93
  async function awaitReturn(val) {
109
- return isPromise(val) ? await val : val;
94
+ return isPromise(val) ? await val : val;
110
95
  }
111
96
  __name(awaitReturn, "awaitReturn");
112
97
  function isClass(obj) {
113
- return typeof obj === "function" && obj.prototype && obj.prototype.constructor === obj;
98
+ return typeof obj === "function" && obj.prototype && obj.prototype.constructor === obj;
114
99
  }
115
100
  __name(isClass, "isClass");
116
101
  function throttle(func, wait) {
117
- let timeout = null;
118
- let lastArgs = null;
119
- return function(...args) {
120
- if (!timeout) {
121
- func(...args);
122
- timeout = setTimeout(() => {
123
- if (lastArgs) {
124
- func(...lastArgs);
125
- lastArgs = null;
126
- }
127
- timeout = null;
128
- }, wait);
129
- } else {
130
- lastArgs = args;
131
- }
132
- };
102
+ let timeout = null;
103
+ let lastArgs = null;
104
+ return function(...args) {
105
+ if (!timeout) {
106
+ func(...args);
107
+ timeout = setTimeout(() => {
108
+ if (lastArgs) {
109
+ func(...lastArgs);
110
+ lastArgs = null;
111
+ }
112
+ timeout = null;
113
+ }, wait);
114
+ } else lastArgs = args;
115
+ };
133
116
  }
134
117
  __name(throttle, "throttle");
135
118
  function extractParams(pattern, str) {
136
- const regexPattern = pattern.replace(/{(\w+)}/g, "(?<$1>[\\w-]+)");
137
- const regex = new RegExp(`^${regexPattern}$`);
138
- const match = regex.exec(str);
139
- if (match && match.groups) {
140
- return match.groups;
141
- } else if (pattern === str) {
142
- return {};
143
- } else {
144
- return null;
145
- }
119
+ const regexPattern = pattern.replace(/{(\w+)}/g, "(?<$1>[\\w-]+)");
120
+ const match = new RegExp(`^${regexPattern}$`).exec(str);
121
+ if (match && match.groups) return match.groups;
122
+ else if (pattern === str) return {};
123
+ else return null;
146
124
  }
147
125
  __name(extractParams, "extractParams");
148
126
  function dremove(obj, keys) {
149
- if (typeof keys === "string") {
150
- keys = keys.split(".");
151
- }
152
- let i = 0;
153
- const l = keys.length;
154
- let t = obj;
155
- let k;
156
- while (i < l - 1) {
157
- k = keys[i++];
158
- if (k === "__proto__" || k === "constructor" || k === "prototype") return;
159
- if (typeof t[k] !== "object" || t[k] === null) return;
160
- t = t[k];
161
- }
162
- k = keys[i];
163
- if (t && typeof t === "object" && !(k === "__proto__" || k === "constructor" || k === "prototype")) {
164
- delete t[k];
165
- }
127
+ if (typeof keys === "string") keys = keys.split(".");
128
+ let i = 0;
129
+ const l = keys.length;
130
+ let t = obj;
131
+ let k;
132
+ while (i < l - 1) {
133
+ k = keys[i++];
134
+ if (k === "__proto__" || k === "constructor" || k === "prototype") return;
135
+ if (typeof t[k] !== "object" || t[k] === null) return;
136
+ t = t[k];
137
+ }
138
+ k = keys[i];
139
+ if (t && typeof t === "object" && !(k === "__proto__" || k === "constructor" || k === "prototype")) delete t[k];
166
140
  }
167
141
  __name(dremove, "dremove");
168
142
  function buildObject(valuesMap, allMemory) {
169
- let memoryObj = {};
170
- for (let path of valuesMap.keys()) {
171
- const value = valuesMap.get(path);
172
- dset(memoryObj, path, value);
173
- if (value === "$delete") {
174
- dremove(allMemory, path);
175
- } else {
176
- dset(allMemory, path, value);
177
- }
178
- }
179
- return memoryObj;
143
+ let memoryObj = {};
144
+ for (let path of valuesMap.keys()) {
145
+ const value = valuesMap.get(path);
146
+ dset(memoryObj, path, value);
147
+ if (value === "$delete") dremove(allMemory, path);
148
+ else dset(allMemory, path, value);
149
+ }
150
+ return memoryObj;
180
151
  }
181
152
  __name(buildObject, "buildObject");
182
153
  function response(status, body) {
183
- return new Response(JSON.stringify(body), {
184
- status,
185
- headers: {
186
- "Content-Type": "application/json"
187
- }
188
- });
154
+ return new Response(JSON.stringify(body), {
155
+ status,
156
+ headers: { "Content-Type": "application/json" }
157
+ });
189
158
  }
190
159
  __name(response, "response");
191
-
192
- // src/request/response.ts
193
160
  var ServerResponse = class {
194
- static {
195
- __name(this, "ServerResponse");
196
- }
197
- interceptors;
198
- statusCode = 200;
199
- responseBody = {};
200
- responseHeaders = {
201
- "Content-Type": "application/json"
202
- };
203
- /**
204
- * Creates a new ServerResponse instance
205
- * @param interceptors Array of interceptor functions that can modify the response
206
- */
207
- constructor(interceptors = []) {
208
- this.interceptors = interceptors;
209
- }
210
- /**
211
- * Sets the status code for the response
212
- * @param code HTTP status code
213
- * @returns this instance for chaining
214
- */
215
- status(code) {
216
- this.statusCode = code;
217
- return this;
218
- }
219
- /**
220
- * Sets the response body and returns this instance (chainable method)
221
- *
222
- * @param body Response body
223
- * @returns this instance for chaining
224
- */
225
- body(body) {
226
- this.responseBody = body;
227
- return this;
228
- }
229
- /**
230
- * Adds a header to the response
231
- * @param name Header name
232
- * @param value Header value
233
- * @returns this instance for chaining
234
- */
235
- header(name, value) {
236
- this.responseHeaders[name] = value;
237
- return this;
238
- }
239
- /**
240
- * Adds multiple headers to the response
241
- * @param headers Object containing headers
242
- * @returns this instance for chaining
243
- */
244
- setHeaders(headers) {
245
- this.responseHeaders = {
246
- ...this.responseHeaders,
247
- ...headers
248
- };
249
- return this;
250
- }
251
- /**
252
- * Add an interceptor to the chain
253
- * @param interceptor Function that takes a Response and returns a modified Response
254
- * @returns this instance for chaining
255
- */
256
- use(interceptor) {
257
- this.interceptors.push(interceptor);
258
- return this;
259
- }
260
- /**
261
- * Builds and returns the Response object after applying all interceptors
262
- * @returns Promise<Response> The final Response object
263
- * @private Internal method used by terminal methods
264
- */
265
- async buildResponse() {
266
- let response2 = new Response(JSON.stringify(this.responseBody), {
267
- status: this.statusCode,
268
- headers: this.responseHeaders
269
- });
270
- for (const interceptor of this.interceptors) {
271
- try {
272
- const interceptedResponse = interceptor(response2);
273
- if (interceptedResponse instanceof Promise) {
274
- response2 = await interceptedResponse;
275
- } else {
276
- response2 = interceptedResponse;
277
- }
278
- } catch (error) {
279
- console.error("Error in interceptor:", error);
280
- }
281
- }
282
- return response2;
283
- }
284
- /**
285
- * Sets the response body to the JSON-stringified version of the provided value
286
- * and sends the response (terminal method)
287
- *
288
- * @param body Response body to be JSON stringified
289
- * @returns Promise<Response> The final Response object
290
- */
291
- async json(body) {
292
- this.responseBody = body;
293
- this.responseHeaders["Content-Type"] = "application/json";
294
- return this.buildResponse();
295
- }
296
- /**
297
- * Sends the response with the current configuration (terminal method)
298
- *
299
- * @param body Optional body to set before sending
300
- * @returns Promise<Response> The final Response object
301
- */
302
- async send(body) {
303
- if (body !== void 0) {
304
- this.responseBody = body;
305
- }
306
- return this.buildResponse();
307
- }
308
- /**
309
- * Sends a plain text response (terminal method)
310
- *
311
- * @param text Text to send
312
- * @returns Promise<Response> The final Response object
313
- */
314
- async text(text) {
315
- this.responseBody = text;
316
- this.responseHeaders["Content-Type"] = "text/plain";
317
- let response2 = new Response(text, {
318
- status: this.statusCode,
319
- headers: this.responseHeaders
320
- });
321
- for (const interceptor of this.interceptors) {
322
- try {
323
- const interceptedResponse = interceptor(response2);
324
- if (interceptedResponse instanceof Promise) {
325
- response2 = await interceptedResponse;
326
- } else {
327
- response2 = interceptedResponse;
328
- }
329
- } catch (error) {
330
- console.error("Error in interceptor:", error);
331
- }
332
- }
333
- return response2;
334
- }
335
- /**
336
- * Redirects to the specified URL (terminal method)
337
- *
338
- * @param url URL to redirect to
339
- * @param statusCode HTTP status code (default: 302)
340
- * @returns Promise<Response> The final Response object
341
- */
342
- async redirect(url, statusCode = 302) {
343
- this.statusCode = statusCode;
344
- this.responseHeaders["Location"] = url;
345
- return this.buildResponse();
346
- }
347
- /**
348
- * Creates a success response with status 200
349
- * @param body Response body
350
- * @returns Promise<Response> The final Response object
351
- */
352
- async success(body = {}) {
353
- return this.status(200).json(body);
354
- }
355
- /**
356
- * Creates an error response with status 400
357
- * @param message Error message
358
- * @param details Additional error details
359
- * @returns Promise<Response> The final Response object
360
- */
361
- async badRequest(message, details = {}) {
362
- return this.status(400).json({
363
- error: message,
364
- ...details
365
- });
366
- }
367
- /**
368
- * Creates an error response with status 403
369
- * @param message Error message
370
- * @returns Promise<Response> The final Response object
371
- */
372
- async notPermitted(message = "Not permitted") {
373
- return this.status(403).json({
374
- error: message
375
- });
376
- }
377
- /**
378
- * Creates an error response with status 401
379
- * @param message Error message
380
- * @returns Promise<Response> The final Response object
381
- */
382
- async unauthorized(message = "Unauthorized") {
383
- return this.status(401).json({
384
- error: message
385
- });
386
- }
387
- /**
388
- * Creates an error response with status 404
389
- * @param message Error message
390
- * @returns Promise<Response> The final Response object
391
- */
392
- async notFound(message = "Not found") {
393
- return this.status(404).json({
394
- error: message
395
- });
396
- }
397
- /**
398
- * Creates an error response with status 500
399
- * @param message Error message
400
- * @returns Promise<Response> The final Response object
401
- */
402
- async serverError(message = "Internal Server Error") {
403
- return this.status(500).json({
404
- error: message
405
- });
406
- }
161
+ static {
162
+ __name(this, "ServerResponse");
163
+ }
164
+ interceptors;
165
+ statusCode = 200;
166
+ responseBody = {};
167
+ responseHeaders = { "Content-Type": "application/json" };
168
+ /**
169
+ * Creates a new ServerResponse instance
170
+ * @param interceptors Array of interceptor functions that can modify the response
171
+ */
172
+ constructor(interceptors = []) {
173
+ this.interceptors = interceptors;
174
+ }
175
+ /**
176
+ * Sets the status code for the response
177
+ * @param code HTTP status code
178
+ * @returns this instance for chaining
179
+ */
180
+ status(code) {
181
+ this.statusCode = code;
182
+ return this;
183
+ }
184
+ /**
185
+ * Sets the response body and returns this instance (chainable method)
186
+ *
187
+ * @param body Response body
188
+ * @returns this instance for chaining
189
+ */
190
+ body(body) {
191
+ this.responseBody = body;
192
+ return this;
193
+ }
194
+ /**
195
+ * Adds a header to the response
196
+ * @param name Header name
197
+ * @param value Header value
198
+ * @returns this instance for chaining
199
+ */
200
+ header(name, value) {
201
+ this.responseHeaders[name] = value;
202
+ return this;
203
+ }
204
+ /**
205
+ * Adds multiple headers to the response
206
+ * @param headers Object containing headers
207
+ * @returns this instance for chaining
208
+ */
209
+ setHeaders(headers) {
210
+ this.responseHeaders = {
211
+ ...this.responseHeaders,
212
+ ...headers
213
+ };
214
+ return this;
215
+ }
216
+ /**
217
+ * Add an interceptor to the chain
218
+ * @param interceptor Function that takes a Response and returns a modified Response
219
+ * @returns this instance for chaining
220
+ */
221
+ use(interceptor) {
222
+ this.interceptors.push(interceptor);
223
+ return this;
224
+ }
225
+ /**
226
+ * Builds and returns the Response object after applying all interceptors
227
+ * @returns Promise<Response> The final Response object
228
+ * @private Internal method used by terminal methods
229
+ */
230
+ async buildResponse() {
231
+ let response2 = new Response(JSON.stringify(this.responseBody), {
232
+ status: this.statusCode,
233
+ headers: this.responseHeaders
234
+ });
235
+ for (const interceptor of this.interceptors) try {
236
+ const interceptedResponse = interceptor(response2);
237
+ if (interceptedResponse instanceof Promise) response2 = await interceptedResponse;
238
+ else response2 = interceptedResponse;
239
+ } catch (error) {
240
+ console.error("Error in interceptor:", error);
241
+ }
242
+ return response2;
243
+ }
244
+ /**
245
+ * Sets the response body to the JSON-stringified version of the provided value
246
+ * and sends the response (terminal method)
247
+ *
248
+ * @param body Response body to be JSON stringified
249
+ * @returns Promise<Response> The final Response object
250
+ */
251
+ async json(body) {
252
+ this.responseBody = body;
253
+ this.responseHeaders["Content-Type"] = "application/json";
254
+ return this.buildResponse();
255
+ }
256
+ /**
257
+ * Sends the response with the current configuration (terminal method)
258
+ *
259
+ * @param body Optional body to set before sending
260
+ * @returns Promise<Response> The final Response object
261
+ */
262
+ async send(body) {
263
+ if (body !== void 0) this.responseBody = body;
264
+ return this.buildResponse();
265
+ }
266
+ /**
267
+ * Sends a plain text response (terminal method)
268
+ *
269
+ * @param text Text to send
270
+ * @returns Promise<Response> The final Response object
271
+ */
272
+ async text(text) {
273
+ this.responseBody = text;
274
+ this.responseHeaders["Content-Type"] = "text/plain";
275
+ let response2 = new Response(text, {
276
+ status: this.statusCode,
277
+ headers: this.responseHeaders
278
+ });
279
+ for (const interceptor of this.interceptors) try {
280
+ const interceptedResponse = interceptor(response2);
281
+ if (interceptedResponse instanceof Promise) response2 = await interceptedResponse;
282
+ else response2 = interceptedResponse;
283
+ } catch (error) {
284
+ console.error("Error in interceptor:", error);
285
+ }
286
+ return response2;
287
+ }
288
+ /**
289
+ * Redirects to the specified URL (terminal method)
290
+ *
291
+ * @param url URL to redirect to
292
+ * @param statusCode HTTP status code (default: 302)
293
+ * @returns Promise<Response> The final Response object
294
+ */
295
+ async redirect(url, statusCode = 302) {
296
+ this.statusCode = statusCode;
297
+ this.responseHeaders["Location"] = url;
298
+ return this.buildResponse();
299
+ }
300
+ /**
301
+ * Creates a success response with status 200
302
+ * @param body Response body
303
+ * @returns Promise<Response> The final Response object
304
+ */
305
+ async success(body = {}) {
306
+ return this.status(200).json(body);
307
+ }
308
+ /**
309
+ * Creates an error response with status 400
310
+ * @param message Error message
311
+ * @param details Additional error details
312
+ * @returns Promise<Response> The final Response object
313
+ */
314
+ async badRequest(message, details = {}) {
315
+ return this.status(400).json({
316
+ error: message,
317
+ ...details
318
+ });
319
+ }
320
+ /**
321
+ * Creates an error response with status 403
322
+ * @param message Error message
323
+ * @returns Promise<Response> The final Response object
324
+ */
325
+ async notPermitted(message = "Not permitted") {
326
+ return this.status(403).json({ error: message });
327
+ }
328
+ /**
329
+ * Creates an error response with status 401
330
+ * @param message Error message
331
+ * @returns Promise<Response> The final Response object
332
+ */
333
+ async unauthorized(message = "Unauthorized") {
334
+ return this.status(401).json({ error: message });
335
+ }
336
+ /**
337
+ * Creates an error response with status 404
338
+ * @param message Error message
339
+ * @returns Promise<Response> The final Response object
340
+ */
341
+ async notFound(message = "Not found") {
342
+ return this.status(404).json({ error: message });
343
+ }
344
+ /**
345
+ * Creates an error response with status 500
346
+ * @param message Error message
347
+ * @returns Promise<Response> The final Response object
348
+ */
349
+ async serverError(message = "Internal Server Error") {
350
+ return this.status(500).json({ error: message });
351
+ }
407
352
  };
408
-
409
- // src/request/cors.ts
410
353
  function cors(res, options = {}) {
411
- const newHeaders = new Headers(res.headers);
412
- const requestOrigin = options.origin || "*";
413
- newHeaders.set("Access-Control-Allow-Origin", requestOrigin);
414
- if (options.credentials) {
415
- newHeaders.set("Access-Control-Allow-Credentials", "true");
416
- }
417
- if (options.exposedHeaders && options.exposedHeaders.length) {
418
- newHeaders.set("Access-Control-Expose-Headers", options.exposedHeaders.join(", "));
419
- }
420
- if (options.methods && options.methods.length) {
421
- newHeaders.set("Access-Control-Allow-Methods", options.methods.join(", "));
422
- } else {
423
- newHeaders.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
424
- }
425
- if (options.allowedHeaders && options.allowedHeaders.length) {
426
- newHeaders.set("Access-Control-Allow-Headers", options.allowedHeaders.join(", "));
427
- } else {
428
- newHeaders.set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
429
- }
430
- if (options.maxAge) {
431
- newHeaders.set("Access-Control-Max-Age", options.maxAge.toString());
432
- } else {
433
- newHeaders.set("Access-Control-Max-Age", "86400");
434
- }
435
- return new Response(res.body, {
436
- status: res.status,
437
- headers: newHeaders
438
- });
354
+ const newHeaders = new Headers(res.headers);
355
+ const requestOrigin = options.origin || "*";
356
+ newHeaders.set("Access-Control-Allow-Origin", requestOrigin);
357
+ if (options.credentials) newHeaders.set("Access-Control-Allow-Credentials", "true");
358
+ if (options.exposedHeaders && options.exposedHeaders.length) newHeaders.set("Access-Control-Expose-Headers", options.exposedHeaders.join(", "));
359
+ if (options.methods && options.methods.length) newHeaders.set("Access-Control-Allow-Methods", options.methods.join(", "));
360
+ else newHeaders.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
361
+ if (options.allowedHeaders && options.allowedHeaders.length) newHeaders.set("Access-Control-Allow-Headers", options.allowedHeaders.join(", "));
362
+ else newHeaders.set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
363
+ if (options.maxAge) newHeaders.set("Access-Control-Max-Age", options.maxAge.toString());
364
+ else newHeaders.set("Access-Control-Max-Age", "86400");
365
+ return new Response(res.body, {
366
+ status: res.status,
367
+ headers: newHeaders
368
+ });
439
369
  }
440
370
  __name(cors, "cors");
441
371
  function createCorsInterceptor(options = {}) {
442
- return (res) => cors(res, options);
372
+ return (res) => cors(res, options);
443
373
  }
444
374
  __name(createCorsInterceptor, "createCorsInterceptor");
445
-
446
- // src/server.ts
447
375
  var Message = z.object({
448
- action: z.string(),
449
- value: z.any()
376
+ action: z.string(),
377
+ value: z.any()
450
378
  });
451
379
  var Server = class {
452
- static {
453
- __name(this, "Server");
454
- }
455
- room;
456
- subRoom;
457
- rooms;
458
- /**
459
- * @constructor
460
- * @param {Party.Room} room - The room object representing the current game or application instance.
461
- *
462
- * @example
463
- * ```typescript
464
- * const server = new MyServer(new ServerIo("game"));
465
- * ```
466
- */
467
- constructor(room) {
468
- this.room = room;
469
- this.subRoom = null;
470
- this.rooms = [];
471
- }
472
- /**
473
- * @readonly
474
- * @property {boolean} isHibernate - Indicates whether the server is in hibernate mode.
475
- *
476
- * @example
477
- * ```typescript
478
- * if (!server.isHibernate) {
479
- * console.log("Server is active");
480
- * }
481
- * ```
482
- */
483
- get isHibernate() {
484
- return !!this["options"]?.hibernate;
485
- }
486
- get roomStorage() {
487
- return this.room.storage;
488
- }
489
- async send(conn, obj, subRoom) {
490
- obj = structuredClone(obj);
491
- if (subRoom.interceptorPacket) {
492
- const signal2 = this.getUsersProperty(subRoom);
493
- const { publicId } = conn.state;
494
- const user = signal2?.()[publicId];
495
- obj = await awaitReturn(subRoom["interceptorPacket"]?.(user, obj, conn));
496
- if (obj === null) return;
497
- }
498
- conn.send(JSON.stringify(obj));
499
- }
500
- broadcast(obj, subRoom) {
501
- for (let conn of this.room.getConnections()) {
502
- this.send(conn, obj, subRoom);
503
- }
504
- }
505
- /**
506
- * @method onStart
507
- * @async
508
- * @description Initializes the server and creates the initial room if not in hibernate mode.
509
- * @returns {Promise<void>}
510
- *
511
- * @example
512
- * ```typescript
513
- * async function initServer() {
514
- * await server.onStart();
515
- * console.log("Server started");
516
- * }
517
- * ```
518
- */
519
- async onStart() {
520
- if (!this.isHibernate) {
521
- this.subRoom = await this.createRoom();
522
- }
523
- }
524
- async runGarbageCollector() {
525
- await this.garbageCollector({
526
- sessionExpiryTime: -1
527
- });
528
- }
529
- async garbageCollector(options) {
530
- const subRoom = await this.getSubRoom();
531
- if (!subRoom) return;
532
- const activeConnections = [
533
- ...this.room.getConnections()
534
- ];
535
- const activePrivateIds = new Set(activeConnections.map((conn) => conn.id));
536
- try {
537
- const sessions = await this.room.storage.list();
538
- const users = this.getUsersProperty(subRoom);
539
- const usersPropName = this.getUsersPropName(subRoom);
540
- const validPublicIds = /* @__PURE__ */ new Set();
541
- const expiredPublicIds = /* @__PURE__ */ new Set();
542
- const SESSION_EXPIRY_TIME = options.sessionExpiryTime;
543
- const now = Date.now();
544
- for (const [key, session] of sessions) {
545
- if (!key.startsWith("session:")) continue;
546
- const privateId = key.replace("session:", "");
547
- const typedSession = session;
548
- if (!activePrivateIds.has(privateId) && !typedSession.connected && now - typedSession.created > SESSION_EXPIRY_TIME) {
549
- await this.deleteSession(privateId);
550
- expiredPublicIds.add(typedSession.publicId);
551
- } else if (typedSession && typedSession.publicId) {
552
- validPublicIds.add(typedSession.publicId);
553
- }
554
- }
555
- if (users && usersPropName) {
556
- const currentUsers = users();
557
- for (const publicId in currentUsers) {
558
- if (expiredPublicIds.has(publicId) && !validPublicIds.has(publicId)) {
559
- delete currentUsers[publicId];
560
- await this.room.storage.delete(`${usersPropName}.${publicId}`);
561
- }
562
- }
563
- }
564
- } catch (error) {
565
- console.error("Error in garbage collector:", error);
566
- }
567
- }
568
- /**
569
- * @method createRoom
570
- * @private
571
- * @async
572
- * @param {CreateRoomOptions} [options={}] - Options for creating the room.
573
- * @returns {Promise<Object>} The created room instance.
574
- *
575
- * @example
576
- * ```typescript
577
- * // This method is private and called internally
578
- * async function internalCreateRoom() {
579
- * const room = await this.createRoom({ getMemoryAll: true });
580
- * console.log("Room created:", room);
581
- * }
582
- * ```
583
- */
584
- async createRoom(options = {}) {
585
- let instance;
586
- let init = true;
587
- let initPersist = true;
588
- for (let room of this.rooms) {
589
- const params = extractParams(room.path, this.room.id);
590
- if (params) {
591
- instance = new room(this.room, params);
592
- break;
593
- }
594
- }
595
- if (!instance) {
596
- return null;
597
- }
598
- const loadMemory = /* @__PURE__ */ __name(async () => {
599
- const root = await this.room.storage.get(".");
600
- const memory = await this.room.storage.list();
601
- const tmpObject = root || {};
602
- for (let [key, value] of memory) {
603
- if (key.startsWith("session:")) {
604
- continue;
605
- }
606
- if (key == ".") {
607
- continue;
608
- }
609
- dset(tmpObject, key, value);
610
- }
611
- load(instance, tmpObject, true);
612
- }, "loadMemory");
613
- instance.$memoryAll = {};
614
- instance.$autoSync = instance["autoSync"] !== false;
615
- instance.$pendingSync = /* @__PURE__ */ new Map();
616
- instance.$pendingInitialSync = /* @__PURE__ */ new Map();
617
- instance.$send = (conn, obj) => {
618
- return this.send(conn, obj, instance);
619
- };
620
- instance.$broadcast = (obj) => {
621
- return this.broadcast(obj, instance);
622
- };
623
- instance.$applySync = () => {
624
- let packet;
625
- if (instance.$pendingSync.size > 0) {
626
- if (options.getMemoryAll) {
627
- buildObject(instance.$pendingSync, instance.$memoryAll);
628
- }
629
- packet = buildObject(instance.$pendingSync, instance.$memoryAll);
630
- instance.$pendingSync.clear();
631
- } else {
632
- packet = instance.$memoryAll;
633
- }
634
- const pendingConnections = new Set(instance.$pendingInitialSync.keys());
635
- for (const [conn, publicId] of instance.$pendingInitialSync) {
636
- this.send(conn, {
637
- type: "sync",
638
- value: {
639
- pId: publicId,
640
- ...packet
641
- }
642
- }, instance);
643
- }
644
- instance.$pendingInitialSync.clear();
645
- for (const conn of this.room.getConnections()) {
646
- if (!pendingConnections.has(conn)) {
647
- this.send(conn, {
648
- type: "sync",
649
- value: packet
650
- }, instance);
651
- }
652
- }
653
- };
654
- instance.$sessionTransfer = async (conn, targetRoomId) => {
655
- let user;
656
- const signal2 = this.getUsersProperty(instance);
657
- if (!signal2) {
658
- console.error("[sessionTransfer] `users` property not defined in the room.");
659
- return null;
660
- }
661
- const { publicId } = conn.state;
662
- user = signal2()[publicId];
663
- if (!user) {
664
- console.error(`[sessionTransfer] User with publicId ${publicId} not found.`);
665
- return null;
666
- }
667
- const sessions = await this.room.storage.list();
668
- let userSession = null;
669
- let privateId = null;
670
- for (const [key, session] of sessions) {
671
- if (key.startsWith("session:") && session.publicId === publicId) {
672
- userSession = session;
673
- privateId = key.replace("session:", "");
674
- break;
675
- }
676
- }
677
- if (!userSession || !privateId) {
678
- console.error(`[sessionTransfer] Session for publicId ${publicId} not found.`);
679
- return null;
680
- }
681
- const usersPropName = this.getUsersPropName(instance);
682
- if (!usersPropName) {
683
- console.error("[sessionTransfer] `users` property not defined in the room.");
684
- return null;
685
- }
686
- const userSnapshot = createStatesSnapshotDeep(user);
687
- const transferData = {
688
- privateId,
689
- userSnapshot,
690
- sessionState: userSession.state,
691
- publicId
692
- };
693
- try {
694
- const targetRoomParty = await this.room.context.parties.main.get(targetRoomId);
695
- const response2 = await targetRoomParty.fetch("/session-transfer", {
696
- method: "POST",
697
- body: JSON.stringify(transferData),
698
- headers: {
699
- "Content-Type": "application/json"
700
- }
701
- });
702
- if (!response2.ok) {
703
- throw new Error(`Transfer request failed: ${await response2.text()}`);
704
- }
705
- const { transferToken } = await response2.json();
706
- return transferToken;
707
- } catch (error) {
708
- console.error(`[sessionTransfer] Failed to transfer session to room ${targetRoomId}:`, error);
709
- return null;
710
- }
711
- };
712
- const syncCb = /* @__PURE__ */ __name((values) => {
713
- if (options.getMemoryAll) {
714
- buildObject(values, instance.$memoryAll);
715
- }
716
- if (init && this.isHibernate) {
717
- init = false;
718
- return;
719
- }
720
- if (!instance.$autoSync) {
721
- for (const [path, value] of values) {
722
- instance.$pendingSync.set(path, value);
723
- }
724
- values.clear();
725
- return;
726
- }
727
- const packet = buildObject(values, instance.$memoryAll);
728
- this.broadcast({
729
- type: "sync",
730
- value: packet
731
- }, instance);
732
- values.clear();
733
- }, "syncCb");
734
- const persistCb = /* @__PURE__ */ __name(async (values) => {
735
- if (initPersist) {
736
- values.clear();
737
- return;
738
- }
739
- for (let [path, value] of values) {
740
- const _instance = path == "." ? instance : getByPath(instance, path);
741
- const itemValue = createStatesSnapshot(_instance);
742
- if (value == DELETE_TOKEN) {
743
- await this.room.storage.delete(path);
744
- } else {
745
- await this.room.storage.put(path, itemValue);
746
- }
747
- }
748
- values.clear();
749
- }, "persistCb");
750
- syncClass(instance, {
751
- onSync: instance["throttleSync"] ? throttle(syncCb, instance["throttleSync"]) : syncCb,
752
- onPersist: instance["throttleStorage"] ? throttle(persistCb, instance["throttleStorage"]) : persistCb
753
- });
754
- await loadMemory();
755
- initPersist = false;
756
- init = false;
757
- return instance;
758
- }
759
- /**
760
- * @method getSubRoom
761
- * @private
762
- * @async
763
- * @param {Object} [options={}] - Options for getting the sub-room.
764
- * @returns {Promise<Object>} The sub-room instance.
765
- *
766
- * @example
767
- * ```typescript
768
- * // This method is private and called internally
769
- * async function internalGetSubRoom() {
770
- * const subRoom = await this.getSubRoom();
771
- * console.log("Sub-room retrieved:", subRoom);
772
- * }
773
- * ```
774
- */
775
- async getSubRoom(options = {}) {
776
- let subRoom;
777
- if (this.isHibernate) {
778
- subRoom = await this.createRoom(options);
779
- } else {
780
- subRoom = this.subRoom;
781
- }
782
- return subRoom;
783
- }
784
- /**
785
- * @method getUsersProperty
786
- * @private
787
- * @param {Object} subRoom - The sub-room instance.
788
- * @returns {Object|null} The users property of the sub-room, or null if not found.
789
- *
790
- * @example
791
- * ```typescript
792
- * // This method is private and called internally
793
- * function internalGetUsers(subRoom) {
794
- * const users = this.getUsersProperty(subRoom);
795
- * console.log("Users:", users);
796
- * }
797
- * ```
798
- */
799
- getUsersProperty(subRoom) {
800
- const meta = subRoom.constructor["_propertyMetadata"];
801
- const propId = meta?.get("users");
802
- if (propId) {
803
- return subRoom[propId];
804
- }
805
- return null;
806
- }
807
- getUsersPropName(subRoom) {
808
- if (!subRoom) return null;
809
- const metadata = subRoom.constructor._propertyMetadata;
810
- if (!metadata) return null;
811
- return metadata.get("users");
812
- }
813
- /**
814
- * Retrieves the connection status property from a user object.
815
- *
816
- * @param {any} user - The user object to get the connection property from.
817
- * @returns {Function|null} - The connection property signal function or null if not found.
818
- * @private
819
- */
820
- getUserConnectionProperty(user) {
821
- if (!user) return null;
822
- const metadata = user.constructor._propertyMetadata;
823
- if (!metadata) return null;
824
- const connectedPropName = metadata.get("connected");
825
- if (!connectedPropName) return null;
826
- return user[connectedPropName];
827
- }
828
- /**
829
- * Updates a user's connection status in the signal.
830
- *
831
- * @param {any} user - The user object to update.
832
- * @param {boolean} isConnected - The new connection status.
833
- * @returns {boolean} - Whether the update was successful.
834
- * @private
835
- */
836
- updateUserConnectionStatus(user, isConnected) {
837
- const connectionSignal = this.getUserConnectionProperty(user);
838
- if (connectionSignal) {
839
- connectionSignal.set(isConnected);
840
- return true;
841
- }
842
- return false;
843
- }
844
- /**
845
- * @method getSession
846
- * @private
847
- * @param {string} privateId - The private ID of the session.
848
- * @returns {Promise<Object|null>} The session object, or null if not found.
849
- *
850
- * @example
851
- * ```typescript
852
- * const session = await server.getSession("privateId");
853
- * console.log(session);
854
- * ```
855
- */
856
- async getSession(privateId) {
857
- if (!privateId) return null;
858
- try {
859
- const session = await this.room.storage.get(`session:${privateId}`);
860
- return session;
861
- } catch (e) {
862
- return null;
863
- }
864
- }
865
- async saveSession(privateId, data) {
866
- const sessionData = {
867
- ...data,
868
- created: data.created || Date.now(),
869
- connected: data.connected !== void 0 ? data.connected : true
870
- };
871
- await this.room.storage.put(`session:${privateId}`, sessionData);
872
- }
873
- async updateSessionConnection(privateId, connected) {
874
- const session = await this.getSession(privateId);
875
- if (session) {
876
- await this.saveSession(privateId, {
877
- ...session,
878
- connected
879
- });
880
- }
881
- }
882
- /**
883
- * @method deleteSession
884
- * @private
885
- * @param {string} privateId - The private ID of the session to delete.
886
- * @returns {Promise<void>}
887
- *
888
- * @example
889
- * ```typescript
890
- * await server.deleteSession("privateId");
891
- * ```
892
- */
893
- async deleteSession(privateId) {
894
- await this.room.storage.delete(`session:${privateId}`);
895
- }
896
- async onConnectClient(conn, ctx) {
897
- const subRoom = await this.getSubRoom({
898
- getMemoryAll: true
899
- });
900
- if (!subRoom) {
901
- conn.close();
902
- return;
903
- }
904
- const sessionExpiryTime = subRoom.constructor.sessionExpiryTime;
905
- await this.garbageCollector({
906
- sessionExpiryTime
907
- });
908
- const roomGuards = subRoom.constructor["_roomGuards"] || [];
909
- for (const guard of roomGuards) {
910
- const isAuthorized = await guard(conn, ctx, this.room);
911
- if (!isAuthorized) {
912
- conn.close();
913
- return;
914
- }
915
- }
916
- let transferToken = null;
917
- if (ctx.request?.url) {
918
- const url = new URL(ctx.request.url);
919
- transferToken = url.searchParams.get("transferToken");
920
- }
921
- let transferData = null;
922
- if (transferToken) {
923
- transferData = await this.room.storage.get(`transfer:${transferToken}`);
924
- if (transferData) {
925
- await this.room.storage.delete(`transfer:${transferToken}`);
926
- }
927
- }
928
- const existingSession = await this.getSession(conn.id);
929
- const publicId = existingSession?.publicId || transferData?.publicId || generateShortUUID$1();
930
- let user = null;
931
- const signal2 = this.getUsersProperty(subRoom);
932
- const usersPropName = this.getUsersPropName(subRoom);
933
- if (signal2) {
934
- const { classType } = signal2.options;
935
- if (!existingSession?.publicId) {
936
- if (transferData?.restored && signal2()[publicId]) {
937
- user = signal2()[publicId];
938
- } else {
939
- user = isClass(classType) ? new classType() : classType(conn, ctx);
940
- signal2()[publicId] = user;
941
- const snapshot = createStatesSnapshotDeep(user);
942
- this.room.storage.put(`${usersPropName}.${publicId}`, snapshot);
943
- }
944
- } else {
945
- user = signal2()[existingSession.publicId];
946
- }
947
- if (!existingSession) {
948
- const sessionPrivateId = transferData?.privateId || conn.id;
949
- await this.saveSession(sessionPrivateId, {
950
- publicId
951
- });
952
- } else {
953
- await this.updateSessionConnection(conn.id, true);
954
- }
955
- }
956
- this.updateUserConnectionStatus(user, true);
957
- conn.setState({
958
- ...conn.state,
959
- publicId
960
- });
961
- await awaitReturn(subRoom["onJoin"]?.(user, conn, ctx));
962
- if (subRoom.$autoSync) {
963
- this.send(conn, {
964
- type: "sync",
965
- value: {
966
- pId: publicId,
967
- ...subRoom.$memoryAll
968
- }
969
- }, subRoom);
970
- } else {
971
- subRoom.$pendingInitialSync.set(conn, publicId);
972
- }
973
- }
974
- /**
975
- * @method onConnect
976
- * @async
977
- * @param {Party.Connection} conn - The connection object for the new user.
978
- * @param {Party.ConnectionContext} ctx - The context of the connection.
979
- * @description Handles a new user connection, creates a user object, and sends initial sync data.
980
- * @returns {Promise<void>}
981
- *
982
- * @example
983
- * ```typescript
984
- * server.onConnect = async (conn, ctx) => {
985
- * await server.onConnect(conn, ctx);
986
- * console.log("New user connected:", conn.id);
987
- * };
988
- * ```
989
- */
990
- async onConnect(conn, ctx) {
991
- if (ctx.request?.headers.has("x-shard-id")) {
992
- this.onConnectShard(conn, ctx);
993
- } else {
994
- await this.onConnectClient(conn, ctx);
995
- }
996
- }
997
- /**
998
- * @method onConnectShard
999
- * @private
1000
- * @param {Party.Connection} conn - The connection object for the new shard.
1001
- * @param {Party.ConnectionContext} ctx - The context of the shard connection.
1002
- * @description Handles a new shard connection, setting up the necessary state.
1003
- * @returns {void}
1004
- */
1005
- onConnectShard(conn, ctx) {
1006
- const shardId = ctx.request?.headers.get("x-shard-id") || "unknown-shard";
1007
- conn.setState({
1008
- shard: true,
1009
- shardId,
1010
- clients: /* @__PURE__ */ new Map()
1011
- // Track clients connected through this shard
1012
- });
1013
- }
1014
- /**
1015
- * @method onMessage
1016
- * @async
1017
- * @param {string} message - The message received from a user or shard.
1018
- * @param {Party.Connection} sender - The connection object of the sender.
1019
- * @description Processes incoming messages, handling differently based on if sender is shard or client.
1020
- * @returns {Promise<void>}
1021
- */
1022
- async onMessage(message, sender) {
1023
- if (sender.state && sender.state.shard) {
1024
- await this.handleShardMessage(message, sender);
1025
- return;
1026
- }
1027
- let json;
1028
- try {
1029
- json = JSON.parse(message);
1030
- } catch (e) {
1031
- return;
1032
- }
1033
- const result = Message.safeParse(json);
1034
- if (!result.success) {
1035
- return;
1036
- }
1037
- const subRoom = await this.getSubRoom();
1038
- if (!subRoom) {
1039
- console.warn("Room not found");
1040
- return;
1041
- }
1042
- const roomGuards = subRoom.constructor["_roomGuards"] || [];
1043
- for (const guard of roomGuards) {
1044
- const isAuthorized = await guard(sender, result.data.value, this.room);
1045
- if (!isAuthorized) {
1046
- return;
1047
- }
1048
- }
1049
- const actions = subRoom.constructor["_actionMetadata"];
1050
- if (actions) {
1051
- const signal2 = this.getUsersProperty(subRoom);
1052
- const { publicId } = sender.state;
1053
- const user = signal2?.()[publicId];
1054
- const actionName = actions.get(result.data.action);
1055
- if (actionName) {
1056
- const guards = subRoom.constructor["_actionGuards"]?.get(actionName.key) || [];
1057
- for (const guard of guards) {
1058
- const isAuthorized = await guard(sender, result.data.value);
1059
- if (!isAuthorized) {
1060
- return;
1061
- }
1062
- }
1063
- if (actionName.bodyValidation) {
1064
- const bodyResult = actionName.bodyValidation.safeParse(result.data.value);
1065
- if (!bodyResult.success) {
1066
- return;
1067
- }
1068
- }
1069
- await awaitReturn(subRoom[actionName.key](user, result.data.value, sender));
1070
- }
1071
- }
1072
- }
1073
- /**
1074
- * @method handleShardMessage
1075
- * @private
1076
- * @async
1077
- * @param {string} message - The message received from a shard.
1078
- * @param {Party.Connection} shardConnection - The connection object of the shard.
1079
- * @description Processes messages from shards, extracting client information.
1080
- * @returns {Promise<void>}
1081
- */
1082
- async handleShardMessage(message, shardConnection) {
1083
- let parsedMessage;
1084
- try {
1085
- parsedMessage = JSON.parse(message);
1086
- } catch (e) {
1087
- console.error("Error parsing shard message:", e);
1088
- return;
1089
- }
1090
- const shardState = shardConnection.state;
1091
- shardState.clients;
1092
- switch (parsedMessage.type) {
1093
- case "shard.clientConnected":
1094
- await this.handleShardClientConnect(parsedMessage, shardConnection);
1095
- break;
1096
- case "shard.clientMessage":
1097
- await this.handleShardClientMessage(parsedMessage, shardConnection);
1098
- break;
1099
- case "shard.clientDisconnected":
1100
- await this.handleShardClientDisconnect(parsedMessage, shardConnection);
1101
- break;
1102
- default:
1103
- console.warn(`Unknown shard message type: ${parsedMessage.type}`);
1104
- }
1105
- }
1106
- /**
1107
- * @method handleShardClientConnect
1108
- * @private
1109
- * @async
1110
- * @param {Object} message - The client connection message from a shard.
1111
- * @param {Party.Connection} shardConnection - The connection object of the shard.
1112
- * @description Handles a new client connection via a shard.
1113
- * @returns {Promise<void>}
1114
- */
1115
- async handleShardClientConnect(message, shardConnection) {
1116
- const { privateId, requestInfo } = message;
1117
- const shardState = shardConnection.state;
1118
- const virtualContext = {
1119
- request: requestInfo ? {
1120
- headers: new Headers(requestInfo.headers),
1121
- method: requestInfo.method,
1122
- url: requestInfo.url
1123
- } : void 0
1124
- };
1125
- const virtualConnection = {
1126
- id: privateId,
1127
- send: /* @__PURE__ */ __name((data) => {
1128
- shardConnection.send(JSON.stringify({
1129
- targetClientId: privateId,
1130
- data
1131
- }));
1132
- }, "send"),
1133
- state: {},
1134
- setState: /* @__PURE__ */ __name((state) => {
1135
- const clients = shardState.clients;
1136
- const currentState = clients.get(privateId) || {};
1137
- const mergedState = Object.assign({}, currentState, state);
1138
- clients.set(privateId, mergedState);
1139
- virtualConnection.state = clients.get(privateId);
1140
- return virtualConnection.state;
1141
- }, "setState"),
1142
- close: /* @__PURE__ */ __name(() => {
1143
- shardConnection.send(JSON.stringify({
1144
- type: "shard.closeClient",
1145
- privateId
1146
- }));
1147
- if (shardState.clients) {
1148
- shardState.clients.delete(privateId);
1149
- }
1150
- }, "close")
1151
- };
1152
- if (!shardState.clients.has(privateId)) {
1153
- shardState.clients.set(privateId, {});
1154
- }
1155
- await this.onConnectClient(virtualConnection, virtualContext);
1156
- }
1157
- /**
1158
- * @method handleShardClientMessage
1159
- * @private
1160
- * @async
1161
- * @param {Object} message - The client message from a shard.
1162
- * @param {Party.Connection} shardConnection - The connection object of the shard.
1163
- * @description Handles a message from a client via a shard.
1164
- * @returns {Promise<void>}
1165
- */
1166
- async handleShardClientMessage(message, shardConnection) {
1167
- const { privateId, publicId, payload } = message;
1168
- const shardState = shardConnection.state;
1169
- const clients = shardState.clients;
1170
- if (!clients.has(privateId)) {
1171
- console.warn(`Received message from unknown client ${privateId}, creating virtual connection`);
1172
- clients.set(privateId, {
1173
- publicId
1174
- });
1175
- }
1176
- const virtualConnection = {
1177
- id: privateId,
1178
- send: /* @__PURE__ */ __name((data) => {
1179
- shardConnection.send(JSON.stringify({
1180
- targetClientId: privateId,
1181
- data
1182
- }));
1183
- }, "send"),
1184
- state: clients.get(privateId),
1185
- setState: /* @__PURE__ */ __name((state) => {
1186
- const currentState = clients.get(privateId) || {};
1187
- const mergedState = Object.assign({}, currentState, state);
1188
- clients.set(privateId, mergedState);
1189
- virtualConnection.state = clients.get(privateId);
1190
- return virtualConnection.state;
1191
- }, "setState"),
1192
- close: /* @__PURE__ */ __name(() => {
1193
- shardConnection.send(JSON.stringify({
1194
- type: "shard.closeClient",
1195
- privateId
1196
- }));
1197
- if (shardState.clients) {
1198
- shardState.clients.delete(privateId);
1199
- }
1200
- }, "close")
1201
- };
1202
- const payloadString = typeof payload === "string" ? payload : JSON.stringify(payload);
1203
- await this.onMessage(payloadString, virtualConnection);
1204
- }
1205
- /**
1206
- * @method handleShardClientDisconnect
1207
- * @private
1208
- * @async
1209
- * @param {Object} message - The client disconnection message from a shard.
1210
- * @param {Party.Connection} shardConnection - The connection object of the shard.
1211
- * @description Handles a client disconnection via a shard.
1212
- * @returns {Promise<void>}
1213
- */
1214
- async handleShardClientDisconnect(message, shardConnection) {
1215
- const { privateId, publicId } = message;
1216
- const shardState = shardConnection.state;
1217
- const clients = shardState.clients;
1218
- const clientState = clients.get(privateId);
1219
- if (!clientState) {
1220
- console.warn(`Disconnection for unknown client ${privateId}`);
1221
- return;
1222
- }
1223
- const virtualConnection = {
1224
- id: privateId,
1225
- send: /* @__PURE__ */ __name(() => {
1226
- }, "send"),
1227
- state: clientState,
1228
- setState: /* @__PURE__ */ __name(() => {
1229
- return {};
1230
- }, "setState"),
1231
- close: /* @__PURE__ */ __name(() => {
1232
- }, "close")
1233
- };
1234
- await this.onClose(virtualConnection);
1235
- clients.delete(privateId);
1236
- }
1237
- /**
1238
- * @method onClose
1239
- * @async
1240
- * @param {Party.Connection} conn - The connection object of the disconnecting user.
1241
- * @description Handles user disconnection, removing them from the room and triggering the onLeave event..
1242
- * @returns {Promise<void>}
1243
- *
1244
- * @example
1245
- * ```typescript
1246
- * server.onClose = async (conn) => {
1247
- * await server.onClose(conn);
1248
- * console.log("User disconnected:", conn.id);
1249
- * };
1250
- * ```
1251
- */
1252
- async onClose(conn) {
1253
- const subRoom = await this.getSubRoom();
1254
- if (!subRoom) {
1255
- return;
1256
- }
1257
- if (subRoom.$pendingInitialSync) {
1258
- subRoom.$pendingInitialSync.delete(conn);
1259
- }
1260
- const signal2 = this.getUsersProperty(subRoom);
1261
- if (!conn.state) {
1262
- return;
1263
- }
1264
- const privateId = conn.id;
1265
- const { publicId } = conn.state;
1266
- const user = signal2?.()[publicId];
1267
- if (!user) return;
1268
- await this.updateSessionConnection(privateId, false);
1269
- const connectionUpdated = this.updateUserConnectionStatus(user, false);
1270
- await awaitReturn(subRoom["onLeave"]?.(user, conn));
1271
- if (!connectionUpdated) {
1272
- this.broadcast({
1273
- type: "user_disconnected",
1274
- value: {
1275
- publicId
1276
- }
1277
- }, subRoom);
1278
- }
1279
- }
1280
- async onAlarm() {
1281
- const subRoom = await this.getSubRoom();
1282
- await awaitReturn(subRoom["onAlarm"]?.(subRoom));
1283
- }
1284
- async onError(connection, error) {
1285
- const subRoom = await this.getSubRoom();
1286
- await awaitReturn(subRoom["onError"]?.(connection, error));
1287
- }
1288
- /**
1289
- * @method onRequest
1290
- * @async
1291
- * @param {Party.Request} req - The HTTP request to handle
1292
- * @description Handles HTTP requests, either directly from clients or forwarded by shards
1293
- * @returns {Promise<Response>} The response to return to the client
1294
- */
1295
- async onRequest(req) {
1296
- const isFromShard = req.headers.has("x-forwarded-by-shard");
1297
- const shardId = req.headers.get("x-shard-id");
1298
- const res = new ServerResponse([
1299
- createCorsInterceptor()
1300
- ]);
1301
- if (req.method === "OPTIONS") {
1302
- return res.status(200).send({});
1303
- }
1304
- if (isFromShard) {
1305
- return this.handleShardRequest(req, res, shardId);
1306
- }
1307
- return this.handleDirectRequest(req, res);
1308
- }
1309
- /**
1310
- * @method handleSessionRestore
1311
- * @private
1312
- * @async
1313
- * @param {Party.Request} req - The HTTP request for session restore
1314
- * @param {ServerResponse} res - The response object
1315
- * @description Handles session restoration from transfer data, creates session from privateId
1316
- * @returns {Promise<Response>} The response to return to the client
1317
- */
1318
- async handleSessionRestore(req, res) {
1319
- try {
1320
- const transferData = await req.json();
1321
- const { privateId, userSnapshot, sessionState, publicId } = transferData;
1322
- if (!privateId || !publicId) {
1323
- return res.badRequest("Missing privateId or publicId in transfer data");
1324
- }
1325
- const subRoom = await this.getSubRoom();
1326
- if (!subRoom) {
1327
- return res.serverError("Room not available");
1328
- }
1329
- await this.saveSession(privateId, {
1330
- publicId,
1331
- state: sessionState,
1332
- created: Date.now(),
1333
- connected: false
1334
- // Will be set to true when user connects
1335
- });
1336
- if (userSnapshot) {
1337
- const signal2 = this.getUsersProperty(subRoom);
1338
- const usersPropName = this.getUsersPropName(subRoom);
1339
- if (signal2 && usersPropName) {
1340
- const { classType } = signal2.options;
1341
- const user = isClass(classType) ? new classType() : classType();
1342
- const hydratedSnapshot = await awaitReturn(subRoom["onSessionRestore"]?.({
1343
- userSnapshot,
1344
- user,
1345
- publicId,
1346
- privateId,
1347
- sessionState,
1348
- room: this.room
1349
- })) ?? userSnapshot;
1350
- signal2()[publicId] = user;
1351
- load(user, hydratedSnapshot, true);
1352
- await this.room.storage.put(`${usersPropName}.${publicId}`, userSnapshot);
1353
- }
1354
- }
1355
- const transferToken = generateShortUUID$1();
1356
- await this.room.storage.put(`transfer:${transferToken}`, {
1357
- privateId,
1358
- publicId,
1359
- restored: true
1360
- });
1361
- return res.success({
1362
- transferToken
1363
- });
1364
- } catch (error) {
1365
- console.error("Error restoring session:", error);
1366
- return res.serverError("Failed to restore session");
1367
- }
1368
- }
1369
- /**
1370
- * @method handleDirectRequest
1371
- * @private
1372
- * @async
1373
- * @param {Party.Request} req - The HTTP request received directly from a client
1374
- * @description Processes requests received directly from clients
1375
- * @returns {Promise<Response>} The response to return to the client
1376
- */
1377
- async handleDirectRequest(req, res) {
1378
- const subRoom = await this.getSubRoom();
1379
- if (!subRoom) {
1380
- return res.notFound();
1381
- }
1382
- const url = new URL(req.url);
1383
- if (url.pathname.endsWith("/session-transfer") && req.method === "POST") {
1384
- return this.handleSessionRestore(req, res);
1385
- }
1386
- const response2 = await this.tryMatchRequestHandler(req, res, subRoom);
1387
- if (response2) {
1388
- return response2;
1389
- }
1390
- const legacyResponse = await awaitReturn(subRoom["onRequest"]?.(req, res));
1391
- if (!legacyResponse) {
1392
- return res.notFound();
1393
- }
1394
- if (legacyResponse instanceof Response) {
1395
- return legacyResponse;
1396
- }
1397
- return res.success(legacyResponse);
1398
- }
1399
- /**
1400
- * @method tryMatchRequestHandler
1401
- * @private
1402
- * @async
1403
- * @param {Party.Request} req - The HTTP request to handle
1404
- * @param {Object} subRoom - The room instance
1405
- * @description Attempts to match the request to a registered @Request handler
1406
- * @returns {Promise<Response | null>} The response or null if no handler matched
1407
- */
1408
- async tryMatchRequestHandler(req, res, subRoom) {
1409
- const requestHandlers = subRoom.constructor["_requestMetadata"];
1410
- if (!requestHandlers) {
1411
- return null;
1412
- }
1413
- const url = new URL(req.url);
1414
- const method = req.method;
1415
- let pathname = url.pathname;
1416
- pathname = "/" + pathname.split("/").slice(4).join("/");
1417
- for (const [routeKey, handler] of requestHandlers.entries()) {
1418
- const firstColonIndex = routeKey.indexOf(":");
1419
- const handlerMethod = routeKey.substring(0, firstColonIndex);
1420
- const handlerPath = routeKey.substring(firstColonIndex + 1);
1421
- if (handlerMethod !== method) {
1422
- continue;
1423
- }
1424
- if (this.pathMatches(pathname, handlerPath)) {
1425
- const params = this.extractPathParams(pathname, handlerPath);
1426
- const guards = subRoom.constructor["_actionGuards"]?.get(handler.key) || [];
1427
- for (const guard of guards) {
1428
- const isAuthorized = await guard(null, req, this.room);
1429
- if (isAuthorized instanceof Response) {
1430
- return isAuthorized;
1431
- }
1432
- if (!isAuthorized) {
1433
- return res.notPermitted();
1434
- }
1435
- }
1436
- let bodyData = null;
1437
- if (handler.bodyValidation && [
1438
- "POST",
1439
- "PUT",
1440
- "PATCH"
1441
- ].includes(method)) {
1442
- try {
1443
- const contentType = req.headers.get("content-type") || "";
1444
- if (contentType.includes("application/json")) {
1445
- const body = await req.json();
1446
- const validation = handler.bodyValidation.safeParse(body);
1447
- if (!validation.success) {
1448
- return res.badRequest("Invalid request body", {
1449
- details: validation.error
1450
- });
1451
- }
1452
- bodyData = validation.data;
1453
- }
1454
- } catch (error) {
1455
- return res.badRequest("Failed to parse request body");
1456
- }
1457
- }
1458
- try {
1459
- req["data"] = bodyData;
1460
- req["params"] = params;
1461
- const result = await awaitReturn(subRoom[handler.key](req, res));
1462
- if (result instanceof Response) {
1463
- return result;
1464
- }
1465
- return res.success(result);
1466
- } catch (error) {
1467
- console.error("Error executing request handler:", error);
1468
- return res.serverError();
1469
- }
1470
- }
1471
- }
1472
- return null;
1473
- }
1474
- /**
1475
- * @method pathMatches
1476
- * @private
1477
- * @param {string} requestPath - The path from the request
1478
- * @param {string} handlerPath - The path pattern from the handler
1479
- * @description Checks if a request path matches a handler path pattern
1480
- * @returns {boolean} True if the paths match
1481
- */
1482
- pathMatches(requestPath, handlerPath) {
1483
- const pathRegexString = handlerPath.replace(/\//g, "\\/").replace(/:([^\/]+)/g, "([^/]+)");
1484
- const pathRegex = new RegExp(`^${pathRegexString}`);
1485
- return pathRegex.test(requestPath);
1486
- }
1487
- /**
1488
- * @method extractPathParams
1489
- * @private
1490
- * @param {string} requestPath - The path from the request
1491
- * @param {string} handlerPath - The path pattern from the handler
1492
- * @description Extracts path parameters from the request path based on the handler pattern
1493
- * @returns {Object} An object containing the path parameters
1494
- */
1495
- extractPathParams(requestPath, handlerPath) {
1496
- const params = {};
1497
- const paramNames = [];
1498
- handlerPath.split("/").forEach((segment) => {
1499
- if (segment.startsWith(":")) {
1500
- paramNames.push(segment.substring(1));
1501
- }
1502
- });
1503
- const pathRegexString = handlerPath.replace(/\//g, "\\/").replace(/:([^\/]+)/g, "([^/]+)");
1504
- const pathRegex = new RegExp(`^${pathRegexString}`);
1505
- const matches = requestPath.match(pathRegex);
1506
- if (matches && matches.length > 1) {
1507
- for (let i = 0; i < paramNames.length; i++) {
1508
- params[paramNames[i]] = matches[i + 1];
1509
- }
1510
- }
1511
- return params;
1512
- }
1513
- /**
1514
- * @method handleShardRequest
1515
- * @private
1516
- * @async
1517
- * @param {Party.Request} req - The HTTP request forwarded by a shard
1518
- * @param {string | null} shardId - The ID of the shard that forwarded the request
1519
- * @description Processes requests forwarded by shards, preserving client context
1520
- * @returns {Promise<Response>} The response to return to the shard (which will forward it to the client)
1521
- */
1522
- async handleShardRequest(req, res, shardId) {
1523
- const subRoom = await this.getSubRoom();
1524
- if (!subRoom) {
1525
- return res.notFound();
1526
- }
1527
- const originalClientIp = req.headers.get("x-original-client-ip");
1528
- const enhancedReq = this.createEnhancedRequest(req, originalClientIp);
1529
- try {
1530
- const response2 = await this.tryMatchRequestHandler(enhancedReq, res, subRoom);
1531
- if (response2) {
1532
- return response2;
1533
- }
1534
- const legacyResponse = await awaitReturn(subRoom["onRequest"]?.(enhancedReq, res));
1535
- if (!legacyResponse) {
1536
- return res.notFound();
1537
- }
1538
- if (legacyResponse instanceof Response) {
1539
- return legacyResponse;
1540
- }
1541
- return res.success(legacyResponse);
1542
- } catch (error) {
1543
- console.error(`Error processing request from shard ${shardId}:`, error);
1544
- return res.serverError();
1545
- }
1546
- }
1547
- /**
1548
- * @method createEnhancedRequest
1549
- * @private
1550
- * @param {Party.Request} originalReq - The original request received from the shard
1551
- * @param {string | null} originalClientIp - The original client IP, if available
1552
- * @description Creates an enhanced request object that preserves the original client context
1553
- * @returns {Party.Request} The enhanced request object
1554
- */
1555
- createEnhancedRequest(originalReq, originalClientIp) {
1556
- const clonedReq = originalReq.clone();
1557
- clonedReq.viaShard = true;
1558
- if (originalClientIp) {
1559
- clonedReq.originalClientIp = originalClientIp;
1560
- }
1561
- return clonedReq;
1562
- }
380
+ static {
381
+ __name(this, "Server");
382
+ }
383
+ room;
384
+ subRoom;
385
+ rooms;
386
+ /**
387
+ * @constructor
388
+ * @param {Party.Room} room - The room object representing the current game or application instance.
389
+ *
390
+ * @example
391
+ * ```typescript
392
+ * const server = new MyServer(new ServerIo("game"));
393
+ * ```
394
+ */
395
+ constructor(room) {
396
+ this.room = room;
397
+ this.subRoom = null;
398
+ this.rooms = [];
399
+ }
400
+ /**
401
+ * @readonly
402
+ * @property {boolean} isHibernate - Indicates whether the server is in hibernate mode.
403
+ *
404
+ * @example
405
+ * ```typescript
406
+ * if (!server.isHibernate) {
407
+ * console.log("Server is active");
408
+ * }
409
+ * ```
410
+ */
411
+ get isHibernate() {
412
+ return !!this["options"]?.hibernate;
413
+ }
414
+ get roomStorage() {
415
+ return this.room.storage;
416
+ }
417
+ async send(conn, obj, subRoom) {
418
+ obj = structuredClone(obj);
419
+ if (subRoom.interceptorPacket) {
420
+ const signal2 = this.getUsersProperty(subRoom);
421
+ const { publicId } = conn.state;
422
+ const user = signal2?.()[publicId];
423
+ obj = await awaitReturn(subRoom["interceptorPacket"]?.(user, obj, conn));
424
+ if (obj === null) return;
425
+ }
426
+ conn.send(JSON.stringify(obj));
427
+ }
428
+ broadcast(obj, subRoom) {
429
+ for (let conn of this.room.getConnections()) this.send(conn, obj, subRoom);
430
+ }
431
+ /**
432
+ * @method onStart
433
+ * @async
434
+ * @description Initializes the server and creates the initial room if not in hibernate mode.
435
+ * @returns {Promise<void>}
436
+ *
437
+ * @example
438
+ * ```typescript
439
+ * async function initServer() {
440
+ * await server.onStart();
441
+ * console.log("Server started");
442
+ * }
443
+ * ```
444
+ */
445
+ async onStart() {
446
+ if (!this.isHibernate) this.subRoom = await this.createRoom();
447
+ }
448
+ async runGarbageCollector() {
449
+ await this.garbageCollector({ sessionExpiryTime: -1 });
450
+ }
451
+ async garbageCollector(options) {
452
+ const subRoom = await this.getSubRoom();
453
+ if (!subRoom) return;
454
+ const activeConnections = [...this.room.getConnections()];
455
+ const activePrivateIds = new Set(activeConnections.map((conn) => conn.id));
456
+ try {
457
+ const sessions = await this.room.storage.list();
458
+ const users = this.getUsersProperty(subRoom);
459
+ const usersPropName = this.getUsersPropName(subRoom);
460
+ const validPublicIds = /* @__PURE__ */ new Set();
461
+ const expiredPublicIds = /* @__PURE__ */ new Set();
462
+ const SESSION_EXPIRY_TIME = options.sessionExpiryTime;
463
+ const now = Date.now();
464
+ for (const [key, session] of sessions) {
465
+ if (!key.startsWith("session:")) continue;
466
+ const privateId = key.replace("session:", "");
467
+ const typedSession = session;
468
+ if (!activePrivateIds.has(privateId) && !typedSession.connected && now - typedSession.created > SESSION_EXPIRY_TIME) {
469
+ await this.deleteSession(privateId);
470
+ expiredPublicIds.add(typedSession.publicId);
471
+ } else if (typedSession && typedSession.publicId) validPublicIds.add(typedSession.publicId);
472
+ }
473
+ if (users && usersPropName) {
474
+ const currentUsers = users();
475
+ for (const publicId in currentUsers) if (expiredPublicIds.has(publicId) && !validPublicIds.has(publicId)) {
476
+ delete currentUsers[publicId];
477
+ await this.room.storage.delete(`${usersPropName}.${publicId}`);
478
+ }
479
+ }
480
+ } catch (error) {
481
+ console.error("Error in garbage collector:", error);
482
+ }
483
+ }
484
+ /**
485
+ * @method createRoom
486
+ * @private
487
+ * @async
488
+ * @param {CreateRoomOptions} [options={}] - Options for creating the room.
489
+ * @returns {Promise<Object>} The created room instance.
490
+ *
491
+ * @example
492
+ * ```typescript
493
+ * // This method is private and called internally
494
+ * async function internalCreateRoom() {
495
+ * const room = await this.createRoom({ getMemoryAll: true });
496
+ * console.log("Room created:", room);
497
+ * }
498
+ * ```
499
+ */
500
+ async createRoom(options = {}) {
501
+ let instance;
502
+ let init = true;
503
+ let initPersist = true;
504
+ for (let room of this.rooms) {
505
+ const params = extractParams(room.path, this.room.id);
506
+ if (params) {
507
+ instance = new room(this.room, params);
508
+ break;
509
+ }
510
+ }
511
+ if (!instance) return null;
512
+ const loadMemory = /* @__PURE__ */ __name(async () => {
513
+ const root = await this.room.storage.get(".");
514
+ const memory = await this.room.storage.list();
515
+ const tmpObject = root || {};
516
+ for (let [key, value] of memory) {
517
+ if (key.startsWith("session:")) continue;
518
+ if (key == ".") continue;
519
+ dset(tmpObject, key, value);
520
+ }
521
+ load(instance, tmpObject, true);
522
+ }, "loadMemory");
523
+ instance.$memoryAll = {};
524
+ instance.$autoSync = instance["autoSync"] !== false;
525
+ instance.$pendingSync = /* @__PURE__ */ new Map();
526
+ instance.$pendingInitialSync = /* @__PURE__ */ new Map();
527
+ instance.$send = (conn, obj) => {
528
+ return this.send(conn, obj, instance);
529
+ };
530
+ instance.$broadcast = (obj) => {
531
+ return this.broadcast(obj, instance);
532
+ };
533
+ instance.$applySync = () => {
534
+ let packet;
535
+ if (instance.$pendingSync.size > 0) {
536
+ if (options.getMemoryAll) buildObject(instance.$pendingSync, instance.$memoryAll);
537
+ packet = buildObject(instance.$pendingSync, instance.$memoryAll);
538
+ instance.$pendingSync.clear();
539
+ } else packet = instance.$memoryAll;
540
+ const pendingConnections = new Set(instance.$pendingInitialSync.keys());
541
+ for (const [conn, publicId] of instance.$pendingInitialSync) this.send(conn, {
542
+ type: "sync",
543
+ value: {
544
+ pId: publicId,
545
+ ...packet
546
+ }
547
+ }, instance);
548
+ instance.$pendingInitialSync.clear();
549
+ for (const conn of this.room.getConnections()) if (!pendingConnections.has(conn)) this.send(conn, {
550
+ type: "sync",
551
+ value: packet
552
+ }, instance);
553
+ };
554
+ instance.$sessionTransfer = async (conn, targetRoomId) => {
555
+ let user;
556
+ const signal2 = this.getUsersProperty(instance);
557
+ if (!signal2) {
558
+ console.error("[sessionTransfer] `users` property not defined in the room.");
559
+ return null;
560
+ }
561
+ const { publicId } = conn.state;
562
+ user = signal2()[publicId];
563
+ if (!user) {
564
+ console.error(`[sessionTransfer] User with publicId ${publicId} not found.`);
565
+ return null;
566
+ }
567
+ const sessions = await this.room.storage.list();
568
+ let userSession = null;
569
+ let privateId = null;
570
+ for (const [key, session] of sessions) if (key.startsWith("session:") && session.publicId === publicId) {
571
+ userSession = session;
572
+ privateId = key.replace("session:", "");
573
+ break;
574
+ }
575
+ if (!userSession || !privateId) {
576
+ console.error(`[sessionTransfer] Session for publicId ${publicId} not found.`);
577
+ return null;
578
+ }
579
+ if (!this.getUsersPropName(instance)) {
580
+ console.error("[sessionTransfer] `users` property not defined in the room.");
581
+ return null;
582
+ }
583
+ const userSnapshot = createStatesSnapshotDeep(user);
584
+ const transferData = {
585
+ privateId,
586
+ userSnapshot,
587
+ sessionState: userSession.state,
588
+ publicId
589
+ };
590
+ try {
591
+ const response2 = await (await this.room.context.parties.main.get(targetRoomId)).fetch("/session-transfer", {
592
+ method: "POST",
593
+ body: JSON.stringify(transferData),
594
+ headers: { "Content-Type": "application/json" }
595
+ });
596
+ if (!response2.ok) throw new Error(`Transfer request failed: ${await response2.text()}`);
597
+ const { transferToken } = await response2.json();
598
+ return transferToken;
599
+ } catch (error) {
600
+ console.error(`[sessionTransfer] Failed to transfer session to room ${targetRoomId}:`, error);
601
+ return null;
602
+ }
603
+ };
604
+ const syncCb = /* @__PURE__ */ __name((values) => {
605
+ if (options.getMemoryAll) buildObject(values, instance.$memoryAll);
606
+ if (init && this.isHibernate) {
607
+ init = false;
608
+ return;
609
+ }
610
+ if (!instance.$autoSync) {
611
+ for (const [path, value] of values) instance.$pendingSync.set(path, value);
612
+ values.clear();
613
+ return;
614
+ }
615
+ const packet = buildObject(values, instance.$memoryAll);
616
+ this.broadcast({
617
+ type: "sync",
618
+ value: packet
619
+ }, instance);
620
+ values.clear();
621
+ }, "syncCb");
622
+ const persistCb = /* @__PURE__ */ __name(async (values) => {
623
+ if (initPersist) {
624
+ values.clear();
625
+ return;
626
+ }
627
+ for (let [path, value] of values) {
628
+ const itemValue = createStatesSnapshot(path == "." ? instance : getByPath(instance, path));
629
+ if (value == "$delete") await this.room.storage.delete(path);
630
+ else await this.room.storage.put(path, itemValue);
631
+ }
632
+ values.clear();
633
+ }, "persistCb");
634
+ syncClass(instance, {
635
+ onSync: instance["throttleSync"] ? throttle(syncCb, instance["throttleSync"]) : syncCb,
636
+ onPersist: instance["throttleStorage"] ? throttle(persistCb, instance["throttleStorage"]) : persistCb
637
+ });
638
+ await loadMemory();
639
+ initPersist = false;
640
+ init = false;
641
+ return instance;
642
+ }
643
+ /**
644
+ * @method getSubRoom
645
+ * @private
646
+ * @async
647
+ * @param {Object} [options={}] - Options for getting the sub-room.
648
+ * @returns {Promise<Object>} The sub-room instance.
649
+ *
650
+ * @example
651
+ * ```typescript
652
+ * // This method is private and called internally
653
+ * async function internalGetSubRoom() {
654
+ * const subRoom = await this.getSubRoom();
655
+ * console.log("Sub-room retrieved:", subRoom);
656
+ * }
657
+ * ```
658
+ */
659
+ async getSubRoom(options = {}) {
660
+ let subRoom;
661
+ if (this.isHibernate) subRoom = await this.createRoom(options);
662
+ else subRoom = this.subRoom;
663
+ return subRoom;
664
+ }
665
+ /**
666
+ * @method getUsersProperty
667
+ * @private
668
+ * @param {Object} subRoom - The sub-room instance.
669
+ * @returns {Object|null} The users property of the sub-room, or null if not found.
670
+ *
671
+ * @example
672
+ * ```typescript
673
+ * // This method is private and called internally
674
+ * function internalGetUsers(subRoom) {
675
+ * const users = this.getUsersProperty(subRoom);
676
+ * console.log("Users:", users);
677
+ * }
678
+ * ```
679
+ */
680
+ getUsersProperty(subRoom) {
681
+ const propId = subRoom.constructor["_propertyMetadata"]?.get("users");
682
+ if (propId) return subRoom[propId];
683
+ return null;
684
+ }
685
+ getUsersPropName(subRoom) {
686
+ if (!subRoom) return null;
687
+ const metadata = subRoom.constructor._propertyMetadata;
688
+ if (!metadata) return null;
689
+ return metadata.get("users");
690
+ }
691
+ /**
692
+ * Retrieves the connection status property from a user object.
693
+ *
694
+ * @param {any} user - The user object to get the connection property from.
695
+ * @returns {Function|null} - The connection property signal function or null if not found.
696
+ * @private
697
+ */
698
+ getUserConnectionProperty(user) {
699
+ if (!user) return null;
700
+ const metadata = user.constructor._propertyMetadata;
701
+ if (!metadata) return null;
702
+ const connectedPropName = metadata.get("connected");
703
+ if (!connectedPropName) return null;
704
+ return user[connectedPropName];
705
+ }
706
+ /**
707
+ * Updates a user's connection status in the signal.
708
+ *
709
+ * @param {any} user - The user object to update.
710
+ * @param {boolean} isConnected - The new connection status.
711
+ * @returns {boolean} - Whether the update was successful.
712
+ * @private
713
+ */
714
+ updateUserConnectionStatus(user, isConnected) {
715
+ const connectionSignal = this.getUserConnectionProperty(user);
716
+ if (connectionSignal) {
717
+ connectionSignal.set(isConnected);
718
+ return true;
719
+ }
720
+ return false;
721
+ }
722
+ /**
723
+ * @method getSession
724
+ * @private
725
+ * @param {string} privateId - The private ID of the session.
726
+ * @returns {Promise<Object|null>} The session object, or null if not found.
727
+ *
728
+ * @example
729
+ * ```typescript
730
+ * const session = await server.getSession("privateId");
731
+ * console.log(session);
732
+ * ```
733
+ */
734
+ async getSession(privateId) {
735
+ if (!privateId) return null;
736
+ try {
737
+ return await this.room.storage.get(`session:${privateId}`);
738
+ } catch (e) {
739
+ return null;
740
+ }
741
+ }
742
+ async saveSession(privateId, data) {
743
+ const sessionData = {
744
+ ...data,
745
+ created: data.created || Date.now(),
746
+ connected: data.connected !== void 0 ? data.connected : true
747
+ };
748
+ await this.room.storage.put(`session:${privateId}`, sessionData);
749
+ }
750
+ async updateSessionConnection(privateId, connected) {
751
+ const session = await this.getSession(privateId);
752
+ if (session) await this.saveSession(privateId, {
753
+ ...session,
754
+ connected
755
+ });
756
+ }
757
+ /**
758
+ * @method deleteSession
759
+ * @private
760
+ * @param {string} privateId - The private ID of the session to delete.
761
+ * @returns {Promise<void>}
762
+ *
763
+ * @example
764
+ * ```typescript
765
+ * await server.deleteSession("privateId");
766
+ * ```
767
+ */
768
+ async deleteSession(privateId) {
769
+ await this.room.storage.delete(`session:${privateId}`);
770
+ }
771
+ async onConnectClient(conn, ctx) {
772
+ const subRoom = await this.getSubRoom({ getMemoryAll: true });
773
+ if (!subRoom) {
774
+ conn.close();
775
+ return;
776
+ }
777
+ const sessionExpiryTime = subRoom.constructor.sessionExpiryTime;
778
+ await this.garbageCollector({ sessionExpiryTime });
779
+ const roomGuards = subRoom.constructor["_roomGuards"] || [];
780
+ for (const guard of roomGuards) if (!await guard(conn, ctx, this.room)) {
781
+ conn.close();
782
+ return;
783
+ }
784
+ let transferToken = null;
785
+ if (ctx.request?.url) transferToken = new URL(ctx.request.url).searchParams.get("transferToken");
786
+ let transferData = null;
787
+ if (transferToken) {
788
+ transferData = await this.room.storage.get(`transfer:${transferToken}`);
789
+ if (transferData) await this.room.storage.delete(`transfer:${transferToken}`);
790
+ }
791
+ const existingSession = await this.getSession(conn.id);
792
+ const publicId = existingSession?.publicId || transferData?.publicId || generateShortUUID$1();
793
+ let user = null;
794
+ const signal2 = this.getUsersProperty(subRoom);
795
+ const usersPropName = this.getUsersPropName(subRoom);
796
+ if (signal2) {
797
+ const { classType } = signal2.options;
798
+ if (!existingSession?.publicId) if (transferData?.restored && signal2()[publicId]) user = signal2()[publicId];
799
+ else {
800
+ user = isClass(classType) ? new classType() : classType(conn, ctx);
801
+ signal2()[publicId] = user;
802
+ const snapshot = createStatesSnapshotDeep(user);
803
+ this.room.storage.put(`${usersPropName}.${publicId}`, snapshot);
804
+ }
805
+ else user = signal2()[existingSession.publicId];
806
+ if (!existingSession) {
807
+ const sessionPrivateId = transferData?.privateId || conn.id;
808
+ await this.saveSession(sessionPrivateId, { publicId });
809
+ } else await this.updateSessionConnection(conn.id, true);
810
+ }
811
+ this.updateUserConnectionStatus(user, true);
812
+ conn.setState({
813
+ ...conn.state,
814
+ publicId
815
+ });
816
+ await awaitReturn(subRoom["onJoin"]?.(user, conn, ctx));
817
+ if (subRoom.$autoSync) this.send(conn, {
818
+ type: "sync",
819
+ value: {
820
+ pId: publicId,
821
+ ...subRoom.$memoryAll
822
+ }
823
+ }, subRoom);
824
+ else subRoom.$pendingInitialSync.set(conn, publicId);
825
+ }
826
+ /**
827
+ * @method onConnect
828
+ * @async
829
+ * @param {Party.Connection} conn - The connection object for the new user.
830
+ * @param {Party.ConnectionContext} ctx - The context of the connection.
831
+ * @description Handles a new user connection, creates a user object, and sends initial sync data.
832
+ * @returns {Promise<void>}
833
+ *
834
+ * @example
835
+ * ```typescript
836
+ * server.onConnect = async (conn, ctx) => {
837
+ * await server.onConnect(conn, ctx);
838
+ * console.log("New user connected:", conn.id);
839
+ * };
840
+ * ```
841
+ */
842
+ async onConnect(conn, ctx) {
843
+ if (ctx.request?.headers.has("x-shard-id")) this.onConnectShard(conn, ctx);
844
+ else await this.onConnectClient(conn, ctx);
845
+ }
846
+ /**
847
+ * @method onConnectShard
848
+ * @private
849
+ * @param {Party.Connection} conn - The connection object for the new shard.
850
+ * @param {Party.ConnectionContext} ctx - The context of the shard connection.
851
+ * @description Handles a new shard connection, setting up the necessary state.
852
+ * @returns {void}
853
+ */
854
+ onConnectShard(conn, ctx) {
855
+ const shardId = ctx.request?.headers.get("x-shard-id") || "unknown-shard";
856
+ conn.setState({
857
+ shard: true,
858
+ shardId,
859
+ clients: /* @__PURE__ */ new Map()
860
+ });
861
+ }
862
+ /**
863
+ * @method onMessage
864
+ * @async
865
+ * @param {string} message - The message received from a user or shard.
866
+ * @param {Party.Connection} sender - The connection object of the sender.
867
+ * @description Processes incoming messages, handling differently based on if sender is shard or client.
868
+ * @returns {Promise<void>}
869
+ */
870
+ async onMessage(message, sender) {
871
+ if (sender.state && sender.state.shard) {
872
+ await this.handleShardMessage(message, sender);
873
+ return;
874
+ }
875
+ let json;
876
+ try {
877
+ json = JSON.parse(message);
878
+ } catch (e) {
879
+ return;
880
+ }
881
+ const result = Message.safeParse(json);
882
+ if (!result.success) return;
883
+ const subRoom = await this.getSubRoom();
884
+ if (!subRoom) {
885
+ console.warn("Room not found");
886
+ return;
887
+ }
888
+ const roomGuards = subRoom.constructor["_roomGuards"] || [];
889
+ for (const guard of roomGuards) if (!await guard(sender, result.data.value, this.room)) return;
890
+ const actions = subRoom.constructor["_actionMetadata"];
891
+ if (actions) {
892
+ const signal2 = this.getUsersProperty(subRoom);
893
+ const { publicId } = sender.state;
894
+ const user = signal2?.()[publicId];
895
+ const actionName = actions.get(result.data.action);
896
+ if (actionName) {
897
+ const guards = subRoom.constructor["_actionGuards"]?.get(actionName.key) || [];
898
+ for (const guard of guards) if (!await guard(sender, result.data.value)) return;
899
+ if (actionName.bodyValidation) {
900
+ if (!actionName.bodyValidation.safeParse(result.data.value).success) return;
901
+ }
902
+ await awaitReturn(subRoom[actionName.key](user, result.data.value, sender));
903
+ }
904
+ }
905
+ }
906
+ /**
907
+ * @method handleShardMessage
908
+ * @private
909
+ * @async
910
+ * @param {string} message - The message received from a shard.
911
+ * @param {Party.Connection} shardConnection - The connection object of the shard.
912
+ * @description Processes messages from shards, extracting client information.
913
+ * @returns {Promise<void>}
914
+ */
915
+ async handleShardMessage(message, shardConnection) {
916
+ let parsedMessage;
917
+ try {
918
+ parsedMessage = JSON.parse(message);
919
+ } catch (e) {
920
+ console.error("Error parsing shard message:", e);
921
+ return;
922
+ }
923
+ shardConnection.state.clients;
924
+ switch (parsedMessage.type) {
925
+ case "shard.clientConnected":
926
+ await this.handleShardClientConnect(parsedMessage, shardConnection);
927
+ break;
928
+ case "shard.clientMessage":
929
+ await this.handleShardClientMessage(parsedMessage, shardConnection);
930
+ break;
931
+ case "shard.clientDisconnected":
932
+ await this.handleShardClientDisconnect(parsedMessage, shardConnection);
933
+ break;
934
+ default: console.warn(`Unknown shard message type: ${parsedMessage.type}`);
935
+ }
936
+ }
937
+ /**
938
+ * @method handleShardClientConnect
939
+ * @private
940
+ * @async
941
+ * @param {Object} message - The client connection message from a shard.
942
+ * @param {Party.Connection} shardConnection - The connection object of the shard.
943
+ * @description Handles a new client connection via a shard.
944
+ * @returns {Promise<void>}
945
+ */
946
+ async handleShardClientConnect(message, shardConnection) {
947
+ const { privateId, requestInfo } = message;
948
+ const shardState = shardConnection.state;
949
+ const virtualContext = { request: requestInfo ? {
950
+ headers: new Headers(requestInfo.headers),
951
+ method: requestInfo.method,
952
+ url: requestInfo.url
953
+ } : void 0 };
954
+ const virtualConnection = {
955
+ id: privateId,
956
+ send: /* @__PURE__ */ __name((data) => {
957
+ shardConnection.send(JSON.stringify({
958
+ targetClientId: privateId,
959
+ data
960
+ }));
961
+ }, "send"),
962
+ state: {},
963
+ setState: /* @__PURE__ */ __name((state) => {
964
+ const clients = shardState.clients;
965
+ const currentState = clients.get(privateId) || {};
966
+ const mergedState = Object.assign({}, currentState, state);
967
+ clients.set(privateId, mergedState);
968
+ virtualConnection.state = clients.get(privateId);
969
+ return virtualConnection.state;
970
+ }, "setState"),
971
+ close: /* @__PURE__ */ __name(() => {
972
+ shardConnection.send(JSON.stringify({
973
+ type: "shard.closeClient",
974
+ privateId
975
+ }));
976
+ if (shardState.clients) shardState.clients.delete(privateId);
977
+ }, "close")
978
+ };
979
+ if (!shardState.clients.has(privateId)) shardState.clients.set(privateId, {});
980
+ await this.onConnectClient(virtualConnection, virtualContext);
981
+ }
982
+ /**
983
+ * @method handleShardClientMessage
984
+ * @private
985
+ * @async
986
+ * @param {Object} message - The client message from a shard.
987
+ * @param {Party.Connection} shardConnection - The connection object of the shard.
988
+ * @description Handles a message from a client via a shard.
989
+ * @returns {Promise<void>}
990
+ */
991
+ async handleShardClientMessage(message, shardConnection) {
992
+ const { privateId, publicId, payload } = message;
993
+ const shardState = shardConnection.state;
994
+ const clients = shardState.clients;
995
+ if (!clients.has(privateId)) {
996
+ console.warn(`Received message from unknown client ${privateId}, creating virtual connection`);
997
+ clients.set(privateId, { publicId });
998
+ }
999
+ const virtualConnection = {
1000
+ id: privateId,
1001
+ send: /* @__PURE__ */ __name((data) => {
1002
+ shardConnection.send(JSON.stringify({
1003
+ targetClientId: privateId,
1004
+ data
1005
+ }));
1006
+ }, "send"),
1007
+ state: clients.get(privateId),
1008
+ setState: /* @__PURE__ */ __name((state) => {
1009
+ const currentState = clients.get(privateId) || {};
1010
+ const mergedState = Object.assign({}, currentState, state);
1011
+ clients.set(privateId, mergedState);
1012
+ virtualConnection.state = clients.get(privateId);
1013
+ return virtualConnection.state;
1014
+ }, "setState"),
1015
+ close: /* @__PURE__ */ __name(() => {
1016
+ shardConnection.send(JSON.stringify({
1017
+ type: "shard.closeClient",
1018
+ privateId
1019
+ }));
1020
+ if (shardState.clients) shardState.clients.delete(privateId);
1021
+ }, "close")
1022
+ };
1023
+ const payloadString = typeof payload === "string" ? payload : JSON.stringify(payload);
1024
+ await this.onMessage(payloadString, virtualConnection);
1025
+ }
1026
+ /**
1027
+ * @method handleShardClientDisconnect
1028
+ * @private
1029
+ * @async
1030
+ * @param {Object} message - The client disconnection message from a shard.
1031
+ * @param {Party.Connection} shardConnection - The connection object of the shard.
1032
+ * @description Handles a client disconnection via a shard.
1033
+ * @returns {Promise<void>}
1034
+ */
1035
+ async handleShardClientDisconnect(message, shardConnection) {
1036
+ const { privateId, publicId } = message;
1037
+ const clients = shardConnection.state.clients;
1038
+ const clientState = clients.get(privateId);
1039
+ if (!clientState) {
1040
+ console.warn(`Disconnection for unknown client ${privateId}`);
1041
+ return;
1042
+ }
1043
+ const virtualConnection = {
1044
+ id: privateId,
1045
+ send: /* @__PURE__ */ __name(() => {}, "send"),
1046
+ state: clientState,
1047
+ setState: /* @__PURE__ */ __name(() => {
1048
+ return {};
1049
+ }, "setState"),
1050
+ close: /* @__PURE__ */ __name(() => {}, "close")
1051
+ };
1052
+ await this.onClose(virtualConnection);
1053
+ clients.delete(privateId);
1054
+ }
1055
+ /**
1056
+ * @method onClose
1057
+ * @async
1058
+ * @param {Party.Connection} conn - The connection object of the disconnecting user.
1059
+ * @description Handles user disconnection, removing them from the room and triggering the onLeave event..
1060
+ * @returns {Promise<void>}
1061
+ *
1062
+ * @example
1063
+ * ```typescript
1064
+ * server.onClose = async (conn) => {
1065
+ * await server.onClose(conn);
1066
+ * console.log("User disconnected:", conn.id);
1067
+ * };
1068
+ * ```
1069
+ */
1070
+ async onClose(conn) {
1071
+ const subRoom = await this.getSubRoom();
1072
+ if (!subRoom) return;
1073
+ if (subRoom.$pendingInitialSync) subRoom.$pendingInitialSync.delete(conn);
1074
+ const signal2 = this.getUsersProperty(subRoom);
1075
+ if (!conn.state) return;
1076
+ const privateId = conn.id;
1077
+ const { publicId } = conn.state;
1078
+ const user = signal2?.()[publicId];
1079
+ if (!user) return;
1080
+ await this.updateSessionConnection(privateId, false);
1081
+ const connectionUpdated = this.updateUserConnectionStatus(user, false);
1082
+ await awaitReturn(subRoom["onLeave"]?.(user, conn));
1083
+ if (!connectionUpdated) this.broadcast({
1084
+ type: "user_disconnected",
1085
+ value: { publicId }
1086
+ }, subRoom);
1087
+ }
1088
+ async onAlarm() {
1089
+ const subRoom = await this.getSubRoom();
1090
+ await awaitReturn(subRoom["onAlarm"]?.(subRoom));
1091
+ }
1092
+ async onError(connection, error) {
1093
+ await awaitReturn((await this.getSubRoom())["onError"]?.(connection, error));
1094
+ }
1095
+ /**
1096
+ * @method onRequest
1097
+ * @async
1098
+ * @param {Party.Request} req - The HTTP request to handle
1099
+ * @description Handles HTTP requests, either directly from clients or forwarded by shards
1100
+ * @returns {Promise<Response>} The response to return to the client
1101
+ */
1102
+ async onRequest(req) {
1103
+ const isFromShard = req.headers.has("x-forwarded-by-shard");
1104
+ const shardId = req.headers.get("x-shard-id");
1105
+ const res = new ServerResponse([createCorsInterceptor()]);
1106
+ if (req.method === "OPTIONS") return res.status(200).send({});
1107
+ if (isFromShard) return this.handleShardRequest(req, res, shardId);
1108
+ return this.handleDirectRequest(req, res);
1109
+ }
1110
+ /**
1111
+ * @method handleSessionRestore
1112
+ * @private
1113
+ * @async
1114
+ * @param {Party.Request} req - The HTTP request for session restore
1115
+ * @param {ServerResponse} res - The response object
1116
+ * @description Handles session restoration from transfer data, creates session from privateId
1117
+ * @returns {Promise<Response>} The response to return to the client
1118
+ */
1119
+ async handleSessionRestore(req, res) {
1120
+ try {
1121
+ const { privateId, userSnapshot, sessionState, publicId } = await req.json();
1122
+ if (!privateId || !publicId) return res.badRequest("Missing privateId or publicId in transfer data");
1123
+ const subRoom = await this.getSubRoom();
1124
+ if (!subRoom) return res.serverError("Room not available");
1125
+ await this.saveSession(privateId, {
1126
+ publicId,
1127
+ state: sessionState,
1128
+ created: Date.now(),
1129
+ connected: false
1130
+ });
1131
+ if (userSnapshot) {
1132
+ const signal2 = this.getUsersProperty(subRoom);
1133
+ const usersPropName = this.getUsersPropName(subRoom);
1134
+ if (signal2 && usersPropName) {
1135
+ const { classType } = signal2.options;
1136
+ const user = isClass(classType) ? new classType() : classType();
1137
+ const hydratedSnapshot = await awaitReturn(subRoom["onSessionRestore"]?.({
1138
+ userSnapshot,
1139
+ user,
1140
+ publicId,
1141
+ privateId,
1142
+ sessionState,
1143
+ room: this.room
1144
+ })) ?? userSnapshot;
1145
+ signal2()[publicId] = user;
1146
+ load(user, hydratedSnapshot, true);
1147
+ await this.room.storage.put(`${usersPropName}.${publicId}`, userSnapshot);
1148
+ }
1149
+ }
1150
+ const transferToken = generateShortUUID$1();
1151
+ await this.room.storage.put(`transfer:${transferToken}`, {
1152
+ privateId,
1153
+ publicId,
1154
+ restored: true
1155
+ });
1156
+ return res.success({ transferToken });
1157
+ } catch (error) {
1158
+ console.error("Error restoring session:", error);
1159
+ return res.serverError("Failed to restore session");
1160
+ }
1161
+ }
1162
+ /**
1163
+ * @method handleDirectRequest
1164
+ * @private
1165
+ * @async
1166
+ * @param {Party.Request} req - The HTTP request received directly from a client
1167
+ * @description Processes requests received directly from clients
1168
+ * @returns {Promise<Response>} The response to return to the client
1169
+ */
1170
+ async handleDirectRequest(req, res) {
1171
+ const subRoom = await this.getSubRoom();
1172
+ if (!subRoom) return res.notFound();
1173
+ if (new URL(req.url).pathname.endsWith("/session-transfer") && req.method === "POST") return this.handleSessionRestore(req, res);
1174
+ const response2 = await this.tryMatchRequestHandler(req, res, subRoom);
1175
+ if (response2) return response2;
1176
+ const legacyResponse = await awaitReturn(subRoom["onRequest"]?.(req, res));
1177
+ if (!legacyResponse) return res.notFound();
1178
+ if (legacyResponse instanceof Response) return legacyResponse;
1179
+ return res.success(legacyResponse);
1180
+ }
1181
+ /**
1182
+ * @method tryMatchRequestHandler
1183
+ * @private
1184
+ * @async
1185
+ * @param {Party.Request} req - The HTTP request to handle
1186
+ * @param {Object} subRoom - The room instance
1187
+ * @description Attempts to match the request to a registered @Request handler
1188
+ * @returns {Promise<Response | null>} The response or null if no handler matched
1189
+ */
1190
+ async tryMatchRequestHandler(req, res, subRoom) {
1191
+ const requestHandlers = subRoom.constructor["_requestMetadata"];
1192
+ if (!requestHandlers) return null;
1193
+ const url = new URL(req.url);
1194
+ const method = req.method;
1195
+ let pathname = url.pathname;
1196
+ pathname = "/" + pathname.split("/").slice(4).join("/");
1197
+ for (const [routeKey, handler] of requestHandlers.entries()) {
1198
+ const firstColonIndex = routeKey.indexOf(":");
1199
+ const handlerMethod = routeKey.substring(0, firstColonIndex);
1200
+ const handlerPath = routeKey.substring(firstColonIndex + 1);
1201
+ if (handlerMethod !== method) continue;
1202
+ if (this.pathMatches(pathname, handlerPath)) {
1203
+ const params = this.extractPathParams(pathname, handlerPath);
1204
+ const guards = subRoom.constructor["_actionGuards"]?.get(handler.key) || [];
1205
+ for (const guard of guards) {
1206
+ const isAuthorized = await guard(null, req, this.room);
1207
+ if (isAuthorized instanceof Response) return isAuthorized;
1208
+ if (!isAuthorized) return res.notPermitted();
1209
+ }
1210
+ let bodyData = null;
1211
+ if (handler.bodyValidation && [
1212
+ "POST",
1213
+ "PUT",
1214
+ "PATCH"
1215
+ ].includes(method)) try {
1216
+ if ((req.headers.get("content-type") || "").includes("application/json")) {
1217
+ const body = await req.json();
1218
+ const validation = handler.bodyValidation.safeParse(body);
1219
+ if (!validation.success) return res.badRequest("Invalid request body", { details: validation.error });
1220
+ bodyData = validation.data;
1221
+ }
1222
+ } catch (error) {
1223
+ return res.badRequest("Failed to parse request body");
1224
+ }
1225
+ try {
1226
+ req["data"] = bodyData;
1227
+ req["params"] = params;
1228
+ const result = await awaitReturn(subRoom[handler.key](req, res));
1229
+ if (result instanceof Response) return result;
1230
+ return res.success(result);
1231
+ } catch (error) {
1232
+ console.error("Error executing request handler:", error);
1233
+ return res.serverError();
1234
+ }
1235
+ }
1236
+ }
1237
+ return null;
1238
+ }
1239
+ /**
1240
+ * @method pathMatches
1241
+ * @private
1242
+ * @param {string} requestPath - The path from the request
1243
+ * @param {string} handlerPath - The path pattern from the handler
1244
+ * @description Checks if a request path matches a handler path pattern
1245
+ * @returns {boolean} True if the paths match
1246
+ */
1247
+ pathMatches(requestPath, handlerPath) {
1248
+ const pathRegexString = handlerPath.replace(/\//g, "\\/").replace(/:([^\/]+)/g, "([^/]+)");
1249
+ return new RegExp(`^${pathRegexString}`).test(requestPath);
1250
+ }
1251
+ /**
1252
+ * @method extractPathParams
1253
+ * @private
1254
+ * @param {string} requestPath - The path from the request
1255
+ * @param {string} handlerPath - The path pattern from the handler
1256
+ * @description Extracts path parameters from the request path based on the handler pattern
1257
+ * @returns {Object} An object containing the path parameters
1258
+ */
1259
+ extractPathParams(requestPath, handlerPath) {
1260
+ const params = {};
1261
+ const paramNames = [];
1262
+ handlerPath.split("/").forEach((segment) => {
1263
+ if (segment.startsWith(":")) paramNames.push(segment.substring(1));
1264
+ });
1265
+ const pathRegexString = handlerPath.replace(/\//g, "\\/").replace(/:([^\/]+)/g, "([^/]+)");
1266
+ const pathRegex = new RegExp(`^${pathRegexString}`);
1267
+ const matches = requestPath.match(pathRegex);
1268
+ if (matches && matches.length > 1) for (let i = 0; i < paramNames.length; i++) params[paramNames[i]] = matches[i + 1];
1269
+ return params;
1270
+ }
1271
+ /**
1272
+ * @method handleShardRequest
1273
+ * @private
1274
+ * @async
1275
+ * @param {Party.Request} req - The HTTP request forwarded by a shard
1276
+ * @param {string | null} shardId - The ID of the shard that forwarded the request
1277
+ * @description Processes requests forwarded by shards, preserving client context
1278
+ * @returns {Promise<Response>} The response to return to the shard (which will forward it to the client)
1279
+ */
1280
+ async handleShardRequest(req, res, shardId) {
1281
+ const subRoom = await this.getSubRoom();
1282
+ if (!subRoom) return res.notFound();
1283
+ const originalClientIp = req.headers.get("x-original-client-ip");
1284
+ const enhancedReq = this.createEnhancedRequest(req, originalClientIp);
1285
+ try {
1286
+ const response2 = await this.tryMatchRequestHandler(enhancedReq, res, subRoom);
1287
+ if (response2) return response2;
1288
+ const legacyResponse = await awaitReturn(subRoom["onRequest"]?.(enhancedReq, res));
1289
+ if (!legacyResponse) return res.notFound();
1290
+ if (legacyResponse instanceof Response) return legacyResponse;
1291
+ return res.success(legacyResponse);
1292
+ } catch (error) {
1293
+ console.error(`Error processing request from shard ${shardId}:`, error);
1294
+ return res.serverError();
1295
+ }
1296
+ }
1297
+ /**
1298
+ * @method createEnhancedRequest
1299
+ * @private
1300
+ * @param {Party.Request} originalReq - The original request received from the shard
1301
+ * @param {string | null} originalClientIp - The original client IP, if available
1302
+ * @description Creates an enhanced request object that preserves the original client context
1303
+ * @returns {Party.Request} The enhanced request object
1304
+ */
1305
+ createEnhancedRequest(originalReq, originalClientIp) {
1306
+ const clonedReq = originalReq.clone();
1307
+ clonedReq.viaShard = true;
1308
+ if (originalClientIp) clonedReq.originalClientIp = originalClientIp;
1309
+ return clonedReq;
1310
+ }
1563
1311
  };
1564
-
1565
- // src/shard.ts
1566
1312
  var Shard = class {
1567
- static {
1568
- __name(this, "Shard");
1569
- }
1570
- room;
1571
- ws;
1572
- connectionMap;
1573
- mainServerStub;
1574
- worldUrl;
1575
- worldId;
1576
- lastReportedConnections;
1577
- statsInterval;
1578
- statsIntervalId;
1579
- constructor(room) {
1580
- this.room = room;
1581
- this.connectionMap = /* @__PURE__ */ new Map();
1582
- this.worldUrl = null;
1583
- this.worldId = "default";
1584
- this.lastReportedConnections = 0;
1585
- this.statsInterval = 3e4;
1586
- this.statsIntervalId = null;
1587
- }
1588
- async onStart() {
1589
- const roomId = this.room.id.split(":")[0];
1590
- const roomStub = this.room.context.parties.main.get(roomId);
1591
- if (!roomStub) {
1592
- console.warn("No room room stub found in main party context");
1593
- return;
1594
- }
1595
- this.mainServerStub = roomStub;
1596
- this.ws = await roomStub.socket({
1597
- headers: {
1598
- "x-shard-id": this.room.id
1599
- }
1600
- });
1601
- this.ws.addEventListener("message", (event) => {
1602
- try {
1603
- const message = JSON.parse(event.data);
1604
- if (message.targetClientId) {
1605
- const clientConn = this.connectionMap.get(message.targetClientId);
1606
- if (clientConn) {
1607
- delete message.targetClientId;
1608
- clientConn.send(message.data);
1609
- }
1610
- } else {
1611
- this.room.broadcast(event.data);
1612
- }
1613
- } catch (error) {
1614
- console.error("Error processing message from main server:", error);
1615
- }
1616
- });
1617
- await this.updateWorldStats();
1618
- this.startPeriodicStatsUpdates();
1619
- }
1620
- startPeriodicStatsUpdates() {
1621
- if (!this.worldUrl) {
1622
- return;
1623
- }
1624
- if (this.statsIntervalId) {
1625
- clearInterval(this.statsIntervalId);
1626
- }
1627
- this.statsIntervalId = setInterval(() => {
1628
- this.updateWorldStats().catch((error) => {
1629
- console.error("Error in periodic stats update:", error);
1630
- });
1631
- }, this.statsInterval);
1632
- }
1633
- stopPeriodicStatsUpdates() {
1634
- if (this.statsIntervalId) {
1635
- clearInterval(this.statsIntervalId);
1636
- this.statsIntervalId = null;
1637
- }
1638
- }
1639
- onConnect(conn, ctx) {
1640
- this.connectionMap.set(conn.id, conn);
1641
- const headers = {};
1642
- if (ctx.request?.headers) {
1643
- ctx.request.headers.forEach((value, key) => {
1644
- headers[key] = value;
1645
- });
1646
- }
1647
- const requestInfo = ctx.request ? {
1648
- headers,
1649
- url: ctx.request.url,
1650
- method: ctx.request.method
1651
- } : null;
1652
- this.ws.send(JSON.stringify({
1653
- type: "shard.clientConnected",
1654
- privateId: conn.id,
1655
- requestInfo
1656
- }));
1657
- this.updateWorldStats();
1658
- }
1659
- onMessage(message, sender) {
1660
- try {
1661
- const parsedMessage = typeof message === "string" ? JSON.parse(message) : message;
1662
- const wrappedMessage = JSON.stringify({
1663
- type: "shard.clientMessage",
1664
- privateId: sender.id,
1665
- publicId: sender.state?.publicId,
1666
- payload: parsedMessage
1667
- });
1668
- this.ws.send(wrappedMessage);
1669
- } catch (error) {
1670
- console.error("Error forwarding message to main server:", error);
1671
- }
1672
- }
1673
- onClose(conn) {
1674
- this.connectionMap.delete(conn.id);
1675
- this.ws.send(JSON.stringify({
1676
- type: "shard.clientDisconnected",
1677
- privateId: conn.id,
1678
- publicId: conn.state?.publicId
1679
- }));
1680
- this.updateWorldStats();
1681
- }
1682
- async updateWorldStats() {
1683
- const currentConnections = this.connectionMap.size;
1684
- if (currentConnections === this.lastReportedConnections) {
1685
- return true;
1686
- }
1687
- try {
1688
- const worldRoom = this.room.context.parties.world.get("world-default");
1689
- const response2 = await worldRoom.fetch("/update-shard", {
1690
- method: "POST",
1691
- headers: {
1692
- "Content-Type": "application/json",
1693
- "x-access-shard": this.room.env.SHARD_SECRET
1694
- },
1695
- body: JSON.stringify({
1696
- shardId: this.room.id,
1697
- connections: currentConnections
1698
- })
1699
- });
1700
- if (!response2.ok) {
1701
- const errorData = await response2.json().catch(() => ({
1702
- error: "Unknown error"
1703
- }));
1704
- console.error(`Failed to update World stats: ${response2.status} - ${errorData.error || "Unknown error"}`);
1705
- return false;
1706
- }
1707
- this.lastReportedConnections = currentConnections;
1708
- return true;
1709
- } catch (error) {
1710
- console.error("Error updating World stats:", error);
1711
- return false;
1712
- }
1713
- }
1714
- /**
1715
- * @method onRequest
1716
- * @async
1717
- * @param {Party.Request} req - The HTTP request to handle
1718
- * @description Forwards HTTP requests to the main server, preserving client context
1719
- * @returns {Promise<Response>} The response from the main server
1720
- */
1721
- async onRequest(req) {
1722
- if (!this.mainServerStub) {
1723
- return response(503, {
1724
- error: "Shard not connected to main server"
1725
- });
1726
- }
1727
- try {
1728
- const url = new URL(req.url);
1729
- const path = url.pathname;
1730
- const method = req.method;
1731
- let body = null;
1732
- if (method !== "GET" && method !== "HEAD") {
1733
- body = await req.text();
1734
- }
1735
- const headers = new Headers();
1736
- req.headers.forEach((value, key) => {
1737
- headers.set(key, value);
1738
- });
1739
- headers.set("x-shard-id", this.room.id);
1740
- headers.set("x-forwarded-by-shard", "true");
1741
- const clientIp = req.headers.get("x-forwarded-for") || "unknown";
1742
- if (clientIp) {
1743
- headers.set("x-original-client-ip", clientIp);
1744
- }
1745
- const requestInit = {
1746
- method,
1747
- headers,
1748
- body
1749
- };
1750
- const response2 = await this.mainServerStub.fetch(path, requestInit);
1751
- return response2;
1752
- } catch (error) {
1753
- return response(500, {
1754
- error: "Error forwarding request"
1755
- });
1756
- }
1757
- }
1758
- /**
1759
- * @method onAlarm
1760
- * @async
1761
- * @description Executed periodically, used to perform maintenance tasks
1762
- */
1763
- async onAlarm() {
1764
- await this.updateWorldStats();
1765
- }
1313
+ static {
1314
+ __name(this, "Shard");
1315
+ }
1316
+ room;
1317
+ ws;
1318
+ connectionMap;
1319
+ mainServerStub;
1320
+ worldUrl;
1321
+ worldId;
1322
+ lastReportedConnections;
1323
+ statsInterval;
1324
+ statsIntervalId;
1325
+ constructor(room) {
1326
+ this.room = room;
1327
+ this.connectionMap = /* @__PURE__ */ new Map();
1328
+ this.worldUrl = null;
1329
+ this.worldId = "default";
1330
+ this.lastReportedConnections = 0;
1331
+ this.statsInterval = 3e4;
1332
+ this.statsIntervalId = null;
1333
+ }
1334
+ async onStart() {
1335
+ const roomId = this.room.id.split(":")[0];
1336
+ const roomStub = this.room.context.parties.main.get(roomId);
1337
+ if (!roomStub) {
1338
+ console.warn("No room room stub found in main party context");
1339
+ return;
1340
+ }
1341
+ this.mainServerStub = roomStub;
1342
+ this.ws = await roomStub.socket({ headers: { "x-shard-id": this.room.id } });
1343
+ this.ws.addEventListener("message", (event) => {
1344
+ try {
1345
+ const message = JSON.parse(event.data);
1346
+ if (message.targetClientId) {
1347
+ const clientConn = this.connectionMap.get(message.targetClientId);
1348
+ if (clientConn) {
1349
+ delete message.targetClientId;
1350
+ clientConn.send(message.data);
1351
+ }
1352
+ } else this.room.broadcast(event.data);
1353
+ } catch (error) {
1354
+ console.error("Error processing message from main server:", error);
1355
+ }
1356
+ });
1357
+ await this.updateWorldStats();
1358
+ this.startPeriodicStatsUpdates();
1359
+ }
1360
+ startPeriodicStatsUpdates() {
1361
+ if (!this.worldUrl) return;
1362
+ if (this.statsIntervalId) clearInterval(this.statsIntervalId);
1363
+ this.statsIntervalId = setInterval(() => {
1364
+ this.updateWorldStats().catch((error) => {
1365
+ console.error("Error in periodic stats update:", error);
1366
+ });
1367
+ }, this.statsInterval);
1368
+ }
1369
+ stopPeriodicStatsUpdates() {
1370
+ if (this.statsIntervalId) {
1371
+ clearInterval(this.statsIntervalId);
1372
+ this.statsIntervalId = null;
1373
+ }
1374
+ }
1375
+ onConnect(conn, ctx) {
1376
+ this.connectionMap.set(conn.id, conn);
1377
+ const headers = {};
1378
+ if (ctx.request?.headers) ctx.request.headers.forEach((value, key) => {
1379
+ headers[key] = value;
1380
+ });
1381
+ const requestInfo = ctx.request ? {
1382
+ headers,
1383
+ url: ctx.request.url,
1384
+ method: ctx.request.method
1385
+ } : null;
1386
+ this.ws.send(JSON.stringify({
1387
+ type: "shard.clientConnected",
1388
+ privateId: conn.id,
1389
+ requestInfo
1390
+ }));
1391
+ this.updateWorldStats();
1392
+ }
1393
+ onMessage(message, sender) {
1394
+ try {
1395
+ const parsedMessage = typeof message === "string" ? JSON.parse(message) : message;
1396
+ const wrappedMessage = JSON.stringify({
1397
+ type: "shard.clientMessage",
1398
+ privateId: sender.id,
1399
+ publicId: sender.state?.publicId,
1400
+ payload: parsedMessage
1401
+ });
1402
+ this.ws.send(wrappedMessage);
1403
+ } catch (error) {
1404
+ console.error("Error forwarding message to main server:", error);
1405
+ }
1406
+ }
1407
+ onClose(conn) {
1408
+ this.connectionMap.delete(conn.id);
1409
+ this.ws.send(JSON.stringify({
1410
+ type: "shard.clientDisconnected",
1411
+ privateId: conn.id,
1412
+ publicId: conn.state?.publicId
1413
+ }));
1414
+ this.updateWorldStats();
1415
+ }
1416
+ async updateWorldStats() {
1417
+ const currentConnections = this.connectionMap.size;
1418
+ if (currentConnections === this.lastReportedConnections) return true;
1419
+ try {
1420
+ const response2 = await this.room.context.parties.world.get("world-default").fetch("/update-shard", {
1421
+ method: "POST",
1422
+ headers: {
1423
+ "Content-Type": "application/json",
1424
+ "x-access-shard": this.room.env.SHARD_SECRET
1425
+ },
1426
+ body: JSON.stringify({
1427
+ shardId: this.room.id,
1428
+ connections: currentConnections
1429
+ })
1430
+ });
1431
+ if (!response2.ok) {
1432
+ const errorData = await response2.json().catch(() => ({ error: "Unknown error" }));
1433
+ console.error(`Failed to update World stats: ${response2.status} - ${errorData.error || "Unknown error"}`);
1434
+ return false;
1435
+ }
1436
+ this.lastReportedConnections = currentConnections;
1437
+ return true;
1438
+ } catch (error) {
1439
+ console.error("Error updating World stats:", error);
1440
+ return false;
1441
+ }
1442
+ }
1443
+ /**
1444
+ * @method onRequest
1445
+ * @async
1446
+ * @param {Party.Request} req - The HTTP request to handle
1447
+ * @description Forwards HTTP requests to the main server, preserving client context
1448
+ * @returns {Promise<Response>} The response from the main server
1449
+ */
1450
+ async onRequest(req) {
1451
+ if (!this.mainServerStub) return response(503, { error: "Shard not connected to main server" });
1452
+ try {
1453
+ const path = new URL(req.url).pathname;
1454
+ const method = req.method;
1455
+ let body = null;
1456
+ if (method !== "GET" && method !== "HEAD") body = await req.text();
1457
+ const headers = new Headers();
1458
+ req.headers.forEach((value, key) => {
1459
+ headers.set(key, value);
1460
+ });
1461
+ headers.set("x-shard-id", this.room.id);
1462
+ headers.set("x-forwarded-by-shard", "true");
1463
+ const clientIp = req.headers.get("x-forwarded-for") || "unknown";
1464
+ if (clientIp) headers.set("x-original-client-ip", clientIp);
1465
+ const requestInit = {
1466
+ method,
1467
+ headers,
1468
+ body
1469
+ };
1470
+ return await this.mainServerStub.fetch(path, requestInit);
1471
+ } catch (error) {
1472
+ return response(500, { error: "Error forwarding request" });
1473
+ }
1474
+ }
1475
+ /**
1476
+ * @method onAlarm
1477
+ * @async
1478
+ * @description Executed periodically, used to perform maintenance tasks
1479
+ */
1480
+ async onAlarm() {
1481
+ await this.updateWorldStats();
1482
+ }
1766
1483
  };
1767
-
1768
- // src/testing.ts
1769
1484
  async function testRoom(Room3, options = {}) {
1770
- const createServer = /* @__PURE__ */ __name((io2) => {
1771
- const server2 = new Server(io2);
1772
- server2.rooms = [
1773
- Room3
1774
- ];
1775
- return server2;
1776
- }, "createServer");
1777
- const isShard = options.shard || false;
1778
- const io = new ServerIo(Room3.path, isShard ? {
1779
- parties: {
1780
- game: createServer,
1781
- ...options.parties || {}
1782
- },
1783
- partyFn: options.partyFn,
1784
- env: options.env
1785
- } : {
1786
- parties: options.parties,
1787
- partyFn: options.partyFn,
1788
- env: options.env
1789
- });
1790
- Room3.prototype.throttleSync = 0;
1791
- Room3.prototype.throttleStorage = 0;
1792
- Room3.prototype.options = options;
1793
- let server;
1794
- if (options.shard) {
1795
- const shardServer = new Shard(io);
1796
- shardServer.subRoom = null;
1797
- server = shardServer;
1798
- if (io.context.parties.main instanceof Map) {
1799
- for (const lobby of io.context.parties.main.values()) {
1800
- await lobby.server.onStart();
1801
- }
1802
- }
1803
- } else {
1804
- server = await createServer(io);
1805
- if (io.context.parties.main instanceof Map) {
1806
- for (const lobby of io.context.parties.main.values()) {
1807
- if (lobby.server && lobby.server !== server) {
1808
- await lobby.server.onStart();
1809
- }
1810
- }
1811
- }
1812
- }
1813
- await server.onStart();
1814
- return {
1815
- server,
1816
- room: server.subRoom,
1817
- createClient: /* @__PURE__ */ __name(async (id2, opts) => {
1818
- const client = await io.connection(server, id2, opts);
1819
- return client;
1820
- }, "createClient"),
1821
- getServerUser: /* @__PURE__ */ __name(async (client, prop = "users") => {
1822
- const privateId = client.conn.id;
1823
- const session = await server.getSession(privateId);
1824
- return server.subRoom[prop]()[session?.publicId];
1825
- }, "getServerUser")
1826
- };
1485
+ const createServer = /* @__PURE__ */ __name((io2) => {
1486
+ const server2 = new Server(io2);
1487
+ server2.rooms = [Room3];
1488
+ return server2;
1489
+ }, "createServer");
1490
+ const isShard = options.shard || false;
1491
+ const io = new ServerIo(Room3.path, isShard ? {
1492
+ parties: {
1493
+ game: createServer,
1494
+ ...options.parties || {}
1495
+ },
1496
+ partyFn: options.partyFn,
1497
+ env: options.env
1498
+ } : {
1499
+ parties: options.parties,
1500
+ partyFn: options.partyFn,
1501
+ env: options.env
1502
+ });
1503
+ Room3.prototype.throttleSync = 0;
1504
+ Room3.prototype.throttleStorage = 0;
1505
+ Room3.prototype.options = options;
1506
+ let server;
1507
+ if (options.shard) {
1508
+ const shardServer = new Shard(io);
1509
+ shardServer.subRoom = null;
1510
+ server = shardServer;
1511
+ if (io.context.parties.main instanceof Map) for (const lobby of io.context.parties.main.values()) await lobby.server.onStart();
1512
+ } else {
1513
+ server = await createServer(io);
1514
+ if (io.context.parties.main instanceof Map) {
1515
+ for (const lobby of io.context.parties.main.values()) if (lobby.server && lobby.server !== server) await lobby.server.onStart();
1516
+ }
1517
+ }
1518
+ await server.onStart();
1519
+ return {
1520
+ server,
1521
+ room: server.subRoom,
1522
+ createClient: /* @__PURE__ */ __name(async (id2, opts) => {
1523
+ return await io.connection(server, id2, opts);
1524
+ }, "createClient"),
1525
+ getServerUser: /* @__PURE__ */ __name(async (client, prop = "users") => {
1526
+ const privateId = client.conn.id;
1527
+ const session = await server.getSession(privateId);
1528
+ return server.subRoom[prop]()[session?.publicId];
1529
+ }, "getServerUser")
1530
+ };
1827
1531
  }
1828
1532
  __name(testRoom, "testRoom");
1829
- async function request(room, path, options = {
1830
- method: "GET"
1831
- }) {
1832
- const url = new URL("http://localhost" + path);
1833
- const request1 = new Request(url.toString(), options);
1834
- const response2 = await room.onRequest(request1);
1835
- return response2;
1533
+ async function request(room, path, options = { method: "GET" }) {
1534
+ const url = new URL("http://localhost" + path);
1535
+ const request1 = new Request(url.toString(), options);
1536
+ return await room.onRequest(request1);
1836
1537
  }
1837
1538
  __name(request, "request");
1838
1539
  function tick(ms = 0) {
1839
- return new Promise((resolve) => setTimeout(resolve, ms));
1540
+ return new Promise((resolve) => setTimeout(resolve, ms));
1840
1541
  }
1841
1542
  __name(tick, "tick");
1842
-
1843
- // src/mock.ts
1844
1543
  var MockPartyClient = class {
1845
- static {
1846
- __name(this, "MockPartyClient");
1847
- }
1848
- server;
1849
- events;
1850
- id;
1851
- conn;
1852
- constructor(server, id2) {
1853
- this.server = server;
1854
- this.events = /* @__PURE__ */ new Map();
1855
- this.id = id2 || generateShortUUID();
1856
- this.conn = new MockConnection(this);
1857
- }
1858
- addEventListener(event, cb) {
1859
- if (!this.events.has(event)) {
1860
- this.events.set(event, []);
1861
- }
1862
- this.events.get(event).push(cb);
1863
- }
1864
- removeEventListener(event, cb) {
1865
- if (!this.events.has(event)) return;
1866
- const callbacks = this.events.get(event);
1867
- const index = callbacks.indexOf(cb);
1868
- if (index !== -1) {
1869
- callbacks.splice(index, 1);
1870
- }
1871
- if (callbacks.length === 0) {
1872
- this.events.delete(event);
1873
- }
1874
- }
1875
- _trigger(event, data) {
1876
- const callbacks = this.events.get(event);
1877
- if (callbacks) {
1878
- for (const cb of callbacks) {
1879
- cb(data);
1880
- }
1881
- }
1882
- }
1883
- send(data) {
1884
- return this.server.onMessage(JSON.stringify(data), this.conn);
1885
- }
1544
+ static {
1545
+ __name(this, "MockPartyClient");
1546
+ }
1547
+ server;
1548
+ events;
1549
+ id;
1550
+ conn;
1551
+ constructor(server, id2) {
1552
+ this.server = server;
1553
+ this.events = /* @__PURE__ */ new Map();
1554
+ this.id = id2 || generateShortUUID();
1555
+ this.conn = new MockConnection(this);
1556
+ }
1557
+ addEventListener(event, cb) {
1558
+ if (!this.events.has(event)) this.events.set(event, []);
1559
+ this.events.get(event).push(cb);
1560
+ }
1561
+ removeEventListener(event, cb) {
1562
+ if (!this.events.has(event)) return;
1563
+ const callbacks = this.events.get(event);
1564
+ const index = callbacks.indexOf(cb);
1565
+ if (index !== -1) callbacks.splice(index, 1);
1566
+ if (callbacks.length === 0) this.events.delete(event);
1567
+ }
1568
+ _trigger(event, data) {
1569
+ const callbacks = this.events.get(event);
1570
+ if (callbacks) for (const cb of callbacks) cb(data);
1571
+ }
1572
+ send(data) {
1573
+ return this.server.onMessage(JSON.stringify(data), this.conn);
1574
+ }
1886
1575
  };
1887
1576
  var MockLobby = class MockLobby2 {
1888
- static {
1889
- __name(this, "MockLobby");
1890
- }
1891
- server;
1892
- lobbyId;
1893
- constructor(server, lobbyId) {
1894
- this.server = server;
1895
- this.lobbyId = lobbyId;
1896
- }
1897
- socket(_init) {
1898
- return new MockPartyClient(this.server);
1899
- }
1900
- async connection(idOrOptions, maybeOptions) {
1901
- const id2 = typeof idOrOptions === "string" ? idOrOptions : idOrOptions?.id;
1902
- const options = (typeof idOrOptions === "string" ? maybeOptions : idOrOptions) || {};
1903
- return this.server.room.connection(this.server, id2, options);
1904
- }
1905
- fetch(url, options) {
1906
- const baseUrl = url.includes("shard") ? "" : "/parties/main/" + this.lobbyId;
1907
- return request(this.server, baseUrl + url, options);
1908
- }
1577
+ static {
1578
+ __name(this, "MockLobby");
1579
+ }
1580
+ server;
1581
+ lobbyId;
1582
+ constructor(server, lobbyId) {
1583
+ this.server = server;
1584
+ this.lobbyId = lobbyId;
1585
+ }
1586
+ socket(_init) {
1587
+ return new MockPartyClient(this.server);
1588
+ }
1589
+ async connection(idOrOptions, maybeOptions) {
1590
+ const id2 = typeof idOrOptions === "string" ? idOrOptions : idOrOptions?.id;
1591
+ const options = (typeof idOrOptions === "string" ? maybeOptions : idOrOptions) || {};
1592
+ return this.server.room.connection(this.server, id2, options);
1593
+ }
1594
+ fetch(url, options) {
1595
+ const baseUrl = url.includes("shard") ? "" : "/parties/main/" + this.lobbyId;
1596
+ return request(this.server, baseUrl + url, options);
1597
+ }
1909
1598
  };
1910
1599
  var MockContext = class MockContext2 {
1911
- static {
1912
- __name(this, "MockContext");
1913
- }
1914
- room;
1915
- parties;
1916
- constructor(room, options = {}) {
1917
- this.room = room;
1918
- this.parties = {
1919
- main: /* @__PURE__ */ new Map()
1920
- };
1921
- const parties = options.parties || {};
1922
- if (options.partyFn) {
1923
- const serverCache = /* @__PURE__ */ new Map();
1924
- this.parties.main = {
1925
- get: /* @__PURE__ */ __name(async (lobbyId) => {
1926
- if (!serverCache.has(lobbyId)) {
1927
- const server = await options.partyFn(lobbyId);
1928
- serverCache.set(lobbyId, new MockLobby(server, lobbyId));
1929
- }
1930
- return serverCache.get(lobbyId);
1931
- }, "get")
1932
- };
1933
- } else {
1934
- for (let lobbyId in parties) {
1935
- const server = parties[lobbyId](room);
1936
- this.parties.main.set(lobbyId, new MockLobby(server, lobbyId));
1937
- }
1938
- }
1939
- }
1600
+ static {
1601
+ __name(this, "MockContext");
1602
+ }
1603
+ room;
1604
+ parties;
1605
+ constructor(room, options = {}) {
1606
+ this.room = room;
1607
+ this.parties = { main: /* @__PURE__ */ new Map() };
1608
+ const parties = options.parties || {};
1609
+ if (options.partyFn) {
1610
+ const serverCache = /* @__PURE__ */ new Map();
1611
+ this.parties.main = { get: /* @__PURE__ */ __name(async (lobbyId) => {
1612
+ if (!serverCache.has(lobbyId)) {
1613
+ const server = await options.partyFn(lobbyId);
1614
+ serverCache.set(lobbyId, new MockLobby(server, lobbyId));
1615
+ }
1616
+ return serverCache.get(lobbyId);
1617
+ }, "get") };
1618
+ } else for (let lobbyId in parties) {
1619
+ const server = parties[lobbyId](room);
1620
+ this.parties.main.set(lobbyId, new MockLobby(server, lobbyId));
1621
+ }
1622
+ }
1940
1623
  };
1941
1624
  var MockPartyRoom = class MockPartyRoom2 {
1942
- static {
1943
- __name(this, "MockPartyRoom");
1944
- }
1945
- id;
1946
- clients;
1947
- storage;
1948
- context;
1949
- env;
1950
- constructor(id2, options = {}) {
1951
- this.id = id2;
1952
- this.clients = /* @__PURE__ */ new Map();
1953
- this.storage = new Storage();
1954
- this.env = {};
1955
- this.id = id2 || generateShortUUID();
1956
- this.context = new MockContext(this, {
1957
- parties: options.parties,
1958
- partyFn: options.partyFn
1959
- });
1960
- this.env = options.env || {};
1961
- }
1962
- async connection(server, id2, opts) {
1963
- const socket = new MockPartyClient(server, id2);
1964
- const url = new URL("http://localhost");
1965
- if (opts?.query) {
1966
- for (const [key, value] of Object.entries(opts.query)) {
1967
- url.searchParams.set(key, String(value));
1968
- }
1969
- }
1970
- const request2 = new Request(url.toString(), {
1971
- method: "GET",
1972
- headers: {
1973
- "Content-Type": "application/json",
1974
- ...opts?.headers || {}
1975
- }
1976
- });
1977
- await server.onConnect(socket.conn, {
1978
- request: request2
1979
- });
1980
- this.clients.set(socket.id, socket);
1981
- return socket;
1982
- }
1983
- broadcast(data) {
1984
- this.clients.forEach((client) => {
1985
- client._trigger("message", data);
1986
- });
1987
- }
1988
- getConnection(id2) {
1989
- return this.clients.get(id2);
1990
- }
1991
- getConnections() {
1992
- return Array.from(this.clients.values()).map((client) => client.conn);
1993
- }
1994
- clear() {
1995
- this.clients.clear();
1996
- }
1625
+ static {
1626
+ __name(this, "MockPartyRoom");
1627
+ }
1628
+ id;
1629
+ clients;
1630
+ storage;
1631
+ context;
1632
+ env;
1633
+ constructor(id2, options = {}) {
1634
+ this.id = id2;
1635
+ this.clients = /* @__PURE__ */ new Map();
1636
+ this.storage = new Storage();
1637
+ this.env = {};
1638
+ this.id = id2 || generateShortUUID();
1639
+ this.context = new MockContext(this, {
1640
+ parties: options.parties,
1641
+ partyFn: options.partyFn
1642
+ });
1643
+ this.env = options.env || {};
1644
+ }
1645
+ async connection(server, id2, opts) {
1646
+ const socket = new MockPartyClient(server, id2);
1647
+ const url = new URL("http://localhost");
1648
+ if (opts?.query) for (const [key, value] of Object.entries(opts.query)) url.searchParams.set(key, String(value));
1649
+ const request2 = new Request(url.toString(), {
1650
+ method: "GET",
1651
+ headers: {
1652
+ "Content-Type": "application/json",
1653
+ ...opts?.headers || {}
1654
+ }
1655
+ });
1656
+ await server.onConnect(socket.conn, { request: request2 });
1657
+ this.clients.set(socket.id, socket);
1658
+ return socket;
1659
+ }
1660
+ broadcast(data) {
1661
+ this.clients.forEach((client) => {
1662
+ client._trigger("message", data);
1663
+ });
1664
+ }
1665
+ getConnection(id2) {
1666
+ return this.clients.get(id2);
1667
+ }
1668
+ getConnections() {
1669
+ return Array.from(this.clients.values()).map((client) => client.conn);
1670
+ }
1671
+ clear() {
1672
+ this.clients.clear();
1673
+ }
1997
1674
  };
1998
1675
  var MockConnection = class {
1999
- static {
2000
- __name(this, "MockConnection");
2001
- }
2002
- client;
2003
- server;
2004
- id;
2005
- constructor(client) {
2006
- this.client = client;
2007
- this.state = {};
2008
- this.server = client.server;
2009
- this.id = client.id;
2010
- }
2011
- state;
2012
- setState(value) {
2013
- this.state = value;
2014
- }
2015
- send(data) {
2016
- this.client._trigger("message", data);
2017
- }
2018
- close() {
2019
- this.server.onClose(this);
2020
- }
1676
+ static {
1677
+ __name(this, "MockConnection");
1678
+ }
1679
+ client;
1680
+ server;
1681
+ id;
1682
+ constructor(client) {
1683
+ this.client = client;
1684
+ this.state = {};
1685
+ this.server = client.server;
1686
+ this.id = client.id;
1687
+ }
1688
+ state;
1689
+ setState(value) {
1690
+ this.state = value;
1691
+ }
1692
+ send(data) {
1693
+ this.client._trigger("message", data);
1694
+ }
1695
+ close() {
1696
+ this.server.onClose(this);
1697
+ }
2021
1698
  };
2022
1699
  var ServerIo = MockPartyRoom;
2023
1700
  var ClientIo = MockPartyClient;
2024
-
2025
- // src/jwt.ts
2026
1701
  var JWTAuth = class {
2027
- static {
2028
- __name(this, "JWTAuth");
2029
- }
2030
- secret;
2031
- encoder;
2032
- decoder;
2033
- /**
2034
- * Constructor for the JWTAuth class
2035
- * @param {string} secret - The secret key used for signing and verifying tokens
2036
- */
2037
- constructor(secret) {
2038
- if (!secret || typeof secret !== "string") {
2039
- throw new Error("Secret is required and must be a string");
2040
- }
2041
- this.secret = secret;
2042
- this.encoder = new TextEncoder();
2043
- this.decoder = new TextDecoder();
2044
- }
2045
- /**
2046
- * Convert the secret to a CryptoKey for HMAC operations
2047
- * @returns {Promise<CryptoKey>} - The CryptoKey for HMAC operations
2048
- */
2049
- async getSecretKey() {
2050
- const keyData = this.encoder.encode(this.secret);
2051
- return await crypto.subtle.importKey(
2052
- "raw",
2053
- keyData,
2054
- {
2055
- name: "HMAC",
2056
- hash: {
2057
- name: "SHA-256"
2058
- }
2059
- },
2060
- false,
2061
- [
2062
- "sign",
2063
- "verify"
2064
- ]
2065
- // key usages
2066
- );
2067
- }
2068
- /**
2069
- * Base64Url encode a buffer
2070
- * @param {ArrayBuffer} buffer - The buffer to encode
2071
- * @returns {string} - The base64url encoded string
2072
- */
2073
- base64UrlEncode(buffer) {
2074
- const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));
2075
- return base64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
2076
- }
2077
- /**
2078
- * Base64Url decode a string
2079
- * @param {string} base64Url - The base64url encoded string
2080
- * @returns {ArrayBuffer} - The decoded buffer
2081
- */
2082
- base64UrlDecode(base64Url) {
2083
- const padding = "=".repeat((4 - base64Url.length % 4) % 4);
2084
- const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/") + padding;
2085
- const rawData = atob(base64);
2086
- const buffer = new Uint8Array(rawData.length);
2087
- for (let i = 0; i < rawData.length; i++) {
2088
- buffer[i] = rawData.charCodeAt(i);
2089
- }
2090
- return buffer.buffer;
2091
- }
2092
- /**
2093
- * Sign a payload and create a JWT token
2094
- * @param {JWTPayload} payload - The payload to include in the token
2095
- * @param {JWTOptions} [options={}] - Options for the token
2096
- * @param {string | number} [options.expiresIn='1h'] - Token expiration time
2097
- * @returns {Promise<string>} - The JWT token
2098
- */
2099
- async sign(payload, options = {}) {
2100
- if (!payload || typeof payload !== "object") {
2101
- throw new Error("Payload must be an object");
2102
- }
2103
- const expiresIn = options.expiresIn || "1h";
2104
- let exp;
2105
- if (typeof expiresIn === "number") {
2106
- exp = Math.floor(Date.now() / 1e3) + expiresIn;
2107
- } else if (typeof expiresIn === "string") {
2108
- const match = expiresIn.match(/^(\d+)([smhd])$/);
2109
- if (match) {
2110
- const value = parseInt(match[1]);
2111
- const unit = match[2];
2112
- const seconds = {
2113
- "s": value,
2114
- "m": value * 60,
2115
- "h": value * 60 * 60,
2116
- "d": value * 60 * 60 * 24
2117
- }[unit];
2118
- exp = Math.floor(Date.now() / 1e3) + seconds;
2119
- } else {
2120
- throw new Error('Invalid expiresIn format. Use a number (seconds) or a string like "1h", "30m", etc.');
2121
- }
2122
- }
2123
- const fullPayload = {
2124
- ...payload,
2125
- iat: Math.floor(Date.now() / 1e3),
2126
- exp
2127
- };
2128
- const header = {
2129
- alg: "HS256",
2130
- typ: "JWT"
2131
- };
2132
- const encodedHeader = this.base64UrlEncode(this.encoder.encode(JSON.stringify(header)));
2133
- const encodedPayload = this.base64UrlEncode(this.encoder.encode(JSON.stringify(fullPayload)));
2134
- const signatureBase = `${encodedHeader}.${encodedPayload}`;
2135
- const key = await this.getSecretKey();
2136
- const signature = await crypto.subtle.sign({
2137
- name: "HMAC"
2138
- }, key, this.encoder.encode(signatureBase));
2139
- const encodedSignature = this.base64UrlEncode(signature);
2140
- return `${signatureBase}.${encodedSignature}`;
2141
- }
2142
- /**
2143
- * Verify a JWT token and return the decoded payload
2144
- * @param {string} token - The JWT token to verify
2145
- * @returns {Promise<JWTPayload>} - The decoded payload if verification succeeds
2146
- * @throws {Error} - If verification fails
2147
- */
2148
- async verify(token) {
2149
- if (!token || typeof token !== "string") {
2150
- throw new Error("Token is required and must be a string");
2151
- }
2152
- const parts = token.split(".");
2153
- if (parts.length !== 3) {
2154
- throw new Error("Invalid token format");
2155
- }
2156
- const [encodedHeader, encodedPayload, encodedSignature] = parts;
2157
- try {
2158
- const header = JSON.parse(this.decoder.decode(this.base64UrlDecode(encodedHeader)));
2159
- const payload = JSON.parse(this.decoder.decode(this.base64UrlDecode(encodedPayload)));
2160
- if (header.alg !== "HS256") {
2161
- throw new Error(`Unsupported algorithm: ${header.alg}`);
2162
- }
2163
- const now = Math.floor(Date.now() / 1e3);
2164
- if (payload.exp && payload.exp < now) {
2165
- throw new Error("Token has expired");
2166
- }
2167
- const key = await this.getSecretKey();
2168
- const signatureBase = `${encodedHeader}.${encodedPayload}`;
2169
- const signature = this.base64UrlDecode(encodedSignature);
2170
- const isValid = await crypto.subtle.verify({
2171
- name: "HMAC"
2172
- }, key, signature, this.encoder.encode(signatureBase));
2173
- if (!isValid) {
2174
- throw new Error("Invalid signature");
2175
- }
2176
- return payload;
2177
- } catch (error) {
2178
- if (error instanceof Error) {
2179
- throw new Error(`Token verification failed: ${error.message}`);
2180
- }
2181
- throw new Error("Token verification failed: Unknown error");
2182
- }
2183
- }
1702
+ static {
1703
+ __name(this, "JWTAuth");
1704
+ }
1705
+ secret;
1706
+ encoder;
1707
+ decoder;
1708
+ /**
1709
+ * Constructor for the JWTAuth class
1710
+ * @param {string} secret - The secret key used for signing and verifying tokens
1711
+ */
1712
+ constructor(secret) {
1713
+ if (!secret || typeof secret !== "string") throw new Error("Secret is required and must be a string");
1714
+ this.secret = secret;
1715
+ this.encoder = new TextEncoder();
1716
+ this.decoder = new TextDecoder();
1717
+ }
1718
+ /**
1719
+ * Convert the secret to a CryptoKey for HMAC operations
1720
+ * @returns {Promise<CryptoKey>} - The CryptoKey for HMAC operations
1721
+ */
1722
+ async getSecretKey() {
1723
+ const keyData = this.encoder.encode(this.secret);
1724
+ return await crypto.subtle.importKey("raw", keyData, {
1725
+ name: "HMAC",
1726
+ hash: { name: "SHA-256" }
1727
+ }, false, ["sign", "verify"]);
1728
+ }
1729
+ /**
1730
+ * Base64Url encode a buffer
1731
+ * @param {ArrayBuffer} buffer - The buffer to encode
1732
+ * @returns {string} - The base64url encoded string
1733
+ */
1734
+ base64UrlEncode(buffer) {
1735
+ return btoa(String.fromCharCode(...new Uint8Array(buffer))).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
1736
+ }
1737
+ /**
1738
+ * Base64Url decode a string
1739
+ * @param {string} base64Url - The base64url encoded string
1740
+ * @returns {ArrayBuffer} - The decoded buffer
1741
+ */
1742
+ base64UrlDecode(base64Url) {
1743
+ const padding = "=".repeat((4 - base64Url.length % 4) % 4);
1744
+ const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/") + padding;
1745
+ const rawData = atob(base64);
1746
+ const buffer = new Uint8Array(rawData.length);
1747
+ for (let i = 0; i < rawData.length; i++) buffer[i] = rawData.charCodeAt(i);
1748
+ return buffer.buffer;
1749
+ }
1750
+ /**
1751
+ * Sign a payload and create a JWT token
1752
+ * @param {JWTPayload} payload - The payload to include in the token
1753
+ * @param {JWTOptions} [options={}] - Options for the token
1754
+ * @param {string | number} [options.expiresIn='1h'] - Token expiration time
1755
+ * @returns {Promise<string>} - The JWT token
1756
+ */
1757
+ async sign(payload, options = {}) {
1758
+ if (!payload || typeof payload !== "object") throw new Error("Payload must be an object");
1759
+ const expiresIn = options.expiresIn || "1h";
1760
+ let exp;
1761
+ if (typeof expiresIn === "number") exp = Math.floor(Date.now() / 1e3) + expiresIn;
1762
+ else if (typeof expiresIn === "string") {
1763
+ const match = expiresIn.match(/^(\d+)([smhd])$/);
1764
+ if (match) {
1765
+ const value = parseInt(match[1]);
1766
+ const unit = match[2];
1767
+ const seconds = {
1768
+ "s": value,
1769
+ "m": value * 60,
1770
+ "h": value * 60 * 60,
1771
+ "d": value * 60 * 60 * 24
1772
+ }[unit];
1773
+ exp = Math.floor(Date.now() / 1e3) + seconds;
1774
+ } else throw new Error("Invalid expiresIn format. Use a number (seconds) or a string like \"1h\", \"30m\", etc.");
1775
+ }
1776
+ const fullPayload = {
1777
+ ...payload,
1778
+ iat: Math.floor(Date.now() / 1e3),
1779
+ exp
1780
+ };
1781
+ const signatureBase = `${this.base64UrlEncode(this.encoder.encode(JSON.stringify({
1782
+ alg: "HS256",
1783
+ typ: "JWT"
1784
+ })))}.${this.base64UrlEncode(this.encoder.encode(JSON.stringify(fullPayload)))}`;
1785
+ const key = await this.getSecretKey();
1786
+ const signature = await crypto.subtle.sign({ name: "HMAC" }, key, this.encoder.encode(signatureBase));
1787
+ return `${signatureBase}.${this.base64UrlEncode(signature)}`;
1788
+ }
1789
+ /**
1790
+ * Verify a JWT token and return the decoded payload
1791
+ * @param {string} token - The JWT token to verify
1792
+ * @returns {Promise<JWTPayload>} - The decoded payload if verification succeeds
1793
+ * @throws {Error} - If verification fails
1794
+ */
1795
+ async verify(token) {
1796
+ if (!token || typeof token !== "string") throw new Error("Token is required and must be a string");
1797
+ const parts = token.split(".");
1798
+ if (parts.length !== 3) throw new Error("Invalid token format");
1799
+ const [encodedHeader, encodedPayload, encodedSignature] = parts;
1800
+ try {
1801
+ const header = JSON.parse(this.decoder.decode(this.base64UrlDecode(encodedHeader)));
1802
+ const payload = JSON.parse(this.decoder.decode(this.base64UrlDecode(encodedPayload)));
1803
+ if (header.alg !== "HS256") throw new Error(`Unsupported algorithm: ${header.alg}`);
1804
+ const now = Math.floor(Date.now() / 1e3);
1805
+ if (payload.exp && payload.exp < now) throw new Error("Token has expired");
1806
+ const key = await this.getSecretKey();
1807
+ const signatureBase = `${encodedHeader}.${encodedPayload}`;
1808
+ const signature = this.base64UrlDecode(encodedSignature);
1809
+ if (!await crypto.subtle.verify({ name: "HMAC" }, key, signature, this.encoder.encode(signatureBase))) throw new Error("Invalid signature");
1810
+ return payload;
1811
+ } catch (error) {
1812
+ if (error instanceof Error) throw new Error(`Token verification failed: ${error.message}`);
1813
+ throw new Error("Token verification failed: Unknown error");
1814
+ }
1815
+ }
2184
1816
  };
2185
-
2186
- // src/world.guard.ts
2187
1817
  var guardManageWorld = /* @__PURE__ */ __name(async (_, req, room) => {
2188
- const tokenShard = req.headers.get("x-access-shard");
2189
- if (tokenShard) {
2190
- if (tokenShard !== room.env.SHARD_SECRET) {
2191
- return false;
2192
- }
2193
- return true;
2194
- }
2195
- const url = new URL(req.url);
2196
- const token = req.headers.get("Authorization") ?? url.searchParams.get("world-auth-token");
2197
- if (!token) {
2198
- return false;
2199
- }
2200
- const jwt = new JWTAuth(room.env.AUTH_JWT_SECRET);
2201
- try {
2202
- const payload = await jwt.verify(token);
2203
- if (!payload) {
2204
- return false;
2205
- }
2206
- } catch (error) {
2207
- return false;
2208
- }
2209
- return true;
1818
+ const tokenShard = req.headers.get("x-access-shard");
1819
+ if (tokenShard) {
1820
+ if (tokenShard !== room.env.SHARD_SECRET) return false;
1821
+ return true;
1822
+ }
1823
+ const url = new URL(req.url);
1824
+ const token = req.headers.get("Authorization") ?? url.searchParams.get("world-auth-token");
1825
+ if (!token) return false;
1826
+ const jwt = new JWTAuth(room.env.AUTH_JWT_SECRET);
1827
+ try {
1828
+ if (!await jwt.verify(token)) return false;
1829
+ } catch (error) {
1830
+ return false;
1831
+ }
1832
+ return true;
2210
1833
  }, "guardManageWorld");
2211
-
2212
- // src/world.ts
2213
1834
  function _ts_decorate(decorators, target, key, desc) {
2214
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2215
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
2216
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
2217
- return c > 3 && r && Object.defineProperty(target, key, r), r;
1835
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1836
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1837
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1838
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
2218
1839
  }
2219
1840
  __name(_ts_decorate, "_ts_decorate");
2220
1841
  function _ts_metadata(k, v) {
2221
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1842
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
2222
1843
  }
2223
1844
  __name(_ts_metadata, "_ts_metadata");
2224
1845
  var MAX_PLAYERS_PER_SHARD = 75;
2225
1846
  z.object({
2226
- name: z.string(),
2227
- balancingStrategy: z.enum([
2228
- "round-robin",
2229
- "least-connections",
2230
- "random"
2231
- ]),
2232
- public: z.boolean(),
2233
- maxPlayersPerShard: z.number().int().positive(),
2234
- minShards: z.number().int().min(0),
2235
- maxShards: z.number().int().positive().optional()
1847
+ name: z.string(),
1848
+ balancingStrategy: z.enum([
1849
+ "round-robin",
1850
+ "least-connections",
1851
+ "random"
1852
+ ]),
1853
+ public: z.boolean(),
1854
+ maxPlayersPerShard: z.number().int().positive(),
1855
+ minShards: z.number().int().min(0),
1856
+ maxShards: z.number().int().positive().optional()
2236
1857
  });
2237
1858
  z.object({
2238
- shardId: z.string(),
2239
- roomId: z.string(),
2240
- url: z.string().url(),
2241
- maxConnections: z.number().int().positive()
1859
+ shardId: z.string(),
1860
+ roomId: z.string(),
1861
+ url: z.string().url(),
1862
+ maxConnections: z.number().int().positive()
2242
1863
  });
2243
1864
  z.object({
2244
- connections: z.number().int().min(0),
2245
- status: z.enum([
2246
- "active",
2247
- "maintenance",
2248
- "draining"
2249
- ]).optional()
1865
+ connections: z.number().int().min(0),
1866
+ status: z.enum([
1867
+ "active",
1868
+ "maintenance",
1869
+ "draining"
1870
+ ]).optional()
2250
1871
  });
2251
1872
  z.object({
2252
- roomId: z.string(),
2253
- targetShardCount: z.number().int().positive(),
2254
- shardTemplate: z.object({
2255
- urlTemplate: z.string(),
2256
- maxConnections: z.number().int().positive()
2257
- }).optional()
1873
+ roomId: z.string(),
1874
+ targetShardCount: z.number().int().positive(),
1875
+ shardTemplate: z.object({
1876
+ urlTemplate: z.string(),
1877
+ maxConnections: z.number().int().positive()
1878
+ }).optional()
2258
1879
  });
2259
1880
  var RoomConfig = class RoomConfig2 {
2260
- static {
2261
- __name(this, "RoomConfig");
2262
- }
2263
- id;
2264
- name = signal("");
2265
- balancingStrategy = signal("round-robin");
2266
- public = signal(true);
2267
- maxPlayersPerShard = signal(MAX_PLAYERS_PER_SHARD);
2268
- minShards = signal(1);
2269
- maxShards = signal(void 0);
1881
+ static {
1882
+ __name(this, "RoomConfig");
1883
+ }
1884
+ id;
1885
+ name = signal("");
1886
+ balancingStrategy = signal("round-robin");
1887
+ public = signal(true);
1888
+ maxPlayersPerShard = signal(MAX_PLAYERS_PER_SHARD);
1889
+ minShards = signal(1);
1890
+ maxShards = signal(void 0);
2270
1891
  };
2271
- _ts_decorate([
2272
- id(),
2273
- _ts_metadata("design:type", String)
2274
- ], RoomConfig.prototype, "id", void 0);
2275
- _ts_decorate([
2276
- sync()
2277
- ], RoomConfig.prototype, "name", void 0);
2278
- _ts_decorate([
2279
- sync()
2280
- ], RoomConfig.prototype, "balancingStrategy", void 0);
2281
- _ts_decorate([
2282
- sync()
2283
- ], RoomConfig.prototype, "public", void 0);
2284
- _ts_decorate([
2285
- sync()
2286
- ], RoomConfig.prototype, "maxPlayersPerShard", void 0);
2287
- _ts_decorate([
2288
- sync()
2289
- ], RoomConfig.prototype, "minShards", void 0);
2290
- _ts_decorate([
2291
- sync()
2292
- ], RoomConfig.prototype, "maxShards", void 0);
1892
+ _ts_decorate([id(), _ts_metadata("design:type", String)], RoomConfig.prototype, "id", void 0);
1893
+ _ts_decorate([sync()], RoomConfig.prototype, "name", void 0);
1894
+ _ts_decorate([sync()], RoomConfig.prototype, "balancingStrategy", void 0);
1895
+ _ts_decorate([sync()], RoomConfig.prototype, "public", void 0);
1896
+ _ts_decorate([sync()], RoomConfig.prototype, "maxPlayersPerShard", void 0);
1897
+ _ts_decorate([sync()], RoomConfig.prototype, "minShards", void 0);
1898
+ _ts_decorate([sync()], RoomConfig.prototype, "maxShards", void 0);
2293
1899
  var ShardInfo = class ShardInfo2 {
2294
- static {
2295
- __name(this, "ShardInfo");
2296
- }
2297
- id;
2298
- roomId = signal("");
2299
- url = signal("");
2300
- currentConnections = signal(0);
2301
- maxConnections = signal(MAX_PLAYERS_PER_SHARD);
2302
- status = signal("active");
2303
- lastHeartbeat = signal(0);
1900
+ static {
1901
+ __name(this, "ShardInfo");
1902
+ }
1903
+ id;
1904
+ roomId = signal("");
1905
+ url = signal("");
1906
+ currentConnections = signal(0);
1907
+ maxConnections = signal(MAX_PLAYERS_PER_SHARD);
1908
+ status = signal("active");
1909
+ lastHeartbeat = signal(0);
2304
1910
  };
2305
- _ts_decorate([
2306
- id(),
2307
- _ts_metadata("design:type", String)
2308
- ], ShardInfo.prototype, "id", void 0);
2309
- _ts_decorate([
2310
- sync()
2311
- ], ShardInfo.prototype, "roomId", void 0);
2312
- _ts_decorate([
2313
- sync()
2314
- ], ShardInfo.prototype, "url", void 0);
2315
- _ts_decorate([
2316
- sync({
2317
- persist: false
2318
- })
2319
- ], ShardInfo.prototype, "currentConnections", void 0);
2320
- _ts_decorate([
2321
- sync()
2322
- ], ShardInfo.prototype, "maxConnections", void 0);
2323
- _ts_decorate([
2324
- sync()
2325
- ], ShardInfo.prototype, "status", void 0);
2326
- _ts_decorate([
2327
- sync()
2328
- ], ShardInfo.prototype, "lastHeartbeat", void 0);
1911
+ _ts_decorate([id(), _ts_metadata("design:type", String)], ShardInfo.prototype, "id", void 0);
1912
+ _ts_decorate([sync()], ShardInfo.prototype, "roomId", void 0);
1913
+ _ts_decorate([sync()], ShardInfo.prototype, "url", void 0);
1914
+ _ts_decorate([sync({ persist: false })], ShardInfo.prototype, "currentConnections", void 0);
1915
+ _ts_decorate([sync()], ShardInfo.prototype, "maxConnections", void 0);
1916
+ _ts_decorate([sync()], ShardInfo.prototype, "status", void 0);
1917
+ _ts_decorate([sync()], ShardInfo.prototype, "lastHeartbeat", void 0);
2329
1918
  var WorldRoom = class {
2330
- static {
2331
- __name(this, "WorldRoom");
2332
- }
2333
- room;
2334
- // Synchronized state
2335
- rooms;
2336
- shards;
2337
- // Only persisted state (not synced to clients)
2338
- rrCounters;
2339
- // Configuration
2340
- defaultShardUrlTemplate;
2341
- defaultMaxConnectionsPerShard;
2342
- constructor(room) {
2343
- this.room = room;
2344
- this.rooms = signal({});
2345
- this.shards = signal({});
2346
- this.rrCounters = signal({});
2347
- this.defaultShardUrlTemplate = signal("{shardId}");
2348
- this.defaultMaxConnectionsPerShard = signal(MAX_PLAYERS_PER_SHARD);
2349
- const { AUTH_JWT_SECRET, SHARD_SECRET } = this.room.env;
2350
- if (!AUTH_JWT_SECRET) {
2351
- throw new Error("AUTH_JWT_SECRET env variable is not set");
2352
- }
2353
- if (!SHARD_SECRET) {
2354
- throw new Error("SHARD_SECRET env variable is not set");
2355
- }
2356
- }
2357
- async onJoin(user, conn, ctx) {
2358
- const canConnect = await guardManageWorld(user, ctx.request, this.room);
2359
- conn.setState({
2360
- ...conn.state,
2361
- isAdmin: canConnect
2362
- });
2363
- }
2364
- interceptorPacket(_, obj, conn) {
2365
- if (!conn.state["isAdmin"]) {
2366
- return null;
2367
- }
2368
- return obj;
2369
- }
2370
- // Helper methods
2371
- cleanupInactiveShards() {
2372
- const now = Date.now();
2373
- const timeout = 5 * 60 * 1e3;
2374
- const shardsValue = this.shards();
2375
- Object.values(shardsValue).forEach((shard) => {
2376
- if (now - shard.lastHeartbeat() > timeout) {
2377
- delete this.shards()[shard.id];
2378
- }
2379
- });
2380
- setTimeout(() => this.cleanupInactiveShards(), 6e4);
2381
- }
2382
- // Actions
2383
- async registerRoom(req) {
2384
- const roomConfig = await req.json();
2385
- const roomId = roomConfig.name;
2386
- if (!this.rooms()[roomId]) {
2387
- const newRoom = new RoomConfig();
2388
- newRoom.id = roomId;
2389
- newRoom.name.set(roomConfig.name);
2390
- newRoom.balancingStrategy.set(roomConfig.balancingStrategy);
2391
- newRoom.public.set(roomConfig.public);
2392
- newRoom.maxPlayersPerShard.set(roomConfig.maxPlayersPerShard);
2393
- newRoom.minShards.set(roomConfig.minShards);
2394
- newRoom.maxShards.set(roomConfig.maxShards);
2395
- this.rooms()[roomId] = newRoom;
2396
- if (roomConfig.minShards > 0) {
2397
- for (let i = 0; i < roomConfig.minShards; i++) {
2398
- await this.createShard(roomId);
2399
- }
2400
- }
2401
- } else {
2402
- const room = this.rooms()[roomId];
2403
- room.balancingStrategy.set(roomConfig.balancingStrategy);
2404
- room.public.set(roomConfig.public);
2405
- room.maxPlayersPerShard.set(roomConfig.maxPlayersPerShard);
2406
- room.minShards.set(roomConfig.minShards);
2407
- room.maxShards.set(roomConfig.maxShards);
2408
- }
2409
- }
2410
- async updateShardStats(req, res) {
2411
- const body = await req.json();
2412
- const { shardId, connections, status } = body;
2413
- const shard = this.shards()[shardId];
2414
- if (!shard) {
2415
- return res.notFound(`Shard ${shardId} not found`);
2416
- }
2417
- shard.currentConnections.set(connections);
2418
- if (status) {
2419
- shard.status.set(status);
2420
- }
2421
- shard.lastHeartbeat.set(Date.now());
2422
- }
2423
- async scaleRoom(req, res) {
2424
- const data = await req.json();
2425
- const { targetShardCount, shardTemplate, roomId } = data;
2426
- const room = this.rooms()[roomId];
2427
- if (!room) {
2428
- return res.notFound(`Room ${roomId} does not exist`);
2429
- }
2430
- const roomShards = Object.values(this.shards()).filter((shard) => shard.roomId() === roomId);
2431
- const previousShardCount = roomShards.length;
2432
- if (room.maxShards() !== void 0 && targetShardCount > room.maxShards()) {
2433
- return res.badRequest(`Cannot scale beyond maximum allowed shards (${room.maxShards()})`, {
2434
- roomId,
2435
- currentShardCount: previousShardCount
2436
- });
2437
- }
2438
- if (targetShardCount < previousShardCount) {
2439
- const shardsToRemove = [
2440
- ...roomShards
2441
- ].sort((a, b) => {
2442
- if (a.status() === "draining" && b.status() !== "draining") return -1;
2443
- if (a.status() !== "draining" && b.status() === "draining") return 1;
2444
- return a.currentConnections() - b.currentConnections();
2445
- }).slice(0, previousShardCount - targetShardCount);
2446
- roomShards.filter((shard) => !shardsToRemove.some((s) => s.id === shard.id));
2447
- for (const shard of shardsToRemove) {
2448
- delete this.shards()[shard.id];
2449
- }
2450
- return;
2451
- }
2452
- if (targetShardCount > previousShardCount) {
2453
- for (let i = 0; i < targetShardCount - previousShardCount; i++) {
2454
- await this.createShard(roomId, shardTemplate?.urlTemplate, shardTemplate?.maxConnections);
2455
- }
2456
- }
2457
- }
2458
- async connect(req, res) {
2459
- try {
2460
- let data;
2461
- try {
2462
- const body = await req.text();
2463
- if (!body || body.trim() === "") {
2464
- return res.badRequest("Request body is empty");
2465
- }
2466
- data = JSON.parse(body);
2467
- } catch (parseError) {
2468
- return res.badRequest("Invalid JSON in request body");
2469
- }
2470
- if (!data.roomId) {
2471
- return res.badRequest("roomId parameter is required");
2472
- }
2473
- const autoCreate = data.autoCreate !== void 0 ? data.autoCreate : true;
2474
- const result = await this.findOptimalShard(data.roomId, autoCreate);
2475
- if ("error" in result) {
2476
- return res.notFound(result.error);
2477
- }
2478
- return res.success({
2479
- success: true,
2480
- shardId: result.shardId,
2481
- url: result.url
2482
- });
2483
- } catch (error) {
2484
- console.error("Error connecting to shard:", error);
2485
- return res.serverError();
2486
- }
2487
- }
2488
- async findOptimalShard(roomId, autoCreate = true) {
2489
- let room = this.rooms()[roomId];
2490
- if (!room) {
2491
- if (autoCreate) {
2492
- const mockRequest = {
2493
- json: /* @__PURE__ */ __name(async () => ({
2494
- name: roomId,
2495
- balancingStrategy: "round-robin",
2496
- public: true,
2497
- maxPlayersPerShard: this.defaultMaxConnectionsPerShard(),
2498
- minShards: 1,
2499
- maxShards: void 0
2500
- }), "json")
2501
- };
2502
- await this.registerRoom(mockRequest);
2503
- room = this.rooms()[roomId];
2504
- if (!room) {
2505
- return {
2506
- error: `Failed to create room ${roomId}`
2507
- };
2508
- }
2509
- } else {
2510
- return {
2511
- error: `Room ${roomId} does not exist`
2512
- };
2513
- }
2514
- }
2515
- const roomShards = Object.values(this.shards()).filter((shard) => shard.roomId() === roomId);
2516
- if (roomShards.length === 0) {
2517
- if (autoCreate) {
2518
- const newShard = await this.createShard(roomId);
2519
- if (newShard) {
2520
- return {
2521
- shardId: newShard.id,
2522
- url: newShard.url()
2523
- };
2524
- } else {
2525
- return {
2526
- error: `Failed to create shard for room ${roomId}`
2527
- };
2528
- }
2529
- } else {
2530
- return {
2531
- error: `No shards available for room ${roomId}`
2532
- };
2533
- }
2534
- }
2535
- const activeShards = roomShards.filter((shard) => shard && shard.status() === "active");
2536
- if (activeShards.length === 0) {
2537
- return {
2538
- error: `No active shards available for room ${roomId}`
2539
- };
2540
- }
2541
- const balancingStrategy = room.balancingStrategy();
2542
- let selectedShard;
2543
- switch (balancingStrategy) {
2544
- case "least-connections":
2545
- selectedShard = activeShards.reduce((min, shard) => shard.currentConnections() < min.currentConnections() ? shard : min, activeShards[0]);
2546
- break;
2547
- case "random":
2548
- selectedShard = activeShards[Math.floor(Math.random() * activeShards.length)];
2549
- break;
2550
- case "round-robin":
2551
- default:
2552
- const counter = this.rrCounters()[roomId] || 0;
2553
- const nextCounter = (counter + 1) % activeShards.length;
2554
- this.rrCounters()[roomId] = nextCounter;
2555
- selectedShard = activeShards[counter];
2556
- break;
2557
- }
2558
- return {
2559
- shardId: selectedShard.id,
2560
- url: selectedShard.url()
2561
- };
2562
- }
2563
- // Private methods
2564
- async createShard(roomId, urlTemplate, maxConnections) {
2565
- const room = this.rooms()[roomId];
2566
- if (!room) {
2567
- console.error(`Cannot create shard for non-existent room: ${roomId}`);
2568
- return null;
2569
- }
2570
- const shardId = `${roomId}:${Date.now()}-${Math.floor(Math.random() * 1e4)}`;
2571
- const template = urlTemplate || this.defaultShardUrlTemplate();
2572
- const url = template.replace("{shardId}", shardId).replace("{roomId}", roomId);
2573
- const max = maxConnections || room.maxPlayersPerShard();
2574
- const newShard = new ShardInfo();
2575
- newShard.id = shardId;
2576
- newShard.roomId.set(roomId);
2577
- newShard.url.set(url);
2578
- newShard.maxConnections.set(max);
2579
- newShard.currentConnections.set(0);
2580
- newShard.status.set("active");
2581
- newShard.lastHeartbeat.set(Date.now());
2582
- this.shards()[shardId] = newShard;
2583
- return newShard;
2584
- }
1919
+ static {
1920
+ __name(this, "WorldRoom");
1921
+ }
1922
+ room;
1923
+ rooms;
1924
+ shards;
1925
+ rrCounters;
1926
+ defaultShardUrlTemplate;
1927
+ defaultMaxConnectionsPerShard;
1928
+ constructor(room) {
1929
+ this.room = room;
1930
+ this.rooms = signal({});
1931
+ this.shards = signal({});
1932
+ this.rrCounters = signal({});
1933
+ this.defaultShardUrlTemplate = signal("{shardId}");
1934
+ this.defaultMaxConnectionsPerShard = signal(MAX_PLAYERS_PER_SHARD);
1935
+ const { AUTH_JWT_SECRET, SHARD_SECRET } = this.room.env;
1936
+ if (!AUTH_JWT_SECRET) throw new Error("AUTH_JWT_SECRET env variable is not set");
1937
+ if (!SHARD_SECRET) throw new Error("SHARD_SECRET env variable is not set");
1938
+ }
1939
+ async onJoin(user, conn, ctx) {
1940
+ const canConnect = await guardManageWorld(user, ctx.request, this.room);
1941
+ conn.setState({
1942
+ ...conn.state,
1943
+ isAdmin: canConnect
1944
+ });
1945
+ }
1946
+ interceptorPacket(_, obj, conn) {
1947
+ if (!conn.state["isAdmin"]) return null;
1948
+ return obj;
1949
+ }
1950
+ cleanupInactiveShards() {
1951
+ const now = Date.now();
1952
+ const timeout = 300 * 1e3;
1953
+ const shardsValue = this.shards();
1954
+ Object.values(shardsValue).forEach((shard) => {
1955
+ if (now - shard.lastHeartbeat() > timeout) delete this.shards()[shard.id];
1956
+ });
1957
+ setTimeout(() => this.cleanupInactiveShards(), 6e4);
1958
+ }
1959
+ async registerRoom(req) {
1960
+ const roomConfig = await req.json();
1961
+ const roomId = roomConfig.name;
1962
+ if (!this.rooms()[roomId]) {
1963
+ const newRoom = new RoomConfig();
1964
+ newRoom.id = roomId;
1965
+ newRoom.name.set(roomConfig.name);
1966
+ newRoom.balancingStrategy.set(roomConfig.balancingStrategy);
1967
+ newRoom.public.set(roomConfig.public);
1968
+ newRoom.maxPlayersPerShard.set(roomConfig.maxPlayersPerShard);
1969
+ newRoom.minShards.set(roomConfig.minShards);
1970
+ newRoom.maxShards.set(roomConfig.maxShards);
1971
+ this.rooms()[roomId] = newRoom;
1972
+ if (roomConfig.minShards > 0) for (let i = 0; i < roomConfig.minShards; i++) await this.createShard(roomId);
1973
+ } else {
1974
+ const room = this.rooms()[roomId];
1975
+ room.balancingStrategy.set(roomConfig.balancingStrategy);
1976
+ room.public.set(roomConfig.public);
1977
+ room.maxPlayersPerShard.set(roomConfig.maxPlayersPerShard);
1978
+ room.minShards.set(roomConfig.minShards);
1979
+ room.maxShards.set(roomConfig.maxShards);
1980
+ }
1981
+ }
1982
+ async updateShardStats(req, res) {
1983
+ const { shardId, connections, status } = await req.json();
1984
+ const shard = this.shards()[shardId];
1985
+ if (!shard) return res.notFound(`Shard ${shardId} not found`);
1986
+ shard.currentConnections.set(connections);
1987
+ if (status) shard.status.set(status);
1988
+ shard.lastHeartbeat.set(Date.now());
1989
+ }
1990
+ async scaleRoom(req, res) {
1991
+ const { targetShardCount, shardTemplate, roomId } = await req.json();
1992
+ const room = this.rooms()[roomId];
1993
+ if (!room) return res.notFound(`Room ${roomId} does not exist`);
1994
+ const roomShards = Object.values(this.shards()).filter((shard) => shard.roomId() === roomId);
1995
+ const previousShardCount = roomShards.length;
1996
+ if (room.maxShards() !== void 0 && targetShardCount > room.maxShards()) return res.badRequest(`Cannot scale beyond maximum allowed shards (${room.maxShards()})`, {
1997
+ roomId,
1998
+ currentShardCount: previousShardCount
1999
+ });
2000
+ if (targetShardCount < previousShardCount) {
2001
+ const shardsToRemove = [...roomShards].sort((a, b) => {
2002
+ if (a.status() === "draining" && b.status() !== "draining") return -1;
2003
+ if (a.status() !== "draining" && b.status() === "draining") return 1;
2004
+ return a.currentConnections() - b.currentConnections();
2005
+ }).slice(0, previousShardCount - targetShardCount);
2006
+ roomShards.filter((shard) => !shardsToRemove.some((s) => s.id === shard.id));
2007
+ for (const shard of shardsToRemove) delete this.shards()[shard.id];
2008
+ return;
2009
+ }
2010
+ if (targetShardCount > previousShardCount) {
2011
+ const newShards = [];
2012
+ for (let i = 0; i < targetShardCount - previousShardCount; i++) {
2013
+ const newShard = await this.createShard(roomId, shardTemplate?.urlTemplate, shardTemplate?.maxConnections);
2014
+ if (newShard) newShards.push(newShard);
2015
+ }
2016
+ }
2017
+ }
2018
+ async connect(req, res) {
2019
+ try {
2020
+ let data;
2021
+ try {
2022
+ const body = await req.text();
2023
+ if (!body || body.trim() === "") return res.badRequest("Request body is empty");
2024
+ data = JSON.parse(body);
2025
+ } catch (parseError) {
2026
+ return res.badRequest("Invalid JSON in request body");
2027
+ }
2028
+ if (!data.roomId) return res.badRequest("roomId parameter is required");
2029
+ const autoCreate = data.autoCreate !== void 0 ? data.autoCreate : true;
2030
+ const result = await this.findOptimalShard(data.roomId, autoCreate);
2031
+ if ("error" in result) return res.notFound(result.error);
2032
+ return res.success({
2033
+ success: true,
2034
+ shardId: result.shardId,
2035
+ url: result.url
2036
+ });
2037
+ } catch (error) {
2038
+ console.error("Error connecting to shard:", error);
2039
+ return res.serverError();
2040
+ }
2041
+ }
2042
+ async findOptimalShard(roomId, autoCreate = true) {
2043
+ let room = this.rooms()[roomId];
2044
+ if (!room) if (autoCreate) {
2045
+ const mockRequest = { json: /* @__PURE__ */ __name(async () => ({
2046
+ name: roomId,
2047
+ balancingStrategy: "round-robin",
2048
+ public: true,
2049
+ maxPlayersPerShard: this.defaultMaxConnectionsPerShard(),
2050
+ minShards: 1,
2051
+ maxShards: void 0
2052
+ }), "json") };
2053
+ await this.registerRoom(mockRequest);
2054
+ room = this.rooms()[roomId];
2055
+ if (!room) return { error: `Failed to create room ${roomId}` };
2056
+ } else return { error: `Room ${roomId} does not exist` };
2057
+ const roomShards = Object.values(this.shards()).filter((shard) => shard.roomId() === roomId);
2058
+ if (roomShards.length === 0) if (autoCreate) {
2059
+ const newShard = await this.createShard(roomId);
2060
+ if (newShard) return {
2061
+ shardId: newShard.id,
2062
+ url: newShard.url()
2063
+ };
2064
+ else return { error: `Failed to create shard for room ${roomId}` };
2065
+ } else return { error: `No shards available for room ${roomId}` };
2066
+ const activeShards = roomShards.filter((shard) => shard && shard.status() === "active");
2067
+ if (activeShards.length === 0) return { error: `No active shards available for room ${roomId}` };
2068
+ const balancingStrategy = room.balancingStrategy();
2069
+ let selectedShard;
2070
+ switch (balancingStrategy) {
2071
+ case "least-connections":
2072
+ selectedShard = activeShards.reduce((min, shard) => shard.currentConnections() < min.currentConnections() ? shard : min, activeShards[0]);
2073
+ break;
2074
+ case "random":
2075
+ selectedShard = activeShards[Math.floor(Math.random() * activeShards.length)];
2076
+ break;
2077
+ default:
2078
+ const counter = this.rrCounters()[roomId] || 0;
2079
+ const nextCounter = (counter + 1) % activeShards.length;
2080
+ this.rrCounters()[roomId] = nextCounter;
2081
+ selectedShard = activeShards[counter];
2082
+ break;
2083
+ }
2084
+ return {
2085
+ shardId: selectedShard.id,
2086
+ url: selectedShard.url()
2087
+ };
2088
+ }
2089
+ async createShard(roomId, urlTemplate, maxConnections) {
2090
+ const room = this.rooms()[roomId];
2091
+ if (!room) {
2092
+ console.error(`Cannot create shard for non-existent room: ${roomId}`);
2093
+ return null;
2094
+ }
2095
+ const shardId = `${roomId}:${Date.now()}-${Math.floor(Math.random() * 1e4)}`;
2096
+ const url = (urlTemplate || this.defaultShardUrlTemplate()).replace("{shardId}", shardId).replace("{roomId}", roomId);
2097
+ const max = maxConnections || room.maxPlayersPerShard();
2098
+ const newShard = new ShardInfo();
2099
+ newShard.id = shardId;
2100
+ newShard.roomId.set(roomId);
2101
+ newShard.url.set(url);
2102
+ newShard.maxConnections.set(max);
2103
+ newShard.currentConnections.set(0);
2104
+ newShard.status.set("active");
2105
+ newShard.lastHeartbeat.set(Date.now());
2106
+ this.shards()[shardId] = newShard;
2107
+ return newShard;
2108
+ }
2585
2109
  };
2110
+ _ts_decorate([sync(RoomConfig)], WorldRoom.prototype, "rooms", void 0);
2111
+ _ts_decorate([sync(ShardInfo)], WorldRoom.prototype, "shards", void 0);
2112
+ _ts_decorate([persist()], WorldRoom.prototype, "rrCounters", void 0);
2586
2113
  _ts_decorate([
2587
- sync(RoomConfig)
2588
- ], WorldRoom.prototype, "rooms", void 0);
2589
- _ts_decorate([
2590
- sync(ShardInfo)
2591
- ], WorldRoom.prototype, "shards", void 0);
2592
- _ts_decorate([
2593
- persist()
2594
- ], WorldRoom.prototype, "rrCounters", void 0);
2595
- _ts_decorate([
2596
- Request2({
2597
- path: "register-room",
2598
- method: "POST"
2599
- }),
2600
- Guard([
2601
- guardManageWorld
2602
- ]),
2603
- _ts_metadata("design:type", Function),
2604
- _ts_metadata("design:paramtypes", [
2605
- Object
2606
- ]),
2607
- _ts_metadata("design:returntype", Promise)
2114
+ Request2({
2115
+ path: "register-room",
2116
+ method: "POST"
2117
+ }),
2118
+ Guard([guardManageWorld]),
2119
+ _ts_metadata("design:type", Function),
2120
+ _ts_metadata("design:paramtypes", [Object]),
2121
+ _ts_metadata("design:returntype", Promise)
2608
2122
  ], WorldRoom.prototype, "registerRoom", null);
2609
2123
  _ts_decorate([
2610
- Request2({
2611
- path: "update-shard",
2612
- method: "POST"
2613
- }),
2614
- Guard([
2615
- guardManageWorld
2616
- ]),
2617
- _ts_metadata("design:type", Function),
2618
- _ts_metadata("design:paramtypes", [
2619
- Object ,
2620
- typeof ServerResponse === "undefined" ? Object : ServerResponse
2621
- ]),
2622
- _ts_metadata("design:returntype", Promise)
2124
+ Request2({
2125
+ path: "update-shard",
2126
+ method: "POST"
2127
+ }),
2128
+ Guard([guardManageWorld]),
2129
+ _ts_metadata("design:type", Function),
2130
+ _ts_metadata("design:paramtypes", [Object, typeof ServerResponse === "undefined" ? Object : ServerResponse]),
2131
+ _ts_metadata("design:returntype", Promise)
2623
2132
  ], WorldRoom.prototype, "updateShardStats", null);
2624
2133
  _ts_decorate([
2625
- Request2({
2626
- path: "scale-room",
2627
- method: "POST"
2628
- }),
2629
- Guard([
2630
- guardManageWorld
2631
- ]),
2632
- _ts_metadata("design:type", Function),
2633
- _ts_metadata("design:paramtypes", [
2634
- Object ,
2635
- typeof ServerResponse === "undefined" ? Object : ServerResponse
2636
- ]),
2637
- _ts_metadata("design:returntype", Promise)
2134
+ Request2({
2135
+ path: "scale-room",
2136
+ method: "POST"
2137
+ }),
2138
+ Guard([guardManageWorld]),
2139
+ _ts_metadata("design:type", Function),
2140
+ _ts_metadata("design:paramtypes", [Object, typeof ServerResponse === "undefined" ? Object : ServerResponse]),
2141
+ _ts_metadata("design:returntype", Promise)
2638
2142
  ], WorldRoom.prototype, "scaleRoom", null);
2639
2143
  _ts_decorate([
2640
- Request2({
2641
- path: "connect",
2642
- method: "POST"
2643
- }),
2644
- _ts_metadata("design:type", Function),
2645
- _ts_metadata("design:paramtypes", [
2646
- Object ,
2647
- typeof ServerResponse === "undefined" ? Object : ServerResponse
2648
- ]),
2649
- _ts_metadata("design:returntype", Promise)
2144
+ Request2({
2145
+ path: "connect",
2146
+ method: "POST"
2147
+ }),
2148
+ _ts_metadata("design:type", Function),
2149
+ _ts_metadata("design:paramtypes", [Object, typeof ServerResponse === "undefined" ? Object : ServerResponse]),
2150
+ _ts_metadata("design:returntype", Promise)
2650
2151
  ], WorldRoom.prototype, "connect", null);
2651
2152
  WorldRoom = _ts_decorate([
2652
- Room({
2653
- path: "world-{worldId}",
2654
- maxUsers: 100,
2655
- throttleStorage: 2e3,
2656
- throttleSync: 500
2657
- }),
2658
- _ts_metadata("design:type", Function),
2659
- _ts_metadata("design:paramtypes", [
2660
- Object
2661
- ])
2153
+ Room({
2154
+ path: "world-{worldId}",
2155
+ maxUsers: 100,
2156
+ throttleStorage: 2e3,
2157
+ throttleSync: 500
2158
+ }),
2159
+ _ts_metadata("design:type", Function),
2160
+ _ts_metadata("design:paramtypes", [Object])
2662
2161
  ], WorldRoom);
2663
-
2664
- // src/session.guard.ts
2665
2162
  function createRequireSessionGuard(storage) {
2666
- return async (sender, value) => {
2667
- if (!sender || !sender.id) {
2668
- return false;
2669
- }
2670
- try {
2671
- const session = await storage.get(`session:${sender.id}`);
2672
- if (!session) {
2673
- return false;
2674
- }
2675
- const typedSession = session;
2676
- if (!typedSession.publicId) {
2677
- return false;
2678
- }
2679
- return true;
2680
- } catch (error) {
2681
- console.error("Error checking session in requireSession guard:", error);
2682
- return false;
2683
- }
2684
- };
2163
+ return async (sender, value) => {
2164
+ if (!sender || !sender.id) return false;
2165
+ try {
2166
+ const session = await storage.get(`session:${sender.id}`);
2167
+ if (!session) return false;
2168
+ if (!session.publicId) return false;
2169
+ return true;
2170
+ } catch (error) {
2171
+ console.error("Error checking session in requireSession guard:", error);
2172
+ return false;
2173
+ }
2174
+ };
2685
2175
  }
2686
2176
  __name(createRequireSessionGuard, "createRequireSessionGuard");
2177
+ //#endregion
2178
+ export { ClientIo, ServerIo };
2687
2179
 
2688
- export { Action, ClientIo, Guard, MockConnection, Request2 as Request, Room, RoomGuard, Server, ServerIo, ServerResponse, Shard, WorldRoom, createRequireSessionGuard, request, testRoom, tick };
2689
- //# sourceMappingURL=index.js.map
2180
+ //# sourceMappingURL=index.js.map