@linebridge/engine-neo 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ import type Engine from "./engine.js";
2
+ import { RouteAlike } from "linebridge/classes/Route/index";
3
+ /**
4
+ * Registers a route with the engine's uWS app.
5
+ *
6
+ * @param route - A Route instance or a Route class (constructor).
7
+ *
8
+ * @throws {Error} If the engine is not initialized or the route is invalid.
9
+ */
10
+ export default function (this: Engine, route: RouteAlike): void;
@@ -0,0 +1,67 @@
1
+ import { Route, } from "linebridge/classes/Route/index";
2
+ import { HandlerKind } from "linebridge/classes/Handler/index";
3
+ /**
4
+ * Registers a route with the engine's uWS app.
5
+ *
6
+ * @param route - A Route instance or a Route class (constructor).
7
+ *
8
+ * @throws {Error} If the engine is not initialized or the route is invalid.
9
+ */
10
+ export default function (route) {
11
+ if (!this.uws) {
12
+ throw new Error("Engine is not initialized");
13
+ }
14
+ let routeInstance;
15
+ // support passing a class constructor or an already-constructed instance
16
+ if (typeof route === "function") {
17
+ try {
18
+ routeInstance = new route();
19
+ }
20
+ catch (err) {
21
+ console.error(`Failed to construct route class:\n`, err);
22
+ return;
23
+ }
24
+ }
25
+ else if (route instanceof Route) {
26
+ routeInstance = route;
27
+ }
28
+ else if (typeof route.fn === "function") {
29
+ routeInstance = new Route();
30
+ routeInstance.kind = HandlerKind.http;
31
+ routeInstance.path = route.path ?? "/";
32
+ routeInstance.method = route.method ?? "get";
33
+ routeInstance.useContexts = route.useContexts ?? [];
34
+ routeInstance.useMiddlewares = route.useMiddlewares ?? [];
35
+ routeInstance.fn = route.fn;
36
+ }
37
+ else {
38
+ throw new Error("Invalid route provided");
39
+ }
40
+ // try to initialize the route
41
+ try {
42
+ routeInstance._initialize(this.server);
43
+ }
44
+ catch (err) {
45
+ console.error(`Failed to initialize route:\n`, err);
46
+ return;
47
+ }
48
+ // normalize DELETE method for uWS compatibility (uWS uses "del" internally)
49
+ if (routeInstance.method === "delete") {
50
+ routeInstance.method = "del";
51
+ }
52
+ // verify the method is a valid uWS route method before registering
53
+ if (typeof this.uws[routeInstance.method] !== "function") {
54
+ console.warn(`Invalid method (${routeInstance.method}) for route handler [${routeInstance.path}]\nSkipping route..`);
55
+ return;
56
+ }
57
+ this.registers.add({
58
+ kind: routeInstance.kind,
59
+ method: routeInstance.method,
60
+ path: routeInstance.path,
61
+ useContexts: routeInstance.useContexts,
62
+ useMiddlewares: routeInstance.useMiddlewares,
63
+ _source_file: routeInstance._source_file,
64
+ handler: routeInstance.handler,
65
+ });
66
+ this.uws[routeInstance.method](routeInstance.path, (res, req) => this.on_request(req, res, routeInstance));
67
+ }
package/lib/utils.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Writes values from focus object onto base object.
3
+ *
4
+ */
5
+ declare function wrap_object(original: any, target: any): void;
6
+ /**
7
+ * This method converts ArrayBuffers to a string.
8
+ *
9
+ * @param {ArrayBuffer} array_buffer
10
+ * @param {BufferEncoding} encoding
11
+ * @returns {String} String
12
+ */
13
+ declare function array_buffer_to_string(array_buffer: ArrayBuffer, encoding?: BufferEncoding): string;
14
+ export { array_buffer_to_string, wrap_object };
package/lib/utils.js ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Writes values from focus object onto base object.
3
+ *
4
+ */
5
+ function wrap_object(original, target) {
6
+ Object.keys(target).forEach((key) => {
7
+ if (typeof target[key] == "object") {
8
+ if (Array.isArray(target[key]))
9
+ return (original[key] = target[key]); // lgtm [js/prototype-pollution-utility]
10
+ if (original[key] === null || typeof original[key] !== "object") {
11
+ original[key] = {};
12
+ }
13
+ wrap_object(original[key], target[key]);
14
+ }
15
+ else {
16
+ original[key] = target[key];
17
+ }
18
+ });
19
+ }
20
+ /**
21
+ * This method converts ArrayBuffers to a string.
22
+ *
23
+ * @param {ArrayBuffer} array_buffer
24
+ * @param {BufferEncoding} encoding
25
+ * @returns {String} String
26
+ */
27
+ function array_buffer_to_string(array_buffer, encoding = "utf8") {
28
+ return Buffer.from(array_buffer).toString(encoding);
29
+ }
30
+ export { array_buffer_to_string, wrap_object };
@@ -0,0 +1,173 @@
1
+ import { EventEmitter } from "tseep/lib/ee-safe.js";
2
+ import { Readable, Writable } from "stream";
3
+ export default class Websocket extends EventEmitter {
4
+ #private;
5
+ constructor(ws: any);
6
+ /**
7
+ * Binds an event listener to this `Websocket` instance.
8
+ * See the Node.js `EventEmitter` documentation for more details on this extended method.
9
+ * @param {('message'|'close'|'drain'|'ping'|'pong')} eventName
10
+ * @param {Function} listener
11
+ * @returns {Websocket}
12
+ */
13
+ on(eventName: ('message' | 'close' | 'drain' | 'ping' | 'pong'), listener: Function): Websocket;
14
+ /**
15
+ * Binds a `one-time` event listener to this `Websocket` instance.
16
+ * See the Node.js `EventEmitter` documentation for more details on this extended method.
17
+ * @param {('message'|'close'|'drain'|'ping'|'pong')} eventName
18
+ * @param {Function} listener
19
+ * @returns {Websocket}
20
+ */
21
+ once(eventName: ('message' | 'close' | 'drain' | 'ping' | 'pong'), listener: Function): Websocket;
22
+ /**
23
+ * Alias of uWS.cork() method. Accepts a callback with multiple operations for network efficiency.
24
+ *
25
+ * @param {Function} callback
26
+ * @returns {Websocket}
27
+ */
28
+ atomic(callback: Function): Websocket;
29
+ /**
30
+ * Sends a message to websocket connection.
31
+ * Returns true if message was sent successfully.
32
+ * Returns false if message was not sent due to buil up backpressure.
33
+ *
34
+ * @param {String|Buffer|ArrayBuffer} message
35
+ * @param {Boolean=} is_binary
36
+ * @param {Boolean=} compress
37
+ * @returns {Boolean}
38
+ */
39
+ send(message: string | Buffer | ArrayBuffer, is_binary?: boolean | undefined, compress?: boolean | undefined): boolean;
40
+ /**
41
+ * Sends a ping control message.
42
+ * Returns Boolean depending on backpressure similar to send().
43
+ *
44
+ * @param {String|Buffer|ArrayBuffer=} message
45
+ * @returns {Boolean}
46
+ */
47
+ ping(message?: (string | Buffer | ArrayBuffer) | undefined): boolean;
48
+ /**
49
+ * Destroys this polyfill Websocket component and derefernces the underlying ws object
50
+ * @private
51
+ */
52
+ private _destroy;
53
+ /**
54
+ * Gracefully closes websocket connection by sending specified code and short message.
55
+ *
56
+ * @param {Number=} code
57
+ * @param {(String|Buffer|ArrayBuffer)=} message
58
+ */
59
+ close(code?: number | undefined, message?: (string | Buffer | ArrayBuffer) | undefined): void;
60
+ /**
61
+ * Forcefully closes websocket connection.
62
+ * No websocket close code/message is sent.
63
+ * This will immediately emit the 'close' event.
64
+ */
65
+ destroy(): void;
66
+ /**
67
+ * Returns whether this `Websocket` is subscribed to the specified topic.
68
+ *
69
+ * @param {String} topic
70
+ * @returns {Boolean}
71
+ */
72
+ is_subscribed(topic: string): boolean;
73
+ /**
74
+ * Subscribe to a topic in MQTT syntax.
75
+ * MQTT syntax includes things like "root/child/+/grandchild" where "+" is a wildcard and "root/#" where "#" is a terminating wildcard.
76
+ *
77
+ * @param {String} topic
78
+ * @returns {Boolean}
79
+ */
80
+ subscribe(topic: string): boolean;
81
+ /**
82
+ * Unsubscribe from a topic.
83
+ * Returns true on success, if the WebSocket was subscribed.
84
+ *
85
+ * @param {String} topic
86
+ * @returns {Boolean}
87
+ */
88
+ unsubscribe(topic: string): boolean;
89
+ /**
90
+ * Publish a message to a topic in MQTT syntax.
91
+ * You cannot publish using wildcards, only fully specified topics.
92
+ *
93
+ * @param {String} topic
94
+ * @param {String|Buffer|ArrayBuffer} message
95
+ * @param {Boolean=} is_binary
96
+ * @param {Boolean=} compress
97
+ */
98
+ publish(topic: string, message: string | Buffer | ArrayBuffer, is_binary?: boolean | undefined, compress?: boolean | undefined): any;
99
+ /**
100
+ * Buffers the provided fragment and returns the last buffered fragment.
101
+ *
102
+ * @param {String|Buffer|ArrayBuffer} fragment
103
+ * @returns {String|Buffer|ArrayBuffer|undefined}
104
+ */
105
+ _buffer_fragment(fragment: string | Buffer | ArrayBuffer): string | Buffer | ArrayBuffer | undefined;
106
+ /**
107
+ * Initiates fragment based message writing with uWS and writes appropriate chunk based on provided type parameter.
108
+ *
109
+ * @param {String} type
110
+ * @param {String|Buffer|ArrayBuffer} chunk
111
+ * @param {Boolean=} is_binary
112
+ * @param {Boolean=} compress
113
+ * @param {Function=} callback
114
+ * @returns {Boolean}
115
+ */
116
+ _write(type: string, chunk: string | Buffer | ArrayBuffer, is_binary?: boolean | undefined, compress?: boolean | undefined, callback?: Function | undefined): boolean;
117
+ /**
118
+ * Streams the provided chunk while pausing the stream being consumed during backpressure.
119
+ *
120
+ * @param {Readable} stream
121
+ * @param {String} type
122
+ * @param {Buffer|ArrayBuffer} chunk
123
+ * @param {Boolean} is_binary
124
+ */
125
+ _stream_chunk(stream: Readable, type: string, chunk: Buffer | ArrayBuffer, is_binary: boolean): void;
126
+ /**
127
+ * This method is used to stream a message to the receiver.
128
+ * Note! The data is by default streamed as Binary due to how partial fragments are sent.
129
+ * This is done to prevent processing errors depending on client's receiver's incoming fragment processing strategy.
130
+ *
131
+ * @param {Readable} readable A Readable stream which will be consumed as message
132
+ * @param {Boolean=} is_binary Whether data being streamed is in binary. Default: true
133
+ * @returns {Promise}
134
+ */
135
+ stream(readable: Readable, is_binary?: boolean | undefined): Promise<any>;
136
+ /**
137
+ * Underlying uWS.Websocket object
138
+ */
139
+ get raw(): any;
140
+ /**
141
+ * Returns IP address of this websocket connection.
142
+ * @returns {String}
143
+ */
144
+ get ip(): string;
145
+ /**
146
+ * Returns context values from the response.update(context) connection upgrade call.
147
+ * @returns {Object}
148
+ */
149
+ get context(): Object;
150
+ /**
151
+ * Returns whether is websocket connection is closed.
152
+ * @returns {Boolean}
153
+ */
154
+ get closed(): boolean;
155
+ /**
156
+ * Returns the bytes buffered in backpressure.
157
+ * This is similar to the bufferedAmount property in the browser counterpart.
158
+ * @returns {Number}
159
+ */
160
+ get buffered(): number;
161
+ /**
162
+ * Returns a list of topics this websocket is subscribed to.
163
+ * @returns {Array<String>}
164
+ */
165
+ get topics(): Array<string>;
166
+ /**
167
+ * Returns a Writable stream associated with this response to be used for piping streams.
168
+ * Note! You can only retrieve/use only one writable at any given time.
169
+ *
170
+ * @returns {Writable}
171
+ */
172
+ get writable(): Writable;
173
+ }
@@ -0,0 +1,381 @@
1
+ import { EventEmitter } from "tseep/lib/ee-safe.js";
2
+ import { Readable, Writable } from "stream";
3
+ import { array_buffer_to_string } from "../utils.js";
4
+ const FRAGMENTS = {
5
+ FIRST: "FIRST",
6
+ MIDDLE: "MIDDLE",
7
+ LAST: "LAST",
8
+ };
9
+ export default class Websocket extends EventEmitter {
10
+ #ws;
11
+ #ip;
12
+ #context;
13
+ #stream;
14
+ #closed = false;
15
+ constructor(ws) {
16
+ // Initialize event emitter
17
+ super();
18
+ // Parse information about websocket connection
19
+ this.#ws = ws;
20
+ this.#context = ws.context || {};
21
+ this.#ip = array_buffer_to_string(ws.getRemoteAddressAsText());
22
+ }
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
+ /**
49
+ * Alias of uWS.cork() method. Accepts a callback with multiple operations for network efficiency.
50
+ *
51
+ * @param {Function} callback
52
+ * @returns {Websocket}
53
+ */
54
+ atomic(callback) {
55
+ return this.#ws ? this.#ws.cork(callback) : this;
56
+ }
57
+ /**
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}
66
+ */
67
+ send(message, is_binary, compress) {
68
+ // Send message through uWS connection
69
+ if (this.#ws)
70
+ return this.#ws.send(message, is_binary, compress);
71
+ return false;
72
+ }
73
+ /**
74
+ * 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
+ */
80
+ ping(message) {
81
+ // Send ping OPCODE message through uWS connection
82
+ return this.#ws ? this.#ws.ping(message) : false;
83
+ }
84
+ /**
85
+ * Destroys this polyfill Websocket component and derefernces the underlying ws object
86
+ * @private
87
+ */
88
+ _destroy() {
89
+ this.#ws = null;
90
+ this.#closed = true;
91
+ }
92
+ /**
93
+ * Gracefully closes websocket connection by sending specified code and short message.
94
+ *
95
+ * @param {Number=} code
96
+ * @param {(String|Buffer|ArrayBuffer)=} message
97
+ */
98
+ close(code, message) {
99
+ // Close websocket using uWS.end() method which gracefully closes connections
100
+ if (this.#ws)
101
+ this.#ws.end(code, message);
102
+ }
103
+ /**
104
+ * Forcefully closes websocket connection.
105
+ * No websocket close code/message is sent.
106
+ * This will immediately emit the 'close' event.
107
+ */
108
+ destroy() {
109
+ if (this.#ws)
110
+ this.#ws.close();
111
+ }
112
+ /**
113
+ * Returns whether this `Websocket` is subscribed to the specified topic.
114
+ *
115
+ * @param {String} topic
116
+ * @returns {Boolean}
117
+ */
118
+ is_subscribed(topic) {
119
+ return this.#ws ? this.#ws.isSubscribed(topic) : false;
120
+ }
121
+ /**
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}
127
+ */
128
+ subscribe(topic) {
129
+ return this.#ws ? this.#ws.subscribe(topic) : false;
130
+ }
131
+ /**
132
+ * Unsubscribe from a topic.
133
+ * Returns true on success, if the WebSocket was subscribed.
134
+ *
135
+ * @param {String} topic
136
+ * @returns {Boolean}
137
+ */
138
+ unsubscribe(topic) {
139
+ return this.#ws ? this.#ws.unsubscribe(topic) : false;
140
+ }
141
+ /**
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
149
+ */
150
+ publish(topic, message, is_binary, compress) {
151
+ return this.#ws
152
+ ? this.#ws.publish(topic, message, is_binary, compress)
153
+ : false;
154
+ }
155
+ #buffered_fragment;
156
+ /**
157
+ * Buffers the provided fragment and returns the last buffered fragment.
158
+ *
159
+ * @param {String|Buffer|ArrayBuffer} fragment
160
+ * @returns {String|Buffer|ArrayBuffer|undefined}
161
+ */
162
+ _buffer_fragment(fragment) {
163
+ const current = this.#buffered_fragment;
164
+ this.#buffered_fragment = fragment;
165
+ return current;
166
+ }
167
+ /**
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}
176
+ */
177
+ _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;
206
+ }
207
+ // Throw an error with NOT_CONNECTED message to be caught by executor
208
+ throw new Error("Websocket is no longer connected.");
209
+ }
210
+ /**
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
217
+ */
218
+ _stream_chunk(stream, type, chunk, is_binary) {
219
+ // Break execution if connection is no longer connected
220
+ if (this.#ws === null)
221
+ return;
222
+ // Attempt to write this chunk
223
+ const sent = this._write(type, chunk, is_binary);
224
+ if (!sent) {
225
+ // Pause the readable stream as we failed to write this chunk
226
+ stream.pause();
227
+ // Wait for this connection to be drained before trying again
228
+ this.once("drain", () => this._stream_chunk(stream, type, chunk, is_binary));
229
+ }
230
+ else if (stream.isPaused()) {
231
+ // Resume the stream if it has been paused and we sent a chunk successfully
232
+ stream.resume();
233
+ }
234
+ }
235
+ /**
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}
243
+ */
244
+ stream(readable, is_binary = true) {
245
+ // Ensure readable is an instance of a stream.Readable
246
+ if (!(readable instanceof Readable))
247
+ 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;
253
+ 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
258
+ 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
+ }
268
+ });
269
+ // Create a callback for ending the readable consumption
270
+ 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
274
+ if (is_first) {
275
+ scope.#ws.send(fragment, is_binary);
276
+ }
277
+ else {
278
+ // Stream the final chunk as last fragment
279
+ scope._stream_chunk(scope.#stream, FRAGMENTS.LAST, fragment, is_binary);
280
+ }
281
+ // Clean up the readable
282
+ scope.#stream = undefined;
283
+ resolve();
284
+ };
285
+ // Bind listeners to end the framented write procedure
286
+ readable.once("end", end_stream);
287
+ });
288
+ }
289
+ /* Websocket Getters */
290
+ /**
291
+ * Underlying uWS.Websocket object
292
+ */
293
+ get raw() {
294
+ return this.#ws;
295
+ }
296
+ /**
297
+ * Returns IP address of this websocket connection.
298
+ * @returns {String}
299
+ */
300
+ get ip() {
301
+ return this.#ip;
302
+ }
303
+ /**
304
+ * Returns context values from the response.update(context) connection upgrade call.
305
+ * @returns {Object}
306
+ */
307
+ get context() {
308
+ return this.#context;
309
+ }
310
+ /**
311
+ * Returns whether is websocket connection is closed.
312
+ * @returns {Boolean}
313
+ */
314
+ get closed() {
315
+ return this.#closed;
316
+ }
317
+ /**
318
+ * Returns the bytes buffered in backpressure.
319
+ * This is similar to the bufferedAmount property in the browser counterpart.
320
+ * @returns {Number}
321
+ */
322
+ get buffered() {
323
+ return this.#ws ? this.#ws.getBufferedAmount() : 0;
324
+ }
325
+ /**
326
+ * Returns a list of topics this websocket is subscribed to.
327
+ * @returns {Array<String>}
328
+ */
329
+ get topics() {
330
+ return this.#ws ? this.#ws.getTopics() : [];
331
+ }
332
+ /**
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}
337
+ */
338
+ 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
344
+ 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
350
+ 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
354
+ if (is_first)
355
+ is_first = false;
356
+ }
357
+ else {
358
+ // Trigger the callback even if don't have a fragment to continue consuming
359
+ callback();
360
+ }
361
+ },
362
+ });
363
+ // Create a callback for ending the writable usage
364
+ const end_stream = () => {
365
+ // Retrieve the last buffered fragment to write as last or only chunk
366
+ const fragment = scope._buffer_fragment();
367
+ if (is_first) {
368
+ scope.#ws.send(fragment, true, false);
369
+ scope.#ws.stream = undefined;
370
+ }
371
+ 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));
374
+ }
375
+ };
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;
380
+ }
381
+ }