@whogoes/server 1.0.0 → 1.0.1
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 +35 -27
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -14,27 +14,26 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
14
14
|
return to;
|
|
15
15
|
};
|
|
16
16
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
21
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
22
|
mod
|
|
23
23
|
));
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
var import_config = require("dotenv/config");
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
var import_fastify = __toESM(require("fastify"));
|
|
30
30
|
var import_websocket = __toESM(require("@fastify/websocket"));
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
var import_uuid = require("uuid");
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
var import_ioredis = __toESM(require("ioredis"));
|
|
37
|
-
var import_ioredis_mock = __toESM(require("ioredis-mock"));
|
|
38
37
|
var redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
|
|
39
38
|
var useMock = process.env.USE_MOCK_REDIS === "true";
|
|
40
39
|
console.log(`Initializing Redis (Mock: ${useMock})`);
|
|
@@ -42,8 +41,9 @@ var redisClient;
|
|
|
42
41
|
var subRedisClient;
|
|
43
42
|
if (useMock) {
|
|
44
43
|
const sharedData = {};
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
const ActualRedisMock = require("ioredis-mock");
|
|
45
|
+
redisClient = new ActualRedisMock({ data: sharedData });
|
|
46
|
+
subRedisClient = new ActualRedisMock({ data: sharedData });
|
|
47
47
|
} else {
|
|
48
48
|
redisClient = new import_ioredis.default(redisUrl);
|
|
49
49
|
subRedisClient = new import_ioredis.default(redisUrl);
|
|
@@ -55,10 +55,10 @@ if (useMock) {
|
|
|
55
55
|
var redis = redisClient;
|
|
56
56
|
var subRedis = subRedisClient;
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
var RoomManager = class {
|
|
60
60
|
constructor() {
|
|
61
|
-
this.subscriptionMap =
|
|
61
|
+
this.subscriptionMap = new Map();
|
|
62
62
|
subRedis.on("message", (channel, message) => {
|
|
63
63
|
this.handleMessage(channel, message);
|
|
64
64
|
});
|
|
@@ -95,21 +95,22 @@ var RoomManager = class {
|
|
|
95
95
|
if (currentData) {
|
|
96
96
|
const current = JSON.parse(currentData);
|
|
97
97
|
const updated = { ...current, ...payload, lastActiveAt: Date.now() };
|
|
98
|
-
console.log("RoomManager.updatePresence", { roomId, sessionId, payload });
|
|
99
98
|
await redis.hset(key, sessionId, JSON.stringify(updated));
|
|
99
|
+
await redis.expire(key, 300);
|
|
100
100
|
await this.broadcast(roomId, { type: "presence.update", sessionId, payload: updated });
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
async getSnapshot(roomId) {
|
|
104
104
|
const key = this.getRoomKey(roomId);
|
|
105
105
|
const data = await redis.hgetall(key);
|
|
106
|
+
if (!data) return [];
|
|
106
107
|
return Object.values(data).map((s) => JSON.parse(s));
|
|
107
108
|
}
|
|
108
109
|
async subscribeToRoom(roomId, callback) {
|
|
109
110
|
const channel = this.getRoomChannel(roomId);
|
|
110
111
|
let handlers = this.subscriptionMap.get(channel);
|
|
111
112
|
if (!handlers) {
|
|
112
|
-
handlers =
|
|
113
|
+
handlers = new Set();
|
|
113
114
|
this.subscriptionMap.set(channel, handlers);
|
|
114
115
|
await subRedis.subscribe(channel);
|
|
115
116
|
}
|
|
@@ -129,9 +130,9 @@ var RoomManager = class {
|
|
|
129
130
|
};
|
|
130
131
|
var roomManager = new RoomManager();
|
|
131
132
|
|
|
132
|
-
|
|
133
|
-
var
|
|
134
|
-
|
|
133
|
+
|
|
134
|
+
var RATE_LIMIT_MS = 50;
|
|
135
|
+
var websocketHandler = async (socket, req) => {
|
|
135
136
|
const { roomId, token } = req.query;
|
|
136
137
|
if (!roomId || !token) {
|
|
137
138
|
socket.close(1008, "Missing roomId or token");
|
|
@@ -139,6 +140,7 @@ var websocketHandler = async (connection, req) => {
|
|
|
139
140
|
}
|
|
140
141
|
const userId = "user-" + (0, import_uuid.v4)().slice(0, 8);
|
|
141
142
|
const sessionId = (0, import_uuid.v4)();
|
|
143
|
+
let lastUpdateAt = 0;
|
|
142
144
|
const initialPayload = {
|
|
143
145
|
sessionId,
|
|
144
146
|
user: { id: userId, name: "Anonymous" },
|
|
@@ -153,16 +155,22 @@ var websocketHandler = async (connection, req) => {
|
|
|
153
155
|
}
|
|
154
156
|
});
|
|
155
157
|
const snapshot = await roomManager.getSnapshot(roomId);
|
|
156
|
-
socket.send(JSON.stringify({
|
|
158
|
+
socket.send(JSON.stringify({
|
|
159
|
+
type: "presence.snapshot",
|
|
160
|
+
sessions: snapshot.map((s) => ({ sessionId: s.sessionId, payload: s }))
|
|
161
|
+
}));
|
|
157
162
|
await roomManager.joinRoom(roomId, sessionId, initialPayload);
|
|
158
|
-
socket.on("message", async (
|
|
159
|
-
console.log("WS message received raw:", message && message.toString && message.toString());
|
|
163
|
+
socket.on("message", async (raw) => {
|
|
160
164
|
try {
|
|
161
|
-
const data = JSON.parse(
|
|
165
|
+
const data = JSON.parse(raw.toString());
|
|
162
166
|
switch (data.type) {
|
|
163
|
-
case "presence.update":
|
|
167
|
+
case "presence.update": {
|
|
168
|
+
const now = Date.now();
|
|
169
|
+
if (now - lastUpdateAt < RATE_LIMIT_MS) break;
|
|
170
|
+
lastUpdateAt = now;
|
|
164
171
|
await roomManager.updatePresence(roomId, sessionId, data.payload);
|
|
165
172
|
break;
|
|
173
|
+
}
|
|
166
174
|
case "ping":
|
|
167
175
|
socket.send(JSON.stringify({ type: "pong" }));
|
|
168
176
|
await roomManager.updatePresence(roomId, sessionId, { lastActiveAt: Date.now() });
|
|
@@ -180,13 +188,13 @@ var websocketHandler = async (connection, req) => {
|
|
|
180
188
|
console.error("WebSocket connection error:", err);
|
|
181
189
|
try {
|
|
182
190
|
socket.close(1011, "Internal Server Error");
|
|
183
|
-
} catch (
|
|
191
|
+
} catch (_) {
|
|
184
192
|
}
|
|
185
193
|
if (unsubscribe) unsubscribe();
|
|
186
194
|
}
|
|
187
195
|
};
|
|
188
196
|
|
|
189
|
-
|
|
197
|
+
|
|
190
198
|
var routes = async (fastify) => {
|
|
191
199
|
fastify.get("/health", async () => {
|
|
192
200
|
return { status: "ok" };
|
|
@@ -217,7 +225,7 @@ var routes = async (fastify) => {
|
|
|
217
225
|
});
|
|
218
226
|
};
|
|
219
227
|
|
|
220
|
-
|
|
228
|
+
|
|
221
229
|
var buildApp = async () => {
|
|
222
230
|
const app = (0, import_fastify.default)({
|
|
223
231
|
logger: true
|
|
@@ -230,7 +238,7 @@ var buildApp = async () => {
|
|
|
230
238
|
return app;
|
|
231
239
|
};
|
|
232
240
|
|
|
233
|
-
|
|
241
|
+
|
|
234
242
|
var start = async () => {
|
|
235
243
|
const app = await buildApp();
|
|
236
244
|
const port = process.env.PORT ? parseInt(process.env.PORT) : 3e3;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whogoes/server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Self-hostable real-time presence server — Fastify + WebSocket + Redis",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -45,4 +45,4 @@
|
|
|
45
45
|
"ts-node-dev": "^2.0.0",
|
|
46
46
|
"tsup": "^8.0.2"
|
|
47
47
|
}
|
|
48
|
-
}
|
|
48
|
+
}
|