@webiny/api-websockets-standalone 0.0.0-unstable.0156a4c312

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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/WebsocketsStandaloneFeature.d.ts +10 -0
  4. package/WebsocketsStandaloneFeature.js +11 -0
  5. package/WebsocketsStandaloneFeature.js.map +1 -0
  6. package/adapter/NodeWsAdapter.d.ts +27 -0
  7. package/adapter/NodeWsAdapter.js +111 -0
  8. package/adapter/NodeWsAdapter.js.map +1 -0
  9. package/adapter/abstractions.d.ts +18 -0
  10. package/adapter/abstractions.js +5 -0
  11. package/adapter/abstractions.js.map +1 -0
  12. package/adapter/types.d.ts +2 -0
  13. package/adapter/types.js +0 -0
  14. package/bus/InProcessMessageBus.d.ts +11 -0
  15. package/bus/InProcessMessageBus.js +24 -0
  16. package/bus/InProcessMessageBus.js.map +1 -0
  17. package/bus/abstractions.d.ts +30 -0
  18. package/bus/abstractions.js +5 -0
  19. package/bus/abstractions.js.map +1 -0
  20. package/bus/createBusMessageHandler.d.ts +10 -0
  21. package/bus/createBusMessageHandler.js +20 -0
  22. package/bus/createBusMessageHandler.js.map +1 -0
  23. package/connectionManager/ServerConnectionManager.d.ts +21 -0
  24. package/connectionManager/ServerConnectionManager.js +85 -0
  25. package/connectionManager/ServerConnectionManager.js.map +1 -0
  26. package/connectionManager/abstractions.d.ts +34 -0
  27. package/connectionManager/abstractions.js +5 -0
  28. package/connectionManager/abstractions.js.map +1 -0
  29. package/delivery/disconnectLocalSockets.d.ts +10 -0
  30. package/delivery/disconnectLocalSockets.js +24 -0
  31. package/delivery/disconnectLocalSockets.js.map +1 -0
  32. package/delivery/sendToLocalSockets.d.ts +12 -0
  33. package/delivery/sendToLocalSockets.js +24 -0
  34. package/delivery/sendToLocalSockets.js.map +1 -0
  35. package/heartbeat/HeartbeatManager.d.ts +13 -0
  36. package/heartbeat/HeartbeatManager.js +46 -0
  37. package/heartbeat/HeartbeatManager.js.map +1 -0
  38. package/index.d.ts +12 -0
  39. package/index.js +11 -0
  40. package/package.json +42 -0
  41. package/server/WebsocketsServer.d.ts +55 -0
  42. package/server/WebsocketsServer.js +211 -0
  43. package/server/WebsocketsServer.js.map +1 -0
  44. package/server/types.d.ts +44 -0
  45. package/server/types.js +0 -0
  46. package/transport/ServerWebsocketsTransport.d.ts +21 -0
  47. package/transport/ServerWebsocketsTransport.js +77 -0
  48. package/transport/ServerWebsocketsTransport.js.map +1 -0
  49. package/upgradeHandler/DefaultUpgradeHandler.d.ts +8 -0
  50. package/upgradeHandler/DefaultUpgradeHandler.js +15 -0
  51. package/upgradeHandler/DefaultUpgradeHandler.js.map +1 -0
  52. package/upgradeHandler/abstractions.d.ts +17 -0
  53. package/upgradeHandler/abstractions.js +5 -0
  54. package/upgradeHandler/abstractions.js.map +1 -0
  55. package/validator/ServerWebsocketsEventValidator.d.ts +4 -0
  56. package/validator/ServerWebsocketsEventValidator.js +53 -0
  57. package/validator/ServerWebsocketsEventValidator.js.map +1 -0
