@webiny/api-websockets-standalone 0.0.0-unstable.5387e5f7c8 → 0.0.0-unstable.9609e4e0c9

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.
@@ -5,10 +5,15 @@ import { WebSocket } from "ws";
5
5
  import { WebsocketsServerAdapter } from "./abstractions.js";
6
6
  declare class NodeWsAdapterImpl implements WebsocketsServerAdapter.Interface<WebSocket> {
7
7
  private wss;
8
+ private pingTimer;
9
+ private readonly answered;
10
+ private pingInterval;
11
+ static withPingInterval(pingInterval: number): NodeWsAdapterImpl;
8
12
  private getWss;
9
13
  start(_server: HttpServer): void;
10
14
  handleUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer): void;
11
15
  stop(): Promise<void>;
16
+ private pingAll;
12
17
  onConnection(cb: (socket: WebSocket, request: import("node:http").IncomingMessage) => void): void;
13
18
  onMessage(socket: WebSocket, cb: (data: Buffer) => void): void;
14
19
  onClose(socket: WebSocket, cb: (code: number, reason: Buffer) => void): void;
@@ -1,32 +1,79 @@
1
- import { WebSocketServer } from "ws";
1
+ import { WebSocket, WebSocketServer } from "ws";
2
2
  import { WebsocketsServerAdapter } from "./abstractions.js";
3
3
  const MAX_PAYLOAD_BYTES = 65536;
