@signe/room 2.8.2 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signe/room",
3
- "version": "2.8.2",
3
+ "version": "2.9.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.8.2"
20
+ "@signe/sync": "2.9.0"
21
21
  },
22
22
  "publishConfig": {
23
23
  "access": "public"
package/readme.md CHANGED
@@ -69,6 +69,54 @@ Function have to be decorated with the `@Action` decorator and have 3 parameter
69
69
  - The second parameter is the value of the action
70
70
  - The third parameter is the Party.Connection instance
71
71
 
72
+ ### Unhandled actions
73
+
74
+ If you want to catch any valid WebSocket message whose `action` is not registered
75
+ with `@Action(...)`, you can use `@UnhandledAction()`.
76
+
77
+ The fallback handler receives:
78
+
79
+ - The first parameter is the player instance
80
+ - The second parameter is the full message object: `{ action, value }`
81
+ - The third parameter is the `Party.Connection` instance
82
+
83
+ ```ts
84
+ import { Action, Guard, Room, UnhandledAction } from "@signe/room";
85
+
86
+ function isAuthenticated(conn: Party.Connection, value: any, room: Party.Room) {
87
+ return !!conn.state?.publicId;
88
+ }
89
+
90
+ @Room({
91
+ path: "game",
92
+ })
93
+ class GameRoom {
94
+ @Action("move")
95
+ move(player: any, value: { x: number; y: number }) {
96
+ player.x.set(value.x);
97
+ player.y.set(value.y);
98
+ }
99
+
100
+ @UnhandledAction()
101
+ @Guard([isAuthenticated])
102
+ onUnhandledAction(
103
+ player: any,
104
+ message: { action: string; value: unknown },
105
+ conn: Party.Connection
106
+ ) {
107
+ console.warn("Unhandled action", message.action, message.value, conn.id);
108
+ }
109
+ }
110
+ ```
111
+
112
+ Notes:
113
+
114
+ - `@UnhandledAction()` is only called if the incoming message matches the expected
115
+ WebSocket shape `{ action, value }`
116
+ - If a matching `@Action("...")` exists, it always has priority over
117
+ `@UnhandledAction()`
118
+ - You can combine `@UnhandledAction()` with `@Guard(...)`
119
+
72
120
  ## HTTP Request Handling
73
121
 
74
122
  The `@Request` decorator allows you to handle HTTP requests with specific routes and methods:
package/src/decorators.ts CHANGED
@@ -15,6 +15,18 @@ export function Action(name: string, bodyValidation?: z.ZodSchema) {
15
15
  };
16
16
  }
17
17
 
18
+ /**
19
+ * Fallback decorator for handling websocket messages whose action
20
+ * does not match any registered @Action decorator.
21
+ */
22
+ export function UnhandledAction() {
23
+ return function (target: any, propertyKey: string) {
24
+ target.constructor._unhandledActionMetadata = {
25
+ key: propertyKey,
26
+ };
27
+ };
28
+ }
29
+
18
30
  /**
19
31
  * Request decorator for handling HTTP requests with path and method routing
20
32
  * @param options Configuration for the HTTP request handler
@@ -98,4 +110,4 @@ export function Guard(guards: GuardFn[]) {
98
110
  }
99
111
  target.constructor['_actionGuards'].set(propertyKey, guards);
100
112
  };
101
- }
113
+ }
package/src/server.ts CHANGED
@@ -801,36 +801,50 @@ export class Server implements Party.Server {
801
801
  }
802
802
 
803
803
  const actions = subRoom.constructor["_actionMetadata"];
804
- if (actions) {
805
- const signal = this.getUsersProperty(subRoom);
806
- const { publicId } = sender.state as any;
807
- const user = signal?.()[publicId];
808
- const actionName = actions.get(result.data.action);
809
- if (actionName) {
804
+ const signal = this.getUsersProperty(subRoom);
805
+ const { publicId } = sender.state as any;
806
+ const user = signal?.()[publicId];
807
+ const actionName = actions?.get(result.data.action);
808
+ if (actionName) {
809
+
810
+ // Check all guards if they exist
811
+ const guards = subRoom.constructor['_actionGuards']?.get(actionName.key) || [];
812
+ for (const guard of guards) {
813
+ const isAuthorized = await guard(sender, result.data.value, this.room);
814
+ if (!isAuthorized) {
815
+ return;
816
+ }
817
+ }
810
818
 
811
- // Check all guards if they exist
812
- const guards = subRoom.constructor['_actionGuards']?.get(actionName.key) || [];
813
- for (const guard of guards) {
814
- const isAuthorized = await guard(sender, result.data.value);
815
- if (!isAuthorized) {
816
- return;
817
- }
819
+ // Validate action body if a validation schema is defined
820
+ if (actionName.bodyValidation) {
821
+ const bodyResult = actionName.bodyValidation.safeParse(
822
+ result.data.value
823
+ );
824
+ if (!bodyResult.success) {
825
+ return;
818
826
  }
827
+ }
828
+ // Execute the action
829
+ await awaitReturn(
830
+ subRoom[actionName.key](user, result.data.value, sender)
831
+ );
832
+ return;
833
+ }
819
834
 
820
- // Validate action body if a validation schema is defined
821
- if (actionName.bodyValidation) {
822
- const bodyResult = actionName.bodyValidation.safeParse(
823
- result.data.value
824
- );
825
- if (!bodyResult.success) {
826
- return;
827
- }
835
+ const unhandledAction = subRoom.constructor["_unhandledActionMetadata"];
836
+ if (unhandledAction) {
837
+ const guards = subRoom.constructor['_actionGuards']?.get(unhandledAction.key) || [];
838
+ for (const guard of guards) {
839
+ const isAuthorized = await guard(sender, result.data, this.room);
840
+ if (!isAuthorized) {
841
+ return;
828
842
  }
829
- // Execute the action
830
- await awaitReturn(
831
- subRoom[actionName.key](user, result.data.value, sender)
832
- );
833
843
  }
844
+
845
+ await awaitReturn(
846
+ subRoom[unhandledAction.key](user, result.data, sender)
847
+ );
834
848
  }
835
849
  }
836
850
 
@@ -1195,14 +1209,14 @@ export class Server implements Party.Server {
1195
1209
  })
1196
1210
  )) ?? userSnapshot;
1197
1211
 
1212
+ // Add user to signal before loading to avoid syncing non-serializable instances
1213
+ signal()[publicId] = user;
1214
+
1198
1215
  // Load user data from snapshot
1199
1216
  load(user, hydratedSnapshot, true);
1200
1217
 
1201
- // Add user to signal
1202
- signal()[publicId] = user;
1203
-
1204
1218
  // Save user snapshot to storage
1205
- await this.room.storage.put(`${usersPropName}.${publicId}`, hydratedSnapshot);
1219
+ await this.room.storage.put(`${usersPropName}.${publicId}`, userSnapshot);
1206
1220
  }
1207
1221
  }
1208
1222