@signe/room 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,8 +22,8 @@ export default function Room() {
22
22
 
23
23
  // Connect to the room through the World service with auto-creation enabled
24
24
  socketRef.current = await connectionWorld({
25
- worldUrl: 'http://localhost:1999',
26
- roomId: roomId,
25
+ host: 'http://localhost:1999',
26
+ room: roomId,
27
27
  autoCreate: true // Enable auto-creation of room and shards
28
28
  }, roomRef.current);
29
29
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signe/room",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
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.0"
20
+ "@signe/sync": "2.1.0"
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, body: any, params: { id: string }) {
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 new Response(JSON.stringify({ error: "Player not found" }), { status: 404 });
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
- @RequestGuard([isAuthenticated])
117
- submitScore(req: Party.Request, body: { playerId: string; score: number }) {
118
- this.scores.update(scores => [...scores, body]);
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
- @RequestGuard([isAdmin]) // Applied only to this request handler
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. Configure your `partykit.json` file:
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
  {
@@ -308,15 +316,12 @@ const room = new YourRoomSchema();
308
316
 
309
317
  // Connect through the World service
310
318
  const connection = await connectionWorld({
311
- worldUrl: 'https://your-app-url.com', // Your application URL
312
- roomId: 'unique-room-id', // Room identifier
319
+ host: 'https://your-app-url.com', // Your application URL
320
+ room: 'unique-room-id', // Room identifier
313
321
  worldId: 'your-world-id', // Optional, defaults to 'world-default'
314
322
  autoCreate: true, // Auto-create room if it doesn't exist
315
323
  retryCount: 3, // Number of connection attempts
316
- retryDelay: 1000, // Delay between retries in ms
317
- socketOptions: { // Optional PartySocket configuration
318
- protocols: ['your-protocol']
319
- }
324
+ retryDelay: 1000 // Delay between retries in ms
320
325
  }, room);
321
326
 
322
327
  // Listen for events
package/src/index.ts CHANGED
@@ -4,4 +4,5 @@ export { Server } from './server';
4
4
  export * from './testing';
5
5
  export * from './shard';
6
6
  export * from './world';
7
- export * from './interfaces';
7
+ export * from './interfaces';
8
+ export * from './request/response';
@@ -0,0 +1,58 @@
1
+ export function cors(res: Response, options: CorsOptions = {}) {
2
+ const newHeaders = new Headers(res.headers);
3
+
4
+ // Set default CORS headers
5
+ const requestOrigin = options.origin || '*';
6
+ newHeaders.set('Access-Control-Allow-Origin', requestOrigin);
7
+
8
+ if (options.credentials) {
9
+ newHeaders.set('Access-Control-Allow-Credentials', 'true');
10
+ }
11
+
12
+ if (options.exposedHeaders && options.exposedHeaders.length) {
13
+ newHeaders.set('Access-Control-Expose-Headers', options.exposedHeaders.join(', '));
14
+ }
15
+
16
+ // Handle preflight requests
17
+ if (options.methods && options.methods.length) {
18
+ newHeaders.set('Access-Control-Allow-Methods', options.methods.join(', '));
19
+ } else {
20
+ newHeaders.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
21
+ }
22
+
23
+ if (options.allowedHeaders && options.allowedHeaders.length) {
24
+ newHeaders.set('Access-Control-Allow-Headers', options.allowedHeaders.join(', '));
25
+ } else {
26
+ newHeaders.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
27
+ }
28
+
29
+ if (options.maxAge) {
30
+ newHeaders.set('Access-Control-Max-Age', options.maxAge.toString());
31
+ } else {
32
+ // Default max-age to 86400 seconds (24 hours)
33
+ newHeaders.set('Access-Control-Max-Age', '86400');
34
+ }
35
+
36
+ return new Response(res.body, {
37
+ status: res.status,
38
+ headers: newHeaders
39
+ });
40
+ }
41
+
42
+ export interface CorsOptions {
43
+ origin?: string;
44
+ methods?: string[];
45
+ allowedHeaders?: string[];
46
+ exposedHeaders?: string[];
47
+ credentials?: boolean;
48
+ maxAge?: number;
49
+ }
50
+
51
+ /**
52
+ * Creates a CORS interceptor with the specified options
53
+ * @param options CORS configuration options
54
+ * @returns An interceptor function that can be used with ServerResponse
55
+ */
56
+ export function createCorsInterceptor(options: CorsOptions = {}): (res: Response) => Response {
57
+ return (res: Response) => cors(res, options);
58
+ }
@@ -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(),
@@ -525,6 +527,11 @@ export class Server implements Party.Server {
525
527
 
526
528
  const subRoom = await this.getSubRoom()
527
529
 
530
+ if (!subRoom) {
531
+ console.warn("Room not found");
532
+ return;
533
+ }
534
+
528
535
  // Check room guards
529
536
  const roomGuards = subRoom.constructor['_roomGuards'] || [];
530
537
  for (const guard of roomGuards) {
@@ -843,13 +850,23 @@ export class Server implements Party.Server {
843
850
  // Check if the request is coming from a shard
844
851
  const isFromShard = req.headers.has('x-forwarded-by-shard');
845
852
  const shardId = req.headers.get('x-shard-id');
853
+
854
+ // Create a response with proper CORS configuration
855
+ const res = new ServerResponse([
856
+ createCorsInterceptor()
857
+ ]);
858
+
859
+ if (req.method === 'OPTIONS') {
860
+ // For OPTIONS requests, just return a 200 OK with CORS headers
861
+ return res.status(200).send({});
862
+ }
846
863
 
847
864
  if (isFromShard) {
848
- return this.handleShardRequest(req, shardId);
865
+ return this.handleShardRequest(req, res, shardId);
849
866
  }
850
867
 
851
868
  // Handle regular client request
852
- return this.handleDirectRequest(req);
869
+ return this.handleDirectRequest(req, res);
853
870
  }
854
871
 
855
872
  /**
@@ -860,35 +877,27 @@ export class Server implements Party.Server {
860
877
  * @description Processes requests received directly from clients
861
878
  * @returns {Promise<Response>} The response to return to the client
862
879
  */
863
- private async handleDirectRequest(req: Party.Request): Promise<Response> {
880
+ private async handleDirectRequest(req: Party.Request, res: ServerResponse): Promise<Response> {
864
881
  const subRoom = await this.getSubRoom();
865
- const res = (body: any, status: number) => {
866
- return new Response(JSON.stringify(body), { status });
867
- };
868
-
869
882
  if (!subRoom) {
870
- return res({
871
- error: "Not found"
872
- }, 404);
883
+ return res.notFound();
873
884
  }
874
885
 
875
886
  // First try to match using the registered @Request handlers
876
- const response = await this.tryMatchRequestHandler(req, subRoom);
887
+ const response = await this.tryMatchRequestHandler(req, res, subRoom);
877
888
  if (response) {
878
889
  return response;
879
890
  }
880
891
 
881
892
  // Fall back to the legacy onRequest method if no handler matched
882
- const legacyResponse = await awaitReturn(subRoom["onRequest"]?.(req, this.room));
893
+ const legacyResponse = await awaitReturn(subRoom["onRequest"]?.(req, res));
883
894
  if (!legacyResponse) {
884
- return res({
885
- error: "Not found"
886
- }, 404);
895
+ return res.notFound();
887
896
  }
888
897
  if (legacyResponse instanceof Response) {
889
898
  return legacyResponse;
890
899
  }
891
- return res(legacyResponse, 200);
900
+ return res.success(legacyResponse);
892
901
  }
893
902
 
894
903
  /**
@@ -900,7 +909,7 @@ export class Server implements Party.Server {
900
909
  * @description Attempts to match the request to a registered @Request handler
901
910
  * @returns {Promise<Response | null>} The response or null if no handler matched
902
911
  */
903
- private async tryMatchRequestHandler(req: Party.Request, subRoom: any): Promise<Response | null> {
912
+ private async tryMatchRequestHandler(req: Party.Request, res: ServerResponse, subRoom: any): Promise<Response | null> {
904
913
  const requestHandlers = subRoom.constructor["_requestMetadata"];
905
914
  if (!requestHandlers) {
906
915
  return null;
@@ -935,7 +944,7 @@ export class Server implements Party.Server {
935
944
  return isAuthorized;
936
945
  }
937
946
  if (!isAuthorized) {
938
- return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 403 });
947
+ return res.notPermitted();
939
948
  }
940
949
  }
941
950
 
@@ -948,44 +957,33 @@ export class Server implements Party.Server {
948
957
  const body = await req.json();
949
958
  const validation = handler.bodyValidation.safeParse(body);
950
959
  if (!validation.success) {
951
- return new Response(
952
- JSON.stringify({ error: "Invalid request body", details: validation.error }),
953
- { status: 400 }
954
- );
960
+ return res.badRequest("Invalid request body", {
961
+ details: validation.error
962
+ });
955
963
  }
956
964
  bodyData = validation.data;
957
965
  }
958
966
  } catch (error) {
959
- return new Response(
960
- JSON.stringify({ error: "Failed to parse request body" }),
961
- { status: 400 }
962
- );
967
+ return res.badRequest("Failed to parse request body");
963
968
  }
964
969
  }
965
970
 
966
971
  // Execute the handler method
967
972
  try {
973
+ req['data'] = bodyData;
974
+ req['params'] = params;
968
975
  const result = await awaitReturn(
969
- subRoom[handler.key](req, bodyData, params, this.room)
976
+ subRoom[handler.key](req, res)
970
977
  );
971
978
 
972
979
  if (result instanceof Response) {
973
980
  return result;
974
981
  }
975
982
 
976
- return new Response(
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
- );
983
+ return res.success(result);
983
984
  } catch (error) {
984
985
  console.error('Error executing request handler:', error);
985
- return new Response(
986
- JSON.stringify({ error: "Internal server error" }),
987
- { status: 500 }
988
- );
986
+ return res.serverError();
989
987
  }
990
988
  }
991
989
  }
@@ -1058,11 +1056,11 @@ export class Server implements Party.Server {
1058
1056
  * @description Processes requests forwarded by shards, preserving client context
1059
1057
  * @returns {Promise<Response>} The response to return to the shard (which will forward it to the client)
1060
1058
  */
1061
- private async handleShardRequest(req: Party.Request, shardId: string | null): Promise<Response> {
1059
+ private async handleShardRequest(req: Party.Request, res: ServerResponse, shardId: string | null): Promise<Response> {
1062
1060
  const subRoom = await this.getSubRoom();
1063
1061
 
1064
1062
  if (!subRoom) {
1065
- return new Response(JSON.stringify({ error: "Not found" }), { status: 404 });
1063
+ return res.notFound();
1066
1064
  }
1067
1065
 
1068
1066
  // Create a context that preserves original client information
@@ -1071,26 +1069,26 @@ export class Server implements Party.Server {
1071
1069
 
1072
1070
  try {
1073
1071
  // First try to match using the registered @Request handlers
1074
- const response = await this.tryMatchRequestHandler(enhancedReq, subRoom);
1072
+ const response = await this.tryMatchRequestHandler(enhancedReq, res, subRoom);
1075
1073
  if (response) {
1076
1074
  return response;
1077
1075
  }
1078
1076
 
1079
1077
  // Fall back to the legacy onRequest handler
1080
- const legacyResponse = await awaitReturn(subRoom["onRequest"]?.(enhancedReq, this.room));
1078
+ const legacyResponse = await awaitReturn(subRoom["onRequest"]?.(enhancedReq, res));
1081
1079
 
1082
1080
  if (!legacyResponse) {
1083
- return new Response(JSON.stringify({ error: "Not found" }), { status: 404 });
1081
+ return res.notFound();
1084
1082
  }
1085
1083
 
1086
1084
  if (legacyResponse instanceof Response) {
1087
1085
  return legacyResponse;
1088
1086
  }
1089
1087
 
1090
- return new Response(JSON.stringify(legacyResponse), { status: 200 });
1088
+ return res.success(legacyResponse);
1091
1089
  } catch (error) {
1092
1090
  console.error(`Error processing request from shard ${shardId}:`, error);
1093
- return new Response(JSON.stringify({ error: "Internal server error" }), { status: 500 });
1091
+ return res.serverError();
1094
1092
  }
1095
1093
  }
1096
1094