From cdb56f1619be4fc1698c4e0c3a701b65d85c79ae Mon Sep 17 00:00:00 2001 From: jevb Date: Sun, 29 Mar 2026 19:40:11 +0200 Subject: [PATCH] chore: ESLint config, 61 lint fixes across 22 client files, CLAUDE.md update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ESLint v9 flat config with typescript-eslint - Fix no-floating-promises, no-unused-vars, consistent-return across client - Refactor livekitSession: delegate entirely to AudioPipeline (1438→1171 lines) - Add 7 delete-account UI tests in settings-overlay.test.ts - Update CLAUDE.md with latest features and project structure - Update .gitignore --- .gitignore | 4 + Client/tauri-client/eslint.config.js | 80 + Client/tauri-client/package-lock.json | 1298 ++++++++++++++++- Client/tauri-client/package.json | 7 +- .../tauri-client/src/components/DmSidebar.ts | 1 - .../src/components/EmojiPicker.ts | 2 +- .../tauri-client/src/components/GifPicker.ts | 2 +- .../src/components/InviteManager.ts | 2 +- .../tauri-client/src/components/MemberList.ts | 2 + .../src/components/MessageList.ts | 4 +- .../src/components/PinnedMessages.ts | 1 - .../src/components/TypingIndicator.ts | 2 +- .../src/components/UpdateNotifier.ts | 2 +- .../src/components/VoiceWidget.ts | 2 +- .../src/components/message-list/fenwick.ts | 2 +- .../src/components/message-list/media.ts | 25 +- .../src/components/settings/AdvancedTab.ts | 21 +- Client/tauri-client/src/lib/deviceManager.ts | 2 +- Client/tauri-client/src/lib/livekitSession.ts | 340 +---- Client/tauri-client/src/lib/logPersistence.ts | 8 +- Client/tauri-client/src/lib/profiles.ts | 5 +- Client/tauri-client/src/lib/ws.ts | 9 +- Client/tauri-client/src/main.ts | 6 +- Client/tauri-client/src/pages/MainPage.ts | 10 +- .../src/pages/connect-page/LoginForm.ts | 40 +- .../src/pages/connect-page/ServerPanel.ts | 4 +- .../src/pages/main-page/ChatArea.ts | 2 +- .../src/pages/main-page/ChatHeader.ts | 2 +- .../src/pages/main-page/OverlayManagers.ts | 2 +- .../src/pages/main-page/SidebarArea.ts | 2 +- .../pages/main-page/VideoModeController.ts | 2 +- Client/tauri-client/src/stores/ui.store.ts | 2 +- .../tests/unit/settings-overlay.test.ts | 144 ++ 33 files changed, 1643 insertions(+), 394 deletions(-) create mode 100644 Client/tauri-client/eslint.config.js diff --git a/.gitignore b/.gitignore index 6baf5f32..9915d1ad 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,10 @@ Server/.claude/ # Claude Code skills skills/ +\ + +#github copilot +.github/ # AI-specific / internal planning docs Obsidian-Brain/ diff --git a/Client/tauri-client/eslint.config.js b/Client/tauri-client/eslint.config.js new file mode 100644 index 00000000..b667dbca --- /dev/null +++ b/Client/tauri-client/eslint.config.js @@ -0,0 +1,80 @@ +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + { + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + // --- Key rules from T-191 --- + "@typescript-eslint/no-floating-promises": "error", + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + "consistent-return": "error", + + // --- Relax rules that conflict with project style --- + // Project uses `any` sparingly with eslint-disable comments + "@typescript-eslint/no-explicit-any": "warn", + // Project uses non-null assertions intentionally + "@typescript-eslint/no-non-null-assertion": "off", + // Empty functions are used for no-op callbacks + "@typescript-eslint/no-empty-function": "off", + // Project uses void for fire-and-forget promises intentionally + "@typescript-eslint/no-misused-promises": [ + "error", + { checksVoidReturn: false }, + ], + // Allow require() in config files + "@typescript-eslint/no-require-imports": "off", + // Unbound methods used in singleton export pattern (bind at export) + "@typescript-eslint/unbound-method": "off", + // Allow unsafe member access on `any` — project narrows manually + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-return": "off", + // Redundant type constituents show up in union types with branded types + "@typescript-eslint/no-redundant-type-constituents": "off", + // Permissions use number bitmasks compared with enum values — intentional + "@typescript-eslint/no-unsafe-enum-comparison": "off", + // Interface-conforming async methods don't always need await + "@typescript-eslint/require-await": "off", + // Re-throwing with different message is a project pattern + "preserve-caught-error": "off", + // Promise rejection with string literals is used in some UI code + "@typescript-eslint/prefer-promise-reject-errors": "off", + }, + }, + { + // Test files get relaxed rules + files: ["tests/**/*.ts"], + rules: { + "@typescript-eslint/no-floating-promises": "off", + "@typescript-eslint/no-explicit-any": "off", + "consistent-return": "off", + }, + }, + { + ignores: [ + "dist/", + "src-tauri/", + "node_modules/", + "public/", + "*.js", + "*.cjs", + ], + }, +); diff --git a/Client/tauri-client/package-lock.json b/Client/tauri-client/package-lock.json index 928e163e..cb4e0337 100644 --- a/Client/tauri-client/package-lock.json +++ b/Client/tauri-client/package-lock.json @@ -1,12 +1,12 @@ { "name": "owncord-client", - "version": "1.2.0", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "owncord-client", - "version": "1.2.0", + "version": "1.3.0", "dependencies": { "@jitsi/rnnoise-wasm": "^0.2.1", "@tauri-apps/api": "^2.10.1", @@ -22,11 +22,14 @@ "livekit-client": "^2.17.3" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@playwright/test": "^1", "@tauri-apps/cli": "^2", "@vitest/coverage-v8": "^3", + "eslint": "^9.39.4", "jsdom": "^29.0.0", "typescript": "^5.7", + "typescript-eslint": "^8.57.2", "vite": "^6", "vitest": "^3" } @@ -767,6 +770,220 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@exodus/bytes": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", @@ -785,6 +1002,58 @@ } } }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1575,13 +1844,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/dom-mediacapture-record": { - "version": "1.0.22", - "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.22.tgz", - "integrity": "sha512-mUMZLK3NvwRLcAAT9qmcK+9p7tpU2FHdDsntR3YI4+GY88XrgG4XiE7u1Q2LAN2/FZOz/tdMDC3GQCR4T8nFuw==", - "license": "MIT", - "peer": true - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1589,6 +1851,256 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.57.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@vitest/coverage-v8": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", @@ -1738,6 +2250,46 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -1764,6 +2316,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1807,9 +2366,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1829,6 +2388,16 @@ "node": ">=8" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -1846,6 +2415,39 @@ "node": ">=18" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/check-error": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", @@ -1876,6 +2478,13 @@ "dev": true, "license": "MIT" }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1954,6 +2563,13 @@ "node": ">=6" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2030,6 +2646,207 @@ "@esbuild/win32-x64": "0.25.12" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -2040,6 +2857,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -2059,6 +2886,27 @@ "node": ">=12.0.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2077,6 +2925,57 @@ } } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -2131,6 +3030,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/glob/node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2139,9 +3051,9 @@ "license": "MIT" }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -2164,6 +3076,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2194,6 +3119,53 @@ "dev": true, "license": "MIT" }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2204,6 +3176,19 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -2304,6 +3289,19 @@ "dev": true, "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsdom": { "version": "29.0.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.0.tgz", @@ -2355,6 +3353,51 @@ "node": "20 || >=22" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/livekit-client": { "version": "2.17.3", "resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.17.3.tgz", @@ -2375,6 +3418,29 @@ "@types/dom-mediacapture-record": "^1" } }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/loglevel": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", @@ -2499,6 +3565,63 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -2506,6 +3629,19 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parse5": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", @@ -2519,6 +3655,16 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2571,9 +3717,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -2659,6 +3805,16 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2679,6 +3835,16 @@ "node": ">=0.10.0" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/rollup": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", @@ -2946,6 +4112,19 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-literal": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", @@ -3108,12 +4287,38 @@ "node": ">=20" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/typed-emitter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz", @@ -3137,6 +4342,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz", + "integrity": "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.2", + "@typescript-eslint/parser": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/undici": { "version": "7.24.3", "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.3.tgz", @@ -3147,6 +4376,16 @@ "node": ">=20.18.1" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/vite": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", @@ -3412,6 +4651,16 @@ "node": ">=8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -3526,6 +4775,19 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true, "license": "MIT" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/Client/tauri-client/package.json b/Client/tauri-client/package.json index ba98a7b3..0df6a671 100644 --- a/Client/tauri-client/package.json +++ b/Client/tauri-client/package.json @@ -16,14 +16,19 @@ "test:e2e:native": "playwright test --config playwright.config.native.ts", "test:e2e:ui": "playwright test --ui", "test:watch": "vitest", - "test:coverage": "vitest run --coverage" + "test:coverage": "vitest run --coverage", + "lint": "eslint src/", + "lint:fix": "eslint src/ --fix" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@playwright/test": "^1", "@tauri-apps/cli": "^2", "@vitest/coverage-v8": "^3", + "eslint": "^9.39.4", "jsdom": "^29.0.0", "typescript": "^5.7", + "typescript-eslint": "^8.57.2", "vite": "^6", "vitest": "^3" }, diff --git a/Client/tauri-client/src/components/DmSidebar.ts b/Client/tauri-client/src/components/DmSidebar.ts index af53f1f2..9c27182a 100644 --- a/Client/tauri-client/src/components/DmSidebar.ts +++ b/Client/tauri-client/src/components/DmSidebar.ts @@ -11,7 +11,6 @@ import { createElement, setText, - clearChildren, appendChildren, } from "@lib/dom"; import { createIcon } from "@lib/icons"; diff --git a/Client/tauri-client/src/components/EmojiPicker.ts b/Client/tauri-client/src/components/EmojiPicker.ts index 93da6514..741a02dc 100644 --- a/Client/tauri-client/src/components/EmojiPicker.ts +++ b/Client/tauri-client/src/components/EmojiPicker.ts @@ -1,7 +1,7 @@ // EmojiPicker — grid-based emoji selector with search and scrollable categories. // Uses @lib/dom helpers exclusively. Never sets innerHTML with user content. -import { createElement, setText, appendChildren, clearChildren } from "@lib/dom"; +import { createElement, setText, clearChildren } from "@lib/dom"; // --------------------------------------------------------------------------- // Types diff --git a/Client/tauri-client/src/components/GifPicker.ts b/Client/tauri-client/src/components/GifPicker.ts index 39d3db47..d84fab45 100644 --- a/Client/tauri-client/src/components/GifPicker.ts +++ b/Client/tauri-client/src/components/GifPicker.ts @@ -1,7 +1,7 @@ // GifPicker — searchable GIF selector powered by Tenor API. // Uses @lib/dom helpers exclusively. Never sets innerHTML with user content. -import { createElement, setText, appendChildren, clearChildren } from "@lib/dom"; +import { createElement, setText, clearChildren } from "@lib/dom"; import { searchGifs, getTrendingGifs } from "@lib/tenor"; import type { TenorGif } from "@lib/tenor"; diff --git a/Client/tauri-client/src/components/InviteManager.ts b/Client/tauri-client/src/components/InviteManager.ts index 13ba7ed4..6ad7fa27 100644 --- a/Client/tauri-client/src/components/InviteManager.ts +++ b/Client/tauri-client/src/components/InviteManager.ts @@ -3,7 +3,7 @@ * Create, copy, and revoke invite codes. */ -import { createElement, appendChildren, clearChildren, setText } from "@lib/dom"; +import { createElement, appendChildren, clearChildren } from "@lib/dom"; import { createIcon } from "@lib/icons"; import type { MountableComponent } from "@lib/safe-render"; diff --git a/Client/tauri-client/src/components/MemberList.ts b/Client/tauri-client/src/components/MemberList.ts index 78fdd9f4..4558a187 100644 --- a/Client/tauri-client/src/components/MemberList.ts +++ b/Client/tauri-client/src/components/MemberList.ts @@ -39,6 +39,7 @@ function statusPriority(status: UserStatus): number { case "idle": return 1; case "dnd": return 2; case "offline": return 3; + default: return 99; } } @@ -48,6 +49,7 @@ function statusColor(status: UserStatus): string { case "idle": return "var(--yellow)"; case "dnd": return "var(--red)"; case "offline": return "var(--text-micro)"; + default: return "#747f8d"; } } diff --git a/Client/tauri-client/src/components/MessageList.ts b/Client/tauri-client/src/components/MessageList.ts index 5409ec05..23b41fa8 100644 --- a/Client/tauri-client/src/components/MessageList.ts +++ b/Client/tauri-client/src/components/MessageList.ts @@ -459,7 +459,7 @@ export function createMessageList(options: MessageListOptions): MessageListCompo let scrollRafId = 0; let resizeRafId = 0; - let resizeDirty = false; + // resizeDirty tracking removed — resize observer batches via RAF directly function handleScroll(): void { if (root === null) return; @@ -519,12 +519,10 @@ export function createMessageList(options: MessageListOptions): MessageListCompo // Batched via RAF with anchor-based scroll preservation. const resizeObserver = new ResizeObserver(() => { if (root === null || contentContainer === null) return; - resizeDirty = true; if (resizeRafId !== 0) return; resizeRafId = requestAnimationFrame(() => { resizeRafId = 0; - resizeDirty = false; if (root === null || contentContainer === null) return; const atBottom = isNearBottom(); diff --git a/Client/tauri-client/src/components/PinnedMessages.ts b/Client/tauri-client/src/components/PinnedMessages.ts index cd5ba8fa..ae84ef82 100644 --- a/Client/tauri-client/src/components/PinnedMessages.ts +++ b/Client/tauri-client/src/components/PinnedMessages.ts @@ -5,7 +5,6 @@ import { createElement, - clearChildren, appendChildren, } from "@lib/dom"; import { createIcon } from "@lib/icons"; diff --git a/Client/tauri-client/src/components/TypingIndicator.ts b/Client/tauri-client/src/components/TypingIndicator.ts index a39198dc..8a8882b4 100644 --- a/Client/tauri-client/src/components/TypingIndicator.ts +++ b/Client/tauri-client/src/components/TypingIndicator.ts @@ -4,7 +4,7 @@ * Uses mockup's .typing-bar and .typing-dots classes. */ -import { createElement, appendChildren, setText, clearChildren } from "@lib/dom"; +import { createElement, appendChildren, clearChildren } from "@lib/dom"; import type { MountableComponent } from "@lib/safe-render"; import { Disposable } from "@lib/disposable"; import { membersStore, getTypingUsers } from "@stores/members.store"; diff --git a/Client/tauri-client/src/components/UpdateNotifier.ts b/Client/tauri-client/src/components/UpdateNotifier.ts index 37b4232f..8e937fba 100644 --- a/Client/tauri-client/src/components/UpdateNotifier.ts +++ b/Client/tauri-client/src/components/UpdateNotifier.ts @@ -27,7 +27,7 @@ export function createUpdateNotifier(options: UpdateNotifierOptions): MountableC showBanner(result.version, result.body ?? ""); } - function showBanner(version: string, notes: string): void { + function showBanner(version: string, _notes: string): void { if (container === null || banner !== null) return; banner = createElement("div", { class: "update-banner" }); diff --git a/Client/tauri-client/src/components/VoiceWidget.ts b/Client/tauri-client/src/components/VoiceWidget.ts index d11c9d7a..41b029c0 100644 --- a/Client/tauri-client/src/components/VoiceWidget.ts +++ b/Client/tauri-client/src/components/VoiceWidget.ts @@ -129,7 +129,7 @@ export function createVoiceWidget(options: VoiceWidgetOptions): MountableCompone if (statsPoller !== null) return; statsPoller = createConnectionStatsPoller(() => getRoomForStats()); statsUnlisten = statsPoller.onUpdate(updateSignalIcon); - qualityUnlisten = statsPoller.onQualityChanged((quality, prevQuality) => { + qualityUnlisten = statsPoller.onQualityChanged((quality, _prevQuality) => { // Auto-expand stats pane when quality degrades if ((quality === "poor" || quality === "bad") && statsPane !== null) { statsPane.classList.add("visible"); diff --git a/Client/tauri-client/src/components/message-list/fenwick.ts b/Client/tauri-client/src/components/message-list/fenwick.ts index ac8431b5..c086dc66 100644 --- a/Client/tauri-client/src/components/message-list/fenwick.ts +++ b/Client/tauri-client/src/components/message-list/fenwick.ts @@ -20,7 +20,7 @@ export class FenwickTree { if (delta === 0) return; this.values[i] = value; for (let x = i + 1; x <= this.size; x += x & (-x)) { - (this.tree as Float64Array)[x] = (this.tree[x] as number) + delta; + (this.tree)[x] = (this.tree[x] as number) + delta; } } diff --git a/Client/tauri-client/src/components/message-list/media.ts b/Client/tauri-client/src/components/message-list/media.ts index a78cf7c6..32a4f995 100644 --- a/Client/tauri-client/src/components/message-list/media.ts +++ b/Client/tauri-client/src/components/message-list/media.ts @@ -87,8 +87,9 @@ export function extractYouTubeId(url: string): string | null { return null; } -/** Cache for YouTube video titles to avoid re-fetching on every re-render. */ +/** Cache for YouTube video titles to avoid re-fetching on every re-render (LRU at 200). */ const ytTitleCache = new Map(); +const YT_TITLE_CACHE_MAX = 200; /** Strict pattern for YouTube video IDs (alphanumeric, hyphens, underscores). */ const YOUTUBE_ID_RE = /^[\w-]{1,20}$/; @@ -127,10 +128,18 @@ export function renderYouTubeEmbed(videoId: string, originalUrl: string): HTMLDi .then((res) => (res.ok ? (res.json() as Promise<{ title?: string } | null>) : null)) .then((data) => { const title = data?.title ?? "YouTube Video"; + if (ytTitleCache.size >= YT_TITLE_CACHE_MAX) { + const firstKey = ytTitleCache.keys().next().value; + if (firstKey !== undefined) ytTitleCache.delete(firstKey); + } ytTitleCache.set(videoId, title); setText(titleLink, title); }) .catch(() => { + if (ytTitleCache.size >= YT_TITLE_CACHE_MAX) { + const firstKey = ytTitleCache.keys().next().value; + if (firstKey !== undefined) ytTitleCache.delete(firstKey); + } ytTitleCache.set(videoId, "YouTube Video"); setText(titleLink, "YouTube Video"); }); @@ -208,7 +217,7 @@ export function renderInlineImage(url: string): HTMLDivElement { // Measure synchronously — deferring to rAF loses the race with // ResizeObserver which can rebuild the DOM before the rAF fires. img.addEventListener("load", () => { - log.info("Image loaded", { url: url.slice(0, 80), naturalW: (img as HTMLImageElement).naturalWidth, naturalH: (img as HTMLImageElement).naturalHeight }); + log.info("Image loaded", { url: url.slice(0, 80), naturalW: (img).naturalWidth, naturalH: (img).naturalHeight }); wrap.style.minHeight = ""; const h = wrap.offsetHeight; if (h > 0) cacheImageHeight(url, h); @@ -304,9 +313,7 @@ export function openImageLightbox(src: string, alt: string): void { function close(): void { overlay.remove(); - document.removeEventListener("keydown", onKey); - document.removeEventListener("mousemove", onMove); - document.removeEventListener("mouseup", onUp); + ac.abort(); if (activeLightboxClose === close) activeLightboxClose = null; } @@ -367,8 +374,10 @@ export function openImageLightbox(src: string, alt: string): void { } }); - document.addEventListener("mousemove", onMove); - document.addEventListener("mouseup", onUp); + // Use AbortController for cleanup of document-level listeners to prevent leaks + const ac = new AbortController(); + document.addEventListener("mousemove", onMove, { signal: ac.signal }); + document.addEventListener("mouseup", onUp, { signal: ac.signal }); closeBtn.addEventListener("click", (e) => { e.stopPropagation(); @@ -391,7 +400,7 @@ export function openImageLightbox(src: string, alt: string): void { } if (e.key === "0") resetZoom(); } - document.addEventListener("keydown", onKey); + document.addEventListener("keydown", onKey, { signal: ac.signal }); activeLightboxClose = close; document.body.appendChild(overlay); diff --git a/Client/tauri-client/src/components/settings/AdvancedTab.ts b/Client/tauri-client/src/components/settings/AdvancedTab.ts index bcf44b80..0205502d 100644 --- a/Client/tauri-client/src/components/settings/AdvancedTab.ts +++ b/Client/tauri-client/src/components/settings/AdvancedTab.ts @@ -69,7 +69,7 @@ export function buildAdvancedTab(signal: AbortSignal): HTMLDivElement { const devtoolsBtn = createElement("button", { class: "ac-btn" }, "Open DevTools"); devtoolsBtn.addEventListener("click", () => { void invoke("open_devtools").catch((err: unknown) => { - console.warn("DevTools not available:", err); + log.warn("DevTools not available", { error: err instanceof Error ? err.message : String(err) }); }); }, { signal }); @@ -134,7 +134,24 @@ export function buildAdvancedTab(signal: AbortSignal): HTMLDivElement { "Clear & Restart", signal, async (btn) => { - if (!confirm("This will clear all cached data and restart the app. Continue?")) return; + // Two-step confirmation: first click shows warning, second click confirms + if (btn.dataset.confirmPending !== "true") { + btn.dataset.confirmPending = "true"; + btn.textContent = "Are you sure? Click again"; + btn.classList.add("ac-btn-danger"); + const resetTimer = setTimeout(() => { + btn.dataset.confirmPending = ""; + btn.textContent = "Clear & Restart"; + btn.classList.remove("ac-btn-danger"); + }, 3000); + // Store timer ID so it can be cleared if the button is clicked again + btn.dataset.resetTimer = String(resetTimer); + return; + } + // Second click — clear the pending state and proceed + const pendingTimer = btn.dataset.resetTimer; + if (pendingTimer) clearTimeout(Number(pendingTimer)); + btn.dataset.confirmPending = ""; btn.textContent = "Clearing..."; btn.setAttribute("disabled", ""); try { diff --git a/Client/tauri-client/src/lib/deviceManager.ts b/Client/tauri-client/src/lib/deviceManager.ts index 3d11ca3e..70b8fe42 100644 --- a/Client/tauri-client/src/lib/deviceManager.ts +++ b/Client/tauri-client/src/lib/deviceManager.ts @@ -4,7 +4,7 @@ // after a device switch so the new source track flows through the GainNode. // Monitors navigator.mediaDevices.ondevicechange for hot-swap (unplug/plug). -import { Room, type LocalAudioTrack, Track } from "livekit-client"; +import { Room } from "livekit-client"; import { loadPref, savePref } from "@components/settings/helpers"; import { createLogger } from "@lib/logger"; import type { AudioPipeline } from "@lib/audioPipeline"; diff --git a/Client/tauri-client/src/lib/livekitSession.ts b/Client/tauri-client/src/lib/livekitSession.ts index 5df467f9..472d6113 100644 --- a/Client/tauri-client/src/lib/livekitSession.ts +++ b/Client/tauri-client/src/lib/livekitSession.ts @@ -11,7 +11,6 @@ import { type RemoteTrackPublication, type RemoteParticipant, type Participant, - type LocalAudioTrack, type LocalVideoTrack, type LocalTrack, type LocalTrackPublication, @@ -30,9 +29,8 @@ import { leaveVoiceChannel, setListenOnly, } from "@stores/voice.store"; -import { loadPref, savePref } from "@components/settings/helpers"; +import { loadPref } from "@components/settings/helpers"; import { createLogger } from "@lib/logger"; -import { createRNNoiseProcessor } from "@lib/noise-suppression"; import { invoke } from "@tauri-apps/api/core"; import { AudioPipeline } from "@lib/audioPipeline"; import { AudioElements } from "@lib/audioElements"; @@ -139,43 +137,6 @@ export class LiveKitSession { private manualCameraTrack: LocalVideoTrack | null = null; private manualScreenTracks: LocalTrack[] = []; - // --- Unified audio pipeline: input volume + VAD gating --- - // Pipeline: rawMicTrack → source → analyser (VAD reads here) - // → gainNode (volume × vadGate) → dest → WebRTC sender - private audioPipelineCtx: AudioContext | null = null; - private audioPipelineGain: GainNode | null = null; - private audioPipelineAnalyser: AnalyserNode | null = null; - private audioPipelineDest: MediaStreamAudioDestinationNode | null = null; - private vadTimer: ReturnType | null = null; - /** When true, mic is currently gated (muted by VAD — gain set to 0). */ - private vadGated = false; - /** The user's input volume gain (0-2.0). VAD multiplies this by 0 or 1. */ - private currentInputGain = 1.0; - - // --- RNNoise processor (LiveKit TrackProcessor API) --- - - /** Attach RNNoise processor to the local mic track. Safe to call if already attached. */ - private async applyNoiseSuppressor(): Promise { - if (this.room === null) return; - const micPub = this.room.localParticipant.getTrackPublication(Track.Source.Microphone); - if (micPub?.track === undefined) return; - if (micPub.track.getProcessor() !== undefined) return; - const processor = createRNNoiseProcessor(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- LocalTrack.setProcessor uses wide generic, but AudioProcessorOptions is guaranteed at runtime with webAudioMix - await micPub.track.setProcessor(processor as any); - log.info("RNNoise processor attached to mic track"); - } - - /** Remove RNNoise processor from the local mic track. Safe to call if none attached. */ - private async removeNoiseSuppressor(): Promise { - if (this.room === null) return; - const micPub = this.room.localParticipant.getTrackPublication(Track.Source.Microphone); - if (micPub?.track === undefined) return; - if (micPub.track.getProcessor() === undefined) return; - await micPub.track.stopProcessor(); - log.info("RNNoise processor removed from mic track"); - } - // --- Room factory --- private createRoom(): Room { @@ -356,7 +317,7 @@ export class LiveKitSession { const channelId = this.currentChannelId; const directUrl = this.lastDirectUrl; // Clean up current room without sending WS leave (we're reconnecting, not leaving). - this.teardownAudioPipeline(); + this._audioPipeline.teardownAudioPipeline(); this.removeAutoplayUnlock(); this.clearTokenRefreshTimer(); // Clear stale remote audio elements so reconnect doesn't leak DOM nodes. @@ -400,7 +361,7 @@ export class LiveKitSession { this.logIceConnectionInfo(); this.room.startAudio().catch((err) => log.debug("Failed to start audio after reconnect", err)); await this.restoreLocalVoiceState("reconnect"); - this.setupAudioPipeline(); + this._audioPipeline.setupAudioPipeline(); this.reapplyMuteGain(); this.startTokenRefreshTimer(); // Clear the abort controller after all post-connect work is done so @@ -552,7 +513,7 @@ export class LiveKitSession { ? "Published mic via LiveKit native capture" : "Auto-reconnect restored live microphone"); if (loadPref("enhancedNoiseSuppression", false)) { - await this.applyNoiseSuppressor(); + await this._audioPipeline.applyNoiseSuppressor(); } } setListenOnly(false); // Mic acquired successfully @@ -702,7 +663,7 @@ export class LiveKitSession { } // Set up unified audio pipeline (input volume + VAD gating via GainNode). // VAD polling only starts if saved sensitivity < 100. - this.setupAudioPipeline(); + this._audioPipeline.setupAudioPipeline(); this.reapplyMuteGain(); this.startTokenRefreshTimer(); log.info("Voice session active", { channelId }); @@ -739,9 +700,9 @@ export class LiveKitSession { setLocalMuted(false); log.info("Microphone permission granted — exited listen-only mode"); // Set up audio pipeline for the new mic track - this.setupAudioPipeline(); + this._audioPipeline.setupAudioPipeline(); if (loadPref("enhancedNoiseSuppression", false)) { - await this.applyNoiseSuppressor(); + await this._audioPipeline.applyNoiseSuppressor(); } } catch (err) { log.warn("Microphone retry failed — still in listen-only mode", err); @@ -756,7 +717,7 @@ export class LiveKitSession { this.reconnectAc = null; } this.clearTokenRefreshTimer(); - this.teardownAudioPipeline(); + this._audioPipeline.teardownAudioPipeline(); this.removeAutoplayUnlock(); this.pendingJoin = null; // Clean up manually published tracks. @@ -817,7 +778,7 @@ export class LiveKitSession { if (this.room === null) return; if (muted) { // Tear down pipeline first so it doesn't hold refs to the track - this.teardownAudioPipeline(); + this._audioPipeline.teardownAudioPipeline(); // Fully disable the mic — this unpublishes the track from the SFU await this.room.localParticipant.setMicrophoneEnabled(false); log.debug("Mic fully unpublished (muted)"); @@ -825,7 +786,7 @@ export class LiveKitSession { // Re-enable mic — this re-publishes the track to the SFU await this.room.localParticipant.setMicrophoneEnabled(true); // Rebuild the audio pipeline on the fresh track - this.setupAudioPipeline(); + this._audioPipeline.setupAudioPipeline(); log.debug("Mic re-published (unmuted)"); } } @@ -858,7 +819,7 @@ export class LiveKitSession { this.ws.send({ type: "voice_camera", payload: { enabled: true } }); // Re-apply audio pipeline — publishing a new track can trigger WebRTC // renegotiation which resets the mic sender, bypassing our GainNode mute. - this.setupAudioPipeline(); + this._audioPipeline.setupAudioPipeline(); this.reapplyMuteGain(); log.info("Camera enabled", { quality, maxBitrate: CAMERA_PUBLISH_BITRATES[quality] }); } catch (err) { @@ -885,7 +846,6 @@ export class LiveKitSession { } finally { setLocalCamera(false); if (this.ws !== null) this.ws.send({ type: "voice_camera", payload: { enabled: false } }); - this.onErrorCallback?.("Camera off"); log.info("Camera disabled"); } } @@ -895,7 +855,7 @@ export class LiveKitSession { const track = this.manualCameraTrack; this.manualCameraTrack = null; try { - this.room.localParticipant.unpublishTrack(track.mediaStreamTrack); + void this.room.localParticipant.unpublishTrack(track.mediaStreamTrack); } catch { /* already unpublished */ } track.stop(); } @@ -927,7 +887,7 @@ export class LiveKitSession { } this.ws.send({ type: "voice_screenshare", payload: { enabled: true } }); // Re-apply audio pipeline — same renegotiation risk as camera. - this.setupAudioPipeline(); + this._audioPipeline.setupAudioPipeline(); this.reapplyMuteGain(); log.info("Screenshare enabled", { quality, maxBitrate: SCREENSHARE_PUBLISH_BITRATES[quality] }); } catch (err) { @@ -950,7 +910,6 @@ export class LiveKitSession { } finally { setLocalScreenshare(false); if (this.ws !== null) this.ws.send({ type: "voice_screenshare", payload: { enabled: false } }); - this.onErrorCallback?.("Screen share ended"); log.info("Screenshare disabled"); } } @@ -961,7 +920,7 @@ export class LiveKitSession { this.manualScreenTracks = []; for (const track of tracks) { try { - this.room.localParticipant.unpublishTrack(track.mediaStreamTrack); + void this.room.localParticipant.unpublishTrack(track.mediaStreamTrack); } catch { /* already unpublished */ } track.stop(); } @@ -997,97 +956,7 @@ export class LiveKitSession { return this._audioElements.getScreenshareAudioMuted(userId); } - // ── Unified audio pipeline: input volume + VAD gating ───────────── - // - // Architecture: - // rawMicTrack → AudioContext source - // ├──→ AnalyserNode (VAD reads raw audio here — always sees real signal) - // └──→ GainNode (inputVolume × vadGate) → MediaStreamDestination → WebRTC sender - // - // The pipeline is always active while in a voice session. This avoids - // creating/destroying it when volume changes, and gives the VAD a stable - // analyser that's independent of LiveKit's track lifecycle. - - /** Build or rebuild the audio pipeline on the current mic track. */ - private setupAudioPipeline(): void { - this.teardownAudioPipeline(); - if (this.room === null) return; - const micPub = this.room.localParticipant.getTrackPublication(Track.Source.Microphone); - if (micPub?.track === undefined) return; - - try { - const mediaTrack = micPub.track.mediaStreamTrack; - const ctx = new AudioContext({ sampleRate: 48000 }); - void ctx.resume(); // Ensure not suspended (WebView2 autoplay policy) - - const source = ctx.createMediaStreamSource(new MediaStream([mediaTrack])); - - // Analyser: VAD reads time-domain data from here (always real audio) - const analyser = ctx.createAnalyser(); - analyser.fftSize = 2048; - analyser.smoothingTimeConstant = 0.3; - - // GainNode: controls both input volume and VAD gating - const gainNode = ctx.createGain(); - this.currentInputGain = loadPref("inputVolume", 100) / 100; - gainNode.gain.setValueAtTime(this.currentInputGain, ctx.currentTime); - - const dest = ctx.createMediaStreamDestination(); - - // Wire: source → analyser (tap) and source → gain → dest - source.connect(analyser); - source.connect(gainNode); - gainNode.connect(dest); - - this.audioPipelineCtx = ctx; - this.audioPipelineGain = gainNode; - this.audioPipelineAnalyser = analyser; - this.audioPipelineDest = dest; - - // Replace the WebRTC sender's track with the pipeline output - const adjustedTrack = dest.stream.getAudioTracks()[0]; - if (adjustedTrack !== undefined && micPub.track.sender) { - void micPub.track.sender.replaceTrack(adjustedTrack).catch((err) => { - log.warn("Failed to replace sender track with pipeline output", err); - }); - } - - log.info("Audio pipeline created", { inputGain: this.currentInputGain }); - - // Start VAD polling if sensitivity < 100 - this.startVadPolling(); - } catch (err) { - log.warn("Failed to set up audio pipeline", err); - } - } - - /** Tear down the audio pipeline and restore the original sender track. */ - private teardownAudioPipeline(): void { - this.stopVadPolling(); - - // Restore original mic track on the WebRTC sender - if (this.room !== null) { - const micPub = this.room.localParticipant.getTrackPublication(Track.Source.Microphone); - if (micPub?.track?.sender !== undefined) { - const originalTrack = micPub.track.mediaStreamTrack; - void micPub.track.sender.replaceTrack(originalTrack).catch((err) => log.debug("Failed to replace track during teardown", err)); - } - } - - if (this.audioPipelineGain !== null) { this.audioPipelineGain.disconnect(); this.audioPipelineGain = null; } - if (this.audioPipelineAnalyser !== null) { this.audioPipelineAnalyser.disconnect(); this.audioPipelineAnalyser = null; } - if (this.audioPipelineDest !== null) { this.audioPipelineDest.disconnect(); this.audioPipelineDest = null; } - if (this.audioPipelineCtx !== null) { void this.audioPipelineCtx.close(); this.audioPipelineCtx = null; } - this.vadGated = false; - } - - /** Update the effective gain on the pipeline (inputVolume × vadGate). - * The pipeline only exists when unmuted — muting tears it down entirely. */ - private updatePipelineGain(): void { - if (this.audioPipelineGain === null || this.audioPipelineCtx === null) return; - const effectiveGain = this.vadGated ? 0 : this.currentInputGain; - this.audioPipelineGain.gain.setTargetAtTime(effectiveGain, this.audioPipelineCtx.currentTime, 0.015); - } + // --- Audio pipeline delegates (all state lives in AudioPipeline) --- /** Re-apply mute/deafen state after events that may reset the audio pipeline. */ private reapplyMuteGain(): void { @@ -1098,156 +967,19 @@ export class LiveKitSession { } setInputVolume(volume: number): void { - const clamped = Math.max(0, Math.min(200, volume)); - savePref("inputVolume", clamped); - this.currentInputGain = clamped / 100; - this.updatePipelineGain(); + this._audioPipeline.setInputVolume(volume); } setOutputVolume(volume: number): void { this._audioElements.setOutputVolume(volume); } - /** - * Apply voice sensitivity as a client-side VAD gate. - * Sensitivity 0 = gate everything (threshold impossibly high). - * Sensitivity 100 = gate nothing (no VAD polling). - * VAD sets gain to 0 when gated, restores inputVolume when ungated. - */ setVoiceSensitivity(sensitivity: number): void { - const clamped = Math.max(0, Math.min(100, sensitivity)); - savePref("voiceSensitivity", clamped); - // Restart VAD polling with the new threshold (pipeline stays intact) - this.stopVadPolling(); - if (clamped >= 100) { - // Ensure ungated - if (this.vadGated) { this.vadGated = false; this.updatePipelineGain(); } - } else { - this.startVadPolling(); - } - log.debug("Voice sensitivity updated", { sensitivity: clamped }); + this._audioPipeline.setVoiceSensitivity(sensitivity); } - /** Start VAD polling loop — reads from the pipeline's analyser. */ - private startVadPolling(): void { - this.stopVadPolling(); - if (this.audioPipelineAnalyser === null) return; - - const sensitivity = loadPref("voiceSensitivity", 50); - if (sensitivity >= 100) return; - - // Convert sensitivity to an RMS threshold (time-domain): - // sensitivity 0 → threshold ~0.10, sensitivity 50 → ~0.05, sensitivity 99 → ~0.001 - const threshold = ((100 - sensitivity) / 100) * 0.10; - const analyser = this.audioPipelineAnalyser; - const dataArray = new Float32Array(analyser.fftSize); - - let silentFrames = 0; - let speechFrames = 0; - const GATE_ON_FRAMES = 12; // ~200ms of silence before gating - const GATE_OFF_FRAMES = 2; // ~33ms of speech before ungating - // Grace period: don't gate for the first ~500ms to let audio settle - let startupFrames = 0; - const STARTUP_GRACE = 30; - - // setTimeout instead of requestAnimationFrame: rAF pauses when the Tauri - // window is minimized/backgrounded, which freezes the VAD gate in whatever - // state it was in. setTimeout continues firing (throttled to ~1 Hz by some - // engines when hidden), which is still fast enough for VAD gate timing - // (200ms gate-on, 100ms gate-off). The CPU cost is negligible since - // getFloatTimeDomainData is a cheap memcpy from the audio thread. - const poll = (): void => { - if (this.audioPipelineAnalyser === null) return; - - analyser.getFloatTimeDomainData(dataArray); - let sum = 0; - for (let i = 0; i < dataArray.length; i++) { - const v = dataArray[i] ?? 0; - sum += v * v; - } - const rms = Math.sqrt(sum / dataArray.length); - - if (startupFrames < STARTUP_GRACE) { - startupFrames++; - this.vadTimer = setTimeout(poll, 16); - return; - } - - if (rms < threshold) { - speechFrames = 0; - silentFrames++; - if (!this.vadGated && silentFrames >= GATE_ON_FRAMES) { - this.vadGated = true; - this.updatePipelineGain(); // gain → 0 - } - } else { - silentFrames = 0; - speechFrames++; - if (this.vadGated && speechFrames >= GATE_OFF_FRAMES) { - this.vadGated = false; - this.updatePipelineGain(); // gain → inputVolume - } - } - - this.vadTimer = setTimeout(poll, 16); - }; - this.vadTimer = setTimeout(poll, 16); - log.info("VAD polling started", { sensitivity, threshold }); - } - - /** Stop VAD polling loop (pipeline stays intact). */ - private stopVadPolling(): void { - if (this.vadTimer !== null) { - clearTimeout(this.vadTimer); - this.vadTimer = null; - } - // Ungate if was gated - if (this.vadGated) { - this.vadGated = false; - this.updatePipelineGain(); - } - } - - /** - * Re-apply audio processing settings (echo cancellation, noise suppression, AGC) - * to the live mic track by restarting it with updated constraints. - */ async reapplyAudioProcessing(): Promise { - if (this.room === null) { - log.debug("Skipping audio processing reapply — no active voice session"); - return; - } - const micPub = this.room.localParticipant.getTrackPublication(Track.Source.Microphone); - if (micPub?.track === undefined) { - log.debug("Skipping audio processing reapply — no mic track"); - return; - } - - const captureOptions = { - echoCancellation: loadPref("echoCancellation", true), - noiseSuppression: loadPref("noiseSuppression", true), - autoGainControl: loadPref("autoGainControl", true), - }; - - try { - // restartTrack re-acquires the mic with new constraints without unpublishing - await (micPub.track as LocalAudioTrack).restartTrack(captureOptions); - log.info("Audio processing reapplied via restartTrack", captureOptions); - - // Rebuild audio pipeline (underlying track changed) - this.setupAudioPipeline(); - - // Re-apply or remove RNNoise processor - const enhancedNS = loadPref("enhancedNoiseSuppression", false); - if (enhancedNS) { - await this.applyNoiseSuppressor(); - } else { - await this.removeNoiseSuppressor(); - } - } catch (err) { - log.error("Failed to reapply audio processing", err); - this.onErrorCallback?.("Failed to update audio settings"); - } + return this._audioPipeline.reapplyAudioProcessing(this.onErrorCallback ?? undefined); } getLocalCameraStream(): MediaStream | null { @@ -1293,11 +1025,11 @@ export class LiveKitSession { hasRNNoiseProcessor: this.room.localParticipant.getTrackPublication(Track.Source.Microphone)?.track?.getProcessor() !== undefined, currentChannelId: this.currentChannelId, outputVolumeMultiplier: this.outputVolumeMultiplier, - audioPipelineActive: this.audioPipelineGain !== null, - audioPipelineGain: this.audioPipelineGain?.gain.value ?? null, - audioPipelineCtxState: this.audioPipelineCtx?.state ?? null, - vadGated: this.vadGated, - currentInputGain: this.currentInputGain, + audioPipelineActive: this._audioPipeline.isActive, + audioPipelineGain: this._audioPipeline.gainValue, + audioPipelineCtxState: this._audioPipeline.ctxState, + vadGated: this._audioPipeline.isVadGated, + currentInputGain: this._audioPipeline.inputGain, localParticipant: this.room.localParticipant.identity, localTracks, remoteParticipants, iceConnectionState: this.getIceConnectionState(), @@ -1310,12 +1042,14 @@ export class LiveKitSession { // Access the underlying RTCPeerConnection via LiveKit's engine. // LiveKit exposes the PeerConnection via room.engine.subscriber/publisher. try { - const engine = (this.room as any).engine; + const engine = (this.room as unknown as Record).engine as Record | undefined; if (!engine) return; + const subscriber = engine.subscriber as Record | undefined; + const publisher = engine.publisher as Record | undefined; const pcs: Array<{ label: string; pc: RTCPeerConnection }> = []; - if (engine.subscriber?.pc) pcs.push({ label: "subscriber", pc: engine.subscriber.pc }); - if (engine.publisher?.pc) pcs.push({ label: "publisher", pc: engine.publisher.pc }); + if (subscriber?.pc) pcs.push({ label: "subscriber", pc: subscriber.pc as RTCPeerConnection }); + if (publisher?.pc) pcs.push({ label: "publisher", pc: publisher.pc as RTCPeerConnection }); for (const { label, pc } of pcs) { log.info(`ICE ${label} connection state`, { @@ -1365,19 +1099,23 @@ export class LiveKitSession { private getIceConnectionState(): Record | null { if (this.room === null) return null; try { - const engine = (this.room as any).engine; + const engine = (this.room as unknown as Record).engine as Record | undefined; if (!engine) return null; + const subscriber = engine.subscriber as Record | undefined; + const publisher = engine.publisher as Record | undefined; const result: Record = {}; - if (engine.subscriber?.pc) { + if (subscriber?.pc) { + const pc = subscriber.pc as RTCPeerConnection; result.subscriber = { - iceConnectionState: engine.subscriber.pc.iceConnectionState, - connectionState: engine.subscriber.pc.connectionState, + iceConnectionState: pc.iceConnectionState, + connectionState: pc.connectionState, }; } - if (engine.publisher?.pc) { + if (publisher?.pc) { + const pc = publisher.pc as RTCPeerConnection; result.publisher = { - iceConnectionState: engine.publisher.pc.iceConnectionState, - connectionState: engine.publisher.pc.connectionState, + iceConnectionState: pc.iceConnectionState, + connectionState: pc.connectionState, }; } return result; diff --git a/Client/tauri-client/src/lib/logPersistence.ts b/Client/tauri-client/src/lib/logPersistence.ts index 3beb060d..e2082904 100644 --- a/Client/tauri-client/src/lib/logPersistence.ts +++ b/Client/tauri-client/src/lib/logPersistence.ts @@ -49,7 +49,7 @@ async function flushBuffer(): Promise { buffer = []; try { - const filePath = logFilePath(logDir, currentDate!); + const filePath = logFilePath(logDir, currentDate); await writeTextFile(filePath, lines, { append: true }); } catch (err) { // Log persistence failure shouldn't crash the app. @@ -62,7 +62,7 @@ function scheduleFlush(): void { if (flushTimer !== null) return; flushTimer = setTimeout(() => { flushTimer = null; - flushBuffer(); + void flushBuffer(); }, 2000); } @@ -76,7 +76,7 @@ async function rotateOldFiles(): Promise { (e) => e.name?.endsWith(".jsonl") && !e.isDirectory, ) - .map((e) => e.name!) + .map((e) => e.name) .sort(); if (jsonlFiles.length > MAX_LOG_FILES) { @@ -174,7 +174,7 @@ export async function readAllPersistedLogs(): Promise { (e) => e.name?.endsWith(".jsonl") && !e.isDirectory, ) - .map((e) => e.name!) + .map((e) => e.name) .sort(); const parts: string[] = []; diff --git a/Client/tauri-client/src/lib/profiles.ts b/Client/tauri-client/src/lib/profiles.ts index 7a9ce517..0d3856c0 100644 --- a/Client/tauri-client/src/lib/profiles.ts +++ b/Client/tauri-client/src/lib/profiles.ts @@ -110,10 +110,7 @@ export function createTauriBackend(): PersistenceBackend { return { async load(): Promise { const { invoke } = await import("@tauri-apps/api/core"); - const settings = (await invoke("get_settings")) as Record< - string, - unknown - >; + const settings = await invoke>("get_settings"); const raw = settings[STORAGE_KEY]; if (raw === undefined || raw === null) return null; if (isValidStoredData(raw)) return raw; diff --git a/Client/tauri-client/src/lib/ws.ts b/Client/tauri-client/src/lib/ws.ts index 01d02714..639c24f3 100644 --- a/Client/tauri-client/src/lib/ws.ts +++ b/Client/tauri-client/src/lib/ws.ts @@ -147,7 +147,7 @@ export function createWsClient() { setState("reconnecting"); reconnectTimer = setTimeout(() => { reconnectAttempt++; - connect(config!); + void connect(config!); }, delay); } @@ -244,8 +244,8 @@ export function createWsClient() { } for (const listener of typeListeners) { try { - (listener as WsListener)( - msg.payload as Extract["payload"], + (listener)( + msg.payload, msg.id, ); } catch (err) { @@ -280,7 +280,8 @@ export function createWsClient() { replayDedup = new Set(); } setState("authenticating"); - send({ type: "auth", payload: { token: config!.token, last_seq: lastSeq } }); + if (config === null) return; + send({ type: "auth", payload: { token: config.token, last_seq: lastSeq } }); } else if (rustState === "closed") { proxyOpen = false; log.info("WebSocket closed", { diff --git a/Client/tauri-client/src/main.ts b/Client/tauri-client/src/main.ts index 7d238154..31d9279b 100644 --- a/Client/tauri-client/src/main.ts +++ b/Client/tauri-client/src/main.ts @@ -11,7 +11,7 @@ import { createRouter } from "@lib/router"; import { createApiClient } from "@lib/api"; import { createWsClient } from "@lib/ws"; import { wireDispatcher } from "@lib/dispatcher"; -import { authStore, setAuth, clearAuth } from "@stores/auth.store"; +import { authStore, clearAuth } from "@stores/auth.store"; import { setTransientError } from "@stores/ui.store"; import { voiceStore, leaveVoiceChannel } from "@stores/voice.store"; import { leaveVoice as voiceSessionLeave } from "@lib/livekitSession"; @@ -51,10 +51,10 @@ document.addEventListener("keydown", (e) => { // Open external links (target="_blank") in the user's default browser. document.addEventListener("click", (e) => { - const link = (e.target as HTMLElement).closest("a[target='_blank']") as HTMLAnchorElement | null; + const link = (e.target as HTMLElement).closest("a[target='_blank']"); if (link === null) return; e.preventDefault(); - const href = link.href; + const href = (link as HTMLAnchorElement).href; if (href && (href.startsWith("http://") || href.startsWith("https://"))) { void openUrl(href); } diff --git a/Client/tauri-client/src/pages/MainPage.ts b/Client/tauri-client/src/pages/MainPage.ts index 633da580..21b9d521 100644 --- a/Client/tauri-client/src/pages/MainPage.ts +++ b/Client/tauri-client/src/pages/MainPage.ts @@ -22,7 +22,6 @@ import { channelsStore, getActiveChannel } from "@stores/channels.store"; import { dmStore } from "@stores/dm.store"; import { voiceStore } from "@stores/voice.store"; import { - leaveVoice as voiceSessionLeave, cleanupAll as voiceCleanupAll, setOnRemoteVideo, setOnRemoteVideoRemoved, @@ -30,7 +29,6 @@ import { setWsClient, setServerHost as setLiveKitServerHost, setOnError as setVoiceOnError, - clearOnError as clearVoiceOnError, } from "@lib/livekitSession"; import { setServerHost } from "@components/message-list/renderers"; import { createQuickSwitcherManager } from "./main-page/OverlayManagers"; @@ -279,9 +277,9 @@ export function createMainPage(options: MainPageOptions): MountableComponent { channelCtrl = createChannelController({ ws, api, - msgCtrl: msgCtrl!, + msgCtrl: msgCtrl, pendingDeleteManager, - reactionCtrl: reactionCtrl!, + reactionCtrl: reactionCtrl, typingLimiter: limiters.typing, showToast: (msg, type) => showToast(msg, type as "success" | "error" | "info"), getCurrentUserId, @@ -369,7 +367,7 @@ export function createMainPage(options: MainPageOptions): MountableComponent { if (active.type === "text") { videoModeCtrl?.showChat(); } - channelCtrl!.mountChannel(active.id, resolveChannelName(active.id, active.name, active.type), active.type); + channelCtrl?.mountChannel(active.id, resolveChannelName(active.id, active.name, active.type), active.type); } } catch (err) { log.error("Channel mount failed", err); @@ -380,7 +378,7 @@ export function createMainPage(options: MainPageOptions): MountableComponent { const active = getActiveChannel(); if (active !== null) { - channelCtrl!.mountChannel(active.id, resolveChannelName(active.id, active.name, active.type), active.type); + channelCtrl?.mountChannel(active.id, resolveChannelName(active.id, active.name, active.type), active.type); } } diff --git a/Client/tauri-client/src/pages/connect-page/LoginForm.ts b/Client/tauri-client/src/pages/connect-page/LoginForm.ts index 6ae3c269..1b2ada4a 100644 --- a/Client/tauri-client/src/pages/connect-page/LoginForm.ts +++ b/Client/tauri-client/src/pages/connect-page/LoginForm.ts @@ -93,13 +93,9 @@ export function createLoginForm(opts: LoginFormOptions): LoginFormApi { let submitBtnText: HTMLSpanElement; let toggleModeBtn: HTMLAnchorElement; let errorBanner: HTMLDivElement; - let totpOverlay: HTMLDivElement; let totpInput: HTMLInputElement; let totpSubmitBtn: HTMLButtonElement; let rememberPasswordCheckbox: HTMLInputElement; - let statusBar: HTMLDivElement; - let statusBarFill: HTMLDivElement; - let autoConnectOverlay: HTMLDivElement; let autoConnectServerName: HTMLSpanElement; // --------------------------------------------------------------------------- @@ -321,7 +317,7 @@ export function createLoginForm(opts: LoginFormOptions): LoginFormApi { (e) => { if (e.key === "Enter") { e.preventDefault(); - handleTotpSubmit(); + void handleTotpSubmit(); } }, { signal }, @@ -358,6 +354,23 @@ export function createLoginForm(opts: LoginFormOptions): LoginFormApi { return overlay; } + // --------------------------------------------------------------------------- + // Build elements (before state transition functions that reference them) + // --------------------------------------------------------------------------- + + const panelEl = buildFormPanel(); + + // Status bar (hidden by default, shown with .visible class) + const statusBar = createElement("div", { class: "status-bar" }); + const statusBarFill = createElement("div", { class: "status-bar-fill" }); + statusBar.appendChild(statusBarFill); + + // TOTP overlay (hidden by default) + const totpOverlay = buildTotpOverlay(); + + // Auto-connect overlay (hidden by default) + const autoConnectOverlay = buildAutoConnectOverlay(); + // --------------------------------------------------------------------------- // State transitions // --------------------------------------------------------------------------- @@ -563,23 +576,6 @@ export function createLoginForm(opts: LoginFormOptions): LoginFormApi { transitionTo("idle"); } - // --------------------------------------------------------------------------- - // Build elements - // --------------------------------------------------------------------------- - - const panelEl = buildFormPanel(); - - // Status bar (hidden by default, shown with .visible class) - statusBar = createElement("div", { class: "status-bar" }); - statusBarFill = createElement("div", { class: "status-bar-fill" }); - statusBar.appendChild(statusBarFill); - - // TOTP overlay (hidden by default) - totpOverlay = buildTotpOverlay(); - - // Auto-connect overlay (hidden by default) - autoConnectOverlay = buildAutoConnectOverlay(); - // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- diff --git a/Client/tauri-client/src/pages/connect-page/ServerPanel.ts b/Client/tauri-client/src/pages/connect-page/ServerPanel.ts index dfcb9d64..befebe2b 100644 --- a/Client/tauri-client/src/pages/connect-page/ServerPanel.ts +++ b/Client/tauri-client/src/pages/connect-page/ServerPanel.ts @@ -296,7 +296,7 @@ export function createServerPanel( const addr = hostAddrInput.value.trim(); if (!name || !addr) return; // Validate address: must be a valid hostname:port — no paths, no special chars - if (!/^[\w.\-]+(:\d+)?$/.test(addr)) { + if (!/^[\w.-]+(:\d+)?$/.test(addr)) { // Show inline validation error via the host input hostAddrInput.setCustomValidity("Invalid server address (expected host or host:port)"); hostAddrInput.reportValidity(); @@ -319,7 +319,7 @@ export function createServerPanel( // Enter key submits hostAddrInput.addEventListener("keydown", (e) => { - if ((e as KeyboardEvent).key === "Enter") handleSave(); + if ((e).key === "Enter") handleSave(); }, { signal }); // Mount onto the panel's closest connect-page root diff --git a/Client/tauri-client/src/pages/main-page/ChatArea.ts b/Client/tauri-client/src/pages/main-page/ChatArea.ts index 0efe9c2e..b7880ddc 100644 --- a/Client/tauri-client/src/pages/main-page/ChatArea.ts +++ b/Client/tauri-client/src/pages/main-page/ChatArea.ts @@ -59,7 +59,7 @@ export interface ChatAreaResult { // --------------------------------------------------------------------------- export function createChatArea(opts: ChatAreaOptions): ChatAreaResult { - const { api, getRoot, getToast, getChannelCtrl } = opts; + const { api, getRoot, getChannelCtrl } = opts; const children: MountableComponent[] = []; const unsubscribers: Array<() => void> = []; diff --git a/Client/tauri-client/src/pages/main-page/ChatHeader.ts b/Client/tauri-client/src/pages/main-page/ChatHeader.ts index 849da6a7..4f6ded56 100644 --- a/Client/tauri-client/src/pages/main-page/ChatHeader.ts +++ b/Client/tauri-client/src/pages/main-page/ChatHeader.ts @@ -53,7 +53,7 @@ export function buildChatHeader( const onFocus = opts.onSearchFocus; searchInput.addEventListener("focus", () => { onFocus(); - (searchInput as HTMLInputElement).blur(); + (searchInput).blur(); }); } appendChildren(tools, searchInput, pinBtn); diff --git a/Client/tauri-client/src/pages/main-page/OverlayManagers.ts b/Client/tauri-client/src/pages/main-page/OverlayManagers.ts index b8f2c2c9..8f4fd82d 100644 --- a/Client/tauri-client/src/pages/main-page/OverlayManagers.ts +++ b/Client/tauri-client/src/pages/main-page/OverlayManagers.ts @@ -29,7 +29,7 @@ export function mapInviteResponse(r: InviteResponse): InviteItem { ? (extra["created_by"] as { username?: string }).username ?? "unknown" : "unknown"; const uses = r.use_count - ?? (typeof extra["uses"] === "number" ? (extra["uses"] as number) : 0); + ?? (typeof extra["uses"] === "number" ? (extra["uses"]) : 0); return { code: r.code, createdBy, diff --git a/Client/tauri-client/src/pages/main-page/SidebarArea.ts b/Client/tauri-client/src/pages/main-page/SidebarArea.ts index 60700244..af74dd70 100644 --- a/Client/tauri-client/src/pages/main-page/SidebarArea.ts +++ b/Client/tauri-client/src/pages/main-page/SidebarArea.ts @@ -819,7 +819,7 @@ export function createSidebarArea(opts: SidebarAreaOptions): SidebarAreaResult { // Load profiles asynchronously, then show overlay void (async () => { - let profiles: readonly QuickSwitchProfile[] = []; + let profiles: readonly QuickSwitchProfile[]; try { if (profileManager === null) { diff --git a/Client/tauri-client/src/pages/main-page/VideoModeController.ts b/Client/tauri-client/src/pages/main-page/VideoModeController.ts index 093cd46e..d0a8d939 100644 --- a/Client/tauri-client/src/pages/main-page/VideoModeController.ts +++ b/Client/tauri-client/src/pages/main-page/VideoModeController.ts @@ -5,7 +5,7 @@ import { voiceStore } from "@stores/voice.store"; import { getLocalCameraStream, getLocalScreenshareStream } from "@lib/livekitSession"; -import type { VideoGridComponent, TileConfig } from "@components/VideoGrid"; +import type { VideoGridComponent } from "@components/VideoGrid"; // --------------------------------------------------------------------------- // Types diff --git a/Client/tauri-client/src/stores/ui.store.ts b/Client/tauri-client/src/stores/ui.store.ts index 2c7276e3..d7355331 100644 --- a/Client/tauri-client/src/stores/ui.store.ts +++ b/Client/tauri-client/src/stores/ui.store.ts @@ -141,7 +141,7 @@ export function loadCollapsedCategories(serverHost: string): void { uiStore.setState((prev) => ({ ...prev, collapsedCategories: new Set() })); return; } - const loaded: ReadonlySet = new Set(parsed as string[]); + const loaded: ReadonlySet = new Set(parsed); uiStore.setState((prev) => ({ ...prev, collapsedCategories: loaded })); } catch { uiStore.setState((prev) => ({ ...prev, collapsedCategories: new Set() })); diff --git a/Client/tauri-client/tests/unit/settings-overlay.test.ts b/Client/tauri-client/tests/unit/settings-overlay.test.ts index 63e3bd33..edc61b1b 100644 --- a/Client/tauri-client/tests/unit/settings-overlay.test.ts +++ b/Client/tauri-client/tests/unit/settings-overlay.test.ts @@ -365,6 +365,150 @@ describe("SettingsOverlay", () => { overlay.destroy?.(); }); + // --- Delete account tests --- + + it("shows confirmation area when Delete Account is clicked", () => { + const overlay = createSettingsOverlay(defaultOptions); + overlay.mount(container); + + const triggerBtn = container.querySelector("[data-testid='delete-account-trigger']") as HTMLElement; + expect(triggerBtn).not.toBeNull(); + + const confirmArea = container.querySelector("[data-testid='delete-account-confirm-area']") as HTMLElement; + expect(confirmArea.style.display).toBe("none"); + + triggerBtn.click(); + + expect(confirmArea.style.display).toBe("block"); + expect(triggerBtn.style.display).toBe("none"); + + overlay.destroy?.(); + }); + + it("hides confirmation area when Cancel is clicked", () => { + const overlay = createSettingsOverlay(defaultOptions); + overlay.mount(container); + + const triggerBtn = container.querySelector("[data-testid='delete-account-trigger']") as HTMLElement; + triggerBtn.click(); + + const confirmArea = container.querySelector("[data-testid='delete-account-confirm-area']") as HTMLElement; + expect(confirmArea.style.display).toBe("block"); + + const cancelBtn = confirmArea.querySelector("button:not(.account-delete-btn)") as HTMLElement; + cancelBtn.click(); + + expect(confirmArea.style.display).toBe("none"); + expect(triggerBtn.style.display).toBe(""); + + overlay.destroy?.(); + }); + + it("shows error when confirming delete without password", () => { + const overlay = createSettingsOverlay(defaultOptions); + overlay.mount(container); + + const triggerBtn = container.querySelector("[data-testid='delete-account-trigger']") as HTMLElement; + triggerBtn.click(); + + const confirmBtn = container.querySelector("[data-testid='delete-account-confirm']") as HTMLElement; + confirmBtn.click(); + + const errorEl = container.querySelector("[data-testid='delete-account-error']") as HTMLElement; + expect(errorEl.textContent).toBe("Password is required."); + expect(defaultOptions.onDeleteAccount).not.toHaveBeenCalled(); + + overlay.destroy?.(); + }); + + it("calls onDeleteAccount with password on confirm", () => { + const overlay = createSettingsOverlay(defaultOptions); + overlay.mount(container); + + const triggerBtn = container.querySelector("[data-testid='delete-account-trigger']") as HTMLElement; + triggerBtn.click(); + + const passwordInput = container.querySelector("[data-testid='delete-account-password']") as HTMLInputElement; + passwordInput.value = "mypassword123"; + + const confirmBtn = container.querySelector("[data-testid='delete-account-confirm']") as HTMLButtonElement; + confirmBtn.click(); + + expect(defaultOptions.onDeleteAccount).toHaveBeenCalledWith("mypassword123"); + + overlay.destroy?.(); + }); + + it("disables confirm button and shows 'Deleting...' during delete", () => { + const overlay = createSettingsOverlay(defaultOptions); + overlay.mount(container); + + const triggerBtn = container.querySelector("[data-testid='delete-account-trigger']") as HTMLElement; + triggerBtn.click(); + + const passwordInput = container.querySelector("[data-testid='delete-account-password']") as HTMLInputElement; + passwordInput.value = "mypassword123"; + + const confirmBtn = container.querySelector("[data-testid='delete-account-confirm']") as HTMLButtonElement; + confirmBtn.click(); + + expect(confirmBtn.disabled).toBe(true); + expect(confirmBtn.textContent).toBe("Deleting..."); + + overlay.destroy?.(); + }); + + it("shows error and re-enables button on delete failure", async () => { + const failOptions = { + ...defaultOptions, + onDeleteAccount: vi.fn().mockRejectedValue(new Error("Wrong password")), + }; + + const overlay = createSettingsOverlay(failOptions); + overlay.mount(container); + + const triggerBtn = container.querySelector("[data-testid='delete-account-trigger']") as HTMLElement; + triggerBtn.click(); + + const passwordInput = container.querySelector("[data-testid='delete-account-password']") as HTMLInputElement; + passwordInput.value = "wrongpassword"; + + const confirmBtn = container.querySelector("[data-testid='delete-account-confirm']") as HTMLButtonElement; + confirmBtn.click(); + + // Wait for the rejected promise to settle + await vi.waitFor(() => { + expect(confirmBtn.disabled).toBe(false); + }); + + const errorEl = container.querySelector("[data-testid='delete-account-error']") as HTMLElement; + expect(errorEl.textContent).toBe("Wrong password"); + expect(confirmBtn.textContent).toBe("Confirm Delete"); + + overlay.destroy?.(); + }); + + it("clears password input when reopening confirmation area", () => { + const overlay = createSettingsOverlay(defaultOptions); + overlay.mount(container); + + const triggerBtn = container.querySelector("[data-testid='delete-account-trigger']") as HTMLElement; + triggerBtn.click(); + + const passwordInput = container.querySelector("[data-testid='delete-account-password']") as HTMLInputElement; + passwordInput.value = "typed-something"; + + // Cancel and reopen + const confirmArea = container.querySelector("[data-testid='delete-account-confirm-area']") as HTMLElement; + const cancelBtn = confirmArea.querySelector("button:not(.account-delete-btn)") as HTMLElement; + cancelBtn.click(); + triggerBtn.click(); + + expect(passwordInput.value).toBe(""); + + overlay.destroy?.(); + }); + // --- Open/Close --- it("open() adds .open class, close() removes it", () => {