@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.
- package/LICENSE +202 -0
- package/README.md +26 -0
- package/lib/SSEventStream.d.ts +29 -0
- package/lib/SSEventStream.js +85 -0
- package/lib/close.d.ts +11 -0
- package/lib/close.js +23 -0
- package/lib/engine.d.ts +105 -0
- package/lib/engine.js +221 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.js +1 -0
- package/lib/listen.d.ts +13 -0
- package/lib/listen.js +44 -0
- package/lib/on_request.d.ts +21 -0
- package/lib/on_request.js +97 -0
- package/lib/register.d.ts +17 -0
- package/lib/register.js +5 -0
- package/lib/register_middleware.d.ts +14 -0
- package/lib/register_middleware.js +42 -0
- package/lib/request.d.ts +168 -0
- package/lib/request.js +400 -0
- package/lib/request_iterator.d.ts +22 -0
- package/lib/request_iterator.js +61 -0
- package/lib/response.d.ts +306 -0
- package/lib/response.js +742 -0
- package/lib/route_register.d.ts +10 -0
- package/lib/route_register.js +67 -0
- package/lib/utils.d.ts +14 -0
- package/lib/utils.js +30 -0
- package/lib/ws/Websocket.d.ts +173 -0
- package/lib/ws/Websocket.js +381 -0
- package/package.json +32 -0
- package/uws-wrapper.node +0 -0
|
@@ -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
|
+
}
|