From 0a572a9528276eef6a2b413d342cfe529ef54c78 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 12 Jun 2026 12:54:49 +0200 Subject: [PATCH 1/2] Add lint rule to prevent ObservableScope resource leaks The rule of thumb to avoid resource leaks is that you should never call ObservableScope methods in a callback unless the ObservableScope is directly passed to or created inside that callback. I had a go at codifying this as a lint rule. --- .eslintrc.cjs | 6 +- eslint/NoObservableScopeLeak.js | 62 ++++++++ eslint/index.js | 5 + eslint/package.json | 4 + knip.ts | 2 +- package.json | 3 + pnpm-lock.yaml | 193 +++--------------------- src/state/media/MemberMediaViewModel.ts | 5 +- 8 files changed, 103 insertions(+), 177 deletions(-) create mode 100644 eslint/NoObservableScopeLeak.js create mode 100644 eslint/index.js create mode 100644 eslint/package.json diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 908981c52..bf494dcda 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details. `; module.exports = { - plugins: ["matrix-org", "rxjs", "jsdoc"], + plugins: ["matrix-org", "rxjs", "jsdoc", "element-call"], extends: [ "plugin:matrix-org/react", "plugin:matrix-org/a11y", @@ -27,6 +27,7 @@ module.exports = { node: true, }, rules: { + "element-call/no-observablescope-leak": "error", "jsdoc/no-types": "error", "jsdoc/empty-tags": "error", "jsdoc/check-property-names": "error", @@ -92,6 +93,9 @@ module.exports = { "**/test-**", ], rules: { + // Tests often initialize an ObservableScope in an outer scope in + // beforeEach, which is not actually a problem + "element-call/no-observablescope-leak": "off", "jsdoc/no-types": "off", "jsdoc/empty-tags": "off", "jsdoc/check-property-names": "off", diff --git a/eslint/NoObservableScopeLeak.js b/eslint/NoObservableScopeLeak.js new file mode 100644 index 000000000..f11bc256b --- /dev/null +++ b/eslint/NoObservableScopeLeak.js @@ -0,0 +1,62 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { ESLintUtils } from "@typescript-eslint/utils"; + +// These ObservableScope methods will not generally cause resource leaks even if +// called from a callback +const safeScopeMethods = ["bind", "end"]; + +const rule = ESLintUtils.RuleCreator( + () => "https://github.com/element-hq/element-call", +)({ + name: "no-observablescope-leak", + meta: { + type: "problem", + docs: { + description: + "Require referenced ObservableScopes to be defined in the very same scope to avoid resource leaks.", + }, + messages: { + scopeLeak: + "Do not reference ObservableScopes defined in an outer scope; this may create resource leaks.", + }, + schema: [], + }, + create(context) { + return { + Identifier(node) { + const scope = context.sourceCode.getScope(node); + if ( + // Is this a reference to a variable defined in an outer ("through") scope? + scope.through.some( + ({ identifier }) => identifier.name === node.name, + ) && + // Exclude calls to "safe" ObservableScope methods + node.parent?.type === "MemberExpression" && + node.parent.object === node && + node.parent.property.type === "Identifier" && + !safeScopeMethods.includes(node.parent.property.name) + ) { + // Verify that the variable is actually of type ObservableScope + // (expensive, so we check this last) + const services = ESLintUtils.getParserServices(context); + const type = services.getTypeAtLocation(node); + if (type.symbol?.name === "ObservableScope") + // This ObservableScope method call may be causing resource leaks. + context.report({ + messageId: "scopeLeak", + loc: node.loc, + node, + }); + } + }, + }; + }, +}); + +export default rule; diff --git a/eslint/index.js b/eslint/index.js new file mode 100644 index 000000000..26bd7f3e8 --- /dev/null +++ b/eslint/index.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + "no-observablescope-leak": require("./NoObservableScopeLeak").default, + }, +}; diff --git a/eslint/package.json b/eslint/package.json new file mode 100644 index 000000000..88c222308 --- /dev/null +++ b/eslint/package.json @@ -0,0 +1,4 @@ +{ + "name": "eslint-plugin-element-call", + "version": "0.0.0" +} diff --git a/knip.ts b/knip.ts index 3c06f97d4..a7e91d4ac 100644 --- a/knip.ts +++ b/knip.ts @@ -11,7 +11,7 @@ export default { vite: { config: ["vite.config.ts", "vite-embedded.config.ts", "vite-sdk.config.ts"], }, - entry: ["src/main.tsx", "i18next.config.ts"], + entry: ["src/main.tsx", "eslint/index.js", "i18next.config.ts"], ignoreBinaries: [ // This is deprecated, so Knip doesn't actually recognize it as a globally // installed binary. TODO We should switch to Compose v2: diff --git a/package.json b/package.json index 01f77d0ff..a8832734e 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.1", + "@types/eslint": "^9.6.1", "@types/grecaptcha": "^3.0.9", "@types/jsdom": "^21.1.7", "@types/lodash-es": "^4.17.12", @@ -80,6 +81,7 @@ "@types/sdp-transform": "^2.4.5", "@typescript-eslint/eslint-plugin": "^8.31.0", "@typescript-eslint/parser": "^8.31.0", + "@typescript-eslint/utils": "^8.61.0", "@use-gesture/react": "^10.2.11", "@vector-im/compound-design-tokens": "^10.0.0", "@vector-im/compound-web": "^9.3.0", @@ -94,6 +96,7 @@ "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^10.0.0", "eslint-plugin-deprecate": "^0.9.0", + "eslint-plugin-element-call": "link:eslint", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^61.5.0", "eslint-plugin-jsx-a11y": "^6.5.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 80d506058..70e6cd36b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,6 +105,9 @@ importers: '@testing-library/user-event': specifier: ^14.5.1 version: 14.6.1(@testing-library/dom@10.4.1) + '@types/eslint': + specifier: ^9.6.1 + version: 9.6.1 '@types/grecaptcha': specifier: ^3.0.9 version: 3.0.9 @@ -138,6 +141,9 @@ importers: '@typescript-eslint/parser': specifier: ^8.31.0 version: 8.60.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': + specifier: ^8.61.0 + version: 8.61.0(eslint@8.57.1)(typescript@5.9.3) '@use-gesture/react': specifier: ^10.2.11 version: 10.3.1(react@19.2.6) @@ -180,6 +186,9 @@ importers: eslint-plugin-deprecate: specifier: ^0.9.0 version: 0.9.0(eslint@8.57.1) + eslint-plugin-element-call: + specifier: link:eslint + version: link:eslint eslint-plugin-import: specifier: ^2.26.0 version: 2.32.0(@typescript-eslint/parser@8.60.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1) @@ -3710,6 +3719,9 @@ packages: '@types/dom-webcodecs@0.1.18': resolution: {integrity: sha512-vAvE8C9DGWR+tkb19xyjk1TSUlJ7RUzzp4a9Anu7mwBT+fpyePWK1UxmH14tMO5zHmrnrRIMg5NutnnDztLxgg==} + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -3805,24 +3817,12 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.58.2': - resolution: {integrity: sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.60.0': resolution: {integrity: sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.60.1': - resolution: {integrity: sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.61.0': resolution: {integrity: sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3833,40 +3833,20 @@ packages: resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@typescript-eslint/scope-manager@8.58.2': - resolution: {integrity: sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.60.0': resolution: {integrity: sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.60.1': - resolution: {integrity: sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.61.0': resolution: {integrity: sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.58.2': - resolution: {integrity: sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/tsconfig-utils@8.60.0': resolution: {integrity: sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/tsconfig-utils@8.60.1': - resolution: {integrity: sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/tsconfig-utils@8.61.0': resolution: {integrity: sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3884,10 +3864,6 @@ packages: resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@typescript-eslint/types@8.58.2': - resolution: {integrity: sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.60.0': resolution: {integrity: sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3909,24 +3885,12 @@ packages: typescript: optional: true - '@typescript-eslint/typescript-estree@8.58.2': - resolution: {integrity: sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/typescript-estree@8.60.0': resolution: {integrity: sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/typescript-estree@8.60.1': - resolution: {integrity: sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/typescript-estree@8.61.0': resolution: {integrity: sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3939,13 +3903,6 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - '@typescript-eslint/utils@8.58.2': - resolution: {integrity: sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.60.0': resolution: {integrity: sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3953,13 +3910,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.60.1': - resolution: {integrity: sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.61.0': resolution: {integrity: sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3971,18 +3921,10 @@ packages: resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@typescript-eslint/visitor-keys@8.58.2': - resolution: {integrity: sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.60.0': resolution: {integrity: sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.60.1': - resolution: {integrity: sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.61.0': resolution: {integrity: sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -10645,7 +10587,7 @@ snapshots: '@stylistic/eslint-plugin@3.1.0(eslint@8.57.1)(typescript@5.9.3)': dependencies: - '@typescript-eslint/utils': 8.58.2(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.61.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-visitor-keys: 4.2.1 espree: 10.4.0 @@ -10864,6 +10806,11 @@ snapshots: '@types/dom-webcodecs@0.1.18': {} + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + '@types/estree@1.0.8': {} '@types/estree@1.0.9': {} @@ -10965,28 +10912,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.58.2(typescript@5.9.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@5.9.3) - '@typescript-eslint/types': 8.58.2 - debug: 4.4.3 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/project-service@8.60.0(typescript@5.9.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@5.9.3) - '@typescript-eslint/types': 8.60.0 - debug: 4.4.3 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/project-service@8.60.1(typescript@5.9.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3) - '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/types': 8.61.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: @@ -11006,38 +10935,20 @@ snapshots: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - '@typescript-eslint/scope-manager@8.58.2': - dependencies: - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/visitor-keys': 8.58.2 - '@typescript-eslint/scope-manager@8.60.0': dependencies: '@typescript-eslint/types': 8.60.0 '@typescript-eslint/visitor-keys': 8.60.0 - '@typescript-eslint/scope-manager@8.60.1': - dependencies: - '@typescript-eslint/types': 8.60.1 - '@typescript-eslint/visitor-keys': 8.60.1 - '@typescript-eslint/scope-manager@8.61.0': dependencies: '@typescript-eslint/types': 8.61.0 '@typescript-eslint/visitor-keys': 8.61.0 - '@typescript-eslint/tsconfig-utils@8.58.2(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.60.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.60.1(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.61.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -11056,8 +10967,6 @@ snapshots: '@typescript-eslint/types@5.62.0': {} - '@typescript-eslint/types@8.58.2': {} - '@typescript-eslint/types@8.60.0': {} '@typescript-eslint/types@8.60.1': {} @@ -11078,21 +10987,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.58.2(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.58.2(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@5.9.3) - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/visitor-keys': 8.58.2 - debug: 4.4.3 - minimatch: 10.2.5 - semver: 7.8.1 - tinyglobby: 0.2.17 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/typescript-estree@8.60.0(typescript@5.9.3)': dependencies: '@typescript-eslint/project-service': 8.60.0(typescript@5.9.3) @@ -11108,21 +11002,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.60.1(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.60.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3) - '@typescript-eslint/types': 8.60.1 - '@typescript-eslint/visitor-keys': 8.60.1 - debug: 4.4.3 - minimatch: 10.2.5 - semver: 7.8.1 - tinyglobby: 0.2.17 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/typescript-estree@8.61.0(typescript@5.9.3)': dependencies: '@typescript-eslint/project-service': 8.61.0(typescript@5.9.3) @@ -11153,17 +11032,6 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@8.58.2(eslint@8.57.1)(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) - '@typescript-eslint/scope-manager': 8.58.2 - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@5.9.3) - eslint: 8.57.1 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/utils@8.60.0(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) @@ -11175,17 +11043,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.60.1(eslint@8.57.1)(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) - '@typescript-eslint/scope-manager': 8.60.1 - '@typescript-eslint/types': 8.60.1 - '@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3) - eslint: 8.57.1 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/utils@8.61.0(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) @@ -11202,21 +11059,11 @@ snapshots: '@typescript-eslint/types': 5.62.0 eslint-visitor-keys: 3.4.3 - '@typescript-eslint/visitor-keys@8.58.2': - dependencies: - '@typescript-eslint/types': 8.58.2 - eslint-visitor-keys: 5.0.1 - '@typescript-eslint/visitor-keys@8.60.0': dependencies: '@typescript-eslint/types': 8.60.0 eslint-visitor-keys: 5.0.1 - '@typescript-eslint/visitor-keys@8.60.1': - dependencies: - '@typescript-eslint/types': 8.60.1 - eslint-visitor-keys: 5.0.1 - '@typescript-eslint/visitor-keys@8.61.0': dependencies: '@typescript-eslint/types': 8.61.0 @@ -12446,7 +12293,7 @@ snapshots: eslint-plugin-storybook@10.4.1(eslint@8.57.1)(storybook@10.4.1(@testing-library/dom@10.4.1)(@types/react@19.2.15)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 8.60.1(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.61.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 storybook: 10.4.1(@testing-library/dom@10.4.1)(@types/react@19.2.15)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) transitivePeerDependencies: diff --git a/src/state/media/MemberMediaViewModel.ts b/src/state/media/MemberMediaViewModel.ts index 969da8996..a7f4612db 100644 --- a/src/state/media/MemberMediaViewModel.ts +++ b/src/state/media/MemberMediaViewModel.ts @@ -92,6 +92,7 @@ export function createMemberMedia( }: MemberMediaInputs, ): BaseMemberMediaViewModel { const trackBehavior$ = ( + scope: ObservableScope, source: Track.Source, ): Behavior => scope.behavior( @@ -102,8 +103,8 @@ export function createMemberMedia( ), ); - const audio$ = trackBehavior$(audioSource); - const video$ = trackBehavior$(videoSource); + const audio$ = trackBehavior$(scope, audioSource); + const video$ = trackBehavior$(scope, videoSource); return { ...createBaseMedia(inputs), From f78f507745f1a7b7dcf53c793b2d7ecd35bc12f9 Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 22 Jun 2026 10:53:08 +0200 Subject: [PATCH 2/2] Address remaining resource leak error --- src/state/CallViewModel/CallNotificationLifecycle.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/state/CallViewModel/CallNotificationLifecycle.ts b/src/state/CallViewModel/CallNotificationLifecycle.ts index 2100bde3e..4d7007b9e 100644 --- a/src/state/CallViewModel/CallNotificationLifecycle.ts +++ b/src/state/CallViewModel/CallNotificationLifecycle.ts @@ -161,6 +161,10 @@ export function createCallNotificationLifecycle$({ recipient, outcome$: race(timeout$, accept$, decline$).pipe( take(1), + // Make this observable 'hot' to avoid running multiple timers. This + // is not actually a resource leak since there will be at most one + // active ring attempt at any given time. + // eslint-disable-next-line element-call/no-observablescope-leak scope.share, ), });