@webiny/api-websockets-standalone 0.0.0-unstable.7c2def32a7 → 0.0.0-unstable.e9333b3f0b

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.
@@ -1,19 +1,5 @@
1
- import { WebSocket, WebSocketServer } from "ws";
1
+ import { WebSocketServer } from "ws";
2
2
  import { WebsocketsServerAdapter } from "./abstractions.js";
3
- const MAX_PAYLOAD_BYTES = 65536;
4
- const SERVICE_RESTART = 1012;
5
- const CLOSE_WAIT_MS = 1000;
6
- const closeForRestart = (client)=>new Promise((resolve)=>{
7
- if (client.readyState === WebSocket.CLOSED) return void resolve();
8
- const timer = setTimeout(()=>{
9
- client.terminate();
10
- }, CLOSE_WAIT_MS);
11
- client.once("close", ()=>{
12
- clearTimeout(timer);
13
- resolve();
14
- });
15
- client.close(SERVICE_RESTART, "Server restarting");
16
- });
17
3
  class NodeWsAdapterImpl {
18
4
  getWss() {
19
5
  if (!this.wss) throw new Error("WebSocketServer has not been started. Call start() first.");
@@ -21,8 +7,7 @@ class NodeWsAdapterImpl {
21
7
  }
22
8
  start(_server) {
23
9
  this.wss = new WebSocketServer({
24
- noServer: true,
25
- maxPayload: MAX_PAYLOAD_BYTES
10
+ noServer: true
26
11
  });
27
12
  }
28
13
  handleUpgrade(request, socket, head) {
@@ -30,16 +15,10 @@ class NodeWsAdapterImpl {
30
15
  this.getWss().emit("connection", ws, request);
31
16
  });
32
17
  }
33
- async stop() {
34
- const wss = this.wss;
35
- if (!wss) return;
36
- this.wss = void 0;
37
- const clients = [
38
- ...wss.clients
39
- ];
40
- const closing = clients.map(closeForRestart);
41
- await Promise.all(closing);
42
- await new Promise((resolve, reject)=>{
18
+ stop() {
19
+ const wss = this.getWss();
20
+ for (const client of wss.clients)client.terminate();
21
+ return new Promise((resolve, reject)=>{
43
22
  wss.close((err)=>{
44
23
  if (err) return void reject(err);
45
24
  resolve();
@@ -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\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 * 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\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 // 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\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 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","closeForRestart","client","Promise","resolve","WebSocket","timer","setTimeout","clearTimeout","NodeWsAdapterImpl","Error","_server","WebSocketServer","request","socket","head","ws","wss","undefined","clients","closing","reject","err","cb","data","code","reason","NodeWsAdapter","WebsocketsServerAdapter"],"mappings":";;AAWA,MAAMA,oBAAoB;AAM1B,MAAMC,kBAAkB;AAGxB,MAAMC,gBAAgB;AAMtB,MAAMC,kBAAkB,CAACC,SACd,IAAIC,QAAQC,CAAAA;QACf,IAAIF,OAAO,UAAU,KAAKG,UAAU,MAAM,EAAE,YACxCD;QAGJ,MAAME,QAAQC,WAAW;YACrBL,OAAO,SAAS;QACpB,GAAGF;QACHE,OAAO,IAAI,CAAC,SAAS;YACjBM,aAAaF;YACbF;QACJ;QACAF,OAAO,KAAK,CAACH,iBAAiB;IAClC;AAGJ,MAAMU;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,YAAYd;QAAkB;IACnF;IAEO,cAAce,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;IAGA,MAAa,OAAsB;QAC/B,MAAMI,MAAM,IAAI,CAAC,GAAG;QACpB,IAAI,CAACA,KACD;QAEJ,IAAI,CAAC,GAAG,GAAGC;QAEX,MAAMC,UAAU;eAAIF,IAAI,OAAO;SAAC;QAChC,MAAMG,UAAUD,QAAQ,GAAG,CAAClB;QAC5B,MAAME,QAAQ,GAAG,CAACiB;QAElB,MAAM,IAAIjB,QAAc,CAACC,SAASiB;YAC9BJ,IAAI,KAAK,CAACK,CAAAA;gBACN,IAAIA,KAAK,YACLD,OAAOC;gBAGXlB;YACJ;QACJ;IACJ;IAEO,aACHmB,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,IAAIrB,QAAQ,CAACC,SAASiB;YACzBP,OAAO,IAAI,CAACU,MAAMF,CAAAA;gBACd,IAAIA,KAAK,YACLD,OAAOC;gBAGXlB;YACJ;QACJ;IACJ;IAEO,MAAMU,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\nclass NodeWsAdapterImpl implements WebsocketsServerAdapter.Interface<WebSocket> {\n private wss: WebSocketServer | undefined;\n\n private getWss(): WebSocketServer {\n if (!this.wss) {\n throw new Error(\"WebSocketServer has not been started. Call start() first.\");\n }\n return this.wss;\n }\n\n /* The server param is kept for interface compliance but not used —\n the HTTP upgrade event is handled externally via handleUpgrade(). */\n public start(_server: HttpServer): void {\n this.wss = new WebSocketServer({ noServer: true });\n }\n\n public handleUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer): void {\n this.getWss().handleUpgrade(request, socket, head, ws => {\n this.getWss().emit(\"connection\", ws, request);\n });\n }\n\n public stop(): Promise<void> {\n const wss = this.getWss();\n /* Terminate all open client connections so wss.close() resolves immediately. */\n for (const client of wss.clients) {\n client.terminate();\n }\n return new Promise((resolve, reject) => {\n wss.close(err => {\n if (err) {\n reject(err);\n return;\n }\n resolve();\n });\n });\n }\n\n public onConnection(\n cb: (socket: WebSocket, request: import(\"node:http\").IncomingMessage) => void\n ): void {\n this.getWss().on(\"connection\", cb);\n }\n\n public onMessage(socket: WebSocket, cb: (data: Buffer) => void): void {\n socket.on(\"message\", cb);\n }\n\n public onClose(socket: WebSocket, cb: (code: number, reason: Buffer) => void): void {\n socket.on(\"close\", cb);\n }\n\n public onError(socket: WebSocket, cb: (error: Error) => void): void {\n socket.on(\"error\", cb);\n }\n\n public send(socket: WebSocket, data: string): Promise<void> {\n return new Promise((resolve, reject) => {\n socket.send(data, err => {\n if (err) {\n reject(err);\n return;\n }\n resolve();\n });\n });\n }\n\n public close(socket: WebSocket, code?: number, reason?: string): void {\n socket.close(code, reason);\n }\n}\n\nexport { NodeWsAdapterImpl };\n\nexport const NodeWsAdapter = WebsocketsServerAdapter.createImplementation({\n implementation: NodeWsAdapterImpl,\n dependencies: []\n});\n"],"names":["NodeWsAdapterImpl","Error","_server","WebSocketServer","request","socket","head","ws","wss","client","Promise","resolve","reject","err","cb","data","code","reason","NodeWsAdapter","WebsocketsServerAdapter"],"mappings":";;AAOA,MAAMA;IAGM,SAA0B;QAC9B,IAAI,CAAC,IAAI,CAAC,GAAG,EACT,MAAM,IAAIC,MAAM;QAEpB,OAAO,IAAI,CAAC,GAAG;IACnB;IAIO,MAAMC,OAAmB,EAAQ;QACpC,IAAI,CAAC,GAAG,GAAG,IAAIC,gBAAgB;YAAE,UAAU;QAAK;IACpD;IAEO,cAAcC,OAAwB,EAAEC,MAAc,EAAEC,IAAY,EAAQ;QAC/E,IAAI,CAAC,MAAM,GAAG,aAAa,CAACF,SAASC,QAAQC,MAAMC,CAAAA;YAC/C,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,cAAcA,IAAIH;QACzC;IACJ;IAEO,OAAsB;QACzB,MAAMI,MAAM,IAAI,CAAC,MAAM;QAEvB,KAAK,MAAMC,UAAUD,IAAI,OAAO,CAC5BC,OAAO,SAAS;QAEpB,OAAO,IAAIC,QAAQ,CAACC,SAASC;YACzBJ,IAAI,KAAK,CAACK,CAAAA;gBACN,IAAIA,KAAK,YACLD,OAAOC;gBAGXF;YACJ;QACJ;IACJ;IAEO,aACHG,EAA6E,EACzE;QACJ,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC,cAAcA;IACnC;IAEO,UAAUT,MAAiB,EAAES,EAA0B,EAAQ;QAClET,OAAO,EAAE,CAAC,WAAWS;IACzB;IAEO,QAAQT,MAAiB,EAAES,EAA0C,EAAQ;QAChFT,OAAO,EAAE,CAAC,SAASS;IACvB;IAEO,QAAQT,MAAiB,EAAES,EAA0B,EAAQ;QAChET,OAAO,EAAE,CAAC,SAASS;IACvB;IAEO,KAAKT,MAAiB,EAAEU,IAAY,EAAiB;QACxD,OAAO,IAAIL,QAAQ,CAACC,SAASC;YACzBP,OAAO,IAAI,CAACU,MAAMF,CAAAA;gBACd,IAAIA,KAAK,YACLD,OAAOC;gBAGXF;YACJ;QACJ;IACJ;IAEO,MAAMN,MAAiB,EAAEW,IAAa,EAAEC,MAAe,EAAQ;QAClEZ,OAAO,KAAK,CAACW,MAAMC;IACvB;AACJ;AAIO,MAAMC,gBAAgBC,wBAAwB,oBAAoB,CAAC;IACtE,gBAAgBnB;IAChB,cAAc,EAAE;AACpB"}
@@ -1,15 +1,12 @@
1
1
  import { ConnectionRegistry } from "@webiny/api-websockets/exports/api.js";
2
- import { ProcessOwner } from "@webiny/event-handler-standalone";
3
2
  import { WebsocketsConnectionManager } from "./abstractions.js";
4
3
  export declare class ServerConnectionManagerImpl implements WebsocketsConnectionManager.Interface<unknown> {
5
4
  private readonly registry;
6
- private readonly owner;
7
5
  private readonly sockets;
8
6
  private readonly metadata;
9
- constructor(registry: ConnectionRegistry.Interface, owner: ProcessOwner.Interface);
7
+ constructor(registry: ConnectionRegistry.Interface);
10
8
  add(params: WebsocketsConnectionManager.AddParams<unknown>): Promise<void>;
11
9
  remove(connectionId: string): Promise<void>;
12
- removeOwnedHere(connectionIds: string[]): Promise<void>;
13
10
  getSocket(connectionId: string): unknown | undefined;
14
11
  getMetadata(connectionId: string): WebsocketsConnectionManager.ConnectionMetadata | undefined;
15
12
  updateLastSeen(connectionId: string): Promise<void>;
@@ -1,10 +1,8 @@
1
1
  import { ConnectionRegistry } from "@webiny/api-websockets/exports/api.js";
2
- import { ProcessOwner } from "@webiny/event-handler-standalone";
3
2
  import { WebsocketsConnectionManager } from "./abstractions.js";
4
3
  class ServerConnectionManagerImpl {
5
- constructor(registry, owner){
4
+ constructor(registry){
6
5
  this.registry = registry;
7
- this.owner = owner;
8
6
  this.sockets = new Map();
9
7
  this.metadata = new Map();
10
8
  }
@@ -22,8 +20,7 @@ class ServerConnectionManagerImpl {
22
20
  identity: params.identity,
23
21
  tenant: params.tenant,
24
22
  endpoint: params.endpoint,
25
- connectedOn: new Date(params.connectedAt).toISOString(),
26
- owner: this.owner.id
23
+ connectedOn: new Date(params.connectedAt).toISOString()
27
24
  });
28
25
  }
29
26
  async remove(connectionId) {
@@ -38,12 +35,6 @@ class ServerConnectionManagerImpl {
38
35
  throw err;
39
36
  }
40
37
  }
41
- async removeOwnedHere(connectionIds) {
42
- if (0 === connectionIds.length) return;
43
- const connections = await this.registry.listViaConnections(connectionIds);
44
- const owned = connections.filter((connection)=>connection.owner === this.owner.id);
45
- for (const connection of owned)await this.remove(connection.connectionId);
46
- }
47
38
  getSocket(connectionId) {
48
39
  return this.sockets.get(connectionId);
49
40
  }
@@ -70,8 +61,7 @@ class ServerConnectionManagerImpl {
70
61
  const ServerConnectionManager = WebsocketsConnectionManager.createImplementation({
71
62
  implementation: ServerConnectionManagerImpl,
72
63
  dependencies: [
73
- ConnectionRegistry,
74
- ProcessOwner
64
+ ConnectionRegistry
75
65
  ]
76
66
  });
77
67
  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 { 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 * For connections this process has no socket for. Only the process that holds a socket can tell\n * that it is gone, so a connection is removed only when this process is its owner. Another\n * process's connections are left alone: with several processes, a socket missing here is\n * usually open there. Rows without an owner are left to the heartbeat's stale cleanup.\n */\n public async removeOwnedHere(connectionIds: string[]): Promise<void> {\n if (connectionIds.length === 0) {\n return;\n }\n const connections = await this.registry.listViaConnections(connectionIds);\n const owned = connections.filter(connection => connection.owner === this.owner.id);\n for (const connection of owned) {\n await this.remove(connection.connectionId);\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, ProcessOwner]\n});\n"],"names":["ServerConnectionManagerImpl","registry","owner","Map","params","Date","connectionId","err","connectionIds","connections","owned","connection","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;IAQA,MAAa,gBAAgBC,aAAuB,EAAiB;QACjE,IAAIA,AAAyB,MAAzBA,cAAc,MAAM,EACpB;QAEJ,MAAMC,cAAc,MAAM,IAAI,CAAC,QAAQ,CAAC,kBAAkB,CAACD;QAC3D,MAAME,QAAQD,YAAY,MAAM,CAACE,CAAAA,aAAcA,WAAW,KAAK,KAAK,IAAI,CAAC,KAAK,CAAC,EAAE;QACjF,KAAK,MAAMA,cAAcD,MACrB,MAAM,IAAI,CAAC,MAAM,CAACC,WAAW,YAAY;IAEjD;IAEO,UAAUL,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,QAAQM,MAAc,EAAqB;QACpD,MAAMC,YAAY,IAAIR,KAAKA,KAAK,GAAG,KAAKO;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,gBAAgBnB;IAChB,cAAc;QAACoB;QAAoBC;KAAa;AACpD"}
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"}
@@ -19,7 +19,6 @@ 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
- removeOwnedHere(connectionIds: string[]): Promise<void>;
23
22
  getSocket(connectionId: string): TSocket | undefined;
24
23
  getMetadata(connectionId: string): IWebsocketsConnectionManagerMetadata | undefined;
25
24
  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 removeOwnedHere(connectionIds: 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":";AAiCO,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 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,13 +1,9 @@
1
1
  import type { WebsocketsConnectionManager } from "../connectionManager/abstractions.js";
2
- import type { WebsocketsServerAdapter } from "../adapter/abstractions.js";
3
2
  export declare class HeartbeatManager {
4
3
  private readonly connectionManager;
5
- private readonly adapter;
6
4
  private readonly interval;
7
- private readonly staleAfter;
8
5
  private timer;
9
- constructor(connectionManager: WebsocketsConnectionManager.Interface<unknown>, adapter: WebsocketsServerAdapter.Interface<unknown>, interval: number, staleAfter: number);
6
+ constructor(connectionManager: WebsocketsConnectionManager.Interface<unknown>, interval: number);
10
7
  start(): void;
11
8
  stop(): void;
12
- evictStale(): Promise<void>;
13
9
  }
@@ -1,15 +1,11 @@
1
- const STALE_CLOSE_CODE = 1001;
2
- const STALE_CLOSE_REASON = "Connection went stale.";
3
1
  class HeartbeatManager {
4
- constructor(connectionManager, adapter, interval, staleAfter){
2
+ constructor(connectionManager, interval){
5
3
  this.connectionManager = connectionManager;
6
- this.adapter = adapter;
7
4
  this.interval = interval;
8
- this.staleAfter = staleAfter;
9
5
  }
10
6
  start() {
11
7
  this.timer = setInterval(()=>{
12
- this.evictStale();
8
+ this.connectionManager.cleanup(5 * this.interval);
13
9
  }, this.interval);
14
10
  }
15
11
  stop() {
@@ -18,28 +14,6 @@ class HeartbeatManager {
18
14
  this.timer = void 0;
19
15
  }
20
16
  }
21
- async evictStale() {
22
- const sockets = new Map();
23
- for (const connectionId of this.connectionManager.getActiveConnectionIds()){
24
- const socket = this.connectionManager.getSocket(connectionId);
25
- sockets.set(connectionId, socket);
26
- }
27
- let evicted;
28
- try {
29
- evicted = await this.connectionManager.cleanup(this.staleAfter);
30
- } catch (error) {
31
- console.error("Failed to evict stale WebSocket connections:", error);
32
- return;
33
- }
34
- for (const connectionId of evicted){
35
- const socket = sockets.get(connectionId);
36
- if (void 0 !== socket) try {
37
- this.adapter.close(socket, STALE_CLOSE_CODE, STALE_CLOSE_REASON);
38
- } catch (error) {
39
- console.error(`Failed to close stale WebSocket "${connectionId}":`, error);
40
- }
41
- }
42
- }
43
17
  }
44
18
  export { HeartbeatManager };
45
19
 
@@ -1 +1 @@
1
- {"version":3,"file":"heartbeat/HeartbeatManager.js","sources":["../../src/heartbeat/HeartbeatManager.ts"],"sourcesContent":["import type { WebsocketsConnectionManager } from \"~/connectionManager/abstractions.js\";\nimport type { WebsocketsServerAdapter } from \"~/adapter/abstractions.js\";\n\nconst STALE_CLOSE_CODE = 1001;\nconst STALE_CLOSE_REASON = \"Connection went stale.\";\n\n/*\n * Evicts connections that have not been seen for `staleAfter` milliseconds: forgets them and closes\n * their sockets.\n *\n * `staleAfter` must sit well above the client's ping interval. The admin pings every 5 minutes and\n * browsers delay timers in hidden tabs by up to a minute, so a threshold equal to the ping interval\n * evicted healthy background tabs. Closing the socket is what lets the client notice and reconnect:\n * an evicted socket left open looks connected to the client but no longer receives anything.\n */\nexport class HeartbeatManager {\n private timer: ReturnType<typeof setInterval> | undefined;\n\n public constructor(\n private readonly connectionManager: WebsocketsConnectionManager.Interface<unknown>,\n private readonly adapter: WebsocketsServerAdapter.Interface<unknown>,\n private readonly interval: number,\n private readonly staleAfter: number\n ) {}\n\n public start(): void {\n this.timer = setInterval(() => {\n void this.evictStale();\n }, this.interval);\n }\n\n public stop(): void {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = undefined;\n }\n }\n\n public async evictStale(): Promise<void> {\n // Cleanup forgets the sockets it evicts, so take them before it runs.\n const sockets = new Map<string, unknown>();\n for (const connectionId of this.connectionManager.getActiveConnectionIds()) {\n const socket = this.connectionManager.getSocket(connectionId);\n sockets.set(connectionId, socket);\n }\n\n let evicted: string[];\n try {\n evicted = await this.connectionManager.cleanup(this.staleAfter);\n } catch (error) {\n console.error(\"Failed to evict stale WebSocket connections:\", error);\n return;\n }\n\n for (const connectionId of evicted) {\n const socket = sockets.get(connectionId);\n if (socket === undefined) {\n continue;\n }\n try {\n this.adapter.close(socket, STALE_CLOSE_CODE, STALE_CLOSE_REASON);\n } catch (error) {\n console.error(`Failed to close stale WebSocket \"${connectionId}\":`, error);\n }\n }\n }\n}\n"],"names":["STALE_CLOSE_CODE","STALE_CLOSE_REASON","HeartbeatManager","connectionManager","adapter","interval","staleAfter","setInterval","clearInterval","undefined","sockets","Map","connectionId","socket","evicted","error","console"],"mappings":"AAGA,MAAMA,mBAAmB;AACzB,MAAMC,qBAAqB;AAWpB,MAAMC;IAGT,YACqBC,iBAAiE,EACjEC,OAAmD,EACnDC,QAAgB,EAChBC,UAAkB,CACrC;aAJmBH,iBAAiB,GAAjBA;aACAC,OAAO,GAAPA;aACAC,QAAQ,GAARA;aACAC,UAAU,GAAVA;IAClB;IAEI,QAAc;QACjB,IAAI,CAAC,KAAK,GAAGC,YAAY;YAChB,IAAI,CAAC,UAAU;QACxB,GAAG,IAAI,CAAC,QAAQ;IACpB;IAEO,OAAa;QAChB,IAAI,IAAI,CAAC,KAAK,EAAE;YACZC,cAAc,IAAI,CAAC,KAAK;YACxB,IAAI,CAAC,KAAK,GAAGC;QACjB;IACJ;IAEA,MAAa,aAA4B;QAErC,MAAMC,UAAU,IAAIC;QACpB,KAAK,MAAMC,gBAAgB,IAAI,CAAC,iBAAiB,CAAC,sBAAsB,GAAI;YACxE,MAAMC,SAAS,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAACD;YAChDF,QAAQ,GAAG,CAACE,cAAcC;QAC9B;QAEA,IAAIC;QACJ,IAAI;YACAA,UAAU,MAAM,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU;QAClE,EAAE,OAAOC,OAAO;YACZC,QAAQ,KAAK,CAAC,gDAAgDD;YAC9D;QACJ;QAEA,KAAK,MAAMH,gBAAgBE,QAAS;YAChC,MAAMD,SAASH,QAAQ,GAAG,CAACE;YAC3B,IAAIC,AAAWJ,WAAXI,QAGJ,IAAI;gBACA,IAAI,CAAC,OAAO,CAAC,KAAK,CAACA,QAAQb,kBAAkBC;YACjD,EAAE,OAAOc,OAAO;gBACZC,QAAQ,KAAK,CAAC,CAAC,iCAAiC,EAAEJ,aAAa,EAAE,CAAC,EAAEG;YACxE;QACJ;IACJ;AACJ"}
1
+ {"version":3,"file":"heartbeat/HeartbeatManager.js","sources":["../../src/heartbeat/HeartbeatManager.ts"],"sourcesContent":["import type { WebsocketsConnectionManager } from \"~/connectionManager/abstractions.js\";\n\nexport class HeartbeatManager {\n private readonly connectionManager: WebsocketsConnectionManager.Interface<unknown>;\n private readonly interval: number;\n private timer: ReturnType<typeof setInterval> | undefined;\n\n public constructor(\n connectionManager: WebsocketsConnectionManager.Interface<unknown>,\n interval: number\n ) {\n this.connectionManager = connectionManager;\n this.interval = interval;\n }\n\n public start(): void {\n this.timer = setInterval(() => {\n this.connectionManager.cleanup(5 * this.interval);\n }, this.interval);\n }\n\n public stop(): void {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = undefined;\n }\n }\n}\n"],"names":["HeartbeatManager","connectionManager","interval","setInterval","clearInterval","undefined"],"mappings":"AAEO,MAAMA;IAKT,YACIC,iBAAiE,EACjEC,QAAgB,CAClB;QACE,IAAI,CAAC,iBAAiB,GAAGD;QACzB,IAAI,CAAC,QAAQ,GAAGC;IACpB;IAEO,QAAc;QACjB,IAAI,CAAC,KAAK,GAAGC,YAAY;YACrB,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,QAAQ;QACpD,GAAG,IAAI,CAAC,QAAQ;IACpB;IAEO,OAAa;QAChB,IAAI,IAAI,CAAC,KAAK,EAAE;YACZC,cAAc,IAAI,CAAC,KAAK;YACxB,IAAI,CAAC,KAAK,GAAGC;QACjB;IACJ;AACJ"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webiny/api-websockets-standalone",
3
- "version": "0.0.0-unstable.7c2def32a7",
3
+ "version": "0.0.0-unstable.e9333b3f0b",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,18 +16,17 @@
16
16
  },
17
17
  "license": "MIT",
18
18
  "dependencies": {
19
- "@webiny/api": "0.0.0-unstable.7c2def32a7",
20
- "@webiny/api-websockets": "0.0.0-unstable.7c2def32a7",
21
- "@webiny/event-handler-standalone": "0.0.0-unstable.7c2def32a7",
22
- "@webiny/feature": "0.0.0-unstable.7c2def32a7",
23
- "@webiny/plugins": "0.0.0-unstable.7c2def32a7",
24
- "@webiny/utils": "0.0.0-unstable.7c2def32a7",
19
+ "@webiny/api": "0.0.0-unstable.e9333b3f0b",
20
+ "@webiny/api-websockets": "0.0.0-unstable.e9333b3f0b",
21
+ "@webiny/feature": "0.0.0-unstable.e9333b3f0b",
22
+ "@webiny/plugins": "0.0.0-unstable.e9333b3f0b",
23
+ "@webiny/utils": "0.0.0-unstable.e9333b3f0b",
25
24
  "ws": "8.21.3",
26
25
  "zod": "4.4.3"
27
26
  },
28
27
  "devDependencies": {
29
28
  "@types/ws": "8.18.1",
30
- "@webiny/build-tools": "0.0.0-unstable.7c2def32a7",
29
+ "@webiny/build-tools": "0.0.0-unstable.e9333b3f0b",
31
30
  "rimraf": "6.1.3",
32
31
  "typescript": "7.0.2",
33
32
  "vitest": "4.1.11"
@@ -11,7 +11,6 @@ interface WebsocketsServerParams {
11
11
  connectionManager?: WebsocketsConnectionManager.Interface<unknown>;
12
12
  authenticate?: WebsocketsConnectionAuthenticator;
13
13
  heartbeatInterval: number;
14
- staleAfter: number;
15
14
  debug: boolean;
16
15
  port: number;
17
16
  host: string;
@@ -45,7 +44,6 @@ declare class WebsocketsServer implements IWebsocketsServer {
45
44
  * don't crash the server or the upgrade, the connection just ends up unaddressable.
46
45
  */
47
46
  private registerConnection;
48
- private removeConnection;
49
47
  private resolvePortFromExistingServer;
50
48
  private listen;
51
49
  private closeHttpServer;
@@ -8,8 +8,6 @@ const ANONYMOUS_IDENTITY = {
8
8
  displayName: "",
9
9
  type: ""
10
10
  };
11
- const DEFAULT_HEARTBEAT_INTERVAL = 60000;
12
- const DEFAULT_STALE_AFTER = 900000;
13
11
  const toHeaders = (raw)=>{
14
12
  const headers = {};
15
13
  for (const [key, value] of Object.entries(raw))if ("string" == typeof value) headers[key] = value;
@@ -29,7 +27,7 @@ class WebsocketsServer {
29
27
  this.debug = params.debug;
30
28
  this.requestedPort = params.port;
31
29
  this.requestedHost = params.host;
32
- if (params.connectionManager) this.heartbeat = new HeartbeatManager(params.connectionManager, params.adapter, params.heartbeatInterval, params.staleAfter);
30
+ if (params.connectionManager) this.heartbeat = new HeartbeatManager(params.connectionManager, params.heartbeatInterval);
33
31
  }
34
32
  async start() {
35
33
  this.adapter.start(this.httpServer);
@@ -43,13 +41,11 @@ class WebsocketsServer {
43
41
  async stop() {
44
42
  this.shuttingDown = true;
45
43
  this.heartbeat?.stop();
46
- const activeIds = this.connectionManager?.getActiveConnectionIds() ?? [];
47
- const removals = activeIds.map((connectionId)=>this.removeConnection(connectionId));
48
- const closing = this.adapter.stop();
49
- await Promise.all([
50
- closing,
51
- ...removals
52
- ]);
44
+ await this.adapter.stop();
45
+ if (this.connectionManager) {
46
+ const activeIds = this.connectionManager.getActiveConnectionIds();
47
+ for (const connectionId of activeIds)await this.connectionManager.remove(connectionId);
48
+ }
53
49
  if (this.ownsHttpServer) await this.closeHttpServer();
54
50
  if (this.debug) console.log("[WebsocketsServer] stopped");
55
51
  }
@@ -59,21 +55,13 @@ class WebsocketsServer {
59
55
  wireUpgradeHandler() {
60
56
  this.httpServer.on("upgrade", async (request, socket, head)=>{
61
57
  if (this.shuttingDown) return void socket.destroy();
62
- socket.on("error", (error)=>{
63
- console.error("WebSocket upgrade socket error:", error.message);
64
- });
65
- try {
66
- const decision = await this.upgradeHandler.shouldUpgrade(request);
67
- if (!decision.allowed) {
68
- socket.write(`HTTP/1.1 ${decision.statusCode} ${decision.reason}\r\n\r\n`);
69
- socket.destroy();
70
- return;
71
- }
72
- this.adapter.handleUpgrade(request, socket, head);
73
- } catch (error) {
74
- console.error("WebSocket upgrade failed:", error);
58
+ const decision = await this.upgradeHandler.shouldUpgrade(request);
59
+ if (!decision.allowed) {
60
+ socket.write(`HTTP/1.1 ${decision.statusCode} ${decision.reason}\r\n\r\n`);
75
61
  socket.destroy();
62
+ return;
76
63
  }
64
+ this.adapter.handleUpgrade(request, socket, head);
77
65
  });
78
66
  }
79
67
  wireConnectionHandler() {
@@ -83,7 +71,7 @@ class WebsocketsServer {
83
71
  const connectedAt = Date.now();
84
72
  const host = request.headers.host || "localhost";
85
73
  const headers = toHeaders(request.headers);
86
- const registration = this.registerConnection({
74
+ this.registerConnection({
87
75
  connectionId,
88
76
  socket,
89
77
  request,
@@ -100,13 +88,11 @@ class WebsocketsServer {
100
88
  } catch {
101
89
  return;
102
90
  }
103
- this.connectionManager?.updateLastSeen(connectionId).catch((error)=>{
104
- console.error(`Failed to update last seen for WebSocket connection "${connectionId}":`, error);
105
- });
91
+ this.connectionManager?.updateLastSeen(connectionId);
106
92
  });
107
93
  this.adapter.onClose(socket, ()=>{
108
94
  if (this.shuttingDown) return;
109
- registration.then(()=>this.removeConnection(connectionId));
95
+ this.connectionManager?.remove(connectionId);
110
96
  });
111
97
  this.adapter.onError(socket, (error)=>{
112
98
  console.error(`WebSocket error for connection "${connectionId}":`, error.message);
@@ -139,13 +125,6 @@ class WebsocketsServer {
139
125
  console.error(`Failed to register WebSocket connection "${connectionId}":`, error instanceof Error ? error.message : String(error));
140
126
  }
141
127
  }
142
- async removeConnection(connectionId) {
143
- try {
144
- await this.connectionManager?.remove(connectionId);
145
- } catch (error) {
146
- console.error(`Failed to remove WebSocket connection "${connectionId}":`, error);
147
- }
148
- }
149
128
  resolvePortFromExistingServer() {
150
129
  const address = this.httpServer.address();
151
130
  if (address && "string" != typeof address) {
@@ -182,8 +161,7 @@ const createWebsocketsServer = (params)=>{
182
161
  upgradeHandler: new DefaultUpgradeHandlerImpl(),
183
162
  connectionManager: params.connectionManager,
184
163
  authenticate: params.authenticate,
185
- heartbeatInterval: params.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL,
186
- staleAfter: params.staleAfter ?? DEFAULT_STALE_AFTER,
164
+ heartbeatInterval: params.heartbeatInterval ?? 60000,
187
165
  debug: params.debug ?? false,
188
166
  port: params.port ?? 0,
189
167
  host: params.host ?? "127.0.0.1"
@@ -198,8 +176,7 @@ const attachWebsocketsServer = (params)=>{
198
176
  upgradeHandler: new DefaultUpgradeHandlerImpl(),
199
177
  connectionManager: params.connectionManager,
200
178
  authenticate: params.authenticate,
201
- heartbeatInterval: params.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL,
202
- staleAfter: params.staleAfter ?? DEFAULT_STALE_AFTER,
179
+ heartbeatInterval: params.heartbeatInterval ?? 60000,
203
180
  debug: params.debug ?? false,
204
181
  port: 0,
205
182
  host: "127.0.0.1"
@@ -1 +1 @@
1
- {"version":3,"file":"server/WebsocketsServer.js","sources":["../../src/server/WebsocketsServer.ts"],"sourcesContent":["import { createServer } from \"node:http\";\nimport type { Server as HttpServer, IncomingMessage } from \"node:http\";\nimport type { Duplex } from \"node:stream\";\nimport { mdbid } from \"@webiny/utils\";\nimport type { WebsocketsServerAdapter } from \"~/adapter/abstractions.js\";\nimport type { WebsocketsUpgradeHandler } from \"~/upgradeHandler/abstractions.js\";\nimport type { WebsocketsConnectionManager } from \"~/connectionManager/abstractions.js\";\nimport type { ConnectionRegistry } from \"@webiny/api-websockets/exports/api.js\";\nimport { NodeWsAdapterImpl } from \"~/adapter/NodeWsAdapter.js\";\nimport { DefaultUpgradeHandlerImpl } from \"~/upgradeHandler/DefaultUpgradeHandler.js\";\nimport { HeartbeatManager } from \"~/heartbeat/HeartbeatManager.js\";\nimport type {\n CreateWebsocketsServerParams,\n AttachWebsocketsServerParams,\n IWebsocketsServer,\n WebsocketsConnectionAuthenticator\n} from \"./types.js\";\n\nconst ANONYMOUS_IDENTITY: ConnectionRegistry.Identity = { id: \"\", displayName: \"\", type: \"\" };\n\nconst DEFAULT_HEARTBEAT_INTERVAL = 60_000;\n// Three admin ping intervals (5 minutes each): one late ping is not enough to evict a live tab.\nconst DEFAULT_STALE_AFTER = 15 * 60_000;\n\nconst toHeaders = (raw: IncomingMessage[\"headers\"]): Record<string, string> => {\n const headers: Record<string, string> = {};\n for (const [key, value] of Object.entries(raw)) {\n if (typeof value === \"string\") {\n headers[key] = value;\n }\n }\n return headers;\n};\n\ninterface WebsocketsServerParams {\n httpServer: HttpServer;\n ownsHttpServer: boolean;\n adapter: WebsocketsServerAdapter.Interface<unknown>;\n upgradeHandler: WebsocketsUpgradeHandler.Interface;\n connectionManager?: WebsocketsConnectionManager.Interface<unknown>;\n authenticate?: WebsocketsConnectionAuthenticator;\n heartbeatInterval: number;\n staleAfter: number;\n debug: boolean;\n port: number;\n host: string;\n}\n\nclass WebsocketsServer implements IWebsocketsServer {\n private readonly httpServer: HttpServer;\n private readonly ownsHttpServer: boolean;\n private readonly adapter: WebsocketsServerAdapter.Interface<unknown>;\n private readonly upgradeHandler: WebsocketsUpgradeHandler.Interface;\n private readonly connectionManager: WebsocketsConnectionManager.Interface<unknown> | undefined;\n private readonly authenticate: WebsocketsConnectionAuthenticator | undefined;\n private readonly heartbeat: HeartbeatManager | undefined;\n private readonly debug: boolean;\n private readonly requestedPort: number;\n private readonly requestedHost: string;\n\n private shuttingDown: boolean = false;\n private endpoint: string = \"\";\n private resolvedPort: number = 0;\n\n public constructor(params: WebsocketsServerParams) {\n this.httpServer = params.httpServer;\n this.ownsHttpServer = params.ownsHttpServer;\n this.adapter = params.adapter;\n this.upgradeHandler = params.upgradeHandler;\n this.connectionManager = params.connectionManager;\n this.authenticate = params.authenticate;\n this.debug = params.debug;\n this.requestedPort = params.port;\n this.requestedHost = params.host;\n\n if (params.connectionManager) {\n this.heartbeat = new HeartbeatManager(\n params.connectionManager,\n params.adapter,\n params.heartbeatInterval,\n params.staleAfter\n );\n }\n }\n\n public async start(): Promise<void> {\n this.adapter.start(this.httpServer);\n this.wireUpgradeHandler();\n this.wireConnectionHandler();\n\n if (this.ownsHttpServer) {\n await this.listen();\n } else {\n this.resolvePortFromExistingServer();\n }\n\n this.heartbeat?.start();\n\n if (this.debug) {\n console.log(`[WebsocketsServer] started on ${this.endpoint}`);\n }\n }\n\n public async stop(): Promise<void> {\n this.shuttingDown = true;\n this.heartbeat?.stop();\n\n /*\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":";;;;;AAkBA,MAAMA,qBAAkD;IAAE,IAAI;IAAI,aAAa;IAAI,MAAM;AAAG;AAE5F,MAAMC,6BAA6B;AAEnC,MAAMC,sBAAsB;AAE5B,MAAMC,YAAY,CAACC;IACf,MAAMC,UAAkC,CAAC;IACzC,KAAK,MAAM,CAACC,KAAKC,MAAM,IAAIC,OAAO,OAAO,CAACJ,KACtC,IAAI,AAAiB,YAAjB,OAAOG,OACPF,OAAO,CAACC,IAAI,GAAGC;IAGvB,OAAOF;AACX;AAgBA,MAAMI;IAgBF,YAAmBC,MAA8B,CAAE;aAJ3C,YAAY,GAAY;aACxB,QAAQ,GAAW;aACnB,YAAY,GAAW;QAG3B,IAAI,CAAC,UAAU,GAAGA,OAAO,UAAU;QACnC,IAAI,CAAC,cAAc,GAAGA,OAAO,cAAc;QAC3C,IAAI,CAAC,OAAO,GAAGA,OAAO,OAAO;QAC7B,IAAI,CAAC,cAAc,GAAGA,OAAO,cAAc;QAC3C,IAAI,CAAC,iBAAiB,GAAGA,OAAO,iBAAiB;QACjD,IAAI,CAAC,YAAY,GAAGA,OAAO,YAAY;QACvC,IAAI,CAAC,KAAK,GAAGA,OAAO,KAAK;QACzB,IAAI,CAAC,aAAa,GAAGA,OAAO,IAAI;QAChC,IAAI,CAAC,aAAa,GAAGA,OAAO,IAAI;QAEhC,IAAIA,OAAO,iBAAiB,EACxB,IAAI,CAAC,SAAS,GAAG,IAAIC,iBACjBD,OAAO,iBAAiB,EACxBA,OAAO,OAAO,EACdA,OAAO,iBAAiB,EACxBA,OAAO,UAAU;IAG7B;IAEA,MAAa,QAAuB;QAChC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU;QAClC,IAAI,CAAC,kBAAkB;QACvB,IAAI,CAAC,qBAAqB;QAE1B,IAAI,IAAI,CAAC,cAAc,EACnB,MAAM,IAAI,CAAC,MAAM;aAEjB,IAAI,CAAC,6BAA6B;QAGtC,IAAI,CAAC,SAAS,EAAE;QAEhB,IAAI,IAAI,CAAC,KAAK,EACVE,QAAQ,GAAG,CAAC,CAAC,8BAA8B,EAAE,IAAI,CAAC,QAAQ,EAAE;IAEpE;IAEA,MAAa,OAAsB;QAC/B,IAAI,CAAC,YAAY,GAAG;QACpB,IAAI,CAAC,SAAS,EAAE;QAOhB,MAAMC,YAAY,IAAI,CAAC,iBAAiB,EAAE,4BAA4B,EAAE;QACxE,MAAMC,WAAWD,UAAU,GAAG,CAACE,CAAAA,eAAgB,IAAI,CAAC,gBAAgB,CAACA;QACrE,MAAMC,UAAU,IAAI,CAAC,OAAO,CAAC,IAAI;QACjC,MAAMC,QAAQ,GAAG,CAAC;YAACD;eAAYF;SAAS;QAExC,IAAI,IAAI,CAAC,cAAc,EACnB,MAAM,IAAI,CAAC,eAAe;QAG9B,IAAI,IAAI,CAAC,KAAK,EACVF,QAAQ,GAAG,CAAC;IAEpB;IAEO,OAAe;QAClB,OAAO,IAAI,CAAC,YAAY;IAC5B;IAEQ,qBAA2B;QAC/B,IAAI,CAAC,UAAU,CAAC,EAAE,CACd,WACA,OAAOM,SAA0BC,QAAgBC;YAC7C,IAAI,IAAI,CAAC,YAAY,EAAE,YACnBD,OAAO,OAAO;YAQlBA,OAAO,EAAE,CAAC,SAASE,CAAAA;gBACfT,QAAQ,KAAK,CAAC,mCAAmCS,MAAM,OAAO;YAClE;YAEA,IAAI;gBACA,MAAMC,WAAW,MAAM,IAAI,CAAC,cAAc,CAAC,aAAa,CAACJ;gBACzD,IAAI,CAACI,SAAS,OAAO,EAAE;oBACnBH,OAAO,KAAK,CAAC,CAAC,SAAS,EAAEG,SAAS,UAAU,CAAC,CAAC,EAAEA,SAAS,MAAM,CAAC,QAAQ,CAAC;oBACzEH,OAAO,OAAO;oBACd;gBACJ;gBAEA,IAAI,CAAC,OAAO,CAAC,aAAa,CAACD,SAASC,QAAQC;YAChD,EAAE,OAAOC,OAAO;gBACZT,QAAQ,KAAK,CAAC,6BAA6BS;gBAC3CF,OAAO,OAAO;YAClB;QACJ;IAER;IAEQ,wBAA8B;QAClC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,CAACA,QAAQD;YAC/B,IAAI,IAAI,CAAC,YAAY,EACjB;YAGJ,MAAMH,eAAeQ;YACrB,MAAMC,cAAcC,KAAK,GAAG;YAC5B,MAAMC,OAAOR,QAAQ,OAAO,CAAC,IAAI,IAAI;YACrC,MAAMb,UAAUF,UAAUe,QAAQ,OAAO;YAQzC,MAAMS,eAAe,IAAI,CAAC,kBAAkB,CAAC;gBACzCZ;gBACAI;gBACAD;gBACAM;gBACAE;gBACArB;YACJ;YAEA,IAAI,CAAC,OAAO,CAAC,SAAS,CAACc,QAAQS,CAAAA;gBAC3B,IAAI,IAAI,CAAC,YAAY,EACjB;gBAGJ,MAAMC,WAAW,IAAI,CAAC,iBAAiB,EAAE,YAAYd;gBACrD,IAAI,CAACc,UACD;gBAGJ,IAAI;oBACAC,KAAK,KAAK,CAACF,KAAK,QAAQ;gBAC5B,EAAE,OAAM;oBACJ;gBACJ;gBAEA,IAAI,CAAC,iBAAiB,EAAE,eAAeb,cAAc,MAAMM,CAAAA;oBACvDT,QAAQ,KAAK,CACT,CAAC,qDAAqD,EAAEG,aAAa,EAAE,CAAC,EACxEM;gBAER;YACJ;YAEA,IAAI,CAAC,OAAO,CAAC,OAAO,CAACF,QAAQ;gBACzB,IAAI,IAAI,CAAC,YAAY,EACjB;gBAMCQ,aAAa,IAAI,CAAC,IAAM,IAAI,CAAC,gBAAgB,CAACZ;YACvD;YAEA,IAAI,CAAC,OAAO,CAAC,OAAO,CAACI,QAAQE,CAAAA;gBACzBT,QAAQ,KAAK,CAAC,CAAC,gCAAgC,EAAEG,aAAa,EAAE,CAAC,EAAEM,MAAM,OAAO;YACpF;QACJ;IACJ;IAUA,MAAc,mBAAmBX,MAOhC,EAAiB;QACd,MAAM,EAAEK,YAAY,EAAEI,MAAM,EAAED,OAAO,EAAEM,WAAW,EAAEE,IAAI,EAAErB,OAAO,EAAE,GAAGK;QAEtE,IAAIqB,WAAwC/B;QAC5C,IAAIgC,SAAS;QAEb,IAAI;YACA,MAAMC,MAAM,IAAIC,IAAIhB,QAAQ,GAAG,IAAI,IAAI,CAAC,OAAO,EAAEQ,MAAM;YACvDM,SAASC,IAAI,YAAY,CAAC,GAAG,CAAC,aAAa;YAC3C,MAAME,QAAQF,IAAI,YAAY,CAAC,GAAG,CAAC,YAAY;YAE/C,IAAI,IAAI,CAAC,YAAY,IAAIE,OAAO;gBAC5B,MAAMC,gBAAgB,MAAM,IAAI,CAAC,YAAY,CAACD;gBAC9C,IAAIC,eAAe,IACfL,WAAWK;YAEnB;YAEA,MAAM,IAAI,CAAC,iBAAiB,EAAE,IAAI;gBAC9BrB;gBACAI;gBACA,UAAU,IAAI,CAAC,QAAQ;gBACvBY;gBACAC;gBACAR;gBACAE;gBACArB;YACJ;QACJ,EAAE,OAAOgB,OAAO;YACZT,QAAQ,KAAK,CACT,CAAC,yCAAyC,EAAEG,aAAa,EAAE,CAAC,EAC5DM,iBAAiBgB,QAAQhB,MAAM,OAAO,GAAGiB,OAAOjB;QAExD;IACJ;IAEA,MAAc,iBAAiBN,YAAoB,EAAiB;QAChE,IAAI;YACA,MAAM,IAAI,CAAC,iBAAiB,EAAE,OAAOA;QACzC,EAAE,OAAOM,OAAO;YACZT,QAAQ,KAAK,CAAC,CAAC,uCAAuC,EAAEG,aAAa,EAAE,CAAC,EAAEM;QAC9E;IACJ;IAEQ,gCAAsC;QAC1C,MAAMkB,UAAU,IAAI,CAAC,UAAU,CAAC,OAAO;QACvC,IAAIA,WAAW,AAAmB,YAAnB,OAAOA,SAAsB;YACxC,IAAI,CAAC,YAAY,GAAGA,QAAQ,IAAI;YAChC,IAAI,CAAC,QAAQ,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,YAAY,EAAE;QACzD;IACJ;IAEQ,SAAwB;QAC5B,OAAO,IAAItB,QAAQ,CAACuB,SAASC;YACzB,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE;gBAC3D,MAAMF,UAAU,IAAI,CAAC,UAAU,CAAC,OAAO;gBACvC,IAAI,CAACA,WAAW,AAAmB,YAAnB,OAAOA,SAAsB,YACzCE,OAAO,IAAIJ,MAAM;gBAGrB,IAAI,CAAC,YAAY,GAAGE,QAAQ,IAAI;gBAChC,IAAI,CAAC,QAAQ,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC,EAAE,IAAI,CAAC,YAAY,EAAE;gBACjEC;YACJ;QACJ;IACJ;IAEQ,kBAAiC;QACrC,OAAO,IAAIvB,QAAQ,CAACuB,SAASC;YACzB,IAAI,CAAC,UAAU,CAAC,KAAK,CAACC,CAAAA;gBAClB,IAAIA,KAAK,YACLD,OAAOC;gBAGXF;YACJ;QACJ;IACJ;AACJ;AAIO,MAAMG,yBAAyB,CAACjC;IACnC,MAAMkC,aAAaC;IACnB,MAAMC,SAAS,IAAIrC,iBAAiB;QAChCmC;QACA,gBAAgB;QAChB,SAAS,IAAIG;QACb,gBAAgB,IAAIC;QACpB,mBAAmBtC,OAAO,iBAAiB;QAC3C,cAAcA,OAAO,YAAY;QACjC,mBAAmBA,OAAO,iBAAiB,IAAIT;QAC/C,YAAYS,OAAO,UAAU,IAAIR;QACjC,OAAOQ,OAAO,KAAK,IAAI;QACvB,MAAMA,OAAO,IAAI,IAAI;QACrB,MAAMA,OAAO,IAAI,IAAI;IACzB;IACA,OAAOoC;AACX;AAEO,MAAMG,yBAAyB,CAACvC;IACnC,MAAMoC,SAAS,IAAIrC,iBAAiB;QAChC,YAAYC,OAAO,MAAM;QACzB,gBAAgB;QAChB,SAAS,IAAIqC;QACb,gBAAgB,IAAIC;QACpB,mBAAmBtC,OAAO,iBAAiB;QAC3C,cAAcA,OAAO,YAAY;QACjC,mBAAmBA,OAAO,iBAAiB,IAAIT;QAC/C,YAAYS,OAAO,UAAU,IAAIR;QACjC,OAAOQ,OAAO,KAAK,IAAI;QACvB,MAAM;QACN,MAAM;IACV;IACA,OAAOoC;AACX"}
1
+ {"version":3,"file":"server/WebsocketsServer.js","sources":["../../src/server/WebsocketsServer.ts"],"sourcesContent":["import { createServer } from \"node:http\";\nimport type { Server as HttpServer, IncomingMessage } from \"node:http\";\nimport type { Duplex } from \"node:stream\";\nimport { mdbid } from \"@webiny/utils\";\nimport type { WebsocketsServerAdapter } from \"~/adapter/abstractions.js\";\nimport type { WebsocketsUpgradeHandler } from \"~/upgradeHandler/abstractions.js\";\nimport type { WebsocketsConnectionManager } from \"~/connectionManager/abstractions.js\";\nimport type { ConnectionRegistry } from \"@webiny/api-websockets/exports/api.js\";\nimport { NodeWsAdapterImpl } from \"~/adapter/NodeWsAdapter.js\";\nimport { DefaultUpgradeHandlerImpl } from \"~/upgradeHandler/DefaultUpgradeHandler.js\";\nimport { HeartbeatManager } from \"~/heartbeat/HeartbeatManager.js\";\nimport type {\n CreateWebsocketsServerParams,\n AttachWebsocketsServerParams,\n IWebsocketsServer,\n WebsocketsConnectionAuthenticator\n} from \"./types.js\";\n\nconst ANONYMOUS_IDENTITY: ConnectionRegistry.Identity = { id: \"\", displayName: \"\", type: \"\" };\n\nconst toHeaders = (raw: IncomingMessage[\"headers\"]): Record<string, string> => {\n const headers: Record<string, string> = {};\n for (const [key, value] of Object.entries(raw)) {\n if (typeof value === \"string\") {\n headers[key] = value;\n }\n }\n return headers;\n};\n\ninterface WebsocketsServerParams {\n httpServer: HttpServer;\n ownsHttpServer: boolean;\n adapter: WebsocketsServerAdapter.Interface<unknown>;\n upgradeHandler: WebsocketsUpgradeHandler.Interface;\n connectionManager?: WebsocketsConnectionManager.Interface<unknown>;\n authenticate?: WebsocketsConnectionAuthenticator;\n heartbeatInterval: number;\n debug: boolean;\n port: number;\n host: string;\n}\n\nclass WebsocketsServer implements IWebsocketsServer {\n private readonly httpServer: HttpServer;\n private readonly ownsHttpServer: boolean;\n private readonly adapter: WebsocketsServerAdapter.Interface<unknown>;\n private readonly upgradeHandler: WebsocketsUpgradeHandler.Interface;\n private readonly connectionManager: WebsocketsConnectionManager.Interface<unknown> | undefined;\n private readonly authenticate: WebsocketsConnectionAuthenticator | undefined;\n private readonly heartbeat: HeartbeatManager | undefined;\n private readonly debug: boolean;\n private readonly requestedPort: number;\n private readonly requestedHost: string;\n\n private shuttingDown: boolean = false;\n private endpoint: string = \"\";\n private resolvedPort: number = 0;\n\n public constructor(params: WebsocketsServerParams) {\n this.httpServer = params.httpServer;\n this.ownsHttpServer = params.ownsHttpServer;\n this.adapter = params.adapter;\n this.upgradeHandler = params.upgradeHandler;\n this.connectionManager = params.connectionManager;\n this.authenticate = params.authenticate;\n this.debug = params.debug;\n this.requestedPort = params.port;\n this.requestedHost = params.host;\n\n if (params.connectionManager) {\n this.heartbeat = new HeartbeatManager(\n params.connectionManager,\n params.heartbeatInterval\n );\n }\n }\n\n public async start(): Promise<void> {\n this.adapter.start(this.httpServer);\n this.wireUpgradeHandler();\n this.wireConnectionHandler();\n\n if (this.ownsHttpServer) {\n await this.listen();\n } else {\n this.resolvePortFromExistingServer();\n }\n\n this.heartbeat?.start();\n\n if (this.debug) {\n console.log(`[WebsocketsServer] started on ${this.endpoint}`);\n }\n }\n\n public async stop(): Promise<void> {\n this.shuttingDown = true;\n this.heartbeat?.stop();\n\n await this.adapter.stop();\n\n if (this.connectionManager) {\n const activeIds = this.connectionManager.getActiveConnectionIds();\n for (const connectionId of activeIds) {\n await this.connectionManager.remove(connectionId);\n }\n }\n\n if (this.ownsHttpServer) {\n await this.closeHttpServer();\n }\n\n if (this.debug) {\n console.log(\"[WebsocketsServer] stopped\");\n }\n }\n\n public port(): number {\n return this.resolvedPort;\n }\n\n private wireUpgradeHandler(): void {\n this.httpServer.on(\n \"upgrade\",\n async (request: IncomingMessage, socket: Duplex, head: Buffer) => {\n if (this.shuttingDown) {\n socket.destroy();\n return;\n }\n\n const decision = await this.upgradeHandler.shouldUpgrade(request);\n if (!decision.allowed) {\n socket.write(`HTTP/1.1 ${decision.statusCode} ${decision.reason}\\r\\n\\r\\n`);\n socket.destroy();\n return;\n }\n\n this.adapter.handleUpgrade(request, socket, head);\n }\n );\n }\n\n private wireConnectionHandler(): void {\n this.adapter.onConnection((socket, request) => {\n if (this.shuttingDown) {\n return;\n }\n\n const connectionId = mdbid();\n const connectedAt = Date.now();\n const host = request.headers.host || \"localhost\";\n const headers = toHeaders(request.headers);\n\n // Register the connection under its real identity, decoded from the `?token` JWT. Both\n // the authentication and the registry write are async, so we kick this off without\n // awaiting it and wire the socket handlers below synchronously — that way we don't miss a\n // close or message that arrives during the gap. If a message does arrive before\n // registration finishes, the `getMetadata` guard below just drops it, which is fine since\n // the client doesn't send anything meaningful right at connect time.\n void this.registerConnection({\n connectionId,\n socket,\n request,\n connectedAt,\n host,\n headers\n });\n\n this.adapter.onMessage(socket, data => {\n if (this.shuttingDown) {\n return;\n }\n\n const metadata = this.connectionManager?.getMetadata(connectionId);\n if (!metadata) {\n return;\n }\n\n try {\n JSON.parse(data.toString());\n } catch {\n return;\n }\n\n this.connectionManager?.updateLastSeen(connectionId);\n });\n\n this.adapter.onClose(socket, () => {\n if (this.shuttingDown) {\n return;\n }\n this.connectionManager?.remove(connectionId);\n });\n\n this.adapter.onError(socket, error => {\n console.error(`WebSocket error for connection \"${connectionId}\":`, error.message);\n });\n });\n }\n\n /**\n * Authenticates the upgrade using the `?token`/`?tenant` query the app-websockets client sends,\n * then registers the live socket in the shared connection manager under the resolved identity and\n * tenant. Getting both right matters for targeted server→client sends: `SendToIdentity` finds\n * connections by identity id in the registry, so a connection that's anonymous (or never made it\n * into the registry) can't be reached. Everything here is guarded — if something goes wrong we\n * don't crash the server or the upgrade, the connection just ends up unaddressable.\n */\n private async registerConnection(params: {\n connectionId: string;\n socket: unknown;\n request: IncomingMessage;\n connectedAt: number;\n host: string;\n headers: Record<string, string>;\n }): Promise<void> {\n const { connectionId, socket, request, connectedAt, host, headers } = params;\n\n let identity: ConnectionRegistry.Identity = ANONYMOUS_IDENTITY;\n let tenant = \"\";\n\n try {\n const url = new URL(request.url ?? \"\", `http://${host}`);\n tenant = url.searchParams.get(\"tenant\") ?? \"\";\n const token = url.searchParams.get(\"token\") ?? \"\";\n\n if (this.authenticate && token) {\n const authenticated = await this.authenticate(token);\n if (authenticated?.id) {\n identity = authenticated;\n }\n }\n\n await this.connectionManager?.add({\n connectionId,\n socket,\n endpoint: this.endpoint,\n identity,\n tenant,\n connectedAt,\n host,\n headers\n });\n } catch (error) {\n console.error(\n `Failed to register WebSocket connection \"${connectionId}\":`,\n error instanceof Error ? error.message : String(error)\n );\n }\n }\n\n private resolvePortFromExistingServer(): void {\n const address = this.httpServer.address();\n if (address && typeof address !== \"string\") {\n this.resolvedPort = address.port;\n this.endpoint = `ws://127.0.0.1:${this.resolvedPort}`;\n }\n }\n\n private listen(): Promise<void> {\n return new Promise((resolve, reject) => {\n this.httpServer.listen(this.requestedPort, this.requestedHost, () => {\n const address = this.httpServer.address();\n if (!address || typeof address === \"string\") {\n reject(new Error(\"Unexpected server address format.\"));\n return;\n }\n this.resolvedPort = address.port;\n this.endpoint = `ws://${this.requestedHost}:${this.resolvedPort}`;\n resolve();\n });\n });\n }\n\n private closeHttpServer(): Promise<void> {\n return new Promise((resolve, reject) => {\n this.httpServer.close(err => {\n if (err) {\n reject(err);\n return;\n }\n resolve();\n });\n });\n }\n}\n\nexport { WebsocketsServer };\n\nexport const createWebsocketsServer = (params: CreateWebsocketsServerParams): IWebsocketsServer => {\n const httpServer = createServer();\n const server = new WebsocketsServer({\n httpServer,\n ownsHttpServer: true,\n adapter: new NodeWsAdapterImpl(),\n upgradeHandler: new DefaultUpgradeHandlerImpl(),\n connectionManager: params.connectionManager,\n authenticate: params.authenticate,\n heartbeatInterval: params.heartbeatInterval ?? 60_000,\n debug: params.debug ?? false,\n port: params.port ?? 0,\n host: params.host ?? \"127.0.0.1\"\n });\n return server;\n};\n\nexport const attachWebsocketsServer = (params: AttachWebsocketsServerParams): IWebsocketsServer => {\n const server = new WebsocketsServer({\n httpServer: params.server,\n ownsHttpServer: false,\n adapter: new NodeWsAdapterImpl(),\n upgradeHandler: new DefaultUpgradeHandlerImpl(),\n connectionManager: params.connectionManager,\n authenticate: params.authenticate,\n heartbeatInterval: params.heartbeatInterval ?? 60_000,\n debug: params.debug ?? false,\n port: 0,\n host: \"127.0.0.1\"\n });\n return server;\n};\n"],"names":["ANONYMOUS_IDENTITY","toHeaders","raw","headers","key","value","Object","WebsocketsServer","params","HeartbeatManager","console","activeIds","connectionId","request","socket","head","decision","mdbid","connectedAt","Date","host","data","metadata","JSON","error","identity","tenant","url","URL","token","authenticated","Error","String","address","Promise","resolve","reject","err","createWebsocketsServer","httpServer","createServer","server","NodeWsAdapterImpl","DefaultUpgradeHandlerImpl","attachWebsocketsServer"],"mappings":"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"}
package/server/types.d.ts CHANGED
@@ -20,11 +20,6 @@ export type WebsocketsConnectionAuthenticator = (token: string) => Promise<Conne
20
20
  interface BaseServerParams {
21
21
  plugins?: PluginsContainer | PluginCollection;
22
22
  heartbeatInterval?: number;
23
- /**
24
- * How long a connection may go without a message before it is closed. Keep it well above the
25
- * client's ping interval.
26
- */
27
- staleAfter?: number;
28
23
  debug?: boolean;
29
24
  connectionManager?: WebsocketsConnectionManager.Interface<unknown>;
30
25
  authenticate?: WebsocketsConnectionAuthenticator;
@@ -7,12 +7,11 @@ class ServerWebsocketsTransportImpl {
7
7
  this.adapter = adapter;
8
8
  }
9
9
  async send(connections, data) {
10
- const missing = [];
11
10
  for (const connection of connections){
12
11
  const { connectionId } = connection;
13
12
  const socket = this.connectionManager.getSocket(connectionId);
14
13
  if (void 0 === socket) {
15
- missing.push(connectionId);
14
+ await this.connectionManager.remove(connectionId);
16
15
  continue;
17
16
  }
18
17
  try {
@@ -22,15 +21,13 @@ class ServerWebsocketsTransportImpl {
22
21
  console.log(ex);
23
22
  }
24
23
  }
25
- await this.connectionManager.removeOwnedHere(missing);
26
24
  }
27
25
  async disconnect(connections) {
28
- const missing = [];
29
26
  for (const connection of connections){
30
27
  const { connectionId } = connection;
31
28
  const socket = this.connectionManager.getSocket(connectionId);
32
29
  if (void 0 === socket) {
33
- missing.push(connectionId);
30
+ await this.connectionManager.remove(connectionId);
34
31
  continue;
35
32
  }
36
33
  try {
@@ -41,7 +38,6 @@ class ServerWebsocketsTransportImpl {
41
38
  console.log(ex);
42
39
  }
43
40
  }
44
- await this.connectionManager.removeOwnedHere(missing);
45
41
  }
46
42
  }
47
43
  const ServerWebsocketsTransport = WebsocketsTransport.createImplementation({
@@ -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 // Connections with no socket in this process; see `removeOwnedHere`.\n const missing: string[] = [];\n for (const connection of connections) {\n const { connectionId } = connection;\n const socket = this.connectionManager.getSocket(connectionId);\n if (socket === undefined) {\n missing.push(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 await this.connectionManager.removeOwnedHere(missing);\n }\n\n public async disconnect(\n connections: WebsocketsTransport.DisconnectConnection[]\n ): Promise<void> {\n const missing: string[] = [];\n for (const connection of connections) {\n const { connectionId } = connection;\n const socket = this.connectionManager.getSocket(connectionId);\n if (socket === undefined) {\n missing.push(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 await this.connectionManager.removeOwnedHere(missing);\n }\n}\n\nexport const ServerWebsocketsTransport = WebsocketsTransport.createImplementation({\n implementation: ServerWebsocketsTransportImpl,\n dependencies: [WebsocketsConnectionManager, WebsocketsServerAdapter]\n});\n"],"names":["ServerWebsocketsTransportImpl","connectionManager","adapter","connections","data","missing","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;QAEb,MAAMC,UAAoB,EAAE;QAC5B,KAAK,MAAMC,cAAcH,YAAa;YAClC,MAAM,EAAEI,YAAY,EAAE,GAAGD;YACzB,MAAME,SAAS,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAACD;YAChD,IAAIC,AAAWC,WAAXD,QAAsB;gBACtBH,QAAQ,IAAI,CAACE;gBACb;YACJ;YACA,IAAI;gBACA,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAACC,QAAQE,KAAK,SAAS,CAACN;YACnD,EAAE,OAAOO,IAAI;gBACTC,QAAQ,KAAK,CACT,CAAC,sCAAsC,EAAEL,aAAa,mCAAmC,CAAC;gBAE9FK,QAAQ,GAAG,CAACD;YAChB;QACJ;QACA,MAAM,IAAI,CAAC,iBAAiB,CAAC,eAAe,CAACN;IACjD;IAEA,MAAa,WACTF,WAAuD,EAC1C;QACb,MAAME,UAAoB,EAAE;QAC5B,KAAK,MAAMC,cAAcH,YAAa;YAClC,MAAM,EAAEI,YAAY,EAAE,GAAGD;YACzB,MAAME,SAAS,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAACD;YAChD,IAAIC,AAAWC,WAAXD,QAAsB;gBACtBH,QAAQ,IAAI,CAACE;gBACb;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;QACA,MAAM,IAAI,CAAC,iBAAiB,CAAC,eAAe,CAACN;IACjD;AACJ;AAEO,MAAMQ,4BAA4BC,oBAAoB,oBAAoB,CAAC;IAC9E,gBAAgBd;IAChB,cAAc;QAACe;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 { 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"}