@yrpri/api 9.0.94 → 9.0.96

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/package.json +2 -2
  2. package/webSockets.js +3 -272
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yrpri/api",
3
- "version": "9.0.94",
3
+ "version": "9.0.96",
4
4
  "license": "MIT",
5
5
  "author": "Robert Bjarnason & Citizens Foundation",
6
6
  "repository": {
@@ -71,7 +71,7 @@
71
71
  "nodemailer": "^6.10.0",
72
72
  "open-graph-scraper": "^6.9.0",
73
73
  "openai": "^4.85.3",
74
- "passport": "^0.7.0",
74
+ "passport": "^0.6.0",
75
75
  "passport-facebook": "^3.0.0",
76
76
  "passport-github": "^1.1.0",
77
77
  "passport-google-oauth": "^2.0.0",
package/webSockets.js CHANGED
@@ -1,4 +1,4 @@
1
- import { WebSocketServer, WebSocket } from "ws";
1
+ import { WebSocketServer } from "ws";
2
2
  import { v4 as uuidv4 } from "uuid";
3
3
  /**
4
4
  * WebSocketsManager:
@@ -10,26 +10,15 @@ export class WebSocketsManager {
10
10
  constructor(wsClients, redisClient, server) {
11
11
  // Ping/pong heartbeat interval
12
12
  this.pingInterval = null;
13
- // Configuration for Redis reconnection attempts
14
- this.maxRedisReconnectAttempts = 5;
15
- this.redisReconnectDelay = 2000; // milliseconds
16
13
  this.wsClients = wsClients;
17
14
  this.redisClient = redisClient;
18
15
  this.ws = new WebSocketServer({ server });
19
- // You could also read this from environment or a hostname if preferred
20
- this.serverId =
21
- process.env.NODE_ENV === "development"
22
- ? "dev-server"
23
- : `server-${Math.random()}`;
24
- console.log("WebSockets: Starting WebSocketsManager on serverId:", this.serverId);
25
16
  }
26
17
  /**
27
18
  * Main entry point to start listening for connections
28
19
  * and initialize Redis pub/sub, heartbeat, etc.
29
20
  */
30
21
  async listen() {
31
- // Setup Redis pub/sub with reconnection logic
32
- await this.setupPubSub();
33
22
  // Start periodic ping/pong for WS clients
34
23
  this.startPingCheck();
35
24
  // Convert to async so we can 'await' inside
@@ -40,82 +29,16 @@ export class WebSocketsManager {
40
29
  if (!clientId) {
41
30
  clientId = uuidv4();
42
31
  }
43
- // 2) Attempt to claim ownership of this clientId in Redis
44
- const claimed = await this.tryClaimClientId(clientId);
45
- if (!claimed) {
46
- // Another server owns it—ask that server to release
47
- await this.requestReleaseClientId(clientId);
48
- // Optionally, you might wait or forcibly overwrite after some delay
49
- console.log(`WebSockets: Server ${this.serverId} could not claim clientId ${clientId} immediately.`);
50
- }
51
- else {
52
- console.log(`WebSockets: Server ${this.serverId} claimed clientId ${clientId}.`);
53
- }
54
- // 3) Locally, if there's already a socket for that ID on *this* server, terminate it
55
- const oldSocket = this.wsClients.get(clientId);
56
- if (oldSocket && oldSocket.readyState === WebSocket.OPEN) {
57
- oldSocket.terminate();
58
- }
59
- // 4) Store this new WebSocket in the map
60
32
  this.wsClients.set(clientId, ws);
61
- // 5) Mark it as alive for ping/pong
62
33
  ws.isAlive = true;
63
34
  ws.on("pong", () => {
64
35
  ws.isAlive = true;
65
36
  });
66
- console.log(`WebSockets: New WebSocket connection on serverId ${this.serverId}: clientId ${clientId}`);
67
- // 6) Send the final clientId back to the client
37
+ console.log(`WebSockets: New WebSocket connection: clientId ${clientId}`);
68
38
  ws.send(JSON.stringify({ clientId }));
69
- // 7) Server-level message listener
70
- ws.on("message", (messageData) => {
71
- // If for some reason we *no longer* own this clientId, publish to Redis
72
- // so that whichever server DOES own it can handle the message on a higher level in the code that adds their own .on("message")
73
- if (!this.wsClients.has(clientId)) {
74
- let parsedMessage;
75
- try {
76
- parsedMessage = JSON.parse(messageData.toString());
77
- }
78
- catch (err) {
79
- console.log(`WebSockets: Received non-JSON message from client ${clientId}:`, messageData.toString());
80
- parsedMessage = messageData.toString();
81
- }
82
- const messageToSend = JSON.stringify({
83
- clientId,
84
- action: "directMessage",
85
- data: parsedMessage,
86
- });
87
- this.pub
88
- ?.publish("ypWebsocketChannel", messageToSend)
89
- .then((reply) => {
90
- console.log(`WebSockets: Message published to ypWebsocketChannel: ${reply}`);
91
- })
92
- .catch((err) => {
93
- console.error("WebSockets: Error publishing to Redis:", err);
94
- });
95
- }
96
- else {
97
- // Otherwise, we handle it locally or pass it on
98
- // Typically you'd have local server logic here,
99
- // or forward it to some local "chatbot" logic, etc.
100
- /*console.log(
101
- `WebSockets: Local server ${this.serverId} handling message from clientId ${clientId}`
102
- );*/
103
- }
104
- });
105
- // 8) Clean up on close/error
106
39
  ws.on("close", async () => {
107
- // If we still own the clientId, remove it from local map
108
40
  this.wsClients.delete(clientId);
109
- console.log(`WebSockets: WebSocket closed: clientId ${clientId} on server ${this.serverId}`);
110
- // Optionally, we can remove ownership from Redis if the user disconnected
111
- // But you may only want to do that after some time, or not at all,
112
- // depending on whether you expect reconnections with the same clientId.
113
- const currentOwner = await this.redisClient.get(`clientOwner:${clientId}`);
114
- if (currentOwner === this.serverId) {
115
- // We are the legitimate owner, so free the key
116
- await this.redisClient.del(`clientOwner:${clientId}`);
117
- console.log(`WebSockets: Server ${this.serverId} removed ownership for clientId ${clientId} from Redis.`);
118
- }
41
+ console.log(`WebSockets: WebSocket closed: clientId ${clientId}`);
119
42
  });
120
43
  ws.on("error", (err) => {
121
44
  this.wsClients.delete(clientId);
@@ -123,179 +46,6 @@ export class WebSocketsManager {
123
46
  });
124
47
  });
125
48
  }
126
- // -------------------------------
127
- // 1) TRY CLAIMING CLIENT OWNERSHIP
128
- // -------------------------------
129
- async tryClaimClientId(clientId) {
130
- const currentOwner = await this.redisClient.get(`clientOwner:${clientId}`);
131
- if (currentOwner === this.serverId) {
132
- // Update expiration if necessary
133
- await this.redisClient.set(`clientOwner:${clientId}`, this.serverId, {
134
- XX: true,
135
- EX: 24 * 60 * 60,
136
- });
137
- return true;
138
- }
139
- // Use SETNX approach
140
- // NX => only set if key does NOT exist
141
- // EX => optional expiry; you can set a day, etc. so it doesn't last forever
142
- const result = await this.redisClient.set(`clientOwner:${clientId}`, this.serverId, {
143
- NX: true, // Set only if it doesn't exist
144
- EX: 24 * 60 * 60, // e.g. 24h, or omit if you prefer no expiry
145
- });
146
- if (result === "OK") {
147
- // We successfully claimed
148
- return true;
149
- }
150
- else {
151
- // Another server owns it
152
- const currentOwner = await this.redisClient.get(`clientOwner:${clientId}`);
153
- console.log(`WebSockets: Unable to claim clientId ${clientId}, already owned by ${currentOwner}`);
154
- return false;
155
- }
156
- }
157
- // -------------------------------
158
- // 2) REQUEST RELEASE FROM OLD OWNER
159
- // -------------------------------
160
- async requestReleaseClientId(clientId) {
161
- const currentOwner = await this.redisClient.get(`clientOwner:${clientId}`);
162
- if (!currentOwner)
163
- return; // No one actually owns it, maybe it expired?
164
- if (currentOwner === this.serverId) {
165
- // We ironically already own it (possibly race condition?), just return
166
- return;
167
- }
168
- console.log(`WebSockets: Requesting server ${currentOwner} to release clientId ${clientId}`);
169
- const msg = {
170
- action: "releaseClientId",
171
- clientId,
172
- fromServer: this.serverId,
173
- oldOwner: currentOwner,
174
- };
175
- await this.pub?.publish("ypControlChannel", JSON.stringify(msg));
176
- }
177
- // -------------------------------
178
- // 3) SETUP REDIS PUB/SUB + CONTROL CHANNEL
179
- // -------------------------------
180
- async setupPubSub() {
181
- this.pub = this.redisClient.duplicate();
182
- this.sub = this.redisClient.duplicate();
183
- // Listen for errors and attempt to reconnect when needed
184
- this.pub.on("error", (err) => {
185
- console.error("WebSockets: Publisher Redis client error:", err);
186
- this.handleRedisError(this.pub);
187
- });
188
- this.sub.on("error", (err) => {
189
- console.error("WebSockets: Subscriber Redis client error:", err);
190
- this.handleRedisError(this.sub);
191
- });
192
- // Connect with retry logic
193
- await Promise.all([
194
- this.connectRedisClient(this.pub, "Publisher"),
195
- this.connectRedisClient(this.sub, "Subscriber"),
196
- ]);
197
- // 3a) Subscribe to the primary channel for direct messages
198
- this.sub.subscribe("ypWebsocketChannel", (message, channel) => {
199
- try {
200
- const parsed = JSON.parse(message);
201
- const { clientId, action, data } = parsed;
202
- console.log(`WebSockets: Received from Redis on ${channel}: ${JSON.stringify(parsed)}`);
203
- switch (action) {
204
- case "directMessage":
205
- // If we own this clientId, forward to local WebSocket
206
- const ws = this.wsClients.get(clientId);
207
- if (ws) {
208
- try {
209
- ws.send(JSON.stringify(data));
210
- }
211
- catch (err) {
212
- console.error(`WebSockets: Error sending direct message to ${clientId}:`, err);
213
- }
214
- }
215
- else {
216
- // We apparently don't have that client
217
- console.warn(`WebSockets: No WebSocket found locally for clientId ${clientId}`);
218
- this.wsClients.delete(clientId);
219
- }
220
- break;
221
- default:
222
- console.warn(`WebSockets: Unknown action '${action}' received from Redis.`);
223
- }
224
- }
225
- catch (err) {
226
- console.error("WebSockets: Error handling Redis message:", err);
227
- }
228
- });
229
- // 3b) Subscribe to the control channel for ownership requests
230
- this.sub.subscribe("ypControlChannel", async (message, channel) => {
231
- try {
232
- const parsed = JSON.parse(message);
233
- const { action, clientId, oldOwner, fromServer } = parsed;
234
- if (action === "releaseClientId") {
235
- // Check if *we* are actually the old owner
236
- const currentOwner = await this.redisClient.get(`clientOwner:${clientId}`);
237
- if (currentOwner === this.serverId && this.serverId === oldOwner) {
238
- // We own this client, so let's release it
239
- console.log(`WebSockets: Server ${this.serverId} is releasing clientId ${clientId} (requested by ${fromServer}).`);
240
- // Close local WebSocket if still open
241
- const ws = this.wsClients.get(clientId);
242
- if (ws && ws.readyState === WebSocket.OPEN) {
243
- ws.close(1000, "Another server claimed ownership");
244
- this.wsClients.delete(clientId);
245
- }
246
- // Remove ownership from Redis
247
- await this.redisClient.del(`clientOwner:${clientId}`);
248
- console.log(`WebSockets: Server ${this.serverId} removed ownership for clientId ${clientId}.`);
249
- }
250
- else {
251
- // We don't own it or the ownership mismatch
252
- // Possibly ignore or log
253
- console.log(`WebSockets: releaseClientId ignored by ${this.serverId}. currentOwner=${currentOwner}, oldOwner=${oldOwner}`);
254
- }
255
- }
256
- }
257
- catch (err) {
258
- console.error("WebSockets: Error handling control message:", err);
259
- }
260
- });
261
- }
262
- /**
263
- * Attempt to connect a Redis client with a few retries.
264
- */
265
- async connectRedisClient(client, clientName) {
266
- let attempts = 0;
267
- while (attempts < this.maxRedisReconnectAttempts) {
268
- try {
269
- attempts++;
270
- await client.connect();
271
- console.log(`WebSockets: ${clientName} connected successfully.`);
272
- return;
273
- }
274
- catch (err) {
275
- console.error(`WebSockets: ${clientName} connection attempt ${attempts} failed:`, err);
276
- await new Promise((resolve) => setTimeout(resolve, this.redisReconnectDelay));
277
- }
278
- }
279
- throw new Error(`WebSockets: ${clientName} failed to connect after ${this.maxRedisReconnectAttempts} attempts`);
280
- }
281
- /**
282
- * Handle Redis errors by attempting to reconnect the client.
283
- */
284
- async handleRedisError(client) {
285
- try {
286
- client.disconnect();
287
- }
288
- catch (err) {
289
- console.error("WebSockets: Error disconnecting Redis client during error handling:", err);
290
- }
291
- // Attempt to reconnect
292
- try {
293
- await this.connectRedisClient(client, "Redis Client");
294
- }
295
- catch (err) {
296
- console.error("WebSockets: Failed to reconnect Redis client:", err);
297
- }
298
- }
299
49
  /**
300
50
  * Ping all clients every 30 seconds. If a client does not respond with 'pong',
301
51
  * we assume it's a stale connection and terminate it.
@@ -324,23 +74,4 @@ export class WebSocketsManager {
324
74
  }
325
75
  });
326
76
  }
327
- /**
328
- * Gracefully shut down the Redis pub/sub clients.
329
- */
330
- async shutdownPubSub() {
331
- try {
332
- console.log("WebSockets: Shutting down Redis pub/sub");
333
- if (this.pub) {
334
- await this.pub.disconnect();
335
- console.log("WebSockets: Publisher disconnected gracefully.");
336
- }
337
- if (this.sub) {
338
- await this.sub.disconnect();
339
- console.log("WebSockets: Subscriber disconnected gracefully.");
340
- }
341
- }
342
- catch (err) {
343
- console.error("WebSockets: Error during Redis pub/sub shutdown:", err);
344
- }
345
- }
346
77
  }