@@ -0,0 +1,24 @@
1
+ const sendToLocalSockets = async (params)=>{
2
+ const { connectionManager, adapter, logger, connectionIds, data } = params;
3
+ const payload = JSON.stringify(data);
4
+ const missing = [];
5
+ for (const connectionId of connectionIds){
6
+ const socket = connectionManager.getSocket(connectionId);
7
+ if (void 0 === socket) {
8
+ missing.push(connectionId);
9
+ continue;
10
+ }
11
+ try {
12
+ await adapter.send(socket, payload);
13
+ } catch (error) {
14
+ logger.error({
15
+ error,
16
+ connectionId
17
+ }, "[websockets] could not send to a connection");
18
+ }
19
+ }
20
+ return missing;
21
+ };
22
+ export { sendToLocalSockets };
23
+
24
+ //# sourceMappingURL=sendToLocalSockets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delivery/sendToLocalSockets.js","sources":["../../src/delivery/sendToLocalSockets.ts"],"sourcesContent":["import type { GenericRecord } from \"@webiny/api/types.js\";\nimport type { Logger } from \"@webiny/api-core/features/logger/abstractions.js\";\nimport type { WebsocketsConnectionManager } from \"~/connectionManager/abstractions.js\";\nimport type { WebsocketsServerAdapter } from \"~/adapter/abstractions.js\";\n\nexport interface ISendToLocalSocketsParams {\n connectionManager: WebsocketsConnectionManager.Interface<unknown>;\n adapter: WebsocketsServerAdapter.Interface<unknown>;\n logger: Logger.Interface;\n connectionIds: string[];\n data: GenericRecord;\n}\n\n/*\n * Sends `data` on each socket this process holds and returns the ids it holds no socket for. A failed\n * send is logged and the others go on.\n */\nexport const sendToLocalSockets = async (params: ISendToLocalSocketsParams): Promise<string[]> => {\n const { connectionManager, adapter, logger, connectionIds, data } = params;\n const payload = JSON.stringify(data);\n const missing: string[] = [];\n for (const connectionId of connectionIds) {\n const socket = connectionManager.getSocket(connectionId);\n if (socket === undefined) {\n missing.push(connectionId);\n continue;\n }\n try {\n await adapter.send(socket, payload);\n } catch (error) {\n logger.error({ error, connectionId }, \"[websockets] could not send to a connection\");\n }\n }\n return missing;\n};\n"],"names":["sendToLocalSockets","params","connectionManager","adapter","logger","connectionIds","data","payload","JSON","missing","connectionId","socket","undefined","error"],"mappings":"AAiBO,MAAMA,qBAAqB,OAAOC;IACrC,MAAM,EAAEC,iBAAiB,EAAEC,OAAO,EAAEC,MAAM,EAAEC,aAAa,EAAEC,IAAI,EAAE,GAAGL;IACpE,MAAMM,UAAUC,KAAK,SAAS,CAACF;IAC/B,MAAMG,UAAoB,EAAE;IAC5B,KAAK,MAAMC,gBAAgBL,cAAe;QACtC,MAAMM,SAAST,kBAAkB,SAAS,CAACQ;QAC3C,IAAIC,AAAWC,WAAXD,QAAsB;YACtBF,QAAQ,IAAI,CAACC;YACb;QACJ;QACA,IAAI;YACA,MAAMP,QAAQ,IAAI,CAACQ,QAAQJ;QAC/B,EAAE,OAAOM,OAAO;YACZT,OAAO,KAAK,CAAC;gBAAES;gBAAOH;YAAa,GAAG;QAC1C;IACJ;IACA,OAAOD;AACX"}
@@ -0,0 +1,13 @@
1
+ import type { WebsocketsConnectionManager } from "../connectionManager/abstractions.js";
2
+ import type { WebsocketsServerAdapter } from "../adapter/abstractions.js";
3
+ export declare class HeartbeatManager {
4
+ private readonly connectionManager;
5
+ private readonly adapter;
6
+ private readonly interval;
7
+ private readonly staleAfter;
8
+ private timer;
9
+ constructor(connectionManager: WebsocketsConnectionManager.Interface<unknown>, adapter: WebsocketsServerAdapter.Interface<unknown>, interval: number, staleAfter: number);
10
+ start(): void;
11
+ stop(): void;
12
+ evictStale(): Promise<void>;
13
+ }
@@ -0,0 +1,46 @@
1
+ const STALE_CLOSE_CODE = 1001;
2
+ const STALE_CLOSE_REASON = "Connection went stale.";
3
+ class HeartbeatManager {
4
+ constructor(connectionManager, adapter, interval, staleAfter){
5
+ this.connectionManager = connectionManager;
6
+ this.adapter = adapter;
7
+ this.interval = interval;
8
+ this.staleAfter = staleAfter;
9
+ }
10
+ start() {
11
+ this.timer = setInterval(()=>{
12
+ this.evictStale();
13
+ }, this.interval);
14
+ }
15
+ stop() {
16
+ if (this.timer) {
17
+ clearInterval(this.timer);
18
+ this.timer = void 0;
19
+ }
20
+ }
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
+ }
44
+ export { HeartbeatManager };
45
+
46
+ //# sourceMappingURL=HeartbeatManager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heartbeat/HeartbeatManager.js","sources":["../../src/heartbeat/HeartbeatManager.ts"],"sourcesContent":["import type { WebsocketsConnectionManager } from \"~/connectionManager/abstractions.js\";\nimport type { WebsocketsServerAdapter } from \"~/adapter/abstractions.js\";\n\nconst STALE_CLOSE_CODE = 1001;\nconst STALE_CLOSE_REASON = \"Connection went stale.\";\n\n/*\n * Evicts connections that have not been seen for `staleAfter` milliseconds: forgets them and closes\n * their sockets.\n *\n * `staleAfter` must sit well above the client's ping interval. The admin pings every 5 minutes and\n * browsers delay timers in hidden tabs by up to a minute, so a threshold equal to the ping interval\n * evicted healthy background tabs. Closing the socket is what lets the client notice and reconnect:\n * an evicted socket left open looks connected to the client but no longer receives anything.\n */\nexport class HeartbeatManager {\n private timer: ReturnType<typeof setInterval> | undefined;\n\n public constructor(\n private readonly connectionManager: WebsocketsConnectionManager.Interface<unknown>,\n private readonly adapter: WebsocketsServerAdapter.Interface<unknown>,\n private readonly interval: number,\n private readonly staleAfter: number\n ) {}\n\n public start(): void {\n this.timer = setInterval(() => {\n void this.evictStale();\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 public async evictStale(): Promise<void> {\n // Cleanup forgets the sockets it evicts, so take them before it runs.\n const sockets = new Map<string, unknown>();\n for (const connectionId of this.connectionManager.getActiveConnectionIds()) {\n const socket = this.connectionManager.getSocket(connectionId);\n sockets.set(connectionId, socket);\n }\n\n let evicted: string[];\n try {\n evicted = await this.connectionManager.cleanup(this.staleAfter);\n } catch (error) {\n console.error(\"Failed to evict stale WebSocket connections:\", error);\n return;\n }\n\n for (const connectionId of evicted) {\n const socket = sockets.get(connectionId);\n if (socket === undefined) {\n continue;\n }\n try {\n this.adapter.close(socket, STALE_CLOSE_CODE, STALE_CLOSE_REASON);\n } catch (error) {\n console.error(`Failed to close stale WebSocket \"${connectionId}\":`, error);\n }\n }\n }\n}\n"],"names":["STALE_CLOSE_CODE","STALE_CLOSE_REASON","HeartbeatManager","connectionManager","adapter","interval","staleAfter","setInterval","clearInterval","undefined","sockets","Map","connectionId","socket","evicted","error","console"],"mappings":"AAGA,MAAMA,mBAAmB;AACzB,MAAMC,qBAAqB;AAWpB,MAAMC;IAGT,YACqBC,iBAAiE,EACjEC,OAAmD,EACnDC,QAAgB,EAChBC,UAAkB,CACrC;aAJmBH,iBAAiB,GAAjBA;aACAC,OAAO,GAAPA;aACAC,QAAQ,GAARA;aACAC,UAAU,GAAVA;IAClB;IAEI,QAAc;QACjB,IAAI,CAAC,KAAK,GAAGC,YAAY;YAChB,IAAI,CAAC,UAAU;QACxB,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;IAEA,MAAa,aAA4B;QAErC,MAAMC,UAAU,IAAIC;QACpB,KAAK,MAAMC,gBAAgB,IAAI,CAAC,iBAAiB,CAAC,sBAAsB,GAAI;YACxE,MAAMC,SAAS,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAACD;YAChDF,QAAQ,GAAG,CAACE,cAAcC;QAC9B;QAEA,IAAIC;QACJ,IAAI;YACAA,UAAU,MAAM,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU;QAClE,EAAE,OAAOC,OAAO;YACZC,QAAQ,KAAK,CAAC,gDAAgDD;YAC9D;QACJ;QAEA,KAAK,MAAMH,gBAAgBE,QAAS;YAChC,MAAMD,SAASH,QAAQ,GAAG,CAACE;YAC3B,IAAIC,AAAWJ,WAAXI,QAGJ,IAAI;gBACA,IAAI,CAAC,OAAO,CAAC,KAAK,CAACA,QAAQb,kBAAkBC;YACjD,EAAE,OAAOc,OAAO;gBACZC,QAAQ,KAAK,CAAC,CAAC,iCAAiC,EAAEJ,aAAa,EAAE,CAAC,EAAEG;YACxE;QACJ;IACJ;AACJ"}
package/index.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ export { createWebsocketsServer, attachWebsocketsServer } from "./server/WebsocketsServer.js";
2
+ export type { IWebsocketsServer, WebsocketsConnectionAuthenticator } from "./server/types.js";
3
+ export * from "./adapter/abstractions.js";
4
+ export * from "./upgradeHandler/abstractions.js";
5
+ export * from "./connectionManager/abstractions.js";
6
+ export { WebsocketsMessageBus } from "./bus/abstractions.js";
7
+ export { createBusMessageHandler } from "./bus/createBusMessageHandler.js";
8
+ export { ServerConnectionManager } from "./connectionManager/ServerConnectionManager.js";
9
+ export { NodeWsAdapter } from "./adapter/NodeWsAdapter.js";
10
+ export { ServerWebsocketsTransport } from "./transport/ServerWebsocketsTransport.js";
11
+ export { InProcessMessageBus } from "./bus/InProcessMessageBus.js";
12
+ export { WebsocketsStandaloneFeature } from "./WebsocketsStandaloneFeature.js";
package/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export * from "./adapter/abstractions.js";
2
+ export * from "./upgradeHandler/abstractions.js";
3
+ export * from "./connectionManager/abstractions.js";
4
+ export { attachWebsocketsServer, createWebsocketsServer } from "./server/WebsocketsServer.js";
5
+ export { WebsocketsMessageBus } from "./bus/abstractions.js";
6
+ export { createBusMessageHandler } from "./bus/createBusMessageHandler.js";
7
+ export { ServerConnectionManager } from "./connectionManager/ServerConnectionManager.js";
8
+ export { NodeWsAdapter } from "./adapter/NodeWsAdapter.js";
9
+ export { ServerWebsocketsTransport } from "./transport/ServerWebsocketsTransport.js";
10
+ export { InProcessMessageBus } from "./bus/InProcessMessageBus.js";
11
+ export { WebsocketsStandaloneFeature } from "./WebsocketsStandaloneFeature.js";
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@webiny/api-websockets-standalone",
3
+ "version": "0.0.0-unstable.0156a4c312",
4
+ "type": "module",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/webiny/webiny-js.git"
8
+ },
9
+ "description": "Standalone WebSocket server transport for @webiny/api-websockets",
10
+ "contributors": [
11
+ "Bruno Zorić <bruno@webiny.com>"
12
+ ],
13
+ "exports": {
14
+ "./*": "./*",
15
+ ".": "./index.js"
16
+ },
17
+ "license": "MIT",
18
+ "dependencies": {
19
+ "@webiny/api": "0.0.0-unstable.0156a4c312",
20
+ "@webiny/api-core": "0.0.0-unstable.0156a4c312",
21
+ "@webiny/api-websockets": "0.0.0-unstable.0156a4c312",
22
+ "@webiny/event-handler-standalone": "0.0.0-unstable.0156a4c312",
23
+ "@webiny/feature": "0.0.0-unstable.0156a4c312",
24
+ "@webiny/plugins": "0.0.0-unstable.0156a4c312",
25
+ "@webiny/utils": "0.0.0-unstable.0156a4c312",
26
+ "ws": "8.22.1",
27
+ "zod": "4.6.7"
28
+ },
29
+ "devDependencies": {
30
+ "@types/ws": "8.18.2",
31
+ "@webiny/build-tools": "0.0.0-unstable.0156a4c312",
32
+ "rimraf": "6.1.3",
33
+ "typescript": "7.0.2",
34
+ "vitest": "5.0.5"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "webiny": {
40
+ "publishFrom": "dist"
41
+ }
42
+ }
@@ -0,0 +1,55 @@
1
+ import type { Server as HttpServer } from "node:http";
2
+ import type { WebsocketsServerAdapter } from "../adapter/abstractions.js";
3
+ import type { WebsocketsUpgradeHandler } from "../upgradeHandler/abstractions.js";
4
+ import type { WebsocketsConnectionManager } from "../connectionManager/abstractions.js";
5
+ import type { CreateWebsocketsServerParams, AttachWebsocketsServerParams, IWebsocketsServer, WebsocketsConnectionAuthenticator } from "./types.js";
6
+ interface WebsocketsServerParams {
7
+ httpServer: HttpServer;
8
+ ownsHttpServer: boolean;
9
+ adapter: WebsocketsServerAdapter.Interface<unknown>;
10
+ upgradeHandler: WebsocketsUpgradeHandler.Interface;
11
+ connectionManager?: WebsocketsConnectionManager.Interface<unknown>;
12
+ authenticate?: WebsocketsConnectionAuthenticator;
13
+ heartbeatInterval: number;
14
+ staleAfter: number;
15
+ debug: boolean;
16
+ port: number;
17
+ host: string;
18
+ }
19
+ declare class WebsocketsServer implements IWebsocketsServer {
20
+ private readonly httpServer;
21
+ private readonly ownsHttpServer;
22
+ private readonly adapter;
23
+ private readonly upgradeHandler;
24
+ private readonly connectionManager;
25
+ private readonly authenticate;
26
+ private readonly heartbeat;
27
+ private readonly debug;
28
+ private readonly requestedPort;
29
+ private readonly requestedHost;
30
+ private shuttingDown;
31
+ private endpoint;
32
+ private resolvedPort;
33
+ constructor(params: WebsocketsServerParams);
34
+ start(): Promise<void>;
35
+ stop(): Promise<void>;
36
+ port(): number;
37
+ private wireUpgradeHandler;
38
+ private wireConnectionHandler;
39
+ /**
40
+ * Authenticates the upgrade using the `?token`/`?tenant` query the app-websockets client sends,
41
+ * then registers the live socket in the shared connection manager under the resolved identity and
42
+ * tenant. Getting both right matters for targeted server→client sends: `SendToIdentity` finds
43
+ * connections by identity id in the registry, so a connection that's anonymous (or never made it
44
+ * into the registry) can't be reached. Everything here is guarded — if something goes wrong we
45
+ * don't crash the server or the upgrade, the connection just ends up unaddressable.
46
+ */
47
+ private registerConnection;
48
+ private removeConnection;
49
+ private resolvePortFromExistingServer;
50
+ private listen;
51
+ private closeHttpServer;
52
+ }
53
+ export { WebsocketsServer };
54
+ export declare const createWebsocketsServer: (params: CreateWebsocketsServerParams) => IWebsocketsServer;
55
+ export declare const attachWebsocketsServer: (params: AttachWebsocketsServerParams) => IWebsocketsServer;
@@ -0,0 +1,211 @@
1
+ import { createServer } from "node:http";
2
+ import { mdbid } from "@webiny/utils";
3
+ import { NodeWsAdapterImpl } from "../adapter/NodeWsAdapter.js";
4
+ import { DefaultUpgradeHandlerImpl } from "../upgradeHandler/DefaultUpgradeHandler.js";
5
+ import { HeartbeatManager } from "../heartbeat/HeartbeatManager.js";
6
+ const ANONYMOUS_IDENTITY = {
7
+ id: "",
8
+ displayName: "",
9
+ type: ""
10
+ };
11
+ const DEFAULT_HEARTBEAT_INTERVAL = 60000;
12
+ const DEFAULT_STALE_AFTER = 900000;
13
+ const toHeaders = (raw)=>{
14
+ const headers = {};
15
+ for (const [key, value] of Object.entries(raw))if ("string" == typeof value) headers[key] = value;
16
+ return headers;
17
+ };
18
+ class WebsocketsServer {
19
+ constructor(params){
20
+ this.shuttingDown = false;
21
+ this.endpoint = "";
22
+ this.resolvedPort = 0;
23
+ this.httpServer = params.httpServer;
24
+ this.ownsHttpServer = params.ownsHttpServer;
25
+ this.adapter = params.adapter;
26
+ this.upgradeHandler = params.upgradeHandler;
27
+ this.connectionManager = params.connectionManager;
28
+ this.authenticate = params.authenticate;
29
+ this.debug = params.debug;
30
+ this.requestedPort = params.port;
31
+ this.requestedHost = params.host;
32
+ if (params.connectionManager) this.heartbeat = new HeartbeatManager(params.connectionManager, params.adapter, params.heartbeatInterval, params.staleAfter);
33
+ }
34
+ async start() {
35
+ this.adapter.start(this.httpServer);
36
+ this.wireUpgradeHandler();
37
+ this.wireConnectionHandler();
38
+ if (this.ownsHttpServer) await this.listen();
39
+ else this.resolvePortFromExistingServer();
40
+ this.heartbeat?.start();
41
+ if (this.debug) console.log(`[WebsocketsServer] started on ${this.endpoint}`);
42
+ }
43
+ async stop() {
44
+ this.shuttingDown = true;
45
+ this.heartbeat?.stop();
46
+ const activeIds = this.connectionManager?.getActiveConnectionIds() ?? [];
47
+ const removals = activeIds.map((connectionId)=>this.removeConnection(connectionId));
48
+ const closing = this.adapter.stop();
49
+ await Promise.all([
50
+ closing,
51
+ ...removals
52
+ ]);
53
+ if (this.ownsHttpServer) await this.closeHttpServer();
54
+ if (this.debug) console.log("[WebsocketsServer] stopped");
55
+ }
56
+ port() {
57
+ return this.resolvedPort;
58
+ }
59
+ wireUpgradeHandler() {
60
+ this.httpServer.on("upgrade", async (request, socket, head)=>{
61
+ if (this.shuttingDown) return void socket.destroy();
62
+ socket.on("error", (error)=>{
63
+ console.error("WebSocket upgrade socket error:", error.message);
64
+ });
65
+ try {
66
+ const decision = await this.upgradeHandler.shouldUpgrade(request);
67
+ if (!decision.allowed) {
68
+ socket.write(`HTTP/1.1 ${decision.statusCode} ${decision.reason}\r\n\r\n`);
69
+ socket.destroy();
70
+ return;
71
+ }
72
+ this.adapter.handleUpgrade(request, socket, head);
73
+ } catch (error) {
74
+ console.error("WebSocket upgrade failed:", error);
75
+ socket.destroy();
76
+ }
77
+ });
78
+ }
79
+ wireConnectionHandler() {
80
+ this.adapter.onConnection((socket, request)=>{
81
+ if (this.shuttingDown) return;
82
+ const connectionId = mdbid();
83
+ const connectedAt = Date.now();
84
+ const host = request.headers.host || "localhost";
85
+ const headers = toHeaders(request.headers);
86
+ const registration = this.registerConnection({
87
+ connectionId,
88
+ socket,
89
+ request,
90
+ connectedAt,
91
+ host,
92
+ headers
93
+ });
94
+ this.adapter.onMessage(socket, (data)=>{
95
+ if (this.shuttingDown) return;
96
+ const metadata = this.connectionManager?.getMetadata(connectionId);
97
+ if (!metadata) return;
98
+ try {
99
+ JSON.parse(data.toString());
100
+ } catch {
101
+ return;
102
+ }
103
+ this.connectionManager?.updateLastSeen(connectionId).catch((error)=>{
104
+ console.error(`Failed to update last seen for WebSocket connection "${connectionId}":`, error);
105
+ });
106
+ });
107
+ this.adapter.onClose(socket, ()=>{
108
+ if (this.shuttingDown) return;
109
+ registration.then(()=>this.removeConnection(connectionId));
110
+ });
111
+ this.adapter.onError(socket, (error)=>{
112
+ console.error(`WebSocket error for connection "${connectionId}":`, error.message);
113
+ });
114
+ });
115
+ }
116
+ async registerConnection(params) {
117
+ const { connectionId, socket, request, connectedAt, host, headers } = params;
118
+ let identity = ANONYMOUS_IDENTITY;
119
+ let tenant = "";
120
+ try {
121
+ const url = new URL(request.url ?? "", `http://${host}`);
122
+ tenant = url.searchParams.get("tenant") ?? "";
123
+ const token = url.searchParams.get("token") ?? "";
124
+ if (this.authenticate && token) {
125
+ const authenticated = await this.authenticate(token);
126
+ if (authenticated?.id) identity = authenticated;
127
+ }
128
+ await this.connectionManager?.add({
129
+ connectionId,
130
+ socket,
131
+ endpoint: this.endpoint,
132
+ identity,
133
+ tenant,
134
+ connectedAt,
135
+ host,
136
+ headers
137
+ });
138
+ } catch (error) {
139
+ console.error(`Failed to register WebSocket connection "${connectionId}":`, error instanceof Error ? error.message : String(error));
140
+ }
141
+ }
142
+ async removeConnection(connectionId) {
143
+ try {
144
+ await this.connectionManager?.remove(connectionId);
145
+ } catch (error) {
146
+ console.error(`Failed to remove WebSocket connection "${connectionId}":`, error);
147
+ }
148
+ }
149
+ resolvePortFromExistingServer() {
150
+ const address = this.httpServer.address();
151
+ if (address && "string" != typeof address) {
152
+ this.resolvedPort = address.port;
153
+ this.endpoint = `ws://127.0.0.1:${this.resolvedPort}`;
154
+ }
155
+ }
156
+ listen() {
157
+ return new Promise((resolve, reject)=>{
158
+ this.httpServer.listen(this.requestedPort, this.requestedHost, ()=>{
159
+ const address = this.httpServer.address();
160
+ if (!address || "string" == typeof address) return void reject(new Error("Unexpected server address format."));
161
+ this.resolvedPort = address.port;
162
+ this.endpoint = `ws://${this.requestedHost}:${this.resolvedPort}`;
163
+ resolve();
164
+ });
165
+ });
166
+ }
167
+ closeHttpServer() {
168
+ return new Promise((resolve, reject)=>{
169
+ this.httpServer.close((err)=>{
170
+ if (err) return void reject(err);
171
+ resolve();
172
+ });
173
+ });
174
+ }
175
+ }
176
+ const createWebsocketsServer = (params)=>{
177
+ const httpServer = createServer();
178
+ const server = new WebsocketsServer({
179
+ httpServer,
180
+ ownsHttpServer: true,
181
+ adapter: new NodeWsAdapterImpl(),
182
+ upgradeHandler: new DefaultUpgradeHandlerImpl(),
183
+ connectionManager: params.connectionManager,
184
+ authenticate: params.authenticate,
185
+ heartbeatInterval: params.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL,
186
+ staleAfter: params.staleAfter ?? DEFAULT_STALE_AFTER,
187
+ debug: params.debug ?? false,
188
+ port: params.port ?? 0,
189
+ host: params.host ?? "127.0.0.1"
190
+ });
191
+ return server;
192
+ };
193
+ const attachWebsocketsServer = (params)=>{
194
+ const server = new WebsocketsServer({
195
+ httpServer: params.server,
196
+ ownsHttpServer: false,
197
+ adapter: new NodeWsAdapterImpl(),
198
+ upgradeHandler: new DefaultUpgradeHandlerImpl(),
199
+ connectionManager: params.connectionManager,
200
+ authenticate: params.authenticate,
201
+ heartbeatInterval: params.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL,
202
+ staleAfter: params.staleAfter ?? DEFAULT_STALE_AFTER,
203
+ debug: params.debug ?? false,
204
+ port: 0,
205
+ host: "127.0.0.1"
206
+ });
207
+ return server;
208
+ };
209
+ export { WebsocketsServer, attachWebsocketsServer, createWebsocketsServer };
210
+
211
+ //# sourceMappingURL=WebsocketsServer.js.map
@@ -0,0 +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 /*\n * Close the sockets and take this process's connections out of the registry at the same\n * time, so both fit in the shutdown. Without the registry step, the rows stay until another\n * process's heartbeat finds them stale, and sends to those users go nowhere until then.\n */\n const activeIds = this.connectionManager?.getActiveConnectionIds() ?? [];\n const removals = activeIds.map(connectionId => this.removeConnection(connectionId));\n const closing = this.adapter.stop();\n await Promise.all([closing, ...removals]);\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","removals","connectionId","closing","Promise","request","socket","head","error","decision","mdbid","connectedAt","Date","host","registration","data","metadata","JSON","identity","tenant","url","URL","token","authenticated","Error","String","address","resolve","reject","err","createWebsocketsServer","httpServer","createServer","server","NodeWsAdapterImpl","DefaultUpgradeHandlerImpl","attachWebsocketsServer"],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import type { Server as HttpServer } from "node:http";
2
+ import type { PluginsContainer } from "@webiny/plugins";
3
+ import type { PluginCollection } from "@webiny/plugins/types.js";
4
+ import type { WebsocketsConnectionManager } from "../connectionManager/abstractions.js";
5
+ import type { ConnectionRegistry } from "@webiny/api-websockets/exports/api.js";
6
+ /**
7
+ * Resolves a connection's identity from the `?token` JWT sent on the WebSocket upgrade — the SAME
8
+ * token→identity step the HTTP request stack runs (RequestIdentityLoader). Returns `null` for an
9
+ * absent/invalid/anonymous token, in which case the connection registers anonymously and won't
10
+ * receive targeted (`SendToIdentity`) messages.
11
+ *
12
+ * It's a plain callback rather than a resolved dependency for two reasons: this WebSocket server is a
13
+ * DI-agnostic class (the handler resolves what it needs and passes plain values in), and the auth it
14
+ * needs — `AuthenticationContext` — lives in the PER-REQUEST stack, not the root container, so a
15
+ * boot-time object like this couldn't resolve it directly anyway. The handler builds the callback
16
+ * (spinning up a request-scoped child container per token). So authenticate has to be a callback the
17
+ * handler supplies, regardless of DI.
18
+ */
19
+ export type WebsocketsConnectionAuthenticator = (token: string) => Promise<ConnectionRegistry.Identity | null>;
20
+ interface BaseServerParams {
21
+ plugins?: PluginsContainer | PluginCollection;
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
+ debug?: boolean;
29
+ connectionManager?: WebsocketsConnectionManager.Interface<unknown>;
30
+ authenticate?: WebsocketsConnectionAuthenticator;
31
+ }
32
+ export interface CreateWebsocketsServerParams extends BaseServerParams {
33
+ port?: number;
34
+ host?: string;
35
+ }
36
+ export interface AttachWebsocketsServerParams extends BaseServerParams {
37
+ server: HttpServer;
38
+ }
39
+ export interface IWebsocketsServer {
40
+ start(): Promise<void>;
41
+ stop(): Promise<void>;
42
+ port(): number;
43
+ }
44
+ export {};
File without changes
@@ -0,0 +1,21 @@
1
+ import { WebsocketsTransport } from "@webiny/api-websockets/exports/api.js";
2
+ import type { GenericRecord } from "@webiny/api/types.js";
3
+ import { Logger } from "@webiny/api-core/features/logger/abstractions.js";
4
+ import { ProcessOwner } from "@webiny/event-handler-standalone";
5
+ import { WebsocketsConnectionManager } from "../connectionManager/abstractions.js";
6
+ import { WebsocketsServerAdapter } from "../adapter/abstractions.js";
7
+ import { WebsocketsMessageBus } from "../bus/abstractions.js";
8
+ export declare class ServerWebsocketsTransportImpl implements WebsocketsTransport.Interface {
9
+ private readonly connectionManager;
10
+ private readonly adapter;
11
+ private readonly bus;
12
+ private readonly owner;
13
+ private readonly logger;
14
+ constructor(connectionManager: WebsocketsConnectionManager.Interface<unknown>, adapter: WebsocketsServerAdapter.Interface<unknown>, bus: WebsocketsMessageBus.Interface, owner: ProcessOwner.Interface, logger: Logger.Interface);
15
+ send<T extends GenericRecord = GenericRecord>(connections: WebsocketsTransport.SendConnection[], data: WebsocketsTransport.SendData<T>): Promise<void>;
16
+ disconnect(connections: WebsocketsTransport.DisconnectConnection[]): Promise<void>;
17
+ private forward;
18
+ }
19
+ export declare const ServerWebsocketsTransport: typeof ServerWebsocketsTransportImpl & {
20
+ __abstraction: import("@webiny/di").Abstraction<import("@webiny/api-websockets/transport").IWebsocketsTransport>;
21
+ };
@@ -0,0 +1,77 @@
1
+ import { WebsocketsTransport } from "@webiny/api-websockets/exports/api.js";
2
+ import { Logger } from "@webiny/api-core/features/logger/abstractions.js";
3
+ import { ProcessOwner } from "@webiny/event-handler-standalone";
4
+ import { WebsocketsConnectionManager } from "../connectionManager/abstractions.js";
5
+ import { WebsocketsServerAdapter } from "../adapter/abstractions.js";
6
+ import { WebsocketsMessageBus } from "../bus/abstractions.js";
7
+ import { sendToLocalSockets } from "../delivery/sendToLocalSockets.js";
8
+ import { disconnectLocalSockets } from "../delivery/disconnectLocalSockets.js";
9
+ class ServerWebsocketsTransportImpl {
10
+ constructor(connectionManager, adapter, bus, owner, logger){
11
+ this.connectionManager = connectionManager;
12
+ this.adapter = adapter;
13
+ this.bus = bus;
14
+ this.owner = owner;
15
+ this.logger = logger;
16
+ }
17
+ async send(connections, data) {
18
+ const connectionIds = connections.map((connection)=>connection.connectionId);
19
+ const missing = await sendToLocalSockets({
20
+ connectionManager: this.connectionManager,
21
+ adapter: this.adapter,
22
+ logger: this.logger,
23
+ connectionIds,
24
+ data
25
+ });
26
+ await this.forward(missing, (ids)=>({
27
+ action: "send",
28
+ connectionIds: ids,
29
+ data
30
+ }));
31
+ }
32
+ async disconnect(connections) {
33
+ const connectionIds = connections.map((connection)=>connection.connectionId);
34
+ const missing = await disconnectLocalSockets({
35
+ connectionManager: this.connectionManager,
36
+ adapter: this.adapter,
37
+ logger: this.logger,
38
+ connectionIds
39
+ });
40
+ await this.forward(missing, (ids)=>({
41
+ action: "disconnect",
42
+ connectionIds: ids
43
+ }));
44
+ }
45
+ async forward(missing, toMessage) {
46
+ const byOwner = await this.connectionManager.groupByOwner(missing);
47
+ for (const [owner, connectionIds] of byOwner){
48
+ if (owner === this.owner.id) {
49
+ for (const connectionId of connectionIds)await this.connectionManager.remove(connectionId);
50
+ continue;
51
+ }
52
+ const message = toMessage(connectionIds);
53
+ try {
54
+ await this.bus.publish(owner, message);
55
+ } catch (error) {
56
+ this.logger.error({
57
+ error,
58
+ owner,
59
+ connectionIds
60
+ }, "[websockets] could not publish to the process holding these connections");
61
+ }
62
+ }
63
+ }
64
+ }
65
+ const ServerWebsocketsTransport = WebsocketsTransport.createImplementation({
66
+ implementation: ServerWebsocketsTransportImpl,
67
+ dependencies: [
68
+ WebsocketsConnectionManager,
69
+ WebsocketsServerAdapter,
70
+ WebsocketsMessageBus,
71
+ ProcessOwner,
72
+ Logger
73
+ ]
74
+ });
75
+ export { ServerWebsocketsTransport, ServerWebsocketsTransportImpl };
76
+
77
+ //# sourceMappingURL=ServerWebsocketsTransport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport/ServerWebsocketsTransport.js","sources":["../../src/transport/ServerWebsocketsTransport.ts"],"sourcesContent":["import { WebsocketsTransport } from \"@webiny/api-websockets/exports/api.js\";\nimport type { GenericRecord } from \"@webiny/api/types.js\";\nimport { Logger } from \"@webiny/api-core/features/logger/abstractions.js\";\nimport { ProcessOwner } from \"@webiny/event-handler-standalone\";\nimport { WebsocketsConnectionManager } from \"~/connectionManager/abstractions.js\";\nimport { WebsocketsServerAdapter } from \"~/adapter/abstractions.js\";\nimport { WebsocketsMessageBus } from \"~/bus/abstractions.js\";\nimport { sendToLocalSockets } from \"~/delivery/sendToLocalSockets.js\";\nimport { disconnectLocalSockets } from \"~/delivery/disconnectLocalSockets.js\";\n\ntype ToMessage = (connectionIds: string[]) => WebsocketsMessageBus.Message;\n\n/*\n * Sends to the sockets this process holds, and hands the rest to the process that holds them, through\n * the message bus. With one process there is no rest, apart from rows a previous run of the process\n * left behind.\n */\nexport class ServerWebsocketsTransportImpl implements WebsocketsTransport.Interface {\n public constructor(\n private readonly connectionManager: WebsocketsConnectionManager.Interface<unknown>,\n private readonly adapter: WebsocketsServerAdapter.Interface<unknown>,\n private readonly bus: WebsocketsMessageBus.Interface,\n private readonly owner: ProcessOwner.Interface,\n private readonly logger: Logger.Interface\n ) {}\n\n public async send<T extends GenericRecord = GenericRecord>(\n connections: WebsocketsTransport.SendConnection[],\n data: WebsocketsTransport.SendData<T>\n ): Promise<void> {\n const connectionIds = connections.map(connection => connection.connectionId);\n const missing = await sendToLocalSockets({\n connectionManager: this.connectionManager,\n adapter: this.adapter,\n logger: this.logger,\n connectionIds,\n data\n });\n await this.forward(missing, ids => {\n return { action: \"send\", connectionIds: ids, data };\n });\n }\n\n public async disconnect(\n connections: WebsocketsTransport.DisconnectConnection[]\n ): Promise<void> {\n const connectionIds = connections.map(connection => connection.connectionId);\n const missing = await disconnectLocalSockets({\n connectionManager: this.connectionManager,\n adapter: this.adapter,\n logger: this.logger,\n connectionIds\n });\n await this.forward(missing, ids => {\n return { action: \"disconnect\", connectionIds: ids };\n });\n }\n\n /*\n * Connections this process has no socket for. Its own are gone, so they are removed; another\n * process's are published to that process. A failed publish is logged, not thrown: the message\n * is lost, as it would be if that process were restarting.\n */\n private async forward(missing: string[], toMessage: ToMessage): Promise<void> {\n const byOwner = await this.connectionManager.groupByOwner(missing);\n for (const [owner, connectionIds] of byOwner) {\n if (owner === this.owner.id) {\n for (const connectionId of connectionIds) {\n await this.connectionManager.remove(connectionId);\n }\n continue;\n }\n const message = toMessage(connectionIds);\n try {\n await this.bus.publish(owner, message);\n } catch (error) {\n this.logger.error(\n { error, owner, connectionIds },\n \"[websockets] could not publish to the process holding these connections\"\n );\n }\n }\n }\n}\n\nexport const ServerWebsocketsTransport = WebsocketsTransport.createImplementation({\n implementation: ServerWebsocketsTransportImpl,\n dependencies: [\n WebsocketsConnectionManager,\n WebsocketsServerAdapter,\n WebsocketsMessageBus,\n ProcessOwner,\n Logger\n ]\n});\n"],"names":["ServerWebsocketsTransportImpl","connectionManager","adapter","bus","owner","logger","connections","data","connectionIds","connection","missing","sendToLocalSockets","ids","disconnectLocalSockets","toMessage","byOwner","connectionId","message","error","ServerWebsocketsTransport","WebsocketsTransport","WebsocketsConnectionManager","WebsocketsServerAdapter","WebsocketsMessageBus","ProcessOwner","Logger"],"mappings":";;;;;;;;AAiBO,MAAMA;IACT,YACqBC,iBAAiE,EACjEC,OAAmD,EACnDC,GAAmC,EACnCC,KAA6B,EAC7BC,MAAwB,CAC3C;aALmBJ,iBAAiB,GAAjBA;aACAC,OAAO,GAAPA;aACAC,GAAG,GAAHA;aACAC,KAAK,GAALA;aACAC,MAAM,GAANA;IAClB;IAEH,MAAa,KACTC,WAAiD,EACjDC,IAAqC,EACxB;QACb,MAAMC,gBAAgBF,YAAY,GAAG,CAACG,CAAAA,aAAcA,WAAW,YAAY;QAC3E,MAAMC,UAAU,MAAMC,mBAAmB;YACrC,mBAAmB,IAAI,CAAC,iBAAiB;YACzC,SAAS,IAAI,CAAC,OAAO;YACrB,QAAQ,IAAI,CAAC,MAAM;YACnBH;YACAD;QACJ;QACA,MAAM,IAAI,CAAC,OAAO,CAACG,SAASE,CAAAA,MACjB;gBAAE,QAAQ;gBAAQ,eAAeA;gBAAKL;YAAK;IAE1D;IAEA,MAAa,WACTD,WAAuD,EAC1C;QACb,MAAME,gBAAgBF,YAAY,GAAG,CAACG,CAAAA,aAAcA,WAAW,YAAY;QAC3E,MAAMC,UAAU,MAAMG,uBAAuB;YACzC,mBAAmB,IAAI,CAAC,iBAAiB;YACzC,SAAS,IAAI,CAAC,OAAO;YACrB,QAAQ,IAAI,CAAC,MAAM;YACnBL;QACJ;QACA,MAAM,IAAI,CAAC,OAAO,CAACE,SAASE,CAAAA,MACjB;gBAAE,QAAQ;gBAAc,eAAeA;YAAI;IAE1D;IAOA,MAAc,QAAQF,OAAiB,EAAEI,SAAoB,EAAiB;QAC1E,MAAMC,UAAU,MAAM,IAAI,CAAC,iBAAiB,CAAC,YAAY,CAACL;QAC1D,KAAK,MAAM,CAACN,OAAOI,cAAc,IAAIO,QAAS;YAC1C,IAAIX,UAAU,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE;gBACzB,KAAK,MAAMY,gBAAgBR,cACvB,MAAM,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAACQ;gBAExC;YACJ;YACA,MAAMC,UAAUH,UAAUN;YAC1B,IAAI;gBACA,MAAM,IAAI,CAAC,GAAG,CAAC,OAAO,CAACJ,OAAOa;YAClC,EAAE,OAAOC,OAAO;gBACZ,IAAI,CAAC,MAAM,CAAC,KAAK,CACb;oBAAEA;oBAAOd;oBAAOI;gBAAc,GAC9B;YAER;QACJ;IACJ;AACJ;AAEO,MAAMW,4BAA4BC,oBAAoB,oBAAoB,CAAC;IAC9E,gBAAgBpB;IAChB,cAAc;QACVqB;QACAC;QACAC;QACAC;QACAC;KACH;AACL"}