@linebridge/engine-neo 1.0.7 → 1.2.2

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.
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from "tseep/lib/ee-safe.js";
2
- import { Readable, Writable } from "stream";
2
+ import { Readable, Writable } from "node:stream";
3
3
  import { array_buffer_to_string } from "../utils.js";
4
4
  const FRAGMENTS = {
5
5
  FIRST: "FIRST",
@@ -11,78 +11,37 @@ export default class Websocket extends EventEmitter {
11
11
  #ip;
12
12
  #context;
13
13
  #stream;
14
+ #buffered_fragment;
14
15
  #closed = false;
15
16
  constructor(ws) {
16
- // Initialize event emitter
17
17
  super();
18
- // Parse information about websocket connection
19
18
  this.#ws = ws;
20
19
  this.#context = ws.context || {};
21
20
  this.#ip = array_buffer_to_string(ws.getRemoteAddressAsText());
22
21
  }
23
- /* EventEmitter overrides */
24
- /**
25
- * Binds an event listener to this `Websocket` instance.
26
- * See the Node.js `EventEmitter` documentation for more details on this extended method.
27
- * @param {('message'|'close'|'drain'|'ping'|'pong')} eventName
28
- * @param {Function} listener
29
- * @returns {Websocket}
30
- */
31
- on(eventName, listener) {
32
- // Pass all events to EventEmitter
33
- super.on(eventName, listener);
34
- return this;
35
- }
36
- /**
37
- * Binds a `one-time` event listener to this `Websocket` instance.
38
- * See the Node.js `EventEmitter` documentation for more details on this extended method.
39
- * @param {('message'|'close'|'drain'|'ping'|'pong')} eventName
40
- * @param {Function} listener
41
- * @returns {Websocket}
42
- */
43
- once(eventName, listener) {
44
- // Pass all events to EventEmitter
45
- super.once(eventName, listener);
46
- return this;
47
- }
48
22
  /**
49
- * Alias of uWS.cork() method. Accepts a callback with multiple operations for network efficiency.
50
- *
51
- * @param {Function} callback
52
- * @returns {Websocket}
23
+ * Alias of uws cork, used to batch multiple operations for efficiency.
53
24
  */
54
25
  atomic(callback) {
55
26
  return this.#ws ? this.#ws.cork(callback) : this;
56
27
  }
57
28
  /**
58
- * Sends a message to websocket connection.
59
- * Returns true if message was sent successfully.
60
- * Returns false if message was not sent due to buil up backpressure.
61
- *
62
- * @param {String|Buffer|ArrayBuffer} message
63
- * @param {Boolean=} is_binary
64
- * @param {Boolean=} compress
65
- * @returns {Boolean}
29
+ * Sends a message over the connection.
30
+ * Returns the uws send status, or false when the socket is closed.
66
31
  */
67
32
  send(message, is_binary, compress) {
68
- // Send message through uWS connection
69
33
  if (this.#ws)
70
34
  return this.#ws.send(message, is_binary, compress);
71
35
  return false;
72
36
  }
73
37
  /**
74
38
  * Sends a ping control message.
75
- * Returns Boolean depending on backpressure similar to send().
76
- *
77
- * @param {String|Buffer|ArrayBuffer=} message
78
- * @returns {Boolean}
79
39
  */
80
40
  ping(message) {
81
- // Send ping OPCODE message through uWS connection
82
41
  return this.#ws ? this.#ws.ping(message) : false;
83
42
  }
84
43
  /**
85
- * Destroys this polyfill Websocket component and derefernces the underlying ws object
44
+ * Destroys this polyfill and dereferences the underlying socket.
86
45
  * @private
87
46
  */
88
47
  _destroy() {
@@ -90,74 +49,47 @@ export default class Websocket extends EventEmitter {
90
49
  this.#closed = true;
91
50
  }
92
51
  /**
93
- * Gracefully closes websocket connection by sending specified code and short message.
94
- *
95
- * @param {Number=} code
96
- * @param {(String|Buffer|ArrayBuffer)=} message
52
+ * Gracefully closes the connection, sending an optional code and message.
97
53
  */
98
54
  close(code, message) {
99
- // Close websocket using uWS.end() method which gracefully closes connections
100
55
  if (this.#ws)
101
56
  this.#ws.end(code, message);
102
57
  }
103
58
  /**
104
- * Forcefully closes websocket connection.
105
- * No websocket close code/message is sent.
106
- * This will immediately emit the 'close' event.
59
+ * Forcefully closes the connection without a close message.
107
60
  */
108
61
  destroy() {
109
62
  if (this.#ws)
110
63
  this.#ws.close();
111
64
  }
112
65
  /**
113
- * Returns whether this `Websocket` is subscribed to the specified topic.
114
- *
115
- * @param {String} topic
116
- * @returns {Boolean}
66
+ * Returns whether this connection is subscribed to the topic.
117
67
  */
118
68
  is_subscribed(topic) {
119
69
  return this.#ws ? this.#ws.isSubscribed(topic) : false;
120
70
  }
121
71
  /**
122
- * Subscribe to a topic in MQTT syntax.
123
- * MQTT syntax includes things like "root/child/+/grandchild" where "+" is a wildcard and "root/#" where "#" is a terminating wildcard.
124
- *
125
- * @param {String} topic
126
- * @returns {Boolean}
72
+ * Subscribes to a topic in mqtt syntax.
127
73
  */
128
74
  subscribe(topic) {
129
75
  return this.#ws ? this.#ws.subscribe(topic) : false;
130
76
  }
131
77
  /**
132
- * Unsubscribe from a topic.
133
- * Returns true on success, if the WebSocket was subscribed.
134
- *
135
- * @param {String} topic
136
- * @returns {Boolean}
78
+ * Unsubscribes from a topic.
137
79
  */
138
80
  unsubscribe(topic) {
139
81
  return this.#ws ? this.#ws.unsubscribe(topic) : false;
140
82
  }
141
83
  /**
142
- * Publish a message to a topic in MQTT syntax.
143
- * You cannot publish using wildcards, only fully specified topics.
144
- *
145
- * @param {String} topic
146
- * @param {String|Buffer|ArrayBuffer} message
147
- * @param {Boolean=} is_binary
148
- * @param {Boolean=} compress
84
+ * Publishes a message to a topic in mqtt syntax.
149
85
  */
150
86
  publish(topic, message, is_binary, compress) {
151
87
  return this.#ws
152
88
  ? this.#ws.publish(topic, message, is_binary, compress)
153
89
  : false;
154
90
  }
155
- #buffered_fragment;
156
91
  /**
157
- * Buffers the provided fragment and returns the last buffered fragment.
158
- *
159
- * @param {String|Buffer|ArrayBuffer} fragment
160
- * @returns {String|Buffer|ArrayBuffer|undefined}
92
+ * Buffers the provided fragment and returns the previously buffered one.
161
93
  */
162
94
  _buffer_fragment(fragment) {
163
95
  const current = this.#buffered_fragment;
@@ -165,217 +97,157 @@ export default class Websocket extends EventEmitter {
165
97
  return current;
166
98
  }
167
99
  /**
168
- * Initiates fragment based message writing with uWS and writes appropriate chunk based on provided type parameter.
169
- *
170
- * @param {String} type
171
- * @param {String|Buffer|ArrayBuffer} chunk
172
- * @param {Boolean=} is_binary
173
- * @param {Boolean=} compress
174
- * @param {Function=} callback
175
- * @returns {Boolean}
100
+ * Writes a fragment using the appropriate uws fragment method.
176
101
  */
177
102
  _write(type, chunk, is_binary, compress, callback) {
178
- // Ensure websocket still exists before attempting to write
179
- if (this.#ws) {
180
- // Attempt to send this fragment using the appropriate fragment method from uWS
181
- let sent;
182
- switch (type) {
183
- case FRAGMENTS.FIRST:
184
- sent = this.#ws.sendFirstFragment(chunk, is_binary, compress);
185
- break;
186
- case FRAGMENTS.MIDDLE:
187
- sent = this.#ws.sendFragment(chunk, is_binary, compress);
188
- break;
189
- case FRAGMENTS.LAST:
190
- sent = this.#ws.sendLastFragment(chunk, is_binary, compress);
191
- break;
192
- default:
193
- throw new Error("Websocket._write() -> Invalid Fragment type constant provided.");
194
- }
195
- if (sent) {
196
- // Invoke the callback if chunk was sent successfully
197
- if (callback)
198
- callback();
199
- }
200
- else {
201
- // Wait for this connection to drain before retrying this chunk
202
- this.once("drain", () => this._write(type, chunk, is_binary, compress, callback));
203
- }
204
- // Return the sent status for consumer
205
- return sent;
103
+ if (!this.#ws) {
104
+ throw new Error("Websocket is no longer connected.");
105
+ }
106
+ let sent;
107
+ switch (type) {
108
+ case FRAGMENTS.FIRST:
109
+ sent = this.#ws.sendFirstFragment(chunk, is_binary, compress);
110
+ break;
111
+ case FRAGMENTS.MIDDLE:
112
+ sent = this.#ws.sendFragment(chunk, compress);
113
+ break;
114
+ case FRAGMENTS.LAST:
115
+ sent = this.#ws.sendLastFragment(chunk, compress);
116
+ break;
117
+ default:
118
+ throw new Error("Websocket._write() -> invalid fragment type constant provided.");
119
+ }
120
+ if (sent) {
121
+ if (callback)
122
+ callback();
206
123
  }
207
- // Throw an error with NOT_CONNECTED message to be caught by executor
208
- throw new Error("Websocket is no longer connected.");
124
+ else {
125
+ // wait for drain before retrying this chunk
126
+ this.once("drain", () => this._write(type, chunk, is_binary, compress, callback));
127
+ }
128
+ return sent;
209
129
  }
210
130
  /**
211
- * Streams the provided chunk while pausing the stream being consumed during backpressure.
212
- *
213
- * @param {Readable} stream
214
- * @param {String} type
215
- * @param {Buffer|ArrayBuffer} chunk
216
- * @param {Boolean} is_binary
131
+ * Streams a chunk, pausing the source stream while under backpressure.
217
132
  */
218
133
  _stream_chunk(stream, type, chunk, is_binary) {
219
- // Break execution if connection is no longer connected
220
134
  if (this.#ws === null)
221
135
  return;
222
- // Attempt to write this chunk
223
136
  const sent = this._write(type, chunk, is_binary);
224
137
  if (!sent) {
225
- // Pause the readable stream as we failed to write this chunk
226
138
  stream.pause();
227
- // Wait for this connection to be drained before trying again
228
139
  this.once("drain", () => this._stream_chunk(stream, type, chunk, is_binary));
229
140
  }
230
141
  else if (stream.isPaused()) {
231
- // Resume the stream if it has been paused and we sent a chunk successfully
232
142
  stream.resume();
233
143
  }
234
144
  }
235
145
  /**
236
- * This method is used to stream a message to the receiver.
237
- * Note! The data is by default streamed as Binary due to how partial fragments are sent.
238
- * This is done to prevent processing errors depending on client's receiver's incoming fragment processing strategy.
239
- *
240
- * @param {Readable} readable A Readable stream which will be consumed as message
241
- * @param {Boolean=} is_binary Whether data being streamed is in binary. Default: true
242
- * @returns {Promise}
146
+ * Streams a readable as a single fragmented message.
243
147
  */
244
148
  stream(readable, is_binary = true) {
245
- // Ensure readable is an instance of a stream.Readable
246
- if (!(readable instanceof Readable))
149
+ if (!(readable instanceof Readable)) {
247
150
  throw new Error("Websocket.stream(readable) -> readable must be a Readable stream.");
248
- // Prevent multiple streams from taking place
249
- if (this.#stream)
250
- throw new Error("Websocket.stream(readable) -> You may not stream data while another stream operation is active on this websocket. Make sure you are not already streaming or piping a stream to this websocket.");
251
- // Return a promise which resolves once stream has finished
252
- const scope = this;
151
+ }
152
+ if (this.#stream) {
153
+ throw new Error("Websocket.stream(readable) -> you may not stream data while another stream operation is active on this websocket.");
154
+ }
253
155
  return new Promise((resolve) => {
254
- // Store the readable as the pending stream for this connection
255
- scope.#stream = readable;
256
- // Bind a listener for the 'data' event to consume chunks
257
- let is_first = true; // By default, we will send the first chunk as a fragment
156
+ this.#stream = readable;
157
+ let is_first = true;
258
158
  readable.on("data", (chunk) => {
259
- // Check to see if we have a fragment to send post buffering
260
- const fragment = scope._buffer_fragment(chunk);
261
- if (fragment) {
262
- // Stream the retrieved current fragment
263
- scope._stream_chunk(readable, is_first ? FRAGMENTS.FIRST : FRAGMENTS.MIDDLE, fragment, is_binary);
264
- // If this was the first chunk, invert the is_first boolean
265
- if (is_first)
266
- is_first = false;
267
- }
159
+ const fragment = this._buffer_fragment(chunk);
160
+ if (!fragment)
161
+ return;
162
+ this._stream_chunk(readable, is_first ? FRAGMENTS.FIRST : FRAGMENTS.MIDDLE, fragment, is_binary);
163
+ if (is_first)
164
+ is_first = false;
268
165
  });
269
- // Create a callback for ending the readable consumption
270
166
  const end_stream = () => {
271
- // Retrieve the last buffered fragment to send as last or only chunk
272
- const fragment = scope._buffer_fragment();
273
- // If we streamed no individual fragments aka. the is_first flag was set to true, then we did no streaming and can simply send the last fragment as a message
167
+ const fragment = this._buffer_fragment();
274
168
  if (is_first) {
275
- scope.#ws.send(fragment, is_binary);
169
+ this.#ws.send(fragment, is_binary);
276
170
  }
277
171
  else {
278
- // Stream the final chunk as last fragment
279
- scope._stream_chunk(scope.#stream, FRAGMENTS.LAST, fragment, is_binary);
172
+ this._stream_chunk(readable, FRAGMENTS.LAST, fragment, is_binary);
280
173
  }
281
- // Clean up the readable
282
- scope.#stream = undefined;
174
+ this.#stream = undefined;
283
175
  resolve();
284
176
  };
285
- // Bind listeners to end the framented write procedure
286
177
  readable.once("end", end_stream);
287
178
  });
288
179
  }
289
- /* Websocket Getters */
180
+ /* getters */
290
181
  /**
291
- * Underlying uWS.Websocket object
182
+ * Underlying uws websocket object.
292
183
  */
293
184
  get raw() {
294
185
  return this.#ws;
295
186
  }
296
187
  /**
297
- * Returns IP address of this websocket connection.
298
- * @returns {String}
188
+ * Remote ip address of this connection.
299
189
  */
300
190
  get ip() {
301
191
  return this.#ip;
302
192
  }
303
193
  /**
304
- * Returns context values from the response.update(context) connection upgrade call.
305
- * @returns {Object}
194
+ * Context values attached during the upgrade.
306
195
  */
307
196
  get context() {
308
197
  return this.#context;
309
198
  }
310
199
  /**
311
- * Returns whether is websocket connection is closed.
312
- * @returns {Boolean}
200
+ * Whether this connection is closed.
313
201
  */
314
202
  get closed() {
315
203
  return this.#closed;
316
204
  }
317
205
  /**
318
- * Returns the bytes buffered in backpressure.
319
- * This is similar to the bufferedAmount property in the browser counterpart.
320
- * @returns {Number}
206
+ * Bytes currently buffered in backpressure.
321
207
  */
322
208
  get buffered() {
323
209
  return this.#ws ? this.#ws.getBufferedAmount() : 0;
324
210
  }
325
211
  /**
326
- * Returns a list of topics this websocket is subscribed to.
327
- * @returns {Array<String>}
212
+ * Topics this connection is subscribed to.
328
213
  */
329
214
  get topics() {
330
215
  return this.#ws ? this.#ws.getTopics() : [];
331
216
  }
332
217
  /**
333
- * Returns a Writable stream associated with this response to be used for piping streams.
334
- * Note! You can only retrieve/use only one writable at any given time.
335
- *
336
- * @returns {Writable}
218
+ * Writable stream bound to this connection for piping.
219
+ * Only one writable can be used at a time.
337
220
  */
338
221
  get writable() {
339
- // Prevent multiple streaming operations from taking place
340
- const scope = this;
341
- if (this.#stream)
342
- throw new Error("Websocket.writable -> You may only access and utilize one writable stream at any given time. Make sure you are not already streaming or piping a stream to this websocket.");
343
- // Create a new writable stream object which will write with the _write method
222
+ if (this.#stream) {
223
+ throw new Error("Websocket.writable -> you may only access and utilize one writable stream at any given time.");
224
+ }
344
225
  let is_first = true;
345
- this.#stream = new Writable({
346
- write: (chunk, encoding, callback) => {
347
- // Buffer the incoming chunk as a fragment
348
- const fragment = scope._buffer_fragment(chunk);
349
- // Check to see if we have a fragment to send post buffering
226
+ const writable = new Writable({
227
+ write: (chunk, _encoding, callback) => {
228
+ const fragment = this._buffer_fragment(chunk);
350
229
  if (fragment) {
351
- // Write the current retrieved fragment
352
- scope._write(is_first ? FRAGMENTS.FIRST : FRAGMENTS.MIDDLE, fragment, true, false, callback);
353
- // Invert the is_first boolean after first fragment
230
+ this._write(is_first ? FRAGMENTS.FIRST : FRAGMENTS.MIDDLE, fragment, true, false, callback);
354
231
  if (is_first)
355
232
  is_first = false;
356
233
  }
357
234
  else {
358
- // Trigger the callback even if don't have a fragment to continue consuming
359
235
  callback();
360
236
  }
361
237
  },
362
238
  });
363
- // Create a callback for ending the writable usage
239
+ this.#stream = writable;
364
240
  const end_stream = () => {
365
- // Retrieve the last buffered fragment to write as last or only chunk
366
- const fragment = scope._buffer_fragment();
241
+ const fragment = this._buffer_fragment();
367
242
  if (is_first) {
368
- scope.#ws.send(fragment, true, false);
369
- scope.#ws.stream = undefined;
243
+ this.#ws.send(fragment, true, false);
244
+ this.#stream = undefined;
370
245
  }
371
246
  else {
372
- // Write the final empty chunk as last fragment and cleanup the writable
373
- scope._write(FRAGMENTS.LAST, fragment, true, false, () => (scope.#stream = undefined));
247
+ this._write(FRAGMENTS.LAST, fragment, true, false, () => (this.#stream = undefined));
374
248
  }
375
249
  };
376
- // Bind listeners to end the fragmented write procedure
377
- this.#stream.on("finish", end_stream);
378
- // Return the writable stream
379
- return this.#stream;
250
+ writable.on("finish", end_stream);
251
+ return writable;
380
252
  }
381
253
  }
@@ -0,0 +1,86 @@
1
+ import { type uWebsockets } from "../uws.js";
2
+ import Websocket, { type WebsocketRaw } from "./Websocket.js";
3
+ export type WebsocketMessageType = "String" | "Buffer" | "ArrayBuffer";
4
+ export interface WebsocketRouteOptions {
5
+ idle_timeout: number;
6
+ message_type: WebsocketMessageType;
7
+ compression: uWebsockets.CompressOptions;
8
+ max_backpressure: number;
9
+ max_payload_length: number;
10
+ }
11
+ interface WebsocketRouteApp {
12
+ routes: Record<string, Record<string, any>>;
13
+ uws_instance: uWebsockets.TemplatedApp;
14
+ _create_route(options: {
15
+ method: string;
16
+ pattern: string;
17
+ handler: (request: any, response: any) => void;
18
+ options?: Record<string, any>;
19
+ }): void;
20
+ _handle_uws_request(route: any, request: uWebsockets.HttpRequest, response: uWebsockets.HttpResponse, socket_context: uWebsockets.us_socket_context_t): void;
21
+ }
22
+ export interface WebsocketRouteConstructorOptions {
23
+ app: WebsocketRouteApp;
24
+ pattern: string;
25
+ handler: (websocket: Websocket) => void;
26
+ options?: Partial<WebsocketRouteOptions>;
27
+ }
28
+ type MessageParser = (message: ArrayBuffer) => string | Buffer | ArrayBuffer;
29
+ export default class WebsocketRoute {
30
+ #private;
31
+ app: WebsocketRouteApp;
32
+ pattern: string;
33
+ handler: (websocket: Websocket) => void;
34
+ options: WebsocketRouteOptions;
35
+ constructor({ app, pattern, handler, options, }: WebsocketRouteConstructorOptions);
36
+ /**
37
+ * Returns a parser that converts a uws ArrayBuffer to the configured type.
38
+ */
39
+ _get_message_parser(type: WebsocketMessageType): MessageParser;
40
+ /**
41
+ * Loads the companion upgrade route from the app routes.
42
+ * @private
43
+ */
44
+ _load_companion_route(): void;
45
+ /**
46
+ * Sets the upgrade route used for incoming upgrade requests.
47
+ * @private
48
+ */
49
+ _set_upgrade_route(route: any): void;
50
+ /**
51
+ * Creates the uws ws route that powers this instance.
52
+ * @private
53
+ */
54
+ _create_uws_route(): void;
55
+ /**
56
+ * Handles the uws open event.
57
+ * @private
58
+ */
59
+ _on_open(ws: WebsocketRaw): void;
60
+ /**
61
+ * Handles the uws ping event.
62
+ * @private
63
+ */
64
+ _on_ping(ws: WebsocketRaw, message?: ArrayBuffer): void;
65
+ /**
66
+ * Handles the uws pong event.
67
+ * @private
68
+ */
69
+ _on_pong(ws: WebsocketRaw, message?: ArrayBuffer): void;
70
+ /**
71
+ * Handles the uws drain event.
72
+ * @private
73
+ */
74
+ _on_drain(ws: WebsocketRaw): void;
75
+ /**
76
+ * Handles the uws message event.
77
+ * @private
78
+ */
79
+ _on_message(ws: WebsocketRaw, message: ArrayBuffer | undefined, is_binary: boolean): void;
80
+ /**
81
+ * Handles the uws close event.
82
+ * @private
83
+ */
84
+ _on_close(ws: WebsocketRaw, code: number, message?: ArrayBuffer): void;
85
+ }
86
+ export {};