@linebridge/engine-neo 1.1.0 → 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.
- package/README.md +2 -3
- package/lib/engine.d.ts +5 -3
- package/lib/engine.js +14 -9
- package/lib/listen.js +3 -1
- package/lib/request_iterator.d.ts +2 -2
- package/lib/route_register.d.ts +3 -3
- package/lib/route_register.js +14 -48
- package/lib/utils.d.ts +1 -1
- package/lib/ws/Websocket.d.ts +70 -118
- package/lib/ws/Websocket.js +81 -209
- package/lib/ws/WebsocketRoute.d.ts +86 -0
- package/lib/ws/WebsocketRoute.js +139 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ The official, high-performance HTTP and WebSocket engine for [Linebridge](https:
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
|
|
8
|
+
pnpm add @linebridge/engine-neo
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
@@ -22,5 +22,4 @@ import "@linebridge/engine-neo"
|
|
|
22
22
|
|
|
23
23
|
This project is licensed under the **MIT License**.
|
|
24
24
|
|
|
25
|
-
It includes software developed by uNetworking AB and uWebSockets.js contributors, which is licensed under the **Apache License 2.0**.
|
|
26
|
-
See the [LICENSE](./LICENSE) file for more details.
|
|
25
|
+
It includes software developed by uNetworking AB and uWebSockets.js contributors, which is licensed under the **Apache License 2.0**. See the [LICENSE](./LICENSE) file for more details.
|
package/lib/engine.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ import uws_wrapper, { type uWebsockets } from "./uws.js";
|
|
|
15
15
|
import RTEngine from "linebridge/classes/RtEngine/index";
|
|
16
16
|
import { EngineAdaptor } from "linebridge/classes/EngineAdaptor/index";
|
|
17
17
|
import { Route } from "linebridge/classes/Route/index";
|
|
18
|
+
import route_register from "./route_register.js";
|
|
19
|
+
import middleware_register from "./register_middleware.js";
|
|
18
20
|
/**
|
|
19
21
|
* Options that control the engine's behaviour.
|
|
20
22
|
*/
|
|
@@ -76,9 +78,9 @@ export declare class NeoEngine extends EngineAdaptor {
|
|
|
76
78
|
/** Gracefully shuts down the server. Bound to the engine instance. */
|
|
77
79
|
close: () => Promise<boolean>;
|
|
78
80
|
/** Registers a route (HTTP method + path + handler). */
|
|
79
|
-
register:
|
|
81
|
+
register: OmitThisParameter<typeof route_register>;
|
|
80
82
|
/** Registers a global middleware that runs before every route handler. */
|
|
81
|
-
register_middleware:
|
|
83
|
+
register_middleware: OmitThisParameter<typeof middleware_register>;
|
|
82
84
|
/**
|
|
83
85
|
* Publish a message to a topic in MQTT syntax to all WebSocket connections on this Server instance.
|
|
84
86
|
* You cannot publish using wildcards, only fully specified topics.
|
|
@@ -91,7 +93,7 @@ export declare class NeoEngine extends EngineAdaptor {
|
|
|
91
93
|
/** Incoming HTTP request handler. Bound to the engine instance. */
|
|
92
94
|
on_request: (native_req: uWebsockets.HttpRequest, native_res: uWebsockets.HttpResponse, route: Route<any, "server"[]>, socket?: uWebsockets.us_socket_context_t | null | undefined) => any;
|
|
93
95
|
/** Middleware/route iteration loop. Bound to the engine instance. */
|
|
94
|
-
request_iterator: (request: Request<any>, response: Response<any>, route: Route<Server<"neo">, "server"[]>, allMiddlewares: Handler<HandlerKind>[], cursor?: number | undefined) => Promise<void>;
|
|
96
|
+
request_iterator: (request: Request<any>, response: Response<any>, route: Route<Server<"neo">, "server"[]>, allMiddlewares: Handler<HandlerKind.middleware>[], cursor?: number | undefined) => Promise<void>;
|
|
95
97
|
/**
|
|
96
98
|
* Decrements the pending request counter and fires the zero-handler
|
|
97
99
|
* when no requests remain in flight.
|
package/lib/engine.js
CHANGED
|
@@ -14,7 +14,7 @@ import toBoolean from "linebridge/utils/toBoolean";
|
|
|
14
14
|
import RTEngine from "linebridge/classes/RtEngine/index";
|
|
15
15
|
import { EngineAdaptor } from "linebridge/classes/EngineAdaptor/index";
|
|
16
16
|
import { Route } from "linebridge/classes/Route/index";
|
|
17
|
-
import
|
|
17
|
+
import Websocket from "./ws/Websocket.js";
|
|
18
18
|
import route_register from "./route_register.js";
|
|
19
19
|
import middleware_register from "./register_middleware.js";
|
|
20
20
|
import listen from "./listen.js";
|
|
@@ -128,7 +128,7 @@ export class NeoEngine extends EngineAdaptor {
|
|
|
128
128
|
upgrade: (res, req, _context) => this.ws.handleUpgrade(req, res),
|
|
129
129
|
// wrap raw uWS websocket with the polyfill so RTEngine gets EventEmitter + helpers
|
|
130
130
|
open: (ws) => {
|
|
131
|
-
const poly = new
|
|
131
|
+
const poly = new Websocket(ws);
|
|
132
132
|
ws.poly = poly;
|
|
133
133
|
//@ts-ignore polyfill context is Object vs RtEngineSocket's typed shape, but runtime is correct
|
|
134
134
|
this.ws.handleConnection(poly);
|
|
@@ -151,12 +151,15 @@ export class NeoEngine extends EngineAdaptor {
|
|
|
151
151
|
// @ts-ignore
|
|
152
152
|
global.websockets = this.ws;
|
|
153
153
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
154
|
+
if (!toBoolean(process.env.LB_SOCKET_MODE)) {
|
|
155
|
+
// create the default route
|
|
156
|
+
const defaultRoute = new Route();
|
|
157
|
+
defaultRoute.method = "any";
|
|
158
|
+
defaultRoute.path = "/*";
|
|
159
|
+
defaultRoute.fn = this._defaultResponse;
|
|
160
|
+
defaultRoute._initialize(this.server);
|
|
161
|
+
this.register(defaultRoute);
|
|
162
|
+
}
|
|
160
163
|
};
|
|
161
164
|
/** Starts the server. Bound to the engine instance. */
|
|
162
165
|
listen = listen.bind(this);
|
|
@@ -200,7 +203,9 @@ export class NeoEngine extends EngineAdaptor {
|
|
|
200
203
|
* Default handler for unmatched routes. Responds with a 404 JSON body.
|
|
201
204
|
*/
|
|
202
205
|
_defaultResponse(req, res) {
|
|
203
|
-
res.status(404)
|
|
206
|
+
res.status(404)
|
|
207
|
+
.setHeader("content-type", "application/json")
|
|
208
|
+
._sendFast(`{"error":"Not found"}`);
|
|
204
209
|
}
|
|
205
210
|
}
|
|
206
211
|
export { uws_wrapper as uWebsockets };
|
package/lib/listen.js
CHANGED
|
@@ -25,7 +25,9 @@ export default async function () {
|
|
|
25
25
|
this.listen_socket = listen_socket;
|
|
26
26
|
// optionally register process-exit listeners for graceful shutdown
|
|
27
27
|
if (this.options.auto_close) {
|
|
28
|
-
EXIT_EVENTS.forEach((type) => process.once(type, () =>
|
|
28
|
+
EXIT_EVENTS.forEach((type) => process.once(type, () => {
|
|
29
|
+
void this.server._fireClose();
|
|
30
|
+
}));
|
|
29
31
|
}
|
|
30
32
|
resolve();
|
|
31
33
|
}
|
|
@@ -9,7 +9,7 @@ import type Engine from "./engine.js";
|
|
|
9
9
|
import type Request from "./request.js";
|
|
10
10
|
import type Response from "./response.js";
|
|
11
11
|
import type { Route } from "linebridge/classes/Route/index";
|
|
12
|
-
import type { Handler } from "linebridge/classes/Handler/index";
|
|
12
|
+
import type { Handler, HandlerKind } from "linebridge/classes/Handler/index";
|
|
13
13
|
/**
|
|
14
14
|
* Recursively iterates the middleware chain, then calls the route handler.
|
|
15
15
|
*
|
|
@@ -19,4 +19,4 @@ import type { Handler } from "linebridge/classes/Handler/index";
|
|
|
19
19
|
*
|
|
20
20
|
* @param cursor - Zero-based index into the middleware array. Increments on each recursion.
|
|
21
21
|
*/
|
|
22
|
-
export default function (this: Engine, request: Request<any>, response: Response<any>, route: Route<typeof this.server>, allMiddlewares: Handler[], cursor?: number): Promise<void>;
|
|
22
|
+
export default function (this: Engine, request: Request<any>, response: Response<any>, route: Route<typeof this.server>, allMiddlewares: Handler<HandlerKind.middleware>[], cursor?: number): Promise<void>;
|
package/lib/route_register.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import {
|
|
1
|
+
import type { Route } from "linebridge/classes/Route/index";
|
|
2
|
+
import type { NeoEngine } from "./engine.js";
|
|
3
3
|
/**
|
|
4
4
|
* Registers a route with the engine's uWS app.
|
|
5
5
|
*
|
|
@@ -7,4 +7,4 @@ import { RouteAlike } from "linebridge/classes/Route/index";
|
|
|
7
7
|
*
|
|
8
8
|
* @throws {Error} If the engine is not initialized or the route is invalid.
|
|
9
9
|
*/
|
|
10
|
-
export default function (this:
|
|
10
|
+
export default function (this: NeoEngine, route: Route): void;
|
package/lib/route_register.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { Route, } from "linebridge/classes/Route/index";
|
|
2
|
-
import { HandlerKind } from "linebridge/classes/Handler/index";
|
|
3
1
|
/**
|
|
4
2
|
* Registers a route with the engine's uWS app.
|
|
5
3
|
*
|
|
@@ -11,57 +9,25 @@ export default function (route) {
|
|
|
11
9
|
if (!this.uws) {
|
|
12
10
|
throw new Error("Engine is not initialized");
|
|
13
11
|
}
|
|
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
12
|
// normalize DELETE method for uWS compatibility (uWS uses "del" internally)
|
|
49
|
-
if (
|
|
50
|
-
|
|
13
|
+
if (route.method === "delete") {
|
|
14
|
+
route.method = "del";
|
|
51
15
|
}
|
|
52
16
|
// verify the method is a valid uWS route method before registering
|
|
53
|
-
if (typeof this.uws[
|
|
54
|
-
console.warn(`Invalid method (${
|
|
17
|
+
if (typeof this.uws[route.method] !== "function") {
|
|
18
|
+
console.warn(`Invalid method (${route.method}) for route handler [${route.path}]\nSkipping route..`);
|
|
55
19
|
return;
|
|
56
20
|
}
|
|
57
21
|
this.registers.add({
|
|
58
|
-
kind:
|
|
59
|
-
method:
|
|
60
|
-
path:
|
|
61
|
-
useContexts:
|
|
62
|
-
useMiddlewares:
|
|
63
|
-
_source_file:
|
|
64
|
-
handler:
|
|
22
|
+
kind: route.kind,
|
|
23
|
+
method: route.method,
|
|
24
|
+
path: route.path,
|
|
25
|
+
useContexts: route.useContexts,
|
|
26
|
+
useMiddlewares: route.useMiddlewares,
|
|
27
|
+
_source_file: route._source_file,
|
|
28
|
+
handler: route.handler,
|
|
65
29
|
});
|
|
66
|
-
|
|
30
|
+
if (route.kind === "http") {
|
|
31
|
+
this.uws[route.method](route.path, (res, req) => this.on_request(req, res, route));
|
|
32
|
+
}
|
|
67
33
|
}
|
package/lib/utils.d.ts
CHANGED
|
@@ -10,5 +10,5 @@ declare function wrap_object(original: any, target: any): void;
|
|
|
10
10
|
* @param {BufferEncoding} encoding
|
|
11
11
|
* @returns {String} String
|
|
12
12
|
*/
|
|
13
|
-
declare function array_buffer_to_string(array_buffer: ArrayBuffer, encoding?: BufferEncoding):
|
|
13
|
+
declare function array_buffer_to_string(array_buffer: ArrayBuffer, encoding?: BufferEncoding): string;
|
|
14
14
|
export { array_buffer_to_string, wrap_object };
|
package/lib/ws/Websocket.d.ts
CHANGED
|
@@ -1,173 +1,125 @@
|
|
|
1
1
|
import { EventEmitter } from "tseep/lib/ee-safe.js";
|
|
2
|
-
import { Readable, Writable } from "stream";
|
|
3
|
-
|
|
2
|
+
import { Readable, Writable } from "node:stream";
|
|
3
|
+
import type { uWebsockets } from "../uws.js";
|
|
4
|
+
/** Any payload accepted by uWebSockets or produced by streams. */
|
|
5
|
+
export type WebsocketMessage = string | Buffer | ArrayBuffer | ArrayBufferView;
|
|
6
|
+
/** Events emitted by the websocket polyfill. */
|
|
7
|
+
export type WebsocketEventMap = {
|
|
8
|
+
[event in string | symbol]: (...args: any[]) => void;
|
|
9
|
+
} & {
|
|
10
|
+
message: (message: WebsocketMessage, is_binary: boolean) => void;
|
|
11
|
+
close: (code: number, message: WebsocketMessage) => void;
|
|
12
|
+
drain: () => void;
|
|
13
|
+
ping: (message: WebsocketMessage) => void;
|
|
14
|
+
pong: (message: WebsocketMessage) => void;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Raw uws websocket with the context and polyfill attached by the engine.
|
|
18
|
+
* The native wrapper merges the upgrade user data into the socket object.
|
|
19
|
+
*/
|
|
20
|
+
export interface WebsocketRaw<TContext = Record<string, any>> extends uWebsockets.WebSocket<{
|
|
21
|
+
context?: TContext;
|
|
22
|
+
}> {
|
|
23
|
+
context?: TContext;
|
|
24
|
+
poly?: Websocket<TContext>;
|
|
25
|
+
}
|
|
26
|
+
declare const FRAGMENTS: {
|
|
27
|
+
readonly FIRST: "FIRST";
|
|
28
|
+
readonly MIDDLE: "MIDDLE";
|
|
29
|
+
readonly LAST: "LAST";
|
|
30
|
+
};
|
|
31
|
+
type Fragment = (typeof FRAGMENTS)[keyof typeof FRAGMENTS];
|
|
32
|
+
export default class Websocket<TContext = Record<string, any>> extends EventEmitter<WebsocketEventMap> {
|
|
4
33
|
#private;
|
|
5
|
-
constructor(ws:
|
|
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;
|
|
34
|
+
constructor(ws: WebsocketRaw<TContext>);
|
|
22
35
|
/**
|
|
23
|
-
* Alias of
|
|
24
|
-
*
|
|
25
|
-
* @param {Function} callback
|
|
26
|
-
* @returns {Websocket}
|
|
36
|
+
* Alias of uws cork, used to batch multiple operations for efficiency.
|
|
27
37
|
*/
|
|
28
|
-
atomic(callback:
|
|
38
|
+
atomic(callback: () => void): Websocket<TContext> | uWebsockets.WebSocket<{
|
|
39
|
+
context?: TContext;
|
|
40
|
+
}>;
|
|
29
41
|
/**
|
|
30
|
-
* Sends a message
|
|
31
|
-
* Returns
|
|
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}
|
|
42
|
+
* Sends a message over the connection.
|
|
43
|
+
* Returns the uws send status, or false when the socket is closed.
|
|
38
44
|
*/
|
|
39
|
-
send(message:
|
|
45
|
+
send(message: WebsocketMessage, is_binary?: boolean, compress?: boolean): number | false;
|
|
40
46
|
/**
|
|
41
47
|
* 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
48
|
*/
|
|
47
|
-
ping(message?:
|
|
49
|
+
ping(message?: WebsocketMessage): number | false;
|
|
48
50
|
/**
|
|
49
|
-
* Destroys this polyfill
|
|
51
|
+
* Destroys this polyfill and dereferences the underlying socket.
|
|
50
52
|
* @private
|
|
51
53
|
*/
|
|
52
|
-
|
|
54
|
+
_destroy(): void;
|
|
53
55
|
/**
|
|
54
|
-
* Gracefully closes
|
|
55
|
-
*
|
|
56
|
-
* @param {Number=} code
|
|
57
|
-
* @param {(String|Buffer|ArrayBuffer)=} message
|
|
56
|
+
* Gracefully closes the connection, sending an optional code and message.
|
|
58
57
|
*/
|
|
59
|
-
close(code?: number
|
|
58
|
+
close(code?: number, message?: WebsocketMessage): void;
|
|
60
59
|
/**
|
|
61
|
-
* Forcefully closes
|
|
62
|
-
* No websocket close code/message is sent.
|
|
63
|
-
* This will immediately emit the 'close' event.
|
|
60
|
+
* Forcefully closes the connection without a close message.
|
|
64
61
|
*/
|
|
65
62
|
destroy(): void;
|
|
66
63
|
/**
|
|
67
|
-
* Returns whether this
|
|
68
|
-
*
|
|
69
|
-
* @param {String} topic
|
|
70
|
-
* @returns {Boolean}
|
|
64
|
+
* Returns whether this connection is subscribed to the topic.
|
|
71
65
|
*/
|
|
72
66
|
is_subscribed(topic: string): boolean;
|
|
73
67
|
/**
|
|
74
|
-
*
|
|
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}
|
|
68
|
+
* Subscribes to a topic in mqtt syntax.
|
|
79
69
|
*/
|
|
80
70
|
subscribe(topic: string): boolean;
|
|
81
71
|
/**
|
|
82
|
-
*
|
|
83
|
-
* Returns true on success, if the WebSocket was subscribed.
|
|
84
|
-
*
|
|
85
|
-
* @param {String} topic
|
|
86
|
-
* @returns {Boolean}
|
|
72
|
+
* Unsubscribes from a topic.
|
|
87
73
|
*/
|
|
88
74
|
unsubscribe(topic: string): boolean;
|
|
89
75
|
/**
|
|
90
|
-
*
|
|
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
|
|
76
|
+
* Publishes a message to a topic in mqtt syntax.
|
|
97
77
|
*/
|
|
98
|
-
publish(topic: string, message:
|
|
78
|
+
publish(topic: string, message: WebsocketMessage, is_binary?: boolean, compress?: boolean): boolean;
|
|
99
79
|
/**
|
|
100
|
-
* Buffers the provided fragment and returns the
|
|
101
|
-
*
|
|
102
|
-
* @param {String|Buffer|ArrayBuffer} fragment
|
|
103
|
-
* @returns {String|Buffer|ArrayBuffer|undefined}
|
|
80
|
+
* Buffers the provided fragment and returns the previously buffered one.
|
|
104
81
|
*/
|
|
105
|
-
_buffer_fragment(fragment
|
|
82
|
+
_buffer_fragment(fragment?: WebsocketMessage): WebsocketMessage | undefined;
|
|
106
83
|
/**
|
|
107
|
-
*
|
|
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}
|
|
84
|
+
* Writes a fragment using the appropriate uws fragment method.
|
|
115
85
|
*/
|
|
116
|
-
_write(type:
|
|
86
|
+
_write(type: Fragment, chunk: WebsocketMessage, is_binary?: boolean, compress?: boolean, callback?: () => void): number;
|
|
117
87
|
/**
|
|
118
|
-
* Streams
|
|
119
|
-
*
|
|
120
|
-
* @param {Readable} stream
|
|
121
|
-
* @param {String} type
|
|
122
|
-
* @param {Buffer|ArrayBuffer} chunk
|
|
123
|
-
* @param {Boolean} is_binary
|
|
88
|
+
* Streams a chunk, pausing the source stream while under backpressure.
|
|
124
89
|
*/
|
|
125
|
-
_stream_chunk(stream: Readable, type:
|
|
90
|
+
_stream_chunk(stream: Readable, type: Fragment, chunk: WebsocketMessage, is_binary: boolean): void;
|
|
126
91
|
/**
|
|
127
|
-
*
|
|
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}
|
|
92
|
+
* Streams a readable as a single fragmented message.
|
|
134
93
|
*/
|
|
135
|
-
stream(readable: Readable, is_binary?: boolean
|
|
94
|
+
stream(readable: Readable, is_binary?: boolean): Promise<void>;
|
|
136
95
|
/**
|
|
137
|
-
* Underlying
|
|
96
|
+
* Underlying uws websocket object.
|
|
138
97
|
*/
|
|
139
|
-
get raw():
|
|
98
|
+
get raw(): WebsocketRaw<TContext> | null;
|
|
140
99
|
/**
|
|
141
|
-
*
|
|
142
|
-
* @returns {String}
|
|
100
|
+
* Remote ip address of this connection.
|
|
143
101
|
*/
|
|
144
102
|
get ip(): string;
|
|
145
103
|
/**
|
|
146
|
-
*
|
|
147
|
-
* @returns {Object}
|
|
104
|
+
* Context values attached during the upgrade.
|
|
148
105
|
*/
|
|
149
|
-
get context():
|
|
106
|
+
get context(): TContext;
|
|
150
107
|
/**
|
|
151
|
-
*
|
|
152
|
-
* @returns {Boolean}
|
|
108
|
+
* Whether this connection is closed.
|
|
153
109
|
*/
|
|
154
110
|
get closed(): boolean;
|
|
155
111
|
/**
|
|
156
|
-
*
|
|
157
|
-
* This is similar to the bufferedAmount property in the browser counterpart.
|
|
158
|
-
* @returns {Number}
|
|
112
|
+
* Bytes currently buffered in backpressure.
|
|
159
113
|
*/
|
|
160
114
|
get buffered(): number;
|
|
161
115
|
/**
|
|
162
|
-
*
|
|
163
|
-
* @returns {Array<String>}
|
|
116
|
+
* Topics this connection is subscribed to.
|
|
164
117
|
*/
|
|
165
|
-
get topics():
|
|
118
|
+
get topics(): string[];
|
|
166
119
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* @returns {Writable}
|
|
120
|
+
* Writable stream bound to this connection for piping.
|
|
121
|
+
* Only one writable can be used at a time.
|
|
171
122
|
*/
|
|
172
123
|
get writable(): Writable;
|
|
173
124
|
}
|
|
125
|
+
export {};
|
package/lib/ws/Websocket.js
CHANGED
|
@@ -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
|
|
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
|
|
59
|
-
* Returns
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
208
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
249
|
-
if (this.#stream)
|
|
250
|
-
throw new Error("Websocket.stream(readable) ->
|
|
251
|
-
|
|
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
|
-
|
|
255
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
+
this.#ws.send(fragment, is_binary);
|
|
276
170
|
}
|
|
277
171
|
else {
|
|
278
|
-
|
|
279
|
-
scope._stream_chunk(scope.#stream, FRAGMENTS.LAST, fragment, is_binary);
|
|
172
|
+
this._stream_chunk(readable, FRAGMENTS.LAST, fragment, is_binary);
|
|
280
173
|
}
|
|
281
|
-
|
|
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
|
-
/*
|
|
180
|
+
/* getters */
|
|
290
181
|
/**
|
|
291
|
-
* Underlying
|
|
182
|
+
* Underlying uws websocket object.
|
|
292
183
|
*/
|
|
293
184
|
get raw() {
|
|
294
185
|
return this.#ws;
|
|
295
186
|
}
|
|
296
187
|
/**
|
|
297
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
334
|
-
*
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
346
|
-
write: (chunk,
|
|
347
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
+
this.#stream = writable;
|
|
364
240
|
const end_stream = () => {
|
|
365
|
-
|
|
366
|
-
const fragment = scope._buffer_fragment();
|
|
241
|
+
const fragment = this._buffer_fragment();
|
|
367
242
|
if (is_first) {
|
|
368
|
-
|
|
369
|
-
|
|
243
|
+
this.#ws.send(fragment, true, false);
|
|
244
|
+
this.#stream = undefined;
|
|
370
245
|
}
|
|
371
246
|
else {
|
|
372
|
-
|
|
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
|
-
|
|
377
|
-
|
|
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 {};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import uws_wrapper from "../uws.js";
|
|
2
|
+
import Websocket from "./Websocket.js";
|
|
3
|
+
import { wrap_object, array_buffer_to_string } from "../utils.js";
|
|
4
|
+
export default class WebsocketRoute {
|
|
5
|
+
#upgrade_with;
|
|
6
|
+
#message_parser;
|
|
7
|
+
app;
|
|
8
|
+
pattern;
|
|
9
|
+
handler;
|
|
10
|
+
options = {
|
|
11
|
+
idle_timeout: 32,
|
|
12
|
+
message_type: "String",
|
|
13
|
+
compression: uws_wrapper.DISABLED,
|
|
14
|
+
max_backpressure: 1024 * 1024,
|
|
15
|
+
max_payload_length: 32 * 1024,
|
|
16
|
+
};
|
|
17
|
+
constructor({ app, pattern, handler, options, }) {
|
|
18
|
+
this.app = app;
|
|
19
|
+
this.pattern = pattern;
|
|
20
|
+
this.handler = handler;
|
|
21
|
+
// merge user options over the local defaults
|
|
22
|
+
wrap_object(this.options, options ?? {});
|
|
23
|
+
this.#message_parser = this._get_message_parser(this.options.message_type);
|
|
24
|
+
this._load_companion_route();
|
|
25
|
+
this._create_uws_route();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Returns a parser that converts a uws ArrayBuffer to the configured type.
|
|
29
|
+
*/
|
|
30
|
+
_get_message_parser(type) {
|
|
31
|
+
switch (type) {
|
|
32
|
+
case "String":
|
|
33
|
+
return (array_buffer) => array_buffer_to_string(array_buffer);
|
|
34
|
+
case "Buffer":
|
|
35
|
+
// copy the buffer because uws deallocates it after this sync call
|
|
36
|
+
return (array_buffer) => Buffer.concat([Buffer.from(array_buffer)]);
|
|
37
|
+
case "ArrayBuffer":
|
|
38
|
+
return (array_buffer) => array_buffer;
|
|
39
|
+
default:
|
|
40
|
+
throw new Error("Server.ws(options) -> options.message_type must be one of ['String', 'Buffer', 'ArrayBuffer']");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Loads the companion upgrade route from the app routes.
|
|
45
|
+
* @private
|
|
46
|
+
*/
|
|
47
|
+
_load_companion_route() {
|
|
48
|
+
const companion = this.app.routes["upgrade"][this.pattern];
|
|
49
|
+
if (companion) {
|
|
50
|
+
this.#upgrade_with = companion;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// create a temporary default route that upgrades every request
|
|
54
|
+
this.app._create_route({
|
|
55
|
+
method: "upgrade",
|
|
56
|
+
pattern: this.pattern,
|
|
57
|
+
handler: (_request, response) => response.upgrade(),
|
|
58
|
+
options: {
|
|
59
|
+
_temporary: true,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
this.#upgrade_with = this.app.routes["upgrade"][this.pattern];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Sets the upgrade route used for incoming upgrade requests.
|
|
66
|
+
* @private
|
|
67
|
+
*/
|
|
68
|
+
_set_upgrade_route(route) {
|
|
69
|
+
this.#upgrade_with = route;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Creates the uws ws route that powers this instance.
|
|
73
|
+
* @private
|
|
74
|
+
*/
|
|
75
|
+
_create_uws_route() {
|
|
76
|
+
const { compression, idle_timeout, max_backpressure, max_payload_length, } = this.options;
|
|
77
|
+
const uws_options = {
|
|
78
|
+
compression,
|
|
79
|
+
idleTimeout: idle_timeout,
|
|
80
|
+
maxBackpressure: max_backpressure,
|
|
81
|
+
maxPayloadLength: max_payload_length,
|
|
82
|
+
};
|
|
83
|
+
// pipe upgrade requests through the app request lifecycle
|
|
84
|
+
uws_options.upgrade = (uws_response, uws_request, socket_context) => this.app._handle_uws_request(this.#upgrade_with, uws_request, uws_response, socket_context);
|
|
85
|
+
uws_options.open = (ws) => this._on_open(ws);
|
|
86
|
+
uws_options.drain = (ws) => this._on_drain(ws);
|
|
87
|
+
uws_options.ping = (ws, message) => this._on_ping(ws, message);
|
|
88
|
+
uws_options.pong = (ws, message) => this._on_pong(ws, message);
|
|
89
|
+
uws_options.close = (ws, code, message) => this._on_close(ws, code, message);
|
|
90
|
+
uws_options.message = (ws, message, is_binary) => this._on_message(ws, message, is_binary);
|
|
91
|
+
this.app.uws_instance.ws(this.pattern, uws_options);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Handles the uws open event.
|
|
95
|
+
* @private
|
|
96
|
+
*/
|
|
97
|
+
_on_open(ws) {
|
|
98
|
+
ws.poly = new Websocket(ws);
|
|
99
|
+
this.handler(ws.poly);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Handles the uws ping event.
|
|
103
|
+
* @private
|
|
104
|
+
*/
|
|
105
|
+
_on_ping(ws, message = new ArrayBuffer(0)) {
|
|
106
|
+
ws.poly.emit("ping", this.#message_parser(message));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Handles the uws pong event.
|
|
110
|
+
* @private
|
|
111
|
+
*/
|
|
112
|
+
_on_pong(ws, message = new ArrayBuffer(0)) {
|
|
113
|
+
ws.poly.emit("pong", this.#message_parser(message));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Handles the uws drain event.
|
|
117
|
+
* @private
|
|
118
|
+
*/
|
|
119
|
+
_on_drain(ws) {
|
|
120
|
+
ws.poly.emit("drain");
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Handles the uws message event.
|
|
124
|
+
* @private
|
|
125
|
+
*/
|
|
126
|
+
_on_message(ws, message = new ArrayBuffer(0), is_binary) {
|
|
127
|
+
ws.poly.emit("message", this.#message_parser(message), is_binary);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Handles the uws close event.
|
|
131
|
+
* @private
|
|
132
|
+
*/
|
|
133
|
+
_on_close(ws, code, message = new ArrayBuffer(0)) {
|
|
134
|
+
ws.poly._destroy();
|
|
135
|
+
ws.poly.emit("close", code, this.#message_parser(message));
|
|
136
|
+
// release the polyfill so it can be garbage collected
|
|
137
|
+
delete ws.poly;
|
|
138
|
+
}
|
|
139
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linebridge/engine-neo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -34,6 +34,6 @@
|
|
|
34
34
|
"tseep": "^1.3.1"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
|
-
"
|
|
37
|
+
"install": "node install.mjs"
|
|
38
38
|
}
|
|
39
39
|
}
|