@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/.github/workflows/automated.yml +26 -39
- package/CHANGELOG.md +40 -0
- package/package.json +8 -16
- package/release.config.js +13 -5
- package/src/index.js +840 -638
- package/src/mesh.js +0 -71
package/src/index.js
CHANGED
|
@@ -1,640 +1,842 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
-
|
|
842
|
+
export default SocketServer;
|