sonic-ws 1.2.2 → 1.3.0-min

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.
@@ -47,14 +47,22 @@ var __importStar = (this && this.__importStar) || (function () {
47
47
  return result;
48
48
  };
49
49
  })();
50
+ var __importDefault = (this && this.__importDefault) || function (mod) {
51
+ return (mod && mod.__esModule) ? mod : { "default": mod };
52
+ };
50
53
  Object.defineProperty(exports, "__esModule", { value: true });
51
54
  exports.SonicWSServer = void 0;
52
55
  const WS = __importStar(require("ws"));
56
+ const http_1 = __importDefault(require("http"));
57
+ const open_1 = __importDefault(require("open"));
53
58
  const SonicWSConnection_1 = require("./SonicWSConnection");
54
59
  const PacketHolder_1 = require("../util/packets/PacketHolder");
55
60
  const CompressionUtil_1 = require("../util/packets/CompressionUtil");
56
61
  const version_1 = require("../../version");
57
62
  const PacketUtils_1 = require("../util/packets/PacketUtils");
63
+ const PacketType_1 = require("../packets/PacketType");
64
+ const HashUtil_1 = require("../util/packets/HashUtil");
65
+ const Connection_1 = require("../Connection");
58
66
  class SonicWSServer {
59
67
  wss;
60
68
  availableIds = [];
@@ -69,6 +77,7 @@ class SonicWSServer {
69
77
  handshakePacket = null;
70
78
  tags = new Map();
71
79
  tagsInv = new Map();
80
+ serverwideSendQueue = [false, [], undefined];
72
81
  /**
73
82
  * Initializes and hosts a websocket with sonic protocol
74
83
  * Rate limits can be set with wss.setClientRateLimit(x) and wss.setServerRateLimit(x); it is defaulted at 500/second per both
@@ -83,15 +92,22 @@ class SonicWSServer {
83
92
  const s_serverPackets = this.serverPackets.serialize();
84
93
  const serverData = [...version_1.SERVER_SUFFIX_NUMS, version_1.VERSION];
85
94
  const keyData = [...(0, CompressionUtil_1.convertVarInt)(s_clientPackets.length), ...s_clientPackets, ...s_serverPackets];
95
+ (0, HashUtil_1.setHashFunc)(settings.sonicServerSettings?.bit64Hash ?? true);
86
96
  this.wss.on('connection', async (socket) => {
87
97
  const sonicConnection = new SonicWSConnection_1.SonicWSConnection(socket, this, this.generateSocketID(), this.handshakePacket, this.clientRateLimit, this.serverRateLimit);
98
+ if (await this.callMiddleware("onClientConnect", sonicConnection)) {
99
+ sonicConnection.close(Connection_1.CloseCodes.MIDDLEWARE, "Connection blocked by middleware.");
100
+ this.callMiddleware("onClientDisconnect", sonicConnection, Connection_1.CloseCodes.MIDDLEWARE, Buffer.from("Connection blocked by middleware."));
101
+ this.availableIds.push(sonicConnection.id);
102
+ return;
103
+ }
88
104
  // send tags to the client so it doesn't have to hard code them in
89
105
  const data = new Uint8Array([...(0, CompressionUtil_1.convertVarInt)(sonicConnection.id), ...keyData]);
90
106
  socket.send([...serverData, ...await (0, CompressionUtil_1.compressGzip)(data)]);
91
107
  this.connections.push(sonicConnection);
92
108
  this.connectionMap[sonicConnection.id] = sonicConnection;
93
109
  this.connectListeners.forEach(l => l(sonicConnection));
94
- socket.on('close', () => {
110
+ socket.on('close', (code, reason) => {
95
111
  this.connections.splice(this.connections.indexOf(sonicConnection), 1);
96
112
  delete this.connectionMap[sonicConnection.id];
97
113
  this.availableIds.push(sonicConnection.id);
@@ -100,9 +116,10 @@ class SonicWSServer {
100
116
  this.tagsInv.get(tag)?.delete(sonicConnection);
101
117
  this.tags.delete(sonicConnection);
102
118
  }
119
+ this.callMiddleware("onClientDisconnect", sonicConnection, code, reason);
103
120
  });
104
121
  });
105
- if (settings.checkForUpdates ?? true) {
122
+ if (settings.sonicServerSettings?.checkForUpdates ?? true) {
106
123
  fetch('https://raw.githubusercontent.com/liwybloc/sonic-ws/refs/heads/main/release/version')
107
124
  .then((res) => res.text())
108
125
  .then((ver) => {
@@ -116,6 +133,35 @@ class SonicWSServer {
116
133
  });
117
134
  }
118
135
  }
136
+ middlewares = [];
137
+ addMiddleware(middleware) {
138
+ this.middlewares.push(middleware);
139
+ const m = middleware;
140
+ try {
141
+ if (typeof m.init === 'function')
142
+ m.init(this);
143
+ }
144
+ catch (e) {
145
+ console.warn('Middleware init threw an error:', e);
146
+ }
147
+ }
148
+ async callMiddleware(method, ...values) {
149
+ let cancelled = false;
150
+ for (const middleware of this.middlewares) {
151
+ const fn = middleware[method];
152
+ if (!fn)
153
+ continue;
154
+ try {
155
+ if (await fn(...values)) {
156
+ cancelled = true;
157
+ }
158
+ }
159
+ catch (e) {
160
+ console.warn(`Middleware ${String(method)} threw an error:`, e);
161
+ }
162
+ }
163
+ return cancelled;
164
+ }
119
165
  generateSocketID() {
120
166
  if (this.availableIds.length == 0)
121
167
  this.availableIds.push(this.lastId + 1);
@@ -210,35 +256,36 @@ class SonicWSServer {
210
256
  shutdown(callback) {
211
257
  this.wss.close(callback);
212
258
  }
213
- /**
214
- * Broadcasts a packet to tagged users; this is fast as it is a record rather than looping and filtering
215
- * @param tag The tag to send packets to
216
- * @param packetTag Packet tag to send
217
- * @param values Values to send
218
- */
219
- async broadcastTagged(tag, packetTag, ...values) {
220
- if (!this.tagsInv.has(tag))
259
+ async broadcastInternal(packetTag, target, values) {
260
+ let recipients;
261
+ if (target.type === "all") {
262
+ recipients = this.connections;
263
+ }
264
+ else if (target.type === "tagged") {
265
+ if (!this.tagsInv.has(target.tag))
266
+ return;
267
+ recipients = Array.from(this.tagsInv.get(target.tag));
268
+ }
269
+ else {
270
+ recipients = this.connections.filter(target.filter);
271
+ }
272
+ if (await this.callMiddleware("onPacketBroadcast_pre", packetTag, { recipients, ...target }, values))
273
+ return;
274
+ if (recipients.length === 0)
221
275
  return;
222
- const data = await (0, PacketUtils_1.processPacket)(this.serverPackets, packetTag, values, -1);
223
- this.tagsInv.get(tag).forEach(conn => conn.send_processed(...data));
276
+ const [code, data, packet] = await (0, PacketUtils_1.processPacket)(this.serverPackets, packetTag, values, this.serverwideSendQueue, -1);
277
+ if (await this.callMiddleware("onPacketBroadcast_post", packetTag, { recipients, ...target }, data, data.length))
278
+ return;
279
+ recipients.forEach(conn => conn.send_processed(code, data, packet));
280
+ }
281
+ async broadcastTagged(tag, packetTag, ...values) {
282
+ await this.broadcastInternal(packetTag, { type: "tagged", tag }, values);
224
283
  }
225
- /**
226
- * Broadcasts a packet to all users connected, but with a filter
227
- * @param tag The tag to send
228
- * @param filter The filter for who to send to
229
- * @param values The values to send
230
- */
231
284
  async broadcastFiltered(tag, filter, ...values) {
232
- const data = await (0, PacketUtils_1.processPacket)(this.serverPackets, tag, values, -1);
233
- this.connections.filter(filter).forEach(conn => conn.send_processed(...data));
285
+ await this.broadcastInternal(tag, { type: "filter", filter }, values);
234
286
  }
235
- /**
236
- * Broadcasts a packet to all users connected
237
- * @param tag The tag to send
238
- * @param values The values to send
239
- */
240
- broadcast(tag, ...values) {
241
- this.broadcastFiltered(tag, () => true, ...values);
287
+ async broadcast(tag, ...values) {
288
+ await this.broadcastInternal(tag, { type: "all" }, values);
242
289
  }
243
290
  /**
244
291
  * @returns All users connected to the socket
@@ -277,5 +324,587 @@ class SonicWSServer {
277
324
  this.tags.get(socket).add(tag);
278
325
  this.tagsInv.get(tag).add(socket);
279
326
  }
327
+ debugServer = null;
328
+ /**
329
+ * Opens a debug menu; this launches the browser and starts a subserver
330
+ * @param port Port of the server/http, defaults to 0 which finds an open port
331
+ * @param password Toggles the requirement of a password to access the server. Defaults to empty, which doesn't ask for a password.
332
+ */
333
+ OpenDebug(data) {
334
+ if (this.debugServer != null)
335
+ throw new Error("Attempted to open a debug server when one has already been opened.");
336
+ data.port ??= 0;
337
+ data.password ??= "";
338
+ if (data.port < 0 || data.port >= 65536)
339
+ throw new Error("Port out of range!");
340
+ /**
341
+ * `
342
+ <!DOCTYPE html>
343
+ <html>
344
+ <head>
345
+ <meta charset="UTF-8">
346
+ <script src="https://cdn.jsdelivr.net/gh/liwybloc/sonic-ws/bundled/SonicWS_bundle.js"></script>
347
+ <title>SonicWS Debug Menu</title>
348
+ <style>
349
+ body {
350
+ margin: 0;
351
+ font-family: Inter, Arial, sans-serif;
352
+ background: #0f1115;
353
+ color: #e6e6e6;
354
+ height: 100vh;
355
+ display: flex;
356
+ }
357
+
358
+ #sidebar {
359
+ width: 260px;
360
+ background: #141821;
361
+ border-right: 1px solid #1f2533;
362
+ display: flex;
363
+ flex-direction: column;
364
+ }
365
+
366
+ #sidebar-header {
367
+ padding: 16px;
368
+ font-weight: 600;
369
+ font-size: 18px;
370
+ border-bottom: 1px solid #1f2533;
371
+ cursor: pointer;
372
+ transition: color 0.2s;
373
+ }
374
+ #sidebar-header:hover {
375
+ color: #dddddd;
376
+ }
377
+
378
+ #socket-list {
379
+ flex: 1;
380
+ overflow-y: auto;
381
+ }
382
+
383
+ .socket-item {
384
+ padding: 10px 14px;
385
+ cursor: pointer;
386
+ border-bottom: 1px solid #1f2533;
387
+ }
388
+
389
+ .socket-item:hover {
390
+ background: #1b2030;
391
+ }
392
+
393
+ .socket-item.active {
394
+ background: #22294a;
395
+ }
396
+
397
+ .socket-id {
398
+ font-size: 12px;
399
+ opacity: 0.7;
400
+ }
401
+
402
+ #main {
403
+ flex: 1;
404
+ display: flex;
405
+ flex-direction: column;
406
+ }
407
+
408
+ #main-header {
409
+ padding: 17px;
410
+ border-bottom: 1px solid #1f2533;
411
+ display: flex;
412
+ justify-content: space-between;
413
+ align-items: center;
414
+ }
415
+
416
+ #stats {
417
+ display: flex;
418
+ gap: 20px;
419
+ font-size: 13px;
420
+ }
421
+
422
+ #packets {
423
+ flex: 1;
424
+ overflow-y: auto;
425
+ padding: 12px;
426
+ }
427
+
428
+ .packet {
429
+ background: #1a1f2e;
430
+ border-radius: 6px;
431
+ padding: 6px 8px;
432
+ margin-bottom: 4px;
433
+ font-size: 12px;
434
+ cursor: pointer;
435
+ }
436
+
437
+ .packet.sent { border-left: 3px solid #3cff7a; }
438
+ .packet.recv { border-left: 3px solid #ff5a5a; }
439
+
440
+ .packet-details {
441
+ display: none;
442
+ margin-top: 4px;
443
+ opacity: 0.75;
444
+ font-size: 11px;
445
+ }
446
+
447
+ .packet.expanded .packet-details {
448
+ display: block;
449
+ }
450
+
451
+ button {
452
+ background: none;
453
+ }
454
+ </style>
455
+ </head>
456
+ <body>
457
+ <div id="sidebar">
458
+ <div id="sidebar-header">Sonic WS Debug Menu</div>
459
+ <div id="socket-list"></div>
460
+ </div>
461
+
462
+ <div id="main">
463
+ <div id="main-header">
464
+ <button id="close-socket" style="display:none;">❌</button>
465
+ <div id="socket-title">Server Home</div>
466
+ <div id="stats"></div>
467
+ </div>
468
+ <div id="home" style="display: block; padding: 16px;">
469
+ <h2>Server Dashboard</h2>
470
+ <div id="global-stats" style="margin-bottom:16px;"></div>
471
+ <h3>Connection Logs</h3>
472
+ <ul id="connection-logs" style="max-height:200px; overflow-y:auto; padding-left:16px;"></ul>
473
+ <div style="margin-top:16px;">
474
+ <table style="width:100%; border-collapse:collapse;">
475
+ <thead>
476
+ <tr>
477
+ <th style="border-bottom:1px solid #444; text-align:left;">Socket ID</th>
478
+ <th style="border-bottom:1px solid #444; text-align:left;">Name</th>
479
+ <th style="border-bottom:1px solid #444; text-align:left;">Status</th>
480
+ </tr>
481
+ </thead>
482
+ <tbody id="connection-table"></tbody>
483
+ </table>
484
+ </div>
485
+ </div>
486
+ <div id="packets"></div>
487
+ </div>
488
+
489
+ <script>
490
+ const socketList = document.getElementById('socket-list');
491
+ const packetsDiv = document.getElementById('packets');
492
+ const socketTitle = document.getElementById('socket-title');
493
+ const debugTitle = document.getElementById('sidebar-header');
494
+ const statsDiv = document.getElementById('stats');
495
+ const home = document.getElementById('home');
496
+ const closeSocketBtn = document.getElementById('close-socket');
497
+
498
+ debugTitle.onclick = () => {
499
+ if(activeId !== null) {
500
+ activeId = null;
501
+ packetsDiv.style.display = 'none';
502
+ home.style.display = 'block';
503
+ closeSocketBtn.style.display = 'none';
504
+ renderGlobalStats();
505
+ }
506
+ };
507
+
508
+ const globalStats = {
509
+ totalSockets: 0,
510
+ totalSent: 0,
511
+ totalRecv: 0,
512
+ totalSentBytes: 0,
513
+ totalRecvBytes: 0,
514
+ totalSaved: 0,
515
+ startTime: 0,
516
+ };
517
+
518
+ function formatMilliseconds(ms) {
519
+ if (ms < 1) return '0.0s';
520
+
521
+ const units = [
522
+ { label: 'week', value: 7 * 24 * 60 * 60 * 1000 },
523
+ { label: 'day', value: 24 * 60 * 60 * 1000 },
524
+ { label: 'hour', value: 60 * 60 * 1000 },
525
+ { label: 'minute', value: 60 * 1000 },
526
+ ];
527
+
528
+ const parts = [];
529
+
530
+ for (const { label, value } of units) {
531
+ const amount = Math.floor(ms / value);
532
+ if (amount > 0) {
533
+ parts.push(amount + ' ' + label + (amount !== 1 ? 's' : ''));
534
+ ms -= amount * value;
535
+ }
536
+ }
537
+
538
+ if (ms > 0 || parts.length === 0) {
539
+ const seconds = (ms / 1000).toFixed(1);
540
+ parts.push(seconds + 's');
541
+ }
542
+
543
+ if (parts.length === 1) return parts[0];
544
+ const last = parts.pop();
545
+ return parts.join(', ') + ' and ' + last;
546
+ }
547
+
548
+
549
+ const globalStatsDiv = document.getElementById('global-stats');
550
+ function renderGlobalStats() {
551
+ const stats = globalStats;
552
+ const uptime = Date.now() - stats.startTime;
553
+ const formattedUptime = formatMilliseconds(uptime);
554
+
555
+ globalStatsDiv.innerHTML = '<div><strong>Total Sockets:</strong> ' + stats.totalSockets + '</div><div><strong>Total Sent Packets:</strong> ' + stats.totalSent + '</div><div><strong>Total Received Packets:</strong> ' + stats.totalRecv + '</div><div><strong>Total Sent Bytes:</strong> ' + stats.totalSentBytes + ' B</div><div><strong>Total Received Bytes:</strong> ' + stats.totalRecvBytes + ' B</div><div><strong>Total Bandwidth Saved:</strong> ' + stats.totalSaved + ' B</div><div><strong>Uptime:</strong> ' + formattedUptime + '</div>';
556
+ }
557
+ setInterval(renderGlobalStats, 50);
558
+
559
+ function updateConnectionTable() {
560
+ const tbody = document.getElementById('connection-table');
561
+ tbody.innerHTML = '';
562
+ sockets.forEach(s => {
563
+ const row = document.createElement('tr');
564
+ row.innerHTML = '<td>' + s.id + '</td><td>' + s.name + '</td><td style="color:' + (s.el.style.color || '#0f0') + '">' + (s.el.style.color === '#f00' ? 'Disconnected' : 'Connected') + '</td>';
565
+ tbody.appendChild(row);
566
+ });
567
+ }
568
+ setInterval(updateConnectionTable, 1000);
569
+
570
+ const sockets = new Map();
571
+ let activeId = null;
572
+
573
+ function selectSocket(id) {
574
+ activeId = id;
575
+ [...socketList.children].forEach(e => e.classList.toggle('active', e.dataset.id == id));
576
+
577
+ const s = sockets.get(id);
578
+ socketTitle.textContent = s.name;
579
+ packetsDiv.innerHTML = '';
580
+ s.packets.forEach(p => packetsDiv.appendChild(p.el));
581
+ renderStats(s);
582
+ home.style.display = 'none';
583
+ packetsDiv.style.display = 'block';
584
+ closeSocketBtn.style.display = 'block';
585
+ }
586
+
587
+ closeSocketBtn.onclick = () => {
588
+ if(activeId === null) return;
589
+ ws.send("close", Number(activeId));
590
+ }
591
+
592
+ function renderStats(s) {
593
+ statsDiv.innerHTML = "<div>Sent: " + s.sent + "</div><div>Recv: " + s.recv + "</div><div>Sent bytes: " + s.sentBytes + "</div><div>Recv bytes: " + s.recvBytes + "</div><div>Saved: " + s.saved + "</div>";
594
+ }
595
+
596
+ function addSocket(id, name) {
597
+ const el = document.createElement('div');
598
+ el.className = 'socket-item';
599
+ el.dataset.id = id;
600
+ el.innerHTML = "<div>" + name + "</div><div class=\\"socket-id\\">#" + id + "</div>";
601
+ el.onclick = () => selectSocket(id);
602
+ socketList.appendChild(el);
603
+
604
+ sockets.set(id, {
605
+ id,
606
+ name,
607
+ el,
608
+ packets: [],
609
+ sent: 0,
610
+ recv: 0,
611
+ sentBytes: 0,
612
+ recvBytes: 0,
613
+ saved: 0
614
+ });
615
+ }
616
+
617
+ function removeSocket(id, code, reason, codeReason) {
618
+ const s = sockets.get(id);
619
+ if (!s) return console.error("Unknown socket!!", id);
620
+
621
+ s.el.dataset.id = id + "-closed";
622
+ s.el.onclick = () => selectSocket(id + "-closed");
623
+ sockets.set(id + "-closed", s);
624
+ sockets.delete(id);
625
+
626
+ if(activeId == id) activeId = id + "-closed";
627
+
628
+ const nameNode = s.el.childNodes[0];
629
+ nameNode.style.color = "#f00";
630
+
631
+ let circle = document.createElement('span');
632
+ circle.style.display = 'inline-block';
633
+ circle.style.width = '10px';
634
+ circle.style.height = '10px';
635
+ circle.style.borderRadius = '50%';
636
+ circle.style.background = '#ff5a5a';
637
+ circle.style.marginLeft = '8px';
638
+ nameNode.appendChild(circle);
639
+
640
+ // add disconnection info to home page logs
641
+ const logItem = document.createElement('li');
642
+ logItem.textContent = 'Socket #' + id + ' disconnected — Code: ' + code + ', Reason: ' + reason + ', Closure Cause: ' + codeReason;
643
+ document.getElementById('connection-logs').appendChild(logItem);
644
+
645
+ requestAnimationFrame(() => circle.style.width = '100%');
646
+ setTimeout(() => {
647
+ circle.remove();
648
+ s.el.remove();
649
+ if(activeId == id + "-closed") {
650
+ packetsDiv.style.display = 'none';
651
+ home.style.display = 'block';
652
+ closeSocketBtn.style.display = 'none';
653
+ }
654
+ }, 30000);
655
+ }
656
+
657
+ function addPacket(id, dir, tag, rawSize, saved, info, date, processTime) {
658
+ const s = sockets.get(id);
659
+ if (!s) return console.error("Unknown socket!!", id);
660
+
661
+ const el = document.createElement('div');
662
+ el.className = 'packet ' + (dir === 'sent' ? 'sent' : 'recv');
663
+ el.innerHTML = '<div>' + tag + (info !== "undefined" ? ' — ' + info : '') + '</div><div class="packet-details">Raw Bytes: ' + rawSize + 'b (saved: ~' + saved + 'b)<br>Processed At: ' + new Date(date).toISOString() + '<br>Processing Time: ' + processTime.toFixed(2) + 'ms</div>';
664
+
665
+ el.onclick = () => el.classList.toggle('expanded');
666
+
667
+ s.packets.push({ el });
668
+ if (dir === 'sent') {
669
+ s.sent++;
670
+ s.sentBytes += rawSize;
671
+ } else {
672
+ s.recv++;
673
+ s.recvBytes += rawSize;
674
+ }
675
+ s.saved += saved;
676
+
677
+ if (activeId === id) {
678
+ packetsDiv.appendChild(el);
679
+ renderStats(s);
680
+ }
681
+ }
682
+
683
+ function setStat(i, v) {
684
+ globalStats[Object.keys(globalStats)[i]] = v;
685
+ if (activeId === null) renderGlobalStats();
686
+ }
687
+
688
+ const ws = new SonicWS('ws://' + location.host);
689
+
690
+ ws.on("connection", id => addSocket(id, "Socket " + id));
691
+ ws.on("disconnection", ([id, code], [reason, codeReason]) => removeSocket(id, code, reason, codeReason));
692
+ ws.on("nameChange", ([id], [name]) => {
693
+ const s = sockets.get(id);
694
+ if (!s) return console.error("Unknown socket!!", id);
695
+ s.name = name;
696
+ s.el.firstChild.textContent = name;
697
+ if (activeId === id) socketTitle.textContent = name;
698
+ });
699
+ ws.on("packet", ([id, size, saved], [dir], [tag], [values], [time, processTime]) => {
700
+ console.log("Received packet", { id, size, saved, dir, tag, values, time, processTime });
701
+ addPacket(id, dir, tag, size, saved, values, time, processTime);
702
+ });
703
+ ws.on("stats", (stats) => {
704
+ console.log("Received stats", stats);
705
+ stats.forEach((v, i) => setStat(i, v));
706
+ });
707
+ ws.on("stat", (i, v) => setStat(i, v));
708
+
709
+ const lastKnownPassword = localStorage.getItem("password");
710
+ const empty = !localStorage.getItem("req");
711
+ let usedPass = "";
712
+ ws.on_ready(() => {
713
+ if(empty) ws.send("password", "");
714
+ else ws.send("password", usedPass = (lastKnownPassword ?? prompt("Please enter password")));
715
+ });
716
+
717
+ ws.on_close(() => {
718
+ localStorage.setItem("req", true);
719
+ localStorage.removeItem("password");
720
+ setTimeout(() => window.location.reload(), 1000);
721
+ });
722
+
723
+ ws.on("authenticated", (success) => {
724
+ console.log("Auth status:", success);
725
+ if(!success) {
726
+ } else {
727
+ localStorage.setItem("req", usedPass.length > 0);
728
+ localStorage.setItem("password", usedPass);
729
+ }
730
+ })
731
+ </script>
732
+ </body>
733
+ </html>
734
+ `
735
+ */
736
+ const server = http_1.default.createServer((req, res) => {
737
+ res.writeHead(200, { 'Content-Type': 'text/html' });
738
+ res.end(`<!doctypehtml><meta charset=UTF-8><script src=https://cdn.jsdelivr.net/gh/liwybloc/sonic-ws/bundled/SonicWS_bundle.js></script><title>SonicWS Debug Menu</title><style>body{margin:0;font-family:Inter,Arial,sans-serif;background:#0f1115;color:#e6e6e6;height:100vh;display:flex}#sidebar{width:260px;background:#141821;border-right:1px solid #1f2533;display:flex;flex-direction:column}#sidebar-header{padding:16px;font-weight:600;font-size:18px;border-bottom:1px solid #1f2533;cursor:pointer;transition:color .2s}#sidebar-header:hover{color:#ddd}#socket-list{flex:1;overflow-y:auto}.socket-item{padding:10px 14px;cursor:pointer;border-bottom:1px solid #1f2533}.socket-item:hover{background:#1b2030}.socket-item.active{background:#22294a}.socket-id{font-size:12px;opacity:.7}#main{flex:1;display:flex;flex-direction:column}#main-header{padding:17px;border-bottom:1px solid #1f2533;display:flex;justify-content:space-between;align-items:center}#stats{display:flex;gap:20px;font-size:13px}#packets{flex:1;overflow-y:auto;padding:12px}.packet{background:#1a1f2e;border-radius:6px;padding:6px 8px;margin-bottom:4px;font-size:12px;cursor:pointer}.packet.sent{border-left:3px solid #3cff7a}.packet.recv{border-left:3px solid #ff5a5a}.packet-details{display:none;margin-top:4px;opacity:.75;font-size:11px}.packet.expanded .packet-details{display:block}button{background:0 0}</style><div id=sidebar><div id=sidebar-header>Sonic WS Debug Menu</div><div id=socket-list></div></div><div id=main><div id=main-header><button id=close-socket style=display:none>❌</button><div id=socket-title>Server Home</div><div id=stats></div></div><div id=home style=display:block;padding:16px><h2>Server Dashboard</h2><div id=global-stats style=margin-bottom:16px></div><h3>Connection Logs</h3><ul id=connection-logs style=max-height:200px;overflow-y:auto;padding-left:16px></ul><div style=margin-top:16px><table style=width:100%;border-collapse:collapse><thead><tr><th style="border-bottom:1px solid #444;text-align:left">Socket ID<th style="border-bottom:1px solid #444;text-align:left">Name<th style="border-bottom:1px solid #444;text-align:left">Status<tbody id=connection-table></table></div></div><div id=packets></div></div><script>const socketList=document.getElementById("socket-list"),packetsDiv=document.getElementById("packets"),socketTitle=document.getElementById("socket-title"),debugTitle=document.getElementById("sidebar-header"),statsDiv=document.getElementById("stats"),home=document.getElementById("home"),closeSocketBtn=document.getElementById("close-socket");debugTitle.onclick=()=>{null!==activeId&&(activeId=null,packetsDiv.style.display="none",home.style.display="block",closeSocketBtn.style.display="none",renderGlobalStats())};const globalStats={totalSockets:0,totalSent:0,totalRecv:0,totalSentBytes:0,totalRecvBytes:0,totalSaved:0,startTime:0};function formatMilliseconds(e){if(e<1)return"0.0s";const t=[{label:"week",value:6048e5},{label:"day",value:864e5},{label:"hour",value:36e5},{label:"minute",value:6e4}],s=[];for(const{label:o,value:n}of t){const t=Math.floor(e/n);t>0&&(s.push(t+" "+o+(1!==t?"s":"")),e-=t*n)}if(e>0||0===s.length){const t=(e/1e3).toFixed(1);s.push(t+"s")}if(1===s.length)return s[0];const o=s.pop();return s.join(", ")+" and "+o}const globalStatsDiv=document.getElementById("global-stats");function renderGlobalStats(){const e=globalStats,t=formatMilliseconds(Date.now()-e.startTime);globalStatsDiv.innerHTML="<div><strong>Total Sockets:</strong> "+e.totalSockets+"</div><div><strong>Total Sent Packets:</strong> "+e.totalSent+"</div><div><strong>Total Received Packets:</strong> "+e.totalRecv+"</div><div><strong>Total Sent Bytes:</strong> "+e.totalSentBytes+" B</div><div><strong>Total Received Bytes:</strong> "+e.totalRecvBytes+" B</div><div><strong>Total Bandwidth Saved:</strong> "+e.totalSaved+" B</div><div><strong>Uptime:</strong> "+t+"</div>"}function updateConnectionTable(){const e=document.getElementById("connection-table");e.innerHTML="",sockets.forEach((t=>{const s=document.createElement("tr");s.innerHTML="<td>"+t.id+"</td><td>"+t.name+'</td><td style="color:'+(t.el.style.color||"#0f0")+'">'+("#f00"===t.el.style.color?"Disconnected":"Connected")+"</td>",e.appendChild(s)}))}setInterval(renderGlobalStats,50),setInterval(updateConnectionTable,1e3);const sockets=new Map;let activeId=null;function selectSocket(e){activeId=e,[...socketList.children].forEach((t=>t.classList.toggle("active",t.dataset.id==e)));const t=sockets.get(e);socketTitle.textContent=t.name,packetsDiv.innerHTML="",t.packets.forEach((e=>packetsDiv.appendChild(e.el))),renderStats(t),home.style.display="none",packetsDiv.style.display="block",closeSocketBtn.style.display="block"}function renderStats(e){statsDiv.innerHTML="<div>Sent: "+e.sent+"</div><div>Recv: "+e.recv+"</div><div>Sent bytes: "+e.sentBytes+"</div><div>Recv bytes: "+e.recvBytes+"</div><div>Saved: "+e.saved+"</div>"}function addSocket(e,t){const s=document.createElement("div");s.className="socket-item",s.dataset.id=e,s.innerHTML="<div>"+t+'</div><div class="socket-id">#'+e+"</div>",s.onclick=()=>selectSocket(e),socketList.appendChild(s),sockets.set(e,{id:e,name:t,el:s,packets:[],sent:0,recv:0,sentBytes:0,recvBytes:0,saved:0})}function removeSocket(e,t,s,o){const n=sockets.get(e);if(!n)return console.error("Unknown socket!!",e);n.el.dataset.id=e+"-closed",n.el.onclick=()=>selectSocket(e+"-closed"),sockets.set(e+"-closed",n),sockets.delete(e),activeId==e&&(activeId=e+"-closed");const c=n.el.childNodes[0];c.style.color="#f00";let l=document.createElement("span");l.style.display="inline-block",l.style.width="10px",l.style.height="10px",l.style.borderRadius="50%",l.style.background="#ff5a5a",l.style.marginLeft="8px",c.appendChild(l);const a=document.createElement("li");a.textContent="Socket #"+e+" disconnected — Code: "+t+", Reason: "+s+", Closure Cause: "+o,document.getElementById("connection-logs").appendChild(a),requestAnimationFrame((()=>l.style.width="100%")),setTimeout((()=>{l.remove(),n.el.remove(),activeId==e+"-closed"&&(packetsDiv.style.display="none",home.style.display="block",closeSocketBtn.style.display="none")}),3e4)}function addPacket(e,t,s,o,n,c,l,a){const d=sockets.get(e);if(!d)return console.error("Unknown socket!!",e);const i=document.createElement("div");i.className="packet "+("sent"===t?"sent":"recv"),i.innerHTML="<div>"+s+("undefined"!==c?" — "+c:"")+'</div><div class="packet-details">Raw Bytes: '+o+"b (saved: ~"+n+"b)<br>Processed At: "+new Date(l).toISOString()+"<br>Processing Time: "+a.toFixed(2)+"ms</div>",i.onclick=()=>i.classList.toggle("expanded"),d.packets.push({el:i}),"sent"===t?(d.sent++,d.sentBytes+=o):(d.recv++,d.recvBytes+=o),d.saved+=n,activeId===e&&(packetsDiv.appendChild(i),renderStats(d))}function setStat(e,t){globalStats[Object.keys(globalStats)[e]]=t,null===activeId&&renderGlobalStats()}closeSocketBtn.onclick=()=>{null!==activeId&&ws.send("close",Number(activeId))};const ws=new SonicWS("ws://"+location.host);ws.on("connection",(e=>addSocket(e,"Socket "+e))),ws.on("disconnection",(([e,t],[s,o])=>removeSocket(e,t,s,o))),ws.on("nameChange",(([e],[t])=>{const s=sockets.get(e);if(!s)return console.error("Unknown socket!!",e);s.name=t,s.el.firstChild.textContent=t,activeId===e&&(socketTitle.textContent=t)})),ws.on("packet",(([e,t,s],[o],[n],[c],[l,a])=>{console.log("Received packet",{id:e,size:t,saved:s,dir:o,tag:n,values:c,time:l,processTime:a}),addPacket(e,o,n,t,s,c,l,a)})),ws.on("stats",(e=>{console.log("Received stats",e),e.forEach(((e,t)=>setStat(t,e)))})),ws.on("stat",((e,t)=>setStat(e,t)));const lastKnownPassword=localStorage.getItem("password"),empty=!localStorage.getItem("req");let usedPass="";ws.on_ready((()=>{empty?ws.send("password",""):ws.send("password",usedPass=lastKnownPassword??prompt("Please enter password"))})),ws.on_close((()=>{localStorage.setItem("req",!0),localStorage.removeItem("password"),setTimeout((()=>window.location.reload()),1e3)})),ws.on("authenticated",(e=>{console.log("Auth status:",e),e&&(localStorage.setItem("req",usedPass.length>0),localStorage.setItem("password",usedPass))}));</script>`);
739
+ });
740
+ const wss = new SonicWSServer({
741
+ clientPackets: [
742
+ (0, PacketUtils_1.CreatePacket)({ tag: "password", type: PacketType_1.PacketType.STRINGS_UTF16 }),
743
+ (0, PacketUtils_1.CreatePacket)({ tag: "close", type: PacketType_1.PacketType.UVARINT }),
744
+ ],
745
+ serverPackets: [
746
+ (0, PacketUtils_1.CreatePacket)({ tag: "authenticated", type: PacketType_1.PacketType.BOOLEANS }),
747
+ (0, PacketUtils_1.CreatePacket)({ tag: "connection", type: PacketType_1.PacketType.UVARINT }),
748
+ (0, PacketUtils_1.CreateObjPacket)({ tag: "disconnection", types: [PacketType_1.PacketType.UVARINT, PacketType_1.PacketType.STRINGS], noDataRange: true }),
749
+ (0, PacketUtils_1.CreateObjPacket)({ tag: "nameChange", types: [PacketType_1.PacketType.UVARINT, PacketType_1.PacketType.STRINGS_UTF16], noDataRange: true }),
750
+ (0, PacketUtils_1.CreateObjPacket)({
751
+ tag: "packet",
752
+ types: [
753
+ PacketType_1.PacketType.VARINT,
754
+ PacketType_1.PacketType.STRINGS,
755
+ PacketType_1.PacketType.STRINGS_UTF16,
756
+ PacketType_1.PacketType.STRINGS_UTF16,
757
+ PacketType_1.PacketType.FLOATS,
758
+ ],
759
+ gzipCompression: true,
760
+ noDataRange: true
761
+ }),
762
+ (0, PacketUtils_1.CreatePacket)({ tag: "stats", type: PacketType_1.PacketType.UVARINT, noDataRange: true, dontSpread: true }),
763
+ (0, PacketUtils_1.CreatePacket)({ tag: "stat", type: PacketType_1.PacketType.UVARINT, dataMax: 2 }),
764
+ ],
765
+ websocketOptions: { server },
766
+ });
767
+ const globalStats = new Proxy({
768
+ totalSockets: 0,
769
+ totalSent: 0,
770
+ totalRecv: 0,
771
+ totalSentBytes: 0,
772
+ totalRecvBytes: 0,
773
+ totalSaved: 0,
774
+ startTime: Date.now(),
775
+ }, {
776
+ set(target, prop, value) {
777
+ const key = String(prop);
778
+ if (target[key] !== value) {
779
+ target[key] = value;
780
+ wss.broadcast("stat", Object.keys(globalStats).indexOf(key), value);
781
+ }
782
+ return true;
783
+ }
784
+ });
785
+ // TODO: i think this is fucked by async
786
+ const storedPacketData = {};
787
+ wss.on_connect(ws => {
788
+ let authenticated = false;
789
+ const ogs = ws.send.bind(ws);
790
+ let queue = [];
791
+ ws.send = async (tag, ...values) => {
792
+ if (!authenticated)
793
+ queue.push([tag, values]);
794
+ else
795
+ ogs(tag, ...values);
796
+ };
797
+ ws.send("stats", ...Object.values(globalStats));
798
+ this.connections.forEach(conn => {
799
+ ws.send("connection", conn.id);
800
+ ws.send("nameChange", conn.id, conn.getName());
801
+ storedPacketData[conn.id]?.forEach((data) => {
802
+ ws.send("packet", ...data);
803
+ });
804
+ });
805
+ ws.on("password", (pword) => {
806
+ if (data.password != pword) {
807
+ ws.send("authenticated", false);
808
+ setTimeout(() => ws.close(1008), 1000);
809
+ }
810
+ else {
811
+ authenticated = true;
812
+ ws.send("authenticated", true);
813
+ queue.forEach(([tag, values]) => ogs(tag, ...values));
814
+ }
815
+ });
816
+ ws.on("close", (id) => {
817
+ if (!authenticated)
818
+ return;
819
+ this.connectionMap[id]?.close(Connection_1.CloseCodes.MANUAL_SHUTDOWN);
820
+ });
821
+ });
822
+ const innerConns = [];
823
+ const broadcastSends = {};
824
+ const textEncoder = new TextEncoder();
825
+ const length = (values) => textEncoder.encode(JSON.stringify(values) ?? "[]").length;
826
+ this.addMiddleware(new (class {
827
+ onClientConnect(connection) {
828
+ globalStats.totalSockets++;
829
+ storedPacketData[connection.id] = [];
830
+ innerConns.push(connection);
831
+ wss.broadcast("connection", connection.id);
832
+ const packetsSend = {};
833
+ const packetsRecv = {};
834
+ connection.addMiddleware(new (class {
835
+ onNameChange(name) {
836
+ wss.broadcast("nameChange", connection.id, name);
837
+ }
838
+ onSend_pre(tag, values, date, perfTime) {
839
+ packetsSend[tag] ??= [];
840
+ packetsSend[tag].push([values, perfTime, date]);
841
+ }
842
+ onSend_post(tag, data, sendSize) {
843
+ globalStats.totalSentBytes += sendSize;
844
+ globalStats.totalSent++;
845
+ const [values, perfTime, date] = packetsSend[tag].shift();
846
+ const jsonLength = length(values);
847
+ const saved = jsonLength - sendSize;
848
+ globalStats.totalSaved += saved;
849
+ const record = [
850
+ [connection.id, sendSize + 1, saved],
851
+ "sent",
852
+ tag,
853
+ JSON.stringify(values),
854
+ [date, performance.now() - perfTime],
855
+ ];
856
+ storedPacketData[connection.id].push(record);
857
+ wss.broadcast("packet", ...record);
858
+ }
859
+ onReceive_pre(tag, data, recvSize) {
860
+ globalStats.totalRecvBytes += recvSize;
861
+ globalStats.totalRecv++;
862
+ packetsRecv[tag] ??= [];
863
+ packetsRecv[tag].push([data, performance.now(), Date.now()]);
864
+ }
865
+ onReceive_post(tag, values) {
866
+ const [data, time, date] = packetsRecv[tag].shift();
867
+ const jsonLength = length(values);
868
+ const saved = jsonLength - data.length;
869
+ globalStats.totalSaved += saved;
870
+ const record = [
871
+ [connection.id, data.length + 1, saved],
872
+ "recv",
873
+ tag,
874
+ JSON.stringify(values),
875
+ [date, performance.now() - time],
876
+ ];
877
+ storedPacketData[connection.id].push(record);
878
+ wss.broadcast("packet", ...record);
879
+ }
880
+ })());
881
+ }
882
+ onClientDisconnect(connection, code, reason) {
883
+ globalStats.totalSockets--;
884
+ wss.broadcast("disconnection", [connection.id, code], [reason?.toString() ?? "UNKNOWN", (0, Connection_1.getClosureCause)(code)]);
885
+ delete storedPacketData[connection.id];
886
+ innerConns.splice(innerConns.indexOf(connection), 1);
887
+ }
888
+ onPacketBroadcast_pre(tag, info, ...values) {
889
+ broadcastSends[tag] ??= [];
890
+ broadcastSends[tag].push([values, performance.now(), Date.now()]);
891
+ }
892
+ onPacketBroadcast_post(tag, info, data, sendSize) {
893
+ const [values, time, date] = broadcastSends[tag].shift();
894
+ info.recipients.forEach(k => {
895
+ if (innerConns.includes(k)) {
896
+ k.callMiddleware("onSend_pre", tag, values, date, time);
897
+ k.callMiddleware("onSend_post", tag, data, sendSize);
898
+ }
899
+ });
900
+ }
901
+ })());
902
+ this.debugServer = server;
903
+ server.listen(data.port, () => {
904
+ const address = server.address();
905
+ console.log(`SWS Debug server running at http://localhost:${address.port}`);
906
+ (0, open_1.default)(`http://localhost:${address.port}`);
907
+ });
908
+ }
280
909
  }
281
910
  exports.SonicWSServer = SonicWSServer;