4
+ const SERVICE_RESTART = 1012;
5
+ const CLOSE_WAIT_MS = 1000;
6
+ const PING_INTERVAL_MS = 30000;
7
+ const closeForRestart = (client)=>new Promise((resolve)=>{
8
+ if (client.readyState === WebSocket.CLOSED) return void resolve();
9
+ const timer = setTimeout(()=>{
10
+ client.terminate();
11
+ }, CLOSE_WAIT_MS);
12
+ client.once("close", ()=>{
13
+ clearTimeout(timer);
14
+ resolve();
15
+ });
16
+ client.close(SERVICE_RESTART, "Server restarting");
17
+ });
4
18
  class NodeWsAdapterImpl {
19
+ static withPingInterval(pingInterval) {
20
+ const adapter = new NodeWsAdapterImpl();
21
+ adapter.pingInterval = pingInterval;
22
+ return adapter;
23
+ }
5
24
  getWss() {
6
25
  if (!this.wss) throw new Error("WebSocketServer has not been started. Call start() first.");
7
26
  return this.wss;
8
27
  }
9
28
  start(_server) {
10
- this.wss = new WebSocketServer({
29
+ if (this.wss) return;
30
+ const wss = new WebSocketServer({
11
31
  noServer: true,
12
32
  maxPayload: MAX_PAYLOAD_BYTES
13
33
  });
34
+ wss.on("connection", (socket)=>{
35
+ this.answered.add(socket);
36
+ socket.on("pong", ()=>{
37
+ this.answered.add(socket);
38
+ });
39
+ });
40
+ this.wss = wss;
41
+ this.pingTimer = setInterval(()=>{
42
+ this.pingAll(wss);
43
+ }, this.pingInterval);
14
44
  }
15
45
  handleUpgrade(request, socket, head) {
16
46
  this.getWss().handleUpgrade(request, socket, head, (ws)=>{
17
47
  this.getWss().emit("connection", ws, request);
18
48
  });
19
49
  }
20
- stop() {
21
- const wss = this.getWss();
22
- for (const client of wss.clients)client.terminate();
23
- return new Promise((resolve, reject)=>{
50
+ async stop() {
51
+ const wss = this.wss;
52
+ if (!wss) return;
53
+ this.wss = void 0;
54
+ clearInterval(this.pingTimer);
55
+ const clients = [
56
+ ...wss.clients
57
+ ];
58
+ const closing = clients.map(closeForRestart);
59
+ await Promise.all(closing);
60
+ await new Promise((resolve, reject)=>{
24
61
  wss.close((err)=>{
25
62
  if (err) return void reject(err);
26
63
  resolve();
27
64
  });
28
65
  });
29
66
  }
67
+ pingAll(wss) {
68
+ for (const socket of wss.clients){
69
+ if (!this.answered.has(socket)) {
70
+ socket.terminate();
71
+ continue;
72
+ }
73
+ this.answered.delete(socket);
74
+ if (socket.readyState === WebSocket.OPEN) socket.ping();
75
+ }
76
+ }
30
77
  onConnection(cb) {
31
78
  this.getWss().on("connection", cb);
32
79
  }
@@ -50,6 +97,10 @@ class NodeWsAdapterImpl {
50
97
  close(socket, code, reason) {
51
98
  socket.close(code, reason);
52
99
  }
100
+ constructor(){
101
+ this.answered = new WeakSet();
102
+ this.pingInterval = PING_INTERVAL_MS;
103
+ }
53
104
  }
54
105
  const NodeWsAdapter = WebsocketsServerAdapter.createImplementation({
55
106
  implementation: NodeWsAdapterImpl,
@@ -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\n/*\n * Clients only send small JSON envelopes (a ping carrying a token). The `ws` default is 100 MiB,\n * which any client, authenticated or not, could make the server buffer per message.\n */\nconst MAX_PAYLOAD_BYTES = 64 * 1024;\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, maxPayload: MAX_PAYLOAD_BYTES });\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":["MAX_PAYLOAD_BYTES","NodeWsAdapterImpl","Error","_server","WebSocketServer","request","socket","head","ws","wss","client","Promise","resolve","reject","err","cb","data","code","reason","NodeWsAdapter","WebsocketsServerAdapter"],"mappings":";;AAWA,MAAMA,oBAAoB;AAE1B,MAAMC;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;YAAM,YAAYJ;QAAkB;IACnF;IAEO,cAAcK,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
+ {"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\n/*\n * Clients only send small JSON envelopes (a ping carrying a token). The `ws` default is 100 MiB,\n * which any client, authenticated or not, could make the server buffer per message.\n */\nconst MAX_PAYLOAD_BYTES = 64 * 1024;\n\n/*\n * Sent to every client when the server stops. 1012 means \"service restart\": the client reconnects,\n * and behind a load balancer it reaches a process that is still up.\n */\nconst SERVICE_RESTART = 1012;\n\n// How long a client gets to answer the close before its socket is cut.\nconst CLOSE_WAIT_MS = 1000;\n\n/*\n * How often every open socket gets a WebSocket ping frame. Proxies and load balancers close a\n * connection that carries nothing for a while (60 seconds by default on nginx and AWS ALB), and the\n * admin's own ping comes only every 5 minutes. Browsers answer ping frames by themselves; no client\n * code is involved, and nothing is written to the registry.\n */\nconst PING_INTERVAL_MS = 30_000;\n\n/*\n * Closes the socket with `SERVICE_RESTART` and resolves once it is closed. A client that has not\n * answered within `CLOSE_WAIT_MS` (a frozen tab, a dead network) is cut.\n */\nconst closeForRestart = (client: WebSocket): Promise<void> => {\n return new Promise(resolve => {\n if (client.readyState === WebSocket.CLOSED) {\n resolve();\n return;\n }\n const timer = setTimeout(() => {\n client.terminate();\n }, CLOSE_WAIT_MS);\n client.once(\"close\", () => {\n clearTimeout(timer);\n resolve();\n });\n client.close(SERVICE_RESTART, \"Server restarting\");\n });\n};\n\nclass NodeWsAdapterImpl implements WebsocketsServerAdapter.Interface<WebSocket> {\n private wss: WebSocketServer | undefined;\n private pingTimer: ReturnType<typeof setInterval> | undefined;\n // Sockets that answered the last ping, or connected since it was sent.\n private readonly answered = new WeakSet<WebSocket>();\n\n private pingInterval = PING_INTERVAL_MS;\n\n /*\n * An adapter that pings more often, for tests. The constructor takes nothing: DI builds this class\n * with no dependencies.\n */\n public static withPingInterval(pingInterval: number): NodeWsAdapterImpl {\n const adapter = new NodeWsAdapterImpl();\n adapter.pingInterval = pingInterval;\n return adapter;\n }\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 // Already started: a second server and timer would be left running, unreachable by stop().\n if (this.wss) {\n return;\n }\n const wss = new WebSocketServer({ noServer: true, maxPayload: MAX_PAYLOAD_BYTES });\n wss.on(\"connection\", socket => {\n this.answered.add(socket);\n socket.on(\"pong\", () => {\n this.answered.add(socket);\n });\n });\n this.wss = wss;\n this.pingTimer = setInterval(() => {\n this.pingAll(wss);\n }, this.pingInterval);\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 // Safe to call again: a stopped adapter has nothing left to close.\n public async stop(): Promise<void> {\n const wss = this.wss;\n if (!wss) {\n return;\n }\n this.wss = undefined;\n clearInterval(this.pingTimer);\n\n const clients = [...wss.clients];\n const closing = clients.map(closeForRestart);\n await Promise.all(closing);\n\n await new Promise<void>((resolve, reject) => {\n wss.close(err => {\n if (err) {\n reject(err);\n return;\n }\n resolve();\n });\n });\n }\n\n /*\n * Pings every open socket. One that has not answered the previous ping within a whole interval is\n * gone without having closed (a laptop asleep, a dropped network), so it is cut; its close\n * handler then removes the connection.\n */\n private pingAll(wss: WebSocketServer): void {\n for (const socket of wss.clients) {\n if (!this.answered.has(socket)) {\n socket.terminate();\n continue;\n }\n this.answered.delete(socket);\n if (socket.readyState === WebSocket.OPEN) {\n socket.ping();\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":["MAX_PAYLOAD_BYTES","SERVICE_RESTART","CLOSE_WAIT_MS","PING_INTERVAL_MS","closeForRestart","client","Promise","resolve","WebSocket","timer","setTimeout","clearTimeout","NodeWsAdapterImpl","pingInterval","adapter","Error","_server","wss","WebSocketServer","socket","setInterval","request","head","ws","undefined","clearInterval","clients","closing","reject","err","cb","data","code","reason","WeakSet","NodeWsAdapter","WebsocketsServerAdapter"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import { WebsocketsMessageBus } from "./abstractions.js";
2
+ declare class InProcessMessageBusImpl implements WebsocketsMessageBus.Interface {
3
+ private readonly handlers;
4
+ publish(owner: string, message: WebsocketsMessageBus.Message): Promise<void>;
5
+ subscribe(owner: string, handler: WebsocketsMessageBus.Handler): Promise<void>;
6
+ close(): Promise<void>;
7
+ }
8
+ export { InProcessMessageBusImpl };
9
+ export declare const InProcessMessageBus: typeof InProcessMessageBusImpl & {
10
+ __abstraction: import("@webiny/di").Abstraction<import("./abstractions.js").IWebsocketsMessageBus>;
11
+ };
@@ -0,0 +1,24 @@
1
+ import { WebsocketsMessageBus } from "./abstractions.js";
2
+ class InProcessMessageBusImpl {
3
+ async publish(owner, message) {
4
+ const handler = this.handlers.get(owner);
5
+ if (!handler) return;
6
+ await handler(message);
7
+ }
8
+ async subscribe(owner, handler) {
9
+ this.handlers.set(owner, handler);
10
+ }
11
+ async close() {
12
+ this.handlers.clear();
13
+ }
14
+ constructor(){
15
+ this.handlers = new Map();
16
+ }
17
+ }
18
+ const InProcessMessageBus = WebsocketsMessageBus.createImplementation({
19
+ implementation: InProcessMessageBusImpl,
20
+ dependencies: []
21
+ });
22
+ export { InProcessMessageBus, InProcessMessageBusImpl };
23
+
24
+ //# sourceMappingURL=InProcessMessageBus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bus/InProcessMessageBus.js","sources":["../../src/bus/InProcessMessageBus.ts"],"sourcesContent":["import { WebsocketsMessageBus } from \"./abstractions.js\";\n\n/*\n * The bus for a single process, and the default. Every socket is in this process, so the only\n * listener it can reach is its own. Rows another process wrote, such as this process's predecessor\n * before a restart, name an owner nobody listens for here; those messages are dropped and the rows\n * are left to the heartbeat's stale cleanup.\n */\nclass InProcessMessageBusImpl implements WebsocketsMessageBus.Interface {\n private readonly handlers = new Map<string, WebsocketsMessageBus.Handler>();\n\n public async publish(owner: string, message: WebsocketsMessageBus.Message): Promise<void> {\n const handler = this.handlers.get(owner);\n if (!handler) {\n return;\n }\n await handler(message);\n }\n\n public async subscribe(owner: string, handler: WebsocketsMessageBus.Handler): Promise<void> {\n this.handlers.set(owner, handler);\n }\n\n public async close(): Promise<void> {\n this.handlers.clear();\n }\n}\n\nexport { InProcessMessageBusImpl };\n\nexport const InProcessMessageBus = WebsocketsMessageBus.createImplementation({\n implementation: InProcessMessageBusImpl,\n dependencies: []\n});\n"],"names":["InProcessMessageBusImpl","owner","message","handler","Map","InProcessMessageBus","WebsocketsMessageBus"],"mappings":";AAQA,MAAMA;IAGF,MAAa,QAAQC,KAAa,EAAEC,OAAqC,EAAiB;QACtF,MAAMC,UAAU,IAAI,CAAC,QAAQ,CAAC,GAAG,CAACF;QAClC,IAAI,CAACE,SACD;QAEJ,MAAMA,QAAQD;IAClB;IAEA,MAAa,UAAUD,KAAa,EAAEE,OAAqC,EAAiB;QACxF,IAAI,CAAC,QAAQ,CAAC,GAAG,CAACF,OAAOE;IAC7B;IAEA,MAAa,QAAuB;QAChC,IAAI,CAAC,QAAQ,CAAC,KAAK;IACvB;;aAhBiB,QAAQ,GAAG,IAAIC;;AAiBpC;AAIO,MAAMC,sBAAsBC,qBAAqB,oBAAoB,CAAC;IACzE,gBAAgBN;IAChB,cAAc,EAAE;AACpB"}
@@ -0,0 +1,30 @@
1
+ import type { GenericRecord } from "@webiny/api/types.js";
2
+ export interface IWebsocketsBusSendMessage {
3
+ action: "send";
4
+ connectionIds: string[];
5
+ data: GenericRecord;
6
+ }
7
+ export interface IWebsocketsBusDisconnectMessage {
8
+ action: "disconnect";
9
+ connectionIds: string[];
10
+ }
11
+ export type IWebsocketsBusMessage = IWebsocketsBusSendMessage | IWebsocketsBusDisconnectMessage;
12
+ export type IWebsocketsBusHandler = (message: IWebsocketsBusMessage) => Promise<void>;
13
+ /**
14
+ * Carries a send or a disconnect to the process that holds the sockets. Each process listens on its
15
+ * own channel, named after its `ProcessOwner` id, and other processes publish to it.
16
+ *
17
+ * Delivery is at most once and nothing is queued: a message for a process that is not listening is
18
+ * dropped, as a push to a reconnecting client is on AWS.
19
+ */
20
+ export interface IWebsocketsMessageBus {
21
+ publish(owner: string, message: IWebsocketsBusMessage): Promise<void>;
22
+ subscribe(owner: string, handler: IWebsocketsBusHandler): Promise<void>;
23
+ close(): Promise<void>;
24
+ }
25
+ export declare const WebsocketsMessageBus: import("@webiny/di").Abstraction<IWebsocketsMessageBus>;
26
+ export declare namespace WebsocketsMessageBus {
27
+ type Interface = IWebsocketsMessageBus;
28
+ type Message = IWebsocketsBusMessage;
29
+ type Handler = IWebsocketsBusHandler;
30
+ }
@@ -0,0 +1,5 @@
1
+ import { createAbstraction } from "@webiny/feature/api";
2
+ const WebsocketsMessageBus = createAbstraction("Websockets/MessageBus");
3
+ export { WebsocketsMessageBus };
4
+
5
+ //# sourceMappingURL=abstractions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bus/abstractions.js","sources":["../../src/bus/abstractions.ts"],"sourcesContent":["import type { GenericRecord } from \"@webiny/api/types.js\";\nimport { createAbstraction } from \"@webiny/feature/api\";\n\nexport interface IWebsocketsBusSendMessage {\n action: \"send\";\n connectionIds: string[];\n data: GenericRecord;\n}\n\nexport interface IWebsocketsBusDisconnectMessage {\n action: \"disconnect\";\n connectionIds: string[];\n}\n\nexport type IWebsocketsBusMessage = IWebsocketsBusSendMessage | IWebsocketsBusDisconnectMessage;\n\nexport type IWebsocketsBusHandler = (message: IWebsocketsBusMessage) => Promise<void>;\n\n/**\n * Carries a send or a disconnect to the process that holds the sockets. Each process listens on its\n * own channel, named after its `ProcessOwner` id, and other processes publish to it.\n *\n * Delivery is at most once and nothing is queued: a message for a process that is not listening is\n * dropped, as a push to a reconnecting client is on AWS.\n */\nexport interface IWebsocketsMessageBus {\n publish(owner: string, message: IWebsocketsBusMessage): Promise<void>;\n subscribe(owner: string, handler: IWebsocketsBusHandler): Promise<void>;\n close(): Promise<void>;\n}\n\nexport const WebsocketsMessageBus =\n createAbstraction<IWebsocketsMessageBus>(\"Websockets/MessageBus\");\n\nexport namespace WebsocketsMessageBus {\n export type Interface = IWebsocketsMessageBus;\n export type Message = IWebsocketsBusMessage;\n export type Handler = IWebsocketsBusHandler;\n}\n"],"names":["WebsocketsMessageBus","createAbstraction"],"mappings":";AA+BO,MAAMA,uBACTC,kBAAyC"}
@@ -0,0 +1,10 @@
1
+ import type { Logger } from "@webiny/api-core/features/logger/abstractions.js";
2
+ import type { WebsocketsConnectionManager } from "../connectionManager/abstractions.js";
3
+ import type { WebsocketsServerAdapter } from "../adapter/abstractions.js";
4
+ import type { WebsocketsMessageBus } from "./abstractions.js";
5
+ export interface ICreateBusMessageHandlerParams {
6
+ connectionManager: WebsocketsConnectionManager.Interface<unknown>;
7
+ adapter: WebsocketsServerAdapter.Interface<unknown>;
8
+ logger: Logger.Interface;
9
+ }
10
+ export declare const createBusMessageHandler: (params: ICreateBusMessageHandlerParams) => WebsocketsMessageBus.Handler;
@@ -0,0 +1,20 @@
1
+ import { sendToLocalSockets } from "../delivery/sendToLocalSockets.js";
2
+ import { disconnectLocalSockets } from "../delivery/disconnectLocalSockets.js";
3
+ const createBusMessageHandler = (params)=>{
4
+ const { connectionManager } = params;
5
+ return async (message)=>{
6
+ let missing;
7
+ missing = "send" === message.action ? await sendToLocalSockets({
8
+ ...params,
9
+ connectionIds: message.connectionIds,
10
+ data: message.data
11
+ }) : await disconnectLocalSockets({
12
+ ...params,
13
+ connectionIds: message.connectionIds
14
+ });
15
+ for (const connectionId of missing)await connectionManager.remove(connectionId);
16
+ };
17
+ };
18
+ export { createBusMessageHandler };
19
+
20
+ //# sourceMappingURL=createBusMessageHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bus/createBusMessageHandler.js","sources":["../../src/bus/createBusMessageHandler.ts"],"sourcesContent":["import type { Logger } from \"@webiny/api-core/features/logger/abstractions.js\";\nimport type { WebsocketsConnectionManager } from \"~/connectionManager/abstractions.js\";\nimport type { WebsocketsServerAdapter } from \"~/adapter/abstractions.js\";\nimport type { WebsocketsMessageBus } from \"./abstractions.js\";\nimport { sendToLocalSockets } from \"~/delivery/sendToLocalSockets.js\";\nimport { disconnectLocalSockets } from \"~/delivery/disconnectLocalSockets.js\";\n\nexport interface ICreateBusMessageHandlerParams {\n connectionManager: WebsocketsConnectionManager.Interface<unknown>;\n adapter: WebsocketsServerAdapter.Interface<unknown>;\n logger: Logger.Interface;\n}\n\n/*\n * Handles what other processes publish to this one: sends and disconnects for sockets this process\n * holds. Messages arrive here only for connections this process owns, so one with no socket left is\n * gone, and its connection is removed.\n */\nexport const createBusMessageHandler = (\n params: ICreateBusMessageHandlerParams\n): WebsocketsMessageBus.Handler => {\n const { connectionManager } = params;\n return async message => {\n let missing: string[];\n if (message.action === \"send\") {\n missing = await sendToLocalSockets({\n ...params,\n connectionIds: message.connectionIds,\n data: message.data\n });\n } else {\n missing = await disconnectLocalSockets({\n ...params,\n connectionIds: message.connectionIds\n });\n }\n for (const connectionId of missing) {\n await connectionManager.remove(connectionId);\n }\n };\n};\n"],"names":["createBusMessageHandler","params","connectionManager","message","missing","sendToLocalSockets","disconnectLocalSockets","connectionId"],"mappings":";;AAkBO,MAAMA,0BAA0B,CACnCC;IAEA,MAAM,EAAEC,iBAAiB,EAAE,GAAGD;IAC9B,OAAO,OAAME;QACT,IAAIC;QAEAA,UADAD,AAAmB,WAAnBA,QAAQ,MAAM,GACJ,MAAME,mBAAmB;YAC/B,GAAGJ,MAAM;YACT,eAAeE,QAAQ,aAAa;YACpC,MAAMA,QAAQ,IAAI;QACtB,KAEU,MAAMG,uBAAuB;YACnC,GAAGL,MAAM;YACT,eAAeE,QAAQ,aAAa;QACxC;QAEJ,KAAK,MAAMI,gBAAgBH,QACvB,MAAMF,kBAAkB,MAAM,CAACK;IAEvC;AACJ"}
@@ -1,12 +1,15 @@
1
1
  import { ConnectionRegistry } from "@webiny/api-websockets/exports/api.js";
2
+ import { ProcessOwner } from "@webiny/event-handler-standalone";
2
3
  import { WebsocketsConnectionManager } from "./abstractions.js";
3
4
  export declare class ServerConnectionManagerImpl implements WebsocketsConnectionManager.Interface<unknown> {
4
5
  private readonly registry;
6
+ private readonly owner;
5
7
  private readonly sockets;
6
8
  private readonly metadata;
7
- constructor(registry: ConnectionRegistry.Interface);
9
+ constructor(registry: ConnectionRegistry.Interface, owner: ProcessOwner.Interface);
8
10
  add(params: WebsocketsConnectionManager.AddParams<unknown>): Promise<void>;
9
11
  remove(connectionId: string): Promise<void>;
12
+ groupByOwner(connectionIds: string[]): Promise<Map<string, string[]>>;
10
13
  getSocket(connectionId: string): unknown | undefined;
11
14
  getMetadata(connectionId: string): WebsocketsConnectionManager.ConnectionMetadata | undefined;
12
15
  updateLastSeen(connectionId: string): Promise<void>;
@@ -1,8 +1,10 @@
1
1
  import { ConnectionRegistry } from "@webiny/api-websockets/exports/api.js";
2
+ import { ProcessOwner } from "@webiny/event-handler-standalone";
2
3
  import { WebsocketsConnectionManager } from "./abstractions.js";
3
4
  class ServerConnectionManagerImpl {
4
- constructor(registry){
5
+ constructor(registry, owner){
5
6
  this.registry = registry;
7
+ this.owner = owner;
6
8
  this.sockets = new Map();
7
9
  this.metadata = new Map();
8
10
  }
@@ -20,7 +22,8 @@ class ServerConnectionManagerImpl {
20
22
  identity: params.identity,
21
23
  tenant: params.tenant,
22
24
  endpoint: params.endpoint,
23
- connectedOn: new Date(params.connectedAt).toISOString()
25
+ connectedOn: new Date(params.connectedAt).toISOString(),
26
+ owner: this.owner.id
24
27
  });
25
28
  }
26
29
  async remove(connectionId) {
@@ -35,6 +38,18 @@ class ServerConnectionManagerImpl {
35
38
  throw err;
36
39
  }
37
40
  }
41
+ async groupByOwner(connectionIds) {
42
+ const byOwner = new Map();
43
+ if (0 === connectionIds.length) return byOwner;
44
+ const connections = await this.registry.listViaConnections(connectionIds);
45
+ for (const connection of connections){
46
+ if (!connection.owner) continue;
47
+ const group = byOwner.get(connection.owner) ?? [];
48
+ group.push(connection.connectionId);
49
+ byOwner.set(connection.owner, group);
50
+ }
51
+ return byOwner;
52
+ }
38
53
  getSocket(connectionId) {
39
54
  return this.sockets.get(connectionId);
40
55
  }
@@ -61,7 +76,8 @@ class ServerConnectionManagerImpl {
61
76
  const ServerConnectionManager = WebsocketsConnectionManager.createImplementation({
62
77
  implementation: ServerConnectionManagerImpl,
63
78
  dependencies: [
64
- ConnectionRegistry
79
+ ConnectionRegistry,
80
+ ProcessOwner
65
81
  ]
66
82
  });
67
83
  export { ServerConnectionManager, ServerConnectionManagerImpl };
@@ -1 +1 @@
1
- {"version":3,"file":"connectionManager/ServerConnectionManager.js","sources":["../../src/connectionManager/ServerConnectionManager.ts"],"sourcesContent":["import { ConnectionRegistry } from \"@webiny/api-websockets/exports/api.js\";\nimport { WebsocketsConnectionManager } from \"./abstractions.js\";\n\nexport class ServerConnectionManagerImpl implements WebsocketsConnectionManager.Interface<unknown> {\n private readonly sockets = new Map<string, unknown>();\n private readonly metadata = new Map<string, WebsocketsConnectionManager.ConnectionMetadata>();\n\n public constructor(private readonly registry: ConnectionRegistry.Interface) {}\n\n public async add(params: WebsocketsConnectionManager.AddParams<unknown>): Promise<void> {\n this.sockets.set(params.connectionId, params.socket);\n this.metadata.set(params.connectionId, {\n connectionId: params.connectionId,\n endpoint: params.endpoint,\n connectedAt: params.connectedAt,\n host: params.host,\n headers: params.headers\n });\n\n // Save the connection to the shared registry. This is what lets the server later push a\n // message to a specific user — e.g. when a background task finishes and calls\n // SendToIdentity. That flow looks connections up in the registry by identity id to get their\n // connection ids, then comes back here to fetch each live socket by connection id and send.\n // The in-memory map above only answers \"socket for this connection id?\" — it can't be\n // searched by identity, so without this registry row a targeted send finds nobody. The\n // registry stores `connectedOn` as an ISO string (it compares connections against a recency\n // cutoff), so we convert the epoch-ms `connectedAt`.\n await this.registry.register({\n connectionId: params.connectionId,\n identity: params.identity,\n tenant: params.tenant,\n endpoint: params.endpoint,\n connectedOn: new Date(params.connectedAt).toISOString()\n });\n }\n\n public async remove(connectionId: string): Promise<void> {\n this.sockets.delete(connectionId);\n this.metadata.delete(connectionId);\n try {\n await this.registry.unregister({ connectionId });\n } catch (err) {\n if ((err as any).code === \"CONNECTION_NOT_FOUND\") {\n return;\n }\n throw err;\n }\n }\n\n public getSocket(connectionId: string): unknown | undefined {\n return this.sockets.get(connectionId);\n }\n\n public getMetadata(\n connectionId: string\n ): WebsocketsConnectionManager.ConnectionMetadata | undefined {\n return this.metadata.get(connectionId);\n }\n\n public async updateLastSeen(connectionId: string): Promise<void> {\n await this.registry.updateLastSeen(connectionId);\n }\n\n public async cleanup(maxAge: number): Promise<string[]> {\n const olderThan = new Date(Date.now() - maxAge);\n const stale = await this.registry.listStale(olderThan);\n const removed: string[] = [];\n for (const entry of stale) {\n await this.remove(entry.connectionId);\n removed.push(entry.connectionId);\n }\n return removed;\n }\n\n public getActiveConnectionIds(): string[] {\n return Array.from(this.sockets.keys());\n }\n}\n\nexport const ServerConnectionManager = WebsocketsConnectionManager.createImplementation({\n implementation: ServerConnectionManagerImpl,\n dependencies: [ConnectionRegistry]\n});\n"],"names":["ServerConnectionManagerImpl","registry","Map","params","Date","connectionId","err","maxAge","olderThan","stale","removed","entry","Array","ServerConnectionManager","WebsocketsConnectionManager","ConnectionRegistry"],"mappings":";;AAGO,MAAMA;IAIT,YAAoCC,QAAsC,CAAE;aAAxCA,QAAQ,GAARA;aAHnB,OAAO,GAAG,IAAIC;aACd,QAAQ,GAAG,IAAIA;IAE6C;IAE7E,MAAa,IAAIC,MAAsD,EAAiB;QACpF,IAAI,CAAC,OAAO,CAAC,GAAG,CAACA,OAAO,YAAY,EAAEA,OAAO,MAAM;QACnD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAACA,OAAO,YAAY,EAAE;YACnC,cAAcA,OAAO,YAAY;YACjC,UAAUA,OAAO,QAAQ;YACzB,aAAaA,OAAO,WAAW;YAC/B,MAAMA,OAAO,IAAI;YACjB,SAASA,OAAO,OAAO;QAC3B;QAUA,MAAM,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACzB,cAAcA,OAAO,YAAY;YACjC,UAAUA,OAAO,QAAQ;YACzB,QAAQA,OAAO,MAAM;YACrB,UAAUA,OAAO,QAAQ;YACzB,aAAa,IAAIC,KAAKD,OAAO,WAAW,EAAE,WAAW;QACzD;IACJ;IAEA,MAAa,OAAOE,YAAoB,EAAiB;QACrD,IAAI,CAAC,OAAO,CAAC,MAAM,CAACA;QACpB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAACA;QACrB,IAAI;YACA,MAAM,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC;gBAAEA;YAAa;QAClD,EAAE,OAAOC,KAAK;YACV,IAAKA,AAAqB,2BAArBA,IAAY,IAAI,EACjB;YAEJ,MAAMA;QACV;IACJ;IAEO,UAAUD,YAAoB,EAAuB;QACxD,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAACA;IAC5B;IAEO,YACHA,YAAoB,EACsC;QAC1D,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAACA;IAC7B;IAEA,MAAa,eAAeA,YAAoB,EAAiB;QAC7D,MAAM,IAAI,CAAC,QAAQ,CAAC,cAAc,CAACA;IACvC;IAEA,MAAa,QAAQE,MAAc,EAAqB;QACpD,MAAMC,YAAY,IAAIJ,KAAKA,KAAK,GAAG,KAAKG;QACxC,MAAME,QAAQ,MAAM,IAAI,CAAC,QAAQ,CAAC,SAAS,CAACD;QAC5C,MAAME,UAAoB,EAAE;QAC5B,KAAK,MAAMC,SAASF,MAAO;YACvB,MAAM,IAAI,CAAC,MAAM,CAACE,MAAM,YAAY;YACpCD,QAAQ,IAAI,CAACC,MAAM,YAAY;QACnC;QACA,OAAOD;IACX;IAEO,yBAAmC;QACtC,OAAOE,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI;IACvC;AACJ;AAEO,MAAMC,0BAA0BC,4BAA4B,oBAAoB,CAAC;IACpF,gBAAgBd;IAChB,cAAc;QAACe;KAAmB;AACtC"}
1
+ {"version":3,"file":"connectionManager/ServerConnectionManager.js","sources":["../../src/connectionManager/ServerConnectionManager.ts"],"sourcesContent":["import { ConnectionRegistry } from \"@webiny/api-websockets/exports/api.js\";\nimport { ProcessOwner } from \"@webiny/event-handler-standalone\";\nimport { WebsocketsConnectionManager } from \"./abstractions.js\";\n\nexport class ServerConnectionManagerImpl implements WebsocketsConnectionManager.Interface<unknown> {\n private readonly sockets = new Map<string, unknown>();\n private readonly metadata = new Map<string, WebsocketsConnectionManager.ConnectionMetadata>();\n\n public constructor(\n private readonly registry: ConnectionRegistry.Interface,\n private readonly owner: ProcessOwner.Interface\n ) {}\n\n public async add(params: WebsocketsConnectionManager.AddParams<unknown>): Promise<void> {\n this.sockets.set(params.connectionId, params.socket);\n this.metadata.set(params.connectionId, {\n connectionId: params.connectionId,\n endpoint: params.endpoint,\n connectedAt: params.connectedAt,\n host: params.host,\n headers: params.headers\n });\n\n // Save the connection to the shared registry. This is what lets the server later push a\n // message to a specific user — e.g. when a background task finishes and calls\n // SendToIdentity. That flow looks connections up in the registry by identity id to get their\n // connection ids, then comes back here to fetch each live socket by connection id and send.\n // The in-memory map above only answers \"socket for this connection id?\" — it can't be\n // searched by identity, so without this registry row a targeted send finds nobody. The\n // registry stores `connectedOn` as an ISO string (it compares connections against a recency\n // cutoff), so we convert the epoch-ms `connectedAt`.\n await this.registry.register({\n connectionId: params.connectionId,\n identity: params.identity,\n tenant: params.tenant,\n endpoint: params.endpoint,\n connectedOn: new Date(params.connectedAt).toISOString(),\n owner: this.owner.id\n });\n }\n\n public async remove(connectionId: string): Promise<void> {\n this.sockets.delete(connectionId);\n this.metadata.delete(connectionId);\n try {\n await this.registry.unregister({ connectionId });\n } catch (err) {\n if ((err as any).code === \"CONNECTION_NOT_FOUND\") {\n return;\n }\n throw err;\n }\n }\n\n /*\n * The process holding each of these connections, for connections this process has no socket for.\n * Rows without an owner are left out: nobody can say who holds them, so they wait for the\n * heartbeat's stale cleanup.\n */\n public async groupByOwner(connectionIds: string[]): Promise<Map<string, string[]>> {\n const byOwner = new Map<string, string[]>();\n if (connectionIds.length === 0) {\n return byOwner;\n }\n const connections = await this.registry.listViaConnections(connectionIds);\n for (const connection of connections) {\n if (!connection.owner) {\n continue;\n }\n const group = byOwner.get(connection.owner) ?? [];\n group.push(connection.connectionId);\n byOwner.set(connection.owner, group);\n }\n return byOwner;\n }\n\n public getSocket(connectionId: string): unknown | undefined {\n return this.sockets.get(connectionId);\n }\n\n public getMetadata(\n connectionId: string\n ): WebsocketsConnectionManager.ConnectionMetadata | undefined {\n return this.metadata.get(connectionId);\n }\n\n public async updateLastSeen(connectionId: string): Promise<void> {\n await this.registry.updateLastSeen(connectionId);\n }\n\n public async cleanup(maxAge: number): Promise<string[]> {\n const olderThan = new Date(Date.now() - maxAge);\n const stale = await this.registry.listStale(olderThan);\n const removed: string[] = [];\n for (const entry of stale) {\n await this.remove(entry.connectionId);\n removed.push(entry.connectionId);\n }\n return removed;\n }\n\n public getActiveConnectionIds(): string[] {\n return Array.from(this.sockets.keys());\n }\n}\n\nexport const ServerConnectionManager = WebsocketsConnectionManager.createImplementation({\n implementation: ServerConnectionManagerImpl,\n dependencies: [ConnectionRegistry, ProcessOwner]\n});\n"],"names":["ServerConnectionManagerImpl","registry","owner","Map","params","Date","connectionId","err","connectionIds","byOwner","connections","connection","group","maxAge","olderThan","stale","removed","entry","Array","ServerConnectionManager","WebsocketsConnectionManager","ConnectionRegistry","ProcessOwner"],"mappings":";;;AAIO,MAAMA;IAIT,YACqBC,QAAsC,EACtCC,KAA6B,CAChD;aAFmBD,QAAQ,GAARA;aACAC,KAAK,GAALA;aALJ,OAAO,GAAG,IAAIC;aACd,QAAQ,GAAG,IAAIA;IAK7B;IAEH,MAAa,IAAIC,MAAsD,EAAiB;QACpF,IAAI,CAAC,OAAO,CAAC,GAAG,CAACA,OAAO,YAAY,EAAEA,OAAO,MAAM;QACnD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAACA,OAAO,YAAY,EAAE;YACnC,cAAcA,OAAO,YAAY;YACjC,UAAUA,OAAO,QAAQ;YACzB,aAAaA,OAAO,WAAW;YAC/B,MAAMA,OAAO,IAAI;YACjB,SAASA,OAAO,OAAO;QAC3B;QAUA,MAAM,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACzB,cAAcA,OAAO,YAAY;YACjC,UAAUA,OAAO,QAAQ;YACzB,QAAQA,OAAO,MAAM;YACrB,UAAUA,OAAO,QAAQ;YACzB,aAAa,IAAIC,KAAKD,OAAO,WAAW,EAAE,WAAW;YACrD,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE;QACxB;IACJ;IAEA,MAAa,OAAOE,YAAoB,EAAiB;QACrD,IAAI,CAAC,OAAO,CAAC,MAAM,CAACA;QACpB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAACA;QACrB,IAAI;YACA,MAAM,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC;gBAAEA;YAAa;QAClD,EAAE,OAAOC,KAAK;YACV,IAAKA,AAAqB,2BAArBA,IAAY,IAAI,EACjB;YAEJ,MAAMA;QACV;IACJ;IAOA,MAAa,aAAaC,aAAuB,EAAkC;QAC/E,MAAMC,UAAU,IAAIN;QACpB,IAAIK,AAAyB,MAAzBA,cAAc,MAAM,EACpB,OAAOC;QAEX,MAAMC,cAAc,MAAM,IAAI,CAAC,QAAQ,CAAC,kBAAkB,CAACF;QAC3D,KAAK,MAAMG,cAAcD,YAAa;YAClC,IAAI,CAACC,WAAW,KAAK,EACjB;YAEJ,MAAMC,QAAQH,QAAQ,GAAG,CAACE,WAAW,KAAK,KAAK,EAAE;YACjDC,MAAM,IAAI,CAACD,WAAW,YAAY;YAClCF,QAAQ,GAAG,CAACE,WAAW,KAAK,EAAEC;QAClC;QACA,OAAOH;IACX;IAEO,UAAUH,YAAoB,EAAuB;QACxD,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAACA;IAC5B;IAEO,YACHA,YAAoB,EACsC;QAC1D,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAACA;IAC7B;IAEA,MAAa,eAAeA,YAAoB,EAAiB;QAC7D,MAAM,IAAI,CAAC,QAAQ,CAAC,cAAc,CAACA;IACvC;IAEA,MAAa,QAAQO,MAAc,EAAqB;QACpD,MAAMC,YAAY,IAAIT,KAAKA,KAAK,GAAG,KAAKQ;QACxC,MAAME,QAAQ,MAAM,IAAI,CAAC,QAAQ,CAAC,SAAS,CAACD;QAC5C,MAAME,UAAoB,EAAE;QAC5B,KAAK,MAAMC,SAASF,MAAO;YACvB,MAAM,IAAI,CAAC,MAAM,CAACE,MAAM,YAAY;YACpCD,QAAQ,IAAI,CAACC,MAAM,YAAY;QACnC;QACA,OAAOD;IACX;IAEO,yBAAmC;QACtC,OAAOE,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI;IACvC;AACJ;AAEO,MAAMC,0BAA0BC,4BAA4B,oBAAoB,CAAC;IACpF,gBAAgBpB;IAChB,cAAc;QAACqB;QAAoBC;KAAa;AACpD"}
@@ -19,6 +19,7 @@ export interface IWebsocketsConnectionManagerMetadata {
19
19
  export interface IWebsocketsConnectionManager<TSocket> {
20
20
  add(params: IWebsocketsConnectionManagerAddParams<TSocket>): Promise<void>;
21
21
  remove(connectionId: string): Promise<void>;
22
+ groupByOwner(connectionIds: string[]): Promise<Map<string, string[]>>;
22
23
  getSocket(connectionId: string): TSocket | undefined;
23
24
  getMetadata(connectionId: string): IWebsocketsConnectionManagerMetadata | undefined;
24
25
  updateLastSeen(connectionId: string): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"connectionManager/abstractions.js","sources":["../../src/connectionManager/abstractions.ts"],"sourcesContent":["import { ConnectionRegistry } from \"@webiny/api-websockets/exports/api.js\";\nimport { createAbstraction } from \"@webiny/feature/api\";\n\nexport interface IWebsocketsConnectionManagerAddParams<TSocket> {\n connectionId: string;\n socket: TSocket;\n endpoint: string;\n identity: ConnectionRegistry.Identity;\n tenant: string;\n connectedAt: number;\n host: string;\n headers: Record<string, string>;\n}\n\nexport interface IWebsocketsConnectionManagerMetadata {\n connectionId: string;\n endpoint: string;\n connectedAt: number;\n host: string;\n headers: Record<string, string>;\n}\n\nexport interface IWebsocketsConnectionManager<TSocket> {\n add(params: IWebsocketsConnectionManagerAddParams<TSocket>): Promise<void>;\n remove(connectionId: string): Promise<void>;\n getSocket(connectionId: string): TSocket | undefined;\n getMetadata(connectionId: string): IWebsocketsConnectionManagerMetadata | undefined;\n updateLastSeen(connectionId: string): Promise<void>;\n cleanup(maxAge: number): Promise<string[]>;\n getActiveConnectionIds(): string[];\n}\n\nexport const WebsocketsConnectionManager = createAbstraction<IWebsocketsConnectionManager<unknown>>(\n \"WebsocketsConnectionManager\"\n);\n\nexport namespace WebsocketsConnectionManager {\n export type Interface<TSocket> = IWebsocketsConnectionManager<TSocket>;\n export type AddParams<TSocket> = IWebsocketsConnectionManagerAddParams<TSocket>;\n export type ConnectionMetadata = IWebsocketsConnectionManagerMetadata;\n}\n"],"names":["WebsocketsConnectionManager","createAbstraction"],"mappings":";AAgCO,MAAMA,8BAA8BC,kBACvC"}
1
+ {"version":3,"file":"connectionManager/abstractions.js","sources":["../../src/connectionManager/abstractions.ts"],"sourcesContent":["import { ConnectionRegistry } from \"@webiny/api-websockets/exports/api.js\";\nimport { createAbstraction } from \"@webiny/feature/api\";\n\nexport interface IWebsocketsConnectionManagerAddParams<TSocket> {\n connectionId: string;\n socket: TSocket;\n endpoint: string;\n identity: ConnectionRegistry.Identity;\n tenant: string;\n connectedAt: number;\n host: string;\n headers: Record<string, string>;\n}\n\nexport interface IWebsocketsConnectionManagerMetadata {\n connectionId: string;\n endpoint: string;\n connectedAt: number;\n host: string;\n headers: Record<string, string>;\n}\n\nexport interface IWebsocketsConnectionManager<TSocket> {\n add(params: IWebsocketsConnectionManagerAddParams<TSocket>): Promise<void>;\n remove(connectionId: string): Promise<void>;\n groupByOwner(connectionIds: string[]): Promise<Map<string, string[]>>;\n getSocket(connectionId: string): TSocket | undefined;\n getMetadata(connectionId: string): IWebsocketsConnectionManagerMetadata | undefined;\n updateLastSeen(connectionId: string): Promise<void>;\n cleanup(maxAge: number): Promise<string[]>;\n getActiveConnectionIds(): string[];\n}\n\nexport const WebsocketsConnectionManager = createAbstraction<IWebsocketsConnectionManager<unknown>>(\n \"WebsocketsConnectionManager\"\n);\n\nexport namespace WebsocketsConnectionManager {\n export type Interface<TSocket> = IWebsocketsConnectionManager<TSocket>;\n export type AddParams<TSocket> = IWebsocketsConnectionManagerAddParams<TSocket>;\n export type ConnectionMetadata = IWebsocketsConnectionManagerMetadata;\n}\n"],"names":["WebsocketsConnectionManager","createAbstraction"],"mappings":";AAiCO,MAAMA,8BAA8BC,kBACvC"}
@@ -0,0 +1,10 @@
1
+ import type { Logger } from "@webiny/api-core/features/logger/abstractions.js";
2
+ import type { WebsocketsConnectionManager } from "../connectionManager/abstractions.js";
3
+ import type { WebsocketsServerAdapter } from "../adapter/abstractions.js";
4
+ export interface IDisconnectLocalSocketsParams {
5
+ connectionManager: WebsocketsConnectionManager.Interface<unknown>;
6
+ adapter: WebsocketsServerAdapter.Interface<unknown>;
7
+ logger: Logger.Interface;
8
+ connectionIds: string[];
9
+ }
10
+ export declare const disconnectLocalSockets: (params: IDisconnectLocalSocketsParams) => Promise<string[]>;
@@ -0,0 +1,24 @@
1
+ const disconnectLocalSockets = async (params)=>{
2
+ const { connectionManager, adapter, logger, connectionIds } = params;
3
+ const missing = [];
4
+ for (const connectionId of connectionIds){
5
+ const socket = connectionManager.getSocket(connectionId);
6
+ if (void 0 === socket) {
7
+ missing.push(connectionId);
8
+ continue;
9
+ }
10
+ try {
11
+ adapter.close(socket);
12
+ await connectionManager.remove(connectionId);
13
+ } catch (error) {
14
+ logger.error({
15
+ error,
16
+ connectionId
17
+ }, "[websockets] could not disconnect a connection");
18
+ }
19
+ }
20
+ return missing;
21
+ };
22
+ export { disconnectLocalSockets };
23
+
24
+ //# sourceMappingURL=disconnectLocalSockets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delivery/disconnectLocalSockets.js","sources":["../../src/delivery/disconnectLocalSockets.ts"],"sourcesContent":["import 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 IDisconnectLocalSocketsParams {\n connectionManager: WebsocketsConnectionManager.Interface<unknown>;\n adapter: WebsocketsServerAdapter.Interface<unknown>;\n logger: Logger.Interface;\n connectionIds: string[];\n}\n\n/*\n * Closes each socket this process holds, removes its connection, and returns the ids it holds no\n * socket for. A failed close is logged and the others go on.\n */\nexport const disconnectLocalSockets = async (\n params: IDisconnectLocalSocketsParams\n): Promise<string[]> => {\n const { connectionManager, adapter, logger, connectionIds } = params;\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 adapter.close(socket);\n await connectionManager.remove(connectionId);\n } catch (error) {\n logger.error({ error, connectionId }, \"[websockets] could not disconnect a connection\");\n }\n }\n return missing;\n};\n"],"names":["disconnectLocalSockets","params","connectionManager","adapter","logger","connectionIds","missing","connectionId","socket","undefined","error"],"mappings":"AAeO,MAAMA,yBAAyB,OAClCC;IAEA,MAAM,EAAEC,iBAAiB,EAAEC,OAAO,EAAEC,MAAM,EAAEC,aAAa,EAAE,GAAGJ;IAC9D,MAAMK,UAAoB,EAAE;IAC5B,KAAK,MAAMC,gBAAgBF,cAAe;QACtC,MAAMG,SAASN,kBAAkB,SAAS,CAACK;QAC3C,IAAIC,AAAWC,WAAXD,QAAsB;YACtBF,QAAQ,IAAI,CAACC;YACb;QACJ;QACA,IAAI;YACAJ,QAAQ,KAAK,CAACK;YACd,MAAMN,kBAAkB,MAAM,CAACK;QACnC,EAAE,OAAOG,OAAO;YACZN,OAAO,KAAK,CAAC;gBAAEM;gBAAOH;YAAa,GAAG;QAC1C;IACJ;IACA,OAAOD;AACX"}
@@ -0,0 +1,12 @@
1
+ import type { GenericRecord } from "@webiny/api/types.js";
2
+ import type { Logger } from "@webiny/api-core/features/logger/abstractions.js";
3
+ import type { WebsocketsConnectionManager } from "../connectionManager/abstractions.js";
4
+ import type { WebsocketsServerAdapter } from "../adapter/abstractions.js";
5
+ export interface ISendToLocalSocketsParams {
6
+ connectionManager: WebsocketsConnectionManager.Interface<unknown>;
7
+ adapter: WebsocketsServerAdapter.Interface<unknown>;
8
+ logger: Logger.Interface;
9
+ connectionIds: string[];
10
+ data: GenericRecord;
11
+ }
12
+ export declare const sendToLocalSockets: (params: ISendToLocalSocketsParams) => Promise<string[]>;
@@ -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"}
package/index.d.ts CHANGED
@@ -3,7 +3,10 @@ export type { IWebsocketsServer, WebsocketsConnectionAuthenticator } from "./ser
3
3
  export * from "./adapter/abstractions.js";
4
4
  export * from "./upgradeHandler/abstractions.js";
5
5
  export * from "./connectionManager/abstractions.js";
6
+ export { WebsocketsMessageBus } from "./bus/abstractions.js";
7
+ export { createBusMessageHandler } from "./bus/createBusMessageHandler.js";
6
8
  export { ServerConnectionManager } from "./connectionManager/ServerConnectionManager.js";
7
9
  export { NodeWsAdapter } from "./adapter/NodeWsAdapter.js";
8
10
  export { ServerWebsocketsTransport } from "./transport/ServerWebsocketsTransport.js";
11
+ export { InProcessMessageBus } from "./bus/InProcessMessageBus.js";
9
12
  export { WebsocketsStandaloneFeature } from "./WebsocketsStandaloneFeature.js";
package/index.js CHANGED
@@ -2,7 +2,10 @@ export * from "./adapter/abstractions.js";
2
2
  export * from "./upgradeHandler/abstractions.js";
3
3
  export * from "./connectionManager/abstractions.js";
4
4
  export { attachWebsocketsServer, createWebsocketsServer } from "./server/WebsocketsServer.js";
5
+ export { WebsocketsMessageBus } from "./bus/abstractions.js";
6
+ export { createBusMessageHandler } from "./bus/createBusMessageHandler.js";
5
7
  export { ServerConnectionManager } from "./connectionManager/ServerConnectionManager.js";
6
8
  export { NodeWsAdapter } from "./adapter/NodeWsAdapter.js";
7
9
  export { ServerWebsocketsTransport } from "./transport/ServerWebsocketsTransport.js";
10
+ export { InProcessMessageBus } from "./bus/InProcessMessageBus.js";
8
11
  export { WebsocketsStandaloneFeature } from "./WebsocketsStandaloneFeature.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webiny/api-websockets-standalone",
3
- "version": "0.0.0-unstable.5387e5f7c8",
3
+ "version": "0.0.0-unstable.9609e4e0c9",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,20 +16,22 @@
16
16
  },
17
17
  "license": "MIT",
18
18
  "dependencies": {
19
- "@webiny/api": "0.0.0-unstable.5387e5f7c8",
20
- "@webiny/api-websockets": "0.0.0-unstable.5387e5f7c8",
21
- "@webiny/feature": "0.0.0-unstable.5387e5f7c8",
22
- "@webiny/plugins": "0.0.0-unstable.5387e5f7c8",
23
- "@webiny/utils": "0.0.0-unstable.5387e5f7c8",
24
- "ws": "8.21.3",
25
- "zod": "4.4.3"
19
+ "@webiny/api": "0.0.0-unstable.9609e4e0c9",
20
+ "@webiny/api-core": "0.0.0-unstable.9609e4e0c9",
21
+ "@webiny/api-websockets": "0.0.0-unstable.9609e4e0c9",
22
+ "@webiny/event-handler-standalone": "0.0.0-unstable.9609e4e0c9",
23
+ "@webiny/feature": "0.0.0-unstable.9609e4e0c9",
24
+ "@webiny/plugins": "0.0.0-unstable.9609e4e0c9",
25
+ "@webiny/utils": "0.0.0-unstable.9609e4e0c9",
26
+ "ws": "8.22.0",
27
+ "zod": "4.6.7"
26
28
  },
27
29
  "devDependencies": {
28
- "@types/ws": "8.18.1",
29
- "@webiny/build-tools": "0.0.0-unstable.5387e5f7c8",
30
+ "@types/ws": "8.18.2",
31
+ "@webiny/build-tools": "0.0.0-unstable.9609e4e0c9",
30
32
  "rimraf": "6.1.3",
31
33
  "typescript": "7.0.2",
32
- "vitest": "4.1.11"
34
+ "vitest": "5.0.5"
33
35
  },
34
36
  "publishConfig": {
35
37
  "access": "public"
@@ -43,11 +43,13 @@ class WebsocketsServer {
43
43
  async stop() {
44
44
  this.shuttingDown = true;
45
45
  this.heartbeat?.stop();
46
- await this.adapter.stop();
47
- if (this.connectionManager) {
48
- const activeIds = this.connectionManager.getActiveConnectionIds();
49
- for (const connectionId of activeIds)await this.connectionManager.remove(connectionId);
50
- }
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
+ ]);
51
53
  if (this.ownsHttpServer) await this.closeHttpServer();
52
54
  if (this.debug) console.log("[WebsocketsServer] stopped");
53
55
  }
@@ -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 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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"}
@@ -1,13 +1,20 @@
1
1
  import { WebsocketsTransport } from "@webiny/api-websockets/exports/api.js";
2
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";
3
5
  import { WebsocketsConnectionManager } from "../connectionManager/abstractions.js";
4
6
  import { WebsocketsServerAdapter } from "../adapter/abstractions.js";
7
+ import { WebsocketsMessageBus } from "../bus/abstractions.js";
5
8
  export declare class ServerWebsocketsTransportImpl implements WebsocketsTransport.Interface {
6
9
  private readonly connectionManager;
7
10
  private readonly adapter;
8
- constructor(connectionManager: WebsocketsConnectionManager.Interface<unknown>, adapter: WebsocketsServerAdapter.Interface<unknown>);
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);
9
15
  send<T extends GenericRecord = GenericRecord>(connections: WebsocketsTransport.SendConnection[], data: WebsocketsTransport.SendData<T>): Promise<void>;
10
16
  disconnect(connections: WebsocketsTransport.DisconnectConnection[]): Promise<void>;
17
+ private forward;
11
18
  }
12
19
  export declare const ServerWebsocketsTransport: typeof ServerWebsocketsTransportImpl & {
13
20
  __abstraction: import("@webiny/di").Abstraction<import("@webiny/api-websockets/transport").IWebsocketsTransport>;
@@ -1,41 +1,63 @@
1
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";
2
4
  import { WebsocketsConnectionManager } from "../connectionManager/abstractions.js";
3
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";
4
9
  class ServerWebsocketsTransportImpl {
5
- constructor(connectionManager, adapter){
10
+ constructor(connectionManager, adapter, bus, owner, logger){
6
11
  this.connectionManager = connectionManager;
7
12
  this.adapter = adapter;
13
+ this.bus = bus;
14
+ this.owner = owner;
15
+ this.logger = logger;
8
16
  }
9
17
  async send(connections, data) {
10
- for (const connection of connections){
11
- const { connectionId } = connection;
12
- const socket = this.connectionManager.getSocket(connectionId);
13
- if (void 0 === socket) {
14
- await this.connectionManager.remove(connectionId);
15
- continue;
16
- }
17
- try {
18
- await this.adapter.send(socket, JSON.stringify(data));
19
- } catch (ex) {
20
- console.error(`Failed to send message to connection "${connectionId}". Check logs for more information.`);
21
- console.log(ex);
22
- }
23
- }
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
+ }));
24
31
  }
25
32
  async disconnect(connections) {
26
- for (const connection of connections){
27
- const { connectionId } = connection;
28
- const socket = this.connectionManager.getSocket(connectionId);
29
- if (void 0 === socket) {
30
- await this.connectionManager.remove(connectionId);
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);
31
50
  continue;
32
51
  }
52
+ const message = toMessage(connectionIds);
33
53
  try {
34
- this.adapter.close(socket);
35
- await this.connectionManager.remove(connectionId);
36
- } catch (ex) {
37
- console.error(`Failed to disconnect connection "${connectionId}". Check logs for more information.`);
38
- console.log(ex);
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");
39
61
  }
40
62
  }
41
63
  }
@@ -44,7 +66,10 @@ const ServerWebsocketsTransport = WebsocketsTransport.createImplementation({
44
66
  implementation: ServerWebsocketsTransportImpl,
45
67
  dependencies: [
46
68
  WebsocketsConnectionManager,
47
- WebsocketsServerAdapter
69
+ WebsocketsServerAdapter,
70
+ WebsocketsMessageBus,
71
+ ProcessOwner,
72
+ Logger
48
73
  ]
49
74
  });
50
75
  export { ServerWebsocketsTransport, ServerWebsocketsTransportImpl };
@@ -1 +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 { WebsocketsConnectionManager } from \"~/connectionManager/abstractions.js\";\nimport { WebsocketsServerAdapter } from \"~/adapter/abstractions.js\";\n\nexport class ServerWebsocketsTransportImpl implements WebsocketsTransport.Interface {\n private readonly connectionManager: WebsocketsConnectionManager.Interface<unknown>;\n private readonly adapter: WebsocketsServerAdapter.Interface<unknown>;\n\n public constructor(\n connectionManager: WebsocketsConnectionManager.Interface<unknown>,\n adapter: WebsocketsServerAdapter.Interface<unknown>\n ) {\n this.connectionManager = connectionManager;\n this.adapter = adapter;\n }\n\n public async send<T extends GenericRecord = GenericRecord>(\n connections: WebsocketsTransport.SendConnection[],\n data: WebsocketsTransport.SendData<T>\n ): Promise<void> {\n for (const connection of connections) {\n const { connectionId } = connection;\n const socket = this.connectionManager.getSocket(connectionId);\n if (socket === undefined) {\n await this.connectionManager.remove(connectionId);\n continue;\n }\n try {\n await this.adapter.send(socket, JSON.stringify(data));\n } catch (ex) {\n console.error(\n `Failed to send message to connection \"${connectionId}\". Check logs for more information.`\n );\n console.log(ex);\n }\n }\n }\n\n public async disconnect(\n connections: WebsocketsTransport.DisconnectConnection[]\n ): Promise<void> {\n for (const connection of connections) {\n const { connectionId } = connection;\n const socket = this.connectionManager.getSocket(connectionId);\n if (socket === undefined) {\n await this.connectionManager.remove(connectionId);\n continue;\n }\n try {\n this.adapter.close(socket);\n await this.connectionManager.remove(connectionId);\n } catch (ex) {\n console.error(\n `Failed to disconnect connection \"${connectionId}\". Check logs for more information.`\n );\n console.log(ex);\n }\n }\n }\n}\n\nexport const ServerWebsocketsTransport = WebsocketsTransport.createImplementation({\n implementation: ServerWebsocketsTransportImpl,\n dependencies: [WebsocketsConnectionManager, WebsocketsServerAdapter]\n});\n"],"names":["ServerWebsocketsTransportImpl","connectionManager","adapter","connections","data","connection","connectionId","socket","undefined","JSON","ex","console","ServerWebsocketsTransport","WebsocketsTransport","WebsocketsConnectionManager","WebsocketsServerAdapter"],"mappings":";;;AAKO,MAAMA;IAIT,YACIC,iBAAiE,EACjEC,OAAmD,CACrD;QACE,IAAI,CAAC,iBAAiB,GAAGD;QACzB,IAAI,CAAC,OAAO,GAAGC;IACnB;IAEA,MAAa,KACTC,WAAiD,EACjDC,IAAqC,EACxB;QACb,KAAK,MAAMC,cAAcF,YAAa;YAClC,MAAM,EAAEG,YAAY,EAAE,GAAGD;YACzB,MAAME,SAAS,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAACD;YAChD,IAAIC,AAAWC,WAAXD,QAAsB;gBACtB,MAAM,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAACD;gBACpC;YACJ;YACA,IAAI;gBACA,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAACC,QAAQE,KAAK,SAAS,CAACL;YACnD,EAAE,OAAOM,IAAI;gBACTC,QAAQ,KAAK,CACT,CAAC,sCAAsC,EAAEL,aAAa,mCAAmC,CAAC;gBAE9FK,QAAQ,GAAG,CAACD;YAChB;QACJ;IACJ;IAEA,MAAa,WACTP,WAAuD,EAC1C;QACb,KAAK,MAAME,cAAcF,YAAa;YAClC,MAAM,EAAEG,YAAY,EAAE,GAAGD;YACzB,MAAME,SAAS,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAACD;YAChD,IAAIC,AAAWC,WAAXD,QAAsB;gBACtB,MAAM,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAACD;gBACpC;YACJ;YACA,IAAI;gBACA,IAAI,CAAC,OAAO,CAAC,KAAK,CAACC;gBACnB,MAAM,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAACD;YACxC,EAAE,OAAOI,IAAI;gBACTC,QAAQ,KAAK,CACT,CAAC,iCAAiC,EAAEL,aAAa,mCAAmC,CAAC;gBAEzFK,QAAQ,GAAG,CAACD;YAChB;QACJ;IACJ;AACJ;AAEO,MAAME,4BAA4BC,oBAAoB,oBAAoB,CAAC;IAC9E,gBAAgBb;IAChB,cAAc;QAACc;QAA6BC;KAAwB;AACxE"}
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"}