sonic-ws 1.0.6 → 1.1.1
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/LICENSE +1 -1
- package/README.md +8 -2
- package/dist/index.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +2 -2
- package/dist/ws/Connection.d.ts +4 -3
- package/dist/ws/Connection.js +1 -1
- package/dist/ws/client/core/ClientCore.d.ts +10 -5
- package/dist/ws/client/core/ClientCore.js +61 -11
- package/dist/ws/client/node/ClientNode.js +1 -1
- package/dist/ws/packets/PacketProcessors.js +1 -1
- package/dist/ws/packets/PacketType.js +1 -1
- package/dist/ws/packets/Packets.d.ts +6 -4
- package/dist/ws/packets/Packets.js +15 -7
- package/dist/ws/server/SonicWSConnection.d.ts +10 -4
- package/dist/ws/server/SonicWSConnection.js +62 -12
- package/dist/ws/server/SonicWSServer.js +6 -1
- package/dist/ws/util/ArrayUtil.js +1 -1
- package/dist/ws/util/BufferUtil.d.ts +1 -1
- package/dist/ws/util/BufferUtil.js +1 -1
- package/dist/ws/util/StringUtil.js +1 -1
- package/dist/ws/util/enums/EnumHandler.js +1 -1
- package/dist/ws/util/enums/EnumType.js +1 -1
- package/dist/ws/util/packets/BatchHelper.d.ts +1 -1
- package/dist/ws/util/packets/BatchHelper.js +3 -3
- package/dist/ws/util/packets/CompressionUtil.js +1 -1
- package/dist/ws/util/packets/PacketHolder.js +2 -2
- package/dist/ws/util/packets/PacketUtils.d.ts +4 -2
- package/dist/ws/util/packets/PacketUtils.js +26 -14
- package/dist/ws/util/packets/RateHandler.js +1 -1
- package/package.json +3 -2
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -18,9 +18,11 @@ Developer Friendly:
|
|
|
18
18
|
|
|
19
19
|
Security:
|
|
20
20
|
- Tamper-proof; any invalid packet instantly causes closure, and tampering becomes incredibly difficult
|
|
21
|
+
- Basic but immensely effective anti-tampering for browser clients
|
|
21
22
|
- Built-in ability for handshake packets, preventing repetitive initiation checks in listeners (for example, removes if(!init) everywhere)
|
|
22
23
|
- Built-in rate limiting for packets; ability for global send & receive, alongside per-packet rate limiting
|
|
23
24
|
- Built-in disabling & enabling of packets to prevent abuse
|
|
25
|
+
- Prevents any race conditions; your callbacks will not be called until they finish.
|
|
24
26
|
|
|
25
27
|
Performance & Scaling:
|
|
26
28
|
- Can handle very large packets in microseconds
|
|
@@ -35,7 +37,7 @@ Developer Experience:
|
|
|
35
37
|
- Many data types to maximize speed, clarity, bandwidth, and security
|
|
36
38
|
- Debug tools for socket ids, byte size, data logging, etc. for troubleshooting
|
|
37
39
|
- Very minimal learning curve, easy to work in
|
|
38
|
-
- JSDoc's for understanding
|
|
40
|
+
- JSDoc's for understanding; immensely intuitive (personally, I took a break for half a year and came back and snapped right back in)
|
|
39
41
|
|
|
40
42
|
Whether you're making a real-time game, a dashboard, a distributed system, or anything else, SonicWS gets you safe, structured packets, fast.
|
|
41
43
|
|
|
@@ -109,6 +111,10 @@ ws.on_close((event) => {
|
|
|
109
111
|
|
|
110
112
|
## KNOWN ISSUES
|
|
111
113
|
|
|
114
|
+
Some weird error messages when invalid inputs are in like CreatePacket() and stuff
|
|
115
|
+
|
|
112
116
|
## PLANNED FEATURES
|
|
113
117
|
|
|
114
|
-
Better error handling
|
|
118
|
+
Better error handling
|
|
119
|
+
|
|
120
|
+
ZLib/gzip compression
|
package/dist/index.js
CHANGED
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/*
|
|
3
|
-
* Copyright
|
|
3
|
+
* Copyright 2026 Lily (liwybloc)
|
|
4
4
|
*
|
|
5
5
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
6
|
* you may not use this file except in compliance with the License.
|
|
@@ -18,7 +18,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
18
18
|
exports.SERVER_SUFFIX_NUMS = exports.SERVER_SUFFIX = exports.VERSION = void 0;
|
|
19
19
|
const StringUtil_1 = require("./ws/util/StringUtil");
|
|
20
20
|
/** Current protocol version */
|
|
21
|
-
exports.VERSION =
|
|
21
|
+
exports.VERSION = 14;
|
|
22
22
|
/** Server data suffix */
|
|
23
23
|
exports.SERVER_SUFFIX = "SWS";
|
|
24
24
|
/** Server data suffix in array */
|
package/dist/ws/Connection.d.ts
CHANGED
|
@@ -3,17 +3,18 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export interface Connection {
|
|
5
5
|
/**
|
|
6
|
-
* List of timers
|
|
6
|
+
* List of timers.
|
|
7
7
|
* For internal use only.
|
|
8
8
|
*/
|
|
9
|
-
_timers: Record<number, number>;
|
|
9
|
+
_timers: Record<number, [number, (closed: boolean) => void, boolean]>;
|
|
10
10
|
/**
|
|
11
11
|
* Sets a timeout that will automatically end when the socket closes
|
|
12
12
|
* @param call The function to call
|
|
13
13
|
* @param time The time between now and the call (ms)
|
|
14
|
+
* @param callOnClose If the callback should be fired anyways when the socket closes
|
|
14
15
|
* @returns The timeout id to be used with socket.clearInterval(id)
|
|
15
16
|
*/
|
|
16
|
-
setTimeout(call: () => void, time: number): number;
|
|
17
|
+
setTimeout(call: () => void, time: number, callOnClose: boolean): number;
|
|
17
18
|
/**
|
|
18
19
|
* Sets an interval that will automatically end when the socket closes
|
|
19
20
|
* @param call The function to call
|
package/dist/ws/Connection.js
CHANGED
|
@@ -21,12 +21,18 @@ export declare abstract class SonicWSCore implements Connection {
|
|
|
21
21
|
private batcher;
|
|
22
22
|
private bufferHandler;
|
|
23
23
|
id: number;
|
|
24
|
-
_timers: Record<number, number>;
|
|
24
|
+
_timers: Record<number, [number, (closed: boolean) => void, boolean]>;
|
|
25
|
+
private asyncData;
|
|
26
|
+
private asyncMap;
|
|
25
27
|
constructor(ws: WebSocket, bufferHandler: (val: MessageEvent) => Promise<Uint8Array>);
|
|
26
28
|
private reading;
|
|
27
29
|
private serverKeyHandler;
|
|
28
30
|
private invalidPacket;
|
|
29
|
-
private
|
|
31
|
+
private listenLock;
|
|
32
|
+
private packetQueue;
|
|
33
|
+
listenPacket(data: string | [any[], boolean], code: number): void;
|
|
34
|
+
private isAsync;
|
|
35
|
+
private enqueuePacket;
|
|
30
36
|
private messageHandler;
|
|
31
37
|
protected listen(key: string, listener: (data: any[]) => void): void;
|
|
32
38
|
/**
|
|
@@ -62,9 +68,8 @@ export declare abstract class SonicWSCore implements Connection {
|
|
|
62
68
|
*/
|
|
63
69
|
on(tag: string, listener: (value: any[]) => void): void;
|
|
64
70
|
raw_send(data: Uint8Array): void;
|
|
65
|
-
setTimeout(call: () => void, time: number): number;
|
|
66
|
-
setInterval(call: () => void, time: number): number;
|
|
67
|
-
private setTimer;
|
|
71
|
+
setTimeout(call: () => void, time: number, callOnClose?: boolean): number;
|
|
72
|
+
setInterval(call: () => void, time: number, callOnClose?: boolean): number;
|
|
68
73
|
clearTimeout(id: number): void;
|
|
69
74
|
clearInterval(id: number): void;
|
|
70
75
|
close(code?: number, reason?: string): void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/*
|
|
3
|
-
* Copyright
|
|
3
|
+
* Copyright 2026 Lily (liwybloc)
|
|
4
4
|
*
|
|
5
5
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
6
|
* you may not use this file except in compliance with the License.
|
|
@@ -36,6 +36,8 @@ class SonicWSCore {
|
|
|
36
36
|
bufferHandler;
|
|
37
37
|
id = -1;
|
|
38
38
|
_timers = {};
|
|
39
|
+
asyncData = {};
|
|
40
|
+
asyncMap = {};
|
|
39
41
|
constructor(ws, bufferHandler) {
|
|
40
42
|
this.socket = ws;
|
|
41
43
|
this.listeners = {
|
|
@@ -52,7 +54,11 @@ class SonicWSCore {
|
|
|
52
54
|
this.socket.addEventListener('message', this.serverKeyHandler);
|
|
53
55
|
this.socket.addEventListener('close', (event) => {
|
|
54
56
|
this.listeners.close.forEach(listener => listener(event));
|
|
55
|
-
Object.values(this._timers)
|
|
57
|
+
for (const [id, callback, shouldCall] of Object.values(this._timers)) {
|
|
58
|
+
this.clearTimeout(id);
|
|
59
|
+
if (shouldCall)
|
|
60
|
+
callback(true);
|
|
61
|
+
}
|
|
56
62
|
});
|
|
57
63
|
this.bufferHandler = bufferHandler;
|
|
58
64
|
}
|
|
@@ -80,6 +86,13 @@ class SonicWSCore {
|
|
|
80
86
|
const skData = data.slice(valuesOff + ckLength, data.length);
|
|
81
87
|
this.serverPackets.holdPackets(Packets_1.Packet.deserializeAll(skData, true));
|
|
82
88
|
this.batcher.registerSendPackets(this.clientPackets, this);
|
|
89
|
+
for (const p of this.serverPackets.getPackets()) {
|
|
90
|
+
const key = this.serverPackets.getKey(p.tag);
|
|
91
|
+
this.asyncMap[key] = p.async;
|
|
92
|
+
if (p.async) {
|
|
93
|
+
this.asyncData[key] = [false, []];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
83
96
|
Object.keys(this.preListen).forEach(tag => this.preListen[tag].forEach(listener => {
|
|
84
97
|
// print the error to console without halting execution
|
|
85
98
|
if (!this.serverPackets.hasTag(tag))
|
|
@@ -97,11 +110,51 @@ class SonicWSCore {
|
|
|
97
110
|
console.error(listened);
|
|
98
111
|
throw new Error("An error occured with data from the server!! This is probably my fault.. make an issue at https://github.com/liwybloc/sonic-ws");
|
|
99
112
|
}
|
|
113
|
+
listenLock = false;
|
|
114
|
+
packetQueue = [];
|
|
100
115
|
listenPacket(data, code) {
|
|
101
116
|
const listeners = this.listeners.event[code];
|
|
102
117
|
if (!listeners)
|
|
103
|
-
return console.warn("Warn: No listener for packet " +
|
|
104
|
-
|
|
118
|
+
return console.warn("Warn: No listener for packet " + code);
|
|
119
|
+
this.enqueuePacket(data, code, listeners);
|
|
120
|
+
}
|
|
121
|
+
isAsync(code) {
|
|
122
|
+
return this.asyncMap[code];
|
|
123
|
+
}
|
|
124
|
+
async enqueuePacket(data, code, listeners) {
|
|
125
|
+
const isAsync = this.isAsync(code);
|
|
126
|
+
let locked;
|
|
127
|
+
let packetQueue;
|
|
128
|
+
let asyncData;
|
|
129
|
+
if (isAsync) {
|
|
130
|
+
asyncData = this.asyncData[code];
|
|
131
|
+
locked = asyncData[0];
|
|
132
|
+
packetQueue = asyncData[1];
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
locked = this.listenLock;
|
|
136
|
+
packetQueue = this.packetQueue;
|
|
137
|
+
}
|
|
138
|
+
if (locked) {
|
|
139
|
+
packetQueue.push([data, code]);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (isAsync)
|
|
143
|
+
asyncData[0] = true;
|
|
144
|
+
else
|
|
145
|
+
this.listenLock = true;
|
|
146
|
+
let currentData = data;
|
|
147
|
+
let currentCode = code;
|
|
148
|
+
while (true) {
|
|
149
|
+
await (0, PacketUtils_1.listenPacket)(currentData, listeners, this.invalidPacket);
|
|
150
|
+
if (packetQueue.length === 0)
|
|
151
|
+
break;
|
|
152
|
+
[currentData, currentCode] = packetQueue.shift();
|
|
153
|
+
}
|
|
154
|
+
if (isAsync)
|
|
155
|
+
asyncData[0] = false;
|
|
156
|
+
else
|
|
157
|
+
this.listenLock = false;
|
|
105
158
|
}
|
|
106
159
|
async messageHandler(event) {
|
|
107
160
|
const data = await this.bufferHandler(event);
|
|
@@ -192,22 +245,19 @@ class SonicWSCore {
|
|
|
192
245
|
this.listeners.send.forEach(d => d(data));
|
|
193
246
|
this.socket.send(data);
|
|
194
247
|
}
|
|
195
|
-
setTimeout(call, time) {
|
|
248
|
+
setTimeout(call, time, callOnClose = false) {
|
|
196
249
|
const timeout = setTimeout(() => {
|
|
197
250
|
call();
|
|
198
251
|
this.clearTimeout(timeout);
|
|
199
252
|
}, time);
|
|
200
|
-
this.
|
|
253
|
+
this._timers[timeout] = [timeout, call, callOnClose];
|
|
201
254
|
return timeout;
|
|
202
255
|
}
|
|
203
|
-
setInterval(call, time) {
|
|
256
|
+
setInterval(call, time, callOnClose = false) {
|
|
204
257
|
const interval = setInterval(call, time);
|
|
205
|
-
this.
|
|
258
|
+
this._timers[interval] = [interval, call, callOnClose];
|
|
206
259
|
return interval;
|
|
207
260
|
}
|
|
208
|
-
setTimer(id) {
|
|
209
|
-
this._timers[id] = id;
|
|
210
|
-
}
|
|
211
261
|
clearTimeout(id) {
|
|
212
262
|
clearTimeout(id);
|
|
213
263
|
delete this._timers[id];
|
|
@@ -5,8 +5,9 @@ export type ValidatorFunction = ((socket: SonicWSConnection, values: any) => boo
|
|
|
5
5
|
export type ConvertType<T> = T extends EnumPackage ? PacketType.ENUMS : T;
|
|
6
6
|
type ImpactType<T extends (PacketType | readonly PacketType[]), K> = T extends PacketType[] ? K[] : K;
|
|
7
7
|
export declare class Packet<T extends (PacketType | readonly PacketType[])> {
|
|
8
|
-
readonly tag: string;
|
|
9
8
|
defaultEnabled: boolean;
|
|
9
|
+
readonly tag: string;
|
|
10
|
+
readonly async: boolean;
|
|
10
11
|
readonly maxSize: number;
|
|
11
12
|
readonly minSize: number;
|
|
12
13
|
readonly type: T;
|
|
@@ -24,7 +25,7 @@ export declare class Packet<T extends (PacketType | readonly PacketType[])> {
|
|
|
24
25
|
private sendProcessor;
|
|
25
26
|
private validator;
|
|
26
27
|
processReceive: (data: Uint8Array, validationResult: any) => any;
|
|
27
|
-
processSend: (data: any[]) =>
|
|
28
|
+
processSend: (data: any[]) => Uint8Array;
|
|
28
29
|
validate: (data: Uint8Array) => boolean;
|
|
29
30
|
customValidator: ((socket: SonicWSConnection, ...values: any[]) => boolean) | null;
|
|
30
31
|
constructor(tag: string, schema: PacketSchema<T>, customValidator: ValidatorFunction, enabled: boolean, client: boolean);
|
|
@@ -44,10 +45,11 @@ export declare class PacketSchema<T extends (PacketType | readonly PacketType[])
|
|
|
44
45
|
enumData: EnumPackage[];
|
|
45
46
|
dontSpread: boolean;
|
|
46
47
|
autoFlatten: boolean;
|
|
48
|
+
async: boolean;
|
|
47
49
|
object: boolean;
|
|
48
50
|
constructor(object: boolean);
|
|
49
51
|
testObject(): this is PacketSchema<PacketType[]>;
|
|
50
|
-
static single<T extends PacketType | EnumPackage>(type: T, dataMax: number, dataMin: number, dontSpread: boolean, dataBatching: number, maxBatchSize: number, rateLimit: number): PacketSchema<ConvertType<T>>;
|
|
51
|
-
static object<T extends readonly (PacketType | EnumPackage)[]>(types: T, dataMaxes: number[], dataMins: number[], dontSpread: boolean, autoFlatten: boolean, dataBatching: number, maxBatchSize: number, rateLimit: number): PacketSchema<ConvertType<T[number]>[]>;
|
|
52
|
+
static single<T extends PacketType | EnumPackage>(type: T, dataMax: number, dataMin: number, dontSpread: boolean, dataBatching: number, maxBatchSize: number, rateLimit: number, async: boolean): PacketSchema<ConvertType<T>>;
|
|
53
|
+
static object<T extends readonly (PacketType | EnumPackage)[]>(types: T, dataMaxes: number[], dataMins: number[], dontSpread: boolean, autoFlatten: boolean, dataBatching: number, maxBatchSize: number, rateLimit: number, async: boolean): PacketSchema<ConvertType<T[number]>[]>;
|
|
52
54
|
}
|
|
53
55
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/*
|
|
3
|
-
* Copyright
|
|
3
|
+
* Copyright 2026 Lily (liwybloc)
|
|
4
4
|
*
|
|
5
5
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
6
|
* you may not use this file except in compliance with the License.
|
|
@@ -25,8 +25,9 @@ const PacketType_1 = require("./PacketType");
|
|
|
25
25
|
const BufferUtil_1 = require("../util/BufferUtil");
|
|
26
26
|
const StringUtil_1 = require("../util/StringUtil");
|
|
27
27
|
class Packet {
|
|
28
|
-
tag;
|
|
29
28
|
defaultEnabled;
|
|
29
|
+
tag;
|
|
30
|
+
async;
|
|
30
31
|
maxSize;
|
|
31
32
|
minSize;
|
|
32
33
|
type;
|
|
@@ -51,6 +52,7 @@ class Packet {
|
|
|
51
52
|
this.tag = tag;
|
|
52
53
|
this.defaultEnabled = enabled;
|
|
53
54
|
this.client = client;
|
|
55
|
+
this.async = schema.async;
|
|
54
56
|
this.enumData = schema.enumData;
|
|
55
57
|
this.rateLimit = schema.rateLimit;
|
|
56
58
|
this.dontSpread = schema.dontSpread;
|
|
@@ -85,7 +87,7 @@ class Packet {
|
|
|
85
87
|
this.sendProcessor = (0, PacketProcessors_1.createSendProcessor)(this.type);
|
|
86
88
|
}
|
|
87
89
|
this.processReceive = (data, validationResult) => this.receiveProcessor(data, validationResult, 0);
|
|
88
|
-
this.processSend = (data) => this.sendProcessor(data);
|
|
90
|
+
this.processSend = (data) => new Uint8Array(this.sendProcessor(data));
|
|
89
91
|
this.validate = (data) => this.validator(data, 0);
|
|
90
92
|
this.customValidator = customValidator;
|
|
91
93
|
}
|
|
@@ -119,6 +121,7 @@ class Packet {
|
|
|
119
121
|
const sharedData = [
|
|
120
122
|
this.tag.length, ...(0, StringUtil_1.processCharCodes)(this.tag),
|
|
121
123
|
this.dontSpread ? 1 : 0,
|
|
124
|
+
this.async ? 1 : 0,
|
|
122
125
|
this.dataBatching,
|
|
123
126
|
this.enumData.length, ...this.enumData.map(x => x.serialize()).flat(),
|
|
124
127
|
];
|
|
@@ -160,6 +163,8 @@ class Packet {
|
|
|
160
163
|
const tag = (0, BufferUtil_1.as8String)(data.slice(offset, offset += tagLength));
|
|
161
164
|
// then read dont spread, go up 1
|
|
162
165
|
const dontSpread = data[offset++] == 1;
|
|
166
|
+
// read async
|
|
167
|
+
const async = data[offset++] == 1;
|
|
163
168
|
// read batching, up 1
|
|
164
169
|
const dataBatching = data[offset++];
|
|
165
170
|
// read enum length, up 1
|
|
@@ -205,7 +210,7 @@ class Packet {
|
|
|
205
210
|
let index = 0;
|
|
206
211
|
const finalTypes = types.map(x => x == PacketType_1.PacketType.ENUMS ? enums[index++] : x); // convert enums to their enum packages
|
|
207
212
|
// make schema
|
|
208
|
-
const schema = PacketSchema.object(finalTypes, dataMaxes, dataMins, dontSpread, autoFlatten, dataBatching, -1, -1);
|
|
213
|
+
const schema = PacketSchema.object(finalTypes, dataMaxes, dataMins, dontSpread, autoFlatten, dataBatching, -1, -1, async);
|
|
209
214
|
return [
|
|
210
215
|
new Packet(tag, schema, null, false, client),
|
|
211
216
|
// +1 to go next
|
|
@@ -224,7 +229,7 @@ class Packet {
|
|
|
224
229
|
// do enum stuff
|
|
225
230
|
const finalType = type == PacketType_1.PacketType.ENUMS ? enums[0] : type; // convert enum to enum package
|
|
226
231
|
// make schema
|
|
227
|
-
const schema = PacketSchema.single(finalType, dataMax, dataMin, dontSpread, dataBatching, -1, -1);
|
|
232
|
+
const schema = PacketSchema.single(finalType, dataMax, dataMin, dontSpread, dataBatching, -1, -1, async);
|
|
228
233
|
return [
|
|
229
234
|
new Packet(tag, schema, null, false, client),
|
|
230
235
|
// +1 to go next
|
|
@@ -253,6 +258,7 @@ class PacketSchema {
|
|
|
253
258
|
enumData = [];
|
|
254
259
|
dontSpread = false;
|
|
255
260
|
autoFlatten = false;
|
|
261
|
+
async = false;
|
|
256
262
|
object;
|
|
257
263
|
constructor(object) {
|
|
258
264
|
this.object = object;
|
|
@@ -260,7 +266,7 @@ class PacketSchema {
|
|
|
260
266
|
testObject() {
|
|
261
267
|
return this.object;
|
|
262
268
|
}
|
|
263
|
-
static single(type, dataMax, dataMin, dontSpread, dataBatching, maxBatchSize, rateLimit) {
|
|
269
|
+
static single(type, dataMax, dataMin, dontSpread, dataBatching, maxBatchSize, rateLimit, async) {
|
|
264
270
|
const schema = new PacketSchema(false);
|
|
265
271
|
if (typeof type == 'number') {
|
|
266
272
|
schema.type = type;
|
|
@@ -277,9 +283,10 @@ class PacketSchema {
|
|
|
277
283
|
schema.dataBatching = dataBatching;
|
|
278
284
|
schema.maxBatchSize = maxBatchSize;
|
|
279
285
|
schema.rateLimit = rateLimit;
|
|
286
|
+
schema.async = async;
|
|
280
287
|
return schema;
|
|
281
288
|
}
|
|
282
|
-
static object(types, dataMaxes, dataMins, dontSpread, autoFlatten, dataBatching, maxBatchSize, rateLimit) {
|
|
289
|
+
static object(types, dataMaxes, dataMins, dontSpread, autoFlatten, dataBatching, maxBatchSize, rateLimit, async) {
|
|
283
290
|
if (types.length != dataMaxes.length || types.length != dataMins.length)
|
|
284
291
|
throw new Error("There is an inbalance between the amount of types, data maxes, and data mins!");
|
|
285
292
|
const schema = new PacketSchema(true);
|
|
@@ -300,6 +307,7 @@ class PacketSchema {
|
|
|
300
307
|
schema.dataBatching = dataBatching;
|
|
301
308
|
schema.maxBatchSize = maxBatchSize;
|
|
302
309
|
schema.rateLimit = rateLimit;
|
|
310
|
+
schema.async = async;
|
|
303
311
|
return schema;
|
|
304
312
|
}
|
|
305
313
|
}
|
|
@@ -19,11 +19,17 @@ export declare class SonicWSConnection implements Connection {
|
|
|
19
19
|
handshakeComplete: boolean;
|
|
20
20
|
/** The index of the connection; unique for all connected, recycles old disconnected ids. Should be safe for INTS_C unless you have more than 27,647 connected at once. */
|
|
21
21
|
id: number;
|
|
22
|
-
_timers: Record<number, number>;
|
|
22
|
+
_timers: Record<number, [number, (closed: boolean) => void, boolean]>;
|
|
23
|
+
private asyncMap;
|
|
24
|
+
private asyncData;
|
|
25
|
+
private closed;
|
|
23
26
|
constructor(socket: WS.WebSocket, host: SonicWSServer, id: number, handshakePacket: string | null, clientRateLimit: number, serverRateLimit: number);
|
|
24
27
|
private parseData;
|
|
25
28
|
private handshakeHandler;
|
|
26
29
|
private invalidPacket;
|
|
30
|
+
private isAsync;
|
|
31
|
+
private listenLock;
|
|
32
|
+
private packetQueue;
|
|
27
33
|
private listenPacket;
|
|
28
34
|
private messageHandler;
|
|
29
35
|
/**
|
|
@@ -55,7 +61,7 @@ export declare class SonicWSConnection implements Connection {
|
|
|
55
61
|
/**
|
|
56
62
|
* For internal use.
|
|
57
63
|
*/
|
|
58
|
-
send_processed(code: number, data:
|
|
64
|
+
send_processed(code: number, data: Uint8Array, packet: Packet<any>): void;
|
|
59
65
|
/**
|
|
60
66
|
* Sends a packet with the tag and values
|
|
61
67
|
* @param tag The tag to send
|
|
@@ -80,8 +86,8 @@ export declare class SonicWSConnection implements Connection {
|
|
|
80
86
|
togglePrint(): void;
|
|
81
87
|
raw_send(data: Uint8Array): void;
|
|
82
88
|
close(code?: number, reason?: string | Buffer): void;
|
|
83
|
-
setTimeout(call: () => void, time: number): number;
|
|
84
|
-
setInterval(call: () => void, time: number): number;
|
|
89
|
+
setTimeout(call: () => void, time: number, callOnClose?: boolean): number;
|
|
90
|
+
setInterval(call: () => void, time: number, callOnClose?: boolean): number;
|
|
85
91
|
clearTimeout(id: number): void;
|
|
86
92
|
clearInterval(id: number): void;
|
|
87
93
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/*
|
|
3
|
-
* Copyright
|
|
3
|
+
* Copyright 2026 Lily (liwybloc)
|
|
4
4
|
*
|
|
5
5
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
6
|
* you may not use this file except in compliance with the License.
|
|
@@ -80,15 +80,22 @@ class SonicWSConnection {
|
|
|
80
80
|
/** The index of the connection; unique for all connected, recycles old disconnected ids. Should be safe for INTS_C unless you have more than 27,647 connected at once. */
|
|
81
81
|
id;
|
|
82
82
|
_timers = {};
|
|
83
|
+
asyncMap = {};
|
|
84
|
+
asyncData = {};
|
|
85
|
+
closed = false;
|
|
83
86
|
constructor(socket, host, id, handshakePacket, clientRateLimit, serverRateLimit) {
|
|
84
87
|
this.socket = socket;
|
|
85
88
|
this.host = host;
|
|
86
89
|
this.id = id;
|
|
87
90
|
this.handshakePacket = handshakePacket;
|
|
88
91
|
this.listeners = {};
|
|
89
|
-
for (const
|
|
90
|
-
this.listeners[
|
|
91
|
-
|
|
92
|
+
for (const tag of host.clientPackets.getTags()) {
|
|
93
|
+
this.listeners[tag] = [];
|
|
94
|
+
const pack = host.clientPackets.getPacket(tag);
|
|
95
|
+
this.enabledPackets[tag] = pack.defaultEnabled;
|
|
96
|
+
this.asyncMap[tag] = pack.async;
|
|
97
|
+
if (pack.async)
|
|
98
|
+
this.asyncData[tag] = [false, []];
|
|
92
99
|
}
|
|
93
100
|
this.setInterval = this.setInterval.bind(this);
|
|
94
101
|
this.batcher = new BatchHelper_1.BatchHelper();
|
|
@@ -108,7 +115,12 @@ class SonicWSConnection {
|
|
|
108
115
|
this.socket.addEventListener('message', this.handshakeLambda);
|
|
109
116
|
}
|
|
110
117
|
this.socket.on('close', () => {
|
|
111
|
-
|
|
118
|
+
this.closed = true;
|
|
119
|
+
for (const [id, callback, shouldCall] of Object.values(this._timers)) {
|
|
120
|
+
this.clearTimeout(id);
|
|
121
|
+
if (shouldCall)
|
|
122
|
+
callback(true);
|
|
123
|
+
}
|
|
112
124
|
});
|
|
113
125
|
}
|
|
114
126
|
parseData(event) {
|
|
@@ -157,8 +169,45 @@ class SonicWSConnection {
|
|
|
157
169
|
console.log("Closure cause", listened);
|
|
158
170
|
this.socket.close(4003, listened);
|
|
159
171
|
}
|
|
160
|
-
|
|
161
|
-
|
|
172
|
+
isAsync(tag) {
|
|
173
|
+
return this.asyncMap[tag];
|
|
174
|
+
}
|
|
175
|
+
listenLock = false;
|
|
176
|
+
packetQueue = [];
|
|
177
|
+
async listenPacket(data, tag) {
|
|
178
|
+
if (this.closed)
|
|
179
|
+
return;
|
|
180
|
+
const isAsync = this.isAsync(tag);
|
|
181
|
+
let locked, packetQueue, asyncData;
|
|
182
|
+
if (isAsync) {
|
|
183
|
+
asyncData = this.asyncData[tag];
|
|
184
|
+
locked = asyncData[0];
|
|
185
|
+
packetQueue = asyncData[1];
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
locked = this.listenLock;
|
|
189
|
+
packetQueue = this.packetQueue;
|
|
190
|
+
}
|
|
191
|
+
if (locked) {
|
|
192
|
+
packetQueue.push([data, tag]);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (isAsync)
|
|
196
|
+
asyncData[0] = true;
|
|
197
|
+
else
|
|
198
|
+
this.listenLock = true;
|
|
199
|
+
let currentData = data;
|
|
200
|
+
let currentTag = tag;
|
|
201
|
+
while (true) {
|
|
202
|
+
await (0, PacketUtils_1.listenPacket)(currentData, this.listeners[currentTag], this.invalidPacket);
|
|
203
|
+
if (packetQueue.length === 0)
|
|
204
|
+
break;
|
|
205
|
+
[currentData, currentTag] = packetQueue.shift();
|
|
206
|
+
}
|
|
207
|
+
if (isAsync)
|
|
208
|
+
asyncData[0] = false;
|
|
209
|
+
else
|
|
210
|
+
this.listenLock = false;
|
|
162
211
|
}
|
|
163
212
|
messageHandler(data) {
|
|
164
213
|
if (data == null)
|
|
@@ -196,7 +245,7 @@ class SonicWSConnection {
|
|
|
196
245
|
* @returns If it's closed or not
|
|
197
246
|
*/
|
|
198
247
|
isClosed() {
|
|
199
|
-
return this.socket.readyState == WS.CLOSED;
|
|
248
|
+
return this.closed || this.socket.readyState == WS.CLOSED;
|
|
200
249
|
}
|
|
201
250
|
/**
|
|
202
251
|
* Listens for when the connection closes
|
|
@@ -270,19 +319,20 @@ class SonicWSConnection {
|
|
|
270
319
|
this.socket.send(data);
|
|
271
320
|
}
|
|
272
321
|
close(code = 1000, reason) {
|
|
322
|
+
this.closed = true;
|
|
273
323
|
this.socket.close(code, reason);
|
|
274
324
|
}
|
|
275
|
-
setTimeout(call, time) {
|
|
325
|
+
setTimeout(call, time, callOnClose = false) {
|
|
276
326
|
const timeout = setTimeout(() => {
|
|
277
327
|
call();
|
|
278
328
|
this.clearTimeout(timeout);
|
|
279
329
|
}, time);
|
|
280
|
-
this._timers[timeout] = timeout;
|
|
330
|
+
this._timers[timeout] = [timeout, call, callOnClose];
|
|
281
331
|
return timeout;
|
|
282
332
|
}
|
|
283
|
-
setInterval(call, time) {
|
|
333
|
+
setInterval(call, time, callOnClose = false) {
|
|
284
334
|
const interval = setInterval(call, time);
|
|
285
|
-
this._timers[interval] = interval;
|
|
335
|
+
this._timers[interval] = [interval, call, callOnClose];
|
|
286
336
|
return interval;
|
|
287
337
|
}
|
|
288
338
|
clearTimeout(id) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/*
|
|
3
|
-
* Copyright
|
|
3
|
+
* Copyright 2026 Lily (liwybloc)
|
|
4
4
|
*
|
|
5
5
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
6
|
* you may not use this file except in compliance with the License.
|
|
@@ -94,6 +94,11 @@ class SonicWSServer {
|
|
|
94
94
|
this.connections.splice(this.connections.indexOf(sonicConnection), 1);
|
|
95
95
|
delete this.connectionMap[sonicConnection.id];
|
|
96
96
|
this.availableIds.push(sonicConnection.id);
|
|
97
|
+
if (this.tags.has(sonicConnection)) {
|
|
98
|
+
for (const tag of this.tags.get(sonicConnection))
|
|
99
|
+
this.tagsInv.get(tag)?.delete(sonicConnection);
|
|
100
|
+
this.tags.delete(sonicConnection);
|
|
101
|
+
}
|
|
97
102
|
});
|
|
98
103
|
});
|
|
99
104
|
fetch('https://raw.githubusercontent.com/liwybloc/sonic-ws/refs/heads/main/release/version')
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare function toPacketBuffer(code: number, data:
|
|
1
|
+
export declare function toPacketBuffer(code: number, data: Uint8Array): Uint8Array;
|
|
2
2
|
export declare function splitBuffer(arr: Uint8Array, x: number): number[][];
|
|
3
3
|
export declare function as8String(data: Uint8Array): string;
|
|
4
4
|
export declare function as16String(data: Uint8Array): string;
|
|
@@ -10,6 +10,6 @@ export declare class BatchHelper {
|
|
|
10
10
|
registerSendPackets(packetHolder: PacketHolder, conn: Connection): void;
|
|
11
11
|
private initiateBatch;
|
|
12
12
|
private startBatch;
|
|
13
|
-
batchPacket(code: number, data:
|
|
13
|
+
batchPacket(code: number, data: Uint8Array): void;
|
|
14
14
|
static unravelBatch(packet: Packet<any>, data: Uint8Array, socket: SonicWSConnection | null): any[] | string;
|
|
15
15
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/*
|
|
3
|
-
* Copyright
|
|
3
|
+
* Copyright 2026 Lily (liwybloc)
|
|
4
4
|
*
|
|
5
5
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
6
|
* you may not use this file except in compliance with the License.
|
|
@@ -39,10 +39,10 @@ class BatchHelper {
|
|
|
39
39
|
}
|
|
40
40
|
startBatch(code) {
|
|
41
41
|
this.batchTimeouts[code] = this.conn.setTimeout(() => {
|
|
42
|
-
this.conn.raw_send((0, BufferUtil_1.toPacketBuffer)(code, this.batchedData[code]));
|
|
42
|
+
this.conn.raw_send((0, BufferUtil_1.toPacketBuffer)(code, new Uint8Array(this.batchedData[code])));
|
|
43
43
|
this.batchedData[code] = [];
|
|
44
44
|
delete this.batchTimeouts[code];
|
|
45
|
-
}, this.batchTimes[code]);
|
|
45
|
+
}, this.batchTimes[code], false);
|
|
46
46
|
}
|
|
47
47
|
batchPacket(code, data) {
|
|
48
48
|
const batch = this.batchedData[code];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/*
|
|
3
|
-
* Copyright
|
|
3
|
+
* Copyright 2026 Lily (liwybloc)
|
|
4
4
|
*
|
|
5
5
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
6
|
* you may not use this file except in compliance with the License.
|
|
@@ -43,7 +43,6 @@ class PacketHolder {
|
|
|
43
43
|
this.packetMap = {};
|
|
44
44
|
if (!packets)
|
|
45
45
|
return;
|
|
46
|
-
this.packets = packets;
|
|
47
46
|
this.holdPackets(packets);
|
|
48
47
|
}
|
|
49
48
|
/** Assigns a new unique key to a tag */
|
|
@@ -57,6 +56,7 @@ class PacketHolder {
|
|
|
57
56
|
* @param packets Array of packets to register
|
|
58
57
|
*/
|
|
59
58
|
holdPackets(packets) {
|
|
59
|
+
this.packets = packets;
|
|
60
60
|
for (const packet of packets)
|
|
61
61
|
this.createKey(packet.tag), this.packetMap[packet.tag] = packet;
|
|
62
62
|
}
|
|
@@ -9,14 +9,14 @@ import { EnumPackage } from "../enums/EnumType";
|
|
|
9
9
|
* @param values The values
|
|
10
10
|
* @returns The indexed code, the data, and the packet schema
|
|
11
11
|
*/
|
|
12
|
-
export declare function processPacket(packets: PacketHolder, tag: string, values: any[]): [code: number, data:
|
|
12
|
+
export declare function processPacket(packets: PacketHolder, tag: string, values: any[]): [code: number, data: Uint8Array, packet: Packet<any>];
|
|
13
13
|
/**
|
|
14
14
|
* Calls the listener for a packet with error callback
|
|
15
15
|
* @param listened The listened data
|
|
16
16
|
* @param listeners The listeners to run
|
|
17
17
|
* @param errorCB The callback if something goes wrong
|
|
18
18
|
*/
|
|
19
|
-
export declare function listenPacket(listened: string | [any[], boolean], listeners: ((...values: any) => void)[], errorCB: (data: string) => void): void
|
|
19
|
+
export declare function listenPacket(listened: string | [any[], boolean], listeners: ((...values: any) => void | Promise<void>)[], errorCB: (data: string) => void): Promise<void>;
|
|
20
20
|
/** Valid packet type */
|
|
21
21
|
export type ArguableType = PacketType | EnumPackage;
|
|
22
22
|
/** Shared packet setting types */
|
|
@@ -47,6 +47,8 @@ export type SharedPacketSettings = {
|
|
|
47
47
|
enabled?: boolean;
|
|
48
48
|
/** A validation function that is called whenever data is received. Return true for success, return false to kick socket. */
|
|
49
49
|
validator?: ValidatorFunction;
|
|
50
|
+
/** If this is true, other packets will be processed even if this one isn't finished; it'll still prevent it from calling twice before this finishes though. Defaults to false. */
|
|
51
|
+
async?: boolean;
|
|
50
52
|
};
|
|
51
53
|
/** Settings for single-typed packets */
|
|
52
54
|
export type SinglePacketSettings = SharedPacketSettings & {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/*
|
|
3
|
-
* Copyright
|
|
3
|
+
* Copyright 2026 Lily (liwybloc)
|
|
4
4
|
*
|
|
5
5
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
6
|
* you may not use this file except in compliance with the License.
|
|
@@ -65,7 +65,7 @@ function processPacket(packets, tag, values) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
-
return [code, values.length > 0 ? packet.processSend(values) : [], packet];
|
|
68
|
+
return [code, values.length > 0 ? packet.processSend(values) : new Uint8Array([]), packet];
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
71
|
* Calls the listener for a packet with error callback
|
|
@@ -73,16 +73,21 @@ function processPacket(packets, tag, values) {
|
|
|
73
73
|
* @param listeners The listeners to run
|
|
74
74
|
* @param errorCB The callback if something goes wrong
|
|
75
75
|
*/
|
|
76
|
-
function listenPacket(listened, listeners, errorCB) {
|
|
77
|
-
|
|
78
|
-
if (typeof listened == 'string')
|
|
76
|
+
async function listenPacket(listened, listeners, errorCB) {
|
|
77
|
+
if (typeof listened === 'string')
|
|
79
78
|
return errorCB(listened);
|
|
80
79
|
const [processed, flatten] = listened;
|
|
81
80
|
try {
|
|
82
|
-
if (flatten && Array.isArray(processed))
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
if (flatten && Array.isArray(processed)) {
|
|
82
|
+
for (const l of listeners) {
|
|
83
|
+
await l(...processed);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
for (const l of listeners) {
|
|
88
|
+
await l(processed);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
86
91
|
}
|
|
87
92
|
catch (err) {
|
|
88
93
|
errorCB(err);
|
|
@@ -127,7 +132,9 @@ function clampDataMin(dataMin, dataMax) {
|
|
|
127
132
|
* @throws {Error} If the `type` is invalid.
|
|
128
133
|
*/
|
|
129
134
|
function CreatePacket(settings) {
|
|
130
|
-
let { tag, type = PacketType_1.PacketType.NONE, dataMax = 1, dataMin, noDataRange = false, dontSpread = false, validator = null, dataBatching = 0, maxBatchSize = 10, rateLimit = 0, enabled = true } = settings;
|
|
135
|
+
let { tag, type = PacketType_1.PacketType.NONE, dataMax = 1, dataMin = 1, noDataRange = false, dontSpread = false, validator = null, dataBatching = 0, maxBatchSize = 10, rateLimit = 0, enabled = true, async = false } = settings;
|
|
136
|
+
if (!tag)
|
|
137
|
+
throw new Error("Tag not selected!");
|
|
131
138
|
if (noDataRange) {
|
|
132
139
|
dataMin = 0;
|
|
133
140
|
dataMax = MAX_DATA_MAX;
|
|
@@ -137,7 +144,7 @@ function CreatePacket(settings) {
|
|
|
137
144
|
if (isInvalidType(type)) {
|
|
138
145
|
throw new Error(`Invalid packet type: ${type}`);
|
|
139
146
|
}
|
|
140
|
-
const schema = Packets_1.PacketSchema.single(type, clampDataMax(dataMax), clampDataMin(dataMin, dataMax), dontSpread, dataBatching, maxBatchSize, rateLimit);
|
|
147
|
+
const schema = Packets_1.PacketSchema.single(type, clampDataMax(dataMax), clampDataMin(dataMin, dataMax), dontSpread, dataBatching, maxBatchSize, rateLimit, async);
|
|
141
148
|
return new Packets_1.Packet(tag, schema, validator, enabled, false);
|
|
142
149
|
}
|
|
143
150
|
/**
|
|
@@ -148,7 +155,11 @@ function CreatePacket(settings) {
|
|
|
148
155
|
* @throws {Error} If any type in `types` is invalid.
|
|
149
156
|
*/
|
|
150
157
|
function CreateObjPacket(settings) {
|
|
151
|
-
let { tag, types, dataMaxes, dataMins, noDataRange = false, dontSpread = false, autoFlatten = false, validator = null, dataBatching = 0, maxBatchSize = 10, rateLimit = 0, enabled = true } = settings;
|
|
158
|
+
let { tag, types = [], dataMaxes, dataMins, noDataRange = false, dontSpread = false, autoFlatten = false, validator = null, dataBatching = 0, maxBatchSize = 10, rateLimit = 0, enabled = true, async = false } = settings;
|
|
159
|
+
if (!tag)
|
|
160
|
+
throw new Error("Tag not selected!");
|
|
161
|
+
if (types.length == 0)
|
|
162
|
+
throw new Error("Types is set to 0 length");
|
|
152
163
|
for (const type of types) {
|
|
153
164
|
if (!isInvalidType(type))
|
|
154
165
|
continue;
|
|
@@ -170,7 +181,7 @@ function CreateObjPacket(settings) {
|
|
|
170
181
|
}
|
|
171
182
|
const clampedDataMaxes = dataMaxes.map(clampDataMax);
|
|
172
183
|
const clampedDataMins = dataMins.map((m, i) => types[i] == PacketType_1.PacketType.NONE ? 0 : clampDataMin(m, clampedDataMaxes[i]));
|
|
173
|
-
const schema = Packets_1.PacketSchema.object(types, clampedDataMaxes, clampedDataMins, dontSpread, autoFlatten, dataBatching, maxBatchSize, rateLimit);
|
|
184
|
+
const schema = Packets_1.PacketSchema.object(types, clampedDataMaxes, clampedDataMins, dontSpread, autoFlatten, dataBatching, maxBatchSize, rateLimit, async);
|
|
174
185
|
return new Packets_1.Packet(tag, schema, validator, enabled, false);
|
|
175
186
|
}
|
|
176
187
|
/**
|
|
@@ -180,7 +191,7 @@ function CreateObjPacket(settings) {
|
|
|
180
191
|
* @returns The constructed packet structure data.
|
|
181
192
|
*/
|
|
182
193
|
function CreateEnumPacket(settings) {
|
|
183
|
-
const { tag, enumData, dataMax = 1, dataMin = 0, noDataRange = false, dontSpread = false, validator = null, dataBatching = 0, maxBatchSize = 10, rateLimit = 0, enabled = true } = settings;
|
|
194
|
+
const { tag, enumData, dataMax = 1, dataMin = 0, noDataRange = false, dontSpread = false, validator = null, dataBatching = 0, maxBatchSize = 10, rateLimit = 0, enabled = true, async = false } = settings;
|
|
184
195
|
return CreatePacket({
|
|
185
196
|
tag: tag,
|
|
186
197
|
type: enumData,
|
|
@@ -193,6 +204,7 @@ function CreateEnumPacket(settings) {
|
|
|
193
204
|
maxBatchSize,
|
|
194
205
|
rateLimit,
|
|
195
206
|
enabled,
|
|
207
|
+
async,
|
|
196
208
|
});
|
|
197
209
|
}
|
|
198
210
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonic-ws",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Ultra-lightweight, high-performance, and bandwidth efficient websocket library",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"author": "lily",
|
|
25
25
|
"license": "Apache-2.0",
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"ws": "^8.18.2"
|
|
27
|
+
"ws": "^8.18.2",
|
|
28
|
+
"zstd-codec": "^0.1.5"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"@types/node": "^24.2.1",
|