sonic-ws 1.1.0 → 1.1.1-patch

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/README.md CHANGED
@@ -22,6 +22,7 @@ Security:
22
22
  - Built-in ability for handshake packets, preventing repetitive initiation checks in listeners (for example, removes if(!init) everywhere)
23
23
  - Built-in rate limiting for packets; ability for global send & receive, alongside per-packet rate limiting
24
24
  - Built-in disabling & enabling of packets to prevent abuse
25
+ - Prevents any race conditions; your callbacks will not be called until they finish.
25
26
 
26
27
  Performance & Scaling:
27
28
  - Can handle very large packets in microseconds
package/dist/version.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Current protocol version */
2
- export declare const VERSION = 14;
2
+ export declare const VERSION = 15;
3
3
  /** Server data suffix */
4
4
  export declare const SERVER_SUFFIX = "SWS";
5
5
  /** Server data suffix in array */
package/dist/version.js CHANGED
@@ -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 = 14;
21
+ exports.VERSION = 15;
22
22
  /** Server data suffix */
23
23
  exports.SERVER_SUFFIX = "SWS";
24
24
  /** Server data suffix in array */
@@ -22,11 +22,17 @@ export declare abstract class SonicWSCore implements Connection {
22
22
  private bufferHandler;
23
23
  id: number;
24
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 listenPacket;
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
  /**
@@ -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 = {
@@ -84,6 +86,13 @@ class SonicWSCore {
84
86
  const skData = data.slice(valuesOff + ckLength, data.length);
85
87
  this.serverPackets.holdPackets(Packets_1.Packet.deserializeAll(skData, true));
86
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
+ }
87
96
  Object.keys(this.preListen).forEach(tag => this.preListen[tag].forEach(listener => {
88
97
  // print the error to console without halting execution
89
98
  if (!this.serverPackets.hasTag(tag))
@@ -101,11 +110,51 @@ class SonicWSCore {
101
110
  console.error(listened);
102
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");
103
112
  }
113
+ listenLock = false;
114
+ packetQueue = [];
104
115
  listenPacket(data, code) {
105
116
  const listeners = this.listeners.event[code];
106
117
  if (!listeners)
107
- return console.warn("Warn: No listener for packet " + this.serverPackets.getTag(code));
108
- (0, PacketUtils_1.listenPacket)(data, listeners, this.invalidPacket);
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;
109
158
  }
110
159
  async messageHandler(event) {
111
160
  const data = await this.bufferHandler(event);
@@ -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;
@@ -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 {};
@@ -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;
@@ -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
  }
@@ -20,11 +20,16 @@ export declare class SonicWSConnection implements Connection {
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
22
  _timers: Record<number, [number, (closed: boolean) => void, boolean]>;
23
+ private asyncMap;
24
+ private asyncData;
23
25
  private closed;
24
26
  constructor(socket: WS.WebSocket, host: SonicWSServer, id: number, handshakePacket: string | null, clientRateLimit: number, serverRateLimit: number);
25
27
  private parseData;
26
28
  private handshakeHandler;
27
29
  private invalidPacket;
30
+ private isAsync;
31
+ private listenLock;
32
+ private packetQueue;
28
33
  private listenPacket;
29
34
  private messageHandler;
30
35
  /**
@@ -80,6 +80,8 @@ 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 = {};
83
85
  closed = false;
84
86
  constructor(socket, host, id, handshakePacket, clientRateLimit, serverRateLimit) {
85
87
  this.socket = socket;
@@ -87,9 +89,13 @@ class SonicWSConnection {
87
89
  this.id = id;
88
90
  this.handshakePacket = handshakePacket;
89
91
  this.listeners = {};
90
- for (const key of host.clientPackets.getTags()) {
91
- this.listeners[key] = [];
92
- this.enabledPackets[key] = host.clientPackets.getPacket(key).defaultEnabled;
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, []];
93
99
  }
94
100
  this.setInterval = this.setInterval.bind(this);
95
101
  this.batcher = new BatchHelper_1.BatchHelper();
@@ -163,10 +169,45 @@ class SonicWSConnection {
163
169
  console.log("Closure cause", listened);
164
170
  this.socket.close(4003, listened);
165
171
  }
166
- listenPacket(data, tag) {
172
+ isAsync(tag) {
173
+ return this.asyncMap[tag];
174
+ }
175
+ listenLock = false;
176
+ packetQueue = [];
177
+ async listenPacket(data, tag) {
167
178
  if (this.closed)
168
179
  return;
169
- (0, PacketUtils_1.listenPacket)(data, this.listeners[tag], this.invalidPacket);
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;
170
211
  }
171
212
  messageHandler(data) {
172
213
  if (data == null)
@@ -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')
@@ -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
  }
@@ -16,7 +16,7 @@ export declare function processPacket(packets: PacketHolder, tag: string, values
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 & {
@@ -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
- // if invalid then ignore it and call back
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
- listeners.forEach(l => l(...processed));
84
- else
85
- listeners.forEach(l => l(processed));
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,7 @@ 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 = 1, 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;
131
136
  if (!tag)
132
137
  throw new Error("Tag not selected!");
133
138
  if (noDataRange) {
@@ -139,7 +144,7 @@ function CreatePacket(settings) {
139
144
  if (isInvalidType(type)) {
140
145
  throw new Error(`Invalid packet type: ${type}`);
141
146
  }
142
- 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);
143
148
  return new Packets_1.Packet(tag, schema, validator, enabled, false);
144
149
  }
145
150
  /**
@@ -150,7 +155,7 @@ function CreatePacket(settings) {
150
155
  * @throws {Error} If any type in `types` is invalid.
151
156
  */
152
157
  function CreateObjPacket(settings) {
153
- 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;
154
159
  if (!tag)
155
160
  throw new Error("Tag not selected!");
156
161
  if (types.length == 0)
@@ -176,7 +181,7 @@ function CreateObjPacket(settings) {
176
181
  }
177
182
  const clampedDataMaxes = dataMaxes.map(clampDataMax);
178
183
  const clampedDataMins = dataMins.map((m, i) => types[i] == PacketType_1.PacketType.NONE ? 0 : clampDataMin(m, clampedDataMaxes[i]));
179
- 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);
180
185
  return new Packets_1.Packet(tag, schema, validator, enabled, false);
181
186
  }
182
187
  /**
@@ -186,7 +191,7 @@ function CreateObjPacket(settings) {
186
191
  * @returns The constructed packet structure data.
187
192
  */
188
193
  function CreateEnumPacket(settings) {
189
- 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;
190
195
  return CreatePacket({
191
196
  tag: tag,
192
197
  type: enumData,
@@ -199,6 +204,7 @@ function CreateEnumPacket(settings) {
199
204
  maxBatchSize,
200
205
  rateLimit,
201
206
  enabled,
207
+ async,
202
208
  });
203
209
  }
204
210
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonic-ws",
3
- "version": "1.1.0",
3
+ "version": "1.1.1-patch",
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",