@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.
- package/dist/index.js +114 -42
- package/dist/index.js.map +1 -1
- package/dist/server-plugin.d.ts +9 -8
- package/package.json +6 -6
- package/src/server-plugin.ts +134 -49
package/dist/server-plugin.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
61
|
+
* Processes the outgoing queue in order.
|
|
64
62
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
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
|
-
*
|
|
93
|
-
*
|
|
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.
|
|
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.
|
|
16
|
-
"@hono/vite-dev-server": "^0.
|
|
17
|
-
"@rpgjs/server": "5.0.0-alpha.
|
|
18
|
-
"@types/node": "^25.
|
|
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.
|
|
25
|
+
"vitest": "^4.0.18",
|
|
26
26
|
"ws": "^8.19.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
package/src/server-plugin.ts
CHANGED
|
@@ -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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
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
|
-
*
|
|
185
|
-
*
|
|
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(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
this.
|
|
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(
|
|
229
|
+
await item.processor([item.message]);
|
|
205
230
|
} catch (err) {
|
|
206
|
-
console.error('Error processing incoming
|
|
231
|
+
console.error('Error processing incoming message:', err);
|
|
207
232
|
}
|
|
208
|
-
}
|
|
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:
|
|
489
|
-
height:
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
|
|
692
|
-
if (req.url?.includes("/test")) {
|
|
779
|
+
if (typeof result === "string") {
|
|
693
780
|
res.statusCode = 200;
|
|
694
|
-
res.setHeader("Content-Type", "
|
|
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
|
-
|
|
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
|
});
|