@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.
Files changed (2) hide show
  1. package/dist/index.js +35 -27
  2. 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
- // If the importer is in node compatibility mode or this is not an ESM
18
- // file that has been converted to a CommonJS file using a Babel-
19
- // compatible transform (i.e. "__esModule" has not been set), then set
20
- // "default" to the CommonJS "module.exports" for node compatibility.
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
- // src/index.ts
25
+
26
26
  var import_config = require("dotenv/config");
27
27
 
28
- // src/app.ts
28
+
29
29
  var import_fastify = __toESM(require("fastify"));
30
30
  var import_websocket = __toESM(require("@fastify/websocket"));
31
31
 
32
- // src/websocket.ts
32
+
33
33
  var import_uuid = require("uuid");
34
34
 
35
- // src/redis.ts
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
- redisClient = new import_ioredis_mock.default({ data: sharedData });
46
- subRedisClient = new import_ioredis_mock.default({ data: sharedData });
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
- // src/roomManager.ts
58
+
59
59
  var RoomManager = class {
60
60
  constructor() {
61
- this.subscriptionMap = /* @__PURE__ */ new Map();
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 = /* @__PURE__ */ new Set();
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
- // src/websocket.ts
133
- var websocketHandler = async (connection, req) => {
134
- const socket = connection && connection.socket ? connection.socket : connection;
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({ type: "presence.snapshot", sessions: snapshot }));
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 (message) => {
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(message.toString());
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 (e) {
191
+ } catch (_) {
184
192
  }
185
193
  if (unsubscribe) unsubscribe();
186
194
  }
187
195
  };
188
196
 
189
- // src/routes.ts
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
- // src/app.ts
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
- // src/index.ts
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.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
+ }