@webiny/api-websockets-standalone 0.0.0-unstable.5387e5f7c8 → 0.0.0-unstable.eb81deee7e
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/adapter/NodeWsAdapter.js +1 -3
- package/adapter/NodeWsAdapter.js.map +1 -1
- package/heartbeat/HeartbeatManager.d.ts +1 -5
- package/heartbeat/HeartbeatManager.js +2 -28
- package/heartbeat/HeartbeatManager.js.map +1 -1
- package/package.json +7 -7
- package/server/WebsocketsServer.d.ts +0 -2
- package/server/WebsocketsServer.js +11 -32
- package/server/WebsocketsServer.js.map +1 -1
- package/server/types.d.ts +0 -5
package/adapter/NodeWsAdapter.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { WebSocketServer } from "ws";
|
|
2
2
|
import { WebsocketsServerAdapter } from "./abstractions.js";
|
|
3
|
-
const MAX_PAYLOAD_BYTES = 65536;
|
|
4
3
|
class NodeWsAdapterImpl {
|
|
5
4
|
getWss() {
|
|
6
5
|
if (!this.wss) throw new Error("WebSocketServer has not been started. Call start() first.");
|
|
@@ -8,8 +7,7 @@ class NodeWsAdapterImpl {
|
|
|
8
7
|
}
|
|
9
8
|
start(_server) {
|
|
10
9
|
this.wss = new WebSocketServer({
|
|
11
|
-
noServer: true
|
|
12
|
-
maxPayload: MAX_PAYLOAD_BYTES
|
|
10
|
+
noServer: true
|
|
13
11
|
});
|
|
14
12
|
}
|
|
15
13
|
handleUpgrade(request, socket, head) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter/NodeWsAdapter.js","sources":["../../src/adapter/NodeWsAdapter.ts"],"sourcesContent":["import type { Server as HttpServer } from \"node:http\";\nimport type { IncomingMessage } from \"node:http\";\nimport type { Duplex } from \"node:stream\";\nimport { WebSocketServer } from \"ws\";\nimport { WebSocket } from \"ws\";\nimport { WebsocketsServerAdapter } from \"./abstractions.js\";\n\
|
|
1
|
+
{"version":3,"file":"adapter/NodeWsAdapter.js","sources":["../../src/adapter/NodeWsAdapter.ts"],"sourcesContent":["import type { Server as HttpServer } from \"node:http\";\nimport type { IncomingMessage } from \"node:http\";\nimport type { Duplex } from \"node:stream\";\nimport { WebSocketServer } from \"ws\";\nimport { WebSocket } from \"ws\";\nimport { WebsocketsServerAdapter } from \"./abstractions.js\";\n\nclass NodeWsAdapterImpl implements WebsocketsServerAdapter.Interface<WebSocket> {\n private wss: WebSocketServer | undefined;\n\n private getWss(): WebSocketServer {\n if (!this.wss) {\n throw new Error(\"WebSocketServer has not been started. Call start() first.\");\n }\n return this.wss;\n }\n\n /* The server param is kept for interface compliance but not used —\n the HTTP upgrade event is handled externally via handleUpgrade(). */\n public start(_server: HttpServer): void {\n this.wss = new WebSocketServer({ noServer: true });\n }\n\n public handleUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer): void {\n this.getWss().handleUpgrade(request, socket, head, ws => {\n this.getWss().emit(\"connection\", ws, request);\n });\n }\n\n public stop(): Promise<void> {\n const wss = this.getWss();\n /* Terminate all open client connections so wss.close() resolves immediately. */\n for (const client of wss.clients) {\n client.terminate();\n }\n return new Promise((resolve, reject) => {\n wss.close(err => {\n if (err) {\n reject(err);\n return;\n }\n resolve();\n });\n });\n }\n\n public onConnection(\n cb: (socket: WebSocket, request: import(\"node:http\").IncomingMessage) => void\n ): void {\n this.getWss().on(\"connection\", cb);\n }\n\n public onMessage(socket: WebSocket, cb: (data: Buffer) => void): void {\n socket.on(\"message\", cb);\n }\n\n public onClose(socket: WebSocket, cb: (code: number, reason: Buffer) => void): void {\n socket.on(\"close\", cb);\n }\n\n public onError(socket: WebSocket, cb: (error: Error) => void): void {\n socket.on(\"error\", cb);\n }\n\n public send(socket: WebSocket, data: string): Promise<void> {\n return new Promise((resolve, reject) => {\n socket.send(data, err => {\n if (err) {\n reject(err);\n return;\n }\n resolve();\n });\n });\n }\n\n public close(socket: WebSocket, code?: number, reason?: string): void {\n socket.close(code, reason);\n }\n}\n\nexport { NodeWsAdapterImpl };\n\nexport const NodeWsAdapter = WebsocketsServerAdapter.createImplementation({\n implementation: NodeWsAdapterImpl,\n dependencies: []\n});\n"],"names":["NodeWsAdapterImpl","Error","_server","WebSocketServer","request","socket","head","ws","wss","client","Promise","resolve","reject","err","cb","data","code","reason","NodeWsAdapter","WebsocketsServerAdapter"],"mappings":";;AAOA,MAAMA;IAGM,SAA0B;QAC9B,IAAI,CAAC,IAAI,CAAC,GAAG,EACT,MAAM,IAAIC,MAAM;QAEpB,OAAO,IAAI,CAAC,GAAG;IACnB;IAIO,MAAMC,OAAmB,EAAQ;QACpC,IAAI,CAAC,GAAG,GAAG,IAAIC,gBAAgB;YAAE,UAAU;QAAK;IACpD;IAEO,cAAcC,OAAwB,EAAEC,MAAc,EAAEC,IAAY,EAAQ;QAC/E,IAAI,CAAC,MAAM,GAAG,aAAa,CAACF,SAASC,QAAQC,MAAMC,CAAAA;YAC/C,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,cAAcA,IAAIH;QACzC;IACJ;IAEO,OAAsB;QACzB,MAAMI,MAAM,IAAI,CAAC,MAAM;QAEvB,KAAK,MAAMC,UAAUD,IAAI,OAAO,CAC5BC,OAAO,SAAS;QAEpB,OAAO,IAAIC,QAAQ,CAACC,SAASC;YACzBJ,IAAI,KAAK,CAACK,CAAAA;gBACN,IAAIA,KAAK,YACLD,OAAOC;gBAGXF;YACJ;QACJ;IACJ;IAEO,aACHG,EAA6E,EACzE;QACJ,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC,cAAcA;IACnC;IAEO,UAAUT,MAAiB,EAAES,EAA0B,EAAQ;QAClET,OAAO,EAAE,CAAC,WAAWS;IACzB;IAEO,QAAQT,MAAiB,EAAES,EAA0C,EAAQ;QAChFT,OAAO,EAAE,CAAC,SAASS;IACvB;IAEO,QAAQT,MAAiB,EAAES,EAA0B,EAAQ;QAChET,OAAO,EAAE,CAAC,SAASS;IACvB;IAEO,KAAKT,MAAiB,EAAEU,IAAY,EAAiB;QACxD,OAAO,IAAIL,QAAQ,CAACC,SAASC;YACzBP,OAAO,IAAI,CAACU,MAAMF,CAAAA;gBACd,IAAIA,KAAK,YACLD,OAAOC;gBAGXF;YACJ;QACJ;IACJ;IAEO,MAAMN,MAAiB,EAAEW,IAAa,EAAEC,MAAe,EAAQ;QAClEZ,OAAO,KAAK,CAACW,MAAMC;IACvB;AACJ;AAIO,MAAMC,gBAAgBC,wBAAwB,oBAAoB,CAAC;IACtE,gBAAgBnB;IAChB,cAAc,EAAE;AACpB"}
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import type { WebsocketsConnectionManager } from "../connectionManager/abstractions.js";
|
|
2
|
-
import type { WebsocketsServerAdapter } from "../adapter/abstractions.js";
|
|
3
2
|
export declare class HeartbeatManager {
|
|
4
3
|
private readonly connectionManager;
|
|
5
|
-
private readonly adapter;
|
|
6
4
|
private readonly interval;
|
|
7
|
-
private readonly staleAfter;
|
|
8
5
|
private timer;
|
|
9
|
-
constructor(connectionManager: WebsocketsConnectionManager.Interface<unknown>,
|
|
6
|
+
constructor(connectionManager: WebsocketsConnectionManager.Interface<unknown>, interval: number);
|
|
10
7
|
start(): void;
|
|
11
8
|
stop(): void;
|
|
12
|
-
evictStale(): Promise<void>;
|
|
13
9
|
}
|
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
const STALE_CLOSE_CODE = 1001;
|
|
2
|
-
const STALE_CLOSE_REASON = "Connection went stale.";
|
|
3
1
|
class HeartbeatManager {
|
|
4
|
-
constructor(connectionManager,
|
|
2
|
+
constructor(connectionManager, interval){
|
|
5
3
|
this.connectionManager = connectionManager;
|
|
6
|
-
this.adapter = adapter;
|
|
7
4
|
this.interval = interval;
|
|
8
|
-
this.staleAfter = staleAfter;
|
|
9
5
|
}
|
|
10
6
|
start() {
|
|
11
7
|
this.timer = setInterval(()=>{
|
|
12
|
-
this.
|
|
8
|
+
this.connectionManager.cleanup(5 * this.interval);
|
|
13
9
|
}, this.interval);
|
|
14
10
|
}
|
|
15
11
|
stop() {
|
|
@@ -18,28 +14,6 @@ class HeartbeatManager {
|
|
|
18
14
|
this.timer = void 0;
|
|
19
15
|
}
|
|
20
16
|
}
|
|
21
|
-
async evictStale() {
|
|
22
|
-
const sockets = new Map();
|
|
23
|
-
for (const connectionId of this.connectionManager.getActiveConnectionIds()){
|
|
24
|
-
const socket = this.connectionManager.getSocket(connectionId);
|
|
25
|
-
sockets.set(connectionId, socket);
|
|
26
|
-
}
|
|
27
|
-
let evicted;
|
|
28
|
-
try {
|
|
29
|
-
evicted = await this.connectionManager.cleanup(this.staleAfter);
|
|
30
|
-
} catch (error) {
|
|
31
|
-
console.error("Failed to evict stale WebSocket connections:", error);
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
for (const connectionId of evicted){
|
|
35
|
-
const socket = sockets.get(connectionId);
|
|
36
|
-
if (void 0 !== socket) try {
|
|
37
|
-
this.adapter.close(socket, STALE_CLOSE_CODE, STALE_CLOSE_REASON);
|
|
38
|
-
} catch (error) {
|
|
39
|
-
console.error(`Failed to close stale WebSocket "${connectionId}":`, error);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
17
|
}
|
|
44
18
|
export { HeartbeatManager };
|
|
45
19
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"heartbeat/HeartbeatManager.js","sources":["../../src/heartbeat/HeartbeatManager.ts"],"sourcesContent":["import type { WebsocketsConnectionManager } from \"~/connectionManager/abstractions.js\";\
|
|
1
|
+
{"version":3,"file":"heartbeat/HeartbeatManager.js","sources":["../../src/heartbeat/HeartbeatManager.ts"],"sourcesContent":["import type { WebsocketsConnectionManager } from \"~/connectionManager/abstractions.js\";\n\nexport class HeartbeatManager {\n private readonly connectionManager: WebsocketsConnectionManager.Interface<unknown>;\n private readonly interval: number;\n private timer: ReturnType<typeof setInterval> | undefined;\n\n public constructor(\n connectionManager: WebsocketsConnectionManager.Interface<unknown>,\n interval: number\n ) {\n this.connectionManager = connectionManager;\n this.interval = interval;\n }\n\n public start(): void {\n this.timer = setInterval(() => {\n this.connectionManager.cleanup(5 * this.interval);\n }, this.interval);\n }\n\n public stop(): void {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = undefined;\n }\n }\n}\n"],"names":["HeartbeatManager","connectionManager","interval","setInterval","clearInterval","undefined"],"mappings":"AAEO,MAAMA;IAKT,YACIC,iBAAiE,EACjEC,QAAgB,CAClB;QACE,IAAI,CAAC,iBAAiB,GAAGD;QACzB,IAAI,CAAC,QAAQ,GAAGC;IACpB;IAEO,QAAc;QACjB,IAAI,CAAC,KAAK,GAAGC,YAAY;YACrB,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,QAAQ;QACpD,GAAG,IAAI,CAAC,QAAQ;IACpB;IAEO,OAAa;QAChB,IAAI,IAAI,CAAC,KAAK,EAAE;YACZC,cAAc,IAAI,CAAC,KAAK;YACxB,IAAI,CAAC,KAAK,GAAGC;QACjB;IACJ;AACJ"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webiny/api-websockets-standalone",
|
|
3
|
-
"version": "0.0.0-unstable.
|
|
3
|
+
"version": "0.0.0-unstable.eb81deee7e",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -16,17 +16,17 @@
|
|
|
16
16
|
},
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@webiny/api": "0.0.0-unstable.
|
|
20
|
-
"@webiny/api-websockets": "0.0.0-unstable.
|
|
21
|
-
"@webiny/feature": "0.0.0-unstable.
|
|
22
|
-
"@webiny/plugins": "0.0.0-unstable.
|
|
23
|
-
"@webiny/utils": "0.0.0-unstable.
|
|
19
|
+
"@webiny/api": "0.0.0-unstable.eb81deee7e",
|
|
20
|
+
"@webiny/api-websockets": "0.0.0-unstable.eb81deee7e",
|
|
21
|
+
"@webiny/feature": "0.0.0-unstable.eb81deee7e",
|
|
22
|
+
"@webiny/plugins": "0.0.0-unstable.eb81deee7e",
|
|
23
|
+
"@webiny/utils": "0.0.0-unstable.eb81deee7e",
|
|
24
24
|
"ws": "8.21.3",
|
|
25
25
|
"zod": "4.4.3"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/ws": "8.18.1",
|
|
29
|
-
"@webiny/build-tools": "0.0.0-unstable.
|
|
29
|
+
"@webiny/build-tools": "0.0.0-unstable.eb81deee7e",
|
|
30
30
|
"rimraf": "6.1.3",
|
|
31
31
|
"typescript": "7.0.2",
|
|
32
32
|
"vitest": "4.1.11"
|
|
@@ -11,7 +11,6 @@ interface WebsocketsServerParams {
|
|
|
11
11
|
connectionManager?: WebsocketsConnectionManager.Interface<unknown>;
|
|
12
12
|
authenticate?: WebsocketsConnectionAuthenticator;
|
|
13
13
|
heartbeatInterval: number;
|
|
14
|
-
staleAfter: number;
|
|
15
14
|
debug: boolean;
|
|
16
15
|
port: number;
|
|
17
16
|
host: string;
|
|
@@ -45,7 +44,6 @@ declare class WebsocketsServer implements IWebsocketsServer {
|
|
|
45
44
|
* don't crash the server or the upgrade, the connection just ends up unaddressable.
|
|
46
45
|
*/
|
|
47
46
|
private registerConnection;
|
|
48
|
-
private removeConnection;
|
|
49
47
|
private resolvePortFromExistingServer;
|
|
50
48
|
private listen;
|
|
51
49
|
private closeHttpServer;
|
|
@@ -8,8 +8,6 @@ const ANONYMOUS_IDENTITY = {
|
|
|
8
8
|
displayName: "",
|
|
9
9
|
type: ""
|
|
10
10
|
};
|
|
11
|
-
const DEFAULT_HEARTBEAT_INTERVAL = 60000;
|
|
12
|
-
const DEFAULT_STALE_AFTER = 900000;
|
|
13
11
|
const toHeaders = (raw)=>{
|
|
14
12
|
const headers = {};
|
|
15
13
|
for (const [key, value] of Object.entries(raw))if ("string" == typeof value) headers[key] = value;
|
|
@@ -29,7 +27,7 @@ class WebsocketsServer {
|
|
|
29
27
|
this.debug = params.debug;
|
|
30
28
|
this.requestedPort = params.port;
|
|
31
29
|
this.requestedHost = params.host;
|
|
32
|
-
if (params.connectionManager) this.heartbeat = new HeartbeatManager(params.connectionManager, params.
|
|
30
|
+
if (params.connectionManager) this.heartbeat = new HeartbeatManager(params.connectionManager, params.heartbeatInterval);
|
|
33
31
|
}
|
|
34
32
|
async start() {
|
|
35
33
|
this.adapter.start(this.httpServer);
|
|
@@ -57,21 +55,13 @@ class WebsocketsServer {
|
|
|
57
55
|
wireUpgradeHandler() {
|
|
58
56
|
this.httpServer.on("upgrade", async (request, socket, head)=>{
|
|
59
57
|
if (this.shuttingDown) return void socket.destroy();
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
const decision = await this.upgradeHandler.shouldUpgrade(request);
|
|
65
|
-
if (!decision.allowed) {
|
|
66
|
-
socket.write(`HTTP/1.1 ${decision.statusCode} ${decision.reason}\r\n\r\n`);
|
|
67
|
-
socket.destroy();
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
this.adapter.handleUpgrade(request, socket, head);
|
|
71
|
-
} catch (error) {
|
|
72
|
-
console.error("WebSocket upgrade failed:", error);
|
|
58
|
+
const decision = await this.upgradeHandler.shouldUpgrade(request);
|
|
59
|
+
if (!decision.allowed) {
|
|
60
|
+
socket.write(`HTTP/1.1 ${decision.statusCode} ${decision.reason}\r\n\r\n`);
|
|
73
61
|
socket.destroy();
|
|
62
|
+
return;
|
|
74
63
|
}
|
|
64
|
+
this.adapter.handleUpgrade(request, socket, head);
|
|
75
65
|
});
|
|
76
66
|
}
|
|
77
67
|
wireConnectionHandler() {
|
|
@@ -81,7 +71,7 @@ class WebsocketsServer {
|
|
|
81
71
|
const connectedAt = Date.now();
|
|
82
72
|
const host = request.headers.host || "localhost";
|
|
83
73
|
const headers = toHeaders(request.headers);
|
|
84
|
-
|
|
74
|
+
this.registerConnection({
|
|
85
75
|
connectionId,
|
|
86
76
|
socket,
|
|
87
77
|
request,
|
|
@@ -98,13 +88,11 @@ class WebsocketsServer {
|
|
|
98
88
|
} catch {
|
|
99
89
|
return;
|
|
100
90
|
}
|
|
101
|
-
this.connectionManager?.updateLastSeen(connectionId)
|
|
102
|
-
console.error(`Failed to update last seen for WebSocket connection "${connectionId}":`, error);
|
|
103
|
-
});
|
|
91
|
+
this.connectionManager?.updateLastSeen(connectionId);
|
|
104
92
|
});
|
|
105
93
|
this.adapter.onClose(socket, ()=>{
|
|
106
94
|
if (this.shuttingDown) return;
|
|
107
|
-
|
|
95
|
+
this.connectionManager?.remove(connectionId);
|
|
108
96
|
});
|
|
109
97
|
this.adapter.onError(socket, (error)=>{
|
|
110
98
|
console.error(`WebSocket error for connection "${connectionId}":`, error.message);
|
|
@@ -137,13 +125,6 @@ class WebsocketsServer {
|
|
|
137
125
|
console.error(`Failed to register WebSocket connection "${connectionId}":`, error instanceof Error ? error.message : String(error));
|
|
138
126
|
}
|
|
139
127
|
}
|
|
140
|
-
async removeConnection(connectionId) {
|
|
141
|
-
try {
|
|
142
|
-
await this.connectionManager?.remove(connectionId);
|
|
143
|
-
} catch (error) {
|
|
144
|
-
console.error(`Failed to remove WebSocket connection "${connectionId}":`, error);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
128
|
resolvePortFromExistingServer() {
|
|
148
129
|
const address = this.httpServer.address();
|
|
149
130
|
if (address && "string" != typeof address) {
|
|
@@ -180,8 +161,7 @@ const createWebsocketsServer = (params)=>{
|
|
|
180
161
|
upgradeHandler: new DefaultUpgradeHandlerImpl(),
|
|
181
162
|
connectionManager: params.connectionManager,
|
|
182
163
|
authenticate: params.authenticate,
|
|
183
|
-
heartbeatInterval: params.heartbeatInterval ??
|
|
184
|
-
staleAfter: params.staleAfter ?? DEFAULT_STALE_AFTER,
|
|
164
|
+
heartbeatInterval: params.heartbeatInterval ?? 60000,
|
|
185
165
|
debug: params.debug ?? false,
|
|
186
166
|
port: params.port ?? 0,
|
|
187
167
|
host: params.host ?? "127.0.0.1"
|
|
@@ -196,8 +176,7 @@ const attachWebsocketsServer = (params)=>{
|
|
|
196
176
|
upgradeHandler: new DefaultUpgradeHandlerImpl(),
|
|
197
177
|
connectionManager: params.connectionManager,
|
|
198
178
|
authenticate: params.authenticate,
|
|
199
|
-
heartbeatInterval: params.heartbeatInterval ??
|
|
200
|
-
staleAfter: params.staleAfter ?? DEFAULT_STALE_AFTER,
|
|
179
|
+
heartbeatInterval: params.heartbeatInterval ?? 60000,
|
|
201
180
|
debug: params.debug ?? false,
|
|
202
181
|
port: 0,
|
|
203
182
|
host: "127.0.0.1"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server/WebsocketsServer.js","sources":["../../src/server/WebsocketsServer.ts"],"sourcesContent":["import { createServer } from \"node:http\";\nimport type { Server as HttpServer, IncomingMessage } from \"node:http\";\nimport type { Duplex } from \"node:stream\";\nimport { mdbid } from \"@webiny/utils\";\nimport type { WebsocketsServerAdapter } from \"~/adapter/abstractions.js\";\nimport type { WebsocketsUpgradeHandler } from \"~/upgradeHandler/abstractions.js\";\nimport type { WebsocketsConnectionManager } from \"~/connectionManager/abstractions.js\";\nimport type { ConnectionRegistry } from \"@webiny/api-websockets/exports/api.js\";\nimport { NodeWsAdapterImpl } from \"~/adapter/NodeWsAdapter.js\";\nimport { DefaultUpgradeHandlerImpl } from \"~/upgradeHandler/DefaultUpgradeHandler.js\";\nimport { HeartbeatManager } from \"~/heartbeat/HeartbeatManager.js\";\nimport type {\n CreateWebsocketsServerParams,\n AttachWebsocketsServerParams,\n IWebsocketsServer,\n WebsocketsConnectionAuthenticator\n} from \"./types.js\";\n\nconst ANONYMOUS_IDENTITY: ConnectionRegistry.Identity = { id: \"\", displayName: \"\", type: \"\" };\n\nconst DEFAULT_HEARTBEAT_INTERVAL = 60_000;\n// Three admin ping intervals (5 minutes each): one late ping is not enough to evict a live tab.\nconst DEFAULT_STALE_AFTER = 15 * 60_000;\n\nconst toHeaders = (raw: IncomingMessage[\"headers\"]): Record<string, string> => {\n const headers: Record<string, string> = {};\n for (const [key, value] of Object.entries(raw)) {\n if (typeof value === \"string\") {\n headers[key] = value;\n }\n }\n return headers;\n};\n\ninterface WebsocketsServerParams {\n httpServer: HttpServer;\n ownsHttpServer: boolean;\n adapter: WebsocketsServerAdapter.Interface<unknown>;\n upgradeHandler: WebsocketsUpgradeHandler.Interface;\n connectionManager?: WebsocketsConnectionManager.Interface<unknown>;\n authenticate?: WebsocketsConnectionAuthenticator;\n heartbeatInterval: number;\n staleAfter: number;\n debug: boolean;\n port: number;\n host: string;\n}\n\nclass WebsocketsServer implements IWebsocketsServer {\n private readonly httpServer: HttpServer;\n private readonly ownsHttpServer: boolean;\n private readonly adapter: WebsocketsServerAdapter.Interface<unknown>;\n private readonly upgradeHandler: WebsocketsUpgradeHandler.Interface;\n private readonly connectionManager: WebsocketsConnectionManager.Interface<unknown> | undefined;\n private readonly authenticate: WebsocketsConnectionAuthenticator | undefined;\n private readonly heartbeat: HeartbeatManager | undefined;\n private readonly debug: boolean;\n private readonly requestedPort: number;\n private readonly requestedHost: string;\n\n private shuttingDown: boolean = false;\n private endpoint: string = \"\";\n private resolvedPort: number = 0;\n\n public constructor(params: WebsocketsServerParams) {\n this.httpServer = params.httpServer;\n this.ownsHttpServer = params.ownsHttpServer;\n this.adapter = params.adapter;\n this.upgradeHandler = params.upgradeHandler;\n this.connectionManager = params.connectionManager;\n this.authenticate = params.authenticate;\n this.debug = params.debug;\n this.requestedPort = params.port;\n this.requestedHost = params.host;\n\n if (params.connectionManager) {\n this.heartbeat = new HeartbeatManager(\n params.connectionManager,\n params.adapter,\n params.heartbeatInterval,\n params.staleAfter\n );\n }\n }\n\n public async start(): Promise<void> {\n this.adapter.start(this.httpServer);\n this.wireUpgradeHandler();\n this.wireConnectionHandler();\n\n if (this.ownsHttpServer) {\n await this.listen();\n } else {\n this.resolvePortFromExistingServer();\n }\n\n this.heartbeat?.start();\n\n if (this.debug) {\n console.log(`[WebsocketsServer] started on ${this.endpoint}`);\n }\n }\n\n public async stop(): Promise<void> {\n this.shuttingDown = true;\n this.heartbeat?.stop();\n\n await this.adapter.stop();\n\n if (this.connectionManager) {\n const activeIds = this.connectionManager.getActiveConnectionIds();\n for (const connectionId of activeIds) {\n await this.connectionManager.remove(connectionId);\n }\n }\n\n if (this.ownsHttpServer) {\n await this.closeHttpServer();\n }\n\n if (this.debug) {\n console.log(\"[WebsocketsServer] stopped\");\n }\n }\n\n public port(): number {\n return this.resolvedPort;\n }\n\n private wireUpgradeHandler(): void {\n this.httpServer.on(\n \"upgrade\",\n async (request: IncomingMessage, socket: Duplex, head: Buffer) => {\n if (this.shuttingDown) {\n socket.destroy();\n return;\n }\n\n /*\n * Node drops its own error listener before emitting `upgrade`, so an error on the\n * raw socket before `ws` takes it over would be unhandled and end the process.\n */\n socket.on(\"error\", error => {\n console.error(\"WebSocket upgrade socket error:\", error.message);\n });\n\n try {\n const decision = await this.upgradeHandler.shouldUpgrade(request);\n if (!decision.allowed) {\n socket.write(`HTTP/1.1 ${decision.statusCode} ${decision.reason}\\r\\n\\r\\n`);\n socket.destroy();\n return;\n }\n\n this.adapter.handleUpgrade(request, socket, head);\n } catch (error) {\n console.error(\"WebSocket upgrade failed:\", error);\n socket.destroy();\n }\n }\n );\n }\n\n private wireConnectionHandler(): void {\n this.adapter.onConnection((socket, request) => {\n if (this.shuttingDown) {\n return;\n }\n\n const connectionId = mdbid();\n const connectedAt = Date.now();\n const host = request.headers.host || \"localhost\";\n const headers = toHeaders(request.headers);\n\n // Register the connection under its real identity, decoded from the `?token` JWT. Both\n // the authentication and the registry write are async, so we kick this off without\n // awaiting it and wire the socket handlers below synchronously — that way we don't miss a\n // close or message that arrives during the gap. If a message does arrive before\n // registration finishes, the `getMetadata` guard below just drops it, which is fine since\n // the client doesn't send anything meaningful right at connect time.\n const registration = this.registerConnection({\n connectionId,\n socket,\n request,\n connectedAt,\n host,\n headers\n });\n\n this.adapter.onMessage(socket, data => {\n if (this.shuttingDown) {\n return;\n }\n\n const metadata = this.connectionManager?.getMetadata(connectionId);\n if (!metadata) {\n return;\n }\n\n try {\n JSON.parse(data.toString());\n } catch {\n return;\n }\n\n this.connectionManager?.updateLastSeen(connectionId).catch(error => {\n console.error(\n `Failed to update last seen for WebSocket connection \"${connectionId}\":`,\n error\n );\n });\n });\n\n this.adapter.onClose(socket, () => {\n if (this.shuttingDown) {\n return;\n }\n /*\n * Wait for registration: a socket that closes while it is still authenticating\n * would otherwise be removed first and then registered, leaving a dead entry.\n */\n void registration.then(() => this.removeConnection(connectionId));\n });\n\n this.adapter.onError(socket, error => {\n console.error(`WebSocket error for connection \"${connectionId}\":`, error.message);\n });\n });\n }\n\n /**\n * Authenticates the upgrade using the `?token`/`?tenant` query the app-websockets client sends,\n * then registers the live socket in the shared connection manager under the resolved identity and\n * tenant. Getting both right matters for targeted server→client sends: `SendToIdentity` finds\n * connections by identity id in the registry, so a connection that's anonymous (or never made it\n * into the registry) can't be reached. Everything here is guarded — if something goes wrong we\n * don't crash the server or the upgrade, the connection just ends up unaddressable.\n */\n private async registerConnection(params: {\n connectionId: string;\n socket: unknown;\n request: IncomingMessage;\n connectedAt: number;\n host: string;\n headers: Record<string, string>;\n }): Promise<void> {\n const { connectionId, socket, request, connectedAt, host, headers } = params;\n\n let identity: ConnectionRegistry.Identity = ANONYMOUS_IDENTITY;\n let tenant = \"\";\n\n try {\n const url = new URL(request.url ?? \"\", `http://${host}`);\n tenant = url.searchParams.get(\"tenant\") ?? \"\";\n const token = url.searchParams.get(\"token\") ?? \"\";\n\n if (this.authenticate && token) {\n const authenticated = await this.authenticate(token);\n if (authenticated?.id) {\n identity = authenticated;\n }\n }\n\n await this.connectionManager?.add({\n connectionId,\n socket,\n endpoint: this.endpoint,\n identity,\n tenant,\n connectedAt,\n host,\n headers\n });\n } catch (error) {\n console.error(\n `Failed to register WebSocket connection \"${connectionId}\":`,\n error instanceof Error ? error.message : String(error)\n );\n }\n }\n\n private async removeConnection(connectionId: string): Promise<void> {\n try {\n await this.connectionManager?.remove(connectionId);\n } catch (error) {\n console.error(`Failed to remove WebSocket connection \"${connectionId}\":`, error);\n }\n }\n\n private resolvePortFromExistingServer(): void {\n const address = this.httpServer.address();\n if (address && typeof address !== \"string\") {\n this.resolvedPort = address.port;\n this.endpoint = `ws://127.0.0.1:${this.resolvedPort}`;\n }\n }\n\n private listen(): Promise<void> {\n return new Promise((resolve, reject) => {\n this.httpServer.listen(this.requestedPort, this.requestedHost, () => {\n const address = this.httpServer.address();\n if (!address || typeof address === \"string\") {\n reject(new Error(\"Unexpected server address format.\"));\n return;\n }\n this.resolvedPort = address.port;\n this.endpoint = `ws://${this.requestedHost}:${this.resolvedPort}`;\n resolve();\n });\n });\n }\n\n private closeHttpServer(): Promise<void> {\n return new Promise((resolve, reject) => {\n this.httpServer.close(err => {\n if (err) {\n reject(err);\n return;\n }\n resolve();\n });\n });\n }\n}\n\nexport { WebsocketsServer };\n\nexport const createWebsocketsServer = (params: CreateWebsocketsServerParams): IWebsocketsServer => {\n const httpServer = createServer();\n const server = new WebsocketsServer({\n httpServer,\n ownsHttpServer: true,\n adapter: new NodeWsAdapterImpl(),\n upgradeHandler: new DefaultUpgradeHandlerImpl(),\n connectionManager: params.connectionManager,\n authenticate: params.authenticate,\n heartbeatInterval: params.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL,\n staleAfter: params.staleAfter ?? DEFAULT_STALE_AFTER,\n debug: params.debug ?? false,\n port: params.port ?? 0,\n host: params.host ?? \"127.0.0.1\"\n });\n return server;\n};\n\nexport const attachWebsocketsServer = (params: AttachWebsocketsServerParams): IWebsocketsServer => {\n const server = new WebsocketsServer({\n httpServer: params.server,\n ownsHttpServer: false,\n adapter: new NodeWsAdapterImpl(),\n upgradeHandler: new DefaultUpgradeHandlerImpl(),\n connectionManager: params.connectionManager,\n authenticate: params.authenticate,\n heartbeatInterval: params.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL,\n staleAfter: params.staleAfter ?? DEFAULT_STALE_AFTER,\n debug: params.debug ?? false,\n port: 0,\n host: \"127.0.0.1\"\n });\n return server;\n};\n"],"names":["ANONYMOUS_IDENTITY","DEFAULT_HEARTBEAT_INTERVAL","DEFAULT_STALE_AFTER","toHeaders","raw","headers","key","value","Object","WebsocketsServer","params","HeartbeatManager","console","activeIds","connectionId","request","socket","head","error","decision","mdbid","connectedAt","Date","host","registration","data","metadata","JSON","identity","tenant","url","URL","token","authenticated","Error","String","address","Promise","resolve","reject","err","createWebsocketsServer","httpServer","createServer","server","NodeWsAdapterImpl","DefaultUpgradeHandlerImpl","attachWebsocketsServer"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server/WebsocketsServer.js","sources":["../../src/server/WebsocketsServer.ts"],"sourcesContent":["import { createServer } from \"node:http\";\nimport type { Server as HttpServer, IncomingMessage } from \"node:http\";\nimport type { Duplex } from \"node:stream\";\nimport { mdbid } from \"@webiny/utils\";\nimport type { WebsocketsServerAdapter } from \"~/adapter/abstractions.js\";\nimport type { WebsocketsUpgradeHandler } from \"~/upgradeHandler/abstractions.js\";\nimport type { WebsocketsConnectionManager } from \"~/connectionManager/abstractions.js\";\nimport type { ConnectionRegistry } from \"@webiny/api-websockets/exports/api.js\";\nimport { NodeWsAdapterImpl } from \"~/adapter/NodeWsAdapter.js\";\nimport { DefaultUpgradeHandlerImpl } from \"~/upgradeHandler/DefaultUpgradeHandler.js\";\nimport { HeartbeatManager } from \"~/heartbeat/HeartbeatManager.js\";\nimport type {\n CreateWebsocketsServerParams,\n AttachWebsocketsServerParams,\n IWebsocketsServer,\n WebsocketsConnectionAuthenticator\n} from \"./types.js\";\n\nconst ANONYMOUS_IDENTITY: ConnectionRegistry.Identity = { id: \"\", displayName: \"\", type: \"\" };\n\nconst toHeaders = (raw: IncomingMessage[\"headers\"]): Record<string, string> => {\n const headers: Record<string, string> = {};\n for (const [key, value] of Object.entries(raw)) {\n if (typeof value === \"string\") {\n headers[key] = value;\n }\n }\n return headers;\n};\n\ninterface WebsocketsServerParams {\n httpServer: HttpServer;\n ownsHttpServer: boolean;\n adapter: WebsocketsServerAdapter.Interface<unknown>;\n upgradeHandler: WebsocketsUpgradeHandler.Interface;\n connectionManager?: WebsocketsConnectionManager.Interface<unknown>;\n authenticate?: WebsocketsConnectionAuthenticator;\n heartbeatInterval: number;\n debug: boolean;\n port: number;\n host: string;\n}\n\nclass WebsocketsServer implements IWebsocketsServer {\n private readonly httpServer: HttpServer;\n private readonly ownsHttpServer: boolean;\n private readonly adapter: WebsocketsServerAdapter.Interface<unknown>;\n private readonly upgradeHandler: WebsocketsUpgradeHandler.Interface;\n private readonly connectionManager: WebsocketsConnectionManager.Interface<unknown> | undefined;\n private readonly authenticate: WebsocketsConnectionAuthenticator | undefined;\n private readonly heartbeat: HeartbeatManager | undefined;\n private readonly debug: boolean;\n private readonly requestedPort: number;\n private readonly requestedHost: string;\n\n private shuttingDown: boolean = false;\n private endpoint: string = \"\";\n private resolvedPort: number = 0;\n\n public constructor(params: WebsocketsServerParams) {\n this.httpServer = params.httpServer;\n this.ownsHttpServer = params.ownsHttpServer;\n this.adapter = params.adapter;\n this.upgradeHandler = params.upgradeHandler;\n this.connectionManager = params.connectionManager;\n this.authenticate = params.authenticate;\n this.debug = params.debug;\n this.requestedPort = params.port;\n this.requestedHost = params.host;\n\n if (params.connectionManager) {\n this.heartbeat = new HeartbeatManager(\n params.connectionManager,\n params.heartbeatInterval\n );\n }\n }\n\n public async start(): Promise<void> {\n this.adapter.start(this.httpServer);\n this.wireUpgradeHandler();\n this.wireConnectionHandler();\n\n if (this.ownsHttpServer) {\n await this.listen();\n } else {\n this.resolvePortFromExistingServer();\n }\n\n this.heartbeat?.start();\n\n if (this.debug) {\n console.log(`[WebsocketsServer] started on ${this.endpoint}`);\n }\n }\n\n public async stop(): Promise<void> {\n this.shuttingDown = true;\n this.heartbeat?.stop();\n\n await this.adapter.stop();\n\n if (this.connectionManager) {\n const activeIds = this.connectionManager.getActiveConnectionIds();\n for (const connectionId of activeIds) {\n await this.connectionManager.remove(connectionId);\n }\n }\n\n if (this.ownsHttpServer) {\n await this.closeHttpServer();\n }\n\n if (this.debug) {\n console.log(\"[WebsocketsServer] stopped\");\n }\n }\n\n public port(): number {\n return this.resolvedPort;\n }\n\n private wireUpgradeHandler(): void {\n this.httpServer.on(\n \"upgrade\",\n async (request: IncomingMessage, socket: Duplex, head: Buffer) => {\n if (this.shuttingDown) {\n socket.destroy();\n return;\n }\n\n const decision = await this.upgradeHandler.shouldUpgrade(request);\n if (!decision.allowed) {\n socket.write(`HTTP/1.1 ${decision.statusCode} ${decision.reason}\\r\\n\\r\\n`);\n socket.destroy();\n return;\n }\n\n this.adapter.handleUpgrade(request, socket, head);\n }\n );\n }\n\n private wireConnectionHandler(): void {\n this.adapter.onConnection((socket, request) => {\n if (this.shuttingDown) {\n return;\n }\n\n const connectionId = mdbid();\n const connectedAt = Date.now();\n const host = request.headers.host || \"localhost\";\n const headers = toHeaders(request.headers);\n\n // Register the connection under its real identity, decoded from the `?token` JWT. Both\n // the authentication and the registry write are async, so we kick this off without\n // awaiting it and wire the socket handlers below synchronously — that way we don't miss a\n // close or message that arrives during the gap. If a message does arrive before\n // registration finishes, the `getMetadata` guard below just drops it, which is fine since\n // the client doesn't send anything meaningful right at connect time.\n void this.registerConnection({\n connectionId,\n socket,\n request,\n connectedAt,\n host,\n headers\n });\n\n this.adapter.onMessage(socket, data => {\n if (this.shuttingDown) {\n return;\n }\n\n const metadata = this.connectionManager?.getMetadata(connectionId);\n if (!metadata) {\n return;\n }\n\n try {\n JSON.parse(data.toString());\n } catch {\n return;\n }\n\n this.connectionManager?.updateLastSeen(connectionId);\n });\n\n this.adapter.onClose(socket, () => {\n if (this.shuttingDown) {\n return;\n }\n this.connectionManager?.remove(connectionId);\n });\n\n this.adapter.onError(socket, error => {\n console.error(`WebSocket error for connection \"${connectionId}\":`, error.message);\n });\n });\n }\n\n /**\n * Authenticates the upgrade using the `?token`/`?tenant` query the app-websockets client sends,\n * then registers the live socket in the shared connection manager under the resolved identity and\n * tenant. Getting both right matters for targeted server→client sends: `SendToIdentity` finds\n * connections by identity id in the registry, so a connection that's anonymous (or never made it\n * into the registry) can't be reached. Everything here is guarded — if something goes wrong we\n * don't crash the server or the upgrade, the connection just ends up unaddressable.\n */\n private async registerConnection(params: {\n connectionId: string;\n socket: unknown;\n request: IncomingMessage;\n connectedAt: number;\n host: string;\n headers: Record<string, string>;\n }): Promise<void> {\n const { connectionId, socket, request, connectedAt, host, headers } = params;\n\n let identity: ConnectionRegistry.Identity = ANONYMOUS_IDENTITY;\n let tenant = \"\";\n\n try {\n const url = new URL(request.url ?? \"\", `http://${host}`);\n tenant = url.searchParams.get(\"tenant\") ?? \"\";\n const token = url.searchParams.get(\"token\") ?? \"\";\n\n if (this.authenticate && token) {\n const authenticated = await this.authenticate(token);\n if (authenticated?.id) {\n identity = authenticated;\n }\n }\n\n await this.connectionManager?.add({\n connectionId,\n socket,\n endpoint: this.endpoint,\n identity,\n tenant,\n connectedAt,\n host,\n headers\n });\n } catch (error) {\n console.error(\n `Failed to register WebSocket connection \"${connectionId}\":`,\n error instanceof Error ? error.message : String(error)\n );\n }\n }\n\n private resolvePortFromExistingServer(): void {\n const address = this.httpServer.address();\n if (address && typeof address !== \"string\") {\n this.resolvedPort = address.port;\n this.endpoint = `ws://127.0.0.1:${this.resolvedPort}`;\n }\n }\n\n private listen(): Promise<void> {\n return new Promise((resolve, reject) => {\n this.httpServer.listen(this.requestedPort, this.requestedHost, () => {\n const address = this.httpServer.address();\n if (!address || typeof address === \"string\") {\n reject(new Error(\"Unexpected server address format.\"));\n return;\n }\n this.resolvedPort = address.port;\n this.endpoint = `ws://${this.requestedHost}:${this.resolvedPort}`;\n resolve();\n });\n });\n }\n\n private closeHttpServer(): Promise<void> {\n return new Promise((resolve, reject) => {\n this.httpServer.close(err => {\n if (err) {\n reject(err);\n return;\n }\n resolve();\n });\n });\n }\n}\n\nexport { WebsocketsServer };\n\nexport const createWebsocketsServer = (params: CreateWebsocketsServerParams): IWebsocketsServer => {\n const httpServer = createServer();\n const server = new WebsocketsServer({\n httpServer,\n ownsHttpServer: true,\n adapter: new NodeWsAdapterImpl(),\n upgradeHandler: new DefaultUpgradeHandlerImpl(),\n connectionManager: params.connectionManager,\n authenticate: params.authenticate,\n heartbeatInterval: params.heartbeatInterval ?? 60_000,\n debug: params.debug ?? false,\n port: params.port ?? 0,\n host: params.host ?? \"127.0.0.1\"\n });\n return server;\n};\n\nexport const attachWebsocketsServer = (params: AttachWebsocketsServerParams): IWebsocketsServer => {\n const server = new WebsocketsServer({\n httpServer: params.server,\n ownsHttpServer: false,\n adapter: new NodeWsAdapterImpl(),\n upgradeHandler: new DefaultUpgradeHandlerImpl(),\n connectionManager: params.connectionManager,\n authenticate: params.authenticate,\n heartbeatInterval: params.heartbeatInterval ?? 60_000,\n debug: params.debug ?? false,\n port: 0,\n host: \"127.0.0.1\"\n });\n return server;\n};\n"],"names":["ANONYMOUS_IDENTITY","toHeaders","raw","headers","key","value","Object","WebsocketsServer","params","HeartbeatManager","console","activeIds","connectionId","request","socket","head","decision","mdbid","connectedAt","Date","host","data","metadata","JSON","error","identity","tenant","url","URL","token","authenticated","Error","String","address","Promise","resolve","reject","err","createWebsocketsServer","httpServer","createServer","server","NodeWsAdapterImpl","DefaultUpgradeHandlerImpl","attachWebsocketsServer"],"mappings":"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"}
|
package/server/types.d.ts
CHANGED
|
@@ -20,11 +20,6 @@ export type WebsocketsConnectionAuthenticator = (token: string) => Promise<Conne
|
|
|
20
20
|
interface BaseServerParams {
|
|
21
21
|
plugins?: PluginsContainer | PluginCollection;
|
|
22
22
|
heartbeatInterval?: number;
|
|
23
|
-
/**
|
|
24
|
-
* How long a connection may go without a message before it is closed. Keep it well above the
|
|
25
|
-
* client's ping interval.
|
|
26
|
-
*/
|
|
27
|
-
staleAfter?: number;
|
|
28
23
|
debug?: boolean;
|
|
29
24
|
connectionManager?: WebsocketsConnectionManager.Interface<unknown>;
|
|
30
25
|
authenticate?: WebsocketsConnectionAuthenticator;
|