@cocreate/socket-server 1.31.0 → 1.33.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/src/index.js CHANGED
@@ -1,640 +1,842 @@
1
- const WebSocket = require("ws");
2
- const { URL } = require("url");
3
- const EventEmitter = require("events").EventEmitter;
4
- const uid = require("@cocreate/uuid");
5
- const config = require("@cocreate/config");
6
-
7
- class SocketServer extends EventEmitter {
8
- constructor(server) {
9
- super();
10
- this.serverId = uid.generate(12);
11
- this.organizations = new Map();
12
- this.clients = new Map();
13
- this.sockets = new Map();
14
- this.users = new Map();
15
-
16
- config({ organization_id: { prompt: "Enter your organization_id: " } });
17
-
18
- this.wss = new WebSocket.Server({ noServer: true });
19
-
20
- this.wss.on("headers", (headers, request) => {
21
- headers.push("Access-Control-Allow-Origin: *");
22
- });
23
-
24
- this.wss.on("error", (error) => {
25
- socket.destroy();
26
- });
27
-
28
- server.https.on("upgrade", (request, socket, head) =>
29
- this.upgrade(request, socket, head, "wss")
30
- );
31
- server.http.on("upgrade", (request, socket, head) =>
32
- this.upgrade(request, socket, head, "ws")
33
- );
34
- }
35
-
36
- upgrade(request, socket, head, protocol) {
37
- const self = this;
38
- let organization_id = request.url.split("/");
39
- organization_id = organization_id[organization_id.length - 1];
40
-
41
- this.wss.handleUpgrade(request, socket, head, async function (socket) {
42
- if (organization_id) {
43
- let organization = self.organizations.get(organization_id);
44
- if (organization && organization.status === false) {
45
- let errors = {};
46
- errors.serverOrganization = organization.serverOrganization;
47
- errors.serverStorage = organization.serverStorage;
48
- errors.organizationBalance =
49
- organization.organizationBalance;
50
- errors.error = organization.error;
51
- return socket.send(
52
- JSON.stringify({
53
- method: "Access Denied",
54
- error: errors
55
- })
56
- );
57
- }
58
-
59
- let options = decodeURIComponent(
60
- request.headers["sec-websocket-protocol"]
61
- );
62
- options = JSON.parse(options);
63
-
64
- socket.organization_id = organization_id;
65
- socket.id = options.socketId;
66
- socket.clientId = options.clientId;
67
- socket.pathname = request.url;
68
- socket.origin = request.headers.origin;
69
- socket.host = request.headers.host;
70
-
71
- if (!socket.host && socket.origin && socket.origin !== "null") {
72
- if (socket.origin.includes("://"))
73
- socket.host = new URL(socket.origin).host;
74
- else socket.host = socket.origin;
75
- }
76
-
77
- socket.socketUrl =
78
- protocol + "://" + socket.host + socket.pathname;
79
-
80
- // if (!await server.acme.checkCertificate(socket.host, organization_id))
81
- // return socket.send(JSON.stringify({ method: 'Access Denied', error: 'Host not whitelisted' }))
82
-
83
- if (
84
- !organization ||
85
- (organization && organization.status !== false)
86
- ) {
87
- let data = {
88
- socket,
89
- method: "object.read",
90
- host: socket.host,
91
- array: "message_log",
92
- $filter: {
93
- sort: [{ key: "_id", direction: "desc" }]
94
- },
95
- sync: true,
96
- organization_id
97
- };
98
-
99
- if (options.lastSynced)
100
- data.$filter.query = {
101
- _id: { $gt: options.lastSynced }
102
- };
103
- else data.$filter.limit = 1;
104
-
105
- self.emit("object.read", data);
106
-
107
- if (self.authenticate) {
108
- const { user_id, expires } =
109
- await self.authenticate.decodeToken(
110
- options.token,
111
- organization_id,
112
- options.clientId
113
- );
114
- const userStatus = {
115
- socket,
116
- method: "userStatus",
117
- host: socket.host,
118
- user_id: options.user_id,
119
- clientId: options.clientId,
120
- userStatus: "off",
121
- organization_id
122
- };
123
- if (user_id) {
124
- options.user_id = user_id;
125
- socket.user_id = user_id;
126
- socket.expires = expires;
127
- userStatus.userStatus = "on";
128
- self.emit("notification.user", socket);
129
- }
130
-
131
- self.emit("userStatus", userStatus);
132
-
133
- self.onWebSocket(socket);
134
- } else self.onWebSocket(socket);
135
- }
136
- } else {
137
- socket.send(
138
- JSON.stringify({
139
- method: "Access Denied",
140
- error: "An organization_id is required"
141
- })
142
- );
143
- }
144
- });
145
- }
146
-
147
- onWebSocket(socket) {
148
- const self = this;
149
- this.add(socket);
150
-
151
- socket.on("message", async (message) => {
152
- self.onMessage(socket, message);
153
- });
154
-
155
- socket.on("close", () => {
156
- self.delete(socket);
157
- });
158
-
159
- socket.on("error", () => {
160
- self.delete(socket);
161
- });
162
-
163
- socket.send(
164
- JSON.stringify({ method: "connect", connectedKey: socket.pathname })
165
- );
166
- }
167
-
168
- add(socket) {
169
- let organization_id = socket.organization_id;
170
-
171
- let organization = this.organizations.get(organization_id);
172
- if (!organization) {
173
- organization = {
174
- status: true,
175
- clients: {}
176
- };
177
-
178
- this.organizations.set(organization_id, organization);
179
-
180
- this.emit("object.update", {
181
- method: "object.update",
182
- host: socket.host,
183
- array: "organizations",
184
- object: {
185
- _id: organization_id,
186
- ["$addToSet.activeHost"]: socket.socketUrl // needs socketId
187
- },
188
- organization_id
189
- });
190
-
191
- this.emit("mesh.create", {
192
- url: socket.socketUrl,
193
- organization_id
194
- });
195
- } else clearTimeout(organization.debounce);
196
-
197
- if (!this.clients.has(socket.clientId)) {
198
- this.clients.set(socket.clientId, {});
199
-
200
- if (!organization.clients)
201
- organization.clients = { [socket.clientId]: {} };
202
- else organization.clients[socket.clientId] = {};
203
- }
204
-
205
- this.sockets.set(socket.id, socket);
206
- this.clients.get(socket.clientId)[socket.id] = socket;
207
- if (!organization.clients[socket.clientId])
208
- organization.clients[socket.clientId] = { [socket.id]: socket };
209
- else organization.clients[socket.clientId][socket.id] = socket;
210
-
211
- if (socket.user_id) {
212
- this.emit("userStatus", {
213
- socket,
214
- host: socket.host,
215
- user_id: socket.user_id,
216
- clientId: socket.clientId,
217
- userStatus: "on",
218
- organization_id
219
- });
220
- let user = this.users.get(socket.user_id);
221
-
222
- if (!user) {
223
- this.users.set(socket.user_id, { [socket.id]: socket });
224
- } else {
225
- clearTimeout(user);
226
- user[socket.id] = socket;
227
- }
228
- }
229
- }
230
-
231
- get(data) {
232
- let sockets = [],
233
- clients;
234
- let organization = this.organizations.get(data.organization_id);
235
- if (organization) clients = organization.clients;
236
- else return [];
237
-
238
- if (data.broadcast !== false) {
239
- for (let client of Object.keys(clients)) {
240
- if (data.broadcastSender === false && client === data.clientId)
241
- continue;
242
-
243
- if (data.broadcastClient) {
244
- if (
245
- client === data.clientId &&
246
- clients[client][data.socket.id]
247
- )
248
- sockets.push(clients[client][data.socket.id]);
249
- else sockets.push(Object.values(clients[client])[0]);
250
- } else
251
- sockets.push(...Array.from(Object.values(clients[client])));
252
- }
253
- } else if (data.broadcastSender !== false) {
254
- if (clients[data.clientId]) {
255
- if (clients[data.clientId][data.socket.id])
256
- sockets.push(clients[data.clientId][data.socket.id]);
257
- else sockets.push(Object.values(clients[data.clientId])[0]);
258
- } else sockets.push(data.socket);
259
- }
260
- return sockets;
261
- }
262
-
263
- delete(socket) {
264
- let organization_id = socket.organization_id;
265
- if (this.organizations.has(organization_id)) {
266
- let clients = this.organizations.get(organization_id);
267
- if (clients) clients.clients;
268
- // Check if the client exists
269
- if (clients && clients[socket.clientId]) {
270
- const client = clients[socket.clientId];
271
- delete client[socket.id];
272
-
273
- if (!Object.keys(client).length)
274
- delete clients[socket.clientId];
275
-
276
- // Check if the socket exists in the client's sockets
277
- // const index = client.findIndex(item => item.id === socket.id);
278
- // if (index !== -1) {
279
- // client.splice(index, 1);
280
- // }
281
-
282
- // if (!client.length) {
283
- // delete clients[socket.clientId];
284
- // }
285
-
286
- if (!Object.keys(clients).length) {
287
- this.organizations.delete(socket.organization_id);
288
- this.emit("object.update", {
289
- method: "object.update",
290
- host: socket.host,
291
- array: "organizations",
292
- object: {
293
- _id: organization_id,
294
- ["$pull.activeHost"]: socket.socketUrl
295
- },
296
- organization_id
297
- });
298
-
299
- this.emit("mesh.update", {
300
- url: socket.socketUrl,
301
- organization_id
302
- });
303
- }
304
- }
305
- }
306
-
307
- if (this.clients.has(socket.clientId)) {
308
- const client = this.clients.get(socket.clientId);
309
- delete client[socket.id];
310
-
311
- if (!Object.keys(client).length)
312
- this.clients.delete(socket.clientId);
313
- }
314
-
315
- if (this.clients.size === 0) {
316
- let organization = this.organizations.get(socket.organization_id);
317
- let debounceTimer;
318
- if (organization) debounceTimer = organization.debounce;
319
-
320
- clearTimeout(debounceTimer);
321
- debounceTimer = setTimeout(() => {
322
- this.organizations.delete(socket.organization_id);
323
- }, 10000);
324
-
325
- if (!organization)
326
- this.organizations.set(socket.organization_id, {
327
- debounce: debounceTimer
328
- });
329
- else organization.debounce = debounceTimer;
330
-
331
- this.sockets.delete(socket.id);
332
-
333
- if (socket.user_id) {
334
- let sockets = this.users.get(socket.user_id);
335
- if (sockets) {
336
- delete sockets[socket.id];
337
- if (!Object.keys(sockets).length) {
338
- let userDebounceTimer = sockets;
339
-
340
- clearTimeout(userDebounceTimer);
341
- userDebounceTimer = setTimeout(() => {
342
- this.users.delete(socket.user_id);
343
- this.emit("userStatus", {
344
- socket,
345
- user_id: socket.user_id,
346
- host: socket.host,
347
- clientId: socket.clientId,
348
- userStatus: "off",
349
- organization_id
350
- });
351
- }, 10000);
352
-
353
- this.users.set(socket.user_id, userDebounceTimer);
354
- }
355
- }
356
- }
357
- }
358
- }
359
-
360
- async onMessage(socket, message) {
361
- try {
362
- this.emit("setBandwidth", {
363
- type: "in",
364
- data: message,
365
- organization_id: socket.organization_id
366
- });
367
-
368
- let data = JSON.parse(message);
369
- if (data.method) this.Message(socket, data);
370
- else {
371
- data.error = "method is required";
372
- return socket.send(JSON.stringify(data));
373
- }
374
- } catch (e) {
375
- console.log(e);
376
- }
377
- }
378
-
379
- async Message(socket, data) {
380
- try {
381
- const organization_id = socket.organization_id;
382
-
383
- const organization = this.organizations.get(organization_id);
384
- if (organization && organization.organizationBalance == false) {
385
- data.organizationBalance = false;
386
- data.error = organization.error;
387
- return socket.send(JSON.stringify(data));
388
- }
389
-
390
- if (
391
- data.method === "region.added" ||
392
- data.method === "region.removed"
393
- )
394
- console.log("data.method: ", data.method);
395
-
396
- if (
397
- socket.user_id &&
398
- socket.expires &&
399
- new Date(new Date().toISOString()).getTime() >= socket.expires
400
- ) {
401
- // data.error = 'Token expired'
402
- // socket.send(JSON.stringify(data))
403
- await this.send({
404
- socket,
405
- method: "updateUserStatus",
406
- user_id: socket.user_id,
407
- host: socket.host,
408
- clientId: data.clientId,
409
- userStatus: "off",
410
- socketId: data.socketId,
411
- organization_id
412
- });
413
- socket.user_id = socket.expires = null;
414
- return;
415
- }
416
-
417
- if (this.authorize) {
418
- if (!this.sockets.has(socket.id)) {
419
- if (
420
- organization &&
421
- organization.organizationBalance == false
422
- ) {
423
- data.organizationBalance = false;
424
- data.error = organization.error;
425
- return socket.send(JSON.stringify(data));
426
- }
427
- }
428
-
429
- data.socket = socket;
430
- data.host = socket.host;
431
-
432
- const authorized = await this.authorize.check(
433
- data,
434
- socket.user_id
435
- );
436
- let errors = {};
437
- if (authorized === false) {
438
- delete data.socket;
439
- data.error = "authorization failed";
440
- return socket.send(JSON.stringify(data));
441
- } else if (authorized.serverOrganization === false) {
442
- organization.status = errors.status = false;
443
- organization.serverOrganization = false;
444
- organization.error = authorized.error;
445
- } else if (authorized.serverStorage === false) {
446
- data.database = process.env.organization_id;
447
- data.organization_id = process.env.organization_id;
448
-
449
- const authorized2 = await this.authorize.check(
450
- data,
451
- req,
452
- socket.user_id
453
- );
454
- if (!authorized2 || authorized2.error) {
455
- organization.status = errors.status = false;
456
- organization.error = errors.error = authorized.error;
457
- if (authorized2.serverOrganization === false) {
458
- organization.serverOrganization = false;
459
- }
460
- if (authorized2.serverStorage === false) {
461
- organization.serverStorage = false;
462
- }
463
- }
464
- } else if (authorized.error) {
465
- organization.status = false;
466
- organization.error = authorized.error;
467
- }
468
-
469
- if (organization && organization.status === false) {
470
- let errors = {};
471
- data.serverOrganization = organization.serverOrganization;
472
- data.serverStorage = organization.serverStorage;
473
- data.organizationBalance = organization.organizationBalance;
474
- data.error = organization.error;
475
- delete data.socket;
476
- return socket.send(JSON.stringify(data));
477
- }
478
-
479
- // dburl is true and db does not have 'keys' array
480
- // action: syncCollection data{array: 'keys', object[]}
481
- // actions: add keys as once keys are added admin user can do anything
482
-
483
- // }
484
-
485
- if (authorized.authorized) data = authorized.authorized;
486
-
487
- if (data.endpoint) {
488
- this.emit("endpoint", data);
489
- } else {
490
- // TODO: handle non module cases where send is required from messages
491
- let moduleName = data.method.split(".")[0];
492
- if (
493
- [
494
- "storage",
495
- "database",
496
- "array",
497
- "index",
498
- "object"
499
- ].includes(moduleName)
500
- ) {
501
- this.emit(data.method, data);
502
- } else if (
503
- data.method === "crdt" ||
504
- data.method === "cursor"
505
- ) {
506
- this.send(data);
507
- } else {
508
- this.emit(moduleName, data);
509
- }
510
- }
511
- }
512
- } catch (e) {
513
- console.log(e);
514
- }
515
- }
516
-
517
- async send(data) {
518
- // const socket = this.sockets.get(data.socketId)
519
- delete data.wsManager;
520
- const socket = data.socket;
521
-
522
- if (data.sync) {
523
- if (!data.object || !data.object.length) return;
524
-
525
- for (let i = 0; i < data.object.length; i++) {
526
- if (data.object[i].data) {
527
- data.object[i].data._id = data.object[i]._id;
528
- data.object[i].data.sync = true;
529
-
530
- const authorized = await this.authorize.check(
531
- data.object[i].data,
532
- socket.user_id
533
- );
534
- if (authorized && authorized.authorized)
535
- socket.send(JSON.stringify(authorized.authorized));
536
- else socket.send(JSON.stringify(data.object[i].data));
537
- } else console.log("server sync missing data");
538
- }
539
- } else {
540
- const sent = [];
541
-
542
- //TODO: get socket using clientId if socket has been deleted from data
543
- const authorized = await this.authorize.check(data, socket.user_id);
544
- if (authorized && authorized.authorized)
545
- data = authorized.authorized;
546
-
547
- if (
548
- data.log !== false &&
549
- data.log !== "false" &&
550
- !data.method.endsWith(".read") &&
551
- data.method !== "updateUserStatus" &&
552
- data.method !== "userStatus" &&
553
- data.method !== "signIn" &&
554
- data.method !== "signUp"
555
- ) {
556
- // TODO: store logged messages more efficently by combing objects wherever possible
557
- let moduleName = data.method.split(".");
558
- if (
559
- [
560
- "storage",
561
- "database",
562
- "array",
563
- "index",
564
- "object"
565
- ].includes(moduleName[0]) &&
566
- ["delete"].includes(moduleName[1])
567
- ) {
568
- let object = { url: socket.socketUrl, data: { ...data } };
569
- delete object.data.socket;
570
-
571
- // object.data.socket = { id: object.data.socket.id }
572
- this.emit("object.create", {
573
- method: "object.create",
574
- host: data.host,
575
- array: "message_log",
576
- object,
577
- organization_id: data.organization_id
578
- });
579
- }
580
- }
581
-
582
- let sockets = this.get(data);
583
-
584
- // delete data.socket
585
-
586
- for (let i = 0; i < sockets.length; i++) {
587
- if (!sockets[i]) continue;
588
-
589
- const authorized = await this.authorize.check(
590
- data,
591
- sockets[i].user_id
592
- );
593
- let Data;
594
- if (authorized && authorized.authorized) {
595
- Data = { ...authorized.authorized };
596
- } else {
597
- Data = { ...data };
598
- }
599
-
600
- // TODO: the following code can cause issues in client and improved approach is to check if user has permission and send or dont send
601
- if (
602
- Data.$filter &&
603
- Data.$filter.query &&
604
- Data.$filter.query._id &&
605
- Data.$filter.query._id.$eq === "$user_id"
606
- )
607
- delete Data.$filter.query._id;
608
-
609
- delete Data.socket;
610
- sockets[i].send(JSON.stringify(Data));
611
-
612
- sent.push(socket.clientId);
613
- this.emit("setBandwidth", {
614
- type: "out",
615
- data: Data,
616
- organization_id: socket.organization_id
617
- });
618
- }
619
-
620
- // TODO: sent is an array of clientId's so that notification can send to subscribed clients that are not currently connected
621
- this.emit("notification", { sent });
622
- }
623
- }
624
-
625
- getConfigFromUrl(pathname) {
626
- const path = pathname.split("/");
627
- if (path[2]) {
628
- path[2] = decodeURIComponent(path[2]);
629
- path[2] = JSON.parse(path[2]) || {};
630
- }
631
-
632
- const config = {
633
- organization_id: path[1],
634
- ...path[2]
635
- };
636
- return config;
637
- }
1
+ /********************************************************************************
2
+ * Copyright (C) 2023 CoCreate and Contributors.
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU Affero General Public License as published
6
+ * by the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU Affero General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ ********************************************************************************/
17
+
18
+ // Commercial Licensing Information:
19
+ // For commercial use of this software without the copyleft provisions of the AGPLv3,
20
+ // you must obtain a commercial license from CoCreate LLC.
21
+ // For details, visit <https://cocreate.app/licenses/> or contact us at sales@cocreate.app.
22
+
23
+ import { EventEmitter } from 'node:events';
24
+ import ws from 'ws';
25
+
26
+ // Robustly fallback across all versions/build environments of ws (CJS vs ESM wrappers)
27
+ const WebSocketServer = ws.Server || ws.WebSocketServer;
28
+
29
+ import { URL } from 'node:url';
30
+ import uid from '@cocreate/uuid';
31
+ import config from '@cocreate/config';
32
+
33
+ export class SocketServer extends EventEmitter {
34
+ constructor(server) {
35
+ super();
36
+
37
+ // Cache reference to primary server process manager - Must be defined
38
+ this.server = server;
39
+ this.serverId = uid.generate(12);
40
+
41
+ // Dynamic reference tracking context bound to our cluster process topology
42
+ this.workerId = server.workerId;
43
+
44
+ // Use ES6 Maps for O(1) lookups and zero-allocation updates
45
+ this.organizations = new Map();
46
+ this.clients = new Map();
47
+ this.sockets = new Map();
48
+ this.users = new Map();
49
+
50
+ // Extract isolated helper properties directly from the server instance
51
+ this.authenticate = server.authenticate;
52
+ this.authorize = server.authorize;
53
+
54
+ config({ organization_id: { prompt: "Enter your organization_id: " } });
55
+
56
+ this.wss = new WebSocketServer({ noServer: true });
57
+
58
+ // We use bound prototype methods instead of inline closures to prevent scope leakage
59
+ this.wss.on("headers", this._handleHeaders.bind(this));
60
+ this.wss.on("error", this._handleWssError.bind(this));
61
+
62
+ if (server.https) {
63
+ server.https.on("upgrade", this._upgradeWss.bind(this));
64
+ }
65
+ if (server.http) {
66
+ server.http.on("upgrade", this._upgradeWs.bind(this));
67
+ }
68
+
69
+ // Hook up process-level events to allow other modules (Telemetry, CoCreateServer) to invoke evictions
70
+ process.on('socket.destroyAll', () => {
71
+ this.destroyAll();
72
+ });
73
+
74
+ process.on('socket.destroyOrgs', (orgIds) => {
75
+ this.destroyOrganizations(orgIds);
76
+ });
77
+
78
+ this.heartbeatInterval = setInterval(() => {
79
+ this.sockets.forEach((socket) => {
80
+ if (socket.isAlive === false) {
81
+ socket.terminate();
82
+ return;
83
+ }
84
+ socket.isAlive = false;
85
+ try {
86
+ if (socket.readyState === ws.OPEN) {
87
+ socket.ping();
88
+ } else {
89
+ socket.terminate();
90
+ }
91
+ } catch (err) {
92
+ console.error("[@cocreate/socket-server] Error pinging socket:", err);
93
+ socket.terminate();
94
+ }
95
+ });
96
+ }, 30000);
97
+ }
98
+
99
+ close() {
100
+ clearInterval(this.heartbeatInterval);
101
+ this.wss.close();
102
+ }
103
+
104
+ _handleHeaders(headers, request) {
105
+ headers.push("Access-Control-Allow-Origin: *");
106
+ }
107
+
108
+ _handleWssError(error) {
109
+ console.error("[@cocreate/socket-server] WebSocketServer error:", error);
110
+ }
111
+
112
+ _upgradeWss(request, socket, head) {
113
+ this.upgrade(request, socket, head, "wss");
114
+ }
115
+
116
+ _upgradeWs(request, socket, head) {
117
+ this.upgrade(request, socket, head, "ws");
118
+ }
119
+
120
+ static async init(server) {
121
+ return new SocketServer(server);
122
+ }
123
+
124
+ upgrade(request, socket, head, protocol) {
125
+ const self = this;
126
+
127
+ const host = request.headers.host || "localhost";
128
+ const parsedUrl = new URL(request.url, `http://${host}`);
129
+
130
+ // .filter(Boolean) removes empty strings, protecting against trailing slashes!
131
+ const paths = parsedUrl.pathname.split("/").filter(Boolean);
132
+ // Grab the FIRST segment as the organization_id (e.g., /org_123/namespace/room -> org_123)
133
+ const organization_id = paths[0];
134
+
135
+ this.wss.handleUpgrade(request, socket, head, async function (socket) {
136
+ if (organization_id) {
137
+ let organization = self.organizations.get(organization_id);
138
+ if (organization && organization.status === false) {
139
+ let errors = {};
140
+ errors.serverOrganization = organization.serverOrganization;
141
+ errors.serverStorage = organization.serverStorage;
142
+ errors.organizationBalance = organization.organizationBalance;
143
+ errors.error = organization.error;
144
+ return self._safeSend(socket, {
145
+ method: "Access Denied",
146
+ error: errors
147
+ });
148
+ }
149
+
150
+ // 1. Extract Query Parameters as the baseline options
151
+ let options = Object.fromEntries(parsedUrl.searchParams.entries());
152
+
153
+ // 2. Safely merge and override with secure protocol headers if they exist
154
+ try {
155
+ const protocolHeader = request.headers["sec-websocket-protocol"];
156
+ if (protocolHeader) {
157
+ const headerOptions = JSON.parse(decodeURIComponent(protocolHeader));
158
+ options = { ...options, ...headerOptions };
159
+ }
160
+ } catch (err) {
161
+ console.error("[@cocreate/socket-server] Error parsing socket options:", err);
162
+ }
163
+
164
+ socket.organization_id = organization_id;
165
+ socket.id = options.socketId;
166
+ socket.clientId = options.clientId;
167
+ socket.pathname = request.url;
168
+ socket.origin = request.headers.origin;
169
+ socket.host = request.headers.host;
170
+
171
+ if (!socket.host && socket.origin && socket.origin !== "null") {
172
+ if (socket.origin.includes("://"))
173
+ socket.host = new URL(socket.origin).host;
174
+ else socket.host = socket.origin;
175
+ }
176
+
177
+ socket.socketUrl =
178
+ protocol + "://" + socket.host + socket.pathname;
179
+
180
+ if (
181
+ !organization ||
182
+ (organization && organization.status !== false)
183
+ ) {
184
+ const authService = self.authenticate;
185
+ if (authService) {
186
+ const { user_id, expires } =
187
+ await authService.decodeToken(
188
+ options.token,
189
+ organization_id,
190
+ options.clientId
191
+ );
192
+ const userStatus = {
193
+ socket,
194
+ method: "userStatus",
195
+ host: socket.host,
196
+ user_id: options.user_id,
197
+ clientId: options.clientId,
198
+ userStatus: "off",
199
+ organization_id
200
+ };
201
+ if (user_id) {
202
+ options.user_id = user_id;
203
+ socket.user_id = user_id;
204
+ socket.expires = expires;
205
+ userStatus.userStatus = "on";
206
+ self.emit("notification.user", socket);
207
+ }
208
+
209
+ self.emit("userStatus", userStatus);
210
+
211
+ self.onWebSocket(socket);
212
+ } else self.onWebSocket(socket);
213
+ }
214
+ } else {
215
+ self._safeSend(socket, {
216
+ method: "Access Denied",
217
+ error: "An organization_id is required"
218
+ });
219
+ }
220
+ });
221
+ }
222
+
223
+ onWebSocket(socket) {
224
+ const self = this;
225
+
226
+ socket.isAlive = true;
227
+ socket.on("pong", () => {
228
+ socket.isAlive = true;
229
+ });
230
+
231
+ this.add(socket);
232
+
233
+ socket.on("message", async (message) => {
234
+ self.onMessage(socket, message);
235
+ });
236
+
237
+ socket.on("close", () => {
238
+ // Guard: If we are intentionally tearing down, bypass redundant delete cycles
239
+ if (socket.bypassDeleteCascade) {
240
+ socket.removeAllListeners();
241
+ return;
242
+ }
243
+ self.delete(socket);
244
+ });
245
+
246
+ socket.on("error", () => {
247
+ if (socket.bypassDeleteCascade) {
248
+ socket.removeAllListeners();
249
+ return;
250
+ }
251
+ self.delete(socket);
252
+ });
253
+
254
+ this._safeSend(socket, { method: "connect", connectedKey: socket.pathname });
255
+ }
256
+
257
+ add(socket) {
258
+ let organization_id = socket.organization_id;
259
+
260
+ let organization = this.organizations.get(organization_id);
261
+ if (!organization) {
262
+ organization = {
263
+ status: true,
264
+ clients: new Map(),
265
+ debounce: null
266
+ };
267
+
268
+ this.organizations.set(organization_id, organization);
269
+
270
+ // Fail-Fast: Directly call send on upstream process. If invalid, crash instantly.
271
+ this.server.send({
272
+ method: 'mesh.registerOrg',
273
+ organization_id,
274
+ workerId: this.workerId
275
+ });
276
+
277
+ this.emit("object.update", {
278
+ method: "object.update",
279
+ host: socket.host,
280
+ array: "organizations",
281
+ object: {
282
+ _id: organization_id,
283
+ ["$addToSet.activeHost"]: socket.socketUrl
284
+ },
285
+ organization_id
286
+ });
287
+
288
+ this.emit("mesh.create", {
289
+ url: socket.socketUrl,
290
+ organization_id
291
+ });
292
+ } else {
293
+ clearTimeout(organization.debounce);
294
+ }
295
+
296
+ if (!this.clients.has(socket.clientId)) {
297
+ this.clients.set(socket.clientId, new Map());
298
+ }
299
+
300
+ if (!organization.clients.has(socket.clientId)) {
301
+ organization.clients.set(socket.clientId, new Map());
302
+ }
303
+
304
+ this.sockets.set(socket.id, socket);
305
+ this.clients.get(socket.clientId).set(socket.id, socket);
306
+ organization.clients.get(socket.clientId).set(socket.id, socket);
307
+
308
+ if (socket.user_id) {
309
+ this.emit("userStatus", {
310
+ socket,
311
+ host: socket.host,
312
+ user_id: socket.user_id,
313
+ clientId: socket.clientId,
314
+ userStatus: "on",
315
+ organization_id
316
+ });
317
+
318
+ let userSession = this.users.get(socket.user_id);
319
+
320
+ if (!userSession) {
321
+ userSession = { sockets: new Map(), debounce: null };
322
+ this.users.set(socket.user_id, userSession);
323
+ } else {
324
+ clearTimeout(userSession.debounce);
325
+ }
326
+ userSession.sockets.set(socket.id, socket);
327
+ }
328
+ }
329
+
330
+ get(data) {
331
+ try {
332
+ const sockets = [];
333
+ const organization = this.organizations.get(data.organization_id);
334
+ if (!organization || !organization.clients) return [];
335
+
336
+ const clientsMap = organization.clients;
337
+
338
+ if (data.broadcast !== false) {
339
+ for (const [clientId, clientSocketsMap] of clientsMap.entries()) {
340
+ if (data.broadcastSender === false && clientId === data.clientId) {
341
+ continue;
342
+ }
343
+
344
+ if (data.broadcastClient) {
345
+ if (clientId === data.clientId) {
346
+ const activeSocket = clientSocketsMap.get(data.socket?.id);
347
+ if (activeSocket) sockets.push(activeSocket);
348
+ } else {
349
+ for (const socketInstance of clientSocketsMap.values()) {
350
+ sockets.push(socketInstance);
351
+ break;
352
+ }
353
+ }
354
+ } else {
355
+ for (const socketInstance of clientSocketsMap.values()) {
356
+ sockets.push(socketInstance);
357
+ }
358
+ }
359
+ }
360
+ } else if (data.broadcastSender !== false) {
361
+ const clientSocketsMap = clientsMap.get(data.clientId);
362
+ if (clientSocketsMap) {
363
+ const activeSocket = clientSocketsMap.get(data.socket?.id);
364
+ if (activeSocket) {
365
+ sockets.push(activeSocket);
366
+ } else {
367
+ for (const socketInstance of clientSocketsMap.values()) {
368
+ sockets.push(socketInstance);
369
+ break;
370
+ }
371
+ }
372
+ } else if (data.socket) {
373
+ sockets.push(data.socket);
374
+ }
375
+ }
376
+ return sockets;
377
+ } catch (error) {
378
+ console.error("[SocketServer] Error in get method:", error);
379
+ return [];
380
+ }
381
+ }
382
+
383
+ delete(socket) {
384
+ const organization_id = socket.organization_id;
385
+ const clientId = socket.clientId;
386
+ const socketId = socket.id;
387
+
388
+ const organization = this.organizations.get(organization_id);
389
+ if (organization && organization.clients) {
390
+ const clientSocketsMap = organization.clients.get(clientId);
391
+
392
+ if (clientSocketsMap) {
393
+ clientSocketsMap.delete(socketId);
394
+
395
+ if (clientSocketsMap.size === 0) {
396
+ organization.clients.delete(clientId);
397
+ }
398
+
399
+ if (organization.clients.size === 0) {
400
+ this.emit("object.update", {
401
+ method: "object.update",
402
+ host: socket.host,
403
+ array: "organizations",
404
+ object: {
405
+ _id: organization_id,
406
+ ["$pull.activeHost"]: socket.socketUrl
407
+ },
408
+ organization_id
409
+ });
410
+
411
+ this.emit("mesh.update", {
412
+ url: socket.socketUrl,
413
+ organization_id
414
+ });
415
+
416
+ // We implement a resilient 60-second grace period (instead of 10 seconds)
417
+ // to gracefully handle page refreshes, cellular/Wi-Fi toggles, or fast disconnect-reconnect tunnels.
418
+ clearTimeout(organization.debounce);
419
+ organization.debounce = setTimeout(() => {
420
+ const currentOrg = this.organizations.get(organization_id);
421
+ if (currentOrg && currentOrg.clients.size === 0) {
422
+ this.organizations.delete(organization_id);
423
+
424
+ // Fail-Fast: Notify Master via upstream process send directly.
425
+ this.server.send({
426
+ method: 'mesh.deregisterOrg',
427
+ organization_id,
428
+ workerId: this.workerId
429
+ });
430
+
431
+ // Dispatched locally: Cascades a teardown across databases, schedulers, and metrics
432
+ process.emit('orgDeleted', organization_id);
433
+ }
434
+ }, 60000); // 60 seconds resilience guard
435
+ }
436
+ }
437
+ }
438
+
439
+ const globalClientMap = this.clients.get(clientId);
440
+ if (globalClientMap) {
441
+ globalClientMap.delete(socketId);
442
+ if (globalClientMap.size === 0) {
443
+ this.clients.delete(clientId);
444
+ }
445
+ }
446
+
447
+ this.sockets.delete(socketId);
448
+
449
+ if (socket.user_id) {
450
+ const userSession = this.users.get(socket.user_id);
451
+ if (userSession) {
452
+ userSession.sockets.delete(socketId);
453
+
454
+ if (userSession.sockets.size === 0) {
455
+ clearTimeout(userSession.debounce);
456
+ userSession.debounce = setTimeout(() => {
457
+ this.users.delete(socket.user_id);
458
+ this.emit("userStatus", {
459
+ socket,
460
+ user_id: socket.user_id,
461
+ host: socket.host,
462
+ clientId: socket.clientId,
463
+ userStatus: "off",
464
+ organization_id
465
+ });
466
+ }, 10000);
467
+ }
468
+ }
469
+ }
470
+
471
+ // Explicit cleanup of listeners to protect V8 GC memory allocation bounds
472
+ socket.removeAllListeners();
473
+ }
474
+
475
+ /**
476
+ * Instantly evicts one or more organizations from this worker node,
477
+ * closing all active sockets, freeing maps, and emitting the unified orgDeleted event.
478
+ * Bypasses the 60-second grace period and avoids redundant master mesh IPC packets.
479
+ * @param {string|string[]} orgIds - Array of organization IDs to shed
480
+ */
481
+ destroyOrganizations(orgIds) {
482
+ const ids = Array.isArray(orgIds) ? orgIds : [orgIds];
483
+
484
+ for (const orgId of ids) {
485
+ const org = this.organizations.get(orgId);
486
+ if (!org) continue;
487
+
488
+ console.log(`[@cocreate/socket-server] Evicting organization instantly (Shedding/Drain): ${orgId}`);
489
+
490
+ // 1. Clear any active inactivity timeouts to prevent double-processing
491
+ if (org.debounce) {
492
+ clearTimeout(org.debounce);
493
+ }
494
+
495
+ // 2. Identify all sockets associated with this organization
496
+ const orgSockets = [];
497
+ if (org.clients) {
498
+ for (const clientSocketsMap of org.clients.values()) {
499
+ for (const socket of clientSocketsMap.values()) {
500
+ orgSockets.push(socket);
501
+ }
502
+ }
503
+ }
504
+
505
+ // 3. Terminate all client connections cleanly
506
+ for (const socket of orgSockets) {
507
+ try {
508
+ // Send a clean departure frame if open
509
+ this._safeSend(socket, { method: "disconnect", reason: "Shedding / Draining Process" });
510
+ socket.bypassDeleteCascade = true; // Mark socket so its 'close' event ignores redundant cascades
511
+ socket.terminate(); // Force terminate the underlying TCP/TLS connection
512
+ } catch (err) {
513
+ console.error(`[@cocreate/socket-server] Error closing socket during eviction of Org ${orgId}:`, err);
514
+ }
515
+
516
+ // Clear from global tracking maps
517
+ this.sockets.delete(socket.id);
518
+ const globalClientMap = this.clients.get(socket.clientId);
519
+ if (globalClientMap) {
520
+ globalClientMap.delete(socket.id);
521
+ if (globalClientMap.size === 0) {
522
+ this.clients.delete(socket.clientId);
523
+ }
524
+ }
525
+
526
+ // Clean up user sessions
527
+ if (socket.user_id) {
528
+ const userSession = this.users.get(socket.user_id);
529
+ if (userSession) {
530
+ userSession.sockets.delete(socket.id);
531
+ if (userSession.sockets.size === 0) {
532
+ clearTimeout(userSession.debounce);
533
+ this.users.delete(socket.user_id);
534
+ }
535
+ }
536
+ }
537
+ }
538
+
539
+ // 4. Remove the organization completely from the registry
540
+ this.organizations.delete(orgId);
541
+
542
+ // 5. Emit the global process event cascade (MongoDB pools close, Schedulers cancel, Usage flushes)
543
+ process.emit('orgDeleted', orgId);
544
+ }
545
+ }
546
+
547
+ /**
548
+ * Purges all organizations and active connections on this worker instantly.
549
+ */
550
+ destroyAll() {
551
+ console.log(`[@cocreate/socket-server] Instantly purging all organization registries...`);
552
+ const allOrgIds = Array.from(this.organizations.keys());
553
+ this.destroyOrganizations(allOrgIds);
554
+ }
555
+
556
+ async onMessage(socket, message) {
557
+ try {
558
+ this.emit("usage", {
559
+ type: "ingress",
560
+ data: message,
561
+ organization_id: socket.organization_id
562
+ });
563
+
564
+ let data;
565
+ try {
566
+ data = JSON.parse(message);
567
+ } catch (err) {
568
+ return this._safeSend(socket, { error: "Invalid JSON payload" });
569
+ }
570
+
571
+ if (data.method) this.Message(socket, data);
572
+ else {
573
+ data.error = "method is required";
574
+ return this._safeSend(socket, data);
575
+ }
576
+ } catch (e) {
577
+ console.log(e);
578
+ }
579
+ }
580
+
581
+ async Message(socket, data) {
582
+ try {
583
+ const organization_id = socket.organization_id;
584
+
585
+ const organization = this.organizations.get(organization_id);
586
+ if (organization && organization.organizationBalance == false) {
587
+ data.organizationBalance = false;
588
+ data.error = organization.error;
589
+ return this._safeSend(socket, data);
590
+ }
591
+
592
+ if (
593
+ data.method === "region.added" ||
594
+ data.method === "region.removed"
595
+ )
596
+ console.log("data.method: ", data.method);
597
+
598
+ if (
599
+ socket.user_id &&
600
+ socket.expires &&
601
+ new Date(new Date().toISOString()).getTime() >= socket.expires
602
+ ) {
603
+ await this.send({
604
+ socket,
605
+ method: "updateUserStatus",
606
+ user_id: socket.user_id,
607
+ host: socket.host,
608
+ clientId: data.clientId,
609
+ userStatus: "off",
610
+ socketId: data.socketId,
611
+ organization_id
612
+ });
613
+ socket.user_id = socket.expires = null;
614
+ return;
615
+ }
616
+
617
+ const authorizeService = this.authorize;
618
+ if (authorizeService) {
619
+ if (!this.sockets.has(socket.id)) {
620
+ if (
621
+ organization &&
622
+ organization.organizationBalance == false
623
+ ) {
624
+ data.organizationBalance = false;
625
+ data.error = organization.error;
626
+ return this._safeSend(socket, data);
627
+ }
628
+ }
629
+
630
+ data.socket = socket;
631
+ data.host = socket.host;
632
+
633
+ const authorized = await authorizeService.check(
634
+ data,
635
+ socket.user_id
636
+ );
637
+ let errors = {};
638
+ if (authorized === false) {
639
+ delete data.socket;
640
+ data.error = "authorization failed";
641
+ return this._safeSend(socket, data);
642
+ } else if (authorized.serverOrganization === false) {
643
+ organization.status = errors.status = false;
644
+ organization.serverOrganization = false;
645
+ organization.error = authorized.error;
646
+ } else if (authorized.serverStorage === false) {
647
+ data.database = process.env.organization_id;
648
+ data.organization_id = process.env.organization_id;
649
+
650
+ const authorized2 = await authorizeService.check(
651
+ data,
652
+ socket.user_id
653
+ );
654
+ if (!authorized2 || authorized2.error) {
655
+ organization.status = errors.status = false;
656
+ organization.error = errors.error = authorized.error;
657
+ if (authorized2.serverOrganization === false) {
658
+ organization.serverOrganization = false;
659
+ }
660
+ if (authorized2.serverStorage === false) {
661
+ organization.serverStorage = false;
662
+ }
663
+ }
664
+ } else if (authorized.error) {
665
+ organization.status = false;
666
+ organization.error = authorized.error;
667
+ }
668
+
669
+ if (organization && organization.status === false) {
670
+ data.serverOrganization = organization.serverOrganization;
671
+ data.serverStorage = organization.serverStorage;
672
+ data.organizationBalance = organization.organizationBalance;
673
+ data.error = organization.error;
674
+ delete data.socket;
675
+ return this._safeSend(socket, data);
676
+ }
677
+
678
+ if (authorized.authorized) data = authorized.authorized;
679
+ } // End of Authorization Block
680
+
681
+ // --- ROUTE TO ENDPOINTS / CRUD OPERATIONS ---
682
+ if (data.endpoint) {
683
+ this.emit("endpoint", data);
684
+ } else if (data.method === "crdt" || data.method === "cursor") {
685
+ this.send(data);
686
+ } else {
687
+ let moduleName = data.method.split(".")[0];
688
+ if (["storage", "database", "array", "index", "object"].includes(moduleName)) {
689
+ this.emit(data.method, data);
690
+ } else {
691
+ this.emit(moduleName, data);
692
+ }
693
+ }
694
+
695
+ } catch (e) {
696
+ console.log(e);
697
+ }
698
+ }
699
+
700
+ async send(data) {
701
+ const socket = data.socket;
702
+ delete data.wsManager;
703
+
704
+ const authorizeService = this.authorize;
705
+
706
+ const sent = [];
707
+
708
+ if (authorizeService && socket) {
709
+ const authorized = await authorizeService.check(data, socket.user_id);
710
+ if (authorized && authorized.authorized)
711
+ data = authorized.authorized;
712
+ }
713
+
714
+ // --- POST-EXECUTION METADATA LOGGING ---
715
+ if (
716
+ !data.error && // Only log successful operations
717
+ data.log !== false &&
718
+ data.log !== "false" &&
719
+ !data.method.endsWith(".read") &&
720
+ data.method !== "updateUserStatus" &&
721
+ data.method !== "userStatus" &&
722
+ data.method !== "crdt" &&
723
+ data.method !== "cursor"
724
+ ) {
725
+ let logPayload = {
726
+ method: data.method,
727
+ organization_id: data.organization_id,
728
+ user_id: (socket && socket.user_id) || data.user_id,
729
+ timestamp: new Date().toISOString()
730
+ };
731
+
732
+ if (data.endpoint) logPayload.endpoint = data.endpoint;
733
+ if (data.array) logPayload.array = data.array;
734
+
735
+ if (data.object) {
736
+ if (Array.isArray(data.object)) {
737
+ const skeletonArray = [];
738
+ for (const item of data.object) {
739
+ if (item && item._id) skeletonArray.push({ _id: item._id });
740
+ }
741
+ if (skeletonArray.length > 0) logPayload.object = skeletonArray;
742
+ } else if (data.object._id) {
743
+ logPayload.object = { _id: data.object._id };
744
+ }
745
+ }
746
+
747
+ this.emit("object.create", {
748
+ method: "object.create",
749
+ host: data.host || (socket && socket.host),
750
+ array: "message_log",
751
+ object: logPayload,
752
+ organization_id: data.organization_id
753
+ });
754
+ }
755
+
756
+ let sockets = this.get(data);
757
+ let cleanData = { ...data };
758
+ delete cleanData.socket;
759
+
760
+ if (
761
+ cleanData.$filter &&
762
+ cleanData.$filter.query &&
763
+ cleanData.$filter.query._id &&
764
+ cleanData.$filter.query._id.$eq === "$user_id"
765
+ ) {
766
+ delete cleanData.$filter.query._id;
767
+ }
768
+
769
+ const defaultPayloadStr = JSON.stringify(cleanData);
770
+
771
+ for (const recipientSocket of sockets) {
772
+ if (!recipientSocket) continue;
773
+
774
+ let payloadStr = defaultPayloadStr;
775
+ let finalDataObj = cleanData;
776
+
777
+ if (authorizeService) {
778
+ const authResult = await authorizeService.check(
779
+ cleanData,
780
+ recipientSocket.user_id
781
+ );
782
+
783
+ if (authResult === false) {
784
+ continue;
785
+ }
786
+
787
+ if (authResult !== true && authResult && authResult.authorized) {
788
+ finalDataObj = { ...authResult.authorized };
789
+ delete finalDataObj.socket;
790
+
791
+ if (
792
+ finalDataObj.$filter &&
793
+ finalDataObj.$filter.query &&
794
+ finalDataObj.$filter.query._id &&
795
+ finalDataObj.$filter.query._id.$eq === "$user_id"
796
+ ) {
797
+ delete finalDataObj.$filter.query._id;
798
+ }
799
+
800
+ payloadStr = JSON.stringify(finalDataObj);
801
+ }
802
+ }
803
+
804
+ this._safeSend(recipientSocket, payloadStr);
805
+
806
+ if (recipientSocket.clientId) {
807
+ sent.push(recipientSocket.clientId);
808
+ }
809
+
810
+ this.emit("usage", {
811
+ type: "egress",
812
+ data: finalDataObj,
813
+ organization_id: recipientSocket.organization_id
814
+ });
815
+ }
816
+
817
+ // Send data to socketMesh
818
+ if (cleanData.organization_id) {
819
+ this.server.send({
820
+ method: 'mesh.orgData',
821
+ organization_id: cleanData.organization_id,
822
+ data: cleanData
823
+ });
824
+ }
825
+
826
+ this.emit("notification", { sent });
827
+ }
828
+
829
+ // Helper to send payloads safely on valid connections without throwing frame failures
830
+ _safeSend(socket, payload) {
831
+ try {
832
+ if (socket && socket.readyState === ws.OPEN) {
833
+ const message = typeof payload === "string" ? payload : JSON.stringify(payload);
834
+ socket.send(message);
835
+ }
836
+ } catch (err) {
837
+ console.error("[@cocreate/socket-server] Error safely writing to socket:", err);
838
+ }
839
+ }
638
840
  }
639
841
 
640
- module.exports = SocketServer;
842
+ export default SocketServer;