@jgengine/node 0.6.0 → 0.8.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/CHANGELOG.md +214 -2
- package/dist/host.d.ts +1 -72
- package/dist/host.js +1 -336
- package/dist/persistence.d.ts +1 -1
- package/dist/persistence.js +5 -103
- package/dist/socketIoServer.d.ts +21 -0
- package/dist/socketIoServer.js +16 -0
- package/dist/webHandler.d.ts +5 -0
- package/dist/webHandler.js +27 -0
- package/dist/wsServer.d.ts +7 -10
- package/dist/wsServer.js +15 -217
- package/llms.txt +1408 -0
- package/package.json +5 -4
package/dist/wsServer.js
CHANGED
|
@@ -1,224 +1,21 @@
|
|
|
1
1
|
import { WebSocketServer } from "ws";
|
|
2
|
-
import {
|
|
3
|
-
import { decodeWsClientMessage, encodeWsMessage, subscriptionKey, } from "@jgengine/ws/protocol";
|
|
4
|
-
const DEFAULT_POSE_RULES = {
|
|
5
|
-
maxSpeed: 12,
|
|
6
|
-
maxVerticalOffset: 3,
|
|
7
|
-
minElapsedSec: 0.05,
|
|
8
|
-
maxElapsedSec: 0.5,
|
|
9
|
-
keepAliveRefreshMs: 10_000,
|
|
10
|
-
};
|
|
2
|
+
import { createHostRouter } from "@jgengine/ws/hostRouter";
|
|
11
3
|
export function createGameWsServer(options) {
|
|
12
|
-
const
|
|
13
|
-
const now = options.now ?? Date.now;
|
|
14
|
-
const poseRules = options.poseRules ?? DEFAULT_POSE_RULES;
|
|
15
|
-
const authenticate = options.authenticate ?? (({ userId }) => userId);
|
|
4
|
+
const router = createHostRouter(options);
|
|
16
5
|
const wss = options.server !== undefined
|
|
17
6
|
? new WebSocketServer({ server: options.server, path: options.path })
|
|
18
7
|
: new WebSocketServer({ port: options.port ?? 0, path: options.path });
|
|
19
|
-
const connections = new Set();
|
|
20
|
-
const presence = new Map();
|
|
21
|
-
const send = (connection, message) => {
|
|
22
|
-
if (connection.socket.readyState !== connection.socket.OPEN)
|
|
23
|
-
return;
|
|
24
|
-
connection.socket.send(encodeWsMessage(message));
|
|
25
|
-
};
|
|
26
|
-
const reply = (connection, id, result) => {
|
|
27
|
-
send(connection, { v: 1, t: "reply", id, ok: true, result });
|
|
28
|
-
};
|
|
29
|
-
const replyError = (connection, id, reason) => {
|
|
30
|
-
send(connection, { v: 1, t: "reply", id, ok: false, reason });
|
|
31
|
-
};
|
|
32
|
-
const presenceRows = (serverId) => {
|
|
33
|
-
const rows = presence.get(serverId);
|
|
34
|
-
if (rows === undefined)
|
|
35
|
-
return [];
|
|
36
|
-
return [...rows.entries()].map(([userId, pose]) => ({
|
|
37
|
-
userId,
|
|
38
|
-
position: pose.position,
|
|
39
|
-
rotationY: pose.rotationY,
|
|
40
|
-
rotationPitch: pose.rotationPitch ?? 0,
|
|
41
|
-
lastSeenAt: pose.lastSeenAtMs ?? 0,
|
|
42
|
-
}));
|
|
43
|
-
};
|
|
44
|
-
const broadcastPresence = (serverId) => {
|
|
45
|
-
const key = subscriptionKey("presence", serverId);
|
|
46
|
-
const data = presenceRows(serverId);
|
|
47
|
-
for (const connection of connections) {
|
|
48
|
-
if (!connection.subscriptions.has(key))
|
|
49
|
-
continue;
|
|
50
|
-
send(connection, { v: 1, t: "update", channel: "presence", serverId, data });
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
const pushSubscription = async (connection, channel, serverId, action) => {
|
|
54
|
-
if (connection.userId === null)
|
|
55
|
-
return;
|
|
56
|
-
if (channel === "server") {
|
|
57
|
-
const data = await host.getServerView({ userId: connection.userId, serverId });
|
|
58
|
-
send(connection, { v: 1, t: "update", channel, serverId, data });
|
|
59
|
-
}
|
|
60
|
-
else if (channel === "player") {
|
|
61
|
-
const data = await host.getPlayerView({ userId: connection.userId, serverId });
|
|
62
|
-
send(connection, { v: 1, t: "update", channel, serverId, data });
|
|
63
|
-
}
|
|
64
|
-
else if (channel === "feed") {
|
|
65
|
-
const data = await host.getFeed({ userId: connection.userId, serverId, action: action ?? "" });
|
|
66
|
-
send(connection, { v: 1, t: "update", channel, serverId, action: action ?? "", data });
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
send(connection, { v: 1, t: "update", channel, serverId, data: presenceRows(serverId) });
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
const onHostEvent = (event) => {
|
|
73
|
-
for (const connection of connections) {
|
|
74
|
-
if (connection.userId === null)
|
|
75
|
-
continue;
|
|
76
|
-
if (event.type === "server") {
|
|
77
|
-
if (connection.subscriptions.has(subscriptionKey("server", event.serverId))) {
|
|
78
|
-
void pushSubscription(connection, "server", event.serverId);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
else if (event.type === "player") {
|
|
82
|
-
if (connection.userId === event.userId &&
|
|
83
|
-
connection.subscriptions.has(subscriptionKey("player", event.serverId))) {
|
|
84
|
-
void pushSubscription(connection, "player", event.serverId);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
if (connection.subscriptions.has(subscriptionKey("feed", event.serverId, event.action))) {
|
|
89
|
-
void pushSubscription(connection, "feed", event.serverId, event.action);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
const unsubscribeHost = host.subscribe(onHostEvent);
|
|
95
|
-
const handlePose = (connection, serverId, pose) => {
|
|
96
|
-
if (connection.userId === null)
|
|
97
|
-
return;
|
|
98
|
-
const rows = presence.get(serverId) ?? new Map();
|
|
99
|
-
presence.set(serverId, rows);
|
|
100
|
-
const timestamp = now();
|
|
101
|
-
const current = rows.get(connection.userId);
|
|
102
|
-
if (current === undefined) {
|
|
103
|
-
rows.set(connection.userId, {
|
|
104
|
-
position: { x: pose.x, y: pose.y, z: pose.z },
|
|
105
|
-
rotationY: pose.rotationY,
|
|
106
|
-
rotationPitch: pose.rotationPitch,
|
|
107
|
-
lastSeenAtMs: timestamp,
|
|
108
|
-
});
|
|
109
|
-
broadcastPresence(serverId);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
const decision = decidePoseSync(current, { position: { x: pose.x, y: pose.y, z: pose.z }, rotationY: pose.rotationY, rotationPitch: pose.rotationPitch }, poseRules, timestamp);
|
|
113
|
-
if (decision.changed || decision.refreshKeepAlive) {
|
|
114
|
-
rows.set(connection.userId, {
|
|
115
|
-
position: decision.position,
|
|
116
|
-
rotationY: decision.rotationY,
|
|
117
|
-
rotationPitch: decision.rotationPitch,
|
|
118
|
-
lastSeenAtMs: timestamp,
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
if (decision.changed)
|
|
122
|
-
broadcastPresence(serverId);
|
|
123
|
-
};
|
|
124
|
-
const dropPresence = (connection) => {
|
|
125
|
-
if (connection.userId === null)
|
|
126
|
-
return;
|
|
127
|
-
for (const [serverId, rows] of presence) {
|
|
128
|
-
if (rows.delete(connection.userId)) {
|
|
129
|
-
if (rows.size === 0)
|
|
130
|
-
presence.delete(serverId);
|
|
131
|
-
broadcastPresence(serverId);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
const handleMessage = async (connection, message) => {
|
|
136
|
-
if (message.t === "hello") {
|
|
137
|
-
const userId = await authenticate({ userId: message.userId, token: message.token });
|
|
138
|
-
if (userId === null) {
|
|
139
|
-
replyError(connection, message.id, "Not authenticated");
|
|
140
|
-
connection.socket.close();
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
connection.userId = userId;
|
|
144
|
-
reply(connection, message.id, { userId });
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
if (message.t === "pose") {
|
|
148
|
-
handlePose(connection, message.serverId, message.pose);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
if (connection.userId === null) {
|
|
152
|
-
replyError(connection, message.id, "Not authenticated");
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
const userId = connection.userId;
|
|
156
|
-
try {
|
|
157
|
-
switch (message.t) {
|
|
158
|
-
case "join": {
|
|
159
|
-
const result = await host.joinServer({
|
|
160
|
-
userId,
|
|
161
|
-
gameId: message.gameId,
|
|
162
|
-
serverId: message.serverId,
|
|
163
|
-
});
|
|
164
|
-
reply(connection, message.id, result);
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
case "leave": {
|
|
168
|
-
await host.leaveServer({ userId, serverId: message.serverId });
|
|
169
|
-
reply(connection, message.id, null);
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
case "runCommand": {
|
|
173
|
-
const result = await host.runCommand({
|
|
174
|
-
userId,
|
|
175
|
-
serverId: message.serverId,
|
|
176
|
-
command: message.command,
|
|
177
|
-
input: message.input,
|
|
178
|
-
});
|
|
179
|
-
reply(connection, message.id, result);
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
case "pushFeed": {
|
|
183
|
-
await host.pushFeedEntry({
|
|
184
|
-
userId,
|
|
185
|
-
serverId: message.serverId,
|
|
186
|
-
action: message.action,
|
|
187
|
-
entry: message.entry,
|
|
188
|
-
});
|
|
189
|
-
reply(connection, message.id, null);
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
case "subscribe": {
|
|
193
|
-
connection.subscriptions.add(subscriptionKey(message.channel, message.serverId, message.action));
|
|
194
|
-
reply(connection, message.id, null);
|
|
195
|
-
await pushSubscription(connection, message.channel, message.serverId, message.action);
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
case "unsubscribe": {
|
|
199
|
-
connection.subscriptions.delete(subscriptionKey(message.channel, message.serverId, message.action));
|
|
200
|
-
reply(connection, message.id, null);
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
catch (error) {
|
|
206
|
-
replyError(connection, message.id, error instanceof Error ? error.message : "Internal error");
|
|
207
|
-
}
|
|
208
|
-
};
|
|
209
8
|
wss.on("connection", (socket) => {
|
|
210
|
-
const connection = {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
});
|
|
218
|
-
socket.on("close", () => {
|
|
219
|
-
connections.delete(connection);
|
|
220
|
-
dropPresence(connection);
|
|
9
|
+
const connection = router.connect({
|
|
10
|
+
send: (data) => {
|
|
11
|
+
if (socket.readyState !== socket.OPEN)
|
|
12
|
+
return;
|
|
13
|
+
socket.send(data);
|
|
14
|
+
},
|
|
15
|
+
close: () => socket.close(),
|
|
221
16
|
});
|
|
17
|
+
socket.on("message", (raw) => connection.handleRaw(raw.toString()));
|
|
18
|
+
socket.on("close", () => connection.close());
|
|
222
19
|
});
|
|
223
20
|
return {
|
|
224
21
|
wss,
|
|
@@ -229,10 +26,11 @@ export function createGameWsServer(options) {
|
|
|
229
26
|
}
|
|
230
27
|
return address.port;
|
|
231
28
|
},
|
|
29
|
+
rewind: router.rewind,
|
|
232
30
|
close: () => new Promise((resolve) => {
|
|
233
|
-
|
|
234
|
-
for (const
|
|
235
|
-
|
|
31
|
+
router.close();
|
|
32
|
+
for (const client of wss.clients) {
|
|
33
|
+
client.terminate();
|
|
236
34
|
}
|
|
237
35
|
let settled = false;
|
|
238
36
|
const finish = () => {
|