@rpgjs/vite 5.0.0-alpha.33 → 5.0.0-alpha.36

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.
@@ -28,10 +28,8 @@ declare class PartyConnection {
28
28
  private messageQueue;
29
29
  private isProcessingQueue;
30
30
  private sequenceCounter;
31
- private lastSendTime;
32
- private outgoingFlushTimeout;
33
31
  private incomingQueue;
34
- private incomingFlushTimeout;
32
+ private isProcessingIncomingQueue;
35
33
  static packetLossRate: number;
36
34
  static packetLossEnabled: boolean;
37
35
  static packetLossFilter: string;
@@ -60,16 +58,18 @@ declare class PartyConnection {
60
58
  */
61
59
  send(data: any): Promise<void>;
62
60
  /**
63
- * Processes the outgoing queue with TCP-like batching.
61
+ * Processes the outgoing queue in order.
64
62
  *
65
- * - If latency is enabled, schedule a single flush after latencyMs to batch messages.
66
- * - At flush, send all queued messages in order, applying bandwidth delays per message.
63
+ * Each message receives its own fixed latency (if enabled), while preserving
64
+ * original spacing and order.
67
65
  */
68
66
  private processMessageQueue;
69
67
  /**
70
68
  * Flushes the send queue sequentially, respecting bandwidth constraints.
71
69
  */
72
70
  private flushSendQueue;
71
+ private shouldApplyLatency;
72
+ private waitUntil;
73
73
  /**
74
74
  * Closes the WebSocket connection
75
75
  */
@@ -89,8 +89,8 @@ declare class PartyConnection {
89
89
  /**
90
90
  * Buffers incoming messages to simulate TCP latency on reception.
91
91
  *
92
- * All messages that arrive within the latency window are flushed together,
93
- * preserving order. The provided processor is called with the ordered batch.
92
+ * Messages are processed in strict order. Each message keeps its own fixed
93
+ * latency delay relative to the moment it arrived.
94
94
  *
95
95
  * @param {string} message - Raw incoming message
96
96
  * @param {(messages: string[]) => Promise<void>} processor - Async batch processor
@@ -101,6 +101,7 @@ declare class PartyConnection {
101
101
  * })
102
102
  */
103
103
  bufferIncoming(message: string, processor: (messages: string[]) => Promise<void>): void;
104
+ private processIncomingQueue;
104
105
  /**
105
106
  * Configures packet loss simulation settings
106
107
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpgjs/vite",
3
- "version": "5.0.0-alpha.33",
3
+ "version": "5.0.0-alpha.36",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "keywords": [],
@@ -12,17 +12,17 @@
12
12
  "access": "public"
13
13
  },
14
14
  "dependencies": {
15
- "@canvasengine/compiler": "2.0.0-beta.50",
16
- "@hono/vite-dev-server": "^0.24.0",
17
- "@rpgjs/server": "5.0.0-alpha.33",
18
- "@types/node": "^25.0.3",
15
+ "@canvasengine/compiler": "2.0.0-beta.53",
16
+ "@hono/vite-dev-server": "^0.25.0",
17
+ "@rpgjs/server": "5.0.0-alpha.36",
18
+ "@types/node": "^25.2.3",
19
19
  "acorn": "^8.15.0",
20
20
  "acorn-walk": "^8.3.4",
21
21
  "magic-string": "^0.30.21",
22
22
  "typescript": "^5.9.3",
23
23
  "vite": "^7.3.1",
24
24
  "vite-plugin-dts": "^4.5.4",
25
- "vitest": "^4.0.16",
25
+ "vitest": "^4.0.18",
26
26
  "ws": "^8.19.0"
27
27
  },
28
28
  "devDependencies": {
@@ -42,10 +42,12 @@ class PartyConnection {
42
42
  private messageQueue: Array<{ message: string; timestamp: number; sequence: number }> = [];
43
43
  private isProcessingQueue: boolean = false;
44
44
  private sequenceCounter: number = 0;
45
- private lastSendTime: number = 0;
46
- private outgoingFlushTimeout: any = null;
47
- private incomingQueue: string[] = [];
48
- private incomingFlushTimeout: any = null;
45
+ private incomingQueue: Array<{
46
+ message: string;
47
+ timestamp: number;
48
+ processor: (messages: string[]) => Promise<void>;
49
+ }> = [];
50
+ private isProcessingIncomingQueue: boolean = false;
49
51
  public static packetLossRate: number = parseFloat(process.env.RPGJS_PACKET_LOSS_RATE || '0.1');
50
52
  public static packetLossEnabled: boolean = process.env.RPGJS_ENABLE_PACKET_LOSS === 'true';
51
53
  public static packetLossFilter: string = process.env.RPGJS_PACKET_LOSS_FILTER || '';
@@ -100,25 +102,12 @@ class PartyConnection {
100
102
  }
101
103
 
102
104
  /**
103
- * Processes the outgoing queue with TCP-like batching.
104
- *
105
- * - If latency is enabled, schedule a single flush after latencyMs to batch messages.
106
- * - At flush, send all queued messages in order, applying bandwidth delays per message.
105
+ * Processes the outgoing queue in order.
106
+ *
107
+ * Each message receives its own fixed latency (if enabled), while preserving
108
+ * original spacing and order.
107
109
  */
108
110
  private async processMessageQueue(): Promise<void> {
109
- if (this.messageQueue.length === 0) return;
110
-
111
- const shouldBatchWithLatency = PartyConnection.latencyEnabled && PartyConnection.latencyMs > 0;
112
-
113
- if (shouldBatchWithLatency) {
114
- if (this.outgoingFlushTimeout) return; // Already scheduled
115
- this.outgoingFlushTimeout = setTimeout(async () => {
116
- this.outgoingFlushTimeout = null;
117
- await this.flushSendQueue();
118
- }, PartyConnection.latencyMs);
119
- return;
120
- }
121
-
122
111
  await this.flushSendQueue();
123
112
  }
124
113
 
@@ -132,6 +121,11 @@ class PartyConnection {
132
121
  while (this.messageQueue.length > 0) {
133
122
  const queueItem = this.messageQueue.shift()!;
134
123
 
124
+ // Apply fixed one-way latency per message (not batched bursts).
125
+ if (this.shouldApplyLatency(queueItem.message)) {
126
+ await this.waitUntil(queueItem.timestamp + PartyConnection.latencyMs);
127
+ }
128
+
135
129
  // Bandwidth simulation per message
136
130
  if (PartyConnection.bandwidthEnabled && PartyConnection.bandwidthKbps > 0) {
137
131
  if (!PartyConnection.bandwidthFilter || queueItem.message.includes(PartyConnection.bandwidthFilter)) {
@@ -150,6 +144,24 @@ class PartyConnection {
150
144
  this.isProcessingQueue = false;
151
145
  }
152
146
 
147
+ private shouldApplyLatency(message: string): boolean {
148
+ if (!PartyConnection.latencyEnabled || PartyConnection.latencyMs <= 0) {
149
+ return false;
150
+ }
151
+ if (!PartyConnection.latencyFilter) {
152
+ return true;
153
+ }
154
+ return message.includes(PartyConnection.latencyFilter);
155
+ }
156
+
157
+ private async waitUntil(targetTimestamp: number): Promise<void> {
158
+ const delayMs = targetTimestamp - Date.now();
159
+ if (delayMs <= 0) {
160
+ return;
161
+ }
162
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
163
+ }
164
+
153
165
  /**
154
166
  * Closes the WebSocket connection
155
167
  */
@@ -180,9 +192,9 @@ class PartyConnection {
180
192
 
181
193
  /**
182
194
  * Buffers incoming messages to simulate TCP latency on reception.
183
- *
184
- * All messages that arrive within the latency window are flushed together,
185
- * preserving order. The provided processor is called with the ordered batch.
195
+ *
196
+ * Messages are processed in strict order. Each message keeps its own fixed
197
+ * latency delay relative to the moment it arrived.
186
198
  *
187
199
  * @param {string} message - Raw incoming message
188
200
  * @param {(messages: string[]) => Promise<void>} processor - Async batch processor
@@ -193,19 +205,33 @@ class PartyConnection {
193
205
  * })
194
206
  */
195
207
  bufferIncoming(message: string, processor: (messages: string[]) => Promise<void>): void {
196
- this.incomingQueue.push(message);
197
- const latencyMs = PartyConnection.latencyEnabled && PartyConnection.latencyMs > 0 ? PartyConnection.latencyMs : 0;
198
- if (this.incomingFlushTimeout) return;
199
- this.incomingFlushTimeout = setTimeout(async () => {
200
- const batch = this.incomingQueue;
201
- this.incomingQueue = [];
202
- this.incomingFlushTimeout = null;
208
+ this.incomingQueue.push({
209
+ message,
210
+ timestamp: Date.now(),
211
+ processor,
212
+ });
213
+ if (!this.isProcessingIncomingQueue) {
214
+ void this.processIncomingQueue();
215
+ }
216
+ }
217
+
218
+ private async processIncomingQueue(): Promise<void> {
219
+ if (this.isProcessingIncomingQueue) {
220
+ return;
221
+ }
222
+ this.isProcessingIncomingQueue = true;
223
+ while (this.incomingQueue.length > 0) {
224
+ const item = this.incomingQueue.shift()!;
225
+ if (this.shouldApplyLatency(item.message)) {
226
+ await this.waitUntil(item.timestamp + PartyConnection.latencyMs);
227
+ }
203
228
  try {
204
- await processor(batch);
229
+ await item.processor([item.message]);
205
230
  } catch (err) {
206
- console.error('Error processing incoming batch:', err);
231
+ console.error('Error processing incoming message:', err);
207
232
  }
208
- }, latencyMs);
233
+ }
234
+ this.isProcessingIncomingQueue = false;
209
235
  }
210
236
 
211
237
  /**
@@ -481,12 +507,16 @@ async function importWebSocketServer(): Promise<any> {
481
507
  }
482
508
 
483
509
  async function updateMap(roomId: string, rpgServer: RpgServerEngine) {
510
+ if (!roomId.startsWith('map-')) {
511
+ return;
512
+ }
513
+
484
514
  try {
485
515
  const mapId = roomId.startsWith('map-') ? roomId.slice(4) : roomId;
486
516
  const defaultMapPayload = {
487
517
  id: mapId,
488
- width: 1000,
489
- height: 1000,
518
+ width: 0,
519
+ height: 0,
490
520
  events: [] as any[],
491
521
  };
492
522
 
@@ -684,27 +714,82 @@ export function serverPlugin(
684
714
  // HTTP request interception for /parties/* routes
685
715
  server.middlewares.use("/parties", async (req, res, next) => {
686
716
  try {
687
- // For now, pass to the next middleware
688
- // The RPG-JS server handles its own routes via @signe/room
689
- console.log(`RPG-JS HTTP request: ${req.method} ${req.url}`);
717
+ const host = req.headers.host || "localhost";
718
+ const incomingUrl = req.url || "/";
719
+ const parsedUrl = new URL(incomingUrl, `http://${host}`);
720
+ const normalizedPath = parsedUrl.pathname.startsWith("/parties")
721
+ ? parsedUrl.pathname
722
+ : `/parties${parsedUrl.pathname.startsWith("/") ? parsedUrl.pathname : `/${parsedUrl.pathname}`}`;
723
+ const pathParts = normalizedPath.split("/").filter(Boolean);
724
+
725
+ if (pathParts[0] !== "parties" || pathParts[1] !== "main" || pathParts.length < 4) {
726
+ next();
727
+ return;
728
+ }
729
+
730
+ const roomId = pathParts[2];
731
+ const requestPath = `/${pathParts.slice(3).join("/")}`;
732
+ const { room, rpgServer } = await ensureRoomAndServer(roomId);
733
+ room.context.parties = buildPartiesContext();
734
+
735
+ const bodyText = await new Promise<string>((resolve, reject) => {
736
+ const chunks: Buffer[] = [];
737
+ req.on("data", (chunk: Buffer | string) => {
738
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
739
+ });
740
+ req.on("end", () => {
741
+ resolve(Buffer.concat(chunks).toString("utf8"));
742
+ });
743
+ req.on("error", reject);
744
+ });
745
+
746
+ const requestHeaders = new Headers();
747
+ Object.entries(req.headers).forEach(([key, value]) => {
748
+ if (Array.isArray(value)) {
749
+ if (value[0] !== undefined) requestHeaders.set(key, value[0]);
750
+ return;
751
+ }
752
+ if (typeof value === "string") {
753
+ requestHeaders.set(key, value);
754
+ }
755
+ });
756
+
757
+ const requestLike = {
758
+ url: `http://${host}/parties/main/${roomId}${requestPath}${parsedUrl.search}`,
759
+ method: (req.method || "GET").toUpperCase(),
760
+ headers: requestHeaders,
761
+ json: async () => {
762
+ if (!bodyText) return undefined as any;
763
+ return JSON.parse(bodyText);
764
+ },
765
+ text: async () => bodyText,
766
+ } as any;
767
+
768
+ const result = await (rpgServer as any).onRequest(requestLike);
769
+
770
+ if (result instanceof Response) {
771
+ res.statusCode = result.status;
772
+ result.headers.forEach((value, key) => {
773
+ res.setHeader(key, value);
774
+ });
775
+ res.end(await result.text());
776
+ return;
777
+ }
690
778
 
691
- // Create a basic response for test routes
692
- if (req.url?.includes("/test")) {
779
+ if (typeof result === "string") {
693
780
  res.statusCode = 200;
694
- res.setHeader("Content-Type", "application/json");
695
- res.end(
696
- JSON.stringify({
697
- message: "RPG-JS server is running",
698
- timestamp: new Date().toISOString(),
699
- })
700
- );
781
+ res.setHeader("Content-Type", "text/plain");
782
+ res.end(result);
701
783
  return;
702
784
  }
703
785
 
704
- next();
786
+ res.statusCode = 200;
787
+ res.setHeader("Content-Type", "application/json");
788
+ res.end(JSON.stringify(result ?? {}));
705
789
  } catch (error) {
706
790
  console.error("Error handling RPG-JS request:", error);
707
791
  res.statusCode = 500;
792
+ res.setHeader("Content-Type", "application/json");
708
793
  res.end(JSON.stringify({ error: "Internal server error" }));
709
794
  }
710
795
  });