diff --git a/package.json b/package.json index a67b8b0d8daa4..38a6f85d2e712 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,8 @@ "typedi@0.10.0": "patches/typedi@0.10.0.patch", "@sentry/cli@2.17.0": "patches/@sentry__cli@2.17.0.patch", "pkce-challenge@3.0.0": "patches/pkce-challenge@3.0.0.patch", - "pyodide@0.23.4": "patches/pyodide@0.23.4.patch" + "pyodide@0.23.4": "patches/pyodide@0.23.4.patch", + "@types/ws@8.5.4": "patches/@types__ws@8.5.4.patch" } } } diff --git a/packages/cli/src/push/websocket.push.ts b/packages/cli/src/push/websocket.push.ts index 93c9a24220759..46c38394df16f 100644 --- a/packages/cli/src/push/websocket.push.ts +++ b/packages/cli/src/push/websocket.push.ts @@ -1,12 +1,29 @@ import type WebSocket from 'ws'; import { AbstractPush } from './abstract.push'; +function heartbeat(this: WebSocket) { + this.isAlive = true; +} + export class WebSocketPush extends AbstractPush { + constructor() { + super(); + + // Ping all connected clients every 60 seconds + setInterval(() => this.pingAll(), 60 * 1000); + } + add(sessionId: string, connection: WebSocket) { + connection.isAlive = true; + connection.on('pong', heartbeat); + super.add(sessionId, connection); // Makes sure to remove the session if the connection is closed - connection.once('close', () => this.remove(sessionId)); + connection.once('close', () => { + connection.off('pong', heartbeat); + this.remove(sessionId); + }); } protected close(connection: WebSocket): void { @@ -16,4 +33,18 @@ export class WebSocketPush extends AbstractPush { protected sendToOne(connection: WebSocket, data: string): void { connection.send(data); } + + private pingAll() { + for (const sessionId in this.connections) { + const connection = this.connections[sessionId]; + // If a connection did not respond with a `PONG` in the last 60 seconds, disconnect + if (!connection.isAlive) { + delete this.connections[sessionId]; + return connection.terminate(); + } + + connection.isAlive = false; + connection.ping(); + } + } } diff --git a/patches/@types__ws@8.5.4.patch b/patches/@types__ws@8.5.4.patch new file mode 100644 index 0000000000000..b489d1e61843a --- /dev/null +++ b/patches/@types__ws@8.5.4.patch @@ -0,0 +1,14 @@ +diff --git a/index.d.ts b/index.d.ts +index 7a8182a94289524851cb08a3b24897f2b6bce747..f5bfb61bdacbae81ca274cc4b5a61e6e7322b7cd 100755 +--- a/index.d.ts ++++ b/index.d.ts +@@ -72,6 +72,9 @@ declare class WebSocket extends EventEmitter { + | typeof WebSocket.CLOSED; + readonly url: string; + ++ /** Indicates if the connection has replied to the last PING */ ++ isAlive: boolean; ++ + /** The connection is not yet open. */ + readonly CONNECTING: 0; + /** The connection is open and ready to communicate. */ \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 991d69345702b..784d8659853c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,6 +25,9 @@ patchedDependencies: '@sentry/cli@2.17.0': hash: nchnoezkq6p37qaiku3vrpwraq path: patches/@sentry__cli@2.17.0.patch + '@types/ws@8.5.4': + hash: nbzuqaoyqbrfwipijj5qriqqju + path: patches/@types__ws@8.5.4.patch pkce-challenge@3.0.0: hash: dypouzb3lve7vncq25i5fuanki path: patches/pkce-challenge@3.0.0.patch @@ -135,7 +138,7 @@ importers: dependencies: axios: specifier: ^0.21.1 - version: 0.21.4(debug@4.3.2) + version: 0.21.4 packages/@n8n_io/eslint-config: devDependencies: @@ -216,7 +219,7 @@ importers: version: 7.28.1 axios: specifier: ^0.21.1 - version: 0.21.4(debug@4.3.2) + version: 0.21.4 basic-auth: specifier: ^2.0.1 version: 2.0.1 @@ -547,7 +550,7 @@ importers: version: 13.7.7 '@types/ws': specifier: ^8.5.4 - version: 8.5.4 + version: 8.5.4(patch_hash=nbzuqaoyqbrfwipijj5qriqqju) '@types/xml2js': specifier: ^0.4.11 version: 0.4.11 @@ -574,7 +577,7 @@ importers: version: link:../@n8n/client-oauth2 axios: specifier: ^0.21.1 - version: 0.21.4(debug@4.3.2) + version: 0.21.4 concat-stream: specifier: ^2.0.0 version: 2.0.0 @@ -834,7 +837,7 @@ importers: version: 10.2.0(vue@3.3.4) axios: specifier: ^0.21.1 - version: 0.21.4(debug@4.3.2) + version: 0.21.4 codemirror-lang-html-n8n: specifier: ^1.0.0 version: 1.0.0 @@ -4454,7 +4457,7 @@ packages: dependencies: '@segment/loosely-validate-event': 2.0.0 auto-changelog: 1.16.4 - axios: 0.21.4(debug@4.3.2) + axios: 0.21.4 axios-retry: 3.3.1 bull: 3.29.3 lodash.clonedeep: 4.5.0 @@ -6676,11 +6679,12 @@ packages: '@types/webidl-conversions': 7.0.0 dev: false - /@types/ws@8.5.4: + /@types/ws@8.5.4(patch_hash=nbzuqaoyqbrfwipijj5qriqqju): resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==} dependencies: '@types/node': 18.16.16 dev: true + patched: true /@types/xml2js@0.4.11: resolution: {integrity: sha512-JdigeAKmCyoJUiQljjr7tQG3if9NkqGUgwEUqBvV0N7LM4HyQk7UXCnusRa1lnvXAEYJ8mw8GtZWioagNztOwA==} @@ -8084,19 +8088,18 @@ packages: is-retry-allowed: 2.2.0 dev: false - /axios@0.21.4(debug@4.3.2): + /axios@0.21.4: resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} dependencies: - follow-redirects: 1.15.2(debug@4.3.2) + follow-redirects: 1.15.2(debug@4.3.4) transitivePeerDependencies: - debug dev: false - /axios@0.27.2: - resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} + /axios@0.21.4(debug@4.3.2): + resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} dependencies: follow-redirects: 1.15.2(debug@4.3.2) - form-data: 4.0.0 transitivePeerDependencies: - debug dev: false @@ -8117,7 +8120,6 @@ packages: form-data: 4.0.0 transitivePeerDependencies: - debug - dev: true /babel-core@7.0.0-bridge.0(@babel/core@7.22.9): resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==} @@ -11698,7 +11700,6 @@ packages: optional: true dependencies: debug: 4.3.4(supports-color@8.1.1) - dev: true /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -17058,7 +17059,7 @@ packages: resolution: {integrity: sha512-aXYe/D+28kF63W8Cz53t09ypEORz+ULeDCahdAqhVrRm2scbOXFbtnn0GGhvMpYe45grepLKuwui9KxrZ2ZuMw==} engines: {node: '>=14.17.0'} dependencies: - axios: 0.27.2 + axios: 0.27.2(debug@4.3.4) transitivePeerDependencies: - debug dev: false