@signe/room 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +121 -14
- package/dist/index.js +298 -97
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/readme.md +18 -10
- package/src/index.ts +2 -1
- package/src/request/cors.ts +54 -0
- package/src/request/response.ts +228 -0
- package/src/server.ts +31 -45
- package/src/world.ts +14 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signe/room",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"keywords": [],
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"dset": "^3.1.3",
|
|
18
18
|
"partysocket": "^1.0.1",
|
|
19
19
|
"zod": "^3.23.8",
|
|
20
|
-
"@signe/sync": "2.0.
|
|
20
|
+
"@signe/sync": "2.0.1"
|
|
21
21
|
},
|
|
22
22
|
"publishConfig": {
|
|
23
23
|
"access": "public"
|
package/readme.md
CHANGED
|
@@ -75,7 +75,7 @@ The `@Request` decorator allows you to handle HTTP requests with specific routes
|
|
|
75
75
|
|
|
76
76
|
```ts
|
|
77
77
|
import { z } from "zod";
|
|
78
|
-
import { Room, Request, RequestGuard } from "@signe/room";
|
|
78
|
+
import { Room, Request, RequestGuard, ServerResponse } from "@signe/room";
|
|
79
79
|
|
|
80
80
|
@Room({
|
|
81
81
|
path: "api"
|
|
@@ -97,10 +97,10 @@ class ApiRoom {
|
|
|
97
97
|
|
|
98
98
|
// Handle requests with path parameters
|
|
99
99
|
@Request({ path: "/players/:id" })
|
|
100
|
-
getPlayer(req: Party.Request,
|
|
101
|
-
const player = this.players()[params.id];
|
|
100
|
+
getPlayer(req: Party.Request, res: ServerResponse) {
|
|
101
|
+
const player = this.players()[req.params.id];
|
|
102
102
|
if (!player) {
|
|
103
|
-
return
|
|
103
|
+
return res.notFound("Player not found");
|
|
104
104
|
}
|
|
105
105
|
return player;
|
|
106
106
|
}
|
|
@@ -113,10 +113,10 @@ class ApiRoom {
|
|
|
113
113
|
score: z.number().min(0)
|
|
114
114
|
})
|
|
115
115
|
)
|
|
116
|
-
@
|
|
117
|
-
submitScore(req: Party.Request,
|
|
118
|
-
this.scores.update(scores => [...scores,
|
|
119
|
-
return { success: true };
|
|
116
|
+
@Guard([isAuthenticated])
|
|
117
|
+
submitScore(req: Party.Request, res: ServerResponse) {
|
|
118
|
+
this.scores.update(scores => [...scores, req.data]);
|
|
119
|
+
return res.success({ success: true });
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
```
|
|
@@ -177,7 +177,7 @@ class AdminRoom {
|
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
@Request({ path: "/admin/users", method: "DELETE" })
|
|
180
|
-
@
|
|
180
|
+
@Guard([isAdmin]) // Applied only to this request handler
|
|
181
181
|
async deleteUserViaHttp(req: Party.Request) {
|
|
182
182
|
// Only authenticated admins can access this endpoint
|
|
183
183
|
}
|
|
@@ -281,7 +281,15 @@ export default class MainServer extends Server {
|
|
|
281
281
|
}
|
|
282
282
|
```
|
|
283
283
|
|
|
284
|
-
2.
|
|
284
|
+
2. Add `Shard` to your server in `party/shard.ts`:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
import { Shard } from '@signe/room';
|
|
288
|
+
|
|
289
|
+
export default class ShardServer extends Shard {}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
3. Configure your `partykit.json` file:
|
|
285
293
|
|
|
286
294
|
```json
|
|
287
295
|
{
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export function cors(res: Response, options: CorsOptions = {}) {
|
|
2
|
+
const newHeaders = new Headers(res.headers);
|
|
3
|
+
|
|
4
|
+
// Set default CORS headers
|
|
5
|
+
newHeaders.set('Access-Control-Allow-Origin', options.origin || '*');
|
|
6
|
+
|
|
7
|
+
if (options.credentials) {
|
|
8
|
+
newHeaders.set('Access-Control-Allow-Credentials', 'true');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (options.exposedHeaders && options.exposedHeaders.length) {
|
|
12
|
+
newHeaders.set('Access-Control-Expose-Headers', options.exposedHeaders.join(', '));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Handle preflight requests
|
|
16
|
+
if (options.methods && options.methods.length) {
|
|
17
|
+
newHeaders.set('Access-Control-Allow-Methods', options.methods.join(', '));
|
|
18
|
+
} else {
|
|
19
|
+
newHeaders.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (options.allowedHeaders && options.allowedHeaders.length) {
|
|
23
|
+
newHeaders.set('Access-Control-Allow-Headers', options.allowedHeaders.join(', '));
|
|
24
|
+
} else {
|
|
25
|
+
newHeaders.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (options.maxAge) {
|
|
29
|
+
newHeaders.set('Access-Control-Max-Age', options.maxAge.toString());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return new Response(res.body, {
|
|
33
|
+
status: res.status,
|
|
34
|
+
headers: newHeaders
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CorsOptions {
|
|
39
|
+
origin?: string;
|
|
40
|
+
methods?: string[];
|
|
41
|
+
allowedHeaders?: string[];
|
|
42
|
+
exposedHeaders?: string[];
|
|
43
|
+
credentials?: boolean;
|
|
44
|
+
maxAge?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Creates a CORS interceptor with the specified options
|
|
49
|
+
* @param options CORS configuration options
|
|
50
|
+
* @returns An interceptor function that can be used with ServerResponse
|
|
51
|
+
*/
|
|
52
|
+
export function createCorsInterceptor(options: CorsOptions = {}): (res: Response) => Response {
|
|
53
|
+
return (res: Response) => cors(res, options);
|
|
54
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
export class ServerResponse {
|
|
2
|
+
private interceptors: ((res: Response) => Promise<Response> | Response)[];
|
|
3
|
+
private statusCode: number = 200;
|
|
4
|
+
private responseBody: any = {};
|
|
5
|
+
private responseHeaders: Record<string, string> = {
|
|
6
|
+
'Content-Type': 'application/json'
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Creates a new ServerResponse instance
|
|
11
|
+
* @param interceptors Array of interceptor functions that can modify the response
|
|
12
|
+
*/
|
|
13
|
+
constructor(interceptors: ((res: Response) => Promise<Response> | Response)[] = []) {
|
|
14
|
+
this.interceptors = interceptors;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Sets the status code for the response
|
|
19
|
+
* @param code HTTP status code
|
|
20
|
+
* @returns this instance for chaining
|
|
21
|
+
*/
|
|
22
|
+
status(code: number): ServerResponse {
|
|
23
|
+
this.statusCode = code;
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Sets the response body and returns this instance (chainable method)
|
|
29
|
+
*
|
|
30
|
+
* @param body Response body
|
|
31
|
+
* @returns this instance for chaining
|
|
32
|
+
*/
|
|
33
|
+
body(body: any): ServerResponse {
|
|
34
|
+
this.responseBody = body;
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Adds a header to the response
|
|
40
|
+
* @param name Header name
|
|
41
|
+
* @param value Header value
|
|
42
|
+
* @returns this instance for chaining
|
|
43
|
+
*/
|
|
44
|
+
header(name: string, value: string): ServerResponse {
|
|
45
|
+
this.responseHeaders[name] = value;
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Adds multiple headers to the response
|
|
51
|
+
* @param headers Object containing headers
|
|
52
|
+
* @returns this instance for chaining
|
|
53
|
+
*/
|
|
54
|
+
setHeaders(headers: Record<string, string>): ServerResponse {
|
|
55
|
+
this.responseHeaders = { ...this.responseHeaders, ...headers };
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Add an interceptor to the chain
|
|
61
|
+
* @param interceptor Function that takes a Response and returns a modified Response
|
|
62
|
+
* @returns this instance for chaining
|
|
63
|
+
*/
|
|
64
|
+
use(interceptor: (res: Response) => Promise<Response> | Response): ServerResponse {
|
|
65
|
+
this.interceptors.push(interceptor);
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Builds and returns the Response object after applying all interceptors
|
|
71
|
+
* @returns Promise<Response> The final Response object
|
|
72
|
+
* @private Internal method used by terminal methods
|
|
73
|
+
*/
|
|
74
|
+
private async buildResponse(): Promise<Response> {
|
|
75
|
+
// Create initial response
|
|
76
|
+
let response = new Response(JSON.stringify(this.responseBody), {
|
|
77
|
+
status: this.statusCode,
|
|
78
|
+
headers: this.responseHeaders
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Apply all interceptors sequentially
|
|
82
|
+
for (const interceptor of this.interceptors) {
|
|
83
|
+
try {
|
|
84
|
+
const interceptedResponse = interceptor(response);
|
|
85
|
+
if (interceptedResponse instanceof Promise) {
|
|
86
|
+
response = await interceptedResponse;
|
|
87
|
+
} else {
|
|
88
|
+
response = interceptedResponse;
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error('Error in interceptor:', error);
|
|
92
|
+
// Continue with the current response if an interceptor fails
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return response;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Sets the response body to the JSON-stringified version of the provided value
|
|
101
|
+
* and sends the response (terminal method)
|
|
102
|
+
*
|
|
103
|
+
* @param body Response body to be JSON stringified
|
|
104
|
+
* @returns Promise<Response> The final Response object
|
|
105
|
+
*/
|
|
106
|
+
async json(body: any): Promise<Response> {
|
|
107
|
+
this.responseBody = body;
|
|
108
|
+
this.responseHeaders['Content-Type'] = 'application/json';
|
|
109
|
+
return this.buildResponse();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Sends the response with the current configuration (terminal method)
|
|
114
|
+
*
|
|
115
|
+
* @param body Optional body to set before sending
|
|
116
|
+
* @returns Promise<Response> The final Response object
|
|
117
|
+
*/
|
|
118
|
+
async send(body?: any): Promise<Response> {
|
|
119
|
+
if (body !== undefined) {
|
|
120
|
+
this.responseBody = body;
|
|
121
|
+
}
|
|
122
|
+
return this.buildResponse();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Sends a plain text response (terminal method)
|
|
127
|
+
*
|
|
128
|
+
* @param text Text to send
|
|
129
|
+
* @returns Promise<Response> The final Response object
|
|
130
|
+
*/
|
|
131
|
+
async text(text: string): Promise<Response> {
|
|
132
|
+
this.responseBody = text;
|
|
133
|
+
this.responseHeaders['Content-Type'] = 'text/plain';
|
|
134
|
+
|
|
135
|
+
// Create a text response without JSON stringifying the body
|
|
136
|
+
let response = new Response(text, {
|
|
137
|
+
status: this.statusCode,
|
|
138
|
+
headers: this.responseHeaders
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Apply interceptors
|
|
142
|
+
for (const interceptor of this.interceptors) {
|
|
143
|
+
try {
|
|
144
|
+
const interceptedResponse = interceptor(response);
|
|
145
|
+
if (interceptedResponse instanceof Promise) {
|
|
146
|
+
response = await interceptedResponse;
|
|
147
|
+
} else {
|
|
148
|
+
response = interceptedResponse;
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error('Error in interceptor:', error);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return response;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Redirects to the specified URL (terminal method)
|
|
160
|
+
*
|
|
161
|
+
* @param url URL to redirect to
|
|
162
|
+
* @param statusCode HTTP status code (default: 302)
|
|
163
|
+
* @returns Promise<Response> The final Response object
|
|
164
|
+
*/
|
|
165
|
+
async redirect(url: string, statusCode: number = 302): Promise<Response> {
|
|
166
|
+
this.statusCode = statusCode;
|
|
167
|
+
this.responseHeaders['Location'] = url;
|
|
168
|
+
return this.buildResponse();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Creates a success response with status 200
|
|
173
|
+
* @param body Response body
|
|
174
|
+
* @returns Promise<Response> The final Response object
|
|
175
|
+
*/
|
|
176
|
+
async success(body: any = {}): Promise<Response> {
|
|
177
|
+
return this.status(200).json(body);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Creates an error response with status 400
|
|
182
|
+
* @param message Error message
|
|
183
|
+
* @param details Additional error details
|
|
184
|
+
* @returns Promise<Response> The final Response object
|
|
185
|
+
*/
|
|
186
|
+
async badRequest(message: string, details: any = {}): Promise<Response> {
|
|
187
|
+
return this.status(400).json({
|
|
188
|
+
error: message,
|
|
189
|
+
...details
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Creates an error response with status 403
|
|
195
|
+
* @param message Error message
|
|
196
|
+
* @returns Promise<Response> The final Response object
|
|
197
|
+
*/
|
|
198
|
+
async notPermitted(message: string = "Not permitted"): Promise<Response> {
|
|
199
|
+
return this.status(403).json({ error: message });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Creates an error response with status 401
|
|
204
|
+
* @param message Error message
|
|
205
|
+
* @returns Promise<Response> The final Response object
|
|
206
|
+
*/
|
|
207
|
+
async unauthorized(message: string = "Unauthorized"): Promise<Response> {
|
|
208
|
+
return this.status(401).json({ error: message });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Creates an error response with status 404
|
|
213
|
+
* @param message Error message
|
|
214
|
+
* @returns Promise<Response> The final Response object
|
|
215
|
+
*/
|
|
216
|
+
async notFound(message: string = "Not found"): Promise<Response> {
|
|
217
|
+
return this.status(404).json({ error: message });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Creates an error response with status 500
|
|
222
|
+
* @param message Error message
|
|
223
|
+
* @returns Promise<Response> The final Response object
|
|
224
|
+
*/
|
|
225
|
+
async serverError(message: string = "Internal Server Error"): Promise<Response> {
|
|
226
|
+
return this.status(500).json({ error: message });
|
|
227
|
+
}
|
|
228
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
isClass,
|
|
17
17
|
throttle,
|
|
18
18
|
} from "./utils";
|
|
19
|
+
import { ServerResponse } from "./request/response";
|
|
20
|
+
import { createCorsInterceptor } from "./request/cors";
|
|
19
21
|
|
|
20
22
|
const Message = z.object({
|
|
21
23
|
action: z.string(),
|
|
@@ -843,13 +845,16 @@ export class Server implements Party.Server {
|
|
|
843
845
|
// Check if the request is coming from a shard
|
|
844
846
|
const isFromShard = req.headers.has('x-forwarded-by-shard');
|
|
845
847
|
const shardId = req.headers.get('x-shard-id');
|
|
848
|
+
const res = new ServerResponse([
|
|
849
|
+
createCorsInterceptor()
|
|
850
|
+
]);
|
|
846
851
|
|
|
847
852
|
if (isFromShard) {
|
|
848
|
-
return this.handleShardRequest(req, shardId);
|
|
853
|
+
return this.handleShardRequest(req, res, shardId);
|
|
849
854
|
}
|
|
850
855
|
|
|
851
856
|
// Handle regular client request
|
|
852
|
-
return this.handleDirectRequest(req);
|
|
857
|
+
return this.handleDirectRequest(req, res);
|
|
853
858
|
}
|
|
854
859
|
|
|
855
860
|
/**
|
|
@@ -860,35 +865,27 @@ export class Server implements Party.Server {
|
|
|
860
865
|
* @description Processes requests received directly from clients
|
|
861
866
|
* @returns {Promise<Response>} The response to return to the client
|
|
862
867
|
*/
|
|
863
|
-
private async handleDirectRequest(req: Party.Request): Promise<Response> {
|
|
868
|
+
private async handleDirectRequest(req: Party.Request, res: ServerResponse): Promise<Response> {
|
|
864
869
|
const subRoom = await this.getSubRoom();
|
|
865
|
-
const res = (body: any, status: number) => {
|
|
866
|
-
return new Response(JSON.stringify(body), { status });
|
|
867
|
-
};
|
|
868
|
-
|
|
869
870
|
if (!subRoom) {
|
|
870
|
-
return res(
|
|
871
|
-
error: "Not found"
|
|
872
|
-
}, 404);
|
|
871
|
+
return res.notFound();
|
|
873
872
|
}
|
|
874
873
|
|
|
875
874
|
// First try to match using the registered @Request handlers
|
|
876
|
-
const response = await this.tryMatchRequestHandler(req, subRoom);
|
|
875
|
+
const response = await this.tryMatchRequestHandler(req, res, subRoom);
|
|
877
876
|
if (response) {
|
|
878
877
|
return response;
|
|
879
878
|
}
|
|
880
879
|
|
|
881
880
|
// Fall back to the legacy onRequest method if no handler matched
|
|
882
|
-
const legacyResponse = await awaitReturn(subRoom["onRequest"]?.(req,
|
|
881
|
+
const legacyResponse = await awaitReturn(subRoom["onRequest"]?.(req, res));
|
|
883
882
|
if (!legacyResponse) {
|
|
884
|
-
return res(
|
|
885
|
-
error: "Not found"
|
|
886
|
-
}, 404);
|
|
883
|
+
return res.notFound();
|
|
887
884
|
}
|
|
888
885
|
if (legacyResponse instanceof Response) {
|
|
889
886
|
return legacyResponse;
|
|
890
887
|
}
|
|
891
|
-
return res(legacyResponse
|
|
888
|
+
return res.success(legacyResponse);
|
|
892
889
|
}
|
|
893
890
|
|
|
894
891
|
/**
|
|
@@ -900,7 +897,7 @@ export class Server implements Party.Server {
|
|
|
900
897
|
* @description Attempts to match the request to a registered @Request handler
|
|
901
898
|
* @returns {Promise<Response | null>} The response or null if no handler matched
|
|
902
899
|
*/
|
|
903
|
-
private async tryMatchRequestHandler(req: Party.Request, subRoom: any): Promise<Response | null> {
|
|
900
|
+
private async tryMatchRequestHandler(req: Party.Request, res: ServerResponse, subRoom: any): Promise<Response | null> {
|
|
904
901
|
const requestHandlers = subRoom.constructor["_requestMetadata"];
|
|
905
902
|
if (!requestHandlers) {
|
|
906
903
|
return null;
|
|
@@ -935,7 +932,7 @@ export class Server implements Party.Server {
|
|
|
935
932
|
return isAuthorized;
|
|
936
933
|
}
|
|
937
934
|
if (!isAuthorized) {
|
|
938
|
-
return
|
|
935
|
+
return res.notPermitted();
|
|
939
936
|
}
|
|
940
937
|
}
|
|
941
938
|
|
|
@@ -948,44 +945,33 @@ export class Server implements Party.Server {
|
|
|
948
945
|
const body = await req.json();
|
|
949
946
|
const validation = handler.bodyValidation.safeParse(body);
|
|
950
947
|
if (!validation.success) {
|
|
951
|
-
return
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
);
|
|
948
|
+
return res.badRequest("Invalid request body", {
|
|
949
|
+
details: validation.error
|
|
950
|
+
});
|
|
955
951
|
}
|
|
956
952
|
bodyData = validation.data;
|
|
957
953
|
}
|
|
958
954
|
} catch (error) {
|
|
959
|
-
return
|
|
960
|
-
JSON.stringify({ error: "Failed to parse request body" }),
|
|
961
|
-
{ status: 400 }
|
|
962
|
-
);
|
|
955
|
+
return res.badRequest("Failed to parse request body");
|
|
963
956
|
}
|
|
964
957
|
}
|
|
965
958
|
|
|
966
959
|
// Execute the handler method
|
|
967
960
|
try {
|
|
961
|
+
req['data'] = bodyData;
|
|
962
|
+
req['params'] = params;
|
|
968
963
|
const result = await awaitReturn(
|
|
969
|
-
subRoom[handler.key](req,
|
|
964
|
+
subRoom[handler.key](req, res)
|
|
970
965
|
);
|
|
971
966
|
|
|
972
967
|
if (result instanceof Response) {
|
|
973
968
|
return result;
|
|
974
969
|
}
|
|
975
970
|
|
|
976
|
-
return
|
|
977
|
-
typeof result === 'string' ? result : JSON.stringify(result),
|
|
978
|
-
{
|
|
979
|
-
status: 200,
|
|
980
|
-
headers: { 'Content-Type': typeof result === 'string' ? 'text/plain' : 'application/json' }
|
|
981
|
-
}
|
|
982
|
-
);
|
|
971
|
+
return res.success(result);
|
|
983
972
|
} catch (error) {
|
|
984
973
|
console.error('Error executing request handler:', error);
|
|
985
|
-
return
|
|
986
|
-
JSON.stringify({ error: "Internal server error" }),
|
|
987
|
-
{ status: 500 }
|
|
988
|
-
);
|
|
974
|
+
return res.serverError();
|
|
989
975
|
}
|
|
990
976
|
}
|
|
991
977
|
}
|
|
@@ -1058,11 +1044,11 @@ export class Server implements Party.Server {
|
|
|
1058
1044
|
* @description Processes requests forwarded by shards, preserving client context
|
|
1059
1045
|
* @returns {Promise<Response>} The response to return to the shard (which will forward it to the client)
|
|
1060
1046
|
*/
|
|
1061
|
-
private async handleShardRequest(req: Party.Request, shardId: string | null): Promise<Response> {
|
|
1047
|
+
private async handleShardRequest(req: Party.Request, res: ServerResponse, shardId: string | null): Promise<Response> {
|
|
1062
1048
|
const subRoom = await this.getSubRoom();
|
|
1063
1049
|
|
|
1064
1050
|
if (!subRoom) {
|
|
1065
|
-
return
|
|
1051
|
+
return res.notFound();
|
|
1066
1052
|
}
|
|
1067
1053
|
|
|
1068
1054
|
// Create a context that preserves original client information
|
|
@@ -1071,26 +1057,26 @@ export class Server implements Party.Server {
|
|
|
1071
1057
|
|
|
1072
1058
|
try {
|
|
1073
1059
|
// First try to match using the registered @Request handlers
|
|
1074
|
-
const response = await this.tryMatchRequestHandler(enhancedReq, subRoom);
|
|
1060
|
+
const response = await this.tryMatchRequestHandler(enhancedReq, res, subRoom);
|
|
1075
1061
|
if (response) {
|
|
1076
1062
|
return response;
|
|
1077
1063
|
}
|
|
1078
1064
|
|
|
1079
1065
|
// Fall back to the legacy onRequest handler
|
|
1080
|
-
const legacyResponse = await awaitReturn(subRoom["onRequest"]?.(enhancedReq,
|
|
1066
|
+
const legacyResponse = await awaitReturn(subRoom["onRequest"]?.(enhancedReq, res));
|
|
1081
1067
|
|
|
1082
1068
|
if (!legacyResponse) {
|
|
1083
|
-
return
|
|
1069
|
+
return res.notFound();
|
|
1084
1070
|
}
|
|
1085
1071
|
|
|
1086
1072
|
if (legacyResponse instanceof Response) {
|
|
1087
1073
|
return legacyResponse;
|
|
1088
1074
|
}
|
|
1089
1075
|
|
|
1090
|
-
return
|
|
1076
|
+
return res.success(legacyResponse);
|
|
1091
1077
|
} catch (error) {
|
|
1092
1078
|
console.error(`Error processing request from shard ${shardId}:`, error);
|
|
1093
|
-
return
|
|
1079
|
+
return res.serverError();
|
|
1094
1080
|
}
|
|
1095
1081
|
}
|
|
1096
1082
|
|
package/src/world.ts
CHANGED
|
@@ -6,6 +6,7 @@ import * as Party from "./types/party";
|
|
|
6
6
|
import { guardManageWorld } from "./world.guard";
|
|
7
7
|
import { response } from "./utils";
|
|
8
8
|
import { RoomInterceptorPacket, RoomOnJoin } from "./interfaces";
|
|
9
|
+
import { ServerResponse } from "./request/response";
|
|
9
10
|
|
|
10
11
|
// Types definitions
|
|
11
12
|
type BalancingStrategy = 'round-robin' | 'least-connections' | 'random';
|
|
@@ -173,13 +174,13 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
173
174
|
method: 'POST',
|
|
174
175
|
})
|
|
175
176
|
@Guard([guardManageWorld])
|
|
176
|
-
async updateShardStats(req: Party.Request) {
|
|
177
|
+
async updateShardStats(req: Party.Request, res: ServerResponse) {
|
|
177
178
|
const body: { shardId: string; connections: number; status: ShardStatus } = await req.json();
|
|
178
179
|
const { shardId, connections, status } = body;
|
|
179
180
|
const shard = this.shards()[shardId];
|
|
180
181
|
|
|
181
182
|
if (!shard) {
|
|
182
|
-
return
|
|
183
|
+
return res.notFound(`Shard ${shardId} not found`);
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
shard.currentConnections.set(connections);
|
|
@@ -194,14 +195,14 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
194
195
|
method: 'POST',
|
|
195
196
|
})
|
|
196
197
|
@Guard([guardManageWorld])
|
|
197
|
-
async scaleRoom(req: Party.Request) {
|
|
198
|
+
async scaleRoom(req: Party.Request, res: ServerResponse) {
|
|
198
199
|
const data: z.infer<typeof ScaleRoomSchema> = await req.json();
|
|
199
200
|
const { targetShardCount, shardTemplate, roomId } = data;
|
|
200
201
|
|
|
201
202
|
// Validate room exists
|
|
202
203
|
const room = this.rooms()[roomId];
|
|
203
204
|
if (!room) {
|
|
204
|
-
return
|
|
205
|
+
return res.notFound(`Room ${roomId} does not exist`);
|
|
205
206
|
}
|
|
206
207
|
|
|
207
208
|
const roomShards = Object.values(this.shards())
|
|
@@ -211,11 +212,10 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
211
212
|
|
|
212
213
|
// Check max shards constraint
|
|
213
214
|
if (room.maxShards() !== undefined && targetShardCount > room.maxShards()!) {
|
|
214
|
-
return {
|
|
215
|
-
error: `Cannot scale beyond maximum allowed shards (${room.maxShards()})`,
|
|
215
|
+
return res.badRequest(`Cannot scale beyond maximum allowed shards (${room.maxShards()})`, {
|
|
216
216
|
roomId,
|
|
217
217
|
currentShardCount: previousShardCount
|
|
218
|
-
};
|
|
218
|
+
});
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
// Handle scaling down
|
|
@@ -268,7 +268,7 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
268
268
|
path: 'connect',
|
|
269
269
|
method: 'POST',
|
|
270
270
|
})
|
|
271
|
-
async connect(req: Party.Request) {
|
|
271
|
+
async connect(req: Party.Request, res: ServerResponse) {
|
|
272
272
|
try {
|
|
273
273
|
// Extract request data
|
|
274
274
|
let data: { roomId: string; autoCreate?: boolean };
|
|
@@ -277,17 +277,17 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
277
277
|
// Handle potential empty body or malformed JSON
|
|
278
278
|
const body = await req.text();
|
|
279
279
|
if (!body || body.trim() === '') {
|
|
280
|
-
return
|
|
280
|
+
return res.badRequest("Request body is empty");
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
data = JSON.parse(body);
|
|
284
284
|
} catch (parseError) {
|
|
285
|
-
return
|
|
285
|
+
return res.badRequest("Invalid JSON in request body");
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
// Verify roomId is provided
|
|
289
289
|
if (!data.roomId) {
|
|
290
|
-
return
|
|
290
|
+
return res.badRequest("roomId parameter is required");
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
// Determine if auto-creation is enabled (default to true)
|
|
@@ -298,18 +298,18 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
298
298
|
|
|
299
299
|
// Check for errors
|
|
300
300
|
if ('error' in result) {
|
|
301
|
-
return
|
|
301
|
+
return res.notFound(result.error);
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
// Return shard information to the client
|
|
305
|
-
return
|
|
305
|
+
return res.success({
|
|
306
306
|
success: true,
|
|
307
307
|
shardId: result.shardId,
|
|
308
308
|
url: result.url
|
|
309
309
|
});
|
|
310
310
|
} catch (error) {
|
|
311
311
|
console.error('Error connecting to shard:', error);
|
|
312
|
-
return
|
|
312
|
+
return res.serverError();
|
|
313
313
|
}
|
|
314
314
|
}
|
|
315
315
|
|