@rpgjs/server 5.0.0-alpha.41 → 5.0.0-alpha.43

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.
@@ -0,0 +1,223 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
+ import { RpgServerEngine } from "../src/RpgServerEngine";
3
+ import {
4
+ MAP_UPDATE_TOKEN_ENV,
5
+ PartyConnection,
6
+ createRpgServerTransport,
7
+ } from "../src/node";
8
+
9
+ function wait(ms = 0): Promise<void> {
10
+ return new Promise((resolve) => setTimeout(resolve, ms));
11
+ }
12
+
13
+ class MockWebSocket {
14
+ readyState = 1;
15
+ sent: string[] = [];
16
+ private handlers = new Map<string, Array<(...args: any[]) => void>>();
17
+
18
+ send(data: string): void {
19
+ this.sent.push(data);
20
+ }
21
+
22
+ close(): void {
23
+ this.readyState = 3;
24
+ this.emit("close");
25
+ }
26
+
27
+ on(event: string, callback: (...args: any[]) => void): void {
28
+ const listeners = this.handlers.get(event) || [];
29
+ listeners.push(callback);
30
+ this.handlers.set(event, listeners);
31
+ }
32
+
33
+ emit(event: string, ...args: any[]): void {
34
+ for (const callback of this.handlers.get(event) || []) {
35
+ callback(...args);
36
+ }
37
+ }
38
+ }
39
+
40
+ class MockServer extends RpgServerEngine {
41
+ requests: Array<{ method: string; roomId: string; url: string; body: any }> = [];
42
+ messages: Array<{ connectionId: string; message: string }> = [];
43
+ closedConnections: string[] = [];
44
+ connectedContexts: Array<{ connectionId: string; url: string }> = [];
45
+
46
+ constructor(public room: any) {
47
+ super();
48
+ }
49
+
50
+ async onRequest(req: any) {
51
+ let body: any;
52
+ try {
53
+ body = await req.json();
54
+ } catch {
55
+ body = await req.text();
56
+ }
57
+ this.requests.push({
58
+ method: req.method,
59
+ roomId: this.room.id,
60
+ url: req.url,
61
+ body,
62
+ });
63
+
64
+ return {
65
+ method: req.method,
66
+ roomId: this.room.id,
67
+ url: req.url,
68
+ body,
69
+ };
70
+ }
71
+
72
+ async onMessage(message: string, connection: any) {
73
+ this.messages.push({
74
+ connectionId: connection.id,
75
+ message,
76
+ });
77
+ }
78
+
79
+ async onConnect(connection: any, context: any) {
80
+ this.connectedContexts.push({
81
+ connectionId: connection.id,
82
+ url: context.request.url,
83
+ });
84
+ }
85
+
86
+ async onClose(connection: any) {
87
+ this.closedConnections.push(connection.id);
88
+ }
89
+ }
90
+
91
+ class RealServer extends RpgServerEngine {}
92
+
93
+ describe("createRpgServerTransport", () => {
94
+ const originalMapUpdateToken = process.env[MAP_UPDATE_TOKEN_ENV];
95
+
96
+ beforeEach(() => {
97
+ PartyConnection.configurePacketLoss(false, 0);
98
+ PartyConnection.configureBandwidth(false, 100);
99
+ PartyConnection.configureLatency(false, 0);
100
+ delete process.env[MAP_UPDATE_TOKEN_ENV];
101
+ });
102
+
103
+ afterEach(() => {
104
+ if (typeof originalMapUpdateToken === "string") {
105
+ process.env[MAP_UPDATE_TOKEN_ENV] = originalMapUpdateToken;
106
+ return;
107
+ }
108
+ delete process.env[MAP_UPDATE_TOKEN_ENV];
109
+ });
110
+
111
+ it("handles fetch-style HTTP requests without Vite", async () => {
112
+ const transport = createRpgServerTransport(MockServer as any, {
113
+ initializeMaps: false,
114
+ });
115
+
116
+ const response = await transport.fetch("http://localhost/parties/main/lobby/echo?foo=bar", {
117
+ method: "POST",
118
+ headers: {
119
+ "content-type": "application/json",
120
+ },
121
+ body: JSON.stringify({ hello: "world" }),
122
+ });
123
+
124
+ expect(response.status).toBe(200);
125
+ expect(await response.json()).toEqual({
126
+ method: "POST",
127
+ roomId: "lobby",
128
+ url: "http://localhost/parties/main/lobby/echo?foo=bar",
129
+ body: { hello: "world" },
130
+ });
131
+ });
132
+
133
+ it("handles websocket connections without the Vite wrapper", async () => {
134
+ const transport = createRpgServerTransport(MockServer as any, {
135
+ initializeMaps: false,
136
+ });
137
+ const ws = new MockWebSocket();
138
+
139
+ const handled = await transport.acceptWebSocket(ws as any, {
140
+ url: "http://localhost/parties/main/lobby?_pk=player-1",
141
+ method: "GET",
142
+ headers: {
143
+ host: "localhost",
144
+ },
145
+ });
146
+
147
+ expect(handled).toBe(true);
148
+
149
+ await wait(10);
150
+
151
+ const server = transport.getServer("lobby") as MockServer;
152
+ expect(server.connectedContexts).toEqual([
153
+ {
154
+ connectionId: "player-1",
155
+ url: "http://localhost/parties/main/lobby?_pk=player-1",
156
+ },
157
+ ]);
158
+ expect(JSON.parse(ws.sent[0])).toMatchObject({
159
+ type: "connected",
160
+ id: "player-1",
161
+ });
162
+
163
+ ws.emit("message", Buffer.from("ping"));
164
+ await wait(10);
165
+
166
+ expect(server.messages).toEqual([
167
+ {
168
+ connectionId: "player-1",
169
+ message: "ping",
170
+ },
171
+ ]);
172
+
173
+ ws.emit("close");
174
+ await wait(10);
175
+
176
+ expect(server.closedConnections).toEqual(["player-1"]);
177
+ });
178
+
179
+ it("rejects map/update in production when the token is missing", async () => {
180
+ process.env[MAP_UPDATE_TOKEN_ENV] = "prod-secret";
181
+
182
+ const transport = createRpgServerTransport(RealServer as any, {
183
+ initializeMaps: false,
184
+ });
185
+
186
+ const response = await transport.fetch("http://localhost/parties/main/map-town/map/update", {
187
+ method: "POST",
188
+ headers: {
189
+ "content-type": "application/json",
190
+ },
191
+ body: JSON.stringify({
192
+ id: "town",
193
+ width: 320,
194
+ height: 240,
195
+ events: [],
196
+ }),
197
+ });
198
+
199
+ expect(response.status).toBe(401);
200
+ expect(await response.json()).toMatchObject({
201
+ error: "Unauthorized map update",
202
+ });
203
+ });
204
+
205
+ it("sends the configured token when transport.updateMap() is used", async () => {
206
+ process.env[MAP_UPDATE_TOKEN_ENV] = "prod-secret";
207
+
208
+ const transport = createRpgServerTransport(RealServer as any, {
209
+ initializeMaps: false,
210
+ mapUpdateToken: "prod-secret",
211
+ });
212
+
213
+ const response = await transport.updateMap("town", {
214
+ id: "town",
215
+ width: 320,
216
+ height: 240,
217
+ events: [],
218
+ });
219
+
220
+ expect(response.status).toBe(200);
221
+ expect(response.headers.get("content-type")).toContain("application/json");
222
+ });
223
+ });
package/vite.config.ts CHANGED
@@ -2,6 +2,14 @@ import { defineConfig } from 'vite'
2
2
  import dts from 'vite-plugin-dts'
