@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.
- package/package.json +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.
|
|
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.
|
|
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
|
|
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
|
|
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}
|
|
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
|
}
|