3
3
  import path from 'path'
4
4
 
5
+ const nodeBuiltins = [
6
+ 'fs', 'path', 'os', 'crypto', 'util', 'events', 'stream', 'buffer',
7
+ 'url', 'querystring', 'http', 'https', 'net', 'tls', 'child_process',
8
+ 'cluster', 'dgram', 'dns', 'domain', 'readline', 'repl', 'tty', 'vm',
9
+ 'zlib', 'assert', 'constants', 'module', 'perf_hooks', 'process',
10
+ 'punycode', 'string_decoder', 'timers', 'trace_events', 'v8', 'worker_threads'
11
+ ]
12
+
5
13
  export default defineConfig({
6
14
  plugins: [
7
15
  dts({
@@ -14,9 +22,18 @@ export default defineConfig({
14
22
  sourcemap: true,
15
23
  minify: false,
16
24
  lib: {
17
- entry: 'src/index.ts',
25
+ entry: {
26
+ index: 'src/index.ts',
27
+ 'node/index': 'src/node/index.ts'
28
+ },
18
29
  formats: ['es'],
19
- fileName: 'index'
30
+ fileName: (format, entryName) => `${entryName}.js`
31
+ },
32
+ rollupOptions: {
33
+ external: [
34
+ ...nodeBuiltins,
35
+ /^node:/
36
+ ]
20
37
  }
21
38
  },
22
39
  resolve: {
@@ -34,4 +51,4 @@ export default defineConfig({
34
51
  setupFiles: ['@rpgjs/testing/dist/setup.js'],
35
52
  globals: true
36
53
  }
37
- })
54
+ })