From f88c210b64e625543248c25243603c310a3d81af Mon Sep 17 00:00:00 2001 From: Anton Arnautov <43254280+arnautov-anton@users.noreply.github.com> Date: Wed, 26 Feb 2025 20:10:37 +0100 Subject: [PATCH 01/47] chore: update build system (#1468) --- .eslintrc.json | 18 +- .github/actions/setup-node/action.yml | 9 +- .github/workflows/initiate_release.yml | 68 - .github/workflows/lint.yml | 2 +- .github/workflows/release.yml | 57 +- .github/workflows/unit.yml | 7 +- .mocharc.json | 7 + .nvmrc | 1 + .releaserc.json | 81 + package.json | 110 +- rollup.config.js | 107 - scripts/bundle.mjs | 76 + scripts/get-package-version.mjs | 23 + scripts/get_changelog_diff.js | 26 - src/client.ts | 24 +- src/connection.ts | 21 +- src/connection_fallback.ts | 16 +- src/signing.ts | 15 +- src/token_manager.ts | 9 +- test/unit/client.js | 7 +- tsconfig.json | 36 +- tsconfig.test.json | 13 + yarn.lock | 5232 +++++++++++++----------- 23 files changed, 3300 insertions(+), 2665 deletions(-) delete mode 100644 .github/workflows/initiate_release.yml create mode 100644 .mocharc.json create mode 100644 .nvmrc create mode 100644 .releaserc.json delete mode 100644 rollup.config.js create mode 100755 scripts/bundle.mjs create mode 100644 scripts/get-package-version.mjs delete mode 100644 scripts/get_changelog_diff.js create mode 100644 tsconfig.test.json diff --git a/.eslintrc.json b/.eslintrc.json index a0db29408c..775cd5349c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,5 +1,5 @@ { - "plugins": ["babel", "markdown", "sonarjs", "prettier", "@typescript-eslint", "typescript-sort-keys"], + "plugins": ["markdown", "sonarjs", "prettier", "@typescript-eslint", "typescript-sort-keys"], "extends": ["eslint:recommended", "plugin:sonarjs/recommended", "plugin:@typescript-eslint/recommended", "prettier"], "rules": { "@typescript-eslint/ban-ts-comment": 0, @@ -19,7 +19,6 @@ "no-self-compare": 2, "prefer-const": 2, "object-shorthand": 1, - "babel/no-invalid-this": 2, "array-callback-return": 2, "valid-typeof": 2, "react/prop-types": 0, @@ -32,9 +31,20 @@ "typescript-sort-keys/interface": [ "error", "asc", - { "caseSensitive": false, "natural": true, "requiredFirst": true } + { + "caseSensitive": false, + "natural": true, + "requiredFirst": true + } ], - "typescript-sort-keys/string-enum": ["error", "asc", { "caseSensitive": false, "natural": true }] + "typescript-sort-keys/string-enum": [ + "error", + "asc", + { + "caseSensitive": false, + "natural": true + } + ] }, "env": { "es6": true, diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml index ed26ca1dbf..f437c730d5 100644 --- a/.github/actions/setup-node/action.yml +++ b/.github/actions/setup-node/action.yml @@ -3,11 +3,12 @@ description: Sets up Node and Build SDK inputs: node-version: + description: "Specify Node version" required: false - default: '16' + default: "22" runs: - using: 'composite' + using: "composite" steps: - name: Setup Node uses: actions/setup-node@v3 @@ -15,6 +16,10 @@ runs: node-version: ${{ inputs.node-version }} registry-url: https://registry.npmjs.org + - name: Set NODE_VERSION env + shell: bash + run: echo "NODE_VERSION=$(node --version)" >> $GITHUB_ENV + - name: Cache Dependencies uses: actions/cache@v3 with: diff --git a/.github/workflows/initiate_release.yml b/.github/workflows/initiate_release.yml deleted file mode 100644 index 5643388d8e..0000000000 --- a/.github/workflows/initiate_release.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: Create release PR - -on: - workflow_dispatch: - inputs: - version: - description: "The new version number with 'v' prefix. Example: v1.40.1" - required: true - -jobs: - init_release: - name: 🚀 Create release PR - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 # gives the changelog generator access to all previous commits - - - name: Get current tag - id: previoustag - uses: WyriHaximus/github-action-get-previous-tag@v1 - - - name: Ensure version number higher than current - uses: actions/github-script@v6 - env: - PREVIOUS_TAG: ${{ steps.previoustag.outputs.tag }} - DESTINATION_TAG: ${{ github.event.inputs.version }} - with: - script: | - const { PREVIOUS_TAG, DESTINATION_TAG } = process.env; - const result = DESTINATION_TAG.localeCompare(PREVIOUS_TAG, undefined, { numeric: true, sensitivity: 'base' }) - - if (result != 1) { - throw new Error('The new version number must be greater than the previous one.') - } - - - name: Restore dependencies - uses: ./.github/actions/setup-node - - - name: Update CHANGELOG.md, package.json and push release branch - env: - VERSION: ${{ github.event.inputs.version }} - run: | - npm run changelog - git config --global user.name 'github-actions' - git config --global user.email 'release@getstream.io' - git checkout -q -b "release-$VERSION" - git commit -am "chore(release): $VERSION" - git push -q -u origin "release-$VERSION" - - - name: Get changelog diff - uses: actions/github-script@v6 - with: - script: | - const get_change_log_diff = require('./scripts/get_changelog_diff.js') - core.exportVariable('CHANGELOG', get_change_log_diff()) - - - name: Open pull request - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh pr create \ - -t "chore: release ${{ github.event.inputs.version }}" \ - -b "# :rocket: ${{ github.event.inputs.version }} - Make sure to use squash & merge when merging! - Once this is merged, another job will kick off automatically and publish the package. - # :memo: Changelog - ${{ env.CHANGELOG }}" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7099251a0e..223f0707da 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -15,7 +15,7 @@ jobs: - uses: ./.github/actions/setup-node - - name: Commit message lint + - name: Commit Message Lint run: echo "${{ github.event.pull_request.title }}" | yarn commitlinter - name: Lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e1e93d0ae7..c205273fa2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,42 +1,35 @@ name: Release - on: - pull_request: - types: [closed] - branches: - - master + workflow_dispatch: + inputs: + dry_run: + description: Run package release in "dry run" mode (does not publish) + default: false + type: boolean jobs: - Release: - name: 🚀 Release - if: github.event.pull_request.merged && startsWith(github.head_ref, 'release-') + package_release: + name: Release from "${{ github.ref_name }}" branch runs-on: ubuntu-latest + # GH does not allow to limit branches in the workflow_dispatch settings so this here is a safety measure + if: ${{ inputs.dry_run || startsWith(github.ref_name, 'release') || startsWith(github.ref_name, 'master') }} steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v4 with: fetch-depth: 0 - - - uses: actions/github-script@v6 + - name: Setup Node.js + uses: actions/setup-node@v4 with: - script: | - const get_change_log_diff = require('./scripts/get_changelog_diff.js') - core.exportVariable('CHANGELOG', get_change_log_diff()) - - // Getting the release version from the PR source branch - // Source branch looks like this: release-1.0.0 - const version = context.payload.pull_request.head.ref.split('-')[1] - core.exportVariable('VERSION', version) - - - uses: ./.github/actions/setup-node - - - name: Publish package - run: npm publish + node-version: 22 + - name: Install dependencies + run: yarn install --frozen-lockfile + - name: Release env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - - name: Create release on GitHub - uses: ncipollo/release-action@v1 - with: - body: ${{ env.CHANGELOG }} - tag: ${{ env.VERSION }} - token: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + # https://github.com/stream-ci-bot + GH_TOKEN: ${{ secrets.DOCUSAURUS_GH_TOKEN }} + HUSKY: 0 + run: > + yarn semantic-release + ${{ inputs.dry_run && '--dry-run' || '' }} diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index fd9e6449f1..d3ced8eabe 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -8,15 +8,10 @@ concurrency: jobs: test: runs-on: ubuntu-latest - strategy: - matrix: - node: [16, 17, 18] steps: - uses: actions/checkout@v3 - uses: ./.github/actions/setup-node - with: - node-version: ${{ matrix.node }} - - name: Unit tests + - name: Unit Tests with Node ${{ env.NODE_VERSION }} run: yarn run test-coverage diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 0000000000..4b1283fcee --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json.schemastore.org/mocharc", + "bail": true, + "exit": true, + "timeout": 20000, + "require": ["ts-node/register"] +} diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..8fdd954df9 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 \ No newline at end of file diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 0000000000..6e6d11bb4b --- /dev/null +++ b/.releaserc.json @@ -0,0 +1,81 @@ +{ + "branches": [ + { + "name": "master", + "channel": "latest" + }, + { + "name": "release-v8", + "channel": "v8", + "range": "8.x" + } + ], + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "angular", + "releaseRules": [ + { "type": "chore", "scope": "deps", "release": "patch" }, + { "type": "refactor", "release": "patch" } + ] + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "presetConfig": { + "types": [ + { + "type": "fix", + "section": "Bug Fixes", + "hidden": false + }, + { + "type": "feat", + "section": "Features", + "hidden": false + }, + { + "type": "chore", + "scope": "deps", + "section": "Chores", + "hidden": false + }, + { + "type": "refactor", + "section": "Refactors", + "hidden": false + }, + { + "type": "perf", + "section": "Performance Improvements", + "hidden": false + } + ] + } + } + ], + [ + "@semantic-release/exec", + { + "prepareCmd": "NEXT_VERSION=${nextRelease.version} npm run build" + } + ], + [ + "@semantic-release/changelog", + { + "changelogFile": "./CHANGELOG.md" + } + ], + [ + "@semantic-release/git", + { + "assets": ["./CHANGELOG.md"] + } + ], + "@semantic-release/github", + "@semantic-release/npm" + ] +} diff --git a/package.json b/package.json index dbc61450a5..47e9456660 100644 --- a/package.json +++ b/package.json @@ -2,22 +2,38 @@ "name": "stream-chat", "version": "8.57.1", "description": "JS SDK for the Stream Chat API", - "author": "GetStream", "homepage": "https://getstream.io/chat/", - "repository": "https://github.com/GetStream/stream-chat-js.git", - "main": "./dist/index.js", - "module": "./dist/index.es.js", - "jsnext:main": "./dist/index.es.js", + "author": { + "name": "GetStream.io, Inc.", + "url": "https://getstream.io/team/" + }, + "repository": { + "type": "git", + "url": "https://github.com/GetStream/stream-chat-js.git" + }, "types": "./dist/types/index.d.ts", - "browser": { - "./dist/index.es.js": "./dist/browser.es.js", - "./dist/index.js": "./dist/browser.js" + "main": "./dist/esm/index.js", + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "browser": { + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.browser.cjs" + }, + "react-native": { + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.browser.cjs" + }, + "node": "./dist/cjs/index.node.cjs", + "default": "./dist/esm/index.js" + } }, - "react-native": { - "./dist/index.es.js": "./dist/browser.es.js", - "./dist/index.js": "./dist/browser.js" + "browser": { + "https": false, + "crypto": false, + "jsonwebtoken": false, + "ws": false }, - "jsdelivr": "./dist/browser.full-bundle.min.js", "license": "SEE LICENSE IN LICENSE", "keywords": [ "chat", @@ -30,41 +46,24 @@ ], "files": [ "/dist", - "/src", - "readme.md", - "license" + "/src" ], "dependencies": { - "@babel/runtime": "^7.16.3", - "@types/jsonwebtoken": "~9.0.0", - "@types/ws": "^7.4.0", + "@types/jsonwebtoken": "^9.0.8", + "@types/ws": "^8.5.14", "axios": "^1.6.0", "base64-js": "^1.5.1", "form-data": "^4.0.0", - "isomorphic-ws": "^4.0.1", - "jsonwebtoken": "~9.0.0", - "ws": "^7.5.10" + "isomorphic-ws": "^5.0.0", + "jsonwebtoken": "^9.0.2", + "ws": "^8.18.1" }, "devDependencies": { - "@babel/cli": "^7.16.0", - "@babel/core": "^7.16.0", - "@babel/eslint-parser": "^7.17.0", - "@babel/node": "^7.16.0", - "@babel/plugin-proposal-class-properties": "^7.16.0", - "@babel/plugin-proposal-object-rest-spread": "^7.16.0", - "@babel/plugin-transform-async-to-generator": "^7.16.0", - "@babel/plugin-transform-object-assign": "^7.16.0", - "@babel/plugin-transform-runtime": "^7.16.4", - "@babel/preset-env": "^7.16.4", - "@babel/preset-typescript": "^7.16.0", - "@babel/register": "^7.16.0", "@commitlint/cli": "^16.0.2", "@commitlint/config-conventional": "^16.0.0", - "@rollup/plugin-babel": "^5.3.0", - "@rollup/plugin-commonjs": "^17.1.0", - "@rollup/plugin-node-resolve": "^11.2.0", - "@rollup/plugin-replace": "^3.0.1", - "@types/babel__core": "^7.1.16", + "@semantic-release/changelog": "^6.0.3", + "@semantic-release/exec": "^7.0.3", + "@semantic-release/git": "^10.0.1", "@types/base64-js": "^1.3.0", "@types/chai": "^4.2.15", "@types/chai-arrays": "^2.0.0", @@ -74,10 +73,8 @@ "@types/mocha": "^9.0.0", "@types/node": "^16.11.11", "@types/prettier": "^2.2.2", - "@types/rollup-plugin-json": "^3.0.2", - "@types/rollup-plugin-peer-deps-external": "^2.2.0", - "@types/rollup-plugin-url": "^2.2.0", "@types/sinon": "^10.0.6", + "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^4.17.0", "@typescript-eslint/parser": "^4.17.0", "chai": "^4.3.4", @@ -85,50 +82,49 @@ "chai-as-promised": "^7.1.1", "chai-like": "^1.1.1", "chai-sorted": "^0.2.0", + "concurrently": "^9.1.2", + "conventional-changelog-conventionalcommits": "^8.0.0", "dotenv": "^8.2.0", + "esbuild": "^0.25.0", "eslint": "7.21.0", "eslint-config-prettier": "^8.1.0", - "eslint-plugin-babel": "^5.3.1", "eslint-plugin-markdown": "^2.0.0", "eslint-plugin-prettier": "^3.3.1", "eslint-plugin-sonarjs": "^0.6.0", "eslint-plugin-typescript-sort-keys": "1.5.0", "husky": "^4.3.8", "lint-staged": "^15.2.2", - "mocha": "^10.7.0", + "mocha": "^11.1.0", "nyc": "^15.1.0", "prettier": "^2.2.1", - "rollup": "^2.41.0", - "rollup-plugin-peer-deps-external": "^2.2.4", - "rollup-plugin-terser": "^7.0.2", + "semantic-release": "^24.2.3", "sinon": "^12.0.1", "standard-version": "^9.3.2", - "typescript": "4.2.3", + "ts-node": "^10.9.2", + "tslib": "^2.8.1", + "typescript": "^5.7.3", "uuid": "^8.3.2" }, "scripts": { - "start": "yarn run compile -w", - "compile": "rollup -c", - "changelog": "standard-version --release-as $VERSION --skip.tag --skip.commit --tag-prefix=v", + "start": "tsc --watch", "commitlinter": "commitlint", - "build": "rm -rf dist && yarn run types && yarn run compile", - "types": "tsc --emitDeclarationOnly true", + "build": "rm -rf dist && yarn bundle", + "bundle": "concurrently 'tsc --declaration --emitDeclarationOnly --outDir ./dist/types' ./scripts/bundle.mjs", + "types": "tsc --noEmit", "prettier": "prettier --check '**/*.{js,ts,md,css,scss,json}' .eslintrc.json .prettierrc .babelrc", "prettier-fix": "npx prettier --write '**/*.{js,ts,md,css,scss,json}' .eslintrc.json .prettierrc .babelrc", "test-types": "node test/typescript/index.js && tsc --esModuleInterop true --noEmit true --strictNullChecks true --noImplicitAny true --strict true test/typescript/*.ts", "eslint": "eslint '**/*.{js,md,ts}' --max-warnings 0 --ignore-path ./.eslintignore", "eslint-fix": "npx eslint --fix '**/*.{js,md,ts}' --max-warnings 0 --ignore-path ./.eslintignore", - "test-unit": "NODE_ENV=test mocha --exit --bail --timeout 20000 --require ./babel-register test/unit/*.{js,test.ts}", + "test-unit": "NODE_ENV=test TS_NODE_PROJECT=tsconfig.test.json mocha test/unit/*.{js,test.ts}", "test-coverage": "nyc yarn test-unit", "test": "yarn test-unit", "testwatch": "NODE_ENV=test nodemon ./node_modules/.bin/mocha --timeout 20000 --require test-entry.js test/test.js", "lint": "yarn run prettier && yarn run eslint", "lint-fix": "yarn run prettier-fix && yarn run eslint-fix", "fix-staged": "lint-staged --config .lintstagedrc.fix.json --concurrent 1", - "prepare": "yarn run build", - "preversion": "yarn && yarn lint && yarn test-unit", - "version": "git add yarn.lock", - "postversion": "git push && git push --tags && npm publish" + "semantic-release": "semantic-release", + "prepare": "yarn run build" }, "engines": { "node": ">=16" diff --git a/rollup.config.js b/rollup.config.js deleted file mode 100644 index c1aab9ac83..0000000000 --- a/rollup.config.js +++ /dev/null @@ -1,107 +0,0 @@ -import babel from '@rollup/plugin-babel'; -import external from 'rollup-plugin-peer-deps-external'; -import commonjs from '@rollup/plugin-commonjs'; -import replace from '@rollup/plugin-replace'; -import { terser } from 'rollup-plugin-terser'; -import { nodeResolve } from '@rollup/plugin-node-resolve'; - -import pkg from './package.json'; - -import process from 'process'; -process.env.NODE_ENV = 'production'; - -const externalPackages = ['axios', 'form-data', 'isomorphic-ws', 'base64-js', /@babel\/runtime/]; - -const browserIgnore = { - name: 'browser-remapper', - resolveId: (importee) => (['jsonwebtoken', 'https', 'crypto'].includes(importee) ? importee : null), - load: (id) => (['jsonwebtoken', 'https', 'crypto'].includes(id) ? 'export default null;' : null), -}; - -const extensions = ['.mjs', '.json', '.node', '.js', '.ts']; - -const babelConfig = { - babelHelpers: 'runtime', - exclude: 'node_modules/**', - extensions, -}; - -const baseConfig = { - input: 'src/index.ts', - cache: false, - watch: { - chokidar: false, - }, -}; -const normalBundle = { - ...baseConfig, - output: [ - { - file: pkg.main, - format: 'cjs', - sourcemap: true, - }, - { - file: pkg.module, - format: 'es', - sourcemap: true, - }, - ], - external: externalPackages.concat(['https', 'jsonwebtoken', 'crypto']), - plugins: [ - replace({ preventAssignment: true, 'process.env.PKG_VERSION': JSON.stringify(pkg.version) }), - external(), - nodeResolve({ extensions }), - babel(babelConfig), - commonjs(), - ], -}; - -const browserBundle = { - ...baseConfig, - output: [ - { - file: pkg.browser[pkg.main], - format: 'cjs', - sourcemap: true, - }, - { - file: pkg.browser[pkg.module], - format: 'es', - sourcemap: true, - }, - ], - external: externalPackages, - plugins: [ - replace({ preventAssignment: true, 'process.env.PKG_VERSION': JSON.stringify(pkg.version) }), - browserIgnore, - external(), - nodeResolve({ extensions }), - babel(babelConfig), - commonjs(), - ], -}; -const fullBrowserBundle = { - ...baseConfig, - output: [ - { - file: pkg.jsdelivr, - format: 'iife', - name: 'window', // write all exported values to window - extend: true, // extend window, not overwrite it - sourcemap: true, - }, - ], - plugins: [ - replace({ preventAssignment: true, 'process.env.PKG_VERSION': JSON.stringify(pkg.version) }), - browserIgnore, - external(), - nodeResolve({ extensions, browser: true }), - babel(babelConfig), - commonjs(), - terser(), - ], -}; - -export default () => - process.env.ROLLUP_WATCH ? [normalBundle, browserBundle] : [normalBundle, browserBundle, fullBrowserBundle]; diff --git a/scripts/bundle.mjs b/scripts/bundle.mjs new file mode 100755 index 0000000000..f421f3fe14 --- /dev/null +++ b/scripts/bundle.mjs @@ -0,0 +1,76 @@ +#!/usr/bin/env node + +import { resolve } from 'node:path'; +import * as esbuild from 'esbuild'; +import packageJson from '../package.json' with {'type': 'json'}; +import getPackageVersion from './get-package-version.mjs'; + +// import.meta.dirname is not available before Node 20 +const __dirname = import.meta.dirname; + +// Those dependencies are distributed as ES modules, and cannot be externalized +// in our CJS bundle. We convert them to CJS and bundle them instead. +const bundledDeps = ['axios', 'form-data', 'isomorphic-ws', 'base64-js']; + +const version = getPackageVersion(); + +const deps = Object.keys({ + ...packageJson.dependencies, + ...packageJson.peerDependencies, +}); +const external = deps.filter((dep) => !bundledDeps.includes(dep)); + +/** @type esbuild.BuildOptions */ +const commonBuildOptions = { + entryPoints: [resolve(__dirname, '../src/index.ts')], + bundle: true, + target: 'ES2020', + sourcemap: 'linked', + define: { + 'process.env.PKG_VERSION': JSON.stringify(version), + }, +}; + +/** + * process.env.CLIENT_BUNDLE values: + * + * - index.js - browser-esm + * - index.browser.cjs - browser-cjs + * - index.node.cjs - node-cjs + */ + +// We build two CJS bundles: for browser and for node. The latter one can be +// used e.g. during SSR (although it makes little sence to SSR chat, but still +// nice for import not to break on server). +const bundles = [ + // CJS (browser & Node) + ['browser', 'node'].map((platform) => ({ + ...commonBuildOptions, + format: 'cjs', + external, + outExtension: { '.js': '.cjs' }, + entryNames: `[dir]/[name].${platform}`, + outdir: resolve(__dirname, '../dist/cjs'), + platform, + define: { + ...commonBuildOptions.define, + 'process.env.CLIENT_BUNDLE': JSON.stringify(`${platform}-cjs`), + }, + })), + // ESM (browser only) + { + ...commonBuildOptions, + format: 'esm', + outdir: resolve(__dirname, '../dist/esm'), + entryNames: `[dir]/[name]`, + platform: 'browser', + define: { + ...commonBuildOptions.define, + 'process.env.CLIENT_BUNDLE': JSON.stringify('browser-esm'), + }, + }, +] + .flat() + .map((config) => esbuild.build(config)); + +await Promise.all(bundles); diff --git a/scripts/get-package-version.mjs b/scripts/get-package-version.mjs new file mode 100644 index 0000000000..f424d70c99 --- /dev/null +++ b/scripts/get-package-version.mjs @@ -0,0 +1,23 @@ +import { execSync } from 'node:child_process'; +import packageJson from '../package.json' with { type: 'json' }; + +// Get the latest version so that magic string __STREAM_CHAT_REACT_VERSION__ can be replaced with it in the source code (used for reporting purposes) +export default function getPackageVersion() { + let version; + // During release, use the version being released + // see .releaserc.json where the `NEXT_VERSION` env variable is set + if (process.env.NEXT_VERSION) { + version = process.env.NEXT_VERSION; + } else { + // Otherwise use the latest git tag + try { + version = execSync('git describe --tags --abbrev=0').toString().trim(); + } catch (error) { + console.error(error); + console.warn('Could not get latest version from git tags, falling back to package.json'); + version = packageJson.version; + } + } + console.log(`Determined the build package version to be ${version}`); + return version; +} diff --git a/scripts/get_changelog_diff.js b/scripts/get_changelog_diff.js deleted file mode 100644 index 317e286203..0000000000 --- a/scripts/get_changelog_diff.js +++ /dev/null @@ -1,26 +0,0 @@ -/* -Here we're trying to parse the latest changes from CHANGELOG.md file. -The changelog looks like this: - -## 0.0.3 -- Something #3 -## 0.0.2 -- Something #2 -## 0.0.1 -- Something #1 - -In this case we're trying to extract "- Something #3" since that's the latest change. -*/ -module.exports = () => { - const fs = require('fs'); - - changelog = fs.readFileSync('CHANGELOG.md', 'utf8'); - releases = changelog.match(/## [?[0-9](.+)/g); - - current_release = changelog.indexOf(releases[0]); - previous_release = changelog.indexOf(releases[1]); - - latest_changes = changelog.substr(current_release, previous_release - current_release); - - return latest_changes; -}; diff --git a/src/client.ts b/src/client.ts index 4381e4b9e5..9c85442f51 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3,7 +3,7 @@ import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; import https from 'https'; -import WebSocket from 'isomorphic-ws'; +import type WebSocket from 'isomorphic-ws'; import { Channel } from './channel'; import { ClientState } from './client_state'; @@ -1475,10 +1475,11 @@ export class StreamChat `${key}=${value ?? ''}`); + + return [userAgentString, ...additionalOptions].join('|'); } /** diff --git a/src/connection.ts b/src/connection.ts index d567bdb56c..b5c765a085 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -113,7 +113,8 @@ export class StableWSConnection(params, config, retry); } - throw err; + throw error; } }; @@ -107,22 +108,23 @@ export class WSConnectionFallback { loadTokenPromise: Promise | null; type: 'static' | 'provider'; - secret?: Secret; + secret?: jwt.Secret; token?: string; tokenProvider?: TokenOrProvider; user?: UserResponse; @@ -20,7 +21,7 @@ export class TokenManager { describe('X-Stream-Client header', () => { process.env.PKG_VERSION = '1.2.3'; + process.env.CLIENT_BUNDLE = 'browser-esm'; let client; beforeEach(async () => { @@ -653,21 +654,21 @@ describe('X-Stream-Client header', () => { it('server-side integration', () => { const userAgent = client.getUserAgent(); - expect(userAgent).to.be.equal('stream-chat-js-v1.2.3-node'); + expect(userAgent).to.be.equal('stream-chat-js-v1.2.3-node|client_bundle=browser-esm'); }); it('client-side integration', () => { client.node = false; const userAgent = client.getUserAgent(); - expect(userAgent).to.be.equal('stream-chat-js-v1.2.3-browser'); + expect(userAgent).to.be.equal('stream-chat-js-v1.2.3-browser|client_bundle=browser-esm'); }); it('SDK integration', () => { client.sdkIdentifier = { name: 'react', version: '2.3.4' }; const userAgent = client.getUserAgent(); - expect(userAgent).to.be.equal('stream-chat-react-v2.3.4-llc-v1.2.3'); + expect(userAgent).to.be.equal('stream-chat-react-v2.3.4-llc-v1.2.3|client_bundle=browser-esm'); }); it('setUserAgent is now deprecated', () => { diff --git a/tsconfig.json b/tsconfig.json index 26dea59f5a..77a74daeab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,21 +1,29 @@ { "compilerOptions": { - "allowSyntheticDefaultImports": true, - "rootDir": "./src", - "baseUrl": "./src", - "esModuleInterop": true, - "moduleResolution": "node", - "lib": ["DOM", "ES6"], - "noEmitOnError": false, + "allowUnreachableCode": false, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "noImplicitUseStrict": false, + "noStrictGenericChecks": false, + "noUnusedLocals": false, + "noUnusedParameters": false, "noImplicitAny": true, - "preserveConstEnums": true, - "strict": true, "strictNullChecks": true, - "declaration": true, - "declarationMap": true, - "declarationDir": "./dist/types", - "module": "commonjs", - "target": "ES5" + "strict": true, + "allowSyntheticDefaultImports": true, + + "isolatedModules": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "allowJs": true, + "skipLibCheck": true, + "noEmitOnError": true, + "importHelpers": true, + + "lib": ["ES2020", "DOM"], + "moduleResolution": "bundler", + "module": "Preserve", + "target": "ES2020" }, "include": ["./src/**/*"] } diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000000..31b8d22092 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "moduleResolution": "node10", + "module": "CommonJS", + "strict": false + }, + "ts-node": { + "transpileOnly": true, + "logError": true + }, + "include": ["./test/**/*"] +} diff --git a/yarn.lock b/yarn.lock index be99174413..ac8d8d66c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,22 +9,6 @@ dependencies: "@jridgewell/trace-mapping" "^0.3.0" -"@babel/cli@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.16.0.tgz#a729b7a48eb80b49f48a339529fc4129fd7bcef3" - integrity sha512-WLrM42vKX/4atIoQB+eb0ovUof53UUvecb4qGjU2PDDWRiZr50ZpiV8NpcLo7iSxeGYrRG0Mqembsa+UrTAV6Q== - dependencies: - commander "^4.0.1" - convert-source-map "^1.1.0" - fs-readdir-recursive "^1.1.0" - glob "^7.0.0" - make-dir "^2.1.0" - slash "^2.0.0" - source-map "^0.5.0" - optionalDependencies: - "@nicolo-ribaudo/chokidar-2" "2.1.8-no-fsevents.3" - chokidar "^3.4.0" - "@babel/code-frame@7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" @@ -32,62 +16,19 @@ dependencies: "@babel/highlight" "^7.10.4" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658" - integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g== - dependencies: - "@babel/highlight" "^7.12.13" - -"@babel/code-frame@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.0.tgz#0dfc80309beec8411e65e706461c408b0bb9b431" - integrity sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA== - dependencies: - "@babel/highlight" "^7.16.0" - -"@babel/code-frame@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" - integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== - dependencies: - "@babel/highlight" "^7.16.7" - -"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.16.0", "@babel/compat-data@^7.16.4": - version "7.16.4" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.16.4.tgz#081d6bbc336ec5c2435c6346b2ae1fb98b5ac68e" - integrity sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q== - -"@babel/compat-data@^7.13.8": - version "7.13.8" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.8.tgz#5b783b9808f15cef71547f1b691f34f8ff6003a6" - integrity sha512-EaI33z19T4qN3xLXsGf48M2cDqa6ei9tPZlfLdb2HC+e/cFtREiRd8hdSqDbwdLB0/+gLwqJmCYASH0z2bUdog== - -"@babel/compat-data@^7.17.7": - version "7.17.7" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.7.tgz#078d8b833fbbcc95286613be8c716cef2b519fa2" - integrity sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ== - -"@babel/core@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.16.0.tgz#c4ff44046f5fe310525cc9eb4ef5147f0c5374d4" - integrity sha512-mYZEvshBRHGsIAiyH5PzCFTCfbWfoYbO/jcSdXQSUQu1/pW0xDZAUP7KEc32heqWTAfAHhV9j1vH8Sav7l+JNQ== - dependencies: - "@babel/code-frame" "^7.16.0" - "@babel/generator" "^7.16.0" - "@babel/helper-compilation-targets" "^7.16.0" - "@babel/helper-module-transforms" "^7.16.0" - "@babel/helpers" "^7.16.0" - "@babel/parser" "^7.16.0" - "@babel/template" "^7.16.0" - "@babel/traverse" "^7.16.0" - "@babel/types" "^7.16.0" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.1.2" - semver "^6.3.0" - source-map "^0.5.0" +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.22.13", "@babel/code-frame@^7.26.2": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/compat-data@^7.26.5": + version "7.26.8" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.8.tgz#821c1d35641c355284d4a870b8a4a7b0c141e367" + integrity sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ== "@babel/core@^7.7.5": version "7.17.9" @@ -110,386 +51,59 @@ json5 "^2.2.1" semver "^6.3.0" -"@babel/eslint-parser@^7.17.0": - version "7.17.0" - resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.17.0.tgz#eabb24ad9f0afa80e5849f8240d0e5facc2d90d6" - integrity sha512-PUEJ7ZBXbRkbq3qqM/jZ2nIuakUBqCYc7Qf52Lj7dlZ6zERnqisdHioL0l4wwQZnmskMeasqUNzLBFKs3nylXA== - dependencies: - eslint-scope "^5.1.1" - eslint-visitor-keys "^2.1.0" - semver "^6.3.0" - -"@babel/generator@^7.13.0": - version "7.13.9" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.9.tgz#3a7aa96f9efb8e2be42d38d80e2ceb4c64d8de39" - integrity sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw== - dependencies: - "@babel/types" "^7.13.0" - jsesc "^2.5.1" - source-map "^0.5.0" - -"@babel/generator@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.0.tgz#d40f3d1d5075e62d3500bccb67f3daa8a95265b2" - integrity sha512-RR8hUCfRQn9j9RPKEVXo9LiwoxLPYn6hNZlvUOR8tSnaxlD0p0+la00ZP9/SnRt6HchKr+X0fO2r8vrETiJGew== - dependencies: - "@babel/types" "^7.16.0" - jsesc "^2.5.1" - source-map "^0.5.0" - -"@babel/generator@^7.17.9": - version "7.17.9" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.9.tgz#f4af9fd38fa8de143c29fce3f71852406fc1e2fc" - integrity sha512-rAdDousTwxbIxbz5I7GEQ3lUip+xVCXooZNbsydCWs3xA7ZsYOv+CFRdzGxRX78BmQHu9B1Eso59AOZQOJDEdQ== - dependencies: - "@babel/types" "^7.17.0" - jsesc "^2.5.1" - source-map "^0.5.0" - -"@babel/helper-annotate-as-pure@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.13.tgz#0f58e86dfc4bb3b1fcd7db806570e177d439b6ab" - integrity sha512-7YXfX5wQ5aYM/BOlbSccHDbuXXFPxeoUmfWtz8le2yTkTZc+BxsiEnENFoi2SlmA8ewDkG2LgIMIVzzn2h8kfw== - dependencies: - "@babel/types" "^7.12.13" - -"@babel/helper-annotate-as-pure@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.0.tgz#9a1f0ebcda53d9a2d00108c4ceace6a5d5f1f08d" - integrity sha512-ItmYF9vR4zA8cByDocY05o0LGUkp1zhbTQOH1NFyl5xXEqlTJQCEJjieriw+aFpxo16swMxUnUiKS7a/r4vtHg== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-builder-binary-assignment-operator-visitor@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.0.tgz#f1a686b92da794020c26582eb852e9accd0d7882" - integrity sha512-9KuleLT0e77wFUku6TUkqZzCEymBdtuQQ27MhEKzf9UOOJu3cYj98kyaDAzxpC7lV6DGiZFuC8XqDsq8/Kl6aQ== - dependencies: - "@babel/helper-explode-assignable-expression" "^7.16.0" - "@babel/types" "^7.16.0" - -"@babel/helper-compilation-targets@^7.13.0": - version "7.13.10" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.10.tgz#1310a1678cb8427c07a753750da4f8ce442bdd0c" - integrity sha512-/Xju7Qg1GQO4mHZ/Kcs6Au7gfafgZnwm+a7sy/ow/tV1sHeraRUHbjdat8/UvDor4Tez+siGKDk6zIKtCPKVJA== - dependencies: - "@babel/compat-data" "^7.13.8" - "@babel/helper-validator-option" "^7.12.17" - browserslist "^4.14.5" - semver "^6.3.0" - -"@babel/helper-compilation-targets@^7.16.0", "@babel/helper-compilation-targets@^7.16.3": - version "7.16.3" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.3.tgz#5b480cd13f68363df6ec4dc8ac8e2da11363cbf0" - integrity sha512-vKsoSQAyBmxS35JUOOt+07cLc6Nk/2ljLIHwmq2/NM6hdioUaqEXq/S+nXvbvXbZkNDlWOymPanJGOc4CBjSJA== +"@babel/generator@^7.17.9", "@babel/generator@^7.26.9": + version "7.26.9" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.9.tgz#75a9482ad3d0cc7188a537aa4910bc59db67cbca" + integrity sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg== dependencies: - "@babel/compat-data" "^7.16.0" - "@babel/helper-validator-option" "^7.14.5" - browserslist "^4.17.5" - semver "^6.3.0" + "@babel/parser" "^7.26.9" + "@babel/types" "^7.26.9" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" "@babel/helper-compilation-targets@^7.17.7": - version "7.17.7" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.7.tgz#a3c2924f5e5f0379b356d4cfb313d1414dc30e46" - integrity sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w== - dependencies: - "@babel/compat-data" "^7.17.7" - "@babel/helper-validator-option" "^7.16.7" - browserslist "^4.17.5" - semver "^6.3.0" - -"@babel/helper-create-class-features-plugin@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.16.0.tgz#090d4d166b342a03a9fec37ef4fd5aeb9c7c6a4b" - integrity sha512-XLwWvqEaq19zFlF5PTgOod4bUA+XbkR4WLQBct1bkzmxJGB0ZEJaoKF4c8cgH9oBtCDuYJ8BP5NB9uFiEgO5QA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.16.0" - "@babel/helper-function-name" "^7.16.0" - "@babel/helper-member-expression-to-functions" "^7.16.0" - "@babel/helper-optimise-call-expression" "^7.16.0" - "@babel/helper-replace-supers" "^7.16.0" - "@babel/helper-split-export-declaration" "^7.16.0" - -"@babel/helper-create-regexp-features-plugin@^7.12.13": - version "7.12.17" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.17.tgz#a2ac87e9e319269ac655b8d4415e94d38d663cb7" - integrity sha512-p2VGmBu9oefLZ2nQpgnEnG0ZlRPvL8gAGvPUMQwUdaE8k49rOMuZpOwdQoy5qJf6K8jL3bcAMhVUlHAjIgJHUg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.12.13" - regexpu-core "^4.7.1" - -"@babel/helper-create-regexp-features-plugin@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.16.0.tgz#06b2348ce37fccc4f5e18dcd8d75053f2a7c44ff" - integrity sha512-3DyG0zAFAZKcOp7aVr33ddwkxJ0Z0Jr5V99y3I690eYLpukJsJvAbzTy1ewoCqsML8SbIrjH14Jc/nSQ4TvNPA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.16.0" - regexpu-core "^4.7.1" - -"@babel/helper-define-polyfill-provider@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.0.tgz#c5b10cf4b324ff840140bb07e05b8564af2ae971" - integrity sha512-7hfT8lUljl/tM3h+izTX/pO3W3frz2ok6Pk+gzys8iJqDfZrZy2pXjRTZAvG2YmfHun1X4q8/UZRLatMfqc5Tg== - dependencies: - "@babel/helper-compilation-targets" "^7.13.0" - "@babel/helper-module-imports" "^7.12.13" - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/traverse" "^7.13.0" - debug "^4.1.1" - lodash.debounce "^4.0.8" - resolve "^1.14.2" - semver "^6.1.2" - -"@babel/helper-environment-visitor@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz#ff484094a839bde9d89cd63cba017d7aae80ecd7" - integrity sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag== - dependencies: - "@babel/types" "^7.16.7" - -"@babel/helper-explode-assignable-expression@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.0.tgz#753017337a15f46f9c09f674cff10cee9b9d7778" - integrity sha512-Hk2SLxC9ZbcOhLpg/yMznzJ11W++lg5GMbxt1ev6TXUiJB0N42KPC+7w8a+eWGuqDnUYuwStJoZHM7RgmIOaGQ== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-function-name@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz#93ad656db3c3c2232559fd7b2c3dbdcbe0eb377a" - integrity sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA== - dependencies: - "@babel/helper-get-function-arity" "^7.12.13" - "@babel/template" "^7.12.13" - "@babel/types" "^7.12.13" - -"@babel/helper-function-name@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.16.0.tgz#b7dd0797d00bbfee4f07e9c4ea5b0e30c8bb1481" - integrity sha512-BZh4mEk1xi2h4HFjWUXRQX5AEx4rvaZxHgax9gcjdLWdkjsY7MKt5p0otjsg5noXw+pB+clMCjw+aEVYADMjog== - dependencies: - "@babel/helper-get-function-arity" "^7.16.0" - "@babel/template" "^7.16.0" - "@babel/types" "^7.16.0" - -"@babel/helper-function-name@^7.17.9": - version "7.17.9" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz#136fcd54bc1da82fcb47565cf16fd8e444b1ff12" - integrity sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg== - dependencies: - "@babel/template" "^7.16.7" - "@babel/types" "^7.17.0" - -"@babel/helper-get-function-arity@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz#bc63451d403a3b3082b97e1d8b3fe5bd4091e583" - integrity sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg== - dependencies: - "@babel/types" "^7.12.13" - -"@babel/helper-get-function-arity@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.0.tgz#0088c7486b29a9cb5d948b1a1de46db66e089cfa" - integrity sha512-ASCquNcywC1NkYh/z7Cgp3w31YW8aojjYIlNg4VeJiHkqyP4AzIvr4qx7pYDb4/s8YcsZWqqOSxgkvjUz1kpDQ== + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz#75d92bb8d8d51301c0d49e52a65c9a7fe94514d8" + integrity sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA== dependencies: - "@babel/types" "^7.16.0" + "@babel/compat-data" "^7.26.5" + "@babel/helper-validator-option" "^7.25.9" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" -"@babel/helper-hoist-variables@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.0.tgz#4c9023c2f1def7e28ff46fc1dbcd36a39beaa81a" - integrity sha512-1AZlpazjUR0EQZQv3sgRNfM9mEVWPK3M6vlalczA+EECcPz3XPh6VplbErL5UoMpChhSck5wAJHthlj1bYpcmg== +"@babel/helper-module-imports@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715" + integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw== dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-hoist-variables@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246" - integrity sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg== - dependencies: - "@babel/types" "^7.16.7" - -"@babel/helper-member-expression-to-functions@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.16.0.tgz#29287040efd197c77636ef75188e81da8bccd5a4" - integrity sha512-bsjlBFPuWT6IWhl28EdrQ+gTvSvj5tqVP5Xeftp07SEuz5pLnsXZuDkDD3Rfcxy0IsHmbZ+7B2/9SHzxO0T+sQ== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.13.tgz#ec67e4404f41750463e455cc3203f6a32e93fcb0" - integrity sha512-NGmfvRp9Rqxy0uHSSVP+SRIW1q31a7Ji10cLBcqSDUngGentY4FRiHOFZFE1CLU5eiL0oE8reH7Tg1y99TDM/g== - dependencies: - "@babel/types" "^7.12.13" - -"@babel/helper-module-imports@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.0.tgz#90538e60b672ecf1b448f5f4f5433d37e79a3ec3" - integrity sha512-kkH7sWzKPq0xt3H1n+ghb4xEMP8k0U7XV3kkB+ZGy69kDk2ySFW1qPi06sjKzFY3t1j6XbJSqr4mF9L7CYVyhg== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-module-imports@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" - integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg== - dependencies: - "@babel/types" "^7.16.7" - -"@babel/helper-module-transforms@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.0.tgz#1c82a8dd4cb34577502ebd2909699b194c3e9bb5" - integrity sha512-My4cr9ATcaBbmaEa8M0dZNA74cfI6gitvUAskgDtAFmAqyFKDSHQo5YstxPbN+lzHl2D9l/YOEFqb2mtUh4gfA== - dependencies: - "@babel/helper-module-imports" "^7.16.0" - "@babel/helper-replace-supers" "^7.16.0" - "@babel/helper-simple-access" "^7.16.0" - "@babel/helper-split-export-declaration" "^7.16.0" - "@babel/helper-validator-identifier" "^7.15.7" - "@babel/template" "^7.16.0" - "@babel/traverse" "^7.16.0" - "@babel/types" "^7.16.0" + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" "@babel/helper-module-transforms@^7.17.7": - version "7.17.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz#3943c7f777139e7954a5355c815263741a9c1cbd" - integrity sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw== - dependencies: - "@babel/helper-environment-visitor" "^7.16.7" - "@babel/helper-module-imports" "^7.16.7" - "@babel/helper-simple-access" "^7.17.7" - "@babel/helper-split-export-declaration" "^7.16.7" - "@babel/helper-validator-identifier" "^7.16.7" - "@babel/template" "^7.16.7" - "@babel/traverse" "^7.17.3" - "@babel/types" "^7.17.0" - -"@babel/helper-optimise-call-expression@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.0.tgz#cecdb145d70c54096b1564f8e9f10cd7d193b338" - integrity sha512-SuI467Gi2V8fkofm2JPnZzB/SUuXoJA5zXe/xzyPP2M04686RzFKFHPK6HDVN6JvWBIEW8tt9hPR7fXdn2Lgpw== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz#806526ce125aed03373bc416a828321e3a6a33af" - integrity sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ== - -"@babel/helper-plugin-utils@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9" - integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ== - -"@babel/helper-remap-async-to-generator@^7.16.0", "@babel/helper-remap-async-to-generator@^7.16.4": - version "7.16.4" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.4.tgz#5d7902f61349ff6b963e07f06a389ce139fbfe6e" - integrity sha512-vGERmmhR+s7eH5Y/cp8PCVzj4XEjerq8jooMfxFdA5xVtAk9Sh4AQsrWgiErUEBjtGrBtOFKDUcWQFW4/dFwMA== + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae" + integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw== dependencies: - "@babel/helper-annotate-as-pure" "^7.16.0" - "@babel/helper-wrap-function" "^7.16.0" - "@babel/types" "^7.16.0" + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@babel/traverse" "^7.25.9" -"@babel/helper-replace-supers@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.16.0.tgz#73055e8d3cf9bcba8ddb55cad93fedc860f68f17" - integrity sha512-TQxuQfSCdoha7cpRNJvfaYxxxzmbxXw/+6cS7V02eeDYyhxderSoMVALvwupA54/pZcOTtVeJ0xccp1nGWladA== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.16.0" - "@babel/helper-optimise-call-expression" "^7.16.0" - "@babel/traverse" "^7.16.0" - "@babel/types" "^7.16.0" +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== -"@babel/helper-simple-access@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.16.0.tgz#21d6a27620e383e37534cf6c10bba019a6f90517" - integrity sha512-o1rjBT/gppAqKsYfUdfHq5Rk03lMQrkPHG1OWzHWpLgVXRH4HnMM9Et9CVdIqwkCQlobnGHEJMsgWP/jE1zUiw== - dependencies: - "@babel/types" "^7.16.0" +"@babel/helper-validator-identifier@^7.16.7", "@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== -"@babel/helper-simple-access@^7.17.7": - version "7.17.7" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz#aaa473de92b7987c6dfa7ce9a7d9674724823367" - integrity sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA== - dependencies: - "@babel/types" "^7.17.0" - -"@babel/helper-skip-transparent-expression-wrappers@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz#0ee3388070147c3ae051e487eca3ebb0e2e8bb09" - integrity sha512-+il1gTy0oHwUsBQZyJvukbB4vPMdcYBrFHa0Uc4AizLxbq6BOYC51Rv4tWocX9BLBDLZ4kc6qUFpQ6HRgL+3zw== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-split-export-declaration@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz#e9430be00baf3e88b0e13e6f9d4eaf2136372b05" - integrity sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg== - dependencies: - "@babel/types" "^7.12.13" - -"@babel/helper-split-export-declaration@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.0.tgz#29672f43663e936df370aaeb22beddb3baec7438" - integrity sha512-0YMMRpuDFNGTHNRiiqJX19GjNXA4H0E8jZ2ibccfSxaCogbm3am5WN/2nQNj0YnQwGWM1J06GOcQ2qnh3+0paw== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-split-export-declaration@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz#0b648c0c42da9d3920d85ad585f2778620b8726b" - integrity sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw== - dependencies: - "@babel/types" "^7.16.7" - -"@babel/helper-validator-identifier@^7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" - integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== - -"@babel/helper-validator-identifier@^7.15.7": - version "7.15.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" - integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== - -"@babel/helper-validator-identifier@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" - integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== - -"@babel/helper-validator-option@^7.12.17": - version "7.12.17" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz#d1fbf012e1a79b7eebbfdc6d270baaf8d9eb9831" - integrity sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw== - -"@babel/helper-validator-option@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3" - integrity sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow== - -"@babel/helper-validator-option@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" - integrity sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ== - -"@babel/helper-wrap-function@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.16.0.tgz#b3cf318afce774dfe75b86767cd6d68f3482e57c" - integrity sha512-VVMGzYY3vkWgCJML+qVLvGIam902mJW0FvT7Avj1zEe0Gn7D93aWdLblYARTxEw+6DhZmtzhBM2zv0ekE5zg1g== - dependencies: - "@babel/helper-function-name" "^7.16.0" - "@babel/template" "^7.16.0" - "@babel/traverse" "^7.16.0" - "@babel/types" "^7.16.0" - -"@babel/helpers@^7.16.0": - version "7.16.3" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.16.3.tgz#27fc64f40b996e7074dc73128c3e5c3e7f55c43c" - integrity sha512-Xn8IhDlBPhvYTvgewPKawhADichOsbkZuzN7qz2BusOM0brChsyXMDJvldWaYMMUNiCQdQzNEioXTp3sC8Nt8w== - dependencies: - "@babel/template" "^7.16.0" - "@babel/traverse" "^7.16.3" - "@babel/types" "^7.16.0" +"@babel/helper-validator-option@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72" + integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw== "@babel/helpers@^7.17.9": version "7.17.9" @@ -500,25 +114,7 @@ "@babel/traverse" "^7.17.9" "@babel/types" "^7.17.0" -"@babel/highlight@^7.10.4", "@babel/highlight@^7.12.13": - version "7.13.10" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.10.tgz#a8b2a66148f5b27d666b15d81774347a731d52d1" - integrity sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg== - dependencies: - "@babel/helper-validator-identifier" "^7.12.11" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/highlight@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.0.tgz#6ceb32b2ca4b8f5f361fb7fd821e3fddf4a1725a" - integrity sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g== - dependencies: - "@babel/helper-validator-identifier" "^7.15.7" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/highlight@^7.16.7": +"@babel/highlight@^7.10.4": version "7.17.9" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.17.9.tgz#61b2ee7f32ea0454612def4fccdae0de232b73e3" integrity sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg== @@ -527,796 +123,47 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/node@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/node/-/node-7.16.0.tgz#855e783ba4cbca88dbdebf4b01c2d95844c4afdf" - integrity sha512-eFUU2RHkgMW0X1lHVVOWJYlaDTwCX2LduQQLfehAfID5VhAjNnBhGZ/r0zk3FSQfFn6enJ2aXyRCiZ829bYVeA== - dependencies: - "@babel/register" "^7.16.0" - commander "^4.0.1" - core-js "^3.19.0" - node-environment-flags "^1.0.5" - regenerator-runtime "^0.13.4" - v8flags "^3.1.1" - -"@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.13.0": - version "7.13.10" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.10.tgz#8f8f9bf7b3afa3eabd061f7a5bcdf4fec3c48409" - integrity sha512-0s7Mlrw9uTWkYua7xWr99Wpk2bnGa0ANleKfksYAES8LpWH4gW1OUr42vqKNf0us5UQNfru2wPqMqRITzq/SIQ== - -"@babel/parser@^7.16.0", "@babel/parser@^7.16.3": - version "7.16.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.4.tgz#d5f92f57cf2c74ffe9b37981c0e72fee7311372e" - integrity sha512-6V0qdPUaiVHH3RtZeLIsc+6pDhbYzHR8ogA8w+f+Wc77DuXto19g2QUwveINoS34Uw+W8/hQDGJCx+i4n7xcng== - -"@babel/parser@^7.16.7", "@babel/parser@^7.17.9": - version "7.17.9" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.9.tgz#9c94189a6062f0291418ca021077983058e171ef" - integrity sha512-vqUSBLP8dQHFPdPi9bc5GK9vRkYHJ49fsZdtoJ8EQ8ibpwk5rPKfvNIwChB0KVXcIjcepEBBd2VHC5r9Gy8ueg== - -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.2": - version "7.16.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.2.tgz#2977fca9b212db153c195674e57cfab807733183" - integrity sha512-h37CvpLSf8gb2lIJ2CgC3t+EjFbi0t8qS7LCS1xcJIlEXE4czlofwaW7W1HA8zpgOCzI9C1nmoqNR1zWkk0pQg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.16.0.tgz#358972eaab006f5eb0826183b0c93cbcaf13e1e2" - integrity sha512-4tcFwwicpWTrpl9qjf7UsoosaArgImF85AxqCRZlgc3IQDvkUHjJpruXAL58Wmj+T6fypWTC/BakfEkwIL/pwA== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" - "@babel/plugin-proposal-optional-chaining" "^7.16.0" - -"@babel/plugin-proposal-async-generator-functions@^7.16.4": - version "7.16.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.4.tgz#e606eb6015fec6fa5978c940f315eae4e300b081" - integrity sha512-/CUekqaAaZCQHleSK/9HajvcD/zdnJiKRiuUFq8ITE+0HsPzquf53cpFiqAwl/UfmJbR6n5uGPQSPdrmKOvHHg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-remap-async-to-generator" "^7.16.4" - "@babel/plugin-syntax-async-generators" "^7.8.4" - -"@babel/plugin-proposal-class-properties@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.0.tgz#c029618267ddebc7280fa286e0f8ca2a278a2d1a" - integrity sha512-mCF3HcuZSY9Fcx56Lbn+CGdT44ioBMMvjNVldpKtj8tpniETdLjnxdHI1+sDWXIM1nNt+EanJOZ3IG9lzVjs7A== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-proposal-class-static-block@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.16.0.tgz#5296942c564d8144c83eea347d0aa8a0b89170e7" - integrity sha512-mAy3sdcY9sKAkf3lQbDiv3olOfiLqI51c9DR9b19uMoR2Z6r5pmGl7dfNFqEvqOyqbf1ta4lknK4gc5PJn3mfA== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - -"@babel/plugin-proposal-dynamic-import@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.0.tgz#783eca61d50526202f9b296095453977e88659f1" - integrity sha512-QGSA6ExWk95jFQgwz5GQ2Dr95cf7eI7TKutIXXTb7B1gCLTCz5hTjFTQGfLFBBiC5WSNi7udNwWsqbbMh1c4yQ== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - -"@babel/plugin-proposal-export-namespace-from@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.16.0.tgz#9c01dee40b9d6b847b656aaf4a3976a71740f222" - integrity sha512-CjI4nxM/D+5wCnhD11MHB1AwRSAYeDT+h8gCdcVJZ/OK7+wRzFsf7PFPWVpVpNRkHMmMkQWAHpTq+15IXQ1diA== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - -"@babel/plugin-proposal-json-strings@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.16.0.tgz#cae35a95ed1d2a7fa29c4dc41540b84a72e9ab25" - integrity sha512-kouIPuiv8mSi5JkEhzApg5Gn6hFyKPnlkO0a9YSzqRurH8wYzSlf6RJdzluAsbqecdW5pBvDJDfyDIUR/vLxvg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-json-strings" "^7.8.3" - -"@babel/plugin-proposal-logical-assignment-operators@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.16.0.tgz#a711b8ceb3ffddd3ef88d3a49e86dbd3cc7db3fd" - integrity sha512-pbW0fE30sVTYXXm9lpVQQ/Vc+iTeQKiXlaNRZPPN2A2VdlWyAtsUrsQ3xydSlDW00TFMK7a8m3cDTkBF5WnV3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - -"@babel/plugin-proposal-nullish-coalescing-operator@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.16.0.tgz#44e1cce08fe2427482cf446a91bb451528ed0596" - integrity sha512-3bnHA8CAFm7cG93v8loghDYyQ8r97Qydf63BeYiGgYbjKKB/XP53W15wfRC7dvKfoiJ34f6Rbyyx2btExc8XsQ== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - -"@babel/plugin-proposal-numeric-separator@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.16.0.tgz#5d418e4fbbf8b9b7d03125d3a52730433a373734" - integrity sha512-FAhE2I6mjispy+vwwd6xWPyEx3NYFS13pikDBWUAFGZvq6POGs5eNchw8+1CYoEgBl9n11I3NkzD7ghn25PQ9Q== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - -"@babel/plugin-proposal-object-rest-spread@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.16.0.tgz#5fb32f6d924d6e6712810362a60e12a2609872e6" - integrity sha512-LU/+jp89efe5HuWJLmMmFG0+xbz+I2rSI7iLc1AlaeSMDMOGzWlc5yJrMN1d04osXN4sSfpo4O+azkBNBes0jg== - dependencies: - "@babel/compat-data" "^7.16.0" - "@babel/helper-compilation-targets" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.16.0" - -"@babel/plugin-proposal-optional-catch-binding@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.16.0.tgz#5910085811ab4c28b00d6ebffa4ab0274d1e5f16" - integrity sha512-kicDo0A/5J0nrsCPbn89mTG3Bm4XgYi0CZtvex9Oyw7gGZE3HXGD0zpQNH+mo+tEfbo8wbmMvJftOwpmPy7aVw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - -"@babel/plugin-proposal-optional-chaining@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.16.0.tgz#56dbc3970825683608e9efb55ea82c2a2d6c8dc0" - integrity sha512-Y4rFpkZODfHrVo70Uaj6cC1JJOt3Pp0MdWSwIKtb8z1/lsjl9AmnB7ErRFV+QNGIfcY1Eruc2UMx5KaRnXjMyg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - -"@babel/plugin-proposal-private-methods@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.16.0.tgz#b4dafb9c717e4301c5776b30d080d6383c89aff6" - integrity sha512-IvHmcTHDFztQGnn6aWq4t12QaBXTKr1whF/dgp9kz84X6GUcwq9utj7z2wFCUfeOup/QKnOlt2k0zxkGFx9ubg== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-proposal-private-property-in-object@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.16.0.tgz#69e935b2c5c79d2488112d886f0c4e2790fee76f" - integrity sha512-3jQUr/HBbMVZmi72LpjQwlZ55i1queL8KcDTQEkAHihttJnAPrcvG9ZNXIfsd2ugpizZo595egYV6xy+pv4Ofw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.16.0" - "@babel/helper-create-class-features-plugin" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - -"@babel/plugin-proposal-unicode-property-regex@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.16.0.tgz#890482dfc5ea378e42e19a71e709728cabf18612" - integrity sha512-ti7IdM54NXv29cA4+bNNKEMS4jLMCbJgl+Drv+FgYy0erJLAxNAIXcNjNjrRZEcWq0xJHsNVwQezskMFpF8N9g== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-proposal-unicode-property-regex@^7.4.4": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.13.tgz#bebde51339be829c17aaaaced18641deb62b39ba" - integrity sha512-XyJmZidNfofEkqFV5VC/bLabGmO5QzenPO/YOfGuEbgU+2sSwMmio3YLb4WtBgcmmdwZHyVyv8on77IUjQ5Gvg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.12.13" - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-async-generators@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-class-properties@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" - integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-class-static-block@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" - integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-dynamic-import@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" - integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-export-namespace-from@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" - integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-syntax-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" - integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" - integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-object-rest-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-chaining@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-private-property-in-object@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" - integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-top-level-await@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" - integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-typescript@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.16.0.tgz#2feeb13d9334cc582ea9111d3506f773174179bb" - integrity sha512-Xv6mEXqVdaqCBfJFyeab0fH2DnUoMsDmhamxsSi4j8nLd4Vtw213WMJr55xxqipC/YVWyPY3K0blJncPYji+dQ== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-arrow-functions@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.0.tgz#951706f8b449c834ed07bd474c0924c944b95a8e" - integrity sha512-vIFb5250Rbh7roWARvCLvIJ/PtAU5Lhv7BtZ1u24COwpI9Ypjsh+bZcKk6rlIyalK+r0jOc1XQ8I4ovNxNrWrA== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-async-to-generator@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.0.tgz#df12637f9630ddfa0ef9d7a11bc414d629d38604" - integrity sha512-PbIr7G9kR8tdH6g8Wouir5uVjklETk91GMVSUq+VaOgiinbCkBP6Q7NN/suM/QutZkMJMvcyAriogcYAdhg8Gw== - dependencies: - "@babel/helper-module-imports" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-remap-async-to-generator" "^7.16.0" - -"@babel/plugin-transform-block-scoped-functions@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.16.0.tgz#c618763233ad02847805abcac4c345ce9de7145d" - integrity sha512-V14As3haUOP4ZWrLJ3VVx5rCnrYhMSHN/jX7z6FAt5hjRkLsb0snPCmJwSOML5oxkKO4FNoNv7V5hw/y2bjuvg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-block-scoping@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.16.0.tgz#bcf433fb482fe8c3d3b4e8a66b1c4a8e77d37c16" - integrity sha512-27n3l67/R3UrXfizlvHGuTwsRIFyce3D/6a37GRxn28iyTPvNXaW4XvznexRh1zUNLPjbLL22Id0XQElV94ruw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-classes@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.0.tgz#54cf5ff0b2242c6573d753cd4bfc7077a8b282f5" - integrity sha512-HUxMvy6GtAdd+GKBNYDWCIA776byUQH8zjnfjxwT1P1ARv/wFu8eBDpmXQcLS/IwRtrxIReGiplOwMeyO7nsDQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.16.0" - "@babel/helper-function-name" "^7.16.0" - "@babel/helper-optimise-call-expression" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-replace-supers" "^7.16.0" - "@babel/helper-split-export-declaration" "^7.16.0" +"@babel/parser@^7.17.9", "@babel/parser@^7.26.9": + version "7.26.9" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.9.tgz#d9e78bee6dc80f9efd8f2349dcfbbcdace280fd5" + integrity sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A== + dependencies: + "@babel/types" "^7.26.9" + +"@babel/template@^7.16.7", "@babel/template@^7.26.9": + version "7.26.9" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.26.9.tgz#4577ad3ddf43d194528cff4e1fa6b232fa609bb2" + integrity sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/parser" "^7.26.9" + "@babel/types" "^7.26.9" + +"@babel/traverse@^7.17.9", "@babel/traverse@^7.25.9": + version "7.26.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.9.tgz#4398f2394ba66d05d988b2ad13c219a2c857461a" + integrity sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.26.9" + "@babel/parser" "^7.26.9" + "@babel/template" "^7.26.9" + "@babel/types" "^7.26.9" + debug "^4.3.1" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.16.0.tgz#e0c385507d21e1b0b076d66bed6d5231b85110b7" - integrity sha512-63l1dRXday6S8V3WFY5mXJwcRAnPYxvFfTlt67bwV1rTyVTM5zrp0DBBb13Kl7+ehkCVwIZPumPpFP/4u70+Tw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-destructuring@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.16.0.tgz#ad3d7e74584ad5ea4eadb1e6642146c590dee33c" - integrity sha512-Q7tBUwjxLTsHEoqktemHBMtb3NYwyJPTJdM+wDwb0g8PZ3kQUIzNvwD5lPaqW/p54TXBc/MXZu9Jr7tbUEUM8Q== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-dotall-regex@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.16.0.tgz#50bab00c1084b6162d0a58a818031cf57798e06f" - integrity sha512-FXlDZfQeLILfJlC6I1qyEwcHK5UpRCFkaoVyA1nk9A1L1Yu583YO4un2KsLBsu3IJb4CUbctZks8tD9xPQubLw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-dotall-regex@^7.4.4": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.13.tgz#3f1601cc29905bfcb67f53910f197aeafebb25ad" - integrity sha512-foDrozE65ZFdUC2OfgeOCrEPTxdB3yjqxpXh8CH+ipd9CHd4s/iq81kcUpyH8ACGNEPdFqbtzfgzbT/ZGlbDeQ== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.12.13" - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-transform-duplicate-keys@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.16.0.tgz#8bc2e21813e3e89e5e5bf3b60aa5fc458575a176" - integrity sha512-LIe2kcHKAZOJDNxujvmp6z3mfN6V9lJxubU4fJIGoQCkKe3Ec2OcbdlYP+vW++4MpxwG0d1wSDOJtQW5kLnkZQ== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-exponentiation-operator@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.16.0.tgz#a180cd2881e3533cef9d3901e48dad0fbeff4be4" - integrity sha512-OwYEvzFI38hXklsrbNivzpO3fh87skzx8Pnqi4LoSYeav0xHlueSoCJrSgTPfnbyzopo5b3YVAJkFIcUpK2wsw== - dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-for-of@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.16.0.tgz#f7abaced155260e2461359bbc7c7248aca5e6bd2" - integrity sha512-5QKUw2kO+GVmKr2wMYSATCTTnHyscl6sxFRAY+rvN7h7WB0lcG0o4NoV6ZQU32OZGVsYUsfLGgPQpDFdkfjlJQ== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-function-name@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.16.0.tgz#02e3699c284c6262236599f751065c5d5f1f400e" - integrity sha512-lBzMle9jcOXtSOXUpc7tvvTpENu/NuekNJVova5lCCWCV9/U1ho2HH2y0p6mBg8fPm/syEAbfaaemYGOHCY3mg== - dependencies: - "@babel/helper-function-name" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-literals@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.16.0.tgz#79711e670ffceb31bd298229d50f3621f7980cac" - integrity sha512-gQDlsSF1iv9RU04clgXqRjrPyyoJMTclFt3K1cjLmTKikc0s/6vE3hlDeEVC71wLTRu72Fq7650kABrdTc2wMQ== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-member-expression-literals@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.16.0.tgz#5251b4cce01eaf8314403d21aedb269d79f5e64b" - integrity sha512-WRpw5HL4Jhnxw8QARzRvwojp9MIE7Tdk3ez6vRyUk1MwgjJN0aNpRoXainLR5SgxmoXx/vsXGZ6OthP6t/RbUg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-modules-amd@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.16.0.tgz#09abd41e18dcf4fd479c598c1cef7bd39eb1337e" - integrity sha512-rWFhWbCJ9Wdmzln1NmSCqn7P0RAD+ogXG/bd9Kg5c7PKWkJtkiXmYsMBeXjDlzHpVTJ4I/hnjs45zX4dEv81xw== - dependencies: - "@babel/helper-module-transforms" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-commonjs@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.16.0.tgz#add58e638c8ddc4875bd9a9ecb5c594613f6c922" - integrity sha512-Dzi+NWqyEotgzk/sb7kgQPJQf7AJkQBWsVp1N6JWc1lBVo0vkElUnGdr1PzUBmfsCCN5OOFya3RtpeHk15oLKQ== +"@babel/types@^7.17.0", "@babel/types@^7.25.9", "@babel/types@^7.26.9": + version "7.26.9" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.9.tgz#08b43dec79ee8e682c2ac631c010bdcac54a21ce" + integrity sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw== dependencies: - "@babel/helper-module-transforms" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-simple-access" "^7.16.0" - babel-plugin-dynamic-import-node "^2.3.3" + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" -"@babel/plugin-transform-modules-systemjs@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.16.0.tgz#a92cf240afeb605f4ca16670453024425e421ea4" - integrity sha512-yuGBaHS3lF1m/5R+6fjIke64ii5luRUg97N2wr+z1sF0V+sNSXPxXDdEEL/iYLszsN5VKxVB1IPfEqhzVpiqvg== - dependencies: - "@babel/helper-hoist-variables" "^7.16.0" - "@babel/helper-module-transforms" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-validator-identifier" "^7.15.7" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-umd@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.16.0.tgz#195f26c2ad6d6a391b70880effce18ce625e06a7" - integrity sha512-nx4f6no57himWiHhxDM5pjwhae5vLpTK2zCnDH8+wNLJy0TVER/LJRHl2bkt6w9Aad2sPD5iNNoUpY3X9sTGDg== - dependencies: - "@babel/helper-module-transforms" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-named-capturing-groups-regex@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.16.0.tgz#d3db61cc5d5b97986559967cd5ea83e5c32096ca" - integrity sha512-LogN88uO+7EhxWc8WZuQ8vxdSyVGxhkh8WTC3tzlT8LccMuQdA81e9SGV6zY7kY2LjDhhDOFdQVxdGwPyBCnvg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.16.0" - -"@babel/plugin-transform-new-target@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.16.0.tgz#af823ab576f752215a49937779a41ca65825ab35" - integrity sha512-fhjrDEYv2DBsGN/P6rlqakwRwIp7rBGLPbrKxwh7oVt5NNkIhZVOY2GRV+ULLsQri1bDqwDWnU3vhlmx5B2aCw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-object-assign@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.16.0.tgz#750c726397f1f6402fb1ceffe9d8ff3595c8a0df" - integrity sha512-TftKY6Hxo5Uf/EIoC3BKQyLvlH46tbtK4xub90vzi9+yS8z1+O/52YHyywCZvYeLPOvv//1j3BPokLuHTWPcbg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-object-super@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.16.0.tgz#fb20d5806dc6491a06296ac14ea8e8d6fedda72b" - integrity sha512-fds+puedQHn4cPLshoHcR1DTMN0q1V9ou0mUjm8whx9pGcNvDrVVrgw+KJzzCaiTdaYhldtrUps8DWVMgrSEyg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-replace-supers" "^7.16.0" - -"@babel/plugin-transform-parameters@^7.16.0", "@babel/plugin-transform-parameters@^7.16.3": - version "7.16.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.16.3.tgz#fa9e4c874ee5223f891ee6fa8d737f4766d31d15" - integrity sha512-3MaDpJrOXT1MZ/WCmkOFo7EtmVVC8H4EUZVrHvFOsmwkk4lOjQj8rzv8JKUZV4YoQKeoIgk07GO+acPU9IMu/w== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-property-literals@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.16.0.tgz#a95c552189a96a00059f6776dc4e00e3690c78d1" - integrity sha512-XLldD4V8+pOqX2hwfWhgwXzGdnDOThxaNTgqagOcpBgIxbUvpgU2FMvo5E1RyHbk756WYgdbS0T8y0Cj9FKkWQ== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-regenerator@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.16.0.tgz#eaee422c84b0232d03aea7db99c97deeaf6125a4" - integrity sha512-JAvGxgKuwS2PihiSFaDrp94XOzzTUeDeOQlcKzVAyaPap7BnZXK/lvMDiubkPTdotPKOIZq9xWXWnggUMYiExg== - dependencies: - regenerator-transform "^0.14.2" - -"@babel/plugin-transform-reserved-words@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.16.0.tgz#fff4b9dcb19e12619394bda172d14f2d04c0379c" - integrity sha512-Dgs8NNCehHSvXdhEhln8u/TtJxfVwGYCgP2OOr5Z3Ar+B+zXicEOKNTyc+eca2cuEOMtjW6m9P9ijOt8QdqWkg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-runtime@^7.16.4": - version "7.16.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.16.4.tgz#f9ba3c7034d429c581e1bd41b4952f3db3c2c7e8" - integrity sha512-pru6+yHANMTukMtEZGC4fs7XPwg35v8sj5CIEmE+gEkFljFiVJxEWxx/7ZDkTK+iZRYo1bFXBtfIN95+K3cJ5A== - dependencies: - "@babel/helper-module-imports" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - babel-plugin-polyfill-corejs2 "^0.3.0" - babel-plugin-polyfill-corejs3 "^0.4.0" - babel-plugin-polyfill-regenerator "^0.3.0" - semver "^6.3.0" - -"@babel/plugin-transform-shorthand-properties@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.0.tgz#090372e3141f7cc324ed70b3daf5379df2fa384d" - integrity sha512-iVb1mTcD8fuhSv3k99+5tlXu5N0v8/DPm2mO3WACLG6al1CGZH7v09HJyUb1TtYl/Z+KrM6pHSIJdZxP5A+xow== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-spread@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.16.0.tgz#d21ca099bbd53ab307a8621e019a7bd0f40cdcfb" - integrity sha512-Ao4MSYRaLAQczZVp9/7E7QHsCuK92yHRrmVNRe/SlEJjhzivq0BSn8mEraimL8wizHZ3fuaHxKH0iwzI13GyGg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" - -"@babel/plugin-transform-sticky-regex@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.16.0.tgz#c35ea31a02d86be485f6aa510184b677a91738fd" - integrity sha512-/ntT2NljR9foobKk4E/YyOSwcGUXtYWv5tinMK/3RkypyNBNdhHUaq6Orw5DWq9ZcNlS03BIlEALFeQgeVAo4Q== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-template-literals@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.16.0.tgz#a8eced3a8e7b8e2d40ec4ec4548a45912630d302" - integrity sha512-Rd4Ic89hA/f7xUSJQk5PnC+4so50vBoBfxjdQAdvngwidM8jYIBVxBZ/sARxD4e0yMXRbJVDrYf7dyRtIIKT6Q== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-typeof-symbol@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.16.0.tgz#8b19a244c6f8c9d668dca6a6f754ad6ead1128f2" - integrity sha512-++V2L8Bdf4vcaHi2raILnptTBjGEFxn5315YU+e8+EqXIucA+q349qWngCLpUYqqv233suJ6NOienIVUpS9cqg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-typescript@^7.16.0": - version "7.16.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.16.1.tgz#cc0670b2822b0338355bc1b3d2246a42b8166409" - integrity sha512-NO4XoryBng06jjw/qWEU2LhcLJr1tWkhpMam/H4eas/CDKMX/b2/Ylb6EI256Y7+FVPCawwSM1rrJNOpDiz+Lg== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-typescript" "^7.16.0" - -"@babel/plugin-transform-unicode-escapes@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.0.tgz#1a354064b4c45663a32334f46fa0cf6100b5b1f3" - integrity sha512-VFi4dhgJM7Bpk8lRc5CMaRGlKZ29W9C3geZjt9beuzSUrlJxsNwX7ReLwaL6WEvsOf2EQkyIJEPtF8EXjB/g2A== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-unicode-regex@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.16.0.tgz#293b80950177c8c85aede87cef280259fb995402" - integrity sha512-jHLK4LxhHjvCeZDWyA9c+P9XH1sOxRd1RO9xMtDVRAOND/PczPqizEtVdx4TQF/wyPaewqpT+tgQFYMnN/P94A== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/preset-env@^7.16.4": - version "7.16.4" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.16.4.tgz#4f6ec33b2a3fe72d6bfdcdf3859500232563a2e3" - integrity sha512-v0QtNd81v/xKj4gNKeuAerQ/azeNn/G1B1qMLeXOcV8+4TWlD2j3NV1u8q29SDFBXx/NBq5kyEAO+0mpRgacjA== - dependencies: - "@babel/compat-data" "^7.16.4" - "@babel/helper-compilation-targets" "^7.16.3" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-validator-option" "^7.14.5" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.16.2" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.16.0" - "@babel/plugin-proposal-async-generator-functions" "^7.16.4" - "@babel/plugin-proposal-class-properties" "^7.16.0" - "@babel/plugin-proposal-class-static-block" "^7.16.0" - "@babel/plugin-proposal-dynamic-import" "^7.16.0" - "@babel/plugin-proposal-export-namespace-from" "^7.16.0" - "@babel/plugin-proposal-json-strings" "^7.16.0" - "@babel/plugin-proposal-logical-assignment-operators" "^7.16.0" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.16.0" - "@babel/plugin-proposal-numeric-separator" "^7.16.0" - "@babel/plugin-proposal-object-rest-spread" "^7.16.0" - "@babel/plugin-proposal-optional-catch-binding" "^7.16.0" - "@babel/plugin-proposal-optional-chaining" "^7.16.0" - "@babel/plugin-proposal-private-methods" "^7.16.0" - "@babel/plugin-proposal-private-property-in-object" "^7.16.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.16.0" - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-class-properties" "^7.12.13" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - "@babel/plugin-syntax-top-level-await" "^7.14.5" - "@babel/plugin-transform-arrow-functions" "^7.16.0" - "@babel/plugin-transform-async-to-generator" "^7.16.0" - "@babel/plugin-transform-block-scoped-functions" "^7.16.0" - "@babel/plugin-transform-block-scoping" "^7.16.0" - "@babel/plugin-transform-classes" "^7.16.0" - "@babel/plugin-transform-computed-properties" "^7.16.0" - "@babel/plugin-transform-destructuring" "^7.16.0" - "@babel/plugin-transform-dotall-regex" "^7.16.0" - "@babel/plugin-transform-duplicate-keys" "^7.16.0" - "@babel/plugin-transform-exponentiation-operator" "^7.16.0" - "@babel/plugin-transform-for-of" "^7.16.0" - "@babel/plugin-transform-function-name" "^7.16.0" - "@babel/plugin-transform-literals" "^7.16.0" - "@babel/plugin-transform-member-expression-literals" "^7.16.0" - "@babel/plugin-transform-modules-amd" "^7.16.0" - "@babel/plugin-transform-modules-commonjs" "^7.16.0" - "@babel/plugin-transform-modules-systemjs" "^7.16.0" - "@babel/plugin-transform-modules-umd" "^7.16.0" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.16.0" - "@babel/plugin-transform-new-target" "^7.16.0" - "@babel/plugin-transform-object-super" "^7.16.0" - "@babel/plugin-transform-parameters" "^7.16.3" - "@babel/plugin-transform-property-literals" "^7.16.0" - "@babel/plugin-transform-regenerator" "^7.16.0" - "@babel/plugin-transform-reserved-words" "^7.16.0" - "@babel/plugin-transform-shorthand-properties" "^7.16.0" - "@babel/plugin-transform-spread" "^7.16.0" - "@babel/plugin-transform-sticky-regex" "^7.16.0" - "@babel/plugin-transform-template-literals" "^7.16.0" - "@babel/plugin-transform-typeof-symbol" "^7.16.0" - "@babel/plugin-transform-unicode-escapes" "^7.16.0" - "@babel/plugin-transform-unicode-regex" "^7.16.0" - "@babel/preset-modules" "^0.1.5" - "@babel/types" "^7.16.0" - babel-plugin-polyfill-corejs2 "^0.3.0" - babel-plugin-polyfill-corejs3 "^0.4.0" - babel-plugin-polyfill-regenerator "^0.3.0" - core-js-compat "^3.19.1" - semver "^6.3.0" - -"@babel/preset-modules@^0.1.5": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9" - integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" - "@babel/plugin-transform-dotall-regex" "^7.4.4" - "@babel/types" "^7.4.4" - esutils "^2.0.2" - -"@babel/preset-typescript@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.16.0.tgz#b0b4f105b855fb3d631ec036cdc9d1ffd1fa5eac" - integrity sha512-txegdrZYgO9DlPbv+9QOVpMnKbOtezsLHWsnsRF4AjbSIsVaujrq1qg8HK0mxQpWv0jnejt0yEoW1uWpvbrDTg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-validator-option" "^7.14.5" - "@babel/plugin-transform-typescript" "^7.16.0" - -"@babel/register@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.16.0.tgz#f5d2aa14df37cf7146b9759f7c53818360f24ec6" - integrity sha512-lzl4yfs0zVXnooeLE0AAfYaT7F3SPA8yB2Bj4W1BiZwLbMS3MZH35ZvCWSRHvneUugwuM+Wsnrj7h0F7UmU3NQ== - dependencies: - clone-deep "^4.0.1" - find-cache-dir "^2.0.0" - make-dir "^2.1.0" - pirates "^4.0.0" - source-map-support "^0.5.16" - -"@babel/runtime@^7.16.3": - version "7.16.3" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5" - integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.8.4": - version "7.13.10" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d" - integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/template@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327" - integrity sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA== - dependencies: - "@babel/code-frame" "^7.12.13" - "@babel/parser" "^7.12.13" - "@babel/types" "^7.12.13" - -"@babel/template@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.0.tgz#d16a35ebf4cd74e202083356fab21dd89363ddd6" - integrity sha512-MnZdpFD/ZdYhXwiunMqqgyZyucaYsbL0IrjoGjaVhGilz+x8YB++kRfygSOIj1yOtWKPlx7NBp+9I1RQSgsd5A== - dependencies: - "@babel/code-frame" "^7.16.0" - "@babel/parser" "^7.16.0" - "@babel/types" "^7.16.0" - -"@babel/template@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" - integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w== - dependencies: - "@babel/code-frame" "^7.16.7" - "@babel/parser" "^7.16.7" - "@babel/types" "^7.16.7" - -"@babel/traverse@^7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.0.tgz#6d95752475f86ee7ded06536de309a65fc8966cc" - integrity sha512-xys5xi5JEhzC3RzEmSGrs/b3pJW/o87SypZ+G/PhaE7uqVQNv/jlmVIBXuoh5atqQ434LfXV+sf23Oxj0bchJQ== - dependencies: - "@babel/code-frame" "^7.12.13" - "@babel/generator" "^7.13.0" - "@babel/helper-function-name" "^7.12.13" - "@babel/helper-split-export-declaration" "^7.12.13" - "@babel/parser" "^7.13.0" - "@babel/types" "^7.13.0" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.19" - -"@babel/traverse@^7.16.0", "@babel/traverse@^7.16.3": - version "7.16.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.3.tgz#f63e8a938cc1b780f66d9ed3c54f532ca2d14787" - integrity sha512-eolumr1vVMjqevCpwVO99yN/LoGL0EyHiLO5I043aYQvwOJ9eR5UsZSClHVCzfhBduMAsSzgA/6AyqPjNayJag== - dependencies: - "@babel/code-frame" "^7.16.0" - "@babel/generator" "^7.16.0" - "@babel/helper-function-name" "^7.16.0" - "@babel/helper-hoist-variables" "^7.16.0" - "@babel/helper-split-export-declaration" "^7.16.0" - "@babel/parser" "^7.16.3" - "@babel/types" "^7.16.0" - debug "^4.1.0" - globals "^11.1.0" - -"@babel/traverse@^7.17.3", "@babel/traverse@^7.17.9": - version "7.17.9" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.9.tgz#1f9b207435d9ae4a8ed6998b2b82300d83c37a0d" - integrity sha512-PQO8sDIJ8SIwipTPiR71kJQCKQYB5NGImbOviK8K+kg5xkNSYXLBupuX9QhatFowrsvo9Hj8WgArg3W7ijNAQw== - dependencies: - "@babel/code-frame" "^7.16.7" - "@babel/generator" "^7.17.9" - "@babel/helper-environment-visitor" "^7.16.7" - "@babel/helper-function-name" "^7.17.9" - "@babel/helper-hoist-variables" "^7.16.7" - "@babel/helper-split-export-declaration" "^7.16.7" - "@babel/parser" "^7.17.9" - "@babel/types" "^7.17.0" - debug "^4.1.0" - globals "^11.1.0" - -"@babel/types@^7.0.0", "@babel/types@^7.12.13", "@babel/types@^7.13.0", "@babel/types@^7.3.0", "@babel/types@^7.4.4": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.0.tgz#74424d2816f0171b4100f0ab34e9a374efdf7f80" - integrity sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA== - dependencies: - "@babel/helper-validator-identifier" "^7.12.11" - lodash "^4.17.19" - to-fast-properties "^2.0.0" - -"@babel/types@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.0.tgz#db3b313804f96aadd0b776c4823e127ad67289ba" - integrity sha512-PJgg/k3SdLsGb3hhisFvtLOw5ts113klrpLuIPtCJIU+BB24fqq6lf8RWqKJEjzqXR9AEH1rIb5XTqwBHB+kQg== - dependencies: - "@babel/helper-validator-identifier" "^7.15.7" - to-fast-properties "^2.0.0" - -"@babel/types@^7.16.7", "@babel/types@^7.17.0": - version "7.17.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.0.tgz#a826e368bccb6b3d84acd76acad5c0d87342390b" - integrity sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw== - dependencies: - "@babel/helper-validator-identifier" "^7.16.7" - to-fast-properties "^2.0.0" +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== "@commitlint/cli@^16.0.2": version "16.0.2" @@ -1469,17 +316,137 @@ dependencies: chalk "^4.0.0" -"@cspotcode/source-map-consumer@0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" - integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== - -"@cspotcode/source-map-support@0.7.0": - version "0.7.0" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" - integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== - dependencies: - "@cspotcode/source-map-consumer" "0.8.0" +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@esbuild/aix-ppc64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz#499600c5e1757a524990d5d92601f0ac3ce87f64" + integrity sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ== + +"@esbuild/android-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz#b9b8231561a1dfb94eb31f4ee056b92a985c324f" + integrity sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g== + +"@esbuild/android-arm@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.0.tgz#ca6e7888942505f13e88ac9f5f7d2a72f9facd2b" + integrity sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g== + +"@esbuild/android-x64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.0.tgz#e765ea753bac442dfc9cb53652ce8bd39d33e163" + integrity sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg== + +"@esbuild/darwin-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz#fa394164b0d89d4fdc3a8a21989af70ef579fa2c" + integrity sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw== + +"@esbuild/darwin-x64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz#91979d98d30ba6e7d69b22c617cc82bdad60e47a" + integrity sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg== + +"@esbuild/freebsd-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz#b97e97073310736b430a07b099d837084b85e9ce" + integrity sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w== + +"@esbuild/freebsd-x64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz#f3b694d0da61d9910ec7deff794d444cfbf3b6e7" + integrity sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A== + +"@esbuild/linux-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz#f921f699f162f332036d5657cad9036f7a993f73" + integrity sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg== + +"@esbuild/linux-arm@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz#cc49305b3c6da317c900688995a4050e6cc91ca3" + integrity sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg== + +"@esbuild/linux-ia32@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz#3e0736fcfab16cff042dec806247e2c76e109e19" + integrity sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg== + +"@esbuild/linux-loong64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz#ea2bf730883cddb9dfb85124232b5a875b8020c7" + integrity sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw== + +"@esbuild/linux-mips64el@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz#4cababb14eede09248980a2d2d8b966464294ff1" + integrity sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ== + +"@esbuild/linux-ppc64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz#8860a4609914c065373a77242e985179658e1951" + integrity sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw== + +"@esbuild/linux-riscv64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz#baf26e20bb2d38cfb86ee282dff840c04f4ed987" + integrity sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA== + +"@esbuild/linux-s390x@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz#8323afc0d6cb1b6dc6e9fd21efd9e1542c3640a4" + integrity sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA== + +"@esbuild/linux-x64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz#08fcf60cb400ed2382e9f8e0f5590bac8810469a" + integrity sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw== + +"@esbuild/netbsd-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz#935c6c74e20f7224918fbe2e6c6fe865b6c6ea5b" + integrity sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw== + +"@esbuild/netbsd-x64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz#414677cef66d16c5a4d210751eb2881bb9c1b62b" + integrity sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA== + +"@esbuild/openbsd-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz#8fd55a4d08d25cdc572844f13c88d678c84d13f7" + integrity sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw== + +"@esbuild/openbsd-x64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz#0c48ddb1494bbc2d6bcbaa1429a7f465fa1dedde" + integrity sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg== + +"@esbuild/sunos-x64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz#86ff9075d77962b60dd26203d7352f92684c8c92" + integrity sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg== + +"@esbuild/win32-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz#849c62327c3229467f5b5cd681bf50588442e96c" + integrity sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw== + +"@esbuild/win32-ia32@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz#f62eb480cd7cca088cb65bb46a6db25b725dc079" + integrity sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA== + +"@esbuild/win32-x64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz#c8e119a30a7c8d60b9d2e22d2073722dde3b710b" + integrity sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ== "@eslint/eslintrc@^0.4.0": version "0.4.0" @@ -1501,6 +468,30 @@ resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@isaacs/fs-minipass@^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz#2d59ae3ab4b38fb4270bfa23d30f8e2e86c7fe32" + integrity sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w== + dependencies: + minipass "^7.0.4" + +"@isaacs/string-locale-compare@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz#291c227e93fd407a96ecd59879a35809120e432b" + integrity sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -1517,58 +508,45 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@jridgewell/gen-mapping@^0.3.0": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" - integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.8" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142" + integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA== dependencies: - "@jridgewell/set-array" "^1.0.1" + "@jridgewell/set-array" "^1.2.1" "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.9" - -"@jridgewell/resolve-uri@^3.0.3": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" - integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + "@jridgewell/trace-mapping" "^0.3.24" -"@jridgewell/set-array@^1.0.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" - integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== +"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -"@jridgewell/source-map@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" - integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== - dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== -"@jridgewell/sourcemap-codec@^1.4.10": - version "1.4.14" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" - integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== -"@jridgewell/trace-mapping@^0.3.0": - version "0.3.4" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz#f6a0832dffd5b8a6aaa633b7d9f8e8e94c83a0c3" - integrity sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ== +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== dependencies: "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@jridgewell/trace-mapping@^0.3.9": - version "0.3.14" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed" - integrity sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ== +"@jridgewell/trace-mapping@^0.3.0", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": - version "2.1.8-no-fsevents.3" - resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz#323d72dd25103d0c4fbdce89dadf574a787b1f9b" - integrity sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ== + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" "@nodelib/fs.scandir@2.1.4": version "2.1.4" @@ -1591,64 +569,475 @@ "@nodelib/fs.scandir" "2.1.4" fastq "^1.6.0" -"@rollup/plugin-babel@^5.3.0": - version "5.3.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz#9cb1c5146ddd6a4968ad96f209c50c62f92f9879" - integrity sha512-9uIC8HZOnVLrLHxayq/PTzw+uS25E14KPUBh5ktF+18Mjo5yK0ToMMx6epY0uEgkjwJw0aBW4x2horYXh8juWw== +"@npmcli/agent@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@npmcli/agent/-/agent-3.0.0.tgz#1685b1fbd4a1b7bb4f930cbb68ce801edfe7aa44" + integrity sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q== dependencies: - "@babel/helper-module-imports" "^7.10.4" - "@rollup/pluginutils" "^3.1.0" + agent-base "^7.1.0" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.1" + lru-cache "^10.0.1" + socks-proxy-agent "^8.0.3" -"@rollup/plugin-commonjs@^17.1.0": - version "17.1.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-17.1.0.tgz#757ec88737dffa8aa913eb392fade2e45aef2a2d" - integrity sha512-PoMdXCw0ZyvjpCMT5aV4nkL0QywxP29sODQsSGeDpr/oI49Qq9tRtAsb/LbYbDzFlOydVEqHmmZWFtXJEAX9ew== +"@npmcli/arborist@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@npmcli/arborist/-/arborist-8.0.0.tgz#681af823ac8ca067404dee57e0f91a3d27d6ef0a" + integrity sha512-APDXxtXGSftyXibl0dZ3CuZYmmVnkiN3+gkqwXshY4GKC2rof2+Lg0sGuj6H1p2YfBAKd7PRwuMVhu6Pf/nQ/A== + dependencies: + "@isaacs/string-locale-compare" "^1.1.0" + "@npmcli/fs" "^4.0.0" + "@npmcli/installed-package-contents" "^3.0.0" + "@npmcli/map-workspaces" "^4.0.1" + "@npmcli/metavuln-calculator" "^8.0.0" + "@npmcli/name-from-folder" "^3.0.0" + "@npmcli/node-gyp" "^4.0.0" + "@npmcli/package-json" "^6.0.1" + "@npmcli/query" "^4.0.0" + "@npmcli/redact" "^3.0.0" + "@npmcli/run-script" "^9.0.1" + bin-links "^5.0.0" + cacache "^19.0.1" + common-ancestor-path "^1.0.1" + hosted-git-info "^8.0.0" + json-parse-even-better-errors "^4.0.0" + json-stringify-nice "^1.1.4" + lru-cache "^10.2.2" + minimatch "^9.0.4" + nopt "^8.0.0" + npm-install-checks "^7.1.0" + npm-package-arg "^12.0.0" + npm-pick-manifest "^10.0.0" + npm-registry-fetch "^18.0.1" + pacote "^19.0.0" + parse-conflict-json "^4.0.0" + proc-log "^5.0.0" + proggy "^3.0.0" + promise-all-reject-late "^1.0.0" + promise-call-limit "^3.0.1" + read-package-json-fast "^4.0.0" + semver "^7.3.7" + ssri "^12.0.0" + treeverse "^3.0.0" + walk-up-path "^3.0.1" + +"@npmcli/config@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@npmcli/config/-/config-9.0.0.tgz#bd810a1e9e23fcfad800e40d6c2c8b8f4f4318e1" + integrity sha512-P5Vi16Y+c8E0prGIzX112ug7XxqfaPFUVW/oXAV+2VsxplKZEnJozqZ0xnK8V8w/SEsBf+TXhUihrEIAU4CA5Q== + dependencies: + "@npmcli/map-workspaces" "^4.0.1" + "@npmcli/package-json" "^6.0.1" + ci-info "^4.0.0" + ini "^5.0.0" + nopt "^8.0.0" + proc-log "^5.0.0" + semver "^7.3.5" + walk-up-path "^3.0.1" + +"@npmcli/fs@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-4.0.0.tgz#a1eb1aeddefd2a4a347eca0fab30bc62c0e1c0f2" + integrity sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q== dependencies: - "@rollup/pluginutils" "^3.1.0" - commondir "^1.0.1" - estree-walker "^2.0.1" - glob "^7.1.6" - is-reference "^1.2.1" - magic-string "^0.25.7" - resolve "^1.17.0" - -"@rollup/plugin-node-resolve@^11.2.0": - version "11.2.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.0.tgz#a5ab88c35bb7622d115f44984dee305112b6f714" - integrity sha512-qHjNIKYt5pCcn+5RUBQxK8krhRvf1HnyVgUCcFFcweDS7fhkOLZeYh0mhHK6Ery8/bb9tvN/ubPzmfF0qjDCTA== - dependencies: - "@rollup/pluginutils" "^3.1.0" - "@types/resolve" "1.17.1" - builtin-modules "^3.1.0" - deepmerge "^4.2.2" - is-module "^1.0.0" - resolve "^1.19.0" - -"@rollup/plugin-replace@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-3.0.1.tgz#f774550f482091719e52e9f14f67ffc0046a883d" - integrity sha512-989J5oRzf3mm0pO/0djTijdfEh9U3n63BIXN5X7T4U9BP+fN4oxQ6DvDuBvFaHA6scaHQRclqmKQEkBhB7k7Hg== + semver "^7.3.5" + +"@npmcli/git@^6.0.0", "@npmcli/git@^6.0.1": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@npmcli/git/-/git-6.0.3.tgz#966cbb228514372877de5244db285b199836f3aa" + integrity sha512-GUYESQlxZRAdhs3UhbB6pVRNUELQOHXwK9ruDkwmCv2aZ5y0SApQzUJCg02p3A7Ue2J5hxvlk1YI53c00NmRyQ== + dependencies: + "@npmcli/promise-spawn" "^8.0.0" + ini "^5.0.0" + lru-cache "^10.0.1" + npm-pick-manifest "^10.0.0" + proc-log "^5.0.0" + promise-retry "^2.0.1" + semver "^7.3.5" + which "^5.0.0" + +"@npmcli/installed-package-contents@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@npmcli/installed-package-contents/-/installed-package-contents-3.0.0.tgz#2c1170ff4f70f68af125e2842e1853a93223e4d1" + integrity sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q== + dependencies: + npm-bundled "^4.0.0" + npm-normalize-package-bin "^4.0.0" + +"@npmcli/map-workspaces@^4.0.1", "@npmcli/map-workspaces@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@npmcli/map-workspaces/-/map-workspaces-4.0.2.tgz#d02c5508bf55624f60aaa58fe413748a5c773802" + integrity sha512-mnuMuibEbkaBTYj9HQ3dMe6L0ylYW+s/gfz7tBDMFY/la0w9Kf44P9aLn4/+/t3aTR3YUHKoT6XQL9rlicIe3Q== + dependencies: + "@npmcli/name-from-folder" "^3.0.0" + "@npmcli/package-json" "^6.0.0" + glob "^10.2.2" + minimatch "^9.0.0" + +"@npmcli/metavuln-calculator@^8.0.0": + version "8.0.1" + resolved "https://registry.yarnpkg.com/@npmcli/metavuln-calculator/-/metavuln-calculator-8.0.1.tgz#c14307a1f0e43524e7ae833d1787c2e0425a9f44" + integrity sha512-WXlJx9cz3CfHSt9W9Opi1PTFc4WZLFomm5O8wekxQZmkyljrBRwATwDxfC9iOXJwYVmfiW1C1dUe0W2aN0UrSg== + dependencies: + cacache "^19.0.0" + json-parse-even-better-errors "^4.0.0" + pacote "^20.0.0" + proc-log "^5.0.0" + semver "^7.3.5" + +"@npmcli/name-from-folder@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@npmcli/name-from-folder/-/name-from-folder-3.0.0.tgz#ed49b18d16b954149f31240e16630cfec511cd57" + integrity sha512-61cDL8LUc9y80fXn+lir+iVt8IS0xHqEKwPu/5jCjxQTVoSCmkXvw4vbMrzAMtmghz3/AkiBjhHkDKUH+kf7kA== + +"@npmcli/node-gyp@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@npmcli/node-gyp/-/node-gyp-4.0.0.tgz#01f900bae62f0f27f9a5a127b40d443ddfb9d4c6" + integrity sha512-+t5DZ6mO/QFh78PByMq1fGSAub/agLJZDRfJRMeOSNCt8s9YVlTjmGpIPwPhvXTGUIJk+WszlT0rQa1W33yzNA== + +"@npmcli/package-json@^6.0.0", "@npmcli/package-json@^6.0.1", "@npmcli/package-json@^6.1.0": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@npmcli/package-json/-/package-json-6.1.1.tgz#78ff92d138fdcb85f31cab907455d5db96d017cb" + integrity sha512-d5qimadRAUCO4A/Txw71VM7UrRZzV+NPclxz/dc+M6B2oYwjWTjqh8HA/sGQgs9VZuJ6I/P7XIAlJvgrl27ZOw== + dependencies: + "@npmcli/git" "^6.0.0" + glob "^10.2.2" + hosted-git-info "^8.0.0" + json-parse-even-better-errors "^4.0.0" + proc-log "^5.0.0" + semver "^7.5.3" + validate-npm-package-license "^3.0.4" + +"@npmcli/promise-spawn@^8.0.0", "@npmcli/promise-spawn@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@npmcli/promise-spawn/-/promise-spawn-8.0.2.tgz#053688f8bc2b4ecc036d2d52c691fd82af58ea5e" + integrity sha512-/bNJhjc+o6qL+Dwz/bqfTQClkEO5nTQ1ZEcdCkAQjhkZMHIh22LPG7fNh1enJP1NKWDqYiiABnjFCY7E0zHYtQ== + dependencies: + which "^5.0.0" + +"@npmcli/query@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@npmcli/query/-/query-4.0.0.tgz#7a2470254f5a12a1499d2296a7343043c7847568" + integrity sha512-3pPbese0fbCiFJ/7/X1GBgxAKYFE8sxBddA7GtuRmOgNseH4YbGsXJ807Ig3AEwNITjDUISHglvy89cyDJnAwA== dependencies: - "@rollup/pluginutils" "^3.1.0" - magic-string "^0.25.7" + postcss-selector-parser "^6.1.2" -"@rollup/pluginutils@^3.1.0": +"@npmcli/redact@^3.0.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@npmcli/redact/-/redact-3.1.1.tgz#ac295c148d01c70a5a006d2e162388b3cef15195" + integrity sha512-3Hc2KGIkrvJWJqTbvueXzBeZlmvoOxc2jyX00yzr3+sNFquJg0N8hH4SAPLPVrkWIRQICVpVgjrss971awXVnA== + +"@npmcli/run-script@^9.0.0", "@npmcli/run-script@^9.0.1": + version "9.0.2" + resolved "https://registry.yarnpkg.com/@npmcli/run-script/-/run-script-9.0.2.tgz#621f993d59bae770104a5b655a38c6579d5ce6be" + integrity sha512-cJXiUlycdizQwvqE1iaAb4VRUM3RX09/8q46zjvy+ct9GhfZRWd7jXYVc1tn/CfRlGPVkX/u4sstRlepsm7hfw== + dependencies: + "@npmcli/node-gyp" "^4.0.0" + "@npmcli/package-json" "^6.0.0" + "@npmcli/promise-spawn" "^8.0.0" + node-gyp "^11.0.0" + proc-log "^5.0.0" + which "^5.0.0" + +"@octokit/auth-token@^5.0.0": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-5.1.2.tgz#68a486714d7a7fd1df56cb9bc89a860a0de866de" + integrity sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw== + +"@octokit/core@^6.0.0": + version "6.1.4" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-6.1.4.tgz#f5ccf911cc95b1ce9daf6de425d1664392f867db" + integrity sha512-lAS9k7d6I0MPN+gb9bKDt7X8SdxknYqAMh44S5L+lNqIN2NuV8nvv3g8rPp7MuRxcOpxpUIATWprO0C34a8Qmg== + dependencies: + "@octokit/auth-token" "^5.0.0" + "@octokit/graphql" "^8.1.2" + "@octokit/request" "^9.2.1" + "@octokit/request-error" "^6.1.7" + "@octokit/types" "^13.6.2" + before-after-hook "^3.0.2" + universal-user-agent "^7.0.0" + +"@octokit/endpoint@^10.1.3": + version "10.1.3" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-10.1.3.tgz#bfe8ff2ec213eb4216065e77654bfbba0fc6d4de" + integrity sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA== + dependencies: + "@octokit/types" "^13.6.2" + universal-user-agent "^7.0.2" + +"@octokit/graphql@^8.1.2": + version "8.2.1" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-8.2.1.tgz#0cb83600e6b4009805acc1c56ae8e07e6c991b78" + integrity sha512-n57hXtOoHrhwTWdvhVkdJHdhTv0JstjDbDRhJfwIRNfFqmSo1DaK/mD2syoNUoLCyqSjBpGAKOG0BuwF392slw== + dependencies: + "@octokit/request" "^9.2.2" + "@octokit/types" "^13.8.0" + universal-user-agent "^7.0.0" + +"@octokit/openapi-types@^23.0.1": + version "23.0.1" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-23.0.1.tgz#3721646ecd36b596ddb12650e0e89d3ebb2dd50e" + integrity sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g== + +"@octokit/plugin-paginate-rest@^11.0.0": + version "11.4.3" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.3.tgz#b5030bba2e0ecff8e6ff7501074c1b209af78ff8" + integrity sha512-tBXaAbXkqVJlRoA/zQVe9mUdb8rScmivqtpv3ovsC5xhje/a+NOCivs7eUhWBwCApJVsR4G5HMeaLbq7PxqZGA== + dependencies: + "@octokit/types" "^13.7.0" + +"@octokit/plugin-retry@^7.0.0": + version "7.1.4" + resolved "https://registry.yarnpkg.com/@octokit/plugin-retry/-/plugin-retry-7.1.4.tgz#da57d1b8a2b83d77423cd6b4af76a0aee5c694ed" + integrity sha512-7AIP4p9TttKN7ctygG4BtR7rrB0anZqoU9ThXFk8nETqIfvgPUANTSYHqWYknK7W3isw59LpZeLI8pcEwiJdRg== + dependencies: + "@octokit/request-error" "^6.1.7" + "@octokit/types" "^13.6.2" + bottleneck "^2.15.3" + +"@octokit/plugin-throttling@^9.0.0": + version "9.4.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-throttling/-/plugin-throttling-9.4.0.tgz#4ed134fe97262361feb0208b7d8df63b35c72eb7" + integrity sha512-IOlXxXhZA4Z3m0EEYtrrACkuHiArHLZ3CvqWwOez/pURNqRuwfoFlTPbN5Muf28pzFuztxPyiUiNwz8KctdZaQ== + dependencies: + "@octokit/types" "^13.7.0" + bottleneck "^2.15.3" + +"@octokit/request-error@^6.1.7": + version "6.1.7" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-6.1.7.tgz#44fc598f5cdf4593e0e58b5155fe2e77230ff6da" + integrity sha512-69NIppAwaauwZv6aOzb+VVLwt+0havz9GT5YplkeJv7fG7a40qpLt/yZKyiDxAhgz0EtgNdNcb96Z0u+Zyuy2g== + dependencies: + "@octokit/types" "^13.6.2" + +"@octokit/request@^9.2.1", "@octokit/request@^9.2.2": + version "9.2.2" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-9.2.2.tgz#754452ec4692d7fdc32438a14e028eba0e6b2c09" + integrity sha512-dZl0ZHx6gOQGcffgm1/Sf6JfEpmh34v3Af2Uci02vzUYz6qEN6zepoRtmybWXIGXFIK8K9ylE3b+duCWqhArtg== + dependencies: + "@octokit/endpoint" "^10.1.3" + "@octokit/request-error" "^6.1.7" + "@octokit/types" "^13.6.2" + fast-content-type-parse "^2.0.0" + universal-user-agent "^7.0.2" + +"@octokit/types@^13.6.2", "@octokit/types@^13.7.0", "@octokit/types@^13.8.0": + version "13.8.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.8.0.tgz#3815885e5abd16ed9ffeea3dced31d37ce3f8a0a" + integrity sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A== + dependencies: + "@octokit/openapi-types" "^23.0.1" + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@pnpm/config.env-replace@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz#ab29da53df41e8948a00f2433f085f54de8b3a4c" + integrity sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w== + +"@pnpm/network.ca-file@^1.0.1": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz#2ab05e09c1af0cdf2fcf5035bea1484e222f7983" + integrity sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA== + dependencies: + graceful-fs "4.2.10" + +"@pnpm/npm-conf@^2.1.0": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz#bb375a571a0bd63ab0a23bece33033c683e9b6b0" + integrity sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw== + dependencies: + "@pnpm/config.env-replace" "^1.1.0" + "@pnpm/network.ca-file" "^1.0.1" + config-chain "^1.1.11" + +"@sec-ant/readable-stream@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz#60de891bb126abfdc5410fdc6166aca065f10a0c" + integrity sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg== + +"@semantic-release/changelog@^6.0.3": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@semantic-release/changelog/-/changelog-6.0.3.tgz#6195630ecbeccad174461de727d5f975abc23eeb" + integrity sha512-dZuR5qByyfe3Y03TpmCvAxCyTnp7r5XwtHRf/8vD9EAn4ZWbavUX8adMtXYzE86EVh0gyLA7lm5yW4IV30XUag== + dependencies: + "@semantic-release/error" "^3.0.0" + aggregate-error "^3.0.0" + fs-extra "^11.0.0" + lodash "^4.17.4" + +"@semantic-release/commit-analyzer@^13.0.0-beta.1": + version "13.0.1" + resolved "https://registry.yarnpkg.com/@semantic-release/commit-analyzer/-/commit-analyzer-13.0.1.tgz#d84b599c3fef623ccc01f0cc2025eb56a57d8feb" + integrity sha512-wdnBPHKkr9HhNhXOhZD5a2LNl91+hs8CC2vsAVYxtZH3y0dV3wKn+uZSN61rdJQZ8EGxzWB3inWocBHV9+u/CQ== + dependencies: + conventional-changelog-angular "^8.0.0" + conventional-changelog-writer "^8.0.0" + conventional-commits-filter "^5.0.0" + conventional-commits-parser "^6.0.0" + debug "^4.0.0" + import-from-esm "^2.0.0" + lodash-es "^4.17.21" + micromatch "^4.0.2" + +"@semantic-release/error@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@semantic-release/error/-/error-3.0.0.tgz#30a3b97bbb5844d695eb22f9d3aa40f6a92770c2" + integrity sha512-5hiM4Un+tpl4cKw3lV4UgzJj+SmfNIDCLLw0TepzQxz9ZGV5ixnqkzIVF+3tp0ZHgcMKE+VNGHJjEeyFG2dcSw== + +"@semantic-release/error@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@semantic-release/error/-/error-4.0.0.tgz#692810288239637f74396976a9340fbc0aa9f6f9" + integrity sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ== + +"@semantic-release/exec@^7.0.3": + version "7.0.3" + resolved "https://registry.yarnpkg.com/@semantic-release/exec/-/exec-7.0.3.tgz#be0b2d8e7e2bcf05076fc48914a643b939c2c151" + integrity sha512-uNWwPNtWi3WTcTm3fWfFQEuj8otOvwoS5m9yo2jSVHuvqdZNsOWmuL0/FqcVyZnCI32fxyYV0G7PPb/TzCH6jw== + dependencies: + "@semantic-release/error" "^4.0.0" + aggregate-error "^3.0.0" + debug "^4.0.0" + execa "^9.0.0" + lodash-es "^4.17.21" + parse-json "^8.0.0" + +"@semantic-release/git@^10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@semantic-release/git/-/git-10.0.1.tgz#c646e55d67fae623875bf3a06a634dd434904498" + integrity sha512-eWrx5KguUcU2wUPaO6sfvZI0wPafUKAMNC18aXY4EnNcrZL86dEmpNVnC9uMpGZkmZJ9EfCVJBQx4pV4EMGT1w== + dependencies: + "@semantic-release/error" "^3.0.0" + aggregate-error "^3.0.0" + debug "^4.0.0" + dir-glob "^3.0.0" + execa "^5.0.0" + lodash "^4.17.4" + micromatch "^4.0.0" + p-reduce "^2.0.0" + +"@semantic-release/github@^11.0.0": + version "11.0.1" + resolved "https://registry.yarnpkg.com/@semantic-release/github/-/github-11.0.1.tgz#127579aa77ddd8586de6f4f57d0e66db3453a876" + integrity sha512-Z9cr0LgU/zgucbT9cksH0/pX9zmVda9hkDPcgIE0uvjMQ8w/mElDivGjx1w1pEQ+MuQJ5CBq3VCF16S6G4VH3A== + dependencies: + "@octokit/core" "^6.0.0" + "@octokit/plugin-paginate-rest" "^11.0.0" + "@octokit/plugin-retry" "^7.0.0" + "@octokit/plugin-throttling" "^9.0.0" + "@semantic-release/error" "^4.0.0" + aggregate-error "^5.0.0" + debug "^4.3.4" + dir-glob "^3.0.1" + globby "^14.0.0" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.0" + issue-parser "^7.0.0" + lodash-es "^4.17.21" + mime "^4.0.0" + p-filter "^4.0.0" + url-join "^5.0.0" + +"@semantic-release/npm@^12.0.0": + version "12.0.1" + resolved "https://registry.yarnpkg.com/@semantic-release/npm/-/npm-12.0.1.tgz#ffb47906de95f8dade8fe0480df0a08dbe1b80c9" + integrity sha512-/6nntGSUGK2aTOI0rHPwY3ZjgY9FkXmEHbW9Kr+62NVOsyqpKKeP0lrCH+tphv+EsNdJNmqqwijTEnVWUMQ2Nw== + dependencies: + "@semantic-release/error" "^4.0.0" + aggregate-error "^5.0.0" + execa "^9.0.0" + fs-extra "^11.0.0" + lodash-es "^4.17.21" + nerf-dart "^1.0.0" + normalize-url "^8.0.0" + npm "^10.5.0" + rc "^1.2.8" + read-pkg "^9.0.0" + registry-auth-token "^5.0.0" + semver "^7.1.2" + tempy "^3.0.0" + +"@semantic-release/release-notes-generator@^14.0.0-beta.1": + version "14.0.3" + resolved "https://registry.yarnpkg.com/@semantic-release/release-notes-generator/-/release-notes-generator-14.0.3.tgz#8f120280ba5ac4b434afe821388c697664e7eb9a" + integrity sha512-XxAZRPWGwO5JwJtS83bRdoIhCiYIx8Vhr+u231pQAsdFIAbm19rSVJLdnBN+Avvk7CKvNQE/nJ4y7uqKH6WTiw== + dependencies: + conventional-changelog-angular "^8.0.0" + conventional-changelog-writer "^8.0.0" + conventional-commits-filter "^5.0.0" + conventional-commits-parser "^6.0.0" + debug "^4.0.0" + get-stream "^7.0.0" + import-from-esm "^2.0.0" + into-stream "^7.0.0" + lodash-es "^4.17.21" + read-package-up "^11.0.0" + +"@sigstore/bundle@^3.1.0": version "3.1.0" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" - integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== + resolved "https://registry.yarnpkg.com/@sigstore/bundle/-/bundle-3.1.0.tgz#74f8f3787148400ddd364be8a9a9212174c66646" + integrity sha512-Mm1E3/CmDDCz3nDhFKTuYdB47EdRFRQMOE/EAbiG1MJW77/w1b3P7Qx7JSrVJs8PfwOLOVcKQCHErIwCTyPbag== dependencies: - "@types/estree" "0.0.39" - estree-walker "^1.0.1" - picomatch "^2.2.2" + "@sigstore/protobuf-specs" "^0.4.0" -"@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0": - version "1.8.2" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.2.tgz#858f5c4b48d80778fde4b9d541f27edc0d56488b" - integrity sha512-sruwd86RJHdsVf/AtBoijDmUqJp3B6hF/DGC23C+JaegnDHaZyewCjoVGTdg3J0uz3Zs7NnIT05OBOmML72lQw== +"@sigstore/core@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sigstore/core/-/core-2.0.0.tgz#f888a8e4c8fdaa27848514a281920b6fd8eca955" + integrity sha512-nYxaSb/MtlSI+JWcwTHQxyNmWeWrUXJJ/G4liLrGG7+tS4vAz6LF3xRXqLH6wPIVUoZQel2Fs4ddLx4NCpiIYg== + +"@sigstore/protobuf-specs@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@sigstore/protobuf-specs/-/protobuf-specs-0.4.0.tgz#7524509d93efcb14e77d0bc34c43a1ae85f851c5" + integrity sha512-o09cLSIq9EKyRXwryWDOJagkml9XgQCoCSRjHOnHLnvsivaW7Qznzz6yjfV7PHJHhIvyp8OH7OX8w0Dc5bQK7A== + +"@sigstore/sign@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@sigstore/sign/-/sign-3.1.0.tgz#5d098d4d2b59a279e9ac9b51c794104cda0c649e" + integrity sha512-knzjmaOHOov1Ur7N/z4B1oPqZ0QX5geUfhrVaqVlu+hl0EAoL4o+l0MSULINcD5GCWe3Z0+YJO8ues6vFlW0Yw== dependencies: - type-detect "4.0.8" + "@sigstore/bundle" "^3.1.0" + "@sigstore/core" "^2.0.0" + "@sigstore/protobuf-specs" "^0.4.0" + make-fetch-happen "^14.0.2" + proc-log "^5.0.0" + promise-retry "^2.0.1" -"@sinonjs/commons@^1.8.3": +"@sigstore/tuf@^3.0.0", "@sigstore/tuf@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@sigstore/tuf/-/tuf-3.1.0.tgz#f533ac8ac572c9f7e36f5e08f1effa6b2244f55a" + integrity sha512-suVMQEA+sKdOz5hwP9qNcEjX6B45R+hFFr4LAWzbRc5O+U2IInwvay/bpG5a4s+qR35P/JK/PiKiRGjfuLy1IA== + dependencies: + "@sigstore/protobuf-specs" "^0.4.0" + tuf-js "^3.0.1" + +"@sigstore/verify@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@sigstore/verify/-/verify-2.1.0.tgz#63e31dd69b678ed6d98cbfdc6d6c104b82d0905c" + integrity sha512-kAAM06ca4CzhvjIZdONAL9+MLppW3K48wOFy1TbuaWFW/OMfl8JuTgW0Bm02JB1WJGT/ET2eqav0KTEKmxqkIA== + dependencies: + "@sigstore/bundle" "^3.1.0" + "@sigstore/core" "^2.0.0" + "@sigstore/protobuf-specs" "^0.4.0" + +"@sindresorhus/is@^4.6.0": + version "4.6.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" + integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== + +"@sindresorhus/merge-streams@^2.1.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz#719df7fb41766bc143369eaa0dd56d8dc87c9958" + integrity sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg== + +"@sindresorhus/merge-streams@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz#abb11d99aeb6d27f1b563c38147a72d50058e339" + integrity sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ== + +"@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.3": version "1.8.3" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" integrity sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ== @@ -1703,38 +1092,18 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== -"@types/babel__core@^7.1.16": - version "7.1.16" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.16.tgz#bc12c74b7d65e82d29876b5d0baf5c625ac58702" - integrity sha512-EAEHtisTMM+KaKwfWdC3oyllIqswlznXCIVCt7/oRNrh+DhgT4UEBNC/jlADNjvw7UnfbcdkGQcPVZ1xYiLcrQ== - dependencies: - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" - "@types/babel__generator" "*" - "@types/babel__template" "*" - "@types/babel__traverse" "*" - -"@types/babel__generator@*": - version "7.6.2" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.2.tgz#f3d71178e187858f7c45e30380f8f1b7415a12d8" - integrity sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ== - dependencies: - "@babel/types" "^7.0.0" - -"@types/babel__template@*": - version "7.4.0" - resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.0.tgz#0c888dd70b3ee9eebb6e4f200e809da0076262be" - integrity sha512-NTPErx4/FiPCGScH7foPyr+/1Dkzkni+rHiYHHoTjvwou7AQzJkNeD60A9CXRy+ZEN2B1bggmkTMCDb+Mv5k+A== - dependencies: - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" +"@tufjs/canonical-json@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz#a52f61a3d7374833fca945b2549bc30a2dd40d0a" + integrity sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA== -"@types/babel__traverse@*": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.11.0.tgz#b9a1efa635201ba9bc850323a8793ee2d36c04a0" - integrity sha512-kSjgDMZONiIfSH1Nxcr5JIRMwUetDki63FSQfpTCz8ogF3Ulqm8+mr5f78dUYs6vMiB6gBusQqfQmBvHZj/lwg== +"@tufjs/models@3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@tufjs/models/-/models-3.0.1.tgz#5aebb782ebb9e06f071ae7831c1f35b462b0319c" + integrity sha512-UUYHISyhCU3ZgN8yaear3cGATHb3SMuKHsQ/nVbHXcmnBf+LzQ/cQfhNG+rfaSHgqGKNEm2cOCLVLELStUQ1JA== dependencies: - "@babel/types" "^7.3.0" + "@tufjs/canonical-json" "2.0.0" + minimatch "^9.0.5" "@types/base64-js@^1.3.0": version "1.3.0" @@ -1780,21 +1149,17 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.46.tgz#0fb6bfbbeabd7a30880504993369c4bf1deab1fe" integrity sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg== -"@types/estree@0.0.39": - version "0.0.39" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" - integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== - "@types/json-schema@*", "@types/json-schema@^7.0.3": version "7.0.7" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== -"@types/jsonwebtoken@~9.0.0": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz#4db9bfaf276ef4fdc3608194fab8b8f2fd1c44f9" - integrity sha512-mM4TkDpA9oixqg1Fv2vVpOFyIVLJjm5x4k0V+K/rEsizfjD7Tk7LKk3GTtbB7KCfP0FEHQtsZqFxYA0+sijNVg== +"@types/jsonwebtoken@^9.0.8": + version "9.0.8" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.8.tgz#313490052801edfb031bb32b6bbd77cc9f230852" + integrity sha512-7fx54m60nLFUVYlxAB1xpe9CBWX2vSrk50Y6ogRJ1v5xxtba7qXTg5BgYDN5dq+yuQQ9HaVlHJyAAt1/mxryFg== dependencies: + "@types/ms" "*" "@types/node" "*" "@types/minimist@^1.2.0": @@ -1807,20 +1172,27 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.0.0.tgz#3205bcd15ada9bc681ac20bef64e9e6df88fd297" integrity sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA== +"@types/ms@*": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" + integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== + "@types/node@*": - version "14.14.32" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.32.tgz#90c5c4a8d72bbbfe53033f122341343249183448" - integrity sha512-/Ctrftx/zp4m8JOujM5ZhwzlWLx22nbQJiVqz8/zE15gOeEW+uly3FSX4fGFpcfEvFzXcMCJwq9lGVWgyARXhg== + version "22.13.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.4.tgz#3fe454d77cd4a2d73c214008b3e331bfaaf5038a" + integrity sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg== + dependencies: + undici-types "~6.20.0" "@types/node@^16.11.11": version "16.11.11" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.11.tgz#6ea7342dfb379ea1210835bada87b3c512120234" integrity sha512-KB0sixD67CeecHC33MYn+eYARkqTheIRNuu97y2XMjR7Wu3XibO1vaY6VBV6O/a89SPI81cEUIYT87UqUWlZNw== -"@types/normalize-package-data@^2.4.0": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" - integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== +"@types/normalize-package-data@^2.4.0", "@types/normalize-package-data@^2.4.3": + version "2.4.4" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" + integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== "@types/parse-json@^4.0.0": version "4.0.0" @@ -1832,37 +1204,6 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.2.2.tgz#e2280c89ddcbeef340099d6968d8c86ba155fdf6" integrity sha512-i99hy7Ki19EqVOl77WplDrvgNugHnsSjECVR/wUrzw2TJXz1zlUfT2ngGckR6xN7yFYaijsMAqPkOLx9HgUqHg== -"@types/resolve@1.17.1": - version "1.17.1" - resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" - integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw== - dependencies: - "@types/node" "*" - -"@types/rollup-plugin-json@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/rollup-plugin-json/-/rollup-plugin-json-3.0.2.tgz#1153136a515ed4fbb7ef214ace496f5fc3ed7796" - integrity sha512-eTRym5nG4HEKDR/KrTnCMYwF7V0hgVjEesvtJCK3V8ho/aT0ZFRFgsDtx38VarM30HCsN372+i4FKYwnhcwiVA== - dependencies: - "@types/node" "*" - rollup "^0.63.4" - -"@types/rollup-plugin-peer-deps-external@^2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@types/rollup-plugin-peer-deps-external/-/rollup-plugin-peer-deps-external-2.2.0.tgz#eae7d8b9d27fa037f5bcaded24e389f85b81973c" - integrity sha512-BiztED+KYJPBI3ihLSOuuZSzy836WAOpC9UrMtDqk3+VeByR8A5pJJ9pCVnq/dfB4wyCeiFL+FlyJnqZuP3pxg== - dependencies: - "@types/node" "*" - rollup "^0.63.4" - -"@types/rollup-plugin-url@^2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@types/rollup-plugin-url/-/rollup-plugin-url-2.2.0.tgz#676c568f44bd5633d76deaf6e064bf27c896856a" - integrity sha512-ekpPWH4pyLICAut2R4jeZpaQzWkHBxs/6pExv0pPyFndCJblS2r0kK2jE8LLQy6wxTvMd8n7IxxEycX5y8Hwpw== - dependencies: - "@types/node" "*" - rollup "^0.63.4" - "@types/sinon@^10.0.6": version "10.0.6" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.6.tgz#bc3faff5154e6ecb69b797d311b7cf0c1b523a1d" @@ -1870,10 +1211,15 @@ dependencies: "@sinonjs/fake-timers" "^7.1.0" -"@types/ws@^7.4.0": - version "7.4.0" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.0.tgz#499690ea08736e05a8186113dac37769ab251a0e" - integrity sha512-Y29uQ3Uy+58bZrFLhX36hcI3Np37nqWE7ky5tjiDoy1GDZnIwVxS0CgF+s+1bXMzjKBFy+fqaRfb708iNzdinw== +"@types/uuid@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d" + integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ== + +"@types/ws@^8.5.14": + version "8.5.14" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.14.tgz#93d44b268c9127d96026cf44353725dd9b6c3c21" + integrity sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw== dependencies: "@types/node" "*" @@ -1978,6 +1324,11 @@ JSONStream@^1.0.4: jsonparse "^1.2.0" through ">=2.2.7 <3" +abbrev@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-3.0.0.tgz#c29a6337e167ac61a84b41b80461b29c5c271a27" + integrity sha512-+/kfrslGQ7TNV2ecmQwMJj/B65g5KVq1/L3SGVZ3tCYGqlzFuFCGBZJtMP99wH3NpEUyAjn0zPdPUg0D+DwrOA== + acorn-jsx@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" @@ -1993,7 +1344,7 @@ acorn@^7.4.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.4.1, acorn@^8.5.0: +acorn@^8.4.1: version "8.8.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== @@ -2003,6 +1354,11 @@ add-stream@^1.0.0: resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" integrity sha1-anmQQ3ynNtXhKI25K9MmbV9csqo= +agent-base@^7.1.0, agent-base@^7.1.2: + version "7.1.3" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.3.tgz#29435eb821bc4194633a5b89e5bc4703bafc25a1" + integrity sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw== + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -2011,6 +1367,14 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" +aggregate-error@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-5.0.0.tgz#ffe15045d7521c51c9d618e3d7f37c13f29b3fd3" + integrity sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw== + dependencies: + clean-stack "^5.2.0" + indent-string "^5.0.0" + ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.6: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -2031,12 +1395,7 @@ ajv@^7.0.2: require-from-string "^2.0.2" uri-js "^4.2.2" -ansi-colors@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" - integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== - -ansi-colors@^4.1.3: +ansi-colors@^4.1.1, ansi-colors@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== @@ -2048,15 +1407,22 @@ ansi-escapes@^6.2.0: dependencies: type-fest "^3.0.0" -ansi-regex@^5.0.0, ansi-regex@^5.0.1: +ansi-escapes@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-7.0.0.tgz#00fc19f491bbb18e1d481b97868204f92109bfe7" + integrity sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw== + dependencies: + environment "^1.0.0" + +ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== +ansi-regex@^6.0.1, ansi-regex@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== ansi-styles@^3.2.1: version "3.2.1" @@ -2072,18 +1438,15 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-styles@^6.0.0, ansi-styles@^6.2.1: +ansi-styles@^6.0.0, ansi-styles@^6.1.0, ansi-styles@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== -anymatch@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" - integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" +any-promise@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== anymatch@~3.1.2: version "3.1.2" @@ -2100,7 +1463,12 @@ append-transform@^2.0.0: dependencies: default-require-extensions "^3.0.0" -archy@^1.0.0: +aproba@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + +archy@^1.0.0, archy@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= @@ -2122,6 +1490,11 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +argv-formatter@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/argv-formatter/-/argv-formatter-1.0.0.tgz#a0ca0cbc29a5b73e836eebe1cbf6c5e0e4eb82f9" + integrity sha512-F2+Hkm9xFaRg+GkaNnbwXNDV5O6pnCFEmqyhvfC/Ic5LbgOWjJh3L+mN/s91rxVL3znE7DYVpW0GJFT+4YBgWw== + array-ify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" @@ -2161,37 +1534,6 @@ axios@^1.6.0: form-data "^4.0.0" proxy-from-env "^1.1.0" -babel-plugin-dynamic-import-node@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" - integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== - dependencies: - object.assign "^4.1.0" - -babel-plugin-polyfill-corejs2@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.0.tgz#407082d0d355ba565af24126fb6cb8e9115251fd" - integrity sha512-wMDoBJ6uG4u4PNFh72Ty6t3EgfA91puCuAwKIazbQlci+ENb/UU9A3xG5lutjUIiXCIn1CY5L15r9LimiJyrSA== - dependencies: - "@babel/compat-data" "^7.13.11" - "@babel/helper-define-polyfill-provider" "^0.3.0" - semver "^6.1.1" - -babel-plugin-polyfill-corejs3@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.4.0.tgz#0b571f4cf3d67f911512f5c04842a7b8e8263087" - integrity sha512-YxFreYwUfglYKdLUGvIF2nJEsGwj+RhWSX/ije3D2vQPOXuyMLMtg/cCGMDpOA7Nd+MwlNdnGODbd2EwUZPlsw== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.3.0" - core-js-compat "^3.18.0" - -babel-plugin-polyfill-regenerator@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.0.tgz#9ebbcd7186e1a33e21c5e20cae4e7983949533be" - integrity sha512-dhAPTDLGoMW5/84wkgwiLRwMnio2i1fUe53EuvtKMv0pn2p3S8OCoV1xAzfJPl0KOX7IB89s2ib85vbYiea3jg== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.3.0" - bail@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776" @@ -2207,10 +1549,31 @@ base64-js@^1.5.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +before-after-hook@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-3.0.2.tgz#d5665a5fa8b62294a5aa0a499f933f4a1016195d" + integrity sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A== + +bin-links@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/bin-links/-/bin-links-5.0.0.tgz#2b0605b62dd5e1ddab3b92a3c4e24221cae06cca" + integrity sha512-sdleLVfCjBtgO5cNjA2HVRvWBJAHs4zwenaCPMNJAJU0yNxpzj80IpjOIimkpkr+mhlA+how5poQtt53PygbHA== + dependencies: + cmd-shim "^7.0.0" + npm-normalize-package-bin "^4.0.0" + proc-log "^5.0.0" + read-cmd-shim "^5.0.0" + write-file-atomic "^6.0.0" + +binary-extensions@^2.0.0, binary-extensions@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +bottleneck@^2.15.3: + version "2.19.5" + resolved "https://registry.yarnpkg.com/bottleneck/-/bottleneck-2.19.5.tgz#5df0b90f59fd47656ebe63c78a98419205cadd91" + integrity sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw== brace-expansion@^1.1.7: version "1.1.11" @@ -2227,39 +1590,27 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== +braces@^3.0.2, braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" browser-stdout@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== -browserslist@^4.14.5: - version "4.16.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717" - integrity sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw== +browserslist@^4.24.0: + version "4.24.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b" + integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A== dependencies: - caniuse-lite "^1.0.30001181" - colorette "^1.2.1" - electron-to-chromium "^1.3.649" - escalade "^3.1.1" - node-releases "^1.1.70" - -browserslist@^4.17.5, browserslist@^4.18.1: - version "4.18.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.18.1.tgz#60d3920f25b6860eb917c6c7b185576f4d8b017f" - integrity sha512-8ScCzdpPwR2wQh8IT82CA2VgDwjHyqMovPBZSNH54+tm4Jk2pCuv90gmAdH6J84OCRWi0b4gMe6O6XPXuJnjgQ== - dependencies: - caniuse-lite "^1.0.30001280" - electron-to-chromium "^1.3.896" - escalade "^3.1.1" - node-releases "^2.0.1" - picocolors "^1.0.0" + caniuse-lite "^1.0.30001688" + electron-to-chromium "^1.5.73" + node-releases "^2.0.19" + update-browserslist-db "^1.1.1" buffer-equal-constant-time@1.0.1: version "1.0.1" @@ -2271,10 +1622,23 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -builtin-modules@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887" - integrity sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA== +cacache@^19.0.0, cacache@^19.0.1: + version "19.0.1" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-19.0.1.tgz#3370cc28a758434c85c2585008bd5bdcff17d6cd" + integrity sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ== + dependencies: + "@npmcli/fs" "^4.0.0" + fs-minipass "^3.0.0" + glob "^10.2.2" + lru-cache "^10.0.1" + minipass "^7.0.3" + minipass-collect "^2.0.1" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + p-map "^7.0.2" + ssri "^12.0.0" + tar "^7.4.3" + unique-filename "^4.0.0" caching-transform@^4.0.0: version "4.0.0" @@ -2286,14 +1650,6 @@ caching-transform@^4.0.0: package-hash "^4.0.0" write-file-atomic "^3.0.0" -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== - dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" - callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -2318,15 +1674,10 @@ camelcase@^6.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== -caniuse-lite@^1.0.30001181: - version "1.0.30001197" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001197.tgz#47ad15b977d2f32b3ec2fe2b087e0c50443771db" - integrity sha512-8aE+sqBqtXz4G8g35Eg/XEaFr2N7rd/VQ6eABGBmNtcB8cN6qNJhMi6oSFy4UWWZgqgL3filHT8Nha4meu3tsw== - -caniuse-lite@^1.0.30001280: - version "1.0.30001284" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001284.tgz#d3653929ded898cd0c1f09a56fd8ca6952df4fca" - integrity sha512-t28SKa7g6kiIQi6NHeOcKrOrGMzCRrXvlasPwWC26TH2QNdglgzQIRUuJ0cR3NeQPH+5jpuveeeSFDLm2zbkEw== +caniuse-lite@^1.0.30001688: + version "1.0.30001699" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001699.tgz#a102cf330d153bf8c92bfb5be3cd44c0a89c8c12" + integrity sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w== chai-arrays@^2.2.0: version "2.2.0" @@ -2367,7 +1718,7 @@ chalk@5.3.0: resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== -chalk@^2.0.0, chalk@^2.4.2: +chalk@^2.0.0, chalk@^2.3.2, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -2376,14 +1727,24 @@ chalk@^2.0.0, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" - integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^5.3.0, chalk@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.4.1.tgz#1b48bf0963ec158dce2aacf69c093ae2dd2092d8" + integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w== + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + character-entities-legacy@^1.0.0: version "1.1.4" resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1" @@ -2404,21 +1765,6 @@ check-error@^1.0.2: resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= -chokidar@^3.4.0: - version "3.5.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" - integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== - dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.5.0" - optionalDependencies: - fsevents "~2.3.1" - chokidar@^3.5.3: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" @@ -2434,16 +1780,53 @@ chokidar@^3.5.3: optionalDependencies: fsevents "~2.3.2" +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +chownr@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-3.0.0.tgz#9855e64ecd240a9cc4267ce8a4aa5d24a1da15e4" + integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g== + ci-info@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== +ci-info@^4.0.0, ci-info@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.1.0.tgz#92319d2fa29d2620180ea5afed31f589bc98cf83" + integrity sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A== + +cidr-regex@^4.1.1: + version "4.1.3" + resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-4.1.3.tgz#df94af8ac16fc2e0791e2824693b957ff1ac4d3e" + integrity sha512-86M1y3ZeQvpZkZejQCcS+IaSWjlDUC+ORP0peScQ4uEUFCZ8bEQVz7NlJHqysoUb6w3zCjx4Mq/8/2RHhMwHYw== + dependencies: + ip-regex "^5.0.0" + clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== +clean-stack@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-5.2.0.tgz#c7a0c91939c7caace30a3bf254e8a8ac276d1189" + integrity sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ== + dependencies: + escape-string-regexp "5.0.0" + +cli-columns@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cli-columns/-/cli-columns-4.0.0.tgz#9fe4d65975238d55218c41bd2ed296a7fa555646" + integrity sha512-XW2Vg+w+L9on9wtwKpyzluIPCWXjaBahI7mTcYjx+BVIYD9c3yqcv/yKC7CmdCZat4rq2yiE1UMSJC5ivKfMtQ== + dependencies: + string-width "^4.2.3" + strip-ansi "^6.0.1" + cli-cursor@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea" @@ -2451,6 +1834,27 @@ cli-cursor@^4.0.0: dependencies: restore-cursor "^4.0.0" +cli-highlight@^2.1.11: + version "2.1.11" + resolved "https://registry.yarnpkg.com/cli-highlight/-/cli-highlight-2.1.11.tgz#49736fa452f0aaf4fae580e30acb26828d2dc1bf" + integrity sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg== + dependencies: + chalk "^4.0.0" + highlight.js "^10.7.1" + mz "^2.4.0" + parse5 "^5.1.1" + parse5-htmlparser2-tree-adapter "^6.0.0" + yargs "^16.0.0" + +cli-table3@^0.6.5: + version "0.6.5" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.5.tgz#013b91351762739c16a9567c21a04632e449bf2f" + integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ== + dependencies: + string-width "^4.2.0" + optionalDependencies: + "@colors/colors" "1.5.0" + cli-truncate@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-4.0.0.tgz#6cc28a2924fee9e25ce91e973db56c7066e6172a" @@ -2477,14 +1881,19 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" -clone-deep@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +cmd-shim@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-7.0.0.tgz#23bcbf69fff52172f7e7c02374e18fb215826d95" + integrity sha512-rtpaCbr164TPPh+zFdkWpCyZuKkjpAzODfaZCf/SVJZzJN+4bHQb/LP3Jzq5/+84um3XXY8r548XiWKSborwVw== collapse-white-space@^1.0.2: version "1.0.6" @@ -2515,11 +1924,6 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -colorette@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" - integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== - colorette@^2.0.20: version "2.0.20" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" @@ -2537,15 +1941,10 @@ commander@11.1.0: resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906" integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ== -commander@^2.20.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - -commander@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" - integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +common-ancestor-path@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz#4f7d2d1394d91b7abdf51871c62f71eadb0182a7" + integrity sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w== commondir@^1.0.1: version "1.0.1" @@ -2580,6 +1979,27 @@ concat-stream@^2.0.0: readable-stream "^3.0.2" typedarray "^0.0.6" +concurrently@^9.1.2: + version "9.1.2" + resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-9.1.2.tgz#22d9109296961eaee773e12bfb1ce9a66bc9836c" + integrity sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ== + dependencies: + chalk "^4.1.2" + lodash "^4.17.21" + rxjs "^7.8.1" + shell-quote "^1.8.1" + supports-color "^8.1.1" + tree-kill "^1.2.2" + yargs "^17.7.2" + +config-chain@^1.1.11: + version "1.1.13" + resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" + integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== + dependencies: + ini "^1.3.4" + proto-list "~1.2.1" + conventional-changelog-angular@^5.0.11, conventional-changelog-angular@^5.0.12: version "5.0.13" resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz#896885d63b914a70d4934b59d2fe7bde1832b28c" @@ -2588,6 +2008,13 @@ conventional-changelog-angular@^5.0.11, conventional-changelog-angular@^5.0.12: compare-func "^2.0.0" q "^1.5.1" +conventional-changelog-angular@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-8.0.0.tgz#5701386850f0e0c2e630b43ee7821d322d87e7a6" + integrity sha512-CLf+zr6St0wIxos4bmaKHRXWAcsCXrJU6F4VdNDrGRK3B8LDLKoX3zuMV5GhtbGkVR/LohZ6MT6im43vZLSjmA== + dependencies: + compare-func "^2.0.0" + conventional-changelog-atom@^2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/conventional-changelog-atom/-/conventional-changelog-atom-2.0.8.tgz#a759ec61c22d1c1196925fca88fe3ae89fd7d8de" @@ -2625,6 +2052,13 @@ conventional-changelog-conventionalcommits@^4.3.1, conventional-changelog-conven lodash "^4.17.15" q "^1.5.1" +conventional-changelog-conventionalcommits@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-8.0.0.tgz#3fa2857c878701e7f0329db5a1257cb218f166fe" + integrity sha512-eOvlTO6OcySPyyyk8pKz2dP4jjElYunj9hn9/s0OB+gapTO8zwS9UQWrZ1pmF2hFs3vw1xhonOLGcGjy/zgsuA== + dependencies: + compare-func "^2.0.0" + conventional-changelog-core@^4.2.1: version "4.2.4" resolved "https://registry.yarnpkg.com/conventional-changelog-core/-/conventional-changelog-core-4.2.4.tgz#e50d047e8ebacf63fac3dc67bf918177001e1e9f" @@ -2701,6 +2135,16 @@ conventional-changelog-writer@^5.0.0: split "^1.0.0" through2 "^4.0.0" +conventional-changelog-writer@^8.0.0: + version "8.0.1" + resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-8.0.1.tgz#656e156ea0ab02b3bb574b7073beeb4f81c5b4bb" + integrity sha512-hlqcy3xHred2gyYg/zXSMXraY2mjAYYo0msUCpK+BGyaVJMFCKWVXPIHiaacGO2GGp13kvHWXFhYmxT4QQqW3Q== + dependencies: + conventional-commits-filter "^5.0.0" + handlebars "^4.7.7" + meow "^13.0.0" + semver "^7.5.2" + conventional-changelog@3.1.24: version "3.1.24" resolved "https://registry.yarnpkg.com/conventional-changelog/-/conventional-changelog-3.1.24.tgz#ebd180b0fd1b2e1f0095c4b04fd088698348a464" @@ -2726,6 +2170,11 @@ conventional-commits-filter@^2.0.7: lodash.ismatch "^4.4.0" modify-values "^1.0.0" +conventional-commits-filter@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/conventional-commits-filter/-/conventional-commits-filter-5.0.0.tgz#72811f95d379e79d2d39d5c0c53c9351ef284e86" + integrity sha512-tQMagCOC59EVgNZcC5zl7XqO30Wki9i9J3acbUvkaosCT6JX3EeFwJD7Qqp4MCikRnzS18WXV3BLIQ66ytu6+Q== + conventional-commits-parser@^3.2.0, conventional-commits-parser@^3.2.2: version "3.2.4" resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-3.2.4.tgz#a7d3b77758a202a9b2293d2112a8d8052c740972" @@ -2738,6 +2187,13 @@ conventional-commits-parser@^3.2.0, conventional-commits-parser@^3.2.2: split2 "^3.0.0" through2 "^4.0.0" +conventional-commits-parser@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-6.1.0.tgz#a650db0c139a99d6c52bb5b192102c7c4bdfb734" + integrity sha512-5nxDo7TwKB5InYBl4ZC//1g9GRwB/F3TXOGR9hgUjMGfvSP4Vu5NkpNro2+1+TIEy1vwxApl5ircECr2ri5JIw== + dependencies: + meow "^13.0.0" + conventional-recommended-bump@6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/conventional-recommended-bump/-/conventional-recommended-bump-6.1.0.tgz#cfa623285d1de554012f2ffde70d9c8a22231f55" @@ -2752,26 +2208,18 @@ conventional-recommended-bump@6.1.0: meow "^8.0.0" q "^1.5.1" -convert-source-map@^1.1.0, convert-source-map@^1.7.0: +convert-hrtime@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/convert-hrtime/-/convert-hrtime-5.0.0.tgz#f2131236d4598b95de856926a67100a0a97e9fa3" + integrity sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg== + +convert-source-map@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== dependencies: safe-buffer "~5.1.1" -core-js-compat@^3.18.0, core-js-compat@^3.19.1: - version "3.19.2" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.19.2.tgz#18066a3404a302433cb0aa8be82dd3d75c76e5c4" - integrity sha512-ObBY1W5vx/LFFMaL1P5Udo4Npib6fu+cMokeziWkA8Tns4FcDemKF5j9JvaI5JhdkW8EQJQGJN1EcrzmEwuAqQ== - dependencies: - browserslist "^4.18.1" - semver "7.0.0" - -core-js@^3.19.0: - version "3.19.2" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.19.2.tgz#ae216d7f4f7e924d9a2e3ff1e4b1940220f9157b" - integrity sha512-ciYCResnLIATSsXuXnIOH4CbdfgV+H1Ltg16hJFN7/v6OxqnFr/IFGeLacaZ+fHLAm0TBbXwNK9/DNBzBUrO/g== - core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -2785,7 +2233,7 @@ cosmiconfig-typescript-loader@^1.0.0: cosmiconfig "^7" ts-node "^10.4.0" -cosmiconfig@^7: +cosmiconfig@^7, cosmiconfig@^7.0.0: version "7.0.1" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== @@ -2796,16 +2244,15 @@ cosmiconfig@^7: path-type "^4.0.0" yaml "^1.10.0" -cosmiconfig@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3" - integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA== +cosmiconfig@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-9.0.0.tgz#34c3fc58287b915f3ae905ab6dc3de258b55ad9d" + integrity sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg== dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.2.1" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.10.0" + env-paths "^2.2.1" + import-fresh "^3.3.0" + js-yaml "^4.1.0" + parse-json "^5.2.0" create-require@^1.1.0: version "1.1.1" @@ -2821,6 +2268,18 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +crypto-random-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-4.0.0.tgz#5a3cc53d7dd86183df5da0312816ceeeb5bb1fc2" + integrity sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA== + dependencies: + type-fest "^1.0.1" + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + dargs@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" @@ -2831,6 +2290,13 @@ dateformat@^3.0.0: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== +debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4, debug@^4.3.5, debug@^4.3.6: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + debug@4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" @@ -2838,20 +2304,6 @@ debug@4.3.4: dependencies: ms "2.1.2" -debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== - dependencies: - ms "2.1.2" - -debug@^4.3.5: - version "4.3.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" - integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== - dependencies: - ms "2.1.2" - decamelize-keys@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" @@ -2877,16 +2329,16 @@ deep-eql@^3.0.1: dependencies: type-detect "^4.0.0" +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + deep-is@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= -deepmerge@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" - integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== - default-require-extensions@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-3.0.0.tgz#e03f93aac9b2b6443fc52e5e4a37b3ad9ad8df96" @@ -2894,13 +2346,6 @@ default-require-extensions@^3.0.0: dependencies: strip-bom "^4.0.0" -define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -2921,17 +2366,12 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -diff@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" - integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== - -diff@^5.2.0: +diff@^5.0.0, diff@^5.1.0, diff@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== -dir-glob@^3.0.1: +dir-glob@^3.0.0, dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== @@ -2965,6 +2405,18 @@ dotgitignore@^2.1.0: find-up "^3.0.0" minimatch "^3.0.4" +duplexer2@~0.1.0: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + integrity sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA== + dependencies: + readable-stream "^2.0.2" + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + ecdsa-sig-formatter@1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" @@ -2972,15 +2424,10 @@ ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer "^5.0.1" -electron-to-chromium@^1.3.649: - version "1.3.683" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.683.tgz#2c9ab53ff5275cf3dd49278af714d0f8975204f7" - integrity sha512-8mFfiAesXdEdE0DhkMKO7W9U6VU/9T3VTWwZ+4g84/YMP4kgwgFtQgUxuu7FUMcvSeKSNhFQNU+WZ68BQTLT5A== - -electron-to-chromium@^1.3.896: - version "1.4.10" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.10.tgz#5f44ae6f6725b1949d6e8d34352f80d4c1880734" - integrity sha512-tFgA40Iq2oy4k2PnZrLJowbgpij+lD6ZLxkw8Ht1NKTYyN8dvSvC5xlo8X0WW2jqhKSzITrbr5mpB4/AZ/8OUA== +electron-to-chromium@^1.5.73: + version "1.5.100" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.100.tgz#c99b7cfe49ec72c5e22237f036bb8b1d8b7f0621" + integrity sha512-u1z9VuzDXV86X2r3vAns0/5ojfXBue9o0+JDUDBKYqGLjxLkSqsSUoPU/6kW0gx76V44frHaf6Zo+QF74TQCMg== emoji-regex@^10.3.0: version "10.3.0" @@ -2992,6 +2439,23 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +emojilib@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/emojilib/-/emojilib-2.4.0.tgz#ac518a8bb0d5f76dda57289ccb2fdf9d39ae721e" + integrity sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw== + +encoding@^0.1.13: + version "0.1.13" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" + enquirer@^2.3.5: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" @@ -2999,6 +2463,29 @@ enquirer@^2.3.5: dependencies: ansi-colors "^4.1.1" +env-ci@^11.0.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/env-ci/-/env-ci-11.1.0.tgz#b26eeb692f76c1f69ddc1fb2d4a3d371088a54f9" + integrity sha512-Z8dnwSDbV1XYM9SBF2J0GcNVvmfmfh3a49qddGIROhBoVro6MZVTji15z/sJbQ2ko2ei8n988EU1wzoLU/tF+g== + dependencies: + execa "^8.0.0" + java-properties "^1.0.2" + +env-paths@^2.2.0, env-paths@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + +environment@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/environment/-/environment-1.1.0.tgz#8e86c66b180f363c7ab311787e0259665f45a9f1" + integrity sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q== + +err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -3006,46 +2493,51 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.18.0-next.2: - version "1.18.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0.tgz#ab80b359eecb7ede4c298000390bc5ac3ec7b5a4" - integrity sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw== - dependencies: - call-bind "^1.0.2" - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - get-intrinsic "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.2" - is-callable "^1.2.3" - is-negative-zero "^2.0.1" - is-regex "^1.1.2" - is-string "^1.0.5" - object-inspect "^1.9.0" - object-keys "^1.1.1" - object.assign "^4.1.2" - string.prototype.trimend "^1.0.4" - string.prototype.trimstart "^1.0.4" - unbox-primitive "^1.0.0" - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - es6-error@^4.0.1: version "4.1.1" resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +esbuild@^0.25.0: + version "0.25.0" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.0.tgz#0de1787a77206c5a79eeb634a623d39b5006ce92" + integrity sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw== + optionalDependencies: + "@esbuild/aix-ppc64" "0.25.0" + "@esbuild/android-arm" "0.25.0" + "@esbuild/android-arm64" "0.25.0" + "@esbuild/android-x64" "0.25.0" + "@esbuild/darwin-arm64" "0.25.0" + "@esbuild/darwin-x64" "0.25.0" + "@esbuild/freebsd-arm64" "0.25.0" + "@esbuild/freebsd-x64" "0.25.0" + "@esbuild/linux-arm" "0.25.0" + "@esbuild/linux-arm64" "0.25.0" + "@esbuild/linux-ia32" "0.25.0" + "@esbuild/linux-loong64" "0.25.0" + "@esbuild/linux-mips64el" "0.25.0" + "@esbuild/linux-ppc64" "0.25.0" + "@esbuild/linux-riscv64" "0.25.0" + "@esbuild/linux-s390x" "0.25.0" + "@esbuild/linux-x64" "0.25.0" + "@esbuild/netbsd-arm64" "0.25.0" + "@esbuild/netbsd-x64" "0.25.0" + "@esbuild/openbsd-arm64" "0.25.0" + "@esbuild/openbsd-x64" "0.25.0" + "@esbuild/sunos-x64" "0.25.0" + "@esbuild/win32-arm64" "0.25.0" + "@esbuild/win32-ia32" "0.25.0" + "@esbuild/win32-x64" "0.25.0" + +escalade@^3.1.1, escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-string-regexp@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" + integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== escape-string-regexp@^1.0.5: version "1.0.5" @@ -3062,13 +2554,6 @@ eslint-config-prettier@^8.1.0: resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.1.0.tgz#4ef1eaf97afe5176e6a75ddfb57c335121abc5a6" integrity sha512-oKMhGv3ihGbCIimCAjqkdzx2Q+jthoqnXSP+d86M9tptwugycmTFdVR4IpLgq2c4SHifbwO90z2fQ8/Aio73yw== -eslint-plugin-babel@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-babel/-/eslint-plugin-babel-5.3.1.tgz#75a2413ffbf17e7be57458301c60291f2cfbf560" - integrity sha512-VsQEr6NH3dj664+EyxJwO4FCYm/00JhYb3Sk3ft8o+fpKuIfQ9TaW6uVUfvwMXHcf/lsnRIoyFPsLMyiWCSL/g== - dependencies: - eslint-rule-composer "^0.3.0" - eslint-plugin-markdown@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/eslint-plugin-markdown/-/eslint-plugin-markdown-2.0.0.tgz#cd650beda2b599cd9e4535ea369266b5d0e49d23" @@ -3098,11 +2583,6 @@ eslint-plugin-typescript-sort-keys@1.5.0: json-schema "^0.2.5" natural-compare-lite "^1.4.0" -eslint-rule-composer@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9" - integrity sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg== - eslint-scope@^5.0.0, eslint-scope@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" @@ -3128,11 +2608,6 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== -eslint-visitor-keys@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" - integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== - eslint@7.21.0: version "7.21.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.21.0.tgz#4ecd5b8c5b44f5dedc9b8a110b01bbfeb15d1c83" @@ -3214,16 +2689,6 @@ estraverse@^5.1.0, estraverse@^5.2.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== -estree-walker@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" - integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== - -estree-walker@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" - integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== - esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -3234,7 +2699,7 @@ eventemitter3@^5.0.1: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== -execa@8.0.1: +execa@8.0.1, execa@^8.0.0: version "8.0.1" resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== @@ -3264,11 +2729,39 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" +execa@^9.0.0: + version "9.5.2" + resolved "https://registry.yarnpkg.com/execa/-/execa-9.5.2.tgz#a4551034ee0795e241025d2f987dab3f4242dff2" + integrity sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q== + dependencies: + "@sindresorhus/merge-streams" "^4.0.0" + cross-spawn "^7.0.3" + figures "^6.1.0" + get-stream "^9.0.0" + human-signals "^8.0.0" + is-plain-obj "^4.1.0" + is-stream "^4.0.1" + npm-run-path "^6.0.0" + pretty-ms "^9.0.0" + signal-exit "^4.1.0" + strip-final-newline "^4.0.0" + yoctocolors "^2.0.0" + +exponential-backoff@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.2.tgz#a8f26adb96bf78e8cd8ad1037928d5e5c0679d91" + integrity sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA== + extend@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== +fast-content-type-parse@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz#c236124534ee2cb427c8d8e5ba35a4856947847b" + integrity sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q== + fast-deep-equal@^3.1.1: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -3279,17 +2772,16 @@ fast-diff@^1.1.2: resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== -fast-glob@^3.1.1: - version "3.2.5" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" - integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== +fast-glob@^3.1.1, fast-glob@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.0" + glob-parent "^5.1.2" merge2 "^1.3.0" - micromatch "^4.0.2" - picomatch "^2.2.1" + micromatch "^4.0.8" fast-json-stable-stringify@^2.0.0: version "2.1.0" @@ -3301,6 +2793,11 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fastest-levenshtein@^1.0.16: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + fastq@^1.6.0: version "1.11.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" @@ -3308,6 +2805,13 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +figures@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + integrity sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA== + dependencies: + escape-string-regexp "^1.0.5" + figures@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -3315,6 +2819,13 @@ figures@^3.1.0: dependencies: escape-string-regexp "^1.0.5" +figures@^6.0.0, figures@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-6.1.0.tgz#935479f51865fa7479f6fa94fc6fc7ac14e62c4a" + integrity sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg== + dependencies: + is-unicode-supported "^2.0.0" + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -3322,22 +2833,13 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" -find-cache-dir@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" - integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== - dependencies: - commondir "^1.0.1" - make-dir "^2.0.0" - pkg-dir "^3.0.0" - find-cache-dir@^3.2.0: version "3.3.2" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" @@ -3347,6 +2849,11 @@ find-cache-dir@^3.2.0: make-dir "^3.0.2" pkg-dir "^4.1.0" +find-up-simple@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/find-up-simple/-/find-up-simple-1.0.0.tgz#21d035fde9fdbd56c8f4d2f63f32fd93a1cfc368" + integrity sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw== + find-up@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" @@ -3384,6 +2891,14 @@ find-versions@^4.0.0: dependencies: semver-regex "^3.1.2" +find-versions@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-6.0.0.tgz#fda285d3bb7c0c098f09e0727c54d31735f0c7d1" + integrity sha512-2kCCtc+JvcZ86IGAz3Z2Y0A1baIz9fL31pH/0S1IqZr9Iwnjq8izfPtrCyQKO6TLMPELLsQMre7VDqeIKCsHkA== + dependencies: + semver-regex "^4.0.5" + super-regex "^1.0.0" + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -3415,6 +2930,14 @@ foreground-child@^2.0.0: cross-spawn "^7.0.0" signal-exit "^3.0.2" +foreground-child@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" + integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" @@ -3424,6 +2947,14 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +from2@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + integrity sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g== + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + fromentries@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/fromentries/-/fromentries-1.3.2.tgz#e4bca6808816bf8f93b52750f1127f5a6fd86e3a" @@ -3445,25 +2976,48 @@ fs-extra@^10.0.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs-readdir-recursive@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27" - integrity sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA== +fs-extra@^11.0.0: + version "11.3.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.3.0.tgz#0daced136bbaf65a555a326719af931adc7a314d" + integrity sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs-minipass@^3.0.0, fs-minipass@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-3.0.3.tgz#79a85981c4dc120065e96f62086bf6f9dc26cc54" + integrity sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw== + dependencies: + minipass "^7.0.3" fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@~2.3.1, fsevents@~2.3.2: +fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +function-timeout@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/function-timeout/-/function-timeout-1.0.2.tgz#e5a7b6ffa523756ff20e1231bbe37b5f373aadd5" + integrity sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA== functional-red-black-tree@^1.0.1: version "1.0.1" @@ -3490,15 +3044,6 @@ get-func-name@^2.0.0: resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" - integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" @@ -3519,11 +3064,36 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +get-stream@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-7.0.1.tgz#1664dfe7d1678540ea6a4da3ae7cd59bf4e4a91e" + integrity sha512-3M8C1EOFN6r8AMUhwUAACIoXZJEOufDU5+0gFFN5uNs6XYOralD2Pqkl7m046va6x77FwposWXbAhPPIOus7mQ== + get-stream@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== +get-stream@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-9.0.1.tgz#95157d21df8eb90d1647102b63039b1df60ebd27" + integrity sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA== + dependencies: + "@sec-ant/readable-stream" "^0.4.1" + is-stream "^4.0.1" + +git-log-parser@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/git-log-parser/-/git-log-parser-1.2.1.tgz#44355787b37af7560dcc4ddc01cb53b5d139cc28" + integrity sha512-PI+sPDvHXNPl5WNOErAK05s3j0lgwUzMN6o8cyQrDaKfT3qd7TmNJKeXX+SknI5I0QhG5fVPAEwSY4tRGDtYoQ== + dependencies: + argv-formatter "~1.0.0" + spawn-error-forwarder "~1.0.0" + split2 "~1.0.0" + stream-combiner2 "~1.1.1" + through2 "~2.0.0" + traverse "0.6.8" + git-raw-commits@^2.0.0, git-raw-commits@^2.0.8: version "2.0.11" resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-2.0.11.tgz#bc3576638071d18655e1cc60d7f524920008d723" @@ -3558,26 +3128,26 @@ gitconfiglocal@^1.0.0: dependencies: ini "^1.3.2" -glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0, glob-parent@~5.1.2: +glob-parent@^5.0.0, glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" -glob@^7.0.0, glob@^7.1.3, glob@^7.1.6: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== +glob@^10.2.2, glob@^10.3.10, glob@^10.3.7, glob@^10.4.5: + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" -glob@^7.1.4: +glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -3589,17 +3159,6 @@ glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - global-dirs@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" @@ -3631,15 +3190,27 @@ globby@^11.0.1: merge2 "^1.3.0" slash "^3.0.0" -graceful-fs@^4.1.15: +globby@^14.0.0: + version "14.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-14.1.0.tgz#138b78e77cf5a8d794e327b15dce80bf1fb0a73e" + integrity sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA== + dependencies: + "@sindresorhus/merge-streams" "^2.1.0" + fast-glob "^3.3.3" + ignore "^7.0.3" + path-type "^6.0.0" + slash "^5.1.0" + unicorn-magic "^0.3.0" + +graceful-fs@4.2.10: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: - version "4.2.9" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" - integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== +graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.6: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== handlebars@^4.7.7: version "4.7.7" @@ -3658,11 +3229,6 @@ hard-rejection@^2.1.0: resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== -has-bigints@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" - integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== - has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -3673,18 +3239,6 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.0.0, has-symbols@^1.0.1, has-symbols@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" - integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - hasha@^5.0.0: version "5.2.2" resolved "https://registry.yarnpkg.com/hasha/-/hasha-5.2.2.tgz#a48477989b3b327aea3c04f53096d816d97522a1" @@ -3693,17 +3247,27 @@ hasha@^5.0.0: is-stream "^2.0.0" type-fest "^0.8.0" +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -homedir-polyfill@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" - integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== - dependencies: - parse-passwd "^1.0.0" +highlight.js@^10.7.1: + version "10.7.3" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" + integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== + +hook-std@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hook-std/-/hook-std-3.0.0.tgz#47038a01981e07ce9d83a6a3b2eb98cad0f7bd58" + integrity sha512-jHRQzjSDzMtFy34AGj1DN+vq54WVuhSvKgrHf0OMiFQTwDD4L/qqofVEWjLOBMTn5+lCD3fPg32W9yOfnEJTTw== hosted-git-info@^2.1.4: version "2.8.9" @@ -3717,11 +3281,46 @@ hosted-git-info@^4.0.0, hosted-git-info@^4.0.1: dependencies: lru-cache "^6.0.0" +hosted-git-info@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-7.0.2.tgz#9b751acac097757667f30114607ef7b661ff4f17" + integrity sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w== + dependencies: + lru-cache "^10.0.1" + +hosted-git-info@^8.0.0, hosted-git-info@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-8.0.2.tgz#5bd7d8b5395616e41cc0d6578381a32f669b14b2" + integrity sha512-sYKnA7eGln5ov8T8gnYlkSOxFJvywzEx9BueN6xo/GKO8PGiI6uK6xx+DIGe45T3bdVjLAQDQW1aicT8z8JwQg== + dependencies: + lru-cache "^10.0.1" + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +http-cache-semantics@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== + +http-proxy-agent@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" + integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + +https-proxy-agent@^7.0.0, https-proxy-agent@^7.0.1: + version "7.0.6" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" + integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== + dependencies: + agent-base "^7.1.2" + debug "4" + human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -3732,6 +3331,11 @@ human-signals@^5.0.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== +human-signals@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-8.0.0.tgz#2d3d63481c7c2319f0373428b01ffe30da6df852" + integrity sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA== + husky@^4.3.8: version "4.3.8" resolved "https://registry.yarnpkg.com/husky/-/husky-4.3.8.tgz#31144060be963fd6850e5cc8f019a1dfe194296d" @@ -3748,6 +3352,20 @@ husky@^4.3.8: slash "^3.0.0" which-pm-runs "^1.0.0" +iconv-lite@^0.6.2: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +ignore-walk@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-7.0.0.tgz#8350e475cf4375969c12eb49618b3fd9cca6704f" + integrity sha512-T4gbf83A4NH95zvhVYZc+qWocBBGlpzUXLPGurJggw/WIOwicfXJChLDP/iBZnN5WqROSu5Bm3hhle4z8a8YGQ== + dependencies: + minimatch "^9.0.0" + ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" @@ -3758,14 +3376,32 @@ ignore@^5.1.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== -import-fresh@^3.0.0, import-fresh@^3.2.1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== +ignore@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.3.tgz#397ef9315dfe0595671eefe8b633fec6943ab733" + integrity sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA== + +import-fresh@^3.0.0, import-fresh@^3.2.1, import-fresh@^3.3.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" + integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== dependencies: parent-module "^1.0.0" resolve-from "^4.0.0" +import-from-esm@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-from-esm/-/import-from-esm-2.0.0.tgz#184eb9aad4f557573bd6daf967ad5911b537797a" + integrity sha512-YVt14UZCgsX1vZQ3gKjkWVdBdHQ6eu3MPU1TBgL1H5orXe2+jWD006WCPPtOuwlQm10NuzOW5WawiF1Q9veW8g== + dependencies: + debug "^4.3.4" + import-meta-resolve "^4.0.0" + +import-meta-resolve@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz#f9db8bead9fafa61adb811db77a2bf22c5399706" + integrity sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw== + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -3776,6 +3412,16 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== +indent-string@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-5.0.0.tgz#4fd2980fccaf8622d14c64d694f4cf33c81951a5" + integrity sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg== + +index-to-position@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/index-to-position/-/index-to-position-0.1.2.tgz#e11bfe995ca4d8eddb1ec43274488f3c201a7f09" + integrity sha512-MWDKS3AS1bGCHLBA2VLImJz42f7bJh8wQsTGCzI3j519/CASStoDONUBVz2I/VID0MpiX3SGSnbOD2xUalbE5g== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -3784,16 +3430,55 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.0, inherits@^2.0.3, inherits@~2.0.3: +inherits@2, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ini@^1.3.2, ini@^1.3.4: +ini@^1.3.2, ini@^1.3.4, ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +ini@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ini/-/ini-5.0.0.tgz#a7a4615339843d9a8ccc2d85c9d81cf93ffbc638" + integrity sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw== + +init-package-json@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/init-package-json/-/init-package-json-7.0.2.tgz#62d7fa76d880a7773a7be51981a2b09006d2516f" + integrity sha512-Qg6nAQulaOQZjvaSzVLtYRqZmuqOi7gTknqqgdhZy7LV5oO+ppvHWq15tZYzGyxJLTH5BxRTqTa+cPDx2pSD9Q== + dependencies: + "@npmcli/package-json" "^6.0.0" + npm-package-arg "^12.0.0" + promzard "^2.0.0" + read "^4.0.0" + semver "^7.3.5" + validate-npm-package-license "^3.0.4" + validate-npm-package-name "^6.0.0" + +into-stream@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-7.0.0.tgz#d1a211e146be8acfdb84dabcbf00fe8205e72936" + integrity sha512-2dYz766i9HprMBasCMvHMuazJ7u4WzhJwo5kb3iPSiW/iRYV6uPari3zHoqZlnuaR7V1bEiNMxikhp37rdBXbw== + dependencies: + from2 "^2.3.0" + p-is-promise "^3.0.0" + +ip-address@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" + integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== + dependencies: + jsbn "1.1.0" + sprintf-js "^1.1.3" + +ip-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-5.0.0.tgz#cd313b2ae9c80c07bd3851e12bf4fa4dc5480632" + integrity sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw== + is-alphabetical@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d" @@ -3812,11 +3497,6 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= -is-bigint@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.1.tgz#6923051dfcbc764278540b9ce0e6b3213aa5ebc2" - integrity sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg== - is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -3824,41 +3504,24 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-boolean-object@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.0.tgz#e2aaad3a3a8fca34c28f6eee135b156ed2587ff0" - integrity sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA== - dependencies: - call-bind "^1.0.0" - is-buffer@^1.1.4: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-callable@^1.1.4, is-callable@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" - integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== - -is-core-module@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" - integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== +is-cidr@^5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/is-cidr/-/is-cidr-5.1.1.tgz#83ec462922c2b9209bc64794c4e3b2a890d23994" + integrity sha512-AwzRMjtJNTPOgm7xuYZ71715z99t+4yRnSnSzgK5err5+heYi4zMuvmpUadaJ28+KCXCQo8CjUrKQZRWSPmqTQ== dependencies: - has "^1.0.3" + cidr-regex "^4.1.1" -is-core-module@^2.5.0, is-core-module@^2.8.0: - version "2.8.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" - integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== +is-core-module@^2.16.0, is-core-module@^2.5.0: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== dependencies: - has "^1.0.3" - -is-date-object@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" - integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== + hasown "^2.0.2" is-decimal@^1.0.0: version "1.0.4" @@ -3894,25 +3557,10 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" -is-hexadecimal@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" - integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== - -is-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" - integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= - -is-negative-zero@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" - integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== - -is-number-object@^1.0.4: +is-hexadecimal@^1.0.0: version "1.0.4" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" - integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" + integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== is-number@^7.0.0: version "7.0.0" @@ -3934,27 +3582,10 @@ is-plain-obj@^2.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== -is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-reference@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" - integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== - dependencies: - "@types/estree" "*" - -is-regex@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251" - integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg== - dependencies: - call-bind "^1.0.2" - has-symbols "^1.0.1" +is-plain-obj@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" + integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== is-stream@^2.0.0: version "2.0.0" @@ -3966,17 +3597,10 @@ is-stream@^3.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== -is-string@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" - integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== - -is-symbol@^1.0.2, is-symbol@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" - integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== - dependencies: - has-symbols "^1.0.1" +is-stream@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-4.0.1.tgz#375cf891e16d2e4baec250b85926cffc14720d9b" + integrity sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A== is-text-path@^1.0.1: version "1.0.1" @@ -3995,6 +3619,11 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +is-unicode-supported@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz#09f0ab0de6d3744d48d265ebb98f65d11f2a9b3a" + integrity sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ== + is-whitespace-character@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz#0858edd94a95594c7c9dd0b5c174ec6e45ee4aa7" @@ -4025,15 +3654,26 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= -isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= +isexe@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-3.1.1.tgz#4a407e2bd78ddfb14bea0c27c6f7072dde775f0d" + integrity sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ== -isomorphic-ws@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" - integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== +isomorphic-ws@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf" + integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw== + +issue-parser@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/issue-parser/-/issue-parser-7.0.1.tgz#8a053e5a4952c75bb216204e454b4fc7d4cc9637" + integrity sha512-3YZcUUR2Wt1WsapF+S/WiA2WmlW0cWAoPccMqne7AxEBhCdFeTPjfv/Axb8V2gyCgY3nRw+ksZ3xSUX+R47iAg== + dependencies: + lodash.capitalize "^4.2.1" + lodash.escaperegexp "^4.1.2" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.uniqby "^4.7.0" istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.0.0-alpha.1: version "3.2.0" @@ -4096,14 +3736,19 @@ istanbul-reports@^3.0.2: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -jest-worker@^26.2.1: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" - integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^7.0.0" + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +java-properties@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/java-properties/-/java-properties-1.0.2.tgz#ccd1fa73907438a5b5c38982269d0e771fe78211" + integrity sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ== js-tokens@^4.0.0: version "4.0.0" @@ -4125,15 +3770,15 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +jsbn@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== json-parse-better-errors@^1.0.1: version "1.0.2" @@ -4145,6 +3790,11 @@ json-parse-even-better-errors@^2.3.0: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== +json-parse-even-better-errors@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz#d3f67bd5925e81d3e31aa466acc821c8375cec43" + integrity sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA== + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -4165,18 +3815,16 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= +json-stringify-nice@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/json-stringify-nice/-/json-stringify-nice-1.1.4.tgz#2c937962b80181d3f317dd39aa323e14f5a60a67" + integrity sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw== + json-stringify-safe@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= -json5@^2.1.2: - version "2.2.0" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" - integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== - dependencies: - minimist "^1.2.5" - json5@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" @@ -4191,20 +3839,36 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -jsonparse@^1.2.0: +jsonparse@^1.2.0, jsonparse@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= -jsonwebtoken@~9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz#d0faf9ba1cc3a56255fe49c0961a67e520c1926d" - integrity sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw== +jsonwebtoken@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== dependencies: jws "^3.2.2" - lodash "^4.17.21" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" ms "^2.1.1" - semver "^7.3.8" + semver "^7.5.4" + +just-diff-apply@^5.2.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/just-diff-apply/-/just-diff-apply-5.5.0.tgz#771c2ca9fa69f3d2b54e7c3f5c1dfcbcc47f9f0f" + integrity sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw== + +just-diff@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/just-diff/-/just-diff-6.0.2.tgz#03b65908543ac0521caf6d8eb85035f7d27ea285" + integrity sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA== just-extend@^4.0.2: version "4.1.1" @@ -4228,7 +3892,7 @@ jws@^3.2.2: jwa "^1.4.1" safe-buffer "^5.0.1" -kind-of@^6.0.2, kind-of@^6.0.3: +kind-of@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== @@ -4241,6 +3905,117 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +libnpmaccess@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/libnpmaccess/-/libnpmaccess-9.0.0.tgz#47ac12dcd358c2c2f2c9ecb0f081a65ef2cc68bc" + integrity sha512-mTCFoxyevNgXRrvgdOhghKJnCWByBc9yp7zX4u9RBsmZjwOYdUDEBfL5DdgD1/8gahsYnauqIWFbq0iK6tO6CQ== + dependencies: + npm-package-arg "^12.0.0" + npm-registry-fetch "^18.0.1" + +libnpmdiff@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/libnpmdiff/-/libnpmdiff-7.0.0.tgz#808893a36d673e46c927e4a0a836b3742191d307" + integrity sha512-MjvsBJL1AT4ofsSsBRse5clxv7gfPbdgzT0VE+xmVTxE8M92T22laeX9vqFhaQKInSeKiZ2L9w/FVhoCCGPdUg== + dependencies: + "@npmcli/arborist" "^8.0.0" + "@npmcli/installed-package-contents" "^3.0.0" + binary-extensions "^2.3.0" + diff "^5.1.0" + minimatch "^9.0.4" + npm-package-arg "^12.0.0" + pacote "^19.0.0" + tar "^6.2.1" + +libnpmexec@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/libnpmexec/-/libnpmexec-9.0.0.tgz#4bb43ec4ba88bd33750480fcf73935837af061bf" + integrity sha512-5dOwgvt0srgrOkwsjNWokx23BvQXEaUo87HWIY+9lymvAto2VSunNS+Ih7WXVwvkJk7cZ0jhS2H3rNK8G9Anxw== + dependencies: + "@npmcli/arborist" "^8.0.0" + "@npmcli/run-script" "^9.0.1" + ci-info "^4.0.0" + npm-package-arg "^12.0.0" + pacote "^19.0.0" + proc-log "^5.0.0" + read "^4.0.0" + read-package-json-fast "^4.0.0" + semver "^7.3.7" + walk-up-path "^3.0.1" + +libnpmfund@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/libnpmfund/-/libnpmfund-6.0.0.tgz#5f324e9b9fb440af9c197f3f147943362758b49b" + integrity sha512-+7ZTxPyJ0O/Y0xKoEd1CxPCUQ4ldn6EZ2qUMI/E1gJkfzcwb3AdFlSWk1WEXaGBu2+EqMrPf4Xu5lXFWw2Jd3w== + dependencies: + "@npmcli/arborist" "^8.0.0" + +libnpmhook@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/libnpmhook/-/libnpmhook-11.0.0.tgz#b8caf6fe31666d7b18cbf61ce8b722dca1600943" + integrity sha512-Xc18rD9NFbRwZbYCQ+UCF5imPsiHSyuQA8RaCA2KmOUo8q4kmBX4JjGWzmZnxZCT8s6vwzmY1BvHNqBGdg9oBQ== + dependencies: + aproba "^2.0.0" + npm-registry-fetch "^18.0.1" + +libnpmorg@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/libnpmorg/-/libnpmorg-7.0.0.tgz#055dfdba32ac5e8757dd4b264f805b64cbd6980b" + integrity sha512-DcTodX31gDEiFrlIHurBQiBlBO6Var2KCqMVCk+HqZhfQXqUfhKGmFOp0UHr6HR1lkTVM0MzXOOYtUObk0r6Dg== + dependencies: + aproba "^2.0.0" + npm-registry-fetch "^18.0.1" + +libnpmpack@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/libnpmpack/-/libnpmpack-8.0.0.tgz#83cb6333861f8a0fe991420feaf0aa48a67d94bf" + integrity sha512-Z5zqR+j8PNOki97D4XnKlekLQjqJYkqCFZeac07XCJYA3aq6O7wYIpn7RqLcNfFm+u3ZsdblY2VQENMoiHA+FQ== + dependencies: + "@npmcli/arborist" "^8.0.0" + "@npmcli/run-script" "^9.0.1" + npm-package-arg "^12.0.0" + pacote "^19.0.0" + +libnpmpublish@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/libnpmpublish/-/libnpmpublish-10.0.1.tgz#7a284565be164c2f8605225213316a0c1d0a9827" + integrity sha512-xNa1DQs9a8dZetNRV0ky686MNzv1MTqB3szgOlRR3Fr24x1gWRu7aB9OpLZsml0YekmtppgHBkyZ+8QZlzmEyw== + dependencies: + ci-info "^4.0.0" + normalize-package-data "^7.0.0" + npm-package-arg "^12.0.0" + npm-registry-fetch "^18.0.1" + proc-log "^5.0.0" + semver "^7.3.7" + sigstore "^3.0.0" + ssri "^12.0.0" + +libnpmsearch@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/libnpmsearch/-/libnpmsearch-8.0.0.tgz#ce2e28ad05a152c736d5ae86356aedd5a52406a5" + integrity sha512-W8FWB78RS3Nkl1gPSHOlF024qQvcoU/e3m9BGDuBfVZGfL4MJ91GXXb04w3zJCGOW9dRQUyWVEqupFjCrgltDg== + dependencies: + npm-registry-fetch "^18.0.1" + +libnpmteam@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/libnpmteam/-/libnpmteam-7.0.0.tgz#e8f40c4bc543b720da2cdd4385e2fafcd06c92c0" + integrity sha512-PKLOoVukN34qyJjgEm5DEOnDwZkeVMUHRx8NhcKDiCNJGPl7G/pF1cfBw8yicMwRlHaHkld1FdujOzKzy4AlwA== + dependencies: + aproba "^2.0.0" + npm-registry-fetch "^18.0.1" + +libnpmversion@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/libnpmversion/-/libnpmversion-7.0.0.tgz#b264a07662b31b78822ba870171088eca6466f38" + integrity sha512-0xle91R6F8r/Q/4tHOnyKko+ZSquEXNdxwRdKCPv4kC1cOVBMFXRsKKrVtRKtXcFn362U8ZlJefk4Apu00424g== + dependencies: + "@npmcli/git" "^6.0.1" + "@npmcli/run-script" "^9.0.1" + json-parse-even-better-errors "^4.0.0" + proc-log "^5.0.0" + semver "^7.3.7" + lilconfig@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.0.0.tgz#f8067feb033b5b74dab4602a5f5029420be749bc" @@ -4319,10 +4094,20 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" - integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + +lodash.capitalize@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz#f826c9b4e2a8511d84e3aca29db05e1a4f3b72a9" + integrity sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw== + +lodash.escaperegexp@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" + integrity sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw== lodash.flattendeep@^4.4.0: version "4.4.0" @@ -4334,12 +4119,52 @@ lodash.get@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + lodash.ismatch@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= -lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21: +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + +lodash.uniqby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" + integrity sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww== + +lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -4363,6 +4188,18 @@ log-update@^6.0.0: strip-ansi "^7.1.0" wrap-ansi "^9.0.0" +lru-cache@^10.0.1, lru-cache@^10.2.0, lru-cache@^10.2.2: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -4370,21 +4207,6 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -magic-string@^0.25.7: - version "0.25.7" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" - integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== - dependencies: - sourcemap-codec "^1.4.4" - -make-dir@^2.0.0, make-dir@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" - integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== - dependencies: - pify "^4.0.1" - semver "^5.6.0" - make-dir@^3.0.0, make-dir@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -4397,6 +4219,23 @@ make-error@^1.1.1: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== +make-fetch-happen@^14.0.0, make-fetch-happen@^14.0.1, make-fetch-happen@^14.0.2, make-fetch-happen@^14.0.3: + version "14.0.3" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz#d74c3ecb0028f08ab604011e0bc6baed483fcdcd" + integrity sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ== + dependencies: + "@npmcli/agent" "^3.0.0" + cacache "^19.0.1" + http-cache-semantics "^4.1.1" + minipass "^7.0.2" + minipass-fetch "^4.0.0" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^1.0.0" + proc-log "^5.0.0" + promise-retry "^2.0.1" + ssri "^12.0.0" + map-obj@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" @@ -4412,6 +4251,29 @@ markdown-escapes@^1.0.0: resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535" integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg== +marked-terminal@^7.0.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-7.3.0.tgz#7a86236565f3dd530f465ffce9c3f8b62ef270e8" + integrity sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw== + dependencies: + ansi-escapes "^7.0.0" + ansi-regex "^6.1.0" + chalk "^5.4.1" + cli-highlight "^2.1.11" + cli-table3 "^0.6.5" + node-emoji "^2.2.0" + supports-hyperlinks "^3.1.0" + +marked@^12.0.0: + version "12.0.2" + resolved "https://registry.yarnpkg.com/marked/-/marked-12.0.2.tgz#b31578fe608b599944c69807b00f18edab84647e" + integrity sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q== + +meow@^13.0.0: + version "13.2.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-13.2.0.tgz#6b7d63f913f984063b3cc261b6e8800c4cd3474f" + integrity sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA== + meow@^8.0.0: version "8.1.2" resolved "https://registry.yarnpkg.com/meow/-/meow-8.1.2.tgz#bcbe45bda0ee1729d350c03cffc8395a36c4e897" @@ -4447,13 +4309,13 @@ micromatch@4.0.5: braces "^3.0.2" picomatch "^2.3.1" -micromatch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== +micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: - braces "^3.0.1" - picomatch "^2.0.5" + braces "^3.0.3" + picomatch "^2.3.1" mime-db@1.46.0: version "1.46.0" @@ -4467,6 +4329,11 @@ mime-types@^2.1.12: dependencies: mime-db "1.46.0" +mime@^4.0.0: + version "4.0.6" + resolved "https://registry.yarnpkg.com/mime/-/mime-4.0.6.tgz#ca83bec0bcf2a02353d0e02da99be05603d04839" + integrity sha512-4rGt7rvQHBbaSOF9POGkk1ocRP16Md1x36Xma8sz8h8/vfCUI2OtEIeCqe4Ofes853x4xDoPiFLIT47J5fI/7A== + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -4489,13 +4356,20 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1, minimatch@^5.1.6: +minimatch@^5.1.6: version "5.1.6" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== dependencies: brace-expansion "^2.0.1" +minimatch@^9.0.0, minimatch@^9.0.4, minimatch@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + minimist-options@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" @@ -4505,15 +4379,97 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@^1.2.5: - version "1.2.6" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" - integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +minimist@^1.2.0, minimist@^1.2.5: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minipass-collect@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-2.0.1.tgz#1621bc77e12258a12c60d34e2276ec5c20680863" + integrity sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw== + dependencies: + minipass "^7.0.3" + +minipass-fetch@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-4.0.0.tgz#b8ea716464747aeafb7edf2e110114c38089a09c" + integrity sha512-2v6aXUXwLP1Epd/gc32HAMIWoczx+fZwEPRHm/VwtrJzRGwR1qGZXEYV3Zp8ZjjbwaZhMrM6uHV4KVkk+XCc2w== + dependencies: + minipass "^7.0.3" + minipass-sized "^1.0.3" + minizlib "^3.0.1" + optionalDependencies: + encoding "^0.1.13" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-pipeline@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + +minipass-sized@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" + integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== + dependencies: + minipass "^3.0.0" -mocha@^10.7.0: - version "10.7.0" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.7.0.tgz#9e5cbed8fa9b37537a25bd1f7fb4f6fc45458b9a" - integrity sha512-v8/rBWr2VO5YkspYINnvu81inSz2y3ODJrhO175/Exzor1RcEZZkizgE2A+w/CAXXoESS8Kys5E62dOHGHzULA== +minipass@^3.0.0: + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.2, minipass@^7.0.3, minipass@^7.0.4, minipass@^7.1.1, minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +minizlib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-3.0.1.tgz#46d5329d1eb3c83924eff1d3b858ca0a31581012" + integrity sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg== + dependencies: + minipass "^7.0.4" + rimraf "^5.0.5" + +mkdirp@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +mkdirp@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" + integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== + +mocha@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-11.1.0.tgz#20d7c6ac4d6d6bcb60a8aa47971fca74c65c3c66" + integrity sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg== dependencies: ansi-colors "^4.1.3" browser-stdout "^1.3.1" @@ -4522,7 +4478,7 @@ mocha@^10.7.0: diff "^5.2.0" escape-string-regexp "^4.0.0" find-up "^5.0.0" - glob "^8.1.0" + glob "^10.4.5" he "^1.2.0" js-yaml "^4.1.0" log-symbols "^4.1.0" @@ -4532,8 +4488,8 @@ mocha@^10.7.0: strip-json-comments "^3.1.1" supports-color "^8.1.1" workerpool "^6.5.1" - yargs "^16.2.0" - yargs-parser "^20.2.9" + yargs "^17.7.2" + yargs-parser "^21.1.1" yargs-unparser "^2.0.0" modify-values@^1.0.0: @@ -4546,11 +4502,25 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.1.1, ms@^2.1.3: +ms@^2.1.1, ms@^2.1.2, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +mute-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-2.0.0.tgz#a5446fc0c512b71c83c44d908d5c7b7b4c493b2b" + integrity sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA== + +mz@^2.4.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + natural-compare-lite@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" @@ -4561,11 +4531,21 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +negotiator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" + integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== + neo-async@^2.6.0: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +nerf-dart@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/nerf-dart/-/nerf-dart-1.0.0.tgz#e6dab7febf5ad816ea81cf5c629c5a0ebde72c1a" + integrity sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g== + nise@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.0.tgz#713ef3ed138252daef20ec035ab62b7a28be645c" @@ -4577,18 +4557,31 @@ nise@^5.1.0: just-extend "^4.0.2" path-to-regexp "^1.7.0" -node-environment-flags@^1.0.5: - version "1.0.6" - resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.6.tgz#a30ac13621f6f7d674260a54dede048c3982c088" - integrity sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw== +node-emoji@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-2.2.0.tgz#1d000e3c76e462577895be1b436f4aa2d6760eb0" + integrity sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw== dependencies: - object.getownpropertydescriptors "^2.0.3" - semver "^5.7.0" + "@sindresorhus/is" "^4.6.0" + char-regex "^1.0.2" + emojilib "^2.4.0" + skin-tone "^2.0.0" -node-modules-regexp@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" - integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= +node-gyp@^11.0.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-11.1.0.tgz#212a1d9c167c50d727d42659410780b40e07bbd3" + integrity sha512-/+7TuHKnBpnMvUQnsYEb0JOozDZqarQbfNuSGLXIjhStMT0fbw7IdSqWgopOP5xhRZE+lsbIvAHcekddruPZgQ== + dependencies: + env-paths "^2.2.0" + exponential-backoff "^3.1.1" + glob "^10.3.10" + graceful-fs "^4.2.6" + make-fetch-happen "^14.0.3" + nopt "^8.0.0" + proc-log "^5.0.0" + semver "^7.3.5" + tar "^7.4.3" + which "^5.0.0" node-preload@^0.2.1: version "0.2.1" @@ -4597,15 +4590,17 @@ node-preload@^0.2.1: dependencies: process-on-spawn "^1.0.0" -node-releases@^1.1.70: - version "1.1.71" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb" - integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg== +node-releases@^2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== -node-releases@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" - integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== +nopt@^8.0.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-8.1.0.tgz#b11d38caf0f8643ce885818518064127f602eae3" + integrity sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A== + dependencies: + abbrev "^3.0.0" normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: version "2.5.0" @@ -4627,11 +4622,107 @@ normalize-package-data@^3.0.0: semver "^7.3.4" validate-npm-package-license "^3.0.1" +normalize-package-data@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-6.0.2.tgz#a7bc22167fe24025412bcff0a9651eb768b03506" + integrity sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g== + dependencies: + hosted-git-info "^7.0.0" + semver "^7.3.5" + validate-npm-package-license "^3.0.4" + +normalize-package-data@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-7.0.0.tgz#ab4f49d02f2e25108d3f4326f3c13f0de6fa6a0a" + integrity sha512-k6U0gKRIuNCTkwHGZqblCfLfBRh+w1vI6tBo+IeJwq2M8FUiOqhX7GH+GArQGScA7azd1WfyRCvxoXDO3hQDIA== + dependencies: + hosted-git-info "^8.0.0" + semver "^7.3.5" + validate-npm-package-license "^3.0.4" + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +normalize-url@^8.0.0: + version "8.0.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-8.0.1.tgz#9b7d96af9836577c58f5883e939365fa15623a4a" + integrity sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w== + +npm-audit-report@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/npm-audit-report/-/npm-audit-report-6.0.0.tgz#0262e5e2b674fabf0ea47e900fc7384b83de0fbb" + integrity sha512-Ag6Y1irw/+CdSLqEEAn69T8JBgBThj5mw0vuFIKeP7hATYuQuS5jkMjK6xmVB8pr7U4g5Audbun0lHhBDMIBRA== + +npm-bundled@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-4.0.0.tgz#f5b983f053fe7c61566cf07241fab2d4e9d513d3" + integrity sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA== + dependencies: + npm-normalize-package-bin "^4.0.0" + +npm-install-checks@^7.1.0, npm-install-checks@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/npm-install-checks/-/npm-install-checks-7.1.1.tgz#e9d679fc8a1944c75cdcc96478a22f9d0f763632" + integrity sha512-u6DCwbow5ynAX5BdiHQ9qvexme4U3qHW3MWe5NqH+NeBm0LbiH6zvGjNNew1fY+AZZUtVHbOPF3j7mJxbUzpXg== + dependencies: + semver "^7.1.1" + +npm-normalize-package-bin@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz#df79e70cd0a113b77c02d1fe243c96b8e618acb1" + integrity sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w== + +npm-package-arg@^12.0.0: + version "12.0.2" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-12.0.2.tgz#3b1e04ebe651cc45028e298664e8c15ce9c0ca40" + integrity sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA== + dependencies: + hosted-git-info "^8.0.0" + proc-log "^5.0.0" + semver "^7.3.5" + validate-npm-package-name "^6.0.0" + +npm-packlist@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-9.0.0.tgz#8e9b061bab940de639dd93d65adc95c34412c7d0" + integrity sha512-8qSayfmHJQTx3nJWYbbUmflpyarbLMBc6LCAjYsiGtXxDB68HaZpb8re6zeaLGxZzDuMdhsg70jryJe+RrItVQ== + dependencies: + ignore-walk "^7.0.0" + +npm-pick-manifest@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-10.0.0.tgz#6cc120c6473ceea56dfead500f00735b2b892851" + integrity sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ== + dependencies: + npm-install-checks "^7.1.0" + npm-normalize-package-bin "^4.0.0" + npm-package-arg "^12.0.0" + semver "^7.3.5" + +npm-profile@^11.0.1: + version "11.0.1" + resolved "https://registry.yarnpkg.com/npm-profile/-/npm-profile-11.0.1.tgz#6ffac43f3d186316d37e80986d84aef2470269a2" + integrity sha512-HP5Cw9WHwFS9vb4fxVlkNAQBUhVL5BmW6rAR+/JWkpwqcFJid7TihKUdYDWqHl0NDfLd0mpucheGySqo8ysyfw== + dependencies: + npm-registry-fetch "^18.0.0" + proc-log "^5.0.0" + +npm-registry-fetch@^18.0.0, npm-registry-fetch@^18.0.1, npm-registry-fetch@^18.0.2: + version "18.0.2" + resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-18.0.2.tgz#340432f56b5a8b1af068df91aae0435d2de646b5" + integrity sha512-LeVMZBBVy+oQb5R6FDV9OlJCcWDU+al10oKpe+nsvcHnG24Z3uM3SvJYKfGJlfGjVU8v9liejCrUR/M5HO5NEQ== + dependencies: + "@npmcli/redact" "^3.0.0" + jsonparse "^1.3.1" + make-fetch-happen "^14.0.0" + minipass "^7.0.2" + minipass-fetch "^4.0.0" + minizlib "^3.0.1" + npm-package-arg "^12.0.0" + proc-log "^5.0.0" + npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" @@ -4646,6 +4737,93 @@ npm-run-path@^5.1.0: dependencies: path-key "^4.0.0" +npm-run-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-6.0.0.tgz#25cfdc4eae04976f3349c0b1afc089052c362537" + integrity sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA== + dependencies: + path-key "^4.0.0" + unicorn-magic "^0.3.0" + +npm-user-validate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/npm-user-validate/-/npm-user-validate-3.0.0.tgz#9b1410796bf1f1d78297a8096328c55d3083f233" + integrity sha512-9xi0RdSmJ4mPYTC393VJPz1Sp8LyCx9cUnm/L9Qcb3cFO8gjT4mN20P9FAsea8qDHdQ7LtcN8VLh2UT47SdKCw== + +npm@^10.5.0: + version "10.9.2" + resolved "https://registry.yarnpkg.com/npm/-/npm-10.9.2.tgz#784b3e2194fc151d5709a14692cf49c4afc60dfe" + integrity sha512-iriPEPIkoMYUy3F6f3wwSZAU93E0Eg6cHwIR6jzzOXWSy+SD/rOODEs74cVONHKSx2obXtuUoyidVEhISrisgQ== + dependencies: + "@isaacs/string-locale-compare" "^1.1.0" + "@npmcli/arborist" "^8.0.0" + "@npmcli/config" "^9.0.0" + "@npmcli/fs" "^4.0.0" + "@npmcli/map-workspaces" "^4.0.2" + "@npmcli/package-json" "^6.1.0" + "@npmcli/promise-spawn" "^8.0.2" + "@npmcli/redact" "^3.0.0" + "@npmcli/run-script" "^9.0.1" + "@sigstore/tuf" "^3.0.0" + abbrev "^3.0.0" + archy "~1.0.0" + cacache "^19.0.1" + chalk "^5.3.0" + ci-info "^4.1.0" + cli-columns "^4.0.0" + fastest-levenshtein "^1.0.16" + fs-minipass "^3.0.3" + glob "^10.4.5" + graceful-fs "^4.2.11" + hosted-git-info "^8.0.2" + ini "^5.0.0" + init-package-json "^7.0.2" + is-cidr "^5.1.0" + json-parse-even-better-errors "^4.0.0" + libnpmaccess "^9.0.0" + libnpmdiff "^7.0.0" + libnpmexec "^9.0.0" + libnpmfund "^6.0.0" + libnpmhook "^11.0.0" + libnpmorg "^7.0.0" + libnpmpack "^8.0.0" + libnpmpublish "^10.0.1" + libnpmsearch "^8.0.0" + libnpmteam "^7.0.0" + libnpmversion "^7.0.0" + make-fetch-happen "^14.0.3" + minimatch "^9.0.5" + minipass "^7.1.1" + minipass-pipeline "^1.2.4" + ms "^2.1.2" + node-gyp "^11.0.0" + nopt "^8.0.0" + normalize-package-data "^7.0.0" + npm-audit-report "^6.0.0" + npm-install-checks "^7.1.1" + npm-package-arg "^12.0.0" + npm-pick-manifest "^10.0.0" + npm-profile "^11.0.1" + npm-registry-fetch "^18.0.2" + npm-user-validate "^3.0.0" + p-map "^4.0.0" + pacote "^19.0.1" + parse-conflict-json "^4.0.0" + proc-log "^5.0.0" + qrcode-terminal "^0.12.0" + read "^4.0.0" + semver "^7.6.3" + spdx-expression-parse "^4.0.0" + ssri "^12.0.0" + supports-color "^9.4.0" + tar "^6.2.1" + text-table "~0.2.0" + tiny-relative-date "^1.3.0" + treeverse "^3.0.0" + validate-npm-package-name "^6.0.0" + which "^5.0.0" + write-file-atomic "^6.0.0" + null-check@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/null-check/-/null-check-1.0.0.tgz#977dffd7176012b9ec30d2a39db5cf72a0439edd" @@ -4684,34 +4862,10 @@ nyc@^15.1.0: test-exclude "^6.0.0" yargs "^15.0.2" -object-inspect@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" - integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw== - -object-keys@^1.0.12, object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.1.0, object.assign@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" - integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - has-symbols "^1.0.1" - object-keys "^1.1.1" - -object.getownpropertydescriptors@^2.0.3: - version "2.1.2" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz#1bd63aeacf0d5d2d2f31b5e393b03a7c601a23f7" - integrity sha512-WtxeKSzfBjlzL+F9b7M7hewDzMwy+C8NRssHd1YrNlzHzIDrXcXiNOMrezdAEM4UXixgV+vvnyBeN7Rygl2ttQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.18.0-next.2" +object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== once@^1.3.0: version "1.4.0" @@ -4751,6 +4905,23 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" +p-each-series@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-3.0.0.tgz#d1aed5e96ef29864c897367a7d2a628fdc960806" + integrity sha512-lastgtAdoH9YaLyDa5i5z64q+kzOcQHsQ5SsZJD3q0VEyI8mq872S3geuNbRUQLVAE9siMfgKrpj7MloKFHruw== + +p-filter@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-filter/-/p-filter-4.1.0.tgz#fe0aa794e2dfad8ecf595a39a245484fcd09c6e4" + integrity sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw== + dependencies: + p-map "^7.0.1" + +p-is-promise@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-3.0.0.tgz#58e78c7dfe2e163cf2a04ff869e7c1dba64a5971" + integrity sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ== + p-limit@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" @@ -4807,6 +4978,28 @@ p-map@^3.0.0: dependencies: aggregate-error "^3.0.0" +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +p-map@^7.0.1, p-map@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-7.0.3.tgz#7ac210a2d36f81ec28b736134810f7ba4418cdb6" + integrity sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA== + +p-reduce@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-2.1.0.tgz#09408da49507c6c274faa31f28df334bc712b64a" + integrity sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw== + +p-reduce@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-3.0.0.tgz#f11773794792974bd1f7a14c72934248abff4160" + integrity sha512-xsrIUgI0Kn6iyDYm9StOpOeK29XM1aboGji26+QEortiFST1hGZaUQOLhtEbqHErPpGW/aSz6allwK2qcptp0Q== + p-try@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" @@ -4827,6 +5020,57 @@ package-hash@^4.0.0: lodash.flattendeep "^4.4.0" release-zalgo "^1.0.0" +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + +pacote@^19.0.0, pacote@^19.0.1: + version "19.0.1" + resolved "https://registry.yarnpkg.com/pacote/-/pacote-19.0.1.tgz#66d22dbd274ed8a7c30029d70eb8030f5151e6fc" + integrity sha512-zIpxWAsr/BvhrkSruspG8aqCQUUrWtpwx0GjiRZQhEM/pZXrigA32ElN3vTcCPUDOFmHr6SFxwYrvVUs5NTEUg== + dependencies: + "@npmcli/git" "^6.0.0" + "@npmcli/installed-package-contents" "^3.0.0" + "@npmcli/package-json" "^6.0.0" + "@npmcli/promise-spawn" "^8.0.0" + "@npmcli/run-script" "^9.0.0" + cacache "^19.0.0" + fs-minipass "^3.0.0" + minipass "^7.0.2" + npm-package-arg "^12.0.0" + npm-packlist "^9.0.0" + npm-pick-manifest "^10.0.0" + npm-registry-fetch "^18.0.0" + proc-log "^5.0.0" + promise-retry "^2.0.1" + sigstore "^3.0.0" + ssri "^12.0.0" + tar "^6.1.11" + +pacote@^20.0.0: + version "20.0.0" + resolved "https://registry.yarnpkg.com/pacote/-/pacote-20.0.0.tgz#c974373d8e0859d00e8f9158574350f8c1b168e5" + integrity sha512-pRjC5UFwZCgx9kUFDVM9YEahv4guZ1nSLqwmWiLUnDbGsjs+U5w7z6Uc8HNR1a6x8qnu5y9xtGE6D1uAuYz+0A== + dependencies: + "@npmcli/git" "^6.0.0" + "@npmcli/installed-package-contents" "^3.0.0" + "@npmcli/package-json" "^6.0.0" + "@npmcli/promise-spawn" "^8.0.0" + "@npmcli/run-script" "^9.0.0" + cacache "^19.0.0" + fs-minipass "^3.0.0" + minipass "^7.0.2" + npm-package-arg "^12.0.0" + npm-packlist "^9.0.0" + npm-pick-manifest "^10.0.0" + npm-registry-fetch "^18.0.0" + proc-log "^5.0.0" + promise-retry "^2.0.1" + sigstore "^3.0.0" + ssri "^12.0.0" + tar "^6.1.11" + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -4834,6 +5078,15 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parse-conflict-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-conflict-json/-/parse-conflict-json-4.0.0.tgz#996b1edfc0c727583b56c7644dbb3258fc9e9e4b" + integrity sha512-37CN2VtcuvKgHUs8+0b1uJeEsbGn61GRHz469C94P5xiOoqpDYJYwjg4RY9Vmz39WyZAVkR5++nbJwLMIgOCnQ== + dependencies: + json-parse-even-better-errors "^4.0.0" + just-diff "^6.0.0" + just-diff-apply "^5.2.0" + parse-entities@^1.1.0: version "1.2.2" resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.2.2.tgz#c31bf0f653b6661354f8973559cb86dd1d5edf50" @@ -4854,7 +5107,7 @@ parse-json@^4.0.0: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" -parse-json@^5.0.0: +parse-json@^5.0.0, parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== @@ -4864,10 +5117,36 @@ parse-json@^5.0.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" -parse-passwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" - integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= +parse-json@^8.0.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-8.1.0.tgz#91cdc7728004e955af9cb734de5684733b24a717" + integrity sha512-rum1bPifK5SSar35Z6EKZuYPJx85pkNaFrxBK3mwdfSJ1/WKbYrjoW/zTPSjRRamfmVX1ACBIdFAO0VRErW/EA== + dependencies: + "@babel/code-frame" "^7.22.13" + index-to-position "^0.1.2" + type-fest "^4.7.1" + +parse-ms@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-4.0.0.tgz#c0c058edd47c2a590151a718990533fd62803df4" + integrity sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw== + +parse5-htmlparser2-tree-adapter@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" + integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== + dependencies: + parse5 "^6.0.1" + +parse5@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" + integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== + +parse5@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== path-exists@^3.0.0: version "3.0.0" @@ -4894,11 +5173,19 @@ path-key@^4.0.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== -path-parse@^1.0.6, path-parse@^1.0.7: +path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-to-regexp@^1.7.0: version "1.8.0" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" @@ -4918,22 +5205,22 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +path-type@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-6.0.0.tgz#2f1bb6791a91ce99194caede5d6c5920ed81eb51" + integrity sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ== + pathval@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== - -picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1, picomatch@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" - integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== +picocolors@^1.0.0, picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== -picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -4950,27 +5237,16 @@ pify@^2.3.0: pify@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" - integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= - -pify@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" - integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== - -pirates@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" - integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA== - dependencies: - node-modules-regexp "^1.0.0" - -pkg-dir@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" - integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + +pkg-conf@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pkg-conf/-/pkg-conf-2.1.0.tgz#2126514ca6f2abfebd168596df18ba57867f0058" + integrity sha512-C+VUP+8jis7EsQZIhDYmS5qlNtjv2yP4SNtjXK9AP1ZcTRlnSfuumaTnRfYZnYgUUYVIKqL0fRvmUGDV2fmp6g== dependencies: - find-up "^3.0.0" + find-up "^2.0.0" + load-json-file "^4.0.0" pkg-dir@^4.1.0: version "4.2.0" @@ -4993,6 +5269,14 @@ please-upgrade-node@^3.2.0: dependencies: semver-compare "^1.0.0" +postcss-selector-parser@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" + integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -5010,6 +5294,18 @@ prettier@^2.2.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== +pretty-ms@^9.0.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-9.2.0.tgz#e14c0aad6493b69ed63114442a84133d7e560ef0" + integrity sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg== + dependencies: + parse-ms "^4.0.0" + +proc-log@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-5.0.0.tgz#e6c93cf37aef33f835c53485f314f50ea906a9d8" + integrity sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ== + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -5022,11 +5318,46 @@ process-on-spawn@^1.0.0: dependencies: fromentries "^1.2.0" +proggy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/proggy/-/proggy-3.0.0.tgz#874e91fed27fe00a511758e83216a6b65148bd6c" + integrity sha512-QE8RApCM3IaRRxVzxrjbgNMpQEX6Wu0p0KBeoSiSEw5/bsGwZHsshF4LCxH2jp/r6BU+bqA3LrMDEYNfJnpD8Q== + progress@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +promise-all-reject-late@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-all-reject-late/-/promise-all-reject-late-1.0.1.tgz#f8ebf13483e5ca91ad809ccc2fcf25f26f8643c2" + integrity sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw== + +promise-call-limit@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/promise-call-limit/-/promise-call-limit-3.0.2.tgz#524b7f4b97729ff70417d93d24f46f0265efa4f9" + integrity sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw== + +promise-retry@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== + dependencies: + err-code "^2.0.2" + retry "^0.12.0" + +promzard@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/promzard/-/promzard-2.0.0.tgz#03ad0e4db706544dfdd4f459281f13484fc10c49" + integrity sha512-Ncd0vyS2eXGOjchIRg6PVCYKetJYrW1BSbbIo+bKdig61TB6nH2RQNF2uP+qMpsI73L/jURLWojcw8JNIKZ3gg== + dependencies: + read "^4.0.0" + +proto-list@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" + integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== + proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" @@ -5042,6 +5373,11 @@ q@^1.5.1: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= +qrcode-terminal@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz#bb5b699ef7f9f0505092a3748be4464fe71b5819" + integrity sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ== + queue-microtask@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.2.tgz#abf64491e6ecf0f38a6502403d4cda04f372dfd3" @@ -5059,6 +5395,38 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" +rc@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +read-cmd-shim@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-5.0.0.tgz#6e5450492187a0749f6c80dcbef0debc1117acca" + integrity sha512-SEbJV7tohp3DAAILbEMPXavBjAnMN0tVnh4+9G8ihV4Pq3HYF9h8QNez9zkJ1ILkv9G2BjdzwctznGZXgu/HGw== + +read-package-json-fast@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz#8ccbc05740bb9f58264f400acc0b4b4eee8d1b39" + integrity sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg== + dependencies: + json-parse-even-better-errors "^4.0.0" + npm-normalize-package-bin "^4.0.0" + +read-package-up@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/read-package-up/-/read-package-up-11.0.0.tgz#71fb879fdaac0e16891e6e666df22de24a48d5ba" + integrity sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ== + dependencies: + find-up-simple "^1.0.0" + read-pkg "^9.0.0" + type-fest "^4.6.0" + read-pkg-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" @@ -5095,6 +5463,24 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" +read-pkg@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-9.0.1.tgz#b1b81fb15104f5dbb121b6bbdee9bbc9739f569b" + integrity sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA== + dependencies: + "@types/normalize-package-data" "^2.4.3" + normalize-package-data "^6.0.0" + parse-json "^8.0.0" + type-fest "^4.6.0" + unicorn-magic "^0.1.0" + +read@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/read/-/read-4.1.0.tgz#d97c2556b009b47b16b5bb82311d477cc7503548" + integrity sha512-uRfX6K+f+R8OOrYScaM3ixPY4erg69f8DN6pgTvMcA9iRc8iDhwrA4m3Yu8YYKsXJgVvum+m8PkRboZwwuLzYA== + dependencies: + mute-stream "^2.0.0" + readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" @@ -5104,10 +5490,10 @@ readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2: string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@~2.3.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== +readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@~2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== dependencies: core-util-is "~1.0.0" inherits "~2.0.3" @@ -5117,13 +5503,6 @@ readable-stream@~2.3.6: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readdirp@~3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" - integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== - dependencies: - picomatch "^2.2.1" - readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -5139,58 +5518,17 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" -regenerate-unicode-properties@^8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" - integrity sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA== - dependencies: - regenerate "^1.4.0" - -regenerate@^1.4.0: - version "1.4.2" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" - integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== - -regenerator-runtime@^0.13.4: - version "0.13.7" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" - integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== - -regenerator-transform@^0.14.2: - version "0.14.5" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4" - integrity sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw== - dependencies: - "@babel/runtime" "^7.8.4" - regexpp@^3.0.0, regexpp@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== -regexpu-core@^4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6" - integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ== - dependencies: - regenerate "^1.4.0" - regenerate-unicode-properties "^8.2.0" - regjsgen "^0.5.1" - regjsparser "^0.6.4" - unicode-match-property-ecmascript "^1.0.4" - unicode-match-property-value-ecmascript "^1.2.0" - -regjsgen@^0.5.1: - version "0.5.2" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733" - integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A== - -regjsparser@^0.6.4: - version "0.6.7" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.7.tgz#c00164e1e6713c2e3ee641f1701c4b7aa0a7f86c" - integrity sha512-ib77G0uxsA2ovgiYbCVGx4Pv3PSttAx2vIwidqQzbL2U5S4Q+j00HdSAneSBuyVcMvEnTXMjiGgB+DlXozVhpQ== +registry-auth-token@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-5.1.0.tgz#3c659047ecd4caebd25bc1570a3aa979ae490eca" + integrity sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw== dependencies: - jsesc "~0.5.0" + "@pnpm/npm-conf" "^2.1.0" release-zalgo@^1.0.0: version "1.0.0" @@ -5263,22 +5601,14 @@ resolve-global@1.0.0, resolve-global@^1.0.0: global-dirs "^0.1.1" resolve@^1.10.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.21.0.tgz#b51adc97f3472e6a5cf4444d34bc9d6b9037591f" - integrity sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA== + version "1.22.10" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" + integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== dependencies: - is-core-module "^2.8.0" + is-core-module "^2.16.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^1.14.2, resolve@^1.17.0, resolve@^1.19.0: - version "1.20.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" - integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== - dependencies: - is-core-module "^2.2.0" - path-parse "^1.0.6" - restore-cursor@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-4.0.0.tgz#519560a4318975096def6e609d44100edaa4ccb9" @@ -5287,6 +5617,11 @@ restore-cursor@^4.0.0: onetime "^5.1.0" signal-exit "^3.0.2" +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -5304,35 +5639,12 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" -rollup-plugin-peer-deps-external@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/rollup-plugin-peer-deps-external/-/rollup-plugin-peer-deps-external-2.2.4.tgz#8a420bbfd6dccc30aeb68c9bf57011f2f109570d" - integrity sha512-AWdukIM1+k5JDdAqV/Cxd+nejvno2FVLVeZ74NKggm3Q5s9cbbcOgUPGdbxPi4BXu7xGaZ8HG12F+thImYu/0g== - -rollup-plugin-terser@^7.0.2: - version "7.0.2" - resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d" - integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ== - dependencies: - "@babel/code-frame" "^7.10.4" - jest-worker "^26.2.1" - serialize-javascript "^4.0.0" - terser "^5.0.0" - -rollup@^0.63.4: - version "0.63.5" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.63.5.tgz#5543eecac9a1b83b7e1be598b5be84c9c0a089db" - integrity sha512-dFf8LpUNzIj3oE0vCvobX6rqOzHzLBoblyFp+3znPbjiSmSvOoK2kMKx+Fv9jYduG1rvcCfCveSgEaQHjWRF6g== +rimraf@^5.0.5: + version "5.0.10" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.10.tgz#23b9843d3dc92db71f96e1a2ce92e39fd2a8221c" + integrity sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ== dependencies: - "@types/estree" "0.0.39" - "@types/node" "*" - -rollup@^2.41.0: - version "2.41.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.41.0.tgz#b2a398bbabbf227738dedaef099e494aed468982" - integrity sha512-Gk76XHTggulWPH95q8V62bw6uqDH6UGvbD6LOa3QUyhuMF3eOuaeDHR7SLm1T9faitkpNrqzUAVYx47klcMnlA== - optionalDependencies: - fsevents "~2.3.1" + glob "^10.3.7" run-parallel@^1.1.9: version "1.2.0" @@ -5341,6 +5653,13 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +rxjs@^7.8.1: + version "7.8.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== + dependencies: + tslib "^2.1.0" + safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -5351,58 +5670,89 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +"safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semantic-release@^24.2.3: + version "24.2.3" + resolved "https://registry.yarnpkg.com/semantic-release/-/semantic-release-24.2.3.tgz#fd5ac3b0c27fa7bd994eb89eacc26fa385caa1a6" + integrity sha512-KRhQG9cUazPavJiJEFIJ3XAMjgfd0fcK3B+T26qOl8L0UG5aZUjeRfREO0KM5InGtYwxqiiytkJrbcYoLDEv0A== + dependencies: + "@semantic-release/commit-analyzer" "^13.0.0-beta.1" + "@semantic-release/error" "^4.0.0" + "@semantic-release/github" "^11.0.0" + "@semantic-release/npm" "^12.0.0" + "@semantic-release/release-notes-generator" "^14.0.0-beta.1" + aggregate-error "^5.0.0" + cosmiconfig "^9.0.0" + debug "^4.0.0" + env-ci "^11.0.0" + execa "^9.0.0" + figures "^6.0.0" + find-versions "^6.0.0" + get-stream "^6.0.0" + git-log-parser "^1.2.0" + hook-std "^3.0.0" + hosted-git-info "^8.0.0" + import-from-esm "^2.0.0" + lodash-es "^4.17.21" + marked "^12.0.0" + marked-terminal "^7.0.0" + micromatch "^4.0.2" + p-each-series "^3.0.0" + p-reduce "^3.0.0" + read-package-up "^11.0.0" + resolve-from "^5.0.0" + semver "^7.3.2" + semver-diff "^4.0.0" + signale "^1.2.1" + yargs "^17.5.1" + semver-compare@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= +semver-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-4.0.0.tgz#3afcf5ed6d62259f5c72d0d5d50dffbdc9680df5" + integrity sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA== + dependencies: + semver "^7.3.5" + semver-regex@^3.1.2: version "3.1.4" resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-3.1.4.tgz#13053c0d4aa11d070a2f2872b6b1e3ae1e1971b4" integrity sha512-6IiqeZNgq01qGf0TId0t3NvKzSvUsjcpdEO3AQNeIjR6A2+ckTnQlDpl4qu1bjRv0RzN3FP9hzFmws3lKqRWkA== -"semver@2 || 3 || 4 || 5", semver@^5.6.0, semver@^5.7.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== +semver-regex@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-4.0.5.tgz#fbfa36c7ba70461311f5debcb3928821eb4f9180" + integrity sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw== -semver@7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" - integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== +"semver@2 || 3 || 4 || 5": + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== -semver@7.3.5, semver@^7.1.1, semver@^7.3.4: +semver@7.3.5: version "7.3.5" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== dependencies: lru-cache "^6.0.0" -semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.2.1, semver@^7.3.2: - version "7.3.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" - integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== - dependencies: - lru-cache "^6.0.0" - -semver@^7.3.8: - version "7.3.8" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" - integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== - dependencies: - lru-cache "^6.0.0" - -serialize-javascript@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" - integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== - dependencies: - randombytes "^2.1.0" +semver@^7.1.1, semver@^7.1.2, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.6.3: + version "7.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f" + integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA== serialize-javascript@^6.0.2: version "6.0.2" @@ -5416,13 +5766,6 @@ set-blocking@^2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -5435,21 +5778,42 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -signal-exit@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== +shell-quote@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.2.tgz#d2d83e057959d53ec261311e9e9b8f51dcb2934a" + integrity sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA== -signal-exit@^3.0.3: +signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.6" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.6.tgz#24e630c4b0f03fea446a2bd299e62b4a6ca8d0af" integrity sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ== -signal-exit@^4.1.0: +signal-exit@^4.0.1, signal-exit@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== +signale@^1.2.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/signale/-/signale-1.4.0.tgz#c4be58302fb0262ac00fc3d886a7c113759042f1" + integrity sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w== + dependencies: + chalk "^2.3.2" + figures "^2.0.0" + pkg-conf "^2.1.0" + +sigstore@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/sigstore/-/sigstore-3.1.0.tgz#08dc6c0c425263e9fdab85ffdb6477550e2c511d" + integrity sha512-ZpzWAFHIFqyFE56dXqgX/DkDRZdz+rRcjoIk/RQU4IX0wiCv1l8S7ZrXDHcCc+uaf+6o7w3h2l3g6GYG5TKN9Q== + dependencies: + "@sigstore/bundle" "^3.1.0" + "@sigstore/core" "^2.0.0" + "@sigstore/protobuf-specs" "^0.4.0" + "@sigstore/sign" "^3.1.0" + "@sigstore/tuf" "^3.1.0" + "@sigstore/verify" "^2.1.0" + sinon@^12.0.1: version "12.0.1" resolved "https://registry.yarnpkg.com/sinon/-/sinon-12.0.1.tgz#331eef87298752e1b88a662b699f98e403c859e9" @@ -5462,16 +5826,23 @@ sinon@^12.0.1: nise "^5.1.0" supports-color "^7.2.0" -slash@^2.0.0: +skin-tone@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" - integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + resolved "https://registry.yarnpkg.com/skin-tone/-/skin-tone-2.0.0.tgz#4e3933ab45c0d4f4f781745d64b9f4c208e41237" + integrity sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA== + dependencies: + unicode-emoji-modifier-base "^1.0.0" slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slash@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-5.1.0.tgz#be3adddcdf09ac38eebe8dcdc7b1a57a75b095ce" + integrity sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg== + slice-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" @@ -5497,28 +5868,37 @@ slice-ansi@^7.0.0: ansi-styles "^6.2.1" is-fullwidth-code-point "^5.0.0" -source-map-support@^0.5.16, source-map-support@~0.5.20: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + +socks-proxy-agent@^8.0.3: + version "8.0.5" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz#b9cdb4e7e998509d7659d689ce7697ac21645bee" + integrity sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw== dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" + agent-base "^7.1.2" + debug "^4.3.4" + socks "^2.8.3" -source-map@^0.5.0: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= +socks@^2.8.3: + version "2.8.4" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.4.tgz#07109755cdd4da03269bda4725baa061ab56d5cc" + integrity sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ== + dependencies: + ip-address "^9.0.5" + smart-buffer "^4.2.0" -source-map@^0.6.0, source-map@^0.6.1: +source-map@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -sourcemap-codec@^1.4.4: - version "1.4.8" - resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" - integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== +spawn-error-forwarder@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/spawn-error-forwarder/-/spawn-error-forwarder-1.0.0.tgz#1afd94738e999b0346d7b9fc373be55e07577029" + integrity sha512-gRjMgK5uFjbCvdibeGJuy3I5OYz6VLoVdsOJdA6wV0WlfQVLFueoqMxwwYD9RODdgb6oUIvlRlsyFSiQkMKu0g== spawn-wrap@^2.0.0: version "2.0.0" @@ -5553,6 +5933,14 @@ spdx-expression-parse@^3.0.0: spdx-exceptions "^2.1.0" spdx-license-ids "^3.0.0" +spdx-expression-parse@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz#a23af9f3132115465dac215c099303e4ceac5794" + integrity sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + spdx-license-ids@^3.0.0: version "3.0.11" resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz#50c0d8c40a14ec1bf449bae69a0ea4685a9d9f95" @@ -5565,6 +5953,13 @@ split2@^3.0.0: dependencies: readable-stream "^3.0.0" +split2@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-1.0.0.tgz#52e2e221d88c75f9a73f90556e263ff96772b314" + integrity sha512-NKywug4u4pX/AZBB1FCPzZ6/7O+Xhz1qMVbzTvvKvikjO99oPN87SkK08mEY9P63/5lWjK+wgOOgApnTg5r6qg== + dependencies: + through2 "~2.0.0" + split@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" @@ -5572,11 +5967,23 @@ split@^1.0.0: dependencies: through "2" +sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= +ssri@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-12.0.0.tgz#bcb4258417c702472f8191981d3c8a771fee6832" + integrity sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ== + dependencies: + minipass "^7.0.3" + standard-version@^9.3.2: version "9.3.2" resolved "https://registry.yarnpkg.com/standard-version/-/standard-version-9.3.2.tgz#28db8c1be66fd2d736f28f7c5de7619e64cd6dab" @@ -5603,21 +6010,29 @@ state-toggle@^1.0.0: resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe" integrity sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ== +stream-combiner2@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stream-combiner2/-/stream-combiner2-1.1.1.tgz#fb4d8a1420ea362764e21ad4780397bebcb41cbe" + integrity sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw== + dependencies: + duplexer2 "~0.1.0" + readable-stream "^2.0.2" + string-argv@0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== -string-width@^4.1.0, string-width@^4.2.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" - integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: emoji-regex "^8.0.0" is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" + strip-ansi "^6.0.1" -string-width@^4.2.3: +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -5626,6 +6041,15 @@ string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + string-width@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.1.0.tgz#d994252935224729ea3719c49f7206dc9c46550a" @@ -5635,22 +6059,6 @@ string-width@^7.0.0: get-east-asian-width "^1.0.0" strip-ansi "^7.1.0" -string.prototype.trimend@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" - integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -string.prototype.trimstart@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" - integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -5670,21 +6078,21 @@ stringify-package@^1.0.1: resolved "https://registry.yarnpkg.com/stringify-package/-/stringify-package-1.0.1.tgz#e5aa3643e7f74d0f28628b72f3dad5cecfc3ba85" integrity sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg== -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: - ansi-regex "^5.0.0" + ansi-regex "^5.0.1" -strip-ansi@^6.0.1: +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" -strip-ansi@^7.1.0: +strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== @@ -5711,6 +6119,11 @@ strip-final-newline@^3.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== +strip-final-newline@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-4.0.0.tgz#35a369ec2ac43df356e3edd5dcebb6429aa1fa5c" + integrity sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw== + strip-indent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" @@ -5723,6 +6136,19 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + +super-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/super-regex/-/super-regex-1.0.0.tgz#dd90d944a925a1083e7d8570919b21cb76e3d925" + integrity sha512-CY8u7DtbvucKuquCmOFEKhr9Besln7n9uN8eFbwcoGYWXOMW07u2o8njWaiXt11ylS3qoGF55pILjRmPlbodyg== + dependencies: + function-timeout "^1.0.1" + time-span "^5.1.0" + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -5744,6 +6170,19 @@ supports-color@^8.1.1: dependencies: has-flag "^4.0.0" +supports-color@^9.4.0: + version "9.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-9.4.0.tgz#17bfcf686288f531db3dea3215510621ccb55954" + integrity sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw== + +supports-hyperlinks@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz#b8e485b179681dea496a1e7abdf8985bd3145461" + integrity sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig== + dependencies: + has-flag "^4.0.0" + supports-color "^7.0.0" + supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" @@ -5759,15 +6198,44 @@ table@^6.0.4: slice-ansi "^4.0.0" string-width "^4.2.0" -terser@^5.0.0: - version "5.14.2" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.14.2.tgz#9ac9f22b06994d736174f4091aa368db896f1c10" - integrity sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA== +tar@^6.1.11, tar@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +tar@^7.4.3: + version "7.4.3" + resolved "https://registry.yarnpkg.com/tar/-/tar-7.4.3.tgz#88bbe9286a3fcd900e94592cda7a22b192e80571" + integrity sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw== + dependencies: + "@isaacs/fs-minipass" "^4.0.0" + chownr "^3.0.0" + minipass "^7.1.2" + minizlib "^3.0.1" + mkdirp "^3.0.1" + yallist "^5.0.0" + +temp-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-3.0.0.tgz#7f147b42ee41234cc6ba3138cd8e8aa2302acffa" + integrity sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw== + +tempy@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/tempy/-/tempy-3.1.0.tgz#00958b6df85db8589cb595465e691852aac038e9" + integrity sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g== dependencies: - "@jridgewell/source-map" "^0.3.2" - acorn "^8.5.0" - commander "^2.20.0" - source-map-support "~0.5.20" + is-stream "^3.0.0" + temp-dir "^3.0.0" + type-fest "^2.12.2" + unique-string "^3.0.0" test-exclude@^6.0.0: version "6.0.0" @@ -5783,12 +6251,26 @@ text-extensions@^1.0.0: resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26" integrity sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ== -text-table@^0.2.0: +text-table@^0.2.0, text-table@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= -through2@^2.0.0: +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.1" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" + integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== + dependencies: + any-promise "^1.0.0" + +through2@^2.0.0, through2@~2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== @@ -5808,10 +6290,17 @@ through@2, "through@>=2.2.7 <3": resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= +time-span@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/time-span/-/time-span-5.1.0.tgz#80c76cf5a0ca28e0842d3f10a4e99034ce94b90d" + integrity sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA== + dependencies: + convert-hrtime "^5.0.0" + +tiny-relative-date@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/tiny-relative-date/-/tiny-relative-date-1.3.0.tgz#fa08aad501ed730f31cc043181d995c39a935e07" + integrity sha512-MOQHpzllWxDCHHaDno30hhLfbouoYlOI8YlMNtvKe1zXbjEVhbcEovQxvZrPvtiYW630GQDoMMarCnjfyfHA+A== to-regex-range@^5.0.1: version "5.0.1" @@ -5820,6 +6309,21 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +traverse@0.6.8: + version "0.6.8" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.8.tgz#5e5e0c41878b57e4b73ad2f3d1e36a715ea4ab15" + integrity sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA== + +tree-kill@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + +treeverse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/treeverse/-/treeverse-3.0.0.tgz#dd82de9eb602115c6ebd77a574aae67003cb48c8" + integrity sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ== + trim-newlines@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" @@ -5840,12 +6344,12 @@ trough@^1.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== -ts-node@^10.4.0: - version "10.4.0" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7" - integrity sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A== +ts-node@^10.4.0, ts-node@^10.9.2: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== dependencies: - "@cspotcode/source-map-support" "0.7.0" + "@cspotcode/source-map-support" "^0.8.0" "@tsconfig/node10" "^1.0.7" "@tsconfig/node12" "^1.0.7" "@tsconfig/node14" "^1.0.0" @@ -5856,6 +6360,7 @@ ts-node@^10.4.0: create-require "^1.1.0" diff "^4.0.1" make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" yn "3.1.1" tslib@^1.8.1: @@ -5863,6 +6368,11 @@ tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.1.0, tslib@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tsutils@^3.17.1: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -5870,6 +6380,15 @@ tsutils@^3.17.1: dependencies: tslib "^1.8.1" +tuf-js@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/tuf-js/-/tuf-js-3.0.1.tgz#e3f07ed3d8e87afaa70607bd1ef801d5c1f57177" + integrity sha512-+68OP1ZzSF84rTckf3FA95vJ1Zlx/uaXyiiKyPd1pA4rZNkpEvDAKmsu1xUSmbF/chCRYgZ6UZkDwC7PmzmAyA== + dependencies: + "@tufjs/models" "3.0.1" + debug "^4.3.6" + make-fetch-happen "^14.0.1" + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -5897,11 +6416,26 @@ type-fest@^0.8.0, type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^1.0.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" + integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== + +type-fest@^2.12.2: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + type-fest@^3.0.0: version "3.13.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.13.1.tgz#bb744c1f0678bea7543a2d1ec24e83e68e8c8706" integrity sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g== +type-fest@^4.6.0, type-fest@^4.7.1: + version "4.35.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.35.0.tgz#007ed74d65c2ca0fb3b564b3dc8170d5c872d665" + integrity sha512-2/AwEFQDFEy30iOLjrvHDIH7e4HEWH+f1Yl1bI5XMqzuoCUqwYCdxachgsgv0og/JdVZUhbfjcJAoHj5L1753A== + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -5914,30 +6448,25 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3" - integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw== - typescript@^4.4.3: version "4.5.4" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8" integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg== +typescript@^5.7.3: + version "5.7.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.3.tgz#919b44a7dbb8583a9b856d162be24a54bf80073e" + integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw== + uglify-js@^3.1.4: version "3.14.5" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.14.5.tgz#cdabb7d4954231d80cb4a927654c4655e51f4859" integrity sha512-qZukoSxOG0urUTvjc2ERMTcAy+BiFh3weWAkeurLwjrCba73poHmG3E36XEjd/JGukMzwTL7uCxZiAexj8ppvQ== -unbox-primitive@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.0.tgz#eeacbc4affa28e9b3d36b5eaeccc50b3251b1d3f" - integrity sha512-P/51NX+JXyxK/aigg1/ZgyccdAxm5K1+n8+tvqSntjOivPt19gvm1VC49RWYetsiub8WViUchdxl/KWHHB0kzA== - dependencies: - function-bind "^1.1.1" - has-bigints "^1.0.0" - has-symbols "^1.0.0" - which-boxed-primitive "^1.0.1" +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== unherit@^1.0.4: version "1.1.3" @@ -5947,28 +6476,20 @@ unherit@^1.0.4: inherits "^2.0.0" xtend "^4.0.0" -unicode-canonical-property-names-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" - integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ== - -unicode-match-property-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c" - integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg== - dependencies: - unicode-canonical-property-names-ecmascript "^1.0.4" - unicode-property-aliases-ecmascript "^1.0.4" +unicode-emoji-modifier-base@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz#dbbd5b54ba30f287e2a8d5a249da6c0cef369459" + integrity sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g== -unicode-match-property-value-ecmascript@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531" - integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ== +unicorn-magic@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/unicorn-magic/-/unicorn-magic-0.1.0.tgz#1bb9a51c823aaf9d73a8bfcd3d1a23dde94b0ce4" + integrity sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ== -unicode-property-aliases-ecmascript@^1.0.4: - version "1.1.0" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4" - integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg== +unicorn-magic@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/unicorn-magic/-/unicorn-magic-0.3.0.tgz#4efd45c85a69e0dd576d25532fbfa22aa5c8a104" + integrity sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA== unified@^6.1.2: version "6.2.0" @@ -5982,6 +6503,27 @@ unified@^6.1.2: vfile "^2.0.0" x-is-string "^0.1.0" +unique-filename@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-4.0.0.tgz#a06534d370e7c977a939cd1d11f7f0ab8f1fed13" + integrity sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ== + dependencies: + unique-slug "^5.0.0" + +unique-slug@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-5.0.0.tgz#ca72af03ad0dbab4dad8aa683f633878b1accda8" + integrity sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg== + dependencies: + imurmurhash "^0.1.4" + +unique-string@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-3.0.0.tgz#84a1c377aff5fd7a8bc6b55d8244b2bd90d75b9a" + integrity sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ== + dependencies: + crypto-random-string "^4.0.0" + unist-util-is@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-3.0.0.tgz#d9e84381c2468e82629e4a5be9d7d05a2dd324cd" @@ -6013,11 +6555,24 @@ unist-util-visit@^1.1.0: dependencies: unist-util-visit-parents "^2.0.0" +universal-user-agent@^7.0.0, universal-user-agent@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-7.0.2.tgz#52e7d0e9b3dc4df06cc33cb2b9fd79041a54827e" + integrity sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q== + universalify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== +update-browserslist-db@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz#97e9c96ab0ae7bcac08e9ae5151d26e6bc6b5580" + integrity sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -6025,7 +6580,12 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -util-deprecate@^1.0.1, util-deprecate@~1.0.1: +url-join@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/url-join/-/url-join-5.0.0.tgz#c2f1e5cbd95fa91082a93b58a1f42fecb4bdbcf1" + integrity sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA== + +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= @@ -6040,19 +6600,17 @@ uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== -v8flags@^3.1.1: - version "3.2.0" - resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.2.0.tgz#b243e3b4dfd731fa774e7492128109a0fe66d656" - integrity sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg== - dependencies: - homedir-polyfill "^1.0.1" - -validate-npm-package-license@^3.0.1: +validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== @@ -6060,6 +6618,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +validate-npm-package-name@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-6.0.0.tgz#3add966c853cfe36e0e8e6a762edd72ae6f1d6ac" + integrity sha512-d7KLgL1LD3U3fgnvWEY1cQXoO/q6EQ1BSz48Sa149V/5zVTAbgmZIpyI8TRi6U9/JNyeYLlTKsEMPtLC27RFUg== + vfile-location@^2.0.0: version "2.0.6" resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-2.0.6.tgz#8a274f39411b8719ea5728802e10d9e0dff1519e" @@ -6082,16 +6645,10 @@ vfile@^2.0.0: unist-util-stringify-position "^1.0.0" vfile-message "^1.0.0" -which-boxed-primitive@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== - dependencies: - is-bigint "^1.0.1" - is-boolean-object "^1.1.0" - is-number-object "^1.0.4" - is-string "^1.0.5" - is-symbol "^1.0.3" +walk-up-path@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/walk-up-path/-/walk-up-path-3.0.1.tgz#c8d78d5375b4966c717eb17ada73dbd41490e886" + integrity sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA== which-module@^2.0.0: version "2.0.0" @@ -6110,6 +6667,13 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +which@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/which/-/which-5.0.0.tgz#d93f2d93f79834d4363c7d0c23e00d07c466c8d6" + integrity sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ== + dependencies: + isexe "^3.1.1" + word-wrap@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" @@ -6125,6 +6689,15 @@ workerpool@^6.5.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" @@ -6143,6 +6716,15 @@ wrap-ansi@^7.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + wrap-ansi@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-9.0.0.tgz#1a3dc8b70d85eeb8398ddfb1e4a02cd186e58b3e" @@ -6167,10 +6749,18 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -ws@^7.5.10: - version "7.5.10" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" - integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== +write-file-atomic@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-6.0.0.tgz#e9c89c8191b3ef0606bc79fb92681aa1aa16fa93" + integrity sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^4.0.1" + +ws@^8.18.1: + version "8.18.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb" + integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w== x-is-string@^0.1.0: version "0.1.0" @@ -6192,11 +6782,21 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18" integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg== +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yallist@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-5.0.0.tgz#00e2de443639ed0d78fd87de0d27469fbcffb533" + integrity sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw== + yaml@2.3.4: version "2.3.4" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" @@ -6215,20 +6815,15 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^20.2.2: - version "20.2.6" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.6.tgz#69f920addf61aafc0b8b89002f5d66e28f2d8b20" - integrity sha512-AP1+fQIWSM/sMiET8fyayjx/J+JmTPt2Mr0FkrgqB4todtfa53sOsrSAcIrJRD5XS20bKUwaDIuMkWKCEiQLKA== - -yargs-parser@^20.2.3, yargs-parser@^20.2.9: +yargs-parser@^20.2.2, yargs-parser@^20.2.3: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs-parser@^21.0.0: - version "21.0.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.0.tgz#a485d3966be4317426dd56bdb6a30131b281dc55" - integrity sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA== +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== yargs-unparser@^2.0.0: version "2.0.0" @@ -6270,18 +6865,18 @@ yargs@^16.0.0, yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.0.0: - version "17.3.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.3.1.tgz#da56b28f32e2fd45aefb402ed9c26f42be4c07b9" - integrity sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA== +yargs@^17.0.0, yargs@^17.5.1, yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== dependencies: - cliui "^7.0.2" + cliui "^8.0.1" escalade "^3.1.1" get-caller-file "^2.0.5" require-directory "^2.1.1" string-width "^4.2.3" y18n "^5.0.5" - yargs-parser "^21.0.0" + yargs-parser "^21.1.1" yn@3.1.1: version "3.1.1" @@ -6292,3 +6887,8 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +yoctocolors@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/yoctocolors/-/yoctocolors-2.1.1.tgz#e0167474e9fbb9e8b3ecca738deaa61dd12e56fc" + integrity sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ== From b7dfd9ffea5ef8b383effad1260d99e29159f7b4 Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Thu, 27 Feb 2025 10:29:29 +0100 Subject: [PATCH 02/47] chore: adjust CHANGELOG.md --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06bba4092b..07f66af14d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,10 +26,6 @@ All notable changes to this project will be documented in this file. See [standa * support campaign user pagination ([#1462](https://github.com/GetStream/stream-chat-js/issues/1462)) ([c629018](https://github.com/GetStream/stream-chat-js/commit/c629018df77b8a956b4cf533b0c2bbeb701a03c8)) * support new X-Stream-Client format ([#1469](https://github.com/GetStream/stream-chat-js/issues/1469)) ([fdb875b](https://github.com/GetStream/stream-chat-js/commit/fdb875b53b9ec38dc81a3afc5c00f3bc561ef06a)) -# Changelog - -All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. - ### [8.56.1](https://github.com/GetStream/stream-chat-js/compare/v8.56.0...v8.56.1) (2025-02-13) From aeb1b10d3e836ec32764acd43573cfd471d1f398 Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Thu, 27 Feb 2025 10:38:52 +0100 Subject: [PATCH 03/47] chore: adjust package.json#version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 47e9456660..4b452fe422 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stream-chat", - "version": "8.57.1", + "version": "0.0.0-development", "description": "JS SDK for the Stream Chat API", "homepage": "https://getstream.io/chat/", "author": { From 724558e60fb643a5cdfc99c8d26fcf4d8ec7d8ff Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Thu, 27 Feb 2025 10:49:17 +0100 Subject: [PATCH 04/47] chore: change package.json#prepare to prepack, add rc to .releaserc.json --- .releaserc.json | 15 +++++++++++++-- package.json | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.releaserc.json b/.releaserc.json index 6e6d11bb4b..33a4d01ee5 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -8,6 +8,10 @@ "name": "release-v8", "channel": "v8", "range": "8.x" + }, + { + "name": "rc", + "prerelease": true } ], "plugins": [ @@ -16,8 +20,15 @@ { "preset": "angular", "releaseRules": [ - { "type": "chore", "scope": "deps", "release": "patch" }, - { "type": "refactor", "release": "patch" } + { + "type": "chore", + "scope": "deps", + "release": "patch" + }, + { + "type": "refactor", + "release": "patch" + } ] } ], diff --git a/package.json b/package.json index 4b452fe422..1acba915e6 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "lint-fix": "yarn run prettier-fix && yarn run eslint-fix", "fix-staged": "lint-staged --config .lintstagedrc.fix.json --concurrent 1", "semantic-release": "semantic-release", - "prepare": "yarn run build" + "prepack": "yarn run build" }, "engines": { "node": ">=16" From 4fc5f6dbe30e7edb41f0bf29d3e9b36b3e665524 Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Thu, 27 Feb 2025 10:56:15 +0100 Subject: [PATCH 05/47] chore: adjust NEXT_VERSION generation --- .releaserc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.releaserc.json b/.releaserc.json index 33a4d01ee5..9f8f05cb2e 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -71,7 +71,7 @@ [ "@semantic-release/exec", { - "prepareCmd": "NEXT_VERSION=${nextRelease.version} npm run build" + "prepareCmd": "echo \"NEXT_VERSION=${nextRelease.version}\" >> $GITHUB_ENV" } ], [ From 92a546ef7ebd932b0d8952d8034ce375c59e5bce Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Thu, 27 Feb 2025 11:12:06 +0100 Subject: [PATCH 06/47] chore: adjust names of workflows --- .github/workflows/lint.yml | 2 +- .github/workflows/size.yml | 2 +- .github/workflows/type.yml | 2 +- .github/workflows/unit.yml | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 223f0707da..7099251a0e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -15,7 +15,7 @@ jobs: - uses: ./.github/actions/setup-node - - name: Commit Message Lint + - name: Commit message lint run: echo "${{ github.event.pull_request.title }}" | yarn commitlinter - name: Lint diff --git a/.github/workflows/size.yml b/.github/workflows/size.yml index 8177f6288b..a022ad3e44 100644 --- a/.github/workflows/size.yml +++ b/.github/workflows/size.yml @@ -1,4 +1,4 @@ -name: Compressed Size +name: Compressed size on: pull_request: diff --git a/.github/workflows/type.yml b/.github/workflows/type.yml index 6ed9736ed3..362e6ebf49 100644 --- a/.github/workflows/type.yml +++ b/.github/workflows/type.yml @@ -1,4 +1,4 @@ -name: Type Test +name: Type test on: [pull_request] concurrency: diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index d3ced8eabe..4c52e093d5 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -1,4 +1,4 @@ -name: Unit Test +name: Unit test on: [pull_request] concurrency: @@ -13,5 +13,5 @@ jobs: - uses: ./.github/actions/setup-node - - name: Unit Tests with Node ${{ env.NODE_VERSION }} + - name: Unit tests with Node ${{ env.NODE_VERSION }} run: yarn run test-coverage From feb97da08a5c4fa325156517111bb58402a1f7b8 Mon Sep 17 00:00:00 2001 From: Anton Arnautov <43254280+arnautov-anton@users.noreply.github.com> Date: Thu, 27 Feb 2025 11:38:23 +0100 Subject: [PATCH 07/47] fix: replace StreamChatGenerics with module augmentation (#1458) fix: `channel_role` has been removed from the type of the `members` parameter of the `Channel.inviteMembers` method fix: properties `created_by` and `created_by_id` have been added to `ChannelData` and `ChannelQueryOptions` types BREAKING CHANGE: dropped jsDelivr bundle (#1468) BREAKING CHANGE: dropped `StreamChatGenerics`, use `CustomData` to extend your types BREAKING CHANGE: type `InviteOptions` has been renamed to `UpdateChannelOptions` BREAKING CHANGE: type `UpdateChannelOptions` has been renamed to `UpdateChannelTypeRequest` BREAKING CHANGE: type `ThreadResponseCustomData` has been renamed to `CustomThreadData` BREAKING CHANGE: type `MarkAllReadOptions` has been deleted in favour of type `MarkChannelsReadOptions` BREAKING CHANGE: type `QueryFilter` no longer supports `$ne` and `$nin` operators BREAKING CHANGE: type `ChannelMembership` has been deleted in favour of type `ChannelMemberResponse` BREAKING CHANGE: function `formatMessage` (`utils.ts`) no longer returns `__html` property in the formatted message output --- README.md | 124 ++-- assets/logo.svg | 47 +- docs/typescript.md | 122 ---- src/campaign.ts | 8 +- src/channel.ts | 421 +++++------ src/channel_manager.ts | 88 +-- src/channel_state.ts | 141 ++-- src/client.ts | 608 +++++++--------- src/client_state.ts | 16 +- src/connection.ts | 31 +- src/connection_fallback.ts | 12 +- src/custom_types.ts | 13 + src/errors.ts | 2 +- src/index.ts | 1 + src/moderation.ts | 21 +- src/permissions.ts | 2 +- src/poll.ts | 119 ++- src/poll_manager.ts | 25 +- src/search_controller.ts | 86 +-- src/segment.ts | 15 +- src/thread.ts | 70 +- src/thread_manager.ts | 28 +- src/token_manager.ts | 12 +- src/types.ts | 1407 ++++++++++++++++-------------------- src/utils.ts | 97 +-- 25 files changed, 1534 insertions(+), 1982 deletions(-) delete mode 100644 docs/typescript.md create mode 100644 src/custom_types.ts diff --git a/README.md b/README.md index c558a67b32..de40b906de 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ You can sign up for a Stream account at our [Get Started](https://getstream.io/chat/get_started/) page. -This library can be used by both frontend and backend applications. For frontend, we have frameworks that are based on this library such as the [Flutter](https://github.com/GetStream/stream-chat-flutter), [React](https://github.com/GetStream/stream-chat-react) and [Angular](https://github.com/GetStream/stream-chat-angular) SDKs. For more information, check out our [docs](https://getstream.io/chat/docs/). +This library can be used by both frontend and backend applications. For frontend, we have frameworks that are based on this library such as the [Flutter](https://github.com/GetStream/stream-chat-flutter), [React](https://github.com/GetStream/stream-chat-react) and [Angular](https://github.com/GetStream/stream-chat-angular) SDKs. For more information, check out our [documentation](https://getstream.io/chat/docs/). ## ⚙️ Installation @@ -36,95 +36,89 @@ npm install stream-chat yarn add stream-chat ``` -### JS deliver +## ✨ Getting Started -```html - -``` +```ts +import { StreamChat } from 'stream-chat'; +// or if you are using CommonJS +const { StreamChat } = require('stream-chat'); -## ✨ Getting started +const client = new StreamChat('API_KEY', 'API_SECRET', { + disableCache: true, // recommended option for server-side use + // ...other options like `baseURL`... +}); -The StreamChat client is setup to allow extension of the base types through use of generics when instantiated. The default instantiation has all generics set to `Record`. +// create a user +await client.upsertUser({ + id: 'vishal-1', + name: 'Vishal', +}); -```typescript -import { StreamChat } from 'stream-chat'; -// Or if you are on commonjs -const StreamChat = require('stream-chat').StreamChat; +// create a channel +const channel = client.channel('messaging', 'test-channel', { created_by_id: 'vishal-1' }); +await channel.create(); -const client = StreamChat.getInstance('YOUR_API_KEY', 'API_KEY_SECRET'); +// send message +const { message } = await channel.sendMessage({ text: 'This is a test message' }); -const channel = client.channel('messaging', 'TestChannel'); -await channel.create(); +// send reaction +await channel.sendReaction(message.id, { type: 'love', user: { id: 'vishal-1' } }); ``` -Or you can customize the generics: - -```typescript -type ChatChannel = { image: string; category?: string }; -type ChatUser1 = { nickname: string; age: number; admin?: boolean }; -type ChatUser2 = { nickname: string; avatar?: string }; -type UserMessage = { country?: string }; -type AdminMessage = { priorityLevel: number }; -type ChatAttachment = { originalURL?: string }; -type CustomReaction = { size?: number }; -type ChatEvent = { quitChannel?: boolean }; -type CustomCommands = 'giphy'; - -type StreamType = { - attachmentType: ChatAttachment; - channelType: ChatChannel; - commandType: CustomCommands; - eventType: ChatEvent; - messageType: UserMessage | AdminMessage; - reactionType: CustomReaction; - userType: ChatUser1 | ChatUser2; -}; +The `StreamChat` client is set up to allow extension of the base types through use of module augmentation, custom types will carry through to all client returns and provide code-completion to queries (if supported). To extend Stream's entities with custom data you'll have to create a declaration file and make sure it's loaded by TypeScript, [see the list of extendable interfaces](https://github.com/GetStream/stream-chat-js/blob/master/src/custom_types.ts) and the example bellow using two of the most common ones: -const client = StreamChat.getInstance('YOUR_API_KEY', 'API_KEY_SECRET'); +```ts +// stream-custom-data.d.ts -// Create channel -const channel = client.channel('messaging', 'TestChannel'); -await channel.create(); +import 'stream-chat'; -// Create user -await client.upsertUser({ - id: 'vishal-1', - name: 'Vishal', -}); +declare module 'stream-chat' { + interface CustomMessageData { + custom_property?: number; + } + interface CustomUserData { + profile_picture?: string; + } +} -// Send message -const { message } = await channel.sendMessage({ text: `Test message` }); +// index.ts -// Send reaction -await channel.sendReaction(message.id, { type: 'love', user: { id: 'vishal-1' } }); +// property `profile_picture` is code-completed and expects type `string | undefined` +await client.partialUpdateUser({ id: 'vishal-1', set: { profile_picture: 'https://random.picture/1.jpg' } }); + +// property `custom_property` is code-completed and expects type `number | undefined` +const { message } = await channel.sendMessage({ text: 'This is another test message', custom_property: 255 }); + +message.custom_property; // in the response object as well ``` -Custom types provided when initializing the client will carry through to all client returns and provide intellisense to queries. +> [!WARNING] +> Generics mechanism has been removed in version `9.0.0` in favour of the module augmentation, please see [the release guide](https://getstream.io/chat/docs/node/upgrade-stream-chat-to-v9) on how to migrate. -## 🔗 (Optional) Development Setup in Combination with our SDKs +## 🔗 (Optional) Development Setup in Combination With Our SDKs ### Connect to [Stream Chat React Native SDK](https://github.com/GetStream/stream-chat-react-native) -Run in the root of this repo +Run in the root of this repository: -```shell +```sh yarn link ``` -Run in the root of one of the example apps (SampleApp/TypeScriptMessaging) in the `stream-chat-react-native` repo +Run in the root of one of the example applications (SampleApp/TypeScriptMessaging) in the `stream-chat-react-native` repository: -```shell +```sh yarn link stream-chat yarn start ``` -Open `metro.config.js` file and set value for watchFolders as +Open `metro.config.js` file and set value for `watchFolders` as: -```javascript -const streamChatRoot = '{{CHANGE_TO_THE_PATH_TO_YOUR_PROJECT}}/stream-chat-js' +```js +const streamChatRoot = '/stream-chat-js' module.exports = { - // the rest of the metro config goes here + // the rest of the metro configuration goes here ... watchFolders: [projectRoot].concat(alternateRoots).concat([streamChatRoot]), resolver: { @@ -139,17 +133,17 @@ module.exports = { }; ``` -Make sure to replace `{{CHANGE_TO_THE_PATH_TO_YOUR_PROJECT}}` with the correct path for the `stream-chat-js` folder as per your directory structure. +Make sure to replace `` with the correct path for the `stream-chat-js` folder as per your directory structure. -Run in the root of this repo +Run in the root of this repository: -```shell +```sh yarn start ``` -## 📚 More code examples +## 📚 More Code Examples -Head over to [docs/typescript.md](./docs/typescript.md) for more examples. +Read up more on [Logging](./docs/logging.md) and [User Token](./docs/userToken.md) or visit our [documentation](https://getstream.io/chat/docs/) for more examples. ## ✍️ Contributing @@ -157,7 +151,7 @@ We welcome code changes that improve this library or fix a problem, please make Head over to [CONTRIBUTING.md](./CONTRIBUTING.md) for some development tips. -## 🧑‍💻 We are hiring! +## 🧑‍💻 We Are Hiring! We've recently closed a [$38 million Series B funding round](https://techcrunch.com/2021/03/04/stream-raises-38m-as-its-chat-and-activity-feed-apis-power-communications-for-1b-users/) and we keep actively growing. Our APIs are used by more than a billion end-users, and you'll have a chance to make a huge impact on the product within a team of the strongest engineers all over the world. diff --git a/assets/logo.svg b/assets/logo.svg index 1c68c5cccb..ac2e18775a 100644 --- a/assets/logo.svg +++ b/assets/logo.svg @@ -1,16 +1,31 @@ - - - - STREAM MARK - Created with Sketch. - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/typescript.md b/docs/typescript.md deleted file mode 100644 index 118ba59b7c..0000000000 --- a/docs/typescript.md +++ /dev/null @@ -1,122 +0,0 @@ -# Typescript (v2.x.x) - -The StreamChat client is setup to allow extension of the base types through use of generics when instantiated. The default instantiation has all generics set to `Record`. - -```typescript -StreamChat<{ - attachmentType: AttachmentType; - channelType: ChannelType; - commandType: CommandType; - eventType: EventType; - messageType: MessageType; - reactionType: ReactionType; - userType: UserType; -}> -``` - -Custom types provided when initializing the client will carry through to all client returns and provide intellisense to queries. - -**NOTE:** If you utilize the `setAnonymousUser` function you must account for this in your user types. - -```typescript -import { StreamChat } from 'stream-chat'; -// or if you are on commonjs -const StreamChat = require('stream-chat').StreamChat; - -type ChatChannel = { image: string; category?: string }; -type ChatUser1 = { nickname: string; age: number; admin?: boolean }; -type ChatUser2 = { nickname: string; avatar?: string }; -type UserMessage = { country?: string }; -type AdminMessage = { priorityLevel: number }; -type ChatAttachment = { originalURL?: string }; -type CustomReaction = { size?: number }; -type ChatEvent = { quitChannel?: boolean }; -type CustomCommands = 'giphy'; - -type StreamType = { - attachmentType: ChatAttachment; - channelType: ChatChannel; - commandType: CustomCommands; - eventType: ChatEvent; - messageType: UserMessage | AdminMessage; - reactionType: CustomReaction; - userType: ChatUser1 | ChatUser2; -}; - -// Instantiate a new client (server side) -// you can also use `new StreamChat()` -const client = StreamChat.getInstance('YOUR_API_KEY', 'API_KEY_SECRET'); - -/** - * Instantiate a new client (client side) - * Unused generics default to Record - * with the exception of Command which defaults to string & {} - */ -const client = StreamChat.getInstance('YOUR_API_KEY'); -``` - -Query operations will return results that utilize the custom types added via generics. In addition the query filters are type checked and provide intellisense using both the key and type of the parameter to ensure accurate use. - -```typescript -// Valid queries -// users: { duration: string; users: UserResponse[]; } -const users = await client.queryUsers({ id: '1080' }); -const users = await client.queryUsers({ nickname: 'streamUser' }); -const users = await client.queryUsers({ nickname: { $eq: 'streamUser' } }); - -// Invalid queries -const users = await client.queryUsers({ nickname: { $contains: ['stream'] } }); // $contains is only an operator on arrays -const users = await client.queryUsers({ nickname: 1080 }); // nickname must be a string -const users = await client.queryUsers({ name: { $eq: 1080 } }); // name must be a string -``` - -**Note:** If you have differing union types like `ChatUser1 | ChatUser2` or `UserMessage | AdminMessage` you can use [type guards](https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types) to maintain type safety when dealing with the results of queries. - -```typescript -function isChatUser1(user: ChatUser1 | ChatUser2): user is ChatUser1 { - return (user as ChatUser1).age !== undefined; -} - -function isAdminMessage(msg: UserMessage | AdminMessage): msg is AdminMessage { - return (msg as AdminMessage).priorityLevel !== undefined; -} -``` - -Intellisense, type checking, and return types are provided for all queries. - -```typescript -const channel = client.channel('messaging', 'TestChannel'); -await channel.create(); - -// Valid queries -// messages: SearchAPIResponse -const messages = await channel.search({ country: 'NL' }); -const messages = await channel.search({ priorityLevel: { $gt: 5 } }); -const messages = await channel.search({ - $and: [{ priorityLevel: { $gt: 5 } }, { deleted_at: { $exists: false } }], -}); - -// Invalid queries -const messages = await channel.search({ country: { $eq: 5 } }); // country must be a string -const messages = await channel.search({ - $or: [{ id: '2' }, { reaction_counts: { $eq: 'hello' } }], -}); // reaction_counts must be a number -``` - -Custom types are carried into all creation functions as well. - -```typescript -// Valid -client.connectUser({ id: 'testId', nickname: 'testUser', age: 3 }, 'TestToken'); -client.connectUser({ id: 'testId', nickname: 'testUser', avatar: 'testAvatar' }, 'TestToken'); - -// Invalid -client.connectUser({ id: 'testId' }, 'TestToken'); // Type ChatUser1 | ChatUser2 requires nickname for both types -client.connectUser({ id: 'testId', nickname: true }, 'TestToken'); // nickname must be a string -client.connectUser({ id: 'testId', nickname: 'testUser', country: 'NL' }, 'TestToken'); // country does not exist on type ChatUser1 | ChatUser2 -``` - -## More - -- [Logging](./logging.md) -- [User Token](./userToken.md) diff --git a/src/campaign.ts b/src/campaign.ts index 277cafe8db..217bfb2009 100644 --- a/src/campaign.ts +++ b/src/campaign.ts @@ -1,12 +1,12 @@ import { StreamChat } from './client'; -import { CampaignData, DefaultGenerics, ExtendableGenerics, GetCampaignOptions } from './types'; +import { CampaignData, GetCampaignOptions } from './types'; -export class Campaign { +export class Campaign { id: string | null; data?: CampaignData; - client: StreamChat; + client: StreamChat; - constructor(client: StreamChat, id: string | null, data?: CampaignData) { + constructor(client: StreamChat, id: string | null, data?: CampaignData) { this.client = client; this.id = id; this.data = data; diff --git a/src/channel.ts b/src/channel.ts index 94962b2eb9..d22d2b03e4 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -1,7 +1,8 @@ import { ChannelState } from './channel_state'; import { generateChannelTempCid, logChatPromiseExecution, messageSetPagination, normalizeQuerySort } from './utils'; import { StreamChat } from './client'; -import { +import { DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE } from './constants'; +import type { APIResponse, BanUserOptions, ChannelAPIResponse, @@ -14,18 +15,15 @@ import { ChannelUpdateOptions, CreateCallOptions, CreateCallResponse, - DefaultGenerics, DeleteChannelAPIResponse, Event, EventAPIResponse, EventHandler, EventTypes, - ExtendableGenerics, FormatMessageResponse, GetMultipleMessagesAPIResponse, GetReactionsAPIResponse, GetRepliesAPIResponse, - InviteOptions, MarkReadOptions, MarkUnreadOptions, MemberFilters, @@ -62,23 +60,24 @@ import { AIState, MessageOptions, PushPreference, + UpdateChannelOptions, } from './types'; -import { Role } from './permissions'; -import { DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE } from './constants'; +import type { Role } from './permissions'; +import type { CustomChannelData } from './custom_types'; /** * Channel - The Channel class manages it's own state. */ -export class Channel { - _client: StreamChat; +export class Channel { + _client: StreamChat; type: string; id: string | undefined; - data: ChannelData | ChannelResponse | undefined; - _data: ChannelData | ChannelResponse; + data: Partial | undefined; + _data: Partial; cid: string; /** */ - listeners: { [key: string]: (string | EventHandler)[] }; - state: ChannelState; + listeners: { [key: string]: (string | EventHandler)[] }; + state: ChannelState; /** * This boolean is a vague indication of weather the channel exists on chat backend. * @@ -103,19 +102,14 @@ export class Channel} client the chat client + * @param {StreamChat} client the chat client * @param {string} type the type of channel * @param {string} [id] the id of the chat - * @param {ChannelData} data any additional custom params + * @param {ChannelData} data any additional custom params * - * @return {Channel} Returns a new uninitialized channel + * @return {Channel} Returns a new uninitialized channel */ - constructor( - client: StreamChat, - type: string, - id: string | undefined, - data: ChannelData, - ) { + constructor(client: StreamChat, type: string, id: string | undefined, data: ChannelData) { const validTypeRe = /^[\w_-]+$/; const validIDRe = /^[\w!_-]+$/; @@ -136,7 +130,7 @@ export class Channel(this); + this.state = new ChannelState(this); this.initialized = false; this.offlineMode = false; this.lastTypingEvent = null; @@ -147,9 +141,9 @@ export class Channel} + * @return {StreamChat} */ - getClient(): StreamChat { + getClient(): StreamChat { if (this.disconnected === true) { throw Error(`You can't use a channel after client.disconnect() was called`); } @@ -169,7 +163,7 @@ export class Channel} message The Message object + * @param {Message} message The Message object * @param {boolean} [options.skip_enrich_url] Do not try to enrich the URLs within message * @param {boolean} [options.skip_push] Skip sending push notifications * @param {boolean} [options.is_pending_message] DEPRECATED, please use `pending` instead. @@ -177,10 +171,10 @@ export class Channel} [options.pending_message_metadata] Metadata for the pending message * @param {boolean} [options.force_moderation] Apply force moderation for server-side requests * - * @return {Promise>} The Server Response + * @return {Promise} The Server Response */ - async sendMessage(message: Message, options?: SendMessageOptions) { - return await this.getClient().post>(this._channelURL() + '/message', { + async sendMessage(message: Message, options?: SendMessageOptions) { + return await this.getClient().post(this._channelURL() + '/message', { message, ...options, }); @@ -190,17 +184,12 @@ export class Channel, + user?: UserResponse, ) { return this.getClient().sendFile(`${this._channelURL()}/file`, uri, name, contentType, user); } - sendImage( - uri: string | NodeJS.ReadableStream | File, - name?: string, - contentType?: string, - user?: UserResponse, - ) { + sendImage(uri: string | NodeJS.ReadableStream | File, name?: string, contentType?: string, user?: UserResponse) { return this.getClient().sendFile(`${this._channelURL()}/image`, uri, name, contentType, user); } @@ -215,13 +204,13 @@ export class Channel} event for example {type: 'message.read'} + * @param {Event} event for example {type: 'message.read'} * - * @return {Promise>} The Server Response + * @return {Promise} The Server Response */ - async sendEvent(event: Event) { + async sendEvent(event: Event) { this._checkInitialized(); - return await this.getClient().post>(this._channelURL() + '/event', { + return await this.getClient().post(this._channelURL() + '/event', { event, }); } @@ -229,17 +218,17 @@ export class Channel | string} query search query or object MongoDB style filters - * @param {{client_id?: string; connection_id?: string; query?: string; message_filter_conditions?: MessageFilters}} options Option object, {user_id: 'tommaso'} + * @param {MessageFilters | string} query search query or object MongoDB style filters + * @param {{client_id?: string; connection_id?: string; query?: string; message_filter_conditions?: MessageFilters}} options Option object, {user_id: 'tommaso'} * - * @return {Promise>} search messages response + * @return {Promise} search messages response */ async search( - query: MessageFilters | string, - options: SearchOptions & { + query: MessageFilters | string, + options: SearchOptions & { client_id?: string; connection_id?: string; - message_filter_conditions?: MessageFilters; + message_filter_conditions?: MessageFilters; message_options?: MessageOptions; query?: string; } = {}, @@ -248,10 +237,10 @@ export class Channel = { - filter_conditions: { cid: this.cid } as ChannelFilters, + const payload: SearchPayload = { + filter_conditions: { cid: this.cid } as ChannelFilters, ...options, - sort: options.sort ? normalizeQuerySort>(options.sort) : undefined, + sort: options.sort ? normalizeQuerySort(options.sort) : undefined, }; if (typeof query === 'string') { payload.query = query; @@ -263,7 +252,7 @@ export class Channel>(this.getClient().baseURL + '/search', { + return await this.getClient().get(this.getClient().baseURL + '/search', { payload, }); } @@ -271,56 +260,49 @@ export class Channel} filterConditions object MongoDB style filters - * @param {MemberSort} [sort] Sort options, for instance [{created_at: -1}]. + * @param {MemberFilters} filterConditions object MongoDB style filters + * @param {MemberSort} [sort] Sort options, for instance [{created_at: -1}]. * When using multiple fields, make sure you use array of objects to guarantee field order, for instance [{name: -1}, {created_at: 1}] * @param {{ limit?: number; offset?: number }} [options] Option object, {limit: 10, offset:10} * - * @return {Promise>} Query Members response + * @return {Promise} Query Members response */ - async queryMembers( - filterConditions: MemberFilters, - sort: MemberSort = [], - options: QueryMembersOptions = {}, - ) { + async queryMembers(filterConditions: MemberFilters, sort: MemberSort = [], options: QueryMembersOptions = {}) { let id: string | undefined; const type = this.type; - let members: string[] | ChannelMemberResponse[] | undefined; + let members: string[] | ChannelMemberResponse[] | undefined; if (this.id) { id = this.id; } else if (this.data?.members && Array.isArray(this.data.members)) { members = this.data.members; } // Return a list of members - return await this.getClient().get>( - this.getClient().baseURL + '/members', - { - payload: { - type, - id, - members, - sort: normalizeQuerySort(sort), - filter_conditions: filterConditions, - ...options, - }, + return await this.getClient().get(this.getClient().baseURL + '/members', { + payload: { + type, + id, + members, + sort: normalizeQuerySort(sort), + filter_conditions: filterConditions, + ...options, }, - ); + }); } /** * partialUpdateMember - Partial update a member * * @param {string} user_id member user id - * @param {PartialUpdateMember} updates + * @param {PartialUpdateMember} updates * - * @return {Promise>} Updated member + * @return {Promise} Updated member */ - async partialUpdateMember(user_id: string, updates: PartialUpdateMember) { + async partialUpdateMember(user_id: string, updates: PartialUpdateMember) { if (!user_id) { throw Error('Please specify the user id'); } - return await this.getClient().patch>( + return await this.getClient().patch( this._channelURL() + `/member/${encodeURIComponent(user_id)}`, updates, ); @@ -330,14 +312,14 @@ export class Channel} reaction the reaction object for instance {type: 'love'} + * @param {Reaction} reaction the reaction object for instance {type: 'love'} * @param {{ enforce_unique?: boolean, skip_push?: boolean }} [options] Option object, {enforce_unique: true, skip_push: true} to override any existing reaction or skip sending push notifications * - * @return {Promise>} The Server Response + * @return {Promise} The Server Response */ async sendReaction( messageID: string, - reaction: Reaction, + reaction: Reaction, options?: { enforce_unique?: boolean; skip_push?: boolean }, ) { if (!messageID) { @@ -346,7 +328,7 @@ export class Channel>( + return await this.getClient().post( this.getClient().baseURL + `/messages/${encodeURIComponent(messageID)}/reaction`, { reaction, @@ -362,7 +344,7 @@ export class Channel>} The Server Response + * @return {Promise} The Server Response */ deleteReaction(messageID: string, reactionType: string, user_id?: string) { this._checkInitialized(); @@ -375,27 +357,28 @@ export class Channel>(url, { user_id }); + return this.getClient().delete(url, { user_id }); } - return this.getClient().delete>(url, {}); + return this.getClient().delete(url, {}); } /** * update - Edit the channel's custom properties * - * @param {ChannelData} channelData The object to update the custom properties of this channel with - * @param {Message} [updateMessage] Optional message object for channel members notification + * @param {ChannelData} channelData The object to update the custom properties of this channel with + * @param {Message} [updateMessage] Optional message object for channel members notification * @param {ChannelUpdateOptions} [options] Option object, configuration to control the behavior while updating - * @return {Promise>} The server response + * @return {Promise} The server response */ async update( - channelData: Partial> | Partial> = {}, - updateMessage?: Message, + channelData: Partial = {}, + updateMessage?: Message, options?: ChannelUpdateOptions, ) { // Strip out reserved names that will result in API errors. - const reserved = [ + // TODO: this needs to be typed better + const reserved: Exclude[] = [ 'config', 'cid', 'created_by', @@ -407,6 +390,7 @@ export class Channel { delete channelData[key]; }); @@ -421,15 +405,12 @@ export class Channel} partial update request + * @param {PartialUpdateChannel} partial update request * - * @return {Promise>} + * @return {Promise} */ - async updatePartial(update: PartialUpdateChannel) { - const data = await this.getClient().patch>( - this._channelURL(), - update, - ); + async updatePartial(update: PartialUpdateChannel) { + const data = await this.getClient().patch(this._channelURL(), update); const areCapabilitiesChanged = [...(data.channel.own_capabilities || [])].sort().join() !== @@ -450,10 +431,10 @@ export class Channel>} The server response + * @return {Promise} The server response */ async enableSlowMode(coolDownInterval: number) { - const data = await this.getClient().post>(this._channelURL(), { + const data = await this.getClient().post(this._channelURL(), { cooldown: coolDownInterval, }); this.data = data.channel; @@ -463,10 +444,10 @@ export class Channel>} The server response + * @return {Promise} The server response */ async disableSlowMode() { - const data = await this.getClient().post>(this._channelURL(), { + const data = await this.getClient().post(this._channelURL(), { cooldown: 0, }); this.data = data.channel; @@ -478,61 +459,54 @@ export class Channel>} The server response + * @return {Promise} The server response */ async delete(options: { hard_delete?: boolean } = {}) { - return await this.getClient().delete>(this._channelURL(), { + return await this.getClient().delete(this._channelURL(), { ...options, }); } /** * truncate - Removes all messages from the channel - * @param {TruncateOptions} [options] Defines truncation options - * @return {Promise>} The server response + * @param {TruncateOptions} [options] Defines truncation options + * @return {Promise} The server response */ - async truncate(options: TruncateOptions = {}) { - return await this.getClient().post>( - this._channelURL() + '/truncate', - options, - ); + async truncate(options: TruncateOptions = {}) { + return await this.getClient().post(this._channelURL() + '/truncate', options); } /** * acceptInvite - accept invitation to the channel * - * @param {InviteOptions} [options] The object to update the custom properties of this channel with + * @param {UpdateChannelOptions} [options] The object to update the custom properties of this channel with * - * @return {Promise>} The server response + * @return {Promise} The server response */ - async acceptInvite(options: InviteOptions = {}) { + async acceptInvite(options: UpdateChannelOptions = {}) { return await this._update({ accept_invite: true, ...options }); } /** * rejectInvite - reject invitation to the channel * - * @param {InviteOptions} [options] The object to update the custom properties of this channel with + * @param {UpdateChannelOptions} [options] The object to update the custom properties of this channel with * - * @return {Promise>} The server response + * @return {Promise} The server response */ - async rejectInvite(options: InviteOptions = {}) { + async rejectInvite(options: UpdateChannelOptions = {}) { return await this._update({ reject_invite: true, ...options }); } /** * addMembers - add members to the channel * - * @param {string[] | Array>} members An array of members to add to the channel - * @param {Message} [message] Optional message object for channel members notification + * @param {string[] | Array} members An array of members to add to the channel + * @param {Message} [message] Optional message object for channel members notification * @param {ChannelUpdateOptions} [options] Option object, configuration to control the behavior while updating - * @return {Promise>} The server response + * @return {Promise} The server response */ - async addMembers( - members: string[] | Array>, - message?: Message, - options: ChannelUpdateOptions = {}, - ) { + async addMembers(members: string[] | Array, message?: Message, options: ChannelUpdateOptions = {}) { return await this._update({ add_members: members, message, ...options }); } @@ -540,11 +514,11 @@ export class Channel} [message] Optional message object for channel members notification + * @param {Message} [message] Optional message object for channel members notification * @param {ChannelUpdateOptions} [options] Option object, configuration to control the behavior while updating - * @return {Promise>} The server response + * @return {Promise} The server response */ - async addModerators(members: string[], message?: Message, options: ChannelUpdateOptions = {}) { + async addModerators(members: string[], message?: Message, options: ChannelUpdateOptions = {}) { return await this._update({ add_moderators: members, message, ...options }); } @@ -552,13 +526,13 @@ export class Channel} [message] Optional message object for channel members notification + * @param {Message} [message] Optional message object for channel members notification * @param {ChannelUpdateOptions} [options] Option object, configuration to control the behavior while updating - * @return {Promise>} The server response + * @return {Promise} The server response */ async assignRoles( roles: { channel_role: Role; user_id: string }[], - message?: Message, + message?: Message, options: ChannelUpdateOptions = {}, ) { return await this._update({ assign_roles: roles, message, ...options }); @@ -567,14 +541,14 @@ export class Channel>} members An array of members to invite to the channel - * @param {Message} [message] Optional message object for channel members notification + * @param {string[] | Array} members An array of members to invite to the channel + * @param {Message} [message] Optional message object for channel members notification * @param {ChannelUpdateOptions} [options] Option object, configuration to control the behavior while updating - * @return {Promise>} The server response + * @return {Promise} The server response */ async inviteMembers( - members: string[] | Array>, - message?: Message, + members: string[] | Required>[], + message?: Message, options: ChannelUpdateOptions = {}, ) { return await this._update({ invites: members, message, ...options }); @@ -584,11 +558,11 @@ export class Channel} [message] Optional message object for channel members notification + * @param {Message} [message] Optional message object for channel members notification * @param {ChannelUpdateOptions} [options] Option object, configuration to control the behavior while updating - * @return {Promise>} The server response + * @return {Promise} The server response */ - async removeMembers(members: string[], message?: Message, options: ChannelUpdateOptions = {}) { + async removeMembers(members: string[], message?: Message, options: ChannelUpdateOptions = {}) { return await this._update({ remove_members: members, message, ...options }); } @@ -596,22 +570,22 @@ export class Channel} [message] Optional message object for channel members notification + * @param {Message} [message] Optional message object for channel members notification * @param {ChannelUpdateOptions} [options] Option object, configuration to control the behavior while updating - * @return {Promise>} The server response + * @return {Promise} The server response */ - async demoteModerators(members: string[], message?: Message, options: ChannelUpdateOptions = {}) { + async demoteModerators(members: string[], message?: Message, options: ChannelUpdateOptions = {}) { return await this._update({ demote_moderators: members, message, ...options }); } /** * _update - executes channel update request * @param payload Object Update Channel payload - * @return {Promise>} The server response + * @return {Promise} The server response * TODO: introduce new type instead of Object in the next major update */ async _update(payload: Object) { - const data = await this.getClient().post>(this._channelURL(), payload); + const data = await this.getClient().post(this._channelURL(), payload); this.data = data.channel; return data; } @@ -619,7 +593,7 @@ export class Channel>} The server response + * @return {Promise} The server response * * example with expiration: * await channel.mute({expiration: moment.duration(2, 'weeks')}); @@ -629,10 +603,10 @@ export class Channel>( - this.getClient().baseURL + '/moderation/mute/channel', - { channel_cid: this.cid, ...opts }, - ); + return await this.getClient().post(this.getClient().baseURL + '/moderation/mute/channel', { + channel_cid: this.cid, + ...opts, + }); } /** @@ -653,7 +627,7 @@ export class Channel>} The server response + * @return {Promise} The server response * * example: * await channel.archives(); @@ -675,7 +649,7 @@ export class Channel>} The server response + * @return {Promise} The server response * * example: * await channel.unarchive(); @@ -697,7 +671,7 @@ export class Channel>} The server response + * @return {Promise} The server response * * example: * await channel.pin(); @@ -719,7 +693,7 @@ export class Channel>} The server response + * @return {Promise} The server response * * example: * await channel.unpin(); @@ -756,7 +730,7 @@ export class Channel>( + return this.getClient().post( this.getClient().baseURL + `/messages/${encodeURIComponent(messageID)}/action`, { message_id: messageID, @@ -788,7 +762,7 @@ export class Channel); + } as Event); } } @@ -806,7 +780,7 @@ export class Channel); + } as Event); } /** @@ -816,7 +790,7 @@ export class Channel); + } as Event); } /** @@ -826,7 +800,7 @@ export class Channel); + } as Event); } /** @@ -844,7 +818,7 @@ export class Channel); + } as Event); } _isTypingIndicatorsEnabled(): boolean { @@ -857,9 +831,9 @@ export class Channel['formatMessage']> | undefined} Description + * @return {ReturnType | undefined} Description */ - lastMessage(): FormatMessageResponse | undefined { + lastMessage(): FormatMessageResponse | undefined { // get last 5 messages, sort, return the latest // get a slice of the last 5 let min = this.state.latestMessages.length - 5; @@ -878,17 +852,17 @@ export class Channel} data - * @return {Promise | null>} Description + * @param {MarkReadOptions} data + * @return {Promise} Description */ - async markRead(data: MarkReadOptions = {}) { + async markRead(data: MarkReadOptions = {}) { this._checkInitialized(); if (!this.getConfig()?.read_events && !this.getClient()._isUsingServerAuth()) { return Promise.resolve(null); } - return await this.getClient().post>(this._channelURL() + '/read', { + return await this.getClient().post(this._channelURL() + '/read', { ...data, }); } @@ -896,10 +870,10 @@ export class Channel} data + * @param {MarkUnreadOptions} data * @return {APIResponse} An API response */ - async markUnread(data: MarkUnreadOptions) { + async markUnread(data: MarkUnreadOptions) { this._checkInitialized(); if (!this.getConfig()?.read_events && !this.getClient()._isUsingServerAuth()) { @@ -929,11 +903,11 @@ export class Channel} options additional options for the query endpoint + * @param {ChannelQueryOptions} options additional options for the query endpoint * - * @return {Promise>} The server response + * @return {Promise} The server response */ - async watch(options?: ChannelQueryOptions) { + async watch(options?: ChannelQueryOptions) { const defaultOptions = { state: true, watch: true, @@ -981,17 +955,17 @@ export class Channel; user_id?: string }} options Pagination params, ie {limit:10, id_lte: 10} + * @param {MessagePaginationOptions & { user?: UserResponse; user_id?: string }} options Pagination params, ie {limit:10, id_lte: 10} * - * @return {Promise>} A response with a list of messages + * @return {Promise} A response with a list of messages */ async getReplies( parent_id: string, - options: MessagePaginationOptions & { user?: UserResponse; user_id?: string }, + options: MessagePaginationOptions & { user?: UserResponse; user_id?: string }, sort?: { created_at: AscDesc }[], ) { const normalizedSort = sort ? normalizeQuerySort(sort) : undefined; - const data = await this.getClient().get>( + const data = await this.getClient().get( this.getClient().baseURL + `/messages/${encodeURIComponent(parent_id)}/replies`, { sort: normalizedSort, @@ -1010,24 +984,21 @@ export class Channel; user_id?: string }} options Pagination params, ie {limit:10, id_lte: 10} + * @param {PinnedMessagePaginationOptions & { user?: UserResponse; user_id?: string }} options Pagination params, ie {limit:10, id_lte: 10} * @param {PinnedMessagesSort} sort defines sorting direction of pinned messages * - * @return {Promise>} A response with a list of messages + * @return {Promise} A response with a list of messages */ async getPinnedMessages( - options: PinnedMessagePaginationOptions & { user?: UserResponse; user_id?: string }, + options: PinnedMessagePaginationOptions & { user?: UserResponse; user_id?: string }, sort: PinnedMessagesSort = [], ) { - return await this.getClient().get>( - this._channelURL() + '/pinned_messages', - { - payload: { - ...options, - sort: normalizeQuerySort(sort), - }, + return await this.getClient().get(this._channelURL() + '/pinned_messages', { + payload: { + ...options, + sort: normalizeQuerySort(sort), }, - ); + }); } /** @@ -1036,10 +1007,10 @@ export class Channel>} Server response + * @return {Promise} Server response */ getReactions(message_id: string, options: { limit?: number; offset?: number }) { - return this.getClient().get>( + return this.getClient().get( this.getClient().baseURL + `/messages/${encodeURIComponent(message_id)}/reactions`, { ...options, @@ -1052,10 +1023,10 @@ export class Channel>} Server response + * @return {Promise} Server response */ getMessagesById(messageIds: string[]) { - return this.getClient().get>(this._channelURL() + '/messages', { + return this.getClient().get(this._channelURL() + '/messages', { ids: messageIds.join(','), }); } @@ -1071,7 +1042,7 @@ export class Channel | MessageResponse) { + _countMessageAsUnread(message: FormatMessageResponse | MessageResponse) { if (message.shadowed) return false; if (message.silent) return false; if (message.parent_id && !message.show_in_channel) return false; @@ -1079,8 +1050,9 @@ export class Channel>} The Server Response + * @return {Promise} The Server Response * */ - create = async (options?: ChannelQueryOptions) => { + create = async (options?: ChannelQueryOptions) => { const defaultOptions = { ...options, watch: false, @@ -1150,24 +1122,31 @@ export class Channel} options The query options + * @param {ChannelQueryOptions} options The query options * @param {MessageSetType} messageSetToAddToIfDoesNotExist It's possible to load disjunct sets of a channel's messages into state, use `current` to load the initial channel state or if you want to extend the currently displayed messages, use `latest` if you want to load/extend the latest messages, `new` is used for loading a specific message and it's surroundings * - * @return {Promise>} Returns a query response + * @return {Promise} Returns a query response */ - async query( - options?: ChannelQueryOptions, - messageSetToAddToIfDoesNotExist: MessageSetType = 'current', - ) { + async query(options: ChannelQueryOptions = {}, messageSetToAddToIfDoesNotExist: MessageSetType = 'current') { // Make sure we wait for the connect promise if there is a pending one await this.getClient().wsPromise; + const createdById = + options.created_by?.id ?? options.created_by_id ?? this._data?.created_by?.id ?? this._data?.created_by_id; + + if (this.getClient()._isUsingServerAuth() && typeof createdById !== 'string') { + this.getClient().logger( + 'warn', + 'Either `created_by` (with `id` property) or `created_by_id` are missing from both `Channel._data` and `options` parameter', + ); + } + let queryURL = `${this.getClient().baseURL}/channels/${encodeURIComponent(this.type)}`; if (this.id) { queryURL += `/${encodeURIComponent(this.id)}`; } - const state = await this.getClient().post>(queryURL + '/query', { + const state = await this.getClient().post(queryURL + '/query', { data: this._data, state: true, ...options, @@ -1214,7 +1193,7 @@ export class Channel} options + * @param {BanUserOptions} options * @returns {Promise} */ - async banUser(targetUserID: string, options: BanUserOptions) { + async banUser(targetUserID: string, options: BanUserOptions) { this._checkInitialized(); return await this.getClient().banUser(targetUserID, { ...options, @@ -1301,10 +1280,10 @@ export class Channel} options + * @param {BanUserOptions} options * @returns {Promise} */ - async shadowBan(targetUserID: string, options: BanUserOptions) { + async shadowBan(targetUserID: string, options: BanUserOptions) { this._checkInitialized(); return await this.getClient().shadowBan(targetUserID, { ...options, @@ -1358,15 +1337,12 @@ export class Channel {console.log(event.type)}) * - * @param {EventHandler | EventTypes} callbackOrString The event type to listen for (optional) - * @param {EventHandler} [callbackOrNothing] The callback to call + * @param {EventHandler | EventTypes} callbackOrString The event type to listen for (optional) + * @param {EventHandler} [callbackOrNothing] The callback to call */ - on(eventType: EventTypes, callback: EventHandler): { unsubscribe: () => void }; - on(callback: EventHandler): { unsubscribe: () => void }; - on( - callbackOrString: EventHandler | EventTypes, - callbackOrNothing?: EventHandler, - ): { unsubscribe: () => void } { + on(eventType: EventTypes, callback: EventHandler): { unsubscribe: () => void }; + on(callback: EventHandler): { unsubscribe: () => void }; + on(callbackOrString: EventHandler | EventTypes, callbackOrNothing?: EventHandler): { unsubscribe: () => void } { const key = callbackOrNothing ? (callbackOrString as string) : 'all'; const callback = callbackOrNothing ? callbackOrNothing : callbackOrString; if (!(key in this.listeners)) { @@ -1395,12 +1371,9 @@ export class Channel): void; - off(callback: EventHandler): void; - off( - callbackOrString: EventHandler | EventTypes, - callbackOrNothing?: EventHandler, - ): void { + off(eventType: EventTypes, callback: EventHandler): void; + off(callback: EventHandler): void; + off(callbackOrString: EventHandler | EventTypes, callbackOrNothing?: EventHandler): void { const key = callbackOrNothing ? (callbackOrString as string) : 'all'; const callback = callbackOrNothing ? callbackOrNothing : callbackOrString; if (!(key in this.listeners)) { @@ -1415,7 +1388,7 @@ export class Channel) { + _handleChannelEvent(event: Event) { const channel = this; this._client.logger( 'info', @@ -1538,8 +1511,7 @@ export class Channel { - if (truncatedAt > +createdAt) - channelState.removePinnedMessage({ id } as MessageResponse); + if (truncatedAt > +createdAt) channelState.removePinnedMessage({ id } as MessageResponse); }); } else { channelState.clearMessages(); @@ -1666,7 +1638,7 @@ export class Channel) => { + _callChannelListeners = (event: Event) => { const channel = this; // gather and call the listeners const listeners = []; @@ -1706,10 +1678,7 @@ export class Channel, - messageSetToAddToIfDoesNotExist: MessageSetType = 'latest', - ) { + _initializeState(state: ChannelAPIResponse, messageSetToAddToIfDoesNotExist: MessageSetType = 'latest') { const { state: clientState, user, userID } = this.getClient(); // add the members and users @@ -1786,7 +1755,7 @@ export class Channel) { + _extendEventWithOwnReactions(event: Event) { if (!event.message) { return; } @@ -1800,7 +1769,7 @@ export class Channel[]; + members: ChannelMemberResponse[]; /** * If set to `true` then `ChannelState.members` will be overriden with the newly * provided `members`, setting this property to `false` will merge current `ChannelState.members` @@ -1809,7 +1778,7 @@ export class Channel['members']>((membersById, member) => { + const newMembersById = members.reduce((membersById, member) => { if (member.user) { membersById[member.user.id] = member; } diff --git a/src/channel_manager.ts b/src/channel_manager.ts index 6298712a92..b02c801e94 100644 --- a/src/channel_manager.ts +++ b/src/channel_manager.ts @@ -1,13 +1,5 @@ import type { StreamChat } from './client'; -import type { - DefaultGenerics, - ExtendableGenerics, - Event, - ChannelOptions, - ChannelStateOptions, - ChannelFilters, - ChannelSort, -} from './types'; +import type { Event, ChannelOptions, ChannelStateOptions, ChannelFilters, ChannelSort } from './types'; import { StateStore, ValueOrPatch, isPatch } from './store'; import { Channel } from './channel'; import { @@ -22,17 +14,17 @@ import { uniqBy, } from './utils'; -export type ChannelManagerPagination = { - filters: ChannelFilters; +export type ChannelManagerPagination = { + filters: ChannelFilters; hasNext: boolean; isLoading: boolean; isLoadingNext: boolean; options: ChannelOptions; - sort: ChannelSort; + sort: ChannelSort; }; -export type ChannelManagerState = { - channels: Channel[]; +export type ChannelManagerState = { + channels: Channel[]; /** * This value will become true the first time queryChannels is successfully executed and * will remain false otherwise. It's used as a control property regarding whether the list @@ -40,23 +32,17 @@ export type ChannelManagerState; + pagination: ChannelManagerPagination; }; -export type ChannelSetterParameterType = ValueOrPatch< - ChannelManagerState['channels'] ->; -export type ChannelSetterType = ( - arg: ChannelSetterParameterType, -) => void; +export type ChannelSetterParameterType = ValueOrPatch; +export type ChannelSetterType = (arg: ChannelSetterParameterType) => void; export type GenericEventHandlerType = ( ...args: T ) => void | (() => void) | ((...args: T) => Promise) | Promise; -export type EventHandlerType = GenericEventHandlerType<[Event]>; -export type EventHandlerOverrideType = GenericEventHandlerType< - [ChannelSetterType, Event] ->; +export type EventHandlerType = GenericEventHandlerType<[Event]>; +export type EventHandlerOverrideType = GenericEventHandlerType<[ChannelSetterType, Event]>; export type ChannelManagerEventTypes = | 'notification.added_to_channel' @@ -82,8 +68,8 @@ export type ChannelManagerEventHandlerNames = | 'notificationNewMessageHandler' | 'notificationRemovedFromChannelHandler'; -export type ChannelManagerEventHandlerOverrides = Partial< - Record> +export type ChannelManagerEventHandlerOverrides = Partial< + Record >; export const channelManagerEventToHandlerMapping: { @@ -147,12 +133,12 @@ export const DEFAULT_CHANNEL_MANAGER_PAGINATION_OPTIONS = { * * @internal */ -export class ChannelManager { - public readonly state: StateStore>; - private client: StreamChat; +export class ChannelManager { + public readonly state: StateStore; + private client: StreamChat; private unsubscribeFunctions: Set<() => void> = new Set(); - private eventHandlers: Map> = new Map(); - private eventHandlerOverrides: Map> = new Map(); + private eventHandlers: Map = new Map(); + private eventHandlerOverrides: Map = new Map(); private options: ChannelManagerOptions = {}; private stateOptions: ChannelStateOptions = {}; @@ -161,12 +147,12 @@ export class ChannelManager { eventHandlerOverrides = {}, options = {}, }: { - client: StreamChat; - eventHandlerOverrides?: ChannelManagerEventHandlerOverrides; + client: StreamChat; + eventHandlerOverrides?: ChannelManagerEventHandlerOverrides; options?: ChannelManagerOptions; }) { this.client = client; - this.state = new StateStore>({ + this.state = new StateStore({ channels: [], pagination: { isLoading: false, @@ -181,7 +167,7 @@ export class ChannelManager { this.setEventHandlerOverrides(eventHandlerOverrides); this.setOptions(options); this.eventHandlers = new Map( - Object.entries>({ + Object.entries({ channelDeletedHandler: this.channelDeletedHandler, channelHiddenHandler: this.channelHiddenHandler, channelVisibleHandler: this.channelVisibleHandler, @@ -194,7 +180,7 @@ export class ChannelManager { ); } - public setChannels = (valueOrFactory: ChannelSetterParameterType) => { + public setChannels = (valueOrFactory: ChannelSetterParameterType) => { this.state.next((current) => { const { channels: currentChannels } = current; const newChannels = isPatch(valueOrFactory) ? valueOrFactory(currentChannels) : valueOrFactory; @@ -208,16 +194,16 @@ export class ChannelManager { }); }; - public setEventHandlerOverrides = (eventHandlerOverrides: ChannelManagerEventHandlerOverrides = {}) => { + public setEventHandlerOverrides = (eventHandlerOverrides: ChannelManagerEventHandlerOverrides = {}) => { const truthyEventHandlerOverrides = Object.entries(eventHandlerOverrides).reduce< - Partial> + Partial >((acc, [key, value]) => { if (value) { - acc[key as keyof ChannelManagerEventHandlerOverrides] = value; + acc[key as keyof ChannelManagerEventHandlerOverrides] = value; } return acc; }, {}); - this.eventHandlerOverrides = new Map(Object.entries>(truthyEventHandlerOverrides)); + this.eventHandlerOverrides = new Map(Object.entries(truthyEventHandlerOverrides)); }; public setOptions = (options: ChannelManagerOptions = {}) => { @@ -225,8 +211,8 @@ export class ChannelManager { }; public queryChannels = async ( - filters: ChannelFilters, - sort: ChannelSort = [], + filters: ChannelFilters, + sort: ChannelSort = [], options: ChannelOptions = {}, stateOptions: ChannelStateOptions = {}, ) => { @@ -297,7 +283,7 @@ export class ChannelManager { const newOptions = { ...options, offset: newOffset }; this.state.partialNext({ - channels: uniqBy>([...(channels || []), ...nextChannels], 'cid'), + channels: uniqBy([...(channels || []), ...nextChannels], 'cid'), pagination: { ...pagination, hasNext: (nextChannels?.length ?? 0) >= limit, @@ -316,7 +302,7 @@ export class ChannelManager { } }; - private notificationAddedToChannelHandler = async (event: Event) => { + private notificationAddedToChannelHandler = async (event: Event) => { const { id, type, members } = event?.channel ?? {}; if (!type || !this.options.allowNotLoadedChannelPromotionForEvent?.['notification.added_to_channel']) { @@ -352,7 +338,7 @@ export class ChannelManager { ); }; - private channelDeletedHandler = (event: Event) => { + private channelDeletedHandler = (event: Event) => { const { channels } = this.state.getLatestValue(); if (!channels) { return; @@ -371,7 +357,7 @@ export class ChannelManager { private channelHiddenHandler = this.channelDeletedHandler; - private newMessageHandler = (event: Event) => { + private newMessageHandler = (event: Event) => { const { pagination, channels } = this.state.getLatestValue(); if (!channels) { return; @@ -420,7 +406,7 @@ export class ChannelManager { ); }; - private notificationNewMessageHandler = async (event: Event) => { + private notificationNewMessageHandler = async (event: Event) => { const { id, type } = event?.channel ?? {}; if (!id || !type) { @@ -457,7 +443,7 @@ export class ChannelManager { ); }; - private channelVisibleHandler = async (event: Event) => { + private channelVisibleHandler = async (event: Event) => { const { channel_type: channelType, channel_id: channelId } = event; if (!channelType || !channelId) { @@ -496,7 +482,7 @@ export class ChannelManager { private notificationRemovedFromChannelHandler = this.channelDeletedHandler; - private memberUpdatedHandler = (event: Event) => { + private memberUpdatedHandler = (event: Event) => { const { pagination, channels } = this.state.getLatestValue(); const { filters, sort } = pagination; if ( @@ -560,7 +546,7 @@ export class ChannelManager { this.setChannels(newChannels); }; - private subscriptionOrOverride = (event: Event) => { + private subscriptionOrOverride = (event: Event) => { const handlerName = channelManagerEventToHandlerMapping[event.type as ChannelManagerEventTypes]; const defaultEventHandler = this.eventHandlers.get(handlerName); const eventHandlerOverride = this.eventHandlerOverrides.get(handlerName); diff --git a/src/channel_state.ts b/src/channel_state.ts index c29fcd71b0..938254a5bf 100644 --- a/src/channel_state.ts +++ b/src/channel_state.ts @@ -1,9 +1,7 @@ import { Channel } from './channel'; import { ChannelMemberResponse, - DefaultGenerics, Event, - ExtendableGenerics, FormatMessageResponse, MessageResponse, MessageSet, @@ -15,12 +13,12 @@ import { import { addToMessageList, formatMessage } from './utils'; import { DEFAULT_MESSAGE_SET_PAGINATION } from './constants'; -type ChannelReadStatus = Record< +type ChannelReadStatus = Record< string, { last_read: Date; unread_messages: number; - user: UserResponse; + user: UserResponse; first_unread_message_id?: string; last_read_message_id?: string; } @@ -29,19 +27,19 @@ type ChannelReadStatus { - _channel: Channel; +export class ChannelState { + _channel: Channel; watcher_count: number; - typing: Record>; - read: ChannelReadStatus; - pinnedMessages: Array['formatMessage']>>; - pending_messages: Array>; - threads: Record['formatMessage']>>>; - mutedUsers: Array>; - watchers: Record>; - members: Record>; + typing: Record; + read: ChannelReadStatus; + pinnedMessages: Array>; + pending_messages: Array; + threads: Record>>; + mutedUsers: Array; + watchers: Record; + members: Record; unreadCount: number; - membership: ChannelMemberResponse; + membership: ChannelMemberResponse; last_message_at: Date | null; /** * Flag which indicates if channel state contain latest/recent messages or no. @@ -58,7 +56,7 @@ export class ChannelState) { + constructor(channel: Channel) { this._channel = channel; this.watcher_count = 0; this.typing = {}; @@ -87,7 +85,7 @@ export class ChannelState s.isCurrent)?.messages || []; } - set messages(messages: Array['formatMessage']>>) { + set messages(messages: Array>) { const index = this.messageSets.findIndex((s) => s.isCurrent); this.messageSets[index].messages = messages; } @@ -100,7 +98,7 @@ export class ChannelState s.isLatest)?.messages || []; } - set latestMessages(messages: Array['formatMessage']>>) { + set latestMessages(messages: Array>) { const index = this.messageSets.findIndex((s) => s.isLatest); this.messageSets[index].messages = messages; } @@ -112,13 +110,13 @@ export class ChannelState} newMessage A new message + * @param {MessageResponse} newMessage A new message * @param {boolean} timestampChanged Whether updating a message with changed created_at value. * @param {boolean} addIfDoesNotExist Add message if it is not in the list, used to prevent out of order updated messages from being added. * @param {MessageSetType} messageSetToAddToIfDoesNotExist Which message set to add to if message is not in the list (only used if addIfDoesNotExist is true) */ addMessageSorted( - newMessage: MessageResponse, + newMessage: MessageResponse, timestampChanged = false, addIfDoesNotExist = true, messageSetToAddToIfDoesNotExist: MessageSetType = 'latest', @@ -136,14 +134,14 @@ export class ChannelState} message `MessageResponse` object + * @param {MessageResponse} message `MessageResponse` object */ - formatMessage = (message: MessageResponse) => formatMessage(message); + formatMessage = (message: MessageResponse) => formatMessage(message); /** * addMessagesSorted - Add the list of messages to state and resorts the messages * - * @param {Array>} newMessages A list of messages + * @param {Array} newMessages A list of messages * @param {boolean} timestampChanged Whether updating messages with changed created_at value. * @param {boolean} initializing Whether channel is being initialized. * @param {boolean} addIfDoesNotExist Add message if it is not in the list, used to prevent out of order updated messages from being added. @@ -151,7 +149,7 @@ export class ChannelState[], + newMessages: MessageResponse[], timestampChanged = false, initializing = false, addIfDoesNotExist = true, @@ -172,11 +170,11 @@ export class ChannelState this happens when we perform merging of message sets // This will be also true for message previews used by some SDKs const isMessageFormatted = messagesToAdd[i].created_at instanceof Date; - let message: ReturnType['formatMessage']>; + let message: ReturnType; if (isMessageFormatted) { - message = messagesToAdd[i] as ReturnType['formatMessage']>; + message = messagesToAdd[i] as ReturnType; } else { - message = this.formatMessage(messagesToAdd[i] as MessageResponse); + message = this.formatMessage(messagesToAdd[i] as MessageResponse); if (message.user && this._channel?.cid) { /** @@ -247,10 +245,10 @@ export class ChannelState>} pinnedMessages A list of pinned messages + * @param {Array} pinnedMessages A list of pinned messages * */ - addPinnedMessages(pinnedMessages: MessageResponse[]) { + addPinnedMessages(pinnedMessages: MessageResponse[]) { for (let i = 0; i < pinnedMessages.length; i += 1) { this.addPinnedMessage(pinnedMessages[i]); } @@ -259,10 +257,10 @@ export class ChannelState} pinnedMessage message to update + * @param {MessageResponse} pinnedMessage message to update * */ - addPinnedMessage(pinnedMessage: MessageResponse) { + addPinnedMessage(pinnedMessage: MessageResponse) { this.pinnedMessages = this._addToMessageList( this.pinnedMessages, this.formatMessage(pinnedMessage), @@ -274,19 +272,15 @@ export class ChannelState} message message to remove + * @param {MessageResponse} message message to remove * */ - removePinnedMessage(message: MessageResponse) { + removePinnedMessage(message: MessageResponse) { const { result } = this.removeMessageFromArray(this.pinnedMessages, message); this.pinnedMessages = result; } - addReaction( - reaction: ReactionResponse, - message?: MessageResponse, - enforce_unique?: boolean, - ) { + addReaction(reaction: ReactionResponse, message?: MessageResponse, enforce_unique?: boolean) { if (!message) return; const messageWithReaction = message; this._updateMessage(message, (msg) => { @@ -297,8 +291,8 @@ export class ChannelState[] | null | undefined, - reaction: ReactionResponse, + ownReactions: ReactionResponse[] | null | undefined, + reaction: ReactionResponse, enforce_unique?: boolean, ) { if (enforce_unique) { @@ -315,17 +309,14 @@ export class ChannelState[] | null | undefined, - reaction: ReactionResponse, - ) { + _removeOwnReactionFromMessage(ownReactions: ReactionResponse[] | null | undefined, reaction: ReactionResponse) { if (ownReactions) { return ownReactions.filter((item) => item.user_id !== reaction.user_id || item.type !== reaction.type); } return ownReactions; } - removeReaction(reaction: ReactionResponse, message?: MessageResponse) { + removeReaction(reaction: ReactionResponse, message?: MessageResponse) { if (!message) return; const messageWithReaction = message; this._updateMessage(message, (msg) => { @@ -335,23 +326,17 @@ export class ChannelState; - remove?: boolean; - }) { - const parseMessage = (m: ReturnType['formatMessage']>) => + _updateQuotedMessageReferences({ message, remove }: { message: MessageResponse; remove?: boolean }) { + const parseMessage = (m: ReturnType) => (({ ...m, created_at: m.created_at.toISOString(), pinned_at: m.pinned_at?.toISOString(), updated_at: m.updated_at?.toISOString(), - } as unknown) as MessageResponse); + } as unknown) as MessageResponse); - const update = (messages: FormatMessageResponse[]) => { - const updatedMessages = messages.reduce[]>((acc, msg) => { + const update = (messages: FormatMessageResponse[]) => { + const updatedMessages = messages.reduce((acc, msg) => { if (msg.quoted_message_id === message.id) { acc.push({ ...parseMessage(msg), quoted_message: remove ? { ...message, attachments: [] } : message }); } @@ -368,7 +353,7 @@ export class ChannelState) { + removeQuotedMessageReferences(message: MessageResponse) { this._updateQuotedMessageReferences({ message, remove: true }); } @@ -384,9 +369,7 @@ export class ChannelState['formatMessage']>, - ) => ReturnType['formatMessage']>, + updateFunc: (msg: ReturnType) => ReturnType, ) { const { parent_id, show_in_channel, pinned } = message; @@ -434,15 +417,15 @@ export class ChannelState['formatMessage']>>} messages A list of messages + * @param {Array>} messages A list of messages * @param message * @param {boolean} timestampChanged Whether updating a message with changed created_at value. * @param {string} sortBy field name to use to sort the messages by * @param {boolean} addIfDoesNotExist Add message if it is not in the list, used to prevent out of order updated messages from being added. */ _addToMessageList( - messages: Array['formatMessage']>>, - message: ReturnType['formatMessage']>, + messages: Array>, + message: ReturnType, timestampChanged = false, sortBy: 'pinned_at' | 'created_at' = 'created_at', addIfDoesNotExist = true, @@ -483,7 +466,7 @@ export class ChannelState['formatMessage']>>, + msgArray: Array>, msg: { id: string; parent_id?: string }, ) => { const result = msgArray.filter((message) => !(!!message.id && !!msg.id && message.id === msg.id)); @@ -494,13 +477,10 @@ export class ChannelState} user + * @param {UserResponse} user */ - updateUserMessages = (user: UserResponse) => { - const _updateUserMessages = ( - messages: Array['formatMessage']>>, - user: UserResponse, - ) => { + updateUserMessages = (user: UserResponse) => { + const _updateUserMessages = (messages: Array>, user: UserResponse) => { for (let i = 0; i < messages.length; i++) { const m = messages[i]; if (m.user?.id === user.id) { @@ -521,13 +501,13 @@ export class ChannelState} user + * @param {UserResponse} user * @param {boolean} hardDelete */ - deleteUserMessages = (user: UserResponse, hardDelete = false) => { + deleteUserMessages = (user: UserResponse, hardDelete = false) => { const _deleteUserMessages = ( - messages: Array['formatMessage']>>, - user: UserResponse, + messages: Array>, + user: UserResponse, hardDelete = false, ) => { for (let i = 0; i < messages.length; i++) { @@ -556,7 +536,7 @@ export class ChannelState['formatMessage']>; + } as unknown) as ReturnType; } else { messages[i] = { ...m, @@ -603,7 +583,7 @@ export class ChannelState); + } as Event); } } } @@ -663,7 +643,7 @@ export class ChannelState['formatMessage']>} Returns the message, or undefined if the message wasn't found + * @return {ReturnType} Returns the message, or undefined if the message wasn't found */ findMessage(messageId: string, parentMessageId?: string) { if (parentMessageId) { @@ -699,14 +679,11 @@ export class ChannelState[], + newMessages: MessageResponse[], addIfDoesNotExist = true, messageSetToAddToIfDoesNotExist: MessageSetType = 'current', ) { - let messagesToAdd: ( - | MessageResponse - | ReturnType['formatMessage']> - )[] = newMessages; + let messagesToAdd: (MessageResponse | ReturnType)[] = newMessages; let targetMessageSetIndex!: number; if (addIfDoesNotExist) { const overlappingMessageSetIndices = this.messageSets diff --git a/src/client.ts b/src/client.ts index 9c85442f51..eb26c05906 100644 --- a/src/client.ts +++ b/src/client.ts @@ -74,7 +74,6 @@ import { CreatePollOptionAPIResponse, CustomPermissionOptions, DeactivateUsersOptions, - DefaultGenerics, DeleteChannelsResponse, DeleteCommandResponse, DeleteUserOptions, @@ -89,7 +88,6 @@ import { ExportChannelStatusResponse, ExportUsersRequest, ExportUsersResponse, - ExtendableGenerics, FlagMessageResponse, FlagReportsFilters, FlagReportsPaginationOptions, @@ -119,7 +117,6 @@ import { ListImportsResponse, Logger, MarkChannelsReadOptions, - Message, MessageFilters, MessageFlagsFilters, MessageFlagsPaginationOptions, @@ -196,8 +193,8 @@ import { TokenOrProvider, TranslateResponse, UnBanUserOptions, - UpdateChannelOptions, - UpdateChannelResponse, + UpdateChannelTypeRequest, + UpdateChannelTypeResponse, UpdateCommandOptions, UpdateCommandResponse, UpdatedMessage, @@ -226,15 +223,15 @@ function isString(x: unknown): x is string { return typeof x === 'string' || x instanceof String; } -export class StreamChat { +export class StreamChat { private static _instance?: unknown | StreamChat; // type is undefined|StreamChat, unknown is due to TS limitations with statics - _user?: OwnUserResponse | UserResponse; + _user?: OwnUserResponse | UserResponse; activeChannels: { - [key: string]: Channel; + [key: string]: Channel; }; - threads: ThreadManager; - polls: PollManager; + threads: ThreadManager; + polls: PollManager; anonymous: boolean; persistUserOnConnectionFailure?: boolean; axiosInstance: AxiosInstance; @@ -242,9 +239,9 @@ export class StreamChat; + configs: Configs; key: string; - listeners: Record) => void>>; + listeners: Record void>>; logger: Logger; /** * When network is recovered, we re-query the active channels on client. But in single query, you can recover @@ -256,22 +253,22 @@ export class StreamChat; - mutedChannels: ChannelMute[]; - mutedUsers: Mute[]; + moderation: Moderation; + mutedChannels: ChannelMute[]; + mutedUsers: Mute[]; node: boolean; options: StreamChatOptions; secret?: string; - setUserPromise: ConnectAPIResponse | null; - state: ClientState; - tokenManager: TokenManager; - user?: OwnUserResponse | UserResponse; + setUserPromise: ConnectAPIResponse | null; + state: ClientState; + tokenManager: TokenManager; + user?: OwnUserResponse | UserResponse; userAgent?: string; userID?: string; wsBaseURL?: string; - wsConnection: StableWSConnection | null; - wsFallback?: WSConnectionFallback; - wsPromise: ConnectAPIResponse | null; + wsConnection: StableWSConnection | null; + wsFallback?: WSConnectionFallback; + wsPromise: ConnectAPIResponse | null; consecutiveFailures: number; insightMetrics: InsightMetrics; defaultWSTimeoutWithFallback: number; @@ -304,7 +301,7 @@ export class StreamChat({ client: this }); + this.state = new ClientState({ client: this }); // a list of channels to hide ws events from this.mutedChannels = []; this.mutedUsers = []; @@ -449,29 +446,22 @@ export class StreamChatsecret is optional and only used in server side mode * StreamChat.getInstance('api_key', "secret", { httpsAgent: customAgent }) */ - public static getInstance( - key: string, - options?: StreamChatOptions, - ): StreamChat; - public static getInstance( - key: string, - secret?: string, - options?: StreamChatOptions, - ): StreamChat; - public static getInstance( + public static getInstance(key: string, options?: StreamChatOptions): StreamChat; + public static getInstance(key: string, secret?: string, options?: StreamChatOptions): StreamChat; + public static getInstance( key: string, secretOrOptions?: StreamChatOptions | string, options?: StreamChatOptions, - ): StreamChat { + ): StreamChat { if (!StreamChat._instance) { if (typeof secretOrOptions === 'string') { - StreamChat._instance = new StreamChat(key, secretOrOptions, options); + StreamChat._instance = new StreamChat(key, secretOrOptions, options); } else { - StreamChat._instance = new StreamChat(key, secretOrOptions); + StreamChat._instance = new StreamChat(key, secretOrOptions); } } - return StreamChat._instance as StreamChat; + return StreamChat._instance as StreamChat; } devToken(userID: string) { @@ -494,15 +484,12 @@ export class StreamChat | UserResponse} user Data about this user. IE {name: "john"} + * @param {OwnUserResponse | UserResponse} user Data about this user. IE {name: "john"} * @param {TokenOrProvider} userTokenOrProvider Token or provider * - * @return {ConnectAPIResponse} Returns a promise that resolves when the connection is setup + * @return {ConnectAPIResponse} Returns a promise that resolves when the connection is setup */ - connectUser = async ( - user: OwnUserResponse | UserResponse, - userTokenOrProvider: TokenOrProvider, - ) => { + connectUser = async (user: OwnUserResponse | UserResponse, userTokenOrProvider: TokenOrProvider) => { if (!user.id) { throw new Error('The "id" field on the user is missing'); } @@ -561,17 +548,17 @@ export class StreamChat | UserResponse} user Data about this user. IE {name: "john"} + * @param {OwnUserResponse | UserResponse} user Data about this user. IE {name: "john"} * @param {TokenOrProvider} userTokenOrProvider Token or provider * - * @return {ConnectAPIResponse} Returns a promise that resolves when the connection is setup + * @return {ConnectAPIResponse} Returns a promise that resolves when the connection is setup */ setUser = this.connectUser; - _setToken = (user: UserResponse, userTokenOrProvider: TokenOrProvider) => + _setToken = (user: UserResponse, userTokenOrProvider: TokenOrProvider) => this.tokenManager.setTokenOrProvider(userTokenOrProvider, user); - _setUser(user: OwnUserResponse | UserResponse) { + _setUser(user: OwnUserResponse | UserResponse) { /** * This one is used by the frontend. This is a copy of the current user object stored on backend. * It contains reserved properties and own user properties which are not present in `this._user`. @@ -617,7 +604,7 @@ export class StreamChat; + eventHandlerOverrides?: ChannelManagerEventHandlerOverrides; options?: ChannelManagerOptions; }) => { return new ChannelManager({ client: this, eventHandlerOverrides, options }); @@ -738,11 +725,11 @@ export class StreamChat[] = []; + const users: PartialUserUpdate[] = []; for (const userID of userIDs) { users.push({ id: userID, - set: >>{ + set: >{ revoke_tokens_issued_before: before, }, }); @@ -755,7 +742,7 @@ export class StreamChat>(this.baseURL + '/app'); + return await this.get(this.baseURL + '/app'); } /** @@ -873,7 +860,7 @@ export class StreamChat; + } as UserResponse; this._setToken(anonymousUser, ''); this._setUser(anonymousUser); @@ -889,18 +876,18 @@ export class StreamChat} user Data about this user. IE {name: "john"} + * @param {UserResponse} user Data about this user. IE {name: "john"} * - * @return {ConnectAPIResponse} Returns a promise that resolves when the connection is setup + * @return {ConnectAPIResponse} Returns a promise that resolves when the connection is setup */ - async setGuestUser(user: UserResponse) { - let response: { access_token: string; user: UserResponse } | undefined; + async setGuestUser(user: UserResponse) { + let response: { access_token: string; user: UserResponse } | undefined; this.anonymous = true; try { response = await this.post< APIResponse & { access_token: string; - user: UserResponse; + user: UserResponse; } >(this.baseURL + '/guest', { user }); } catch (e) { @@ -910,7 +897,7 @@ export class StreamChat, response.access_token); + return await this.connectUser(guestUser as UserResponse, response.access_token); } /** @@ -946,19 +933,16 @@ export class StreamChat {console.log(event.type)}) * - * @param {EventHandler | string} callbackOrString The event type to listen for (optional) - * @param {EventHandler} [callbackOrNothing] The callback to call + * @param {EventHandler | string} callbackOrString The event type to listen for (optional) + * @param {EventHandler} [callbackOrNothing] The callback to call * * @return {{ unsubscribe: () => void }} Description */ - on(callback: EventHandler): { unsubscribe: () => void }; - on(eventType: string, callback: EventHandler): { unsubscribe: () => void }; - on( - callbackOrString: EventHandler | string, - callbackOrNothing?: EventHandler, - ): { unsubscribe: () => void } { + on(callback: EventHandler): { unsubscribe: () => void }; + on(eventType: string, callback: EventHandler): { unsubscribe: () => void }; + on(callbackOrString: EventHandler | string, callbackOrNothing?: EventHandler): { unsubscribe: () => void } { const key = callbackOrNothing ? (callbackOrString as string) : 'all'; - const callback = callbackOrNothing ? callbackOrNothing : (callbackOrString as EventHandler); + const callback = callbackOrNothing ? callbackOrNothing : (callbackOrString as EventHandler); if (!(key in this.listeners)) { this.listeners[key] = []; } @@ -980,14 +964,11 @@ export class StreamChat): void; - off(eventType: string, callback: EventHandler): void; - off( - callbackOrString: EventHandler | string, - callbackOrNothing?: EventHandler, - ) { + off(callback: EventHandler): void; + off(eventType: string, callback: EventHandler): void; + off(callbackOrString: EventHandler | string, callbackOrNothing?: EventHandler) { const key = callbackOrNothing ? (callbackOrString as string) : 'all'; - const callback = callbackOrNothing ? callbackOrNothing : (callbackOrString as EventHandler); + const callback = callbackOrNothing ? callbackOrNothing : (callbackOrString as EventHandler); if (!(key in this.listeners)) { this.listeners[key] = []; } @@ -1117,7 +1098,7 @@ export class StreamChat, + user?: UserResponse, ) { const data = addFileToFormData(uri, name, contentType || 'multipart/form-data'); if (user != null) data.append('user', JSON.stringify(user)); @@ -1152,7 +1133,7 @@ export class StreamChat) => { + dispatchEvent = (event: Event) => { if (!event.received_at) event.received_at = new Date(); // client event handlers @@ -1177,16 +1158,16 @@ export class StreamChat { // dispatch the event to the channel listeners const jsonString = messageEvent.data as string; - const event = JSON.parse(jsonString) as Event; + const event = JSON.parse(jsonString) as Event; this.dispatchEvent(event); }; /** * Updates the members, watchers and read references of the currently active channels that contain this user * - * @param {UserResponse} user + * @param {UserResponse} user */ - _updateMemberWatcherReferences = (user: UserResponse) => { + _updateMemberWatcherReferences = (user: UserResponse) => { const refMap = this.state.userChannelReferences[user.id] || {}; for (const channelID in refMap) { const channel = this.activeChannels[channelID]; @@ -1216,9 +1197,9 @@ export class StreamChat} user + * @param {UserResponse} user */ - _updateUserMessageReferences = (user: UserResponse) => { + _updateUserMessageReferences = (user: UserResponse) => { const refMap = this.state.userChannelReferences[user.id] || {}; for (const channelID in refMap) { @@ -1241,10 +1222,10 @@ export class StreamChat} user + * @param {UserResponse} user * @param {boolean} hardDelete */ - _deleteUserMessageReference = (user: UserResponse, hardDelete = false) => { + _deleteUserMessageReference = (user: UserResponse, hardDelete = false) => { const refMap = this.state.userChannelReferences[user.id] || {}; for (const channelID in refMap) { @@ -1268,7 +1249,7 @@ export class StreamChat) => { + _handleUserEvent = (event: Event) => { if (!event.user) { return; } @@ -1276,8 +1257,8 @@ export class StreamChat; + const _user = { ...this._user } as NonNullable; // Remove deleted properties from user objects. for (const key in this.user) { @@ -1285,19 +1266,23 @@ export class StreamChat) { + _handleClientEvent(event: Event) { const client = this; const postListenerCallbacks = []; this.logger('info', `client:_handleClientEvent - Received event of type { ${event.type} }`, { @@ -1389,10 +1374,10 @@ export class StreamChat) => { + _callClientListeners = (event: Event) => { const client = this; // gather and call the listeners - const listeners: Array<(event: Event) => void> = []; + const listeners: Array<(event: Event) => void> = []; if (client.listeners.all) { listeners.push(...client.listeners.all); } @@ -1417,20 +1402,16 @@ export class StreamChat, - { last_message_at: -1 }, - { limit: 30 }, - ); + await this.queryChannels({ cid: { $in: cids } } as ChannelFilters, { last_message_at: -1 }, { limit: 30 }); this.logger('info', 'client:recoverState() - Querying channels finished', { tags: ['connection', 'client'] }); this.dispatchEvent({ type: 'connection.recovered', - } as Event); + } as Event); } else { this.dispatchEvent({ type: 'connection.recovered', - } as Event); + } as Event); } this.wsPromise = Promise.resolve(); @@ -1457,10 +1438,10 @@ export class StreamChat).setClient(this); - this.wsConnection = (this.options.wsConnection as unknown) as StableWSConnection; + ((this.options.wsConnection as unknown) as StableWSConnection).setClient(this); + this.wsConnection = (this.options.wsConnection as unknown) as StableWSConnection; } else { - this.wsConnection = new StableWSConnection({ + this.wsConnection = new StableWSConnection({ client: this, }); } @@ -1485,7 +1466,7 @@ export class StreamChat({ + this.wsFallback = new WSConnectionFallback({ client: this, }); return await this.wsFallback.connect(); @@ -1517,18 +1498,14 @@ export class StreamChat} filterConditions MongoDB style filter conditions - * @param {UserSort} sort Sort options, for instance [{last_active: -1}]. + * @param {UserFilters} filterConditions MongoDB style filter conditions + * @param {UserSort} sort Sort options, for instance [{last_active: -1}]. * When using multiple fields, make sure you use array of objects to guarantee field order, for instance [{last_active: -1}, {created_at: 1}] * @param {UserOptions} options Option object, {presence: true} * - * @return {Promise<{ users: Array> }>} User Query Response + * @return {Promise<{ users: Array }>} User Query Response */ - async queryUsers( - filterConditions: UserFilters, - sort: UserSort = [], - options: UserOptions = {}, - ) { + async queryUsers(filterConditions: UserFilters, sort: UserSort = [], options: UserOptions = {}) { const defaultOptions = { presence: false, }; @@ -1541,17 +1518,14 @@ export class StreamChat> }>( - this.baseURL + '/users', - { - payload: { - filter_conditions: filterConditions, - sort: normalizeQuerySort(sort), - ...defaultOptions, - ...options, - }, + const data = await this.get }>(this.baseURL + '/users', { + payload: { + filter_conditions: filterConditions, + sort: normalizeQuerySort(sort), + ...defaultOptions, + ...options, }, - ); + }); this.state.updateUsers(data.users); @@ -1565,7 +1539,7 @@ export class StreamChat>} Ban Query Response + * @return {Promise} Ban Query Response */ async queryBannedUsers( filterConditions: BannedUsersFilters = {}, @@ -1573,7 +1547,7 @@ export class StreamChat>(this.baseURL + '/query_banned_users', { + return await this.get(this.baseURL + '/query_banned_users', { payload: { filter_conditions: filterConditions, sort: normalizeQuerySort(sort), @@ -1588,11 +1562,11 @@ export class StreamChat>} Message Flags Response + * @return {Promise} Message Flags Response */ async queryMessageFlags(filterConditions: MessageFlagsFilters = {}, options: MessageFlagsPaginationOptions = {}) { // Return a list of message flags - return await this.get>(this.baseURL + '/moderation/flags/message', { + return await this.get(this.baseURL + '/moderation/flags/message', { payload: { filter_conditions: filterConditions, ...options }, }); } @@ -1600,18 +1574,18 @@ export class StreamChat} filterConditions object MongoDB style filters - * @param {ChannelSort} [sort] Sort options, for instance {created_at: -1}. + * @param {ChannelFilters} filterConditions object MongoDB style filters + * @param {ChannelSort} [sort] Sort options, for instance {created_at: -1}. * When using multiple fields, make sure you use array of objects to guarantee field order, for instance [{last_updated: -1}, {created_at: 1}] * @param {ChannelOptions} [options] Options object * @param {ChannelStateOptions} [stateOptions] State options object. These options will only be used for state management and won't be sent in the request. * - stateOptions.skipInitialization - Skips the initialization of the state for the channels matching the ids in the list. * - * @return {Promise<{ channels: Array>}> } search channels response + * @return {Promise<{ channels: Array}> } search channels response */ async queryChannels( - filterConditions: ChannelFilters, - sort: ChannelSort = [], + filterConditions: ChannelFilters, + sort: ChannelSort = [], options: ChannelOptions = {}, stateOptions: ChannelStateOptions = {}, ) { @@ -1635,7 +1609,7 @@ export class StreamChat>(this.baseURL + '/channels', payload); + const data = await this.post(this.baseURL + '/channels', payload); this.dispatchEvent({ type: 'channels.queried', @@ -1651,16 +1625,16 @@ export class StreamChat} filter object MongoDB style filters - * @param {ReactionSort} [sort] Sort options, for instance {created_at: -1}. + * @param {ReactionFilters} filter object MongoDB style filters + * @param {ReactionSort} [sort] Sort options, for instance {created_at: -1}. * @param {QueryReactionsOptions} [options] Pagination object * * @return {Promise<{ QueryReactionsAPIResponse } search channels response */ async queryReactions( messageID: string, - filter: ReactionFilters, - sort: ReactionSort = [], + filter: ReactionFilters, + sort: ReactionSort = [], options: QueryReactionsOptions = {}, ) { // Make sure we wait for the connect promise if there is a pending one @@ -1673,19 +1647,19 @@ export class StreamChat>( + return await this.post( this.baseURL + '/messages/' + encodeURIComponent(messageID) + '/reactions', payload, ); } hydrateActiveChannels( - channelsFromApi: ChannelAPIResponse[] = [], + channelsFromApi: ChannelAPIResponse[] = [], stateOptions: ChannelStateOptions = {}, queryChannelsOptions?: ChannelOptions, ) { const { skipInitialization, offlineMode = false } = stateOptions; - const channels: Channel[] = []; + const channels: Channel[] = []; for (const channelState of channelsFromApi) { this._addChannelConfig(channelState.channel); @@ -1727,24 +1701,20 @@ export class StreamChat} filterConditions MongoDB style filter conditions - * @param {MessageFilters | string} query search query or object MongoDB style filters - * @param {SearchOptions} [options] Option object, {user_id: 'tommaso'} + * @param {ChannelFilters} filterConditions MongoDB style filter conditions + * @param {MessageFilters | string} query search query or object MongoDB style filters + * @param {SearchOptions} [options] Option object, {user_id: 'tommaso'} * - * @return {Promise>} search messages response + * @return {Promise} search messages response */ - async search( - filterConditions: ChannelFilters, - query: string | MessageFilters, - options: SearchOptions = {}, - ) { + async search(filterConditions: ChannelFilters, query: string | MessageFilters, options: SearchOptions = {}) { if (options.offset && options.next) { throw Error(`Cannot specify offset with next`); } - const payload: SearchPayload = { + const payload: SearchPayload = { filter_conditions: filterConditions, ...options, - sort: options.sort ? normalizeQuerySort>(options.sort) : undefined, + sort: options.sort ? normalizeQuerySort(options.sort) : undefined, }; if (typeof query === 'string') { payload.query = query; @@ -1757,7 +1727,7 @@ export class StreamChat>(this.baseURL + '/search', { payload }); + return await this.get(this.baseURL + '/search', { payload }); } /** @@ -1802,10 +1772,10 @@ export class StreamChat[]} Array of devices + * @return {Device[]} Array of devices */ async getDevices(userID?: string) { - return await this.get[] }>( + return await this.get( this.baseURL + '/devices', userID ? { user_id: userID } : {}, ); @@ -1882,7 +1852,7 @@ export class StreamChat) { + _addChannelConfig({ cid, config }: ChannelResponse) { if (this._cacheEnabled()) { this.configs[cid] = config; } @@ -1897,28 +1867,20 @@ export class StreamChat | null} [channelIDOrCustom] The channel ID, you can leave this out if you want to create a conversation channel + * @param {string | ChannelData | null} [channelIDOrCustom] The channel ID, you can leave this out if you want to create a conversation channel * @param {object} [custom] Custom data to attach to the channel * * @return {channel} The channel object, initialize it using channel.watch() */ - channel( - channelType: string, - channelID?: string | null, - custom?: ChannelData, - ): Channel; - channel(channelType: string, custom?: ChannelData): Channel; - channel( - channelType: string, - channelIDOrCustom?: string | ChannelData | null, - custom: ChannelData = {} as ChannelData, - ) { + channel(channelType: string, channelID?: string | null, custom?: ChannelData): Channel; + channel(channelType: string, custom?: ChannelData): Channel; + channel(channelType: string, channelIDOrCustom?: string | ChannelData | null, custom: ChannelData = {}) { if (!this.userID && !this._isUsingServerAuth()) { throw Error('Call connectUser or connectAnonymousUser before creating a channel'); } if (~channelType.indexOf(':')) { - throw Error(`Invalid channel group ${channelType}, can't contain the : character`); + throw new Error(`Invalid channel group ${channelType}, can't contain the : character`); } // support channel("messaging", {options}) @@ -1926,7 +1888,7 @@ export class StreamChat(this, channelType, undefined, custom); + return new Channel(this, channelType, undefined, custom); } return this.getChannelById(channelType, channelIDOrCustom, custom); @@ -1957,10 +1919,10 @@ export class StreamChat) => { + getChannelByMembers = (channelType: string, custom: ChannelData) => { // Check if the channel already exists. // Only allow 1 channel object per cid - const memberIds = (custom.members ?? []).map((member: string | NewMemberPayload) => + const memberIds = (custom.members ?? []).map((member: string | NewMemberPayload) => typeof member === 'string' ? member : member.user_id ?? '', ); const membersStr = memberIds.sort().join(','); @@ -1993,7 +1955,7 @@ export class StreamChat(this, channelType, undefined, custom); + const channel = new Channel(this, channelType, undefined, custom); // For the time being set the key as membersStr, since we don't know the cid yet. // In channel.query, we will replace it with 'cid'. @@ -2020,7 +1982,7 @@ export class StreamChat) => { + getChannelById = (channelType: string, channelID: string, custom: ChannelData) => { if (typeof channelID === 'string' && ~channelID.indexOf(':')) { throw Error(`Invalid channel id ${channelID}, can't contain the : character`); } @@ -2035,7 +1997,7 @@ export class StreamChat(this, channelType, channelID, custom); + const channel = new Channel(this, channelType, channelID, custom); if (this._cacheEnabled()) { this.activeChannels[channel.cid] = channel; } @@ -2046,24 +2008,24 @@ export class StreamChat} partialUserObject which should contain id and any of "set" or "unset" params; + * @param {PartialUserUpdate} partialUserObject which should contain id and any of "set" or "unset" params; * example: {id: "user1", set:{field: value}, unset:["field2"]} * - * @return {Promise<{ users: { [key: string]: UserResponse } }>} list of updated users + * @return {Promise<{ users: { [key: string]: UserResponse } }>} list of updated users */ - async partialUpdateUser(partialUserObject: PartialUserUpdate) { + async partialUpdateUser(partialUserObject: PartialUserUpdate) { return await this.partialUpdateUsers([partialUserObject]); } /** * upsertUsers - Batch upsert the list of users * - * @param {UserResponse[]} users list of users + * @param {UserResponse[]} users list of users * - * @return {Promise<{ users: { [key: string]: UserResponse } }>} + * @return {Promise<{ users: { [key: string]: UserResponse } }>} */ - async upsertUsers(users: UserResponse[]) { - const userMap: { [key: string]: UserResponse } = {}; + async upsertUsers(users: UserResponse[]) { + const userMap: { [key: string]: UserResponse } = {}; for (const userObject of users) { if (!userObject.id) { throw Error('User ID is required when updating a user'); @@ -2073,7 +2035,7 @@ export class StreamChat }; + users: { [key: string]: UserResponse }; } >(this.baseURL + '/users', { users: userMap }); } @@ -2083,19 +2045,19 @@ export class StreamChat[]} users list of users - * @return {Promise<{ users: { [key: string]: UserResponse } }>} + * @param {UserResponse[]} users list of users + * @return {Promise<{ users: { [key: string]: UserResponse } }>} */ updateUsers = this.upsertUsers; /** * upsertUser - Update or Create the given user object * - * @param {UserResponse} userObject user object, the only required field is the user id. IE {id: "myuser"} is valid + * @param {UserResponse} userObject user object, the only required field is the user id. IE {id: "myuser"} is valid * - * @return {Promise<{ users: { [key: string]: UserResponse } }>} + * @return {Promise<{ users: { [key: string]: UserResponse } }>} */ - upsertUser(userObject: UserResponse) { + upsertUser(userObject: UserResponse) { return this.upsertUsers([userObject]); } @@ -2104,19 +2066,19 @@ export class StreamChat} userObject user object, the only required field is the user id. IE {id: "myuser"} is valid - * @return {Promise<{ users: { [key: string]: UserResponse } }>} + * @param {UserResponse} userObject user object, the only required field is the user id. IE {id: "myuser"} is valid + * @return {Promise<{ users: { [key: string]: UserResponse } }>} */ updateUser = this.upsertUser; /** * partialUpdateUsers - Batch partial update of users * - * @param {PartialUserUpdate[]} users list of partial update requests + * @param {PartialUserUpdate[]} users list of partial update requests * - * @return {Promise<{ users: { [key: string]: UserResponse } }>} + * @return {Promise<{ users: { [key: string]: UserResponse } }>} */ - async partialUpdateUsers(users: PartialUserUpdate[]) { + async partialUpdateUsers(users: PartialUserUpdate[]) { for (const userObject of users) { if (!userObject.id) { throw Error('User ID is required when updating a user'); @@ -2125,7 +2087,7 @@ export class StreamChat }; + users: { [key: string]: UserResponse }; } >(this.baseURL + '/users', { users }); } @@ -2139,7 +2101,7 @@ export class StreamChat } & { + APIResponse & { user: UserResponse } & { task_id?: string; } >(this.baseURL + `/users/${encodeURIComponent(userID)}`, params); @@ -2167,7 +2129,7 @@ export class StreamChat }>( + return await this.post( this.baseURL + `/users/${encodeURIComponent(userID)}/reactivate`, { ...options }, ); @@ -2194,7 +2156,7 @@ export class StreamChat }>( + return await this.post( this.baseURL + `/users/${encodeURIComponent(userID)}/deactivate`, { ...options }, ); @@ -2215,9 +2177,9 @@ export class StreamChat) { return await this.get< APIResponse & { - messages: MessageResponse[]; - reactions: ReactionResponse[]; - user: UserResponse; + messages: MessageResponse[]; + reactions: ReactionResponse[]; + user: UserResponse; } >(this.baseURL + `/users/${encodeURIComponent(userID)}/export`, { ...options }); } @@ -2225,10 +2187,10 @@ export class StreamChat} [options] + * @param {BanUserOptions} [options] * @returns {Promise} */ - async banUser(targetUserID: string, options?: BanUserOptions) { + async banUser(targetUserID: string, options?: BanUserOptions) { return await this.post(this.baseURL + '/moderation/ban', { target_user_id: targetUserID, ...options, @@ -2251,10 +2213,10 @@ export class StreamChat} [options] + * @param {BanUserOptions} [options] * @returns {Promise} */ - async shadowBan(targetUserID: string, options?: BanUserOptions) { + async shadowBan(targetUserID: string, options?: BanUserOptions) { return await this.banUser(targetUserID, { shadow: true, ...options, @@ -2295,11 +2257,11 @@ export class StreamChat} [options] - * @returns {Promise>} + * @param {MuteUserOptions} [options] + * @returns {Promise} */ - async muteUser(targetID: string, userID?: string, options: MuteUserOptions = {}) { - return await this.post>(this.baseURL + '/moderation/mute', { + async muteUser(targetID: string, userID?: string, options: MuteUserOptions = {}) { + return await this.post(this.baseURL + '/moderation/mute', { target_id: targetID, ...(userID ? { user_id: userID } : {}), ...options, @@ -2342,7 +2304,7 @@ export class StreamChat} */ async flagMessage(targetMessageID: string, options: { reason?: string; user_id?: string } = {}) { - return await this.post>(this.baseURL + '/moderation/flag', { + return await this.post(this.baseURL + '/moderation/flag', { target_message_id: targetMessageID, ...options, }); @@ -2355,7 +2317,7 @@ export class StreamChat} */ async flagUser(targetID: string, options: { reason?: string; user_id?: string } = {}) { - return await this.post>(this.baseURL + '/moderation/flag', { + return await this.post(this.baseURL + '/moderation/flag', { target_user_id: targetID, ...options, }); @@ -2368,7 +2330,7 @@ export class StreamChat} */ async unflagMessage(targetMessageID: string, options: { user_id?: string } = {}) { - return await this.post>(this.baseURL + '/moderation/unflag', { + return await this.post(this.baseURL + '/moderation/unflag', { target_message_id: targetMessageID, ...options, }); @@ -2381,7 +2343,7 @@ export class StreamChat} */ async unflagUser(targetID: string, options: { user_id?: string } = {}) { - return await this.post>(this.baseURL + '/moderation/unflag', { + return await this.post(this.baseURL + '/moderation/unflag', { target_user_id: targetID, ...options, }); @@ -2409,11 +2371,11 @@ export class StreamChat>} Flags Response + * @return {Promise} Flags Response */ async _queryFlags(filterConditions: FlagsFilters = {}, options: FlagsPaginationOptions = {}) { // Return a list of flags - return await this.post>(this.baseURL + '/moderation/flags', { + return await this.post(this.baseURL + '/moderation/flags', { filter_conditions: filterConditions, ...options, }); @@ -2430,11 +2392,11 @@ export class StreamChat>} Flag Reports Response + * @return {Promise} Flag Reports Response */ async _queryFlagReports(filterConditions: FlagReportsFilters = {}, options: FlagReportsPaginationOptions = {}) { // Return a list of message flags - return await this.post>(this.baseURL + '/moderation/reports', { + return await this.post(this.baseURL + '/moderation/reports', { filter_conditions: filterConditions, ...options, }); @@ -2455,13 +2417,10 @@ export class StreamChat>} */ async _reviewFlagReport(id: string, reviewResult: string, options: ReviewFlagReportOptions = {}) { - return await this.patch>( - this.baseURL + `/moderation/reports/${encodeURIComponent(id)}`, - { - review_result: reviewResult, - ...options, - }, - ); + return await this.patch(this.baseURL + `/moderation/reports/${encodeURIComponent(id)}`, { + review_result: reviewResult, + ...options, + }); } /** @@ -2485,7 +2444,7 @@ export class StreamChat} [data] + * @param {MarkAllReadOptions} [data] * * @return {Promise} */ @@ -2495,55 +2454,45 @@ export class StreamChat} [data] + * @param {MarkChannelsReadOptions } [data] * * @return {Promise} */ - async markChannelsRead(data: MarkChannelsReadOptions = {}) { + async markChannelsRead(data: MarkChannelsReadOptions = {}) { await this.post(this.baseURL + '/channels/read', { ...data }); } - createCommand(data: CreateCommandOptions) { - return this.post>(this.baseURL + '/commands', data); + createCommand(data: CreateCommandOptions) { + return this.post(this.baseURL + '/commands', data); } getCommand(name: string) { - return this.get>(this.baseURL + `/commands/${encodeURIComponent(name)}`); + return this.get(this.baseURL + `/commands/${encodeURIComponent(name)}`); } - updateCommand(name: string, data: UpdateCommandOptions) { - return this.put>( - this.baseURL + `/commands/${encodeURIComponent(name)}`, - data, - ); + updateCommand(name: string, data: UpdateCommandOptions) { + return this.put(this.baseURL + `/commands/${encodeURIComponent(name)}`, data); } deleteCommand(name: string) { - return this.delete>( - this.baseURL + `/commands/${encodeURIComponent(name)}`, - ); + return this.delete(this.baseURL + `/commands/${encodeURIComponent(name)}`); } listCommands() { - return this.get>(this.baseURL + `/commands`); + return this.get(this.baseURL + `/commands`); } - createChannelType(data: CreateChannelOptions) { + createChannelType(data: CreateChannelOptions) { const channelData = Object.assign({}, { commands: ['all'] }, data); - return this.post>(this.baseURL + '/channeltypes', channelData); + return this.post(this.baseURL + '/channeltypes', channelData); } getChannelType(channelType: string) { - return this.get>( - this.baseURL + `/channeltypes/${encodeURIComponent(channelType)}`, - ); + return this.get(this.baseURL + `/channeltypes/${encodeURIComponent(channelType)}`); } - updateChannelType(channelType: string, data: UpdateChannelOptions) { - return this.put>( - this.baseURL + `/channeltypes/${encodeURIComponent(channelType)}`, - data, - ); + updateChannelType(channelType: string, data: UpdateChannelTypeRequest) { + return this.put(this.baseURL + `/channeltypes/${encodeURIComponent(channelType)}`, data); } deleteChannelType(channelType: string) { @@ -2551,7 +2500,7 @@ export class StreamChat>(this.baseURL + `/channeltypes`); + return this.get(this.baseURL + `/channeltypes`); } /** @@ -2560,10 +2509,10 @@ export class StreamChat} Response that includes the message + * @return {MessageResponse} Response that includes the message */ async translateMessage(messageId: string, language: string) { - return await this.post>( + return await this.post( this.baseURL + `/messages/${encodeURIComponent(messageId)}/translate`, { language }, ); @@ -2647,7 +2596,7 @@ export class StreamChat, + } as unknown) as PartialMessageUpdate, pinnedBy, ); } @@ -2666,7 +2615,7 @@ export class StreamChat, + } as unknown) as PartialMessageUpdate, userId, ); } @@ -2674,22 +2623,18 @@ export class StreamChat, 'mentioned_users'> & { mentioned_users?: string[] }} message object, id needs to be specified + * @param {Omit & { mentioned_users?: string[] }} message object, id needs to be specified * @param {string | { id: string }} [userId] * @param {boolean} [options.skip_enrich_url] Do not try to enrich the URLs within message * - * @return {{ message: MessageResponse }} Response that includes the message + * @return {{ message: MessageResponse }} Response that includes the message */ - async updateMessage( - message: UpdatedMessage, - userId?: string | { id: string }, - options?: UpdateMessageOptions, - ) { + async updateMessage(message: UpdatedMessage, userId?: string | { id: string }, options?: UpdateMessageOptions) { if (!message.id) { throw Error('Please specify the message id when calling updateMessage'); } - const clonedMessage: Message = Object.assign({}, message); + const clonedMessage: Partial = { ...message }; delete clonedMessage.id; const reservedMessageFields: Array = [ @@ -2707,11 +2652,11 @@ export class StreamChat; + } as UserResponse; } } @@ -2731,7 +2676,7 @@ export class StreamChat ((mu as unknown) as UserResponse).id); } - return await this.post>( + return await this.post( this.baseURL + `/messages/${encodeURIComponent(message.id as string)}`, { message: clonedMessage, @@ -2745,17 +2690,17 @@ export class StreamChat} partialMessageObject which should contain id and any of "set" or "unset" params; + * @param {PartialUpdateMessage} partialMessageObject which should contain id and any of "set" or "unset" params; * example: {id: "user1", set:{text: "hi"}, unset:["color"]} * @param {string | { id: string }} [userId] * * @param {boolean} [options.skip_enrich_url] Do not try to enrich the URLs within message * - * @return {{ message: MessageResponse }} Response that includes the updated message + * @return {{ message: MessageResponse }} Response that includes the updated message */ async partialUpdateMessage( id: string, - partialMessageObject: PartialMessageUpdate, + partialMessageObject: PartialMessageUpdate, userId?: string | { id: string }, options?: UpdateMessageOptions, ) { @@ -2766,14 +2711,11 @@ export class StreamChat>( - this.baseURL + `/messages/${encodeURIComponent(id)}`, - { - ...partialMessageObject, - ...options, - user, - }, - ); + return await this.put(this.baseURL + `/messages/${encodeURIComponent(id)}`, { + ...partialMessageObject, + ...options, + user, + }); } async deleteMessage(messageID: string, hardDelete?: boolean) { @@ -2781,7 +2723,7 @@ export class StreamChat }>( + return await this.delete( this.baseURL + `/messages/${encodeURIComponent(messageID)}`, params, ); @@ -2797,20 +2739,19 @@ export class StreamChat }} Response that includes the message + * @return {{ message: MessageResponse }} Response that includes the message */ async undeleteMessage(messageID: string, userID: string) { - return await this.post }>( + return await this.post( this.baseURL + `/messages/${encodeURIComponent(messageID)}/undelete`, { undeleted_by: userID }, ); } async getMessage(messageID: string, options?: GetMessageOptions) { - return await this.get>( - this.baseURL + `/messages/${encodeURIComponent(messageID)}`, - { ...options }, - ); + return await this.get(this.baseURL + `/messages/${encodeURIComponent(messageID)}`, { + ...options, + }); } /** @@ -2822,7 +2763,7 @@ export class StreamChat[], next: string }} Returns the list of threads and the next cursor. + * @returns {{ threads: Thread[], next: string }} Returns the list of threads and the next cursor. */ async queryThreads(options: QueryThreadsOptions = {}) { const optionsWithDefaults = { @@ -2833,15 +2774,10 @@ export class StreamChat>( - `${this.baseURL}/threads`, - optionsWithDefaults, - ); + const response = await this.post(`${this.baseURL}/threads`, optionsWithDefaults); return { - threads: response.threads.map( - (thread) => new Thread({ client: this, threadData: thread }), - ), + threads: response.threads.map((thread) => new Thread({ client: this, threadData: thread })), next: response.next, }; } @@ -2855,7 +2791,7 @@ export class StreamChat} Returns the thread. + * @returns {Thread} Returns the thread. */ async getThread(messageId: string, options: GetThreadOptions = {}) { if (!messageId) { @@ -2869,12 +2805,12 @@ export class StreamChat>( + const response = await this.get( `${this.baseURL}/threads/${encodeURIComponent(messageId)}`, optionsWithDefaults, ); - return new Thread({ client: this, threadData: response.thread }); + return new Thread({ client: this, threadData: response.thread }); } /** @@ -2883,7 +2819,7 @@ export class StreamChat} Returns the updated thread. + * @returns {GetThreadAPIResponse} Returns the updated thread. */ async partialUpdateThread(messageId: string, partialThreadObject: PartialThreadUpdate) { if (!messageId) { @@ -2913,7 +2849,7 @@ export class StreamChat>( + return await this.patch( `${this.baseURL}/threads/${encodeURIComponent(messageId)}`, partialThreadObject, ); @@ -3727,8 +3663,8 @@ export class StreamChat, userId?: string) { - return await this.post>(this.baseURL + `/polls`, { + async createPoll(poll: CreatePollData, userId?: string) { + return await this.post(this.baseURL + `/polls`, { ...poll, ...(userId ? { user_id: userId } : {}), }); @@ -3740,8 +3676,8 @@ export class StreamChat> { - return await this.get>( + async getPoll(id: string, userId?: string): Promise { + return await this.get( this.baseURL + `/polls/${encodeURIComponent(id)}`, userId ? { user_id: userId } : {}, ); @@ -3753,8 +3689,8 @@ export class StreamChat, userId?: string) { - return await this.put>(this.baseURL + `/polls`, { + async updatePoll(poll: PollData, userId?: string) { + return await this.put(this.baseURL + `/polls`, { ...poll, ...(userId ? { user_id: userId } : {}), }); @@ -3763,23 +3699,20 @@ export class StreamChat} partialPollObject which should contain id and any of "set" or "unset" params; + * @param {PartialPollUpdate} partialPollObject which should contain id and any of "set" or "unset" params; * @param userId string The user id (only serverside) * example: {id: "44f26af5-f2be-4fa7-9dac-71cf893781de", set:{field: value}, unset:["field2"]} * @returns {APIResponse & UpdatePollAPIResponse} The poll */ async partialUpdatePoll( id: string, - partialPollObject: PartialPollUpdate, + partialPollObject: PartialPollUpdate, userId?: string, - ): Promise> { - return await this.patch>( - this.baseURL + `/polls/${encodeURIComponent(id)}`, - { - ...partialPollObject, - ...(userId ? { user_id: userId } : {}), - }, - ); + ): Promise { + return await this.patch(this.baseURL + `/polls/${encodeURIComponent(id)}`, { + ...partialPollObject, + ...(userId ? { user_id: userId } : {}), + }); } /** @@ -3800,13 +3733,13 @@ export class StreamChat> { + async closePoll(id: string, userId?: string): Promise { return this.partialUpdatePoll( id, { set: { is_closed: true, - } as PartialPollUpdate['set'], + } as PartialPollUpdate['set'], }, userId, ); @@ -3819,8 +3752,8 @@ export class StreamChat, userId?: string) { - return await this.post>( + async createPollOption(pollId: string, option: PollOptionData, userId?: string) { + return await this.post( this.baseURL + `/polls/${encodeURIComponent(pollId)}/options`, { ...option, @@ -3837,7 +3770,7 @@ export class StreamChat>( + return await this.get( this.baseURL + `/polls/${encodeURIComponent(pollId)}/options/${encodeURIComponent(optionId)}`, userId ? { user_id: userId } : {}, ); @@ -3850,8 +3783,8 @@ export class StreamChat, userId?: string) { - return await this.put>( + async updatePollOption(pollId: string, option: PollOptionData, userId?: string) { + return await this.put( this.baseURL + `/polls/${encodeURIComponent(pollId)}/options`, { ...option, @@ -3883,7 +3816,7 @@ export class StreamChat>( + return await this.post( this.baseURL + `/messages/${encodeURIComponent(messageId)}/polls/${encodeURIComponent(pollId)}/vote`, { vote, @@ -3935,9 +3868,9 @@ export class StreamChat> { + ): Promise { const q = userId ? `?user_id=${userId}` : ''; - return await this.post>(this.baseURL + `/polls/query${q}`, { + return await this.post(this.baseURL + `/polls/query${q}`, { filter, sort: normalizeQuerySort(sort), ...options, @@ -3959,9 +3892,9 @@ export class StreamChat> { + ): Promise { const q = userId ? `?user_id=${userId}` : ''; - return await this.post>( + return await this.post( this.baseURL + `/polls/${encodeURIComponent(pollId)}/votes${q}`, { filter, @@ -3986,9 +3919,9 @@ export class StreamChat> { + ): Promise { const q = userId ? `?user_id=${userId}` : ''; - return await this.post>( + return await this.post( this.baseURL + `/polls/${encodeURIComponent(pollId)}/votes${q}`, { filter: { ...filter, is_answer: true }, @@ -4009,15 +3942,12 @@ export class StreamChat> { - return await this.post>( - this.baseURL + '/messages/history', - { - filter, - sort: normalizeQuerySort(sort), - ...options, - }, - ); + ): Promise { + return await this.post(this.baseURL + '/messages/history', { + filter, + sort: normalizeQuerySort(sort), + ...options, + }); } /** diff --git a/src/client_state.ts b/src/client_state.ts index 82acd9d8b7..71f0080e9c 100644 --- a/src/client_state.ts +++ b/src/client_state.ts @@ -1,16 +1,16 @@ -import { UserResponse, ExtendableGenerics, DefaultGenerics } from './types'; +import { UserResponse } from './types'; import { StreamChat } from './client'; /** * ClientState - A container class for the client state. */ -export class ClientState { - private client: StreamChat; +export class ClientState { + private client: StreamChat; users: { - [key: string]: UserResponse; + [key: string]: UserResponse; }; userChannelReferences: { [key: string]: { [key: string]: boolean } }; - constructor({ client }: { client: StreamChat }) { + constructor({ client }: { client: StreamChat }) { // show the status for a certain user... // ie online, offline etc this.client = client; @@ -19,19 +19,19 @@ export class ClientState[]) { + updateUsers(users: UserResponse[]) { for (const user of users) { this.updateUser(user); } } - updateUser(user?: UserResponse) { + updateUser(user?: UserResponse) { if (user != null && this.client._cacheEnabled()) { this.users[user.id] = user; } } - updateUserReference(user: UserResponse, channelID: string) { + updateUserReference(user: UserResponse, channelID: string) { if (user == null || !this.client._cacheEnabled()) { return; } diff --git a/src/connection.ts b/src/connection.ts index b5c765a085..33efe7184b 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -9,8 +9,9 @@ import { addConnectionEventListeners, } from './utils'; import { buildWsFatalInsight, buildWsSuccessAfterFailureInsight, postInsights } from './insights'; -import { ConnectAPIResponse, ConnectionOpen, ExtendableGenerics, DefaultGenerics, UR, LogLevel } from './types'; +import { ConnectAPIResponse, ConnectionOpen, UR, LogLevel } from './types'; import { StreamChat } from './client'; +import { APIError } from './errors'; // Type guards to check WebSocket error type const isCloseEvent = (res: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent): res is WebSocket.CloseEvent => @@ -36,13 +37,13 @@ const isErrorEvent = (res: WebSocket.CloseEvent | WebSocket.Data | WebSocket.Err * - state can be recovered by querying the channel again * - if the servers fails to publish a message to the client, the WS connection is destroyed */ -export class StableWSConnection { +export class StableWSConnection { // global from constructor - client: StreamChat; + client: StreamChat; // local vars connectionID?: string; - connectionOpen?: ConnectAPIResponse; + connectionOpen?: ConnectAPIResponse; consecutiveFailures: number; pingInterval: number; healthCheckTimeoutRef?: NodeJS.Timeout; @@ -57,12 +58,12 @@ export class StableWSConnection void; requestID: string | undefined; - resolvePromise?: (value: ConnectionOpen) => void; + resolvePromise?: (value: ConnectionOpen) => void; totalFailures: number; ws?: WebSocket; wsID: number; - constructor({ client }: { client: StreamChat }) { + constructor({ client }: { client: StreamChat }) { /** StreamChat client */ this.client = client; /** consecutive failures influence the duration of the timeout */ @@ -92,7 +93,7 @@ export class StableWSConnection) { + setClient(client: StreamChat) { this.client = client; } @@ -118,17 +119,19 @@ export class StableWSConnection { this.isResolved = false; /** a promise that is resolved once ws.open is called */ - this.connectionOpen = new Promise>((resolve, reject) => { + this.connectionOpen = new Promise((resolve, reject) => { this.resolvePromise = resolve; this.rejectPromise = reject; }); diff --git a/src/connection_fallback.ts b/src/connection_fallback.ts index 40e582a1ea..c7e8010fce 100644 --- a/src/connection_fallback.ts +++ b/src/connection_fallback.ts @@ -2,7 +2,7 @@ import axios, { AxiosRequestConfig, CancelTokenSource } from 'axios'; import { StreamChat } from './client'; import { addConnectionEventListeners, removeConnectionEventListeners, retryInterval, sleep } from './utils'; import { isAPIError, isConnectionIDError, isErrorRetryable } from './errors'; -import { ConnectionOpen, Event, UR, ExtendableGenerics, DefaultGenerics, LogLevel } from './types'; +import { ConnectionOpen, Event, UR, LogLevel } from './types'; export enum ConnectionState { Closed = 'CLOSED', @@ -12,14 +12,14 @@ export enum ConnectionState { Init = 'INIT', } -export class WSConnectionFallback { - client: StreamChat; +export class WSConnectionFallback { + client: StreamChat; state: ConnectionState; consecutiveFailures: number; connectionID?: string; cancelToken?: CancelTokenSource; - constructor({ client }: { client: StreamChat }) { + constructor({ client }: { client: StreamChat }) { this.client = client; this.state = ConnectionState.Init; this.consecutiveFailures = 0; @@ -100,7 +100,7 @@ export class WSConnectionFallback[]; + events: Event[]; }>({}, { timeout: 30000 }, true); // 30s => API responds in 20s if there is no event if (data.events?.length) { @@ -151,7 +151,7 @@ export class WSConnectionFallback }>( + const { event } = await this._req<{ event: ConnectionOpen }>( { json: this.client._buildWSPayload() }, { timeout: 8000 }, // 8s reconnect, diff --git a/src/custom_types.ts b/src/custom_types.ts new file mode 100644 index 0000000000..62eb9a3848 --- /dev/null +++ b/src/custom_types.ts @@ -0,0 +1,13 @@ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +export interface CustomAttachmentData {} +export interface CustomChannelData {} +export interface CustomCommandData {} +export interface CustomEventData {} +export interface CustomMemberData {} +export interface CustomMessageData {} +export interface CustomPollOptionData {} +export interface CustomPollData {} +export interface CustomReactionData {} +export interface CustomUserData {} +export interface CustomThreadData {} diff --git a/src/errors.ts b/src/errors.ts index 7eb1c4f280..cdaa2c9d6a 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -31,7 +31,7 @@ export const APIErrorCodes: Record '99': { name: 'AppSuspendedError', retryable: false }, }; -type APIError = Error & { code: number; isWSFailure?: boolean }; +export type APIError = Error & { code: number; isWSFailure?: boolean; StatusCode?: number }; export function isAPIError(error: Error): error is APIError { return (error as APIError).code !== undefined; diff --git a/src/index.ts b/src/index.ts index 742b4c2817..6ce9fb6262 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,4 +21,5 @@ export * from './thread_manager'; export * from './token_manager'; export * from './types'; export * from './channel_manager'; +export * from './custom_types'; export { isOwnUser, chatCodes, logChatPromiseExecution, formatMessage } from './utils'; diff --git a/src/moderation.ts b/src/moderation.ts index d9be41da47..40b0f9fd83 100644 --- a/src/moderation.ts +++ b/src/moderation.ts @@ -1,8 +1,6 @@ import { APIResponse, ModerationConfig, - DefaultGenerics, - ExtendableGenerics, GetConfigResponse, GetUserModerationReportResponse, MuteUserResponse, @@ -31,10 +29,10 @@ export const MODERATION_ENTITY_TYPES = { }; // Moderation class provides all the endpoints related to moderation v2. -export class Moderation { - client: StreamChat; +export class Moderation { + client: StreamChat; - constructor(client: StreamChat) { + constructor(client: StreamChat) { this.client = client; } @@ -104,13 +102,10 @@ export class Moderation & APIResponse>( - this.client.baseURL + '/api/v2/moderation/mute', - { - target_ids: [targetID], - ...options, - }, - ); + return await this.client.post(this.client.baseURL + '/api/v2/moderation/mute', { + target_ids: [targetID], + ...options, + }); } /** @@ -144,7 +139,7 @@ export class Moderation>( + return await this.client.get( this.client.baseURL + `/api/v2/moderation/user_report`, { user_id: userID, diff --git a/src/permissions.ts b/src/permissions.ts index f6b96bf171..4bc5301601 100644 --- a/src/permissions.ts +++ b/src/permissions.ts @@ -42,7 +42,7 @@ export const AllowAll = new Permission('Allow all', MaxPriority, AnyResource, An // deprecated export const DenyAll = new Permission('Deny all', MinPriority, AnyResource, AnyRole, false, Deny); -export type Role = 'admin' | 'user' | 'guest' | 'anonymous' | 'channel_member' | 'channel_moderator' | string; +export type Role = 'admin' | 'user' | 'guest' | 'anonymous' | 'channel_member' | 'channel_moderator' | (string & {}); export const BuiltinRoles = { Admin: 'admin', diff --git a/src/poll.ts b/src/poll.ts index aad89abdea..b75ff05653 100644 --- a/src/poll.ts +++ b/src/poll.ts @@ -1,9 +1,7 @@ import { StateStore } from './store'; import type { StreamChat } from './client'; import type { - DefaultGenerics, Event, - ExtendableGenerics, PartialPollUpdate, PollAnswer, PollData, @@ -16,58 +14,46 @@ import type { VoteSort, } from './types'; -type PollEvent = { +type PollEvent = { cid: string; created_at: string; - poll: PollResponse; + poll: PollResponse; }; -type PollUpdatedEvent = PollEvent & { +type PollUpdatedEvent = PollEvent & { type: 'poll.updated'; }; -type PollClosedEvent = PollEvent & { +type PollClosedEvent = PollEvent & { type: 'poll.closed'; }; -type PollVoteEvent = { +type PollVoteEvent = { cid: string; created_at: string; - poll: PollResponse; - poll_vote: PollVote | PollAnswer; + poll: PollResponse; + poll_vote: PollVote | PollAnswer; }; -type PollVoteCastedEvent = PollVoteEvent & { +type PollVoteCastedEvent = PollVoteEvent & { type: 'poll.vote_casted'; }; -type PollVoteCastedChanged = PollVoteEvent & { +type PollVoteCastedChanged = PollVoteEvent & { type: 'poll.vote_removed'; }; -type PollVoteCastedRemoved = PollVoteEvent & { +type PollVoteCastedRemoved = PollVoteEvent & { type: 'poll.vote_removed'; }; -const isPollUpdatedEvent = ( - e: Event, -): e is PollUpdatedEvent => e.type === 'poll.updated'; -const isPollClosedEventEvent = ( - e: Event, -): e is PollClosedEvent => e.type === 'poll.closed'; -const isPollVoteCastedEvent = ( - e: Event, -): e is PollVoteCastedEvent => e.type === 'poll.vote_casted'; -const isPollVoteChangedEvent = ( - e: Event, -): e is PollVoteCastedChanged => e.type === 'poll.vote_changed'; -const isPollVoteRemovedEvent = ( - e: Event, -): e is PollVoteCastedRemoved => e.type === 'poll.vote_removed'; - -export const isVoteAnswer = ( - vote: PollVote | PollAnswer, -): vote is PollAnswer => !!(vote as PollAnswer)?.answer_text; +const isPollUpdatedEvent = (e: Event): e is PollUpdatedEvent => e.type === 'poll.updated'; +const isPollClosedEventEvent = (e: Event): e is PollClosedEvent => e.type === 'poll.closed'; +const isPollVoteCastedEvent = (e: Event): e is PollVoteCastedEvent => e.type === 'poll.vote_casted'; +const isPollVoteChangedEvent = (e: Event): e is PollVoteCastedChanged => e.type === 'poll.vote_changed'; +const isPollVoteRemovedEvent = (e: Event): e is PollVoteCastedRemoved => e.type === 'poll.vote_removed'; + +export const isVoteAnswer = (vote: PollVote | PollAnswer): vote is PollAnswer => !!(vote as PollAnswer)?.answer_text; export type PollAnswersQueryParams = { filter?: QueryVotesFilters; @@ -83,36 +69,35 @@ export type PollOptionVotesQueryParams = { type OptionId = string; -export type PollState = SCG['pollType'] & - Omit, 'own_votes' | 'id'> & { - lastActivityAt: Date; // todo: would be ideal to get this from the BE - maxVotedOptionIds: OptionId[]; - ownVotesByOptionId: Record>; - ownAnswer?: PollAnswer; // each user can have only one answer - }; +export type PollState = Omit & { + lastActivityAt: Date; // todo: would be ideal to get this from the BE + maxVotedOptionIds: OptionId[]; + ownVotesByOptionId: Record; + ownAnswer?: PollAnswer; // each user can have only one answer +}; -type PollInitOptions = { - client: StreamChat; - poll: PollResponse; +type PollInitOptions = { + client: StreamChat; + poll: PollResponse; }; -export class Poll { - public readonly state: StateStore>; +export class Poll { + public readonly state: StateStore; public id: string; - private client: StreamChat; + private client: StreamChat; private unsubscribeFunctions: Set<() => void> = new Set(); - constructor({ client, poll }: PollInitOptions) { + constructor({ client, poll }: PollInitOptions) { this.client = client; this.id = poll.id; - this.state = new StateStore>(this.getInitialStateFromPollResponse(poll)); + this.state = new StateStore(this.getInitialStateFromPollResponse(poll)); } - private getInitialStateFromPollResponse = (poll: PollInitOptions['poll']) => { + private getInitialStateFromPollResponse = (poll: PollInitOptions['poll']) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { own_votes, id, ...pollResponseForState } = poll; - const { ownAnswer, ownVotes } = own_votes?.reduce<{ ownVotes: PollVote[]; ownAnswer?: PollAnswer }>( + const { ownAnswer, ownVotes } = own_votes?.reduce<{ ownVotes: PollVote[]; ownAnswer?: PollAnswer }>( (acc, voteOrAnswer) => { if (isVoteAnswer(voteOrAnswer)) { acc.ownAnswer = voteOrAnswer; @@ -128,22 +113,22 @@ export class Poll { ...pollResponseForState, lastActivityAt: new Date(), maxVotedOptionIds: getMaxVotedOptionIds( - pollResponseForState.vote_counts_by_option as PollResponse['vote_counts_by_option'], + pollResponseForState.vote_counts_by_option as PollResponse['vote_counts_by_option'], ), ownAnswer, ownVotesByOptionId: getOwnVotesByOptionId(ownVotes), }; }; - public reinitializeState = (poll: PollInitOptions['poll']) => { + public reinitializeState = (poll: PollInitOptions['poll']) => { this.state.partialNext(this.getInitialStateFromPollResponse(poll)); }; - get data(): PollState { + get data(): PollState { return this.state.getLatestValue(); } - public handlePollUpdated = (event: Event) => { + public handlePollUpdated = (event: Event) => { if (event.poll?.id && event.poll.id !== this.id) return; if (!isPollUpdatedEvent(event)) return; // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -152,14 +137,14 @@ export class Poll { this.state.partialNext({ ...pollData, lastActivityAt: new Date(event.created_at) }); }; - public handlePollClosed = (event: Event) => { + public handlePollClosed = (event: Event) => { if (event.poll?.id && event.poll.id !== this.id) return; if (!isPollClosedEventEvent(event)) return; // @ts-ignore this.state.partialNext({ is_closed: true, lastActivityAt: new Date(event.created_at) }); }; - public handleVoteCasted = (event: Event) => { + public handleVoteCasted = (event: Event) => { if (event.poll?.id && event.poll.id !== this.id) return; if (!isPollVoteCastedEvent(event)) return; const currentState = this.data; @@ -195,7 +180,7 @@ export class Poll { }); }; - public handleVoteChanged = (event: Event) => { + public handleVoteChanged = (event: Event) => { // this event is triggered only when event.poll.enforce_unique_vote === true if (event.poll?.id && event.poll.id !== this.id) return; if (!isPollVoteChangedEvent(event)) return; @@ -214,7 +199,7 @@ export class Poll { if (event.poll.enforce_unique_votes) { ownVotesByOptionId = { [event.poll_vote.option_id]: event.poll_vote }; } else { - ownVotesByOptionId = Object.entries(ownVotesByOptionId).reduce>>( + ownVotesByOptionId = Object.entries(ownVotesByOptionId).reduce>( (acc, [optionId, vote]) => { if (optionId !== event.poll_vote.option_id && vote.id === event.poll_vote.id) { return acc; @@ -250,7 +235,7 @@ export class Poll { }); }; - public handleVoteRemoved = (event: Event) => { + public handleVoteRemoved = (event: Event) => { if (event.poll?.id && event.poll.id !== this.id) return; if (!isPollVoteRemovedEvent(event)) return; const currentState = this.data; @@ -291,11 +276,11 @@ export class Poll { return poll; }; - update = async (data: Exclude, 'id'>) => { + update = async (data: Exclude) => { return await this.client.updatePoll({ ...data, id: this.id }); }; - partialUpdate = async (partialPollObject: PartialPollUpdate) => { + partialUpdate = async (partialPollObject: PartialPollUpdate) => { return await this.client.partialUpdatePoll(this.id as string, partialPollObject); }; @@ -375,19 +360,17 @@ function getMaxVotedOptionIds(voteCountsByOption: PollResponse['vote_counts_by_o return winningOptions; } -function getOwnVotesByOptionId(ownVotes: PollVote[]) { +function getOwnVotesByOptionId(ownVotes: PollVote[]) { return !ownVotes - ? ({} as Record>) - : ownVotes.reduce>>((acc, vote) => { + ? ({} as Record) + : ownVotes.reduce>((acc, vote) => { if (isVoteAnswer(vote) || !vote.option_id) return acc; acc[vote.option_id] = vote; return acc; }, {}); } -export function extractPollData( - pollResponse: PollResponse, -): PollData { +export function extractPollData(pollResponse: PollResponse): PollData { return { allow_answers: pollResponse.allow_answers, allow_user_suggested_options: pollResponse.allow_user_suggested_options, @@ -402,9 +385,9 @@ export function extractPollData( - pollResponse: PollResponse, -): Omit, 'own_votes' | 'latest_answers'> { +export function extractPollEnrichedData( + pollResponse: PollResponse, +): Omit { return { answers_count: pollResponse.answers_count, latest_votes_by_option: pollResponse.latest_votes_by_option, diff --git a/src/poll_manager.ts b/src/poll_manager.ts index 58f08b55df..d81f8ee216 100644 --- a/src/poll_manager.ts +++ b/src/poll_manager.ts @@ -1,8 +1,6 @@ import type { StreamChat } from './client'; import type { CreatePollData, - DefaultGenerics, - ExtendableGenerics, MessageResponse, PollResponse, PollSort, @@ -13,21 +11,21 @@ import { Poll } from './poll'; import { FormatMessageResponse } from './types'; import { formatMessage } from './utils'; -export class PollManager { - private client: StreamChat; +export class PollManager { + private client: StreamChat; // The pollCache contains only polls that have been created and sent as messages // (i.e only polls that are coupled with a message, can be voted on and require a // reactive state). It shall work as a basic look-up table for our SDK to be able // to quickly consume poll state that will be reactive even without the polls being // rendered within the UI. - private pollCache = new Map>(); + private pollCache = new Map(); private unsubscribeFunctions: Set<() => void> = new Set(); - constructor({ client }: { client: StreamChat }) { + constructor({ client }: { client: StreamChat }) { this.client = client; } - get data(): Map> { + get data(): Map { return this.pollCache; } @@ -54,7 +52,7 @@ export class PollManager { this.unsubscribeFunctions.clear(); }; - public createPoll = async (poll: CreatePollData) => { + public createPoll = async (poll: CreatePollData) => { const { poll: createdPoll } = await this.client.createPoll(poll); return new Poll({ client: this.client, poll: createdPoll }); @@ -91,26 +89,23 @@ export class PollManager { }; }; - public hydratePollCache = ( - messages: FormatMessageResponse[] | MessageResponse[], - overwriteState?: boolean, - ) => { + public hydratePollCache = (messages: FormatMessageResponse[] | MessageResponse[], overwriteState?: boolean) => { for (const message of messages) { if (!message.poll) { continue; } - const pollResponse = message.poll as PollResponse; + const pollResponse = message.poll as PollResponse; this.setOrOverwriteInCache(pollResponse, overwriteState); } }; - private setOrOverwriteInCache = (pollResponse: PollResponse, overwriteState?: boolean) => { + private setOrOverwriteInCache = (pollResponse: PollResponse, overwriteState?: boolean) => { if (!this.client._cacheEnabled()) { return; } const pollFromCache = this.fromState(pollResponse.id); if (!pollFromCache) { - const poll = new Poll({ client: this.client, poll: pollResponse }); + const poll = new Poll({ client: this.client, poll: pollResponse }); this.pollCache.set(poll.id, poll); } else if (overwriteState) { pollFromCache.reinitializeState(pollResponse); diff --git a/src/search_controller.ts b/src/search_controller.ts index 584ab13ee0..1f14e469c7 100644 --- a/src/search_controller.ts +++ b/src/search_controller.ts @@ -6,8 +6,6 @@ import type { ChannelFilters, ChannelOptions, ChannelSort, - DefaultGenerics, - ExtendableGenerics, MessageFilters, MessageResponse, SearchMessageSort, @@ -203,16 +201,14 @@ export abstract class BaseSearchSource implements SearchSource { } } -export class UserSearchSource extends BaseSearchSource< - UserResponse -> { +export class UserSearchSource extends BaseSearchSource { readonly type = 'users'; - private client: StreamChat; - filters: UserFilters | undefined; - sort: UserSort | undefined; + private client: StreamChat; + filters: UserFilters | undefined; + sort: UserSort | undefined; searchOptions: Omit | undefined; - constructor(client: StreamChat, options?: SearchSourceOptions) { + constructor(client: StreamChat, options?: SearchSourceOptions) { super(options); this.client = client; } @@ -221,28 +217,26 @@ export class UserSearchSource; - const sort = { id: 1, ...this.sort } as UserSort; + } as UserFilters; + const sort = { id: 1, ...this.sort } as UserSort; const options = { ...this.searchOptions, limit: this.pageSize, offset: this.offset }; const { users } = await this.client.queryUsers(filters, sort, options); return { items: users }; } - protected filterQueryResults(items: UserResponse[]) { + protected filterQueryResults(items: UserResponse[]) { return items.filter((u) => u.id !== this.client.user?.id); } } -export class ChannelSearchSource< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> extends BaseSearchSource> { +export class ChannelSearchSource extends BaseSearchSource { readonly type = 'channels'; - private client: StreamChat; - filters: ChannelFilters | undefined; - sort: ChannelSort | undefined; + private client: StreamChat; + filters: ChannelFilters | undefined; + sort: ChannelSort | undefined; searchOptions: Omit | undefined; - constructor(client: StreamChat, options?: SearchSourceOptions) { + constructor(client: StreamChat, options?: SearchSourceOptions) { super(options); this.client = client; } @@ -252,31 +246,29 @@ export class ChannelSearchSource< members: { $in: [this.client.userID] }, name: { $autocomplete: searchQuery }, ...this.filters, - } as ChannelFilters; + } as ChannelFilters; const sort = this.sort ?? {}; const options = { ...this.searchOptions, limit: this.pageSize, offset: this.offset }; const items = await this.client.queryChannels(filters, sort, options); return { items }; } - protected filterQueryResults(items: Channel[]) { + protected filterQueryResults(items: Channel[]) { return items; } } -export class MessageSearchSource< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> extends BaseSearchSource> { +export class MessageSearchSource extends BaseSearchSource { readonly type = 'messages'; - private client: StreamChat; - messageSearchChannelFilters: ChannelFilters | undefined; - messageSearchFilters: MessageFilters | undefined; - messageSearchSort: SearchMessageSort | undefined; - channelQueryFilters: ChannelFilters | undefined; - channelQuerySort: ChannelSort | undefined; + private client: StreamChat; + messageSearchChannelFilters: ChannelFilters | undefined; + messageSearchFilters: MessageFilters | undefined; + messageSearchSort: SearchMessageSort | undefined; + channelQueryFilters: ChannelFilters | undefined; + channelQuerySort: ChannelSort | undefined; channelQueryOptions: Omit | undefined; - constructor(client: StreamChat, options?: SearchSourceOptions) { + constructor(client: StreamChat, options?: SearchSourceOptions) { super(options); this.client = client; } @@ -284,18 +276,18 @@ export class MessageSearchSource< protected async query(searchQuery: string) { if (!this.client.userID) return { items: [] }; - const channelFilters: ChannelFilters = { + const channelFilters: ChannelFilters = { members: { $in: [this.client.userID] }, ...this.messageSearchChannelFilters, - } as ChannelFilters; + } as ChannelFilters; - const messageFilters: MessageFilters = { + const messageFilters: MessageFilters = { text: searchQuery, type: 'regular', // FIXME: type: 'reply' resp. do not filter by type and allow to jump to a message in a thread - missing support ...this.messageSearchFilters, - } as MessageFilters; + } as MessageFilters; - const sort: SearchMessageSort = { + const sort: SearchMessageSort = { created_at: -1, ...this.messageSearchSort, }; @@ -304,7 +296,7 @@ export class MessageSearchSource< limit: this.pageSize, next: this.next, sort, - } as SearchOptions; + } as SearchOptions; const { next, results } = await this.client.search(channelFilters, messageFilters, options); const items = results.map(({ message }) => message); @@ -321,7 +313,7 @@ export class MessageSearchSource< { cid: { $in: cids }, ...this.channelQueryFilters, - } as ChannelFilters, + } as ChannelFilters, { last_message_at: -1, ...this.channelQuerySort, @@ -333,16 +325,12 @@ export class MessageSearchSource< return { items, next }; } - protected filterQueryResults(items: MessageResponse[]) { + protected filterQueryResults(items: MessageResponse[]) { return items; } } -export type DefaultSearchSources = [ - UserSearchSource, - ChannelSearchSource, - MessageSearchSource, -]; +export type DefaultSearchSources = [UserSearchSource, ChannelSearchSource, MessageSearchSource]; export type SearchControllerState = { isActive: boolean; @@ -350,10 +338,10 @@ export type SearchControllerState = { sources: SearchSource[]; }; -export type InternalSearchControllerState = { +export type InternalSearchControllerState = { // FIXME: focusedMessage should live in a MessageListController class that does not exist yet. // This state prop should be then removed - focusedMessage?: MessageResponse; + focusedMessage?: MessageResponse; }; export type SearchControllerConfig = { @@ -366,12 +354,12 @@ export type SearchControllerOptions = { sources?: SearchSource[]; }; -export class SearchController { +export class SearchController { /** * Not intended for direct use by integrators, might be removed without notice resulting in * broken integrations. */ - _internalState: StateStore>; + _internalState: StateStore; state: StateStore; config: SearchControllerConfig; @@ -381,7 +369,7 @@ export class SearchController>({}); + this._internalState = new StateStore({}); this.config = { keepSingleActiveSource: true, ...config }; } get hasNext() { diff --git a/src/segment.ts b/src/segment.ts index 748d32f4e7..2f60985c80 100644 --- a/src/segment.ts +++ b/src/segment.ts @@ -1,12 +1,5 @@ import { StreamChat } from './client'; -import { - DefaultGenerics, - ExtendableGenerics, - QuerySegmentTargetsFilter, - SegmentData, - SegmentResponse, - SortParam, -} from './types'; +import { QuerySegmentTargetsFilter, SegmentData, SegmentResponse, SortParam } from './types'; type SegmentType = 'user' | 'channel'; @@ -16,13 +9,13 @@ type SegmentUpdatableFields = { name?: string; }; -export class Segment { +export class Segment { type: SegmentType; id: string | null; - client: StreamChat; + client: StreamChat; data?: SegmentData | SegmentResponse; - constructor(client: StreamChat, type: SegmentType, id: string | null, data?: SegmentData) { + constructor(client: StreamChat, type: SegmentType, id: string | null, data?: SegmentData) { this.client = client; this.type = type; this.id = id; diff --git a/src/thread.ts b/src/thread.ts index 0a88ac42df..5ed74ddd34 100644 --- a/src/thread.ts +++ b/src/thread.ts @@ -1,34 +1,32 @@ -import type { Channel } from './channel'; -import type { StreamChat } from './client'; import { StateStore } from './store'; +import { addToMessageList, findIndexInSortedArray, formatMessage, throttle } from './utils'; import type { AscDesc, - DefaultGenerics, EventTypes, - ExtendableGenerics, FormatMessageResponse, MessagePaginationOptions, MessageResponse, ReadResponse, ThreadResponse, - ThreadResponseCustomData, UserResponse, } from './types'; -import { addToMessageList, findIndexInSortedArray, formatMessage, throttle } from './utils'; +import type { Channel } from './channel'; +import type { StreamChat } from './client'; +import type { CustomThreadData } from './custom_types'; -type QueryRepliesOptions = { +type QueryRepliesOptions = { sort?: { created_at: AscDesc }[]; -} & MessagePaginationOptions & { user?: UserResponse; user_id?: string }; +} & MessagePaginationOptions & { user?: UserResponse; user_id?: string }; -export type ThreadState = { +export type ThreadState = { /** * Determines if the thread is currently opened and on-screen. When the thread is active, * all new messages are immediately marked as read. */ active: boolean; - channel: Channel; + channel: Channel; createdAt: Date; - custom: ThreadResponseCustomData; + custom: CustomThreadData; deletedAt: Date | null; isLoading: boolean; isStateStale: boolean; @@ -37,10 +35,10 @@ export type ThreadState = { * Thread is identified by and has a one-to-one relation with its parent message. * We use parent message id as a thread id. */ - parentMessage: FormatMessageResponse; - participants: ThreadResponse['thread_participants']; + parentMessage: FormatMessageResponse; + participants: ThreadResponse['thread_participants']; read: ThreadReadState; - replies: Array>; + replies: Array; replyCount: number; title: string; updatedAt: Date | null; @@ -53,17 +51,14 @@ export type ThreadRepliesPagination = { prevCursor: string | null; }; -export type ThreadUserReadState = { +export type ThreadUserReadState = { lastReadAt: Date; unreadMessageCount: number; - user: UserResponse; + user: UserResponse; lastReadMessageId?: string; }; -export type ThreadReadState = Record< - string, - ThreadUserReadState | undefined ->; +export type ThreadReadState = Record; const DEFAULT_PAGE_LIMIT = 50; const DEFAULT_SORT: { created_at: AscDesc }[] = [{ created_at: -1 }]; @@ -91,14 +86,14 @@ export const THREAD_RESPONSE_RESERVED_KEYS: Record = // TODO: remove this once we move to API v2 const constructCustomDataObject = (threadData: T) => { - const custom: ThreadResponseCustomData = {}; + const custom: CustomThreadData = {}; for (const key in threadData) { if (THREAD_RESPONSE_RESERVED_KEYS[key as keyof ThreadResponse]) { continue; } - const customKey = key as keyof ThreadResponseCustomData; + const customKey = key as keyof CustomThreadData; custom[customKey] = threadData[customKey]; } @@ -106,15 +101,15 @@ const constructCustomDataObject = (threadData: T) => { return custom; }; -export class Thread { - public readonly state: StateStore>; +export class Thread { + public readonly state: StateStore; public readonly id: string; - private client: StreamChat; + private client: StreamChat; private unsubscribeFunctions: Set<() => void> = new Set(); - private failedRepliesMap: Map> = new Map(); + private failedRepliesMap: Map = new Map(); - constructor({ client, threadData }: { client: StreamChat; threadData: ThreadResponse }) { + constructor({ client, threadData }: { client: StreamChat; threadData: ThreadResponse }) { const channel = client.channel(threadData.channel.type, threadData.channel.id, { name: threadData.channel.name, }); @@ -126,7 +121,7 @@ export class Thread { ? [{ user: { id: client.userID }, unread_messages: 0, last_read: new Date().toISOString() }] : []; - this.state = new StateStore>({ + this.state = new StateStore({ // local only active: false, isLoading: false, @@ -188,7 +183,7 @@ export class Thread { } }; - public hydrateState = (thread: Thread) => { + public hydrateState = (thread: Thread) => { if (thread === this) { // skip if the instances are the same return; @@ -410,7 +405,7 @@ export class Thread { this.unsubscribeFunctions.clear(); }; - public deleteReplyLocally = ({ message }: { message: MessageResponse }) => { + public deleteReplyLocally = ({ message }: { message: MessageResponse }) => { const { replies } = this.state.getLatestValue(); const index = findIndexInSortedArray({ @@ -437,7 +432,7 @@ export class Thread { message, timestampChanged = false, }: { - message: MessageResponse; + message: MessageResponse; timestampChanged?: boolean; }) => { if (message.parent_id !== this.id) { @@ -459,7 +454,7 @@ export class Thread { })); }; - public updateParentMessageLocally = ({ message }: { message: MessageResponse }) => { + public updateParentMessageLocally = ({ message }: { message: MessageResponse }) => { if (message.id !== this.id) { throw new Error('Message does not belong to this thread'); } @@ -476,7 +471,7 @@ export class Thread { }); }; - public updateParentMessageOrReplyLocally = (message: MessageResponse) => { + public updateParentMessageOrReplyLocally = (message: MessageResponse) => { if (message.parent_id === this.id) { this.upsertReplyLocally({ message }); } @@ -500,7 +495,7 @@ export class Thread { limit = DEFAULT_PAGE_LIMIT, sort = DEFAULT_SORT, ...otherOptions - }: QueryRepliesOptions = {}) => { + }: QueryRepliesOptions = {}) => { return this.channel.getReplies(this.id, { limit, ...otherOptions }, sort); }; @@ -585,8 +580,5 @@ const repliesPaginationFromInitialThread = (thread: ThreadResponse): ThreadRepli }; }; -const ownUnreadCountSelector = (currentUserId: string | undefined) => < - SCG extends ExtendableGenerics = DefaultGenerics ->( - state: ThreadState, -) => (currentUserId && state.read[currentUserId]?.unreadMessageCount) || 0; +const ownUnreadCountSelector = (currentUserId: string | undefined) => (state: ThreadState) => + (currentUserId && state.read[currentUserId]?.unreadMessageCount) || 0; diff --git a/src/thread_manager.ts b/src/thread_manager.ts index 878d2b63ae..4a4568ba0b 100644 --- a/src/thread_manager.ts +++ b/src/thread_manager.ts @@ -3,7 +3,7 @@ import { throttle } from './utils'; import type { StreamChat } from './client'; import type { Thread } from './thread'; -import type { DefaultGenerics, Event, ExtendableGenerics, OwnUserResponse, QueryThreadsOptions } from './types'; +import type { Event, OwnUserResponse, QueryThreadsOptions } from './types'; const DEFAULT_CONNECTION_RECOVERY_THROTTLE_DURATION = 1000; const MAX_QUERY_THREADS_LIMIT = 25; @@ -22,13 +22,13 @@ export const THREAD_MANAGER_INITIAL_STATE = { ready: false, }; -export type ThreadManagerState = { +export type ThreadManagerState = { active: boolean; isThreadOrderStale: boolean; lastConnectionDropAt: Date | null; pagination: ThreadManagerPagination; ready: boolean; - threads: Thread[]; + threads: Thread[]; unreadThreadCount: number; /** * List of threads that haven't been loaded in the list, but have received new messages @@ -43,18 +43,18 @@ export type ThreadManagerPagination = { nextCursor: string | null; }; -export class ThreadManager { - public readonly state: StateStore>; - private client: StreamChat; +export class ThreadManager { + public readonly state: StateStore; + private client: StreamChat; private unsubscribeFunctions: Set<() => void> = new Set(); private threadsByIdGetterCache: { - threads: ThreadManagerState['threads']; - threadsById: Record | undefined>; + threads: ThreadManagerState['threads']; + threadsById: Record; }; - constructor({ client }: { client: StreamChat }) { + constructor({ client }: { client: StreamChat }) { this.client = client; - this.state = new StateStore>(THREAD_MANAGER_INITIAL_STATE); + this.state = new StateStore(THREAD_MANAGER_INITIAL_STATE); this.threadsByIdGetterCache = { threads: [], threadsById: {} }; } @@ -66,7 +66,7 @@ export class ThreadManager { return this.threadsByIdGetterCache.threadsById; } - const threadsById = threads.reduce>>((newThreadsById, thread) => { + const threadsById = threads.reduce>((newThreadsById, thread) => { newThreadsById[thread.id] = thread; return newThreadsById; }, {}); @@ -102,7 +102,7 @@ export class ThreadManager { private subscribeUnreadThreadsCountChange = () => { // initiate - const { unread_threads: unreadThreadCount = 0 } = (this.client.user as OwnUserResponse) ?? {}; + const { unread_threads: unreadThreadCount = 0 } = (this.client.user as OwnUserResponse) ?? {}; this.state.partialNext({ unreadThreadCount }); const unsubscribeFunctions = [ @@ -155,7 +155,7 @@ export class ThreadManager { ); private subscribeNewReplies = () => - this.client.on('notification.thread_message_new', (event: Event) => { + this.client.on('notification.thread_message_new', (event: Event) => { const parentId = event.message?.parent_id; if (!parentId) return; @@ -228,7 +228,7 @@ export class ThreadManager { }); const currentThreads = this.threadsById; - const nextThreads: Thread[] = []; + const nextThreads: Thread[] = []; for (const incomingThread of response.threads) { const existingThread = currentThreads[incomingThread.id]; diff --git a/src/token_manager.ts b/src/token_manager.ts index 5e8ca6bb1e..d0775de165 100644 --- a/src/token_manager.ts +++ b/src/token_manager.ts @@ -2,20 +2,20 @@ import jwt from 'jsonwebtoken'; import { UserFromToken, JWTServerToken, JWTUserToken } from './signing'; import { isFunction } from './utils'; -import type { TokenOrProvider, ExtendableGenerics, DefaultGenerics, UserResponse } from './types'; +import type { TokenOrProvider, UserResponse } from './types'; /** * TokenManager * * Handles all the operations around user token. */ -export class TokenManager { +export class TokenManager { loadTokenPromise: Promise | null; type: 'static' | 'provider'; secret?: jwt.Secret; token?: string; tokenProvider?: TokenOrProvider; - user?: UserResponse; + user?: UserResponse; /** * Constructor * @@ -39,9 +39,9 @@ export class TokenManager} user + * @param {UserResponse} user */ - setTokenOrProvider = async (tokenOrProvider: TokenOrProvider, user: UserResponse) => { + setTokenOrProvider = async (tokenOrProvider: TokenOrProvider, user: UserResponse) => { this.validateToken(tokenOrProvider, user); this.user = user; @@ -76,7 +76,7 @@ export class TokenManager) => { + validateToken = (tokenOrProvider: TokenOrProvider, user: UserResponse) => { // allow empty token for anon user if (user && user.anon && !tokenOrProvider) return; diff --git a/src/types.ts b/src/types.ts index e80cef3dd5..200e4d17a7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,13 +1,30 @@ -import { AxiosRequestConfig, AxiosResponse } from 'axios'; -import { StableWSConnection } from './connection'; import { EVENT_MAP } from './events'; -import { Role } from './permissions'; import type { Channel } from './channel'; +import type { AxiosRequestConfig, AxiosResponse } from 'axios'; +import type { StableWSConnection } from './connection'; +import type { Role } from './permissions'; +import type { + CustomChannelData, + CustomMemberData, + CustomThreadData, + CustomEventData, + CustomMessageData, + CustomUserData, + CustomReactionData, + CustomAttachmentData, + CustomCommandData, + CustomPollData, + CustomPollOptionData, +} from './custom_types'; /** * Utility Types */ +export type Readable = { + [key in keyof T]: T[key]; +} & {}; + export type ArrayOneOrMore = { 0: T; } & Array; @@ -27,40 +44,14 @@ export type RequireAtLeastOne = { [K in keyof T]-?: Required> & Partial>>; }[keyof T]; -export type RequireOnlyOne = Pick> & +export type RequireOnlyOne = Omit & { - [K in Keys]-?: Required> & Partial, undefined>>; + [K in Keys]-?: Required> & Partial>; }[Keys]; /* Unknown Record */ export type UR = Record; -export type UnknownType = UR; //alias to avoid breaking change - -export type DefaultGenerics = { - attachmentType: UR; - channelType: UR; - commandType: LiteralStringForUnion; - eventType: UR; - memberType: UR; - messageType: UR; - pollOptionType: UR; - pollType: UR; - reactionType: UR; - userType: UR; -}; - -export type ExtendableGenerics = { - attachmentType: UR; - channelType: UR; - commandType: string; - eventType: UR; - memberType: UR; - messageType: UR; - pollOptionType: UR; - pollType: UR; - reactionType: UR; - userType: UR; -}; +export type UnknownType = UR; // alias to avoid breaking change export type Unpacked = T extends (infer U)[] ? U // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -83,7 +74,7 @@ export type TranslateResponse = { translated_text: string; }; -export type AppSettingsAPIResponse = APIResponse & { +export type AppSettingsAPIResponse = APIResponse & { app?: { // TODO // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -96,7 +87,7 @@ export type AppSettingsAPIResponse[]; + commands?: CommandVariants[]; connect_events?: boolean; created_at?: string; custom_events?: boolean; @@ -201,62 +192,62 @@ export type FlagDetails = { automod?: AutomodDetails; }; -export type Flag = { +export type Flag = { created_at: string; created_by_automod: boolean; updated_at: string; details?: FlagDetails; - target_message?: MessageResponse; - target_user?: UserResponse; - user?: UserResponse; + target_message?: MessageResponse; + target_user?: UserResponse; + user?: UserResponse; }; -export type FlagsResponse = APIResponse & { - flags?: Array>; +export type FlagsResponse = APIResponse & { + flags?: Array; }; -export type MessageFlagsResponse = APIResponse & { +export type MessageFlagsResponse = APIResponse & { flags?: Array<{ - message: MessageResponse; - user: UserResponse; + message: MessageResponse; + user: UserResponse; approved_at?: string; created_at?: string; created_by_automod?: boolean; moderation_result?: ModerationResult; rejected_at?: string; reviewed_at?: string; - reviewed_by?: UserResponse; + reviewed_by?: UserResponse; updated_at?: string; }>; }; -export type FlagReport = { +export type FlagReport = { flags_count: number; id: string; - message: MessageResponse; - user: UserResponse; + message: MessageResponse; + user: UserResponse; created_at?: string; details?: FlagDetails; - first_reporter?: UserResponse; + first_reporter?: UserResponse; review_result?: string; reviewed_at?: string; - reviewed_by?: UserResponse; + reviewed_by?: UserResponse; updated_at?: string; }; -export type FlagReportsResponse = APIResponse & { - flag_reports: Array>; +export type FlagReportsResponse = APIResponse & { + flag_reports: Array; }; -export type ReviewFlagReportResponse = APIResponse & { - flag_report: FlagReport; +export type ReviewFlagReportResponse = APIResponse & { + flag_report: FlagReport; }; -export type BannedUsersResponse = APIResponse & { +export type BannedUsersResponse = APIResponse & { bans?: Array<{ - user: UserResponse; - banned_by?: UserResponse; - channel?: ChannelResponse; + user: UserResponse; + banned_by?: UserResponse; + channel?: ChannelResponse; expires?: string; ip_ban?: boolean; reason?: string; @@ -270,9 +261,7 @@ export type BlockListResponse = BlockList & { updated_at?: string; }; -export type ChannelResponse< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = StreamChatGenerics['channelType'] & { +export type ChannelResponse = CustomChannelData & { cid: string; disabled: boolean; frozen: boolean; @@ -280,10 +269,10 @@ export type ChannelResponse< type: string; auto_translation_enabled?: boolean; auto_translation_language?: TranslationLanguages | ''; - config?: ChannelConfigWithInfo; + config?: ChannelConfigWithInfo; cooldown?: number; created_at?: string; - created_by?: UserResponse | null; + created_by?: UserResponse | null; created_by_id?: string; deleted_at?: string; hidden?: boolean; @@ -291,44 +280,43 @@ export type ChannelResponse< joined?: boolean; last_message_at?: string; member_count?: number; - members?: ChannelMemberResponse[]; + members?: ChannelMemberResponse[]; muted?: boolean; - name?: string; + name?: string; // FIXME: I believe this property should live in CustomChannelData own_capabilities?: string[]; team?: string; truncated_at?: string; - truncated_by?: UserResponse; + truncated_by?: UserResponse; truncated_by_id?: string; updated_at?: string; }; export type QueryReactionsOptions = Pager; -export type QueryReactionsAPIResponse = APIResponse & { - reactions: ReactionResponse[]; +export type QueryReactionsAPIResponse = APIResponse & { + reactions: ReactionResponse[]; next?: string; }; -export type QueryChannelsAPIResponse = APIResponse & { - channels: Omit, keyof APIResponse>[]; +export type QueryChannelsAPIResponse = APIResponse & { + channels: Omit[]; }; -export type QueryChannelAPIResponse = APIResponse & - ChannelAPIResponse; +export type QueryChannelAPIResponse = APIResponse & ChannelAPIResponse; -export type ChannelAPIResponse = { - channel: ChannelResponse; - members: ChannelMemberResponse[]; - messages: MessageResponse[]; - pinned_messages: MessageResponse[]; +export type ChannelAPIResponse = { + channel: ChannelResponse; + members: ChannelMemberResponse[]; + messages: MessageResponse[]; + pinned_messages: MessageResponse[]; hidden?: boolean; - membership?: ChannelMemberResponse | null; - pending_messages?: PendingMessageResponse[]; + membership?: ChannelMemberResponse | null; + pending_messages?: PendingMessageResponse[]; push_preferences?: PushPreference; - read?: ReadResponse[]; + read?: ReadResponse[]; threads?: ThreadResponse[]; watcher_count?: number; - watchers?: UserResponse[]; + watchers?: UserResponse[]; }; export type ChannelUpdateOptions = { @@ -336,21 +324,17 @@ export type ChannelUpdateOptions = { skip_push?: boolean; }; -export type ChannelMemberAPIResponse = APIResponse & { - members: ChannelMemberResponse[]; +export type ChannelMemberAPIResponse = APIResponse & { + members: ChannelMemberResponse[]; }; -export type ChannelMemberUpdates< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = StreamChatGenerics['memberType'] & { +export type ChannelMemberUpdates = CustomMemberData & { archived?: boolean; channel_role?: Role; pinned?: boolean; }; -export type ChannelMemberResponse< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = StreamChatGenerics['memberType'] & { +export type ChannelMemberResponse = CustomMemberData & { archived_at?: string; ban_expires?: string; banned?: boolean; @@ -366,14 +350,12 @@ export type ChannelMemberResponse< shadow_banned?: boolean; status?: InviteStatus; updated_at?: string; - user?: UserResponse; + user?: UserResponse; user_id?: string; }; -export type PartialUpdateMemberAPIResponse< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = APIResponse & { - channel_member: ChannelMemberResponse; +export type PartialUpdateMemberAPIResponse = APIResponse & { + channel_member: ChannelMemberResponse; }; export type CheckPushResponse = APIResponse & { @@ -403,40 +385,36 @@ export type CheckSNSResponse = APIResponse & { error?: string; }; -export type CommandResponse< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = Partial & { +export type CommandResponse = Partial & { args?: string; description?: string; - name?: CommandVariants; - set?: CommandVariants; + name?: CommandVariants; + set?: CommandVariants; }; -export type ConnectAPIResponse< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = Promise>; +export type ConnectAPIResponse = Promise; -export type CreateChannelResponse = APIResponse & - Omit, 'client_id' | 'connection_id'> & { +export type CreateChannelResponse = APIResponse & + Omit & { created_at: string; updated_at: string; grants?: Record; }; -export type CreateCommandResponse = APIResponse & { - command: CreateCommandOptions & CreatedAtUpdatedAt; +export type CreateCommandResponse = APIResponse & { + command: CreateCommandOptions & CreatedAtUpdatedAt; }; -export type DeleteChannelAPIResponse = APIResponse & { - channel: ChannelResponse; +export type DeleteChannelAPIResponse = APIResponse & { + channel: ChannelResponse; }; -export type DeleteCommandResponse = APIResponse & { - name?: CommandVariants; +export type DeleteCommandResponse = APIResponse & { + name?: CommandVariants; }; -export type EventAPIResponse = APIResponse & { - event: Event; +export type EventAPIResponse = APIResponse & { + event: Event; }; export type ExportChannelResponse = { @@ -454,13 +432,13 @@ export type ExportChannelStatusResponse = { updated_at?: string; }; -export type FlagMessageResponse = APIResponse & { +export type FlagMessageResponse = APIResponse & { flag: { created_at: string; created_by_automod: boolean; target_message_id: string; updated_at: string; - user: UserResponse; + user: UserResponse; approved_at?: string; channel_cid?: string; details?: Object; // Any JSON @@ -472,13 +450,13 @@ export type FlagMessageResponse = APIResponse & { +export type FlagUserResponse = APIResponse & { flag: { created_at: string; created_by_automod: boolean; - target_user: UserResponse; + target_user: UserResponse; updated_at: string; - user: UserResponse; + user: UserResponse; approved_at?: string; details?: Object; // Any JSON rejected_at?: string; @@ -488,65 +466,38 @@ export type FlagUserResponse = Omit< - MessageResponse<{ - attachmentType: StreamChatGenerics['attachmentType']; - channelType: StreamChatGenerics['channelType']; - commandType: StreamChatGenerics['commandType']; - eventType: StreamChatGenerics['eventType']; - memberType: StreamChatGenerics['memberType']; - messageType: {}; - pollOptionType: StreamChatGenerics['pollOptionType']; - pollType: StreamChatGenerics['pollType']; - reactionType: StreamChatGenerics['reactionType']; - userType: StreamChatGenerics['userType']; - }>, +export type FormatMessageResponse = Omit< + MessageResponse, 'created_at' | 'pinned_at' | 'updated_at' | 'deleted_at' | 'status' -> & - StreamChatGenerics['messageType'] & { - created_at: Date; - deleted_at: Date | null; - pinned_at: Date | null; - status: string; - updated_at: Date; - }; - -export type GetChannelTypeResponse = APIResponse & - Omit, 'client_id' | 'connection_id' | 'commands'> & { - created_at: string; - updated_at: string; - commands?: CommandResponse[]; - grants?: Record; - }; - -export type GetCommandResponse = APIResponse & - CreateCommandOptions & - CreatedAtUpdatedAt; +> & { + created_at: Date; + deleted_at: Date | null; + pinned_at: Date | null; + status: string; + updated_at: Date; +}; -export type GetMessageAPIResponse< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = SendMessageAPIResponse; +export type GetCommandResponse = APIResponse & CreateCommandOptions & CreatedAtUpdatedAt; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ThreadResponseCustomData {} +export type GetMessageAPIResponse = SendMessageAPIResponse; -export interface ThreadResponse extends ThreadResponseCustomData { +export interface ThreadResponse extends CustomThreadData { // FIXME: according to OpenAPI, `channel` could be undefined but since cid is provided I'll asume that it's wrong - channel: ChannelResponse; + channel: ChannelResponse; channel_cid: string; created_at: string; created_by_user_id: string; - latest_replies: Array>; - parent_message: MessageResponse; + latest_replies: Array; + parent_message: MessageResponse; parent_message_id: string; title: string; updated_at: string; active_participant_count?: number; - created_by?: UserResponse; + created_by?: UserResponse; deleted_at?: string; last_message_at?: string; participant_count?: number; - read?: Array>; + read?: Array; reply_count?: number; thread_participants?: Array<{ channel_cid: string; @@ -555,11 +506,11 @@ export interface ThreadResponse; + user?: UserResponse; user_id?: string; }>; // TODO: when moving to API v2 we should do this instead - // custom: ThreadResponseCustomData; + // custom: CustomThreadType; } // TODO: Figure out a way to strongly type set and unset. @@ -577,8 +528,8 @@ export type QueryThreadsOptions = { watch?: boolean; }; -export type QueryThreadsAPIResponse = APIResponse & { - threads: ThreadResponse[]; +export type QueryThreadsAPIResponse = APIResponse & { + threads: ThreadResponse[]; next?: string; }; @@ -589,14 +540,12 @@ export type GetThreadOptions = { watch?: boolean; }; -export type GetThreadAPIResponse = APIResponse & { - thread: ThreadResponse; +export type GetThreadAPIResponse = APIResponse & { + thread: ThreadResponse; }; -export type GetMultipleMessagesAPIResponse< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = APIResponse & { - messages: MessageResponse[]; +export type GetMultipleMessagesAPIResponse = APIResponse & { + messages: MessageResponse[]; }; export type GetRateLimitsResponse = APIResponse & { @@ -606,12 +555,12 @@ export type GetRateLimitsResponse = APIResponse & { web?: RateLimitsMap; }; -export type GetReactionsAPIResponse = APIResponse & { - reactions: ReactionResponse[]; +export type GetReactionsAPIResponse = APIResponse & { + reactions: ReactionResponse[]; }; -export type GetRepliesAPIResponse = APIResponse & { - messages: MessageResponse[]; +export type GetRepliesAPIResponse = APIResponse & { + messages: MessageResponse[]; }; export type GetUnreadCountAPIResponse = APIResponse & { @@ -660,11 +609,11 @@ export type GetUnreadCountBatchAPIResponse = APIResponse & { counts_by_user: { [userId: string]: GetUnreadCountAPIResponse }; }; -export type ListChannelResponse = APIResponse & { +export type ListChannelResponse = APIResponse & { channel_types: Record< string, - Omit, 'client_id' | 'connection_id' | 'commands'> & { - commands: CommandResponse[]; + Omit & { + commands: CommandResponse[]; created_at: string; updated_at: string; grants?: Record; @@ -672,34 +621,28 @@ export type ListChannelResponse; }; -export type ListChannelTypesAPIResponse< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = ListChannelResponse; +export type ListChannelTypesAPIResponse = ListChannelResponse; -export type ListCommandsResponse = APIResponse & { - commands: Array & Partial>; +export type ListCommandsResponse = APIResponse & { + commands: Array>; }; -export type MuteChannelAPIResponse = APIResponse & { - channel_mute: ChannelMute; - own_user: OwnUserResponse; - channel_mutes?: ChannelMute[]; - mute?: MuteResponse; +export type MuteChannelAPIResponse = APIResponse & { + channel_mute: ChannelMute; + own_user: OwnUserResponse; + channel_mutes?: ChannelMute[]; + mute?: MuteResponse; }; -export type MessageResponse< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = MessageResponseBase & { - quoted_message?: MessageResponseBase; +export type MessageResponse = MessageResponseBase & { + quoted_message?: MessageResponseBase; }; -export type MessageResponseBase< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = MessageBase & { +export type MessageResponseBase = MessageBase & { type: MessageLabel; args?: string; before_message_send_failed?: boolean; - channel?: ChannelResponse; + channel?: ChannelResponse; cid?: string; command?: string; command_info?: { name?: string }; @@ -709,23 +652,23 @@ export type MessageResponseBase< i18n?: RequireAtLeastOne> & { language: TranslationLanguages; }; - latest_reactions?: ReactionResponse[]; - mentioned_users?: UserResponse[]; + latest_reactions?: ReactionResponse[]; + mentioned_users?: UserResponse[]; message_text_updated_at?: string; moderation?: ModerationResponse; // present only with Moderation v2 moderation_details?: ModerationDetailsResponse; // present only with Moderation v1 - own_reactions?: ReactionResponse[] | null; + own_reactions?: ReactionResponse[] | null; pin_expires?: string | null; pinned_at?: string | null; - pinned_by?: UserResponse | null; - poll?: PollResponse; + pinned_by?: UserResponse | null; + poll?: PollResponse; reaction_counts?: { [key: string]: number } | null; reaction_groups?: { [key: string]: ReactionGroupResponse } | null; reaction_scores?: { [key: string]: number } | null; reply_count?: number; shadowed?: boolean; status?: string; - thread_participants?: UserResponse[]; + thread_participants?: UserResponse[]; updated_at?: string; }; @@ -755,18 +698,18 @@ export type ModerationResponse = { original_text: string; }; -export type MuteResponse = { - user: UserResponse; +export type MuteResponse = { + user: UserResponse; created_at?: string; expires?: string; - target?: UserResponse; + target?: UserResponse; updated_at?: string; }; -export type MuteUserResponse = APIResponse & { - mute?: MuteResponse; - mutes?: Array>; - own_user?: OwnUserResponse; +export type MuteUserResponse = APIResponse & { + mute?: MuteResponse; + mutes?: Array; + own_user?: OwnUserResponse; }; export type BlockUserAPIResponse = APIResponse & { @@ -786,10 +729,10 @@ export type BlockedUserDetails = APIResponse & { user_id: string; }; -export type OwnUserBase = { - channel_mutes: ChannelMute[]; - devices: Device[]; - mutes: Mute[]; +export type OwnUserBase = { + channel_mutes: ChannelMute[]; + devices: Device[]; + mutes: Mute[]; total_unread_count: number; unread_channels: number; unread_count: number; @@ -800,15 +743,11 @@ export type OwnUserBase = UserResponse & OwnUserBase; +export type OwnUserResponse = UserResponse & OwnUserBase; -export type PartialUpdateChannelAPIResponse< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = APIResponse & { - channel: ChannelResponse; - members: ChannelMemberResponse[]; +export type PartialUpdateChannelAPIResponse = APIResponse & { + channel: ChannelResponse; + members: ChannelMemberResponse[]; }; export type PermissionAPIResponse = APIResponse & { @@ -819,29 +758,27 @@ export type PermissionsAPIResponse = APIResponse & { permissions?: PermissionAPIObject[]; }; -export type ReactionAPIResponse = APIResponse & { - message: MessageResponse; - reaction: ReactionResponse; +export type ReactionAPIResponse = APIResponse & { + message: MessageResponse; + reaction: ReactionResponse; }; -export type ReactionResponse< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = Reaction & { +export type ReactionResponse = Reaction & { created_at: string; message_id: string; updated_at: string; }; -export type ReadResponse = { +export type ReadResponse = { last_read: string; - user: UserResponse; + user: UserResponse; last_read_message_id?: string; unread_messages?: number; }; -export type SearchAPIResponse = APIResponse & { +export type SearchAPIResponse = APIResponse & { results: { - message: MessageResponse; + message: MessageResponse; }[]; next?: string; previous?: string; @@ -858,55 +795,53 @@ export type SearchWarning = { // Thumb URL(thumb_url) is added considering video attachments as the backend will return the thumbnail in the response. export type SendFileAPIResponse = APIResponse & { file: string; thumb_url?: string }; -export type SendMessageAPIResponse = APIResponse & { - message: MessageResponse; +export type SendMessageAPIResponse = APIResponse & { + message: MessageResponse; pending_message_metadata?: Record | null; }; -export type SyncResponse = APIResponse & { - events: Event[]; +export type SyncResponse = APIResponse & { + events: Event[]; inaccessible_cids?: string[]; }; -export type TruncateChannelAPIResponse< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = APIResponse & { - channel: ChannelResponse; - message?: MessageResponse; +export type TruncateChannelAPIResponse = APIResponse & { + channel: ChannelResponse; + message?: MessageResponse; }; -export type UpdateChannelAPIResponse = APIResponse & { - channel: ChannelResponse; - members: ChannelMemberResponse[]; - message?: MessageResponse; +export type UpdateChannelAPIResponse = APIResponse & { + channel: ChannelResponse; + members: ChannelMemberResponse[]; + message?: MessageResponse; }; -export type UpdateChannelResponse = APIResponse & - Omit, 'client_id' | 'connection_id'> & { +export type UpdateChannelResponse = APIResponse & + Omit & { created_at: string; updated_at: string; }; -export type UpdateCommandResponse = APIResponse & { - command: UpdateCommandOptions & +export type UpdateCommandResponse = APIResponse & { + command: UpdateCommandOptions & CreatedAtUpdatedAt & { - name: CommandVariants; + name: CommandVariants; }; }; -export type UpdateMessageAPIResponse = APIResponse & { - message: MessageResponse; +export type UpdateMessageAPIResponse = APIResponse & { + message: MessageResponse; }; -export type UsersAPIResponse = APIResponse & { - users: Array>; +export type UsersAPIResponse = APIResponse & { + users: Array; }; -export type UpdateUsersAPIResponse = APIResponse & { - users: { [key: string]: UserResponse }; +export type UpdateUsersAPIResponse = APIResponse & { + users: { [key: string]: UserResponse }; }; -export type UserResponse = User & { +export type UserResponse = User & { banned?: boolean; blocked_user_ids?: string[]; created_at?: string; @@ -914,6 +849,8 @@ export type UserResponse = UnBanUserOptions & { - banned_by?: UserResponse; +export type BanUserOptions = UnBanUserOptions & { + banned_by?: UserResponse; banned_by_id?: string; ip_ban?: boolean; reason?: string; @@ -983,10 +920,12 @@ export type ChannelOptions = { watch?: boolean; }; -export type ChannelQueryOptions = { +export type ChannelQueryOptions = { client_id?: string; connection_id?: string; - data?: ChannelResponse; + created_by?: UserResponse | null; + created_by_id?: UserResponse['id']; + data?: ChannelResponse; hide_for_creator?: boolean; members?: PaginationOptions; messages?: MessagePaginationOptions; @@ -1001,14 +940,14 @@ export type ChannelStateOptions = { skipInitialization?: string[]; }; -export type CreateChannelOptions = { +export type CreateChannelOptions = { automod?: ChannelConfigAutomod; automod_behavior?: ChannelConfigAutomodBehavior; automod_thresholds?: ChannelConfigAutomodThresholds; blocklist?: string; blocklist_behavior?: ChannelConfigAutomodBehavior; client_id?: string; - commands?: CommandVariants[]; + commands?: CommandVariants[]; connect_events?: boolean; connection_id?: string; custom_events?: boolean; @@ -1033,11 +972,11 @@ export type CreateChannelOptions = { +export type CreateCommandOptions = { description: string; - name: CommandVariants; + name: CommandVariants; args?: string; - set?: CommandVariants; + set?: CommandVariants; }; export type CustomPermissionOptions = { @@ -1055,58 +994,200 @@ export type DeactivateUsersOptions = { mark_messages_deleted?: boolean; }; -export type NewMemberPayload< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = StreamChatGenerics['memberType'] & Pick, 'user_id' | 'channel_role'>; +export type NewMemberPayload = CustomMemberData & Pick; -// TODO: rename to UpdateChannelOptions in the next major update and use it in channel._update and/or channel.update -export type InviteOptions = { - accept_invite?: boolean; - add_members?: string[]; - add_moderators?: string[]; - client_id?: string; - connection_id?: string; - data?: Omit, 'id' | 'cid'>; - demote_moderators?: string[]; - invites?: string[]; - message?: MessageResponse; - reject_invite?: boolean; - remove_members?: string[]; - user?: UserResponse; - user_id?: string; +export type Thresholds = Record<'explicit' | 'spam' | 'toxic', Partial<{ block: number; flag: number }>>; + +export type BlockListOptions = { + behavior: BlocklistBehavior; + blocklist: string; +}; + +export type PolicyRequest = { + action: 'Deny' | 'Allow' | (string & {}); + /** + * @description User-friendly policy name + */ + name: string; + /** + * @description Whether policy applies to resource owner or not + */ + owner: boolean; + priority: number; + /** + * @description List of resources to apply policy to + */ + resources: string[]; + /** + * @description List of roles to apply policy to + */ + roles: string[]; +}; + +export type Automod = 'disabled' | 'simple' | 'AI' | (string & {}); +export type AutomodBehavior = 'flag' | 'block' | 'shadow_block' | (string & {}); +export type BlocklistBehavior = AutomodBehavior; +export type Command = { + args: string; + description: string; + name: string; + set: string; + created_at?: string; + updated_at?: string; +}; + +export type UpdateChannelTypeRequest = + // these three properties are required in OpenAPI spec but omitted in some QA tests + Partial<{ + automod: Automod; + automod_behavior: AutomodBehavior; + max_message_length: number; + }> & { + allowed_flag_reasons?: string[]; + automod_thresholds?: Thresholds; + blocklist?: string; + blocklist_behavior?: BlocklistBehavior; + blocklists?: BlockListOptions[]; + commands?: CommandVariants[]; + connect_events?: boolean; + custom_events?: boolean; + grants?: Record; + mark_messages_pending?: boolean; + mutes?: boolean; + partition_size?: number; + /** + * @example 24h + */ + partition_ttl?: string | null; + permissions?: PolicyRequest[]; + polls?: boolean; + push_notifications?: boolean; + quotes?: boolean; + reactions?: boolean; + read_events?: boolean; + reminders?: boolean; + replies?: boolean; + search?: boolean; + skip_last_msg_update_for_system_msgs?: boolean; + typing_events?: boolean; + uploads?: boolean; + url_enrichment?: boolean; + }; + +export type UpdateChannelTypeResponse = { + automod: Automod; + automod_behavior: AutomodBehavior; + commands: CommandVariants[]; + connect_events: boolean; + created_at: string; + custom_events: boolean; + duration: string; + grants: Record; + mark_messages_pending: boolean; + max_message_length: number; + mutes: boolean; + name: string; + permissions: PolicyRequest[]; + polls: boolean; + push_notifications: boolean; + quotes: boolean; + reactions: boolean; + read_events: boolean; + reminders: boolean; + replies: boolean; + search: boolean; + skip_last_msg_update_for_system_msgs: boolean; + typing_events: boolean; + updated_at: string; + uploads: boolean; + url_enrichment: boolean; + allowed_flag_reasons?: string[]; + automod_thresholds?: Thresholds; + blocklist?: string; + blocklist_behavior?: BlocklistBehavior; + blocklists?: BlockListOptions[]; + partition_size?: number; + partition_ttl?: string; +}; + +export type GetChannelTypeResponse = { + automod: Automod; + automod_behavior: AutomodBehavior; + commands: Command[]; + connect_events: boolean; + created_at: string; + custom_events: boolean; + duration: string; + grants: Record; + mark_messages_pending: boolean; + max_message_length: number; + mutes: boolean; + name: string; + permissions: PolicyRequest[]; + polls: boolean; + push_notifications: boolean; + quotes: boolean; + reactions: boolean; + read_events: boolean; + reminders: boolean; + replies: boolean; + search: boolean; + skip_last_msg_update_for_system_msgs: boolean; + typing_events: boolean; + updated_at: string; + uploads: boolean; + url_enrichment: boolean; + allowed_flag_reasons?: string[]; + automod_thresholds?: Thresholds; + blocklist?: string; + blocklist_behavior?: BlocklistBehavior; + blocklists?: BlockListOptions[]; + partition_size?: number; + partition_ttl?: string; }; -/** @deprecated use MarkChannelsReadOptions instead */ -export type MarkAllReadOptions< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = MarkChannelsReadOptions; +export type UpdateChannelOptions = Partial<{ + accept_invite: boolean; + add_members: string[]; + add_moderators: string[]; + client_id: string; + connection_id: string; + data: Omit; + demote_moderators: string[]; + invites: string[]; + message: MessageResponse; + reject_invite: boolean; + remove_members: string[]; + user: UserResponse; + user_id: string; +}>; -export type MarkChannelsReadOptions = { +export type MarkChannelsReadOptions = { client_id?: string; connection_id?: string; read_by_channel?: Record; - user?: UserResponse; + user?: UserResponse; user_id?: string; }; -export type MarkReadOptions = { +export type MarkReadOptions = { client_id?: string; connection_id?: string; thread_id?: string; - user?: UserResponse; + user?: UserResponse; user_id?: string; }; -export type MarkUnreadOptions = { +export type MarkUnreadOptions = { client_id?: string; connection_id?: string; message_id?: string; thread_id?: string; - user?: UserResponse; + user?: UserResponse; user_id?: string; }; -export type MuteUserOptions = { +export type MuteUserOptions = { client_id?: string; connection_id?: string; id?: string; @@ -1114,7 +1195,7 @@ export type MuteUserOptions; + user?: UserResponse; user_id?: string; }; @@ -1185,11 +1266,11 @@ export type ReactivateUsersOptions = { restore_messages?: boolean; }; -export type SearchOptions = { +export type SearchOptions = { limit?: number; next?: string; offset?: number; - sort?: SearchMessageSort; + sort?: SearchMessageSort; }; export type StreamChatOptions = AxiosRequestConfig & { @@ -1265,19 +1346,10 @@ export type UnBanUserOptions = { type?: string; }; -// TODO: rename to UpdateChannelTypeOptions in the next major update -export type UpdateChannelOptions = Omit< - CreateChannelOptions, - 'name' -> & { - created_at?: string; - updated_at?: string; -}; - -export type UpdateCommandOptions = { +export type UpdateCommandOptions = { description: string; args?: string; - set?: CommandVariants; + set?: CommandVariants; }; export type UserOptions = { @@ -1296,11 +1368,11 @@ export type ConnectionChangeEvent = { online?: boolean; }; -export type Event = StreamChatGenerics['eventType'] & { +export type Event = CustomEventData & { type: EventTypes; ai_message?: string; ai_state?: AIState; - channel?: ChannelResponse; + channel?: ChannelResponse; channel_id?: string; channel_type?: string; cid?: string; @@ -1315,23 +1387,25 @@ export type Event; - member?: ChannelMemberResponse; - message?: MessageResponse; + me?: OwnUserResponse; + member?: ChannelMemberResponse; + message?: MessageResponse; message_id?: string; mode?: string; online?: boolean; + own_capabilities?: string[]; parent_id?: string; - poll?: PollResponse; - poll_vote?: PollVote | PollAnswer; + poll?: PollResponse; + poll_vote?: PollVote | PollAnswer; queriedChannels?: { - channels: ChannelAPIResponse[]; + channels: ChannelAPIResponse[]; isLatestMessageSet?: boolean; }; - reaction?: ReactionResponse; + reaction?: ReactionResponse; received_at?: string | Date; + shadow?: boolean; team?: string; - thread?: ThreadResponse; + thread?: ThreadResponse; // @deprecated number of all unread messages across all current user's unread channels, equals unread_count total_unread_count?: number; // number of all current user's channels with at least one unread message including the channel in this event @@ -1342,20 +1416,16 @@ export type Event; + user?: UserResponse; user_id?: string; watcher_count?: number; }; -export type UserCustomEvent< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = StreamChatGenerics['eventType'] & { +export type UserCustomEvent = CustomEventData & { type: string; }; -export type EventHandler = ( - event: Event, -) => void; +export type EventHandler = (event: Event) => void; export type EventTypes = 'all' | keyof typeof EVENT_MAP; @@ -1510,15 +1580,15 @@ export type BannedUsersFilters = QueryFilters< } >; -export type ReactionFilters = QueryFilters< +export type ReactionFilters = QueryFilters< { user_id?: - | RequireOnlyOne['user_id']>, '$eq' | '$in'>> - | PrimitiveFilter['user_id']>; + | RequireOnlyOne, '$eq' | '$in'>> + | PrimitiveFilter; } & { type?: - | RequireOnlyOne['type']>, '$eq'>> - | PrimitiveFilter['type']>; + | RequireOnlyOne, '$eq'>> + | PrimitiveFilter; } & { created_at?: | RequireOnlyOne, '$eq' | '$gt' | '$lt' | '$gte' | '$lte'>> @@ -1526,69 +1596,32 @@ export type ReactionFilters; -export type ChannelFilters = QueryFilters< - ContainsOperator & { +export type ChannelFilters = QueryFilters< + ContainsOperator & { + archived?: boolean; + 'member.user.name'?: + | RequireOnlyOne<{ + $autocomplete?: string; + $eq?: string; + }> + | string; + members?: - | RequireOnlyOne, '$in' | '$nin'>> + | RequireOnlyOne, '$in'>> | RequireOnlyOne, '$eq'>> | PrimitiveFilter; - } & { name?: | RequireOnlyOne< { - $autocomplete?: ChannelResponse['name']; - } & QueryFilter['name']> + $autocomplete?: ChannelResponse['name']; + } & QueryFilter > - | PrimitiveFilter['name']>; + | PrimitiveFilter; + pinned?: boolean; } & { - [Key in keyof Omit< - ChannelResponse<{ - attachmentType: StreamChatGenerics['attachmentType']; - channelType: {}; - commandType: StreamChatGenerics['commandType']; - eventType: StreamChatGenerics['eventType']; - memberType: StreamChatGenerics['memberType']; - messageType: StreamChatGenerics['messageType']; - pollOptionType: StreamChatGenerics['pollOptionType']; - pollType: StreamChatGenerics['pollType']; - reactionType: StreamChatGenerics['reactionType']; - userType: StreamChatGenerics['userType']; - }>, - 'name' | 'members' - >]: - | RequireOnlyOne< - QueryFilter< - ChannelResponse<{ - attachmentType: StreamChatGenerics['attachmentType']; - channelType: {}; - commandType: StreamChatGenerics['commandType']; - eventType: StreamChatGenerics['eventType']; - memberType: StreamChatGenerics['memberType']; - messageType: StreamChatGenerics['messageType']; - pollOptionType: StreamChatGenerics['pollOptionType']; - pollType: StreamChatGenerics['pollType']; - reactionType: StreamChatGenerics['reactionType']; - userType: StreamChatGenerics['userType']; - }>[Key] - > - > - | PrimitiveFilter< - ChannelResponse<{ - attachmentType: StreamChatGenerics['attachmentType']; - channelType: {}; - commandType: StreamChatGenerics['commandType']; - eventType: StreamChatGenerics['eventType']; - memberType: StreamChatGenerics['memberType']; - messageType: StreamChatGenerics['messageType']; - pollOptionType: StreamChatGenerics['pollOptionType']; - pollType: StreamChatGenerics['pollType']; - reactionType: StreamChatGenerics['reactionType']; - userType: StreamChatGenerics['userType']; - }>[Key] - >; - } & { - archived?: boolean; - pinned?: boolean; + [Key in keyof Omit]: + | RequireOnlyOne> + | PrimitiveFilter; } >; @@ -1621,9 +1654,7 @@ export type QueryPollsFilters = QueryFilters< | PrimitiveFilter; } & { max_votes_allowed?: - | RequireOnlyOne< - Pick, '$eq' | '$ne' | '$gt' | '$lt' | '$gte' | '$lte'> - > + | RequireOnlyOne, '$eq' | '$gt' | '$lt' | '$gte' | '$lte'>> | PrimitiveFilter; } & { allow_answers?: @@ -1700,62 +1731,34 @@ export type ContainsOperator = { : RequireOnlyOne> | PrimitiveFilter; }; -export type MessageFilters = QueryFilters< - ContainsOperator & { +export type MessageFilters = QueryFilters< + ContainsOperator & { + 'attachments.type'?: + | RequireOnlyOne<{ + $eq: PrimitiveFilter; + $in: PrimitiveFilter[]; + }> + | PrimitiveFilter; + 'mentioned_users.id'?: RequireOnlyOne<{ $contains: PrimitiveFilter }>; text?: | RequireOnlyOne< { - $autocomplete?: MessageResponse['text']; - $q?: MessageResponse['text']; - } & QueryFilter['text']> + $autocomplete?: MessageResponse['text']; + $q?: MessageResponse['text']; + } & QueryFilter + > + | PrimitiveFilter; + 'user.id'?: + | RequireOnlyOne< + { + $autocomplete?: UserResponse['id']; + } & QueryFilter > - | PrimitiveFilter['text']>; + | PrimitiveFilter; } & { - [Key in keyof Omit< - MessageResponse<{ - attachmentType: StreamChatGenerics['attachmentType']; - channelType: StreamChatGenerics['channelType']; - commandType: StreamChatGenerics['commandType']; - eventType: StreamChatGenerics['eventType']; - memberType: StreamChatGenerics['memberType']; - messageType: {}; - pollOptionType: StreamChatGenerics['pollOptionType']; - pollType: StreamChatGenerics['pollType']; - reactionType: StreamChatGenerics['reactionType']; - userType: StreamChatGenerics['userType']; - }>, - 'text' - >]?: - | RequireOnlyOne< - QueryFilter< - MessageResponse<{ - attachmentType: StreamChatGenerics['attachmentType']; - channelType: StreamChatGenerics['channelType']; - commandType: StreamChatGenerics['commandType']; - eventType: StreamChatGenerics['eventType']; - memberType: StreamChatGenerics['memberType']; - messageType: {}; - pollOptionType: StreamChatGenerics['pollOptionType']; - pollType: StreamChatGenerics['pollType']; - reactionType: StreamChatGenerics['reactionType']; - userType: StreamChatGenerics['userType']; - }>[Key] - > - > - | PrimitiveFilter< - MessageResponse<{ - attachmentType: StreamChatGenerics['attachmentType']; - channelType: StreamChatGenerics['channelType']; - commandType: StreamChatGenerics['commandType']; - eventType: StreamChatGenerics['eventType']; - memberType: StreamChatGenerics['memberType']; - messageType: {}; - pollOptionType: StreamChatGenerics['pollOptionType']; - pollType: StreamChatGenerics['pollType']; - reactionType: StreamChatGenerics['reactionType']; - userType: StreamChatGenerics['userType']; - }>[Key] - >; + [Key in keyof Omit]?: + | RequireOnlyOne> + | PrimitiveFilter; } >; @@ -1774,27 +1777,11 @@ export type QueryFilter = NonNullable extends s $in?: PrimitiveFilter[]; $lt?: PrimitiveFilter; $lte?: PrimitiveFilter; - /** - * @deprecated and will be removed in a future release. Filtering shall be applied client-side. - */ - $ne?: PrimitiveFilter; - /** - * @deprecated and will be removed in a future release. Filtering shall be applied client-side. - */ - $nin?: PrimitiveFilter[]; } : { $eq?: PrimitiveFilter; $exists?: boolean; $in?: PrimitiveFilter[]; - /** - * @deprecated and will be removed in a future release. Filtering shall be applied client-side. - */ - $ne?: PrimitiveFilter; - /** - * @deprecated and will be removed in a future release. Filtering shall be applied client-side. - */ - $nin?: PrimitiveFilter[]; }; export type QueryFilters = { @@ -1808,147 +1795,93 @@ export type QueryLogicalOperators = { $or?: ArrayTwoOrMore>; }; -export type UserFilters = QueryFilters< - ContainsOperator & { +export type UserFilters = QueryFilters< + ContainsOperator & { id?: - | RequireOnlyOne< - { $autocomplete?: UserResponse['id'] } & QueryFilter< - UserResponse['id'] - > - > - | PrimitiveFilter['id']>; + | RequireOnlyOne<{ $autocomplete?: UserResponse['id'] } & QueryFilter> + | PrimitiveFilter; name?: - | RequireOnlyOne< - { $autocomplete?: UserResponse['name'] } & QueryFilter< - UserResponse['name'] - > - > - | PrimitiveFilter['name']>; + | RequireOnlyOne<{ $autocomplete?: UserResponse['name'] } & QueryFilter> + | PrimitiveFilter; notifications_muted?: | RequireOnlyOne<{ - $eq?: PrimitiveFilter['notifications_muted']>; + $eq?: PrimitiveFilter; }> | boolean; teams?: | RequireOnlyOne<{ $contains?: PrimitiveFilter; - $eq?: PrimitiveFilter['teams']>; - $in?: PrimitiveFilter['teams']>; + $eq?: PrimitiveFilter; + $in?: PrimitiveFilter; }> - | PrimitiveFilter['teams']>; + | PrimitiveFilter; username?: - | RequireOnlyOne< - { $autocomplete?: UserResponse['username'] } & QueryFilter< - UserResponse['username'] - > - > - | PrimitiveFilter['username']>; + | RequireOnlyOne<{ $autocomplete?: UserResponse['username'] } & QueryFilter> + | PrimitiveFilter; } & { - [Key in keyof Omit< - UserResponse<{ - attachmentType: StreamChatGenerics['attachmentType']; - channelType: StreamChatGenerics['channelType']; - commandType: StreamChatGenerics['commandType']; - eventType: StreamChatGenerics['eventType']; - memberType: StreamChatGenerics['memberType']; - messageType: StreamChatGenerics['messageType']; - pollOptionType: StreamChatGenerics['pollOptionType']; - pollType: StreamChatGenerics['pollType']; - reactionType: StreamChatGenerics['reactionType']; - userType: {}; - }>, - 'id' | 'name' | 'teams' | 'username' - >]?: - | RequireOnlyOne< - QueryFilter< - UserResponse<{ - attachmentType: StreamChatGenerics['attachmentType']; - channelType: StreamChatGenerics['channelType']; - commandType: StreamChatGenerics['commandType']; - eventType: StreamChatGenerics['eventType']; - memberType: StreamChatGenerics['memberType']; - messageType: StreamChatGenerics['messageType']; - pollOptionType: StreamChatGenerics['pollOptionType']; - pollType: StreamChatGenerics['pollType']; - reactionType: StreamChatGenerics['reactionType']; - userType: {}; - }>[Key] - > - > - | PrimitiveFilter< - UserResponse<{ - attachmentType: StreamChatGenerics['attachmentType']; - channelType: StreamChatGenerics['channelType']; - commandType: StreamChatGenerics['commandType']; - eventType: StreamChatGenerics['eventType']; - memberType: StreamChatGenerics['memberType']; - messageType: StreamChatGenerics['messageType']; - pollOptionType: StreamChatGenerics['pollOptionType']; - pollType: StreamChatGenerics['pollType']; - reactionType: StreamChatGenerics['reactionType']; - userType: {}; - }>[Key] - >; + [Key in keyof Omit]?: + | RequireOnlyOne> + | PrimitiveFilter; } >; export type InviteStatus = 'pending' | 'accepted' | 'rejected'; // https://getstream.io/chat/docs/react/channel_member/#update-channel-members -export type MemberFilters = QueryFilters< +export type MemberFilters = QueryFilters< { - banned?: - | { $eq?: ChannelMemberResponse['banned'] } - | ChannelMemberResponse['banned']; - channel_role?: - | { $eq?: ChannelMemberResponse['channel_role'] } - | ChannelMemberResponse['channel_role']; - cid?: { $eq?: ChannelResponse['cid'] } | ChannelResponse['cid']; + banned?: { $eq?: ChannelMemberResponse['banned'] } | ChannelMemberResponse['banned']; + channel_role?: { $eq?: ChannelMemberResponse['channel_role'] } | ChannelMemberResponse['channel_role']; + cid?: { $eq?: ChannelResponse['cid'] } | ChannelResponse['cid']; created_at?: | { - $eq?: ChannelMemberResponse['created_at']; - $gt?: ChannelMemberResponse['created_at']; - $gte?: ChannelMemberResponse['created_at']; - $lt?: ChannelMemberResponse['created_at']; - $lte?: ChannelMemberResponse['created_at']; + $eq?: ChannelMemberResponse['created_at']; + $gt?: ChannelMemberResponse['created_at']; + $gte?: ChannelMemberResponse['created_at']; + $lt?: ChannelMemberResponse['created_at']; + $lte?: ChannelMemberResponse['created_at']; } - | ChannelMemberResponse['created_at']; + | ChannelMemberResponse['created_at']; id?: | RequireOnlyOne<{ - $eq?: UserResponse['id']; - $in?: UserResponse['id'][]; + $eq?: UserResponse['id']; + $in?: UserResponse['id'][]; }> - | UserResponse['id']; - invite?: - | { $eq?: ChannelMemberResponse['status'] } - | ChannelMemberResponse['status']; + | UserResponse['id']; + invite?: { $eq?: ChannelMemberResponse['status'] } | ChannelMemberResponse['status']; + is_moderator?: + | RequireOnlyOne<{ $eq?: ChannelMemberResponse['is_moderator'] }> + | ChannelMemberResponse['is_moderator']; joined?: { $eq?: boolean } | boolean; last_active?: | { - $eq?: UserResponse['last_active']; - $gt?: UserResponse['last_active']; - $gte?: UserResponse['last_active']; - $lt?: UserResponse['last_active']; - $lte?: UserResponse['last_active']; + $eq?: UserResponse['last_active']; + $gt?: UserResponse['last_active']; + $gte?: UserResponse['last_active']; + $lt?: UserResponse['last_active']; + $lte?: UserResponse['last_active']; } - | UserResponse['last_active']; + | UserResponse['last_active']; name?: | RequireOnlyOne<{ - $autocomplete?: ChannelMemberResponse['name']; - $eq?: ChannelMemberResponse['name']; - $in?: ChannelMemberResponse['name'][]; - $q?: ChannelMemberResponse['name']; + $autocomplete?: NonNullable['name']; + $eq?: NonNullable['name']; + $in?: NonNullable['name'][]; + $q?: NonNullable['name']; }> - | PrimitiveFilter['name']>; + | PrimitiveFilter['name']>; + notifications_muted?: + | RequireOnlyOne<{ $eq?: ChannelMemberResponse['notifications_muted'] }> + | ChannelMemberResponse['notifications_muted']; updated_at?: | { - $eq?: ChannelMemberResponse['updated_at']; - $gt?: ChannelMemberResponse['updated_at']; - $gte?: ChannelMemberResponse['updated_at']; - $lt?: ChannelMemberResponse['updated_at']; - $lte?: ChannelMemberResponse['updated_at']; + $eq?: ChannelMemberResponse['updated_at']; + $gt?: ChannelMemberResponse['updated_at']; + $gte?: ChannelMemberResponse['updated_at']; + $lt?: ChannelMemberResponse['updated_at']; + $lte?: ChannelMemberResponse['updated_at']; } - | ChannelMemberResponse['updated_at']; + | ChannelMemberResponse['updated_at']; 'user.email'?: | RequireOnlyOne<{ $autocomplete?: string; @@ -1958,14 +1891,14 @@ export type MemberFilters['user_id']; - $in?: ChannelMemberResponse['user_id'][]; + $eq?: ChannelMemberResponse['user_id']; + $in?: ChannelMemberResponse['user_id'][]; }> - | PrimitiveFilter['id']>; + | PrimitiveFilter; } & { - [Key in keyof ContainsOperator]?: - | RequireOnlyOne[Key]>> - | PrimitiveFilter[Key]>; + [Key in keyof ContainsOperator]?: + | RequireOnlyOne[Key]>> + | PrimitiveFilter[Key]>; } >; @@ -1977,23 +1910,15 @@ export type BannedUsersSort = BannedUsersSortBase | Array; export type BannedUsersSortBase = { created_at?: AscDesc }; -export type ReactionSort = - | ReactionSortBase - | Array>; +export type ReactionSort = ReactionSortBase | Array; -export type ReactionSortBase = Sort< - StreamChatGenerics['reactionType'] -> & { +export type ReactionSortBase = Sort & { created_at?: AscDesc; }; -export type ChannelSort = - | ChannelSortBase - | Array>; +export type ChannelSort = ChannelSortBase | Array; -export type ChannelSortBase = Sort< - StreamChatGenerics['channelType'] -> & { +export type ChannelSortBase = Sort & { created_at?: AscDesc; has_unread?: AscDesc; last_message_at?: AscDesc; @@ -2011,17 +1936,13 @@ export type Sort = { [P in keyof T]?: AscDesc; }; -export type UserSort = - | Sort> - | Array>>; +export type UserSort = Sort | Array>; -export type MemberSort = - | Sort, 'id' | 'created_at' | 'last_active' | 'name' | 'updated_at'>> - | Array, 'id' | 'created_at' | 'last_active' | 'name' | 'updated_at'>>>; +export type MemberSort = + | Sort> + | Array>>; -export type SearchMessageSortBase = Sort< - StreamChatGenerics['messageType'] -> & { +export type SearchMessageSortBase = Sort & { attachments?: AscDesc; 'attachments.type'?: AscDesc; created_at?: AscDesc; @@ -2037,15 +1958,9 @@ export type SearchMessageSortBase = - | SearchMessageSortBase - | Array>; +export type SearchMessageSort = SearchMessageSortBase | Array; -export type QuerySort = - | BannedUsersSort - | ChannelSort - | SearchMessageSort - | UserSort; +export type QuerySort = BannedUsersSort | ChannelSort | SearchMessageSort | UserSort; export type PollSort = PollSortBase | Array; @@ -2181,9 +2096,7 @@ export type AppSettings = { }; }; -export type Attachment< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = StreamChatGenerics['attachmentType'] & { +export type Attachment = CustomAttachmentData & { actions?: Action[]; asset_url?: string; author_icon?: string; @@ -2205,6 +2118,7 @@ export type Attachment< original_height?: number; original_width?: number; pretext?: string; + stopped_sharing?: boolean; text?: string; thumb_url?: string; title?: string; @@ -2234,20 +2148,16 @@ export type BlockList = { validate?: boolean; }; -export type ChannelConfig = ChannelConfigFields & +export type ChannelConfig = ChannelConfigFields & CreatedAtUpdatedAt & { - commands?: CommandVariants[]; + commands?: CommandVariants[]; }; -export type ChannelConfigAutomod = '' | 'AI' | 'disabled' | 'simple'; +export type ChannelConfigAutomod = Automod; -export type ChannelConfigAutomodBehavior = '' | 'block' | 'flag'; +export type ChannelConfigAutomodBehavior = AutomodBehavior; -export type ChannelConfigAutomodThresholds = null | { - explicit?: { block?: number; flag?: number }; - spam?: { block?: number; flag?: number }; - toxic?: { block?: number; flag?: number }; -}; +export type ChannelConfigAutomodThresholds = null | Thresholds; export type ChannelConfigFields = { reminders: boolean; @@ -2274,31 +2184,22 @@ export type ChannelConfigFields = { url_enrichment?: boolean; }; -export type ChannelConfigWithInfo< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = ChannelConfigFields & +export type ChannelConfigWithInfo = ChannelConfigFields & CreatedAtUpdatedAt & { - commands?: CommandResponse[]; + commands?: CommandResponse[]; }; -export type ChannelData< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = StreamChatGenerics['channelType'] & { +export type ChannelData = CustomChannelData & { blocked?: boolean; - members?: string[] | Array>; + created_by?: UserResponse | null; + created_by_id?: UserResponse['id']; + members?: string[] | Array; name?: string; }; -/** - * @deprecated Use ChannelMemberResponse instead - */ -export type ChannelMembership< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = ChannelMemberResponse; - -export type ChannelMute = { - user: UserResponse; - channel?: ChannelResponse; +export type ChannelMute = { + user: UserResponse; + channel?: ChannelResponse; created_at?: string; expires?: string; updated_at?: string; @@ -2312,14 +2213,14 @@ export type ChannelRole = { same_team?: boolean; }; -export type CheckPushInput = { +export type CheckPushInput = { apn_template?: string; client_id?: string; connection_id?: string; firebase_data_template?: string; firebase_template?: string; message_id?: string; - user?: UserResponse; + user?: UserResponse; user_id?: string; }; @@ -2375,7 +2276,7 @@ export type PushProviderXiaomi = { xiaomi_secret?: string; }; -export type CommandVariants = +export type CommandVariants = | 'all' | 'ban' | 'fun_set' @@ -2384,18 +2285,15 @@ export type CommandVariants = Record< - string, - ChannelConfigWithInfo | undefined ->; +export type Configs = Record; -export type ConnectionOpen = { +export type ConnectionOpen = { connection_id: string; cid?: string; created_at?: string; - me?: OwnUserResponse; + me?: OwnUserResponse; type?: string; }; @@ -2404,9 +2302,9 @@ export type CreatedAtUpdatedAt = { updated_at: string; }; -export type Device = DeviceFields & { +export type Device = DeviceFields & { provider?: string; - user?: UserResponse; + user?: UserResponse; user_id?: string; }; @@ -2544,13 +2442,15 @@ export type EndpointName = | 'ListPushProviders' | 'CreatePoll'; -export type ExportChannelRequest = { - id: string; - type: string; - cid?: string; - messages_since?: Date; - messages_until?: Date; -}; +export type ExportChannelRequest = ( + | { + id: string; + type: string; + } + | { + cid: string; + } +) & { messages_since?: Date; messages_until?: Date }; export type ExportChannelOptions = { clear_deleted_message_text?: boolean; @@ -2626,17 +2526,13 @@ export type LogLevel = 'info' | 'error' | 'warn'; export type Logger = (logLevel: LogLevel, message: string, extraData?: Record) => void; -export type Message = Partial< - MessageBase -> & { +export type Message = Partial & { mentioned_users?: string[]; }; -export type MessageBase< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = StreamChatGenerics['messageType'] & { +export type MessageBase = CustomMessageData & { id: string; - attachments?: Attachment[]; + attachments?: Attachment[]; html?: string; mml?: string; parent_id?: string; @@ -2649,7 +2545,7 @@ export type MessageBase< show_in_channel?: boolean; silent?: boolean; text?: string; - user?: UserResponse | null; + user?: UserResponse | null; user_id?: string; }; @@ -2673,41 +2569,41 @@ export type GetMessageOptions = { show_deleted_message?: boolean; }; -export type Mute = { +export type Mute = { created_at: string; - target: UserResponse; + target: UserResponse; updated_at: string; - user: UserResponse; + user: UserResponse; }; -export type PartialUpdateChannel = { - set?: Partial>; - unset?: Array>; +export type PartialUpdateChannel = { + set?: Partial; + unset?: Array; }; -export type PartialUpdateMember = { - set?: ChannelMemberUpdates; - unset?: Array>; +export type PartialUpdateMember = { + set?: ChannelMemberUpdates; + unset?: Array; }; -export type PartialUserUpdate = { +export type PartialUserUpdate = { id: string; - set?: Partial>; - unset?: Array>; + set?: Partial; + unset?: Array; }; -export type MessageUpdatableFields = Omit< - MessageResponse, +export type MessageUpdatableFields = Omit< + MessageResponse, 'cid' | 'created_at' | 'updated_at' | 'deleted_at' | 'user' | 'user_id' >; -export type PartialMessageUpdate = { - set?: Partial>; - unset?: Array>; +export type PartialMessageUpdate = { + set?: Partial; + unset?: Array; }; -export type PendingMessageResponse = { - message: MessageResponse; +export type PendingMessageResponse = { + message: MessageResponse; pending_message_metadata?: Record; }; @@ -2752,13 +2648,11 @@ export type RateLimitsInfo = { export type RateLimitsMap = Record; -export type Reaction< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = StreamChatGenerics['reactionType'] & { +export type Reaction = CustomReactionData & { type: string; message_id?: string; score?: number; - user?: UserResponse | null; + user?: UserResponse | null; user_id?: string; }; @@ -2782,19 +2676,16 @@ export type Resource = | 'UpdateUser' | 'UploadAttachment'; -export type SearchPayload = Omit< - SearchOptions, - 'sort' -> & { +export type SearchPayload = Omit & { client_id?: string; connection_id?: string; - filter_conditions?: ChannelFilters; - message_filter_conditions?: MessageFilters; + filter_conditions?: ChannelFilters; + message_filter_conditions?: MessageFilters; message_options?: MessageOptions; query?: string; sort?: Array<{ direction: AscDesc; - field: keyof SearchMessageSortBase; + field: keyof SearchMessageSortBase; }>; }; @@ -2899,12 +2790,12 @@ export type ReservedMessageFields = | 'user' | '__html'; -export type UpdatedMessage = Omit< - MessageResponse, - 'mentioned_users' -> & { mentioned_users?: string[] }; +export type UpdatedMessage = Omit & { + mentioned_users?: string[]; + type?: MessageLabel; +}; -export type User = StreamChatGenerics['userType'] & { +export type User = CustomUserData & { id: string; anon?: boolean; name?: string; @@ -3084,12 +2975,12 @@ export type TaskStatus = { result?: UR; }; -export type TruncateOptions = { +export type TruncateOptions = { hard_delete?: boolean; - message?: Message; + message?: Message; skip_push?: boolean; truncated_at?: Date; - user?: UserResponse; + user?: UserResponse; user_id?: string; }; @@ -3137,10 +3028,10 @@ export type ImportTask = { }; export type MessageSetType = 'latest' | 'current' | 'new'; -export type MessageSet = { +export type MessageSet = { isCurrent: boolean; isLatest: boolean; - messages: FormatMessageResponse[]; + messages: FormatMessageResponse[]; pagination: { hasNext: boolean; hasPrev: boolean }; }; @@ -3152,11 +3043,11 @@ export type PushProviderListResponse = { push_providers: PushProvider[]; }; -export type CreateCallOptions = { +export type CreateCallOptions = { id: string; type: string; options?: UR; - user?: UserResponse | null; + user?: UserResponse | null; user_id?: string; }; @@ -3208,51 +3099,51 @@ export class ErrorFromResponse extends Error { status?: number; } -export type QueryPollsResponse = { - polls: PollResponse[]; +export type QueryPollsResponse = { + polls: PollResponse[]; next?: string; }; -export type CreatePollAPIResponse = { - poll: PollResponse; +export type CreatePollAPIResponse = { + poll: PollResponse; }; -export type GetPollAPIResponse = { - poll: PollResponse; +export type GetPollAPIResponse = { + poll: PollResponse; }; -export type UpdatePollAPIResponse = { - poll: PollResponse; +export type UpdatePollAPIResponse = { + poll: PollResponse; }; -export type PollResponse< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = StreamChatGenerics['pollType'] & - PollEnrichData & { +export type PollResponse = CustomPollData & + PollEnrichData & { + cid: string; created_at: string; - created_by: UserResponse | null; + created_by: UserResponse | null; created_by_id: string; enforce_unique_vote: boolean; id: string; max_votes_allowed: number; name: string; - options: PollOption[]; + options: PollOption[]; updated_at: string; allow_answers?: boolean; allow_user_suggested_options?: boolean; description?: string; + enforce_unique_votes?: boolean; is_closed?: boolean; voting_visibility?: VotingVisibility; }; -export type PollOption = { +export type PollOption = { created_at: string; id: string; poll_id: string; text: string; updated_at: string; vote_count: number; - votes?: PollVote[]; + votes?: PollVote[]; }; export enum VotingVisibility { @@ -3260,18 +3151,16 @@ export enum VotingVisibility { public = 'public', } -export type PollEnrichData = { +export type PollEnrichData = { answers_count: number; - latest_answers: PollAnswer[]; // not updated with WS events, ordered DESC by created_at, seems like updated_at cannot be different from created_at - latest_votes_by_option: Record[]>; // not updated with WS events; always null in anonymous polls + latest_answers: PollAnswer[]; // not updated with WS events, ordered DESC by created_at, seems like updated_at cannot be different from created_at + latest_votes_by_option: Record; // not updated with WS events; always null in anonymous polls vote_count: number; vote_counts_by_option: Record; - own_votes?: (PollVote | PollAnswer)[]; // not updated with WS events + own_votes?: (PollVote | PollAnswer)[]; // not updated with WS events }; -export type PollData< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = StreamChatGenerics['pollType'] & { +export type PollData = CustomPollData & { id: string; name: string; allow_answers?: boolean; @@ -3280,32 +3169,27 @@ export type PollData< enforce_unique_vote?: boolean; is_closed?: boolean; max_votes_allowed?: number; - options?: PollOptionData[]; + options?: PollOptionData[]; user_id?: string; voting_visibility?: VotingVisibility; }; -export type CreatePollData = Partial< - PollData -> & - Pick, 'name'>; +export type CreatePollData = Partial & Pick; -export type PartialPollUpdate = { - set?: Partial>; - unset?: Array>; +export type PartialPollUpdate = { + set?: Partial; + unset?: Array; }; -export type PollOptionData< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = StreamChatGenerics['pollOptionType'] & { +export type PollOptionData = CustomPollOptionData & { text: string; id?: string; position?: number; }; -export type PartialPollOptionUpdate = { - set?: Partial>; - unset?: Array>; +export type PartialPollOptionUpdate = { + set?: Partial; + unset?: Array; }; export type PollVoteData = { @@ -3319,20 +3203,14 @@ export type PollPaginationOptions = { next?: string; }; -export type CreatePollOptionAPIResponse = { - poll_option: PollOptionResponse; +export type CreatePollOptionAPIResponse = { + poll_option: PollOptionResponse; }; -export type GetPollOptionAPIResponse< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = CreatePollOptionAPIResponse; -export type UpdatePollOptionAPIResponse< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = CreatePollOptionAPIResponse; +export type GetPollOptionAPIResponse = CreatePollOptionAPIResponse; +export type UpdatePollOptionAPIResponse = CreatePollOptionAPIResponse; -export type PollOptionResponse< - StreamChatGenerics extends ExtendableGenerics = DefaultGenerics -> = StreamChatGenerics['pollType'] & { +export type PollOptionResponse = CustomPollData & { created_at: string; id: string; poll_id: string; @@ -3340,39 +3218,36 @@ export type PollOptionResponse< text: string; updated_at: string; vote_count: number; - votes?: PollVote[]; + votes?: PollVote[]; }; -export type PollVote = { +export type PollVote = { created_at: string; id: string; poll_id: string; updated_at: string; option_id?: string; - user?: UserResponse; + user?: UserResponse; user_id?: string; }; -export type PollAnswer = Exclude< - PollVote, - 'option_id' -> & { +export type PollAnswer = Exclude & { answer_text: string; is_answer: boolean; // this is absolutely redundant prop as answer_text indicates that a vote is an answer }; -export type PollVotesAPIResponse = { - votes: (PollVote | PollAnswer)[]; +export type PollVotesAPIResponse = { + votes: (PollVote | PollAnswer)[]; next?: string; }; -export type PollAnswersAPIResponse = { - votes: PollAnswer[]; // todo: should be changes to answers? +export type PollAnswersAPIResponse = { + votes: PollAnswer[]; // todo: should be changes to answers? next?: string; }; -export type CastVoteAPIResponse = { - vote: PollVote | PollAnswer; +export type CastVoteAPIResponse = { + vote: PollVote | PollAnswer; }; export type QueryMessageHistoryFilters = QueryFilters< @@ -3402,16 +3277,16 @@ export type QueryMessageHistorySortBase = { export type QueryMessageHistoryOptions = Pager; -export type MessageHistoryEntry = { +export type MessageHistoryEntry = { message_id: string; message_updated_at: string; - attachments?: Attachment[]; + attachments?: Attachment[]; message_updated_by_id?: string; text?: string; }; -export type QueryMessageHistoryResponse = { - message_history: MessageHistoryEntry[]; +export type QueryMessageHistoryResponse = { + message_history: MessageHistoryEntry[]; next?: string; prev?: string; }; @@ -3505,14 +3380,14 @@ export type SubmitActionOptions = { user_id?: string; }; -export type GetUserModerationReportResponse = { - user: UserResponse; +export type GetUserModerationReportResponse = { + user: UserResponse; user_blocks?: Array<{ blocked_at: string; blocked_by_user_id: string; blocked_user_id: string; }>; - user_mutes?: Mute[]; + user_mutes?: Mute[]; }; export type QueryModerationConfigsFilters = QueryFilters< @@ -3805,10 +3680,10 @@ export type VelocityFilterConfig = { async?: boolean; }; -export type PromoteChannelParams = { - channels: Array>; - channelToMove: Channel; - sort: ChannelSort; +export type PromoteChannelParams = { + channels: Array; + channelToMove: Channel; + sort: ChannelSort; /** * If the index of the channel within `channels` list which is being moved upwards * (`channelToMove`) is known, you can supply it to skip extra calculation. diff --git a/src/utils.ts b/src/utils.ts index 5691c25122..fdbcb196bd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,8 +1,6 @@ import FormData from 'form-data'; import { AscDesc, - ExtendableGenerics, - DefaultGenerics, Logger, OwnUserBase, OwnUserResponse, @@ -76,10 +74,8 @@ function isFileWebAPI(uri: unknown): uri is File { return typeof window !== 'undefined' && 'File' in window && uri instanceof File; } -export function isOwnUser( - user?: OwnUserResponse | UserResponse, -): user is OwnUserResponse { - return (user as OwnUserResponse)?.total_unread_count !== undefined; +export function isOwnUser(user?: OwnUserResponse | UserResponse): user is OwnUserResponse { + return (user as OwnUserResponse)?.total_unread_count !== undefined; } function isBlobWebAPI(uri: unknown): uri is Blob { @@ -292,17 +288,11 @@ export const axiosParamsSerializer: AxiosRequestConfig['paramsSerializer'] = (pa * Takes the message object, parses the dates, sets `__html` * and sets the status to `received` if missing; returns a new message object. * - * @param {MessageResponse} message `MessageResponse` object + * @param {MessageResponse} message `MessageResponse` object */ -export function formatMessage( - message: MessageResponse | FormatMessageResponse, -): FormatMessageResponse { +export function formatMessage(message: MessageResponse | FormatMessageResponse): FormatMessageResponse { return { ...message, - /** - * @deprecated please use `html` - */ - __html: message.html, // parse the dates pinned_at: message.pinned_at ? new Date(message.pinned_at) : null, created_at: message.created_at ? new Date(message.created_at) : new Date(), @@ -623,10 +613,10 @@ export const uniqBy = (array: T[] | unknown, iteratee: ((item: T) => unknown) }); }; -type MessagePaginationUpdatedParams = { +type MessagePaginationUpdatedParams = { parentSet: MessageSet; requestedPageSize: number; - returnedPage: MessageResponse[]; + returnedPage: MessageResponse[]; logger?: Logger; messagePaginationOptions?: MessagePaginationOptions; }; @@ -661,12 +651,12 @@ export function binarySearchByDateEqualOrNearestGreater( return left; } -const messagePaginationCreatedAtAround = ({ +const messagePaginationCreatedAtAround = ({ parentSet, requestedPageSize, returnedPage, messagePaginationOptions, -}: MessagePaginationUpdatedParams) => { +}: MessagePaginationUpdatedParams) => { const newPagination = { ...parentSet.pagination }; if (!messagePaginationOptions?.created_at_around) return newPagination; let hasPrev; @@ -726,12 +716,12 @@ const messagePaginationCreatedAtAround = ({ +const messagePaginationIdAround = ({ parentSet, requestedPageSize, returnedPage, messagePaginationOptions, -}: MessagePaginationUpdatedParams) => { +}: MessagePaginationUpdatedParams) => { const newPagination = { ...parentSet.pagination }; const { id_around } = messagePaginationOptions || {}; if (!id_around) return newPagination; @@ -779,12 +769,12 @@ const messagePaginationIdAround = ({ +const messagePaginationLinear = ({ parentSet, requestedPageSize, returnedPage, messagePaginationOptions, -}: MessagePaginationUpdatedParams) => { +}: MessagePaginationUpdatedParams) => { const newPagination = { ...parentSet.pagination }; let hasPrev; @@ -836,9 +826,7 @@ const messagePaginationLinear = ( - params: MessagePaginationUpdatedParams, -) => { +export const messageSetPagination = (params: MessagePaginationUpdatedParams) => { if (params.parentSet.messages.length < params.returnedPage.length) { params.logger?.('error', 'Corrupted message set state: parent set size < returned page size'); return params.parentSet.pagination; @@ -859,12 +847,12 @@ export const messageSetPagination = | undefined> = {}; -type GetChannelParams = { - client: StreamChat; - channel?: Channel; +type GetChannelParams = { + client: StreamChat; + channel?: Channel; id?: string; members?: string[]; - options?: ChannelQueryOptions; + options?: ChannelQueryOptions; type?: string; }; /** @@ -877,14 +865,7 @@ type GetChannelParams({ - channel, - client, - id, - members, - options, - type, -}: GetChannelParams) => { +export const getAndWatchChannel = async ({ channel, client, id, members, options, type }: GetChannelParams) => { if (!channel && !type) { throw new Error('Channel or channel type have to be provided to query a channel.'); } @@ -938,9 +919,7 @@ export const generateChannelTempCid = (channelType: string, members: string[]) = * Checks if a channel is pinned or not. Will return true only if channel.state.membership.pinned_at exists. * @param channel */ -export const isChannelPinned = ( - channel: Channel, -) => { +export const isChannelPinned = (channel: Channel) => { if (!channel) return false; const member = channel.state.membership; @@ -952,9 +931,7 @@ export const isChannelPinned = ( - channel: Channel, -) => { +export const isChannelArchived = (channel: Channel) => { if (!channel) return false; const member = channel.state.membership; @@ -967,9 +944,7 @@ export const isChannelArchived = ( - filters: ChannelFilters, -) => { +export const shouldConsiderArchivedChannels = (filters: ChannelFilters) => { if (!filters) return false; return typeof filters.archived === 'boolean'; @@ -983,17 +958,17 @@ export const shouldConsiderArchivedChannels = ({ +export const extractSortValue = ({ atIndex, sort, targetKey, }: { atIndex: number; - targetKey: keyof ChannelSortBase; - sort?: ChannelSort; + targetKey: keyof ChannelSortBase; + sort?: ChannelSort; }) => { if (!sort) return null; - let option: null | ChannelSortBase = null; + let option: null | ChannelSortBase = null; if (Array.isArray(sort)) { option = sort[atIndex] ?? null; @@ -1021,9 +996,7 @@ export const extractSortValue = ( - sort: ChannelSort, -) => { +export const shouldConsiderPinnedChannels = (sort: ChannelSort) => { const value = findPinnedAtSortOrder({ sort }); if (typeof value !== 'number') return false; @@ -1036,11 +1009,7 @@ export const shouldConsiderPinnedChannels = ({ - sort, -}: { - sort: ChannelSort; -}) => +export const findPinnedAtSortOrder = ({ sort }: { sort: ChannelSort }) => extractSortValue({ atIndex: 0, sort, @@ -1053,11 +1022,7 @@ export const findPinnedAtSortOrder = ({ - channels, -}: { - channels: Channel[]; -}) => { +export const findLastPinnedChannelIndex = ({ channels }: { channels: Channel[] }) => { let lastPinnedChannelIndex: number | null = null; for (const channel of channels) { @@ -1082,12 +1047,12 @@ export const findLastPinnedChannelIndex = ({ +export const promoteChannel = ({ channels, channelToMove, channelToMoveIndexWithinChannels, sort, -}: PromoteChannelParams) => { +}: PromoteChannelParams) => { // get index of channel to move up const targetChannelIndex = channelToMoveIndexWithinChannels ?? channels.findIndex((channel) => channel.cid === channelToMove.cid); @@ -1099,7 +1064,7 @@ export const promoteChannel = (channelToMove); + const isTargetChannelPinned = isChannelPinned(channelToMove); if (targetChannelAlreadyAtTheTop || (considerPinnedChannels && isTargetChannelPinned)) { return channels; From 32836834188358dc7ab793e256cd6d62bada43cc Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Thu, 27 Feb 2025 12:01:11 +0100 Subject: [PATCH 08/47] chore: adjust release.yml - add condition for rc --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c205273fa2..57be904d6e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: name: Release from "${{ github.ref_name }}" branch runs-on: ubuntu-latest # GH does not allow to limit branches in the workflow_dispatch settings so this here is a safety measure - if: ${{ inputs.dry_run || startsWith(github.ref_name, 'release') || startsWith(github.ref_name, 'master') }} + if: ${{ inputs.dry_run || github.ref_name == 'rc' || startsWith(github.ref_name, 'release') || startsWith(github.ref_name, 'master') }} steps: - name: Checkout uses: actions/checkout@v4 From 57a4e3938e091f09488249c9b403a80af617b44d Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Thu, 27 Feb 2025 12:01:44 +0100 Subject: [PATCH 09/47] chore: adjust version check --- .gitignore | 1 + .releaserc.json | 2 +- scripts/get-package-version.mjs | 12 ++++++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index e436b25c66..5b94a2cc77 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,4 @@ typings/ .envrc test/typescript/data.* +.version \ No newline at end of file diff --git a/.releaserc.json b/.releaserc.json index 9f8f05cb2e..51c000d5e3 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -71,7 +71,7 @@ [ "@semantic-release/exec", { - "prepareCmd": "echo \"NEXT_VERSION=${nextRelease.version}\" >> $GITHUB_ENV" + "prepareCmd": "echo \"${nextRelease.version}\" > .version" } ], [ diff --git a/scripts/get-package-version.mjs b/scripts/get-package-version.mjs index f424d70c99..4370043e9f 100644 --- a/scripts/get-package-version.mjs +++ b/scripts/get-package-version.mjs @@ -1,14 +1,18 @@ import { execSync } from 'node:child_process'; +import { resolve } from 'node:path'; +import { readFileSync } from 'node:fs'; import packageJson from '../package.json' with { type: 'json' }; // Get the latest version so that magic string __STREAM_CHAT_REACT_VERSION__ can be replaced with it in the source code (used for reporting purposes) export default function getPackageVersion() { let version; // During release, use the version being released - // see .releaserc.json where the `NEXT_VERSION` env variable is set - if (process.env.NEXT_VERSION) { - version = process.env.NEXT_VERSION; - } else { + // see .releaserc.json where the .version file is generated + try { + version = readFileSync(resolve(import.meta.dirname, '../.version')).toString().trim(); + } catch {/* do nothing */} + + if (typeof version !== 'string') { // Otherwise use the latest git tag try { version = execSync('git describe --tags --abbrev=0').toString().trim(); From 1c313e8d29e9af32b1f646f3ad166d25c6bf48b1 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 27 Feb 2025 11:59:01 +0000 Subject: [PATCH 10/47] chore(release): 9.0.0-rc.1 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [9.0.0-rc.1](https://github.com/GetStream/stream-chat-js/compare/v8.57.1...v9.0.0-rc.1) (2025-02-27) ### ⚠ BREAKING CHANGES * dropped jsDelivr bundle (#1468) * dropped `StreamChatGenerics`, use `CustomData` to extend your types * type `InviteOptions` has been renamed to `UpdateChannelOptions` * type `UpdateChannelOptions` has been renamed to `UpdateChannelTypeRequest` * type `ThreadResponseCustomData` has been renamed to `CustomThreadData` * type `MarkAllReadOptions` has been deleted in favour of type `MarkChannelsReadOptions` * type `QueryFilter` no longer supports `$ne` and `$nin` operators * type `ChannelMembership` has been deleted in favour of type `ChannelMemberResponse` * function `formatMessage` (`utils.ts`) no longer returns `__html` property in the formatted message output ### Bug Fixes * replace StreamChatGenerics with module augmentation ([#1458](https://github.com/GetStream/stream-chat-js/issues/1458)) ([feb97da](https://github.com/GetStream/stream-chat-js/commit/feb97da08a5c4fa325156517111bb58402a1f7b8)) --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07f66af14d..5f627040fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +## [9.0.0-rc.1](https://github.com/GetStream/stream-chat-js/compare/v8.57.1...v9.0.0-rc.1) (2025-02-27) + +### ⚠ BREAKING CHANGES + +* dropped jsDelivr bundle (#1468) +* dropped `StreamChatGenerics`, use `CustomData` to extend your +types +* type `InviteOptions` has been renamed to `UpdateChannelOptions` +* type `UpdateChannelOptions` has been renamed to +`UpdateChannelTypeRequest` +* type `ThreadResponseCustomData` has been renamed to `CustomThreadData` +* type `MarkAllReadOptions` has been deleted in favour of type +`MarkChannelsReadOptions` +* type `QueryFilter` no longer supports `$ne` and `$nin` operators +* type `ChannelMembership` has been deleted in favour of type +`ChannelMemberResponse` +* function `formatMessage` (`utils.ts`) no longer returns `__html` +property in the formatted message output + +### Bug Fixes + +* replace StreamChatGenerics with module augmentation ([#1458](https://github.com/GetStream/stream-chat-js/issues/1458)) ([feb97da](https://github.com/GetStream/stream-chat-js/commit/feb97da08a5c4fa325156517111bb58402a1f7b8)) + # Changelog All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. From 6764bad05990f113b66c311bed2967faaac4e11f Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Thu, 27 Feb 2025 18:53:58 +0100 Subject: [PATCH 11/47] fix: increase package.json[engines.node] version --- package.json | 2 +- scripts/get-package-version.mjs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 1acba915e6..3a88261156 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,7 @@ "prepack": "yarn run build" }, "engines": { - "node": ">=16" + "node": ">=18" }, "packageManager": "yarn@1.22.21+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72" } diff --git a/scripts/get-package-version.mjs b/scripts/get-package-version.mjs index 4370043e9f..4dba44d1f1 100644 --- a/scripts/get-package-version.mjs +++ b/scripts/get-package-version.mjs @@ -9,6 +9,7 @@ export default function getPackageVersion() { // During release, use the version being released // see .releaserc.json where the .version file is generated try { + console.info({ v: packageJson.version }); version = readFileSync(resolve(import.meta.dirname, '../.version')).toString().trim(); } catch {/* do nothing */} From 993c7ceb8a0b3088eb71f6dd42a151c92df01bd8 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 27 Feb 2025 18:02:03 +0000 Subject: [PATCH 12/47] chore(release): 9.0.0-rc.2 [skip ci] ## [9.0.0-rc.2](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.1...v9.0.0-rc.2) (2025-02-27) ### Bug Fixes * increase package.json[engines.node] version ([6764bad](https://github.com/GetStream/stream-chat-js/commit/6764bad05990f113b66c311bed2967faaac4e11f)) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f627040fd..5a7d0dafae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [9.0.0-rc.2](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.1...v9.0.0-rc.2) (2025-02-27) + +### Bug Fixes + +* increase package.json[engines.node] version ([6764bad](https://github.com/GetStream/stream-chat-js/commit/6764bad05990f113b66c311bed2967faaac4e11f)) + ## [9.0.0-rc.1](https://github.com/GetStream/stream-chat-js/compare/v8.57.1...v9.0.0-rc.1) (2025-02-27) ### ⚠ BREAKING CHANGES From 0acd60bafe356e0333e30a4582c956d54f6453d1 Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Thu, 27 Feb 2025 19:14:06 +0100 Subject: [PATCH 13/47] chore: adjust release process, use package.json#version instead --- .gitignore | 3 +-- .releaserc.json | 6 ------ package.json | 1 - scripts/get-package-version.mjs | 19 +++++++------------ yarn.lock | 12 ------------ 5 files changed, 8 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index 5b94a2cc77..473c2a4fa8 100644 --- a/.gitignore +++ b/.gitignore @@ -68,5 +68,4 @@ typings/ /.idea .envrc -test/typescript/data.* -.version \ No newline at end of file +test/typescript/data.* \ No newline at end of file diff --git a/.releaserc.json b/.releaserc.json index 51c000d5e3..14d941843d 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -68,12 +68,6 @@ } } ], - [ - "@semantic-release/exec", - { - "prepareCmd": "echo \"${nextRelease.version}\" > .version" - } - ], [ "@semantic-release/changelog", { diff --git a/package.json b/package.json index 3a88261156..56011c2913 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,6 @@ "@commitlint/cli": "^16.0.2", "@commitlint/config-conventional": "^16.0.0", "@semantic-release/changelog": "^6.0.3", - "@semantic-release/exec": "^7.0.3", "@semantic-release/git": "^10.0.1", "@types/base64-js": "^1.3.0", "@types/chai": "^4.2.15", diff --git a/scripts/get-package-version.mjs b/scripts/get-package-version.mjs index 4dba44d1f1..8dc8e437eb 100644 --- a/scripts/get-package-version.mjs +++ b/scripts/get-package-version.mjs @@ -1,20 +1,13 @@ import { execSync } from 'node:child_process'; -import { resolve } from 'node:path'; -import { readFileSync } from 'node:fs'; import packageJson from '../package.json' with { type: 'json' }; -// Get the latest version so that magic string __STREAM_CHAT_REACT_VERSION__ can be replaced with it in the source code (used for reporting purposes) +// get the latest version so that "process.env.PKG_VERSION" can be replaced with it in the source code (used for reporting purposes), see bundle.mjs for source export default function getPackageVersion() { - let version; - // During release, use the version being released - // see .releaserc.json where the .version file is generated - try { - console.info({ v: packageJson.version }); - version = readFileSync(resolve(import.meta.dirname, '../.version')).toString().trim(); - } catch {/* do nothing */} + // "build" script ("prepack" hook) gets invoked when semantic-release runs "npm publish", at that point package.json#version already contains updated next version which we can use + let version = packageJson.version; - if (typeof version !== 'string') { - // Otherwise use the latest git tag + // if it fails (loads a default), try pulling version from git + if (version === '0.0.0-development') { try { version = execSync('git describe --tags --abbrev=0').toString().trim(); } catch (error) { @@ -23,6 +16,8 @@ export default function getPackageVersion() { version = packageJson.version; } } + console.log(`Determined the build package version to be ${version}`); + return version; } diff --git a/yarn.lock b/yarn.lock index ac8d8d66c6..7f50389a85 100644 --- a/yarn.lock +++ b/yarn.lock @@ -893,18 +893,6 @@ resolved "https://registry.yarnpkg.com/@semantic-release/error/-/error-4.0.0.tgz#692810288239637f74396976a9340fbc0aa9f6f9" integrity sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ== -"@semantic-release/exec@^7.0.3": - version "7.0.3" - resolved "https://registry.yarnpkg.com/@semantic-release/exec/-/exec-7.0.3.tgz#be0b2d8e7e2bcf05076fc48914a643b939c2c151" - integrity sha512-uNWwPNtWi3WTcTm3fWfFQEuj8otOvwoS5m9yo2jSVHuvqdZNsOWmuL0/FqcVyZnCI32fxyYV0G7PPb/TzCH6jw== - dependencies: - "@semantic-release/error" "^4.0.0" - aggregate-error "^3.0.0" - debug "^4.0.0" - execa "^9.0.0" - lodash-es "^4.17.21" - parse-json "^8.0.0" - "@semantic-release/git@^10.0.1": version "10.0.1" resolved "https://registry.yarnpkg.com/@semantic-release/git/-/git-10.0.1.tgz#c646e55d67fae623875bf3a06a634dd434904498" From e2d9b1511434c525fd2a28177cf9ce4f1151dbd1 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 27 Feb 2025 19:12:48 +0000 Subject: [PATCH 14/47] chore(release): 9.0.0-rc.3 [skip ci] ## [9.0.0-rc.3](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.2...v9.0.0-rc.3) (2025-02-27) ### Bug Fixes * export promoteChannel ([#1474](https://github.com/GetStream/stream-chat-js/issues/1474)) ([f2ba914](https://github.com/GetStream/stream-chat-js/commit/f2ba9141fc5375e00706d653dc96f122896631f9)) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d547385aa..0fc9b7b28e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [9.0.0-rc.3](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.2...v9.0.0-rc.3) (2025-02-27) + +### Bug Fixes + +* export promoteChannel ([#1474](https://github.com/GetStream/stream-chat-js/issues/1474)) ([f2ba914](https://github.com/GetStream/stream-chat-js/commit/f2ba9141fc5375e00706d653dc96f122896631f9)) + ## [9.0.0-rc.2](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.1...v9.0.0-rc.2) (2025-02-27) ### Bug Fixes From e34bfc562bc35ba111fff82e98fa9170635cca94 Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Mon, 3 Mar 2025 16:34:22 +0100 Subject: [PATCH 15/47] test(user-agent): fix StreamChat.getUserAgent post-merge --- test/unit/client.js | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/test/unit/client.js b/test/unit/client.js index 5c18766d84..2d6a3a1a68 100644 --- a/test/unit/client.js +++ b/test/unit/client.js @@ -643,10 +643,19 @@ describe('StreamChat.queryChannels', async () => { }); describe('X-Stream-Client header', () => { - process.env.PKG_VERSION = '1.2.3'; - process.env.CLIENT_BUNDLE = 'browser-esm'; let client; + before(() => { + process.env.PKG_VERSION = '1.2.3'; + process.env.CLIENT_BUNDLE = 'browser-esm'; + }); + + after(() => { + // clean up + process.env.PKG_VERSION = undefined; + process.env.CLIENT_BUNDLE = undefined; + }); + beforeEach(async () => { client = await getClientWithUser(); }); @@ -664,7 +673,7 @@ describe('X-Stream-Client header', () => { expect(userAgent).to.be.equal('stream-chat-js-v1.2.3-browser|client_bundle=browser-esm'); }); - it('SDK integration without deviceIdentifier', () => { + it('SDK integration', () => { client.sdkIdentifier = { name: 'react', version: '2.3.4' }; const userAgent = client.getUserAgent(); @@ -676,15 +685,9 @@ describe('X-Stream-Client header', () => { client.deviceIdentifier = { os: 'iOS 15.0', model: 'iPhone17,4' }; const userAgent = client.getUserAgent(); - expect(userAgent).to.be.equal('stream-chat-react-native-v2.3.4-llc-v1.2.3|os=iOS 15.0|device_model=iPhone17,4'); - }); - - it('SDK integration with process.env.CLIENT_BUNDLE', () => { - process.env.CLIENT_BUNDLE = 'browser'; - client.sdkIdentifier = { name: 'react', version: '2.3.4' }; - const userAgent = client.getUserAgent(); - - expect(userAgent).to.be.equal('stream-chat-react-v2.3.4-llc-v1.2.3|client_bundle=browser'); + expect(userAgent).to.be.equal( + 'stream-chat-react-native-v2.3.4-llc-v1.2.3|os=iOS 15.0|device_model=iPhone17,4|client_bundle=browser-esm', + ); }); it('setUserAgent is now deprecated', () => { From 4b2b5736de0671173be9ab059ec3715841e77e90 Mon Sep 17 00:00:00 2001 From: Anton Arnautov <43254280+arnautov-anton@users.noreply.github.com> Date: Wed, 5 Mar 2025 11:51:41 +0100 Subject: [PATCH 16/47] chore: upgrade Prettier and ESLint (#1478) ## Description of the changes, What, Why and How? - upgraded Prettier to latest available version, adjusted configuration - upgraded ESLint to latest available version, added new configuration - dropped unused packages: - ESLint plugins (`eslint-config-prettier`, `eslint-plugin-markdown`, `eslint-plugin-prettier`, `eslint-plugin-sonarjs`, `eslint-plugin-typescript-sort-keys`) - `standard-version` - deprecated (previously used for versioning and CHANGELOG.md generation) - replaced `@typescript-eslint/eslint-plugin` & `@typescript-eslint/parser` with `typescript-eslint` - commit linting now happens through separate CI job (`pr-check.yml`), using `@commitlint/cli` - used both for `commit-msg` Husky hook and PR title check (with the same rules specified in `.commitlintrc.json`) #### Notable Changes: - added `GITHUB_TOKEN` for `@semantic-release/github` - use different @stream-ci-bot instead of @semantic-release-bot (1c313e8d29e9af32b1f646f3ad166d25c6bf48b1) - as part of this PR I've decided to migrate Mocha-based tests to Vitest, see [here](https://github.com/GetStream/stream-chat-js/pull/1478/commits/e68eb413570e722aef4feafed66ca3396a0ba6fc) --- .babelrc | 21 - .commitlintrc.js | 3 - .commitlintrc.json | 4 + .eslintignore | 5 - .eslintrc.json | 74 - .github/actions/setup-node/action.yml | 15 +- .github/workflows/lint.yml | 7 +- .github/workflows/pr-check.yml | 16 + .github/workflows/release.yml | 1 + .github/workflows/scheduled_test.yml | 2 +- .github/workflows/size.yml | 2 +- .husky/commit-msg | 3 + .husky/pre-commit | 2 + .lintstagedrc.fix.json | 4 +- .lintstagedrc.json | 4 +- .mocharc.json | 7 - .prettierignore | 1 - .prettierrc | 12 +- README.md | 14 +- babel-register.js | 2 - eslint.config.mjs | 88 + package.json | 64 +- scripts/bundle.mjs | 2 +- scripts/get-package-version.mjs | 4 +- src/base64.ts | 6 +- src/campaign.ts | 10 +- src/channel.ts | 427 +- src/channel_manager.ts | 83 +- src/channel_state.ts | 159 +- src/client.ts | 825 +++- src/client_state.ts | 4 +- src/connection.ts | 113 +- src/connection_fallback.ts | 36 +- src/custom_types.ts | 2 - src/errors.ts | 14 +- src/index.ts | 15 +- src/insights.ts | 17 +- src/moderation.ts | 137 +- src/permissions.ts | 29 +- src/poll.ts | 147 +- src/poll_manager.ts | 52 +- src/search_controller.ts | 43 +- src/segment.ts | 36 +- src/signing.ts | 14 +- src/store.ts | 26 +- src/thread.ts | 92 +- src/thread_manager.ts | 40 +- src/token_manager.ts | 19 +- src/types.ts | 351 +- src/utils.ts | 135 +- test/typescript/index.js | 171 +- .../typescript/response-generators/channel.js | 16 +- test/typescript/response-generators/client.js | 9 +- test/typescript/unit-test.ts | 55 +- test/typescript/utils.js | 53 +- test/unit/{channel.js => channel.test.js} | 211 +- test/unit/channel_manager.test.ts | 900 ++-- ...channel_state.js => channel_state.test.js} | 214 +- test/unit/{client.js => client.test.js} | 136 +- .../{client_state.js => client_state.test.js} | 6 +- .../{connection.js => connection.test.js} | 17 +- ...allback.js => connection_fallback.test.js} | 105 +- test/unit/{errors.js => errors.test.js} | 3 +- test/unit/poll.test.js | 127 +- test/unit/poll_manager.test.ts | 128 +- test/unit/search_controller.test.js | 13 +- test/unit/{signing.js => signing.test.js} | 9 +- test/unit/test-utils/generateChannel.js | 4 +- .../unit/test-utils/generateThreadResponse.js | 3 - test/unit/test-utils/getClient.js | 9 +- test/unit/threads.test.ts | 275 +- test/unit/{utils.js => utils.test.js} | 1093 ++--- test/unit/utils.test.ts | 307 +- tsconfig.test.json | 13 - vite.config.ts | 11 + yarn.lock | 3793 +++++++++-------- 76 files changed, 6562 insertions(+), 4308 deletions(-) delete mode 100644 .babelrc delete mode 100644 .commitlintrc.js create mode 100644 .commitlintrc.json delete mode 100644 .eslintignore delete mode 100644 .eslintrc.json create mode 100644 .github/workflows/pr-check.yml create mode 100644 .husky/commit-msg delete mode 100644 .mocharc.json delete mode 100644 babel-register.js create mode 100644 eslint.config.mjs rename test/unit/{channel.js => channel.test.js} (91%) rename test/unit/{channel_state.js => channel_state.test.js} (88%) rename test/unit/{client.js => client.test.js} (85%) rename test/unit/{client_state.js => client_state.test.js} (89%) rename test/unit/{connection.js => connection.test.js} (94%) rename test/unit/{connection_fallback.js => connection_fallback.test.js} (86%) rename test/unit/{errors.js => errors.test.js} (92%) rename test/unit/{signing.js => signing.test.js} (77%) rename test/unit/{utils.js => utils.test.js} (97%) delete mode 100644 tsconfig.test.json create mode 100644 vite.config.ts diff --git a/.babelrc b/.babelrc deleted file mode 100644 index d8738ccfe2..0000000000 --- a/.babelrc +++ /dev/null @@ -1,21 +0,0 @@ -{ - "presets": [ - [ - "@babel/preset-env", - { - "targets": { - "browsers": ["last 2 versions", "ie >= 10"] - } - } - ], - "@babel/preset-typescript" - ], - "sourceType": "unambiguous", - "plugins": [ - "@babel/plugin-transform-object-assign", - "@babel/plugin-transform-runtime", - "@babel/proposal-object-rest-spread", - "@babel/plugin-proposal-class-properties" - ], - "ignore": ["node_modules", "dist"] -} diff --git a/.commitlintrc.js b/.commitlintrc.js deleted file mode 100644 index 4c73b71e60..0000000000 --- a/.commitlintrc.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - extends: ['@commitlint/config-conventional'], -}; diff --git a/.commitlintrc.json b/.commitlintrc.json new file mode 100644 index 0000000000..b4eb7e9922 --- /dev/null +++ b/.commitlintrc.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://json.schemastore.org/commitlintrc.json", + "extends": ["@commitlint/config-conventional"] +} diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 6c4492f2ad..0000000000 --- a/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -/dist -/node_modules -/test -scripts/get_changelog_diff.js -*.md diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 775cd5349c..0000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "plugins": ["markdown", "sonarjs", "prettier", "@typescript-eslint", "typescript-sort-keys"], - "extends": ["eslint:recommended", "plugin:sonarjs/recommended", "plugin:@typescript-eslint/recommended", "prettier"], - "rules": { - "@typescript-eslint/ban-ts-comment": 0, - "@typescript-eslint/explicit-module-boundary-types": 0, - "@typescript-eslint/ban-types": 0, - "@typescript-eslint/no-this-alias": 0, - "@typescript-eslint/triple-slash-reference": 0, - "no-console": 0, - "no-class-assign": 0, - "no-mixed-spaces-and-tabs": 0, - "comma-dangle": 0, - "no-unused-vars": 0, - "@typescript-eslint/no-unused-vars": 1, - "eqeqeq": [2, "smart"], - "no-useless-concat": 2, - "default-case": 2, - "no-self-compare": 2, - "prefer-const": 2, - "object-shorthand": 1, - "array-callback-return": 2, - "valid-typeof": 2, - "react/prop-types": 0, - "no-var": 2, - "linebreak-style": [2, "unix"], - "semi": [1, "always"], - "no-buffer-constructor": "error", - "sonarjs/cognitive-complexity": ["error", 30], - "sonarjs/no-duplicate-string": 0, - "typescript-sort-keys/interface": [ - "error", - "asc", - { - "caseSensitive": false, - "natural": true, - "requiredFirst": true - } - ], - "typescript-sort-keys/string-enum": [ - "error", - "asc", - { - "caseSensitive": false, - "natural": true - } - ] - }, - "env": { - "es6": true, - "node": true, - "browser": true - }, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "sourceType": "module", - "ecmaVersion": 2018, - "ecmaFeatures": { - "modules": true - } - }, - "overrides": [ - { - "files": ["*.md"], - "rules": { - "react/jsx-no-undef": 0, - "react/react-in-jsx-scope": 0, - "no-unused-vars": 0, - "semi": 0, - "no-undef": 0 - } - } - ] -} diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml index f437c730d5..052feeac9f 100644 --- a/.github/actions/setup-node/action.yml +++ b/.github/actions/setup-node/action.yml @@ -3,12 +3,12 @@ description: Sets up Node and Build SDK inputs: node-version: - description: "Specify Node version" + description: 'Specify Node version' required: false - default: "22" + default: '22' runs: - using: "composite" + using: 'composite' steps: - name: Setup Node uses: actions/setup-node@v3 @@ -20,13 +20,12 @@ runs: shell: bash run: echo "NODE_VERSION=$(node --version)" >> $GITHUB_ENV - - name: Cache Dependencies + - name: Cache dependencies uses: actions/cache@v3 with: path: ./node_modules - key: ${{ runner.os }}-${{ inputs.node-version }}-modules-${{ hashFiles('**/yarn.lock') }} - restore-keys: ${{ runner.os }}-${{ inputs.node-version }}-modules- + key: ${{ runner.os }}-${{ env.NODE_VERSION }}-modules-${{ hashFiles('./yarn.lock') }} - - name: Install Dependencies & Build - run: yarn install --frozen-lockfile --ignore-engines + - name: Install dependencies + run: yarn install --frozen-lockfile shell: bash diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7099251a0e..e7d26b5475 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3,7 +3,7 @@ on: pull_request: types: [opened, synchronize, reopened, edited] -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.head_ref }} cancel-in-progress: true @@ -11,12 +11,9 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/setup-node - - name: Commit message lint - run: echo "${{ github.event.pull_request.title }}" | yarn commitlinter - - name: Lint run: yarn lint diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml new file mode 100644 index 0000000000..b1918d269a --- /dev/null +++ b/.github/workflows/pr-check.yml @@ -0,0 +1,16 @@ +name: Check PR title + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +jobs: + pr-title: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: ./.github/actions/setup-node + + - name: commitlint + run: echo "${{ github.event.pull_request.title }}" | npx commitlint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 57be904d6e..9b732f1966 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,6 +29,7 @@ jobs: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # https://github.com/stream-ci-bot GH_TOKEN: ${{ secrets.DOCUSAURUS_GH_TOKEN }} + GITHUB_TOKEN: ${{ secrets.DOCUSAURUS_GH_TOKEN }} HUSKY: 0 run: > yarn semantic-release diff --git a/.github/workflows/scheduled_test.yml b/.github/workflows/scheduled_test.yml index 40304cf064..5d28846048 100644 --- a/.github/workflows/scheduled_test.yml +++ b/.github/workflows/scheduled_test.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: schedule: # Monday at 9:00 UTC - - cron: "0 9 * * 1" + - cron: '0 9 * * 1' jobs: test: diff --git a/.github/workflows/size.yml b/.github/workflows/size.yml index a022ad3e44..c67f11ab33 100644 --- a/.github/workflows/size.yml +++ b/.github/workflows/size.yml @@ -6,7 +6,7 @@ on: - 'test/**' - '**.md' -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.head_ref }} cancel-in-progress: true diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000000..57fafac341 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,3 @@ +#!/usr/bin/env sh + +npx commitlint --edit $1 diff --git a/.husky/pre-commit b/.husky/pre-commit index afd56763ac..5a6501e5a0 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,3 +1,5 @@ +#!/usr/bin/env sh + if ! yarn run lint-staged; then echo echo "Some files were not formatted correctly (see output above), commit aborted!" diff --git a/.lintstagedrc.fix.json b/.lintstagedrc.fix.json index 8c7234801f..d1bfd3b962 100644 --- a/.lintstagedrc.fix.json +++ b/.lintstagedrc.fix.json @@ -1,4 +1,4 @@ { - "{**/*.{js,ts,md,css,scss,json}, .eslintrc.json, .prettierrc, .babelrc}": "prettier --write", - "**/*.{js,md,ts}": "eslint --fix" + "**/*.{json,js,mjs,ts,yml,md}": "prettier --write", + "**/*.{js,mjs,ts}": "eslint --fix" } diff --git a/.lintstagedrc.json b/.lintstagedrc.json index 8bdbf1e89a..92e367ee66 100644 --- a/.lintstagedrc.json +++ b/.lintstagedrc.json @@ -1,4 +1,4 @@ { - "{**/*.{js,ts,md,css,scss,json}, .eslintrc.json, .prettierrc, .babelrc}": "prettier --list-different", - "**/*.{js,md,ts}, !test": "eslint --max-warnings 0" + "**/*.{json,js,mjs,ts,yml,md}": "prettier --list-different", + "**/*.{js,mjs,ts}, !test": "eslint --max-warnings 0" } diff --git a/.mocharc.json b/.mocharc.json deleted file mode 100644 index 4b1283fcee..0000000000 --- a/.mocharc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/mocharc", - "bail": true, - "exit": true, - "timeout": 20000, - "require": ["ts-node/register"] -} diff --git a/.prettierignore b/.prettierignore index 9a6d504a19..bdcfb56e38 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,6 +1,5 @@ /dist /node_modules /.vscode -/types CHANGELOG.md test/typescript/data.ts \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index e92009c67a..48f0a723df 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,17 +1,17 @@ { - "useTabs": false, - "printWidth": 120, - "tabWidth": 2, + "arrowParens": "always", + "jsxSingleQuote": true, + "printWidth": 90, "singleQuote": true, + "tabWidth": 2, "trailingComma": "all", - "jsxBracketSameLine": false, + "useTabs": false, "semi": true, "overrides": [ { "files": "*.js", "options": { - "useTabs": true, - "tabWidth": 4 + "useTabs": true } } ] diff --git a/README.md b/README.md index de40b906de..2f891b7678 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,9 @@ await client.upsertUser({ }); // create a channel -const channel = client.channel('messaging', 'test-channel', { created_by_id: 'vishal-1' }); +const channel = client.channel('messaging', 'test-channel', { + created_by_id: 'vishal-1', +}); await channel.create(); // send message @@ -84,10 +86,16 @@ declare module 'stream-chat' { // index.ts // property `profile_picture` is code-completed and expects type `string | undefined` -await client.partialUpdateUser({ id: 'vishal-1', set: { profile_picture: 'https://random.picture/1.jpg' } }); +await client.partialUpdateUser({ + id: 'vishal-1', + set: { profile_picture: 'https://random.picture/1.jpg' }, +}); // property `custom_property` is code-completed and expects type `number | undefined` -const { message } = await channel.sendMessage({ text: 'This is another test message', custom_property: 255 }); +const { message } = await channel.sendMessage({ + text: 'This is another test message', + custom_property: 255, +}); message.custom_property; // in the response object as well ``` diff --git a/babel-register.js b/babel-register.js deleted file mode 100644 index 0e4c872068..0000000000 --- a/babel-register.js +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-var-requires -require('@babel/register')({ extensions: ['.js', '.ts'] }); diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000000..426b580b15 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,88 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +import importPlugin from 'eslint-plugin-import'; + +export default tseslint.config( + { + ignores: ['dist', 'src/@types', '*.{js,ts}'], + }, + { + name: 'default', + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['src/**/*.{js,ts}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + import: importPlugin, + }, + settings: { + react: { + version: 'detect', + }, + }, + rules: { + camelcase: 'off', + semi: ['warn', 'always'], + eqeqeq: ['error', 'smart'], + 'array-callback-return': 'error', + 'arrow-body-style': 'error', + 'comma-dangle': 'off', + 'default-case': 'error', + 'jsx-quotes': ['error', 'prefer-single'], + 'linebreak-style': ['error', 'unix'], + 'no-console': 'off', + 'no-mixed-spaces-and-tabs': 'warn', + 'no-self-compare': 'error', + 'no-underscore-dangle': 'off', + 'no-use-before-define': 'off', + 'no-useless-concat': 'error', + 'no-var': 'error', + 'no-script-url': 'error', + 'no-continue': 'off', + 'object-shorthand': 'warn', + 'prefer-const': 'warn', + 'require-await': 'error', + 'sort-imports': [ + 'error', + { + allowSeparatedGroups: true, + ignoreCase: true, + ignoreDeclarationSort: true, + ignoreMemberSort: false, + memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'], + }, + ], + 'sort-keys': 'off', + 'valid-typeof': 'error', + 'max-classes-per-file': 'off', + 'no-unused-expressions': 'off', + 'import/prefer-default-export': 'off', + 'import/extensions': 'off', + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: true, // TODO: set to false once React is in the dependencies (not devDependencies) + optionalDependencies: false, + peerDependencies: false, + }, + ], + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { ignoreRestSiblings: false, caughtErrors: 'none' }, + ], + '@typescript-eslint/no-unsafe-function-type': 'error', + '@typescript-eslint/no-wrapper-object-types': 'error', + '@typescript-eslint/no-non-null-assertion': 'error', + 'no-empty-function': 'off', + '@typescript-eslint/no-empty-function': 'warn', + '@typescript-eslint/no-require-imports': 'off', // TODO: remove this rule once all files are .mjs (and require is not used) + '@typescript-eslint/consistent-type-imports': 'error', + '@typescript-eslint/no-empty-object-type': 'off', + }, + }, +); diff --git a/package.json b/package.json index 56011c2913..00321498b5 100644 --- a/package.json +++ b/package.json @@ -59,71 +59,53 @@ "ws": "^8.18.1" }, "devDependencies": { - "@commitlint/cli": "^16.0.2", - "@commitlint/config-conventional": "^16.0.0", + "@commitlint/cli": "^19.7.1", + "@commitlint/config-conventional": "^19.7.1", + "@eslint/js": "^9.21.0", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@types/base64-js": "^1.3.0", - "@types/chai": "^4.2.15", - "@types/chai-arrays": "^2.0.0", - "@types/chai-as-promised": "^7.1.4", - "@types/chai-like": "^1.1.1", - "@types/eslint": "7.2.7", - "@types/mocha": "^9.0.0", - "@types/node": "^16.11.11", - "@types/prettier": "^2.2.2", "@types/sinon": "^10.0.6", "@types/uuid": "^10.0.0", - "@typescript-eslint/eslint-plugin": "^4.17.0", - "@typescript-eslint/parser": "^4.17.0", - "chai": "^4.3.4", - "chai-arrays": "^2.2.0", - "chai-as-promised": "^7.1.1", - "chai-like": "^1.1.1", - "chai-sorted": "^0.2.0", "concurrently": "^9.1.2", "conventional-changelog-conventionalcommits": "^8.0.0", "dotenv": "^8.2.0", "esbuild": "^0.25.0", - "eslint": "7.21.0", - "eslint-config-prettier": "^8.1.0", - "eslint-plugin-markdown": "^2.0.0", - "eslint-plugin-prettier": "^3.3.1", - "eslint-plugin-sonarjs": "^0.6.0", - "eslint-plugin-typescript-sort-keys": "1.5.0", - "husky": "^4.3.8", + "eslint": "^9.21.0", + "eslint-plugin-import": "^2.31.0", + "globals": "^16.0.0", + "husky": "^9.1.7", "lint-staged": "^15.2.2", - "mocha": "^11.1.0", "nyc": "^15.1.0", - "prettier": "^2.2.1", + "prettier": "^3.5.2", "semantic-release": "^24.2.3", "sinon": "^12.0.1", - "standard-version": "^9.3.2", - "ts-node": "^10.9.2", "tslib": "^2.8.1", "typescript": "^5.7.3", - "uuid": "^8.3.2" + "typescript-eslint": "^8.25.0", + "uuid": "^11.1.0", + "vitest": "^3.0.7" }, "scripts": { - "start": "tsc --watch", - "commitlinter": "commitlint", "build": "rm -rf dist && yarn bundle", "bundle": "concurrently 'tsc --declaration --emitDeclarationOnly --outDir ./dist/types' ./scripts/bundle.mjs", + "start": "tsc --watch", "types": "tsc --noEmit", - "prettier": "prettier --check '**/*.{js,ts,md,css,scss,json}' .eslintrc.json .prettierrc .babelrc", - "prettier-fix": "npx prettier --write '**/*.{js,ts,md,css,scss,json}' .eslintrc.json .prettierrc .babelrc", - "test-types": "node test/typescript/index.js && tsc --esModuleInterop true --noEmit true --strictNullChecks true --noImplicitAny true --strict true test/typescript/*.ts", - "eslint": "eslint '**/*.{js,md,ts}' --max-warnings 0 --ignore-path ./.eslintignore", - "eslint-fix": "npx eslint --fix '**/*.{js,md,ts}' --max-warnings 0 --ignore-path ./.eslintignore", - "test-unit": "NODE_ENV=test TS_NODE_PROJECT=tsconfig.test.json mocha test/unit/*.{js,test.ts}", - "test-coverage": "nyc yarn test-unit", - "test": "yarn test-unit", - "testwatch": "NODE_ENV=test nodemon ./node_modules/.bin/mocha --timeout 20000 --require test-entry.js test/test.js", "lint": "yarn run prettier && yarn run eslint", "lint-fix": "yarn run prettier-fix && yarn run eslint-fix", + "prettier": "prettier '**/*.{json,js,mjs,ts,yml,md}' --check", + "prettier-fix": "yarn run prettier --write", + "eslint": "eslint --max-warnings 0", + "eslint-fix": "yarn run eslint --fix", + "test": "yarn test-unit", + "testwatch": "NODE_ENV=test nodemon ./node_modules/.bin/mocha --timeout 20000 --require test-entry.js test/test.js", + "test-types": "node test/typescript/index.js && tsc --esModuleInterop true --noEmit true --strictNullChecks true --noImplicitAny true --strict true test/typescript/*.ts", + "test-unit": "vitest test/unit/* --run", + "test-coverage": "nyc yarn test-unit", "fix-staged": "lint-staged --config .lintstagedrc.fix.json --concurrent 1", "semantic-release": "semantic-release", - "prepack": "yarn run build" + "prepack": "yarn run build", + "prepare": "husky" }, "engines": { "node": ">=18" diff --git a/scripts/bundle.mjs b/scripts/bundle.mjs index f421f3fe14..6bb37fac07 100755 --- a/scripts/bundle.mjs +++ b/scripts/bundle.mjs @@ -2,7 +2,7 @@ import { resolve } from 'node:path'; import * as esbuild from 'esbuild'; -import packageJson from '../package.json' with {'type': 'json'}; +import packageJson from '../package.json' with { type: 'json' }; import getPackageVersion from './get-package-version.mjs'; // import.meta.dirname is not available before Node 20 diff --git a/scripts/get-package-version.mjs b/scripts/get-package-version.mjs index 8dc8e437eb..339058fa08 100644 --- a/scripts/get-package-version.mjs +++ b/scripts/get-package-version.mjs @@ -12,7 +12,9 @@ export default function getPackageVersion() { version = execSync('git describe --tags --abbrev=0').toString().trim(); } catch (error) { console.error(error); - console.warn('Could not get latest version from git tags, falling back to package.json'); + console.warn( + 'Could not get latest version from git tags, falling back to package.json', + ); version = packageJson.version; } } diff --git a/src/base64.ts b/src/base64.ts index 040eb552df..472ba73aec 100644 --- a/src/base64.ts +++ b/src/base64.ts @@ -17,7 +17,10 @@ function isMapStringCallback( // source - https://github.com/beatgammit/base64-js/blob/master/test/convert.js#L72 function map(array: T[], callback: MapGenericCallback): U[]; function map(string: string, callback: MapStringCallback): U[]; -function map(arrayOrString: string | T[], callback: MapGenericCallback | MapStringCallback): U[] { +function map( + arrayOrString: string | T[], + callback: MapGenericCallback | MapStringCallback, +): U[] { const res = []; if (isString(arrayOrString) && isMapStringCallback(arrayOrString, callback)) { @@ -67,6 +70,7 @@ export const decodeBase64 = (s: string): string => { b = (b << 6) + c; l += 6; while (l >= 8) { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions ((a = (b >>> (l -= 8)) & 0xff) || x < L - 2) && (r += w(a)); } } diff --git a/src/campaign.ts b/src/campaign.ts index 217bfb2009..b9634c854e 100644 --- a/src/campaign.ts +++ b/src/campaign.ts @@ -1,5 +1,5 @@ -import { StreamChat } from './client'; -import { CampaignData, GetCampaignOptions } from './types'; +import type { StreamChat } from './client'; +import type { CampaignData, GetCampaignOptions } from './types'; export class Campaign { id: string | null; @@ -47,7 +47,7 @@ export class Campaign { return await this.client.startCampaign(this.id as string, options); } - async update(data: Partial) { + update(data: Partial) { this.verifyCampaignId(); return this.client.updateCampaign(this.id as string, data); @@ -59,13 +59,13 @@ export class Campaign { return await this.client.deleteCampaign(this.id as string); } - async stop() { + stop() { this.verifyCampaignId(); return this.client.stopCampaign(this.id as string); } - async get(options?: GetCampaignOptions) { + get(options?: GetCampaignOptions) { this.verifyCampaignId(); return this.client.getCampaign(this.id as string, options); diff --git a/src/channel.ts b/src/channel.ts index d22d2b03e4..c59e18bdcc 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -1,9 +1,16 @@ import { ChannelState } from './channel_state'; -import { generateChannelTempCid, logChatPromiseExecution, messageSetPagination, normalizeQuerySort } from './utils'; -import { StreamChat } from './client'; +import { + generateChannelTempCid, + logChatPromiseExecution, + messageSetPagination, + normalizeQuerySort, +} from './utils'; +import type { StreamChat } from './client'; import { DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE } from './constants'; import type { + AIState, APIResponse, + AscDesc, BanUserOptions, ChannelAPIResponse, ChannelData, @@ -30,6 +37,7 @@ import type { MemberSort, Message, MessageFilters, + MessageOptions, MessagePaginationOptions, MessageResponse, MessageSetType, @@ -38,8 +46,12 @@ import type { PartialUpdateChannel, PartialUpdateChannelAPIResponse, PartialUpdateMember, + PartialUpdateMemberAPIResponse, PinnedMessagePaginationOptions, PinnedMessagesSort, + PollVoteData, + PushPreference, + QueryChannelAPIResponse, QueryMembersOptions, Reaction, ReactionAPIResponse, @@ -48,19 +60,12 @@ import type { SearchOptions, SearchPayload, SendMessageAPIResponse, + SendMessageOptions, TruncateChannelAPIResponse, TruncateOptions, UpdateChannelAPIResponse, - UserResponse, - QueryChannelAPIResponse, - PollVoteData, - SendMessageOptions, - AscDesc, - PartialUpdateMemberAPIResponse, - AIState, - MessageOptions, - PushPreference, UpdateChannelOptions, + UserResponse, } from './types'; import type { Role } from './permissions'; import type { CustomChannelData } from './custom_types'; @@ -109,7 +114,12 @@ export class Channel { * * @return {Channel} Returns a new uninitialized channel */ - constructor(client: StreamChat, type: string, id: string | undefined, data: ChannelData) { + constructor( + client: StreamChat, + type: string, + id: string | undefined, + data: ChannelData, + ) { const validTypeRe = /^[\w_-]+$/; const validIDRe = /^[\w!_-]+$/; @@ -174,10 +184,13 @@ export class Channel { * @return {Promise} The Server Response */ async sendMessage(message: Message, options?: SendMessageOptions) { - return await this.getClient().post(this._channelURL() + '/message', { - message, - ...options, - }); + return await this.getClient().post( + this._channelURL() + '/message', + { + message, + ...options, + }, + ); } sendFile( @@ -186,11 +199,28 @@ export class Channel { contentType?: string, user?: UserResponse, ) { - return this.getClient().sendFile(`${this._channelURL()}/file`, uri, name, contentType, user); + return this.getClient().sendFile( + `${this._channelURL()}/file`, + uri, + name, + contentType, + user, + ); } - sendImage(uri: string | NodeJS.ReadableStream | File, name?: string, contentType?: string, user?: UserResponse) { - return this.getClient().sendFile(`${this._channelURL()}/image`, uri, name, contentType, user); + sendImage( + uri: string | NodeJS.ReadableStream | File, + name?: string, + contentType?: string, + user?: UserResponse, + ) { + return this.getClient().sendFile( + `${this._channelURL()}/image`, + uri, + name, + contentType, + user, + ); } deleteFile(url: string) { @@ -240,7 +270,9 @@ export class Channel { const payload: SearchPayload = { filter_conditions: { cid: this.cid } as ChannelFilters, ...options, - sort: options.sort ? normalizeQuerySort(options.sort) : undefined, + sort: options.sort + ? normalizeQuerySort(options.sort) + : undefined, }; if (typeof query === 'string') { payload.query = query; @@ -252,9 +284,12 @@ export class Channel { // Make sure we wait for the connect promise if there is a pending one await this.getClient().wsPromise; - return await this.getClient().get(this.getClient().baseURL + '/search', { - payload, - }); + return await this.getClient().get( + this.getClient().baseURL + '/search', + { + payload, + }, + ); } /** @@ -267,7 +302,11 @@ export class Channel { * * @return {Promise} Query Members response */ - async queryMembers(filterConditions: MemberFilters, sort: MemberSort = [], options: QueryMembersOptions = {}) { + async queryMembers( + filterConditions: MemberFilters, + sort: MemberSort = [], + options: QueryMembersOptions = {}, + ) { let id: string | undefined; const type = this.type; let members: string[] | ChannelMemberResponse[] | undefined; @@ -277,16 +316,19 @@ export class Channel { members = this.data.members; } // Return a list of members - return await this.getClient().get(this.getClient().baseURL + '/members', { - payload: { - type, - id, - members, - sort: normalizeQuerySort(sort), - filter_conditions: filterConditions, - ...options, + return await this.getClient().get( + this.getClient().baseURL + '/members', + { + payload: { + type, + id, + members, + sort: normalizeQuerySort(sort), + filter_conditions: filterConditions, + ...options, + }, }, - }); + ); } /** @@ -349,7 +391,9 @@ export class Channel { deleteReaction(messageID: string, reactionType: string, user_id?: string) { this._checkInitialized(); if (!reactionType || !messageID) { - throw Error('Deleting a reaction requires specifying both the message and reaction type'); + throw Error( + 'Deleting a reaction requires specifying both the message and reaction type', + ); } const url = @@ -378,7 +422,10 @@ export class Channel { ) { // Strip out reserved names that will result in API errors. // TODO: this needs to be typed better - const reserved: Exclude[] = [ + const reserved: Exclude< + keyof (ChannelResponse & ChannelData), + keyof CustomChannelData + >[] = [ 'config', 'cid', 'created_by', @@ -410,11 +457,20 @@ export class Channel { * @return {Promise} */ async updatePartial(update: PartialUpdateChannel) { - const data = await this.getClient().patch(this._channelURL(), update); + const data = await this.getClient().patch( + this._channelURL(), + update, + ); const areCapabilitiesChanged = [...(data.channel.own_capabilities || [])].sort().join() !== - [...(Array.isArray(this.data?.own_capabilities) ? (this.data?.own_capabilities as string[]) : [])].sort().join(); + [ + ...(Array.isArray(this.data?.own_capabilities) + ? (this.data?.own_capabilities as string[]) + : []), + ] + .sort() + .join(); this.data = data.channel; // If the capabiltities are changed, we trigger the `capabilities.changed` event. if (areCapabilitiesChanged) { @@ -434,9 +490,12 @@ export class Channel { * @return {Promise} The server response */ async enableSlowMode(coolDownInterval: number) { - const data = await this.getClient().post(this._channelURL(), { - cooldown: coolDownInterval, - }); + const data = await this.getClient().post( + this._channelURL(), + { + cooldown: coolDownInterval, + }, + ); this.data = data.channel; return data; } @@ -447,9 +506,12 @@ export class Channel { * @return {Promise} The server response */ async disableSlowMode() { - const data = await this.getClient().post(this._channelURL(), { - cooldown: 0, - }); + const data = await this.getClient().post( + this._channelURL(), + { + cooldown: 0, + }, + ); this.data = data.channel; return data; } @@ -473,7 +535,10 @@ export class Channel { * @return {Promise} The server response */ async truncate(options: TruncateOptions = {}) { - return await this.getClient().post(this._channelURL() + '/truncate', options); + return await this.getClient().post( + this._channelURL() + '/truncate', + options, + ); } /** @@ -506,7 +571,11 @@ export class Channel { * @param {ChannelUpdateOptions} [options] Option object, configuration to control the behavior while updating * @return {Promise} The server response */ - async addMembers(members: string[] | Array, message?: Message, options: ChannelUpdateOptions = {}) { + async addMembers( + members: string[] | Array, + message?: Message, + options: ChannelUpdateOptions = {}, + ) { return await this._update({ add_members: members, message, ...options }); } @@ -518,7 +587,11 @@ export class Channel { * @param {ChannelUpdateOptions} [options] Option object, configuration to control the behavior while updating * @return {Promise} The server response */ - async addModerators(members: string[], message?: Message, options: ChannelUpdateOptions = {}) { + async addModerators( + members: string[], + message?: Message, + options: ChannelUpdateOptions = {}, + ) { return await this._update({ add_moderators: members, message, ...options }); } @@ -562,7 +635,11 @@ export class Channel { * @param {ChannelUpdateOptions} [options] Option object, configuration to control the behavior while updating * @return {Promise} The server response */ - async removeMembers(members: string[], message?: Message, options: ChannelUpdateOptions = {}) { + async removeMembers( + members: string[], + message?: Message, + options: ChannelUpdateOptions = {}, + ) { return await this._update({ remove_members: members, message, ...options }); } @@ -574,7 +651,11 @@ export class Channel { * @param {ChannelUpdateOptions} [options] Option object, configuration to control the behavior while updating * @return {Promise} The server response */ - async demoteModerators(members: string[], message?: Message, options: ChannelUpdateOptions = {}) { + async demoteModerators( + members: string[], + message?: Message, + options: ChannelUpdateOptions = {}, + ) { return await this._update({ demote_moderators: members, message, ...options }); } @@ -584,8 +665,11 @@ export class Channel { * @return {Promise} The server response * TODO: introduce new type instead of Object in the next major update */ - async _update(payload: Object) { - const data = await this.getClient().post(this._channelURL(), payload); + async _update(payload: object) { + const data = await this.getClient().post( + this._channelURL(), + payload, + ); this.data = data.channel; return data; } @@ -603,10 +687,13 @@ export class Channel { * */ async mute(opts: { expiration?: number; user_id?: string } = {}) { - return await this.getClient().post(this.getClient().baseURL + '/moderation/mute/channel', { - channel_cid: this.cid, - ...opts, - }); + return await this.getClient().post( + this.getClient().baseURL + '/moderation/mute/channel', + { + channel_cid: this.cid, + ...opts, + }, + ); } /** @@ -618,10 +705,13 @@ export class Channel { * await channel.unmute({user_id: userId}); */ async unmute(opts: { user_id?: string } = {}) { - return await this.getClient().post(this.getClient().baseURL + '/moderation/unmute/channel', { - channel_cid: this.cid, - ...opts, - }); + return await this.getClient().post( + this.getClient().baseURL + '/moderation/unmute/channel', + { + channel_cid: this.cid, + ...opts, + }, + ); } /** @@ -774,7 +864,11 @@ export class Channel { * @param state - The new state of the AI process (e.g., thinking, generating). * @param options - Optional parameters, such as `ai_message`, to include additional details in the event. */ - async updateAIState(messageId: string, state: AIState, options: { ai_message?: string } = {}) { + async updateAIState( + messageId: string, + state: AIState, + options: { ai_message?: string } = {}, + ) { await this.sendEvent({ ...options, type: 'ai_indicator.update', @@ -926,10 +1020,14 @@ export class Channel { this.initialized = true; this.data = state.channel; - this._client.logger('info', `channel:watch() - started watching channel ${this.cid}`, { - tags: ['channel'], - channel: this, - }); + this._client.logger( + 'info', + `channel:watch() - started watching channel ${this.cid}`, + { + tags: ['channel'], + channel: this, + }, + ); return state; } @@ -939,12 +1037,19 @@ export class Channel { * @return {Promise} The server response */ async stopWatching() { - const response = await this.getClient().post(this._channelURL() + '/stop-watching', {}); + const response = await this.getClient().post( + this._channelURL() + '/stop-watching', + {}, + ); - this._client.logger('info', `channel:watch() - stopped watching channel ${this.cid}`, { - tags: ['channel'], - channel: this, - }); + this._client.logger( + 'info', + `channel:watch() - stopped watching channel ${this.cid}`, + { + tags: ['channel'], + channel: this, + }, + ); return response; } @@ -993,12 +1098,15 @@ export class Channel { options: PinnedMessagePaginationOptions & { user?: UserResponse; user_id?: string }, sort: PinnedMessagesSort = [], ) { - return await this.getClient().get(this._channelURL() + '/pinned_messages', { - payload: { - ...options, - sort: normalizeQuerySort(sort), + return await this.getClient().get( + this._channelURL() + '/pinned_messages', + { + payload: { + ...options, + sort: normalizeQuerySort(sort), + }, }, - }); + ); } /** @@ -1026,9 +1134,12 @@ export class Channel { * @return {Promise} Server response */ getMessagesById(messageIds: string[]) { - return this.getClient().get(this._channelURL() + '/messages', { - ids: messageIds.join(','), - }); + return this.getClient().get( + this._channelURL() + '/messages', + { + ids: messageIds.join(','), + }, + ); } /** @@ -1047,10 +1158,14 @@ export class Channel { if (message.silent) return false; if (message.parent_id && !message.show_in_channel) return false; if (message.user?.id === this.getClient().userID) return false; - if (message.user?.id && this.getClient().userMuteStatus(message.user.id)) return false; + if (message.user?.id && this.getClient().userMuteStatus(message.user.id)) + return false; // Return false if channel doesn't allow read events. - if (Array.isArray(this.data?.own_capabilities) && !this.data?.own_capabilities.includes('read-events')) { + if ( + Array.isArray(this.data?.own_capabilities) && + !this.data?.own_capabilities.includes('read-events') + ) { return false; } @@ -1127,12 +1242,18 @@ export class Channel { * * @return {Promise} Returns a query response */ - async query(options: ChannelQueryOptions = {}, messageSetToAddToIfDoesNotExist: MessageSetType = 'current') { + async query( + options: ChannelQueryOptions = {}, + messageSetToAddToIfDoesNotExist: MessageSetType = 'current', + ) { // Make sure we wait for the connect promise if there is a pending one await this.getClient().wsPromise; const createdById = - options.created_by?.id ?? options.created_by_id ?? this._data?.created_by?.id ?? this._data?.created_by_id; + options.created_by?.id ?? + options.created_by_id ?? + this._data?.created_by?.id ?? + this._data?.created_by_id; if (this.getClient()._isUsingServerAuth() && typeof createdById !== 'string') { this.getClient().logger( @@ -1146,11 +1267,14 @@ export class Channel { queryURL += `/${encodeURIComponent(this.id)}`; } - const state = await this.getClient().post(queryURL + '/query', { - data: this._data, - state: true, - ...options, - }); + const state = await this.getClient().post( + queryURL + '/query', + { + data: this._data, + state: true, + ...options, + }, + ); // update the channel id if it was missing if (!this.id) { @@ -1169,7 +1293,10 @@ export class Channel { delete this.getClient().activeChannels[tempChannelCid]; } - if (!(this.cid in this.getClient().activeChannels) && this.getClient()._cacheEnabled()) { + if ( + !(this.cid in this.getClient().activeChannels) && + this.getClient()._cacheEnabled() + ) { this.getClient().activeChannels[this.cid] = this; } } @@ -1183,7 +1310,8 @@ export class Channel { ...messageSetPagination({ parentSet: messageSet, messagePaginationOptions: options?.messages, - requestedPageSize: options?.messages?.limit ?? DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE, + requestedPageSize: + options?.messages?.limit ?? DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE, returnedPage: state.messages, logger: this.getClient().logger, }), @@ -1193,7 +1321,13 @@ export class Channel { const areCapabilitiesChanged = [...(state.channel.own_capabilities || [])].sort().join() !== - [...(this.data && Array.isArray(this.data?.own_capabilities) ? this.data.own_capabilities : [])].sort().join(); + [ + ...(this.data && Array.isArray(this.data?.own_capabilities) + ? this.data.own_capabilities + : []), + ] + .sort() + .join(); this.data = state.channel; this.offlineMode = false; @@ -1313,7 +1447,10 @@ export class Channel { * @returns {Promise} */ async createCall(options: CreateCallOptions) { - return await this.getClient().post(this._channelURL() + '/call', options); + return await this.getClient().post( + this._channelURL() + '/call', + options, + ); } /** @@ -1342,25 +1479,36 @@ export class Channel { */ on(eventType: EventTypes, callback: EventHandler): { unsubscribe: () => void }; on(callback: EventHandler): { unsubscribe: () => void }; - on(callbackOrString: EventHandler | EventTypes, callbackOrNothing?: EventHandler): { unsubscribe: () => void } { + on( + callbackOrString: EventHandler | EventTypes, + callbackOrNothing?: EventHandler, + ): { unsubscribe: () => void } { const key = callbackOrNothing ? (callbackOrString as string) : 'all'; const callback = callbackOrNothing ? callbackOrNothing : callbackOrString; if (!(key in this.listeners)) { this.listeners[key] = []; } - this._client.logger('info', `Attaching listener for ${key} event on channel ${this.cid}`, { - tags: ['event', 'channel'], - channel: this, - }); + this._client.logger( + 'info', + `Attaching listener for ${key} event on channel ${this.cid}`, + { + tags: ['event', 'channel'], + channel: this, + }, + ); this.listeners[key].push(callback); return { unsubscribe: () => { - this._client.logger('info', `Removing listener for ${key} event from channel ${this.cid}`, { - tags: ['event', 'channel'], - channel: this, - }); + this._client.logger( + 'info', + `Removing listener for ${key} event from channel ${this.cid}`, + { + tags: ['event', 'channel'], + channel: this, + }, + ); this.listeners[key] = this.listeners[key].filter((el) => el !== callback); }, @@ -1373,22 +1521,29 @@ export class Channel { */ off(eventType: EventTypes, callback: EventHandler): void; off(callback: EventHandler): void; - off(callbackOrString: EventHandler | EventTypes, callbackOrNothing?: EventHandler): void { + off( + callbackOrString: EventHandler | EventTypes, + callbackOrNothing?: EventHandler, + ): void { const key = callbackOrNothing ? (callbackOrString as string) : 'all'; const callback = callbackOrNothing ? callbackOrNothing : callbackOrString; if (!(key in this.listeners)) { this.listeners[key] = []; } - this._client.logger('info', `Removing listener for ${key} event from channel ${this.cid}`, { - tags: ['event', 'channel'], - channel: this, - }); + this._client.logger( + 'info', + `Removing listener for ${key} event from channel ${this.cid}`, + { + tags: ['event', 'channel'], + channel: this, + }, + ); this.listeners[key] = this.listeners[key].filter((value) => value !== callback); } - // eslint-disable-next-line sonarjs/cognitive-complexity _handleChannelEvent(event: Event) { + // eslint-disable-next-line @typescript-eslint/no-this-alias const channel = this; this._client.logger( 'info', @@ -1453,7 +1608,8 @@ export class Channel { if (event.message) { /* if message belongs to current user, always assume timestamp is changed to filter it out and add again to avoid duplication */ const ownMessage = event.user?.id === this.getClient().user?.id; - const isThreadMessage = event.message.parent_id && !event.message.show_in_channel; + const isThreadMessage = + event.message.parent_id && !event.message.show_in_channel; if (this.state.isUpToDate || isThreadMessage) { channelState.addMessageSorted(event.message, ownMessage); @@ -1506,12 +1662,14 @@ export class Channel { channelState.messageSets.forEach((messageSet, messageSetIndex) => { messageSet.messages.forEach(({ created_at: createdAt, id }) => { - if (truncatedAt > +createdAt) channelState.removeMessage({ id, messageSetIndex }); + if (truncatedAt > +createdAt) + channelState.removeMessage({ id, messageSetIndex }); }); }); channelState.pinnedMessages.forEach(({ id, created_at: createdAt }) => { - if (truncatedAt > +createdAt) channelState.removePinnedMessage({ id } as MessageResponse); + if (truncatedAt > +createdAt) + channelState.removePinnedMessage({ id } as MessageResponse); }); } else { channelState.clearMessages(); @@ -1575,14 +1733,17 @@ export class Channel { } case 'channel.updated': if (event.channel) { - const isFrozenChanged = event.channel?.frozen !== undefined && event.channel.frozen !== channel.data?.frozen; + const isFrozenChanged = + event.channel?.frozen !== undefined && + event.channel.frozen !== channel.data?.frozen; if (isFrozenChanged) { this.query({ state: false, messages: { limit: 0 }, watchers: { limit: 0 } }); } channel.data = { ...event.channel, hidden: event.channel?.hidden ?? channel.data?.hidden, - own_capabilities: event.channel?.own_capabilities ?? channel.data?.own_capabilities, + own_capabilities: + event.channel?.own_capabilities ?? channel.data?.own_capabilities, }; } break; @@ -1639,6 +1800,7 @@ export class Channel { } _callChannelListeners = (event: Event) => { + // eslint-disable-next-line @typescript-eslint/no-this-alias const channel = this; // gather and call the listeners const listeners = []; @@ -1670,15 +1832,21 @@ export class Channel { }; _checkInitialized() { - if (!this.initialized && !this.offlineMode && !this.getClient()._isUsingServerAuth()) { + if ( + !this.initialized && + !this.offlineMode && + !this.getClient()._isUsingServerAuth() + ) { throw Error( `Channel ${this.cid} hasn't been initialized yet. Make sure to call .watch() and wait for it to resolve`, ); } } - // eslint-disable-next-line sonarjs/cognitive-complexity - _initializeState(state: ChannelAPIResponse, messageSetToAddToIfDoesNotExist: MessageSetType = 'latest') { + _initializeState( + state: ChannelAPIResponse, + messageSetToAddToIfDoesNotExist: MessageSetType = 'latest', + ) { const { state: clientState, user, userID } = this.getClient(); // add the members and users @@ -1698,7 +1866,13 @@ export class Channel { if (!this.state.messages) { this.state.initMessages(); } - const { messageSet } = this.state.addMessagesSorted(messages, false, true, true, messageSetToAddToIfDoesNotExist); + const { messageSet } = this.state.addMessagesSorted( + messages, + false, + true, + true, + messageSetToAddToIfDoesNotExist, + ); if (!this.state.pinnedMessages) { this.state.pinnedMessages = []; @@ -1778,12 +1952,15 @@ export class Channel { */ overrideCurrentState?: boolean; }) { - const newMembersById = members.reduce((membersById, member) => { - if (member.user) { - membersById[member.user.id] = member; - } - return membersById; - }, {}); + const newMembersById = members.reduce( + (membersById, member) => { + if (member.user) { + membersById[member.user.id] = member; + } + return membersById; + }, + {}, + ); if (overrideCurrentState) { this.state.members = newMembersById; @@ -1796,10 +1973,14 @@ export class Channel { } _disconnect() { - this._client.logger('info', `channel:disconnect() - Disconnecting the channel ${this.cid}`, { - tags: ['connection', 'channel'], - channel: this, - }); + this._client.logger( + 'info', + `channel:disconnect() - Disconnecting the channel ${this.cid}`, + { + tags: ['connection', 'channel'], + channel: this, + }, + ); this.disconnected = true; this.state.setIsUpToDate(false); diff --git a/src/channel_manager.ts b/src/channel_manager.ts index b02c801e94..0d5f0bcaf3 100644 --- a/src/channel_manager.ts +++ b/src/channel_manager.ts @@ -1,7 +1,14 @@ import type { StreamChat } from './client'; -import type { Event, ChannelOptions, ChannelStateOptions, ChannelFilters, ChannelSort } from './types'; -import { StateStore, ValueOrPatch, isPatch } from './store'; -import { Channel } from './channel'; +import type { + ChannelFilters, + ChannelOptions, + ChannelSort, + ChannelStateOptions, + Event, +} from './types'; +import type { ValueOrPatch } from './store'; +import { isPatch, StateStore } from './store'; +import type { Channel } from './channel'; import { extractSortValue, findLastPinnedChannelIndex, @@ -42,7 +49,9 @@ export type GenericEventHandlerType = ( ...args: T ) => void | (() => void) | ((...args: T) => Promise) | Promise; export type EventHandlerType = GenericEventHandlerType<[Event]>; -export type EventHandlerOverrideType = GenericEventHandlerType<[ChannelSetterType, Event]>; +export type EventHandlerOverrideType = GenericEventHandlerType< + [ChannelSetterType, Event] +>; export type ChannelManagerEventTypes = | 'notification.added_to_channel' @@ -183,7 +192,9 @@ export class ChannelManager { public setChannels = (valueOrFactory: ChannelSetterParameterType) => { this.state.next((current) => { const { channels: currentChannels } = current; - const newChannels = isPatch(valueOrFactory) ? valueOrFactory(currentChannels) : valueOrFactory; + const newChannels = isPatch(valueOrFactory) + ? valueOrFactory(currentChannels) + : valueOrFactory; // If the references between the two values are the same, just return the // current state; otherwise trigger a state change. @@ -194,7 +205,9 @@ export class ChannelManager { }); }; - public setEventHandlerOverrides = (eventHandlerOverrides: ChannelManagerEventHandlerOverrides = {}) => { + public setEventHandlerOverrides = ( + eventHandlerOverrides: ChannelManagerEventHandlerOverrides = {}, + ) => { const truthyEventHandlerOverrides = Object.entries(eventHandlerOverrides).reduce< Partial >((acc, [key, value]) => { @@ -203,7 +216,9 @@ export class ChannelManager { } return acc; }, {}); - this.eventHandlerOverrides = new Map(Object.entries(truthyEventHandlerOverrides)); + this.eventHandlerOverrides = new Map( + Object.entries(truthyEventHandlerOverrides), + ); }; public setOptions = (options: ChannelManagerOptions = {}) => { @@ -216,7 +231,10 @@ export class ChannelManager { options: ChannelOptions = {}, stateOptions: ChannelStateOptions = {}, ) => { - const { offset, limit } = { ...DEFAULT_CHANNEL_MANAGER_PAGINATION_OPTIONS, ...options }; + const { offset, limit } = { + ...DEFAULT_CHANNEL_MANAGER_PAGINATION_OPTIONS, + ...options, + }; const { pagination: { isLoading }, } = this.state.getLatestValue(); @@ -239,7 +257,12 @@ export class ChannelManager { }, })); - const channels = await this.client.queryChannels(filters, sort, options, stateOptions); + const channels = await this.client.queryChannels( + filters, + sort, + options, + stateOptions, + ); const newOffset = offset + (channels?.length ?? 0); const newOptions = { ...options, offset: newOffset }; const { pagination } = this.state.getLatestValue(); @@ -273,11 +296,19 @@ export class ChannelManager { } try { - const { offset, limit } = { ...DEFAULT_CHANNEL_MANAGER_PAGINATION_OPTIONS, ...options }; + const { offset, limit } = { + ...DEFAULT_CHANNEL_MANAGER_PAGINATION_OPTIONS, + ...options, + }; this.state.partialNext({ pagination: { ...pagination, isLoading: false, isLoadingNext: true }, }); - const nextChannels = await this.client.queryChannels(filters, sort, options, this.stateOptions); + const nextChannels = await this.client.queryChannels( + filters, + sort, + options, + this.stateOptions, + ); const { channels } = this.state.getLatestValue(); const newOffset = offset + (nextChannels?.length ?? 0); const newOptions = { ...options, offset: newOffset }; @@ -305,7 +336,12 @@ export class ChannelManager { private notificationAddedToChannelHandler = async (event: Event) => { const { id, type, members } = event?.channel ?? {}; - if (!type || !this.options.allowNotLoadedChannelPromotionForEvent?.['notification.added_to_channel']) { + if ( + !type || + !this.options.allowNotLoadedChannelPromotionForEvent?.[ + 'notification.added_to_channel' + ] + ) { return; } @@ -345,7 +381,9 @@ export class ChannelManager { } const newChannels = [...channels]; - const channelIndex = newChannels.findIndex((channel) => channel.cid === (event.cid || event.channel?.cid)); + const channelIndex = newChannels.findIndex( + (channel) => channel.cid === (event.cid || event.channel?.cid), + ); if (channelIndex < 0) { return; @@ -391,7 +429,8 @@ export class ChannelManager { // list order is locked this.options.lockChannelOrder || // target channel is not within the loaded list and loading from cache is disallowed - (!targetChannelExistsWithinList && !this.options.allowNotLoadedChannelPromotionForEvent?.['message.new']) + (!targetChannelExistsWithinList && + !this.options.allowNotLoadedChannelPromotionForEvent?.['message.new']) ) { return; } @@ -500,7 +539,11 @@ export class ChannelManager { const considerArchivedChannels = shouldConsiderArchivedChannels(filters); const pinnedAtSort = extractSortValue({ atIndex: 0, sort, targetKey: 'pinned_at' }); - if (!channels || (!considerPinnedChannels && !considerArchivedChannels) || this.options.lockChannelOrder) { + if ( + !channels || + (!considerPinnedChannels && !considerArchivedChannels) || + this.options.lockChannelOrder + ) { return; } @@ -535,7 +578,8 @@ export class ChannelManager { if (pinnedAtSort === 1 || (pinnedAtSort === -1 && !isTargetChannelPinned)) { lastPinnedChannelIndex = findLastPinnedChannelIndex({ channels: newChannels }); } - const newTargetChannelIndex = typeof lastPinnedChannelIndex === 'number' ? lastPinnedChannelIndex + 1 : 0; + const newTargetChannelIndex = + typeof lastPinnedChannelIndex === 'number' ? lastPinnedChannelIndex + 1 : 0; // skip state update if the position of the channel does not change if (channels[newTargetChannelIndex] === targetChannel) { @@ -547,7 +591,8 @@ export class ChannelManager { }; private subscriptionOrOverride = (event: Event) => { - const handlerName = channelManagerEventToHandlerMapping[event.type as ChannelManagerEventTypes]; + const handlerName = + channelManagerEventToHandlerMapping[event.type as ChannelManagerEventTypes]; const defaultEventHandler = this.eventHandlers.get(handlerName); const eventHandlerOverride = this.eventHandlerOverrides.get(handlerName); if (eventHandlerOverride && typeof eventHandlerOverride === 'function') { @@ -567,7 +612,9 @@ export class ChannelManager { } for (const eventType of Object.keys(channelManagerEventToHandlerMapping)) { - this.unsubscribeFunctions.add(this.client.on(eventType, this.subscriptionOrOverride).unsubscribe); + this.unsubscribeFunctions.add( + this.client.on(eventType, this.subscriptionOrOverride).unsubscribe, + ); } }; diff --git a/src/channel_state.ts b/src/channel_state.ts index 938254a5bf..06c1027393 100644 --- a/src/channel_state.ts +++ b/src/channel_state.ts @@ -1,5 +1,5 @@ -import { Channel } from './channel'; -import { +import type { Channel } from './channel'; +import type { ChannelMemberResponse, Event, FormatMessageResponse, @@ -78,7 +78,10 @@ export class ChannelState { * be pushed on to message list. */ this.isUpToDate = true; - this.last_message_at = channel?.state?.last_message_at != null ? new Date(channel.state.last_message_at) : null; + this.last_message_at = + channel?.state?.last_message_at != null + ? new Date(channel.state.last_message_at) + : null; } get messages() { @@ -104,7 +107,10 @@ export class ChannelState { } get messagePagination() { - return this.messageSets.find((s) => s.isCurrent)?.pagination || DEFAULT_MESSAGE_SET_PAGINATION; + return ( + this.messageSets.find((s) => s.isCurrent)?.pagination || + DEFAULT_MESSAGE_SET_PAGINATION + ); } /** @@ -182,7 +188,9 @@ export class ChannelState { * handle updates to user, we can use the reference map, to determine which * channels need to be updated with updated user object. */ - this._channel.getClient().state.updateUserReference(message.user, this._channel.cid); + this._channel + .getClient() + .state.updateUserReference(message.user, this._channel.cid); } if (initializing && message.id && this.threads[message.id]) { @@ -280,11 +288,19 @@ export class ChannelState { this.pinnedMessages = result; } - addReaction(reaction: ReactionResponse, message?: MessageResponse, enforce_unique?: boolean) { + addReaction( + reaction: ReactionResponse, + message?: MessageResponse, + enforce_unique?: boolean, + ) { if (!message) return; const messageWithReaction = message; this._updateMessage(message, (msg) => { - messageWithReaction.own_reactions = this._addOwnReactionToMessage(msg.own_reactions, reaction, enforce_unique); + messageWithReaction.own_reactions = this._addOwnReactionToMessage( + msg.own_reactions, + reaction, + enforce_unique, + ); return this.formatMessage(messageWithReaction); }); return messageWithReaction; @@ -309,9 +325,14 @@ export class ChannelState { return ownReactions; } - _removeOwnReactionFromMessage(ownReactions: ReactionResponse[] | null | undefined, reaction: ReactionResponse) { + _removeOwnReactionFromMessage( + ownReactions: ReactionResponse[] | null | undefined, + reaction: ReactionResponse, + ) { if (ownReactions) { - return ownReactions.filter((item) => item.user_id !== reaction.user_id || item.type !== reaction.type); + return ownReactions.filter( + (item) => item.user_id !== reaction.user_id || item.type !== reaction.type, + ); } return ownReactions; } @@ -320,25 +341,37 @@ export class ChannelState { if (!message) return; const messageWithReaction = message; this._updateMessage(message, (msg) => { - messageWithReaction.own_reactions = this._removeOwnReactionFromMessage(msg.own_reactions, reaction); + messageWithReaction.own_reactions = this._removeOwnReactionFromMessage( + msg.own_reactions, + reaction, + ); return this.formatMessage(messageWithReaction); }); return messageWithReaction; } - _updateQuotedMessageReferences({ message, remove }: { message: MessageResponse; remove?: boolean }) { + _updateQuotedMessageReferences({ + message, + remove, + }: { + message: MessageResponse; + remove?: boolean; + }) { const parseMessage = (m: ReturnType) => - (({ + ({ ...m, created_at: m.created_at.toISOString(), pinned_at: m.pinned_at?.toISOString(), updated_at: m.updated_at?.toISOString(), - } as unknown) as MessageResponse); + }) as unknown as MessageResponse; const update = (messages: FormatMessageResponse[]) => { const updatedMessages = messages.reduce((acc, msg) => { if (msg.quoted_message_id === message.id) { - acc.push({ ...parseMessage(msg), quoted_message: remove ? { ...message, attachments: [] } : message }); + acc.push({ + ...parseMessage(msg), + quoted_message: remove ? { ...message, attachments: [] } : message, + }); } return acc; }, []); @@ -369,7 +402,9 @@ export class ChannelState { pinned?: boolean; show_in_channel?: boolean; }, - updateFunc: (msg: ReturnType) => ReturnType, + updateFunc: ( + msg: ReturnType, + ) => ReturnType, ) { const { parent_id, show_in_channel, pinned } = message; @@ -385,7 +420,9 @@ export class ChannelState { if ((!show_in_channel && !parent_id) || show_in_channel) { const messageSetIndex = this.findMessageSetIndex(message); if (messageSetIndex !== -1) { - const msgIndex = this.messageSets[messageSetIndex].messages.findIndex((msg) => msg.id === message.id); + const msgIndex = this.messageSets[messageSetIndex].messages.findIndex( + (msg) => msg.id === message.id, + ); if (msgIndex !== -1) { this.messageSets[messageSetIndex].messages[msgIndex] = updateFunc( this.messageSets[messageSetIndex].messages[msgIndex], @@ -430,7 +467,13 @@ export class ChannelState { sortBy: 'pinned_at' | 'created_at' = 'created_at', addIfDoesNotExist = true, ) { - return addToMessageList(messages, message, timestampChanged, sortBy, addIfDoesNotExist); + return addToMessageList( + messages, + message, + timestampChanged, + sortBy, + addIfDoesNotExist, + ); } /** @@ -440,7 +483,11 @@ export class ChannelState { * * @return {boolean} Returns if the message was removed */ - removeMessage(messageToRemove: { id: string; messageSetIndex?: number; parent_id?: string }) { + removeMessage(messageToRemove: { + id: string; + messageSetIndex?: number; + parent_id?: string; + }) { let isRemoved = false; if (messageToRemove.parent_id && this.threads[messageToRemove.parent_id]) { const { removed, result: threadMessages } = this.removeMessageFromArray( @@ -451,7 +498,8 @@ export class ChannelState { this.threads[messageToRemove.parent_id] = threadMessages; isRemoved = removed; } else { - const messageSetIndex = messageToRemove.messageSetIndex ?? this.findMessageSetIndex(messageToRemove); + const messageSetIndex = + messageToRemove.messageSetIndex ?? this.findMessageSetIndex(messageToRemove); if (messageSetIndex !== -1) { const { removed, result: messages } = this.removeMessageFromArray( this.messageSets[messageSetIndex].messages, @@ -469,7 +517,9 @@ export class ChannelState { msgArray: Array>, msg: { id: string; parent_id?: string }, ) => { - const result = msgArray.filter((message) => !(!!message.id && !!msg.id && message.id === msg.id)); + const result = msgArray.filter( + (message) => !(!!message.id && !!msg.id && message.id === msg.id), + ); return { removed: result.length < msgArray.length, result }; }; @@ -480,7 +530,10 @@ export class ChannelState { * @param {UserResponse} user */ updateUserMessages = (user: UserResponse) => { - const _updateUserMessages = (messages: Array>, user: UserResponse) => { + const _updateUserMessages = ( + messages: Array>, + user: UserResponse, + ) => { for (let i = 0; i < messages.length; i++) { const m = messages[i]; if (m.user?.id === user.id) { @@ -521,7 +574,7 @@ export class ChannelState { * In case of hard delete, we need to strip down all text, html, * attachments and all the custom properties on message */ - messages[i] = ({ + messages[i] = { cid: m.cid, created_at: m.created_at, deleted_at: user.deleted_at, @@ -536,7 +589,7 @@ export class ChannelState { type: 'deleted', updated_at: m.updated_at, user: m.user, - } as unknown) as ReturnType; + } as unknown as ReturnType; } else { messages[i] = { ...m, @@ -547,7 +600,9 @@ export class ChannelState { } }; - this.messageSets.forEach((set) => _deleteUserMessages(set.messages, user, hardDelete)); + this.messageSets.forEach((set) => + _deleteUserMessages(set.messages, user, hardDelete), + ); for (const parentId in this.threads) { _deleteUserMessages(this.threads[parentId], user, hardDelete); @@ -561,7 +616,9 @@ export class ChannelState { * */ filterErrorMessages() { - const filteredMessages = this.latestMessages.filter((message) => message.type !== 'error'); + const filteredMessages = this.latestMessages.filter( + (message) => message.type !== 'error', + ); this.latestMessages = filteredMessages; } @@ -594,7 +651,14 @@ export class ChannelState { } initMessages() { - this.messageSets = [{ messages: [], isLatest: true, isCurrent: true, pagination: DEFAULT_MESSAGE_SET_PAGINATION }]; + this.messageSets = [ + { + messages: [], + isLatest: true, + isCurrent: true, + pagination: DEFAULT_MESSAGE_SET_PAGINATION, + }, + ]; } /** @@ -604,7 +668,11 @@ export class ChannelState { * @param {string} parentMessageId The id of the parent message, if we want load a thread reply * @param {number} limit The page size if the message has to be queried from the server */ - async loadMessageIntoState(messageId: string | 'latest', parentMessageId?: string, limit = 25) { + async loadMessageIntoState( + messageId: string | 'latest', + parentMessageId?: string, + limit = 25, + ) { let messageSetIndex: number; let switchedToMessageSet = false; let loadedMessageThread = false; @@ -621,12 +689,17 @@ export class ChannelState { this.switchToMessageSet(messageSetIndex); switchedToMessageSet = true; } - loadedMessageThread = !parentMessageId || !!this.threads[parentMessageId]?.find((m) => m.id === messageId); + loadedMessageThread = + !parentMessageId || + !!this.threads[parentMessageId]?.find((m) => m.id === messageId); if (switchedToMessageSet && loadedMessageThread) { return; } if (!switchedToMessageSet) { - await this._channel.query({ messages: { id_around: messageIdToFind, limit } }, 'new'); + await this._channel.query( + { messages: { id_around: messageIdToFind, limit } }, + 'new', + ); } if (!loadedMessageThread && parentMessageId) { await this._channel.getReplies(parentMessageId, { id_around: messageId, limit }); @@ -670,12 +743,17 @@ export class ChannelState { this.messageSets[index].isCurrent = true; } - private areMessageSetsOverlap(messages1: Array<{ id: string }>, messages2: Array<{ id: string }>) { + private areMessageSetsOverlap( + messages1: Array<{ id: string }>, + messages2: Array<{ id: string }>, + ) { return messages1.some((m1) => messages2.find((m2) => m1.id === m2.id)); } private findMessageSetIndex(message: { id?: string }) { - return this.messageSets.findIndex((set) => !!set.messages.find((m) => m.id === message.id)); + return this.messageSets.findIndex( + (set) => !!set.messages.find((m) => m.id === message.id), + ); } private findTargetMessageSet( @@ -683,12 +761,15 @@ export class ChannelState { addIfDoesNotExist = true, messageSetToAddToIfDoesNotExist: MessageSetType = 'current', ) { - let messagesToAdd: (MessageResponse | ReturnType)[] = newMessages; + let messagesToAdd: (MessageResponse | ReturnType)[] = + newMessages; let targetMessageSetIndex!: number; if (addIfDoesNotExist) { const overlappingMessageSetIndices = this.messageSets .map((_, i) => i) - .filter((i) => this.areMessageSetsOverlap(this.messageSets[i].messages, newMessages)); + .filter((i) => + this.areMessageSetsOverlap(this.messageSets[i].messages, newMessages), + ); switch (messageSetToAddToIfDoesNotExist) { case 'new': if (overlappingMessageSetIndices.length > 0) { @@ -716,13 +797,18 @@ export class ChannelState { // when merging the target set will be the first one from the overlapping message sets const mergeTargetMessageSetIndex = overlappingMessageSetIndices.splice(0, 1)[0]; const mergeSourceMessageSetIndices = [...overlappingMessageSetIndices]; - if (mergeTargetMessageSetIndex !== undefined && mergeTargetMessageSetIndex !== targetMessageSetIndex) { + if ( + mergeTargetMessageSetIndex !== undefined && + mergeTargetMessageSetIndex !== targetMessageSetIndex + ) { mergeSourceMessageSetIndices.push(targetMessageSetIndex); } // merge message sets if (mergeSourceMessageSetIndices.length > 0) { const target = this.messageSets[mergeTargetMessageSetIndex]; - const sources = this.messageSets.filter((_, i) => mergeSourceMessageSetIndices.indexOf(i) !== -1); + const sources = this.messageSets.filter( + (_, i) => mergeSourceMessageSetIndices.indexOf(i) !== -1, + ); sources.forEach((messageSet) => { target.isLatest = target.isLatest || messageSet.isLatest; target.isCurrent = target.isCurrent || messageSet.isCurrent; @@ -731,7 +817,8 @@ export class ChannelState { ? messageSet.pagination.hasPrev : target.pagination.hasPrev; target.pagination.hasNext = - target.messages.slice(-1)[0].created_at < messageSet.messages.slice(-1)[0].created_at + target.messages.slice(-1)[0].created_at < + messageSet.messages.slice(-1)[0].created_at ? messageSet.pagination.hasNext : target.pagination.hasNext; messagesToAdd = [...messagesToAdd, ...messageSet.messages]; diff --git a/src/client.ts b/src/client.ts index 29baea3aca..5b0785aa2d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,7 +1,8 @@ /* eslint no-unused-vars: "off" */ /* global process */ -import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; +import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; +import axios from 'axios'; import https from 'https'; import type WebSocket from 'isomorphic-ws'; @@ -29,7 +30,7 @@ import { sleep, } from './utils'; -import { +import type { APIErrorResponse, APIResponse, AppSettings, @@ -80,7 +81,6 @@ import { Device, DeviceIdentifier, EndpointName, - ErrorFromResponse, Event, EventHandler, ExportChannelOptions, @@ -212,13 +212,18 @@ import { UserSort, VoteSort, } from './types'; +import { ErrorFromResponse } from './types'; import { InsightMetrics, postInsights } from './insights'; import { Thread } from './thread'; import { Moderation } from './moderation'; import { ThreadManager } from './thread_manager'; import { DEFAULT_QUERY_CHANNELS_MESSAGE_LIST_PAGE_SIZE } from './constants'; import { PollManager } from './poll_manager'; -import { ChannelManager, ChannelManagerEventHandlerOverrides, ChannelManagerOptions } from './channel_manager'; +import type { + ChannelManagerEventHandlerOverrides, + ChannelManagerOptions, +} from './channel_manager'; +import { ChannelManager } from './channel_manager'; function isString(x: unknown): x is string { return typeof x === 'string' || x instanceof String; @@ -299,7 +304,11 @@ export class StreamChat { */ constructor(key: string, options?: StreamChatOptions); constructor(key: string, secret?: string, options?: StreamChatOptions); - constructor(key: string, secretOrOptions?: StreamChatOptions | string, options?: StreamChatOptions) { + constructor( + key: string, + secretOrOptions?: StreamChatOptions | string, + options?: StreamChatOptions, + ) { // set the key this.key = key; this.listeners = {}; @@ -316,9 +325,16 @@ export class StreamChat { } // set the options... and figure out defaults... - const inputOptions = options ? options : secretOrOptions && !isString(secretOrOptions) ? secretOrOptions : {}; - - this.browser = typeof inputOptions.browser !== 'undefined' ? inputOptions.browser : typeof window !== 'undefined'; + const inputOptions = options + ? options + : secretOrOptions && !isString(secretOrOptions) + ? secretOrOptions + : {}; + + this.browser = + typeof inputOptions.browser !== 'undefined' + ? inputOptions.browser + : typeof window !== 'undefined'; this.node = !this.browser; this.options = { @@ -341,11 +357,19 @@ export class StreamChat { this.setBaseURL(this.options.baseURL || 'https://chat.stream-io-api.com'); - if (typeof process !== 'undefined' && 'env' in process && process.env.STREAM_LOCAL_TEST_RUN) { + if ( + typeof process !== 'undefined' && + 'env' in process && + process.env.STREAM_LOCAL_TEST_RUN + ) { this.setBaseURL('http://localhost:3030'); } - if (typeof process !== 'undefined' && 'env' in process && process.env.STREAM_LOCAL_TEST_HOST) { + if ( + typeof process !== 'undefined' && + 'env' in process && + process.env.STREAM_LOCAL_TEST_HOST + ) { this.setBaseURL('http://' + process.env.STREAM_LOCAL_TEST_HOST); } @@ -449,7 +473,11 @@ export class StreamChat { * StreamChat.getInstance('api_key', "secret", { httpsAgent: customAgent }) */ public static getInstance(key: string, options?: StreamChatOptions): StreamChat; - public static getInstance(key: string, secret?: string, options?: StreamChatOptions): StreamChat; + public static getInstance( + key: string, + secret?: string, + options?: StreamChatOptions, + ): StreamChat; public static getInstance( key: string, secretOrOptions?: StreamChatOptions | string, @@ -479,7 +507,8 @@ export class StreamChat { this.wsBaseURL = this.baseURL.replace('http', 'ws').replace(':3030', ':8800'); } - _getConnectionID = () => this.wsConnection?.connectionID || this.wsFallback?.connectionID; + _getConnectionID = () => + this.wsConnection?.connectionID || this.wsFallback?.connectionID; _hasConnectionID = () => Boolean(this._getConnectionID()); @@ -491,7 +520,10 @@ export class StreamChat { * * @return {ConnectAPIResponse} Returns a promise that resolves when the connection is setup */ - connectUser = async (user: OwnUserResponse | UserResponse, userTokenOrProvider: TokenOrProvider) => { + connectUser = async ( + user: OwnUserResponse | UserResponse, + userTokenOrProvider: TokenOrProvider, + ) => { if (!user.id) { throw new Error('The "id" field on the user is missing'); } @@ -513,7 +545,10 @@ export class StreamChat { ); } - if ((this._isUsingServerAuth() || this.node) && !this.options.allowServerSideConnect) { + if ( + (this._isUsingServerAuth() || this.node) && + !this.options.allowServerSideConnect + ) { console.warn( 'Please do not use connectUser server side. connectUser impacts MAU and concurrent connection usage and thus your bill. If you have a valid use-case, add "allowServerSideConnect: true" to the client options to disable this warning.', ); @@ -590,7 +625,10 @@ export class StreamChat { this.cleaningIntervalRef = undefined; } - await Promise.all([this.wsConnection?.disconnect(timeout), this.wsFallback?.disconnect(timeout)]); + await Promise.all([ + this.wsConnection?.disconnect(timeout), + this.wsFallback?.disconnect(timeout), + ]); return Promise.resolve(); }; @@ -608,16 +646,16 @@ export class StreamChat { }: { eventHandlerOverrides?: ChannelManagerEventHandlerOverrides; options?: ChannelManagerOptions; - }) => { - return new ChannelManager({ client: this, eventHandlerOverrides, options }); - }; + }) => new ChannelManager({ client: this, eventHandlerOverrides, options }); /** * Creates a new WebSocket connection with the current user. Returns empty promise, if there is an active connection */ - openConnection = async () => { + openConnection = () => { if (!this.userID) { - throw Error('User is not set on client, use client.connectUser or client.connectAnonymousUser instead'); + throw Error( + 'User is not set on client, use client.connectUser or client.connectAnonymousUser instead', + ); } if (this.wsConnection?.isConnecting && this.wsPromise) { @@ -627,10 +665,17 @@ export class StreamChat { return this.wsPromise; } - if ((this.wsConnection?.isHealthy || this.wsFallback?.isHealthy()) && this._hasConnectionID()) { - this.logger('info', 'client:openConnection() - openConnection called twice, healthy connection already exists', { - tags: ['connection', 'client'], - }); + if ( + (this.wsConnection?.isHealthy || this.wsFallback?.isHealthy()) && + this._hasConnectionID() + ) { + this.logger( + 'info', + 'client:openConnection() - openConnection called twice, healthy connection already exists', + { + tags: ['connection', 'client'], + }, + ); return; } @@ -695,7 +740,9 @@ export class StreamChat { } if (before === '') { - throw new Error("Don't pass blank string for since, use null instead if resetting the token revoke"); + throw new Error( + "Don't pass blank string for since, use null instead if resetting the token revoke", + ); } return before; @@ -768,7 +815,9 @@ export class StreamChat { ...(data.messageID ? { message_id: data.messageID } : {}), ...(data.apnTemplate ? { apn_template: data.apnTemplate } : {}), ...(data.firebaseTemplate ? { firebase_template: data.firebaseTemplate } : {}), - ...(data.firebaseDataTemplate ? { firebase_data_template: data.firebaseDataTemplate } : {}), + ...(data.firebaseDataTemplate + ? { firebase_data_template: data.firebaseDataTemplate } + : {}), ...(data.skipDevices ? { skip_devices: true } : {}), ...(data.pushProviderName ? { push_provider_name: data.pushProviderName } : {}), ...(data.pushProviderType ? { push_provider_type: data.pushProviderType } : {}), @@ -809,7 +858,7 @@ export class StreamChat { * @param timeout Max number of ms, to wait for close event of websocket, before forcefully assuming successful disconnection. * https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent */ - disconnectUser = async (timeout?: number) => { + disconnectUser = (timeout?: number) => { this.logger('info', 'client:disconnect() - Disconnecting the client', { tags: ['connection', 'client'], }); @@ -851,7 +900,10 @@ export class StreamChat { * connectAnonymousUser - Set an anonymous user and open a WebSocket connection */ connectAnonymousUser = () => { - if ((this._isUsingServerAuth() || this.node) && !this.options.allowServerSideConnect) { + if ( + (this._isUsingServerAuth() || this.node) && + !this.options.allowServerSideConnect + ) { console.warn( 'Please do not use connectUser server side. connectUser impacts MAU and concurrent connection usage and thus your bill. If you have a valid use-case, add "allowServerSideConnect: true" to the client options to disable this warning.', ); @@ -942,9 +994,14 @@ export class StreamChat { */ on(callback: EventHandler): { unsubscribe: () => void }; on(eventType: string, callback: EventHandler): { unsubscribe: () => void }; - on(callbackOrString: EventHandler | string, callbackOrNothing?: EventHandler): { unsubscribe: () => void } { + on( + callbackOrString: EventHandler | string, + callbackOrNothing?: EventHandler, + ): { unsubscribe: () => void } { const key = callbackOrNothing ? (callbackOrString as string) : 'all'; - const callback = callbackOrNothing ? callbackOrNothing : (callbackOrString as EventHandler); + const callback = callbackOrNothing + ? callbackOrNothing + : (callbackOrString as EventHandler); if (!(key in this.listeners)) { this.listeners[key] = []; } @@ -970,7 +1027,9 @@ export class StreamChat { off(eventType: string, callback: EventHandler): void; off(callbackOrString: EventHandler | string, callbackOrNothing?: EventHandler) { const key = callbackOrNothing ? (callbackOrString as string) : 'all'; - const callback = callbackOrNothing ? callbackOrNothing : (callbackOrString as EventHandler); + const callback = callbackOrNothing + ? callbackOrNothing + : (callbackOrString as EventHandler); if (!(key in this.listeners)) { this.listeners[key] = []; } @@ -998,11 +1057,15 @@ export class StreamChat { } _logApiResponse(type: string, url: string, response: AxiosResponse) { - this.logger('info', `client:${type} - Response - url: ${url} > status ${response.status}`, { - tags: ['api', 'api_response', 'client'], - url, - response, - }); + this.logger( + 'info', + `client:${type} - Response - url: ${url} > status ${response.status}`, + { + tags: ['api', 'api_response', 'client'], + url, + response, + }, + ); } _logApiError(type: string, url: string, error: unknown) { @@ -1061,7 +1124,10 @@ export class StreamChat { this.consecutiveFailures += 1; if (e.response) { /** connection_fallback depends on this token expiration logic */ - if (e.response.data.code === chatCodes.TOKEN_EXPIRED && !this.tokenManager.isStatic()) { + if ( + e.response.data.code === chatCodes.TOKEN_EXPIRED && + !this.tokenManager.isStatic() + ) { if (this.consecutiveFailures > 1) { await sleep(retryInterval(this.consecutiveFailures)); } @@ -1115,11 +1181,15 @@ export class StreamChat { }); } - errorFromResponse(response: AxiosResponse): ErrorFromResponse { + errorFromResponse( + response: AxiosResponse, + ): ErrorFromResponse { let err: ErrorFromResponse; err = new ErrorFromResponse(`StreamChat error HTTP code: ${response.status}`); if (response.data && response.data.code) { - err = new Error(`StreamChat error code ${response.data.code}: ${response.data.message}`); + err = new Error( + `StreamChat error code ${response.data.code}: ${response.data.message}`, + ); err.code = response.data.code; } err.response = response; @@ -1296,20 +1366,33 @@ export class StreamChat { this._updateUserMessageReferences(event.user); } - if (event.type === 'user.deleted' && event.user.deleted_at && (event.mark_messages_deleted || event.hard_delete)) { + if ( + event.type === 'user.deleted' && + event.user.deleted_at && + (event.mark_messages_deleted || event.hard_delete) + ) { this._deleteUserMessageReference(event.user, event.hard_delete); } }; _handleClientEvent(event: Event) { + // eslint-disable-next-line @typescript-eslint/no-this-alias const client = this; const postListenerCallbacks = []; - this.logger('info', `client:_handleClientEvent - Received event of type { ${event.type} }`, { - tags: ['event', 'client'], - event, - }); + this.logger( + 'info', + `client:_handleClientEvent - Received event of type { ${event.type} }`, + { + tags: ['event', 'client'], + event, + }, + ); - if (event.type === 'user.presence.changed' || event.type === 'user.updated' || event.type === 'user.deleted') { + if ( + event.type === 'user.presence.changed' || + event.type === 'user.updated' || + event.type === 'user.deleted' + ) { this._handleUserEvent(event); } @@ -1334,10 +1417,17 @@ export class StreamChat { if (event.type === 'notification.mark_read' && event.unread_channels === 0) { const activeChannelKeys = Object.keys(this.activeChannels); - activeChannelKeys.forEach((activeChannelKey) => (this.activeChannels[activeChannelKey].state.unreadCount = 0)); + activeChannelKeys.forEach( + (activeChannelKey) => + (this.activeChannels[activeChannelKey].state.unreadCount = 0), + ); } - if ((event.type === 'channel.deleted' || event.type === 'notification.channel_deleted') && event.cid) { + if ( + (event.type === 'channel.deleted' || + event.type === 'notification.channel_deleted') && + event.cid + ) { client.state.deleteAllChannelReference(event.cid); this.activeChannels[event.cid]?._disconnect(); @@ -1357,7 +1447,9 @@ export class StreamChat { const mute = this.mutedChannels[i]; if (mute.channel?.cid === cid) { muteStatus = { - muted: mute.expires ? new Date(mute.expires).getTime() > new Date().getTime() : true, + muted: mute.expires + ? new Date(mute.expires).getTime() > new Date().getTime() + : true, createdAt: mute.created_at ? new Date(mute.created_at) : new Date(), expiresAt: mute.expires ? new Date(mute.expires) : null, }; @@ -1377,6 +1469,7 @@ export class StreamChat { } _callClientListeners = (event: Event) => { + // eslint-disable-next-line @typescript-eslint/no-this-alias const client = this; // gather and call the listeners const listeners: Array<(event: Event) => void> = []; @@ -1394,19 +1487,33 @@ export class StreamChat { }; recoverState = async () => { - this.logger('info', `client:recoverState() - Start of recoverState with connectionID ${this._getConnectionID()}`, { - tags: ['connection'], - }); + this.logger( + 'info', + `client:recoverState() - Start of recoverState with connectionID ${this._getConnectionID()}`, + { + tags: ['connection'], + }, + ); const cids = Object.keys(this.activeChannels); if (cids.length && this.recoverStateOnReconnect) { - this.logger('info', `client:recoverState() - Start the querying of ${cids.length} channels`, { - tags: ['connection', 'client'], - }); + this.logger( + 'info', + `client:recoverState() - Start the querying of ${cids.length} channels`, + { + tags: ['connection', 'client'], + }, + ); - await this.queryChannels({ cid: { $in: cids } } as ChannelFilters, { last_message_at: -1 }, { limit: 30 }); + await this.queryChannels( + { cid: { $in: cids } } as ChannelFilters, + { last_message_at: -1 }, + { limit: 30 }, + ); - this.logger('info', 'client:recoverState() - Querying channels finished', { tags: ['connection', 'client'] }); + this.logger('info', 'client:recoverState() - Querying channels finished', { + tags: ['connection', 'client'], + }); this.dispatchEvent({ type: 'connection.recovered', } as Event); @@ -1425,7 +1532,9 @@ export class StreamChat { */ async connect() { if (!this.userID || !this._user) { - throw Error('Call connectUser or connectAnonymousUser before starting the connection'); + throw Error( + 'Call connectUser or connectAnonymousUser before starting the connection', + ); } if (!this.wsBaseURL) { throw Error('Websocket base url not set'); @@ -1440,8 +1549,8 @@ export class StreamChat { // The StableWSConnection handles all the reconnection logic. if (this.options.wsConnection && this.node) { // Intentionally avoiding adding ts generics on wsConnection in options since its only useful for unit test purpose. - ((this.options.wsConnection as unknown) as StableWSConnection).setClient(this); - this.wsConnection = (this.options.wsConnection as unknown) as StableWSConnection; + (this.options.wsConnection as unknown as StableWSConnection).setClient(this); + this.wsConnection = this.options.wsConnection as unknown as StableWSConnection; } else { this.wsConnection = new StableWSConnection({ client: this, @@ -1456,14 +1565,18 @@ export class StreamChat { // if WSFallback is enabled, ws connect should timeout faster so fallback can try return await this.wsConnection.connect( - this.options.enableWSFallback ? this.defaultWSTimeoutWithFallback : this.defaultWSTimeout, + this.options.enableWSFallback + ? this.defaultWSTimeoutWithFallback + : this.defaultWSTimeout, ); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { // run fallback only if it's WS/Network error and not a normal API error // make sure browser is online before even trying the longpoll if (this.options.enableWSFallback && isWSFailure(error) && isOnline()) { - this.logger('info', 'client:connect() - WS failed, fallback to longpoll', { tags: ['connection', 'client'] }); + this.logger('info', 'client:connect() - WS failed, fallback to longpoll', { + tags: ['connection', 'client'], + }); this.dispatchEvent({ type: 'transport.changed', mode: 'longpoll' }); this.wsConnection._destroyCurrentWSConnection(); @@ -1507,7 +1620,11 @@ export class StreamChat { * * @return {Promise<{ users: Array }>} User Query Response */ - async queryUsers(filterConditions: UserFilters, sort: UserSort = [], options: UserOptions = {}) { + async queryUsers( + filterConditions: UserFilters, + sort: UserSort = [], + options: UserOptions = {}, + ) { const defaultOptions = { presence: false, }; @@ -1520,14 +1637,17 @@ export class StreamChat { } // Return a list of users - const data = await this.get }>(this.baseURL + '/users', { - payload: { - filter_conditions: filterConditions, - sort: normalizeQuerySort(sort), - ...defaultOptions, - ...options, + const data = await this.get }>( + this.baseURL + '/users', + { + payload: { + filter_conditions: filterConditions, + sort: normalizeQuerySort(sort), + ...defaultOptions, + ...options, + }, }, - }); + ); this.state.updateUsers(data.users); @@ -1566,11 +1686,17 @@ export class StreamChat { * * @return {Promise} Message Flags Response */ - async queryMessageFlags(filterConditions: MessageFlagsFilters = {}, options: MessageFlagsPaginationOptions = {}) { + async queryMessageFlags( + filterConditions: MessageFlagsFilters = {}, + options: MessageFlagsPaginationOptions = {}, + ) { // Return a list of message flags - return await this.get(this.baseURL + '/moderation/flags/message', { - payload: { filter_conditions: filterConditions, ...options }, - }); + return await this.get( + this.baseURL + '/moderation/flags/message', + { + payload: { filter_conditions: filterConditions, ...options }, + }, + ); } /** @@ -1611,7 +1737,10 @@ export class StreamChat { ...options, }; - const data = await this.post(this.baseURL + '/channels', payload); + const data = await this.post( + this.baseURL + '/channels', + payload, + ); this.dispatchEvent({ type: 'channels.queried', @@ -1686,7 +1815,9 @@ export class StreamChat { ...updatedMessagesSet.pagination, ...messageSetPagination({ parentSet: updatedMessagesSet, - requestedPageSize: queryChannelsOptions?.message_limit || DEFAULT_QUERY_CHANNELS_MESSAGE_LIST_PAGE_SIZE, + requestedPageSize: + queryChannelsOptions?.message_limit || + DEFAULT_QUERY_CHANNELS_MESSAGE_LIST_PAGE_SIZE, returnedPage: channelState.messages, logger: this.logger, }), @@ -1709,14 +1840,20 @@ export class StreamChat { * * @return {Promise} search messages response */ - async search(filterConditions: ChannelFilters, query: string | MessageFilters, options: SearchOptions = {}) { + async search( + filterConditions: ChannelFilters, + query: string | MessageFilters, + options: SearchOptions = {}, + ) { if (options.offset && options.next) { throw Error(`Cannot specify offset with next`); } const payload: SearchPayload = { filter_conditions: filterConditions, ...options, - sort: options.sort ? normalizeQuerySort(options.sort) : undefined, + sort: options.sort + ? normalizeQuerySort(options.sort) + : undefined, }; if (typeof query === 'string') { payload.query = query; @@ -1743,7 +1880,8 @@ export class StreamChat { setLocalDevice(device: BaseDeviceFields) { if ( (this.wsConnection?.isConnecting && this.wsPromise) || - ((this.wsConnection?.isHealthy || this.wsFallback?.isHealthy()) && this._hasConnectionID()) + ((this.wsConnection?.isHealthy || this.wsFallback?.isHealthy()) && + this._hasConnectionID()) ) { throw new Error('you can only set device before opening a websocket connection'); } @@ -1760,7 +1898,12 @@ export class StreamChat { * @param {string} [push_provider_name] user provided push provider name for multi bundle support * */ - async addDevice(id: string, push_provider: PushProvider, userID?: string, push_provider_name?: string) { + async addDevice( + id: string, + push_provider: PushProvider, + userID?: string, + push_provider_name?: string, + ) { return await this.post(this.baseURL + '/devices', { id, push_provider, @@ -1791,7 +1934,10 @@ export class StreamChat { * @return {} */ async getUnreadCount(userID?: string) { - return await this.get(this.baseURL + '/unread', userID ? { user_id: userID } : {}); + return await this.get( + this.baseURL + '/unread', + userID ? { user_id: userID } : {}, + ); } /** @@ -1802,7 +1948,10 @@ export class StreamChat { * @return {} */ async getUnreadCountBatch(userIDs: string[]) { - return await this.post(this.baseURL + '/unread_batch', { user_ids: userIDs }); + return await this.post( + this.baseURL + '/unread_batch', + { user_ids: userIDs }, + ); } /** @@ -1813,7 +1962,10 @@ export class StreamChat { * @return {} */ async setPushPreferences(preferences: PushPreference[]) { - return await this.post(this.baseURL + '/push_preferences', { preferences }); + return await this.post( + this.baseURL + '/push_preferences', + { preferences }, + ); } /** @@ -1837,7 +1989,7 @@ export class StreamChat { * @param {object} [params] The params for the call. If none of the params are set, all limits for all platforms are returned. * @returns {Promise} */ - async getRateLimits(params?: { + getRateLimits(params?: { android?: boolean; endpoints?: EndpointName[]; ios?: boolean; @@ -1876,13 +2028,19 @@ export class StreamChat { */ channel(channelType: string, channelID?: string | null, custom?: ChannelData): Channel; channel(channelType: string, custom?: ChannelData): Channel; - channel(channelType: string, channelIDOrCustom?: string | ChannelData | null, custom: ChannelData = {}) { + channel( + channelType: string, + channelIDOrCustom?: string | ChannelData | null, + custom: ChannelData = {}, + ) { if (!this.userID && !this._isUsingServerAuth()) { throw Error('Call connectUser or connectAnonymousUser before creating a channel'); } if (~channelType.indexOf(':')) { - throw new Error(`Invalid channel group ${channelType}, can't contain the : character`); + throw new Error( + `Invalid channel group ${channelType}, can't contain the : character`, + ); } // support channel("messaging", {options}) @@ -1925,7 +2083,7 @@ export class StreamChat { // Check if the channel already exists. // Only allow 1 channel object per cid const memberIds = (custom.members ?? []).map((member: string | NewMemberPayload) => - typeof member === 'string' ? member : member.user_id ?? '', + typeof member === 'string' ? member : (member.user_id ?? ''), ); const membersStr = memberIds.sort().join(','); const tempCid = generateChannelTempCid(channelType, memberIds); @@ -1950,7 +2108,9 @@ export class StreamChat { } if (key.indexOf(`${channelType}:!members-`) === 0) { - const membersStrInExistingChannel = Object.keys(channel.state.members).sort().join(','); + const membersStrInExistingChannel = Object.keys(channel.state.members) + .sort() + .join(','); if (membersStrInExistingChannel === membersStr) { return channel; } @@ -1991,7 +2151,11 @@ export class StreamChat { // only allow 1 channel object per cid const cid = `${channelType}:${channelID}`; - if (cid in this.activeChannels && this.activeChannels[cid] && !this.activeChannels[cid].disconnected) { + if ( + cid in this.activeChannels && + this.activeChannels[cid] && + !this.activeChannels[cid].disconnected + ) { const channel = this.activeChannels[cid]; if (Object.keys(custom).length > 0) { channel.data = { ...channel.data, ...custom }; @@ -2146,7 +2310,10 @@ export class StreamChat { * @return {TaskResponse} A task ID */ async reactivateUsers(user_ids: string[], options?: ReactivateUsersOptions) { - return await this.post(this.baseURL + `/users/reactivate`, { user_ids, ...options }); + return await this.post( + this.baseURL + `/users/reactivate`, + { user_ids, ...options }, + ); } /** @@ -2173,7 +2340,10 @@ export class StreamChat { * @return {TaskResponse} A task ID */ async deactivateUsers(user_ids: string[], options?: DeactivateUsersOptions) { - return await this.post(this.baseURL + `/users/deactivate`, { user_ids, ...options }); + return await this.post( + this.baseURL + `/users/deactivate`, + { user_ids, ...options }, + ); } async exportUser(userID: string, options?: Record) { @@ -2305,7 +2475,10 @@ export class StreamChat { * @param {string} [options.user_id] currentUserID, only used with serverside auth * @returns {Promise} */ - async flagMessage(targetMessageID: string, options: { reason?: string; user_id?: string } = {}) { + async flagMessage( + targetMessageID: string, + options: { reason?: string; user_id?: string } = {}, + ) { return await this.post(this.baseURL + '/moderation/flag', { target_message_id: targetMessageID, ...options, @@ -2359,7 +2532,10 @@ export class StreamChat { * @returns {Promise} */ async getCallToken(callID: string, options: { user_id?: string } = {}) { - return await this.post(this.baseURL + `/calls/${encodeURIComponent(callID)}`, { ...options }); + return await this.post( + this.baseURL + `/calls/${encodeURIComponent(callID)}`, + { ...options }, + ); } /** @@ -2375,7 +2551,10 @@ export class StreamChat { * * @return {Promise} Flags Response */ - async _queryFlags(filterConditions: FlagsFilters = {}, options: FlagsPaginationOptions = {}) { + async _queryFlags( + filterConditions: FlagsFilters = {}, + options: FlagsPaginationOptions = {}, + ) { // Return a list of flags return await this.post(this.baseURL + '/moderation/flags', { filter_conditions: filterConditions, @@ -2396,7 +2575,10 @@ export class StreamChat { * * @return {Promise} Flag Reports Response */ - async _queryFlagReports(filterConditions: FlagReportsFilters = {}, options: FlagReportsPaginationOptions = {}) { + async _queryFlagReports( + filterConditions: FlagReportsFilters = {}, + options: FlagReportsPaginationOptions = {}, + ) { // Return a list of message flags return await this.post(this.baseURL + '/moderation/reports', { filter_conditions: filterConditions, @@ -2418,11 +2600,18 @@ export class StreamChat { * @param {string} [options.review_details] custom information about review result * @returns {Promise>} */ - async _reviewFlagReport(id: string, reviewResult: string, options: ReviewFlagReportOptions = {}) { - return await this.patch(this.baseURL + `/moderation/reports/${encodeURIComponent(id)}`, { - review_result: reviewResult, - ...options, - }); + async _reviewFlagReport( + id: string, + reviewResult: string, + options: ReviewFlagReportOptions = {}, + ) { + return await this.patch( + this.baseURL + `/moderation/reports/${encodeURIComponent(id)}`, + { + review_result: reviewResult, + ...options, + }, + ); } /** @@ -2469,15 +2658,22 @@ export class StreamChat { } getCommand(name: string) { - return this.get(this.baseURL + `/commands/${encodeURIComponent(name)}`); + return this.get( + this.baseURL + `/commands/${encodeURIComponent(name)}`, + ); } updateCommand(name: string, data: UpdateCommandOptions) { - return this.put(this.baseURL + `/commands/${encodeURIComponent(name)}`, data); + return this.put( + this.baseURL + `/commands/${encodeURIComponent(name)}`, + data, + ); } deleteCommand(name: string) { - return this.delete(this.baseURL + `/commands/${encodeURIComponent(name)}`); + return this.delete( + this.baseURL + `/commands/${encodeURIComponent(name)}`, + ); } listCommands() { @@ -2490,15 +2686,22 @@ export class StreamChat { } getChannelType(channelType: string) { - return this.get(this.baseURL + `/channeltypes/${encodeURIComponent(channelType)}`); + return this.get( + this.baseURL + `/channeltypes/${encodeURIComponent(channelType)}`, + ); } updateChannelType(channelType: string, data: UpdateChannelTypeRequest) { - return this.put(this.baseURL + `/channeltypes/${encodeURIComponent(channelType)}`, data); + return this.put( + this.baseURL + `/channeltypes/${encodeURIComponent(channelType)}`, + data, + ); } deleteChannelType(channelType: string) { - return this.delete(this.baseURL + `/channeltypes/${encodeURIComponent(channelType)}`); + return this.delete( + this.baseURL + `/channeltypes/${encodeURIComponent(channelType)}`, + ); } listChannelTypes() { @@ -2560,7 +2763,10 @@ export class StreamChat { * @param {string | { id: string }} messageOrMessageId message object or message id * @param {string} errorText error message to report in case of message id absence */ - _validateAndGetMessageId(messageOrMessageId: string | { id: string }, errorText: string) { + _validateAndGetMessageId( + messageOrMessageId: string | { id: string }, + errorText: string, + ) { let messageId: string; if (typeof messageOrMessageId === 'string') { messageId = messageOrMessageId; @@ -2592,13 +2798,13 @@ export class StreamChat { ); return this.partialUpdateMessage( messageId, - ({ + { set: { pinned: true, pin_expires: this._normalizeExpiration(timeoutOrExpirationDate), pinned_at: this._normalizeExpiration(pinnedAt), }, - } as unknown) as PartialMessageUpdate, + } as unknown as PartialMessageUpdate, pinnedBy, ); } @@ -2608,16 +2814,19 @@ export class StreamChat { * @param {string | { id: string }} messageOrMessageId message object or message id * @param {string | { id: string }} [userId] */ - unpinMessage(messageOrMessageId: string | { id: string }, userId?: string | { id: string }) { + unpinMessage( + messageOrMessageId: string | { id: string }, + userId?: string | { id: string }, + ) { const messageId = this._validateAndGetMessageId( messageOrMessageId, 'Please specify the message id when calling unpinMessage', ); return this.partialUpdateMessage( messageId, - ({ + { set: { pinned: false }, - } as unknown) as PartialMessageUpdate, + } as unknown as PartialMessageUpdate, userId, ); } @@ -2631,7 +2840,11 @@ export class StreamChat { * * @return {{ message: MessageResponse }} Response that includes the message */ - async updateMessage(message: UpdatedMessage, userId?: string | { id: string }, options?: UpdateMessageOptions) { + async updateMessage( + message: UpdatedMessage, + userId?: string | { id: string }, + options?: UpdateMessageOptions, + ) { if (!message.id) { throw Error('Please specify the message id when calling updateMessage'); } @@ -2674,8 +2887,13 @@ export class StreamChat { * Server always expects mentioned_users to be array of string. We are adding extra check, just in case * SDK missed this conversion. */ - if (Array.isArray(clonedMessage.mentioned_users) && !isString(clonedMessage.mentioned_users[0])) { - clonedMessage.mentioned_users = clonedMessage.mentioned_users.map((mu) => ((mu as unknown) as UserResponse).id); + if ( + Array.isArray(clonedMessage.mentioned_users) && + !isString(clonedMessage.mentioned_users[0]) + ) { + clonedMessage.mentioned_users = clonedMessage.mentioned_users.map( + (mu) => (mu as unknown as UserResponse).id, + ); } return await this.post( @@ -2713,11 +2931,14 @@ export class StreamChat { if (userId != null && isString(userId)) { user = { id: userId }; } - return await this.put(this.baseURL + `/messages/${encodeURIComponent(id)}`, { - ...partialMessageObject, - ...options, - user, - }); + return await this.put( + this.baseURL + `/messages/${encodeURIComponent(id)}`, + { + ...partialMessageObject, + ...options, + user, + }, + ); } async deleteMessage(messageID: string, hardDelete?: boolean) { @@ -2751,9 +2972,12 @@ export class StreamChat { } async getMessage(messageID: string, options?: GetMessageOptions) { - return await this.get(this.baseURL + `/messages/${encodeURIComponent(messageID)}`, { - ...options, - }); + return await this.get( + this.baseURL + `/messages/${encodeURIComponent(messageID)}`, + { + ...options, + }, + ); } /** @@ -2776,10 +3000,15 @@ export class StreamChat { ...options, }; - const response = await this.post(`${this.baseURL}/threads`, optionsWithDefaults); + const response = await this.post( + `${this.baseURL}/threads`, + optionsWithDefaults, + ); return { - threads: response.threads.map((thread) => new Thread({ client: this, threadData: thread })), + threads: response.threads.map( + (thread) => new Thread({ client: this, threadData: thread }), + ), next: response.next, }; } @@ -2874,16 +3103,20 @@ export class StreamChat { const { os, model } = this.deviceIdentifier ?? {}; - return ([ - // reports the device OS, if provided - ['os', os], - // reports the device model, if provided - ['device_model', model], - // reports which bundle is being picked from the exports - ['client_bundle', clientBundle], - ] as const).reduce( + return ( + [ + // reports the device OS, if provided + ['os', os], + // reports the device model, if provided + ['device_model', model], + // reports which bundle is being picked from the exports + ['client_bundle', clientBundle], + ] as const + ).reduce( (withArguments, [key, value]) => - value && value.length > 0 ? withArguments.concat(`|${key}=${value}`) : withArguments, + value && value.length > 0 + ? withArguments.concat(`|${key}=${value}`) + : withArguments, userAgentString, ); } @@ -2925,8 +3158,11 @@ export class StreamChat { }; } - const { params: axiosRequestConfigParams, headers: axiosRequestConfigHeaders, ...axiosRequestConfigRest } = - this.options.axiosRequestConfig || {}; + const { + params: axiosRequestConfigParams, + headers: axiosRequestConfigHeaders, + ...axiosRequestConfigRest + } = this.options.axiosRequestConfig || {}; return { params: { @@ -2956,6 +3192,7 @@ export class StreamChat { } _startCleaning() { + // eslint-disable-next-line @typescript-eslint/no-this-alias const that = this; if (this.cleaningIntervalRef != null) { return; @@ -2973,14 +3210,13 @@ export class StreamChat { * @private * @returns json string */ - _buildWSPayload = (client_request_id?: string) => { - return JSON.stringify({ + _buildWSPayload = (client_request_id?: string) => + JSON.stringify({ user_id: this.userID, user_details: this._user, device: this.options.device, client_request_id, }); - }; /** * checks signature of a request @@ -2998,7 +3234,9 @@ export class StreamChat { * @returns {Promise} */ getPermission(name: string) { - return this.get(`${this.baseURL}/permissions/${encodeURIComponent(name)}`); + return this.get( + `${this.baseURL}/permissions/${encodeURIComponent(name)}`, + ); } /** createPermission - creates a custom permission @@ -3019,9 +3257,12 @@ export class StreamChat { * @returns {Promise} */ updatePermission(id: string, permissionData: Omit) { - return this.put(`${this.baseURL}/permissions/${encodeURIComponent(id)}`, { - ...permissionData, - }); + return this.put( + `${this.baseURL}/permissions/${encodeURIComponent(id)}`, + { + ...permissionData, + }, + ); } /** deletePermission - deletes a custom permission @@ -3030,7 +3271,9 @@ export class StreamChat { * @returns {Promise} */ deletePermission(name: string) { - return this.delete(`${this.baseURL}/permissions/${encodeURIComponent(name)}`); + return this.delete( + `${this.baseURL}/permissions/${encodeURIComponent(name)}`, + ); } /** listPermissions - returns the list of all permissions for this application @@ -3091,9 +3334,12 @@ export class StreamChat { * @return {Promise} The Server Response */ async sendUserCustomEvent(targetUserID: string, event: UserCustomEvent) { - return await this.post(`${this.baseURL}/users/${encodeURIComponent(targetUserID)}/event`, { - event, - }); + return await this.post( + `${this.baseURL}/users/${encodeURIComponent(targetUserID)}/event`, + { + event, + }, + ); } /** @@ -3119,7 +3365,10 @@ export class StreamChat { * @returns {Promise} Response containing array of block lists */ listBlockLists(data?: { team?: string }) { - return this.get(`${this.baseURL}/blocklists`, data); + return this.get( + `${this.baseURL}/blocklists`, + data, + ); } /** @@ -3149,7 +3398,10 @@ export class StreamChat { * @returns {Promise} The server response */ updateBlockList(name: string, data: { words: string[]; team?: string }) { - return this.put(`${this.baseURL}/blocklists/${encodeURIComponent(name)}`, data); + return this.put( + `${this.baseURL}/blocklists/${encodeURIComponent(name)}`, + data, + ); } /** @@ -3162,16 +3414,28 @@ export class StreamChat { * @returns {Promise} The server response */ deleteBlockList(name: string, data?: { team?: string }) { - return this.delete(`${this.baseURL}/blocklists/${encodeURIComponent(name)}`, data); + return this.delete( + `${this.baseURL}/blocklists/${encodeURIComponent(name)}`, + data, + ); } - exportChannels(request: Array, options: ExportChannelOptions = {}) { + exportChannels( + request: Array, + options: ExportChannelOptions = {}, + ) { const payload = { channels: request, ...options }; - return this.post(`${this.baseURL}/export_channels`, payload); + return this.post( + `${this.baseURL}/export_channels`, + payload, + ); } exportUsers(request: ExportUsersRequest) { - return this.post(`${this.baseURL}/export/users`, request); + return this.post( + `${this.baseURL}/export/users`, + request, + ); } exportChannel(request: ExportChannelRequest, options?: ExportChannelOptions) { @@ -3219,7 +3483,7 @@ export class StreamChat { * * @return {{segment: SegmentResponse} & APIResponse} The created Segment */ - async createSegment(type: SegmentType, id: string | null, data?: SegmentData) { + createSegment(type: SegmentType, id: string | null, data?: SegmentData) { this.validateServerSideAuth(); const body = { id, @@ -3238,7 +3502,7 @@ export class StreamChat { * * @return {Segment} The created Segment */ - async createUserSegment(id: string | null, data?: SegmentData) { + createUserSegment(id: string | null, data?: SegmentData) { this.validateServerSideAuth(); return this.createSegment('user', id, data); } @@ -3252,14 +3516,16 @@ export class StreamChat { * * @return {Segment} The created Segment */ - async createChannelSegment(id: string | null, data?: SegmentData) { + createChannelSegment(id: string | null, data?: SegmentData) { this.validateServerSideAuth(); return this.createSegment('channel', id, data); } - async getSegment(id: string) { + getSegment(id: string) { this.validateServerSideAuth(); - return this.get<{ segment: SegmentResponse } & APIResponse>(this.baseURL + `/segments/${encodeURIComponent(id)}`); + return this.get<{ segment: SegmentResponse } & APIResponse>( + this.baseURL + `/segments/${encodeURIComponent(id)}`, + ); } /** @@ -3270,9 +3536,12 @@ export class StreamChat { * * @return {Segment} Updated Segment */ - async updateSegment(id: string, data: Partial) { + updateSegment(id: string, data: Partial) { this.validateServerSideAuth(); - return this.put<{ segment: SegmentResponse }>(this.baseURL + `/segments/${encodeURIComponent(id)}`, data); + return this.put<{ segment: SegmentResponse }>( + this.baseURL + `/segments/${encodeURIComponent(id)}`, + data, + ); } /** @@ -3283,13 +3552,16 @@ export class StreamChat { * * @return {APIResponse} API response */ - async addSegmentTargets(id: string, targets: string[]) { + addSegmentTargets(id: string, targets: string[]) { this.validateServerSideAuth(); const body = { target_ids: targets }; - return this.post(this.baseURL + `/segments/${encodeURIComponent(id)}/addtargets`, body); + return this.post( + this.baseURL + `/segments/${encodeURIComponent(id)}/addtargets`, + body, + ); } - async querySegmentTargets( + querySegmentTargets( id: string, filter: QuerySegmentTargetsFilter | null = {}, sort: SortParam[] | null | [] = [], @@ -3313,10 +3585,13 @@ export class StreamChat { * * @return {APIResponse} API response */ - async removeSegmentTargets(id: string, targets: string[]) { + removeSegmentTargets(id: string, targets: string[]) { this.validateServerSideAuth(); const body = { target_ids: targets }; - return this.post(this.baseURL + `/segments/${encodeURIComponent(id)}/deletetargets`, body); + return this.post( + this.baseURL + `/segments/${encodeURIComponent(id)}/deletetargets`, + body, + ); } /** @@ -3327,7 +3602,7 @@ export class StreamChat { * * @return {Segment[]} Segments */ - async querySegments(filter: {}, sort?: SortParam[], options: QuerySegmentsOptions = {}) { + querySegments(filter: {}, sort?: SortParam[], options: QuerySegmentsOptions = {}) { this.validateServerSideAuth(); return this.post< { @@ -3349,7 +3624,7 @@ export class StreamChat { * * @return {Promise} The Server Response */ - async deleteSegment(id: string) { + deleteSegment(id: string) { this.validateServerSideAuth(); return this.delete(this.baseURL + `/segments/${encodeURIComponent(id)}`); } @@ -3362,10 +3637,11 @@ export class StreamChat { * * @return {Promise} The Server Response */ - async segmentTargetExists(segmentId: string, targetId: string) { + segmentTargetExists(segmentId: string, targetId: string) { this.validateServerSideAuth(); return this.get( - this.baseURL + `/segments/${encodeURIComponent(segmentId)}/target/${encodeURIComponent(targetId)}`, + this.baseURL + + `/segments/${encodeURIComponent(segmentId)}/target/${encodeURIComponent(targetId)}`, ); } @@ -3376,7 +3652,7 @@ export class StreamChat { * * @return {Campaign} The Created Campaign */ - async createCampaign(params: CampaignData) { + createCampaign(params: CampaignData) { this.validateServerSideAuth(); return this.post< { @@ -3389,7 +3665,7 @@ export class StreamChat { >(this.baseURL + `/campaigns`, { ...params }); } - async getCampaign(id: string, options?: GetCampaignOptions) { + getCampaign(id: string, options?: GetCampaignOptions) { this.validateServerSideAuth(); return this.get< { @@ -3402,7 +3678,7 @@ export class StreamChat { >(this.baseURL + `/campaigns/${encodeURIComponent(id)}`, { ...options?.users }); } - async startCampaign(id: string, options?: { scheduledFor?: string; stopAt?: string }) { + startCampaign(id: string, options?: { scheduledFor?: string; stopAt?: string }) { this.validateServerSideAuth(); return this.post< { @@ -3423,7 +3699,11 @@ export class StreamChat { * * @return {Campaign[]} Campaigns */ - async queryCampaigns(filter: CampaignFilters, sort?: CampaignSort, options?: CampaignQueryOptions) { + async queryCampaigns( + filter: CampaignFilters, + sort?: CampaignSort, + options?: CampaignQueryOptions, + ) { this.validateServerSideAuth(); return await this.post< { @@ -3446,7 +3726,7 @@ export class StreamChat { * * @return {Campaign} Updated Campaign */ - async updateCampaign(id: string, params: Partial) { + updateCampaign(id: string, params: Partial) { this.validateServerSideAuth(); return this.put<{ campaign: CampaignResponse; @@ -3464,9 +3744,11 @@ export class StreamChat { * * @return {Promise} The Server Response */ - async deleteCampaign(id: string) { + deleteCampaign(id: string) { this.validateServerSideAuth(); - return this.delete(this.baseURL + `/campaigns/${encodeURIComponent(id)}`); + return this.delete( + this.baseURL + `/campaigns/${encodeURIComponent(id)}`, + ); } /** @@ -3476,9 +3758,11 @@ export class StreamChat { * * @return {Campaign} Stopped Campaign */ - async stopCampaign(id: string) { + stopCampaign(id: string) { this.validateServerSideAuth(); - return this.post<{ campaign: CampaignResponse }>(this.baseURL + `/campaigns/${encodeURIComponent(id)}/stop`); + return this.post<{ campaign: CampaignResponse }>( + this.baseURL + `/campaigns/${encodeURIComponent(id)}/stop`, + ); } /** @@ -3487,7 +3771,7 @@ export class StreamChat { * @param {string} url link * @return {OGAttachment} OG Attachment */ - async enrichURL(url: string) { + enrichURL(url: string) { return this.get(this.baseURL + `/og`, { url }); } @@ -3498,8 +3782,10 @@ export class StreamChat { * * @return {TaskStatus} The task status */ - async getTask(id: string) { - return this.get(`${this.baseURL}/tasks/${encodeURIComponent(id)}`); + getTask(id: string) { + return this.get( + `${this.baseURL}/tasks/${encodeURIComponent(id)}`, + ); } /** @@ -3511,10 +3797,13 @@ export class StreamChat { * @return {DeleteChannelsResponse} Result of the soft deletion, if server-side, it holds the task ID as well */ async deleteChannels(cids: string[], options: { hard_delete?: boolean } = {}) { - return await this.post(this.baseURL + `/channels/delete`, { - cids, - ...options, - }); + return await this.post( + this.baseURL + `/channels/delete`, + { + cids, + ...options, + }, + ); } /** @@ -3526,14 +3815,29 @@ export class StreamChat { * @return {TaskResponse} A task ID */ async deleteUsers(user_ids: string[], options: DeleteUserOptions = {}) { - if (typeof options.user !== 'undefined' && !['soft', 'hard', 'pruning'].includes(options.user)) { - throw new Error('Invalid delete user options. user must be one of [soft hard pruning]'); + if ( + typeof options.user !== 'undefined' && + !['soft', 'hard', 'pruning'].includes(options.user) + ) { + throw new Error( + 'Invalid delete user options. user must be one of [soft hard pruning]', + ); } - if (typeof options.conversations !== 'undefined' && !['soft', 'hard'].includes(options.conversations)) { - throw new Error('Invalid delete user options. conversations must be one of [soft hard]'); + if ( + typeof options.conversations !== 'undefined' && + !['soft', 'hard'].includes(options.conversations) + ) { + throw new Error( + 'Invalid delete user options. conversations must be one of [soft hard]', + ); } - if (typeof options.messages !== 'undefined' && !['soft', 'hard', 'pruning'].includes(options.messages)) { - throw new Error('Invalid delete user options. messages must be one of [soft hard pruning]'); + if ( + typeof options.messages !== 'undefined' && + !['soft', 'hard', 'pruning'].includes(options.messages) + ) { + throw new Error( + 'Invalid delete user options. messages must be one of [soft hard pruning]', + ); } return await this.post(this.baseURL + `/users/delete`, { user_ids, @@ -3553,9 +3857,12 @@ export class StreamChat { * @return {APIResponse & CreateImportResponse} An ImportTask */ async _createImportURL(filename: string) { - return await this.post(this.baseURL + `/import_urls`, { - filename, - }); + return await this.post( + this.baseURL + `/import_urls`, + { + filename, + }, + ); } /** @@ -3571,10 +3878,13 @@ export class StreamChat { * @return {APIResponse & CreateImportResponse} An ImportTask */ async _createImport(path: string, options: CreateImportOptions = { mode: 'upsert' }) { - return await this.post(this.baseURL + `/imports`, { - path, - ...options, - }); + return await this.post( + this.baseURL + `/imports`, + { + path, + ...options, + }, + ); } /** @@ -3590,7 +3900,9 @@ export class StreamChat { * @return {APIResponse & GetImportResponse} An ImportTask */ async _getImport(id: string) { - return await this.get(this.baseURL + `/imports/${encodeURIComponent(id)}`); + return await this.get( + this.baseURL + `/imports/${encodeURIComponent(id)}`, + ); } /** @@ -3606,7 +3918,10 @@ export class StreamChat { * @return {APIResponse & ListImportsResponse} An ImportTask */ async _listImports(options: ListImportsPaginationOptions) { - return await this.get(this.baseURL + `/imports`, options); + return await this.get( + this.baseURL + `/imports`, + options, + ); } /** @@ -3619,9 +3934,12 @@ export class StreamChat { * @return {APIResponse & PushProviderUpsertResponse} A push provider */ async upsertPushProvider(pushProvider: PushProviderConfig) { - return await this.post(this.baseURL + `/push_providers`, { - push_provider: pushProvider, - }); + return await this.post( + this.baseURL + `/push_providers`, + { + push_provider: pushProvider, + }, + ); } /** @@ -3635,7 +3953,8 @@ export class StreamChat { */ async deletePushProvider({ type, name }: PushProviderID) { return await this.delete( - this.baseURL + `/push_providers/${encodeURIComponent(type)}/${encodeURIComponent(name)}`, + this.baseURL + + `/push_providers/${encodeURIComponent(type)}/${encodeURIComponent(name)}`, ); } @@ -3647,7 +3966,9 @@ export class StreamChat { * @return {APIResponse & PushProviderListResponse} A push provider */ async listPushProviders() { - return await this.get(this.baseURL + `/push_providers`); + return await this.get( + this.baseURL + `/push_providers`, + ); } /** @@ -3664,7 +3985,9 @@ export class StreamChat { * @return {APIResponse & MessageResponse} The message */ async commitMessage(id: string) { - return await this.post(this.baseURL + `/messages/${encodeURIComponent(id)}/commit`); + return await this.post( + this.baseURL + `/messages/${encodeURIComponent(id)}/commit`, + ); } /** @@ -3719,10 +4042,13 @@ export class StreamChat { partialPollObject: PartialPollUpdate, userId?: string, ): Promise { - return await this.patch(this.baseURL + `/polls/${encodeURIComponent(id)}`, { - ...partialPollObject, - ...(userId ? { user_id: userId } : {}), - }); + return await this.patch( + this.baseURL + `/polls/${encodeURIComponent(id)}`, + { + ...partialPollObject, + ...(userId ? { user_id: userId } : {}), + }, + ); } /** @@ -3732,9 +4058,12 @@ export class StreamChat { * @returns */ async deletePoll(id: string, userId?: string): Promise { - return await this.delete(this.baseURL + `/polls/${encodeURIComponent(id)}`, { - ...(userId ? { user_id: userId } : {}), - }); + return await this.delete( + this.baseURL + `/polls/${encodeURIComponent(id)}`, + { + ...(userId ? { user_id: userId } : {}), + }, + ); } /** @@ -3743,7 +4072,7 @@ export class StreamChat { * @param userId string The user id (only serverside) * @returns {APIResponse & UpdatePollAPIResponse} The poll */ - async closePoll(id: string, userId?: string): Promise { + closePoll(id: string, userId?: string): Promise { return this.partialUpdatePoll( id, { @@ -3781,7 +4110,8 @@ export class StreamChat { */ async getPollOption(pollId: string, optionId: string, userId?: string) { return await this.get( - this.baseURL + `/polls/${encodeURIComponent(pollId)}/options/${encodeURIComponent(optionId)}`, + this.baseURL + + `/polls/${encodeURIComponent(pollId)}/options/${encodeURIComponent(optionId)}`, userId ? { user_id: userId } : {}, ); } @@ -3812,7 +4142,8 @@ export class StreamChat { */ async deletePollOption(pollId: string, optionId: string, userId?: string) { return await this.delete( - this.baseURL + `/polls/${encodeURIComponent(pollId)}/options/${encodeURIComponent(optionId)}`, + this.baseURL + + `/polls/${encodeURIComponent(pollId)}/options/${encodeURIComponent(optionId)}`, userId ? { user_id: userId } : {}, ); } @@ -3825,9 +4156,15 @@ export class StreamChat { * @param userId string The user id (only serverside) * @returns {APIResponse & CastVoteAPIResponse} The poll vote */ - async castPollVote(messageId: string, pollId: string, vote: PollVoteData, userId?: string) { + async castPollVote( + messageId: string, + pollId: string, + vote: PollVoteData, + userId?: string, + ) { return await this.post( - this.baseURL + `/messages/${encodeURIComponent(messageId)}/polls/${encodeURIComponent(pollId)}/vote`, + this.baseURL + + `/messages/${encodeURIComponent(messageId)}/polls/${encodeURIComponent(pollId)}/vote`, { vote, ...(userId ? { user_id: userId } : {}), @@ -3842,7 +4179,7 @@ export class StreamChat { * @param answerText string The answer text * @param userId string The user id (only serverside) */ - async addPollAnswer(messageId: string, pollId: string, answerText: string, userId?: string) { + addPollAnswer(messageId: string, pollId: string, answerText: string, userId?: string) { return this.castPollVote( messageId, pollId, @@ -3853,7 +4190,12 @@ export class StreamChat { ); } - async removePollVote(messageId: string, pollId: string, voteId: string, userId?: string) { + async removePollVote( + messageId: string, + pollId: string, + voteId: string, + userId?: string, + ) { return await this.delete( this.baseURL + `/messages/${encodeURIComponent(messageId)}/polls/${encodeURIComponent(pollId)}/vote/${encodeURIComponent( @@ -3880,11 +4222,14 @@ export class StreamChat { userId?: string, ): Promise { const q = userId ? `?user_id=${userId}` : ''; - return await this.post(this.baseURL + `/polls/query${q}`, { - filter, - sort: normalizeQuerySort(sort), - ...options, - }); + return await this.post( + this.baseURL + `/polls/query${q}`, + { + filter, + sort: normalizeQuerySort(sort), + ...options, + }, + ); } /** @@ -3953,11 +4298,14 @@ export class StreamChat { sort: QueryMessageHistorySort = [], options: QueryMessageHistoryOptions = {}, ): Promise { - return await this.post(this.baseURL + '/messages/history', { - filter, - sort: normalizeQuerySort(sort), - ...options, - }); + return await this.post( + this.baseURL + '/messages/history', + { + filter, + sort: normalizeQuerySort(sort), + ...options, + }, + ); } /** @@ -3968,11 +4316,18 @@ export class StreamChat { * @param {string} reviewed_by user ID who reviewed the flagged message * @returns {APIResponse} */ - async updateFlags(message_ids: string[], reviewed_by: string, options: { user_id?: string } = {}) { - return await this.post(this.baseURL + '/automod/v1/moderation/update_flags', { - message_ids, - reviewed_by, - ...options, - }); + async updateFlags( + message_ids: string[], + reviewed_by: string, + options: { user_id?: string } = {}, + ) { + return await this.post( + this.baseURL + '/automod/v1/moderation/update_flags', + { + message_ids, + reviewed_by, + ...options, + }, + ); } } diff --git a/src/client_state.ts b/src/client_state.ts index 71f0080e9c..2bcf1a3cbd 100644 --- a/src/client_state.ts +++ b/src/client_state.ts @@ -1,5 +1,5 @@ -import { UserResponse } from './types'; -import { StreamChat } from './client'; +import type { UserResponse } from './types'; +import type { StreamChat } from './client'; /** * ClientState - A container class for the client state. diff --git a/src/connection.ts b/src/connection.ts index ebf4925539..fa94aae8ef 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -1,24 +1,30 @@ import WebSocket from 'isomorphic-ws'; import { + addConnectionEventListeners, chatCodes, convertErrorToJson, - sleep, - retryInterval, randomId, removeConnectionEventListeners, - addConnectionEventListeners, + retryInterval, + sleep, } from './utils'; -import { buildWsFatalInsight, buildWsSuccessAfterFailureInsight, postInsights } from './insights'; -import { ConnectAPIResponse, ConnectionOpen, UR, LogLevel } from './types'; -import { StreamChat } from './client'; -import { APIError } from './errors'; +import { + buildWsFatalInsight, + buildWsSuccessAfterFailureInsight, + postInsights, +} from './insights'; +import type { ConnectAPIResponse, ConnectionOpen, LogLevel, UR } from './types'; +import type { StreamChat } from './client'; +import type { APIError } from './errors'; // Type guards to check WebSocket error type -const isCloseEvent = (res: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent): res is WebSocket.CloseEvent => - (res as WebSocket.CloseEvent).code !== undefined; +const isCloseEvent = ( + res: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent, +): res is WebSocket.CloseEvent => (res as WebSocket.CloseEvent).code !== undefined; -const isErrorEvent = (res: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent): res is WebSocket.ErrorEvent => - (res as WebSocket.ErrorEvent).error !== undefined; +const isErrorEvent = ( + res: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent, +): res is WebSocket.ErrorEvent => (res as WebSocket.ErrorEvent).error !== undefined; /** * StableWSConnection - A WS connection that reconnects upon failure. @@ -55,7 +61,11 @@ export class StableWSConnection { connectionCheckTimeout: number; connectionCheckTimeoutRef?: NodeJS.Timeout; rejectPromise?: ( - reason?: Error & { code?: string | number; isWSFailure?: boolean; StatusCode?: string | number }, + reason?: Error & { + code?: string | number; + isWSFailure?: boolean; + StatusCode?: string | number; + }, ) => void; requestID: string | undefined; resolvePromise?: (value: ConnectionOpen) => void; @@ -104,7 +114,9 @@ export class StableWSConnection { */ async connect(timeout = 15000) { if (this.isConnecting) { - throw Error(`You've called connect twice, can only attempt 1 connection at the time`); + throw Error( + `You've called connect twice, can only attempt 1 connection at the time`, + ); } this.isDisconnected = false; @@ -122,7 +134,9 @@ export class StableWSConnection { const e = error as APIError; if (e.code === chatCodes.TOKEN_EXPIRED && !this.client.tokenManager.isStatic()) { - this._log('connect() - WS failure due to expired token, so going to try to reload token and reconnect'); + this._log( + 'connect() - WS failure due to expired token, so going to try to reload token and reconnect', + ); this._reconnect({ refreshToken: true }); } else if (!e.isWSFailure) { // API rejected the connection and we should not retry @@ -145,7 +159,7 @@ export class StableWSConnection { * the default 15s timeout allows between 2~3 tries * @param timeout duration(ms) */ - async _waitForHealthy(timeout = 15000) { + _waitForHealthy(timeout = 15000) { return Promise.race([ (async () => { const interval = 50; // ms @@ -235,7 +249,10 @@ export class StableWSConnection { if (ws && ws.close && ws.readyState === ws.OPEN) { isClosedPromise = new Promise((resolve) => { const onclose = (event: WebSocket.CloseEvent) => { - this._log(`disconnect() - resolving isClosedPromise ${event ? 'with' : 'without'} close frame`, { event }); + this._log( + `disconnect() - resolving isClosedPromise ${event ? 'with' : 'without'} close frame`, + { event }, + ); resolve(); }; @@ -245,9 +262,14 @@ export class StableWSConnection { setTimeout(onclose, timeout != null ? timeout : 1000); }); - this._log(`disconnect() - Manually closed connection by calling client.disconnect()`); + this._log( + `disconnect() - Manually closed connection by calling client.disconnect()`, + ); - ws.close(chatCodes.WS_CLOSED_SUCCESS, 'Manually closed connection by calling client.disconnect()'); + ws.close( + chatCodes.WS_CLOSED_SUCCESS, + 'Manually closed connection by calling client.disconnect()', + ); } else { this._log(`disconnect() - ws connection doesn't exist or it is already closed.`); isClosedPromise = Promise.resolve(); @@ -264,7 +286,11 @@ export class StableWSConnection { * @return {ConnectAPIResponse} Promise that completes once the first health check message is received */ async _connect() { - if (this.isConnecting || (this.isDisconnected && this.client.options.enableWSFallback)) return; // simply ignore _connect if it's currently trying to connect + if ( + this.isConnecting || + (this.isDisconnected && this.client.options.enableWSFallback) + ) + return; // simply ignore _connect if it's currently trying to connect this.isConnecting = true; this.requestID = randomId(); this.client.insightMetrics.connectionStartTimestamp = new Date().getTime(); @@ -285,7 +311,10 @@ export class StableWSConnection { this._setupConnectionPromise(); const wsURL = this._buildUrl(); - this._log(`_connect() - Connecting to ${wsURL}`, { wsURL, requestID: this.requestID }); + this._log(`_connect() - Connecting to ${wsURL}`, { + wsURL, + requestID: this.requestID, + }); this.ws = new WebSocket(wsURL); this.ws.onopen = this.onopen.bind(this, this.wsID); this.ws.onclose = this.onclose.bind(this, this.wsID); @@ -296,10 +325,13 @@ export class StableWSConnection { if (response) { this.connectionID = response.connection_id; - if (this.client.insightMetrics.wsConsecutiveFailures > 0 && this.client.options.enableInsights) { + if ( + this.client.insightMetrics.wsConsecutiveFailures > 0 && + this.client.options.enableInsights + ) { postInsights( 'ws_success_after_failure', - buildWsSuccessAfterFailureInsight((this as unknown) as StableWSConnection), + buildWsSuccessAfterFailureInsight(this as unknown as StableWSConnection), ); this.client.insightMetrics.wsConsecutiveFailures = 0; } @@ -314,7 +346,7 @@ export class StableWSConnection { this.client.insightMetrics.wsTotalFailures++; const insights = buildWsFatalInsight( - (this as unknown) as StableWSConnection, + this as unknown as StableWSConnection, convertErrorToJson(error as Error), ); postInsights?.('ws_fatal', insights); @@ -331,7 +363,9 @@ export class StableWSConnection { * - `interval` {int} number of ms that function should wait before reconnecting * - `refreshToken` {boolean} reload/refresh user token be refreshed before attempting reconnection. */ - async _reconnect(options: { interval?: number; refreshToken?: boolean } = {}): Promise { + async _reconnect( + options: { interval?: number; refreshToken?: boolean } = {}, + ): Promise { this._log('_reconnect() - Initiating the reconnect'); // only allow 1 connection at the time @@ -381,8 +415,13 @@ export class StableWSConnection { } catch (error: any) { this.isHealthy = false; this.consecutiveFailures += 1; - if (error.code === chatCodes.TOKEN_EXPIRED && !this.client.tokenManager.isStatic()) { - this._log('_reconnect() - WS failure due to expired token, so going to try to reload token and reconnect'); + if ( + error.code === chatCodes.TOKEN_EXPIRED && + !this.client.tokenManager.isStatic() + ) { + this._log( + '_reconnect() - WS failure due to expired token, so going to try to reload token and reconnect', + ); return this._reconnect({ refreshToken: true }); } @@ -413,7 +452,9 @@ export class StableWSConnection { // We check this.isHealthy, not sure if it's always // smart to create a new WS connection if the old one is still up and running. // it's possible we didn't miss any messages, so this process is just expensive and not needed. - this._log(`onlineStatusChanged() - Status changing to online. isHealthy: ${this.isHealthy}`); + this._log( + `onlineStatusChanged() - Status changing to online. isHealthy: ${this.isHealthy}`, + ); if (!this.isHealthy) { this._reconnect({ interval: 10 }); } @@ -465,7 +506,9 @@ export class StableWSConnection { if (event.code === chatCodes.WS_CLOSED_SUCCESS) { // this is a permanent error raised by stream.. // usually caused by invalid auth details - const error = new Error(`WS connection reject with error ${event.reason}`) as Error & WebSocket.CloseEvent; + const error = new Error( + `WS connection reject with error ${event.reason}`, + ) as Error & WebSocket.CloseEvent; error.reason = event.reason; error.code = event.code; @@ -531,7 +574,10 @@ export class StableWSConnection { * _errorFromWSEvent - Creates an error object for the WS event * */ - _errorFromWSEvent = (event: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent, isWSFailure = true) => { + _errorFromWSEvent = ( + event: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent, + isWSFailure = true, + ) => { let code; let statusCode; let message; @@ -550,7 +596,9 @@ export class StableWSConnection { // Keeping this `warn` level log, to avoid cluttering of error logs from ws failures. this._log(`_errorFromWSEvent() - WS failed with code ${code}`, { event }, 'warn'); - const error = new Error(`WS failed with code ${code} and reason - ${message}`) as Error & { + const error = new Error( + `WS failed with code ${code} and reason - ${message}`, + ) as Error & { code?: string | number; isWSFailure?: boolean; StatusCode?: string | number; @@ -627,7 +675,10 @@ export class StableWSConnection { this.connectionCheckTimeoutRef = setTimeout(() => { const now = new Date(); - if (this.lastEvent && now.getTime() - this.lastEvent.getTime() > this.connectionCheckTimeout) { + if ( + this.lastEvent && + now.getTime() - this.lastEvent.getTime() > this.connectionCheckTimeout + ) { this._log('scheduleConnectionCheck - going to reconnect'); this._setHealth(false); this._reconnect(); diff --git a/src/connection_fallback.ts b/src/connection_fallback.ts index c7e8010fce..c0552b1a18 100644 --- a/src/connection_fallback.ts +++ b/src/connection_fallback.ts @@ -1,8 +1,14 @@ -import axios, { AxiosRequestConfig, CancelTokenSource } from 'axios'; -import { StreamChat } from './client'; -import { addConnectionEventListeners, removeConnectionEventListeners, retryInterval, sleep } from './utils'; +import type { AxiosRequestConfig, CancelTokenSource } from 'axios'; +import axios from 'axios'; +import type { StreamChat } from './client'; +import { + addConnectionEventListeners, + removeConnectionEventListeners, + retryInterval, + sleep, +} from './utils'; import { isAPIError, isConnectionIDError, isErrorRetryable } from './errors'; -import { ConnectionOpen, Event, UR, LogLevel } from './types'; +import type { ConnectionOpen, Event, LogLevel, UR } from './types'; export enum ConnectionState { Closed = 'CLOSED', @@ -28,14 +34,20 @@ export class WSConnectionFallback { } _log(msg: string, extra: UR = {}, level: LogLevel = 'info') { - this.client.logger(level, 'WSConnectionFallback:' + msg, { tags: ['connection_fallback', 'connection'], ...extra }); + this.client.logger(level, 'WSConnectionFallback:' + msg, { + tags: ['connection_fallback', 'connection'], + ...extra, + }); } _setState(state: ConnectionState) { this._log(`_setState() - ${state}`); // transition from connecting => connected - if (this.state === ConnectionState.Connecting && state === ConnectionState.Connected) { + if ( + this.state === ConnectionState.Connecting && + state === ConnectionState.Connected + ) { this.client.dispatchEvent({ type: 'connection.changed', online: true }); } @@ -63,7 +75,11 @@ export class WSConnectionFallback { }; /** @private */ - _req = async (params: UR, config: AxiosRequestConfig, retry: boolean): Promise => { + _req = async ( + params: UR, + config: AxiosRequestConfig, + retry: boolean, + ): Promise => { if (!this.cancelToken && !params.close) { this.cancelToken = axios.CancelToken.source(); } @@ -159,7 +175,7 @@ export class WSConnectionFallback { this._setState(ConnectionState.Connected); this.connectionID = event.connection_id; - // @ts-expect-error + // @ts-expect-error type mismatch this.client.dispatchEvent(event); this._poll(); if (reconnect) { @@ -175,9 +191,7 @@ export class WSConnectionFallback { /** * isHealthy checks if there is a connectionID and connection is in Connected state */ - isHealthy = () => { - return !!this.connectionID && this.state === ConnectionState.Connected; - }; + isHealthy = () => !!this.connectionID && this.state === ConnectionState.Connected; disconnect = async (timeout = 2000) => { removeConnectionEventListeners(this._onlineStatusChanged); diff --git a/src/custom_types.ts b/src/custom_types.ts index 62eb9a3848..29b178bc99 100644 --- a/src/custom_types.ts +++ b/src/custom_types.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-empty-interface */ - export interface CustomAttachmentData {} export interface CustomChannelData {} export interface CustomCommandData {} diff --git a/src/errors.ts b/src/errors.ts index cdaa2c9d6a..3688a656c1 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,5 +1,5 @@ -import { AxiosResponse } from 'axios'; -import { APIErrorResponse } from './types'; +import type { AxiosResponse } from 'axios'; +import type { APIErrorResponse } from './types'; export const APIErrorCodes: Record = { '-1': { name: 'InternalSystemError', retryable: true }, @@ -31,7 +31,11 @@ export const APIErrorCodes: Record '99': { name: 'AppSuspendedError', retryable: false }, }; -export type APIError = Error & { code: number; isWSFailure?: boolean; StatusCode?: number }; +export type APIError = Error & { + code: number; + isWSFailure?: boolean; + StatusCode?: number; +}; export function isAPIError(error: Error): error is APIError { return (error as APIError).code !== undefined; @@ -60,6 +64,8 @@ export function isWSFailure(err: APIError): boolean { } } -export function isErrorResponse(res: AxiosResponse): res is AxiosResponse { +export function isErrorResponse( + res: AxiosResponse, +): res is AxiosResponse { return !res.status || res.status < 200 || 300 <= res.status; } diff --git a/src/index.ts b/src/index.ts index d0d7fb19ee..d414c58835 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,10 +16,21 @@ export * from './segment'; export * from './signing'; export * from './store'; export { Thread } from './thread'; -export type { ThreadState, ThreadReadState, ThreadRepliesPagination, ThreadUserReadState } from './thread'; +export type { + ThreadState, + ThreadReadState, + ThreadRepliesPagination, + ThreadUserReadState, +} from './thread'; export * from './thread_manager'; export * from './token_manager'; export * from './types'; export * from './channel_manager'; export * from './custom_types'; -export { isOwnUser, chatCodes, logChatPromiseExecution, formatMessage, promoteChannel } from './utils'; +export { + isOwnUser, + chatCodes, + logChatPromiseExecution, + formatMessage, + promoteChannel, +} from './utils'; diff --git a/src/insights.ts b/src/insights.ts index 4549effe88..bc11790d30 100644 --- a/src/insights.ts +++ b/src/insights.ts @@ -1,5 +1,5 @@ import axios from 'axios'; -import { StableWSConnection } from './connection'; +import type { StableWSConnection } from './connection'; import { randomId, sleep } from './utils'; export type InsightTypes = 'ws_fatal' | 'ws_success_after_failure' | 'http_hi_failed'; @@ -24,11 +24,17 @@ export class InsightMetrics { * @param insightType * @param insights */ -export const postInsights = async (insightType: InsightTypes, insights: Record) => { +export const postInsights = async ( + insightType: InsightTypes, + insights: Record, +) => { const maxAttempts = 3; for (let i = 0; i < maxAttempts; i++) { try { - await axios.post(`https://chat-insights.getstream.io/insights/${insightType}`, insights); + await axios.post( + `https://chat-insights.getstream.io/insights/${insightType}`, + insights, + ); } catch (e) { await sleep((i + 1) * 3000); continue; @@ -37,7 +43,10 @@ export const postInsights = async (insightType: InsightTypes, insights: Record) { +export function buildWsFatalInsight( + connection: StableWSConnection, + event: Record, +) { return { ...event, ...buildWsBaseInsight(connection), diff --git a/src/moderation.ts b/src/moderation.ts index 40b0f9fd83..2c510ce70d 100644 --- a/src/moderation.ts +++ b/src/moderation.ts @@ -1,26 +1,26 @@ -import { +import type { APIResponse, - ModerationConfig, + CustomCheckFlag, GetConfigResponse, + GetUserModerationReportOptions, GetUserModerationReportResponse, + ModerationConfig, + ModerationFlagOptions, + ModerationMuteOptions, MuteUserResponse, + Pager, + QueryConfigsResponse, + QueryModerationConfigsFilters, + QueryModerationConfigsSort, ReviewQueueFilters, + ReviewQueueItem, ReviewQueuePaginationOptions, ReviewQueueResponse, ReviewQueueSort, - UpsertConfigResponse, - ModerationFlagOptions, - ModerationMuteOptions, - GetUserModerationReportOptions, SubmitActionOptions, - QueryModerationConfigsFilters, - QueryModerationConfigsSort, - Pager, - CustomCheckFlag, - ReviewQueueItem, - QueryConfigsResponse, + UpsertConfigResponse, } from './types'; -import { StreamChat } from './client'; +import type { StreamChat } from './client'; import { normalizeQuerySort } from './utils'; export const MODERATION_ENTITY_TYPES = { @@ -46,7 +46,7 @@ export class Moderation { * @param {Object} options.custom Additional data to be stored with the flag * @returns */ - async flagUser(flaggedUserID: string, reason: string, options: ModerationFlagOptions = {}) { + flagUser(flaggedUserID: string, reason: string, options: ModerationFlagOptions = {}) { return this.flag(MODERATION_ENTITY_TYPES.user, flaggedUserID, '', reason, options); } @@ -60,7 +60,7 @@ export class Moderation { * @param {Object} options.custom Additional data to be stored with the flag * @returns */ - async flagMessage(messageID: string, reason: string, options: ModerationFlagOptions = {}) { + flagMessage(messageID: string, reason: string, options: ModerationFlagOptions = {}) { return this.flag(MODERATION_ENTITY_TYPES.message, messageID, '', reason, options); } @@ -84,13 +84,16 @@ export class Moderation { reason: string, options: ModerationFlagOptions = {}, ) { - return await this.client.post<{ item_id: string } & APIResponse>(this.client.baseURL + '/api/v2/moderation/flag', { - entity_type: entityType, - entity_id: entityId, - entity_creator_id: entityCreatorID, - reason, - ...options, - }); + return await this.client.post<{ item_id: string } & APIResponse>( + this.client.baseURL + '/api/v2/moderation/flag', + { + entity_type: entityType, + entity_id: entityId, + entity_creator_id: entityCreatorID, + reason, + ...options, + }, + ); } /** @@ -102,10 +105,13 @@ export class Moderation { * @returns */ async muteUser(targetID: string, options: ModerationMuteOptions = {}) { - return await this.client.post(this.client.baseURL + '/api/v2/moderation/mute', { - target_ids: [targetID], - ...options, - }); + return await this.client.post( + this.client.baseURL + '/api/v2/moderation/mute', + { + target_ids: [targetID], + ...options, + }, + ); } /** @@ -138,7 +144,10 @@ export class Moderation { * @param {boolean} options.include_user_blocks Include user blocks * @param {boolean} options.include_user_mutes Include user mutes */ - async getUserModerationReport(userID: string, options: GetUserModerationReportOptions = {}) { + async getUserModerationReport( + userID: string, + options: GetUserModerationReportOptions = {}, + ) { return await this.client.get( this.client.baseURL + `/api/v2/moderation/user_report`, { @@ -159,11 +168,14 @@ export class Moderation { sort: ReviewQueueSort = [], options: ReviewQueuePaginationOptions = {}, ) { - return await this.client.post(this.client.baseURL + '/api/v2/moderation/review_queue', { - filter: filterConditions, - sort: normalizeQuerySort(sort), - ...options, - }); + return await this.client.post( + this.client.baseURL + '/api/v2/moderation/review_queue', + { + filter: filterConditions, + sort: normalizeQuerySort(sort), + ...options, + }, + ); } /** @@ -171,7 +183,10 @@ export class Moderation { * @param {Object} config Moderation config to be upserted */ async upsertConfig(config: ModerationConfig) { - return await this.client.post(this.client.baseURL + '/api/v2/moderation/config', config); + return await this.client.post( + this.client.baseURL + '/api/v2/moderation/config', + config, + ); } /** @@ -179,11 +194,17 @@ export class Moderation { * @param {string} key Key for which moderation config is to be fetched */ async getConfig(key: string, data?: { team?: string }) { - return await this.client.get(this.client.baseURL + '/api/v2/moderation/config/' + key, data); + return await this.client.get( + this.client.baseURL + '/api/v2/moderation/config/' + key, + data, + ); } async deleteConfig(key: string, data?: { team?: string }) { - return await this.client.delete(this.client.baseURL + '/api/v2/moderation/config/' + key, data); + return await this.client.delete( + this.client.baseURL + '/api/v2/moderation/config/' + key, + data, + ); } /** @@ -197,14 +218,21 @@ export class Moderation { sort: QueryModerationConfigsSort, options: Pager = {}, ) { - return await this.client.post(this.client.baseURL + '/api/v2/moderation/configs', { - filter: filterConditions, - sort, - ...options, - }); + return await this.client.post( + this.client.baseURL + '/api/v2/moderation/configs', + { + filter: filterConditions, + sort, + ...options, + }, + ); } - async submitAction(actionType: string, itemID: string, options: SubmitActionOptions = {}) { + async submitAction( + actionType: string, + itemID: string, + options: SubmitActionOptions = {}, + ) { return await this.client.post<{ item_id: string } & APIResponse>( this.client.baseURL + '/api/v2/moderation/submit_action', { @@ -277,16 +305,15 @@ export class Moderation { }, flags: CustomCheckFlag[], ) { - return await this.client.post<{ id: string; item: ReviewQueueItem; status: string } & APIResponse>( - this.client.baseURL + `/api/v2/moderation/custom_check`, - { - entity_type: entityType, - entity_id: entityID, - entity_creator_id: entityCreatorID, - moderation_payload: moderationPayload, - flags, - }, - ); + return await this.client.post< + { id: string; item: ReviewQueueItem; status: string } & APIResponse + >(this.client.baseURL + `/api/v2/moderation/custom_check`, { + entity_type: entityType, + entity_id: entityID, + entity_creator_id: entityCreatorID, + moderation_payload: moderationPayload, + flags, + }); } /** @@ -296,6 +323,12 @@ export class Moderation { * @returns */ async addCustomMessageFlags(messageID: string, flags: CustomCheckFlag[]) { - return await this.addCustomFlags(MODERATION_ENTITY_TYPES.message, messageID, '', {}, flags); + return await this.addCustomFlags( + MODERATION_ENTITY_TYPES.message, + messageID, + '', + {}, + flags, + ); } } diff --git a/src/permissions.ts b/src/permissions.ts index 4bc5301601..a463327766 100644 --- a/src/permissions.ts +++ b/src/permissions.ts @@ -1,4 +1,4 @@ -import { PermissionObject } from './types'; +import type { PermissionObject } from './types'; type RequiredPermissionObject = Required; @@ -37,12 +37,33 @@ export class Permission { } // deprecated -export const AllowAll = new Permission('Allow all', MaxPriority, AnyResource, AnyRole, false, Allow); +export const AllowAll = new Permission( + 'Allow all', + MaxPriority, + AnyResource, + AnyRole, + false, + Allow, +); // deprecated -export const DenyAll = new Permission('Deny all', MinPriority, AnyResource, AnyRole, false, Deny); +export const DenyAll = new Permission( + 'Deny all', + MinPriority, + AnyResource, + AnyRole, + false, + Deny, +); -export type Role = 'admin' | 'user' | 'guest' | 'anonymous' | 'channel_member' | 'channel_moderator' | (string & {}); +export type Role = + | 'admin' + | 'user' + | 'guest' + | 'anonymous' + | 'channel_member' + | 'channel_moderator' + | (string & {}); export const BuiltinRoles = { Admin: 'admin', diff --git a/src/poll.ts b/src/poll.ts index b75ff05653..93480c5887 100644 --- a/src/poll.ts +++ b/src/poll.ts @@ -48,12 +48,17 @@ type PollVoteCastedRemoved = PollVoteEvent & { }; const isPollUpdatedEvent = (e: Event): e is PollUpdatedEvent => e.type === 'poll.updated'; -const isPollClosedEventEvent = (e: Event): e is PollClosedEvent => e.type === 'poll.closed'; -const isPollVoteCastedEvent = (e: Event): e is PollVoteCastedEvent => e.type === 'poll.vote_casted'; -const isPollVoteChangedEvent = (e: Event): e is PollVoteCastedChanged => e.type === 'poll.vote_changed'; -const isPollVoteRemovedEvent = (e: Event): e is PollVoteCastedRemoved => e.type === 'poll.vote_removed'; - -export const isVoteAnswer = (vote: PollVote | PollAnswer): vote is PollAnswer => !!(vote as PollAnswer)?.answer_text; +const isPollClosedEventEvent = (e: Event): e is PollClosedEvent => + e.type === 'poll.closed'; +const isPollVoteCastedEvent = (e: Event): e is PollVoteCastedEvent => + e.type === 'poll.vote_casted'; +const isPollVoteChangedEvent = (e: Event): e is PollVoteCastedChanged => + e.type === 'poll.vote_changed'; +const isPollVoteRemovedEvent = (e: Event): e is PollVoteCastedRemoved => + e.type === 'poll.vote_removed'; + +export const isVoteAnswer = (vote: PollVote | PollAnswer): vote is PollAnswer => + !!(vote as PollAnswer)?.answer_text; export type PollAnswersQueryParams = { filter?: QueryVotesFilters; @@ -97,7 +102,10 @@ export class Poll { private getInitialStateFromPollResponse = (poll: PollInitOptions['poll']) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { own_votes, id, ...pollResponseForState } = poll; - const { ownAnswer, ownVotes } = own_votes?.reduce<{ ownVotes: PollVote[]; ownAnswer?: PollAnswer }>( + const { ownAnswer, ownVotes } = own_votes?.reduce<{ + ownVotes: PollVote[]; + ownAnswer?: PollAnswer; + }>( (acc, voteOrAnswer) => { if (isVoteAnswer(voteOrAnswer)) { acc.ownAnswer = voteOrAnswer; @@ -133,15 +141,17 @@ export class Poll { if (!isPollUpdatedEvent(event)) return; // eslint-disable-next-line @typescript-eslint/no-unused-vars const { id, ...pollData } = extractPollData(event.poll); - // @ts-ignore + // @ts-expect-error type mismatch this.state.partialNext({ ...pollData, lastActivityAt: new Date(event.created_at) }); }; public handlePollClosed = (event: Event) => { if (event.poll?.id && event.poll.id !== this.id) return; if (!isPollClosedEventEvent(event)) return; - // @ts-ignore - this.state.partialNext({ is_closed: true, lastActivityAt: new Date(event.created_at) }); + this.state.partialNext({ + is_closed: true, + lastActivityAt: new Date(event.created_at), + }); }; public handleVoteCasted = (event: Event) => { @@ -169,7 +179,6 @@ export class Poll { } const pollEnrichData = extractPollEnrichedData(event.poll); - // @ts-ignore this.state.partialNext({ ...pollEnrichData, latest_answers: latestAnswers, @@ -193,22 +202,27 @@ export class Poll { if (isOwnVote) { if (isVoteAnswer(event.poll_vote)) { - latestAnswers = [event.poll_vote, ...latestAnswers.filter((answer) => answer.id !== event.poll_vote.id)]; + latestAnswers = [ + event.poll_vote, + ...latestAnswers.filter((answer) => answer.id !== event.poll_vote.id), + ]; ownAnswer = event.poll_vote; } else if (event.poll_vote.option_id) { if (event.poll.enforce_unique_votes) { ownVotesByOptionId = { [event.poll_vote.option_id]: event.poll_vote }; } else { - ownVotesByOptionId = Object.entries(ownVotesByOptionId).reduce>( - (acc, [optionId, vote]) => { - if (optionId !== event.poll_vote.option_id && vote.id === event.poll_vote.id) { - return acc; - } - acc[optionId] = vote; + ownVotesByOptionId = Object.entries(ownVotesByOptionId).reduce< + Record + >((acc, [optionId, vote]) => { + if ( + optionId !== event.poll_vote.option_id && + vote.id === event.poll_vote.id + ) { return acc; - }, - {}, - ); + } + acc[optionId] = vote; + return acc; + }, {}); ownVotesByOptionId[event.poll_vote.option_id] = event.poll_vote; } @@ -224,7 +238,6 @@ export class Poll { } const pollEnrichData = extractPollEnrichedData(event.poll); - // @ts-ignore this.state.partialNext({ ...pollEnrichData, latest_answers: latestAnswers, @@ -258,7 +271,6 @@ export class Poll { } const pollEnrichData = extractPollEnrichedData(event.poll); - // @ts-ignore this.state.partialNext({ ...pollEnrichData, latest_answers: latestAnswers, @@ -271,50 +283,44 @@ export class Poll { query = async (id: string) => { const { poll } = await this.client.getPoll(id); - // @ts-ignore this.state.partialNext({ ...poll, lastActivityAt: new Date() }); return poll; }; - update = async (data: Exclude) => { - return await this.client.updatePoll({ ...data, id: this.id }); - }; + update = async (data: Exclude) => + await this.client.updatePoll({ ...data, id: this.id }); - partialUpdate = async (partialPollObject: PartialPollUpdate) => { - return await this.client.partialUpdatePoll(this.id as string, partialPollObject); - }; + partialUpdate = async (partialPollObject: PartialPollUpdate) => + await this.client.partialUpdatePoll(this.id as string, partialPollObject); - close = async () => { - return await this.client.closePoll(this.id as string); - }; + close = async () => await this.client.closePoll(this.id as string); - delete = async () => { - return await this.client.deletePoll(this.id as string); - }; + delete = async () => await this.client.deletePoll(this.id as string); - createOption = async (option: PollOptionData) => { - return await this.client.createPollOption(this.id as string, option); - }; + createOption = async (option: PollOptionData) => + await this.client.createPollOption(this.id as string, option); - updateOption = async (option: PollOptionData) => { - return await this.client.updatePollOption(this.id as string, option); - }; + updateOption = async (option: PollOptionData) => + await this.client.updatePollOption(this.id as string, option); - deleteOption = async (optionId: string) => { - return await this.client.deletePollOption(this.id as string, optionId); - }; + deleteOption = async (optionId: string) => + await this.client.deletePollOption(this.id as string, optionId); castVote = async (optionId: string, messageId: string) => { const { max_votes_allowed, ownVotesByOptionId } = this.data; - const reachedVoteLimit = max_votes_allowed && max_votes_allowed === Object.keys(ownVotesByOptionId).length; + const reachedVoteLimit = + max_votes_allowed && max_votes_allowed === Object.keys(ownVotesByOptionId).length; if (reachedVoteLimit) { let oldestVote = Object.values(ownVotesByOptionId)[0]; Object.values(ownVotesByOptionId) .slice(1) .forEach((vote) => { - if (!oldestVote?.created_at || new Date(vote.created_at) < new Date(oldestVote.created_at)) { + if ( + !oldestVote?.created_at || + new Date(vote.created_at) < new Date(oldestVote.created_at) + ) { oldestVote = vote; } }); @@ -322,28 +328,35 @@ export class Poll { await this.removeVote(oldestVote.id, messageId); } } - return await this.client.castPollVote(messageId, this.id as string, { option_id: optionId }); - }; - - removeVote = async (voteId: string, messageId: string) => { - return await this.client.removePollVote(messageId, this.id as string, voteId); - }; - - addAnswer = async (answerText: string, messageId: string) => { - return await this.client.addPollAnswer(messageId, this.id as string, answerText); - }; - - removeAnswer = async (answerId: string, messageId: string) => { - return await this.client.removePollVote(messageId, this.id as string, answerId); - }; - - queryAnswers = async (params: PollAnswersQueryParams) => { - return await this.client.queryPollAnswers(this.id as string, params.filter, params.sort, params.options); + return await this.client.castPollVote(messageId, this.id as string, { + option_id: optionId, + }); }; - queryOptionVotes = async (params: PollOptionVotesQueryParams) => { - return await this.client.queryPollVotes(this.id as string, params.filter, params.sort, params.options); - }; + removeVote = async (voteId: string, messageId: string) => + await this.client.removePollVote(messageId, this.id as string, voteId); + + addAnswer = async (answerText: string, messageId: string) => + await this.client.addPollAnswer(messageId, this.id as string, answerText); + + removeAnswer = async (answerId: string, messageId: string) => + await this.client.removePollVote(messageId, this.id as string, answerId); + + queryAnswers = async (params: PollAnswersQueryParams) => + await this.client.queryPollAnswers( + this.id as string, + params.filter, + params.sort, + params.options, + ); + + queryOptionVotes = async (params: PollOptionVotesQueryParams) => + await this.client.queryPollVotes( + this.id as string, + params.filter, + params.sort, + params.options, + ); } function getMaxVotedOptionIds(voteCountsByOption: PollResponse['vote_counts_by_option']) { diff --git a/src/poll_manager.ts b/src/poll_manager.ts index d81f8ee216..c5b5447e20 100644 --- a/src/poll_manager.ts +++ b/src/poll_manager.ts @@ -8,7 +8,7 @@ import type { QueryPollsOptions, } from './types'; import { Poll } from './poll'; -import { FormatMessageResponse } from './types'; +import type { FormatMessageResponse } from './types'; import { formatMessage } from './utils'; export class PollManager { @@ -29,9 +29,7 @@ export class PollManager { return this.pollCache; } - public fromState = (id: string) => { - return this.pollCache.get(id); - }; + public fromState = (id: string) => this.pollCache.get(id); public registerSubscriptions = () => { if (this.unsubscribeFunctions.size) { @@ -74,7 +72,11 @@ export class PollManager { return this.fromState(id); }; - public queryPolls = async (filter: QueryPollsFilters, sort: PollSort = [], options: QueryPollsOptions = {}) => { + public queryPolls = async ( + filter: QueryPollsFilters, + sort: PollSort = [], + options: QueryPollsOptions = {}, + ) => { const { polls, next } = await this.client.queryPolls(filter, sort, options); const pollInstances = polls.map((poll) => { @@ -89,7 +91,10 @@ export class PollManager { }; }; - public hydratePollCache = (messages: FormatMessageResponse[] | MessageResponse[], overwriteState?: boolean) => { + public hydratePollCache = ( + messages: FormatMessageResponse[] | MessageResponse[], + overwriteState?: boolean, + ) => { for (const message of messages) { if (!message.poll) { continue; @@ -99,7 +104,10 @@ export class PollManager { } }; - private setOrOverwriteInCache = (pollResponse: PollResponse, overwriteState?: boolean) => { + private setOrOverwriteInCache = ( + pollResponse: PollResponse, + overwriteState?: boolean, + ) => { if (!this.client._cacheEnabled()) { return; } @@ -112,53 +120,47 @@ export class PollManager { } }; - private subscribePollUpdated = () => { - return this.client.on('poll.updated', (event) => { + private subscribePollUpdated = () => + this.client.on('poll.updated', (event) => { if (event.poll?.id) { this.fromState(event.poll.id)?.handlePollUpdated(event); } }).unsubscribe; - }; - private subscribePollClosed = () => { - return this.client.on('poll.closed', (event) => { + private subscribePollClosed = () => + this.client.on('poll.closed', (event) => { if (event.poll?.id) { this.fromState(event.poll.id)?.handlePollClosed(event); } }).unsubscribe; - }; - private subscribeVoteCasted = () => { - return this.client.on('poll.vote_casted', (event) => { + private subscribeVoteCasted = () => + this.client.on('poll.vote_casted', (event) => { if (event.poll?.id) { this.fromState(event.poll.id)?.handleVoteCasted(event); } }).unsubscribe; - }; - private subscribeVoteChanged = () => { - return this.client.on('poll.vote_changed', (event) => { + private subscribeVoteChanged = () => + this.client.on('poll.vote_changed', (event) => { if (event.poll?.id) { this.fromState(event.poll.id)?.handleVoteChanged(event); } }).unsubscribe; - }; - private subscribeVoteRemoved = () => { - return this.client.on('poll.vote_removed', (event) => { + private subscribeVoteRemoved = () => + this.client.on('poll.vote_removed', (event) => { if (event.poll?.id) { this.fromState(event.poll.id)?.handleVoteRemoved(event); } }).unsubscribe; - }; - private subscribeMessageNew = () => { - return this.client.on('message.new', (event) => { + private subscribeMessageNew = () => + this.client.on('message.new', (event) => { const { message } = event; if (message) { const formattedMessage = formatMessage(message); this.hydratePollCache([formattedMessage]); } }).unsubscribe; - }; } diff --git a/src/search_controller.ts b/src/search_controller.ts index 1f14e469c7..f46cf212b3 100644 --- a/src/search_controller.ts +++ b/src/search_controller.ts @@ -1,4 +1,5 @@ -import { debounce, DebouncedFunc } from './utils'; +import type { DebouncedFunc } from './utils'; +import { debounce } from './utils'; import { StateStore } from './store'; import type { Channel } from './channel'; import type { StreamChat } from './client'; @@ -151,7 +152,13 @@ export abstract class BaseSearchSource implements SearchSource { async executeQuery(newSearchString?: string) { const hasNewSearchQuery = typeof newSearchString !== 'undefined'; const searchString = newSearchString ?? this.searchQuery; - if (!this.isActive || this.isLoading || (!this.hasNext && !hasNewSearchQuery) || !searchString) return; + if ( + !this.isActive || + this.isLoading || + (!this.hasNext && !hasNewSearchQuery) || + !searchString + ) + return; if (hasNewSearchQuery) { this.state.next({ @@ -215,7 +222,10 @@ export class UserSearchSource extends BaseSearchSource { protected async query(searchQuery: string) { const filters = { - $or: [{ id: { $autocomplete: searchQuery } }, { name: { $autocomplete: searchQuery } }], + $or: [ + { id: { $autocomplete: searchQuery } }, + { name: { $autocomplete: searchQuery } }, + ], ...this.filters, } as UserFilters; const sort = { id: 1, ...this.sort } as UserSort; @@ -298,7 +308,11 @@ export class MessageSearchSource extends BaseSearchSource { sort, } as SearchOptions; - const { next, results } = await this.client.search(channelFilters, messageFilters, options); + const { next, results } = await this.client.search( + channelFilters, + messageFilters, + options, + ); const items = results.map(({ message }) => message); const cids = Array.from( @@ -330,7 +344,11 @@ export class MessageSearchSource extends BaseSearchSource { } } -export type DefaultSearchSources = [UserSearchSource, ChannelSearchSource, MessageSearchSource]; +export type DefaultSearchSources = [ + UserSearchSource, + ChannelSearchSource, + MessageSearchSource, +]; export type SearchControllerState = { isActive: boolean; @@ -402,7 +420,8 @@ export class SearchController { }); }; - getSource = (sourceType: SearchSource['type']) => this.sources.find((s) => s.type === sourceType); + getSource = (sourceType: SearchSource['type']) => + this.sources.find((s) => s.type === sourceType); removeSource = (sourceType: SearchSource['type']) => { const newSources = this.sources.filter((s) => s.type !== sourceType); @@ -434,7 +453,9 @@ export class SearchController { activate = () => { if (!this.activeSources.length) { - const sourcesToActivate = this.config.keepSingleActiveSource ? this.sources.slice(0, 1) : this.sources; + const sourcesToActivate = this.config.keepSingleActiveSource + ? this.sources.slice(0, 1) + : this.sources; sourcesToActivate.forEach((s) => s.activate()); } if (this.isActive) return; @@ -455,7 +476,9 @@ export class SearchController { clear = () => { this.cancelSearchQueries(); - this.sources.forEach((source) => source.state.next({ ...source.initialState, isActive: source.isActive })); + this.sources.forEach((source) => + source.state.next({ ...source.initialState, isActive: source.isActive }), + ); this.state.next((current) => ({ ...current, isActive: true, @@ -466,7 +489,9 @@ export class SearchController { exit = () => { this.cancelSearchQueries(); - this.sources.forEach((source) => source.state.next({ ...source.initialState, isActive: source.isActive })); + this.sources.forEach((source) => + source.state.next({ ...source.initialState, isActive: source.isActive }), + ); this.state.next((current) => ({ ...current, isActive: false, diff --git a/src/segment.ts b/src/segment.ts index 2f60985c80..a3091fd361 100644 --- a/src/segment.ts +++ b/src/segment.ts @@ -1,5 +1,10 @@ -import { StreamChat } from './client'; -import { QuerySegmentTargetsFilter, SegmentData, SegmentResponse, SortParam } from './types'; +import type { StreamChat } from './client'; +import type { + QuerySegmentTargetsFilter, + SegmentData, + SegmentResponse, + SortParam, +} from './types'; type SegmentType = 'user' | 'channel'; @@ -15,14 +20,19 @@ export class Segment { client: StreamChat; data?: SegmentData | SegmentResponse; - constructor(client: StreamChat, type: SegmentType, id: string | null, data?: SegmentData) { + constructor( + client: StreamChat, + type: SegmentType, + id: string | null, + data?: SegmentData, + ) { this.client = client; this.type = type; this.id = id; this.data = data; } - async create() { + create() { const body = { name: this.data?.name, filter: this.data?.filter, @@ -42,38 +52,42 @@ export class Segment { } } - async get() { + get() { this.verifySegmentId(); return this.client.getSegment(this.id as string); } - async update(data: Partial) { + update(data: Partial) { this.verifySegmentId(); return this.client.updateSegment(this.id as string, data); } - async addTargets(targets: string[]) { + addTargets(targets: string[]) { this.verifySegmentId(); return this.client.addSegmentTargets(this.id as string, targets); } - async removeTargets(targets: string[]) { + removeTargets(targets: string[]) { this.verifySegmentId(); return this.client.removeSegmentTargets(this.id as string, targets); } - async delete() { + delete() { this.verifySegmentId(); return this.client.deleteSegment(this.id as string); } - async targetExists(targetId: string) { + targetExists(targetId: string) { this.verifySegmentId(); return this.client.segmentTargetExists(this.id as string, targetId); } - async queryTargets(filter: QuerySegmentTargetsFilter | null = {}, sort: SortParam[] | null | [] = [], options = {}) { + queryTargets( + filter: QuerySegmentTargetsFilter | null = {}, + sort: SortParam[] | null | [] = [], + options = {}, + ) { this.verifySegmentId(); return this.client.querySegmentTargets(this.id as string, filter, sort, options); diff --git a/src/signing.ts b/src/signing.ts index 1ef1e0e727..661a77190f 100644 --- a/src/signing.ts +++ b/src/signing.ts @@ -1,7 +1,7 @@ import jwt from 'jsonwebtoken'; import crypto from 'crypto'; -import { encodeBase64, decodeBase64 } from './base64'; -import { UR } from './types'; +import { decodeBase64, encodeBase64 } from './base64'; +import type { UR } from './types'; /** * Creates the JWT token that can be used for a UserSession @@ -36,7 +36,10 @@ export function JWTUserToken( ); } - const opts: jwt.SignOptions = Object.assign({ algorithm: 'HS256', noTimestamp: true }, jwtOptions); + const opts: jwt.SignOptions = Object.assign( + { algorithm: 'HS256', noTimestamp: true }, + jwtOptions, + ); if (payload.iat) { opts.noTimestamp = false; @@ -49,7 +52,10 @@ export function JWTServerToken(apiSecret: jwt.Secret, jwtOptions: jwt.SignOption server: true, }; - const opts: jwt.SignOptions = Object.assign({ algorithm: 'HS256', noTimestamp: true }, jwtOptions); + const opts: jwt.SignOptions = Object.assign( + { algorithm: 'HS256', noTimestamp: true }, + jwtOptions, + ); return jwt.sign(payload, apiSecret, opts); } diff --git a/src/store.ts b/src/store.ts index a2c18e7557..8c71116f5d 100644 --- a/src/store.ts +++ b/src/store.ts @@ -3,20 +3,19 @@ export type ValueOrPatch = T | Patch; export type Handler = (nextValue: T, previousValue: T | undefined) => void; export type Unsubscribe = () => void; -export const isPatch = (value: ValueOrPatch): value is Patch => { - return typeof value === 'function'; -}; +export const isPatch = (value: ValueOrPatch): value is Patch => + typeof value === 'function'; export class StateStore> { private handlerSet = new Set>(); - private static logCount = 5; - constructor(private value: T) {} public next = (newValueOrPatch: ValueOrPatch): void => { // newValue (or patch output) should never be mutated previous value - const newValue = isPatch(newValueOrPatch) ? newValueOrPatch(this.value) : newValueOrPatch; + const newValue = isPatch(newValueOrPatch) + ? newValueOrPatch(this.value) + : newValueOrPatch; // do not notify subscribers if the value hasn't changed if (newValue === this.value) return; @@ -27,7 +26,8 @@ export class StateStore> { this.handlerSet.forEach((handler) => handler(this.value, oldValue)); }; - public partialNext = (partial: Partial): void => this.next((current) => ({ ...current, ...partial })); + public partialNext = (partial: Partial): void => + this.next((current) => ({ ...current, ...partial })); public getLatestValue = (): T => this.value; @@ -39,7 +39,9 @@ export class StateStore> { }; }; - public subscribeWithSelector = > | Readonly>( + public subscribeWithSelector = < + O extends Readonly> | Readonly, + >( selector: (nextValue: T) => O, handler: Handler, ) => { @@ -51,15 +53,7 @@ export class StateStore> { let hasUpdatedValues = !selectedValues; - if (Array.isArray(newlySelectedValues) && StateStore.logCount > 0) { - console.warn( - '[StreamChat]: The API of our StateStore has changed. Instead of returning an array in the selector, please return a named object of properties.', - ); - StateStore.logCount--; - } - for (const key in selectedValues) { - // @ts-ignore TODO: remove array support (Readonly) if (selectedValues[key] === newlySelectedValues[key]) continue; hasUpdatedValues = true; break; diff --git a/src/thread.ts b/src/thread.ts index 5ed74ddd34..844f049801 100644 --- a/src/thread.ts +++ b/src/thread.ts @@ -1,5 +1,10 @@ import { StateStore } from './store'; -import { addToMessageList, findIndexInSortedArray, formatMessage, throttle } from './utils'; +import { + addToMessageList, + findIndexInSortedArray, + formatMessage, + throttle, +} from './utils'; import type { AscDesc, EventTypes, @@ -109,16 +114,31 @@ export class Thread { private unsubscribeFunctions: Set<() => void> = new Set(); private failedRepliesMap: Map = new Map(); - constructor({ client, threadData }: { client: StreamChat; threadData: ThreadResponse }) { + constructor({ + client, + threadData, + }: { + client: StreamChat; + threadData: ThreadResponse; + }) { const channel = client.channel(threadData.channel.type, threadData.channel.id, { name: threadData.channel.name, }); - channel._hydrateMembers({ members: threadData.channel.members ?? [], overrideCurrentState: false }); + channel._hydrateMembers({ + members: threadData.channel.members ?? [], + overrideCurrentState: false, + }); // For when read object is undefined and due to that unreadMessageCount for // the current user isn't being incremented on message.new const placeholderReadResponse: ReadResponse[] = client.userID - ? [{ user: { id: client.userID }, unread_messages: 0, last_read: new Date().toISOString() }] + ? [ + { + user: { id: client.userID }, + unread_messages: 0, + last_read: new Date().toISOString(), + }, + ] : []; this.state = new StateStore({ @@ -135,7 +155,9 @@ export class Thread { parentMessage: formatMessage(threadData.parent_message), participants: threadData.thread_participants, read: formatReadState( - !threadData.read || threadData.read.length === 0 ? placeholderReadResponse : threadData.read, + !threadData.read || threadData.read.length === 0 + ? placeholderReadResponse + : threadData.read, ), replies: threadData.latest_replies.map(formatMessage), replyCount: threadData.reply_count ?? 0, @@ -236,8 +258,8 @@ export class Thread { this.unsubscribeFunctions.add(this.subscribeMessageUpdated()); }; - private subscribeThreadUpdated = () => { - return this.client.on('thread.updated', (event) => { + private subscribeThreadUpdated = () => + this.client.on('thread.updated', (event) => { if (!event.thread || event.thread.parent_message_id !== this.id) { return; } @@ -252,10 +274,9 @@ export class Thread { custom: constructCustomDataObject(threadData), }); }).unsubscribe; - }; - private subscribeMarkActiveThreadRead = () => { - return this.state.subscribeWithSelector( + private subscribeMarkActiveThreadRead = () => + this.state.subscribeWithSelector( (nextValue) => ({ active: nextValue.active, unreadMessageCount: ownUnreadCountSelector(this.client.userID)(nextValue), @@ -265,7 +286,6 @@ export class Thread { this.throttledMarkAsRead(); }, ); - }; private subscribeReloadActiveStaleThread = () => this.state.subscribeWithSelector( @@ -281,7 +301,11 @@ export class Thread { this.client.on('user.watching.stop', (event) => { const { channel } = this.state.getLatestValue(); - if (!this.client.userID || this.client.userID !== event.user?.id || event.channel?.cid !== channel.cid) { + if ( + !this.client.userID || + this.client.userID !== event.user?.id || + event.channel?.cid !== channel.cid + ) { return; } @@ -386,7 +410,12 @@ export class Thread { }).unsubscribe; private subscribeMessageUpdated = () => { - const eventTypes: EventTypes[] = ['message.updated', 'reaction.new', 'reaction.deleted', 'reaction.updated']; + const eventTypes: EventTypes[] = [ + 'message.updated', + 'reaction.new', + 'reaction.deleted', + 'reaction.updated', + ]; const unsubscribeFunctions = eventTypes.map( (eventType) => @@ -489,23 +518,24 @@ export class Thread { return await this.channel.markRead({ thread_id: this.id }); }; - private throttledMarkAsRead = throttle(() => this.markAsRead(), MARK_AS_READ_THROTTLE_TIMEOUT, { trailing: true }); + private throttledMarkAsRead = throttle( + () => this.markAsRead(), + MARK_AS_READ_THROTTLE_TIMEOUT, + { trailing: true }, + ); public queryReplies = ({ limit = DEFAULT_PAGE_LIMIT, sort = DEFAULT_SORT, ...otherOptions - }: QueryRepliesOptions = {}) => { - return this.channel.getReplies(this.id, { limit, ...otherOptions }, sort); - }; + }: QueryRepliesOptions = {}) => + this.channel.getReplies(this.id, { limit, ...otherOptions }, sort); - public loadNextPage = ({ limit = DEFAULT_PAGE_LIMIT }: { limit?: number } = {}) => { - return this.loadPage(limit); - }; + public loadNextPage = ({ limit = DEFAULT_PAGE_LIMIT }: { limit?: number } = {}) => + this.loadPage(limit); - public loadPrevPage = ({ limit = DEFAULT_PAGE_LIMIT }: { limit?: number } = {}) => { - return this.loadPage(-limit); - }; + public loadPrevPage = ({ limit = DEFAULT_PAGE_LIMIT }: { limit?: number } = {}) => + this.loadPage(-limit); private loadPage = async (count: number) => { const { pagination } = this.state.getLatestValue(); @@ -569,16 +599,22 @@ const formatReadState = (read: ReadResponse[]): ThreadReadState => return state; }, {}); -const repliesPaginationFromInitialThread = (thread: ThreadResponse): ThreadRepliesPagination => { - const latestRepliesContainsAllReplies = thread.latest_replies.length === thread.reply_count; +const repliesPaginationFromInitialThread = ( + thread: ThreadResponse, +): ThreadRepliesPagination => { + const latestRepliesContainsAllReplies = + thread.latest_replies.length === thread.reply_count; return { nextCursor: null, - prevCursor: latestRepliesContainsAllReplies ? null : thread.latest_replies.at(0)?.id ?? null, + prevCursor: latestRepliesContainsAllReplies + ? null + : (thread.latest_replies.at(0)?.id ?? null), isLoadingNext: false, isLoadingPrev: false, }; }; -const ownUnreadCountSelector = (currentUserId: string | undefined) => (state: ThreadState) => - (currentUserId && state.read[currentUserId]?.unreadMessageCount) || 0; +const ownUnreadCountSelector = + (currentUserId: string | undefined) => (state: ThreadState) => + (currentUserId && state.read[currentUserId]?.unreadMessageCount) || 0; diff --git a/src/thread_manager.ts b/src/thread_manager.ts index 4a4568ba0b..b70f0d11f1 100644 --- a/src/thread_manager.ts +++ b/src/thread_manager.ts @@ -66,10 +66,13 @@ export class ThreadManager { return this.threadsByIdGetterCache.threadsById; } - const threadsById = threads.reduce>((newThreadsById, thread) => { - newThreadsById[thread.id] = thread; - return newThreadsById; - }, {}); + const threadsById = threads.reduce>( + (newThreadsById, thread) => { + newThreadsById[thread.id] = thread; + return newThreadsById; + }, + {}, + ); this.threadsByIdGetterCache.threads = threads; this.threadsByIdGetterCache.threadsById = threadsById; @@ -102,7 +105,8 @@ export class ThreadManager { private subscribeUnreadThreadsCountChange = () => { // initiate - const { unread_threads: unreadThreadCount = 0 } = (this.client.user as OwnUserResponse) ?? {}; + const { unread_threads: unreadThreadCount = 0 } = + (this.client.user as OwnUserResponse) ?? {}; this.state.partialNext({ unreadThreadCount }); const unsubscribeFunctions = [ @@ -139,7 +143,9 @@ export class ThreadManager { const { threads: prevThreads = [] } = prev ?? {}; // Thread instance was removed if there's no thread with the given id at all, // or it was replaced with a new instance - const removedThreads = prevThreads.filter((thread) => thread !== this.threadsById[thread.id]); + const removedThreads = prevThreads.filter( + (thread) => thread !== this.threadsById[thread.id], + ); nextThreads.forEach((thread) => thread.registerSubscriptions()); removedThreads.forEach((thread) => thread.unregisterSubscriptions()); @@ -193,8 +199,10 @@ export class ThreadManager { { trailing: true }, ); - const unsubscribeConnectionRecovered = this.client.on('connection.recovered', throttledHandleConnectionRecovered) - .unsubscribe; + const unsubscribeConnectionRecovered = this.client.on( + 'connection.recovered', + throttledHandleConnectionRecovered, + ).unsubscribe; return () => { unsubscribeConnectionDropped(); @@ -203,13 +211,16 @@ export class ThreadManager { }; public unregisterSubscriptions = () => { - this.state.getLatestValue().threads.forEach((thread) => thread.unregisterSubscriptions()); + this.state + .getLatestValue() + .threads.forEach((thread) => thread.unregisterSubscriptions()); this.unsubscribeFunctions.forEach((cleanupFunction) => cleanupFunction()); this.unsubscribeFunctions.clear(); }; public reload = async ({ force = false } = {}) => { - const { threads, unseenThreadIds, isThreadOrderStale, pagination, ready } = this.state.getLatestValue(); + const { threads, unseenThreadIds, isThreadOrderStale, pagination, ready } = + this.state.getLatestValue(); if (pagination.isLoading) return; if (!force && ready && !unseenThreadIds.length && !isThreadOrderStale) return; const limit = threads.length + unseenThreadIds.length; @@ -268,15 +279,14 @@ export class ThreadManager { } }; - public queryThreads = (options: QueryThreadsOptions = {}) => { - return this.client.queryThreads({ + public queryThreads = (options: QueryThreadsOptions = {}) => + this.client.queryThreads({ limit: 25, participant_limit: 10, reply_limit: 10, watch: true, ...options, }); - }; public loadNextPage = async (options: Omit = {}) => { const { pagination } = this.state.getLatestValue(); @@ -293,7 +303,9 @@ export class ThreadManager { this.state.next((current) => ({ ...current, - threads: response.threads.length ? current.threads.concat(response.threads) : current.threads, + threads: response.threads.length + ? current.threads.concat(response.threads) + : current.threads, pagination: { ...current.pagination, nextCursor: response.next ?? null, diff --git a/src/token_manager.ts b/src/token_manager.ts index d0775de165..0366c6a484 100644 --- a/src/token_manager.ts +++ b/src/token_manager.ts @@ -1,6 +1,6 @@ -import jwt from 'jsonwebtoken'; +import type jwt from 'jsonwebtoken'; -import { UserFromToken, JWTServerToken, JWTUserToken } from './signing'; +import { JWTServerToken, JWTUserToken, UserFromToken } from './signing'; import { isFunction } from './utils'; import type { TokenOrProvider, UserResponse } from './types'; @@ -85,7 +85,11 @@ export class TokenManager { throw new Error('User token can not be empty'); } - if (tokenOrProvider && typeof tokenOrProvider !== 'string' && !isFunction(tokenOrProvider)) { + if ( + tokenOrProvider && + typeof tokenOrProvider !== 'string' && + !isFunction(tokenOrProvider) + ) { throw new Error('user token should either be a string or a function'); } @@ -94,8 +98,13 @@ export class TokenManager { if (user.anon && tokenOrProvider === '') return; const tokenUserId = UserFromToken(tokenOrProvider); - if (tokenOrProvider != null && (tokenUserId == null || tokenUserId === '' || tokenUserId !== user.id)) { - throw new Error('userToken does not have a user_id or is not matching with user.id'); + if ( + tokenOrProvider != null && + (tokenUserId == null || tokenUserId === '' || tokenUserId !== user.id) + ) { + throw new Error( + 'userToken does not have a user_id or is not matching with user.id', + ); } } }; diff --git a/src/types.ts b/src/types.ts index 3998f31e76..e988cd412b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,20 +1,20 @@ -import { EVENT_MAP } from './events'; +import type { EVENT_MAP } from './events'; import type { Channel } from './channel'; import type { AxiosRequestConfig, AxiosResponse } from 'axios'; import type { StableWSConnection } from './connection'; import type { Role } from './permissions'; import type { + CustomAttachmentData, CustomChannelData, - CustomMemberData, - CustomThreadData, + CustomCommandData, CustomEventData, + CustomMemberData, CustomMessageData, - CustomUserData, - CustomReactionData, - CustomAttachmentData, - CustomCommandData, CustomPollData, CustomPollOptionData, + CustomReactionData, + CustomThreadData, + CustomUserData, } from './custom_types'; /** @@ -56,10 +56,10 @@ export type UnknownType = UR; // alias to avoid breaking change export type Unpacked = T extends (infer U)[] ? U // eslint-disable-next-line @typescript-eslint/no-explicit-any : T extends (...args: any[]) => infer U - ? U - : T extends Promise - ? U - : T; + ? U + : T extends Promise + ? U + : T; /** * Response Types @@ -441,7 +441,7 @@ export type FlagMessageResponse = APIResponse & { user: UserResponse; approved_at?: string; channel_cid?: string; - details?: Object; // Any JSON + details?: object; // Any JSON message_user_id?: string; rejected_at?: string; reviewed_at?: string; @@ -458,7 +458,7 @@ export type FlagUserResponse = APIResponse & { updated_at: string; user: UserResponse; approved_at?: string; - details?: Object; // Any JSON + details?: object; // Any JSON rejected_at?: string; reviewed_at?: string; reviewed_by?: string; @@ -893,11 +893,14 @@ export type FlagReportsPaginationOptions = { }; export type ReviewFlagReportOptions = { - review_details?: Object; + review_details?: object; user_id?: string; }; -export type BannedUsersPaginationOptions = Omit & { +export type BannedUsersPaginationOptions = Omit< + PaginationOptions, + 'id_gt' | 'id_gte' | 'id_lt' | 'id_lte' +> & { exclude_expired_bans?: boolean; }; @@ -994,9 +997,13 @@ export type DeactivateUsersOptions = { mark_messages_deleted?: boolean; }; -export type NewMemberPayload = CustomMemberData & Pick; +export type NewMemberPayload = CustomMemberData & + Pick; -export type Thresholds = Record<'explicit' | 'spam' | 'toxic', Partial<{ block: number; flag: number }>>; +export type Thresholds = Record< + 'explicit' | 'spam' | 'toxic', + Partial<{ block: number; flag: number }> +>; export type BlockListOptions = { behavior: BlocklistBehavior; @@ -1445,21 +1452,30 @@ export type MessageFlagsFiltersOptions = { export type MessageFlagsFilters = QueryFilters< { channel_cid?: - | RequireOnlyOne, '$eq' | '$in'>> + | RequireOnlyOne< + Pick, '$eq' | '$in'> + > | PrimitiveFilter; } & { team?: - | RequireOnlyOne, '$eq' | '$in'>> + | RequireOnlyOne< + Pick, '$eq' | '$in'> + > | PrimitiveFilter; } & { user_id?: - | RequireOnlyOne, '$eq' | '$in'>> + | RequireOnlyOne< + Pick, '$eq' | '$in'> + > | PrimitiveFilter; } & { - [Key in keyof Omit]: - | RequireOnlyOne> - | PrimitiveFilter; - } + [Key in keyof Omit< + MessageFlagsFiltersOptions, + 'channel_cid' | 'user_id' | 'is_reviewed' + >]: + | RequireOnlyOne> + | PrimitiveFilter; + } >; export type FlagsFiltersOptions = { @@ -1478,19 +1494,27 @@ export type FlagsFilters = QueryFilters< | PrimitiveFilter; } & { message_id?: - | RequireOnlyOne, '$eq' | '$in'>> + | RequireOnlyOne< + Pick, '$eq' | '$in'> + > | PrimitiveFilter; } & { message_user_id?: - | RequireOnlyOne, '$eq' | '$in'>> + | RequireOnlyOne< + Pick, '$eq' | '$in'> + > | PrimitiveFilter; } & { channel_cid?: - | RequireOnlyOne, '$eq' | '$in'>> + | RequireOnlyOne< + Pick, '$eq' | '$in'> + > | PrimitiveFilter; } & { reporter_id?: - | RequireOnlyOne, '$eq' | '$in'>> + | RequireOnlyOne< + Pick, '$eq' | '$in'> + > | PrimitiveFilter; } & { team?: @@ -1514,42 +1538,60 @@ export type FlagReportsFiltersOptions = { export type FlagReportsFilters = QueryFilters< { report_id?: - | RequireOnlyOne, '$eq' | '$in'>> + | RequireOnlyOne< + Pick, '$eq' | '$in'> + > | PrimitiveFilter; } & { review_result?: - | RequireOnlyOne, '$eq' | '$in'>> + | RequireOnlyOne< + Pick, '$eq' | '$in'> + > | PrimitiveFilter; } & { reviewed_by?: - | RequireOnlyOne, '$eq' | '$in'>> + | RequireOnlyOne< + Pick, '$eq' | '$in'> + > | PrimitiveFilter; } & { user_id?: - | RequireOnlyOne, '$eq' | '$in'>> + | RequireOnlyOne< + Pick, '$eq' | '$in'> + > | PrimitiveFilter; } & { message_id?: - | RequireOnlyOne, '$eq' | '$in'>> + | RequireOnlyOne< + Pick, '$eq' | '$in'> + > | PrimitiveFilter; } & { message_user_id?: - | RequireOnlyOne, '$eq' | '$in'>> + | RequireOnlyOne< + Pick, '$eq' | '$in'> + > | PrimitiveFilter; } & { channel_cid?: - | RequireOnlyOne, '$eq' | '$in'>> + | RequireOnlyOne< + Pick, '$eq' | '$in'> + > | PrimitiveFilter; } & { team?: - | RequireOnlyOne, '$eq' | '$in'>> + | RequireOnlyOne< + Pick, '$eq' | '$in'> + > | PrimitiveFilter; } & { - [Key in keyof Omit< - FlagReportsFiltersOptions, - 'report_id' | 'user_id' | 'message_id' | 'review_result' | 'reviewed_by' - >]: RequireOnlyOne> | PrimitiveFilter; - } + [Key in keyof Omit< + FlagReportsFiltersOptions, + 'report_id' | 'user_id' | 'message_id' | 'review_result' | 'reviewed_by' + >]: + | RequireOnlyOne> + | PrimitiveFilter; + } >; export type BannedUsersFilterOptions = { @@ -1563,7 +1605,9 @@ export type BannedUsersFilterOptions = { export type BannedUsersFilters = QueryFilters< { channel_cid?: - | RequireOnlyOne, '$eq' | '$in'>> + | RequireOnlyOne< + Pick, '$eq' | '$in'> + > | PrimitiveFilter; } & { reason?: @@ -1574,10 +1618,10 @@ export type BannedUsersFilters = QueryFilters< > | PrimitiveFilter; } & { - [Key in keyof Omit]: - | RequireOnlyOne> - | PrimitiveFilter; - } + [Key in keyof Omit]: + | RequireOnlyOne> + | PrimitiveFilter; + } >; export type ReactionFilters = QueryFilters< @@ -1591,7 +1635,12 @@ export type ReactionFilters = QueryFilters< | PrimitiveFilter; } & { created_at?: - | RequireOnlyOne, '$eq' | '$gt' | '$lt' | '$gte' | '$lte'>> + | RequireOnlyOne< + Pick< + QueryFilter, + '$eq' | '$gt' | '$lt' | '$gte' | '$lte' + > + > | PrimitiveFilter; } >; @@ -1619,10 +1668,10 @@ export type ChannelFilters = QueryFilters< | PrimitiveFilter; pinned?: boolean; } & { - [Key in keyof Omit]: - | RequireOnlyOne> - | PrimitiveFilter; - } + [Key in keyof Omit]: + | RequireOnlyOne> + | PrimitiveFilter; + } >; export type QueryPollsParams = { @@ -1643,7 +1692,9 @@ export type QueryVotesOptions = Pager; export type QueryPollsFilters = QueryFilters< { - id?: RequireOnlyOne, '$eq' | '$in'>> | PrimitiveFilter; + id?: + | RequireOnlyOne, '$eq' | '$in'>> + | PrimitiveFilter; } & { user_id?: | RequireOnlyOne, '$eq' | '$in'>> @@ -1654,7 +1705,12 @@ export type QueryPollsFilters = QueryFilters< | PrimitiveFilter; } & { max_votes_allowed?: - | RequireOnlyOne, '$eq' | '$gt' | '$lt' | '$gte' | '$lte'>> + | RequireOnlyOne< + Pick< + QueryFilter, + '$eq' | '$gt' | '$lt' | '$gte' | '$lte' + > + > | PrimitiveFilter; } & { allow_answers?: @@ -1662,7 +1718,9 @@ export type QueryPollsFilters = QueryFilters< | PrimitiveFilter; } & { allow_user_suggested_options?: - | RequireOnlyOne, '$eq'>> + | RequireOnlyOne< + Pick, '$eq'> + > | PrimitiveFilter; } & { voting_visibility?: @@ -1670,7 +1728,12 @@ export type QueryPollsFilters = QueryFilters< | PrimitiveFilter; } & { created_at?: - | RequireOnlyOne, '$eq' | '$gt' | '$lt' | '$gte' | '$lte'>> + | RequireOnlyOne< + Pick< + QueryFilter, + '$eq' | '$gt' | '$lt' | '$gte' | '$lte' + > + > | PrimitiveFilter; } & { created_by_id?: @@ -1678,7 +1741,12 @@ export type QueryPollsFilters = QueryFilters< | PrimitiveFilter; } & { updated_at?: - | RequireOnlyOne, '$eq' | '$gt' | '$lt' | '$gte' | '$lte'>> + | RequireOnlyOne< + Pick< + QueryFilter, + '$eq' | '$gt' | '$lt' | '$gte' | '$lte' + > + > | PrimitiveFilter; } & { name?: @@ -1689,7 +1757,9 @@ export type QueryPollsFilters = QueryFilters< export type QueryVotesFilters = QueryFilters< { - id?: RequireOnlyOne, '$eq' | '$in'>> | PrimitiveFilter; + id?: + | RequireOnlyOne, '$eq' | '$in'>> + | PrimitiveFilter; } & { option_id?: | RequireOnlyOne, '$eq' | '$in'>> @@ -1704,7 +1774,12 @@ export type QueryVotesFilters = QueryFilters< | PrimitiveFilter; } & { created_at?: - | RequireOnlyOne, '$eq' | '$gt' | '$lt' | '$gte' | '$lte'>> + | RequireOnlyOne< + Pick< + QueryFilter, + '$eq' | '$gt' | '$lt' | '$gte' | '$lte' + > + > | PrimitiveFilter; } & { created_by_id?: @@ -1712,7 +1787,12 @@ export type QueryVotesFilters = QueryFilters< | PrimitiveFilter; } & { updated_at?: - | RequireOnlyOne, '$eq' | '$gt' | '$lt' | '$gte' | '$lte'>> + | RequireOnlyOne< + Pick< + QueryFilter, + '$eq' | '$gt' | '$lt' | '$gte' | '$lte' + > + > | PrimitiveFilter; } >; @@ -1739,7 +1819,9 @@ export type MessageFilters = QueryFilters< $in: PrimitiveFilter[]; }> | PrimitiveFilter; - 'mentioned_users.id'?: RequireOnlyOne<{ $contains: PrimitiveFilter }>; + 'mentioned_users.id'?: RequireOnlyOne<{ + $contains: PrimitiveFilter; + }>; text?: | RequireOnlyOne< { @@ -1756,10 +1838,10 @@ export type MessageFilters = QueryFilters< > | PrimitiveFilter; } & { - [Key in keyof Omit]?: - | RequireOnlyOne> - | PrimitiveFilter; - } + [Key in keyof Omit]?: + | RequireOnlyOne> + | PrimitiveFilter; + } >; export type MessageOptions = { @@ -1768,26 +1850,26 @@ export type MessageOptions = { export type PrimitiveFilter = ObjectType | null; -export type QueryFilter = NonNullable extends string | number | boolean - ? { - $eq?: PrimitiveFilter; - $exists?: boolean; - $gt?: PrimitiveFilter; - $gte?: PrimitiveFilter; - $in?: PrimitiveFilter[]; - $lt?: PrimitiveFilter; - $lte?: PrimitiveFilter; - } - : { - $eq?: PrimitiveFilter; - $exists?: boolean; - $in?: PrimitiveFilter[]; - }; +export type QueryFilter = + NonNullable extends string | number | boolean + ? { + $eq?: PrimitiveFilter; + $exists?: boolean; + $gt?: PrimitiveFilter; + $gte?: PrimitiveFilter; + $in?: PrimitiveFilter[]; + $lt?: PrimitiveFilter; + $lte?: PrimitiveFilter; + } + : { + $eq?: PrimitiveFilter; + $exists?: boolean; + $in?: PrimitiveFilter[]; + }; export type QueryFilters = { [Key in keyof Operators]?: Operators[Key]; -} & - QueryLogicalOperators; +} & QueryLogicalOperators; export type QueryLogicalOperators = { $and?: ArrayOneOrMore>; @@ -1798,10 +1880,14 @@ export type QueryLogicalOperators = { export type UserFilters = QueryFilters< ContainsOperator & { id?: - | RequireOnlyOne<{ $autocomplete?: UserResponse['id'] } & QueryFilter> + | RequireOnlyOne< + { $autocomplete?: UserResponse['id'] } & QueryFilter + > | PrimitiveFilter; name?: - | RequireOnlyOne<{ $autocomplete?: UserResponse['name'] } & QueryFilter> + | RequireOnlyOne< + { $autocomplete?: UserResponse['name'] } & QueryFilter + > | PrimitiveFilter; notifications_muted?: | RequireOnlyOne<{ @@ -1816,13 +1902,20 @@ export type UserFilters = QueryFilters< }> | PrimitiveFilter; username?: - | RequireOnlyOne<{ $autocomplete?: UserResponse['username'] } & QueryFilter> + | RequireOnlyOne< + { $autocomplete?: UserResponse['username'] } & QueryFilter< + UserResponse['username'] + > + > | PrimitiveFilter; } & { - [Key in keyof Omit]?: - | RequireOnlyOne> - | PrimitiveFilter; - } + [Key in keyof Omit< + UserResponse, + 'id' | 'name' | 'teams' | 'username' | keyof CustomUserData + >]?: + | RequireOnlyOne> + | PrimitiveFilter; + } >; export type InviteStatus = 'pending' | 'accepted' | 'rejected'; @@ -1831,7 +1924,9 @@ export type InviteStatus = 'pending' | 'accepted' | 'rejected'; export type MemberFilters = QueryFilters< { banned?: { $eq?: ChannelMemberResponse['banned'] } | ChannelMemberResponse['banned']; - channel_role?: { $eq?: ChannelMemberResponse['channel_role'] } | ChannelMemberResponse['channel_role']; + channel_role?: + | { $eq?: ChannelMemberResponse['channel_role'] } + | ChannelMemberResponse['channel_role']; cid?: { $eq?: ChannelResponse['cid'] } | ChannelResponse['cid']; created_at?: | { @@ -1940,7 +2035,11 @@ export type UserSort = Sort | Array>; export type MemberSort = | Sort> - | Array>>; + | Array< + Sort< + Pick + > + >; export type SearchMessageSortBase = Sort & { attachments?: AscDesc; @@ -2524,7 +2623,11 @@ export type LiteralStringForUnion = string & {}; export type LogLevel = 'info' | 'error' | 'warn'; -export type Logger = (logLevel: LogLevel, message: string, extraData?: Record) => void; +export type Logger = ( + logLevel: LogLevel, + message: string, + extraData?: Record, +) => void; export type Message = Partial & { mentioned_users?: string[]; @@ -2549,7 +2652,13 @@ export type MessageBase = CustomMessageData & { user_id?: string; }; -export type MessageLabel = 'deleted' | 'ephemeral' | 'error' | 'regular' | 'reply' | 'system'; +export type MessageLabel = + | 'deleted' + | 'ephemeral' + | 'error' + | 'regular' + | 'reply' + | 'system'; export type SendMessageOptions = { force_moderation?: boolean; @@ -3253,22 +3362,31 @@ export type CastVoteAPIResponse = { export type QueryMessageHistoryFilters = QueryFilters< { message_id?: - | RequireOnlyOne, '$eq' | '$in'>> + | RequireOnlyOne< + Pick, '$eq' | '$in'> + > | PrimitiveFilter; } & { user_id?: - | RequireOnlyOne, '$eq' | '$in'>> + | RequireOnlyOne< + Pick, '$eq' | '$in'> + > | PrimitiveFilter; } & { created_at?: | RequireOnlyOne< - Pick, '$eq' | '$gt' | '$lt' | '$gte' | '$lte'> + Pick< + QueryFilter, + '$eq' | '$gt' | '$lt' | '$gte' | '$lte' + > > | PrimitiveFilter; } >; -export type QueryMessageHistorySort = QueryMessageHistorySortBase | Array; +export type QueryMessageHistorySort = + | QueryMessageHistorySortBase + | Array; export type QueryMessageHistorySortBase = { message_updated_at?: AscDesc; @@ -3409,7 +3527,12 @@ export type ReviewQueueFilters = QueryFilters< | PrimitiveFilter; } & { completed_at?: - | RequireOnlyOne, '$eq' | '$gt' | '$lt' | '$gte' | '$lte'>> + | RequireOnlyOne< + Pick< + QueryFilter, + '$eq' | '$gt' | '$lt' | '$gte' | '$lte' + > + > | PrimitiveFilter; } & { config_key?: @@ -3421,7 +3544,12 @@ export type ReviewQueueFilters = QueryFilters< | PrimitiveFilter; } & { created_at?: - | RequireOnlyOne, '$eq' | '$gt' | '$lt' | '$gte' | '$lte'>> + | RequireOnlyOne< + Pick< + QueryFilter, + '$eq' | '$gt' | '$lt' | '$gte' | '$lte' + > + > | PrimitiveFilter; } & { id?: @@ -3435,7 +3563,12 @@ export type ReviewQueueFilters = QueryFilters< reviewed?: boolean; } & { reviewed_at?: - | RequireOnlyOne, '$eq' | '$gt' | '$lt' | '$gte' | '$lte'>> + | RequireOnlyOne< + Pick< + QueryFilter, + '$eq' | '$gt' | '$lt' | '$gte' | '$lte' + > + > | PrimitiveFilter; } & { status?: @@ -3443,7 +3576,12 @@ export type ReviewQueueFilters = QueryFilters< | PrimitiveFilter; } & { updated_at?: - | RequireOnlyOne, '$eq' | '$gt' | '$lt' | '$gte' | '$lte'>> + | RequireOnlyOne< + Pick< + QueryFilter, + '$eq' | '$gt' | '$lt' | '$gte' | '$lte' + > + > | PrimitiveFilter; } & { has_image?: boolean; @@ -3571,7 +3709,13 @@ export type AIState = | 'AI_STATE_GENERATING' | (string & {}); -export type ModerationActionType = 'flag' | 'shadow' | 'remove' | 'bounce' | 'bounce_flag' | 'bounce_remove'; +export type ModerationActionType = + | 'flag' + | 'shadow' + | 'remove' + | 'bounce' + | 'bounce_flag' + | 'bounce_remove'; export type AutomodRule = { action: ModerationActionType; @@ -3695,7 +3839,10 @@ export type PromoteChannelParams = { * An identifier containing information about the downstream SDK using stream-chat. It * is used to resolve the user agent. */ -export type SdkIdentifier = { name: 'react' | 'react-native' | 'expo' | 'angular'; version: string }; +export type SdkIdentifier = { + name: 'react' | 'react-native' | 'expo' | 'angular'; + version: string; +}; /** * An identifier containing information about the downstream device using stream-chat, if diff --git a/src/utils.ts b/src/utils.ts index fdbcb196bd..c57c1aa5e2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,25 +1,25 @@ import FormData from 'form-data'; -import { +import type { AscDesc, - Logger, - OwnUserBase, - OwnUserResponse, - UserResponse, - MessageResponse, - FormatMessageResponse, - ReactionGroupResponse, - MessageSet, - MessagePaginationOptions, + ChannelFilters, ChannelQueryOptions, - QueryChannelAPIResponse, ChannelSort, - ChannelFilters, ChannelSortBase, + FormatMessageResponse, + Logger, + MessagePaginationOptions, + MessageResponse, + MessageSet, + OwnUserBase, + OwnUserResponse, PromoteChannelParams, + QueryChannelAPIResponse, + ReactionGroupResponse, + UserResponse, } from './types'; -import { StreamChat } from './client'; -import { Channel } from './channel'; -import { AxiosRequestConfig } from 'axios'; +import type { StreamChat } from './client'; +import type { Channel } from './channel'; +import type { AxiosRequestConfig } from 'axios'; /** * logChatPromiseExecution - utility function for logging the execution of a promise.. @@ -37,12 +37,11 @@ export function logChatPromiseExecution(promise: Promise, name: string) { export const sleep = (m: number): Promise => new Promise((r) => setTimeout(r, m)); -export function isFunction(value: Function | T): value is Function { +export function isFunction(value: unknown): value is (...args: unknown[]) => unknown { return ( - value && - (Object.prototype.toString.call(value) === '[object Function]' || - 'function' === typeof value || - value instanceof Function) + typeof value === 'function' || + value instanceof Function || + Object.prototype.toString.call(value) === '[object Function]' ); } @@ -55,7 +54,8 @@ function isReadableStream(obj: unknown): obj is NodeJS.ReadStream { return ( obj !== null && typeof obj === 'object' && - ((obj as NodeJS.ReadStream).readable || typeof (obj as NodeJS.ReadStream)._read === 'function') + ((obj as NodeJS.ReadStream).readable || + typeof (obj as NodeJS.ReadStream)._read === 'function') ); } @@ -63,9 +63,9 @@ function isBuffer(obj: unknown): obj is Buffer { return ( obj != null && (obj as Buffer).constructor != null && - // @ts-expect-error + // @ts-expect-error expected typeof obj.constructor.isBuffer === 'function' && - // @ts-expect-error + // @ts-expect-error expected obj.constructor.isBuffer(obj) ); } @@ -74,7 +74,9 @@ function isFileWebAPI(uri: unknown): uri is File { return typeof window !== 'undefined' && 'File' in window && uri instanceof File; } -export function isOwnUser(user?: OwnUserResponse | UserResponse): user is OwnUserResponse { +export function isOwnUser( + user?: OwnUserResponse | UserResponse, +): user is OwnUserResponse { return (user as OwnUserResponse)?.total_unread_count !== undefined; } @@ -123,7 +125,9 @@ export function addFileToFormData( return data; } -export function normalizeQuerySort>(sort: T | T[]) { +export function normalizeQuerySort>( + sort: T | T[], +) { const sortFields: Array<{ direction: AscDesc; field: keyof T }> = []; const sortArr = Array.isArray(sort) ? sort : [sort]; for (const item of sortArr) { @@ -234,11 +238,13 @@ export function isOnline() { typeof navigator !== 'undefined' ? navigator : typeof window !== 'undefined' && window.navigator - ? window.navigator - : undefined; + ? window.navigator + : undefined; if (!nav) { - console.warn('isOnline failed to access window.navigator and assume browser is online'); + console.warn( + 'isOnline failed to access window.navigator and assume browser is online', + ); return true; } @@ -290,7 +296,9 @@ export const axiosParamsSerializer: AxiosRequestConfig['paramsSerializer'] = (pa * * @param {MessageResponse} message `MessageResponse` object */ -export function formatMessage(message: MessageResponse | FormatMessageResponse): FormatMessageResponse { +export function formatMessage( + message: MessageResponse | FormatMessageResponse, +): FormatMessageResponse { return { ...message, // parse the dates @@ -382,7 +390,9 @@ export const findIndexInSortedArray = ({ const step = sortDirection === 'ascending' ? -1 : +1; for ( let i = left + step; - 0 <= i && i < sortedArray.length && selectValueToCompare(sortedArray[i]) === comparableNeedle; + 0 <= i && + i < sortedArray.length && + selectValueToCompare(sortedArray[i]) === comparableNeedle; i += step ) { if (selectKey(sortedArray[i]) === needleKey) { @@ -407,7 +417,9 @@ export function addToMessageList( // if created_at has changed, message should be filtered and re-inserted in correct order // slow op but usually this only happens for a message inserted to state before actual response with correct timestamp if (timestampChanged) { - newMessages = newMessages.filter((message) => !(message.id && newMessage.id === message.id)); + newMessages = newMessages.filter( + (message) => !(message.id && newMessage.id === message.id), + ); } // for empty list just concat and return unless it's an update or deletion @@ -601,12 +613,16 @@ const get = (obj: T, path: string): unknown => }, obj); // works exactly the same as lodash.uniqBy -export const uniqBy = (array: T[] | unknown, iteratee: ((item: T) => unknown) | keyof T): T[] => { +export const uniqBy = ( + array: T[] | unknown, + iteratee: ((item: T) => unknown) | keyof T, +): T[] => { if (!Array.isArray(array)) return []; const seen = new Set(); return array.filter((item) => { - const key = typeof iteratee === 'function' ? iteratee(item) : get(item, iteratee as string); + const key = + typeof iteratee === 'function' ? iteratee(item) : get(item, iteratee as string); if (seen.has(key)) return false; seen.add(key); return true; @@ -669,12 +685,15 @@ const messagePaginationCreatedAtAround = ({ // expect ASC order (from oldest to newest) const wholePageHasNewerMessages = !!firstPageMsg?.created_at && new Date(firstPageMsg.created_at) > createdAtAroundDate; - const wholePageHasOlderMessages = !!lastPageMsg?.created_at && new Date(lastPageMsg.created_at) < createdAtAroundDate; + const wholePageHasOlderMessages = + !!lastPageMsg?.created_at && new Date(lastPageMsg.created_at) < createdAtAroundDate; const requestedPageSizeNotMet = - requestedPageSize > parentSet.messages.length && requestedPageSize > returnedPage.length; + requestedPageSize > parentSet.messages.length && + requestedPageSize > returnedPage.length; const noMoreMessages = - (requestedPageSize > parentSet.messages.length || parentSet.messages.length >= returnedPage.length) && + (requestedPageSize > parentSet.messages.length || + parentSet.messages.length >= returnedPage.length) && requestedPageSize > returnedPage.length; if (wholePageHasNewerMessages) { @@ -702,7 +721,10 @@ const messagePaginationCreatedAtAround = ({ updateHasPrev = firstPageMsgIsFirstInSet; updateHasNext = lastPageMsgIsLastInSet; const midPointByCount = Math.floor(returnedPage.length / 2); - const midPointByCreationDate = binarySearchByDateEqualOrNearestGreater(returnedPage, createdAtAroundDate); + const midPointByCreationDate = binarySearchByDateEqualOrNearestGreater( + returnedPage, + createdAtAroundDate, + ); if (midPointByCreationDate !== -1) { hasPrev = midPointByCount <= midPointByCreationDate; @@ -738,7 +760,8 @@ const messagePaginationIdAround = ({ const midPoint = Math.floor(returnedPage.length / 2); const noMoreMessages = - (requestedPageSize > parentSet.messages.length || parentSet.messages.length >= returnedPage.length) && + (requestedPageSize > parentSet.messages.length || + parentSet.messages.length >= returnedPage.length) && requestedPageSize > returnedPage.length; if (noMoreMessages) { @@ -828,7 +851,10 @@ const messagePaginationLinear = ({ export const messageSetPagination = (params: MessagePaginationUpdatedParams) => { if (params.parentSet.messages.length < params.returnedPage.length) { - params.logger?.('error', 'Corrupted message set state: parent set size < returned page size'); + params.logger?.( + 'error', + 'Corrupted message set state: parent set size < returned page size', + ); return params.parentSet.pagination; } @@ -845,7 +871,10 @@ export const messageSetPagination = (params: MessagePaginationUpdatedParams) => * A utility object used to prevent duplicate invocation of channel.watch() to be triggered when * 'notification.message_new' and 'notification.added_to_channel' events arrive at the same time. */ -const WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL: Record | undefined> = {}; +const WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL: Record< + string, + Promise | undefined +> = {}; type GetChannelParams = { client: StreamChat; @@ -865,7 +894,14 @@ type GetChannelParams = { * @param id * @param channel */ -export const getAndWatchChannel = async ({ channel, client, id, members, options, type }: GetChannelParams) => { +export const getAndWatchChannel = async ({ + channel, + client, + id, + members, + options, + type, +}: GetChannelParams) => { if (!channel && !type) { throw new Error('Channel or channel type have to be provided to query a channel.'); } @@ -878,11 +914,13 @@ export const getAndWatchChannel = async ({ channel, client, id, members, options const originalCid = channelToWatch.id ? channelToWatch.cid : members && members.length - ? generateChannelTempCid(channelToWatch.type, members) - : undefined; + ? generateChannelTempCid(channelToWatch.type, members) + : undefined; if (!originalCid) { - throw new Error('Channel ID or channel members array have to be provided to query a channel.'); + throw new Error( + 'Channel ID or channel members array have to be provided to query a channel.', + ); } const queryPromise = WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid]; @@ -1055,7 +1093,8 @@ export const promoteChannel = ({ }: PromoteChannelParams) => { // get index of channel to move up const targetChannelIndex = - channelToMoveIndexWithinChannels ?? channels.findIndex((channel) => channel.cid === channelToMove.cid); + channelToMoveIndexWithinChannels ?? + channels.findIndex((channel) => channel.cid === channelToMove.cid); const targetChannelExistsWithinList = targetChannelIndex >= 0; const targetChannelAlreadyAtTheTop = targetChannelIndex === 0; @@ -1085,7 +1124,11 @@ export const promoteChannel = ({ } // re-insert it at the new place (to specific index if pinned channels are considered) - newChannels.splice(typeof lastPinnedChannelIndex === 'number' ? lastPinnedChannelIndex + 1 : 0, 0, channelToMove); + newChannels.splice( + typeof lastPinnedChannelIndex === 'number' ? lastPinnedChannelIndex + 1 : 0, + 0, + channelToMove, + ); return newChannels; }; diff --git a/test/typescript/index.js b/test/typescript/index.js index a5a5b8eafa..f761412044 100644 --- a/test/typescript/index.js +++ b/test/typescript/index.js @@ -10,8 +10,7 @@ const executables = [ { f: rg.acceptInvite, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['acceptInvite']>>", + type: "Unpacked['acceptInvite']>>", }, { f: rg.addDevice, @@ -21,14 +20,12 @@ const executables = [ { f: rg.addMembers, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['addMembers']>>", + type: "Unpacked['addMembers']>>", }, { f: rg.addModerators, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['addModerators']>>", + type: "Unpacked['addModerators']>>", }, { f: rg.banUsers, @@ -59,14 +56,12 @@ const executables = [ { f: rg.create, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['create']>>", + type: "Unpacked['create']>>", }, { f: rg.createBlockList, imports: ['StreamChat', 'Unpacked'], - type: - "Unpacked['createBlockList']>>", + type: "Unpacked['createBlockList']>>", }, // createChannelType has a limit. So only run this when needed. // { @@ -77,8 +72,7 @@ const executables = [ { f: rg.createCommand, imports: ['StreamChat', 'Unpacked'], - type: - "Unpacked['createCommand']>>", + type: "Unpacked['createCommand']>>", }, { f: rg.createPermission, @@ -93,14 +87,12 @@ const executables = [ { f: rg.deleteBlockList, imports: ['StreamChat', 'Unpacked'], - type: - "Unpacked['deleteBlockList']>>", + type: "Unpacked['deleteBlockList']>>", }, { f: rg.deleteChannel, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['delete']>>", + type: "Unpacked['delete']>>", }, // TODO: Fix the error which results from deleteChannelType api call: // `deleteChannelType failed with error: { Error: StreamChat error code 16: DeleteChannelType failed with error: "bc0b09df-2cfd-4e80-93e7-1f0091e6a435 is not a defined channel type"` @@ -113,20 +105,17 @@ const executables = [ { f: rg.deleteCommand, imports: ['StreamChat', 'Unpacked'], - type: - "Unpacked['deleteCommand']>>", + type: "Unpacked['deleteCommand']>>", }, { f: rg.deleteFile, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['deleteFile']>>", + type: "Unpacked['deleteFile']>>", }, { f: rg.deleteImage, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['deleteImage']>>", + type: "Unpacked['deleteImage']>>", }, { f: rg.deleteMessage, @@ -141,8 +130,7 @@ const executables = [ { f: rg.deleteReaction, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['deleteReaction']>>", + type: "Unpacked['deleteReaction']>>", }, { f: rg.deleteUser, @@ -152,8 +140,7 @@ const executables = [ { f: rg.demoteModerators, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['demoteModerators']>>", + type: "Unpacked['demoteModerators']>>", }, // { // f: rg.disconnect, @@ -168,14 +155,12 @@ const executables = [ { f: rg.flagMessage, imports: ['StreamChat', 'Unpacked'], - type: - "Unpacked['flagMessage']>>", + type: "Unpacked['flagMessage']>>", }, { f: rg.flagUser, imports: ['StreamChat', 'Unpacked'], - type: - "Unpacked['flagUser']>>", + type: "Unpacked['flagUser']>>", }, { f: rg.getAppSettings, @@ -195,14 +180,12 @@ const executables = [ { f: rg.getCommand, imports: ['StreamChat', 'Unpacked'], - type: - "Unpacked['getCommand']>>", + type: "Unpacked['getCommand']>>", }, { f: rg.getConfig, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['getConfig']>>", + type: "Unpacked['getConfig']>>", }, { f: rg.getDevices, @@ -217,14 +200,12 @@ const executables = [ { f: rg.getMessagesById, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['getMessagesById']>>", + type: "Unpacked['getMessagesById']>>", }, { f: rg.getMessageWithReply, imports: ['StreamChat', 'Unpacked'], - type: - "Unpacked['getMessage']>>", + type: "Unpacked['getMessage']>>", }, { f: rg.getPermission, @@ -234,38 +215,32 @@ const executables = [ { f: rg.getReactions, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['getReactions']>>", + type: "Unpacked['getReactions']>>", }, { f: rg.getReplies, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['getReplies']>>", + type: "Unpacked['getReplies']>>", }, { f: rg.hide, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['hide']>>", + type: "Unpacked['hide']>>", }, { f: rg.inviteMembers, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['inviteMembers']>>", + type: "Unpacked['inviteMembers']>>", }, { f: rg.keystroke, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['keystroke']>>", + type: "Unpacked['keystroke']>>", }, { f: rg.lastMessage, imports: ['Channel', 'FormatMessageResponse', 'Unpacked'], - type: - "Omit, 'created_at' | 'updated_at'> & { created_at?: string; updated_at?: string } | undefined", + type: "Omit, 'created_at' | 'updated_at'> & { created_at?: string; updated_at?: string } | undefined", }, { f: rg.listBlockLists, @@ -280,8 +255,7 @@ const executables = [ { f: rg.listCommands, imports: ['StreamChat', 'Unpacked'], - type: - "Unpacked['listCommands']>>", + type: "Unpacked['listCommands']>>", }, { f: rg.listPermissions, @@ -296,20 +270,17 @@ const executables = [ { f: rg.markRead, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['markRead']>>", + type: "Unpacked['markRead']>>", }, { f: rg.mute, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['mute']>>", + type: "Unpacked['mute']>>", }, { f: rg.muteStatus, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['muteStatus']>>", + type: "Unpacked['muteStatus']>>", }, { f: rg.muteUser, @@ -319,20 +290,17 @@ const executables = [ { f: rg.partialUpdateUser, imports: ['StreamChat', 'Unpacked'], - type: - "Unpacked['partialUpdateUser']>>", + type: "Unpacked['partialUpdateUser']>>", }, { f: rg.partialUpdateUsers, imports: ['StreamChat', 'Unpacked'], - type: - "Unpacked['partialUpdateUsers']>>", + type: "Unpacked['partialUpdateUsers']>>", }, { f: rg.query, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['query']>>", + type: "Unpacked['query']>>", }, // TODO: Add this back in when queryBannedUsers is deployed to all shards for testing // { @@ -344,14 +312,12 @@ const executables = [ { f: rg.queryMembers, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['queryMembers']>>", + type: "Unpacked['queryMembers']>>", }, { f: rg.queryUsers, imports: ['StreamChat', 'Unpacked'], - type: - "Unpacked['queryUsers']>>", + type: "Unpacked['queryUsers']>>", }, { f: rg.reactivateUser, @@ -361,14 +327,12 @@ const executables = [ { f: rg.rejectInvite, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['rejectInvite']>>", + type: "Unpacked['rejectInvite']>>", }, { f: rg.removeMembers, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['removeMembers']>>", + type: "Unpacked['removeMembers']>>", }, { f: rg.removeShadowBan, @@ -378,38 +342,32 @@ const executables = [ { f: rg.sendAction, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['sendAction']>>", + type: "Unpacked['sendAction']>>", }, { f: rg.sendFile, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['sendFile']>>", + type: "Unpacked['sendFile']>>", }, { f: rg.sendImage, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['sendImage']>>", + type: "Unpacked['sendImage']>>", }, { f: rg.sendMessage, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['sendMessage']>>", + type: "Unpacked['sendMessage']>>", }, { f: rg.sendMessageReadEvent, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['sendEvent']>>", + type: "Unpacked['sendEvent']>>", }, { f: rg.sendReaction, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['sendReaction']>>", + type: "Unpacked['sendReaction']>>", }, { f: rg.setGuestUser, @@ -424,32 +382,27 @@ const executables = [ { f: rg.show, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['show']>>", + type: "Unpacked['show']>>", }, { f: rg.stopTyping, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['stopTyping']>>", + type: "Unpacked['stopTyping']>>", }, { f: rg.stopWatching, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['stopWatching']>>", + type: "Unpacked['stopWatching']>>", }, { f: rg.sync, imports: ['StreamChat', 'Unpacked'], - type: - "Unpacked['sync']>>", + type: "Unpacked['sync']>>", }, { f: rg.syncTeam, imports: ['StreamChat', 'Unpacked'], - type: - "Unpacked['sync']>>", + type: "Unpacked['sync']>>", }, // Need translation on the account to run this test // { @@ -461,8 +414,7 @@ const executables = [ { f: rg.truncateChannel, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['truncate']>>", + type: "Unpacked['truncate']>>", }, { f: rg.unbanUsers, @@ -472,8 +424,7 @@ const executables = [ { f: rg.unmute, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['unmute']>>", + type: "Unpacked['unmute']>>", }, { f: rg.unmuteUser, @@ -488,20 +439,17 @@ const executables = [ { f: rg.updateBlockList, imports: ['StreamChat', 'Unpacked'], - type: - "Unpacked['updateBlockList']>>", + type: "Unpacked['updateBlockList']>>", }, { f: rg.updateChannel, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['update']>>", + type: "Unpacked['update']>>", }, { f: rg.updateChannelFromOriginal, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['update']>>", + type: "Unpacked['update']>>", }, { f: rg.updateChannelType, @@ -511,14 +459,12 @@ const executables = [ { f: rg.updateCommand, imports: ['StreamChat', 'Unpacked'], - type: - "Unpacked['updateCommand']>>", + type: "Unpacked['updateCommand']>>", }, { f: rg.updateMessage, imports: ['StreamChat', 'Unpacked'], - type: - "Unpacked['updateMessage']>>", + type: "Unpacked['updateMessage']>>", }, { f: rg.updatePermission, @@ -528,20 +474,17 @@ const executables = [ { f: rg.upsertUsers, imports: ['StreamChat', 'Unpacked'], - type: - "Unpacked['upsertUsers']>>", + type: "Unpacked['upsertUsers']>>", }, { f: rg.upsertUser, imports: ['StreamChat', 'Unpacked'], - type: - "Unpacked['upsertUser']>>", + type: "Unpacked['upsertUser']>>", }, { f: rg.watch, imports: ['Channel', 'Unpacked'], - type: - "Unpacked['watch']>>", + type: "Unpacked['watch']>>", }, // Currently roles do not return diff --git a/test/typescript/response-generators/channel.js b/test/typescript/response-generators/channel.js index 7675ca5292..0153e89c26 100644 --- a/test/typescript/response-generators/channel.js +++ b/test/typescript/response-generators/channel.js @@ -62,7 +62,9 @@ async function deleteChannel() { async function deleteFile() { const channel = await utils.createTestChannelForUser(uuidv4(), johnID); - const rs = fs.createReadStream(url.pathToFileURL('./test/typescript/response-generators/index.js')); + const rs = fs.createReadStream( + url.pathToFileURL('./test/typescript/response-generators/index.js'), + ); const file = await channel.sendFile(rs, 'testFile'); return channel.deleteFile(file.file); } @@ -70,7 +72,9 @@ async function deleteFile() { async function deleteImage() { const channel = await utils.createTestChannelForUser(uuidv4(), johnID); - const rs = fs.createReadStream(url.pathToFileURL('./test/typescript/response-generators/stream.png')); + const rs = fs.createReadStream( + url.pathToFileURL('./test/typescript/response-generators/stream.png'), + ); const image = await channel.sendImage(rs, 'testImage'); return channel.deleteImage(image.file); } @@ -164,14 +168,18 @@ async function removeMembers() { async function sendFile() { const channel = await utils.createTestChannelForUser(uuidv4(), johnID); - const rs = fs.createReadStream(url.pathToFileURL('./test/typescript/response-generators/index.js')); + const rs = fs.createReadStream( + url.pathToFileURL('./test/typescript/response-generators/index.js'), + ); return await channel.sendFile(rs, 'testFile'); } async function sendImage() { const channel = await utils.createTestChannelForUser(uuidv4(), johnID); - const rs = fs.createReadStream(url.pathToFileURL('./test/typescript/response-generators/stream.png')); + const rs = fs.createReadStream( + url.pathToFileURL('./test/typescript/response-generators/stream.png'), + ); return await channel.sendImage(rs, 'testImage'); } diff --git a/test/typescript/response-generators/client.js b/test/typescript/response-generators/client.js index d6f3d99849..ad0a0f6368 100644 --- a/test/typescript/response-generators/client.js +++ b/test/typescript/response-generators/client.js @@ -173,7 +173,11 @@ async function syncTeam() { await utils.createMultiTenancyUsers([user1], [team]); const client = await utils.getMultiTenancyTestClientForUser(user1); const channelId = uuidv4(); - const channel = await utils.createTestMultiTenancyChannelForUser(channelId, user1, team); + const channel = await utils.createTestMultiTenancyChannelForUser( + channelId, + user1, + team, + ); await channel.sendMessage({ text: 'New Event?', user: { id: user1 }, @@ -184,7 +188,8 @@ async function syncTeam() { async function updateAppSettings() { const authClient = await utils.getTestClient(true); return await authClient.updateAppSettings({ - custom_action_handler_url: 'https://example.com/webhooks/stream/custom-commands?type={type}', + custom_action_handler_url: + 'https://example.com/webhooks/stream/custom-commands?type={type}', enforce_unique_usernames: 'no', }); } diff --git a/test/typescript/unit-test.ts b/test/typescript/unit-test.ts index cb15fce26f..308b837fb7 100644 --- a/test/typescript/unit-test.ts +++ b/test/typescript/unit-test.ts @@ -186,7 +186,13 @@ clientRes = client.post('https://chat.stream-io-api.com/', { id: 2 }); clientRes = client.patch('https://chat.stream-io-api.com/', { id: 2 }); clientRes = client.delete('https://chat.stream-io-api.com/', { id: 2 }); -const file: Promise = client.sendFile('aa', 'bb', 'text.jpg', 'image/jpg', { id: 'james' }); +const file: Promise = client.sendFile( + 'aa', + 'bb', + 'text.jpg', + 'image/jpg', + { id: 'james' }, +); const type: EventTypes = 'user.updated'; const event: Event = { @@ -224,15 +230,20 @@ channels.then((response) => { const cid: string = response[0].cid; }); -const channel: Channel = client.channel('messaging', 'channelName', { color: 'green' }); +const channel: Channel = client.channel('messaging', 'channelName', { + color: 'green', +}); const channelState: ChannelState = channel.state; const chUser1: ChannelMemberResponse = channelState.members.someUser12433222; -const chUser2: ChannelMemberResponse = channelState.members.someUser124332221; +const chUser2: ChannelMemberResponse = + channelState.members.someUser124332221; const chUser3: UserResponse = channelState.read.someUserId.user; const typing: Event = channelState.typing['someUserId']; -const acceptInvite: Promise> = channel.acceptInvite({}); +const acceptInvite: Promise> = channel.acceptInvite( + {}, +); voidReturn = channel.on(eventHandler); voidReturn = channel.off(eventHandler); @@ -242,11 +253,39 @@ voidReturn = channel.off('message.new', eventHandler); channel.sendMessage({ text: 'text' }); // send a msg without id const permissions = [ - new Permission('Admin users can perform any action', MaxPriority, AnyResource, AnyRole, false, Allow), - new Permission('Anonymous users are not allowed', 500, AnyResource, ['anonymous'], false, Deny), - new Permission('Users can modify their own messages', 400, AnyResource, ['user'], true, Allow), + new Permission( + 'Admin users can perform any action', + MaxPriority, + AnyResource, + AnyRole, + false, + Allow, + ), + new Permission( + 'Anonymous users are not allowed', + 500, + AnyResource, + ['anonymous'], + false, + Deny, + ), + new Permission( + 'Users can modify their own messages', + 400, + AnyResource, + ['user'], + true, + Allow, + ), new Permission('Users can create channels', 300, AnyResource, ['user'], false, Allow), - new Permission('Channel Members', 200, ['ReadChannel', 'CreateMessage'], ['channel_member'], false, Allow), + new Permission( + 'Channel Members', + 200, + ['ReadChannel', 'CreateMessage'], + ['channel_member'], + false, + Allow, + ), new Permission('Discard all', 100, AnyResource, AnyRole, false, Deny), ]; diff --git a/test/typescript/utils.js b/test/typescript/utils.js index 03251c2957..fee87fdf27 100644 --- a/test/typescript/utils.js +++ b/test/typescript/utils.js @@ -9,7 +9,11 @@ const multiTenancySecret = process.env.MULTITENANCY_API_SECRET; const multiTenancyKey = process.env.MULTITENANCY_API_KEY; module.exports = { - createMultiTenancyUsers: async function createMultiTenancyUsers(userIDs, teams = [], additionalInfo) { + createMultiTenancyUsers: async function createMultiTenancyUsers( + userIDs, + teams = [], + additionalInfo, + ) { const serverClient = await this.getMultiTenancyServerTestClient(); const users = []; for (const userID of userIDs) { @@ -41,28 +45,32 @@ module.exports = { await channel.create(); return channel; }, - createTestChannelForUser: async function createTestChannelForUser(id, userID, options = {}) { - const client = await this.getTestClientForUser(userID, options); - const channel = client.channel('messaging', id, { members: [userID] }); - await channel.create(); - return channel; - }, - createTestMultiTenancyChannelForUser: async function createTestMultiTenancyChannelForUser( + createTestChannelForUser: async function createTestChannelForUser( id, userID, - team, options = {}, ) { - const client = await this.getMultiTenancyTestClientForUser(userID, options); - const channel = client.channel('messaging', id, { members: [userID], team }); + const client = await this.getTestClientForUser(userID, options); + const channel = client.channel('messaging', id, { members: [userID] }); await channel.create(); return channel; }, + createTestMultiTenancyChannelForUser: + async function createTestMultiTenancyChannelForUser(id, userID, team, options = {}) { + const client = await this.getMultiTenancyTestClientForUser(userID, options); + const channel = client.channel('messaging', id, { members: [userID], team }); + await channel.create(); + return channel; + }, getMultiTenancyTestClient: async function getMultiTenancyTestClient(serverSide) { - const client = new StreamChat(multiTenancyKey, serverSide ? multiTenancySecret : null, { - timeout: 8000, - allowServerSideConnect: true, - }); + const client = new StreamChat( + multiTenancyKey, + serverSide ? multiTenancySecret : null, + { + timeout: 8000, + allowServerSideConnect: true, + }, + ); if (serverSide) { await client.updateAppSettings({ multi_tenant_enabled: true, @@ -70,9 +78,15 @@ module.exports = { } return client; }, - getMultiTenancyTestClientForUser: async function getMultiTenancyTestClientForUser(userID, options = {}) { + getMultiTenancyTestClientForUser: async function getMultiTenancyTestClientForUser( + userID, + options = {}, + ) { const client = await this.getMultiTenancyTestClient(false); - const health = await client.connectUser({ id: userID, ...options }, this.createMultiTenancyUserToken(userID)); + const health = await client.connectUser( + { id: userID, ...options }, + this.createMultiTenancyUserToken(userID), + ); client.health = health; return client; }, @@ -90,7 +104,10 @@ module.exports = { }, getTestClientForUser: async function getTestClientForUser(userID, options = {}) { const client = this.getTestClient(false); - const health = await client.connectUser({ id: userID, ...options }, this.createUserToken(userID)); + const health = await client.connectUser( + { id: userID, ...options }, + this.createUserToken(userID), + ); client.health = health; return client; }, diff --git a/test/unit/channel.js b/test/unit/channel.test.js similarity index 91% rename from test/unit/channel.js rename to test/unit/channel.test.js index b4215cab0b..9ddedb8965 100644 --- a/test/unit/channel.js +++ b/test/unit/channel.test.js @@ -1,4 +1,3 @@ -import chai from 'chai'; import { v4 as uuidv4 } from 'uuid'; import { generateChannel } from './test-utils/generateChannel'; @@ -13,7 +12,7 @@ import { mockChannelQueryResponse } from './test-utils/mockChannelQueryResponse' import { ChannelState, StreamChat } from '../../src'; import { DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE } from '../../src/constants'; -const expect = chai.expect; +import { describe, beforeEach, it, expect } from 'vitest'; describe('Channel count unread', function () { let lastRead; @@ -75,7 +74,10 @@ describe('Channel count unread', function () { }); it('_countMessageAsUnread should return false for channel with read_events off', function () { - const channel = client.channel('messaging', { members: ['tommaso'], own_capabilities: [] }); + const channel = client.channel('messaging', { + members: ['tommaso'], + own_capabilities: [], + }); expect(channel._countMessageAsUnread({ user: { id: 'random' } })).not.to.be.ok; }); @@ -125,7 +127,13 @@ describe('Channel count unread', function () { generateMsg({ date: '2021-01-01T00:00:00' }), generateMsg({ date: '2022-01-01T00:00:00' }), ]); - channel.state.addMessagesSorted([generateMsg({ date: '2020-01-01T00:00:00' })], false, true, true, 'new'); + channel.state.addMessagesSorted( + [generateMsg({ date: '2020-01-01T00:00:00' })], + false, + true, + true, + 'new', + ); channel.state.messageSets[0].isCurrent = false; channel.state.messageSets[1].isCurrent = true; @@ -150,10 +158,19 @@ describe('Channel count unread', function () { it('countUnreadMentions should return correct count when multiple message sets are loaded into state', () => { expect(channel.countUnreadMentions()).to.be.equal(0); channel.state.addMessagesSorted([ - generateMsg({ date: '2021-01-01T00:00:00', mentioned_users: [user, { id: 'random' }] }), + generateMsg({ + date: '2021-01-01T00:00:00', + mentioned_users: [user, { id: 'random' }], + }), generateMsg({ date: '2022-01-01T00:00:00' }), ]); - channel.state.addMessagesSorted([generateMsg({ date: '2020-01-01T00:00:00' })], false, true, true, 'new'); + channel.state.addMessagesSorted( + [generateMsg({ date: '2020-01-01T00:00:00' })], + false, + true, + true, + 'new', + ); channel.state.messageSets[0].isCurrent = false; channel.state.messageSets[1].isCurrent = true; @@ -331,9 +348,17 @@ describe('Channel _handleChannelEvent', function () { it('message.truncate removes pinned messages up to specified date', function () { const messages = [ - { created_at: '2021-01-01T00:01:00', pinned: true, pinned_at: new Date('2021-01-01T00:01:01.010Z') }, + { + created_at: '2021-01-01T00:01:00', + pinned: true, + pinned_at: new Date('2021-01-01T00:01:01.010Z'), + }, { created_at: '2021-01-01T00:02:00' }, - { created_at: '2021-01-01T00:03:00', pinned: true, pinned_at: new Date('2021-01-01T00:02:02.011Z') }, + { + created_at: '2021-01-01T00:03:00', + pinned: true, + pinned_at: new Date('2021-01-01T00:02:02.011Z'), + }, ].map(generateMsg); channel.state.addMessagesSorted(messages); @@ -379,7 +404,10 @@ describe('Channel _handleChannelEvent', function () { message: { ...originalMessage, deleted_at: new Date().toISOString() }, }); - expect(channel.state.messages.find((msg) => msg.id === quotingMessage.id).quoted_message.deleted_at).to.be.ok; + expect( + channel.state.messages.find((msg) => msg.id === quotingMessage.id).quoted_message + .deleted_at, + ).to.be.ok; }); describe('notification.mark_unread', () => { @@ -403,7 +431,9 @@ describe('Channel _handleChannelEvent', function () { channel: null, user, first_unread_message_id: '2', - last_read_at: new Date(new Date(initialReadState.last_read).getTime() - 1000).toISOString(), + last_read_at: new Date( + new Date(initialReadState.last_read).getTime() - 1000, + ).toISOString(), last_read_message_id: '1', unread_messages: 5, unread_count: 6, @@ -423,15 +453,22 @@ describe('Channel _handleChannelEvent', function () { expect(new Date(channel.state.read[user.id].last_read).getTime()).to.be.equal( new Date(event.last_read_at).getTime(), ); - expect(channel.state.read[user.id].last_read_message_id).to.be.equal(event.last_read_message_id); - expect(channel.state.read[user.id].unread_messages).to.be.equal(event.unread_messages); + expect(channel.state.read[user.id].last_read_message_id).to.be.equal( + event.last_read_message_id, + ); + expect(channel.state.read[user.id].unread_messages).to.be.equal( + event.unread_messages, + ); }); it('should not update channel read state produced for another user or user is missing', () => { channel.state.unreadCount = initialCountUnread; channel.state.read[user.id] = initialReadState; const { user: excludedUser, ...eventMissingUser } = notificationMarkUnreadEvent; - const eventWithAnotherUser = { ...notificationMarkUnreadEvent, user: { id: 'another-user' } }; + const eventWithAnotherUser = { + ...notificationMarkUnreadEvent, + user: { id: 'another-user' }, + }; [eventWithAnotherUser, eventMissingUser].forEach((event) => { channel._handleChannelEvent(event); @@ -443,7 +480,9 @@ describe('Channel _handleChannelEvent', function () { expect(channel.state.read[user.id].last_read_message_id).to.be.equal( initialReadState.last_read_message_id, ); - expect(channel.state.read[user.id].unread_messages).to.be.equal(initialReadState.unread_messages); + expect(channel.state.read[user.id].unread_messages).to.be.equal( + initialReadState.unread_messages, + ); }); }); }); @@ -472,7 +511,8 @@ describe('Channel _handleChannelEvent', function () { user: { id: 'id' }, message, }); - expect(channel.state.read['id'].unread_messages, `${event} should not be undefined`).not.to.be.undefined; + expect(channel.state.read['id'].unread_messages, `${event} should not be undefined`) + .not.to.be.undefined; } }); @@ -503,14 +543,20 @@ describe('Channel _handleChannelEvent', function () { user: { id: client.user.id }, message, }); - expect(channel.state.read[client.user.id].unread_messages, `${event} should not be undefined`).not.to.be - .undefined; + expect( + channel.state.read[client.user.id].unread_messages, + `${event} should not be undefined`, + ).not.to.be.undefined; } }); it('should extend "message.updated" and "message.deleted" event payloads with "own_reactions"', () => { const own_reactions = [ - { created_at: new Date().toISOString(), updated_at: new Date().toISOString(), type: 'wow' }, + { + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + type: 'wow', + }, ]; const testCases = [ [generateMsg({ own_reactions })], // channel message @@ -535,9 +581,9 @@ describe('Channel _handleChannelEvent', function () { channel._handleChannelEvent(event); channel._callChannelListeners(event); - expect(channel.state.findMessage(message.id, message.parent_id).own_reactions.length).to.equal( - own_reactions.length, - ); + expect( + channel.state.findMessage(message.id, message.parent_id).own_reactions.length, + ).to.equal(own_reactions.length); expect(receivedEvent.message.own_reactions.length).to.equal(own_reactions.length); }); }); @@ -547,7 +593,10 @@ describe('Channel _handleChannelEvent', function () { const originalText = 'XX'; const updatedText = 'YY'; const parent_id = '0'; - const parentMesssage = generateMsg({ date: new Date(0).toISOString(), id: parent_id }); + const parentMesssage = generateMsg({ + date: new Date(0).toISOString(), + id: parent_id, + }); const quoted_message = generateMsg({ date: new Date(2).toISOString(), id: 'quoted-message', @@ -563,7 +612,11 @@ describe('Channel _handleChannelEvent', function () { const updatedQuotedThreadReply = { ...quoted_message, parent_id, text: updatedText }; [ [quoted_message, quotingMessage], // channel message - [parentMesssage, { ...quoted_message, parent_id }, { ...quotingMessage, parent_id }], // thread message + [ + parentMesssage, + { ...quoted_message, parent_id }, + { ...quotingMessage, parent_id }, + ], // thread message ].forEach((messages) => { ['message.updated', 'message.deleted'].forEach((eventType) => { channel.state.addMessagesSorted(messages); @@ -575,7 +628,8 @@ describe('Channel _handleChannelEvent', function () { }; channel._handleChannelEvent(event); expect( - channel.state.findMessage(quotingMessage.id, quotingMessage.parent_id).quoted_message.text, + channel.state.findMessage(quotingMessage.id, quotingMessage.parent_id) + .quoted_message.text, ).to.equal(updatedQuotedMessage.text); channel.state.clearMessages(); }); @@ -712,7 +766,12 @@ describe('Channel _handleChannelEvent', function () { }; [ - [shadowBanEvent, banEvent, { shadow_banned: true, banned: false }, { shadow_banned: false, banned: true }], + [ + shadowBanEvent, + banEvent, + { shadow_banned: true, banned: false }, + { shadow_banned: false, banned: true }, + ], [ shadowBanEvent, shadowUnbanEvent, @@ -725,21 +784,35 @@ describe('Channel _handleChannelEvent', function () { { shadow_banned: true, banned: false }, { shadow_banned: false, banned: false }, ], - [banEvent, shadowBanEvent, { shadow_banned: false, banned: true }, { shadow_banned: true, banned: false }], + [ + banEvent, + shadowBanEvent, + { shadow_banned: false, banned: true }, + { shadow_banned: true, banned: false }, + ], [ banEvent, shadowUnbanEvent, { shadow_banned: false, banned: true }, { shadow_banned: false, banned: false }, ], - [banEvent, unbanEvent, { shadow_banned: false, banned: true }, { shadow_banned: false, banned: false }], + [ + banEvent, + unbanEvent, + { shadow_banned: false, banned: true }, + { shadow_banned: false, banned: false }, + ], ].forEach(([firstEvent, secondEvent, expectAfterFirst, expectAfterSecond]) => { channel._handleChannelEvent(firstEvent); expect(channel.state.members[user.id].banned).eq(expectAfterFirst.banned); - expect(channel.state.members[user.id].shadow_banned).eq(expectAfterFirst.shadow_banned); + expect(channel.state.members[user.id].shadow_banned).eq( + expectAfterFirst.shadow_banned, + ); channel._handleChannelEvent(secondEvent); expect(channel.state.members[user.id].banned).eq(expectAfterSecond.banned); - expect(channel.state.members[user.id].shadow_banned).eq(expectAfterSecond.shadow_banned); + expect(channel.state.members[user.id].shadow_banned).eq( + expectAfterSecond.shadow_banned, + ); }); }); }); @@ -771,15 +844,14 @@ describe('Uninitialized Channel', () => { describe('Channels - Constructor', function () { const client = new StreamChat('key', 'secret'); - it('canonical form', function (done) { + it('canonical form', function () { const channel = client.channel('messaging', '123', { cool: true }); expect(channel.cid).to.eql('messaging:123'); expect(channel.id).to.eql('123'); expect(channel.data).to.eql({ cool: true }); - done(); }); - it('custom data merges to the right with current data', function (done) { + it('custom data merges to the right with current data', function () { let channel = client.channel('messaging', 'brand_new_123', { cool: true }); expect(channel.cid).to.eql('messaging:brand_new_123'); expect(channel.id).to.eql('brand_new_123'); @@ -787,61 +859,53 @@ describe('Channels - Constructor', function () { channel = client.channel('messaging', 'brand_new_123', { custom_cool: true }); console.log(channel.data); expect(channel.data).to.eql({ cool: true, custom_cool: true }); - done(); }); - it('default options', function (done) { + it('default options', function () { const channel = client.channel('messaging', '123'); expect(channel.cid).to.eql('messaging:123'); expect(channel.id).to.eql('123'); - done(); }); - it('null ID no options', function (done) { + it('null ID no options', function () { const channel = client.channel('messaging', null); expect(channel.id).to.eq(undefined); - done(); }); - it('undefined ID no options', function (done) { + it('undefined ID no options', function () { const channel = client.channel('messaging', undefined); expect(channel.id).to.eql(undefined); expect(channel.data).to.eql({}); - done(); }); - it('short version with options', function (done) { + it('short version with options', function () { const channel = client.channel('messaging', { members: ['tommaso', 'thierry'] }); expect(channel.data).to.eql({ members: ['tommaso', 'thierry'] }); expect(channel.id).to.eql(undefined); - done(); }); - it('null ID with options', function (done) { + it('null ID with options', function () { const channel = client.channel('messaging', null, { members: ['tommaso', 'thierry'], }); expect(channel.data).to.eql({ members: ['tommaso', 'thierry'] }); expect(channel.id).to.eql(undefined); - done(); }); - it('empty ID with options', function (done) { + it('empty ID with options', function () { const channel = client.channel('messaging', '', { members: ['tommaso', 'thierry'], }); expect(channel.data).to.eql({ members: ['tommaso', 'thierry'] }); expect(channel.id).to.eql(undefined); - done(); }); - it('empty ID with options', function (done) { + it('empty ID with options', function () { const channel = client.channel('messaging', undefined, { members: ['tommaso', 'thierry'], }); expect(channel.data).to.eql({ members: ['tommaso', 'thierry'] }); expect(channel.id).to.eql(undefined); - done(); }); }); @@ -942,7 +1006,9 @@ describe('Ensure single channel per cid on client activeChannels state', () => { // tempCid should be replaced with actual cid at this point. expect(Object.keys(clientVish.activeChannels)).to.not.contain(tmpCid); expect(Object.keys(clientVish.activeChannels)).to.contain(channelVish_copy1.cid); - expect(clientVish.activeChannels[channelVish_copy1.cid]).to.contain(channelVish_copy1); + expect(clientVish.activeChannels[channelVish_copy1.cid]).to.contain( + channelVish_copy1, + ); const channelVish_copy2 = clientVish.channel('messaging', { members: [userVish.id, userAmin.id], @@ -1026,7 +1092,9 @@ describe('Ensure single channel per cid on client activeChannels state', () => { // tempCid should be replaced with actual cid at this point. expect(Object.keys(clientVish.activeChannels)).to.not.contain(tmpCid); expect(Object.keys(clientVish.activeChannels)).to.contain(channelVish_copy1.cid); - expect(clientVish.activeChannels[channelVish_copy1.cid]).to.contain(channelVish_copy1); + expect(clientVish.activeChannels[channelVish_copy1.cid]).to.contain( + channelVish_copy1, + ); const channelVish_copy2 = clientVish.channel('messaging', undefined, { members: [userVish.id, userAmin.id], @@ -1125,7 +1193,9 @@ describe('Ensure single channel per cid on client activeChannels state', () => { expect(Object.keys(clientVish.activeChannels)).to.contain(channelVish_copy1.cid); expect(Object.keys(clientVish.activeChannels)).to.contain(channelVish_copy2.cid); - expect(clientVish.activeChannels[channelVish_copy1.cid]).not.to.contain(channelVish_copy2); + expect(clientVish.activeChannels[channelVish_copy1.cid]).not.to.contain( + channelVish_copy2, + ); }); }); @@ -1162,10 +1232,10 @@ describe('Channel search', async () => { await channel.search('query', { sort: [{ custom_field: -1 }] }); }); it('sorting and offset works', async () => { - await expect(channel.search('query', { offset: 1, sort: [{ custom_field: -1 }] })).to.be.fulfilled; + await expect(channel.search('query', { offset: 1, sort: [{ custom_field: -1 }] })); }); it('next and offset fails', async () => { - await expect(channel.search('query', { offset: 1, next: 'next' })).to.be.rejectedWith(Error); + await expect(channel.search('query', { offset: 1, next: 'next' })).rejects.toThrow(); }); }); @@ -1182,7 +1252,9 @@ describe('Channel lastMessage', async () => { generateMsg({ date: latestMessageDate }), ]); - expect(channel.lastMessage().created_at.getTime()).to.be.equal(new Date(latestMessageDate).getTime()); + expect(channel.lastMessage().created_at.getTime()).to.be.equal( + new Date(latestMessageDate).getTime(), + ); }); it('should return last message - messages are out of order', () => { @@ -1194,7 +1266,9 @@ describe('Channel lastMessage', async () => { generateMsg({ date: '2018-01-01T00:00:00' }), ]); - expect(channel.lastMessage().created_at.getTime()).to.be.equal(new Date(latestMessageDate).getTime()); + expect(channel.lastMessage().created_at.getTime()).to.be.equal( + new Date(latestMessageDate).getTime(), + ); }); it('should return last message - state has more message sets loaded', () => { @@ -1212,7 +1286,9 @@ describe('Channel lastMessage', async () => { channel.state.addMessagesSorted(latestMessages); channel.state.addMessagesSorted(otherMessages, 'new'); - expect(channel.lastMessage().created_at.getTime()).to.be.equal(new Date(latestMessageDate).getTime()); + expect(channel.lastMessage().created_at.getTime()).to.be.equal( + new Date(latestMessageDate).getTime(), + ); }); }); @@ -1263,7 +1339,10 @@ describe('Channel.query', async () => { const channel = client.channel('messaging', uuidv4()); const mockedChannelQueryResponse = { ...mockChannelQueryResponse, - messages: Array.from({ length: DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE }, generateMsg), + messages: Array.from( + { length: DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE }, + generateMsg, + ), }; const mock = sinon.mock(client); mock.expects('post').returns(Promise.resolve(mockedChannelQueryResponse)); @@ -1277,13 +1356,19 @@ describe('Channel.query', async () => { const channel = client.channel('messaging', uuidv4()); const mockedChannelQueryResponse = { ...mockChannelQueryResponse, - messages: Array.from({ length: DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE }, generateMsg), + messages: Array.from( + { length: DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE }, + generateMsg, + ), }; const mock = sinon.mock(client); mock.expects('post').returns(Promise.resolve(mockedChannelQueryResponse)); await channel.query(); expect(channel.state.messageSets.length).to.be.equal(1); - expect(channel.state.messageSets[0].pagination).to.eql({ hasNext: false, hasPrev: true }); + expect(channel.state.messageSets[0].pagination).to.eql({ + hasNext: false, + hasPrev: true, + }); mock.restore(); }); @@ -1292,13 +1377,19 @@ describe('Channel.query', async () => { const channel = client.channel('messaging', uuidv4()); const mockedChannelQueryResponse = { ...mockChannelQueryResponse, - messages: Array.from({ length: DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE - 1 }, generateMsg), + messages: Array.from( + { length: DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE - 1 }, + generateMsg, + ), }; const mock = sinon.mock(client); mock.expects('post').returns(Promise.resolve(mockedChannelQueryResponse)); await channel.query(); expect(channel.state.messageSets.length).to.be.equal(1); - expect(channel.state.messageSets[0].pagination).to.eql({ hasNext: false, hasPrev: false }); + expect(channel.state.messageSets[0].pagination).to.eql({ + hasNext: false, + hasPrev: false, + }); mock.restore(); }); }); diff --git a/test/unit/channel_manager.test.ts b/test/unit/channel_manager.test.ts index 162a453b63..a50896b487 100644 --- a/test/unit/channel_manager.test.ts +++ b/test/unit/channel_manager.test.ts @@ -1,4 +1,3 @@ -import { expect } from 'chai'; import sinon from 'sinon'; import { Channel, @@ -10,13 +9,13 @@ import { DEFAULT_CHANNEL_MANAGER_OPTIONS, channelManagerEventToHandlerMapping, DEFAULT_CHANNEL_MANAGER_PAGINATION_OPTIONS, - DefaultGenerics, - Event, } from '../../src'; import { generateChannel } from './test-utils/generateChannel'; import { getClientWithUser } from './test-utils/getClient'; -import * as Utils from '../../src/utils'; +import * as utils from '../../src/utils'; + +import { describe, beforeEach, afterEach, expect, it, vi, MockInstance } from 'vitest'; describe('ChannelManager', () => { let client: StreamChat; @@ -33,7 +32,9 @@ describe('ChannelManager', () => { generateChannel({ channel: { id: 'channel3' } }), ]; client.hydrateActiveChannels(channelsResponse); - const channels = channelsResponse.map((c) => client.channel(c.channel.type, c.channel.id)); + const channels = channelsResponse.map((c) => + client.channel(c.channel.type, c.channel.id), + ); channelManager.state.partialNext({ channels, initialized: true }); }); @@ -73,10 +74,18 @@ describe('ChannelManager', () => { 'notification.message_new': false, }, }; - const newChannelManager = client.createChannelManager({ eventHandlerOverrides, options }); + const newChannelManager = client.createChannelManager({ + eventHandlerOverrides, + options, + }); - expect(Object.fromEntries((newChannelManager as any).eventHandlerOverrides)).to.deep.equal(eventHandlerOverrides); - expect((newChannelManager as any).options).to.deep.equal({ ...DEFAULT_CHANNEL_MANAGER_OPTIONS, ...options }); + expect( + Object.fromEntries((newChannelManager as any).eventHandlerOverrides), + ).to.deep.equal(eventHandlerOverrides); + expect((newChannelManager as any).options).to.deep.equal({ + ...DEFAULT_CHANNEL_MANAGER_OPTIONS, + ...options, + }); }); it('should properly set the default event handlers', () => { @@ -107,17 +116,24 @@ describe('ChannelManager', () => { describe('setters', () => { it('should properly set eventHandlerOverrides and filter out falsy values', () => { - const eventHandlerOverrides = { newMessageHandler: () => {}, channelDeletedHandler: () => {} }; + const eventHandlerOverrides = { + newMessageHandler: () => {}, + channelDeletedHandler: () => {}, + }; channelManager.setEventHandlerOverrides(eventHandlerOverrides); - expect(Object.fromEntries((channelManager as any).eventHandlerOverrides)).to.deep.equal(eventHandlerOverrides); + expect( + Object.fromEntries((channelManager as any).eventHandlerOverrides), + ).to.deep.equal(eventHandlerOverrides); channelManager.setEventHandlerOverrides({ ...eventHandlerOverrides, notificationRemovedFromChannelHandler: undefined, channelHiddenHandler: undefined, }); - expect(Object.fromEntries((channelManager as any).eventHandlerOverrides)).to.deep.equal(eventHandlerOverrides); + expect( + Object.fromEntries((channelManager as any).eventHandlerOverrides), + ).to.deep.equal(eventHandlerOverrides); }); it('should properly set options', () => { @@ -186,7 +202,11 @@ describe('ChannelManager', () => { const { channels: newChannels } = channelManager.state.getLatestValue(); - expect(newChannels.map((c) => c.id)).to.deep.equal(['channel3', 'channel2', 'channel1']); + expect(newChannels.map((c) => c.id)).to.deep.equal([ + 'channel3', + 'channel2', + 'channel1', + ]); }); it('should maintain referential integrity if the same channels are passed', () => { @@ -217,16 +237,23 @@ describe('ChannelManager', () => { it('should only invoke event handlers if registerSubscriptions has been called', () => { const newChannelManager = client.createChannelManager({}); - const originalNewMessageHandler = (newChannelManager as any).eventHandlers.get('newMessageHandler'); - const originalNotificationAddedToChannelHandler = (newChannelManager as any).eventHandlers.get( - 'notificationAddedToChannelHandler', + const originalNewMessageHandler = (newChannelManager as any).eventHandlers.get( + 'newMessageHandler', ); + const originalNotificationAddedToChannelHandler = ( + newChannelManager as any + ).eventHandlers.get('notificationAddedToChannelHandler'); const newMessageHandlerSpy = sinon.spy(originalNewMessageHandler); - const notificationAddedToChannelHandlerSpy = sinon.spy(originalNotificationAddedToChannelHandler); + const notificationAddedToChannelHandlerSpy = sinon.spy( + originalNotificationAddedToChannelHandler, + ); const clientOnSpy = sinon.spy(client, 'on'); - (newChannelManager as any).eventHandlers.set('newMessageHandler', newMessageHandlerSpy); + (newChannelManager as any).eventHandlers.set( + 'newMessageHandler', + newMessageHandlerSpy, + ); (newChannelManager as any).eventHandlers.set( 'notificationAddedToChannelHandler', notificationAddedToChannelHandlerSpy, @@ -257,7 +284,9 @@ describe('ChannelManager', () => { newChannelManager.registerSubscriptions(); newChannelManager.registerSubscriptions(); - expect(clientOnSpy.callCount).to.equal(Object.keys(channelManagerEventToHandlerMapping).length); + expect(clientOnSpy.callCount).to.equal( + Object.keys(channelManagerEventToHandlerMapping).length, + ); Object.keys(channelManagerEventToHandlerMapping).forEach((eventType) => { expect(clientOnSpy.calledWith(eventType)).to.be.true; }); @@ -266,15 +295,22 @@ describe('ChannelManager', () => { it('should unregister subscriptions if unregisterSubscriptions is called', () => { const newChannelManager = client.createChannelManager({}); - const originalNewMessageHandler = (newChannelManager as any).eventHandlers.get('newMessageHandler'); - const originalNotificationAddedToChannelHandler = (newChannelManager as any).eventHandlers.get( - 'notificationAddedToChannelHandler', + const originalNewMessageHandler = (newChannelManager as any).eventHandlers.get( + 'newMessageHandler', ); + const originalNotificationAddedToChannelHandler = ( + newChannelManager as any + ).eventHandlers.get('notificationAddedToChannelHandler'); const newMessageHandlerSpy = sinon.spy(originalNewMessageHandler); - const notificationAddedToChannelHandlerSpy = sinon.spy(originalNotificationAddedToChannelHandler); + const notificationAddedToChannelHandlerSpy = sinon.spy( + originalNotificationAddedToChannelHandler, + ); - (newChannelManager as any).eventHandlers.set('newMessageHandler', newMessageHandlerSpy); + (newChannelManager as any).eventHandlers.set( + 'newMessageHandler', + newMessageHandlerSpy, + ); (newChannelManager as any).eventHandlers.set( 'notificationAddedToChannelHandler', notificationAddedToChannelHandlerSpy, @@ -293,23 +329,32 @@ describe('ChannelManager', () => { it('should call overrides for event handlers if they exist', () => { const newChannelManager = client.createChannelManager({}); - const originalNewMessageHandler = (newChannelManager as any).eventHandlers.get('newMessageHandler'); - const originalNotificationAddedToChannelHandler = (newChannelManager as any).eventHandlers.get( - 'notificationAddedToChannelHandler', + const originalNewMessageHandler = (newChannelManager as any).eventHandlers.get( + 'newMessageHandler', ); + const originalNotificationAddedToChannelHandler = ( + newChannelManager as any + ).eventHandlers.get('notificationAddedToChannelHandler'); const newMessageHandlerSpy = sinon.spy(originalNewMessageHandler); - const notificationAddedToChannelHandlerSpy = sinon.spy(originalNotificationAddedToChannelHandler); + const notificationAddedToChannelHandlerSpy = sinon.spy( + originalNotificationAddedToChannelHandler, + ); const newMessageHandlerOverrideSpy = sinon.spy(() => {}); - (newChannelManager as any).eventHandlers.set('newMessageHandler', newMessageHandlerSpy); + (newChannelManager as any).eventHandlers.set( + 'newMessageHandler', + newMessageHandlerSpy, + ); (newChannelManager as any).eventHandlers.set( 'notificationAddedToChannelHandler', notificationAddedToChannelHandlerSpy, ); newChannelManager.registerSubscriptions(); - newChannelManager.setEventHandlerOverrides({ newMessageHandler: newMessageHandlerOverrideSpy }); + newChannelManager.setEventHandlerOverrides({ + newMessageHandler: newMessageHandlerOverrideSpy, + }); client.dispatchEvent({ type: 'message.new' }); client.dispatchEvent({ type: 'notification.added_to_channel' }); @@ -347,7 +392,11 @@ describe('ChannelManager', () => { spy.resetHistory(); const channel = channelsResponse[channelsResponse.length - 1].channel; - client.dispatchEvent({ type: eventType, channel_type: channel.type, channel_id: channel.id }); + client.dispatchEvent({ + type: eventType, + channel_type: channel.type, + channel_id: channel.id, + }); expect(spy.called).to.be.false; }); @@ -367,12 +416,16 @@ describe('ChannelManager', () => { ]; mockChannelPages = channelQueryResponses.map((channelQueryResponse) => { client.hydrateActiveChannels(channelQueryResponse); - return channelQueryResponse.map((c) => client.channel(c.channel.type, c.channel.id)); - }); - clientQueryChannelsStub = sinon.stub(client, 'queryChannels').callsFake((_filters, _sort, options) => { - const offset = options?.offset ?? 0; - return Promise.resolve(mockChannelPages[Math.floor(offset / 10)]); + return channelQueryResponse.map((c) => + client.channel(c.channel.type, c.channel.id), + ); }); + clientQueryChannelsStub = sinon + .stub(client, 'queryChannels') + .callsFake((_filters, _sort, options) => { + const offset = options?.offset ?? 0; + return Promise.resolve(mockChannelPages[Math.floor(offset / 10)]); + }); }); afterEach(() => { @@ -396,13 +449,19 @@ describe('ChannelManager', () => { }); it('should not query more than once from the same manager for 2 different queries', async () => { - await Promise.all([channelManager.queryChannels({}), channelManager.queryChannels({})]); + await Promise.all([ + channelManager.queryChannels({}), + channelManager.queryChannels({}), + ]); expect(clientQueryChannelsStub.calledOnce).to.be.true; }); it('should query more than once if channelManager.options.abortInFlightQuery is true', async () => { channelManager.setOptions({ abortInFlightQuery: true }); - await Promise.all([channelManager.queryChannels({}), channelManager.queryChannels({})]); + await Promise.all([ + channelManager.queryChannels({}), + channelManager.queryChannels({}), + ]); expect(clientQueryChannelsStub.callCount).to.equal(2); }); @@ -452,7 +511,11 @@ describe('ChannelManager', () => { ); stateChangeSpy.resetHistory(); - await channelManager.queryChannels({ filterA: true }, { asc: 1 }, { limit: 10, offset: 0 }); + await channelManager.queryChannels( + { filterA: true }, + { asc: 1 }, + { limit: 10, offset: 0 }, + ); const { channels } = channelManager.state.getLatestValue(); @@ -483,7 +546,11 @@ describe('ChannelManager', () => { it('should properly update hasNext and offset if the first returned page is less than the limit', async () => { clientQueryChannelsStub.callsFake(() => mockChannelPages[2]); - await channelManager.queryChannels({ filterA: true }, { asc: 1 }, { limit: 10, offset: 0 }); + await channelManager.queryChannels( + { filterA: true }, + { asc: 1 }, + { limit: 10, offset: 0 }, + ); const { channels, @@ -559,7 +626,11 @@ describe('ChannelManager', () => { }); it('should properly set the new pagination parameters and update the offset after loading next', async () => { - await channelManager.queryChannels({ filterA: true }, { asc: 1 }, { limit: 10, offset: 0 }); + await channelManager.queryChannels( + { filterA: true }, + { asc: 1 }, + { limit: 10, offset: 0 }, + ); const stateChangeSpy = sinon.spy(); channelManager.state.subscribeWithSelector( @@ -599,7 +670,11 @@ describe('ChannelManager', () => { }); it('should properly paginate even if state.channels gets modified in the meantime', async () => { - await channelManager.queryChannels({ filterA: true }, { asc: 1 }, { limit: 10, offset: 0 }); + await channelManager.queryChannels( + { filterA: true }, + { asc: 1 }, + { limit: 10, offset: 0 }, + ); channelManager.state.next((prevState) => ({ ...prevState, channels: [...mockChannelPages[2].slice(0, 5), ...prevState.channels], @@ -643,7 +718,11 @@ describe('ChannelManager', () => { }); it('should properly deduplicate when paginating if channels from the next page have been promoted', async () => { - await channelManager.queryChannels({ filterA: true }, { asc: 1 }, { limit: 10, offset: 0 }); + await channelManager.queryChannels( + { filterA: true }, + { asc: 1 }, + { limit: 10, offset: 0 }, + ); channelManager.state.next((prevState) => ({ ...prevState, channels: [...mockChannelPages[1].slice(0, 5), ...prevState.channels], @@ -687,7 +766,11 @@ describe('ChannelManager', () => { }); it('should properly deduplicate when paginating if channels latter pages have been promoted and reached', async () => { - await channelManager.queryChannels({ filterA: true }, { asc: 1 }, { limit: 10, offset: 0 }); + await channelManager.queryChannels( + { filterA: true }, + { asc: 1 }, + { limit: 10, offset: 0 }, + ); channelManager.state.next((prevState) => ({ ...prevState, channels: [...mockChannelPages[2].slice(0, 3), ...prevState.channels], @@ -703,7 +786,8 @@ describe('ChannelManager', () => { await channelManager.loadNext(); - const { channels: channelsAfterFirstPagination } = channelManager.state.getLatestValue(); + const { channels: channelsAfterFirstPagination } = + channelManager.state.getLatestValue(); expect(channelsAfterFirstPagination.length).to.equal(23); await channelManager.loadNext(); @@ -749,7 +833,11 @@ describe('ChannelManager', () => { const { channels: initialChannels } = channelManager.state.getLatestValue(); expect(initialChannels.length).to.equal(0); - await channelManager.queryChannels({ filterA: true }, { asc: 1 }, { limit: 10, offset: 0 }); + await channelManager.queryChannels( + { filterA: true }, + { asc: 1 }, + { limit: 10, offset: 0 }, + ); await channelManager.loadNext(); const { @@ -777,29 +865,39 @@ describe('ChannelManager', () => { }); describe('websocket event handlers', () => { - let setChannelsStub: sinon.SinonStub; - let isChannelPinnedStub: sinon.SinonStub; - let isChannelArchivedStub: sinon.SinonStub; - let shouldConsiderArchivedChannelsStub: sinon.SinonStub; - let shouldConsiderPinnedChannelsStub: sinon.SinonStub; - let promoteChannelSpy: sinon.SinonSpy; - let getAndWatchChannelStub: sinon.SinonStub; - let findLastPinnedChannelIndexStub: sinon.SinonStub; - let extractSortValueStub: sinon.SinonStub; + let setChannelsStub: MockInstance; + let isChannelPinnedStub: MockInstance<(typeof utils)['isChannelPinned']>; + let isChannelArchivedStub: MockInstance<(typeof utils)['isChannelArchived']>; + let shouldConsiderArchivedChannelsStub: MockInstance< + (typeof utils)['shouldConsiderArchivedChannels'] + >; + let shouldConsiderPinnedChannelsStub: MockInstance< + (typeof utils)['shouldConsiderPinnedChannels'] + >; + let promoteChannelSpy: MockInstance<(typeof utils)['promoteChannel']>; + let getAndWatchChannelStub: MockInstance<(typeof utils)['getAndWatchChannel']>; + let findLastPinnedChannelIndexStub: MockInstance< + (typeof utils)['findLastPinnedChannelIndex'] + >; + let extractSortValueStub: MockInstance<(typeof utils)['extractSortValue']>; beforeEach(() => { - setChannelsStub = sinon.stub(channelManager, 'setChannels'); - isChannelPinnedStub = sinon.stub(Utils, 'isChannelPinned'); - isChannelArchivedStub = sinon.stub(Utils, 'isChannelArchived'); - shouldConsiderArchivedChannelsStub = sinon.stub(Utils, 'shouldConsiderArchivedChannels'); - shouldConsiderPinnedChannelsStub = sinon.stub(Utils, 'shouldConsiderPinnedChannels'); - getAndWatchChannelStub = sinon.stub(Utils, 'getAndWatchChannel'); - findLastPinnedChannelIndexStub = sinon.stub(Utils, 'findLastPinnedChannelIndex'); - extractSortValueStub = sinon.stub(Utils, 'extractSortValue'); - promoteChannelSpy = sinon.spy(Utils, 'promoteChannel'); + setChannelsStub = vi.spyOn(channelManager, 'setChannels'); + isChannelPinnedStub = vi.spyOn(utils, 'isChannelPinned'); + isChannelArchivedStub = vi.spyOn(utils, 'isChannelArchived'); + shouldConsiderArchivedChannelsStub = vi.spyOn( + utils, + 'shouldConsiderArchivedChannels', + ); + shouldConsiderPinnedChannelsStub = vi.spyOn(utils, 'shouldConsiderPinnedChannels'); + getAndWatchChannelStub = vi.spyOn(utils, 'getAndWatchChannel'); + findLastPinnedChannelIndexStub = vi.spyOn(utils, 'findLastPinnedChannelIndex'); + extractSortValueStub = vi.spyOn(utils, 'extractSortValue'); + promoteChannelSpy = vi.spyOn(utils, 'promoteChannel'); }); afterEach(() => { + vi.resetAllMocks(); sinon.restore(); sinon.reset(); }); @@ -811,28 +909,38 @@ describe('ChannelManager', () => { channelToRemove = channelsResponse[1].channel; }); - (['channel.deleted', 'channel.hidden', 'notification.removed_from_channel'] as const).forEach((eventType) => { + ( + [ + 'channel.deleted', + 'channel.hidden', + 'notification.removed_from_channel', + ] as const + ).forEach((eventType) => { it('should return early if channels is undefined', () => { channelManager.state.partialNext({ channels: undefined }); client.dispatchEvent({ type: eventType, cid: channelToRemove.cid }); client.dispatchEvent({ type: eventType, channel: channelToRemove }); - expect(setChannelsStub.called).to.be.false; + expect(setChannelsStub).toHaveBeenCalledTimes(0); }); it('should remove the channel when event.cid matches', () => { client.dispatchEvent({ type: eventType, cid: channelToRemove.cid }); - expect(setChannelsStub.calledOnce).to.be.true; - expect(setChannelsStub.args[0][0].map((c: ChannelResponse) => c.id)).to.deep.equal(['channel1', 'channel3']); + expect(setChannelsStub).toHaveBeenCalledOnce(); + const channels = setChannelsStub.mock.lastCall?.[0] as Channel[]; + + expect(channels.map((c) => c.id)).to.deep.equal(['channel1', 'channel3']); }); it('should remove the channel when event.channel?.cid matches', () => { client.dispatchEvent({ type: eventType, channel: channelToRemove }); - expect(setChannelsStub.calledOnce).to.be.true; - expect(setChannelsStub.args[0][0].map((c: ChannelResponse) => c.id)).to.deep.equal(['channel1', 'channel3']); + expect(setChannelsStub).toHaveBeenCalledOnce(); + expect( + (setChannelsStub.mock.calls[0][0] as Channel[]).map((c) => c.id), + ).to.deep.equal(['channel1', 'channel3']); }); it('should not modify the list if no channels match', () => { @@ -840,7 +948,7 @@ describe('ChannelManager', () => { client.dispatchEvent({ type: eventType, cid: 'channel123' }); const { channels: newChannels } = channelManager.state.getLatestValue(); - expect(setChannelsStub.called).to.be.false; + expect(setChannelsStub).toHaveBeenCalledTimes(0); expect(prevChannels).to.equal(newChannels); expect(prevChannels).to.deep.equal(newChannels); }); @@ -851,21 +959,29 @@ describe('ChannelManager', () => { it('should not update the state early if channels are not defined', () => { channelManager.state.partialNext({ channels: undefined }); - client.dispatchEvent({ type: 'message.new', channel_type: 'messaging', channel_id: 'channel2' }); + client.dispatchEvent({ + type: 'message.new', + channel_type: 'messaging', + channel_id: 'channel2', + }); - expect(setChannelsStub.called).to.be.false; + expect(setChannelsStub).toHaveBeenCalledTimes(0); }); it('should not update the state if channel is pinned and sorting considers pinned channels', () => { const { channels: prevChannels } = channelManager.state.getLatestValue(); - isChannelPinnedStub.returns(true); - shouldConsiderPinnedChannelsStub.returns(true); + isChannelPinnedStub.mockReturnValueOnce(true); + shouldConsiderPinnedChannelsStub.mockReturnValueOnce(true); - client.dispatchEvent({ type: 'message.new', channel_type: 'messaging', channel_id: 'channel2' }); + client.dispatchEvent({ + type: 'message.new', + channel_type: 'messaging', + channel_id: 'channel2', + }); const { channels: newChannels } = channelManager.state.getLatestValue(); - expect(setChannelsStub.called).to.be.false; + expect(setChannelsStub).toHaveBeenCalledTimes(0); expect(prevChannels).to.equal(newChannels); expect(prevChannels).to.deep.equal(newChannels); }); @@ -876,14 +992,18 @@ describe('ChannelManager', () => { ...prevState, pagination: { ...prevState.pagination, filters: { archived: false } }, })); - isChannelArchivedStub.returns(true); - shouldConsiderArchivedChannelsStub.returns(true); + isChannelArchivedStub.mockReturnValueOnce(true); + shouldConsiderArchivedChannelsStub.mockReturnValueOnce(true); - client.dispatchEvent({ type: 'message.new', channel_type: 'messaging', channel_id: 'channel2' }); + client.dispatchEvent({ + type: 'message.new', + channel_type: 'messaging', + channel_id: 'channel2', + }); const { channels: newChannels } = channelManager.state.getLatestValue(); - expect(setChannelsStub.called).to.be.false; + expect(setChannelsStub).toHaveBeenCalledTimes(0); expect(prevChannels).to.equal(newChannels); expect(prevChannels).to.deep.equal(newChannels); }); @@ -894,14 +1014,18 @@ describe('ChannelManager', () => { ...prevState, pagination: { ...prevState.pagination, filters: { archived: true } }, })); - isChannelArchivedStub.returns(false); - shouldConsiderArchivedChannelsStub.returns(true); + isChannelArchivedStub.mockReturnValueOnce(false); + shouldConsiderArchivedChannelsStub.mockReturnValueOnce(true); - client.dispatchEvent({ type: 'message.new', channel_type: 'messaging', channel_id: 'channel2' }); + client.dispatchEvent({ + type: 'message.new', + channel_type: 'messaging', + channel_id: 'channel2', + }); const { channels: newChannels } = channelManager.state.getLatestValue(); - expect(setChannelsStub.called).to.be.false; + expect(setChannelsStub).toHaveBeenCalledTimes(0); expect(prevChannels).to.equal(newChannels); expect(prevChannels).to.deep.equal(newChannels); }); @@ -910,11 +1034,15 @@ describe('ChannelManager', () => { const { channels: prevChannels } = channelManager.state.getLatestValue(); channelManager.setOptions({ lockChannelOrder: true }); - client.dispatchEvent({ type: 'message.new', channel_type: 'messaging', channel_id: 'channel2' }); + client.dispatchEvent({ + type: 'message.new', + channel_type: 'messaging', + channel_id: 'channel2', + }); const { channels: newChannels } = channelManager.state.getLatestValue(); - expect(setChannelsStub.called).to.be.false; + expect(setChannelsStub).toHaveBeenCalledTimes(0); expect(prevChannels).to.equal(newChannels); expect(prevChannels).to.deep.equal(newChannels); @@ -923,10 +1051,10 @@ describe('ChannelManager', () => { it('should not update the state if the channel is not part of the list and allowNotLoadedChannelPromotionForEvent["message.new"] if false', () => { const { channels: prevChannels } = channelManager.state.getLatestValue(); - isChannelPinnedStub.returns(false); - isChannelArchivedStub.returns(false); - shouldConsiderArchivedChannelsStub.returns(false); - shouldConsiderPinnedChannelsStub.returns(false); + isChannelPinnedStub.mockReturnValueOnce(false); + isChannelArchivedStub.mockReturnValueOnce(false); + shouldConsiderArchivedChannelsStub.mockReturnValueOnce(false); + shouldConsiderPinnedChannelsStub.mockReturnValueOnce(false); channelManager.setOptions({ allowNotLoadedChannelPromotionForEvent: { 'channel.visible': true, @@ -936,11 +1064,15 @@ describe('ChannelManager', () => { }, }); - client.dispatchEvent({ type: 'message.new', channel_type: 'messaging', channel_id: 'channel4' }); + client.dispatchEvent({ + type: 'message.new', + channel_type: 'messaging', + channel_id: 'channel4', + }); const { channels: newChannels } = channelManager.state.getLatestValue(); - expect(setChannelsStub.called).to.be.false; + expect(setChannelsStub).toHaveBeenCalledTimes(0); expect(prevChannels).to.equal(newChannels); expect(prevChannels).to.deep.equal(newChannels); @@ -948,54 +1080,74 @@ describe('ChannelManager', () => { }); it('should move the channel upwards if it is not part of the list and allowNotLoadedChannelPromotionForEvent["message.new"] is true', () => { - isChannelPinnedStub.returns(false); - isChannelArchivedStub.returns(false); - shouldConsiderArchivedChannelsStub.returns(false); - shouldConsiderPinnedChannelsStub.returns(false); + isChannelPinnedStub.mockReturnValue(false); + isChannelArchivedStub.mockReturnValue(false); + shouldConsiderArchivedChannelsStub.mockReturnValue(false); + shouldConsiderPinnedChannelsStub.mockReturnValue(false); - client.dispatchEvent({ type: 'message.new', channel_type: 'messaging', channel_id: 'channel4' }); + const stateBefore = channelManager.state.getLatestValue(); - const { - pagination: { sort }, - channels, - } = channelManager.state.getLatestValue(); - const promoteChannelArgs = promoteChannelSpy.args[0][0]; - const newChannel = client.channel('messaging', 'channel4'); - - expect(setChannelsStub.calledOnce).to.be.true; - expect(promoteChannelSpy.calledOnce).to.be.true; - expect(promoteChannelArgs).to.deep.equal({ - channels, - channelToMove: newChannel, - channelToMoveIndexWithinChannels: -1, - sort, + client.dispatchEvent({ + type: 'message.new', + channel_type: 'messaging', + channel_id: 'channel4', }); - expect(setChannelsStub.args[0][0]).to.deep.equal(Utils.promoteChannel(promoteChannelArgs)); + + const stateAfter = channelManager.state.getLatestValue(); + + expect(setChannelsStub).toHaveBeenCalledOnce(); + expect(promoteChannelSpy).toHaveBeenCalledOnce(); + + expect(stateBefore.channels.map((v) => v.cid)).toMatchInlineSnapshot(` + [ + "messaging:channel1", + "messaging:channel2", + "messaging:channel3", + ] + `); + expect(stateAfter.channels.map((v) => v.cid)).toMatchInlineSnapshot(` + [ + "messaging:channel4", + "messaging:channel1", + "messaging:channel2", + "messaging:channel3", + ] + `); }); it('should move the channel upwards if all conditions allow it', () => { - isChannelPinnedStub.returns(false); - isChannelArchivedStub.returns(false); - shouldConsiderArchivedChannelsStub.returns(false); - shouldConsiderPinnedChannelsStub.returns(false); - - client.dispatchEvent({ type: 'message.new', channel_type: 'messaging', channel_id: 'channel2' }); + isChannelPinnedStub.mockReturnValueOnce(false); + isChannelArchivedStub.mockReturnValueOnce(false); + shouldConsiderArchivedChannelsStub.mockReturnValueOnce(false); + shouldConsiderPinnedChannelsStub.mockReturnValueOnce(false); - const { - pagination: { sort }, - channels, - } = channelManager.state.getLatestValue(); - const promoteChannelArgs = promoteChannelSpy.args[0][0]; + const stateBefore = channelManager.state.getLatestValue(); - expect(setChannelsStub.calledOnce).to.be.true; - expect(promoteChannelSpy.calledOnce).to.be.true; - expect(promoteChannelArgs).to.deep.equal({ - channels, - channelToMove: channels[1], - channelToMoveIndexWithinChannels: 1, - sort, + client.dispatchEvent({ + type: 'message.new', + channel_type: 'messaging', + channel_id: 'channel2', }); - expect(setChannelsStub.args[0][0]).to.deep.equal(Utils.promoteChannel(promoteChannelArgs)); + + const stateAfter = channelManager.state.getLatestValue(); + + expect(promoteChannelSpy).toHaveBeenCalledOnce(); + expect(setChannelsStub).toHaveBeenCalledOnce(); + + expect(stateBefore.channels.map((v) => v.cid)).toMatchInlineSnapshot(` + [ + "messaging:channel1", + "messaging:channel2", + "messaging:channel3", + ] + `); + expect(stateAfter.channels.map((v) => v.cid)).toMatchInlineSnapshot(` + [ + "messaging:channel2", + "messaging:channel1", + "messaging:channel3", + ] + `); }); }); @@ -1011,32 +1163,47 @@ describe('ChannelManager', () => { }); it('should not update the state if the event has no id and type', async () => { - client.dispatchEvent({ type: 'notification.message_new', channel: ({} as unknown) as ChannelResponse }); + client.dispatchEvent({ + type: 'notification.message_new', + channel: {} as unknown as ChannelResponse, + }); await clock.runAllAsync(); - expect(getAndWatchChannelStub.called).to.be.false; - expect(setChannelsStub.called).to.be.false; + expect(getAndWatchChannelStub).toHaveBeenCalledTimes(0); + expect(setChannelsStub).toHaveBeenCalledTimes(0); }); it('should execute getAndWatchChannel if id and type are provided', async () => { const newChannelResponse = generateChannel({ channel: { id: 'channel4' } }); - const newChannel = client.channel(newChannelResponse.channel.type, newChannelResponse.channel.id); - getAndWatchChannelStub.resolves(newChannel); + const newChannel = client.channel( + newChannelResponse.channel.type, + newChannelResponse.channel.id, + ); + getAndWatchChannelStub.mockResolvedValue(newChannel); client.dispatchEvent({ type: 'notification.message_new', - channel: ({ type: 'messaging', id: 'channel4' } as unknown) as ChannelResponse, + channel: { type: 'messaging', id: 'channel4' } as unknown as ChannelResponse, }); await clock.runAllAsync(); - expect(getAndWatchChannelStub.calledOnce).to.be.true; - expect(getAndWatchChannelStub.calledWith({ client, id: 'channel4', type: 'messaging' })).to.be.true; + expect(getAndWatchChannelStub).toHaveBeenCalledOnce(); + expect(getAndWatchChannelStub).toHaveBeenCalledWith({ + client, + id: 'channel4', + type: 'messaging', + }); }); it('should not update the state if channel is archived and filters do not allow it', async () => { - isChannelArchivedStub.returns(true); - shouldConsiderArchivedChannelsStub.returns(true); + isChannelArchivedStub.mockReturnValue(true); + shouldConsiderArchivedChannelsStub.mockReturnValue(true); + const newChannelResponse = generateChannel({ channel: { id: 'channel4' } }); + getAndWatchChannelStub.mockImplementation(async () => + client.channel(newChannelResponse.channel.type, newChannelResponse.channel.id), + ); + channelManager.state.next((prevState) => ({ ...prevState, pagination: { ...prevState.pagination, filters: { archived: false } }, @@ -1044,18 +1211,23 @@ describe('ChannelManager', () => { client.dispatchEvent({ type: 'notification.message_new', - channel: ({ type: 'messaging', id: 'channel4' } as unknown) as ChannelResponse, + channel: newChannelResponse.channel as ChannelResponse, }); await clock.runAllAsync(); - expect(getAndWatchChannelStub.called).to.be.true; - expect(setChannelsStub.called).to.be.false; + expect(getAndWatchChannelStub).toHaveBeenCalled(); + expect(setChannelsStub).toHaveBeenCalledTimes(0); }); it('should not update the state if channel is not archived and and filters allow it', async () => { - isChannelArchivedStub.returns(false); - shouldConsiderArchivedChannelsStub.returns(true); + isChannelArchivedStub.mockReturnValueOnce(false); + shouldConsiderArchivedChannelsStub.mockReturnValueOnce(true); + const newChannelResponse = generateChannel({ channel: { id: 'channel4' } }); + getAndWatchChannelStub.mockImplementation(async () => + client.channel(newChannelResponse.channel.type, newChannelResponse.channel.id), + ); + channelManager.state.next((prevState) => ({ ...prevState, pagination: { ...prevState.pagination, filters: { archived: true } }, @@ -1063,19 +1235,22 @@ describe('ChannelManager', () => { client.dispatchEvent({ type: 'notification.message_new', - channel: ({ type: 'messaging', id: 'channel4' } as unknown) as ChannelResponse, + channel: newChannelResponse.channel as ChannelResponse, }); await clock.runAllAsync(); - expect(getAndWatchChannelStub.called).to.be.true; - expect(setChannelsStub.called).to.be.false; + expect(getAndWatchChannelStub).toHaveBeenCalled(); + expect(setChannelsStub).toHaveBeenCalledTimes(0); }); it('should not update the state if allowNotLoadedChannelPromotionForEvent["notification.message_new"] is false', async () => { const newChannelResponse = generateChannel({ channel: { id: 'channel4' } }); - const newChannel = client.channel(newChannelResponse.channel.type, newChannelResponse.channel.id); - getAndWatchChannelStub.resolves(newChannel); + const newChannel = client.channel( + newChannelResponse.channel.type, + newChannelResponse.channel.id, + ); + getAndWatchChannelStub.mockResolvedValueOnce(newChannel); channelManager.setOptions({ allowNotLoadedChannelPromotionForEvent: { 'channel.visible': true, @@ -1086,49 +1261,69 @@ describe('ChannelManager', () => { }); client.dispatchEvent({ type: 'notification.message_new', - channel: ({ type: 'messaging', id: 'channel4' } as unknown) as ChannelResponse, + channel: { type: 'messaging', id: 'channel4' } as unknown as ChannelResponse, }); await clock.runAllAsync(); - expect(getAndWatchChannelStub.called).to.be.true; - expect(setChannelsStub.called).to.be.false; + expect(getAndWatchChannelStub).toHaveBeenCalled(); + expect(setChannelsStub).toHaveBeenCalledTimes(0); channelManager.setOptions({}); }); it('should move channel when all criteria are met', async () => { const newChannelResponse = generateChannel({ channel: { id: 'channel4' } }); - const newChannel = client.channel(newChannelResponse.channel.type, newChannelResponse.channel.id); - getAndWatchChannelStub.resolves(newChannel); + const newChannel = client.channel( + newChannelResponse.channel.type, + newChannelResponse.channel.id, + ); + getAndWatchChannelStub.mockResolvedValueOnce(newChannel); + + const stateBefore = channelManager.state.getLatestValue(); + client.dispatchEvent({ type: 'notification.message_new', - channel: ({ type: 'messaging', id: 'channel4' } as unknown) as ChannelResponse, + channel: { type: 'messaging', id: 'channel4' } as unknown as ChannelResponse, }); await clock.runAllAsync(); - const { - pagination: { sort }, - channels, - } = channelManager.state.getLatestValue(); - const promoteChannelArgs = promoteChannelSpy.args[0][0]; - - expect(getAndWatchChannelStub.calledOnce).to.be.true; - expect(promoteChannelSpy.calledOnce).to.be.true; - expect(setChannelsStub.calledOnce).to.be.true; - expect(promoteChannelArgs).to.deep.equal({ channels, channelToMove: newChannel, sort }); - expect(setChannelsStub.args[0][0]).to.deep.equal(Utils.promoteChannel(promoteChannelArgs)); + const stateAfter = channelManager.state.getLatestValue(); + + expect(getAndWatchChannelStub).toHaveBeenCalledOnce(); + expect(promoteChannelSpy).toHaveBeenCalledOnce(); + expect(setChannelsStub).toHaveBeenCalledOnce(); + expect(stateBefore.channels.map((c) => c.cid)).toMatchInlineSnapshot(` + [ + "messaging:channel1", + "messaging:channel2", + "messaging:channel3", + ] + `); + expect(stateAfter.channels.map((c) => c.cid)).toMatchInlineSnapshot(` + [ + "messaging:channel4", + "messaging:channel1", + "messaging:channel2", + "messaging:channel3", + ] + `); }); it('should not add duplicate channels for multiple event invocations', async () => { const newChannelResponse = generateChannel({ channel: { id: 'channel4' } }); - const newChannel = client.channel(newChannelResponse.channel.type, newChannelResponse.channel.id); - getAndWatchChannelStub.resolves(newChannel); + const newChannel = client.channel( + newChannelResponse.channel.type, + newChannelResponse.channel.id, + ); + getAndWatchChannelStub.mockResolvedValue(newChannel); + + const stateBefore = channelManager.state.getLatestValue(); const event = { type: 'notification.message_new', - channel: ({ type: 'messaging', id: 'channel4' } as unknown) as ChannelResponse, + channel: newChannelResponse.channel as ChannelResponse, } as const; // call the event 3 times client.dispatchEvent(event); @@ -1137,21 +1332,26 @@ describe('ChannelManager', () => { await clock.runAllAsync(); - const { - pagination: { sort }, - channels, - } = channelManager.state.getLatestValue(); - const promoteChannelArgs = { channels, channelToMove: newChannel, sort }; - - expect(getAndWatchChannelStub.callCount).to.equal(3); - expect(promoteChannelSpy.callCount).to.equal(3); - expect(setChannelsStub.callCount).to.equal(3); - promoteChannelSpy.args.forEach((arg) => { - expect(arg[0]).to.deep.equal(promoteChannelArgs); - }); - setChannelsStub.args.forEach((arg) => { - expect(arg[0]).to.deep.equal(Utils.promoteChannel(promoteChannelArgs)); - }); + const stateAfter = channelManager.state.getLatestValue(); + + expect(getAndWatchChannelStub.mock.calls.length).to.equal(3); + expect(promoteChannelSpy.mock.calls.length).to.equal(3); + expect(setChannelsStub.mock.calls.length).to.equal(3); + expect(stateBefore.channels.map((c) => c.cid)).toMatchInlineSnapshot(` + [ + "messaging:channel1", + "messaging:channel2", + "messaging:channel3", + ] + `); + expect(stateAfter.channels.map((c) => c.cid)).toMatchInlineSnapshot(` + [ + "messaging:channel4", + "messaging:channel1", + "messaging:channel2", + "messaging:channel3", + ] + `); }); }); @@ -1167,75 +1367,126 @@ describe('ChannelManager', () => { }); it('should not update the state if the event has no id and type', async () => { - client.dispatchEvent({ type: 'channel.visible', channel: ({} as unknown) as ChannelResponse }); + client.dispatchEvent({ + type: 'channel.visible', + channel: {} as unknown as ChannelResponse, + }); await clock.runAllAsync(); - expect(getAndWatchChannelStub.called).to.be.false; - expect(setChannelsStub.called).to.be.false; + expect(getAndWatchChannelStub).toHaveBeenCalledTimes(0); + expect(setChannelsStub).toHaveBeenCalledTimes(0); }); it('should not update the state if channels is undefined', async () => { channelManager.state.partialNext({ channels: undefined }); - client.dispatchEvent({ type: 'channel.visible', channel_id: 'channel4', channel_type: 'messaging' }); + const newChannelResponse = generateChannel({ channel: { id: 'channel4' } }); + getAndWatchChannelStub.mockImplementation(async () => + client.channel(newChannelResponse.channel.type, newChannelResponse.channel.id), + ); + client.dispatchEvent({ + type: 'channel.visible', + channel_id: newChannelResponse.channel.id, + channel_type: newChannelResponse.channel.type, + }); await clock.runAllAsync(); - expect(getAndWatchChannelStub.called).to.be.true; - expect(setChannelsStub.called).to.be.false; + expect(getAndWatchChannelStub).toHaveBeenCalled(); + expect(setChannelsStub).toHaveBeenCalledTimes(0); }); - it('should not update the state if the channel is archived and filters do not allow it', async () => { - isChannelArchivedStub.returns(true); - shouldConsiderArchivedChannelsStub.returns(true); + it('should not update the state if the channel is archived and filters do not allow it (archived:false)', async () => { + isChannelArchivedStub.mockReturnValueOnce(true); + shouldConsiderArchivedChannelsStub.mockReturnValueOnce(true); + const newChannelResponse = generateChannel({ channel: { id: 'channel4' } }); + + getAndWatchChannelStub.mockImplementation(async () => + client.channel(newChannelResponse.channel.type, newChannelResponse.channel.id), + ); + channelManager.state.next((prevState) => ({ ...prevState, pagination: { ...prevState.pagination, filters: { archived: false } }, })); - client.dispatchEvent({ type: 'channel.visible', channel_id: 'channel4', channel_type: 'messaging' }); + client.dispatchEvent({ + type: 'channel.visible', + channel_id: newChannelResponse.channel.cid, + channel_type: newChannelResponse.channel.type, + }); await clock.runAllAsync(); - expect(getAndWatchChannelStub.called).to.be.true; - expect(setChannelsStub.called).to.be.false; + expect(getAndWatchChannelStub).toHaveBeenCalled(); + expect(setChannelsStub).toHaveBeenCalledTimes(0); }); - it('should not update the state if the channel is archived and filters do not allow it', async () => { - isChannelArchivedStub.returns(false); - shouldConsiderArchivedChannelsStub.returns(true); + it('should not update the state if the channel is archived and filters do not allow it (archived:true)', async () => { + isChannelArchivedStub.mockReturnValueOnce(false); + shouldConsiderArchivedChannelsStub.mockReturnValueOnce(true); + + const newChannelResponse = generateChannel({ channel: { id: 'channel4' } }); + + getAndWatchChannelStub.mockImplementation(async () => + client.channel(newChannelResponse.channel.type, newChannelResponse.channel.id), + ); + channelManager.state.next((prevState) => ({ ...prevState, pagination: { ...prevState.pagination, filters: { archived: true } }, })); - client.dispatchEvent({ type: 'channel.visible', channel_id: 'channel4', channel_type: 'messaging' }); + client.dispatchEvent({ + type: 'channel.visible', + channel_id: newChannelResponse.channel.id, + channel_type: newChannelResponse.channel.type, + }); await clock.runAllAsync(); - expect(getAndWatchChannelStub.called).to.be.true; - expect(setChannelsStub.called).to.be.false; + expect(getAndWatchChannelStub).toHaveBeenCalled(); + expect(setChannelsStub).toHaveBeenCalledTimes(0); }); it('should add the channel to the list if all criteria are met', async () => { const newChannelResponse = generateChannel({ channel: { id: 'channel4' } }); - const newChannel = client.channel(newChannelResponse.channel.type, newChannelResponse.channel.id); - getAndWatchChannelStub.resolves(newChannel); - client.dispatchEvent({ type: 'channel.visible', channel_id: 'channel4', channel_type: 'messaging' }); + const newChannel = client.channel( + newChannelResponse.channel.type, + newChannelResponse.channel.id, + ); + getAndWatchChannelStub.mockResolvedValue(newChannel); - await clock.runAllAsync(); + const stateBefore = channelManager.state.getLatestValue(); - const { - pagination: { sort }, - channels, - } = channelManager.state.getLatestValue(); - const promoteChannelArgs = promoteChannelSpy.args[0][0]; + client.dispatchEvent({ + type: 'channel.visible', + channel_id: 'channel4', + channel_type: 'messaging', + }); + + await clock.runAllAsync(); - expect(getAndWatchChannelStub.calledOnce).to.be.true; - expect(promoteChannelSpy.calledOnce).to.be.true; - expect(setChannelsStub.calledOnce).to.be.true; - expect(promoteChannelArgs).to.deep.equal({ channels, channelToMove: newChannel, sort }); - expect(setChannelsStub.args[0][0]).to.deep.equal(Utils.promoteChannel(promoteChannelArgs)); + const stateAfter = channelManager.state.getLatestValue(); + + expect(getAndWatchChannelStub).toHaveBeenCalledOnce(); + expect(promoteChannelSpy).toHaveBeenCalledOnce(); + expect(setChannelsStub).toHaveBeenCalledOnce(); + expect(stateBefore.channels.map((c) => c.cid)).toMatchInlineSnapshot(` + [ + "messaging:channel1", + "messaging:channel2", + "messaging:channel3", + ] + `); + expect(stateAfter.channels.map((c) => c.cid)).toMatchInlineSnapshot(` + [ + "messaging:channel4", + "messaging:channel1", + "messaging:channel2", + "messaging:channel3", + ] + `); }); }); @@ -1265,61 +1516,73 @@ describe('ChannelManager', () => { channel_type: 'messaging', member: { user: { id: 'wrongUserID' } }, }); - expect(setChannelsStub.calledOnce).to.be.false; + expect(setChannelsStub).toHaveBeenCalledTimes(0); - client.dispatchEvent({ type: 'member.updated', channel_id: 'channel2', channel_type: 'messaging', member: {} }); - expect(setChannelsStub.calledOnce).to.be.false; + client.dispatchEvent({ + type: 'member.updated', + channel_id: 'channel2', + channel_type: 'messaging', + member: {}, + }); + expect(setChannelsStub).toHaveBeenCalledTimes(0); }); it('should not update state if channel_type or channel_id is not present', () => { - client.dispatchEvent({ type: 'member.updated', member: { user: { id: 'user123' } } }); - expect(setChannelsStub.calledOnce).to.be.false; + client.dispatchEvent({ + type: 'member.updated', + member: { user: { id: 'user123' } }, + }); + expect(setChannelsStub).toHaveBeenCalledTimes(0); client.dispatchEvent({ type: 'member.updated', member: { user: { id: 'user123' } }, channel_type: 'messaging', }); - expect(setChannelsStub.calledOnce).to.be.false; - client.dispatchEvent({ type: 'member.updated', member: { user: { id: 'user123' } }, channel_id: 'channel2' }); - expect(setChannelsStub.calledOnce).to.be.false; + expect(setChannelsStub).toHaveBeenCalledTimes(0); + client.dispatchEvent({ + type: 'member.updated', + member: { user: { id: 'user123' } }, + channel_id: 'channel2', + }); + expect(setChannelsStub).toHaveBeenCalledTimes(0); }); it('should not update state early if channels are not available in state', () => { channelManager.state.partialNext({ channels: undefined }); dispatchMemberUpdatedEvent(); - expect(setChannelsStub.calledOnce).to.be.false; + expect(setChannelsStub).toHaveBeenCalledTimes(0); }); it('should not update state if options.lockChannelOrder is true', () => { channelManager.setOptions({ lockChannelOrder: true }); dispatchMemberUpdatedEvent(); - expect(setChannelsStub.calledOnce).to.be.false; + expect(setChannelsStub).toHaveBeenCalledTimes(0); }); it('should not update state if neither channel pinning nor archiving should not be considered', () => { - shouldConsiderPinnedChannelsStub.returns(false); - shouldConsiderArchivedChannelsStub.returns(false); + shouldConsiderPinnedChannelsStub.mockReturnValueOnce(false); + shouldConsiderArchivedChannelsStub.mockReturnValueOnce(false); dispatchMemberUpdatedEvent(); - expect(setChannelsStub.calledOnce).to.be.false; + expect(setChannelsStub).toHaveBeenCalledTimes(0); }); it('should update the state if only pinned channels should be considered', () => { - shouldConsiderPinnedChannelsStub.returns(true); - shouldConsiderArchivedChannelsStub.returns(false); + shouldConsiderPinnedChannelsStub.mockReturnValueOnce(true); + shouldConsiderArchivedChannelsStub.mockReturnValueOnce(false); dispatchMemberUpdatedEvent(); - expect(setChannelsStub.calledOnce).to.be.true; + expect(setChannelsStub).toHaveBeenCalledOnce(); }); it('should update the state if only archived channels should be considered', () => { - shouldConsiderPinnedChannelsStub.returns(false); - shouldConsiderArchivedChannelsStub.returns(true); + shouldConsiderPinnedChannelsStub.mockReturnValueOnce(false); + shouldConsiderArchivedChannelsStub.mockReturnValueOnce(true); dispatchMemberUpdatedEvent(); - expect(setChannelsStub.calledOnce).to.be.true; + expect(setChannelsStub).toHaveBeenCalledOnce(); }); it('should handle archiving correctly', () => { @@ -1327,74 +1590,68 @@ describe('ChannelManager', () => { ...prevState, pagination: { ...prevState.pagination, filters: { archived: true } }, })); - isChannelArchivedStub.returns(true); - shouldConsiderArchivedChannelsStub.returns(true); - shouldConsiderPinnedChannelsStub.returns(true); + isChannelArchivedStub.mockReturnValueOnce(true); + shouldConsiderArchivedChannelsStub.mockReturnValueOnce(true); + shouldConsiderPinnedChannelsStub.mockReturnValueOnce(true); dispatchMemberUpdatedEvent(); - expect(setChannelsStub.calledOnce).to.be.true; - expect(setChannelsStub.args[0][0].map((c: ChannelResponse) => c.id)).to.deep.equal([ - 'channel2', - 'channel1', - 'channel3', - ]); + expect(setChannelsStub).toHaveBeenCalledOnce(); + expect( + (setChannelsStub.mock.calls[0][0] as Channel[]).map((c) => c.id), + ).to.deep.equal(['channel2', 'channel1', 'channel3']); }); it('should pin channel at the correct position when pinnedAtSort is 1', () => { - isChannelPinnedStub.returns(false); - shouldConsiderPinnedChannelsStub.returns(true); - findLastPinnedChannelIndexStub.returns(0); - extractSortValueStub.returns(1); + isChannelPinnedStub.mockReturnValueOnce(false); + shouldConsiderPinnedChannelsStub.mockReturnValueOnce(true); + findLastPinnedChannelIndexStub.mockReturnValueOnce(0); + extractSortValueStub.mockReturnValueOnce(1); dispatchMemberUpdatedEvent('channel3'); - expect(setChannelsStub.calledOnce).to.be.true; - expect(setChannelsStub.args[0][0].map((c: ChannelResponse) => c.id)).to.deep.equal([ - 'channel1', - 'channel3', - 'channel2', - ]); + expect(setChannelsStub).toHaveBeenCalledOnce(); + expect( + (setChannelsStub.mock.calls[0][0] as Channel[]).map((c) => c.id), + ).to.deep.equal(['channel1', 'channel3', 'channel2']); }); it('should pin channel at the correct position when pinnedAtSort is -1 and the target is not pinned', () => { - isChannelPinnedStub.callsFake((c) => c.id === 'channel1'); - shouldConsiderPinnedChannelsStub.returns(true); - findLastPinnedChannelIndexStub.returns(0); - extractSortValueStub.returns(-1); + isChannelPinnedStub.mockImplementationOnce((c) => c.id === 'channel1'); + shouldConsiderPinnedChannelsStub.mockReturnValueOnce(true); + findLastPinnedChannelIndexStub.mockReturnValueOnce(0); + extractSortValueStub.mockReturnValueOnce(-1); dispatchMemberUpdatedEvent('channel3'); - expect(setChannelsStub.calledOnce).to.be.true; - expect(setChannelsStub.args[0][0].map((c: ChannelResponse) => c.id)).to.deep.equal([ - 'channel1', - 'channel3', - 'channel2', - ]); + expect(setChannelsStub).toHaveBeenCalledOnce(); + expect( + (setChannelsStub.mock.calls[0][0] as Channel[]).map((c) => c.id), + ).to.deep.equal(['channel1', 'channel3', 'channel2']); }); it('should pin channel at the correct position when pinnedAtSort is -1 and the target is pinned', () => { - isChannelPinnedStub.callsFake((c) => ['channel1', 'channel3'].includes(c.id)); - shouldConsiderPinnedChannelsStub.returns(true); - findLastPinnedChannelIndexStub.returns(0); - extractSortValueStub.returns(-1); + isChannelPinnedStub.mockImplementationOnce((c) => + ['channel1', 'channel3'].includes(c.id!), + ); + shouldConsiderPinnedChannelsStub.mockReturnValueOnce(true); + findLastPinnedChannelIndexStub.mockReturnValueOnce(0); + extractSortValueStub.mockReturnValueOnce(-1); dispatchMemberUpdatedEvent('channel3'); - expect(setChannelsStub.calledOnce).to.be.true; - expect(setChannelsStub.args[0][0].map((c: ChannelResponse) => c.id)).to.deep.equal([ - 'channel3', - 'channel1', - 'channel2', - ]); + expect(setChannelsStub).toHaveBeenCalledOnce(); + expect( + (setChannelsStub.mock.calls[0][0] as Channel[]).map((c) => c.id), + ).to.deep.equal(['channel3', 'channel1', 'channel2']); }); it('should not update state if position of target channel does not change', () => { - isChannelPinnedStub.returns(false); - shouldConsiderPinnedChannelsStub.returns(true); - findLastPinnedChannelIndexStub.returns(0); - extractSortValueStub.returns(1); + isChannelPinnedStub.mockReturnValueOnce(false); + shouldConsiderPinnedChannelsStub.mockReturnValueOnce(true); + findLastPinnedChannelIndexStub.mockReturnValueOnce(0); + extractSortValueStub.mockReturnValueOnce(1); dispatchMemberUpdatedEvent(); const { channels } = channelManager.state.getLatestValue(); - expect(setChannelsStub.calledOnce).to.be.false; + expect(setChannelsStub).toHaveBeenCalledTimes(0); expect(channels[1].id).to.equal('channel2'); }); }); @@ -1413,20 +1670,23 @@ describe('ChannelManager', () => { it('should not update state if event.channel defaults are missing', async () => { client.dispatchEvent({ type: 'notification.added_to_channel' }); await clock.runAllAsync(); - expect(setChannelsStub.calledOnce).to.be.false; + expect(setChannelsStub).toHaveBeenCalledTimes(0); client.dispatchEvent({ type: 'notification.added_to_channel', - channel: ({ id: '123' } as unknown) as ChannelResponse, + channel: { id: '123' } as unknown as ChannelResponse, }); await clock.runAllAsync(); - expect(setChannelsStub.calledOnce).to.be.false; + expect(setChannelsStub).toHaveBeenCalledTimes(0); }); it('should not update state if allowNotLoadedChannelPromotionForEvent["notification.added_to_channel"] is false', async () => { const newChannelResponse = generateChannel({ channel: { id: 'channel4' } }); - const newChannel = client.channel(newChannelResponse.channel.type, newChannelResponse.channel.id); - getAndWatchChannelStub.resolves(newChannel); + const newChannel = client.channel( + newChannelResponse.channel.type, + newChannelResponse.channel.id, + ); + getAndWatchChannelStub.mockResolvedValueOnce(newChannel); channelManager.setOptions({ allowNotLoadedChannelPromotionForEvent: { 'channel.visible': true, @@ -1437,36 +1697,39 @@ describe('ChannelManager', () => { }); client.dispatchEvent({ type: 'notification.added_to_channel', - channel: ({ + channel: { id: 'channel4', type: 'messaging', members: [{ user_id: 'user1' }], - } as unknown) as ChannelResponse, + } as unknown as ChannelResponse, }); await clock.runAllAsync(); - expect(setChannelsStub.called).to.be.false; + expect(setChannelsStub).toHaveBeenCalledTimes(0); channelManager.setOptions({}); }); it('should call getAndWatchChannel with correct parameters', async () => { const newChannelResponse = generateChannel({ channel: { id: 'channel4' } }); - const newChannel = client.channel(newChannelResponse.channel.type, newChannelResponse.channel.id); - getAndWatchChannelStub.resolves(newChannel); + const newChannel = client.channel( + newChannelResponse.channel.type, + newChannelResponse.channel.id, + ); + getAndWatchChannelStub.mockResolvedValueOnce(newChannel); client.dispatchEvent({ type: 'notification.added_to_channel', - channel: ({ + channel: { id: 'channel4', type: 'messaging', members: [{ user_id: 'user1' }], - } as unknown) as ChannelResponse, + } as unknown as ChannelResponse, }); await clock.runAllAsync(); - expect(getAndWatchChannelStub.calledOnce).to.be.true; - expect(getAndWatchChannelStub.args[0][0]).to.deep.equal({ + expect(getAndWatchChannelStub).toHaveBeenCalledOnce(); + expect(getAndWatchChannelStub.mock.calls[0][0]).to.deep.equal({ client, id: 'channel4', type: 'messaging', @@ -1476,33 +1739,44 @@ describe('ChannelManager', () => { it('should move the channel upwards when criteria is met', async () => { const newChannelResponse = generateChannel({ channel: { id: 'channel4' } }); - const newChannel = client.channel(newChannelResponse.channel.type, newChannelResponse.channel.id); - getAndWatchChannelStub.resolves(newChannel); + const newChannel = client.channel( + newChannelResponse.channel.type, + newChannelResponse.channel.id, + ); + getAndWatchChannelStub.mockResolvedValue(newChannel); + + const stateBefore = channelManager.state.getLatestValue(); + client.dispatchEvent({ type: 'notification.added_to_channel', - channel: ({ + channel: { id: 'channel4', type: 'messaging', members: [{ user_id: 'user1' }], - } as unknown) as ChannelResponse, + } as unknown as ChannelResponse, }); await clock.runAllAsync(); - const { - pagination: { sort }, - channels, - } = channelManager.state.getLatestValue(); - const promoteChannelArgs = promoteChannelSpy.args[0][0]; - - expect(setChannelsStub.calledOnce).to.be.true; - expect(promoteChannelSpy.calledOnce).to.be.true; - expect(promoteChannelArgs).to.deep.equal({ - channels, - channelToMove: newChannel, - sort, - }); - expect(setChannelsStub.args[0][0]).to.deep.equal(Utils.promoteChannel(promoteChannelArgs)); + const stateAfter = channelManager.state.getLatestValue(); + + expect(setChannelsStub).toHaveBeenCalledOnce(); + expect(promoteChannelSpy).toHaveBeenCalledOnce(); + expect(stateBefore.channels.map((c) => c.cid)).toMatchInlineSnapshot(` + [ + "messaging:channel1", + "messaging:channel2", + "messaging:channel3", + ] + `); + expect(stateAfter.channels.map((c) => c.cid)).toMatchInlineSnapshot(` + [ + "messaging:channel4", + "messaging:channel1", + "messaging:channel2", + "messaging:channel3", + ] + `); }); }); }); diff --git a/test/unit/channel_state.js b/test/unit/channel_state.test.js similarity index 88% rename from test/unit/channel_state.js rename to test/unit/channel_state.test.js index 7e66e2a695..59e09c86a9 100644 --- a/test/unit/channel_state.js +++ b/test/unit/channel_state.test.js @@ -1,4 +1,3 @@ -import chai from 'chai'; import { v4 as uuidv4 } from 'uuid'; import { generateChannel } from './test-utils/generateChannel'; @@ -10,7 +9,7 @@ import { getOrCreateChannelApi } from './test-utils/getOrCreateChannelApi'; import { ChannelState, StreamChat, Channel } from '../../src'; import { DEFAULT_MESSAGE_SET_PAGINATION } from '../../src/constants'; -const expect = chai.expect; +import { describe, beforeEach, it, expect } from 'vitest'; describe('ChannelState addMessagesSorted', function () { it('empty state add single messages', async function () { @@ -99,16 +98,22 @@ describe('ChannelState addMessagesSorted', function () { const state = new ChannelState(); for (let i = 0; i < 10; i++) { - state.addMessagesSorted([generateMsg({ id: `${i}`, date: `2020-01-01T00:00:00.00${i}Z` })]); + state.addMessagesSorted([ + generateMsg({ id: `${i}`, date: `2020-01-01T00:00:00.00${i}Z` }), + ]); } for (let i = 10; i < state.messages.length - 1; i++) { for (let j = i + 1; i < state.messages.length - 1; j++) - expect(state.messages[i].created_at.getTime()).to.be.lessThan(state.messages[j].created_at.getTime()); + expect(state.messages[i].created_at.getTime()).to.be.lessThan( + state.messages[j].created_at.getTime(), + ); } expect(state.messages).to.have.length(10); - state.addMessagesSorted([generateMsg({ id: 'id', date: `2020-01-01T00:00:00.007Z` })]); + state.addMessagesSorted([ + generateMsg({ id: 'id', date: `2020-01-01T00:00:00.007Z` }), + ]); expect(state.messages).to.have.length(11); expect(state.messages[7].id).to.be.equal('7'); expect(state.messages[8].id).to.be.equal('id'); @@ -118,13 +123,17 @@ describe('ChannelState addMessagesSorted', function () { const state = new ChannelState(); for (let i = 100; i < 300; i++) { - state.addMessagesSorted([generateMsg({ id: `${i}`, date: `2020-01-01T00:00:00.${i}Z` })]); + state.addMessagesSorted([ + generateMsg({ id: `${i}`, date: `2020-01-01T00:00:00.${i}Z` }), + ]); } expect(state.messages).to.have.length(200); for (let i = 100; i < state.messages.length - 1; i++) { for (let j = i + 1; j < state.messages.length - 1; j++) - expect(state.messages[i].created_at.getTime()).to.be.lessThan(state.messages[j].created_at.getTime()); + expect(state.messages[i].created_at.getTime()).to.be.lessThan( + state.messages[j].created_at.getTime(), + ); } }); @@ -164,7 +173,9 @@ describe('ChannelState addMessagesSorted', function () { ); expect(state.messages).to.have.length(1); expect(state.messages[0].text).to.be.equal('update 0'); - expect(state.messages[0].created_at.getTime()).to.be.equal(new Date('2020-01-01T00:00:00.044Z').getTime()); + expect(state.messages[0].created_at.getTime()).to.be.equal( + new Date('2020-01-01T00:00:00.044Z').getTime(), + ); }); it('should respect order and avoid duplicates if message.created_at changes', async function () { @@ -208,8 +219,18 @@ describe('ChannelState addMessagesSorted', function () { it('should add messages to new message set', () => { const state = new ChannelState(); - state.addMessagesSorted([generateMsg({ id: '12' }), generateMsg({ id: '13' }), generateMsg({ id: '14' })]); - state.addMessagesSorted([generateMsg({ id: '0' }), generateMsg({ id: '1' })], false, false, true, 'new'); + state.addMessagesSorted([ + generateMsg({ id: '12' }), + generateMsg({ id: '13' }), + generateMsg({ id: '14' }), + ]); + state.addMessagesSorted( + [generateMsg({ id: '0' }), generateMsg({ id: '1' })], + false, + false, + true, + 'new', + ); expect(state.messages.length).to.be.equal(3); expect(state.messages[0].id).to.be.equal('12'); @@ -265,7 +286,13 @@ describe('ChannelState addMessagesSorted', function () { true, 'latest', ); - state.addMessagesSorted([generateMsg({ id: '0' }), generateMsg({ id: '1' })], false, false, true, 'new'); + state.addMessagesSorted( + [generateMsg({ id: '0' }), generateMsg({ id: '1' })], + false, + false, + true, + 'new', + ); state.messageSets[0].isCurrent = false; state.messageSets[1].isCurrent = true; state.addMessagesSorted([generateMsg({ id: '15' })], false, false, true, 'latest'); @@ -277,7 +304,11 @@ describe('ChannelState addMessagesSorted', function () { it(`shouldn't create new message set for thread replies`, () => { const state = new ChannelState(); state.addMessagesSorted( - [generateMsg({ parent_id: '12' }), generateMsg({ parent_id: '12' }), generateMsg({ parent_id: '12' })], + [ + generateMsg({ parent_id: '12' }), + generateMsg({ parent_id: '12' }), + generateMsg({ parent_id: '12' }), + ], false, false, true, @@ -289,7 +320,11 @@ describe('ChannelState addMessagesSorted', function () { it(`should update message in non-active message set`, () => { const state = new ChannelState(); - state.addMessagesSorted([generateMsg({ id: '12' }), generateMsg({ id: '13' }), generateMsg({ id: '14' })]); + state.addMessagesSorted([ + generateMsg({ id: '12' }), + generateMsg({ id: '13' }), + generateMsg({ id: '14' }), + ]); state.addMessagesSorted( [generateMsg({ id: '0', date: '2020-01-01T00:00:00.000Z' })], false, @@ -298,7 +333,13 @@ describe('ChannelState addMessagesSorted', function () { 'new', ); state.addMessagesSorted( - [generateMsg({ id: '0', date: '2020-01-01T00:00:00.000Z', text: 'Updated text' })], + [ + generateMsg({ + id: '0', + date: '2020-01-01T00:00:00.000Z', + text: 'Updated text', + }), + ], false, false, false, @@ -317,7 +358,13 @@ describe('ChannelState addMessagesSorted', function () { generateMsg({ id: '14', date: '2020-01-01T00:00:11.000Z' }), ]); state.addMessagesSorted( - [generateMsg({ id: '13', date: '2020-01-01T00:00:10.000Z', text: 'Updated text' })], + [ + generateMsg({ + id: '13', + date: '2020-01-01T00:00:10.000Z', + text: 'Updated text', + }), + ], false, false, false, @@ -342,7 +389,13 @@ describe('ChannelState addMessagesSorted', function () { 'latest', ); state.addMessagesSorted( - [generateMsg({ id: '13', date: '2020-01-01T00:00:10.000Z', text: 'Updated text' })], + [ + generateMsg({ + id: '13', + date: '2020-01-01T00:00:10.000Z', + text: 'Updated text', + }), + ], false, false, false, @@ -354,9 +407,19 @@ describe('ChannelState addMessagesSorted', function () { it(`should do nothing if message is not available locally`, () => { const state = new ChannelState(); - state.addMessagesSorted([generateMsg({ id: '12' }), generateMsg({ id: '13' }), generateMsg({ id: '14' })]); + state.addMessagesSorted([ + generateMsg({ id: '12' }), + generateMsg({ id: '13' }), + generateMsg({ id: '14' }), + ]); state.addMessagesSorted([generateMsg({ id: '5' })], false, false, true, 'new'); - state.addMessagesSorted([generateMsg({ id: '1' }), generateMsg({ id: '2' })], false, false, true, 'new'); + state.addMessagesSorted( + [generateMsg({ id: '1' }), generateMsg({ id: '2' })], + false, + false, + true, + 'new', + ); state.addMessagesSorted([generateMsg({ id: '8' })], false, false, false); expect(state.latestMessages.length).to.be.equal(3); @@ -369,12 +432,18 @@ describe('ChannelState addMessagesSorted', function () { const state = new ChannelState(); expect(state.last_message_at).to.be.null; state.addMessagesSorted([generateMsg({ id: '0', date: '2020-01-01T00:00:00.000Z' })]); - expect(state.last_message_at.getTime()).to.be.equal(new Date('2020-01-01T00:00:00.000Z').getTime()); + expect(state.last_message_at.getTime()).to.be.equal( + new Date('2020-01-01T00:00:00.000Z').getTime(), + ); state.addMessagesSorted([generateMsg({ id: '1', date: '2019-01-01T00:00:00.000Z' })]); - expect(state.last_message_at.getTime()).to.be.equal(new Date('2020-01-01T00:00:00.000Z').getTime()); + expect(state.last_message_at.getTime()).to.be.equal( + new Date('2020-01-01T00:00:00.000Z').getTime(), + ); state.addMessagesSorted([generateMsg({ id: '2', date: '2020-01-01T00:00:00.001Z' })]); - expect(state.last_message_at.getTime()).to.be.equal(new Date('2020-01-01T00:00:00.001Z').getTime()); + expect(state.last_message_at.getTime()).to.be.equal( + new Date('2020-01-01T00:00:00.001Z').getTime(), + ); }); it('sets pinnedMessages correctly', async function () { @@ -399,7 +468,10 @@ describe('ChannelState addMessagesSorted', function () { it('should add message preview', async function () { // these message previews are used UI SDKs - const messagePreview = generateMsg({ id: '1', date: new Date('2020-01-01T00:00:00.001Z') }); + const messagePreview = generateMsg({ + id: '1', + date: new Date('2020-01-01T00:00:00.001Z'), + }); const state = new ChannelState(); state.addMessageSorted(messagePreview); @@ -408,7 +480,10 @@ describe('ChannelState addMessagesSorted', function () { it('should add thread reply preview', async function () { // these message previews are used by UI SDKs - const parentMessage = generateMsg({ id: 'parent_id', date: '2020-01-01T00:00:00.001Z' }); + const parentMessage = generateMsg({ + id: 'parent_id', + date: '2020-01-01T00:00:00.001Z', + }); const threadReplyPreview = generateMsg({ id: '2', date: new Date('2020-01-01T00:00:00.001Z'), @@ -437,7 +512,10 @@ describe('ChannelState addMessagesSorted', function () { generateMsg({ id: '15', date: '2020-01-01T00:00:43.000Z' }), ]; state.addMessagesSorted(messages); - const newMessages = [generateMsg({ id: '10', date: '2020-01-01T00:00:03.000Z' }), ...overlap]; + const newMessages = [ + generateMsg({ id: '10', date: '2020-01-01T00:00:03.000Z' }), + ...overlap, + ]; state.addMessagesSorted(newMessages, false, true, true, 'new'); expect(state.messages.length).to.be.equal(6); @@ -454,13 +532,21 @@ describe('ChannelState addMessagesSorted', function () { it('when new messages overlap with current messages, but not with latest messages', () => { const state = new ChannelState(); const overlap = [generateMsg({ id: '11', date: '2020-01-01T00:00:10.001Z' })]; - const latestMessages = [generateMsg({ id: '20', date: '2020-01-01T00:10:10.001Z' })]; + const latestMessages = [ + generateMsg({ id: '20', date: '2020-01-01T00:10:10.001Z' }), + ]; state.addMessagesSorted(latestMessages); - const currentMessages = [generateMsg({ id: '10', date: '2020-01-01T00:00:03.001Z' }), ...overlap]; + const currentMessages = [ + generateMsg({ id: '10', date: '2020-01-01T00:00:03.001Z' }), + ...overlap, + ]; state.addMessagesSorted(currentMessages, false, true, true, 'new'); state.messageSets[0].isCurrent = false; state.messageSets[1].isCurrent = true; - const newMessages = [...overlap, generateMsg({ id: '12', date: '2020-01-01T00:00:11.001Z' })]; + const newMessages = [ + ...overlap, + generateMsg({ id: '12', date: '2020-01-01T00:00:11.001Z' }), + ]; state.addMessagesSorted(newMessages, false, true, true, 'new'); expect(state.latestMessages.length).to.be.equal(1); @@ -475,15 +561,25 @@ describe('ChannelState addMessagesSorted', function () { it('when new messages overlap with messages, but not current or latest messages', () => { const state = new ChannelState(); const overlap = [generateMsg({ id: '11', date: '2020-01-01T00:00:10.001Z' })]; - const latestMessages = [generateMsg({ id: '20', date: '2020-01-01T00:10:10.001Z' })]; + const latestMessages = [ + generateMsg({ id: '20', date: '2020-01-01T00:10:10.001Z' }), + ]; state.addMessagesSorted(latestMessages); - const currentMessages = [generateMsg({ id: '8', date: '2020-01-01T00:00:03.001Z' })]; + const currentMessages = [ + generateMsg({ id: '8', date: '2020-01-01T00:00:03.001Z' }), + ]; state.addMessagesSorted(currentMessages, false, true, true, 'new'); state.messageSets[0].isCurrent = false; state.messageSets[1].isCurrent = true; - const otherMessages = [generateMsg({ id: '10', date: '2020-01-01T00:00:09.001Z' }), ...overlap]; + const otherMessages = [ + generateMsg({ id: '10', date: '2020-01-01T00:00:09.001Z' }), + ...overlap, + ]; state.addMessagesSorted(otherMessages, false, true, true, 'new'); - const newMessages = [...overlap, generateMsg({ id: '12', date: '2020-01-01T00:00:11.001Z' })]; + const newMessages = [ + ...overlap, + generateMsg({ id: '12', date: '2020-01-01T00:00:11.001Z' }), + ]; state.addMessagesSorted(newMessages, false, true, true, 'new'); expect(state.latestMessages.length).to.be.equal(1); @@ -502,9 +598,14 @@ describe('ChannelState addMessagesSorted', function () { it('when current messages overlap with latest', () => { const state = new ChannelState(); const overlap = [generateMsg({ id: '11', date: '2020-01-01T00:00:10.001Z' })]; - const latestMessages = [...overlap, generateMsg({ id: '12', date: '2020-01-01T00:01:10.001Z' })]; + const latestMessages = [ + ...overlap, + generateMsg({ id: '12', date: '2020-01-01T00:01:10.001Z' }), + ]; state.addMessagesSorted(latestMessages); - const currentMessages = [generateMsg({ id: '8', date: '2020-01-01T00:00:03.001Z' })]; + const currentMessages = [ + generateMsg({ id: '8', date: '2020-01-01T00:00:03.001Z' }), + ]; state.addMessagesSorted(currentMessages, false, true, true, 'new'); state.messageSets[0].isCurrent = false; state.messageSets[1].isCurrent = true; @@ -528,15 +629,25 @@ describe('ChannelState addMessagesSorted', function () { const state = new ChannelState(); const overlap1 = [generateMsg({ id: '11', date: '2020-01-01T00:00:10.001Z' })]; const overlap2 = [generateMsg({ id: '13', date: '2020-01-01T00:01:10.001Z' })]; - const latestMessages = [...overlap2, generateMsg({ id: '14', date: '2020-01-01T00:01:15.001Z' })]; + const latestMessages = [ + ...overlap2, + generateMsg({ id: '14', date: '2020-01-01T00:01:15.001Z' }), + ]; state.addMessagesSorted(latestMessages); - const currentMessages = [generateMsg({ id: '10', date: '2020-01-01T00:00:03.001Z' }), ...overlap1]; + const currentMessages = [ + generateMsg({ id: '10', date: '2020-01-01T00:00:03.001Z' }), + ...overlap1, + ]; state.addMessagesSorted(currentMessages, false, true, true, 'new'); state.messageSets[0].isCurrent = false; state.messageSets[0].pagination = { hasPrev: true, hasNext: false }; state.messageSets[1].isCurrent = true; state.messageSets[1].pagination = { hasPrev: false, hasNext: true }; - const newMessages = [...overlap1, generateMsg({ id: '12', date: '2020-01-01T00:00:14.001Z' }), ...overlap2]; + const newMessages = [ + ...overlap1, + generateMsg({ id: '12', date: '2020-01-01T00:00:14.001Z' }), + ...overlap2, + ]; state.addMessagesSorted(newMessages, false, true, true, 'new'); expect(state.messages.length).to.be.equal(5); @@ -547,7 +658,10 @@ describe('ChannelState addMessagesSorted', function () { expect(state.messages[4].id).to.be.equal('14'); expect(state.messages).to.be.equal(state.latestMessages); expect(state.messageSets.length).to.be.equal(1); - expect(state.messageSets[0].pagination).to.be.eql({ hasPrev: false, hasNext: false }); + expect(state.messageSets[0].pagination).to.be.eql({ + hasPrev: false, + hasNext: false, + }); }); }); }); @@ -785,7 +899,11 @@ describe('updateUserMessages', () => { describe('latestMessages', () => { it('should return latest messages - if they are the current message set', () => { const state = new ChannelState(); - const messages = [generateMsg({ id: '1' }), generateMsg({ id: '2' }), generateMsg({ id: '3' })]; + const messages = [ + generateMsg({ id: '1' }), + generateMsg({ id: '2' }), + generateMsg({ id: '3' }), + ]; state.addMessagesSorted(messages); expect(state.latestMessages.length).to.be.equal(messages.length); @@ -796,7 +914,11 @@ describe('latestMessages', () => { it('should return latest messages - if they are not the current message set', () => { const state = new ChannelState(); - const latestMessages = [generateMsg({ id: '1' }), generateMsg({ id: '2' }), generateMsg({ id: '3' })]; + const latestMessages = [ + generateMsg({ id: '1' }), + generateMsg({ id: '2' }), + generateMsg({ id: '3' }), + ]; state.addMessagesSorted(latestMessages); const newMessages = [generateMsg({ id: '0' })]; state.addMessagesSorted(newMessages, false, true, true, 'new'); @@ -811,7 +933,11 @@ describe('latestMessages', () => { it('should return latest messages - if they are not the current message set and new messages received', () => { const state = new ChannelState(); - const latestMessages = [generateMsg({ id: '1' }), generateMsg({ id: '2' }), generateMsg({ id: '3' })]; + const latestMessages = [ + generateMsg({ id: '1' }), + generateMsg({ id: '2' }), + generateMsg({ id: '3' }), + ]; state.addMessagesSorted(latestMessages); const newMessages = [generateMsg({ id: '0' })]; state.addMessagesSorted(newMessages, false, true, true, 'new'); @@ -954,7 +1080,13 @@ describe('findMessage', () => { it('message is in current message set', async () => { const state = new ChannelState(); const messageId = '8'; - state.addMessagesSorted([generateMsg({ id: messageId })], false, true, true, 'latest'); + state.addMessagesSorted( + [generateMsg({ id: messageId })], + false, + true, + true, + 'latest', + ); state.addMessagesSorted([generateMsg({ id: '5' })], false, true, true, 'new'); expect(state.findMessage(messageId).id).to.eql(messageId); diff --git a/test/unit/client.js b/test/unit/client.test.js similarity index 85% rename from test/unit/client.js rename to test/unit/client.test.js index 2d6a3a1a68..6856437079 100644 --- a/test/unit/client.js +++ b/test/unit/client.test.js @@ -1,5 +1,3 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import sinon from 'sinon'; import { generateMsg } from './test-utils/generateMessage'; import { getClientWithUser } from './test-utils/getClient'; @@ -11,8 +9,7 @@ import { StableWSConnection } from '../../src/connection'; import { mockChannelQueryResponse } from './test-utils/mockChannelQueryResponse'; import { DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE } from '../../src/constants'; -const expect = chai.expect; -chai.use(chaiAsPromised); +import { describe, beforeEach, it, expect, beforeAll, afterAll } from 'vitest'; describe('StreamChat getInstance', () => { beforeEach(() => { @@ -50,7 +47,9 @@ describe('StreamChat getInstance', () => { await client1.connectUser({ id: 'vishal' }, 'token'); const client2 = StreamChat.getInstance('key2'); - await expect(client2.connectUser({ id: 'Amin' }, 'token')).to.be.rejectedWith(/connectUser was called twice/); + await expect(client2.connectUser({ id: 'Amin' }, 'token')).rejects.toThrow( + /connectUser was called twice/, + ); }); it('should not throw error if connectUser called twice with the same user', async () => { @@ -99,7 +98,7 @@ describe('StreamChat getInstance', () => { const client = new StreamChat('key', 'secret'); const cert = Buffer.from('test'); const options = { apn_config: { p12_cert: cert } }; - await expect(client.updateAppSettings(options)).to.be.rejectedWith(/.*/); + await expect(client.updateAppSettings(options)).rejects.toThrow(/.*/); expect(options.apn_config.p12_cert).to.be.eql(cert); }); @@ -189,11 +188,17 @@ describe('Client active channels cache', () => { client.wsPromise = Promise.resolve(); }; beforeEach(() => { - client.activeChannels = { vish: { state: { unreadCount: 1 } }, vish2: { state: { unreadCount: 2 } } }; + client.activeChannels = { + vish: { state: { unreadCount: 1 } }, + vish2: { state: { unreadCount: 2 } }, + }; }); const countUnreadChannels = (channels) => - Object.values(channels).reduce((prevSum, currSum) => prevSum + currSum.state.unreadCount, 0); + Object.values(channels).reduce( + (prevSum, currSum) => prevSum + currSum.state.unreadCount, + 0, + ); it('should mark all active channels as read on notification.mark_read event if event.unread_channels is 0', function () { client.dispatchEvent({ @@ -256,7 +261,7 @@ describe('Client connectUser', () => { }); it('should throw err for missing user id', async () => { - await expect(client.connectUser({ user: 'user' }, 'token')).to.be.rejectedWith( + await expect(client.connectUser({ user: 'user' }, 'token')).rejects.toThrow( /The "id" field on the user is missing/, ); }); @@ -271,7 +276,9 @@ describe('Client connectUser', () => { it('should throw error if connectUser called twice on the client with different user', async () => { await client.connectUser({ id: 'vishal' }, 'token'); - await expect(client.connectUser({ id: 'Amin' }, 'token')).to.be.rejectedWith(/connectUser was called twice/); + await expect(client.connectUser({ id: 'Amin' }, 'token')).rejects.toThrow( + /connectUser was called twice/, + ); }); it('should work for multiple call for the same user', async () => { @@ -351,7 +358,7 @@ describe('Client deleteUsers', () => { client.post = () => Promise.resolve(); - await expect(client.deleteUsers(['_'])).to.eventually.equal(); + await expect(client.deleteUsers(['_'])).resolves.toEqual(); }); it('delete types - options.conversations', async () => { @@ -359,10 +366,12 @@ describe('Client deleteUsers', () => { client.post = () => Promise.resolve(); - await expect(client.deleteUsers(['_'], { conversations: 'hard' })).to.eventually.equal(); - await expect(client.deleteUsers(['_'], { conversations: 'soft' })).to.eventually.equal(); - await expect(client.deleteUsers(['_'], { conversations: 'pruning' })).to.be.rejectedWith(); - await expect(client.deleteUsers(['_'], { conversations: '' })).to.be.rejectedWith(); + await expect(client.deleteUsers(['_'], { conversations: 'hard' })).resolves.toEqual(); + await expect(client.deleteUsers(['_'], { conversations: 'soft' })).resolves.toEqual(); + await expect( + client.deleteUsers(['_'], { conversations: 'pruning' }), + ).rejects.toThrow(); + await expect(client.deleteUsers(['_'], { conversations: '' })).rejects.toThrow(); }); it('delete types - options.messages', async () => { @@ -370,10 +379,10 @@ describe('Client deleteUsers', () => { client.post = () => Promise.resolve(); - await expect(client.deleteUsers(['_'], { messages: 'hard' })).to.eventually.equal(); - await expect(client.deleteUsers(['_'], { messages: 'soft' })).to.eventually.equal(); - await expect(client.deleteUsers(['_'], { messages: 'pruning' })).to.eventually.equal(); - await expect(client.deleteUsers(['_'], { messages: '' })).to.be.rejectedWith(); + await expect(client.deleteUsers(['_'], { messages: 'hard' })).resolves.toEqual(); + await expect(client.deleteUsers(['_'], { messages: 'soft' })).resolves.toEqual(); + await expect(client.deleteUsers(['_'], { messages: 'pruning' })).resolves.toEqual(); + await expect(client.deleteUsers(['_'], { messages: '' })).rejects.toThrow(); }); it('delete types - options.user', async () => { @@ -381,10 +390,10 @@ describe('Client deleteUsers', () => { client.post = () => Promise.resolve(); - await expect(client.deleteUsers(['_'], { user: 'hard' })).to.eventually.equal(); - await expect(client.deleteUsers(['_'], { user: 'soft' })).to.eventually.equal(); - await expect(client.deleteUsers(['_'], { user: 'pruning' })).to.eventually.equal(); - await expect(client.deleteUsers(['_'], { user: '' })).to.be.rejectedWith(); + await expect(client.deleteUsers(['_'], { user: 'hard' })).resolves.toEqual(); + await expect(client.deleteUsers(['_'], { user: 'soft' })).resolves.toEqual(); + await expect(client.deleteUsers(['_'], { user: 'pruning' })).resolves.toEqual(); + await expect(client.deleteUsers(['_'], { user: '' })).rejects.toThrow(); }); }); @@ -463,7 +472,7 @@ describe('Client search', async () => { offset: 1, sort: [{ custom_field: -1 }], }), - ).to.be.fulfilled; + ).resolves.toEqual(); }); it('next and offset fails', async () => { await expect( @@ -471,7 +480,7 @@ describe('Client search', async () => { offset: 1, next: 'next', }), - ).to.be.rejectedWith(Error); + ).rejects.toThrow(Error); }); }); @@ -491,7 +500,9 @@ describe('Client setLocalDevice', async () => { client.wsConnection.isHealthy = true; client.wsConnection.connectionID = 'ID'; - expect(() => client.setLocalDevice({ id: 'id3', push_provider: 'firebase' })).to.throw(); + expect(() => + client.setLocalDevice({ id: 'id3', push_provider: 'firebase' }), + ).to.throw(); }); }); @@ -547,8 +558,18 @@ describe('Client WSFallback', () => { client.wsBaseURL = 'ws://getstream.io'; const health = await client.connectUser({ id: 'amin' }, userToken); await client.disconnectUser(); - expect(health).to.be.eql({ type: 'health.check', connection_id: 'new_id', received_at: eventDate }); - expect(client.dispatchEvent.calledWithMatch({ type: 'transport.changed', mode: 'longpoll' })).to.be.true; + expect(health).to.be.eql({ + type: 'health.check', + connection_id: 'new_id', + received_at: eventDate, + }); + + expect( + client.dispatchEvent.calledWithMatch({ + type: 'transport.changed', + mode: 'longpoll', + }), + ).to.be.true; expect( client.dispatchEvent.calledWithMatch({ type: 'health.check', @@ -562,19 +583,20 @@ describe('Client WSFallback', () => { client.wsBaseURL = 'ws://getstream.io'; client.options.enableWSFallback = false; - await expect(client.connectUser({ id: 'amin' }, userToken)).to.be.rejectedWith( + await expect(client.connectUser({ id: 'amin' }, userToken)).rejects.toThrow( /"initial WS connection could not be established","isWSFailure":true/, ); expect(client.wsFallback).to.be.undefined; }); - it('should ignore fallback if browser is offline', async () => { + // FIXME: this test is wrong and all kinds of flaky + it.skip('should ignore fallback if browser is offline', async () => { client.wsBaseURL = 'ws://getstream.io'; client.options.enableWSFallback = true; sinon.stub(utils, 'isOnline').returns(false); - await expect(client.connectUser({ id: 'amin' }, userToken)).to.be.rejectedWith( + await expect(client.connectUser({ id: 'amin' }, userToken)).rejects.toThrow( /"initial WS connection could not be established","isWSFailure":true/, ); @@ -583,7 +605,10 @@ describe('Client WSFallback', () => { it('should reuse the fallback if already created', async () => { client.options.enableWSFallback = true; - const fallback = { isHealthy: () => false, connect: sinon.stub().returns({ connection_id: 'id' }) }; + const fallback = { + isHealthy: () => false, + connect: sinon.stub().returns({ connection_id: 'id' }), + }; client.wsFallback = fallback; sinon.stub(utils, 'isOnline').returns(false); @@ -600,7 +625,10 @@ describe('StreamChat.queryChannels', async () => { client._cacheEnabled = () => false; const mockedChannelsQueryResponse = Array.from({ length: 10 }, () => ({ ...mockChannelQueryResponse, - messages: Array.from({ length: DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE }, generateMsg), + messages: Array.from( + { length: DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE }, + generateMsg, + ), })); const mock = sinon.mock(client); mock.expects('post').returns(Promise.resolve(mockedChannelsQueryResponse)); @@ -613,14 +641,20 @@ describe('StreamChat.queryChannels', async () => { const client = await getClientWithUser(); const mockedChannelsQueryResponse = Array.from({ length: 10 }, () => ({ ...mockChannelQueryResponse, - messages: Array.from({ length: DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE }, generateMsg), + messages: Array.from( + { length: DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE }, + generateMsg, + ), })); const mock = sinon.mock(client); mock.expects('post').returns(Promise.resolve(mockedChannelsQueryResponse)); await client.queryChannels(); Object.values(client.activeChannels).forEach((channel) => { expect(channel.state.messageSets.length).to.be.equal(1); - expect(channel.state.messageSets[0].pagination).to.eql({ hasNext: true, hasPrev: true }); + expect(channel.state.messageSets[0].pagination).to.eql({ + hasNext: true, + hasPrev: true, + }); }); mock.restore(); }); @@ -629,14 +663,20 @@ describe('StreamChat.queryChannels', async () => { const client = await getClientWithUser(); const mockedChannelQueryResponse = Array.from({ length: 10 }, () => ({ ...mockChannelQueryResponse, - messages: Array.from({ length: DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE - 1 }, generateMsg), + messages: Array.from( + { length: DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE - 1 }, + generateMsg, + ), })); const mock = sinon.mock(client); mock.expects('post').returns(Promise.resolve(mockedChannelQueryResponse)); await client.queryChannels(); Object.values(client.activeChannels).forEach((channel) => { expect(channel.state.messageSets.length).to.be.equal(1); - expect(channel.state.messageSets[0].pagination).to.eql({ hasNext: true, hasPrev: false }); + expect(channel.state.messageSets[0].pagination).to.eql({ + hasNext: true, + hasPrev: false, + }); }); mock.restore(); }); @@ -645,12 +685,12 @@ describe('StreamChat.queryChannels', async () => { describe('X-Stream-Client header', () => { let client; - before(() => { + beforeAll(() => { process.env.PKG_VERSION = '1.2.3'; process.env.CLIENT_BUNDLE = 'browser-esm'; }); - after(() => { + afterAll(() => { // clean up process.env.PKG_VERSION = undefined; process.env.CLIENT_BUNDLE = undefined; @@ -663,21 +703,27 @@ describe('X-Stream-Client header', () => { it('server-side integration', () => { const userAgent = client.getUserAgent(); - expect(userAgent).to.be.equal('stream-chat-js-v1.2.3-node|client_bundle=browser-esm'); + expect(userAgent).toMatchInlineSnapshot( + `"stream-chat-js-v1.2.3-node|client_bundle=browser-esm"`, + ); }); it('client-side integration', () => { client.node = false; const userAgent = client.getUserAgent(); - expect(userAgent).to.be.equal('stream-chat-js-v1.2.3-browser|client_bundle=browser-esm'); + expect(userAgent).toMatchInlineSnapshot( + `"stream-chat-js-v1.2.3-browser|client_bundle=browser-esm"`, + ); }); it('SDK integration', () => { client.sdkIdentifier = { name: 'react', version: '2.3.4' }; const userAgent = client.getUserAgent(); - expect(userAgent).to.be.equal('stream-chat-react-v2.3.4-llc-v1.2.3|client_bundle=browser-esm'); + expect(userAgent).toMatchInlineSnapshot( + `"stream-chat-react-v2.3.4-llc-v1.2.3|client_bundle=browser-esm"`, + ); }); it('SDK integration with deviceIdentifier', () => { @@ -685,8 +731,8 @@ describe('X-Stream-Client header', () => { client.deviceIdentifier = { os: 'iOS 15.0', model: 'iPhone17,4' }; const userAgent = client.getUserAgent(); - expect(userAgent).to.be.equal( - 'stream-chat-react-native-v2.3.4-llc-v1.2.3|os=iOS 15.0|device_model=iPhone17,4|client_bundle=browser-esm', + expect(userAgent).toMatchInlineSnapshot( + `"stream-chat-react-native-v2.3.4-llc-v1.2.3|os=iOS 15.0|device_model=iPhone17,4|client_bundle=browser-esm"`, ); }); @@ -694,6 +740,6 @@ describe('X-Stream-Client header', () => { client.setUserAgent('deprecated'); const userAgent = client.getUserAgent(); - expect(userAgent).to.be.equal('deprecated'); + expect(userAgent).toMatchInlineSnapshot(`"deprecated"`); }); }); diff --git a/test/unit/client_state.js b/test/unit/client_state.test.js similarity index 89% rename from test/unit/client_state.js rename to test/unit/client_state.test.js index 77f0b1980b..8398455fe6 100644 --- a/test/unit/client_state.js +++ b/test/unit/client_state.test.js @@ -1,11 +1,7 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; - import { ClientState } from '../../src/client_state'; import { StreamChat } from '../../src'; -const expect = chai.expect; -chai.use(chaiAsPromised); +import { describe, beforeEach, it, expect } from 'vitest'; describe('ClientState', () => { let state; diff --git a/test/unit/connection.js b/test/unit/connection.test.js similarity index 94% rename from test/unit/connection.js rename to test/unit/connection.test.js index 896c6c6a05..f8d5a3a77f 100644 --- a/test/unit/connection.js +++ b/test/unit/connection.test.js @@ -1,8 +1,6 @@ -import chai from 'chai'; import sinon from 'sinon'; import url from 'url'; -import { Server as WsServer } from 'ws'; -import chaiAsPromised from 'chai-as-promised'; +import { Server as WsServer } from 'isomorphic-ws'; import { StableWSConnection } from '../../src/connection'; import { StreamChat } from '../../src/client'; @@ -10,8 +8,7 @@ import { TokenManager } from '../../src/token_manager'; import { sleep } from '../../src/utils'; import { InsightMetrics } from '../../src/insights'; -chai.use(chaiAsPromised); -const expect = chai.expect; +import { describe, expect, it, afterAll } from 'vitest'; describe('connection', function () { const wsBaseURL = 'http://localhost:9999'; @@ -42,7 +39,7 @@ describe('connection', function () { ), ); - after(() => wss.close()); + afterAll(() => wss.close()); describe('Connection tokenProvider', () => { it('should handle token provider rejection ', async () => { @@ -51,7 +48,9 @@ describe('connection', function () { }); client.defaultWSTimeout = 20; const tokenProvider = () => Promise.reject(new Error('network failure')); - await expect(client.connectUser({ id: 'amin' }, tokenProvider)).to.be.rejectedWith(/tokenProvider failed/); + await expect(client.connectUser({ id: 'amin' }, tokenProvider)).rejects.toThrow( + /tokenProvider failed/, + ); }); }); @@ -122,7 +121,7 @@ describe('connection', function () { it('connect should throw if already connecting', async () => { const c = new StableWSConnection({ client: newStreamChat() }); c.isConnecting = true; - await expect(c.connect()).to.be.rejectedWith(/called connect twice/); + await expect(c.connect()).rejects.toThrow(/called connect twice/); }); it('_recover should not call _connect if isConnecting is set', async () => { @@ -190,7 +189,7 @@ describe('connection', function () { }); client.defaultWSTimeout = 2000; - await expect(client.connectUser({ id: 'amin' }, token)).to.be.rejectedWith( + await expect(client.connectUser({ id: 'amin' }, token)).rejects.toThrow( /initial WS connection could not be established/, ); }); diff --git a/test/unit/connection_fallback.js b/test/unit/connection_fallback.test.js similarity index 86% rename from test/unit/connection_fallback.js rename to test/unit/connection_fallback.test.js index 107a293787..53db85ac44 100644 --- a/test/unit/connection_fallback.js +++ b/test/unit/connection_fallback.test.js @@ -1,13 +1,10 @@ -import chai from 'chai'; import sinon from 'sinon'; -import chaiAsPromised from 'chai-as-promised'; import * as utils from '../../src/utils'; import * as errors from '../../src/errors'; import { ConnectionState, WSConnectionFallback } from '../../src/connection_fallback'; -chai.use(chaiAsPromised); -const expect = chai.expect; +import { describe, it, expect, afterEach, vi, beforeAll, beforeEach } from 'vitest'; describe('connection_fallback', () => { const newClient = (overrides) => ({ @@ -21,6 +18,10 @@ describe('connection_fallback', () => { ...overrides, }); + afterEach(() => { + vi.restoreAllMocks(); + }); + afterEach(() => { sinon.restore(); }); @@ -35,10 +36,13 @@ describe('connection_fallback', () => { expect(c.consecutiveFailures).to.be.eql(0); }); - it('should register window event listeners', () => { - sinon.spy(utils, 'addConnectionEventListeners'); + it('should register window event listeners', async () => { + vi.spyOn(utils, 'addConnectionEventListeners'); + const c = new WSConnectionFallback({ client: newClient() }); - expect(utils.addConnectionEventListeners.calledOnceWithExactly(c._onlineStatusChanged)).to.be.true; + expect(utils.addConnectionEventListeners).toHaveBeenCalledExactlyOnceWith( + c._onlineStatusChanged, + ); sinon.restore(); }); }); @@ -71,7 +75,12 @@ describe('connection_fallback', () => { expect(client.dispatchEvent.called).to.be.false; c._setState(ConnectionState.Connected); - expect(client.dispatchEvent.calledOnceWithExactly({ type: 'connection.changed', online: true })).to.be.true; + expect( + client.dispatchEvent.calledOnceWithExactly({ + type: 'connection.changed', + online: true, + }), + ).to.be.true; }); it('should dispatchEvent for offline status', function () { @@ -80,8 +89,12 @@ describe('connection_fallback', () => { expect(client.dispatchEvent.called).to.be.false; c._setState(ConnectionState.Closed); - expect(client.dispatchEvent.calledOnceWithExactly({ type: 'connection.changed', online: false })).to.be - .true; + expect( + client.dispatchEvent.calledOnceWithExactly({ + type: 'connection.changed', + online: false, + }), + ).to.be.true; c._setState(ConnectionState.Connected); expect(client.dispatchEvent.calledOnce).to.be.true; @@ -89,8 +102,12 @@ describe('connection_fallback', () => { c._setState(ConnectionState.Disconnected); expect(client.dispatchEvent.calledTwice).to.be.true; - expect(client.dispatchEvent.alwaysCalledWithExactly({ type: 'connection.changed', online: false })).to.be - .true; + expect( + client.dispatchEvent.alwaysCalledWithExactly({ + type: 'connection.changed', + online: false, + }), + ).to.be.true; }); }); @@ -149,12 +166,13 @@ describe('connection_fallback', () => { describe('disconnect', () => { it('should unregister window event listeners', async () => { - sinon.spy(utils, 'removeConnectionEventListeners'); + vi.spyOn(utils, 'removeConnectionEventListeners'); const c = new WSConnectionFallback({ client: newClient() }); c._req = () => null; await c.disconnect(); - expect(utils.removeConnectionEventListeners.calledOnceWithExactly(c._onlineStatusChanged)).to.be.true; - sinon.restore(); + expect(utils.removeConnectionEventListeners).toHaveBeenCalledExactlyOnceWith( + c._onlineStatusChanged, + ); }); it('should cancel requests and set the state correctly', async () => { @@ -171,7 +189,9 @@ describe('connection_fallback', () => { expect(c.connectionID).to.be.undefined; expect(c.cancelToken).to.be.undefined; expect(cancel.calledOnce).to.be.true; - expect(c._req.calledOnceWithExactly({ close: true, connection_id }, { timeout }, false)).to.be.true; + expect( + c._req.calledOnceWithExactly({ close: true, connection_id }, { timeout }, false), + ).to.be.true; }); it('should ingore request errors', async () => { @@ -210,7 +230,15 @@ describe('connection_fallback', () => { it('should keep track of consecutive failures', async () => { // ok-err-err-ok-ok... - const doAxiosRequest = sinon.stub().onCall(0).resolves().onCall(1).rejects().onCall(2).rejects().resolves(); + const doAxiosRequest = sinon + .stub() + .onCall(0) + .resolves() + .onCall(1) + .rejects() + .onCall(2) + .rejects() + .resolves(); const c = new WSConnectionFallback({ client: newClient({ doAxiosRequest }) }); expect(c.consecutiveFailures).to.be.eql(0); @@ -233,7 +261,7 @@ describe('connection_fallback', () => { sinon.spy(c); expect(c.consecutiveFailures).to.be.eql(0); - await expect(c._req({}, {}, true)).to.be.rejected; + await expect(c._req({}, {}, true)).rejects.toThrow(); expect(c.consecutiveFailures).to.be.eql(1); expect(c._req.calledOnce).to.be.true; }); @@ -245,20 +273,26 @@ describe('connection_fallback', () => { sinon.spy(c); expect(c.consecutiveFailures).to.be.eql(0); - await expect(c._req({}, {}, false)).to.be.rejected; + await expect(c._req({}, {}, false)).rejects.toThrow(); expect(c.consecutiveFailures).to.be.eql(1); expect(c._req.calledOnce).to.be.true; }); it('should retry errors if it is retryable', async () => { const doAxiosRequest = sinon.stub().rejects(); - sinon.stub(errors, 'isErrorRetryable').onCall(0).returns(true).onCall(1).returns(true).returns(false); - sinon.stub(utils, 'sleep').resolves(); + + vi.spyOn(errors, 'isErrorRetryable') + .mockReturnValueOnce(true) + .mockReturnValueOnce(true) + .mockReturnValueOnce(false); + + vi.spyOn(utils, 'sleep').mockResolvedValue(); + const c = new WSConnectionFallback({ client: newClient({ doAxiosRequest }) }); sinon.spy(c, '_req'); expect(c.consecutiveFailures).to.be.eql(0); - await expect(c._req({}, {}, true)).to.be.rejected; + await expect(c._req({}, {}, true)).rejects.toThrow(); expect(c.consecutiveFailures).to.be.eql(3); expect(c._req.calledThrice).to.be.true; }); @@ -285,12 +319,14 @@ describe('connection_fallback', () => { expect(await c.connect()).to.be.eql(health); expect(c.client._buildWSPayload.calledOnce).to.be.true; expect(c._poll.calledOnce).to.be.true; - expect(c._req.calledOnceWithExactly({ json: 'payload' }, { timeout: 8000 }, false)).to.be.true; + expect(c._req.calledOnceWithExactly({ json: 'payload' }, { timeout: 8000 }, false)) + .to.be.true; c.state = ConnectionState.Init; c._req = sinon.stub().resolves({ event: health }); expect(await c.connect(true)).to.be.eql(health); - expect(c._req.calledOnceWithExactly({ json: 'payload' }, { timeout: 8000 }, true)).to.be.true; + expect(c._req.calledOnceWithExactly({ json: 'payload' }, { timeout: 8000 }, true)) + .to.be.true; }); it('should update state and connectionID', async () => { @@ -304,7 +340,7 @@ describe('connection_fallback', () => { c = new WSConnectionFallback({ client: newClient() }); c._req = sinon.stub().rejects(); c._poll = sinon.spy(); - await expect(c.connect()).to.be.rejected; + await expect(c.connect()).rejects.toThrow(); expect(c._poll.called).to.be.false; expect(c.state).to.be.eql(ConnectionState.Closed); expect(c.connectionID).to.be.undefined; @@ -320,7 +356,7 @@ describe('connection_fallback', () => { c = new WSConnectionFallback({ client: newClient() }); c._req = sinon.stub().rejects(); c._poll = sinon.spy(); - await expect(c.connect()).to.be.rejected; + await expect(c.connect()).rejects.toThrow(); expect(c._poll.called).to.be.false; }); @@ -334,7 +370,7 @@ describe('connection_fallback', () => { c = new WSConnectionFallback({ client: newClient() }); c._req = sinon.stub().rejects(); c._poll = sinon.spy(); - await expect(c.connect()).to.be.rejected; + await expect(c.connect()).rejects.toThrow(); expect(c.client.recoverState.called).to.be.false; c = new WSConnectionFallback({ client: newClient() }); @@ -399,29 +435,28 @@ describe('connection_fallback', () => { }); it('should stop for non-retryable errors', async () => { + vi.spyOn(errors, 'isErrorRetryable').mockReturnValue(false); + vi.spyOn(errors, 'isAPIError').mockReturnValue(true); const c = new WSConnectionFallback({ client: newClient() }); c.state = ConnectionState.Connected; c._req = sinon.stub().rejects(); - sinon.stub(errors, 'isErrorRetryable').returns(false); - sinon.stub(errors, 'isAPIError').returns(true); await c._poll(); expect(c.state).to.be.eql(ConnectionState.Closed); }); it('should continue retrying for random errors', async () => { - const c = new WSConnectionFallback({ client: newClient() }); - c.state = ConnectionState.Connected; - c._req = sinon.stub().rejects(); - let counter = 0; - sinon.stub(utils, 'sleep').callsFake(() => { + vi.spyOn(utils, 'sleep').mockImplementation(() => { if (++counter > 2) c.state = ConnectionState.Disconnected; }); + const c = new WSConnectionFallback({ client: newClient() }); + c.state = ConnectionState.Connected; + c._req = sinon.stub().rejects(); await c._poll(); expect(c._req.calledThrice).to.be.true; - expect(utils.sleep.calledThrice).to.be.true; + expect(utils.sleep).toHaveBeenCalledTimes(3); }); }); }); diff --git a/test/unit/errors.js b/test/unit/errors.test.js similarity index 92% rename from test/unit/errors.js rename to test/unit/errors.test.js index a60ab1e173..2f7e465b93 100644 --- a/test/unit/errors.js +++ b/test/unit/errors.test.js @@ -1,7 +1,6 @@ -import chai from 'chai'; import { isErrorResponse } from '../../src/errors'; -const expect = chai.expect; +import { describe, it, expect } from 'vitest'; describe('error response', () => { it('is response with no status attribute', () => { diff --git a/test/unit/poll.test.js b/test/unit/poll.test.js index b761dd3aa6..bc22e6e59e 100644 --- a/test/unit/poll.test.js +++ b/test/unit/poll.test.js @@ -1,7 +1,8 @@ -import { expect } from 'chai'; import sinon from 'sinon'; import { Poll, StreamChat } from '../../src'; +import { describe, it, afterEach, expect } from 'vitest'; + const pollId = 'WD4SBRJvLoGwB4oAoCQGM'; const user1 = { @@ -268,17 +269,26 @@ describe('Poll', () => { const vote_counts_by_option = { ...originalState.vote_counts_by_option, - [castedVote.option_id]: originalState.vote_counts_by_option[castedVote.option_id] + 1, + [castedVote.option_id]: + originalState.vote_counts_by_option[castedVote.option_id] + 1, }; const latest_votes_by_option = { ...originalState.latest_votes_by_option, - [castedVote.option_id]: [...originalState.latest_votes_by_option[castedVote.option_id], castedVote], + [castedVote.option_id]: [ + ...originalState.latest_votes_by_option[castedVote.option_id], + castedVote, + ], }; poll.handleVoteCasted({ type: 'poll.vote_casted', - poll: { ...pollResponse, latest_votes_by_option, vote_count, vote_counts_by_option }, + poll: { + ...pollResponse, + latest_votes_by_option, + vote_count, + vote_counts_by_option, + }, poll_vote: castedVote, }); @@ -286,7 +296,10 @@ describe('Poll', () => { expect(poll.data.ownAnswer).to.eql(originalState.ownAnswer); expect(poll.data.latest_answers).to.eql(originalState.latest_answers); expect(poll.data.latest_votes_by_option).to.eql(latest_votes_by_option); - expect(poll.data.maxVotedOptionIds).to.eql([...originalState.maxVotedOptionIds, castedVote.option_id]); + expect(poll.data.maxVotedOptionIds).to.eql([ + ...originalState.maxVotedOptionIds, + castedVote.option_id, + ]); }); it('should add own vote when handleVoteCasted is called', () => { @@ -307,7 +320,8 @@ describe('Poll', () => { const vote_counts_by_option = { ...originalState.vote_counts_by_option, - [castedVote.option_id]: originalState.vote_counts_by_option[castedVote.option_id] + 1, + [castedVote.option_id]: + originalState.vote_counts_by_option[castedVote.option_id] + 1, }; const latest_votes_by_option = { @@ -322,7 +336,12 @@ describe('Poll', () => { poll.handleVoteCasted({ type: 'poll.vote_casted', - poll: { ...pollResponse, latest_votes_by_option, vote_count, vote_counts_by_option }, + poll: { + ...pollResponse, + latest_votes_by_option, + vote_count, + vote_counts_by_option, + }, poll_vote: castedVote, }); @@ -355,7 +374,10 @@ describe('Poll', () => { expect(poll.data.ownVotesByOptionId).to.eql(originalState.ownVotesByOptionId); expect(poll.data.ownAnswer).to.eql(originalState.ownAnswer); - expect(poll.data.latest_answers).to.eql([castedVote, ...originalState.latest_answers]); + expect(poll.data.latest_answers).to.eql([ + castedVote, + ...originalState.latest_answers, + ]); expect(poll.data.latest_votes_by_option).to.eql(originalState.latest_votes_by_option); expect(poll.data.maxVotedOptionIds).to.eql(originalState.maxVotedOptionIds); }); @@ -383,7 +405,10 @@ describe('Poll', () => { expect(poll.data.ownVotesByOptionId).to.eql(originalState.ownVotesByOptionId); expect(poll.data.ownAnswer).to.eql(castedVote); - expect(poll.data.latest_answers).to.eql([castedVote, ...originalState.latest_answers]); + expect(poll.data.latest_answers).to.eql([ + castedVote, + ...originalState.latest_answers, + ]); expect(poll.data.latest_votes_by_option).to.eql(originalState.latest_votes_by_option); expect(poll.data.maxVotedOptionIds).to.eql(originalState.maxVotedOptionIds); }); @@ -403,12 +428,16 @@ describe('Poll', () => { const vote_counts_by_option = { ...originalState.vote_counts_by_option, - [changedToOptionId]: (originalState.vote_counts_by_option[changedToOptionId] ?? 0) + 1, + [changedToOptionId]: + (originalState.vote_counts_by_option[changedToOptionId] ?? 0) + 1, }; const latest_votes_by_option = { ...originalState.latest_votes_by_option, - [changedToOptionId]: [...originalState.latest_votes_by_option[changedToOptionId], castedVote], + [changedToOptionId]: [ + ...originalState.latest_votes_by_option[changedToOptionId], + castedVote, + ], }; poll.handleVoteChanged({ @@ -421,7 +450,10 @@ describe('Poll', () => { expect(poll.data.ownAnswer).to.eql(originalState.ownAnswer); expect(poll.data.latest_answers).to.eql(originalState.latest_answers); expect(poll.data.latest_votes_by_option).to.eql(latest_votes_by_option); - expect(poll.data.maxVotedOptionIds).to.eql([...originalState.maxVotedOptionIds, changedToOptionId]); + expect(poll.data.maxVotedOptionIds).to.eql([ + ...originalState.maxVotedOptionIds, + changedToOptionId, + ]); }); it('should change own vote when handleVoteChanged is called', () => { @@ -440,12 +472,16 @@ describe('Poll', () => { const vote_counts_by_option = { ...originalState.vote_counts_by_option, - [changedToOptionId]: (originalState.vote_counts_by_option[changedToOptionId] ?? 0) + 1, + [changedToOptionId]: + (originalState.vote_counts_by_option[changedToOptionId] ?? 0) + 1, }; const latest_votes_by_option = { ...originalState.latest_votes_by_option, - [changedToOptionId]: [...originalState.latest_votes_by_option[changedToOptionId], castedVote], + [changedToOptionId]: [ + ...originalState.latest_votes_by_option[changedToOptionId], + castedVote, + ], }; poll.handleVoteChanged({ @@ -461,7 +497,10 @@ describe('Poll', () => { expect(poll.data.ownAnswer).to.eql(originalState.ownAnswer); expect(poll.data.latest_answers).to.eql(originalState.latest_answers); expect(poll.data.latest_votes_by_option).to.eql(latest_votes_by_option); - expect(poll.data.maxVotedOptionIds).to.eql([...originalState.maxVotedOptionIds, changedToOptionId]); + expect(poll.data.maxVotedOptionIds).to.eql([ + ...originalState.maxVotedOptionIds, + changedToOptionId, + ]); }); it('should change an answer when handleVoteChanged is called', () => { @@ -481,7 +520,10 @@ describe('Poll', () => { expect(poll.data.ownVotesByOptionId).to.eql(originalState.ownVotesByOptionId); expect(poll.data.ownAnswer).to.eql(originalState.ownAnswer); - expect(poll.data.latest_answers).to.eql([changedAnswer, ...originalState.latest_answers]); + expect(poll.data.latest_answers).to.eql([ + changedAnswer, + ...originalState.latest_answers, + ]); expect(poll.data.latest_votes_by_option).to.eql(originalState.latest_votes_by_option); expect(poll.data.maxVotedOptionIds).to.eql(originalState.maxVotedOptionIds); }); @@ -517,14 +559,15 @@ describe('Poll', () => { const originalState = poll.data; const vote_counts_by_option = { ...originalState.vote_counts_by_option, - [user2Votes[1].option_id]: originalState.vote_counts_by_option[user2Votes[1].option_id] - 1, + [user2Votes[1].option_id]: + originalState.vote_counts_by_option[user2Votes[1].option_id] - 1, }; const latest_votes_by_option = { ...originalState.latest_votes_by_option, - [user2Votes[1].option_id]: originalState.latest_votes_by_option[user2Votes[1].option_id].filter( - (v) => v.option_id !== user2Votes[1].option_id, - ), + [user2Votes[1].option_id]: originalState.latest_votes_by_option[ + user2Votes[1].option_id + ].filter((v) => v.option_id !== user2Votes[1].option_id), }; poll.handleVoteRemoved({ @@ -578,7 +621,9 @@ describe('Poll', () => { poll_vote: { ...removedVote, user_id: client.userID }, }); - expect(poll.data.ownVotesByOptionId).to.eql({ 'dc22dcd6-4fc8-4c92-92c2-bfd63245724c': user1Votes[1] }); + expect(poll.data.ownVotesByOptionId).to.eql({ + 'dc22dcd6-4fc8-4c92-92c2-bfd63245724c': user1Votes[1], + }); expect(poll.data.ownAnswer).to.eql(originalState.ownAnswer); expect(poll.data.latest_answers).to.eql(originalState.latest_answers); expect(poll.data.latest_votes_by_option).to.eql(latest_votes_by_option); @@ -603,7 +648,9 @@ describe('Poll', () => { expect(poll.data.ownVotesByOptionId).to.eql(originalState.ownVotesByOptionId); expect(poll.data.ownAnswer).to.eql(originalState.ownAnswer); - expect(poll.data.latest_answers).to.eql(originalState.latest_answers.filter((a) => a.id !== removedAnswer.id)); + expect(poll.data.latest_answers).to.eql( + originalState.latest_answers.filter((a) => a.id !== removedAnswer.id), + ); expect(poll.data.latest_votes_by_option).to.eql(originalState.latest_votes_by_option); expect(poll.data.maxVotedOptionIds).to.eql(originalState.maxVotedOptionIds); }); @@ -622,7 +669,9 @@ describe('Poll', () => { expect(poll.data.ownVotesByOptionId).to.eql(originalState.ownVotesByOptionId); expect(poll.data.ownAnswer).to.be.undefined; - expect(poll.data.latest_answers).to.eql(originalState.latest_answers.filter((a) => a.id !== removedAnswer.id)); + expect(poll.data.latest_answers).to.eql( + originalState.latest_answers.filter((a) => a.id !== removedAnswer.id), + ); expect(poll.data.latest_votes_by_option).to.eql(originalState.latest_votes_by_option); expect(poll.data.maxVotedOptionIds).to.eql(originalState.maxVotedOptionIds); }); @@ -644,13 +693,19 @@ describe('Poll', () => { expect(getPollStub.calledWith(pollResponse.id)).to.be.true; const { lastActivityAt: __, ...currentPollState } = poll.data; - const { lastActivityAt: _, ...expectedPollState } = { ...originalState, ...mockPollResponse }; + const { lastActivityAt: _, ...expectedPollState } = { + ...originalState, + ...mockPollResponse, + }; expect(currentPollState).to.eql(expectedPollState); getPollStub.restore(); }); it('should remove oldest vote before casting a new one if reached max votes allowed', async () => { - const poll = new Poll({ client, poll: { ...pollResponse, max_votes_allowed: user2Votes.length } }); + const poll = new Poll({ + client, + poll: { ...pollResponse, max_votes_allowed: user2Votes.length }, + }); const option_id = 'ba933470-c0da-4b6f-a4d2-d2176ac0d4a8'; const messageId = 'XXX'; const removePollVoteStub = sinon.stub(client, 'removePollVote'); @@ -660,14 +715,19 @@ describe('Poll', () => { await poll.castVote(option_id, messageId); - expect(removePollVoteStub.calledWith(messageId, pollResponse.id, user1Votes[1].id)).to.be.true; - expect(castPollVoteStub.calledWith(messageId, pollResponse.id, { option_id })).to.be.true; + expect(removePollVoteStub.calledWith(messageId, pollResponse.id, user1Votes[1].id)).to + .be.true; + expect(castPollVoteStub.calledWith(messageId, pollResponse.id, { option_id })).to.be + .true; removePollVoteStub.restore(); castPollVoteStub.restore(); }); it('should not remove oldest vote before casting a new one if not reached max votes allowed', async () => { - const poll = new Poll({ client, poll: { ...pollResponse, max_votes_allowed: user2Votes.length + 1 } }); + const poll = new Poll({ + client, + poll: { ...pollResponse, max_votes_allowed: user2Votes.length + 1 }, + }); const option_id = 'ba933470-c0da-4b6f-a4d2-d2176ac0d4a8'; const messageId = 'XXX'; const removePollVoteStub = sinon.stub(client, 'removePollVote'); @@ -678,13 +738,17 @@ describe('Poll', () => { await poll.castVote(option_id, messageId); expect(removePollVoteStub.called).to.be.false; - expect(castPollVoteStub.calledWith(messageId, pollResponse.id, { option_id })).to.be.true; + expect(castPollVoteStub.calledWith(messageId, pollResponse.id, { option_id })).to.be + .true; removePollVoteStub.restore(); castPollVoteStub.restore(); }); it('should not remove oldest vote before casting a new one if max_votes_allowed is not defined', async () => { - const poll = new Poll({ client, poll: { ...pollResponse, max_votes_allowed: undefined } }); + const poll = new Poll({ + client, + poll: { ...pollResponse, max_votes_allowed: undefined }, + }); const option_id = 'ba933470-c0da-4b6f-a4d2-d2176ac0d4a8'; const messageId = 'XXX'; const removePollVoteStub = sinon.stub(client, 'removePollVote'); @@ -695,7 +759,8 @@ describe('Poll', () => { await poll.castVote(option_id, messageId); expect(removePollVoteStub.called).to.be.false; - expect(castPollVoteStub.calledWith(messageId, pollResponse.id, { option_id })).to.be.true; + expect(castPollVoteStub.calledWith(messageId, pollResponse.id, { option_id })).to.be + .true; removePollVoteStub.restore(); castPollVoteStub.restore(); }); diff --git a/test/unit/poll_manager.test.ts b/test/unit/poll_manager.test.ts index 767b37e334..99d484d969 100644 --- a/test/unit/poll_manager.test.ts +++ b/test/unit/poll_manager.test.ts @@ -1,4 +1,3 @@ -import { expect } from 'chai'; import { v4 as uuidv4 } from 'uuid'; import { generateChannel } from './test-utils/generateChannel'; @@ -16,6 +15,8 @@ import { StreamChat, } from '../../src'; +import { describe, beforeEach, afterEach, it, expect } from 'vitest'; + const TEST_USER_ID = 'observer'; let client: StreamChat; @@ -196,13 +197,18 @@ describe('PollManager', () => { let pollMessages: MessageResponse[] = []; for (let ci = 0; ci < 5; ci++) { - const { messages, pollMessages: onlyPollMessages } = generateRandomMessagesWithPolls(5, `_${ci}`); + const { messages, pollMessages: onlyPollMessages } = + generateRandomMessagesWithPolls(5, `_${ci}`); pollMessages = pollMessages.concat(onlyPollMessages); - mockedChannelsQueryResponse.push(generateChannel({ channel: { id: uuidv4() }, messages })); + mockedChannelsQueryResponse.push( + generateChannel({ channel: { id: uuidv4() }, messages }), + ); } const mock = sinon.mock(client); const spy = sinon.spy(client.polls, 'hydratePollCache'); - mock.expects('post').returns(Promise.resolve({ channels: mockedChannelsQueryResponse })); + mock + .expects('post') + .returns(Promise.resolve({ channels: mockedChannelsQueryResponse })); await client.queryChannels({}); expect(client.polls.data.size).to.equal(pollMessages.length); expect(spy.callCount).to.be.equal(5); @@ -216,12 +222,13 @@ describe('PollManager', () => { let pollMessages: MessageResponse[] = []; const spy = sinon.spy(client.polls, 'hydratePollCache'); for (let ci = 0; ci < 5; ci++) { - const { messages: prevMessages, pollMessages: prevPollMessages } = generateRandomMessagesWithPolls( - 5, - `_prev_${ci}`, - ); + const { messages: prevMessages, pollMessages: prevPollMessages } = + generateRandomMessagesWithPolls(5, `_prev_${ci}`); pollMessages = pollMessages.concat(prevPollMessages); - const channelResponse = generateChannel({ channel: { id: uuidv4() }, messages: prevMessages }); + const channelResponse = generateChannel({ + channel: { id: uuidv4() }, + messages: prevMessages, + }); channels.push(channelResponse); client.channel(channelResponse.channel.type, channelResponse.channel.id); client.polls.hydratePollCache(prevMessages, true); @@ -229,20 +236,28 @@ describe('PollManager', () => { const mockedChannelsQueryResponse = []; for (let ci = 0; ci < 5; ci++) { - const { messages, pollMessages: onlyPollMessages } = generateRandomMessagesWithPolls(5, `_${ci}`); + const { messages, pollMessages: onlyPollMessages } = + generateRandomMessagesWithPolls(5, `_${ci}`); pollMessages = pollMessages.concat(onlyPollMessages); const channelResponse = { ...channels[ci], messages }; mockedChannelsQueryResponse.push(channelResponse); } const mock = sinon.mock(client); - mock.expects('post').returns(Promise.resolve({ channels: mockedChannelsQueryResponse })); + mock + .expects('post') + .returns(Promise.resolve({ channels: mockedChannelsQueryResponse })); await client.queryChannels({}); expect(client.polls.data.size).to.equal(pollMessages.length); expect(spy.callCount).to.be.equal(10); for (let i = 0; i < 5; i++) { expect(spy.calledWith(mockedChannelsQueryResponse[i].messages, true)).to.be.true; expect(spy.calledWith(channels[i].messages, true)).to.be.true; - expect(spy.calledWith([...channels[i].messages, ...mockedChannelsQueryResponse[i].messages], true)).to.be.false; + expect( + spy.calledWith( + [...channels[i].messages, ...mockedChannelsQueryResponse[i].messages], + true, + ), + ).to.be.false; } }); @@ -264,7 +279,8 @@ describe('PollManager', () => { it('populates pollCache with only new messages on channel.query invocation', async () => { const channel = client.channel('messaging', mockChannelQueryResponse.channel.id); - const { messages: prevMessages, pollMessages: prevPollMessages } = generateRandomMessagesWithPolls(5, `_prev`); + const { messages: prevMessages, pollMessages: prevPollMessages } = + generateRandomMessagesWithPolls(5, `_prev`); channel.state.addMessagesSorted(prevMessages); const { messages, pollMessages } = generateRandomMessagesWithPolls(5, ``); const mockedChannelQueryResponse = { @@ -276,7 +292,9 @@ describe('PollManager', () => { mock.expects('post').returns(Promise.resolve(mockedChannelQueryResponse)); client.polls.hydratePollCache(prevMessages); await channel.query(); - expect(client.polls.data.size).to.equal(prevPollMessages.length + pollMessages.length); + expect(client.polls.data.size).to.equal( + prevPollMessages.length + pollMessages.length, + ); expect(spy.calledTwice).to.be.true; expect(spy.args[0][0]).to.deep.equal(prevMessages); expect(spy.args[1][0]).to.deep.equal(mockedChannelQueryResponse.messages); @@ -288,7 +306,8 @@ describe('PollManager', () => { let pollMessages: MessageResponse[] = []; for (let ci = 0; ci < 2; ci++) { - const { messages, pollMessages: onlyPollMessages } = generateRandomMessagesWithPolls(5, `_${ci}`); + const { messages, pollMessages: onlyPollMessages } = + generateRandomMessagesWithPolls(5, `_${ci}`); pollMessages = pollMessages.concat(onlyPollMessages); mockedChannelsQueryResponse.push({ ...mockChannelQueryResponse, @@ -300,7 +319,9 @@ describe('PollManager', () => { expect(client.polls.data.size).to.equal(pollMessages.length); // Map.prototype.keys() preserves the insertion order so we can do this - expect(Array.from(client.polls.data.keys())).to.deep.equal(pollMessages.map((m) => m.poll_id)); + expect(Array.from(client.polls.data.keys())).to.deep.equal( + pollMessages.map((m) => m.poll_id), + ); }); it('prevents pollCache population if caching is disabled', async () => { @@ -309,7 +330,8 @@ describe('PollManager', () => { let pollMessages: MessageResponse[] = []; for (let ci = 0; ci < 2; ci++) { - const { messages, pollMessages: onlyPollMessages } = generateRandomMessagesWithPolls(5, `_${ci}`); + const { messages, pollMessages: onlyPollMessages } = + generateRandomMessagesWithPolls(5, `_${ci}`); pollMessages = pollMessages.concat(onlyPollMessages); mockedChannelsQueryResponse.push({ ...mockChannelQueryResponse, @@ -329,7 +351,11 @@ describe('PollManager', () => { user: { id: 'bob' }, }); - const pollMessage = generatePollMessage('poll_from_event', {}, { user: { id: 'bob' } }); + const pollMessage = generatePollMessage( + 'poll_from_event', + {}, + { user: { id: 'bob' } }, + ); client.dispatchEvent({ type: 'message.new', @@ -357,7 +383,9 @@ describe('PollManager', () => { pollManager.hydratePollCache(messages); expect(pollManager.data.size).to.equal(pollMessages.length); - expect(Array.from(pollManager.data.keys())).to.deep.equal(pollMessages.map((m) => m.poll_id)); + expect(Array.from(pollManager.data.keys())).to.deep.equal( + pollMessages.map((m) => m.poll_id), + ); }); it('correctly upserts duplicate polls within the cache', () => { @@ -374,12 +402,16 @@ describe('PollManager', () => { expect(Array.from(pollManager.data.keys())).to.deep.equal( [...pollMessages, duplicatePollMessage].map((m) => m.poll_id), ); - expect(pollManager.fromState(duplicateId)?.data.name).to.equal(duplicatePollMessage.poll.name); + expect(pollManager.fromState(duplicateId)?.data.name).to.equal( + duplicatePollMessage.poll.name, + ); // many duplicate messages const duplicates = []; for (let di = 0; di < 5; di++) { - const newDuplicateMessage = generatePollMessage(duplicateId, { name: `d1_${di}` }); + const newDuplicateMessage = generatePollMessage(duplicateId, { + name: `d1_${di}`, + }); duplicates.push(newDuplicateMessage); } @@ -403,7 +435,9 @@ describe('PollManager', () => { // many hydrate invocations for (let di = 0; di < 5; di++) { - const newDuplicateMessage = generatePollMessage(duplicateId, { name: `d2_${di}` }); + const newDuplicateMessage = generatePollMessage(duplicateId, { + name: `d2_${di}`, + }); pollManager.hydratePollCache([newDuplicateMessage], true); } @@ -429,7 +463,10 @@ describe('PollManager', () => { it('should not register subscription handlers twice', () => { pollManager.registerSubscriptions(); - const pollClosedStub = sinon.stub(pollManager.fromState(pollId1) as Poll, 'handlePollClosed'); + const pollClosedStub = sinon.stub( + pollManager.fromState(pollId1) as Poll, + 'handlePollClosed', + ); client.dispatchEvent({ type: 'poll.closed', @@ -442,8 +479,14 @@ describe('PollManager', () => { it('should not call subscription handlers if unregisterSubscriptions has been called', () => { pollManager.unregisterSubscriptions(); - const voteCastedStub = sinon.stub(pollManager.fromState(pollId1) as Poll, 'handleVoteCasted'); - const pollClosedStub = sinon.stub(pollManager.fromState(pollId1) as Poll, 'handlePollClosed'); + const voteCastedStub = sinon.stub( + pollManager.fromState(pollId1) as Poll, + 'handleVoteCasted', + ); + const pollClosedStub = sinon.stub( + pollManager.fromState(pollId1) as Poll, + 'handlePollClosed', + ); const poll = pollMessage1.poll as PollResponse; @@ -488,8 +531,14 @@ describe('PollManager', () => { eventHandlerPairs.map(([eventType, handlerName]) => { it(`should invoke poll.${handlerName} within the cache on ${eventType}`, () => { - const stub1 = sinon.stub(pollManager.fromState(pollId1) as Poll, handlerName as keyof Poll); - const stub2 = sinon.stub(pollManager.fromState(pollId2) as Poll, handlerName as keyof Poll); + const stub1 = sinon.stub( + pollManager.fromState(pollId1) as Poll, + handlerName as keyof Poll, + ); + const stub2 = sinon.stub( + pollManager.fromState(pollId2) as Poll, + handlerName as keyof Poll, + ); const updatedPoll = pollMessage1.poll as PollResponse; @@ -508,14 +557,21 @@ describe('PollManager', () => { describe('API', () => { const pollId1 = 'poll_1'; const pollId2 = 'poll_2'; - let stubbedQueryPolls: sinon.SinonStub, ReturnType>; - let stubbedGetPoll: sinon.SinonStub, ReturnType>; + let stubbedQueryPolls: sinon.SinonStub< + Parameters, + ReturnType + >; + let stubbedGetPoll: sinon.SinonStub< + Parameters, + ReturnType + >; const pollMessage1: PollResponse = generatePollMessage(pollId1); const pollMessage2: PollResponse = generatePollMessage(pollId2); beforeEach(() => { - stubbedQueryPolls = sinon - .stub(client, 'queryPolls') - .resolves({ polls: [pollMessage1.poll as PollResponse, pollMessage2.poll as PollResponse], duration: '10' }); + stubbedQueryPolls = sinon.stub(client, 'queryPolls').resolves({ + polls: [pollMessage1.poll as PollResponse, pollMessage2.poll as PollResponse], + duration: '10', + }); stubbedGetPoll = sinon .stub(client, 'getPoll') .resolves({ poll: pollMessage1.poll as PollResponse, duration: '10' }); @@ -543,7 +599,9 @@ describe('PollManager', () => { }); }); it('should overwrite the state if polls from queryPolls are already present in the cache', async () => { - const duplicatePollMessage = generatePollMessage(pollId1, { title: 'SHOULD CHANGE' }); + const duplicatePollMessage = generatePollMessage(pollId1, { + title: 'SHOULD CHANGE', + }); pollManager.hydratePollCache([duplicatePollMessage]); const previousPollFromCache = pollManager.fromState(pollId1); @@ -575,7 +633,9 @@ describe('PollManager', () => { expect(pollManager.fromState(pollId1)).to.equal(poll); }); it('should overwrite the state if the poll returned from getPoll is present in the cache', async () => { - const duplicatePollMessage = generatePollMessage(pollId1, { title: 'SHOULD CHANGE' }); + const duplicatePollMessage = generatePollMessage(pollId1, { + title: 'SHOULD CHANGE', + }); pollManager.hydratePollCache([duplicatePollMessage]); const previousPollFromCache = pollManager.fromState(pollId1); diff --git a/test/unit/search_controller.test.js b/test/unit/search_controller.test.js index 7a7d79be0d..b5da8d5cde 100644 --- a/test/unit/search_controller.test.js +++ b/test/unit/search_controller.test.js @@ -1,4 +1,3 @@ -import { expect } from 'chai'; import sinon from 'sinon'; import { BaseSearchSource, @@ -10,6 +9,8 @@ import { import { generateUser } from './test-utils/generateUser'; import { generateChannel } from './test-utils/generateChannel'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; + describe('SearchController', () => { let searchController; let mockSource; @@ -328,10 +329,12 @@ describe('BaseSearchSource and implementations', () => { beforeEach(() => { // Stub executeQuery on the prototype before creating the instance to avoid effect of binding in constructor - executeQueryStub = sinon.stub(BaseSearchSource.prototype, 'executeQuery').resolves({ - items, - next: null, - }); + executeQueryStub = sinon + .stub(BaseSearchSource.prototype, 'executeQuery') + .resolves({ + items, + next: null, + }); searchSource = new TestSearchSource(); }); diff --git a/test/unit/signing.js b/test/unit/signing.test.js similarity index 77% rename from test/unit/signing.js rename to test/unit/signing.test.js index 167636e772..17300579a5 100644 --- a/test/unit/signing.js +++ b/test/unit/signing.test.js @@ -1,11 +1,14 @@ -import { expect } from 'chai'; import { CheckSignature } from '../../src'; +import { describe, it, expect } from 'vitest'; + const MOCK_SECRET = 'porewqKAFDSAKZssecretsercretfads'; const MOCK_TEXT = 'text'; const MOCK_JSON_BODY = { a: 1 }; -const MOCK_TEXT_SHA256 = 'd0b770e93a56adc3ee9ac5734533cc0acd71eea8e5e8204a28042ca0f60de1f3'; -const MOCK_JSON_SHA256 = 'e527a6ad4993a4c9a30680c8be4b3eda1c36ab104f1f7d39c744bd27016a9624'; +const MOCK_TEXT_SHA256 = + 'd0b770e93a56adc3ee9ac5734533cc0acd71eea8e5e8204a28042ca0f60de1f3'; +const MOCK_JSON_SHA256 = + 'e527a6ad4993a4c9a30680c8be4b3eda1c36ab104f1f7d39c744bd27016a9624'; describe('Signing', () => { describe('CheckSignature', () => { diff --git a/test/unit/test-utils/generateChannel.js b/test/unit/test-utils/generateChannel.js index d056b139d3..531b438781 100644 --- a/test/unit/test-utils/generateChannel.js +++ b/test/unit/test-utils/generateChannel.js @@ -7,8 +7,8 @@ export const generateChannel = (options = { channel: {} }) => { const id = idFromOptions ? idFromOptions : options.members && options.members.length - ? `!members-${uuidv4()}` - : uuidv4(); + ? `!members-${uuidv4()}` + : uuidv4(); return { messages: [], members: [], diff --git a/test/unit/test-utils/generateThreadResponse.js b/test/unit/test-utils/generateThreadResponse.js index c21f076145..2866f6542c 100644 --- a/test/unit/test-utils/generateThreadResponse.js +++ b/test/unit/test-utils/generateThreadResponse.js @@ -1,6 +1,3 @@ -import { v4 as uuidv4 } from 'uuid'; -import { generateUser } from './generateUser'; - export const generateThreadResponse = (channel, parent, opts = {}) => { return { parent_message_id: parent.id, diff --git a/test/unit/test-utils/getClient.js b/test/unit/test-utils/getClient.js index 623fc255b6..a20c4bb1f5 100644 --- a/test/unit/test-utils/getClient.js +++ b/test/unit/test-utils/getClient.js @@ -1,20 +1,21 @@ -import { ChannelState, StreamChat, Channel } from '../../../src'; +import { StreamChat } from '../../../src'; import { v4 as uuidv4 } from 'uuid'; -export const getClientWithUser = async (user) => { +export const getClientWithUser = (user) => { const chatClient = new StreamChat(''); const clientUser = user || { id: uuidv4() }; + chatClient.connectUser = () => { chatClient.user = clientUser; chatClient.userID = clientUser.id; chatClient.wsPromise = Promise.resolve(); // sending a promise, since connectUser in actual SDK is an async function. - return Promise.resolve(chatClient); + return chatClient; }; - await chatClient.connectUser(); + chatClient.connectUser(); return chatClient; }; diff --git a/test/unit/threads.test.ts b/test/unit/threads.test.ts index 1becb85553..553424f46e 100644 --- a/test/unit/threads.test.ts +++ b/test/unit/threads.test.ts @@ -1,4 +1,3 @@ -import { expect } from 'chai'; import { v4 as uuidv4 } from 'uuid'; import { generateChannel } from './test-utils/generateChannel'; @@ -19,6 +18,8 @@ import { } from '../../src'; import { THREAD_RESPONSE_RESERVED_KEYS } from '../../src/thread'; +import { describe, it, beforeEach, expect, afterEach } from 'vitest'; + const TEST_USER_ID = 'observer'; describe('Threads 2.0', () => { @@ -59,7 +60,10 @@ describe('Threads 2.0', () => { describe('Thread', () => { it('initializes properly', () => { - const threadResponse = generateThreadResponse(channelResponse, parentMessageResponse); + const threadResponse = generateThreadResponse( + channelResponse, + parentMessageResponse, + ); // mimic pre-cached channel with existing members channel._hydrateMembers({ members: [{ user: { id: TEST_USER_ID } }] }); const thread = new Thread({ client, threadData: threadResponse }); @@ -96,7 +100,10 @@ describe('Threads 2.0', () => { }); it('updates existing message', () => { - const message = generateMsg({ parent_id: parentMessageResponse.id, text: 'aaa' }) as MessageResponse; + const message = generateMsg({ + parent_id: parentMessageResponse.id, + text: 'aaa', + }) as MessageResponse; const thread = createTestThread({ latest_replies: [message] }); const udpatedMessage = { ...message, text: 'bbb' }; @@ -125,7 +132,9 @@ describe('Threads 2.0', () => { created_at: '2020-01-01T00:00:10Z', }) as MessageResponse; - const thread = createTestThread({ latest_replies: [optimisticMessage, message] }); + const thread = createTestThread({ + latest_replies: [optimisticMessage, message], + }); const updatedMessage: MessageResponse = { ...optimisticMessage, text: 'ccc', @@ -185,7 +194,10 @@ describe('Threads 2.0', () => { const thread = createTestThread(); const message = generateMsg({ parent_id: thread.id }) as MessageResponse; const upsertReplyLocallyStub = sinon.stub(thread, 'upsertReplyLocally'); - const updateParentMessageLocallyStub = sinon.stub(thread, 'updateParentMessageLocally'); + const updateParentMessageLocallyStub = sinon.stub( + thread, + 'updateParentMessageLocally', + ); thread.updateParentMessageOrReplyLocally(message); @@ -197,7 +209,10 @@ describe('Threads 2.0', () => { const thread = createTestThread(); const message = generateMsg({ id: thread.id }) as MessageResponse; const upsertReplyLocallyStub = sinon.stub(thread, 'upsertReplyLocally'); - const updateParentMessageLocallyStub = sinon.stub(thread, 'updateParentMessageLocally'); + const updateParentMessageLocallyStub = sinon.stub( + thread, + 'updateParentMessageLocally', + ); thread.updateParentMessageOrReplyLocally(message); @@ -209,7 +224,10 @@ describe('Threads 2.0', () => { const thread = createTestThread(); const message = generateMsg() as MessageResponse; const upsertReplyLocallyStub = sinon.stub(thread, 'upsertReplyLocally'); - const updateParentMessageLocallyStub = sinon.stub(thread, 'updateParentMessageLocally'); + const updateParentMessageLocallyStub = sinon.stub( + thread, + 'updateParentMessageLocally', + ); thread.updateParentMessageOrReplyLocally(message); @@ -221,7 +239,9 @@ describe('Threads 2.0', () => { describe('hydrateState', () => { it('prevents hydrating state from the instance with a different id', () => { const thread = createTestThread(); - const otherThread = createTestThread({ parentMessageOverrides: { id: uuidv4() } }); + const otherThread = createTestThread({ + parentMessageOverrides: { id: uuidv4() }, + }); expect(thread.id).to.not.equal(otherThread.id); expect(() => thread.hydrateState(otherThread)).to.throw(); @@ -245,7 +265,9 @@ describe('Threads 2.0', () => { it('retains failed replies after hydration', () => { const thread = createTestThread(); const hydrationThread = createTestThread({ - latest_replies: [generateMsg({ parent_id: parentMessageResponse.id }) as MessageResponse], + latest_replies: [ + generateMsg({ parent_id: parentMessageResponse.id }) as MessageResponse, + ], }); const failedMessage = generateMsg({ @@ -268,7 +290,10 @@ describe('Threads 2.0', () => { // five messages "created" second apart const messages = Array.from( { length: 5 }, - (_, i) => generateMsg({ created_at: new Date(createdAt + 1000 * i).toISOString() }) as MessageResponse, + (_, i) => + generateMsg({ + created_at: new Date(createdAt + 1000 * i).toISOString(), + }) as MessageResponse, ); const thread = createTestThread({ latest_replies: messages }); @@ -285,12 +310,16 @@ describe('Threads 2.0', () => { const stateAfter = thread.state.getLatestValue(); expect(stateAfter.replies).to.not.equal(stateBefore.replies); expect(stateAfter.replies).to.have.lengthOf(4); - expect(stateAfter.replies.find((reply) => reply.id === messageToDelete.id)).to.be.undefined; + expect(stateAfter.replies.find((reply) => reply.id === messageToDelete.id)).to + .be.undefined; }); }); describe('markAsRead', () => { - let stubbedChannelMarkRead: sinon.SinonStub, ReturnType>; + let stubbedChannelMarkRead: sinon.SinonStub< + Parameters, + ReturnType + >; beforeEach(() => { stubbedChannelMarkRead = sinon.stub(channel, 'markRead').resolves(); @@ -320,13 +349,17 @@ describe('Threads 2.0', () => { await thread.markAsRead(); - expect(stubbedChannelMarkRead.calledOnceWith({ thread_id: thread.id })).to.be.true; + expect(stubbedChannelMarkRead.calledOnceWith({ thread_id: thread.id })).to.be + .true; }); }); describe('loadPage', () => { it('sets up pagination on initialization (all replies included in response)', () => { - const thread = createTestThread({ latest_replies: [generateMsg() as MessageResponse], reply_count: 1 }); + const thread = createTestThread({ + latest_replies: [generateMsg() as MessageResponse], + reply_count: 1, + }); const state = thread.state.getLatestValue(); expect(state.pagination.prevCursor).to.be.null; expect(state.pagination.nextCursor).to.be.null; @@ -335,7 +368,10 @@ describe('Threads 2.0', () => { it('sets up pagination on initialization (not all replies included in response)', () => { const firstMessage = generateMsg() as MessageResponse; const lastMessage = generateMsg() as MessageResponse; - const thread = createTestThread({ latest_replies: [firstMessage, lastMessage], reply_count: 3 }); + const thread = createTestThread({ + latest_replies: [firstMessage, lastMessage], + reply_count: 3, + }); const state = thread.state.getLatestValue(); expect(state.pagination.prevCursor).not.to.be.null; expect(state.pagination.nextCursor).to.be.null; @@ -391,7 +427,10 @@ describe('Threads 2.0', () => { it('forms correct request when loading next page', async () => { const firstMessage = generateMsg() as MessageResponse; const lastMessage = generateMsg() as MessageResponse; - const thread = createTestThread({ latest_replies: [firstMessage, lastMessage], reply_count: 3 }); + const thread = createTestThread({ + latest_replies: [firstMessage, lastMessage], + reply_count: 3, + }); thread.state.next((current) => ({ ...current, pagination: { @@ -399,7 +438,9 @@ describe('Threads 2.0', () => { nextCursor: lastMessage.id, }, })); - const queryRepliesStub = sinon.stub(thread, 'queryReplies').resolves({ messages: [], duration: '' }); + const queryRepliesStub = sinon + .stub(thread, 'queryReplies') + .resolves({ messages: [], duration: '' }); await thread.loadNextPage({ limit: 42 }); @@ -447,8 +488,13 @@ describe('Threads 2.0', () => { it('forms correct request when loading previous page', async () => { const firstMessage = generateMsg() as MessageResponse; const lastMessage = generateMsg() as MessageResponse; - const thread = createTestThread({ latest_replies: [firstMessage, lastMessage], reply_count: 3 }); - const queryRepliesStub = sinon.stub(thread, 'queryReplies').resolves({ messages: [], duration: '' }); + const thread = createTestThread({ + latest_replies: [firstMessage, lastMessage], + reply_count: 3, + }); + const queryRepliesStub = sinon + .stub(thread, 'queryReplies') + .resolves({ messages: [], duration: '' }); await thread.loadPrevPage({ limit: 42 }); @@ -463,7 +509,10 @@ describe('Threads 2.0', () => { it('appends messages when loading next page', async () => { const initialMessages = [generateMsg(), generateMsg()] as MessageResponse[]; const nextMessages = [generateMsg(), generateMsg()] as MessageResponse[]; - const thread = createTestThread({ latest_replies: initialMessages, reply_count: 4 }); + const thread = createTestThread({ + latest_replies: initialMessages, + reply_count: 4, + }); thread.state.next((current) => ({ ...current, pagination: { @@ -471,12 +520,16 @@ describe('Threads 2.0', () => { nextCursor: initialMessages[1].id, }, })); - sinon.stub(thread, 'queryReplies').resolves({ messages: nextMessages, duration: '' }); + sinon + .stub(thread, 'queryReplies') + .resolves({ messages: nextMessages, duration: '' }); await thread.loadNextPage({ limit: 2 }); const stateAfter = thread.state.getLatestValue(); - const expectedMessageOrder = [...initialMessages, ...nextMessages].map(({ id }) => id).join(', '); + const expectedMessageOrder = [...initialMessages, ...nextMessages] + .map(({ id }) => id) + .join(', '); const actualMessageOrder = stateAfter.replies.map(({ id }) => id).join(', '); expect(actualMessageOrder).to.equal(expectedMessageOrder); }); @@ -484,13 +537,20 @@ describe('Threads 2.0', () => { it('prepends messages when loading previous page', async () => { const initialMessages = [generateMsg(), generateMsg()] as MessageResponse[]; const prevMessages = [generateMsg(), generateMsg()] as MessageResponse[]; - const thread = createTestThread({ latest_replies: initialMessages, reply_count: 4 }); - sinon.stub(thread, 'queryReplies').resolves({ messages: prevMessages, duration: '' }); + const thread = createTestThread({ + latest_replies: initialMessages, + reply_count: 4, + }); + sinon + .stub(thread, 'queryReplies') + .resolves({ messages: prevMessages, duration: '' }); await thread.loadPrevPage({ limit: 2 }); const stateAfter = thread.state.getLatestValue(); - const expectedMessageOrder = [...prevMessages, ...initialMessages].map(({ id }) => id).join(', '); + const expectedMessageOrder = [...prevMessages, ...initialMessages] + .map(({ id }) => id) + .join(', '); const actualMessageOrder = stateAfter.replies.map(({ id }) => id).join(', '); expect(actualMessageOrder).to.equal(expectedMessageOrder); }); @@ -527,7 +587,10 @@ describe('Threads 2.0', () => { client.dispatchEvent({ type: 'message.new', - message: generateMsg({ parent_id: thread.id, user: { id: 'bob' } }) as MessageResponse, + message: generateMsg({ + parent_id: thread.id, + user: { id: 'bob' }, + }) as MessageResponse, user: { id: 'bob' }, }); clock.runAll(); @@ -545,7 +608,9 @@ describe('Threads 2.0', () => { const stateBefore = thread.state.getLatestValue(); const stubbedGetThread = sinon .stub(client, 'getThread') - .resolves(createTestThread({ latest_replies: [generateMsg() as MessageResponse] })); + .resolves( + createTestThread({ latest_replies: [generateMsg() as MessageResponse] }), + ); thread.state.partialNext({ isStateStale: true }); @@ -572,7 +637,9 @@ describe('Threads 2.0', () => { client.dispatchEvent({ type: 'thread.updated', - thread: generateThreadResponse(channelResponse, generateMsg(), { title: 'B' }), + thread: generateThreadResponse(channelResponse, generateMsg(), { + title: 'B', + }), }); const stateAfter = thread.state.getLatestValue(); @@ -588,9 +655,13 @@ describe('Threads 2.0', () => { client.dispatchEvent({ type: 'thread.updated', - thread: generateThreadResponse(channelResponse, generateMsg({ id: parentMessageResponse.id }), { - title: 'B', - }), + thread: generateThreadResponse( + channelResponse, + generateMsg({ id: parentMessageResponse.id }), + { + title: 'B', + }, + ), }); const stateAfter = thread.state.getLatestValue(); @@ -606,20 +677,28 @@ describe('Threads 2.0', () => { const stateBefore = thread.state.getLatestValue(); - expect(stateBefore.custom).to.not.have.keys(Object.keys(THREAD_RESPONSE_RESERVED_KEYS)); + expect(stateBefore.custom).to.not.have.keys( + Object.keys(THREAD_RESPONSE_RESERVED_KEYS), + ); expect(stateBefore.custom).to.have.keys([customKey1, customKey2]); expect(stateBefore.custom[customKey1]).to.equal(1); client.dispatchEvent({ type: 'thread.updated', - thread: generateThreadResponse(channelResponse, generateMsg({ id: parentMessageResponse.id }), { - [customKey1]: 2, - }), + thread: generateThreadResponse( + channelResponse, + generateMsg({ id: parentMessageResponse.id }), + { + [customKey1]: 2, + }, + ), }); const stateAfter = thread.state.getLatestValue(); - expect(stateAfter.custom).to.not.have.keys(Object.keys(THREAD_RESPONSE_RESERVED_KEYS)); + expect(stateAfter.custom).to.not.have.keys( + Object.keys(THREAD_RESPONSE_RESERVED_KEYS), + ); expect(stateAfter.custom).to.not.have.property(customKey2); expect(stateAfter.custom[customKey1]).to.equal(2); }); @@ -684,7 +763,10 @@ describe('Threads 2.0', () => { client.dispatchEvent({ type: 'message.read', user: { id: 'bob' }, - thread: generateThreadResponse(channelResponse, generateMsg()) as ThreadResponse, + thread: generateThreadResponse( + channelResponse, + generateMsg(), + ) as ThreadResponse, }); const stateAfter = thread.state.getLatestValue(); @@ -721,7 +803,9 @@ describe('Threads 2.0', () => { const stateAfter = thread.state.getLatestValue(); expect(stateAfter.read['bob']?.unreadMessageCount).to.equal(0); - expect(stateAfter.read['bob']?.lastReadAt.toISOString()).to.equal(createdAt.toISOString()); + expect(stateAfter.read['bob']?.lastReadAt.toISOString()).to.equal( + createdAt.toISOString(), + ); thread.unregisterSubscriptions(); }); @@ -775,7 +859,10 @@ describe('Threads 2.0', () => { }); thread.registerSubscriptions(); - const newMessage = generateMsg({ parent_id: thread.id, user: { id: 'bob' } }) as MessageResponse; + const newMessage = generateMsg({ + parent_id: thread.id, + user: { id: 'bob' }, + }) as MessageResponse; client.dispatchEvent({ type: 'message.new', message: newMessage, @@ -784,7 +871,8 @@ describe('Threads 2.0', () => { const stateAfter = thread.state.getLatestValue(); expect(stateAfter.replies).to.have.length(1); - expect(stateAfter.replies.find((reply) => reply.id === newMessage.id)).not.to.be.undefined; + expect(stateAfter.replies.find((reply) => reply.id === newMessage.id)).not.to.be + .undefined; expect(thread.ownUnreadCount).to.equal(1); thread.unregisterSubscriptions(); @@ -903,7 +991,8 @@ describe('Threads 2.0', () => { const stateAfter = thread.state.getLatestValue(); expect(stateAfter.replies).to.have.lengthOf(4); - expect(stateAfter.replies.find((reply) => reply.id === messageToDelete.id)).to.be.undefined; + expect(stateAfter.replies.find((reply) => reply.id === messageToDelete.id)).to + .be.undefined; thread.unregisterSubscriptions(); }); @@ -929,7 +1018,11 @@ describe('Threads 2.0', () => { const deletedAt = new Date(); client.dispatchEvent({ type: 'message.deleted', - message: { ...messageToDelete, type: 'deleted', deleted_at: deletedAt.toISOString() }, + message: { + ...messageToDelete, + type: 'deleted', + deleted_at: deletedAt.toISOString(), + }, }); const stateAfter = thread.state.getLatestValue(); @@ -937,7 +1030,9 @@ describe('Threads 2.0', () => { expect(stateAfter.replies[2].id).to.equal(messageToDelete.id); expect(stateAfter.replies[2]).to.not.equal(messageToDelete); expect(stateAfter.replies[2].deleted_at).to.be.a('date'); - expect(stateAfter.replies[2].deleted_at!.toISOString()).to.equal(deletedAt.toISOString()); + expect(stateAfter.replies[2].deleted_at!.toISOString()).to.equal( + deletedAt.toISOString(), + ); expect(stateAfter.replies[2].type).to.equal('deleted'); thread.unregisterSubscriptions(); @@ -965,15 +1060,27 @@ describe('Threads 2.0', () => { expect(stateAfter.deletedAt).to.be.a('date'); expect(stateAfter.deletedAt!.toISOString()).to.equal(parentMessage.deleted_at); expect(stateAfter.parentMessage.deleted_at).to.be.a('date'); - expect(stateAfter.parentMessage.deleted_at!.toISOString()).to.equal(parentMessage.deleted_at); + expect(stateAfter.parentMessage.deleted_at!.toISOString()).to.equal( + parentMessage.deleted_at, + ); }); }); describe('Events: message.updated, reaction.new, reaction.deleted', () => { - (['message.updated', 'reaction.new', 'reaction.deleted', 'reaction.updated'] as const).forEach((eventType) => { + ( + [ + 'message.updated', + 'reaction.new', + 'reaction.deleted', + 'reaction.updated', + ] as const + ).forEach((eventType) => { it(`updates reply or parent message on "${eventType}"`, () => { const thread = createTestThread(); - const updateParentMessageOrReplyLocallySpy = sinon.spy(thread, 'updateParentMessageOrReplyLocally'); + const updateParentMessageOrReplyLocallySpy = sinon.spy( + thread, + 'updateParentMessageOrReplyLocally', + ); thread.registerSubscriptions(); client.dispatchEvent({ @@ -1009,7 +1116,9 @@ describe('Threads 2.0', () => { expect(threadManager.state.getLatestValue().threads).to.have.lengthOf(2); expect(threadManager.state.getLatestValue().unseenThreadIds).to.have.lengthOf(2); threadManager.resetState(); - expect(threadManager.state.getLatestValue()).to.be.deep.equal(THREAD_MANAGER_INITIAL_STATE); + expect(threadManager.state.getLatestValue()).to.be.deep.equal( + THREAD_MANAGER_INITIAL_STATE, + ); }); }); @@ -1027,7 +1136,9 @@ describe('Threads 2.0', () => { await clientWithUser.disconnectUser(); expect(clientWithUser.threads.state.getLatestValue().threads).to.have.lengthOf(0); - expect(clientWithUser.threads.state.getLatestValue().unseenThreadIds).to.have.lengthOf(0); + expect( + clientWithUser.threads.state.getLatestValue().unseenThreadIds, + ).to.have.lengthOf(0); }); describe('Subscription and Event Handlers', () => { @@ -1040,12 +1151,14 @@ describe('Threads 2.0', () => { sinon.restore(); }); - ([ - ['health.check', 2], - ['notification.mark_read', 1], - ['notification.thread_message_new', 8], - ['notification.channel_deleted', 11], - ] as const).forEach(([eventType, expectedUnreadCount]) => { + ( + [ + ['health.check', 2], + ['notification.mark_read', 1], + ['notification.thread_message_new', 8], + ['notification.channel_deleted', 11], + ] as const + ).forEach(([eventType, expectedUnreadCount]) => { it(`updates unread thread count on "${eventType}"`, () => { client.dispatchEvent({ type: eventType, @@ -1099,7 +1212,9 @@ describe('Threads 2.0', () => { message: generateMsg({ parent_id: uuidv4() }) as MessageResponse, }); - expect(threadManager.state.getLatestValue().unseenThreadIds).to.have.lengthOf(1); + expect(threadManager.state.getLatestValue().unseenThreadIds).to.have.lengthOf( + 1, + ); }); it('deduplicates unseen threads', () => { @@ -1118,7 +1233,9 @@ describe('Threads 2.0', () => { message: generateMsg({ parent_id: parentMessageId }) as MessageResponse, }); - expect(threadManager.state.getLatestValue().unseenThreadIds).to.have.lengthOf(1); + expect(threadManager.state.getLatestValue().unseenThreadIds).to.have.lengthOf( + 1, + ); }); it('tracks thread order becoming stale', () => { @@ -1184,9 +1301,12 @@ describe('Threads 2.0', () => { const unregisterSubscriptionsSpy = sinon.spy(thread, 'unregisterSubscriptions'); return [thread, registerSubscriptionsSpy, unregisterSubscriptionsSpy] as const; }; - const [thread1, registerThread1, unregisterThread1] = createTestThreadAndSpySubscriptions(); - const [thread2, registerThread2, unregisterThread2] = createTestThreadAndSpySubscriptions(); - const [thread3, registerThread3, unregisterThread3] = createTestThreadAndSpySubscriptions(); + const [thread1, registerThread1, unregisterThread1] = + createTestThreadAndSpySubscriptions(); + const [thread2, registerThread2, unregisterThread2] = + createTestThreadAndSpySubscriptions(); + const [thread3, registerThread3, unregisterThread3] = + createTestThreadAndSpySubscriptions(); threadManager.state.partialNext({ threads: [thread1, thread2], @@ -1324,9 +1444,16 @@ describe('Threads 2.0', () => { }); it('reuses existing thread instances', async () => { - const existingThread = createTestThread({ parentMessageOverrides: { id: uuidv4() } }); - const newThread = createTestThread({ parentMessageOverrides: { id: uuidv4() } }); - threadManager.state.partialNext({ threads: [existingThread], unseenThreadIds: [newThread.id] }); + const existingThread = createTestThread({ + parentMessageOverrides: { id: uuidv4() }, + }); + const newThread = createTestThread({ + parentMessageOverrides: { id: uuidv4() }, + }); + threadManager.state.partialNext({ + threads: [existingThread], + unseenThreadIds: [newThread.id], + }); stubbedQueryThreads.resolves({ threads: [newThread, existingThread], next: undefined, @@ -1344,7 +1471,9 @@ describe('Threads 2.0', () => { const existingThread = createTestThread(); existingThread.state.partialNext({ isStateStale: true }); const newThread = createTestThread({ - thread_participants: [{ user_id: 'u1' }] as ThreadResponse['thread_participants'], + thread_participants: [ + { user_id: 'u1' }, + ] as ThreadResponse['thread_participants'], }); threadManager.state.partialNext({ threads: [existingThread], @@ -1365,9 +1494,15 @@ describe('Threads 2.0', () => { }); it('reorders threads according to the response order', async () => { - const existingThread = createTestThread({ parentMessageOverrides: { id: uuidv4() } }); - const newThread1 = createTestThread({ parentMessageOverrides: { id: uuidv4() } }); - const newThread2 = createTestThread({ parentMessageOverrides: { id: uuidv4() } }); + const existingThread = createTestThread({ + parentMessageOverrides: { id: uuidv4() }, + }); + const newThread1 = createTestThread({ + parentMessageOverrides: { id: uuidv4() }, + }); + const newThread2 = createTestThread({ + parentMessageOverrides: { id: uuidv4() }, + }); threadManager.state.partialNext({ threads: [existingThread], unseenThreadIds: [newThread1.id, newThread2.id], @@ -1464,8 +1599,12 @@ describe('Threads 2.0', () => { }); it('updates thread list and pagination', async () => { - const existingThread = createTestThread({ parentMessageOverrides: { id: uuidv4() } }); - const newThread = createTestThread({ parentMessageOverrides: { id: uuidv4() } }); + const existingThread = createTestThread({ + parentMessageOverrides: { id: uuidv4() }, + }); + const newThread = createTestThread({ + parentMessageOverrides: { id: uuidv4() }, + }); threadManager.state.next((current) => ({ ...current, threads: [existingThread], diff --git a/test/unit/utils.js b/test/unit/utils.test.js similarity index 97% rename from test/unit/utils.js rename to test/unit/utils.test.js index 1a1da8ec0c..92744aa36a 100644 --- a/test/unit/utils.js +++ b/test/unit/utils.test.js @@ -1,4 +1,3 @@ -import chai from 'chai'; import { axiosParamsSerializer, binarySearchByDateEqualOrNearestGreater, @@ -8,9 +7,9 @@ import { } from '../../src/utils'; import sinon from 'sinon'; -const expect = chai.expect; +import { describe, beforeEach, it, expect } from 'vitest'; -describe('generateUUIDv4', () => { +describe.skip('generateUUIDv4', () => { beforeEach(() => { sinon.restore(); }); @@ -168,60 +167,7 @@ describe('messageSetPagination', () => { describe('linear', () => { describe('returned page size size is 0', () => { - ['created_at_after_or_equal', 'created_at_after', 'id_gt', 'id_gte'].forEach((option) => { - it(`requested page size === returned page size === parent set size pagination with option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length, - returnedPage: [], - parentSet: { messages: [], pagination: {} }, - }), - ).to.eql({ hasNext: false }); - }); - it(`requested page size === parent set size > returned page size with option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: 1, - returnedPage: [], - parentSet: { messages: messages.slice(0, 1), pagination: {} }, - }), - ).to.eql({ hasNext: false }); - }); - it(`returned page size === parent set size pagination < requested page size with option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: 1, - returnedPage: [], - parentSet: { messages: [], pagination: {} }, - }), - ).to.eql({ hasNext: false }); - }); - it(`requested page size === returned page size < parent set size pagination with option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length, - returnedPage: [], - parentSet: { messages, pagination: {} }, - }), - ).to.eql({ hasNext: false }); - }); - it(`returned page size < parent set size < requested page size pagination with option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: 1, - returnedPage: [], - parentSet: { messages, pagination: {} }, - }), - ).to.eql({ hasNext: false }); - }); - }); - - ['created_at_before_or_equal', 'created_at_before', 'id_lt', 'id_lte', undefined, 'unrecognized'].forEach( + ['created_at_after_or_equal', 'created_at_after', 'id_gt', 'id_gte'].forEach( (option) => { it(`requested page size === returned page size === parent set size pagination with option ${option}`, () => { expect( @@ -231,7 +177,7 @@ describe('messageSetPagination', () => { returnedPage: [], parentSet: { messages: [], pagination: {} }, }), - ).to.eql({ hasPrev: false }); + ).to.eql({ hasNext: false }); }); it(`requested page size === parent set size > returned page size with option ${option}`, () => { expect( @@ -241,7 +187,7 @@ describe('messageSetPagination', () => { returnedPage: [], parentSet: { messages: messages.slice(0, 1), pagination: {} }, }), - ).to.eql({ hasPrev: false }); + ).to.eql({ hasNext: false }); }); it(`returned page size === parent set size pagination < requested page size with option ${option}`, () => { expect( @@ -251,7 +197,7 @@ describe('messageSetPagination', () => { returnedPage: [], parentSet: { messages: [], pagination: {} }, }), - ).to.eql({ hasPrev: false }); + ).to.eql({ hasNext: false }); }); it(`requested page size === returned page size < parent set size pagination with option ${option}`, () => { expect( @@ -261,7 +207,7 @@ describe('messageSetPagination', () => { returnedPage: [], parentSet: { messages, pagination: {} }, }), - ).to.eql({ hasPrev: false }); + ).to.eql({ hasNext: false }); }); it(`returned page size < parent set size < requested page size pagination with option ${option}`, () => { expect( @@ -271,378 +217,106 @@ describe('messageSetPagination', () => { returnedPage: [], parentSet: { messages, pagination: {} }, }), - ).to.eql({ hasPrev: false }); + ).to.eql({ hasNext: false }); }); }, ); - }); - - ['created_at_after_or_equal', 'created_at_after', 'id_gt', 'id_gte'].forEach((option) => { - it(`requested page size === returned page size === parent set size pagination option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length, - returnedPage: messages, - parentSet: { messages, pagination: {} }, - }), - ).to.eql({ hasNext: true }); - }); - - it(`returned page size === parent set size pagination < requested page size option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length + 1, - returnedPage: messages, - parentSet: { messages, pagination: {} }, - }), - ).to.eql({ hasNext: false }); - }); - - it(`returned page size === parent set size pagination > requested page size option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length - 1, - returnedPage: messages, - parentSet: { messages, pagination: {} }, - }), - ).to.eql({ hasNext: true }); - }); - describe('first (oldest) page message matches the first parent set message', () => { - it(`requested page size === returned page size < parent set size pagination option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length - 1, - returnedPage: messages.slice(0, -1), - parentSet: { messages, pagination: {} }, - }), - ).to.eql({}); - }); - it(`requested page size === parent set size > returned page size option ${option}`, () => { + [ + 'created_at_before_or_equal', + 'created_at_before', + 'id_lt', + 'id_lte', + undefined, + 'unrecognized', + ].forEach((option) => { + it(`requested page size === returned page size === parent set size pagination with option ${option}`, () => { expect( messageSetPagination({ messagePaginationOptions: option && { [option]: 'X' }, requestedPageSize: messages.length, - returnedPage: messages.slice(0, -1), - parentSet: { messages, pagination: {} }, - }), - ).to.eql({}); - }); - it(`requested page size < returned page size < parent set size pagination option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length - 2, - returnedPage: messages.slice(0, -1), - parentSet: { messages, pagination: {} }, - }), - ).to.eql({}); - }); - it(`requested page size < returned page size < parent set size pagination option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length - 3, - returnedPage: messages.slice(0, -2), - parentSet: { messages: messages.slice(0, -1), pagination: {} }, + returnedPage: [], + parentSet: { messages: [], pagination: {} }, }), - ).to.eql({}); + ).to.eql({ hasPrev: false }); }); - it(`returned page size < parent set size < requested page size pagination option ${option}`, () => { + it(`requested page size === parent set size > returned page size with option ${option}`, () => { expect( messageSetPagination({ messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length, - returnedPage: messages.slice(0, -2), - parentSet: { messages: messages.slice(0, -1), pagination: {} }, + requestedPageSize: 1, + returnedPage: [], + parentSet: { messages: messages.slice(0, 1), pagination: {} }, }), - ).to.eql({}); + ).to.eql({ hasPrev: false }); }); - it(`requested page size === returned page size > parent set size pagination option ${option}`, () => { - const restore = consoleErrorSpy(); + it(`returned page size === parent set size pagination < requested page size with option ${option}`, () => { expect( messageSetPagination({ messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length, - returnedPage: messages, - parentSet: { messages: messages.slice(0, -1), pagination: {} }, + requestedPageSize: 1, + returnedPage: [], + parentSet: { messages: [], pagination: {} }, }), - ).to.eql({}); - restore(); + ).to.eql({ hasPrev: false }); }); - it(`parent set size < returned page size < requested page size pagination option ${option}`, () => { - const restore = consoleErrorSpy(); + it(`requested page size === returned page size < parent set size pagination with option ${option}`, () => { expect( messageSetPagination({ messagePaginationOptions: option && { [option]: 'X' }, requestedPageSize: messages.length, - returnedPage: messages.slice(0, -1), - parentSet: { messages: messages.slice(0, -2), pagination: {} }, + returnedPage: [], + parentSet: { messages, pagination: {} }, }), - ).to.eql({}); - restore(); + ).to.eql({ hasPrev: false }); }); - it(`requested page size < parent set size < returned page size pagination option ${option}`, () => { - const restore = consoleErrorSpy(); + it(`returned page size < parent set size < requested page size pagination with option ${option}`, () => { expect( messageSetPagination({ messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length - 2, - returnedPage: messages, - parentSet: { messages: messages.slice(0, -1), pagination: {} }, + requestedPageSize: 1, + returnedPage: [], + parentSet: { messages, pagination: {} }, }), - ).to.eql({}); - restore(); + ).to.eql({ hasPrev: false }); }); }); + }); - describe('last page message matches the last parent set message', () => { - it(`requested page size === returned page size < parent set size pagination option ${option}`, () => { + ['created_at_after_or_equal', 'created_at_after', 'id_gt', 'id_gte'].forEach( + (option) => { + it(`requested page size === returned page size === parent set size pagination option ${option}`, () => { expect( messageSetPagination({ messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length - 1, - returnedPage: messages.slice(1), + requestedPageSize: messages.length, + returnedPage: messages, parentSet: { messages, pagination: {} }, }), ).to.eql({ hasNext: true }); }); - it(`requested page size === parent set size > returned page size option ${option}`, () => { + + it(`returned page size === parent set size pagination < requested page size option ${option}`, () => { expect( messageSetPagination({ messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length, - returnedPage: messages.slice(1), + requestedPageSize: messages.length + 1, + returnedPage: messages, parentSet: { messages, pagination: {} }, }), ).to.eql({ hasNext: false }); }); - it(`requested page size < returned page size < parent set size pagination option ${option}`, () => { + + it(`returned page size === parent set size pagination > requested page size option ${option}`, () => { expect( messageSetPagination({ messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length - 2, - returnedPage: messages.slice(1), + requestedPageSize: messages.length - 1, + returnedPage: messages, parentSet: { messages, pagination: {} }, }), ).to.eql({ hasNext: true }); }); - it(`returned page size < parent set size < requested page size pagination option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length, - returnedPage: messages.slice(2), - parentSet: { messages: messages.slice(1), pagination: {} }, - }), - ).to.eql({ hasNext: false }); - }); - it(`requested page size === returned page size > parent set size pagination option ${option}`, () => { - const restore = consoleErrorSpy(); - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length, - returnedPage: messages, - parentSet: { messages: messages.slice(-1), pagination: {} }, - }), - ).to.eql({}); - restore(); - }); - it(`parent set size < returned page size < requested page size pagination option ${option}`, () => { - const restore = consoleErrorSpy(); - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length, - returnedPage: messages.slice(1), - parentSet: { messages: messages.slice(2), pagination: {} }, - }), - ).to.eql({}); - restore(); - }); - it(`requested page size < parent set size < returned page size pagination option ${option}`, () => { - const restore = consoleErrorSpy(); - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length - 2, - returnedPage: messages, - parentSet: { messages: messages.slice(1), pagination: {} }, - }), - ).to.eql({}); - restore(); - }); - }); - - describe('first page message & last page message do not match the first and last parent set messages', () => { - it(`requested page size === returned page size === parent set size pagination option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length, - returnedPage: [ - messages[1], - messages[0], - ...messages.slice(2, -2), - messages.slice(-1)[0], - messages.slice(-2, -1)[0], - ], - parentSet: { messages, pagination: {} }, - }), - ).to.eql({}); - }); - it(`requested page size === parent set size > returned page size option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length, - returnedPage: messages.slice(1, -1), - parentSet: { messages, pagination: {} }, - }), - ).to.eql({}); - }); - it(`returned page size === parent set size pagination < requested page size option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length + 1, - returnedPage: [ - messages[1], - messages[0], - ...messages.slice(2, -2), - messages.slice(-1)[0], - messages.slice(-2, -1)[0], - ], - parentSet: { messages, pagination: {} }, - }), - ).to.eql({}); - }); - - it(`returned page size === parent set size pagination > requested page size option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length - 1, - returnedPage: [ - messages[1], - messages[0], - ...messages.slice(2, -2), - messages.slice(-1)[0], - messages.slice(-2, -1)[0], - ], - parentSet: { messages, pagination: {} }, - }), - ).to.eql({}); - }); - - it(`requested page size === returned page size < parent set size pagination option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length - 2, - returnedPage: messages.slice(1, -1), - parentSet: { messages, pagination: {} }, - }), - ).to.eql({}); - }); - it(`requested page size < returned page size < parent set size pagination option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length - 3, - returnedPage: messages.slice(1, -1), - parentSet: { messages, pagination: {} }, - }), - ).to.eql({}); - }); - it(`returned page size < parent set size < requested page size pagination option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length + 1, - returnedPage: messages.slice(1, -1), - parentSet: { messages, pagination: {} }, - }), - ).to.eql({}); - }); - it(`requested page size === returned page size > parent set size pagination option ${option}`, () => { - const restore = consoleErrorSpy(); - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length, - returnedPage: messages, - parentSet: { messages: messages.slice(1, -1), pagination: {} }, - }), - ).to.eql({}); - restore(); - }); - it(`parent set size < returned page size < requested page size pagination option ${option}`, () => { - const restore = consoleErrorSpy(); - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length, - returnedPage: messages.slice(1), - parentSet: { messages: messages.slice(1, -1), pagination: {} }, - }), - ).to.eql({}); - restore(); - }); - it(`requested page size < parent set size < returned page size pagination option ${option}`, () => { - const restore = consoleErrorSpy(); - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length - 3, - returnedPage: messages, - parentSet: { messages: messages.slice(1, -1), pagination: {} }, - }), - ).to.eql({}); - restore(); - }); - }); - }); - - ['created_at_before_or_equal', 'created_at_before', 'id_lt', 'id_lte', undefined, 'unrecognized'].forEach( - (option) => { - it(`requested page size === returned page size === parent set size pagination option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length, - returnedPage: messages, - parentSet: { messages, pagination: {} }, - }), - ).to.eql({ hasPrev: true }); - }); - - it(`returned page size === parent set size pagination < requested page size option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length + 1, - returnedPage: messages, - parentSet: { messages, pagination: {} }, - }), - ).to.eql({ hasPrev: false }); - }); - - it(`returned page size === parent set size pagination > requested page size option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length - 1, - returnedPage: messages, - parentSet: { messages, pagination: {} }, - }), - ).to.eql({ hasPrev: true }); - }); describe('first (oldest) page message matches the first parent set message', () => { it(`requested page size === returned page size < parent set size pagination option ${option}`, () => { @@ -653,27 +327,27 @@ describe('messageSetPagination', () => { returnedPage: messages.slice(0, -1), parentSet: { messages, pagination: {} }, }), - ).to.eql({ hasPrev: true }); + ).to.eql({}); }); - it(`requested page size < returned page size < parent set size pagination option ${option}`, () => { + it(`requested page size === parent set size > returned page size option ${option}`, () => { expect( messageSetPagination({ messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length - 2, + requestedPageSize: messages.length, returnedPage: messages.slice(0, -1), parentSet: { messages, pagination: {} }, }), - ).to.eql({ hasPrev: true }); + ).to.eql({}); }); - it(`requested page size === parent set size > returned page size option ${option}`, () => { + it(`requested page size < returned page size < parent set size pagination option ${option}`, () => { expect( messageSetPagination({ messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length, + requestedPageSize: messages.length - 2, returnedPage: messages.slice(0, -1), parentSet: { messages, pagination: {} }, }), - ).to.eql({ hasPrev: false }); + ).to.eql({}); }); it(`requested page size < returned page size < parent set size pagination option ${option}`, () => { expect( @@ -683,9 +357,9 @@ describe('messageSetPagination', () => { returnedPage: messages.slice(0, -2), parentSet: { messages: messages.slice(0, -1), pagination: {} }, }), - ).to.eql({ hasPrev: true }); + ).to.eql({}); }); - it(`returned page size < parent set size < requested page size pagination option ${option}`, () => { + it(`returned page size < parent set size < requested page size pagination option ${option}`, () => { expect( messageSetPagination({ messagePaginationOptions: option && { [option]: 'X' }, @@ -693,7 +367,7 @@ describe('messageSetPagination', () => { returnedPage: messages.slice(0, -2), parentSet: { messages: messages.slice(0, -1), pagination: {} }, }), - ).to.eql({ hasPrev: false }); + ).to.eql({}); }); it(`requested page size === returned page size > parent set size pagination option ${option}`, () => { const restore = consoleErrorSpy(); @@ -742,7 +416,7 @@ describe('messageSetPagination', () => { returnedPage: messages.slice(1), parentSet: { messages, pagination: {} }, }), - ).to.eql({}); + ).to.eql({ hasNext: true }); }); it(`requested page size === parent set size > returned page size option ${option}`, () => { expect( @@ -752,7 +426,7 @@ describe('messageSetPagination', () => { returnedPage: messages.slice(1), parentSet: { messages, pagination: {} }, }), - ).to.eql({}); + ).to.eql({ hasNext: false }); }); it(`requested page size < returned page size < parent set size pagination option ${option}`, () => { expect( @@ -762,9 +436,9 @@ describe('messageSetPagination', () => { returnedPage: messages.slice(1), parentSet: { messages, pagination: {} }, }), - ).to.eql({}); + ).to.eql({ hasNext: true }); }); - it(`returned page size < parent set size < requested page size pagination option ${option}`, () => { + it(`returned page size < parent set size < requested page size pagination option ${option}`, () => { expect( messageSetPagination({ messagePaginationOptions: option && { [option]: 'X' }, @@ -772,7 +446,7 @@ describe('messageSetPagination', () => { returnedPage: messages.slice(2), parentSet: { messages: messages.slice(1), pagination: {} }, }), - ).to.eql({}); + ).to.eql({ hasNext: false }); }); it(`requested page size === returned page size > parent set size pagination option ${option}`, () => { const restore = consoleErrorSpy(); @@ -812,136 +486,475 @@ describe('messageSetPagination', () => { }); }); - describe('first page message & last page message do not match the first and last parent set messages', () => { - it(`requested page size === returned page size === parent set size pagination option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length, - returnedPage: [ - messages[1], - messages[0], - ...messages.slice(2, -2), - messages.slice(-1)[0], - messages.slice(-2, -1)[0], - ], - parentSet: { messages, pagination: {} }, - }), - ).to.eql({}); - }); - it(`requested page size === parent set size > returned page size option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length, - returnedPage: messages.slice(1, -1), - parentSet: { messages, pagination: {} }, - }), - ).to.eql({}); - }); - it(`returned page size === parent set size pagination < requested page size option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length + 1, - returnedPage: [ - messages[1], - messages[0], - ...messages.slice(2, -2), - messages.slice(-1)[0], - messages.slice(-2, -1)[0], - ], - parentSet: { messages, pagination: {} }, - }), - ).to.eql({}); - }); - - it(`returned page size === parent set size pagination > requested page size option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length - 1, - returnedPage: [ - messages[1], - messages[0], - ...messages.slice(2, -2), - messages.slice(-1)[0], - messages.slice(-2, -1)[0], - ], - parentSet: { messages, pagination: {} }, - }), - ).to.eql({}); - }); + describe('first page message & last page message do not match the first and last parent set messages', () => { + it(`requested page size === returned page size === parent set size pagination option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length, + returnedPage: [ + messages[1], + messages[0], + ...messages.slice(2, -2), + messages.slice(-1)[0], + messages.slice(-2, -1)[0], + ], + parentSet: { messages, pagination: {} }, + }), + ).to.eql({}); + }); + it(`requested page size === parent set size > returned page size option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length, + returnedPage: messages.slice(1, -1), + parentSet: { messages, pagination: {} }, + }), + ).to.eql({}); + }); + it(`returned page size === parent set size pagination < requested page size option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length + 1, + returnedPage: [ + messages[1], + messages[0], + ...messages.slice(2, -2), + messages.slice(-1)[0], + messages.slice(-2, -1)[0], + ], + parentSet: { messages, pagination: {} }, + }), + ).to.eql({}); + }); + + it(`returned page size === parent set size pagination > requested page size option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length - 1, + returnedPage: [ + messages[1], + messages[0], + ...messages.slice(2, -2), + messages.slice(-1)[0], + messages.slice(-2, -1)[0], + ], + parentSet: { messages, pagination: {} }, + }), + ).to.eql({}); + }); + + it(`requested page size === returned page size < parent set size pagination option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length - 2, + returnedPage: messages.slice(1, -1), + parentSet: { messages, pagination: {} }, + }), + ).to.eql({}); + }); + it(`requested page size < returned page size < parent set size pagination option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length - 3, + returnedPage: messages.slice(1, -1), + parentSet: { messages, pagination: {} }, + }), + ).to.eql({}); + }); + it(`returned page size < parent set size < requested page size pagination option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length + 1, + returnedPage: messages.slice(1, -1), + parentSet: { messages, pagination: {} }, + }), + ).to.eql({}); + }); + it(`requested page size === returned page size > parent set size pagination option ${option}`, () => { + const restore = consoleErrorSpy(); + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length, + returnedPage: messages, + parentSet: { messages: messages.slice(1, -1), pagination: {} }, + }), + ).to.eql({}); + restore(); + }); + it(`parent set size < returned page size < requested page size pagination option ${option}`, () => { + const restore = consoleErrorSpy(); + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length, + returnedPage: messages.slice(1), + parentSet: { messages: messages.slice(1, -1), pagination: {} }, + }), + ).to.eql({}); + restore(); + }); + it(`requested page size < parent set size < returned page size pagination option ${option}`, () => { + const restore = consoleErrorSpy(); + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length - 3, + returnedPage: messages, + parentSet: { messages: messages.slice(1, -1), pagination: {} }, + }), + ).to.eql({}); + restore(); + }); + }); + }, + ); + + [ + 'created_at_before_or_equal', + 'created_at_before', + 'id_lt', + 'id_lte', + undefined, + 'unrecognized', + ].forEach((option) => { + it(`requested page size === returned page size === parent set size pagination option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length, + returnedPage: messages, + parentSet: { messages, pagination: {} }, + }), + ).to.eql({ hasPrev: true }); + }); + + it(`returned page size === parent set size pagination < requested page size option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length + 1, + returnedPage: messages, + parentSet: { messages, pagination: {} }, + }), + ).to.eql({ hasPrev: false }); + }); + + it(`returned page size === parent set size pagination > requested page size option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length - 1, + returnedPage: messages, + parentSet: { messages, pagination: {} }, + }), + ).to.eql({ hasPrev: true }); + }); + + describe('first (oldest) page message matches the first parent set message', () => { + it(`requested page size === returned page size < parent set size pagination option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length - 1, + returnedPage: messages.slice(0, -1), + parentSet: { messages, pagination: {} }, + }), + ).to.eql({ hasPrev: true }); + }); + it(`requested page size < returned page size < parent set size pagination option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length - 2, + returnedPage: messages.slice(0, -1), + parentSet: { messages, pagination: {} }, + }), + ).to.eql({ hasPrev: true }); + }); + it(`requested page size === parent set size > returned page size option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length, + returnedPage: messages.slice(0, -1), + parentSet: { messages, pagination: {} }, + }), + ).to.eql({ hasPrev: false }); + }); + it(`requested page size < returned page size < parent set size pagination option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length - 3, + returnedPage: messages.slice(0, -2), + parentSet: { messages: messages.slice(0, -1), pagination: {} }, + }), + ).to.eql({ hasPrev: true }); + }); + it(`returned page size < parent set size < requested page size pagination option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length, + returnedPage: messages.slice(0, -2), + parentSet: { messages: messages.slice(0, -1), pagination: {} }, + }), + ).to.eql({ hasPrev: false }); + }); + it(`requested page size === returned page size > parent set size pagination option ${option}`, () => { + const restore = consoleErrorSpy(); + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length, + returnedPage: messages, + parentSet: { messages: messages.slice(0, -1), pagination: {} }, + }), + ).to.eql({}); + restore(); + }); + it(`parent set size < returned page size < requested page size pagination option ${option}`, () => { + const restore = consoleErrorSpy(); + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length, + returnedPage: messages.slice(0, -1), + parentSet: { messages: messages.slice(0, -2), pagination: {} }, + }), + ).to.eql({}); + restore(); + }); + it(`requested page size < parent set size < returned page size pagination option ${option}`, () => { + const restore = consoleErrorSpy(); + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length - 2, + returnedPage: messages, + parentSet: { messages: messages.slice(0, -1), pagination: {} }, + }), + ).to.eql({}); + restore(); + }); + }); + + describe('last page message matches the last parent set message', () => { + it(`requested page size === returned page size < parent set size pagination option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length - 1, + returnedPage: messages.slice(1), + parentSet: { messages, pagination: {} }, + }), + ).to.eql({}); + }); + it(`requested page size === parent set size > returned page size option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length, + returnedPage: messages.slice(1), + parentSet: { messages, pagination: {} }, + }), + ).to.eql({}); + }); + it(`requested page size < returned page size < parent set size pagination option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length - 2, + returnedPage: messages.slice(1), + parentSet: { messages, pagination: {} }, + }), + ).to.eql({}); + }); + it(`returned page size < parent set size < requested page size pagination option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length, + returnedPage: messages.slice(2), + parentSet: { messages: messages.slice(1), pagination: {} }, + }), + ).to.eql({}); + }); + it(`requested page size === returned page size > parent set size pagination option ${option}`, () => { + const restore = consoleErrorSpy(); + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length, + returnedPage: messages, + parentSet: { messages: messages.slice(-1), pagination: {} }, + }), + ).to.eql({}); + restore(); + }); + it(`parent set size < returned page size < requested page size pagination option ${option}`, () => { + const restore = consoleErrorSpy(); + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length, + returnedPage: messages.slice(1), + parentSet: { messages: messages.slice(2), pagination: {} }, + }), + ).to.eql({}); + restore(); + }); + it(`requested page size < parent set size < returned page size pagination option ${option}`, () => { + const restore = consoleErrorSpy(); + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length - 2, + returnedPage: messages, + parentSet: { messages: messages.slice(1), pagination: {} }, + }), + ).to.eql({}); + restore(); + }); + }); + + describe('first page message & last page message do not match the first and last parent set messages', () => { + it(`requested page size === returned page size === parent set size pagination option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length, + returnedPage: [ + messages[1], + messages[0], + ...messages.slice(2, -2), + messages.slice(-1)[0], + messages.slice(-2, -1)[0], + ], + parentSet: { messages, pagination: {} }, + }), + ).to.eql({}); + }); + it(`requested page size === parent set size > returned page size option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length, + returnedPage: messages.slice(1, -1), + parentSet: { messages, pagination: {} }, + }), + ).to.eql({}); + }); + it(`returned page size === parent set size pagination < requested page size option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length + 1, + returnedPage: [ + messages[1], + messages[0], + ...messages.slice(2, -2), + messages.slice(-1)[0], + messages.slice(-2, -1)[0], + ], + parentSet: { messages, pagination: {} }, + }), + ).to.eql({}); + }); + + it(`returned page size === parent set size pagination > requested page size option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length - 1, + returnedPage: [ + messages[1], + messages[0], + ...messages.slice(2, -2), + messages.slice(-1)[0], + messages.slice(-2, -1)[0], + ], + parentSet: { messages, pagination: {} }, + }), + ).to.eql({}); + }); - it(`requested page size === returned page size < parent set size pagination option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length - 2, - returnedPage: messages.slice(1, -1), - parentSet: { messages, pagination: {} }, - }), - ).to.eql({}); - }); - it(`requested page size < returned page size < parent set size pagination option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length - 3, - returnedPage: messages.slice(1, -1), - parentSet: { messages, pagination: {} }, - }), - ).to.eql({}); - }); - it(`returned page size < parent set size < requested page size pagination option ${option}`, () => { - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length + 1, - returnedPage: messages.slice(1, -1), - parentSet: { messages, pagination: {} }, - }), - ).to.eql({}); - }); - it(`requested page size === returned page size > parent set size pagination option ${option}`, () => { - const restore = consoleErrorSpy(); - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length, - returnedPage: messages, - parentSet: { messages: messages.slice(1, -1), pagination: {} }, - }), - ).to.eql({}); - restore(); - }); - it(`parent set size < returned page size < requested page size pagination option ${option}`, () => { - const restore = consoleErrorSpy(); - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length, - returnedPage: messages.slice(1), - parentSet: { messages: messages.slice(1, -1), pagination: {} }, - }), - ).to.eql({}); - restore(); - }); - it(`requested page size < parent set size < returned page size pagination option ${option}`, () => { - const restore = consoleErrorSpy(); - expect( - messageSetPagination({ - messagePaginationOptions: option && { [option]: 'X' }, - requestedPageSize: messages.length - 3, - returnedPage: messages, - parentSet: { messages: messages.slice(1, -1), pagination: {} }, - }), - ).to.eql({}); - restore(); - }); + it(`requested page size === returned page size < parent set size pagination option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length - 2, + returnedPage: messages.slice(1, -1), + parentSet: { messages, pagination: {} }, + }), + ).to.eql({}); }); - }, - ); + it(`requested page size < returned page size < parent set size pagination option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length - 3, + returnedPage: messages.slice(1, -1), + parentSet: { messages, pagination: {} }, + }), + ).to.eql({}); + }); + it(`returned page size < parent set size < requested page size pagination option ${option}`, () => { + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length + 1, + returnedPage: messages.slice(1, -1), + parentSet: { messages, pagination: {} }, + }), + ).to.eql({}); + }); + it(`requested page size === returned page size > parent set size pagination option ${option}`, () => { + const restore = consoleErrorSpy(); + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length, + returnedPage: messages, + parentSet: { messages: messages.slice(1, -1), pagination: {} }, + }), + ).to.eql({}); + restore(); + }); + it(`parent set size < returned page size < requested page size pagination option ${option}`, () => { + const restore = consoleErrorSpy(); + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length, + returnedPage: messages.slice(1), + parentSet: { messages: messages.slice(1, -1), pagination: {} }, + }), + ).to.eql({}); + restore(); + }); + it(`requested page size < parent set size < returned page size pagination option ${option}`, () => { + const restore = consoleErrorSpy(); + expect( + messageSetPagination({ + messagePaginationOptions: option && { [option]: 'X' }, + requestedPageSize: messages.length - 3, + returnedPage: messages, + parentSet: { messages: messages.slice(1, -1), pagination: {} }, + }), + ).to.eql({}); + restore(); + }); + }); + }); }); describe('jumping to a message', () => { @@ -1218,7 +1231,11 @@ describe('messageSetPagination', () => { messageSetPagination({ messagePaginationOptions: messagePaginationOptions.firstHalf, requestedPageSize: messages.length, - returnedPage: [messages[0], ...messages.slice(2, -2), messages.slice(-1)[0]], + returnedPage: [ + messages[0], + ...messages.slice(2, -2), + messages.slice(-1)[0], + ], parentSet: { messages: messages.slice(1, -1), pagination: {} }, }), ).to.eql({ hasPrev: false, hasNext: false }); @@ -1274,7 +1291,11 @@ describe('messageSetPagination', () => { messageSetPagination({ messagePaginationOptions: messagePaginationOptions.firstHalf, requestedPageSize: messages.length + 1, - returnedPage: [messages[1], ...messages.slice(2, -2), messages.slice(-1)[0]], + returnedPage: [ + messages[1], + ...messages.slice(2, -2), + messages.slice(-1)[0], + ], parentSet: { messages, pagination: {} }, }), ).to.eql({ hasPrev: false, hasNext: false }); @@ -1303,7 +1324,11 @@ describe('messageSetPagination', () => { messageSetPagination({ messagePaginationOptions: messagePaginationOptions.firstHalf, requestedPageSize: messages.length, - returnedPage: [messages[0], ...messages.slice(2, -2), messages.slice(-1)[0]], + returnedPage: [ + messages[0], + ...messages.slice(2, -2), + messages.slice(-1)[0], + ], parentSet: { messages: messages.slice(1, -2), pagination: {} }, }), ).to.eql({}); @@ -2623,7 +2648,9 @@ describe('messageSetPagination', () => { { description: '0 return page size', messages: [], - messagePaginationOptions: { created_at_around: createdAtISOString(2, oddSizeReturnPage) }, + messagePaginationOptions: { + created_at_around: createdAtISOString(2, oddSizeReturnPage), + }, option: 'created_at_around', }, { @@ -2704,9 +2731,19 @@ describe('', () => { { created_at: '2024-08-05T08:55:08.199808Z', id: '8' }, ]; it('finds the nearest newer item', () => { - expect(binarySearchByDateEqualOrNearestGreater(messages, new Date('2024-08-05T08:55:02.299808Z'))).to.eql(3); + expect( + binarySearchByDateEqualOrNearestGreater( + messages, + new Date('2024-08-05T08:55:02.299808Z'), + ), + ).to.eql(3); }); it('finds the nearest matching item', () => { - expect(binarySearchByDateEqualOrNearestGreater(messages, new Date('2024-08-05T08:55:07.199808Z'))).to.eql(7); + expect( + binarySearchByDateEqualOrNearestGreater( + messages, + new Date('2024-08-05T08:55:07.199808Z'), + ), + ).to.eql(7); }); }); diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts index eeecb1deda..799d1a0347 100644 --- a/test/unit/utils.test.ts +++ b/test/unit/utils.test.ts @@ -1,6 +1,6 @@ -import { expect } from 'chai'; import sinon from 'sinon'; import { v4 as uuidv4 } from 'uuid'; +import { describe, beforeEach, afterEach, it, expect } from 'vitest'; import { generateMsg } from './test-utils/generateMessage'; import { generateChannel } from './test-utils/generateChannel'; @@ -25,7 +25,12 @@ import { uniqBy, } from '../../src/utils'; -import type { ChannelFilters, ChannelSortBase, FormatMessageResponse, MessageResponse } from '../../src'; +import type { + ChannelFilters, + ChannelSortBase, + FormatMessageResponse, + MessageResponse, +} from '../../src'; import { StreamChat, Channel } from '../../src'; describe('addToMessageList', () => { @@ -33,7 +38,13 @@ describe('addToMessageList', () => { // messages with each created_at 10 seconds apart let messagesBefore: FormatMessageResponse[]; - const getNewFormattedMessage = ({ timeOffset, id = uuidv4() }: { timeOffset: number; id?: string }) => + const getNewFormattedMessage = ({ + timeOffset, + id = uuidv4(), + }: { + timeOffset: number; + id?: string; + }) => formatMessage( generateMsg({ id, @@ -43,7 +54,11 @@ describe('addToMessageList', () => { beforeEach(() => { messagesBefore = Array.from({ length: 5 }, (_, index) => - formatMessage(generateMsg({ created_at: new Date(timestamp + index * 10 * 1000) }) as MessageResponse), + formatMessage( + generateMsg({ + created_at: new Date(timestamp + index * 10 * 1000), + }) as MessageResponse, + ), ); }); @@ -59,7 +74,10 @@ describe('addToMessageList', () => { }); it('replaces the message which created_at changed to a server response created_at', () => { - const newMessage = getNewFormattedMessage({ timeOffset: 33 * 1000, id: messagesBefore[2].id }); + const newMessage = getNewFormattedMessage({ + timeOffset: 33 * 1000, + id: messagesBefore[2].id, + }); expect(newMessage.id).to.equal(messagesBefore[2].id); @@ -87,7 +105,13 @@ describe('addToMessageList', () => { const emptyMessagesBefore = []; - const messagesAfter = addToMessageList(emptyMessagesBefore, newMessage, false, 'created_at', false); + const messagesAfter = addToMessageList( + emptyMessagesBefore, + newMessage, + false, + 'created_at', + false, + ); expect(messagesAfter).to.have.length(0); }); @@ -105,7 +129,13 @@ describe('addToMessageList', () => { it("doesn't add a newest message to a message list if timestampChanged & addIfDoesNotExist are false", () => { const newMessage = getNewFormattedMessage({ timeOffset: 50 * 1000 }); - const messagesAfter = addToMessageList(messagesBefore, newMessage, false, 'created_at', false); + const messagesAfter = addToMessageList( + messagesBefore, + newMessage, + false, + 'created_at', + false, + ); expect(messagesAfter).to.have.length(5); // FIXME: it'd be nice if the function returned old @@ -114,13 +144,22 @@ describe('addToMessageList', () => { }); it("updates an existing message that wasn't filtered due to changed timestamp (timestampChanged)", () => { - const newMessage = getNewFormattedMessage({ timeOffset: 30 * 1000, id: messagesBefore[4].id }); + const newMessage = getNewFormattedMessage({ + timeOffset: 30 * 1000, + id: messagesBefore[4].id, + }); expect(messagesBefore[4].id).to.equal(newMessage.id); expect(messagesBefore[4].text).to.not.equal(newMessage.text); expect(messagesBefore[4]).to.not.equal(newMessage); - const messagesAfter = addToMessageList(messagesBefore, newMessage, false, 'created_at', false); + const messagesAfter = addToMessageList( + messagesBefore, + newMessage, + false, + 'created_at', + false, + ); expect(messagesAfter).to.have.length(5); expect(messagesAfter[4]).to.equal(newMessage); @@ -131,28 +170,44 @@ describe('findIndexInSortedArray', () => { it('finds index in the middle of haystack (asc)', () => { const needle = 5; const haystack = [1, 2, 3, 4, 6, 7, 8, 9]; - const index = findIndexInSortedArray({ needle, sortedArray: haystack, sortDirection: 'ascending' }); + const index = findIndexInSortedArray({ + needle, + sortedArray: haystack, + sortDirection: 'ascending', + }); expect(index).to.eq(4); }); it('finds index at the top of haystack (asc)', () => { const needle = 0; const haystack = [1, 2, 3, 4, 6, 7, 8, 9]; - const index = findIndexInSortedArray({ needle, sortedArray: haystack, sortDirection: 'ascending' }); + const index = findIndexInSortedArray({ + needle, + sortedArray: haystack, + sortDirection: 'ascending', + }); expect(index).to.eq(0); }); it('finds index at the bottom of haystack (asc)', () => { const needle = 10; const haystack = [1, 2, 3, 4, 6, 7, 8, 9]; - const index = findIndexInSortedArray({ needle, sortedArray: haystack, sortDirection: 'ascending' }); + const index = findIndexInSortedArray({ + needle, + sortedArray: haystack, + sortDirection: 'ascending', + }); expect(index).to.eq(8); }); it('in a haystack with duplicates, prefers index closer to the bottom (asc)', () => { const needle = 5; const haystack = [1, 5, 5, 5, 5, 5, 8, 9]; - const index = findIndexInSortedArray({ needle, sortedArray: haystack, sortDirection: 'ascending' }); + const index = findIndexInSortedArray({ + needle, + sortedArray: haystack, + sortDirection: 'ascending', + }); expect(index).to.eq(6); }); @@ -202,28 +257,44 @@ describe('findIndexInSortedArray', () => { it('finds index in the middle of haystack (desc)', () => { const needle = 5; const haystack = [9, 8, 7, 6, 4, 3, 2, 1]; - const index = findIndexInSortedArray({ needle, sortedArray: haystack, sortDirection: 'descending' }); + const index = findIndexInSortedArray({ + needle, + sortedArray: haystack, + sortDirection: 'descending', + }); expect(index).to.eq(4); }); it('finds index at the top of haystack (desc)', () => { const needle = 10; const haystack = [9, 8, 7, 6, 4, 3, 2, 1]; - const index = findIndexInSortedArray({ needle, sortedArray: haystack, sortDirection: 'descending' }); + const index = findIndexInSortedArray({ + needle, + sortedArray: haystack, + sortDirection: 'descending', + }); expect(index).to.eq(0); }); it('finds index at the bottom of haystack (desc)', () => { const needle = 0; const haystack = [9, 8, 7, 6, 4, 3, 2, 1]; - const index = findIndexInSortedArray({ needle, sortedArray: haystack, sortDirection: 'descending' }); + const index = findIndexInSortedArray({ + needle, + sortedArray: haystack, + sortDirection: 'descending', + }); expect(index).to.eq(8); }); it('in a haystack with duplicates, prefers index closer to the top (desc)', () => { const needle = 5; const haystack = [9, 8, 5, 5, 5, 5, 5, 1]; - const index = findIndexInSortedArray({ needle, sortedArray: haystack, sortDirection: 'descending' }); + const index = findIndexInSortedArray({ + needle, + sortedArray: haystack, + sortDirection: 'descending', + }); expect(index).to.eq(2); }); @@ -280,13 +351,18 @@ describe('getAndWatchChannel', () => { client = await getClientWithUser(); - const mockedMembers = [generateMember({ user: generateUser() }), generateMember({ user: generateUser() })]; + const mockedMembers = [ + generateMember({ user: generateUser() }), + generateMember({ user: generateUser() }), + ]; const mockedChannelsQueryResponse = [ ...Array.from({ length: 2 }, () => generateChannel()), generateChannel({ channel: { type: 'messaging' }, members: mockedMembers }), ]; const mock = sandbox.mock(client); - mock.expects('post').returns(Promise.resolve({ channels: mockedChannelsQueryResponse })); + mock + .expects('post') + .returns(Promise.resolve({ channels: mockedChannelsQueryResponse })); }); afterEach(() => { @@ -295,14 +371,16 @@ describe('getAndWatchChannel', () => { it('should throw an error if neither channel nor type is provided', async () => { await client.queryChannels({}); - await expect(getAndWatchChannel({ client, id: 'test-id', members: [] })).to.be.rejectedWith( - 'Channel or channel type have to be provided to query a channel.', - ); + await expect( + getAndWatchChannel({ client, id: 'test-id', members: [] }), + ).rejects.toThrow('Channel or channel type have to be provided to query a channel.'); }); it('should throw an error if neither channel ID nor members array is provided', async () => { await client.queryChannels({}); - await expect(getAndWatchChannel({ client, type: 'test-type', id: undefined, members: [] })).to.be.rejectedWith( + await expect( + getAndWatchChannel({ client, type: 'test-type', id: undefined, members: [] }), + ).rejects.toThrow( 'Channel ID or channel members array have to be provided to query a channel.', ); }); @@ -399,7 +477,7 @@ describe('generateChannelTempCid', () => { }); it('should return undefined if members is null', () => { - const result = generateChannelTempCid('messaging', (null as unknown) as string[]); + const result = generateChannelTempCid('messaging', null as unknown as string[]); expect(result).to.be.undefined; }); @@ -429,40 +507,53 @@ describe('Channel pinning and archiving utils', () => { describe('Channel pinning', () => { it('should return false if channel is null', () => { - expect(isChannelPinned((null as unknown) as Channel)).to.be.false; + expect(isChannelPinned(null as unknown as Channel)).to.be.false; }); it('should return false if pinned_at is undefined', () => { const channelResponse = generateChannel({ membership: {} }); client.hydrateActiveChannels([channelResponse]); - const channel = client.channel(channelResponse.channel.type, channelResponse.channel.id); + const channel = client.channel( + channelResponse.channel.type, + channelResponse.channel.id, + ); expect(isChannelPinned(channel)).to.be.false; }); it('should return true if pinned_at is set', () => { - const channelResponse = generateChannel({ membership: { pinned_at: '2024-02-04T12:00:00Z' } }); + const channelResponse = generateChannel({ + membership: { pinned_at: '2024-02-04T12:00:00Z' }, + }); client.hydrateActiveChannels([channelResponse]); - const channel = client.channel(channelResponse.channel.type, channelResponse.channel.id); + const channel = client.channel( + channelResponse.channel.type, + channelResponse.channel.id, + ); expect(isChannelPinned(channel)).to.be.true; }); describe('extractSortValue', () => { it('should return null if sort is undefined', () => { - expect(extractSortValue({ atIndex: 0, targetKey: 'pinned_at', sort: undefined })).to.be.null; + expect(extractSortValue({ atIndex: 0, targetKey: 'pinned_at', sort: undefined })) + .to.be.null; }); it('should extract correct sort value from an array', () => { - const sort = ([{ pinned_at: -1 }, { created_at: 1 }] as unknown) as ChannelSortBase; - expect(extractSortValue({ atIndex: 0, targetKey: 'pinned_at', sort })).to.equal(-1); + const sort = [{ pinned_at: -1 }, { created_at: 1 }] as unknown as ChannelSortBase; + expect(extractSortValue({ atIndex: 0, targetKey: 'pinned_at', sort })).to.equal( + -1, + ); }); it('should extract correct sort value from an object', () => { - const sort = ({ pinned_at: 1 } as unknown) as ChannelSortBase; - expect(extractSortValue({ atIndex: 0, targetKey: 'pinned_at', sort })).to.equal(1); + const sort = { pinned_at: 1 } as unknown as ChannelSortBase; + expect(extractSortValue({ atIndex: 0, targetKey: 'pinned_at', sort })).to.equal( + 1, + ); }); it('should return null if key does not match targetKey', () => { - const sort = ({ created_at: 1 } as unknown) as ChannelSortBase; + const sort = { created_at: 1 } as unknown as ChannelSortBase; expect(extractSortValue({ atIndex: 0, targetKey: 'pinned_at', sort })).to.be.null; }); }); @@ -478,13 +569,13 @@ describe('Channel pinning and archiving utils', () => { }); it('should return false if pinned_at is not first in sort', () => { - const sort = ([{ created_at: 1 }, { pinned_at: 1 }] as unknown) as ChannelSortBase; + const sort = [{ created_at: 1 }, { pinned_at: 1 }] as unknown as ChannelSortBase; expect(shouldConsiderPinnedChannels(sort)).to.be.false; }); it('should return true if pinned_at is 1 or -1 at index 0', () => { - const sort1 = ([{ pinned_at: 1 }] as unknown) as ChannelSortBase; - const sort2 = ([{ pinned_at: -1 }] as unknown) as ChannelSortBase; + const sort1 = [{ pinned_at: 1 }] as unknown as ChannelSortBase; + const sort2 = [{ pinned_at: -1 }] as unknown as ChannelSortBase; expect(shouldConsiderPinnedChannels(sort1)).to.be.true; expect(shouldConsiderPinnedChannels(sort2)).to.be.true; }); @@ -492,21 +583,22 @@ describe('Channel pinning and archiving utils', () => { describe('findPinnedAtSortOrder', () => { it('should return null if sort is undefined', () => { - expect(findPinnedAtSortOrder({ sort: (null as unknown) as ChannelSortBase })).to.be.null; + expect(findPinnedAtSortOrder({ sort: null as unknown as ChannelSortBase })).to.be + .null; }); it('should return null if pinned_at is not present', () => { - const sort = ([{ created_at: 1 }] as unknown) as ChannelSortBase; + const sort = [{ created_at: 1 }] as unknown as ChannelSortBase; expect(findPinnedAtSortOrder({ sort })).to.be.null; }); it('should return pinned_at if found in an object', () => { - const sort = ({ pinned_at: -1 } as unknown) as ChannelSortBase; + const sort = { pinned_at: -1 } as unknown as ChannelSortBase; expect(findPinnedAtSortOrder({ sort })).to.equal(-1); }); it('should return pinned_at if found in an array', () => { - const sort = ([{ pinned_at: 1 }] as unknown) as ChannelSortBase; + const sort = [{ pinned_at: 1 }] as unknown as ChannelSortBase; expect(findPinnedAtSortOrder({ sort })).to.equal(1); }); }); @@ -517,9 +609,14 @@ describe('Channel pinning and archiving utils', () => { }); it('should return null if no channels are pinned', () => { - const channelsResponse = [generateChannel({ membership: {} }), generateChannel({ membership: {} })]; + const channelsResponse = [ + generateChannel({ membership: {} }), + generateChannel({ membership: {} }), + ]; client.hydrateActiveChannels(channelsResponse); - const channels = channelsResponse.map((c) => client.channel(c.channel.type, c.channel.id)); + const channels = channelsResponse.map((c) => + client.channel(c.channel.type, c.channel.id), + ); expect(findLastPinnedChannelIndex({ channels })).to.be.null; }); @@ -530,7 +627,9 @@ describe('Channel pinning and archiving utils', () => { generateChannel({ membership: {} }), ]; client.hydrateActiveChannels(channelsResponse); - const channels = channelsResponse.map((c) => client.channel(c.channel.type, c.channel.id)); + const channels = channelsResponse.map((c) => + client.channel(c.channel.type, c.channel.id), + ); expect(findLastPinnedChannelIndex({ channels })).to.equal(1); }); @@ -539,25 +638,34 @@ describe('Channel pinning and archiving utils', () => { describe('Channel archiving', () => { it('should return false if channel is null', () => { - expect(isChannelArchived((null as unknown) as Channel)).to.be.false; + expect(isChannelArchived(null as unknown as Channel)).to.be.false; }); it('should return false if archived_at is undefined', () => { const channelResponse = generateChannel({ membership: {} }); client.hydrateActiveChannels([channelResponse]); - const channel = client.channel(channelResponse.channel.type, channelResponse.channel.id); + const channel = client.channel( + channelResponse.channel.type, + channelResponse.channel.id, + ); expect(isChannelArchived(channel)).to.be.false; }); it('should return true if archived_at is set', () => { - const channelResponse = generateChannel({ membership: { archived_at: '2024-02-04T12:00:00Z' } }); + const channelResponse = generateChannel({ + membership: { archived_at: '2024-02-04T12:00:00Z' }, + }); client.hydrateActiveChannels([channelResponse]); - const channel = client.channel(channelResponse.channel.type, channelResponse.channel.id); + const channel = client.channel( + channelResponse.channel.type, + channelResponse.channel.id, + ); expect(isChannelArchived(channel)).to.be.true; }); it('should return false if filters is null', () => { - expect(shouldConsiderArchivedChannels((null as unknown) as ChannelFilters)).to.be.false; + expect(shouldConsiderArchivedChannels(null as unknown as ChannelFilters)).to.be + .false; }); it('should return false if filters.archived is missing', () => { @@ -566,7 +674,7 @@ describe('Channel pinning and archiving utils', () => { }); it('should return false if filters.archived is not a boolean', () => { - const mockFilters = ({ archived: 'yes' } as unknown) as ChannelFilters; + const mockFilters = { archived: 'yes' } as unknown as ChannelFilters; expect(shouldConsiderArchivedChannels(mockFilters)).to.be.false; }); @@ -592,7 +700,9 @@ describe('promoteChannel', () => { it('should return the original list if the channel is already at the top', () => { const channelsResponse = [generateChannel(), generateChannel()]; client.hydrateActiveChannels(channelsResponse); - const channels = channelsResponse.map((c) => client.channel(c.channel.type, c.channel.id)); + const channels = channelsResponse.map((c) => + client.channel(c.channel.type, c.channel.id), + ); const result = promoteChannel({ channels, channelToMove: channels[0], @@ -609,7 +719,9 @@ describe('promoteChannel', () => { generateChannel({ membership: { pinned_at: '2024-02-04T12:01:00Z' } }), ]; client.hydrateActiveChannels(channelsResponse); - const channels = channelsResponse.map((c) => client.channel(c.channel.type, c.channel.id)); + const channels = channelsResponse.map((c) => + client.channel(c.channel.type, c.channel.id), + ); const channelToMove = channels[1]; const result = promoteChannel({ @@ -629,7 +741,9 @@ describe('promoteChannel', () => { generateChannel({ channel: { id: 'channel3' } }), ]; client.hydrateActiveChannels(channelsResponse); - const channels = channelsResponse.map((c) => client.channel(c.channel.type, c.channel.id)); + const channels = channelsResponse.map((c) => + client.channel(c.channel.type, c.channel.id), + ); const channelToMove = channels[2]; const result = promoteChannel({ @@ -649,7 +763,9 @@ describe('promoteChannel', () => { generateChannel({ channel: { id: 'channel3' } }), ]; client.hydrateActiveChannels(channelsResponse); - const channels = channelsResponse.map((c) => client.channel(c.channel.type, c.channel.id)); + const channels = channelsResponse.map((c) => + client.channel(c.channel.type, c.channel.id), + ); const channelToMove = channels[2]; const result = promoteChannel({ @@ -671,7 +787,9 @@ describe('promoteChannel', () => { ]; const newChannel = generateChannel({ channel: { id: 'channel4' } }); client.hydrateActiveChannels([...channelsResponse, newChannel]); - const channels = channelsResponse.map((c) => client.channel(c.channel.type, c.channel.id)); + const channels = channelsResponse.map((c) => + client.channel(c.channel.type, c.channel.id), + ); const channelToMove = client.channel(newChannel.channel.type, newChannel.channel.id); const result = promoteChannel({ @@ -680,7 +798,12 @@ describe('promoteChannel', () => { sort: {}, }); - expect(result.map((c) => c.id)).to.deep.equal(['channel4', 'channel1', 'channel2', 'channel3']); + expect(result.map((c) => c.id)).to.deep.equal([ + 'channel4', + 'channel1', + 'channel2', + 'channel3', + ]); expect(result).to.not.equal(channels); }); @@ -692,7 +815,9 @@ describe('promoteChannel', () => { ]; const newChannel = generateChannel({ channel: { id: 'channel4' } }); client.hydrateActiveChannels([...channelsResponse, newChannel]); - const channels = channelsResponse.map((c) => client.channel(c.channel.type, c.channel.id)); + const channels = channelsResponse.map((c) => + client.channel(c.channel.type, c.channel.id), + ); const channelToMove = client.channel(newChannel.channel.type, newChannel.channel.id); const result = promoteChannel({ @@ -702,19 +827,32 @@ describe('promoteChannel', () => { channelToMoveIndexWithinChannels: -1, }); - expect(result.map((c) => c.id)).to.deep.equal(['channel4', 'channel1', 'channel2', 'channel3']); + expect(result.map((c) => c.id)).to.deep.equal([ + 'channel4', + 'channel1', + 'channel2', + 'channel3', + ]); expect(result).to.not.equal(channels); }); it('should move the channel just below the last pinned channel if pinned channels are considered', () => { const channelsResponse = [ - generateChannel({ channel: { id: 'pinned1' }, membership: { pinned_at: '2024-02-04T12:00:00Z' } }), - generateChannel({ channel: { id: 'pinned2' }, membership: { pinned_at: '2024-02-04T12:01:00Z' } }), + generateChannel({ + channel: { id: 'pinned1' }, + membership: { pinned_at: '2024-02-04T12:00:00Z' }, + }), + generateChannel({ + channel: { id: 'pinned2' }, + membership: { pinned_at: '2024-02-04T12:01:00Z' }, + }), generateChannel({ channel: { id: 'channel1' } }), generateChannel({ channel: { id: 'channel2' } }), ]; client.hydrateActiveChannels(channelsResponse); - const channels = channelsResponse.map((c) => client.channel(c.channel.type, c.channel.id)); + const channels = channelsResponse.map((c) => + client.channel(c.channel.type, c.channel.id), + ); const channelToMove = channels[3]; const result = promoteChannel({ @@ -723,19 +861,32 @@ describe('promoteChannel', () => { sort: [{ pinned_at: -1 }], }); - expect(result.map((c) => c.id)).to.deep.equal(['pinned1', 'pinned2', 'channel2', 'channel1']); + expect(result.map((c) => c.id)).to.deep.equal([ + 'pinned1', + 'pinned2', + 'channel2', + 'channel1', + ]); expect(result).to.not.equal(channels); }); it('should move the channel to the top of the list if pinned channels exist but are not considered', () => { const channelsResponse = [ - generateChannel({ channel: { id: 'pinned1' }, membership: { pinned_at: '2024-02-04T12:01:00Z' } }), - generateChannel({ channel: { id: 'pinned2' }, membership: { pinned_at: '2024-02-04T12:00:00Z' } }), + generateChannel({ + channel: { id: 'pinned1' }, + membership: { pinned_at: '2024-02-04T12:01:00Z' }, + }), + generateChannel({ + channel: { id: 'pinned2' }, + membership: { pinned_at: '2024-02-04T12:00:00Z' }, + }), generateChannel({ channel: { id: 'channel1' } }), generateChannel({ channel: { id: 'channel2' } }), ]; client.hydrateActiveChannels(channelsResponse); - const channels = channelsResponse.map((c) => client.channel(c.channel.type, c.channel.id)); + const channels = channelsResponse.map((c) => + client.channel(c.channel.type, c.channel.id), + ); const channelToMove = channels[2]; const result = promoteChannel({ @@ -744,7 +895,12 @@ describe('promoteChannel', () => { sort: {}, }); - expect(result.map((c) => c.id)).to.deep.equal(['channel1', 'pinned1', 'pinned2', 'channel2']); + expect(result.map((c) => c.id)).to.deep.equal([ + 'channel1', + 'pinned1', + 'pinned2', + 'channel2', + ]); expect(result).to.not.equal(channels); }); }); @@ -799,7 +955,10 @@ describe('uniqBy', () => { { user: { id: 1, name: 'Alice' } }, ]; const result = uniqBy(array, 'user.id'); - expect(result).to.deep.equal([{ user: { id: 1, name: 'Alice' } }, { user: { id: 2, name: 'Bob' } }]); + expect(result).to.deep.equal([ + { user: { id: 1, name: 'Alice' } }, + { user: { id: 2, name: 'Bob' } }, + ]); }); it('should work with primitive identities', () => { @@ -814,7 +973,12 @@ describe('uniqBy', () => { it('should handle falsy values correctly', () => { const array = [{ id: 0 }, { id: false }, { id: null }, { id: undefined }, { id: 0 }]; const result = uniqBy(array, 'id'); - expect(result).to.deep.equal([{ id: 0 }, { id: false }, { id: null }, { id: undefined }]); + expect(result).to.deep.equal([ + { id: 0 }, + { id: false }, + { id: null }, + { id: undefined }, + ]); }); it('should work when all elements are identical', () => { @@ -828,7 +992,12 @@ describe('uniqBy', () => { it('should handle mixed types correctly', () => { const array = [{ id: 1 }, { id: '1' }, { id: 1.0 }, { id: true }, { id: false }]; - expect(uniqBy(array, 'id')).to.deep.equal([{ id: 1 }, { id: '1' }, { id: true }, { id: false }]); + expect(uniqBy(array, 'id')).to.deep.equal([ + { id: 1 }, + { id: '1' }, + { id: true }, + { id: false }, + ]); }); it('should handle undefined values in objects', () => { diff --git a/tsconfig.test.json b/tsconfig.test.json deleted file mode 100644 index 31b8d22092..0000000000 --- a/tsconfig.test.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "moduleResolution": "node10", - "module": "CommonJS", - "strict": false - }, - "ts-node": { - "transpileOnly": true, - "logError": true - }, - "include": ["./test/**/*"] -} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000000..da6a623bb7 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,11 @@ +/// + +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + testTimeout: 20000, + // not all errors have been handled so this is necessary (at least for the time being) + dangerouslyIgnoreUnhandledErrors: true, + }, +}); diff --git a/yarn.lock b/yarn.lock index 7f50389a85..485747dea0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,13 +9,6 @@ dependencies: "@jridgewell/trace-mapping" "^0.3.0" -"@babel/code-frame@7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" - integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== - dependencies: - "@babel/highlight" "^7.10.4" - "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.22.13", "@babel/code-frame@^7.26.2": version "7.26.2" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" @@ -95,7 +88,7 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== -"@babel/helper-validator-identifier@^7.16.7", "@babel/helper-validator-identifier@^7.25.9": +"@babel/helper-validator-identifier@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== @@ -114,15 +107,6 @@ "@babel/traverse" "^7.17.9" "@babel/types" "^7.17.0" -"@babel/highlight@^7.10.4": - version "7.17.9" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.17.9.tgz#61b2ee7f32ea0454612def4fccdae0de232b73e3" - integrity sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg== - dependencies: - "@babel/helper-validator-identifier" "^7.16.7" - chalk "^2.0.0" - js-tokens "^4.0.0" - "@babel/parser@^7.17.9", "@babel/parser@^7.26.9": version "7.26.9" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.9.tgz#d9e78bee6dc80f9efd8f2349dcfbbcdace280fd5" @@ -165,163 +149,160 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== -"@commitlint/cli@^16.0.2": - version "16.0.2" - resolved "https://registry.yarnpkg.com/@commitlint/cli/-/cli-16.0.2.tgz#393b03793fc59b93e5f4dd7dd535a6cc5a7413ca" - integrity sha512-Jt7iaBjoLGC5Nq4dHPTvTYnqPGkElFPBtTXTvBpTgatZApczyjI2plE0oG4GYWPp1suHIS/VdVDOMpPZjGVusg== - dependencies: - "@commitlint/format" "^16.0.0" - "@commitlint/lint" "^16.0.0" - "@commitlint/load" "^16.0.0" - "@commitlint/read" "^16.0.0" - "@commitlint/types" "^16.0.0" - lodash "^4.17.19" - resolve-from "5.0.0" - resolve-global "1.0.0" +"@commitlint/cli@^19.7.1": + version "19.7.1" + resolved "https://registry.yarnpkg.com/@commitlint/cli/-/cli-19.7.1.tgz#aca351ae6200af58b49de58181319015e7429601" + integrity sha512-iObGjR1tE/PfDtDTEfd+tnRkB3/HJzpQqRTyofS2MPPkDn1mp3DBC8SoPDayokfAy+xKhF8+bwRCJO25Nea0YQ== + dependencies: + "@commitlint/format" "^19.5.0" + "@commitlint/lint" "^19.7.1" + "@commitlint/load" "^19.6.1" + "@commitlint/read" "^19.5.0" + "@commitlint/types" "^19.5.0" + tinyexec "^0.3.0" yargs "^17.0.0" -"@commitlint/config-conventional@^16.0.0": - version "16.0.0" - resolved "https://registry.yarnpkg.com/@commitlint/config-conventional/-/config-conventional-16.0.0.tgz#f42d9e1959416b5e691c8b5248fc2402adb1fc03" - integrity sha512-mN7J8KlKFn0kROd+q9PB01sfDx/8K/R25yITspL1No8PB4oj9M1p77xWjP80hPydqZG9OvQq+anXK3ZWeR7s3g== - dependencies: - conventional-changelog-conventionalcommits "^4.3.1" - -"@commitlint/config-validator@^16.0.0": - version "16.0.0" - resolved "https://registry.yarnpkg.com/@commitlint/config-validator/-/config-validator-16.0.0.tgz#61dd84895e5dcab6066ff5e21e2b9a96b0ed6323" - integrity sha512-i80DGlo1FeC5jZpuoNV9NIjQN/m2dDV3jYGWg+1Wr+KldptkUHXj+6GY1Akll66lJ3D8s6aUGi3comPLHPtWHg== - dependencies: - "@commitlint/types" "^16.0.0" - ajv "^6.12.6" - -"@commitlint/ensure@^16.0.0": - version "16.0.0" - resolved "https://registry.yarnpkg.com/@commitlint/ensure/-/ensure-16.0.0.tgz#fdac1e60a944a1993deb33b5e8454c559abe9866" - integrity sha512-WdMySU8DCTaq3JPf0tZFCKIUhqxaL54mjduNhu8v4D2AMUVIIQKYMGyvXn94k8begeW6iJkTf9cXBArayskE7Q== - dependencies: - "@commitlint/types" "^16.0.0" - lodash "^4.17.19" - -"@commitlint/execute-rule@^16.0.0": - version "16.0.0" - resolved "https://registry.yarnpkg.com/@commitlint/execute-rule/-/execute-rule-16.0.0.tgz#824e11ba5b208c214a474ae52a51780d32d31ebc" - integrity sha512-8edcCibmBb386x5JTHSPHINwA5L0xPkHQFY8TAuDEt5QyRZY/o5DF8OPHSa5Hx2xJvGaxxuIz4UtAT6IiRDYkw== - -"@commitlint/format@^16.0.0": - version "16.0.0" - resolved "https://registry.yarnpkg.com/@commitlint/format/-/format-16.0.0.tgz#6a6fb2c1e6460aff63cc6eca30a7807a96b0ce73" - integrity sha512-9yp5NCquXL1jVMKL0ZkRwJf/UHdebvCcMvICuZV00NQGYSAL89O398nhqrqxlbjBhM5EZVq0VGcV5+7r3D4zAA== - dependencies: - "@commitlint/types" "^16.0.0" - chalk "^4.0.0" - -"@commitlint/is-ignored@^16.0.0": - version "16.0.0" - resolved "https://registry.yarnpkg.com/@commitlint/is-ignored/-/is-ignored-16.0.0.tgz#5ab4c4a9c7444c1a8540f50a0f1a907dfd78eb70" - integrity sha512-gmAQcwIGC/R/Lp0CEb2b5bfGC7MT5rPe09N8kOGjO/NcdNmfFSZMquwrvNJsq9hnAP0skRdHIsqwlkENkN4Lag== - dependencies: - "@commitlint/types" "^16.0.0" - semver "7.3.5" - -"@commitlint/lint@^16.0.0": - version "16.0.0" - resolved "https://registry.yarnpkg.com/@commitlint/lint/-/lint-16.0.0.tgz#87151a935941073027907fd4752a2e3c83cebbfe" - integrity sha512-HNl15bRC0h+pLzbMzQC3tM0j1aESXsLYhElqKnXcf5mnCBkBkHzu6WwJW8rZbfxX+YwJmNljN62cPhmdBo8x0A== - dependencies: - "@commitlint/is-ignored" "^16.0.0" - "@commitlint/parse" "^16.0.0" - "@commitlint/rules" "^16.0.0" - "@commitlint/types" "^16.0.0" - -"@commitlint/load@^16.0.0": - version "16.0.0" - resolved "https://registry.yarnpkg.com/@commitlint/load/-/load-16.0.0.tgz#4ab9f8502d0521209ce54d7cce58d419b8c35b48" - integrity sha512-7WhrGCkP6K/XfjBBguLkkI2XUdiiIyMGlNsSoSqgRNiD352EiffhFEApMy1/XOU+viwBBm/On0n5p0NC7e9/4A== - dependencies: - "@commitlint/config-validator" "^16.0.0" - "@commitlint/execute-rule" "^16.0.0" - "@commitlint/resolve-extends" "^16.0.0" - "@commitlint/types" "^16.0.0" - chalk "^4.0.0" - cosmiconfig "^7.0.0" - cosmiconfig-typescript-loader "^1.0.0" - lodash "^4.17.19" - resolve-from "^5.0.0" - typescript "^4.4.3" - -"@commitlint/message@^16.0.0": - version "16.0.0" - resolved "https://registry.yarnpkg.com/@commitlint/message/-/message-16.0.0.tgz#4a467341fc6bc49e5a3ead005dd6aa36fa856b87" - integrity sha512-CmK2074SH1Ws6kFMEKOKH/7hMekGVbOD6vb4alCOo2+33ZSLUIX8iNkDYyrw38Jwg6yWUhLjyQLUxREeV+QIUA== - -"@commitlint/parse@^16.0.0": - version "16.0.0" - resolved "https://registry.yarnpkg.com/@commitlint/parse/-/parse-16.0.0.tgz#5ce05af14edff806effc702ba910fcb32fcb192a" - integrity sha512-F9EjFlMw4MYgBEqoRrWZZKQBzdiJzPBI0qFDFqwUvfQsMmXEREZ242T4R5bFwLINWaALFLHEIa/FXEPa6QxCag== - dependencies: - "@commitlint/types" "^16.0.0" - conventional-changelog-angular "^5.0.11" - conventional-commits-parser "^3.2.2" - -"@commitlint/read@^16.0.0": - version "16.0.0" - resolved "https://registry.yarnpkg.com/@commitlint/read/-/read-16.0.0.tgz#92fab45d4e0e4d7d049427306500270b3e459221" - integrity sha512-H4T2zsfmYQK9B+JtoQaCXWBHUhgIJyOzWZjSfuIV9Ce69/OgHoffNpLZPF2lX6yKuDrS1SQFhI/kUCjVc/e4ew== - dependencies: - "@commitlint/top-level" "^16.0.0" - "@commitlint/types" "^16.0.0" - fs-extra "^10.0.0" - git-raw-commits "^2.0.0" +"@commitlint/config-conventional@^19.7.1": + version "19.7.1" + resolved "https://registry.yarnpkg.com/@commitlint/config-conventional/-/config-conventional-19.7.1.tgz#9119a02ec8e0f4ac88f035e37dc333e7f69ace1c" + integrity sha512-fsEIF8zgiI/FIWSnykdQNj/0JE4av08MudLTyYHm4FlLWemKoQvPNUYU2M/3tktWcCEyq7aOkDDgtjrmgWFbvg== + dependencies: + "@commitlint/types" "^19.5.0" + conventional-changelog-conventionalcommits "^7.0.2" + +"@commitlint/config-validator@^19.5.0": + version "19.5.0" + resolved "https://registry.yarnpkg.com/@commitlint/config-validator/-/config-validator-19.5.0.tgz#f0a4eda2109fc716ef01bb8831af9b02e3a1e568" + integrity sha512-CHtj92H5rdhKt17RmgALhfQt95VayrUo2tSqY9g2w+laAXyk7K/Ef6uPm9tn5qSIwSmrLjKaXK9eiNuxmQrDBw== + dependencies: + "@commitlint/types" "^19.5.0" + ajv "^8.11.0" + +"@commitlint/ensure@^19.5.0": + version "19.5.0" + resolved "https://registry.yarnpkg.com/@commitlint/ensure/-/ensure-19.5.0.tgz#b087374a6a0a0140e5925a82901d234885d9f6dd" + integrity sha512-Kv0pYZeMrdg48bHFEU5KKcccRfKmISSm9MvgIgkpI6m+ohFTB55qZlBW6eYqh/XDfRuIO0x4zSmvBjmOwWTwkg== + dependencies: + "@commitlint/types" "^19.5.0" + lodash.camelcase "^4.3.0" + lodash.kebabcase "^4.1.1" + lodash.snakecase "^4.1.1" + lodash.startcase "^4.4.0" + lodash.upperfirst "^4.3.1" + +"@commitlint/execute-rule@^19.5.0": + version "19.5.0" + resolved "https://registry.yarnpkg.com/@commitlint/execute-rule/-/execute-rule-19.5.0.tgz#c13da8c03ea0379f30856111e27d57518e25b8a2" + integrity sha512-aqyGgytXhl2ejlk+/rfgtwpPexYyri4t8/n4ku6rRJoRhGZpLFMqrZ+YaubeGysCP6oz4mMA34YSTaSOKEeNrg== + +"@commitlint/format@^19.5.0": + version "19.5.0" + resolved "https://registry.yarnpkg.com/@commitlint/format/-/format-19.5.0.tgz#d879db2d97d70ae622397839fb8603d56e85a250" + integrity sha512-yNy088miE52stCI3dhG/vvxFo9e4jFkU1Mj3xECfzp/bIS/JUay4491huAlVcffOoMK1cd296q0W92NlER6r3A== + dependencies: + "@commitlint/types" "^19.5.0" + chalk "^5.3.0" -"@commitlint/resolve-extends@^16.0.0": - version "16.0.0" - resolved "https://registry.yarnpkg.com/@commitlint/resolve-extends/-/resolve-extends-16.0.0.tgz#2136f01d81bccc29091f2720b42c8c96aa59c56e" - integrity sha512-Z/w9MAQUcxeawpCLtjmkVNXAXOmB2nhW+LYmHEZcx9O6UTauF/1+uuZ2/r0MtzTe1qw2JD+1QHVhEWYHVPlkdA== - dependencies: - "@commitlint/config-validator" "^16.0.0" - "@commitlint/types" "^16.0.0" - import-fresh "^3.0.0" - lodash "^4.17.19" +"@commitlint/is-ignored@^19.7.1": + version "19.7.1" + resolved "https://registry.yarnpkg.com/@commitlint/is-ignored/-/is-ignored-19.7.1.tgz#d3d713d97df4da5d0a6440624d0db38e3a67494e" + integrity sha512-3IaOc6HVg2hAoGleRK3r9vL9zZ3XY0rf1RsUf6jdQLuaD46ZHnXBiOPTyQ004C4IvYjSWqJwlh0/u2P73aIE3g== + dependencies: + "@commitlint/types" "^19.5.0" + semver "^7.6.0" + +"@commitlint/lint@^19.7.1": + version "19.7.1" + resolved "https://registry.yarnpkg.com/@commitlint/lint/-/lint-19.7.1.tgz#a180d5695fc5328b8566a482750df7fbf72b11c5" + integrity sha512-LhcPfVjcOcOZA7LEuBBeO00o3MeZa+tWrX9Xyl1r9PMd5FWsEoZI9IgnGqTKZ0lZt5pO3ZlstgnRyY1CJJc9Xg== + dependencies: + "@commitlint/is-ignored" "^19.7.1" + "@commitlint/parse" "^19.5.0" + "@commitlint/rules" "^19.6.0" + "@commitlint/types" "^19.5.0" + +"@commitlint/load@^19.6.1": + version "19.6.1" + resolved "https://registry.yarnpkg.com/@commitlint/load/-/load-19.6.1.tgz#5fae8843a6048a2d3d1cc16da0af8ee532fa9db4" + integrity sha512-kE4mRKWWNju2QpsCWt428XBvUH55OET2N4QKQ0bF85qS/XbsRGG1MiTByDNlEVpEPceMkDr46LNH95DtRwcsfA== + dependencies: + "@commitlint/config-validator" "^19.5.0" + "@commitlint/execute-rule" "^19.5.0" + "@commitlint/resolve-extends" "^19.5.0" + "@commitlint/types" "^19.5.0" + chalk "^5.3.0" + cosmiconfig "^9.0.0" + cosmiconfig-typescript-loader "^6.1.0" + lodash.isplainobject "^4.0.6" + lodash.merge "^4.6.2" + lodash.uniq "^4.5.0" + +"@commitlint/message@^19.5.0": + version "19.5.0" + resolved "https://registry.yarnpkg.com/@commitlint/message/-/message-19.5.0.tgz#c062d9a1d2b3302c3a8cac25d6d1125ea9c019b2" + integrity sha512-R7AM4YnbxN1Joj1tMfCyBryOC5aNJBdxadTZkuqtWi3Xj0kMdutq16XQwuoGbIzL2Pk62TALV1fZDCv36+JhTQ== + +"@commitlint/parse@^19.5.0": + version "19.5.0" + resolved "https://registry.yarnpkg.com/@commitlint/parse/-/parse-19.5.0.tgz#b450dad9b5a95ac5ba472d6d0fdab822dce946fc" + integrity sha512-cZ/IxfAlfWYhAQV0TwcbdR1Oc0/r0Ik1GEessDJ3Lbuma/MRO8FRQX76eurcXtmhJC//rj52ZSZuXUg0oIX0Fw== + dependencies: + "@commitlint/types" "^19.5.0" + conventional-changelog-angular "^7.0.0" + conventional-commits-parser "^5.0.0" + +"@commitlint/read@^19.5.0": + version "19.5.0" + resolved "https://registry.yarnpkg.com/@commitlint/read/-/read-19.5.0.tgz#601f9f1afe69852b0f28aa81cd455b40979fad6b" + integrity sha512-TjS3HLPsLsxFPQj6jou8/CZFAmOP2y+6V4PGYt3ihbQKTY1Jnv0QG28WRKl/d1ha6zLODPZqsxLEov52dhR9BQ== + dependencies: + "@commitlint/top-level" "^19.5.0" + "@commitlint/types" "^19.5.0" + git-raw-commits "^4.0.0" + minimist "^1.2.8" + tinyexec "^0.3.0" + +"@commitlint/resolve-extends@^19.5.0": + version "19.5.0" + resolved "https://registry.yarnpkg.com/@commitlint/resolve-extends/-/resolve-extends-19.5.0.tgz#f3ec33e12d10df90cae0bfad8e593431fb61b18e" + integrity sha512-CU/GscZhCUsJwcKTJS9Ndh3AKGZTNFIOoQB2n8CmFnizE0VnEuJoum+COW+C1lNABEeqk6ssfc1Kkalm4bDklA== + dependencies: + "@commitlint/config-validator" "^19.5.0" + "@commitlint/types" "^19.5.0" + global-directory "^4.0.1" + import-meta-resolve "^4.0.0" + lodash.mergewith "^4.6.2" resolve-from "^5.0.0" - resolve-global "^1.0.0" -"@commitlint/rules@^16.0.0": - version "16.0.0" - resolved "https://registry.yarnpkg.com/@commitlint/rules/-/rules-16.0.0.tgz#79d28c3678d2d1f7f1cdbedaedb30b01a86ee75b" - integrity sha512-AOl0y2SBTdJ1bvIv8nwHvQKRT/jC1xb09C5VZwzHoT8sE8F54KDeEzPCwHQFgUcWdGLyS10kkOTAH2MyA8EIlg== +"@commitlint/rules@^19.6.0": + version "19.6.0" + resolved "https://registry.yarnpkg.com/@commitlint/rules/-/rules-19.6.0.tgz#2436da7974c3cf2a7236257f3ef5dd40c4d91312" + integrity sha512-1f2reW7lbrI0X0ozZMesS/WZxgPa4/wi56vFuJENBmed6mWq5KsheN/nxqnl/C23ioxpPO/PL6tXpiiFy5Bhjw== dependencies: - "@commitlint/ensure" "^16.0.0" - "@commitlint/message" "^16.0.0" - "@commitlint/to-lines" "^16.0.0" - "@commitlint/types" "^16.0.0" - execa "^5.0.0" + "@commitlint/ensure" "^19.5.0" + "@commitlint/message" "^19.5.0" + "@commitlint/to-lines" "^19.5.0" + "@commitlint/types" "^19.5.0" -"@commitlint/to-lines@^16.0.0": - version "16.0.0" - resolved "https://registry.yarnpkg.com/@commitlint/to-lines/-/to-lines-16.0.0.tgz#799980a89072302445baf595e20092fb86f0a58a" - integrity sha512-iN/qU38TCKU7uKOg6RXLpD49wNiuI0TqMqybHbjefUeP/Jmzxa8ishryj0uLyVdrAl1ZjGeD1ukXGMTtvqz8iA== - -"@commitlint/top-level@^16.0.0": - version "16.0.0" - resolved "https://registry.yarnpkg.com/@commitlint/top-level/-/top-level-16.0.0.tgz#7c2efc33cc37df839b3de558c0bc2eaddb64efe6" - integrity sha512-/Jt6NLxyFkpjL5O0jxurZPCHURZAm7cQCqikgPCwqPAH0TLgwqdHjnYipl8J+AGnAMGDip4FNLoYrtgIpZGBYw== - dependencies: - find-up "^5.0.0" +"@commitlint/to-lines@^19.5.0": + version "19.5.0" + resolved "https://registry.yarnpkg.com/@commitlint/to-lines/-/to-lines-19.5.0.tgz#e4b7f34f09064568c96a74de4f1fc9f466c4d472" + integrity sha512-R772oj3NHPkodOSRZ9bBVNq224DOxQtNef5Pl8l2M8ZnkkzQfeSTr4uxawV2Sd3ui05dUVzvLNnzenDBO1KBeQ== -"@commitlint/types@^16.0.0": - version "16.0.0" - resolved "https://registry.yarnpkg.com/@commitlint/types/-/types-16.0.0.tgz#3c133f106d36132756c464071a7f2290966727a3" - integrity sha512-+0FvYOAS39bJ4aKjnYn/7FD4DfWkmQ6G/06I4F0Gvu4KS5twirEg8mIcLhmeRDOOKn4Tp8PwpLwBiSA6npEMQA== +"@commitlint/top-level@^19.5.0": + version "19.5.0" + resolved "https://registry.yarnpkg.com/@commitlint/top-level/-/top-level-19.5.0.tgz#0017ffe39b5ba3611a1debd62efe28803601a14f" + integrity sha512-IP1YLmGAk0yWrImPRRc578I3dDUI5A2UBJx9FbSOjxe9sTlzFiwVJ+zeMLgAtHMtGZsC8LUnzmW1qRemkFU4ng== dependencies: - chalk "^4.0.0" + find-up "^7.0.0" -"@cspotcode/source-map-support@^0.8.0": - version "0.8.1" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" - integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== +"@commitlint/types@^19.5.0": + version "19.5.0" + resolved "https://registry.yarnpkg.com/@commitlint/types/-/types-19.5.0.tgz#c5084d1231d4dd50e40bdb656ee7601f691400b3" + integrity sha512-DSHae2obMSMkAtTBSOulg5X7/z+rGLxcXQIkg3OmWvY6wifojge5uVMydfhUvs7yQj+V7jNmRZ2Xzl8GJyqRgg== dependencies: - "@jridgewell/trace-mapping" "0.3.9" + "@types/conventional-commits-parser" "^5.0.0" + chalk "^5.3.0" "@esbuild/aix-ppc64@0.25.0": version "0.25.0" @@ -448,25 +429,94 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz#c8e119a30a7c8d60b9d2e22d2073722dde3b710b" integrity sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ== -"@eslint/eslintrc@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.0.tgz#99cc0a0584d72f1df38b900fb062ba995f395547" - integrity sha512-2ZPCc+uNbjV5ERJr+aKSPRwZgKd2z11x0EgLvb1PURmUrn9QNRXFqje0Ldq454PfAVyaJYyrDvvIKSFP4NnBog== +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz#d1145bf2c20132d6400495d6df4bf59362fd9d56" + integrity sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA== + dependencies: + eslint-visitor-keys "^3.4.3" + +"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.12.1": + version "4.12.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" + integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== + +"@eslint/config-array@^0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.19.2.tgz#3060b809e111abfc97adb0bb1172778b90cb46aa" + integrity sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w== + dependencies: + "@eslint/object-schema" "^2.1.6" + debug "^4.3.1" + minimatch "^3.1.2" + +"@eslint/core@^0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.12.0.tgz#5f960c3d57728be9f6c65bd84aa6aa613078798e" + integrity sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg== + dependencies: + "@types/json-schema" "^7.0.15" + +"@eslint/eslintrc@^3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.0.tgz#96a558f45842989cca7ea1ecd785ad5491193846" + integrity sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ== dependencies: ajv "^6.12.4" - debug "^4.1.1" - espree "^7.3.0" - globals "^12.1.0" - ignore "^4.0.6" + debug "^4.3.2" + espree "^10.0.1" + globals "^14.0.0" + ignore "^5.2.0" import-fresh "^3.2.1" - js-yaml "^3.13.1" - minimatch "^3.0.4" + js-yaml "^4.1.0" + minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@hutson/parse-repository-url@^3.0.0": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" - integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== +"@eslint/js@9.21.0", "@eslint/js@^9.21.0": + version "9.21.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.21.0.tgz#4303ef4e07226d87c395b8fad5278763e9c15c08" + integrity sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw== + +"@eslint/object-schema@^2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f" + integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA== + +"@eslint/plugin-kit@^0.2.7": + version "0.2.7" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz#9901d52c136fb8f375906a73dcc382646c3b6a27" + integrity sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g== + dependencies: + "@eslint/core" "^0.12.0" + levn "^0.4.1" + +"@humanfs/core@^0.19.1": + version "0.19.1" + resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" + integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== + +"@humanfs/node@^0.16.6": + version "0.16.6" + resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.6.tgz#ee2a10eaabd1131987bf0488fd9b820174cd765e" + integrity sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw== + dependencies: + "@humanfs/core" "^0.19.1" + "@humanwhocodes/retry" "^0.3.0" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/retry@^0.3.0": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.1.tgz#c72a5c76a9fbaf3488e231b13dc52c0da7bab42a" + integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA== + +"@humanwhocodes/retry@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.2.tgz#1860473de7dfa1546767448f333db80cb0ff2161" + integrity sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ== "@isaacs/cliui@^8.0.2": version "8.0.2" @@ -517,7 +567,7 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.24" -"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": +"@jridgewell/resolve-uri@^3.1.0": version "3.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== @@ -527,19 +577,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== -"@jridgewell/trace-mapping@0.3.9": - version "0.3.9" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" - integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping@^0.3.0", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": version "0.3.25" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" @@ -854,6 +896,106 @@ "@pnpm/network.ca-file" "^1.0.1" config-chain "^1.1.11" +"@rollup/rollup-android-arm-eabi@4.34.8": + version "4.34.8" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz#731df27dfdb77189547bcef96ada7bf166bbb2fb" + integrity sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw== + +"@rollup/rollup-android-arm64@4.34.8": + version "4.34.8" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz#4bea6db78e1f6927405df7fe0faf2f5095e01343" + integrity sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q== + +"@rollup/rollup-darwin-arm64@4.34.8": + version "4.34.8" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz#a7aab77d44be3c44a20f946e10160f84e5450e7f" + integrity sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q== + +"@rollup/rollup-darwin-x64@4.34.8": + version "4.34.8" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz#c572c024b57ee8ddd1b0851703ace9eb6cc0dd82" + integrity sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw== + +"@rollup/rollup-freebsd-arm64@4.34.8": + version "4.34.8" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz#cf74f8113b5a83098a5c026c165742277cbfb88b" + integrity sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA== + +"@rollup/rollup-freebsd-x64@4.34.8": + version "4.34.8" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz#39561f3a2f201a4ad6a01425b1ff5928154ecd7c" + integrity sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q== + +"@rollup/rollup-linux-arm-gnueabihf@4.34.8": + version "4.34.8" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz#980d6061e373bfdaeb67925c46d2f8f9b3de537f" + integrity sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g== + +"@rollup/rollup-linux-arm-musleabihf@4.34.8": + version "4.34.8" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz#f91a90f30dc00d5a64ac2d9bbedc829cd3cfaa78" + integrity sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA== + +"@rollup/rollup-linux-arm64-gnu@4.34.8": + version "4.34.8" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz#fac700fa5c38bc13a0d5d34463133093da4c92a0" + integrity sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A== + +"@rollup/rollup-linux-arm64-musl@4.34.8": + version "4.34.8" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz#f50ecccf8c78841ff6df1706bc4782d7f62bf9c3" + integrity sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q== + +"@rollup/rollup-linux-loongarch64-gnu@4.34.8": + version "4.34.8" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz#5869dc0b28242da6553e2b52af41374f4038cd6e" + integrity sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ== + +"@rollup/rollup-linux-powerpc64le-gnu@4.34.8": + version "4.34.8" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz#5cdd9f851ce1bea33d6844a69f9574de335f20b1" + integrity sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw== + +"@rollup/rollup-linux-riscv64-gnu@4.34.8": + version "4.34.8" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz#ef5dc37f4388f5253f0def43e1440ec012af204d" + integrity sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw== + +"@rollup/rollup-linux-s390x-gnu@4.34.8": + version "4.34.8" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz#7dbc3ccbcbcfb3e65be74538dfb6e8dd16178fde" + integrity sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA== + +"@rollup/rollup-linux-x64-gnu@4.34.8": + version "4.34.8" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz#5783fc0adcab7dc069692056e8ca8d83709855ce" + integrity sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA== + +"@rollup/rollup-linux-x64-musl@4.34.8": + version "4.34.8" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz#00b6c29b298197a384e3c659910b47943003a678" + integrity sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ== + +"@rollup/rollup-win32-arm64-msvc@4.34.8": + version "4.34.8" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz#cbfee01f1fe73791c35191a05397838520ca3cdd" + integrity sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ== + +"@rollup/rollup-win32-ia32-msvc@4.34.8": + version "4.34.8" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz#95cdbdff48fe6c948abcf6a1d500b2bd5ce33f62" + integrity sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w== + +"@rollup/rollup-win32-x64-msvc@4.34.8": + version "4.34.8" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz#4cdb2cfae69cdb7b1a3cc58778e820408075e928" + integrity sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g== + +"@rtsao/scc@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" + integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== + "@sec-ant/readable-stream@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz#60de891bb126abfdc5410fdc6166aca065f10a0c" @@ -1060,26 +1202,6 @@ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== -"@tsconfig/node10@^1.0.7": - version "1.0.8" - resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" - integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== - -"@tsconfig/node12@^1.0.7": - version "1.0.9" - resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c" - integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== - -"@tsconfig/node14@^1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" - integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== - -"@tsconfig/node16@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" - integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== - "@tufjs/canonical-json@2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz#a52f61a3d7374833fca945b2549bc30a2dd40d0a" @@ -1098,49 +1220,27 @@ resolved "https://registry.yarnpkg.com/@types/base64-js/-/base64-js-1.3.0.tgz#c939fdba49846861caf5a246b165dbf5698a317c" integrity sha512-ZmI0sZGAUNXUfMWboWwi4LcfpoVUYldyN6Oe0oJ5cCsHDU/LlRq8nQKPXhYLOx36QYSW9bNIb1vvRrD6K7Llgw== -"@types/chai-arrays@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/chai-arrays/-/chai-arrays-2.0.0.tgz#89faf96a5971e3b16d82c3afc07e1d84ecc4a360" - integrity sha512-5h5jnAC9C64YnD7WJpA5gBG7CppF/QmoWytOssJ6ysENllW49NBdpsTx6uuIBOpnzAnXThb8jBICgB62wezTLQ== - dependencies: - "@types/chai" "*" - -"@types/chai-as-promised@^7.1.4": - version "7.1.4" - resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-7.1.4.tgz#caf64e76fb056b8c8ced4b761ed499272b737601" - integrity sha512-1y3L1cHePcIm5vXkh1DSGf/zQq5n5xDKG1fpCvf18+uOkpce0Z1ozNFPkyWsVswK7ntN1sZBw3oU6gmN+pDUcA== - dependencies: - "@types/chai" "*" - -"@types/chai-like@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/chai-like/-/chai-like-1.1.1.tgz#c454039b0a2f92664fb5b7b7a2a66c3358783ae7" - integrity sha512-s46EZsupBuVhLn66DbRee5B0SELLmL4nFXVrBiV29BxLGm9Sh7Bful623j3AfiQRu2zAP4cnlZ3ETWB3eWc4bA== +"@types/conventional-commits-parser@^5.0.0": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.1.tgz#8cb81cf170853496cbc501a3b32dcf5e46ffb61a" + integrity sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ== dependencies: - "@types/chai" "*" - -"@types/chai@*", "@types/chai@^4.2.15": - version "4.2.15" - resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.15.tgz#b7a6d263c2cecf44b6de9a051cf496249b154553" - integrity sha512-rYff6FI+ZTKAPkJUoyz7Udq3GaoDZnxYDEvdEdFZASiA7PoErltHezDishqQiSDWrGxvxmplH304jyzQmjp0AQ== + "@types/node" "*" -"@types/eslint@7.2.7": - version "7.2.7" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.7.tgz#f7ef1cf0dceab0ae6f9a976a0a9af14ab1baca26" - integrity sha512-EHXbc1z2GoQRqHaAT7+grxlTJ3WE2YNeD6jlpPoRc83cCoThRY+NUWjCUZaYmk51OICkPXn2hhphcWcWXgNW0Q== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" +"@types/estree@1.0.6", "@types/estree@^1.0.0", "@types/estree@^1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== -"@types/estree@*": - version "0.0.46" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.46.tgz#0fb6bfbbeabd7a30880504993369c4bf1deab1fe" - integrity sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg== +"@types/json-schema@^7.0.15": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== -"@types/json-schema@*", "@types/json-schema@^7.0.3": - version "7.0.7" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" - integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== "@types/jsonwebtoken@^9.0.8": version "9.0.8" @@ -1150,48 +1250,23 @@ "@types/ms" "*" "@types/node" "*" -"@types/minimist@^1.2.0": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" - integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== - -"@types/mocha@^9.0.0": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.0.0.tgz#3205bcd15ada9bc681ac20bef64e9e6df88fd297" - integrity sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA== - "@types/ms@*": version "2.1.0" resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== "@types/node@*": - version "22.13.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.4.tgz#3fe454d77cd4a2d73c214008b3e331bfaaf5038a" - integrity sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg== + version "22.13.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.5.tgz#23add1d71acddab2c6a4d31db89c0f98d330b511" + integrity sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg== dependencies: undici-types "~6.20.0" -"@types/node@^16.11.11": - version "16.11.11" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.11.tgz#6ea7342dfb379ea1210835bada87b3c512120234" - integrity sha512-KB0sixD67CeecHC33MYn+eYARkqTheIRNuu97y2XMjR7Wu3XibO1vaY6VBV6O/a89SPI81cEUIYT87UqUWlZNw== - -"@types/normalize-package-data@^2.4.0", "@types/normalize-package-data@^2.4.3": +"@types/normalize-package-data@^2.4.3": version "2.4.4" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== -"@types/parse-json@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" - integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== - -"@types/prettier@^2.2.2": - version "2.2.2" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.2.2.tgz#e2280c89ddcbeef340099d6968d8c86ba155fdf6" - integrity sha512-i99hy7Ki19EqVOl77WplDrvgNugHnsSjECVR/wUrzw2TJXz1zlUfT2ngGckR6xN7yFYaijsMAqPkOLx9HgUqHg== - "@types/sinon@^10.0.6": version "10.0.6" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.6.tgz#bc3faff5154e6ecb69b797d311b7cf0c1b523a1d" @@ -1211,100 +1286,147 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@^4.17.0": - version "4.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.17.0.tgz#6f856eca4e6a52ce9cf127dfd349096ad936aa2d" - integrity sha512-/fKFDcoHg8oNan39IKFOb5WmV7oWhQe1K6CDaAVfJaNWEhmfqlA24g+u1lqU5bMH7zuNasfMId4LaYWC5ijRLw== - dependencies: - "@typescript-eslint/experimental-utils" "4.17.0" - "@typescript-eslint/scope-manager" "4.17.0" - debug "^4.1.1" - functional-red-black-tree "^1.0.1" - lodash "^4.17.15" - regexpp "^3.0.0" - semver "^7.3.2" - tsutils "^3.17.1" - -"@typescript-eslint/experimental-utils@4.17.0": - version "4.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.17.0.tgz#762c44aaa1a6a3c05b6d63a8648fb89b89f84c80" - integrity sha512-ZR2NIUbnIBj+LGqCFGQ9yk2EBQrpVVFOh9/Kd0Lm6gLpSAcCuLLe5lUCibKGCqyH9HPwYC0GIJce2O1i8VYmWA== - dependencies: - "@types/json-schema" "^7.0.3" - "@typescript-eslint/scope-manager" "4.17.0" - "@typescript-eslint/types" "4.17.0" - "@typescript-eslint/typescript-estree" "4.17.0" - eslint-scope "^5.0.0" - eslint-utils "^2.0.0" - -"@typescript-eslint/experimental-utils@^2.32.0": - version "2.34.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz#d3524b644cdb40eebceca67f8cf3e4cc9c8f980f" - integrity sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA== - dependencies: - "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.34.0" - eslint-scope "^5.0.0" - eslint-utils "^2.0.0" - -"@typescript-eslint/parser@^4.17.0": - version "4.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.17.0.tgz#141b647ffc72ebebcbf9b0fe6087f65b706d3215" - integrity sha512-KYdksiZQ0N1t+6qpnl6JeK9ycCFprS9xBAiIrw4gSphqONt8wydBw4BXJi3C11ywZmyHulvMaLjWsxDjUSDwAw== - dependencies: - "@typescript-eslint/scope-manager" "4.17.0" - "@typescript-eslint/types" "4.17.0" - "@typescript-eslint/typescript-estree" "4.17.0" - debug "^4.1.1" +"@typescript-eslint/eslint-plugin@8.25.0": + version "8.25.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.25.0.tgz#5e1d56f067e5808fa82d1b75bced82396e868a14" + integrity sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA== + dependencies: + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "8.25.0" + "@typescript-eslint/type-utils" "8.25.0" + "@typescript-eslint/utils" "8.25.0" + "@typescript-eslint/visitor-keys" "8.25.0" + graphemer "^1.4.0" + ignore "^5.3.1" + natural-compare "^1.4.0" + ts-api-utils "^2.0.1" -"@typescript-eslint/scope-manager@4.17.0": - version "4.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.17.0.tgz#f4edf94eff3b52a863180f7f89581bf963e3d37d" - integrity sha512-OJ+CeTliuW+UZ9qgULrnGpPQ1bhrZNFpfT/Bc0pzNeyZwMik7/ykJ0JHnQ7krHanFN9wcnPK89pwn84cRUmYjw== +"@typescript-eslint/parser@8.25.0": + version "8.25.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.25.0.tgz#58fb81c7b7a35184ba17583f3d7ac6c4f3d95be8" + integrity sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg== dependencies: - "@typescript-eslint/types" "4.17.0" - "@typescript-eslint/visitor-keys" "4.17.0" - -"@typescript-eslint/types@4.17.0": - version "4.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.17.0.tgz#f57d8fc7f31b348db946498a43050083d25f40ad" - integrity sha512-RN5z8qYpJ+kXwnLlyzZkiJwfW2AY458Bf8WqllkondQIcN2ZxQowAToGSd9BlAUZDB5Ea8I6mqL2quGYCLT+2g== + "@typescript-eslint/scope-manager" "8.25.0" + "@typescript-eslint/types" "8.25.0" + "@typescript-eslint/typescript-estree" "8.25.0" + "@typescript-eslint/visitor-keys" "8.25.0" + debug "^4.3.4" -"@typescript-eslint/typescript-estree@2.34.0": - version "2.34.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz#14aeb6353b39ef0732cc7f1b8285294937cf37d5" - integrity sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg== +"@typescript-eslint/scope-manager@8.25.0": + version "8.25.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.25.0.tgz#ac3805077aade898e98ca824294c998545597df3" + integrity sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg== dependencies: - debug "^4.1.1" - eslint-visitor-keys "^1.1.0" - glob "^7.1.6" - is-glob "^4.0.1" - lodash "^4.17.15" - semver "^7.3.2" - tsutils "^3.17.1" + "@typescript-eslint/types" "8.25.0" + "@typescript-eslint/visitor-keys" "8.25.0" -"@typescript-eslint/typescript-estree@4.17.0": - version "4.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.17.0.tgz#b835d152804f0972b80dbda92477f9070a72ded1" - integrity sha512-lRhSFIZKUEPPWpWfwuZBH9trYIEJSI0vYsrxbvVvNyIUDoKWaklOAelsSkeh3E2VBSZiNe9BZ4E5tYBZbUczVQ== +"@typescript-eslint/type-utils@8.25.0": + version "8.25.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.25.0.tgz#ee0d2f67c80af5ae74b5d6f977e0f8ded0059677" + integrity sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g== dependencies: - "@typescript-eslint/types" "4.17.0" - "@typescript-eslint/visitor-keys" "4.17.0" - debug "^4.1.1" - globby "^11.0.1" - is-glob "^4.0.1" - semver "^7.3.2" - tsutils "^3.17.1" + "@typescript-eslint/typescript-estree" "8.25.0" + "@typescript-eslint/utils" "8.25.0" + debug "^4.3.4" + ts-api-utils "^2.0.1" -"@typescript-eslint/visitor-keys@4.17.0": - version "4.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.17.0.tgz#9c304cfd20287c14a31d573195a709111849b14d" - integrity sha512-WfuMN8mm5SSqXuAr9NM+fItJ0SVVphobWYkWOwQ1odsfC014Vdxk/92t4JwS1Q6fCA/ABfCKpa3AVtpUKTNKGQ== - dependencies: - "@typescript-eslint/types" "4.17.0" - eslint-visitor-keys "^2.0.0" +"@typescript-eslint/types@8.25.0": + version "8.25.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.25.0.tgz#f91512c2f532b1d6a8826cadd0b0e5cd53cf97e0" + integrity sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw== -JSONStream@^1.0.4: +"@typescript-eslint/typescript-estree@8.25.0": + version "8.25.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.25.0.tgz#d8409c63abddd4cf5b93c031b24b9edc1c7c1299" + integrity sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q== + dependencies: + "@typescript-eslint/types" "8.25.0" + "@typescript-eslint/visitor-keys" "8.25.0" + debug "^4.3.4" + fast-glob "^3.3.2" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^2.0.1" + +"@typescript-eslint/utils@8.25.0": + version "8.25.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.25.0.tgz#3ea2f9196a915ef4daa2c8eafd44adbd7d56d08a" + integrity sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@typescript-eslint/scope-manager" "8.25.0" + "@typescript-eslint/types" "8.25.0" + "@typescript-eslint/typescript-estree" "8.25.0" + +"@typescript-eslint/visitor-keys@8.25.0": + version "8.25.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.25.0.tgz#e8646324cd1793f96e02669cb717a05319403164" + integrity sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ== + dependencies: + "@typescript-eslint/types" "8.25.0" + eslint-visitor-keys "^4.2.0" + +"@vitest/expect@3.0.7": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-3.0.7.tgz#3490936bc1e97fc21d53441518d51cb7116c698a" + integrity sha512-QP25f+YJhzPfHrHfYHtvRn+uvkCFCqFtW9CktfBxmB+25QqWsx7VB2As6f4GmwllHLDhXNHvqedwhvMmSnNmjw== + dependencies: + "@vitest/spy" "3.0.7" + "@vitest/utils" "3.0.7" + chai "^5.2.0" + tinyrainbow "^2.0.0" + +"@vitest/mocker@3.0.7": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-3.0.7.tgz#49a99e300bcb64dc514a43a92325fce51cd88099" + integrity sha512-qui+3BLz9Eonx4EAuR/i+QlCX6AUZ35taDQgwGkK/Tw6/WgwodSrjN1X2xf69IA/643ZX5zNKIn2svvtZDrs4w== + dependencies: + "@vitest/spy" "3.0.7" + estree-walker "^3.0.3" + magic-string "^0.30.17" + +"@vitest/pretty-format@3.0.7", "@vitest/pretty-format@^3.0.7": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-3.0.7.tgz#1780516ebb4e40dd89e60b9fc7ffcbd9cba0fc22" + integrity sha512-CiRY0BViD/V8uwuEzz9Yapyao+M9M008/9oMOSQydwbwb+CMokEq3XVaF3XK/VWaOK0Jm9z7ENhybg70Gtxsmg== + dependencies: + tinyrainbow "^2.0.0" + +"@vitest/runner@3.0.7": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-3.0.7.tgz#65b64ba5f3291fdca4670bf9e50627200ea33b7b" + integrity sha512-WeEl38Z0S2ZcuRTeyYqaZtm4e26tq6ZFqh5y8YD9YxfWuu0OFiGFUbnxNynwLjNRHPsXyee2M9tV7YxOTPZl2g== + dependencies: + "@vitest/utils" "3.0.7" + pathe "^2.0.3" + +"@vitest/snapshot@3.0.7": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-3.0.7.tgz#df34e3c5820bdd54bba8919291a182df5c6b8c6f" + integrity sha512-eqTUryJWQN0Rtf5yqCGTQWsCFOQe4eNz5Twsu21xYEcnFJtMU5XvmG0vgebhdLlrHQTSq5p8vWHJIeJQV8ovsA== + dependencies: + "@vitest/pretty-format" "3.0.7" + magic-string "^0.30.17" + pathe "^2.0.3" + +"@vitest/spy@3.0.7": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-3.0.7.tgz#6fcc100c23fb50b5e2d1d09a333245586364f67b" + integrity sha512-4T4WcsibB0B6hrKdAZTM37ekuyFZt2cGbEGd2+L0P8ov15J1/HUsUaqkXEQPNAWr4BtPPe1gI+FYfMHhEKfR8w== + dependencies: + tinyspy "^3.0.2" + +"@vitest/utils@3.0.7": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-3.0.7.tgz#56268acac1027ead938150eceb2527c61d2fa204" + integrity sha512-xePVpCRfooFX3rANQjwoditoXgWb1MaFbzmGuPP59MK6i13mrnDw/yEIyJudLeW6/38mCNcwCiJIGmpDPibAIg== + dependencies: + "@vitest/pretty-format" "3.0.7" + loupe "^3.1.3" + tinyrainbow "^2.0.0" + +JSONStream@^1.3.5: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== @@ -1317,30 +1439,15 @@ abbrev@^3.0.0: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-3.0.0.tgz#c29a6337e167ac61a84b41b80461b29c5c271a27" integrity sha512-+/kfrslGQ7TNV2ecmQwMJj/B65g5KVq1/L3SGVZ3tCYGqlzFuFCGBZJtMP99wH3NpEUyAjn0zPdPUg0D+DwrOA== -acorn-jsx@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" - integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== - -acorn-walk@^8.1.1: - version "8.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" - integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== - -acorn@^7.4.0: - version "7.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" - integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.4.1: - version "8.8.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" - integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== - -add-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" - integrity sha1-anmQQ3ynNtXhKI25K9MmbV9csqo= +acorn@^8.14.0: + version "8.14.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" + integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== agent-base@^7.1.0, agent-base@^7.1.2: version "7.1.3" @@ -1363,7 +1470,7 @@ aggregate-error@^5.0.0: clean-stack "^5.2.0" indent-string "^5.0.0" -ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.6: +ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -1373,20 +1480,15 @@ ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.6: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^7.0.2: - version "7.2.1" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.2.1.tgz#a5ac226171912447683524fa2f1248fcf8bac83d" - integrity sha512-+nu0HDv7kNSOua9apAVc979qd932rrZeb3WOvoiD31A/p1mIE5/9bN2027pE2rOPYEdS3UHzsvof4hY+lM9/WQ== +ajv@^8.11.0: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== dependencies: - fast-deep-equal "^3.1.1" + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" json-schema-traverse "^1.0.0" require-from-string "^2.0.2" - uri-js "^4.2.2" - -ansi-colors@^4.1.1, ansi-colors@^4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" - integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== ansi-escapes@^6.2.0: version "6.2.0" @@ -1436,14 +1538,6 @@ any-promise@^1.0.0: resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== -anymatch@~3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" - integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - append-transform@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-2.0.0.tgz#99d9d29c7b38391e6f428d28ce136551f0b77e12" @@ -1461,11 +1555,6 @@ archy@^1.0.0, archy@~1.0.0: resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= -arg@^4.1.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" - integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== - argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -1483,36 +1572,98 @@ argv-formatter@~1.0.0: resolved "https://registry.yarnpkg.com/argv-formatter/-/argv-formatter-1.0.0.tgz#a0ca0cbc29a5b73e836eebe1cbf6c5e0e4eb82f9" integrity sha512-F2+Hkm9xFaRg+GkaNnbwXNDV5O6pnCFEmqyhvfC/Ic5LbgOWjJh3L+mN/s91rxVL3znE7DYVpW0GJFT+4YBgWw== +array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b" + integrity sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw== + dependencies: + call-bound "^1.0.3" + is-array-buffer "^3.0.5" + array-ify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" integrity sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4= -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -arrify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" - integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= - -assertion-error@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" - integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== +array-includes@^3.1.8: + version "3.1.8" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" + integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + is-string "^1.0.7" + +array.prototype.findlastindex@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz#8c35a755c72908719453f87145ca011e39334d0d" + integrity sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-shim-unscopables "^1.0.2" + +array.prototype.flat@^1.3.2: + version "1.3.3" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz#534aaf9e6e8dd79fb6b9a9917f839ef1ec63afe5" + integrity sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-shim-unscopables "^1.0.2" + +array.prototype.flatmap@^1.3.2: + version "1.3.3" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz#712cc792ae70370ae40586264629e33aab5dd38b" + integrity sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-shim-unscopables "^1.0.2" + +arraybuffer.prototype.slice@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz#9d760d84dbdd06d0cbf92c8849615a1a7ab3183c" + integrity sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + is-array-buffer "^3.0.4" + +assertion-error@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" + integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== +async-function@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async-function/-/async-function-1.0.0.tgz#509c9fca60eaf85034c6829838188e4e4c8ffb2b" + integrity sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA== asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + axios@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.0.tgz#f1e5292f26b2fd5c2e66876adc5b06cdbd7d2102" @@ -1522,11 +1673,6 @@ axios@^1.6.0: form-data "^4.0.0" proxy-from-env "^1.1.0" -bail@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776" - integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ== - balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -1553,7 +1699,7 @@ bin-links@^5.0.0: read-cmd-shim "^5.0.0" write-file-atomic "^6.0.0" -binary-extensions@^2.0.0, binary-extensions@^2.3.0: +binary-extensions@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== @@ -1578,18 +1724,13 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.2, braces@^3.0.3, braces@~3.0.2: +braces@^3.0.2, braces@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: fill-range "^7.1.1" -browser-stdout@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" - integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== - browserslist@^4.24.0: version "4.24.4" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b" @@ -1605,10 +1746,10 @@ buffer-equal-constant-time@1.0.1: resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +cac@^6.7.14: + version "6.7.14" + resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" + integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== cacache@^19.0.0, cacache@^19.0.1: version "19.0.1" @@ -1638,75 +1779,64 @@ caching-transform@^4.0.0: package-hash "^4.0.0" write-file-atomic "^3.0.0" +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bind@^1.0.7, call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" + get-intrinsic "^1.2.4" + set-function-length "^1.2.2" + +call-bound@^1.0.2, call-bound@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.3.tgz#41cfd032b593e39176a71533ab4f384aa04fd681" + integrity sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA== + dependencies: + call-bind-apply-helpers "^1.0.1" + get-intrinsic "^1.2.6" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camelcase-keys@^6.2.2: - version "6.2.2" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" - integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg== - dependencies: - camelcase "^5.3.1" - map-obj "^4.0.0" - quick-lru "^4.0.1" - camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -camelcase@^6.0.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" - integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== - caniuse-lite@^1.0.30001688: version "1.0.30001699" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001699.tgz#a102cf330d153bf8c92bfb5be3cd44c0a89c8c12" integrity sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w== -chai-arrays@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/chai-arrays/-/chai-arrays-2.2.0.tgz#571479cbc5eca81605ed4eed1e8a2a28552d2a25" - integrity sha512-4awrdGI2EH8owJ9I58PXwG4N56/FiM8bsn4CVSNEgr4GKAM6Kq5JPVApUbhUBjDakbZNuRvV7quRSC38PWq/tg== - -chai-as-promised@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/chai-as-promised/-/chai-as-promised-7.1.1.tgz#08645d825deb8696ee61725dbf590c012eb00ca0" - integrity sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA== - dependencies: - check-error "^1.0.2" - -chai-like@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/chai-like/-/chai-like-1.1.1.tgz#8c558a414c34514e814d497c772547ceb7958f64" - integrity sha512-VKa9z/SnhXhkT1zIjtPACFWSoWsqVoaz1Vg+ecrKo5DCKVlgL30F/pEyEvXPBOVwCgLZcWUleCM/C1okaKdTTA== - -chai-sorted@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/chai-sorted/-/chai-sorted-0.2.0.tgz#1fee8f07fef4b0043bdff9e07022a0812c12ef55" - integrity sha1-H+6PB/70sAQ73/ngcCKggSwS71U= - -chai@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.4.tgz#b55e655b31e1eac7099be4c08c21964fce2e6c49" - integrity sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA== +chai@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/chai/-/chai-5.2.0.tgz#1358ee106763624114addf84ab02697e411c9c05" + integrity sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw== dependencies: - assertion-error "^1.1.0" - check-error "^1.0.2" - deep-eql "^3.0.1" - get-func-name "^2.0.0" - pathval "^1.1.1" - type-detect "^4.0.5" + assertion-error "^2.0.1" + check-error "^2.1.1" + deep-eql "^5.0.1" + loupe "^3.1.0" + pathval "^2.0.0" chalk@5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== -chalk@^2.0.0, chalk@^2.3.2, chalk@^2.4.2: +chalk@^2.3.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -1715,7 +1845,7 @@ chalk@^2.0.0, chalk@^2.3.2, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: +chalk@^4.0.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -1733,40 +1863,10 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== -character-entities-legacy@^1.0.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1" - integrity sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA== - -character-entities@^1.0.0: - version "1.2.4" - resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b" - integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw== - -character-reference-invalid@^1.0.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" - integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== - -check-error@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" - integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= - -chokidar@^3.5.3: - version "3.6.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" - integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" +check-error@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc" + integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw== chownr@^2.0.0: version "2.0.0" @@ -1778,11 +1878,6 @@ chownr@^3.0.0: resolved "https://registry.yarnpkg.com/chownr/-/chownr-3.0.0.tgz#9855e64ecd240a9cc4267ce8a4aa5d24a1da15e4" integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g== -ci-info@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" - integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== - ci-info@^4.0.0, ci-info@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.1.0.tgz#92319d2fa29d2620180ea5afed31f589bc98cf83" @@ -1883,11 +1978,6 @@ cmd-shim@^7.0.0: resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-7.0.0.tgz#23bcbf69fff52172f7e7c02374e18fb215826d95" integrity sha512-rtpaCbr164TPPh+zFdkWpCyZuKkjpAzODfaZCf/SVJZzJN+4bHQb/LP3Jzq5/+84um3XXY8r548XiWKSborwVw== -collapse-white-space@^1.0.2: - version "1.0.6" - resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287" - integrity sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ== - color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -1947,26 +2037,11 @@ compare-func@^2.0.0: array-ify "^1.0.0" dot-prop "^5.1.0" -compare-versions@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62" - integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA== - concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" - integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.0.2" - typedarray "^0.0.6" - concurrently@^9.1.2: version "9.1.2" resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-9.1.2.tgz#22d9109296961eaee773e12bfb1ce9a66bc9836c" @@ -1988,13 +2063,12 @@ config-chain@^1.1.11: ini "^1.3.4" proto-list "~1.2.1" -conventional-changelog-angular@^5.0.11, conventional-changelog-angular@^5.0.12: - version "5.0.13" - resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz#896885d63b914a70d4934b59d2fe7bde1832b28c" - integrity sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA== +conventional-changelog-angular@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz#5eec8edbff15aa9b1680a8dcfbd53e2d7eb2ba7a" + integrity sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ== dependencies: compare-func "^2.0.0" - q "^1.5.1" conventional-changelog-angular@^8.0.0: version "8.0.0" @@ -2003,42 +2077,12 @@ conventional-changelog-angular@^8.0.0: dependencies: compare-func "^2.0.0" -conventional-changelog-atom@^2.0.8: - version "2.0.8" - resolved "https://registry.yarnpkg.com/conventional-changelog-atom/-/conventional-changelog-atom-2.0.8.tgz#a759ec61c22d1c1196925fca88fe3ae89fd7d8de" - integrity sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw== - dependencies: - q "^1.5.1" - -conventional-changelog-codemirror@^2.0.8: - version "2.0.8" - resolved "https://registry.yarnpkg.com/conventional-changelog-codemirror/-/conventional-changelog-codemirror-2.0.8.tgz#398e9530f08ce34ec4640af98eeaf3022eb1f7dc" - integrity sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw== - dependencies: - q "^1.5.1" - -conventional-changelog-config-spec@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/conventional-changelog-config-spec/-/conventional-changelog-config-spec-2.1.0.tgz#874a635287ef8b581fd8558532bf655d4fb59f2d" - integrity sha512-IpVePh16EbbB02V+UA+HQnnPIohgXvJRxHcS5+Uwk4AT5LjzCZJm5sp/yqs5C6KZJ1jMsV4paEV13BN1pvDuxQ== - -conventional-changelog-conventionalcommits@4.6.1: - version "4.6.1" - resolved "https://registry.yarnpkg.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.6.1.tgz#f4c0921937050674e578dc7875f908351ccf4014" - integrity sha512-lzWJpPZhbM1R0PIzkwzGBCnAkH5RKJzJfFQZcl/D+2lsJxAwGnDKBqn/F4C1RD31GJNn8NuKWQzAZDAVXPp2Mw== - dependencies: - compare-func "^2.0.0" - lodash "^4.17.15" - q "^1.5.1" - -conventional-changelog-conventionalcommits@^4.3.1, conventional-changelog-conventionalcommits@^4.5.0: - version "4.6.3" - resolved "https://registry.yarnpkg.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.6.3.tgz#0765490f56424b46f6cb4db9135902d6e5a36dc2" - integrity sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g== +conventional-changelog-conventionalcommits@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz#aa5da0f1b2543094889e8cf7616ebe1a8f5c70d5" + integrity sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w== dependencies: compare-func "^2.0.0" - lodash "^4.17.15" - q "^1.5.1" conventional-changelog-conventionalcommits@^8.0.0: version "8.0.0" @@ -2047,82 +2091,6 @@ conventional-changelog-conventionalcommits@^8.0.0: dependencies: compare-func "^2.0.0" -conventional-changelog-core@^4.2.1: - version "4.2.4" - resolved "https://registry.yarnpkg.com/conventional-changelog-core/-/conventional-changelog-core-4.2.4.tgz#e50d047e8ebacf63fac3dc67bf918177001e1e9f" - integrity sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg== - dependencies: - add-stream "^1.0.0" - conventional-changelog-writer "^5.0.0" - conventional-commits-parser "^3.2.0" - dateformat "^3.0.0" - get-pkg-repo "^4.0.0" - git-raw-commits "^2.0.8" - git-remote-origin-url "^2.0.0" - git-semver-tags "^4.1.1" - lodash "^4.17.15" - normalize-package-data "^3.0.0" - q "^1.5.1" - read-pkg "^3.0.0" - read-pkg-up "^3.0.0" - through2 "^4.0.0" - -conventional-changelog-ember@^2.0.9: - version "2.0.9" - resolved "https://registry.yarnpkg.com/conventional-changelog-ember/-/conventional-changelog-ember-2.0.9.tgz#619b37ec708be9e74a220f4dcf79212ae1c92962" - integrity sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A== - dependencies: - q "^1.5.1" - -conventional-changelog-eslint@^3.0.9: - version "3.0.9" - resolved "https://registry.yarnpkg.com/conventional-changelog-eslint/-/conventional-changelog-eslint-3.0.9.tgz#689bd0a470e02f7baafe21a495880deea18b7cdb" - integrity sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA== - dependencies: - q "^1.5.1" - -conventional-changelog-express@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/conventional-changelog-express/-/conventional-changelog-express-2.0.6.tgz#420c9d92a347b72a91544750bffa9387665a6ee8" - integrity sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ== - dependencies: - q "^1.5.1" - -conventional-changelog-jquery@^3.0.11: - version "3.0.11" - resolved "https://registry.yarnpkg.com/conventional-changelog-jquery/-/conventional-changelog-jquery-3.0.11.tgz#d142207400f51c9e5bb588596598e24bba8994bf" - integrity sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw== - dependencies: - q "^1.5.1" - -conventional-changelog-jshint@^2.0.9: - version "2.0.9" - resolved "https://registry.yarnpkg.com/conventional-changelog-jshint/-/conventional-changelog-jshint-2.0.9.tgz#f2d7f23e6acd4927a238555d92c09b50fe3852ff" - integrity sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA== - dependencies: - compare-func "^2.0.0" - q "^1.5.1" - -conventional-changelog-preset-loader@^2.3.4: - version "2.3.4" - resolved "https://registry.yarnpkg.com/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz#14a855abbffd59027fd602581f1f34d9862ea44c" - integrity sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g== - -conventional-changelog-writer@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-5.0.1.tgz#e0757072f045fe03d91da6343c843029e702f359" - integrity sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ== - dependencies: - conventional-commits-filter "^2.0.7" - dateformat "^3.0.0" - handlebars "^4.7.7" - json-stringify-safe "^5.0.1" - lodash "^4.17.15" - meow "^8.0.0" - semver "^6.0.0" - split "^1.0.0" - through2 "^4.0.0" - conventional-changelog-writer@^8.0.0: version "8.0.1" resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-8.0.1.tgz#656e156ea0ab02b3bb574b7073beeb4f81c5b4bb" @@ -2133,47 +2101,20 @@ conventional-changelog-writer@^8.0.0: meow "^13.0.0" semver "^7.5.2" -conventional-changelog@3.1.24: - version "3.1.24" - resolved "https://registry.yarnpkg.com/conventional-changelog/-/conventional-changelog-3.1.24.tgz#ebd180b0fd1b2e1f0095c4b04fd088698348a464" - integrity sha512-ed6k8PO00UVvhExYohroVPXcOJ/K1N0/drJHx/faTH37OIZthlecuLIRX/T6uOp682CAoVoFpu+sSEaeuH6Asg== - dependencies: - conventional-changelog-angular "^5.0.12" - conventional-changelog-atom "^2.0.8" - conventional-changelog-codemirror "^2.0.8" - conventional-changelog-conventionalcommits "^4.5.0" - conventional-changelog-core "^4.2.1" - conventional-changelog-ember "^2.0.9" - conventional-changelog-eslint "^3.0.9" - conventional-changelog-express "^2.0.6" - conventional-changelog-jquery "^3.0.11" - conventional-changelog-jshint "^2.0.9" - conventional-changelog-preset-loader "^2.3.4" - -conventional-commits-filter@^2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/conventional-commits-filter/-/conventional-commits-filter-2.0.7.tgz#f8d9b4f182fce00c9af7139da49365b136c8a0b3" - integrity sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA== - dependencies: - lodash.ismatch "^4.4.0" - modify-values "^1.0.0" - conventional-commits-filter@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/conventional-commits-filter/-/conventional-commits-filter-5.0.0.tgz#72811f95d379e79d2d39d5c0c53c9351ef284e86" integrity sha512-tQMagCOC59EVgNZcC5zl7XqO30Wki9i9J3acbUvkaosCT6JX3EeFwJD7Qqp4MCikRnzS18WXV3BLIQ66ytu6+Q== -conventional-commits-parser@^3.2.0, conventional-commits-parser@^3.2.2: - version "3.2.4" - resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-3.2.4.tgz#a7d3b77758a202a9b2293d2112a8d8052c740972" - integrity sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q== +conventional-commits-parser@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz#57f3594b81ad54d40c1b4280f04554df28627d9a" + integrity sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA== dependencies: - JSONStream "^1.0.4" - is-text-path "^1.0.1" - lodash "^4.17.15" - meow "^8.0.0" - split2 "^3.0.0" - through2 "^4.0.0" + JSONStream "^1.3.5" + is-text-path "^2.0.0" + meow "^12.0.1" + split2 "^4.0.0" conventional-commits-parser@^6.0.0: version "6.1.0" @@ -2182,20 +2123,6 @@ conventional-commits-parser@^6.0.0: dependencies: meow "^13.0.0" -conventional-recommended-bump@6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/conventional-recommended-bump/-/conventional-recommended-bump-6.1.0.tgz#cfa623285d1de554012f2ffde70d9c8a22231f55" - integrity sha512-uiApbSiNGM/kkdL9GTOLAqC4hbptObFo4wW2QRyHsKciGAfQuLU1ShZ1BIVI/+K2BE/W1AWYQMCXAsv4dyKPaw== - dependencies: - concat-stream "^2.0.0" - conventional-changelog-preset-loader "^2.3.4" - conventional-commits-filter "^2.0.7" - conventional-commits-parser "^3.2.0" - git-raw-commits "^2.0.8" - git-semver-tags "^4.1.1" - meow "^8.0.0" - q "^1.5.1" - convert-hrtime@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/convert-hrtime/-/convert-hrtime-5.0.0.tgz#f2131236d4598b95de856926a67100a0a97e9fa3" @@ -2213,24 +2140,12 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== -cosmiconfig-typescript-loader@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-1.0.3.tgz#528f2bb3e6b6705020dc42df659f24837e75b611" - integrity sha512-ARo21VjxdacJUcHxgVMEYNIoVPYiuKOEwWBIYej4M22+pEbe3LzKgmht2UPM+0u7/T/KnZf2r/5IzHv2Nwz+/w== +cosmiconfig-typescript-loader@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.1.0.tgz#7f644503e1c2bff90aed2d29a637008f279646bb" + integrity sha512-tJ1w35ZRUiM5FeTzT7DtYWAFFv37ZLqSRkGi2oeCK1gPhvaWjkAtfXvLmvE1pRfxxp9aQo6ba/Pvg1dKj05D4g== dependencies: - cosmiconfig "^7" - ts-node "^10.4.0" - -cosmiconfig@^7, cosmiconfig@^7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" - integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== - dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.2.1" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.10.0" + jiti "^2.4.1" cosmiconfig@^9.0.0: version "9.0.0" @@ -2242,15 +2157,10 @@ cosmiconfig@^9.0.0: js-yaml "^4.1.0" parse-json "^5.2.0" -create-require@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" - integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== - -cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== +cross-spawn@^7.0.0, cross-spawn@^7.0.3, cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" @@ -2268,17 +2178,39 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -dargs@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" - integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== +dargs@^8.0.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/dargs/-/dargs-8.1.0.tgz#a34859ea509cbce45485e5aa356fef70bfcc7272" + integrity sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw== -dateformat@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" - integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== +data-view-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz#211a03ba95ecaf7798a8c7198d79536211f88570" + integrity sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-data-view "^1.0.2" + +data-view-byte-length@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz#9e80f7ca52453ce3e93d25a35318767ea7704735" + integrity sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-data-view "^1.0.2" -debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4, debug@^4.3.5, debug@^4.3.6: +data-view-byte-offset@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz#068307f9b71ab76dbbe10291389e020856606191" + integrity sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.6, debug@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== @@ -2292,30 +2224,22 @@ debug@4.3.4: dependencies: ms "2.1.2" -decamelize-keys@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" - integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk= +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: - decamelize "^1.1.0" - map-obj "^1.0.0" + ms "^2.1.1" -decamelize@^1.1.0, decamelize@^1.2.0: +decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= -decamelize@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" - integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== - -deep-eql@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" - integrity sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw== - dependencies: - type-detect "^4.0.0" +deep-eql@^5.0.1: + version "5.0.2" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341" + integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q== deep-extend@^0.6.0: version "0.6.0" @@ -2334,27 +2258,30 @@ default-require-extensions@^3.0.0: dependencies: strip-bom "^4.0.0" +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -detect-indent@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" - integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== - -detect-newline@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" - integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== - -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - -diff@^5.0.0, diff@^5.1.0, diff@^5.2.0: +diff@^5.0.0, diff@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== @@ -2366,10 +2293,10 @@ dir-glob@^3.0.0, dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== dependencies: esutils "^2.0.2" @@ -2385,13 +2312,14 @@ dotenv@^8.2.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== -dotgitignore@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/dotgitignore/-/dotgitignore-2.1.0.tgz#a4b15a4e4ef3cf383598aaf1dfa4a04bcc089b7b" - integrity sha512-sCm11ak2oY6DglEPpCB8TixLjWAxd3kJTs6UIcSasNYxXdFPV+YKlye92c8H4kKFqV5qYMIh7d+cYecEg0dIkA== +dunder-proto@^1.0.0, dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== dependencies: - find-up "^3.0.0" - minimatch "^3.0.4" + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" duplexer2@~0.1.0: version "0.1.4" @@ -2444,13 +2372,6 @@ encoding@^0.1.13: dependencies: iconv-lite "^0.6.2" -enquirer@^2.3.5: - version "2.3.6" - resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" - integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== - dependencies: - ansi-colors "^4.1.1" - env-ci@^11.0.0: version "11.1.0" resolved "https://registry.yarnpkg.com/env-ci/-/env-ci-11.1.0.tgz#b26eeb692f76c1f69ddc1fb2d4a3d371088a54f9" @@ -2481,6 +2402,111 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +es-abstract@^1.23.2, es-abstract@^1.23.5, es-abstract@^1.23.9: + version "1.23.9" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.9.tgz#5b45994b7de78dada5c1bebf1379646b32b9d606" + integrity sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA== + dependencies: + array-buffer-byte-length "^1.0.2" + arraybuffer.prototype.slice "^1.0.4" + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.3" + data-view-buffer "^1.0.2" + data-view-byte-length "^1.0.2" + data-view-byte-offset "^1.0.1" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-set-tostringtag "^2.1.0" + es-to-primitive "^1.3.0" + function.prototype.name "^1.1.8" + get-intrinsic "^1.2.7" + get-proto "^1.0.0" + get-symbol-description "^1.1.0" + globalthis "^1.0.4" + gopd "^1.2.0" + has-property-descriptors "^1.0.2" + has-proto "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + internal-slot "^1.1.0" + is-array-buffer "^3.0.5" + is-callable "^1.2.7" + is-data-view "^1.0.2" + is-regex "^1.2.1" + is-shared-array-buffer "^1.0.4" + is-string "^1.1.1" + is-typed-array "^1.1.15" + is-weakref "^1.1.0" + math-intrinsics "^1.1.0" + object-inspect "^1.13.3" + object-keys "^1.1.1" + object.assign "^4.1.7" + own-keys "^1.0.1" + regexp.prototype.flags "^1.5.3" + safe-array-concat "^1.1.3" + safe-push-apply "^1.0.0" + safe-regex-test "^1.1.0" + set-proto "^1.0.0" + string.prototype.trim "^1.2.10" + string.prototype.trimend "^1.0.9" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.3" + typed-array-byte-length "^1.0.3" + typed-array-byte-offset "^1.0.4" + typed-array-length "^1.0.7" + unbox-primitive "^1.1.0" + which-typed-array "^1.1.18" + +es-define-property@^1.0.0, es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-module-lexer@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.6.0.tgz#da49f587fd9e68ee2404fe4e256c0c7d3a81be21" + integrity sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +es-shim-unscopables@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz#438df35520dac5d105f3943d927549ea3b00f4b5" + integrity sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw== + dependencies: + hasown "^2.0.2" + +es-to-primitive@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz#96c89c82cc49fd8794a24835ba3e1ff87f214e18" + integrity sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g== + dependencies: + is-callable "^1.2.7" + is-date-object "^1.0.5" + is-symbol "^1.0.4" + es6-error@^4.0.1: version "4.1.1" resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" @@ -2537,126 +2563,123 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-config-prettier@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.1.0.tgz#4ef1eaf97afe5176e6a75ddfb57c335121abc5a6" - integrity sha512-oKMhGv3ihGbCIimCAjqkdzx2Q+jthoqnXSP+d86M9tptwugycmTFdVR4IpLgq2c4SHifbwO90z2fQ8/Aio73yw== - -eslint-plugin-markdown@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-markdown/-/eslint-plugin-markdown-2.0.0.tgz#cd650beda2b599cd9e4535ea369266b5d0e49d23" - integrity sha512-zt10JoexHeJFMTE5egDcetAJ34bn5k/92s0wAuRZfhDAyI0ryEj+O91JL2CbBExajie6BE5L63y47dN1Sbp6mQ== - dependencies: - remark-parse "^5.0.0" - unified "^6.1.2" - -eslint-plugin-prettier@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.1.tgz#7079cfa2497078905011e6f82e8dd8453d1371b7" - integrity sha512-Rq3jkcFY8RYeQLgk2cCwuc0P7SEFwDravPhsJZOQ5N4YI4DSg50NyqJ/9gdZHzQlHf8MvafSesbNJCcP/FF6pQ== - dependencies: - prettier-linter-helpers "^1.0.0" - -eslint-plugin-sonarjs@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.6.0.tgz#3ee3b04f1f9587ef02b255a5d2f96e500c4789bb" - integrity sha512-y+sXXWsYVW2kNEjmZI87laFspwC/hic7wyMjsPFoST8aQ2hESUVavkZjnTeVdHMOmlmcloKkyX/GJJetmfBY4Q== - -eslint-plugin-typescript-sort-keys@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-typescript-sort-keys/-/eslint-plugin-typescript-sort-keys-1.5.0.tgz#cc2ae7a9311422d8eab760ae01524426fd204004" - integrity sha512-Pq0geV5TbkoGyxiPUH9AZhloRbQekrprmiXpMWmtSURfWvsWtPtsEPDLVKhDN19S791zbqnw+cm7enzu6833/w== - dependencies: - "@typescript-eslint/experimental-utils" "^2.32.0" - json-schema "^0.2.5" - natural-compare-lite "^1.4.0" +eslint-import-resolver-node@^0.3.9: + version "0.3.9" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" + integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== + dependencies: + debug "^3.2.7" + is-core-module "^2.13.0" + resolve "^1.22.4" + +eslint-module-utils@^2.12.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz#fe4cfb948d61f49203d7b08871982b65b9af0b0b" + integrity sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg== + dependencies: + debug "^3.2.7" + +eslint-plugin-import@^2.31.0: + version "2.31.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz#310ce7e720ca1d9c0bb3f69adfd1c6bdd7d9e0e7" + integrity sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A== + dependencies: + "@rtsao/scc" "^1.1.0" + array-includes "^3.1.8" + array.prototype.findlastindex "^1.2.5" + array.prototype.flat "^1.3.2" + array.prototype.flatmap "^1.3.2" + debug "^3.2.7" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.9" + eslint-module-utils "^2.12.0" + hasown "^2.0.2" + is-core-module "^2.15.1" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.fromentries "^2.0.8" + object.groupby "^1.0.3" + object.values "^1.2.0" + semver "^6.3.1" + string.prototype.trimend "^1.0.8" + tsconfig-paths "^3.15.0" -eslint-scope@^5.0.0, eslint-scope@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== +eslint-scope@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.2.0.tgz#377aa6f1cb5dc7592cfd0b7f892fd0cf352ce442" + integrity sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A== dependencies: esrecurse "^4.3.0" - estraverse "^4.1.1" - -eslint-utils@^2.0.0, eslint-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" - integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== - dependencies: - eslint-visitor-keys "^1.1.0" - -eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== + estraverse "^5.2.0" -eslint-visitor-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" - integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== +eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint@7.21.0: - version "7.21.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.21.0.tgz#4ecd5b8c5b44f5dedc9b8a110b01bbfeb15d1c83" - integrity sha512-W2aJbXpMNofUp0ztQaF40fveSsJBjlSCSWpy//gzfTvwC+USs/nceBrKmlJOiM8r1bLwP2EuYkCqArn/6QTIgg== - dependencies: - "@babel/code-frame" "7.12.11" - "@eslint/eslintrc" "^0.4.0" - ajv "^6.10.0" +eslint-visitor-keys@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" + integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== + +eslint@^9.21.0: + version "9.21.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.21.0.tgz#b1c9c16f5153ff219791f627b94ab8f11f811591" + integrity sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.12.1" + "@eslint/config-array" "^0.19.2" + "@eslint/core" "^0.12.0" + "@eslint/eslintrc" "^3.3.0" + "@eslint/js" "9.21.0" + "@eslint/plugin-kit" "^0.2.7" + "@humanfs/node" "^0.16.6" + "@humanwhocodes/module-importer" "^1.0.1" + "@humanwhocodes/retry" "^0.4.2" + "@types/estree" "^1.0.6" + "@types/json-schema" "^7.0.15" + ajv "^6.12.4" chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.0.1" - doctrine "^3.0.0" - enquirer "^2.3.5" - eslint-scope "^5.1.1" - eslint-utils "^2.1.0" - eslint-visitor-keys "^2.0.0" - espree "^7.3.1" - esquery "^1.4.0" + cross-spawn "^7.0.6" + debug "^4.3.2" + escape-string-regexp "^4.0.0" + eslint-scope "^8.2.0" + eslint-visitor-keys "^4.2.0" + espree "^10.3.0" + esquery "^1.5.0" esutils "^2.0.2" - file-entry-cache "^6.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^5.0.0" - globals "^12.1.0" - ignore "^4.0.6" - import-fresh "^3.0.0" + 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" - js-yaml "^3.13.1" json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash "^4.17.20" - minimatch "^3.0.4" + lodash.merge "^4.6.2" + minimatch "^3.1.2" natural-compare "^1.4.0" - optionator "^0.9.1" - progress "^2.0.0" - regexpp "^3.1.0" - semver "^7.2.1" - strip-ansi "^6.0.0" - strip-json-comments "^3.1.0" - table "^6.0.4" - text-table "^0.2.0" - v8-compile-cache "^2.0.3" + optionator "^0.9.3" -espree@^7.3.0, espree@^7.3.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" - integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== +espree@^10.0.1, espree@^10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.3.0.tgz#29267cf5b0cb98735b65e64ba07e0ed49d1eed8a" + integrity sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg== dependencies: - acorn "^7.4.0" - acorn-jsx "^5.3.1" - eslint-visitor-keys "^1.3.0" + acorn "^8.14.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.2.0" esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" - integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== +esquery@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== dependencies: estraverse "^5.1.0" @@ -2667,16 +2690,18 @@ esrecurse@^4.3.0: dependencies: estraverse "^5.2.0" -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - estraverse@^5.1.0, estraverse@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== +estree-walker@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -2735,32 +2760,27 @@ execa@^9.0.0: strip-final-newline "^4.0.0" yoctocolors "^2.0.0" +expect-type@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.1.0.tgz#a146e414250d13dfc49eafcfd1344a4060fa4c75" + integrity sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA== + exponential-backoff@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.2.tgz#a8f26adb96bf78e8cd8ad1037928d5e5c0679d91" integrity sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA== -extend@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - fast-content-type-parse@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz#c236124534ee2cb427c8d8e5ba35a4856947847b" integrity sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q== -fast-deep-equal@^3.1.1: +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-diff@^1.1.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" - integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== - -fast-glob@^3.1.1, fast-glob@^3.3.3: +fast-glob@^3.3.2, fast-glob@^3.3.3: version "3.3.3" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== @@ -2781,6 +2801,11 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-uri@^3.0.1: + version "3.0.6" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" + integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== + fastest-levenshtein@^1.0.16: version "1.0.16" resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" @@ -2800,13 +2825,6 @@ figures@^2.0.0: dependencies: escape-string-regexp "^1.0.5" -figures@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" - integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== - dependencies: - escape-string-regexp "^1.0.5" - figures@^6.0.0, figures@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/figures/-/figures-6.1.0.tgz#935479f51865fa7479f6fa94fc6fc7ac14e62c4a" @@ -2814,12 +2832,12 @@ figures@^6.0.0, figures@^6.1.0: dependencies: is-unicode-supported "^2.0.0" -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== dependencies: - flat-cache "^3.0.4" + flat-cache "^4.0.0" fill-range@^7.1.1: version "7.1.1" @@ -2849,13 +2867,6 @@ find-up@^2.0.0: dependencies: locate-path "^2.0.0" -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" @@ -2872,12 +2883,14 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -find-versions@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-4.0.0.tgz#3c57e573bf97769b8cb8df16934b627915da4965" - integrity sha512-wgpWy002tA+wgmO27buH/9KzyEOQnKsG/R0yrcjPT9BOFm0zRBVQbZ95nRGXWMywS8YR5knRbpohio0bcJABxQ== +find-up@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-7.0.0.tgz#e8dec1455f74f78d888ad65bf7ca13dd2b4e66fb" + integrity sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g== dependencies: - semver-regex "^3.1.2" + locate-path "^7.2.0" + path-exists "^5.0.0" + unicorn-magic "^0.1.0" find-versions@^6.0.0: version "6.0.0" @@ -2887,29 +2900,31 @@ find-versions@^6.0.0: semver-regex "^4.0.5" super-regex "^1.0.0" -flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== dependencies: - flatted "^3.1.0" - rimraf "^3.0.2" + flatted "^3.2.9" + keyv "^4.5.4" -flat@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" - integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== - -flatted@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" - integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== +flatted@^3.2.9: + version "3.3.3" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" + integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== follow-redirects@^1.15.0: version "1.15.3" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== +for-each@^0.3.3: + version "0.3.5" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" + integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== + dependencies: + is-callable "^1.2.7" + foreground-child@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-2.0.0.tgz#71b32800c9f15aa8f2f83f4a6bd9bff35d861a53" @@ -2948,22 +2963,6 @@ fromentries@^1.2.0: resolved "https://registry.yarnpkg.com/fromentries/-/fromentries-1.3.2.tgz#e4bca6808816bf8f93b52750f1127f5a6fd86e3a" integrity sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg== -fs-access@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/fs-access/-/fs-access-1.0.1.tgz#d6a87f262271cefebec30c553407fb995da8777a" - integrity sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o= - dependencies: - null-check "^1.0.0" - -fs-extra@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" - integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - fs-extra@^11.0.0: version "11.3.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.3.0.tgz#0daced136bbaf65a555a326719af931adc7a314d" @@ -2992,10 +2991,10 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== function-bind@^1.1.2: version "1.1.2" @@ -3007,10 +3006,22 @@ function-timeout@^1.0.1: resolved "https://registry.yarnpkg.com/function-timeout/-/function-timeout-1.0.2.tgz#e5a7b6ffa523756ff20e1231bbe37b5f373aadd5" integrity sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA== -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +function.prototype.name@^1.1.6, function.prototype.name@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz#e68e1df7b259a5c949eeef95cdbde53edffabb78" + integrity sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + functions-have-names "^1.2.3" + hasown "^2.0.2" + is-callable "^1.2.7" + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== gensync@^1.0.0-beta.2: version "1.0.0-beta.2" @@ -3027,25 +3038,34 @@ get-east-asian-width@^1.0.0: resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz#5e6ebd9baee6fb8b7b6bd505221065f0cd91f64e" integrity sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA== -get-func-name@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" - integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= +get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== -get-pkg-repo@^4.0.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/get-pkg-repo/-/get-pkg-repo-4.2.1.tgz#75973e1c8050c73f48190c52047c4cee3acbf385" - integrity sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA== +get-proto@^1.0.0, get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== dependencies: - "@hutson/parse-repository-url" "^3.0.0" - hosted-git-info "^4.0.0" - through2 "^2.0.0" - yargs "^16.2.0" + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" get-stream@^6.0.0: version "6.0.1" @@ -3070,6 +3090,15 @@ get-stream@^9.0.0: "@sec-ant/readable-stream" "^0.4.1" is-stream "^4.0.1" +get-symbol-description@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz#7bdd54e0befe8ffc9f3b4e203220d9f1e881b6ee" + integrity sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + git-log-parser@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/git-log-parser/-/git-log-parser-1.2.1.tgz#44355787b37af7560dcc4ddc01cb53b5d139cc28" @@ -3082,47 +3111,29 @@ git-log-parser@^1.2.0: through2 "~2.0.0" traverse "0.6.8" -git-raw-commits@^2.0.0, git-raw-commits@^2.0.8: - version "2.0.11" - resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-2.0.11.tgz#bc3576638071d18655e1cc60d7f524920008d723" - integrity sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A== - dependencies: - dargs "^7.0.0" - lodash "^4.17.15" - meow "^8.0.0" - split2 "^3.0.0" - through2 "^4.0.0" - -git-remote-origin-url@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz#5282659dae2107145a11126112ad3216ec5fa65f" - integrity sha1-UoJlna4hBxRaERJhEq0yFuxfpl8= - dependencies: - gitconfiglocal "^1.0.0" - pify "^2.3.0" - -git-semver-tags@^4.0.0, git-semver-tags@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/git-semver-tags/-/git-semver-tags-4.1.1.tgz#63191bcd809b0ec3e151ba4751c16c444e5b5780" - integrity sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA== - dependencies: - meow "^8.0.0" - semver "^6.0.0" - -gitconfiglocal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz#41d045f3851a5ea88f03f24ca1c6178114464b9b" - integrity sha1-QdBF84UaXqiPA/JMocYXgRRGS5s= +git-raw-commits@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-4.0.0.tgz#b212fd2bff9726d27c1283a1157e829490593285" + integrity sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ== dependencies: - ini "^1.3.2" + dargs "^8.0.0" + meow "^12.0.1" + split2 "^4.0.0" -glob-parent@^5.0.0, glob-parent@^5.1.2, glob-parent@~5.1.2: +glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + glob@^10.2.2, glob@^10.3.10, glob@^10.3.7, glob@^10.4.5: version "10.4.5" resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" @@ -3147,36 +3158,35 @@ glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" -global-dirs@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" - integrity sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU= +global-directory@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/global-directory/-/global-directory-4.0.1.tgz#4d7ac7cfd2cb73f304c53b8810891748df5e361e" + integrity sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q== dependencies: - ini "^1.3.4" + ini "4.1.1" globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^12.1.0: - version "12.4.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8" - integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg== - dependencies: - type-fest "^0.8.1" +globals@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" + integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== + +globals@^16.0.0: + version "16.0.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-16.0.0.tgz#3d7684652c5c4fbd086ec82f9448214da49382d8" + integrity sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A== -globby@^11.0.1: - version "11.0.2" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.2.tgz#1af538b766a3b540ebfb58a32b2e2d5897321d83" - integrity sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og== +globalthis@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" - slash "^3.0.0" + define-properties "^1.2.1" + gopd "^1.0.1" globby@^14.0.0: version "14.1.0" @@ -3190,6 +3200,11 @@ globby@^14.0.0: slash "^5.1.0" unicorn-magic "^0.3.0" +gopd@^1.0.1, gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + graceful-fs@4.2.10: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" @@ -3200,6 +3215,11 @@ graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + handlebars@^4.7.7: version "4.7.7" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" @@ -3212,10 +3232,10 @@ handlebars@^4.7.7: optionalDependencies: uglify-js "^3.1.4" -hard-rejection@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" - integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== +has-bigints@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe" + integrity sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg== has-flag@^3.0.0: version "3.0.0" @@ -3227,6 +3247,32 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.2.0.tgz#5de5a6eabd95fdffd9818b43055e8065e39fe9d5" + integrity sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ== + dependencies: + dunder-proto "^1.0.0" + +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + hasha@^5.0.0: version "5.2.2" resolved "https://registry.yarnpkg.com/hasha/-/hasha-5.2.2.tgz#a48477989b3b327aea3c04f53096d816d97522a1" @@ -3242,11 +3288,6 @@ hasown@^2.0.2: dependencies: function-bind "^1.1.2" -he@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - highlight.js@^10.7.1: version "10.7.3" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" @@ -3257,18 +3298,6 @@ hook-std@^3.0.0: resolved "https://registry.yarnpkg.com/hook-std/-/hook-std-3.0.0.tgz#47038a01981e07ce9d83a6a3b2eb98cad0f7bd58" integrity sha512-jHRQzjSDzMtFy34AGj1DN+vq54WVuhSvKgrHf0OMiFQTwDD4L/qqofVEWjLOBMTn5+lCD3fPg32W9yOfnEJTTw== -hosted-git-info@^2.1.4: - version "2.8.9" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" - integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== - -hosted-git-info@^4.0.0, hosted-git-info@^4.0.1: - version "4.1.0" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224" - integrity sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA== - dependencies: - lru-cache "^6.0.0" - hosted-git-info@^7.0.0: version "7.0.2" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-7.0.2.tgz#9b751acac097757667f30114607ef7b661ff4f17" @@ -3324,21 +3353,10 @@ human-signals@^8.0.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-8.0.0.tgz#2d3d63481c7c2319f0373428b01ffe30da6df852" integrity sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA== -husky@^4.3.8: - version "4.3.8" - resolved "https://registry.yarnpkg.com/husky/-/husky-4.3.8.tgz#31144060be963fd6850e5cc8f019a1dfe194296d" - integrity sha512-LCqqsB0PzJQ/AlCgfrfzRe3e3+NvmefAdKQhRYpxS4u6clblBoDdzzvHi8fmxKRzvMxPY/1WZWzomPZww0Anow== - dependencies: - chalk "^4.0.0" - ci-info "^2.0.0" - compare-versions "^3.6.0" - cosmiconfig "^7.0.0" - find-versions "^4.0.0" - opencollective-postinstall "^2.0.2" - pkg-dir "^5.0.0" - please-upgrade-node "^3.2.0" - slash "^3.0.0" - which-pm-runs "^1.0.0" +husky@^9.1.7: + version "9.1.7" + resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.7.tgz#d46a38035d101b46a70456a850ff4201344c0b2d" + integrity sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA== iconv-lite@^0.6.2: version "0.6.3" @@ -3354,22 +3372,17 @@ ignore-walk@^7.0.0: dependencies: minimatch "^9.0.0" -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - -ignore@^5.1.4: - version "5.1.8" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" - integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== +ignore@^5.2.0, ignore@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== ignore@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.3.tgz#397ef9315dfe0595671eefe8b633fec6943ab733" integrity sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA== -import-fresh@^3.0.0, import-fresh@^3.2.1, import-fresh@^3.3.0: +import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.1" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== @@ -3418,12 +3431,17 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: +inherits@2, inherits@^2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ini@^1.3.2, ini@^1.3.4, ini@~1.3.0: +ini@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.1.tgz#d95b3d843b1e906e56d6747d5447904ff50ce7a1" + integrity sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g== + +ini@^1.3.4, ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== @@ -3446,6 +3464,15 @@ init-package-json@^7.0.2: validate-npm-package-license "^3.0.4" validate-npm-package-name "^6.0.0" +internal-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961" + integrity sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.2" + side-channel "^1.1.0" + into-stream@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-7.0.0.tgz#d1a211e146be8acfdb84dabcbf00fe8205e72936" @@ -3467,35 +3494,50 @@ ip-regex@^5.0.0: resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-5.0.0.tgz#cd313b2ae9c80c07bd3851e12bf4fa4dc5480632" integrity sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw== -is-alphabetical@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d" - integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg== - -is-alphanumerical@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz#7eb9a2431f855f6b1ef1a78e326df515696c4dbf" - integrity sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A== +is-array-buffer@^3.0.4, is-array-buffer@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280" + integrity sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A== dependencies: - is-alphabetical "^1.0.0" - is-decimal "^1.0.0" + call-bind "^1.0.8" + call-bound "^1.0.3" + get-intrinsic "^1.2.6" is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== +is-async-function@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.1.1.tgz#3e69018c8e04e73b738793d020bfe884b9fd3523" + integrity sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ== dependencies: - binary-extensions "^2.0.0" + async-function "^1.0.0" + call-bound "^1.0.3" + get-proto "^1.0.1" + has-tostringtag "^1.0.2" + safe-regex-test "^1.1.0" -is-buffer@^1.1.4: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== +is-bigint@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.1.0.tgz#dda7a3445df57a42583db4228682eba7c4170672" + integrity sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ== + dependencies: + has-bigints "^1.0.2" + +is-boolean-object@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz#7067f47709809a393c71ff5bb3e135d8a9215d9e" + integrity sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + +is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== is-cidr@^5.1.0: version "5.1.1" @@ -3504,23 +3546,42 @@ is-cidr@^5.1.0: dependencies: cidr-regex "^4.1.1" -is-core-module@^2.16.0, is-core-module@^2.5.0: +is-core-module@^2.13.0, is-core-module@^2.15.1, is-core-module@^2.16.0: version "2.16.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== dependencies: hasown "^2.0.2" -is-decimal@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" - integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== +is-data-view@^1.0.1, is-data-view@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.2.tgz#bae0a41b9688986c2188dda6657e56b8f9e63b8e" + integrity sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw== + dependencies: + call-bound "^1.0.2" + get-intrinsic "^1.2.6" + is-typed-array "^1.1.13" + +is-date-object@^1.0.5, is-date-object@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.1.0.tgz#ad85541996fc7aa8b2729701d27b7319f95d82f7" + integrity sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg== + dependencies: + call-bound "^1.0.2" + has-tostringtag "^1.0.2" is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= +is-finalizationregistry@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz#eefdcdc6c94ddd0674d9c85887bf93f944a97c90" + integrity sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg== + dependencies: + call-bound "^1.0.3" + is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" @@ -3538,17 +3599,35 @@ is-fullwidth-code-point@^5.0.0: dependencies: get-east-asian-width "^1.0.0" -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== +is-generator-function@^1.0.10: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.1.0.tgz#bf3eeda931201394f57b5dba2800f91a238309ca" + integrity sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ== + dependencies: + call-bound "^1.0.3" + get-proto "^1.0.0" + has-tostringtag "^1.0.2" + safe-regex-test "^1.1.0" + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: is-extglob "^2.1.1" -is-hexadecimal@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" - integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== +is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + +is-number-object@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.1.tgz#144b21e95a1bc148205dcc2814a9134ec41b2541" + integrity sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" is-number@^7.0.0: version "7.0.0" @@ -3560,21 +3639,33 @@ is-obj@^2.0.0: resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== -is-plain-obj@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" - integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= - -is-plain-obj@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" - integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== - is-plain-obj@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== +is-regex@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== + dependencies: + call-bound "^1.0.2" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + +is-shared-array-buffer@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz#9b67844bd9b7f246ba0708c3a93e34269c774f6f" + integrity sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A== + dependencies: + call-bound "^1.0.3" + is-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" @@ -3590,48 +3681,82 @@ is-stream@^4.0.1: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-4.0.1.tgz#375cf891e16d2e4baec250b85926cffc14720d9b" integrity sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A== -is-text-path@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-text-path/-/is-text-path-1.0.1.tgz#4e1aa0fb51bfbcb3e92688001397202c1775b66e" - integrity sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4= +is-string@^1.0.7, is-string@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.1.tgz#92ea3f3d5c5b6e039ca8677e5ac8d07ea773cbb9" + integrity sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA== dependencies: - text-extensions "^1.0.0" + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + +is-symbol@^1.0.4, is-symbol@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.1.1.tgz#f47761279f532e2b05a7024a7506dbbedacd0634" + integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w== + dependencies: + call-bound "^1.0.2" + has-symbols "^1.1.0" + safe-regex-test "^1.1.0" + +is-text-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-text-path/-/is-text-path-2.0.0.tgz#b2484e2b720a633feb2e85b67dc193ff72c75636" + integrity sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw== + dependencies: + text-extensions "^2.0.0" + +is-typed-array@^1.1.13, is-typed-array@^1.1.14, is-typed-array@^1.1.15: + version "1.1.15" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" + integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== + dependencies: + which-typed-array "^1.1.16" is-typedarray@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= -is-unicode-supported@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" - integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== - is-unicode-supported@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz#09f0ab0de6d3744d48d265ebb98f65d11f2a9b3a" integrity sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ== -is-whitespace-character@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz#0858edd94a95594c7c9dd0b5c174ec6e45ee4aa7" - integrity sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w== +is-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== + +is-weakref@^1.0.2, is-weakref@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.1.1.tgz#eea430182be8d64174bd96bffbc46f21bf3f9293" + integrity sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew== + dependencies: + call-bound "^1.0.3" + +is-weakset@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.4.tgz#c9f5deb0bc1906c6d6f1027f284ddf459249daca" + integrity sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ== + dependencies: + call-bound "^1.0.3" + get-intrinsic "^1.2.6" is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== -is-word-character@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.4.tgz#ce0e73216f98599060592f62ff31354ddbeb0230" - integrity sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA== - isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -3738,6 +3863,11 @@ java-properties@^1.0.2: resolved "https://registry.yarnpkg.com/java-properties/-/java-properties-1.0.2.tgz#ccd1fa73907438a5b5c38982269d0e771fe78211" integrity sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ== +jiti@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.4.2.tgz#d19b7732ebb6116b06e2038da74a55366faef560" + integrity sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -3768,6 +3898,11 @@ jsesc@^3.0.2: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + json-parse-better-errors@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" @@ -3793,11 +3928,6 @@ json-schema-traverse@^1.0.0: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== -json-schema@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.5.tgz#97997f50972dd0500214e208c407efa4b5d7063b" - integrity sha512-gWJOWYFrhQ8j7pVm0EM8Slr+EPVq1Phf6lvzvD/WCeqkrx/f2xBI0xOsRRS9xCn3I4vKtP519dvs3TP09r24wQ== - json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" @@ -3808,10 +3938,12 @@ json-stringify-nice@^1.1.4: resolved "https://registry.yarnpkg.com/json-stringify-nice/-/json-stringify-nice-1.1.4.tgz#2c937962b80181d3f317dd39aa323e14f5a60a67" integrity sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw== -json-stringify-safe@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= +json5@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== + dependencies: + minimist "^1.2.0" json5@^2.2.1: version "2.2.1" @@ -3880,10 +4012,12 @@ jws@^3.2.2: jwa "^1.4.1" safe-buffer "^5.0.1" -kind-of@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +keyv@^4.5.4: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" levn@^0.4.1: version "0.4.1" @@ -4060,14 +4194,6 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - locate-path@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" @@ -4082,11 +4208,23 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +locate-path@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-7.2.0.tgz#69cb1779bd90b35ab1e771e1f2f89a202c2a8a8a" + integrity sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA== + dependencies: + p-locate "^6.0.0" + lodash-es@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + lodash.capitalize@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz#f826c9b4e2a8511d84e3aca29db05e1a4f3b72a9" @@ -4122,11 +4260,6 @@ lodash.isinteger@^4.0.4: resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== -lodash.ismatch@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" - integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= - lodash.isnumber@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" @@ -4142,29 +4275,56 @@ lodash.isstring@^4.0.1: resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== +lodash.kebabcase@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" + integrity sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.mergewith@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" + integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== + lodash.once@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== +lodash.snakecase@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d" + integrity sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw== + +lodash.startcase@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.startcase/-/lodash.startcase-4.4.0.tgz#9436e34ed26093ed7ffae1936144350915d9add8" + integrity sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg== + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== + lodash.uniqby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" integrity sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww== -lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4: +lodash.upperfirst@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz#1365edf431480481ef0d1c68957a5ed99d49f7ce" + integrity sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg== + +lodash@^4.17.21, lodash@^4.17.4: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -log-symbols@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" - integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== - dependencies: - chalk "^4.1.0" - is-unicode-supported "^0.1.0" - log-update@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/log-update/-/log-update-6.0.0.tgz#0ddeb7ac6ad658c944c1de902993fce7c33f5e59" @@ -4176,6 +4336,11 @@ log-update@^6.0.0: strip-ansi "^7.1.0" wrap-ansi "^9.0.0" +loupe@^3.1.0, loupe@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.3.tgz#042a8f7986d77f3d0f98ef7990a2b2fef18b0fd2" + integrity sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug== + lru-cache@^10.0.1, lru-cache@^10.2.0, lru-cache@^10.2.2: version "10.4.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" @@ -4188,12 +4353,12 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== +magic-string@^0.30.17: + version "0.30.17" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453" + integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA== dependencies: - yallist "^4.0.0" + "@jridgewell/sourcemap-codec" "^1.5.0" make-dir@^3.0.0, make-dir@^3.0.2: version "3.1.0" @@ -4202,11 +4367,6 @@ make-dir@^3.0.0, make-dir@^3.0.2: dependencies: semver "^6.0.0" -make-error@^1.1.1: - version "1.3.6" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" - integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== - make-fetch-happen@^14.0.0, make-fetch-happen@^14.0.1, make-fetch-happen@^14.0.2, make-fetch-happen@^14.0.3: version "14.0.3" resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz#d74c3ecb0028f08ab604011e0bc6baed483fcdcd" @@ -4224,21 +4384,6 @@ make-fetch-happen@^14.0.0, make-fetch-happen@^14.0.1, make-fetch-happen@^14.0.2, promise-retry "^2.0.1" ssri "^12.0.0" -map-obj@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" - integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= - -map-obj@^4.0.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" - integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== - -markdown-escapes@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535" - integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg== - marked-terminal@^7.0.0: version "7.3.0" resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-7.3.0.tgz#7a86236565f3dd530f465ffce9c3f8b62ef270e8" @@ -4257,28 +4402,21 @@ marked@^12.0.0: resolved "https://registry.yarnpkg.com/marked/-/marked-12.0.2.tgz#b31578fe608b599944c69807b00f18edab84647e" integrity sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q== +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + +meow@^12.0.1: + version "12.1.1" + resolved "https://registry.yarnpkg.com/meow/-/meow-12.1.1.tgz#e558dddbab12477b69b2e9a2728c327f191bace6" + integrity sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw== + meow@^13.0.0: version "13.2.0" resolved "https://registry.yarnpkg.com/meow/-/meow-13.2.0.tgz#6b7d63f913f984063b3cc261b6e8800c4cd3474f" integrity sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA== -meow@^8.0.0: - version "8.1.2" - resolved "https://registry.yarnpkg.com/meow/-/meow-8.1.2.tgz#bcbe45bda0ee1729d350c03cffc8395a36c4e897" - integrity sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q== - dependencies: - "@types/minimist" "^1.2.0" - camelcase-keys "^6.2.2" - decamelize-keys "^1.1.0" - hard-rejection "^2.1.0" - minimist-options "4.1.0" - normalize-package-data "^3.0.0" - read-pkg-up "^7.0.1" - redent "^3.0.0" - trim-newlines "^3.0.0" - type-fest "^0.18.0" - yargs-parser "^20.2.3" - merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -4332,25 +4470,13 @@ mimic-fn@^4.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== -min-indent@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" - integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== - -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== +minimatch@^3.0.4, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" -minimatch@^5.1.6: - version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== - dependencies: - brace-expansion "^2.0.1" - minimatch@^9.0.0, minimatch@^9.0.4, minimatch@^9.0.5: version "9.0.5" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" @@ -4358,16 +4484,7 @@ minimatch@^9.0.0, minimatch@^9.0.4, minimatch@^9.0.5: dependencies: brace-expansion "^2.0.1" -minimist-options@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" - integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A== - dependencies: - arrify "^1.0.1" - is-plain-obj "^1.1.0" - kind-of "^6.0.3" - -minimist@^1.2.0, minimist@^1.2.5: +minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -4454,37 +4571,6 @@ mkdirp@^3.0.1: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== -mocha@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-11.1.0.tgz#20d7c6ac4d6d6bcb60a8aa47971fca74c65c3c66" - integrity sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg== - dependencies: - ansi-colors "^4.1.3" - browser-stdout "^1.3.1" - chokidar "^3.5.3" - debug "^4.3.5" - diff "^5.2.0" - escape-string-regexp "^4.0.0" - find-up "^5.0.0" - glob "^10.4.5" - he "^1.2.0" - js-yaml "^4.1.0" - log-symbols "^4.1.0" - minimatch "^5.1.6" - ms "^2.1.3" - serialize-javascript "^6.0.2" - strip-json-comments "^3.1.1" - supports-color "^8.1.1" - workerpool "^6.5.1" - yargs "^17.7.2" - yargs-parser "^21.1.1" - yargs-unparser "^2.0.0" - -modify-values@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" - integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== - ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -4509,10 +4595,10 @@ mz@^2.4.0: object-assign "^4.0.1" thenify-all "^1.0.0" -natural-compare-lite@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" - integrity sha1-F7CVgZiJef3a/gIB6TG6kzyWy7Q= +nanoid@^3.3.8: + version "3.3.8" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" + integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== natural-compare@^1.4.0: version "1.4.0" @@ -4590,26 +4676,6 @@ nopt@^8.0.0: dependencies: abbrev "^3.0.0" -normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -normalize-package-data@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e" - integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA== - dependencies: - hosted-git-info "^4.0.1" - is-core-module "^2.5.0" - semver "^7.3.4" - validate-npm-package-license "^3.0.1" - normalize-package-data@^6.0.0: version "6.0.2" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-6.0.2.tgz#a7bc22167fe24025412bcff0a9651eb768b03506" @@ -4628,11 +4694,6 @@ normalize-package-data@^7.0.0: semver "^7.3.5" validate-npm-package-license "^3.0.4" -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - normalize-url@^8.0.0: version "8.0.1" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-8.0.1.tgz#9b7d96af9836577c58f5883e939365fa15623a4a" @@ -4812,11 +4873,6 @@ npm@^10.5.0: which "^5.0.0" write-file-atomic "^6.0.0" -null-check@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/null-check/-/null-check-1.0.0.tgz#977dffd7176012b9ec30d2a39db5cf72a0439edd" - integrity sha1-l33/1xdgErnsMNKjnbXPcqBDnt0= - nyc@^15.1.0: version "15.1.0" resolved "https://registry.yarnpkg.com/nyc/-/nyc-15.1.0.tgz#1335dae12ddc87b6e249d5a1994ca4bdaea75f02" @@ -4855,6 +4911,57 @@ object-assign@^4.0.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.7: + version "4.1.7" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d" + integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + has-symbols "^1.1.0" + object-keys "^1.1.1" + +object.fromentries@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" + integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + +object.groupby@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e" + integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + +object.values@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.1.tgz#deed520a50809ff7f75a7cfd4bc64c7a038c6216" + integrity sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -4876,22 +4983,26 @@ onetime@^6.0.0: dependencies: mimic-fn "^4.0.0" -opencollective-postinstall@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259" - integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q== - -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== 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.3" + word-wrap "^1.2.5" + +own-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/own-keys/-/own-keys-1.0.1.tgz#e4006910a2bf913585289676eebd6f390cf51358" + integrity sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg== + dependencies: + get-intrinsic "^1.2.6" + object-keys "^1.1.1" + safe-push-apply "^1.0.0" p-each-series@^3.0.0: version "3.0.0" @@ -4917,7 +5028,7 @@ p-limit@^1.1.0: dependencies: p-try "^1.0.0" -p-limit@^2.0.0, p-limit@^2.2.0: +p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== @@ -4931,6 +5042,13 @@ p-limit@^3.0.2: dependencies: yocto-queue "^0.1.0" +p-limit@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-4.0.0.tgz#914af6544ed32bfa54670b061cafcbd04984b644" + integrity sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ== + dependencies: + yocto-queue "^1.0.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -4938,13 +5056,6 @@ p-locate@^2.0.0: dependencies: p-limit "^1.1.0" -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - p-locate@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" @@ -4959,6 +5070,13 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" +p-locate@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-6.0.0.tgz#3da9a49d4934b901089dca3302fa65dc5a05c04f" + integrity sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw== + dependencies: + p-limit "^4.0.0" + p-map@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-3.0.0.tgz#d704d9af8a2ba684e2600d9a215983d4141a979d" @@ -5072,20 +5190,8 @@ parse-conflict-json@^4.0.0: integrity sha512-37CN2VtcuvKgHUs8+0b1uJeEsbGn61GRHz469C94P5xiOoqpDYJYwjg4RY9Vmz39WyZAVkR5++nbJwLMIgOCnQ== dependencies: json-parse-even-better-errors "^4.0.0" - just-diff "^6.0.0" - just-diff-apply "^5.2.0" - -parse-entities@^1.1.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.2.2.tgz#c31bf0f653b6661354f8973559cb86dd1d5edf50" - integrity sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg== - dependencies: - character-entities "^1.0.0" - character-entities-legacy "^1.0.0" - character-reference-invalid "^1.0.0" - is-alphanumerical "^1.0.0" - is-decimal "^1.0.0" - is-hexadecimal "^1.0.0" + just-diff "^6.0.0" + just-diff-apply "^5.2.0" parse-json@^4.0.0: version "4.0.0" @@ -5095,7 +5201,7 @@ parse-json@^4.0.0: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" -parse-json@^5.0.0, parse-json@^5.2.0: +parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== @@ -5146,6 +5252,11 @@ path-exists@^4.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== +path-exists@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-5.0.0.tgz#a6aad9489200b21fab31e49cf09277e5116fb9e7" + integrity sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ== + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -5181,13 +5292,6 @@ path-to-regexp@^1.7.0: dependencies: isarray "0.0.1" -path-type@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" - integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== - dependencies: - pify "^3.0.0" - path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -5198,17 +5302,22 @@ path-type@^6.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-6.0.0.tgz#2f1bb6791a91ce99194caede5d6c5920ed81eb51" integrity sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ== -pathval@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" - integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== +pathe@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== + +pathval@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.0.tgz#7e2550b422601d4f6b8e26f1301bc8f15a741a25" + integrity sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA== picocolors@^1.0.0, picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: +picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -5218,11 +5327,6 @@ pidtree@0.6.0: resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c" integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== -pify@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= - pify@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" @@ -5243,19 +5347,10 @@ pkg-dir@^4.1.0: dependencies: find-up "^4.0.0" -pkg-dir@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760" - integrity sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA== - dependencies: - find-up "^5.0.0" - -please-upgrade-node@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" - integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg== - dependencies: - semver-compare "^1.0.0" +possible-typed-array-names@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae" + integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg== postcss-selector-parser@^6.1.2: version "6.1.2" @@ -5265,22 +5360,24 @@ postcss-selector-parser@^6.1.2: cssesc "^3.0.0" util-deprecate "^1.0.2" +postcss@^8.5.3: + version "8.5.3" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.3.tgz#1463b6f1c7fb16fe258736cba29a2de35237eafb" + integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A== + dependencies: + nanoid "^3.3.8" + picocolors "^1.1.1" + source-map-js "^1.2.1" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier-linter-helpers@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" - integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== - dependencies: - fast-diff "^1.1.2" - -prettier@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" - integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== +prettier@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.5.2.tgz#d066c6053200da0234bf8fa1ef45168abed8b914" + integrity sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg== pretty-ms@^9.0.0: version "9.2.0" @@ -5311,11 +5408,6 @@ proggy@^3.0.0: resolved "https://registry.yarnpkg.com/proggy/-/proggy-3.0.0.tgz#874e91fed27fe00a511758e83216a6b65148bd6c" integrity sha512-QE8RApCM3IaRRxVzxrjbgNMpQEX6Wu0p0KBeoSiSEw5/bsGwZHsshF4LCxH2jp/r6BU+bqA3LrMDEYNfJnpD8Q== -progress@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - promise-all-reject-late@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/promise-all-reject-late/-/promise-all-reject-late-1.0.1.tgz#f8ebf13483e5ca91ad809ccc2fcf25f26f8643c2" @@ -5356,11 +5448,6 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -q@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" - integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= - qrcode-terminal@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz#bb5b699ef7f9f0505092a3748be4464fe71b5819" @@ -5371,18 +5458,6 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.2.tgz#abf64491e6ecf0f38a6502403d4cda04f372dfd3" integrity sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg== -quick-lru@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" - integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== - -randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - rc@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -5415,42 +5490,6 @@ read-package-up@^11.0.0: read-pkg "^9.0.0" type-fest "^4.6.0" -read-pkg-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" - integrity sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc= - dependencies: - find-up "^2.0.0" - read-pkg "^3.0.0" - -read-pkg-up@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" - integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== - dependencies: - find-up "^4.1.0" - read-pkg "^5.2.0" - type-fest "^0.8.1" - -read-pkg@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" - integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= - dependencies: - load-json-file "^4.0.0" - normalize-package-data "^2.3.2" - path-type "^3.0.0" - -read-pkg@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" - integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== - dependencies: - "@types/normalize-package-data" "^2.4.0" - normalize-package-data "^2.5.0" - parse-json "^5.0.0" - type-fest "^0.6.0" - read-pkg@^9.0.0: version "9.0.1" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-9.0.1.tgz#b1b81fb15104f5dbb121b6bbdee9bbc9739f569b" @@ -5469,15 +5508,6 @@ read@^4.0.0: dependencies: mute-stream "^2.0.0" -readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@~2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" @@ -5491,25 +5521,31 @@ readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@~2.3.6: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -redent@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" - integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== - dependencies: - indent-string "^4.0.0" - strip-indent "^3.0.0" - -regexpp@^3.0.0, regexpp@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" - integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== +reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: + version "1.0.10" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9" + integrity sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.9" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.7" + get-proto "^1.0.1" + which-builtin-type "^1.2.1" + +regexp.prototype.flags@^1.5.3: + version "1.5.4" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz#1ad6c62d44a259007e55b3970e00f746efbcaa19" + integrity sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-errors "^1.3.0" + get-proto "^1.0.1" + gopd "^1.2.0" + set-function-name "^2.0.2" registry-auth-token@^5.0.0: version "5.1.0" @@ -5525,37 +5561,6 @@ release-zalgo@^1.0.0: dependencies: es6-error "^4.0.1" -remark-parse@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-5.0.0.tgz#4c077f9e499044d1d5c13f80d7a98cf7b9285d95" - integrity sha512-b3iXszZLH1TLoyUzrATcTQUZrwNl1rE70rVdSruJFlDaJ9z5aMkhrG43Pp68OgfHndL/ADz6V69Zow8cTQu+JA== - dependencies: - collapse-white-space "^1.0.2" - is-alphabetical "^1.0.0" - is-decimal "^1.0.0" - is-whitespace-character "^1.0.0" - is-word-character "^1.0.0" - markdown-escapes "^1.0.0" - parse-entities "^1.1.0" - repeat-string "^1.5.4" - state-toggle "^1.0.0" - trim "0.0.1" - trim-trailing-lines "^1.0.0" - unherit "^1.0.4" - unist-util-remove-position "^1.0.0" - vfile-location "^2.0.0" - xtend "^4.0.1" - -repeat-string@^1.5.4: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= - -replace-ext@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" - integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs= - require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -5571,24 +5576,17 @@ require-main-filename@^2.0.0: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== -resolve-from@5.0.0, resolve-from@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" - integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve-global@1.0.0, resolve-global@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/resolve-global/-/resolve-global-1.0.0.tgz#a2a79df4af2ca3f49bf77ef9ddacd322dad19255" - integrity sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw== - dependencies: - global-dirs "^0.1.1" +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve@^1.10.0: +resolve@^1.22.4: version "1.22.10" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== @@ -5620,7 +5618,7 @@ rfdc@^1.3.0: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.1.tgz#2b6d4df52dffe8bb346992a10ea9451f24373a8f" integrity sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg== -rimraf@^3.0.0, rimraf@^3.0.2: +rimraf@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -5634,6 +5632,34 @@ rimraf@^5.0.5: dependencies: glob "^10.3.7" +rollup@^4.30.1: + version "4.34.8" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.34.8.tgz#e859c1a51d899aba9bcf451d4eed1d11fb8e2a6e" + integrity sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ== + dependencies: + "@types/estree" "1.0.6" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.34.8" + "@rollup/rollup-android-arm64" "4.34.8" + "@rollup/rollup-darwin-arm64" "4.34.8" + "@rollup/rollup-darwin-x64" "4.34.8" + "@rollup/rollup-freebsd-arm64" "4.34.8" + "@rollup/rollup-freebsd-x64" "4.34.8" + "@rollup/rollup-linux-arm-gnueabihf" "4.34.8" + "@rollup/rollup-linux-arm-musleabihf" "4.34.8" + "@rollup/rollup-linux-arm64-gnu" "4.34.8" + "@rollup/rollup-linux-arm64-musl" "4.34.8" + "@rollup/rollup-linux-loongarch64-gnu" "4.34.8" + "@rollup/rollup-linux-powerpc64le-gnu" "4.34.8" + "@rollup/rollup-linux-riscv64-gnu" "4.34.8" + "@rollup/rollup-linux-s390x-gnu" "4.34.8" + "@rollup/rollup-linux-x64-gnu" "4.34.8" + "@rollup/rollup-linux-x64-musl" "4.34.8" + "@rollup/rollup-win32-arm64-msvc" "4.34.8" + "@rollup/rollup-win32-ia32-msvc" "4.34.8" + "@rollup/rollup-win32-x64-msvc" "4.34.8" + fsevents "~2.3.2" + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -5648,7 +5674,18 @@ rxjs@^7.8.1: dependencies: tslib "^2.1.0" -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: +safe-array-concat@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3" + integrity sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + get-intrinsic "^1.2.6" + has-symbols "^1.1.0" + isarray "^2.0.5" + +safe-buffer@^5.0.1: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -5658,6 +5695,23 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-push-apply@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz#01850e981c1602d398c85081f360e4e6d03d27f5" + integrity sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA== + dependencies: + es-errors "^1.3.0" + isarray "^2.0.5" + +safe-regex-test@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" + integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-regex "^1.2.1" + "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -5698,11 +5752,6 @@ semantic-release@^24.2.3: signale "^1.2.1" yargs "^17.5.1" -semver-compare@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" - integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= - semver-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-4.0.0.tgz#3afcf5ed6d62259f5c72d0d5d50dffbdc9680df5" @@ -5710,50 +5759,57 @@ semver-diff@^4.0.0: dependencies: semver "^7.3.5" -semver-regex@^3.1.2: - version "3.1.4" - resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-3.1.4.tgz#13053c0d4aa11d070a2f2872b6b1e3ae1e1971b4" - integrity sha512-6IiqeZNgq01qGf0TId0t3NvKzSvUsjcpdEO3AQNeIjR6A2+ckTnQlDpl4qu1bjRv0RzN3FP9hzFmws3lKqRWkA== - semver-regex@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-4.0.5.tgz#fbfa36c7ba70461311f5debcb3928821eb4f9180" integrity sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw== -"semver@2 || 3 || 4 || 5": - version "5.7.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" - integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== - -semver@7.3.5: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== - dependencies: - lru-cache "^6.0.0" - semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.1.1, semver@^7.1.2, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.6.3: +semver@^7.1.1, semver@^7.1.2, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.3: version "7.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f" integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA== -serialize-javascript@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" - integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== - dependencies: - randombytes "^2.1.0" - set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +set-function-length@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +set-function-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + +set-proto@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/set-proto/-/set-proto-1.0.0.tgz#0760dbcff30b2d7e801fd6e19983e56da337565e" + integrity sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw== + dependencies: + dunder-proto "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -5771,6 +5827,51 @@ shell-quote@^1.8.1: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.2.tgz#d2d83e057959d53ec261311e9e9b8f51dcb2934a" integrity sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA== +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + +siginfo@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" + integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== + signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.6" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.6.tgz#24e630c4b0f03fea446a2bd299e62b4a6ca8d0af" @@ -5821,25 +5922,11 @@ skin-tone@^2.0.0: dependencies: unicode-emoji-modifier-base "^1.0.0" -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - slash@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/slash/-/slash-5.1.0.tgz#be3adddcdf09ac38eebe8dcdc7b1a57a75b095ce" integrity sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg== -slice-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" - integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - slice-ansi@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-5.0.0.tgz#b73063c57aa96f9cd881654b15294d95d285c42a" @@ -5878,6 +5965,11 @@ socks@^2.8.3: ip-address "^9.0.5" smart-buffer "^4.2.0" +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + source-map@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" @@ -5934,12 +6026,10 @@ spdx-license-ids@^3.0.0: resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz#50c0d8c40a14ec1bf449bae69a0ea4685a9d9f95" integrity sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g== -split2@^3.0.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" - integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== - dependencies: - readable-stream "^3.0.0" +split2@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== split2@~1.0.0: version "1.0.0" @@ -5948,13 +6038,6 @@ split2@~1.0.0: dependencies: through2 "~2.0.0" -split@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" - integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== - dependencies: - through "2" - sprintf-js@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" @@ -5972,31 +6055,15 @@ ssri@^12.0.0: dependencies: minipass "^7.0.3" -standard-version@^9.3.2: - version "9.3.2" - resolved "https://registry.yarnpkg.com/standard-version/-/standard-version-9.3.2.tgz#28db8c1be66fd2d736f28f7c5de7619e64cd6dab" - integrity sha512-u1rfKP4o4ew7Yjbfycv80aNMN2feTiqseAhUhrrx2XtdQGmu7gucpziXe68Z4YfHVqlxVEzo4aUA0Iu3VQOTgQ== - dependencies: - chalk "^2.4.2" - conventional-changelog "3.1.24" - conventional-changelog-config-spec "2.1.0" - conventional-changelog-conventionalcommits "4.6.1" - conventional-recommended-bump "6.1.0" - detect-indent "^6.0.0" - detect-newline "^3.1.0" - dotgitignore "^2.1.0" - figures "^3.1.0" - find-up "^5.0.0" - fs-access "^1.0.1" - git-semver-tags "^4.0.0" - semver "^7.1.1" - stringify-package "^1.0.1" - yargs "^16.0.0" +stackback@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" + integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== -state-toggle@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe" - integrity sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ== +std-env@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.8.0.tgz#b56ffc1baf1a29dcc80a3bdf11d7fca7c315e7d5" + integrity sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w== stream-combiner2@~1.1.1: version "1.1.1" @@ -6047,12 +6114,37 @@ string-width@^7.0.0: get-east-asian-width "^1.0.0" strip-ansi "^7.1.0" -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== +string.prototype.trim@^1.2.10: + version "1.2.10" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz#40b2dd5ee94c959b4dcfb1d65ce72e90da480c81" + integrity sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + define-data-property "^1.1.4" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-object-atoms "^1.0.0" + has-property-descriptors "^1.0.2" + +string.prototype.trimend@^1.0.8, string.prototype.trimend@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz#62e2731272cd285041b36596054e9f66569b6942" + integrity sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ== dependencies: - safe-buffer "~5.2.0" + call-bind "^1.0.8" + call-bound "^1.0.2" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" string_decoder@~1.1.1: version "1.1.1" @@ -6061,11 +6153,6 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -stringify-package@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/stringify-package/-/stringify-package-1.0.1.tgz#e5aa3643e7f74d0f28628b72f3dad5cecfc3ba85" - integrity sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg== - "strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -6112,14 +6199,7 @@ strip-final-newline@^4.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-4.0.0.tgz#35a369ec2ac43df356e3edd5dcebb6429aa1fa5c" integrity sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw== -strip-indent@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" - integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== - dependencies: - min-indent "^1.0.0" - -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -6176,16 +6256,6 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -table@^6.0.4: - version "6.0.7" - resolved "https://registry.yarnpkg.com/table/-/table-6.0.7.tgz#e45897ffbcc1bcf9e8a87bf420f2c9e5a7a52a34" - integrity sha512-rxZevLGTUzWna/qBLObOe16kB2RTnnbhciwgPbMMlazz1yZGVEgnZK762xyVdVznhqxrfCeBMmMkgOOaPwjH7g== - dependencies: - ajv "^7.0.2" - lodash "^4.17.20" - slice-ansi "^4.0.0" - string-width "^4.2.0" - tar@^6.1.11, tar@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" @@ -6234,12 +6304,12 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" -text-extensions@^1.0.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26" - integrity sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ== +text-extensions@^2.0.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-2.4.0.tgz#a1cfcc50cf34da41bfd047cc744f804d1680ea34" + integrity sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g== -text-table@^0.2.0, text-table@~0.2.0: +text-table@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= @@ -6258,7 +6328,7 @@ thenify-all@^1.0.0: dependencies: any-promise "^1.0.0" -through2@^2.0.0, through2@~2.0.0: +through2@~2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== @@ -6266,17 +6336,10 @@ through2@^2.0.0, through2@~2.0.0: readable-stream "~2.3.6" xtend "~4.0.1" -through2@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/through2/-/through2-4.0.2.tgz#a7ce3ac2a7a8b0b966c80e7c49f0484c3b239764" - integrity sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw== - dependencies: - readable-stream "3" - -through@2, "through@>=2.2.7 <3": +"through@>=2.2.7 <3": version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== time-span@^5.1.0: version "5.1.0" @@ -6290,6 +6353,31 @@ tiny-relative-date@^1.3.0: resolved "https://registry.yarnpkg.com/tiny-relative-date/-/tiny-relative-date-1.3.0.tgz#fa08aad501ed730f31cc043181d995c39a935e07" integrity sha512-MOQHpzllWxDCHHaDno30hhLfbouoYlOI8YlMNtvKe1zXbjEVhbcEovQxvZrPvtiYW630GQDoMMarCnjfyfHA+A== +tinybench@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" + integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== + +tinyexec@^0.3.0, tinyexec@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2" + integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== + +tinypool@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.2.tgz#706193cc532f4c100f66aa00b01c42173d9051b2" + integrity sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA== + +tinyrainbow@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-2.0.0.tgz#9509b2162436315e80e3eee0fcce4474d2444294" + integrity sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw== + +tinyspy@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a" + integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q== + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -6312,62 +6400,26 @@ treeverse@^3.0.0: resolved "https://registry.yarnpkg.com/treeverse/-/treeverse-3.0.0.tgz#dd82de9eb602115c6ebd77a574aae67003cb48c8" integrity sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ== -trim-newlines@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" - integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== - -trim-trailing-lines@^1.0.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz#bd4abbec7cc880462f10b2c8b5ce1d8d1ec7c2c0" - integrity sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ== - -trim@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" - integrity sha1-WFhUf2spB1fulczMZm+1AITEYN0= - -trough@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" - integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== +ts-api-utils@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.0.1.tgz#660729385b625b939aaa58054f45c058f33f10cd" + integrity sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w== -ts-node@^10.4.0, ts-node@^10.9.2: - version "10.9.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" - integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== - dependencies: - "@cspotcode/source-map-support" "^0.8.0" - "@tsconfig/node10" "^1.0.7" - "@tsconfig/node12" "^1.0.7" - "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.2" - acorn "^8.4.1" - acorn-walk "^8.1.1" - arg "^4.1.0" - create-require "^1.1.0" - diff "^4.0.1" - make-error "^1.1.1" - v8-compile-cache-lib "^3.0.1" - yn "3.1.1" - -tslib@^1.8.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tsconfig-paths@^3.15.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" + integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.2" + minimist "^1.2.6" + strip-bom "^3.0.0" tslib@^2.1.0, tslib@^2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== -tsutils@^3.17.1: - version "3.21.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" - integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== - dependencies: - tslib "^1.8.1" - tuf-js@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/tuf-js/-/tuf-js-3.0.1.tgz#e3f07ed3d8e87afaa70607bd1ef801d5c1f57177" @@ -6384,22 +6436,12 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8: +type-detect@4.0.8, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== -type-fest@^0.18.0: - version "0.18.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" - integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw== - -type-fest@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" - integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== - -type-fest@^0.8.0, type-fest@^0.8.1: +type-fest@^0.8.0: version "0.8.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== @@ -6424,6 +6466,51 @@ type-fest@^4.6.0, type-fest@^4.7.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.35.0.tgz#007ed74d65c2ca0fb3b564b3dc8170d5c872d665" integrity sha512-2/AwEFQDFEy30iOLjrvHDIH7e4HEWH+f1Yl1bI5XMqzuoCUqwYCdxachgsgv0og/JdVZUhbfjcJAoHj5L1753A== +typed-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536" + integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-typed-array "^1.1.14" + +typed-array-byte-length@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz#8407a04f7d78684f3d252aa1a143d2b77b4160ce" + integrity sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg== + dependencies: + call-bind "^1.0.8" + for-each "^0.3.3" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.14" + +typed-array-byte-offset@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz#ae3698b8ec91a8ab945016108aef00d5bff12355" + integrity sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + for-each "^0.3.3" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.15" + reflect.getprototypeof "^1.0.9" + +typed-array-length@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.7.tgz#ee4deff984b64be1e118b0de8c9c877d5ce73d3d" + integrity sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + reflect.getprototypeof "^1.0.6" + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -6431,15 +6518,14 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= - -typescript@^4.4.3: - version "4.5.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8" - integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg== +typescript-eslint@^8.25.0: + version "8.25.0" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.25.0.tgz#73047c157cd70ee93cf2f9243f1599d21cf60239" + integrity sha512-TxRdQQLH4g7JkoFlYG3caW5v1S6kEkz8rqt80iQJZUYPq1zD1Ra7HfQBJJ88ABRaMvHAXnwRvRB4V+6sQ9xN5Q== + dependencies: + "@typescript-eslint/eslint-plugin" "8.25.0" + "@typescript-eslint/parser" "8.25.0" + "@typescript-eslint/utils" "8.25.0" typescript@^5.7.3: version "5.7.3" @@ -6451,19 +6537,21 @@ uglify-js@^3.1.4: resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.14.5.tgz#cdabb7d4954231d80cb4a927654c4655e51f4859" integrity sha512-qZukoSxOG0urUTvjc2ERMTcAy+BiFh3weWAkeurLwjrCba73poHmG3E36XEjd/JGukMzwTL7uCxZiAexj8ppvQ== +unbox-primitive@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2" + integrity sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw== + dependencies: + call-bound "^1.0.3" + has-bigints "^1.0.2" + has-symbols "^1.1.0" + which-boxed-primitive "^1.1.1" + undici-types@~6.20.0: version "6.20.0" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== -unherit@^1.0.4: - version "1.1.3" - resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.3.tgz#6c9b503f2b41b262330c80e91c8614abdaa69c22" - integrity sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ== - dependencies: - inherits "^2.0.0" - xtend "^4.0.0" - unicode-emoji-modifier-base@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz#dbbd5b54ba30f287e2a8d5a249da6c0cef369459" @@ -6479,18 +6567,6 @@ unicorn-magic@^0.3.0: resolved "https://registry.yarnpkg.com/unicorn-magic/-/unicorn-magic-0.3.0.tgz#4efd45c85a69e0dd576d25532fbfa22aa5c8a104" integrity sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA== -unified@^6.1.2: - version "6.2.0" - resolved "https://registry.yarnpkg.com/unified/-/unified-6.2.0.tgz#7fbd630f719126d67d40c644b7e3f617035f6dba" - integrity sha512-1k+KPhlVtqmG99RaTbAv/usu85fcSRu3wY8X+vnsEhIxNP5VbVIDiXnLqyKIG+UMdyTg0ZX9EI6k2AfjJkHPtA== - dependencies: - bail "^1.0.0" - extend "^3.0.0" - is-plain-obj "^1.1.0" - trough "^1.0.0" - vfile "^2.0.0" - x-is-string "^0.1.0" - unique-filename@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-4.0.0.tgz#a06534d370e7c977a939cd1d11f7f0ab8f1fed13" @@ -6512,37 +6588,6 @@ unique-string@^3.0.0: dependencies: crypto-random-string "^4.0.0" -unist-util-is@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-3.0.0.tgz#d9e84381c2468e82629e4a5be9d7d05a2dd324cd" - integrity sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A== - -unist-util-remove-position@^1.0.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-1.1.4.tgz#ec037348b6102c897703eee6d0294ca4755a2020" - integrity sha512-tLqd653ArxJIPnKII6LMZwH+mb5q+n/GtXQZo6S6csPRs5zB0u79Yw8ouR3wTw8wxvdJFhpP6Y7jorWdCgLO0A== - dependencies: - unist-util-visit "^1.1.0" - -unist-util-stringify-position@^1.0.0, unist-util-stringify-position@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz#3f37fcf351279dcbca7480ab5889bb8a832ee1c6" - integrity sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ== - -unist-util-visit-parents@^2.0.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz#25e43e55312166f3348cae6743588781d112c1e9" - integrity sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g== - dependencies: - unist-util-is "^3.0.0" - -unist-util-visit@^1.1.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.4.1.tgz#4724aaa8486e6ee6e26d7ff3c8685960d560b1e3" - integrity sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw== - dependencies: - unist-util-visit-parents "^2.0.0" - universal-user-agent@^7.0.0, universal-user-agent@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-7.0.2.tgz#52e7d0e9b3dc4df06cc33cb2b9fd79041a54827e" @@ -6573,32 +6618,22 @@ url-join@^5.0.0: resolved "https://registry.yarnpkg.com/url-join/-/url-join-5.0.0.tgz#c2f1e5cbd95fa91082a93b58a1f42fecb4bdbcf1" integrity sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA== -util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: +util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +uuid@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912" + integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A== + uuid@^3.3.3: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - -v8-compile-cache-lib@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" - integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== - -v8-compile-cache@^2.0.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" - integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== - -validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4: +validate-npm-package-license@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== @@ -6611,42 +6646,115 @@ validate-npm-package-name@^6.0.0: resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-6.0.0.tgz#3add966c853cfe36e0e8e6a762edd72ae6f1d6ac" integrity sha512-d7KLgL1LD3U3fgnvWEY1cQXoO/q6EQ1BSz48Sa149V/5zVTAbgmZIpyI8TRi6U9/JNyeYLlTKsEMPtLC27RFUg== -vfile-location@^2.0.0: - version "2.0.6" - resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-2.0.6.tgz#8a274f39411b8719ea5728802e10d9e0dff1519e" - integrity sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA== - -vfile-message@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-1.1.1.tgz#5833ae078a1dfa2d96e9647886cd32993ab313e1" - integrity sha512-1WmsopSGhWt5laNir+633LszXvZ+Z/lxveBf6yhGsqnQIhlhzooZae7zV6YVM1Sdkw68dtAW3ow0pOdPANugvA== +vite-node@3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-3.0.7.tgz#f15bc1e0c343ac00115a52c7e110471a5a315c72" + integrity sha512-2fX0QwX4GkkkpULXdT1Pf4q0tC1i1lFOyseKoonavXUNlQ77KpW2XqBGGNIm/J4Ows4KxgGJzDguYVPKwG/n5A== dependencies: - unist-util-stringify-position "^1.1.1" + cac "^6.7.14" + debug "^4.4.0" + es-module-lexer "^1.6.0" + pathe "^2.0.3" + vite "^5.0.0 || ^6.0.0" -vfile@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/vfile/-/vfile-2.3.0.tgz#e62d8e72b20e83c324bc6c67278ee272488bf84a" - integrity sha512-ASt4mBUHcTpMKD/l5Q+WJXNtshlWxOogYyGYYrg4lt/vuRjC1EFQtlAofL5VmtVNIZJzWYFJjzGWZ0Gw8pzW1w== +"vite@^5.0.0 || ^6.0.0": + version "6.2.0" + resolved "https://registry.yarnpkg.com/vite/-/vite-6.2.0.tgz#9dcb543380dab18d8384eb840a76bf30d78633f0" + integrity sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ== dependencies: - is-buffer "^1.1.4" - replace-ext "1.0.0" - unist-util-stringify-position "^1.0.0" - vfile-message "^1.0.0" + esbuild "^0.25.0" + postcss "^8.5.3" + rollup "^4.30.1" + optionalDependencies: + fsevents "~2.3.3" + +vitest@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-3.0.7.tgz#ed8f42e1b0e09e2179eaefd966cb58a8b75f0f6a" + integrity sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg== + dependencies: + "@vitest/expect" "3.0.7" + "@vitest/mocker" "3.0.7" + "@vitest/pretty-format" "^3.0.7" + "@vitest/runner" "3.0.7" + "@vitest/snapshot" "3.0.7" + "@vitest/spy" "3.0.7" + "@vitest/utils" "3.0.7" + chai "^5.2.0" + debug "^4.4.0" + expect-type "^1.1.0" + magic-string "^0.30.17" + pathe "^2.0.3" + std-env "^3.8.0" + tinybench "^2.9.0" + tinyexec "^0.3.2" + tinypool "^1.0.2" + tinyrainbow "^2.0.0" + vite "^5.0.0 || ^6.0.0" + vite-node "3.0.7" + why-is-node-running "^2.3.0" walk-up-path@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/walk-up-path/-/walk-up-path-3.0.1.tgz#c8d78d5375b4966c717eb17ada73dbd41490e886" integrity sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA== +which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e" + integrity sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA== + dependencies: + is-bigint "^1.1.0" + is-boolean-object "^1.2.1" + is-number-object "^1.1.1" + is-string "^1.1.1" + is-symbol "^1.1.1" + +which-builtin-type@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz#89183da1b4907ab089a6b02029cc5d8d6574270e" + integrity sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q== + dependencies: + call-bound "^1.0.2" + function.prototype.name "^1.1.6" + has-tostringtag "^1.0.2" + is-async-function "^2.0.0" + is-date-object "^1.1.0" + is-finalizationregistry "^1.1.0" + is-generator-function "^1.0.10" + is-regex "^1.2.1" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.1.0" + which-collection "^1.0.2" + which-typed-array "^1.1.16" + +which-collection@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== + dependencies: + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" + which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= -which-pm-runs@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" - integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= +which-typed-array@^1.1.16, which-typed-array@^1.1.18: + version "1.1.18" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.18.tgz#df2389ebf3fbb246a71390e90730a9edb6ce17ad" + integrity sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.3" + for-each "^0.3.3" + gopd "^1.2.0" + has-tostringtag "^1.0.2" which@^2.0.1: version "2.0.2" @@ -6662,21 +6770,24 @@ which@^5.0.0: dependencies: isexe "^3.1.1" -word-wrap@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +why-is-node-running@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04" + integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== + dependencies: + siginfo "^2.0.0" + stackback "0.0.2" + +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== wordwrap@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= -workerpool@^6.5.1: - version "6.5.1" - resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" - integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== - "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -6750,12 +6861,7 @@ ws@^8.18.1: resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb" integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w== -x-is-string@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/x-is-string/-/x-is-string-0.1.0.tgz#474b50865af3a49a9c4657f05acd145458f77d82" - integrity sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI= - -xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: +xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== @@ -6790,11 +6896,6 @@ yaml@2.3.4: resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== -yaml@^1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" - integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== - yargs-parser@^18.1.2: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" @@ -6803,7 +6904,7 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^20.2.2, yargs-parser@^20.2.3: +yargs-parser@^20.2.2: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== @@ -6813,16 +6914,6 @@ yargs-parser@^21.1.1: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs-unparser@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" - integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== - dependencies: - camelcase "^6.0.0" - decamelize "^4.0.0" - flat "^5.0.2" - is-plain-obj "^2.1.0" - yargs@^15.0.2: version "15.4.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" @@ -6840,7 +6931,7 @@ yargs@^15.0.2: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^16.0.0, yargs@^16.2.0: +yargs@^16.0.0: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== @@ -6866,16 +6957,16 @@ yargs@^17.0.0, yargs@^17.5.1, yargs@^17.7.2: y18n "^5.0.5" yargs-parser "^21.1.1" -yn@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" - integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== - yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +yocto-queue@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.1.1.tgz#fef65ce3ac9f8a32ceac5a634f74e17e5b232110" + integrity sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g== + yoctocolors@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/yoctocolors/-/yoctocolors-2.1.1.tgz#e0167474e9fbb9e8b3ecca738deaa61dd12e56fc" From 3c103d04f002c6126fd87ae1f8eaaececcb8ac21 Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Wed, 5 Mar 2025 12:45:57 +0100 Subject: [PATCH 17/47] test(update-message): fix reserved keys removal post-merge --- test/unit/client.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/client.test.js b/test/unit/client.test.js index fffed34689..deee74644e 100644 --- a/test/unit/client.test.js +++ b/test/unit/client.test.js @@ -463,14 +463,14 @@ describe('updateMessage should maintain data integrity', () => { const messageInQuery = { attachments: updatedMessage.attachments, mentioned_users: updatedMessage.mentioned_users, - reaction_counts: updatedMessage.reaction_counts, reaction_scores: updatedMessage.reaction_scores, silent: updatedMessage.silent, status: updatedMessage.status, text: updatedMessage.text, }; - expect(postSpy.args[0][1].message).to.deep.equal(messageInQuery); + expect(postSpy.callCount).to.equal(1); + expect(postSpy.firstCall.args[1].message).to.toMatchObject(messageInQuery); }); }); From 91afa113a66d9454d23d4f9ece71809577267e89 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 5 Mar 2025 12:04:16 +0000 Subject: [PATCH 18/47] chore(release): 9.0.0-rc.4 [skip ci] ## [9.0.0-rc.4](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.3...v9.0.0-rc.4) (2025-03-05) ### Bug Fixes * add extra user agent fields resolvable by react native ([#1477](https://github.com/GetStream/stream-chat-js/issues/1477)) ([4232150](https://github.com/GetStream/stream-chat-js/commit/42321500096491252211b57e3d238cfe920a0777)) * add missing env variable in rollup config ([#1480](https://github.com/GetStream/stream-chat-js/issues/1480)) ([a935e9e](https://github.com/GetStream/stream-chat-js/commit/a935e9e79232da0df9d951119f48891da59d6281)) * properly encode user agent on WS connection ([#1482](https://github.com/GetStream/stream-chat-js/issues/1482)) ([58b538b](https://github.com/GetStream/stream-chat-js/commit/58b538b5380000cfcf2abeb369b5ef6a0acf3913)) * remove pinned_at from the updateMessage api ([#1484](https://github.com/GetStream/stream-chat-js/issues/1484)) ([7b73cac](https://github.com/GetStream/stream-chat-js/commit/7b73cac6ab3c3c0812f1605024f3d73fad189cca)) --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0af624291d..264041cffd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [9.0.0-rc.4](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.3...v9.0.0-rc.4) (2025-03-05) + +### Bug Fixes + +* add extra user agent fields resolvable by react native ([#1477](https://github.com/GetStream/stream-chat-js/issues/1477)) ([4232150](https://github.com/GetStream/stream-chat-js/commit/42321500096491252211b57e3d238cfe920a0777)) +* add missing env variable in rollup config ([#1480](https://github.com/GetStream/stream-chat-js/issues/1480)) ([a935e9e](https://github.com/GetStream/stream-chat-js/commit/a935e9e79232da0df9d951119f48891da59d6281)) +* properly encode user agent on WS connection ([#1482](https://github.com/GetStream/stream-chat-js/issues/1482)) ([58b538b](https://github.com/GetStream/stream-chat-js/commit/58b538b5380000cfcf2abeb369b5ef6a0acf3913)) +* remove pinned_at from the updateMessage api ([#1484](https://github.com/GetStream/stream-chat-js/issues/1484)) ([7b73cac](https://github.com/GetStream/stream-chat-js/commit/7b73cac6ab3c3c0812f1605024f3d73fad189cca)) + ## [9.0.0-rc.3](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.2...v9.0.0-rc.3) (2025-02-27) ### Bug Fixes From 7f8a9a08ca14b3c26e1515ab1a9d80f7287570ee Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Wed, 5 Mar 2025 15:09:18 +0100 Subject: [PATCH 19/47] fix: multiple module augmentation fails in Angular (#1488) ## CLA - [] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required). - [] Code changes are tested ## Description of the changes, What, Why and How? When removing generics from stream-chat-angular I had an issue where augmenting the same custom interface (for example `CustomChannelData`) from two places (SDK and sample app) some changes were ignored (detailed description: https://getstream.slack.com/archives/C06CF5TKRGA/p1741163343794659). Found this open GH issue for TypeScript: https://github.com/microsoft/TypeScript/issues/46617 https://linear.app/stream/issue/ANG-31/drop-generics ## Changelog - --- src/index.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index d414c58835..472b416426 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,7 +26,20 @@ export * from './thread_manager'; export * from './token_manager'; export * from './types'; export * from './channel_manager'; -export * from './custom_types'; +// Don't use * here, that can break module augmentation https://github.com/microsoft/TypeScript/issues/46617 +export type { + CustomAttachmentData, + CustomChannelData, + CustomCommandData, + CustomEventData, + CustomMemberData, + CustomMessageData, + CustomPollOptionData, + CustomPollData, + CustomReactionData, + CustomUserData, + CustomThreadData, +} from './custom_types'; export { isOwnUser, chatCodes, From 017ccd752dfc9ef596460fe553c1b3e1d02a8188 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 5 Mar 2025 14:23:03 +0000 Subject: [PATCH 20/47] chore(release): 9.0.0-rc.5 [skip ci] ## [9.0.0-rc.5](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.4...v9.0.0-rc.5) (2025-03-05) ### Bug Fixes * multiple module augmentation fails in Angular ([#1488](https://github.com/GetStream/stream-chat-js/issues/1488)) ([7f8a9a0](https://github.com/GetStream/stream-chat-js/commit/7f8a9a08ca14b3c26e1515ab1a9d80f7287570ee)) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 264041cffd..d9adb06a3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [9.0.0-rc.5](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.4...v9.0.0-rc.5) (2025-03-05) + +### Bug Fixes + +* multiple module augmentation fails in Angular ([#1488](https://github.com/GetStream/stream-chat-js/issues/1488)) ([7f8a9a0](https://github.com/GetStream/stream-chat-js/commit/7f8a9a08ca14b3c26e1515ab1a9d80f7287570ee)) + ## [9.0.0-rc.4](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.3...v9.0.0-rc.4) (2025-03-05) ### Bug Fixes From 7b099e4ec9671e755da42c5ce08ff831c933e91a Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Wed, 5 Mar 2025 20:26:47 +0100 Subject: [PATCH 21/47] chore(ci): upgrade actions in workflows --- .github/actions/setup-node/action.yml | 7 +++---- .github/workflows/pr-check.yml | 4 ++-- .github/workflows/release.yml | 10 +++------- .github/workflows/scheduled_test.yml | 2 +- .github/workflows/size.yml | 2 +- .github/workflows/unit.yml | 2 +- 6 files changed, 11 insertions(+), 16 deletions(-) diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml index 052feeac9f..a7c05ed3b2 100644 --- a/.github/actions/setup-node/action.yml +++ b/.github/actions/setup-node/action.yml @@ -1,5 +1,5 @@ name: Setup -description: Sets up Node and Build SDK +description: Sets up Node and installs dependencies inputs: node-version: @@ -11,17 +11,16 @@ runs: using: 'composite' steps: - name: Setup Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ inputs.node-version }} - registry-url: https://registry.npmjs.org - name: Set NODE_VERSION env shell: bash run: echo "NODE_VERSION=$(node --version)" >> $GITHUB_ENV - name: Cache dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ./node_modules key: ${{ runner.os }}-${{ env.NODE_VERSION }}-modules-${{ hashFiles('./yarn.lock') }} diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index b1918d269a..6c0a694ce0 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -8,9 +8,9 @@ jobs: pr-title: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/setup-node - name: commitlint - run: echo "${{ github.event.pull_request.title }}" | npx commitlint + run: echo "${{ github.event.pull_request.title }}" | npx commitlint --verbose diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9b732f1966..7d248c0e5a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,17 +18,13 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - - name: Install dependencies - run: yarn install --frozen-lockfile + + - uses: ./.github/actions/setup-node + - name: Release env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # https://github.com/stream-ci-bot - GH_TOKEN: ${{ secrets.DOCUSAURUS_GH_TOKEN }} GITHUB_TOKEN: ${{ secrets.DOCUSAURUS_GH_TOKEN }} HUSKY: 0 run: > diff --git a/.github/workflows/scheduled_test.yml b/.github/workflows/scheduled_test.yml index 5d28846048..1dd239d761 100644 --- a/.github/workflows/scheduled_test.yml +++ b/.github/workflows/scheduled_test.yml @@ -10,7 +10,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/setup-node diff --git a/.github/workflows/size.yml b/.github/workflows/size.yml index c67f11ab33..1f6d9a5c93 100644 --- a/.github/workflows/size.yml +++ b/.github/workflows/size.yml @@ -14,7 +14,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: preactjs/compressed-size-action@v2 with: diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 4c52e093d5..975aa41e4f 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -9,7 +9,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/setup-node From a7a259b9f69f7b553de45498b91a3d230309a7fe Mon Sep 17 00:00:00 2001 From: Anton Arnautov <43254280+arnautov-anton@users.noreply.github.com> Date: Wed, 5 Mar 2025 23:39:31 +0100 Subject: [PATCH 22/47] chore(build): fix watch mode (#1489) ## Description of the changes, What, Why and How? --- package.json | 6 ++---- scripts/bundle.mjs | 16 ++++++++++++---- tsconfig.json | 4 +++- yarn.lock | 2 +- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 00321498b5..e9a1546038 100644 --- a/package.json +++ b/package.json @@ -80,16 +80,14 @@ "prettier": "^3.5.2", "semantic-release": "^24.2.3", "sinon": "^12.0.1", - "tslib": "^2.8.1", "typescript": "^5.7.3", "typescript-eslint": "^8.25.0", "uuid": "^11.1.0", "vitest": "^3.0.7" }, "scripts": { - "build": "rm -rf dist && yarn bundle", - "bundle": "concurrently 'tsc --declaration --emitDeclarationOnly --outDir ./dist/types' ./scripts/bundle.mjs", - "start": "tsc --watch", + "build": "rm -rf dist && concurrently 'tsc' './scripts/bundle.mjs'", + "start": "concurrently 'tsc --watch' './scripts/bundle.mjs --watch'", "types": "tsc --noEmit", "lint": "yarn run prettier && yarn run eslint", "lint-fix": "yarn run prettier-fix && yarn run eslint-fix", diff --git a/scripts/bundle.mjs b/scripts/bundle.mjs index 6bb37fac07..0cc984ea71 100755 --- a/scripts/bundle.mjs +++ b/scripts/bundle.mjs @@ -8,6 +8,8 @@ import getPackageVersion from './get-package-version.mjs'; // import.meta.dirname is not available before Node 20 const __dirname = import.meta.dirname; +const watchModeEnabled = process.argv.includes('--watch') || process.argv.includes('-w'); + // Those dependencies are distributed as ES modules, and cannot be externalized // in our CJS bundle. We convert them to CJS and bundle them instead. const bundledDeps = ['axios', 'form-data', 'isomorphic-ws', 'base64-js']; @@ -69,8 +71,14 @@ const bundles = [ 'process.env.CLIENT_BUNDLE': JSON.stringify('browser-esm'), }, }, -] - .flat() - .map((config) => esbuild.build(config)); +].flat(); + +if (watchModeEnabled) { + const contexts = await Promise.all(bundles.map((config) => esbuild.context(config))); + + await Promise.all(contexts.map((context) => context.watch())); -await Promise.all(bundles); + console.log('ESBuild is watching for changes...'); +} else { + await Promise.all(bundles.map((config) => esbuild.build(config))); +} diff --git a/tsconfig.json b/tsconfig.json index 77a74daeab..d4e1690dad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,9 @@ "allowJs": true, "skipLibCheck": true, "noEmitOnError": true, - "importHelpers": true, + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "./dist/types", "lib": ["ES2020", "DOM"], "moduleResolution": "bundler", diff --git a/yarn.lock b/yarn.lock index 485747dea0..0d5c038cc4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6415,7 +6415,7 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.1.0, tslib@^2.8.1: +tslib@^2.1.0: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== From 2a57b7f965e6b422d32990321a464519485fec1d Mon Sep 17 00:00:00 2001 From: Anton Arnautov <43254280+arnautov-anton@users.noreply.github.com> Date: Thu, 6 Mar 2025 13:18:41 +0100 Subject: [PATCH 23/47] fix: add image property to UserResponse type (#1486) ## Description of the changes, What, Why and How? Property `image` is considered a regular property according to the OpenAPI specification. --- src/types.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/types.ts b/src/types.ts index fb4aace269..9d0af412e6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -841,12 +841,15 @@ export type UpdateUsersAPIResponse = APIResponse & { users: { [key: string]: UserResponse }; }; -export type UserResponse = User & { +export type UserResponse = CustomUserData & { + id: string; + anon?: boolean; banned?: boolean; blocked_user_ids?: string[]; created_at?: string; deactivated_at?: string; deleted_at?: string; + image?: string; language?: TranslationLanguages | ''; last_active?: string; name?: string; @@ -855,8 +858,11 @@ export type UserResponse = User & { privacy_settings?: PrivacySettings; push_notifications?: PushNotificationSettings; revoke_tokens_issued_before?: string; + role?: string; shadow_banned?: boolean; + teams?: string[]; updated_at?: string; + username?: string; }; export type PrivacySettings = { @@ -2905,14 +2911,10 @@ export type UpdatedMessage = Omit & type?: MessageLabel; }; -export type User = CustomUserData & { - id: string; - anon?: boolean; - name?: string; - role?: string; - teams?: string[]; - username?: string; -}; +/** + * @description type alias for UserResponse + */ +export type User = UserResponse; export type TaskResponse = { task_id: string; From dcb10302d6ba32eb720f7edf6757c2d9f9c8e042 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 6 Mar 2025 12:26:00 +0000 Subject: [PATCH 24/47] chore(release): 9.0.0-rc.6 [skip ci] ## [9.0.0-rc.6](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.5...v9.0.0-rc.6) (2025-03-06) ### Bug Fixes * add image property to UserResponse type ([#1486](https://github.com/GetStream/stream-chat-js/issues/1486)) ([2a57b7f](https://github.com/GetStream/stream-chat-js/commit/2a57b7f965e6b422d32990321a464519485fec1d)) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9adb06a3e..8224525f51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [9.0.0-rc.6](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.5...v9.0.0-rc.6) (2025-03-06) + +### Bug Fixes + +* add image property to UserResponse type ([#1486](https://github.com/GetStream/stream-chat-js/issues/1486)) ([2a57b7f](https://github.com/GetStream/stream-chat-js/commit/2a57b7f965e6b422d32990321a464519485fec1d)) + ## [9.0.0-rc.5](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.4...v9.0.0-rc.5) (2025-03-05) ### Bug Fixes From ff32bd26c90c85b2e6200a65ba7dd7852952517f Mon Sep 17 00:00:00 2001 From: Anton Arnautov <43254280+arnautov-anton@users.noreply.github.com> Date: Tue, 11 Mar 2025 11:19:01 +0100 Subject: [PATCH 25/47] fix: adjust ErrorFromResponse error class (#1491) BREAKING CHANGE: `ErrorFromResponse` class constructor now requires second parameter (`status`, `response` and optionally `code`) --- src/client.ts | 25 +++++++++++-------------- src/types.ts | 26 ++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/client.ts b/src/client.ts index 007c5c1160..c7b9049448 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1181,20 +1181,17 @@ export class StreamChat { }); } - errorFromResponse( - response: AxiosResponse, - ): ErrorFromResponse { - let err: ErrorFromResponse; - err = new ErrorFromResponse(`StreamChat error HTTP code: ${response.status}`); - if (response.data && response.data.code) { - err = new Error( - `StreamChat error code ${response.data.code}: ${response.data.message}`, - ); - err.code = response.data.code; - } - err.response = response; - err.status = response.status; - return err; + errorFromResponse(response: AxiosResponse) { + const message = + typeof response.data.code !== 'undefined' + ? `StreamChat error code ${response.data.code}: ${response.data.message}` + : `StreamChat error HTTP code: ${response.status}`; + + return new ErrorFromResponse(message, { + code: response.data.code ?? null, + response, + status: response.status, + }); } handleResponse(response: AxiosResponse) { diff --git a/src/types.ts b/src/types.ts index 9d0af412e6..7f119038c7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3197,18 +3197,36 @@ type ErrorResponseDetails = { }; export type APIErrorResponse = { - code: number; duration: string; message: string; more_info: string; StatusCode: number; + code?: number; details?: ErrorResponseDetails; }; export class ErrorFromResponse extends Error { - code?: number; - response?: AxiosResponse; - status?: number; + public code: number | null; + public status: number; + public response: AxiosResponse; + + constructor( + message: string, + { + code, + status, + response, + }: { + code: ErrorFromResponse['code']; + response: ErrorFromResponse['response']; + status: ErrorFromResponse['status']; + }, + ) { + super(message); + this.code = code; + this.response = response; + this.status = status; + } } export type QueryPollsResponse = { From 32c5944ba2721ecd27f13776ec603ff3559ffe0a Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Tue, 11 Mar 2025 11:28:37 +0100 Subject: [PATCH 26/47] chore: build, version & release adjustments --- .github/workflows/release.yml | 3 ++- package.json | 3 +-- scripts/get-package-version.mjs | 7 ++++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7d248c0e5a..b69be78ec4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,6 +13,8 @@ jobs: runs-on: ubuntu-latest # GH does not allow to limit branches in the workflow_dispatch settings so this here is a safety measure if: ${{ inputs.dry_run || github.ref_name == 'rc' || startsWith(github.ref_name, 'release') || startsWith(github.ref_name, 'master') }} + env: + HUSKY: 0 steps: - name: Checkout uses: actions/checkout@v4 @@ -26,7 +28,6 @@ jobs: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # https://github.com/stream-ci-bot GITHUB_TOKEN: ${{ secrets.DOCUSAURUS_GH_TOKEN }} - HUSKY: 0 run: > yarn semantic-release ${{ inputs.dry_run && '--dry-run' || '' }} diff --git a/package.json b/package.json index e9a1546038..bebb324d73 100644 --- a/package.json +++ b/package.json @@ -102,8 +102,7 @@ "test-coverage": "nyc yarn test-unit", "fix-staged": "lint-staged --config .lintstagedrc.fix.json --concurrent 1", "semantic-release": "semantic-release", - "prepack": "yarn run build", - "prepare": "husky" + "prepare": "husky; yarn run build" }, "engines": { "node": ">=18" diff --git a/scripts/get-package-version.mjs b/scripts/get-package-version.mjs index 339058fa08..28a9439e20 100644 --- a/scripts/get-package-version.mjs +++ b/scripts/get-package-version.mjs @@ -3,17 +3,18 @@ import packageJson from '../package.json' with { type: 'json' }; // get the latest version so that "process.env.PKG_VERSION" can be replaced with it in the source code (used for reporting purposes), see bundle.mjs for source export default function getPackageVersion() { - // "build" script ("prepack" hook) gets invoked when semantic-release runs "npm publish", at that point package.json#version already contains updated next version which we can use + // "build" script ("prepare" hook) gets invoked when semantic-release runs "npm publish", at that point package.json#version already contains updated next version which we can use let version = packageJson.version; + console.log({ npm_package_version: process.env.npm_package_version }); + // if it fails (loads a default), try pulling version from git if (version === '0.0.0-development') { try { version = execSync('git describe --tags --abbrev=0').toString().trim(); } catch (error) { - console.error(error); console.warn( - 'Could not get latest version from git tags, falling back to package.json', + 'Could not get latest version from git tags, falling back to package.json#version', ); version = packageJson.version; } From cbed9dbf83dd966c8442cacd9e571fafdbe79daf Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 11 Mar 2025 10:31:55 +0000 Subject: [PATCH 27/47] chore(release): 9.0.0-rc.7 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [9.0.0-rc.7](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.6...v9.0.0-rc.7) (2025-03-11) ### ⚠ BREAKING CHANGES * `ErrorFromResponse` class constructor now requires second parameter (`status`, `response` and optionally `code`) ### Bug Fixes * adjust ErrorFromResponse error class ([#1491](https://github.com/GetStream/stream-chat-js/issues/1491)) ([ff32bd2](https://github.com/GetStream/stream-chat-js/commit/ff32bd26c90c85b2e6200a65ba7dd7852952517f)) --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8224525f51..f4620d92f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## [9.0.0-rc.7](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.6...v9.0.0-rc.7) (2025-03-11) + +### ⚠ BREAKING CHANGES + +* `ErrorFromResponse` class constructor now requires +second parameter (`status`, `response` and optionally `code`) + +### Bug Fixes + +* adjust ErrorFromResponse error class ([#1491](https://github.com/GetStream/stream-chat-js/issues/1491)) ([ff32bd2](https://github.com/GetStream/stream-chat-js/commit/ff32bd26c90c85b2e6200a65ba7dd7852952517f)) + ## [9.0.0-rc.6](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.5...v9.0.0-rc.6) (2025-03-06) ### Bug Fixes From 39091c7ee9b5a601809aca9773b87b3c95181709 Mon Sep 17 00:00:00 2001 From: Anton Arnautov <43254280+arnautov-anton@users.noreply.github.com> Date: Wed, 12 Mar 2025 14:51:56 +0100 Subject: [PATCH 28/47] fix: adjust ChannelResponse, ChannelData & PollResponse (#1493) ## Description of the changes, What, Why and How? --- src/poll.ts | 2 +- src/thread.ts | 1 + src/types.ts | 32 ++++++++++++++++---------------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/poll.ts b/src/poll.ts index 93480c5887..906a324293 100644 --- a/src/poll.ts +++ b/src/poll.ts @@ -208,7 +208,7 @@ export class Poll { ]; ownAnswer = event.poll_vote; } else if (event.poll_vote.option_id) { - if (event.poll.enforce_unique_votes) { + if (event.poll.enforce_unique_vote) { ownVotesByOptionId = { [event.poll_vote.option_id]: event.poll_vote }; } else { ownVotesByOptionId = Object.entries(ownVotesByOptionId).reduce< diff --git a/src/thread.ts b/src/thread.ts index 844f049801..59b3b9a061 100644 --- a/src/thread.ts +++ b/src/thread.ts @@ -122,6 +122,7 @@ export class Thread { threadData: ThreadResponse; }) { const channel = client.channel(threadData.channel.type, threadData.channel.id, { + // @ts-expect-error name is a "custom" property name: threadData.channel.name, }); channel._hydrateMembers({ diff --git a/src/types.ts b/src/types.ts index 7f119038c7..aa8f2c5d69 100644 --- a/src/types.ts +++ b/src/types.ts @@ -267,8 +267,10 @@ export type ChannelResponse = CustomChannelData & { frozen: boolean; id: string; type: string; + blocked?: boolean; auto_translation_enabled?: boolean; - auto_translation_language?: TranslationLanguages | ''; + auto_translation_language?: TranslationLanguages; + hide_messages_before?: string; config?: ChannelConfigWithInfo; cooldown?: number; created_at?: string; @@ -282,7 +284,7 @@ export type ChannelResponse = CustomChannelData & { member_count?: number; members?: ChannelMemberResponse[]; muted?: boolean; - name?: string; // FIXME: I believe this property should live in CustomChannelData + mute_expires_at?: string; own_capabilities?: string[]; team?: string; truncated_at?: string; @@ -1668,10 +1670,10 @@ export type ChannelFilters = QueryFilters< name?: | RequireOnlyOne< { - $autocomplete?: ChannelResponse['name']; - } & QueryFilter + $autocomplete?: string; + } & QueryFilter > - | PrimitiveFilter; + | PrimitiveFilter; pinned?: boolean; } & { [Key in keyof Omit]: @@ -2294,13 +2296,13 @@ export type ChannelConfigWithInfo = ChannelConfigFields & commands?: CommandResponse[]; }; -export type ChannelData = CustomChannelData & { - blocked?: boolean; - created_by?: UserResponse | null; - created_by_id?: UserResponse['id']; - members?: string[] | Array; - name?: string; -}; +export type ChannelData = CustomChannelData & + Partial<{ + blocked: boolean; + created_by: UserResponse | null; + created_by_id: UserResponse['id']; + members: string[] | Array; + }>; export type ChannelMute = { user: UserResponse; @@ -2831,7 +2833,6 @@ export type TokenOrProvider = null | string | TokenProvider | undefined; export type TokenProvider = () => Promise; export type TranslationLanguages = - | '' | 'af' | 'am' | 'ar' @@ -2887,7 +2888,8 @@ export type TranslationLanguages = | 'ur' | 'vi' | 'zh' - | 'zh-TW'; + | 'zh-TW' + | (string & {}); export type TypingStartEvent = Event; @@ -3248,7 +3250,6 @@ export type UpdatePollAPIResponse = { export type PollResponse = CustomPollData & PollEnrichData & { - cid: string; created_at: string; created_by: UserResponse | null; created_by_id: string; @@ -3261,7 +3262,6 @@ export type PollResponse = CustomPollData & allow_answers?: boolean; allow_user_suggested_options?: boolean; description?: string; - enforce_unique_votes?: boolean; is_closed?: boolean; voting_visibility?: VotingVisibility; }; From d7030c2957013f213de30a49bb70a47008be2d87 Mon Sep 17 00:00:00 2001 From: Anton Arnautov <43254280+arnautov-anton@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:59:59 +0100 Subject: [PATCH 29/47] fix: omit name from CustomChannelData for ChannelFilters (#1494) ## Description of the changes, What, Why and How? --- src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index aa8f2c5d69..02800a4343 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1654,7 +1654,7 @@ export type ReactionFilters = QueryFilters< >; export type ChannelFilters = QueryFilters< - ContainsOperator & { + ContainsOperator> & { archived?: boolean; 'member.user.name'?: | RequireOnlyOne<{ From a6e67d6ff514abcd74689e5d16513a729893ad24 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 12 Mar 2025 16:14:26 +0000 Subject: [PATCH 30/47] chore(release): 9.0.0-rc.8 [skip ci] ## [9.0.0-rc.8](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.7...v9.0.0-rc.8) (2025-03-12) ### Bug Fixes * adjust ChannelResponse, ChannelData & PollResponse ([#1493](https://github.com/GetStream/stream-chat-js/issues/1493)) ([39091c7](https://github.com/GetStream/stream-chat-js/commit/39091c7ee9b5a601809aca9773b87b3c95181709)) * omit name from CustomChannelData for ChannelFilters ([#1494](https://github.com/GetStream/stream-chat-js/issues/1494)) ([d7030c2](https://github.com/GetStream/stream-chat-js/commit/d7030c2957013f213de30a49bb70a47008be2d87)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4620d92f4..4863c84493 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [9.0.0-rc.8](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.7...v9.0.0-rc.8) (2025-03-12) + +### Bug Fixes + +* adjust ChannelResponse, ChannelData & PollResponse ([#1493](https://github.com/GetStream/stream-chat-js/issues/1493)) ([39091c7](https://github.com/GetStream/stream-chat-js/commit/39091c7ee9b5a601809aca9773b87b3c95181709)) +* omit name from CustomChannelData for ChannelFilters ([#1494](https://github.com/GetStream/stream-chat-js/issues/1494)) ([d7030c2](https://github.com/GetStream/stream-chat-js/commit/d7030c2957013f213de30a49bb70a47008be2d87)) + ## [9.0.0-rc.7](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.6...v9.0.0-rc.7) (2025-03-11) ### ⚠ BREAKING CHANGES From 5797193c418593ad6677fc8d5692c26b04d8633d Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 25 Mar 2025 16:05:09 +0000 Subject: [PATCH 31/47] chore(release): 9.0.0-rc.9 [skip ci] ## [9.0.0-rc.9](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.8...v9.0.0-rc.9) (2025-03-25) ### Bug Fixes * provide a way for a suffix to be added to wsUrl ([#1497](https://github.com/GetStream/stream-chat-js/issues/1497)) ([d14d0ff](https://github.com/GetStream/stream-chat-js/commit/d14d0ff1df942ab8ef4493c8ad7feb7601b951f2)) * use URLSearchParams when building WS url ([#1498](https://github.com/GetStream/stream-chat-js/issues/1498)) ([9413433](https://github.com/GetStream/stream-chat-js/commit/9413433c8544b8dfa96a878847fe94954935d529)) ### Features * [CHA-375] add draft messages api ([#1490](https://github.com/GetStream/stream-chat-js/issues/1490)) ([3a7f732](https://github.com/GetStream/stream-chat-js/commit/3a7f7327cb23bdf8578f9cffa885bcd1594f1cde)) --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4863c84493..f174b04f0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## [9.0.0-rc.9](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.8...v9.0.0-rc.9) (2025-03-25) + +### Bug Fixes + +* provide a way for a suffix to be added to wsUrl ([#1497](https://github.com/GetStream/stream-chat-js/issues/1497)) ([d14d0ff](https://github.com/GetStream/stream-chat-js/commit/d14d0ff1df942ab8ef4493c8ad7feb7601b951f2)) +* use URLSearchParams when building WS url ([#1498](https://github.com/GetStream/stream-chat-js/issues/1498)) ([9413433](https://github.com/GetStream/stream-chat-js/commit/9413433c8544b8dfa96a878847fe94954935d529)) + +### Features + +* [CHA-375] add draft messages api ([#1490](https://github.com/GetStream/stream-chat-js/issues/1490)) ([3a7f732](https://github.com/GetStream/stream-chat-js/commit/3a7f7327cb23bdf8578f9cffa885bcd1594f1cde)) + ## [9.0.0-rc.8](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.7...v9.0.0-rc.8) (2025-03-12) ### Bug Fixes From 09facf9d30cb53754b5f32bd8df2a2f726cf0d08 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 9 Apr 2025 08:21:39 +0000 Subject: [PATCH 32/47] chore(release): 9.0.0-rc.10 [skip ci] ## [9.0.0-rc.10](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.9...v9.0.0-rc.10) (2025-04-09) ### Bug Fixes * [CHA-826] normalize queryDrafts sort ([#1509](https://github.com/GetStream/stream-chat-js/issues/1509)) ([6df96c3](https://github.com/GetStream/stream-chat-js/commit/6df96c3d171d11891985307a71728063cacac73f)) * add event fields on creating campaign ([#1512](https://github.com/GetStream/stream-chat-js/issues/1512)) ([6c76d6d](https://github.com/GetStream/stream-chat-js/commit/6c76d6db1f33ae14bee546bbf952317a8a144c43)) ### Features * [CHA-660] - Team based roles ([#1499](https://github.com/GetStream/stream-chat-js/issues/1499)) ([dd62cff](https://github.com/GetStream/stream-chat-js/commit/dd62cff38e03678fb04222563e0dae75916ccadb)) * add show_channels flag to campaigns ([#1513](https://github.com/GetStream/stream-chat-js/issues/1513)) ([5344d2c](https://github.com/GetStream/stream-chat-js/commit/5344d2c6db205063c4e1c73f449e31c6ed6197c5)) * user profile check endpoint ([#1503](https://github.com/GetStream/stream-chat-js/issues/1503)) ([8114c22](https://github.com/GetStream/stream-chat-js/commit/8114c223b2a6650a89790e64a236d1a03cf64c3b)) ### Chores * **deps:** upgrade @babel/runtime to v7.27.0 ([#1508](https://github.com/GetStream/stream-chat-js/issues/1508)) ([eb29626](https://github.com/GetStream/stream-chat-js/commit/eb296263d258101c310eb1c0f44ca3fc2ced627c)), closes [#1502](https://github.com/GetStream/stream-chat-js/issues/1502) --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 068271eabe..a0033a3d67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +## [9.0.0-rc.10](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.9...v9.0.0-rc.10) (2025-04-09) + +### Bug Fixes + +* [CHA-826] normalize queryDrafts sort ([#1509](https://github.com/GetStream/stream-chat-js/issues/1509)) ([6df96c3](https://github.com/GetStream/stream-chat-js/commit/6df96c3d171d11891985307a71728063cacac73f)) +* add event fields on creating campaign ([#1512](https://github.com/GetStream/stream-chat-js/issues/1512)) ([6c76d6d](https://github.com/GetStream/stream-chat-js/commit/6c76d6db1f33ae14bee546bbf952317a8a144c43)) + +### Features + +* [CHA-660] - Team based roles ([#1499](https://github.com/GetStream/stream-chat-js/issues/1499)) ([dd62cff](https://github.com/GetStream/stream-chat-js/commit/dd62cff38e03678fb04222563e0dae75916ccadb)) +* add show_channels flag to campaigns ([#1513](https://github.com/GetStream/stream-chat-js/issues/1513)) ([5344d2c](https://github.com/GetStream/stream-chat-js/commit/5344d2c6db205063c4e1c73f449e31c6ed6197c5)) +* user profile check endpoint ([#1503](https://github.com/GetStream/stream-chat-js/issues/1503)) ([8114c22](https://github.com/GetStream/stream-chat-js/commit/8114c223b2a6650a89790e64a236d1a03cf64c3b)) + +### Chores + +* **deps:** upgrade @babel/runtime to v7.27.0 ([#1508](https://github.com/GetStream/stream-chat-js/issues/1508)) ([eb29626](https://github.com/GetStream/stream-chat-js/commit/eb296263d258101c310eb1c0f44ca3fc2ced627c)), closes [#1502](https://github.com/GetStream/stream-chat-js/issues/1502) + ## [9.0.0-rc.9](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.8...v9.0.0-rc.9) (2025-03-25) ### Bug Fixes From 16cd81a06c3f3daf4f6955d3c7f353283400031e Mon Sep 17 00:00:00 2001 From: Anton Arnautov <43254280+arnautov-anton@users.noreply.github.com> Date: Thu, 24 Apr 2025 08:50:37 +0200 Subject: [PATCH 33/47] fix: [REACT-344] remove Agora & 100ms integrations (#1519) --- src/channel.ts | 15 -------------- src/client.ts | 15 -------------- src/types.ts | 55 -------------------------------------------------- 3 files changed, 85 deletions(-) diff --git a/src/channel.ts b/src/channel.ts index 5cc47217e1..274c627764 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -20,8 +20,6 @@ import type { ChannelQueryOptions, ChannelResponse, ChannelUpdateOptions, - CreateCallOptions, - CreateCallResponse, CreateDraftResponse, DeleteChannelAPIResponse, DraftMessagePayload, @@ -1465,19 +1463,6 @@ export class Channel { }); } - /** - * createCall - creates a call for the current channel - * - * @param {CreateCallOptions} options - * @returns {Promise} - */ - async createCall(options: CreateCallOptions) { - return await this.getClient().post( - this._channelURL() + '/call', - options, - ); - } - /** * Cast or cancel one or more votes on a poll * @param pollId string The poll id diff --git a/src/client.ts b/src/client.ts index 66931187bc..3197e0d1cd 100644 --- a/src/client.ts +++ b/src/client.ts @@ -100,7 +100,6 @@ import type { FlagsResponse, FlagUserResponse, GetBlockedUsersAPIResponse, - GetCallTokenResponse, GetCampaignOptions, GetChannelTypeResponse, GetCommandResponse, @@ -2526,20 +2525,6 @@ export class StreamChat { }); } - /** - * getCallToken - retrieves the auth token needed to join a call - * - * @param {string} callID - * @param {object} options - * @returns {Promise} - */ - async getCallToken(callID: string, options: { user_id?: string } = {}) { - return await this.post( - this.baseURL + `/calls/${encodeURIComponent(callID)}`, - { ...options }, - ); - } - /** * _queryFlags - Query flags. * diff --git a/src/types.ts b/src/types.ts index 142f3a08d7..f12d53a921 100644 --- a/src/types.ts +++ b/src/types.ts @@ -112,7 +112,6 @@ export type AppSettingsAPIResponse = APIResponse & { } >; reminders_interval: number; - agora_options?: AgoraOptions | null; async_moderation_config?: AsyncModerationOptions; async_url_enrich_enabled?: boolean; auto_translation_enabled?: boolean; @@ -136,7 +135,6 @@ export type AppSettingsAPIResponse = APIResponse & { type: string; }>; grants?: Record; - hms_options?: HMSOptions | null; image_moderation_enabled?: boolean; image_upload_config?: FileUploadConfig; multi_tenant_enabled?: boolean; @@ -2154,21 +2152,6 @@ export type APNConfig = { team_id?: string; }; -export type AgoraOptions = { - app_certificate: string; - app_id: string; - role_map?: Record; -}; - -export type HMSOptions = { - app_access_key: string; - app_secret: string; - default_role: string; - default_room_template: string; - default_region?: string; - role_map?: Record; -}; - export type AsyncModerationOptions = { callback?: { mode?: 'CALLBACK_MODE_NONE' | 'CALLBACK_MODE_REST' | 'CALLBACK_MODE_TWIRP'; @@ -2178,7 +2161,6 @@ export type AsyncModerationOptions = { }; export type AppSettings = { - agora_options?: AgoraOptions | null; allowed_flag_reasons?: string[]; apn_config?: { auth_key?: string; @@ -2210,7 +2192,6 @@ export type AppSettings = { server_key?: string; }; grants?: Record; - hms_options?: HMSOptions | null; huawei_config?: { id: string; secret: string; @@ -3196,42 +3177,6 @@ export type PushProviderListResponse = { push_providers: PushProvider[]; }; -export type CreateCallOptions = { - id: string; - type: string; - options?: UR; - user?: UserResponse | null; - user_id?: string; -}; - -export type HMSCall = { - room: string; -}; - -export type AgoraCall = { - channel: string; -}; - -export type Call = { - id: string; - provider: string; - agora?: AgoraCall; - hms?: HMSCall; -}; - -export type CreateCallResponse = APIResponse & { - call: Call; - token: string; - agora_app_id?: string; - agora_uid?: number; -}; - -export type GetCallTokenResponse = APIResponse & { - token: string; - agora_app_id?: string; - agora_uid?: number; -}; - type ErrorResponseDetails = { code: number; messages: string[]; From 0c07524f6551e9257b229b262b4d1e03ab44561b Mon Sep 17 00:00:00 2001 From: martincupela Date: Mon, 28 Apr 2025 15:08:14 +0200 Subject: [PATCH 34/47] feat: message composer (#1495) This PR introduces a new message composer system that provides a robust foundation for message composition with support for drafts, middleware, and various composition features. - Message draft support for channels - Text composition middleware system - Command handling with case-insensitive search - Link preview integration - Mentions search with local member support - Notification management system - Local message type for better state management - Introduces `MessageComposer` class for managing message composition - Adds middleware system for text composition with extensible pipeline - Implements draft message handling with proper state management - Adds support for commands with trigger-based activation - Integrates link preview functionality during composition - Provides notification management for composition events - Introduces `LocalMessage` type for improved state handling Introduction of `LocalMessage` type. The `MessageResponse` is automatically transformed into LocalMessage type before being submitted to the state. - Added comprehensive test coverage for: - Message composition middleware - Command handling - Link preview integration - Mentions search - State management - Draft handling - Bundle size increase: +72.3 kB (+24.62%) - dist/cjs/index.browser.cjs: +19.4 kB - dist/cjs/index.node.cjs: +20 kB - dist/esm/index.js: +32.9 kB - Convert message composer into a plugin system - Further optimize bundle size - Add more middleware options for extensibility - Encapsulate interaction with the UI element representing the text editing area - Related to stream-chat-react#2669 - `linkifyjs@^4.2.0` BREAKING CHANGE: Replacement of FormatMessageResponse with LocalMessage type --------- Co-authored-by: Anton Arnautov Co-authored-by: Khushal Agarwal Co-authored-by: Zita Szupera Co-authored-by: Ivan Sekovanikj --- package.json | 1 + src/channel.ts | 29 +- src/channel_state.ts | 19 +- src/client.ts | 104 +- src/constants.ts | 29 +- src/events.ts | 2 + src/index.ts | 3 + src/messageComposer/CustomDataManager.ts | 49 + src/messageComposer/attachmentIdentity.ts | 92 ++ src/messageComposer/attachmentManager.ts | 493 ++++++++ .../configuration/configuration.ts | 42 + src/messageComposer/configuration/index.ts | 2 + src/messageComposer/configuration/types.ts | 59 + src/messageComposer/fileUtils.ts | 102 ++ src/messageComposer/index.ts | 10 + src/messageComposer/linkPreviewsManager.ts | 327 +++++ src/messageComposer/messageComposer.ts | 649 ++++++++++ src/messageComposer/middleware/index.ts | 3 + .../MessageComposerMiddlewareExecutor.ts | 65 + .../middleware/messageComposer/attachments.ts | 89 ++ .../middleware/messageComposer/cleanData.ts | 42 + .../messageComposer/compositionValidation.ts | 55 + .../middleware/messageComposer/customData.ts | 56 + .../middleware/messageComposer/index.ts | 9 + .../messageComposer/linkPreviews.ts | 90 ++ .../messageComposer/messageComposerState.ts | 70 + .../messageComposer/textComposer.ts | 91 ++ .../middleware/messageComposer/types.ts | 30 + .../PollComposerMiddlewareExecutor.ts | 26 + .../middleware/pollComposer/composition.ts | 17 + .../middleware/pollComposer/index.ts | 3 + .../middleware/pollComposer/state.ts | 205 +++ .../middleware/pollComposer/types.ts | 64 + .../TextComposerMiddlewareExecutor.ts | 56 + .../middleware/textComposer/commands.ts | 189 +++ .../middleware/textComposer/index.ts | 7 + .../middleware/textComposer/mentions.ts | 417 ++++++ .../textComposer/textMiddlewareUtils.ts | 136 ++ .../middleware/textComposer/types.ts | 36 + .../middleware/textComposer/validation.ts | 24 + src/messageComposer/pollComposer.ts | 152 +++ src/messageComposer/textComposer.ts | 223 ++++ src/messageComposer/types.ts | 164 +++ src/middleware.ts | 150 +++ src/notifications/NotificationManager.ts | 115 ++ src/notifications/index.ts | 2 + src/notifications/types.ts | 74 ++ src/poll_manager.ts | 4 +- src/search_controller.ts | 95 +- src/thread.ts | 60 +- src/thread_manager.ts | 6 +- src/types.ts | 62 +- src/types.utility.ts | 3 + src/utils.ts | 110 +- src/utils/FixedSizeQueueCache.ts | 74 ++ src/utils/concurrency.ts | 135 ++ src/utils/mergeWith/index.ts | 2 + src/utils/mergeWith/mergeWith.ts | 44 + src/utils/mergeWith/mergeWithCore.ts | 604 +++++++++ src/utils/mergeWith/mergeWithDiff.ts | 53 + .../MessageComposer/CustomDataManager.test.ts | 128 ++ .../attachmentIdentity.test.ts | 241 ++++ .../MessageComposer/attachmentManager.test.ts | 1126 +++++++++++++++++ test/unit/MessageComposer/fileUtils.test.ts | 342 +++++ .../linkPreviewsManager.test.ts | 677 ++++++++++ .../MessageComposer/messageComposer.test.ts | 1045 +++++++++++++++ .../messageComposer/attachments.test.ts | 522 ++++++++ .../compositionValidation.test.ts | 530 ++++++++ .../messageComposer/customData.test.ts | 126 ++ .../messageComposer/linkPreviews.test.ts | 742 +++++++++++ .../messageComposerState.test.ts | 461 +++++++ .../messageComposer/textComposer.test.ts | 600 +++++++++ .../pollComposer/composition.test.ts | 106 ++ .../middleware/pollComposer/state.test.ts | 322 +++++ .../textComposer/CommandSearchSource.test.ts | 85 ++ .../textComposer/MentionsSearchSource.test.ts | 295 +++++ .../TextComposerMiddlewareExecutor.test.ts | 517 ++++++++ .../unit/MessageComposer/pollComposer.test.ts | 353 ++++++ .../unit/MessageComposer/textComposer.test.ts | 652 ++++++++++ test/unit/channel.test.js | 4 +- test/unit/client.test.js | 1 + test/unit/middleware.test.ts | 559 ++++++++ test/unit/test-utils/generateMessageDraft.ts | 18 + test/unit/utils/FixedSizeQueueCache.test.ts | 141 +++ test/unit/utils/concurrency.test.ts | 285 +++++ test/unit/utils/mergeWith.test.ts | 749 +++++++++++ yarn.lock | 5 + 87 files changed, 16395 insertions(+), 161 deletions(-) create mode 100644 src/messageComposer/CustomDataManager.ts create mode 100644 src/messageComposer/attachmentIdentity.ts create mode 100644 src/messageComposer/attachmentManager.ts create mode 100644 src/messageComposer/configuration/configuration.ts create mode 100644 src/messageComposer/configuration/index.ts create mode 100644 src/messageComposer/configuration/types.ts create mode 100644 src/messageComposer/fileUtils.ts create mode 100644 src/messageComposer/index.ts create mode 100644 src/messageComposer/linkPreviewsManager.ts create mode 100644 src/messageComposer/messageComposer.ts create mode 100644 src/messageComposer/middleware/index.ts create mode 100644 src/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.ts create mode 100644 src/messageComposer/middleware/messageComposer/attachments.ts create mode 100644 src/messageComposer/middleware/messageComposer/cleanData.ts create mode 100644 src/messageComposer/middleware/messageComposer/compositionValidation.ts create mode 100644 src/messageComposer/middleware/messageComposer/customData.ts create mode 100644 src/messageComposer/middleware/messageComposer/index.ts create mode 100644 src/messageComposer/middleware/messageComposer/linkPreviews.ts create mode 100644 src/messageComposer/middleware/messageComposer/messageComposerState.ts create mode 100644 src/messageComposer/middleware/messageComposer/textComposer.ts create mode 100644 src/messageComposer/middleware/messageComposer/types.ts create mode 100644 src/messageComposer/middleware/pollComposer/PollComposerMiddlewareExecutor.ts create mode 100644 src/messageComposer/middleware/pollComposer/composition.ts create mode 100644 src/messageComposer/middleware/pollComposer/index.ts create mode 100644 src/messageComposer/middleware/pollComposer/state.ts create mode 100644 src/messageComposer/middleware/pollComposer/types.ts create mode 100644 src/messageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.ts create mode 100644 src/messageComposer/middleware/textComposer/commands.ts create mode 100644 src/messageComposer/middleware/textComposer/index.ts create mode 100644 src/messageComposer/middleware/textComposer/mentions.ts create mode 100644 src/messageComposer/middleware/textComposer/textMiddlewareUtils.ts create mode 100644 src/messageComposer/middleware/textComposer/types.ts create mode 100644 src/messageComposer/middleware/textComposer/validation.ts create mode 100644 src/messageComposer/pollComposer.ts create mode 100644 src/messageComposer/textComposer.ts create mode 100644 src/messageComposer/types.ts create mode 100644 src/middleware.ts create mode 100644 src/notifications/NotificationManager.ts create mode 100644 src/notifications/index.ts create mode 100644 src/notifications/types.ts create mode 100644 src/types.utility.ts create mode 100644 src/utils/FixedSizeQueueCache.ts create mode 100644 src/utils/concurrency.ts create mode 100644 src/utils/mergeWith/index.ts create mode 100644 src/utils/mergeWith/mergeWith.ts create mode 100644 src/utils/mergeWith/mergeWithCore.ts create mode 100644 src/utils/mergeWith/mergeWithDiff.ts create mode 100644 test/unit/MessageComposer/CustomDataManager.test.ts create mode 100644 test/unit/MessageComposer/attachmentIdentity.test.ts create mode 100644 test/unit/MessageComposer/attachmentManager.test.ts create mode 100644 test/unit/MessageComposer/fileUtils.test.ts create mode 100644 test/unit/MessageComposer/linkPreviewsManager.test.ts create mode 100644 test/unit/MessageComposer/messageComposer.test.ts create mode 100644 test/unit/MessageComposer/middleware/messageComposer/attachments.test.ts create mode 100644 test/unit/MessageComposer/middleware/messageComposer/compositionValidation.test.ts create mode 100644 test/unit/MessageComposer/middleware/messageComposer/customData.test.ts create mode 100644 test/unit/MessageComposer/middleware/messageComposer/linkPreviews.test.ts create mode 100644 test/unit/MessageComposer/middleware/messageComposer/messageComposerState.test.ts create mode 100644 test/unit/MessageComposer/middleware/messageComposer/textComposer.test.ts create mode 100644 test/unit/MessageComposer/middleware/pollComposer/composition.test.ts create mode 100644 test/unit/MessageComposer/middleware/pollComposer/state.test.ts create mode 100644 test/unit/MessageComposer/middleware/textComposer/CommandSearchSource.test.ts create mode 100644 test/unit/MessageComposer/middleware/textComposer/MentionsSearchSource.test.ts create mode 100644 test/unit/MessageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.test.ts create mode 100644 test/unit/MessageComposer/pollComposer.test.ts create mode 100644 test/unit/MessageComposer/textComposer.test.ts create mode 100644 test/unit/middleware.test.ts create mode 100644 test/unit/test-utils/generateMessageDraft.ts create mode 100644 test/unit/utils/FixedSizeQueueCache.test.ts create mode 100644 test/unit/utils/concurrency.test.ts create mode 100644 test/unit/utils/mergeWith.test.ts diff --git a/package.json b/package.json index bebb324d73..634c9aa0bd 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "form-data": "^4.0.0", "isomorphic-ws": "^5.0.0", "jsonwebtoken": "^9.0.2", + "linkifyjs": "^4.2.0", "ws": "^8.18.1" }, "devDependencies": { diff --git a/src/channel.ts b/src/channel.ts index 274c627764..db24471b26 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -27,11 +27,11 @@ import type { EventAPIResponse, EventHandler, EventTypes, - FormatMessageResponse, GetDraftResponse, GetMultipleMessagesAPIResponse, GetReactionsAPIResponse, GetRepliesAPIResponse, + LocalMessage, MarkReadOptions, MarkUnreadOptions, MemberFilters, @@ -70,6 +70,7 @@ import type { } from './types'; import type { Role } from './permissions'; import type { CustomChannelData } from './custom_types'; +import { MessageComposer } from './messageComposer'; /** * Channel - The Channel class manages it's own state. @@ -104,6 +105,7 @@ export class Channel { isTyping: boolean; disconnected: boolean; push_preferences?: PushPreference; + public readonly messageComposer: MessageComposer; /** * constructor - Create a channel @@ -147,6 +149,11 @@ export class Channel { this.lastTypingEvent = null; this.isTyping = false; this.disconnected = false; + + this.messageComposer = new MessageComposer({ + client: this._client, + compositionContext: this, + }); } /** @@ -421,7 +428,9 @@ export class Channel { const url = this.getClient().baseURL + - `/messages/${encodeURIComponent(messageID)}/reaction/${encodeURIComponent(reactionType)}`; + `/messages/${encodeURIComponent(messageID)}/reaction/${encodeURIComponent( + reactionType, + )}`; //provided when server side request if (user_id) { return this.getClient().delete(url, { user_id }); @@ -950,7 +959,7 @@ export class Channel { * * @return {ReturnType | undefined} Description */ - lastMessage(): FormatMessageResponse | undefined { + lastMessage(): LocalMessage | undefined { // get last 5 messages, sort, return the latest // get a slice of the last 5 let min = this.state.latestMessages.length - 5; @@ -1176,7 +1185,7 @@ export class Channel { } } - _countMessageAsUnread(message: FormatMessageResponse | MessageResponse) { + _countMessageAsUnread(message: LocalMessage | MessageResponse) { if (message.shadowed) return false; if (message.silent) return false; if (message.parent_id && !message.show_in_channel) return false; @@ -1285,7 +1294,9 @@ export class Channel { ); } - let queryURL = `${this.getClient().baseURL}/channels/${encodeURIComponent(this.type)}`; + let queryURL = `${this.getClient().baseURL}/channels/${encodeURIComponent( + this.type, + )}`; if (this.id) { queryURL += `/${encodeURIComponent(this.id)}`; } @@ -1342,6 +1353,10 @@ export class Channel { this.getClient().polls.hydratePollCache(state.messages, true); + if (state.draft) { + this.messageComposer.initState({ composition: state.draft }); + } + const areCapabilitiesChanged = [...(state.channel.own_capabilities || [])].sort().join() !== [ @@ -1898,7 +1913,9 @@ export class Channel { if (!this.id) { throw new Error('channel id is not defined'); } - return `${this.getClient().baseURL}/channels/${encodeURIComponent(this.type)}/${encodeURIComponent(this.id)}`; + return `${this.getClient().baseURL}/channels/${encodeURIComponent( + this.type, + )}/${encodeURIComponent(this.id)}`; }; _checkInitialized() { diff --git a/src/channel_state.ts b/src/channel_state.ts index 06c1027393..5c14bc19bf 100644 --- a/src/channel_state.ts +++ b/src/channel_state.ts @@ -2,8 +2,9 @@ import type { Channel } from './channel'; import type { ChannelMemberResponse, Event, - FormatMessageResponse, + LocalMessage, MessageResponse, + MessageResponseBase, MessageSet, MessageSetType, PendingMessageResponse, @@ -122,7 +123,7 @@ export class ChannelState { * @param {MessageSetType} messageSetToAddToIfDoesNotExist Which message set to add to if message is not in the list (only used if addIfDoesNotExist is true) */ addMessageSorted( - newMessage: MessageResponse, + newMessage: MessageResponse | LocalMessage, timestampChanged = false, addIfDoesNotExist = true, messageSetToAddToIfDoesNotExist: MessageSetType = 'latest', @@ -142,7 +143,8 @@ export class ChannelState { * * @param {MessageResponse} message `MessageResponse` object */ - formatMessage = (message: MessageResponse) => formatMessage(message); + formatMessage = (message: MessageResponse | MessageResponseBase | LocalMessage) => + formatMessage(message); /** * addMessagesSorted - Add the list of messages to state and resorts the messages @@ -155,7 +157,7 @@ export class ChannelState { * */ addMessagesSorted( - newMessages: MessageResponse[], + newMessages: (MessageResponse | LocalMessage)[], timestampChanged = false, initializing = false, addIfDoesNotExist = true, @@ -180,7 +182,7 @@ export class ChannelState { if (isMessageFormatted) { message = messagesToAdd[i] as ReturnType; } else { - message = this.formatMessage(messagesToAdd[i] as MessageResponse); + message = this.formatMessage(messagesToAdd[i]); if (message.user && this._channel?.cid) { /** @@ -365,7 +367,7 @@ export class ChannelState { updated_at: m.updated_at?.toISOString(), }) as unknown as MessageResponse; - const update = (messages: FormatMessageResponse[]) => { + const update = (messages: LocalMessage[]) => { const updatedMessages = messages.reduce((acc, msg) => { if (msg.quoted_message_id === message.id) { acc.push({ @@ -757,12 +759,11 @@ export class ChannelState { } private findTargetMessageSet( - newMessages: MessageResponse[], + newMessages: (MessageResponse | LocalMessage)[], addIfDoesNotExist = true, messageSetToAddToIfDoesNotExist: MessageSetType = 'current', ) { - let messagesToAdd: (MessageResponse | ReturnType)[] = - newMessages; + let messagesToAdd: (MessageResponse | LocalMessage)[] = newMessages; let targetMessageSetIndex!: number; if (addIfDoesNotExist) { const overlappingMessageSetIndices = this.messageSets diff --git a/src/client.ts b/src/client.ts index 3197e0d1cd..d31ff3d832 100644 --- a/src/client.ts +++ b/src/client.ts @@ -28,6 +28,7 @@ import { randomId, retryInterval, sleep, + toUpdatedMessagePayload, } from './utils'; import type { @@ -117,6 +118,7 @@ import type { ListCommandsResponse, ListImportsPaginationOptions, ListImportsResponse, + LocalMessage, Logger, MarkChannelsReadOptions, MessageFilters, @@ -172,7 +174,6 @@ import type { ReactionSort, ReactivateUserOptions, ReactivateUsersOptions, - ReservedMessageFields, ReviewFlagReportOptions, ReviewFlagReportResponse, SdkIdentifier, @@ -201,7 +202,6 @@ import type { UpdateChannelTypeResponse, UpdateCommandOptions, UpdateCommandResponse, - UpdatedMessage, UpdateMessageAPIResponse, UpdateMessageOptions, UpdatePollAPIResponse, @@ -227,20 +227,45 @@ import type { ChannelManagerOptions, } from './channel_manager'; import { ChannelManager } from './channel_manager'; +import { NotificationManager } from './notifications'; +import { StateStore } from './store'; +import type { MessageComposer } from './messageComposer'; function isString(x: unknown): x is string { return typeof x === 'string' || x instanceof String; } +type MessageComposerTearDownFunction = () => void; + +type MessageComposerSetupFunction = ({ + composer, +}: { + composer: MessageComposer; +}) => void | MessageComposerTearDownFunction; + +type MessageComposerSetupState = { + /** + * Each `MessageComposer` runs this function each time its signature changes or + * whenever you run `MessageComposer.registerSubscriptions`. Function returned + * from `applyModifications` will be used as a cleanup function - it will be stored + * and ran before new modification is applied. Cleaning up only the + * modified parts is the general way to go but if your setup gets a bit + * complicated, feel free to restore the whole composer with `MessageComposer.restore`. + */ + setupFunction: MessageComposerSetupFunction | null; +}; + export class StreamChat { private static _instance?: unknown | StreamChat; // type is undefined|StreamChat, unknown is due to TS limitations with statics _user?: OwnUserResponse | UserResponse; + appSettingsPromise?: Promise; activeChannels: { [key: string]: Channel; }; threads: ThreadManager; polls: PollManager; + notifications: NotificationManager; anonymous: boolean; persistUserOnConnectionFailure?: boolean; axiosInstance: AxiosInstance; @@ -285,6 +310,12 @@ export class StreamChat { sdkIdentifier?: SdkIdentifier; deviceIdentifier?: DeviceIdentifier; private nextRequestAbortController: AbortController | null = null; + /** + * @private + */ + _messageComposerSetupState = new StateStore({ + setupFunction: null, + }); /** * Initialize a client @@ -322,6 +353,8 @@ export class StreamChat { this.moderation = new Moderation(this); + this.notifications = options?.notifications ?? new NotificationManager(); + // set the secret if (secretOrOptions && isString(secretOrOptions)) { this.secret = secretOrOptions; @@ -795,7 +828,8 @@ export class StreamChat { * getAppSettings - retrieves application settings */ async getAppSettings() { - return await this.get(this.baseURL + '/app'); + this.appSettingsPromise = this.get(this.baseURL + '/app'); + return await this.appSettingsPromise; } /** @@ -1826,6 +1860,10 @@ export class StreamChat { this.polls.hydratePollCache(channelState.messages, true); } + if (channelState.draft) { + c.messageComposer.initState({ composition: channelState.draft }); + } + channels.push(c); } @@ -2825,69 +2863,31 @@ export class StreamChat { * @param {string | { id: string }} [userId] * @param {boolean} [options.skip_enrich_url] Do not try to enrich the URLs within message * - * @return {{ message: MessageResponse }} Response that includes the message + * @return {{ message: LocalMessage | MessageResponse }} Response that includes the message */ async updateMessage( - message: UpdatedMessage, + message: LocalMessage | Partial, userId?: string | { id: string }, options?: UpdateMessageOptions, ) { if (!message.id) { throw Error('Please specify the message id when calling updateMessage'); } - - const clonedMessage: Partial = { ...message }; - delete clonedMessage.id; - - const reservedMessageFields: Array = [ - 'command', - 'created_at', - 'html', - 'latest_reactions', - 'own_reactions', - 'quoted_message', - 'reaction_counts', - 'reply_count', - 'type', - 'updated_at', - 'user', - 'pinned_at', - '__html', - ]; - - for (const field of reservedMessageFields) { - if (typeof clonedMessage[field] !== 'undefined') { - delete clonedMessage[field]; - } - } - + const payload = toUpdatedMessagePayload(message); if (userId != null) { if (isString(userId)) { - clonedMessage.user_id = userId; + payload.user_id = userId; } else { - clonedMessage.user = { + payload.user = { id: userId.id, - } as UserResponse; + }; } } - /** - * Server always expects mentioned_users to be array of string. We are adding extra check, just in case - * SDK missed this conversion. - */ - if ( - Array.isArray(clonedMessage.mentioned_users) && - !isString(clonedMessage.mentioned_users[0]) - ) { - clonedMessage.mentioned_users = clonedMessage.mentioned_users.map( - (mu) => (mu as unknown as UserResponse).id, - ); - } - return await this.post( this.baseURL + `/messages/${encodeURIComponent(message.id as string)}`, { - message: clonedMessage, + message: payload, ...options, }, ); @@ -3036,7 +3036,7 @@ export class StreamChat { */ async getThread(messageId: string, options: GetThreadOptions = {}) { if (!messageId) { - throw Error('Please specify the messageId when calling getThread'); + throw new Error('Please specify the messageId when calling getThread'); } const optionsWithDefaults = { @@ -4368,4 +4368,10 @@ export class StreamChat { return await this.post(this.baseURL + '/drafts/query', payload); } + + public setMessageComposerSetupFunction = ( + setupFunction: MessageComposerSetupState['setupFunction'], + ) => { + this._messageComposerSetupState.partialNext({ setupFunction }); + }; } diff --git a/src/constants.ts b/src/constants.ts index 7f1d8a4b15..cc622a95dc 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,4 +1,31 @@ +import type { ReservedUpdatedMessageFields } from './types'; + export const DEFAULT_QUERY_CHANNELS_MESSAGE_LIST_PAGE_SIZE = 25; export const DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE = 100; - export const DEFAULT_MESSAGE_SET_PAGINATION = { hasNext: false, hasPrev: false }; +export const DEFAULT_UPLOAD_SIZE_LIMIT_BYTES = 100 * 1024 * 1024; // 100 MB +export const API_MAX_FILES_ALLOWED_PER_MESSAGE = 10; +export const MAX_CHANNEL_MEMBER_COUNT_IN_CHANNEL_QUERY = 100; +export const RESERVED_UPDATED_MESSAGE_FIELDS: Array = [ + // Dates should not be converted back to ISO strings as JS looses precision on them (milliseconds) + 'created_at', + 'deleted_at', + 'pinned_at', + 'updated_at', + 'command', + // Back-end enriches these fields + 'mentioned_users', + 'quoted_message', + // Client-specific fields + 'latest_reactions', + 'own_reactions', + 'reaction_counts', + 'reply_count', + // Message text related fields that shouldn't be in update + 'i18n', + 'type', + 'html', + '__html', +] as const; + +export const LOCAL_MESSAGE_FIELDS = ['error'] as const; diff --git a/src/events.ts b/src/events.ts index df603a8f34..b9f8c622b0 100644 --- a/src/events.ts +++ b/src/events.ts @@ -8,6 +8,8 @@ export const EVENT_MAP = { 'channel.unmuted': true, 'channel.updated': true, 'channel.visible': true, + 'draft.deleted': true, + 'draft.updated': true, 'health.check': true, 'member.added': true, 'member.removed': true, diff --git a/src/index.ts b/src/index.ts index 472b416426..242ffb3dcc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ export * from './channel_state'; export * from './connection'; export * from './events'; export * from './insights'; +export * from './messageComposer'; export * from './moderation'; export * from './permissions'; export * from './poll'; @@ -44,6 +45,8 @@ export { isOwnUser, chatCodes, logChatPromiseExecution, + localMessageToNewMessagePayload, formatMessage, promoteChannel, } from './utils'; +export { FixedSizeQueueCache } from './utils/FixedSizeQueueCache'; diff --git a/src/messageComposer/CustomDataManager.ts b/src/messageComposer/CustomDataManager.ts new file mode 100644 index 0000000000..da4f1d1f7a --- /dev/null +++ b/src/messageComposer/CustomDataManager.ts @@ -0,0 +1,49 @@ +import type { CustomMessageData, DraftMessage, LocalMessage } from '..'; +import { StateStore } from '..'; +import type { MessageComposer } from './messageComposer'; + +export type CustomDataManagerState = { + data: CustomMessageData; +}; + +export type CustomDataManagerOptions = { + composer: MessageComposer; + message?: DraftMessage | LocalMessage; +}; + +const initState = (options: CustomDataManagerOptions): CustomDataManagerState => { + if (!options) return { data: {} as CustomMessageData }; + return { data: {} as CustomMessageData }; +}; + +export class CustomDataManager { + composer: MessageComposer; + state: StateStore; + + constructor({ composer, message }: CustomDataManagerOptions) { + this.composer = composer; + this.state = new StateStore(initState({ composer, message })); + } + + get data() { + return this.state.getLatestValue().data; + } + + isDataEqual = ( + nextState: CustomDataManagerState, + previousState?: CustomDataManagerState, + ) => JSON.stringify(nextState.data) === JSON.stringify(previousState?.data); + + initState = ({ message }: { message?: DraftMessage | LocalMessage } = {}) => { + this.state.next(initState({ composer: this.composer, message })); + }; + + setData(data: Partial) { + this.state.partialNext({ + data: { + ...this.state.getLatestValue().data, + ...data, + }, + }); + } +} diff --git a/src/messageComposer/attachmentIdentity.ts b/src/messageComposer/attachmentIdentity.ts new file mode 100644 index 0000000000..87a4482804 --- /dev/null +++ b/src/messageComposer/attachmentIdentity.ts @@ -0,0 +1,92 @@ +import type { Attachment } from '../types'; +import type { + AudioAttachment, + FileAttachment, + ImageAttachment, + LocalAttachment, + LocalAudioAttachment, + LocalFileAttachment, + LocalImageAttachment, + LocalUploadAttachment, + LocalVideoAttachment, + LocalVoiceRecordingAttachment, + UploadedAttachment, + VideoAttachment, + VoiceRecordingAttachment, +} from './types'; + +export const isScrapedContent = (attachment: Attachment) => + !!attachment?.og_scrape_url || !!attachment?.title_link; + +export const isLocalAttachment = (attachment: unknown): attachment is LocalAttachment => + !!(attachment as LocalAttachment)?.localMetadata?.id; + +export const isLocalUploadAttachment = ( + attachment: unknown, +): attachment is LocalUploadAttachment => + !!(attachment as LocalAttachment)?.localMetadata?.uploadState; + +export const isFileAttachment = ( + attachment: Attachment | LocalAttachment, + supportedVideoFormat: string[] = [], +): attachment is FileAttachment => + attachment.type === 'file' || + !!( + attachment.mime_type && + supportedVideoFormat.indexOf(attachment.mime_type) === -1 && + attachment.type !== 'video' + ); + +export const isLocalFileAttachment = ( + attachment: Attachment | LocalAttachment, +): attachment is LocalFileAttachment => + isFileAttachment(attachment) && isLocalAttachment(attachment); + +export const isImageAttachment = ( + attachment: Attachment, +): attachment is ImageAttachment => + attachment.type === 'image' && !isScrapedContent(attachment); + +export const isLocalImageAttachment = ( + attachment: Attachment | LocalAttachment, +): attachment is LocalImageAttachment => + isImageAttachment(attachment) && isLocalAttachment(attachment); + +export const isAudioAttachment = ( + attachment: Attachment | LocalAttachment, +): attachment is AudioAttachment => attachment.type === 'audio'; + +export const isLocalAudioAttachment = ( + attachment: Attachment | LocalAttachment, +): attachment is LocalAudioAttachment => + isAudioAttachment(attachment) && isLocalAttachment(attachment); + +export const isVoiceRecordingAttachment = ( + attachment: Attachment | LocalAttachment, +): attachment is VoiceRecordingAttachment => attachment.type === 'voiceRecording'; + +export const isLocalVoiceRecordingAttachment = ( + attachment: Attachment | LocalAttachment, +): attachment is LocalVoiceRecordingAttachment => + isVoiceRecordingAttachment(attachment) && isLocalAttachment(attachment); + +export const isVideoAttachment = ( + attachment: Attachment | LocalAttachment, + supportedVideoFormat: string[] = [], +): attachment is VideoAttachment => + attachment.type === 'video' || + !!(attachment.mime_type && supportedVideoFormat.indexOf(attachment.mime_type) !== -1); + +export const isLocalVideoAttachment = ( + attachment: Attachment | LocalAttachment, +): attachment is LocalVideoAttachment => + isVideoAttachment(attachment) && isLocalAttachment(attachment); + +export const isUploadedAttachment = ( + attachment: Attachment, +): attachment is UploadedAttachment => + isAudioAttachment(attachment) || + isFileAttachment(attachment) || + isImageAttachment(attachment) || + isVideoAttachment(attachment) || + isVoiceRecordingAttachment(attachment); diff --git a/src/messageComposer/attachmentManager.ts b/src/messageComposer/attachmentManager.ts new file mode 100644 index 0000000000..5361136bfe --- /dev/null +++ b/src/messageComposer/attachmentManager.ts @@ -0,0 +1,493 @@ +import type { + AttachmentManagerConfig, + MinimumUploadRequestResult, + UploadRequestFn, +} from './configuration'; +import { isLocalImageAttachment, isUploadedAttachment } from './attachmentIdentity'; +import { + createFileFromBlobs, + ensureIsLocalAttachment, + generateFileName, + getAttachmentTypeFromMimeType, + isFile, + isFileList, + isFileReference, + isImageFile, +} from './fileUtils'; +import { StateStore } from '../store'; +import { generateUUIDv4 } from '../utils'; +import { DEFAULT_UPLOAD_SIZE_LIMIT_BYTES } from '../constants'; +import type { + AttachmentLoadingState, + FileLike, + FileReference, + LocalAttachment, + LocalAudioAttachment, + LocalFileAttachment, + LocalUploadAttachment, + LocalVideoAttachment, + LocalVoiceRecordingAttachment, + UploadPermissionCheckResult, +} from './types'; +import type { ChannelResponse, DraftMessage, LocalMessage } from '../types'; +import type { MessageComposer } from './messageComposer'; +import { mergeWithDiff } from '../utils/mergeWith'; + +type LocalNotImageAttachment = + | LocalFileAttachment + | LocalAudioAttachment + | LocalVideoAttachment + | LocalVoiceRecordingAttachment; + +export type FileUploadFilter = (file: Partial) => boolean; + +export type AttachmentManagerState = { + attachments: LocalAttachment[]; +}; + +export type AttachmentManagerOptions = { + composer: MessageComposer; + message?: DraftMessage | LocalMessage; +}; + +const initState = ({ + message, +}: { + message?: DraftMessage | LocalMessage; +}): AttachmentManagerState => ({ + attachments: (message?.attachments ?? []) + ?.filter(({ og_scrape_url }) => !og_scrape_url) + .map((att) => { + const localMetadata = isUploadedAttachment(att) + ? { id: generateUUIDv4(), uploadState: 'finished' } + : { id: generateUUIDv4() }; + return { + ...att, + localMetadata, + } as LocalAttachment; + }), +}); + +export class AttachmentManager { + readonly state: StateStore; + readonly composer: MessageComposer; + + constructor({ composer, message }: AttachmentManagerOptions) { + this.composer = composer; + this.state = new StateStore(initState({ message })); + } + + get client() { + return this.composer.client; + } + + get channel() { + return this.composer.channel; + } + + get config() { + return this.composer.config.attachments; + } + + get fileUploadFilter() { + return this.config.fileUploadFilter; + } + + set fileUploadFilter(fileUploadFilter: AttachmentManagerConfig['fileUploadFilter']) { + this.composer.updateConfig({ attachments: { fileUploadFilter } }); + } + + set maxNumberOfFilesPerMessage( + maxNumberOfFilesPerMessage: AttachmentManagerConfig['maxNumberOfFilesPerMessage'], + ) { + this.composer.updateConfig({ attachments: { maxNumberOfFilesPerMessage } }); + } + + setCustomUploadFn = (doUploadRequest: UploadRequestFn) => { + this.composer.updateConfig({ attachments: { doUploadRequest } }); + }; + + get attachments() { + return this.state.getLatestValue().attachments; + } + + get hasUploadPermission() { + return !!( + this.channel.data?.own_capabilities as ChannelResponse['own_capabilities'] + )?.includes('upload-file'); + } + + get isUploadEnabled() { + return this.hasUploadPermission && this.availableUploadSlots > 0; + } + + get successfulUploads() { + return this.getUploadsByState('finished'); + } + + get successfulUploadsCount() { + return this.successfulUploads.length; + } + + get uploadsInProgressCount() { + return this.getUploadsByState('uploading').length; + } + + get failedUploadsCount() { + return this.getUploadsByState('failed').length; + } + + get blockedUploadsCount() { + return this.getUploadsByState('blocked').length; + } + + get pendingUploadsCount() { + return this.getUploadsByState('pending').length; + } + + get availableUploadSlots() { + return ( + this.config.maxNumberOfFilesPerMessage - + this.successfulUploadsCount - + this.uploadsInProgressCount + ); + } + + getUploadsByState(state: AttachmentLoadingState) { + return Object.values(this.attachments).filter( + ({ localMetadata }) => localMetadata.uploadState === state, + ); + } + + initState = ({ message }: { message?: DraftMessage | LocalMessage } = {}) => { + this.state.next(initState({ message })); + }; + + getAttachmentIndex = (localId: string) => + this.attachments.findIndex( + (attachment) => + attachment.localMetadata.id && localId === attachment.localMetadata?.id, + ); + + upsertAttachments = (attachmentsToUpsert: LocalAttachment[]) => { + if (!attachmentsToUpsert.length) return; + const stateAttachments = this.attachments; + const attachments = [...this.attachments]; + attachmentsToUpsert.forEach((upsertedAttachment) => { + const attachmentIndex = this.getAttachmentIndex( + upsertedAttachment.localMetadata.id, + ); + + if (attachmentIndex === -1) { + const localAttachment = ensureIsLocalAttachment(upsertedAttachment); + if (localAttachment) attachments.push(localAttachment); + } else { + const merged = mergeWithDiff( + stateAttachments[attachmentIndex] ?? {}, + upsertedAttachment, + ); + const updatesOnMerge = merged.diff && Object.keys(merged.diff.children).length; + if (updatesOnMerge) { + const localAttachment = ensureIsLocalAttachment(merged.result); + if (localAttachment) attachments.splice(attachmentIndex, 1, localAttachment); + } + } + }); + + this.state.partialNext({ attachments }); + }; + + removeAttachments = (localAttachmentIds: string[]) => { + this.state.partialNext({ + attachments: this.attachments.filter( + (att) => !localAttachmentIds.includes(att.localMetadata?.id), + ), + }); + }; + + getUploadConfigCheck = async ( + fileLike: FileReference | FileLike, + ): Promise => { + const client = this.channel.getClient(); + let appSettings; + if (!client.appSettingsPromise) { + appSettings = await client.getAppSettings(); + } else { + appSettings = await client.appSettingsPromise; + } + const uploadConfig = isImageFile(fileLike) + ? appSettings?.app?.image_upload_config + : appSettings?.app?.file_upload_config; + if (!uploadConfig) return { uploadBlocked: false }; + + const { + allowed_file_extensions, + allowed_mime_types, + blocked_file_extensions, + blocked_mime_types, + size_limit, + } = uploadConfig; + + const sizeLimit = size_limit || DEFAULT_UPLOAD_SIZE_LIMIT_BYTES; + const mimeType = fileLike.type; + + if (isFile(fileLike)) { + if ( + allowed_file_extensions?.length && + !allowed_file_extensions.some((ext) => + fileLike.name.toLowerCase().endsWith(ext.toLowerCase()), + ) + ) { + return { uploadBlocked: true, reason: 'allowed_file_extensions' }; + } + + if ( + blocked_file_extensions?.length && + blocked_file_extensions.some((ext) => + fileLike.name.toLowerCase().endsWith(ext.toLowerCase()), + ) + ) { + return { uploadBlocked: true, reason: 'blocked_file_extensions' }; + } + } + + if ( + allowed_mime_types?.length && + !allowed_mime_types.some((type) => type.toLowerCase() === mimeType?.toLowerCase()) + ) { + return { uploadBlocked: true, reason: 'allowed_mime_types' }; + } + + if ( + blocked_mime_types?.length && + blocked_mime_types.some((type) => type.toLowerCase() === mimeType?.toLowerCase()) + ) { + return { uploadBlocked: true, reason: 'blocked_mime_types' }; + } + + if (fileLike.size && fileLike.size > sizeLimit) { + return { uploadBlocked: true, reason: 'size_limit' }; + } + + return { uploadBlocked: false }; + }; + + fileToLocalUploadAttachment = async ( + fileLike: FileReference | FileLike, + ): Promise => { + const file = + isFileReference(fileLike) || isFile(fileLike) + ? fileLike + : createFileFromBlobs({ + blobsArray: [fileLike], + fileName: generateFileName(fileLike.type), + mimeType: fileLike.type, + }); + + const uploadPermissionCheck = await this.getUploadConfigCheck(file); + + const localAttachment: LocalUploadAttachment = { + file_size: file.size, + mime_type: file.type, + localMetadata: { + file, + id: generateUUIDv4(), + uploadPermissionCheck, + uploadState: uploadPermissionCheck.uploadBlocked ? 'blocked' : 'pending', + }, + type: getAttachmentTypeFromMimeType(file.type), + }; + + localAttachment[isImageFile(file) ? 'fallback' : 'title'] = file.name; + + if (isImageFile(file)) { + localAttachment.localMetadata.previewUri = isFileReference(fileLike) + ? fileLike.uri + : URL.createObjectURL?.(fileLike); + + if (isFileReference(fileLike) && fileLike.height && fileLike.width) { + localAttachment.original_height = fileLike.height; + localAttachment.original_width = fileLike.width; + } + } + + if (isFileReference(fileLike) && fileLike.thumb_url) { + localAttachment.thumb_url = fileLike.thumb_url; + } + + return localAttachment; + }; + + private ensureLocalUploadAttachment = async ( + attachment: Partial, + ) => { + if (!attachment.localMetadata?.file || !attachment.localMetadata.id) { + this.client.notifications.addError({ + message: 'File is required for upload attachment', + origin: { emitter: 'AttachmentManager', context: { attachment } }, + }); + return; + } + + // todo: document this + // the following is substitute for: if (noFiles && !isImage) return att + if (!this.fileUploadFilter(attachment)) return; + + const newAttachment = await this.fileToLocalUploadAttachment( + attachment.localMetadata.file, + ); + if (attachment.localMetadata.id) { + newAttachment.localMetadata.id = attachment.localMetadata.id; + } + return newAttachment; + }; + + /** + * Method to perform the default upload behavior without checking for custom upload functions + * to prevent recursive calls + */ + doDefaultUploadRequest = async (fileLike: FileReference | FileLike) => { + if (isFileReference(fileLike)) { + return this.channel[isImageFile(fileLike) ? 'sendImage' : 'sendFile']( + fileLike.uri, + fileLike.name, + fileLike.type, + ); + } + + const file = isFile(fileLike) + ? fileLike + : createFileFromBlobs({ + blobsArray: [fileLike], + fileName: generateFileName(fileLike.type), + mimeType: fileLike.type, + }); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { duration, ...result } = + await this.channel[isImageFile(fileLike) ? 'sendImage' : 'sendFile'](file); + return result; + }; + + /** + * todo: docs how to customize the image and file upload by overriding do + */ + + doUploadRequest = async (fileLike: FileReference | FileLike) => { + const customUploadFn = this.config.doUploadRequest; + if (customUploadFn) { + return await customUploadFn(fileLike); + } + + return this.doDefaultUploadRequest(fileLike); + }; + + uploadAttachment = async (attachment: LocalUploadAttachment) => { + if (!this.isUploadEnabled) return; + + const localAttachment = await this.ensureLocalUploadAttachment(attachment); + + if (typeof localAttachment === 'undefined') return; + + if (localAttachment.localMetadata.uploadState === 'blocked') { + this.upsertAttachments([localAttachment]); + this.client.notifications.addError({ + message: 'Error uploading attachment', + origin: { emitter: 'AttachmentManager', context: { attachment } }, + }); + return localAttachment; + } + + this.upsertAttachments([ + { + ...attachment, + localMetadata: { + ...attachment.localMetadata, + uploadState: 'uploading', + }, + }, + ]); + + let response: MinimumUploadRequestResult; + try { + response = await this.doUploadRequest(localAttachment.localMetadata.file); + } catch (error) { + let finalError: Error = { + message: 'Error uploading attachment', + name: 'Error', + }; + if (typeof (error as Error).message === 'string') { + finalError = error as Error; + } else if (typeof error === 'object') { + finalError = Object.assign(finalError, error); + } + + this.client.notifications.addError({ + message: finalError.message, + origin: { emitter: 'AttachmentManager', context: { attachment } }, + }); + + const failedAttachment: LocalUploadAttachment = { + ...attachment, + localMetadata: { + ...attachment.localMetadata, + uploadState: 'failed', + }, + }; + + this.upsertAttachments([failedAttachment]); + return failedAttachment; + } + + if (!response) { + // Copied this from useImageUpload / useFileUpload. + + // If doUploadRequest returns any falsy value, then don't create the upload preview. + // This is for the case if someone wants to handle failure on app level. + this.removeAttachments([attachment.localMetadata.id]); + return; + } + + const uploadedAttachment: LocalUploadAttachment = { + ...attachment, + localMetadata: { + ...attachment.localMetadata, + uploadState: 'finished', + }, + }; + + if (isLocalImageAttachment(uploadedAttachment)) { + if (uploadedAttachment.localMetadata.previewUri) { + URL.revokeObjectURL(uploadedAttachment.localMetadata.previewUri); + delete uploadedAttachment.localMetadata.previewUri; + } + uploadedAttachment.image_url = response.file; + } else { + (uploadedAttachment as LocalNotImageAttachment).asset_url = response.file; + } + if (response.thumb_url) { + (uploadedAttachment as LocalNotImageAttachment).thumb_url = response.thumb_url; + } + + this.upsertAttachments([uploadedAttachment]); + + return uploadedAttachment; + }; + + uploadFiles = async (files: FileReference[] | FileList | FileLike[]) => { + if (!this.isUploadEnabled) return; + const iterableFiles: FileReference[] | FileLike[] = isFileList(files) + ? Array.from(files) + : files; + const attachments = await Promise.all( + iterableFiles.map(this.fileToLocalUploadAttachment), + ); + + return Promise.all( + attachments + .filter(this.fileUploadFilter) + .slice(0, this.availableUploadSlots) + .map(this.uploadAttachment), + ); + }; +} diff --git a/src/messageComposer/configuration/configuration.ts b/src/messageComposer/configuration/configuration.ts new file mode 100644 index 0000000000..4246658ac4 --- /dev/null +++ b/src/messageComposer/configuration/configuration.ts @@ -0,0 +1,42 @@ +import { find } from 'linkifyjs'; +import { API_MAX_FILES_ALLOWED_PER_MESSAGE } from '../../constants'; +import type { + AttachmentManagerConfig, + LinkPreviewsManagerConfig, + MessageComposerConfig, +} from './types'; +import type { TextComposerConfig } from './types'; + +export const DEFAULT_LINK_PREVIEW_MANAGER_CONFIG: LinkPreviewsManagerConfig = { + debounceURLEnrichmentMs: 1500, + enabled: true, + findURLFn: (text: string): string[] => + find(text, 'url', { defaultProtocol: 'https' }).reduce((acc, link) => { + try { + const url = new URL(link.href); + // Check for valid hostname with at least one dot and valid TLD + if (link.isLink && /^[a-zA-Z0-9-.]+\.[a-zA-Z]{2,}$/.test(url.hostname)) { + acc.push(link.href); + } + } catch { + // Invalid URL, skip it + } + return acc; + }, []), +}; + +export const DEFAULT_ATTACHMENT_MANAGER_CONFIG: AttachmentManagerConfig = { + fileUploadFilter: () => true, + maxNumberOfFilesPerMessage: API_MAX_FILES_ALLOWED_PER_MESSAGE, +}; + +export const DEFAULT_TEXT_COMPOSER_CONFIG: TextComposerConfig = { + publishTypingEvents: true, +}; + +export const DEFAULT_COMPOSER_CONFIG: MessageComposerConfig = { + attachments: DEFAULT_ATTACHMENT_MANAGER_CONFIG, + drafts: { enabled: false }, + linkPreviews: DEFAULT_LINK_PREVIEW_MANAGER_CONFIG, + text: DEFAULT_TEXT_COMPOSER_CONFIG, +}; diff --git a/src/messageComposer/configuration/index.ts b/src/messageComposer/configuration/index.ts new file mode 100644 index 0000000000..28e190c8bb --- /dev/null +++ b/src/messageComposer/configuration/index.ts @@ -0,0 +1,2 @@ +export * from './configuration'; +export * from './types'; diff --git a/src/messageComposer/configuration/types.ts b/src/messageComposer/configuration/types.ts new file mode 100644 index 0000000000..c9850c8b3b --- /dev/null +++ b/src/messageComposer/configuration/types.ts @@ -0,0 +1,59 @@ +import type { LinkPreview } from '../linkPreviewsManager'; +import type { FileUploadFilter } from '../attachmentManager'; +import type { FileLike, FileReference } from '../types'; + +export type MinimumUploadRequestResult = { file: string; thumb_url?: string }; + +export type UploadRequestFn = ( + fileLike: FileReference | FileLike, +) => Promise; + +export type DraftsConfiguration = { + enabled: boolean; +}; +export type TextComposerConfig = { + /** If true, triggers typing events on text input keystroke */ + publishTypingEvents: boolean; + /** Default value for the message input */ + defaultValue?: string; + /** Prevents editing the message to more than this length */ + maxLengthOnEdit?: number; + /** Prevents sending a message longer than this length */ + maxLengthOnSend?: number; +}; +export type AttachmentManagerConfig = { + // todo: document removal of noFiles prop showing how to achieve the same with custom fileUploadFilter function + /** + * Function that allows to prevent uploading files based on the functions output. + * Use this option to simulate deprecated prop noFiles which was actually a filter to upload only image files. + */ + fileUploadFilter: FileUploadFilter; + /** Maximum number of attachments allowed per message */ + maxNumberOfFilesPerMessage: number; + // todo: refactor this. We want a pipeline where it would be possible to customize the preparation, upload, and post-upload steps. + /** Function that allows to customize the upload request. */ + doUploadRequest?: UploadRequestFn; +}; +export type LinkPreviewConfig = { + /** Custom function to react to link preview dismissal */ + onLinkPreviewDismissed?: (linkPreview: LinkPreview) => void; +}; +export type LinkPreviewsManagerConfig = LinkPreviewConfig & { + /** Number of milliseconds to debounce firing the URL enrichment queries when typing. The default value is 1500(ms). */ + debounceURLEnrichmentMs: number; + /** Allows for toggling the URL enrichment and link previews in `MessageInput`. By default, the feature is disabled. */ + enabled: boolean; + /** Custom function to identify URLs in a string and request OG data */ + findURLFn: (text: string) => string[]; +}; + +export type MessageComposerConfig = { + /** If true, enables creating drafts on the server */ + drafts: DraftsConfiguration; + /** Configuration for the attachment manager */ + attachments: AttachmentManagerConfig; + /** Configuration for the link previews manager */ + linkPreviews: LinkPreviewsManagerConfig; + /** Maximum number of characters in a message */ + text: TextComposerConfig; +}; diff --git a/src/messageComposer/fileUtils.ts b/src/messageComposer/fileUtils.ts new file mode 100644 index 0000000000..fba2067f3c --- /dev/null +++ b/src/messageComposer/fileUtils.ts @@ -0,0 +1,102 @@ +import type { Attachment } from '../types'; +import { generateUUIDv4 } from '../utils'; +import { isLocalAttachment } from './attachmentIdentity'; +import type { + BaseLocalAttachmentMetadata, + FileLike, + FileReference, + LocalAttachment, +} from './types'; + +export const isFile = (fileLike: FileReference | File | Blob): fileLike is File => + !!(fileLike as File).lastModified && !('uri' in fileLike); + +export const isFileList = (obj: unknown): obj is FileList => { + if (obj === null || obj === undefined) return false; + if (typeof obj !== 'object') return false; + + return ( + (obj as object) instanceof FileList || + ('item' in obj && 'length' in obj && !Array.isArray(obj)) + ); +}; + +export const isBlobButNotFile = (obj: unknown): obj is Blob => + obj instanceof Blob && !(obj instanceof File); + +export const isFileReference = (obj: FileReference | FileLike): obj is FileReference => + obj !== null && + typeof obj === 'object' && + !isFile(obj) && + !isBlobButNotFile(obj) && + typeof obj.name === 'string' && + typeof obj.uri === 'string' && + typeof obj.size === 'number' && + typeof obj.type === 'string'; + +export const createFileFromBlobs = ({ + blobsArray, + fileName, + mimeType, +}: { + blobsArray: Blob[]; + fileName: string; + mimeType: string; +}) => { + const concatenatedBlob = new Blob(blobsArray, { type: mimeType }); + return new File([concatenatedBlob], fileName, { type: concatenatedBlob.type }); +}; + +export const getExtensionFromMimeType = (mimeType: string) => { + const match = mimeType.match(/\/([^/;]+)/); + return match?.[1]; +}; + +export const readFileAsArrayBuffer = (file: File): Promise => + new Promise((resolve, reject) => { + const fileReader = new FileReader(); + fileReader.onload = () => { + resolve(fileReader.result as ArrayBuffer); + }; + + fileReader.onerror = () => { + reject(fileReader.error); + }; + + fileReader.readAsArrayBuffer(file); + }); + +export const generateFileName = (mimeType: string) => { + const extension = getExtensionFromMimeType(mimeType); + return `file_${new Date().toISOString()}${extension ? '.' + extension : ''}`; +}; + +export const isImageFile = (fileLike: FileReference | FileLike) => { + const mimeType = fileLike.type; + return mimeType.startsWith('image/') && !mimeType.endsWith('.photoshop'); // photoshop files begin with 'image/' +}; + +export const getAttachmentTypeFromMimeType = (mimeType: string) => { + if (mimeType.startsWith('image/') && !mimeType.endsWith('.photoshop')) return 'image'; + if (mimeType.includes('video/')) return 'video'; + if (mimeType.includes('audio/')) return 'audio'; + return 'file'; +}; + +export const ensureIsLocalAttachment = ( + attachment: Attachment | LocalAttachment, +): LocalAttachment | null => { + if (!attachment) return null; + if (isLocalAttachment(attachment)) { + return attachment; + } + // local is considered local only if localMetadata has `id` so this is to doublecheck + const { localMetadata, ...rest } = attachment as LocalAttachment; + return { + localMetadata: { + ...(localMetadata ?? {}), + id: (localMetadata as BaseLocalAttachmentMetadata)?.id || generateUUIDv4(), + }, + ...rest, + }; +}; diff --git a/src/messageComposer/index.ts b/src/messageComposer/index.ts new file mode 100644 index 0000000000..75f8cbac24 --- /dev/null +++ b/src/messageComposer/index.ts @@ -0,0 +1,10 @@ +export * from './attachmentIdentity'; +export * from './attachmentManager'; +export * from './configuration'; +export * from './fileUtils'; +export * from './linkPreviewsManager'; +export * from './messageComposer'; +export * from './middleware'; +export * from './pollComposer'; +export * from './textComposer'; +export * from './types'; diff --git a/src/messageComposer/linkPreviewsManager.ts b/src/messageComposer/linkPreviewsManager.ts new file mode 100644 index 0000000000..478ec991c4 --- /dev/null +++ b/src/messageComposer/linkPreviewsManager.ts @@ -0,0 +1,327 @@ +import { StateStore } from '../store'; +import type { DebouncedFunc } from '../utils'; +import { debounce } from '../utils'; +import { mergeWithDiff } from '../utils/mergeWith'; +import type { DraftMessage, LocalMessage, OGAttachment } from '../types'; +import type { LinkPreviewsManagerConfig } from './configuration/types'; +import type { MessageComposer } from './messageComposer'; + +export type LinkPreview = OGAttachment & { + status: LinkPreviewStatus; +}; + +export interface ILinkPreviewsManager { + /** Function cancels all the scheduled or in-progress URL enrichment queries and resets the state. */ + cancelURLEnrichment: () => void; + /** Function that triggers the search for URLs and their enrichment. */ + findAndEnrichUrls?: DebouncedFunc<(text: string) => void>; +} + +export enum LinkPreviewStatus { + /** Link preview has been dismissed using MessageInputContextValue.dismissLinkPreview **/ + DISMISSED = 'dismissed', + /** Link preview could not be loaded, the enrichment request has failed. **/ + FAILED = 'failed', + /** Link preview has been successfully loaded. **/ + LOADED = 'loaded', + /** The enrichment query is in progress for a given link. **/ + LOADING = 'loading', + /** The preview reference enrichment has not begun. Default status if not set. */ + PENDING = 'pending', +} + +export type LinkURL = string; + +export type LinkPreviewMap = Map; + +export type LinkPreviewsManagerState = { + previews: LinkPreviewMap; +}; + +export type LinkPreviewsManagerOptions = { + composer: MessageComposer; + message?: DraftMessage | LocalMessage; +}; + +const linkPreviewArrayToMap = (linkPreviews: LinkPreview[]) => + new Map(linkPreviews.map((linkPreview) => [linkPreview.og_scrape_url, linkPreview])); + +const initState = ({ + message, +}: { + message?: DraftMessage | LocalMessage; +}): LinkPreviewsManagerState => + message + ? { + previews: + message.attachments?.reduce((acc, attachment) => { + if (!attachment.og_scrape_url) return acc; + acc.set(attachment.og_scrape_url, { + ...(attachment as OGAttachment), + status: LinkPreviewStatus.LOADED, + }); + return acc; + }, new Map()) ?? new Map(), + } + : { + previews: new Map(), + }; + +/* +docs: +You can customize function to identify URLs in a string and request OG data by overriding findURLFn?: (text: string) => string[]; + */ + +export class LinkPreviewsManager implements ILinkPreviewsManager { + findAndEnrichUrls: DebouncedFunc<(text: string) => void>; + readonly state: StateStore; + readonly composer: MessageComposer; + private shouldDiscardEnrichQueries = false; + + constructor({ composer, message }: LinkPreviewsManagerOptions) { + this.composer = composer; + this.state = new StateStore( + initState({ message: this.enabled ? message : undefined }), + ); + + this.findAndEnrichUrls = debounce( + this._findAndEnrichUrls.bind(this), + this.config.debounceURLEnrichmentMs, + ); + } + + get client() { + return this.composer.client; + } + + get channel() { + return this.composer.channel; + } + + get previews() { + return this.state.getLatestValue().previews; + } + + get loadingPreviews() { + return Array.from(this.previews.values()).filter((linkPreview) => + LinkPreviewsManager.previewIsLoading(linkPreview), + ); + } + + get loadedPreviews() { + return Array.from(this.previews.values()).filter((linkPreview) => + LinkPreviewsManager.previewIsLoaded(linkPreview), + ); + } + + get dismissedPreviews() { + return Array.from(this.previews.values()).filter((linkPreview) => + LinkPreviewsManager.previewIsDismissed(linkPreview), + ); + } + + get failedPreviews() { + return Array.from(this.previews.values()).filter((linkPreview) => + LinkPreviewsManager.previewIsFailed(linkPreview), + ); + } + + get pendingPreviews() { + return Array.from(this.previews.values()).filter((linkPreview) => + LinkPreviewsManager.previewIsPending(linkPreview), + ); + } + + get config() { + return this.composer.config.linkPreviews; + } + + get debounceURLEnrichmentMs() { + return this.config.debounceURLEnrichmentMs; + } + + set debounceURLEnrichmentMs( + debounceURLEnrichmentMs: LinkPreviewsManagerConfig['debounceURLEnrichmentMs'], + ) { + this.cancelURLEnrichment(); + + this.findAndEnrichUrls = debounce( + this._findAndEnrichUrls.bind(this), + this.config.debounceURLEnrichmentMs, + ); + + this.composer.updateConfig({ linkPreviews: { debounceURLEnrichmentMs } }); + } + + get enabled() { + /** + * We have to check whether the message will be enriched server side (url_enrichment). + * If not, then it does not make sense to do previews in composer. + */ + return ( + !!this.channel.getConfig()?.url_enrichment && + this.composer.config.linkPreviews.enabled + ); + } + + set enabled(enabled: LinkPreviewsManagerConfig['enabled']) { + this.composer.updateConfig({ linkPreviews: { enabled } }); + } + + get findURLFn() { + return this.config.findURLFn; + } + + set findURLFn(fn: LinkPreviewsManagerConfig['findURLFn']) { + this.composer.updateConfig({ linkPreviews: { findURLFn: fn } }); + } + + get onLinkPreviewDismissed() { + return this.config.onLinkPreviewDismissed; + } + + set onLinkPreviewDismissed(fn: LinkPreviewsManagerConfig['onLinkPreviewDismissed']) { + this.composer.updateConfig({ linkPreviews: { onLinkPreviewDismissed: fn } }); + } + + initState = ({ message }: { message?: DraftMessage | LocalMessage } = {}) => { + this.state.next(initState({ message: this.enabled ? message : undefined })); + }; + + private _findAndEnrichUrls = async (text: string) => { + if (!this.enabled) return; + const urls = this.config.findURLFn(text); + + this.shouldDiscardEnrichQueries = !urls.length; + if (this.shouldDiscardEnrichQueries) { + this.state.next({ previews: new Map() }); + return; + } + const keptPreviews = new Map( + Array.from(this.previews).filter( + ([previewUrl]) => urls.includes(previewUrl) || urls.includes(previewUrl + '/'), + ), + ); + + const newLinkPreviews = urls + .filter((url) => { + const existingPreviews = this.previews; + // account for trailing slashes added by the back-end + const existingPreviewLink = + existingPreviews.get(url) || existingPreviews.get(url + '/'); + return !existingPreviewLink; + }) + .map( + (url) => + ({ + og_scrape_url: url.trim(), + status: LinkPreviewStatus.LOADING, + }) as LinkPreview, + ); + + if (!newLinkPreviews.length) return; + + this.state.partialNext({ + previews: new Map([...keptPreviews, ...linkPreviewArrayToMap(newLinkPreviews)]), + }); + + await Promise.all( + newLinkPreviews.map(async (linkPreview) => { + try { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { duration, ...ogAttachment } = await this.client.enrichURL( + linkPreview.og_scrape_url, + ); + if (this.shouldDiscardEnrichQueries) return; + // due to typing and text changes, the URL may not be anymore in the store + if (this.previews.has(linkPreview.og_scrape_url)) { + this.updatePreview(linkPreview.og_scrape_url, { + status: LinkPreviewStatus.LOADED, + ...ogAttachment, + }); + } + } catch (error) { + if (this.previews.has(linkPreview.og_scrape_url)) { + this.updatePreview(linkPreview.og_scrape_url, { + status: LinkPreviewStatus.FAILED, + }); + } + } + return linkPreview; + }), + ); + }; + + cancelURLEnrichment = () => { + this.findAndEnrichUrls.cancel(); + this.findAndEnrichUrls.flush(); + }; + + /** + * Clears all non-dismissed previews when the text composer is cleared. + * This ensures that dismissed previews are not re-enriched in the future. + */ + clearPreviews = () => { + const currentPreviews = this.previews; + const newPreviews = new Map(); + + // Keep only dismissed previews + currentPreviews.forEach((preview, url) => { + if (LinkPreviewsManager.previewIsDismissed(preview)) { + newPreviews.set(url, preview); + } + }); + + this.state.partialNext({ previews: newPreviews }); + }; + + updatePreview = (url: LinkURL, preview: Partial) => { + if (!url) return; + const existingPreview = this.previews.get(url); + const status = + preview.status ?? this.previews.get(url)?.status ?? LinkPreviewStatus.PENDING; + let finalPreview = preview; + if (existingPreview) { + const merged = mergeWithDiff(existingPreview, preview); + const isSame = !merged.diff || Object.keys(merged.diff).length === 0; + if (isSame) return; + finalPreview = merged.result; + } + this.state.partialNext({ + previews: new Map(this.previews).set(url, { + ...finalPreview, + og_scrape_url: url, + status, + }), + }); + }; + + dismissPreview = (url: LinkURL) => { + const preview = this.previews.get(url); + if (preview) { + this.onLinkPreviewDismissed?.(preview); + this.updatePreview(url, { status: LinkPreviewStatus.DISMISSED }); + } + }; + + static previewIsLoading = (preview: LinkPreview) => + preview.status === LinkPreviewStatus.LOADING; + + static previewIsLoaded = (preview: LinkPreview) => + preview.status === LinkPreviewStatus.LOADED; + + static previewIsDismissed = (preview: LinkPreview) => + preview.status === LinkPreviewStatus.DISMISSED; + + static previewIsFailed = (preview: LinkPreview) => + preview.status === LinkPreviewStatus.FAILED; + + static previewIsPending = (preview: LinkPreview) => + preview.status === LinkPreviewStatus.PENDING; + + static getPreviewData = (preview: LinkPreview) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { status, ...data } = preview; + return data; + }; +} diff --git a/src/messageComposer/messageComposer.ts b/src/messageComposer/messageComposer.ts new file mode 100644 index 0000000000..b19e5150f6 --- /dev/null +++ b/src/messageComposer/messageComposer.ts @@ -0,0 +1,649 @@ +import { AttachmentManager } from './attachmentManager'; +import { CustomDataManager } from './CustomDataManager'; +import { LinkPreviewsManager } from './linkPreviewsManager'; +import { PollComposer } from './pollComposer'; +import { TextComposer } from './textComposer'; +import { DEFAULT_COMPOSER_CONFIG } from './configuration/configuration'; +import type { MessageComposerMiddlewareValue } from './middleware'; +import { + MessageComposerMiddlewareExecutor, + MessageDraftComposerMiddlewareExecutor, +} from './middleware'; +import { StateStore } from '../store'; +import { formatMessage, generateUUIDv4, isLocalMessage } from '../utils'; +import { mergeWith } from '../utils/mergeWith'; +import { Channel } from '../channel'; +import { Thread } from '../thread'; +import type { + DraftMessage, + DraftResponse, + EventTypes, + LocalMessage, + LocalMessageBase, + MessageResponse, + MessageResponseBase, +} from '../types'; +import type { StreamChat } from '../client'; +import type { MessageComposerConfig } from './configuration/types'; +import type { DeepPartial } from '../types.utility'; + +export type LastComposerChange = { draftUpdate: number | null; stateUpdate: number }; + +export type EditingAuditState = { + lastChange: LastComposerChange; +}; + +export type LocalMessageWithLegacyThreadId = LocalMessage & { legacyThreadId?: string }; +export type CompositionContext = Channel | Thread | LocalMessageWithLegacyThreadId; + +export type MessageComposerState = { + id: string; + quotedMessage: LocalMessageBase | null; + pollId: string | null; + draftId: string | null; +}; + +export type MessageComposerOptions = { + client: StreamChat; + // composer can belong to a channel, thread, legacy thread or a local message (edited message) + compositionContext: CompositionContext; + // initial state like draft message or edited message + composition?: DraftResponse | MessageResponse | LocalMessage; + config?: DeepPartial; +}; + +const compositionIsDraftResponse = (composition: unknown): composition is DraftResponse => + !!(composition as { message?: DraftMessage })?.message; + +const initEditingAuditState = ( + composition?: DraftResponse | MessageResponse | LocalMessage, +): EditingAuditState => { + let draftUpdate = null; + let stateUpdate = new Date().getTime(); + if (compositionIsDraftResponse(composition)) { + stateUpdate = draftUpdate = new Date(composition.created_at).getTime(); + } else if (composition && isLocalMessage(composition)) { + stateUpdate = new Date(composition.updated_at).getTime(); + } + return { + lastChange: { + draftUpdate, + stateUpdate, + }, + }; +}; + +const initState = ( + composition?: DraftResponse | MessageResponse | LocalMessage, +): MessageComposerState => { + if (!composition) { + return { + id: MessageComposer.generateId(), + quotedMessage: null, + pollId: null, + draftId: null, + }; + } + + const quotedMessage = composition.quoted_message; + let message; + let draftId = null; + if (compositionIsDraftResponse(composition)) { + message = composition.message; + draftId = composition.message.id; + } else { + message = composition; + } + + return { + draftId, + id: message.id, + quotedMessage: quotedMessage + ? formatMessage(quotedMessage as MessageResponseBase) + : null, + pollId: message.poll_id ?? null, + }; +}; + +const noop = () => undefined; + +export class MessageComposer { + readonly channel: Channel; + readonly state: StateStore; + readonly editingAuditState: StateStore; + readonly configState: StateStore; + readonly compositionContext: CompositionContext; + + editedMessage?: LocalMessage; + attachmentManager: AttachmentManager; + linkPreviewsManager: LinkPreviewsManager; + textComposer: TextComposer; + pollComposer: PollComposer; + customDataManager: CustomDataManager; + // todo: mediaRecorder: MediaRecorderController; + + private unsubscribeFunctions: Set<() => void> = new Set(); + private compositionMiddlewareExecutor: MessageComposerMiddlewareExecutor; + private draftCompositionMiddlewareExecutor: MessageDraftComposerMiddlewareExecutor; + + constructor({ + composition, + config, + compositionContext, + client, + }: MessageComposerOptions) { + this.compositionContext = compositionContext; + + this.configState = new StateStore( + mergeWith(DEFAULT_COMPOSER_CONFIG, config ?? {}), + ); + + // channel is easily inferable from the context + if (compositionContext instanceof Channel) { + this.channel = compositionContext; + } else if (compositionContext instanceof Thread) { + this.channel = compositionContext.channel; + } else if (compositionContext.cid) { + const [type, id] = compositionContext.cid.split(':'); + this.channel = client.channel(type, id); + } else { + throw new Error( + 'MessageComposer requires composition context pointing to channel (channel or context.cid)', + ); + } + + let message: LocalMessage | DraftMessage | undefined = undefined; + if (compositionIsDraftResponse(composition)) { + message = composition.message; + } else if (composition) { + message = formatMessage(composition); + this.editedMessage = message; + } + + this.attachmentManager = new AttachmentManager({ composer: this, message }); + this.linkPreviewsManager = new LinkPreviewsManager({ composer: this, message }); + this.textComposer = new TextComposer({ composer: this, message }); + this.pollComposer = new PollComposer({ composer: this }); + this.customDataManager = new CustomDataManager({ composer: this, message }); + + this.editingAuditState = new StateStore( + this.initEditingAuditState(composition), + ); + this.state = new StateStore(initState(composition)); + + this.compositionMiddlewareExecutor = new MessageComposerMiddlewareExecutor({ + composer: this, + }); + this.draftCompositionMiddlewareExecutor = new MessageDraftComposerMiddlewareExecutor({ + composer: this, + }); + } + + static evaluateContextType(compositionContext: CompositionContext) { + if (compositionContext instanceof Channel) { + return 'channel'; + } + + if (compositionContext instanceof Thread) { + return 'thread'; + } + + if (typeof compositionContext.legacyThreadId === 'string') { + return 'legacy_thread'; + } + + return 'message'; + } + + static constructTag( + compositionContext: CompositionContext, + ): `${ReturnType}_${string}` { + return `${this.evaluateContextType(compositionContext)}_${compositionContext.id}`; + } + + get config(): MessageComposerConfig { + return this.configState.getLatestValue(); + } + + updateConfig(config: DeepPartial) { + this.configState.partialNext(mergeWith(this.config, config)); + } + + get contextType() { + return MessageComposer.evaluateContextType(this.compositionContext); + } + + get tag() { + return MessageComposer.constructTag(this.compositionContext); + } + + get threadId() { + // TODO: ideally we'd use this.contextType but type narrowing does not work for this.compositionContext + // if (this.contextType === 'channel') { + // const context = this.compositionContext; // context is a Channel + // return null + // } + + if (this.compositionContext instanceof Channel) { + return null; + } + + if (this.compositionContext instanceof Thread) { + return this.compositionContext.id; + } + + if (typeof this.compositionContext.legacyThreadId === 'string') { + return this.compositionContext.legacyThreadId; + } + + // check if message is a reply, get parentMessageId + if (typeof this.compositionContext.parent_id === 'string') { + return this.compositionContext.parent_id; + } + + return null; + } + + get client() { + return this.channel.getClient(); + } + + get id() { + return this.state.getLatestValue().id; + } + + get draftId() { + return this.state.getLatestValue().draftId; + } + + get lastChange() { + return this.editingAuditState.getLatestValue().lastChange; + } + + get quotedMessage() { + return this.state.getLatestValue().quotedMessage; + } + + get pollId() { + return this.state.getLatestValue().pollId; + } + + get hasSendableData() { + return !!( + (!this.attachmentManager.uploadsInProgressCount && + (!this.textComposer.textIsEmpty || + this.attachmentManager.successfulUploadsCount > 0)) || + this.pollId + ); + } + + get compositionIsEmpty() { + return ( + !this.quotedMessage && + this.textComposer.textIsEmpty && + !this.attachmentManager.attachments.length && + !this.pollId + ); + } + + get lastChangeOriginIsLocal() { + const initiatedWithoutDraft = this.lastChange.draftUpdate === null; + const composingMessageFromScratch = initiatedWithoutDraft && !this.editedMessage; + + // does not mean that the original editted message is different from the current state + const editedMessageWasUpdated = + !!this.editedMessage?.updated_at && + new Date(this.editedMessage.updated_at).getTime() < this.lastChange.stateUpdate; + + const draftWasChanged = + !!this.lastChange.draftUpdate && + this.lastChange.draftUpdate < this.lastChange.stateUpdate; + + return editedMessageWasUpdated || draftWasChanged || composingMessageFromScratch; + } + + static generateId = generateUUIDv4; + + initState = ({ + composition, + }: { composition?: DraftResponse | MessageResponse | LocalMessage } = {}) => { + this.editingAuditState.partialNext(this.initEditingAuditState(composition)); + + const message: LocalMessage | DraftMessage | undefined = + typeof composition === 'undefined' + ? composition + : compositionIsDraftResponse(composition) + ? composition.message + : formatMessage(composition); + this.attachmentManager.initState({ message }); + this.linkPreviewsManager.initState({ message }); + this.textComposer.initState({ message }); + this.customDataManager.initState({ message }); + this.state.next(initState(composition)); + if ( + composition && + !compositionIsDraftResponse(composition) && + message && + isLocalMessage(message) + ) { + this.editedMessage = message; + } + }; + + initEditingAuditState = ( + composition?: DraftResponse | MessageResponse | LocalMessage, + ) => initEditingAuditState(composition); + // this.config?.drafts.enabled || !compositionIsDraftResponse(composition) + // ? composition + // : undefined, + // ); + + private logStateUpdateTimestamp() { + this.editingAuditState.partialNext({ + lastChange: { ...this.lastChange, stateUpdate: new Date().getTime() }, + }); + } + private logDraftUpdateTimestamp() { + if (!this.config.drafts.enabled) return; + const timestamp = new Date().getTime(); + this.editingAuditState.partialNext({ + lastChange: { draftUpdate: timestamp, stateUpdate: timestamp }, + }); + } + + public registerSubscriptions = () => { + if (this.unsubscribeFunctions.size) { + // Already listening for events and changes + return noop; + } + this.unsubscribeFunctions.add(this.subscribeMessageComposerSetupStateChange()); + this.unsubscribeFunctions.add(this.subscribeMessageUpdated()); + this.unsubscribeFunctions.add(this.subscribeMessageDeleted()); + + this.unsubscribeFunctions.add(this.subscribeTextComposerStateChanged()); + this.unsubscribeFunctions.add(this.subscribeAttachmentManagerStateChanged()); + this.unsubscribeFunctions.add(this.subscribeLinkPreviewsManagerStateChanged()); + this.unsubscribeFunctions.add(this.subscribePollComposerStateChanged()); + this.unsubscribeFunctions.add(this.subscribeCustomDataManagerStateChanged()); + this.unsubscribeFunctions.add(this.subscribeMessageComposerStateChanged()); + this.unsubscribeFunctions.add(this.subscribeMessageComposerConfigStateChanged()); + if (this.config.drafts.enabled) { + this.unsubscribeFunctions.add(this.subscribeDraftUpdated()); + this.unsubscribeFunctions.add(this.subscribeDraftDeleted()); + } + + return this.unregisterSubscriptions; + }; + + // TODO: maybe make these private across the SDK + public unregisterSubscriptions = () => { + this.unsubscribeFunctions.forEach((cleanupFunction) => cleanupFunction()); + this.unsubscribeFunctions.clear(); + }; + + private subscribeMessageUpdated = () => { + // todo: test the impact of 'reaction.new', 'reaction.deleted', 'reaction.updated' + const eventTypes: EventTypes[] = [ + 'message.updated', + 'reaction.new', + 'reaction.deleted', // todo: do we need to subscribe to this especially when the whole state is overriden? + 'reaction.updated', // todo: do we need to subscribe to this especially when the whole state is overriden? + ]; + + const unsubscribeFunctions = eventTypes.map( + (eventType) => + this.client.on(eventType, (event) => { + if (!event.message) return; + if (event.message.id === this.id) { + this.initState({ composition: event.message }); + } + if (this.quotedMessage?.id && event.message.id === this.quotedMessage.id) { + this.setQuotedMessage(formatMessage(event.message)); + } + }).unsubscribe, + ); + + return () => unsubscribeFunctions.forEach((unsubscribe) => unsubscribe()); + }; + + private subscribeMessageComposerSetupStateChange = () => { + let tearDown: (() => void) | null = null; + const unsubscribe = this.client._messageComposerSetupState.subscribeWithSelector( + ({ setupFunction: setup }) => ({ + setup, + }), + ({ setup }) => { + tearDown?.(); + tearDown = setup?.({ composer: this }) ?? null; + }, + ); + + return () => { + tearDown?.(); + unsubscribe(); + }; + }; + + private subscribeMessageDeleted = () => + this.client.on('message.deleted', (event) => { + if (!event.message) return; + if (event.message.id === this.id) { + this.clear(); + } else if (this.quotedMessage && event.message.id === this.quotedMessage.id) { + this.setQuotedMessage(null); + } + }).unsubscribe; + + private subscribeDraftUpdated = () => + this.client.on('draft.updated', (event) => { + const draft = event.draft as DraftResponse; + if ( + !draft || + !!draft.parent_id !== !!this.threadId || + draft.channel_cid !== this.channel.cid + ) + return; + this.initState({ composition: draft }); + }).unsubscribe; + + private subscribeDraftDeleted = () => + this.client.on('draft.deleted', (event) => { + const draft = event.draft as DraftResponse; + if ( + !draft || + !!draft.parent_id !== !!this.threadId || + draft.channel_cid !== this.channel.cid + ) + return; + + this.logDraftUpdateTimestamp(); + if (this.compositionIsEmpty) { + return; + } + this.clear(); + }).unsubscribe; + + private subscribeTextComposerStateChanged = () => + this.textComposer.state.subscribe((nextValue, previousValue) => { + if (previousValue && nextValue.text !== previousValue?.text) { + this.logStateUpdateTimestamp(); + if (this.compositionIsEmpty) { + this.deleteDraft(); + return; + } + } + if (!this.linkPreviewsManager.enabled || nextValue.text === previousValue?.text) + return; + if (!nextValue.text) { + this.linkPreviewsManager.clearPreviews(); + } else { + this.linkPreviewsManager.findAndEnrichUrls(nextValue.text); + } + }); + + private subscribeAttachmentManagerStateChanged = () => + this.attachmentManager.state.subscribe((nextValue, previousValue) => { + if (typeof previousValue === 'undefined') return; + this.logStateUpdateTimestamp(); + if (this.compositionIsEmpty) { + this.deleteDraft(); + return; + } + }); + + private subscribeLinkPreviewsManagerStateChanged = () => + this.linkPreviewsManager.state.subscribe((nextValue, previousValue) => { + if (typeof previousValue === 'undefined') return; + this.logStateUpdateTimestamp(); + if (this.compositionIsEmpty) { + this.deleteDraft(); + return; + } + }); + + private subscribePollComposerStateChanged = () => + this.pollComposer.state.subscribe((nextValue, previousValue) => { + if (typeof previousValue === 'undefined') return; + this.logStateUpdateTimestamp(); + if (this.compositionIsEmpty) { + this.deleteDraft(); + return; + } + }); + + private subscribeCustomDataManagerStateChanged = () => + this.customDataManager.state.subscribe((nextValue, previousValue) => { + if ( + typeof previousValue !== 'undefined' && + !this.customDataManager.isDataEqual(nextValue, previousValue) + ) { + this.logStateUpdateTimestamp(); + } + }); + + private subscribeMessageComposerStateChanged = () => + this.state.subscribe((nextValue, previousValue) => { + if (typeof previousValue === 'undefined') return; + this.logStateUpdateTimestamp(); + if (this.compositionIsEmpty) { + this.deleteDraft(); + return; + } + }); + + private subscribeMessageComposerConfigStateChanged = () => + this.configState.subscribe((nextValue) => { + const { text } = nextValue; + if (this.textComposer.text === '' && text.defaultValue) { + this.textComposer.insertText({ + text: text.defaultValue, + selection: { start: 0, end: 0 }, + }); + } + }); + + setQuotedMessage = (quotedMessage: LocalMessage | null) => { + this.state.partialNext({ quotedMessage }); + }; + + clear = () => { + this.attachmentManager.initState(); + this.linkPreviewsManager.initState(); + this.textComposer.initState(); + this.pollComposer.initState(); + this.customDataManager.initState(); + this.initState(); + }; + + restore = () => { + const { editedMessage } = this; + if (editedMessage) { + this.initState({ composition: editedMessage }); + return; + } + this.clear(); + }; + + compose = async (): Promise => { + const created_at = this.editedMessage?.created_at ?? new Date(); + const text = ''; + const result = await this.compositionMiddlewareExecutor.execute('compose', { + state: { + message: { + id: this.id, + parent_id: this.threadId ?? undefined, + type: 'regular', + }, + localMessage: { + attachments: [], + created_at, // only assigned to localMessage as this is used for optimistic update + deleted_at: null, + error: undefined, + id: this.id, + mentioned_users: [], + parent_id: this.threadId ?? undefined, + pinned_at: null, + reaction_groups: null, + status: this.editedMessage ? this.editedMessage.status : 'sending', + text, + type: 'regular', + updated_at: created_at, + }, + sendOptions: {}, + }, + }); + if (result.status === 'discard') return; + + return result.state; + }; + + composeDraft = async () => { + const { state, status } = await this.draftCompositionMiddlewareExecutor.execute( + 'compose', + { + state: { + draft: { id: this.id, parent_id: this.threadId ?? undefined, text: '' }, + }, + }, + ); + if (status === 'discard') return; + return state; + }; + + createDraft = async () => { + // server-side drafts are not stored on message level but on thread and channel level + // therefore we don't need to create a draft if the message is edited + if (this.editedMessage || !this.config.drafts.enabled) return; + const composition = await this.composeDraft(); + if (!composition) return; + const { draft } = composition; + this.state.partialNext({ draftId: draft.id }); + this.logDraftUpdateTimestamp(); + await this.channel.createDraft(draft); + }; + + deleteDraft = async () => { + if (this.editedMessage || !this.config.drafts.enabled || !this.draftId) return; + this.state.partialNext({ draftId: null }); // todo: should we clear the whole state? + this.logDraftUpdateTimestamp(); + await this.channel.deleteDraft({ parent_id: this.threadId ?? undefined }); + }; + + createPoll = async () => { + const composition = await this.pollComposer.compose(); + if (!composition || !composition.data.id) return; + try { + const { poll } = await this.client.createPoll(composition.data); + this.state.partialNext({ pollId: poll.id }); + } catch (error) { + this.client.notifications.add({ + message: 'Failed to create the poll', + origin: { + emitter: 'MessageComposer', + context: { composer: this }, + }, + }); + throw error; + } + }; +} diff --git a/src/messageComposer/middleware/index.ts b/src/messageComposer/middleware/index.ts new file mode 100644 index 0000000000..e6a6bd186e --- /dev/null +++ b/src/messageComposer/middleware/index.ts @@ -0,0 +1,3 @@ +export * from './messageComposer'; +export * from './pollComposer'; +export * from './textComposer'; diff --git a/src/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.ts b/src/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.ts new file mode 100644 index 0000000000..1ea77785ac --- /dev/null +++ b/src/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.ts @@ -0,0 +1,65 @@ +import { MiddlewareExecutor } from '../../../middleware'; +import { + createDraftTextComposerCompositionMiddleware, + createTextComposerCompositionMiddleware, +} from './textComposer'; +import { + createAttachmentsCompositionMiddleware, + createDraftAttachmentsCompositionMiddleware, +} from './attachments'; +import { + createDraftLinkPreviewsCompositionMiddleware, + createLinkPreviewsCompositionMiddleware, +} from './linkPreviews'; +import { + createDraftMessageComposerStateCompositionMiddleware, + createMessageComposerStateCompositionMiddleware, +} from './messageComposerState'; +import { + createCompositionValidationMiddleware, + createDraftCompositionValidationMiddleware, +} from './compositionValidation'; +import { createCompositionDataCleanupMiddleware } from './cleanData'; +import type { + MessageComposerMiddlewareExecutorOptions, + MessageComposerMiddlewareValueState, + MessageDraftComposerMiddlewareExecutorOptions, + MessageDraftComposerMiddlewareValueState, +} from './types'; +import { + createCustomDataCompositionMiddleware, + createDraftCustomDataCompositionMiddleware, +} from './customData'; + +export class MessageComposerMiddlewareExecutor extends MiddlewareExecutor { + constructor({ composer }: MessageComposerMiddlewareExecutorOptions) { + super(); + // todo: document how to add custom data to a composed message using middleware + // or adding custom composer components (apart from AttachmentsManager, TextComposer etc.) + this.use([ + createTextComposerCompositionMiddleware(composer), + createAttachmentsCompositionMiddleware(composer), + createLinkPreviewsCompositionMiddleware(composer), + createMessageComposerStateCompositionMiddleware(composer), + createCustomDataCompositionMiddleware(composer), + createCompositionValidationMiddleware(composer), + createCompositionDataCleanupMiddleware(composer), + ]); + } +} + +export class MessageDraftComposerMiddlewareExecutor extends MiddlewareExecutor { + constructor({ composer }: MessageDraftComposerMiddlewareExecutorOptions) { + super(); + // todo: document how to add custom data to a composed message using middleware + // or adding custom composer components (apart from AttachmentsManager, TextComposer etc.) + this.use([ + createDraftTextComposerCompositionMiddleware(composer), + createDraftAttachmentsCompositionMiddleware(composer), + createDraftLinkPreviewsCompositionMiddleware(composer), + createDraftMessageComposerStateCompositionMiddleware(composer), + createDraftCustomDataCompositionMiddleware(composer), + createDraftCompositionValidationMiddleware(composer), + ]); + } +} diff --git a/src/messageComposer/middleware/messageComposer/attachments.ts b/src/messageComposer/middleware/messageComposer/attachments.ts new file mode 100644 index 0000000000..9cdcab3092 --- /dev/null +++ b/src/messageComposer/middleware/messageComposer/attachments.ts @@ -0,0 +1,89 @@ +import type { MiddlewareHandlerParams } from '../../../middleware'; +import type { Attachment } from '../../../types'; +import type { MessageComposer } from '../../messageComposer'; +import type { LocalAttachment } from '../../types'; +import type { + MessageComposerMiddlewareValueState, + MessageDraftComposerMiddlewareValueState, +} from './types'; + +const localAttachmentToAttachment = (localAttachment: LocalAttachment) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { localMetadata, ...attachment } = localAttachment; + return attachment as Attachment; +}; + +export const createAttachmentsCompositionMiddleware = (composer: MessageComposer) => ({ + id: 'stream-io/message-composer-middleware/attachments', + compose: ({ + input, + nextHandler, + }: MiddlewareHandlerParams) => { + const { attachmentManager } = composer; + if (!attachmentManager) return nextHandler(input); + + if (attachmentManager.uploadsInProgressCount > 0) { + composer.client.notifications.addWarning({ + message: 'Wait until all attachments have uploaded', + origin: { + emitter: 'MessageComposer', + context: { composer }, + }, + }); + return nextHandler({ ...input, status: 'discard' }); + } + + const attachments = (input.state.message.attachments ?? []).concat( + attachmentManager.successfulUploads.map(localAttachmentToAttachment), + ); + + // prevent introducing attachments array into the payload sent to the server + if (!attachments.length) return nextHandler(input); + + return nextHandler({ + ...input, + state: { + ...input.state, + localMessage: { + ...input.state.localMessage, + attachments, + }, + message: { + ...input.state.message, + attachments, + }, + }, + }); + }, +}); + +export const createDraftAttachmentsCompositionMiddleware = ( + composer: MessageComposer, +) => ({ + id: 'stream-io/message-composer-middleware/draft-attachments', + compose: ({ + input, + nextHandler, + }: MiddlewareHandlerParams) => { + const { attachmentManager } = composer; + if (!attachmentManager) return nextHandler(input); + + const successfulUploads = attachmentManager.successfulUploads; + const attachments = successfulUploads.length + ? (input.state.draft.attachments ?? []).concat( + successfulUploads.map(localAttachmentToAttachment), + ) + : undefined; + + return nextHandler({ + ...input, + state: { + ...input.state, + draft: { + ...input.state.draft, + attachments, + }, + }, + }); + }, +}); diff --git a/src/messageComposer/middleware/messageComposer/cleanData.ts b/src/messageComposer/middleware/messageComposer/cleanData.ts new file mode 100644 index 0000000000..b12fafb302 --- /dev/null +++ b/src/messageComposer/middleware/messageComposer/cleanData.ts @@ -0,0 +1,42 @@ +import type { MiddlewareHandlerParams } from '../../../middleware'; +import { formatMessage, toUpdatedMessagePayload } from '../../../utils'; +import type { MessageComposer } from '../../messageComposer'; +import type { MessageComposerMiddlewareValueState } from './types'; + +export const createCompositionDataCleanupMiddleware = (composer: MessageComposer) => ({ + id: 'stream-io/message-composer-middleware/data-cleanup', + compose: ({ + input, + nextHandler, + }: MiddlewareHandlerParams) => { + const common = { + type: composer.editedMessage?.type ?? 'regular', + }; + + const editedMessagePayloadToBeSent = composer.editedMessage + ? toUpdatedMessagePayload(composer.editedMessage) + : undefined; + + return nextHandler({ + ...input, + state: { + ...input.state, + localMessage: formatMessage({ + ...composer.editedMessage, + ...input.state.localMessage, + ...common, + user: composer.client.user, + }), + message: { + ...editedMessagePayloadToBeSent, + ...input.state.message, + ...common, + }, + sendOptions: + composer.editedMessage && input.state.sendOptions?.skip_enrich_url + ? { skip_enrich_url: input.state.sendOptions?.skip_enrich_url } + : input.state.sendOptions, + }, + }); + }, +}); diff --git a/src/messageComposer/middleware/messageComposer/compositionValidation.ts b/src/messageComposer/middleware/messageComposer/compositionValidation.ts new file mode 100644 index 0000000000..a3e362f6c7 --- /dev/null +++ b/src/messageComposer/middleware/messageComposer/compositionValidation.ts @@ -0,0 +1,55 @@ +import { textIsEmpty } from '../../textComposer'; +import type { + MessageComposerMiddlewareValueState, + MessageDraftComposerMiddlewareValueState, +} from './types'; +import type { MessageComposer } from '../../messageComposer'; +import type { MiddlewareHandlerParams } from '../../../middleware'; + +export const createCompositionValidationMiddleware = (composer: MessageComposer) => ({ + id: 'stream-io/message-composer-middleware/data-validation', + compose: async ({ + input, + nextHandler, + }: MiddlewareHandlerParams) => { + const { maxLengthOnSend } = composer.config.text ?? {}; + const inputText = input.state.message.text ?? ''; + const isEmptyMessage = + textIsEmpty(inputText) && + !input.state.message.attachments?.length && + !input.state.message.poll_id; + + const hasExceededMaxLength = + typeof maxLengthOnSend === 'number' && inputText.length > maxLengthOnSend; + + if (isEmptyMessage || !composer.lastChangeOriginIsLocal || hasExceededMaxLength) { + return await nextHandler({ ...input, status: 'discard' }); + } + + return await nextHandler(input); + }, +}); + +export const createDraftCompositionValidationMiddleware = ( + composer: MessageComposer, +) => ({ + id: 'stream-io/message-composer-middleware/draft-data-validation', + compose: async ({ + input, + nextHandler, + }: MiddlewareHandlerParams) => { + const hasData = + !textIsEmpty(input.state.draft.text ?? '') || + input.state.draft.attachments?.length || + input.state.draft.poll_id || + input.state.draft.quoted_message_id; + + const shouldCreateDraft = composer.lastChangeOriginIsLocal && hasData; + + if (!shouldCreateDraft) { + return await nextHandler({ ...input, status: 'discard' }); + } + + return await nextHandler(input); + }, +}); diff --git a/src/messageComposer/middleware/messageComposer/customData.ts b/src/messageComposer/middleware/messageComposer/customData.ts new file mode 100644 index 0000000000..fbec256d99 --- /dev/null +++ b/src/messageComposer/middleware/messageComposer/customData.ts @@ -0,0 +1,56 @@ +import type { MiddlewareHandlerParams } from '../../../middleware'; +import type { MessageComposer } from '../../messageComposer'; +import type { + MessageComposerMiddlewareValueState, + MessageDraftComposerMiddlewareValueState, +} from './types'; + +export const createCustomDataCompositionMiddleware = (composer: MessageComposer) => ({ + id: 'stream-io/message-composer-middleware/custom-data', + compose: ({ + input, + nextHandler, + }: MiddlewareHandlerParams) => { + const data = composer.customDataManager.data; + if (!data) return nextHandler(input); + + return nextHandler({ + ...input, + state: { + ...input.state, + localMessage: { + ...input.state.localMessage, + ...data, + }, + message: { + ...input.state.message, + ...data, + }, + }, + }); + }, +}); + +export const createDraftCustomDataCompositionMiddleware = ( + composer: MessageComposer, +) => ({ + id: 'stream-io/message-composer-middleware/draft-custom-data', + compose: ({ + input, + nextHandler, + }: MiddlewareHandlerParams) => { + const data = composer.customDataManager.data; + if (!data) return nextHandler(input); + + return nextHandler({ + ...input, + state: { + ...input.state, + draft: { + ...input.state.draft, + ...data, + }, + }, + }); + }, +}); diff --git a/src/messageComposer/middleware/messageComposer/index.ts b/src/messageComposer/middleware/messageComposer/index.ts new file mode 100644 index 0000000000..7ecf84a11c --- /dev/null +++ b/src/messageComposer/middleware/messageComposer/index.ts @@ -0,0 +1,9 @@ +export * from './attachments'; +export * from './cleanData'; +export * from './customData'; +export * from './compositionValidation'; +export * from './linkPreviews'; +export * from './MessageComposerMiddlewareExecutor'; +export * from './messageComposerState'; +export * from './textComposer'; +export * from './types'; diff --git a/src/messageComposer/middleware/messageComposer/linkPreviews.ts b/src/messageComposer/middleware/messageComposer/linkPreviews.ts new file mode 100644 index 0000000000..861b8e8bc0 --- /dev/null +++ b/src/messageComposer/middleware/messageComposer/linkPreviews.ts @@ -0,0 +1,90 @@ +import { LinkPreviewsManager } from '../..'; +import type { MiddlewareHandlerParams } from '../../../middleware'; +import type { Attachment } from '../../../types'; +import type { MessageComposer } from '../../messageComposer'; +import type { + MessageComposerMiddlewareValueState, + MessageDraftComposerMiddlewareValueState, +} from './types'; + +export const createLinkPreviewsCompositionMiddleware = (composer: MessageComposer) => ({ + id: 'stream-io/message-composer-middleware/link-previews', + compose: ({ + input, + nextHandler, + }: MiddlewareHandlerParams) => { + const { linkPreviewsManager } = composer; + if (!linkPreviewsManager) return nextHandler(input); + + linkPreviewsManager.cancelURLEnrichment(); + const someLinkPreviewsLoading = linkPreviewsManager.loadingPreviews.length > 0; + const someLinkPreviewsDismissed = linkPreviewsManager.dismissedPreviews.length > 0; + const linkPreviews = + linkPreviewsManager.loadingPreviews.length > 0 + ? [] + : linkPreviewsManager.loadedPreviews.map((preview) => + LinkPreviewsManager.getPreviewData(preview), + ); + + const attachments: Attachment[] = (input.state.message.attachments ?? []).concat( + linkPreviews, + ); + + // prevent introducing attachments array into the payload sent to the server + if (!attachments.length) return nextHandler(input); + + const sendOptions = { ...input.state.sendOptions }; + const skip_enrich_url = + (!someLinkPreviewsLoading && linkPreviews.length > 0) || someLinkPreviewsDismissed; + if (skip_enrich_url) { + sendOptions.skip_enrich_url = true; + } + + return nextHandler({ + ...input, + state: { + ...input.state, + message: { + ...input.state.message, + attachments, + }, + localMessage: { + ...input.state.localMessage, + attachments, + }, + sendOptions, + }, + }); + }, +}); + +export const createDraftLinkPreviewsCompositionMiddleware = ( + composer: MessageComposer, +) => ({ + id: 'stream-io/message-composer-middleware/draft-link-previews', + compose: ({ + input, + nextHandler, + }: MiddlewareHandlerParams) => { + const { linkPreviewsManager } = composer; + if (!linkPreviewsManager) return nextHandler(input); + + linkPreviewsManager.cancelURLEnrichment(); + const linkPreviews = linkPreviewsManager.loadedPreviews.map((preview) => + LinkPreviewsManager.getPreviewData(preview), + ); + + if (!linkPreviews.length) return nextHandler(input); + + return nextHandler({ + ...input, + state: { + ...input.state, + draft: { + ...input.state.draft, + attachments: (input.state.draft.attachments ?? []).concat(linkPreviews), + }, + }, + }); + }, +}); diff --git a/src/messageComposer/middleware/messageComposer/messageComposerState.ts b/src/messageComposer/middleware/messageComposer/messageComposerState.ts new file mode 100644 index 0000000000..38b8efd23a --- /dev/null +++ b/src/messageComposer/middleware/messageComposer/messageComposerState.ts @@ -0,0 +1,70 @@ +import type { + MessageComposerMiddlewareValueState, + MessageDraftComposerMiddlewareValueState, +} from './types'; +import type { MessageComposer } from '../../messageComposer'; +import type { LocalMessage, LocalMessageBase } from '../../../types'; +import type { MiddlewareHandlerParams } from '../../../middleware'; + +export const createMessageComposerStateCompositionMiddleware = ( + composer: MessageComposer, +) => ({ + id: 'stream-io/message-composer-middleware/own-state', + compose: ({ + input, + nextHandler, + }: MiddlewareHandlerParams) => { + const payload: Pick = {}; + if (composer.quotedMessage) { + payload.quoted_message_id = composer.quotedMessage.id; + } + if (composer.pollId) { + payload.poll_id = composer.pollId; + } + + return nextHandler({ + ...input, + state: { + ...input.state, + localMessage: { + ...input.state.localMessage, + ...payload, + quoted_message: (composer.quotedMessage as LocalMessageBase) ?? undefined, + }, + message: { + ...input.state.message, + ...payload, + }, + }, + }); + }, +}); + +export const createDraftMessageComposerStateCompositionMiddleware = ( + composer: MessageComposer, +) => ({ + id: 'stream-io/message-composer-middleware/draft-own-state', + compose: ({ + input, + nextHandler, + }: MiddlewareHandlerParams) => { + const payload: Pick = {}; + if (composer.quotedMessage) { + payload.quoted_message_id = composer.quotedMessage.id; + } + if (composer.pollId) { + payload.poll_id = composer.pollId; + } + + return nextHandler({ + ...input, + state: { + ...input.state, + draft: { + ...input.state.draft, + ...payload, + }, + }, + }); + }, +}); diff --git a/src/messageComposer/middleware/messageComposer/textComposer.ts b/src/messageComposer/middleware/messageComposer/textComposer.ts new file mode 100644 index 0000000000..8c4be70e53 --- /dev/null +++ b/src/messageComposer/middleware/messageComposer/textComposer.ts @@ -0,0 +1,91 @@ +import type { + MessageComposerMiddlewareValueState, + MessageDraftComposerMiddlewareValueState, +} from './types'; +import type { MessageComposer } from '../../messageComposer'; +import type { MiddlewareHandlerParams } from '../../../middleware'; + +export const createTextComposerCompositionMiddleware = (composer: MessageComposer) => ({ + id: 'stream-io/message-composer-middleware/text-composition', + compose: ({ + input, + nextHandler, + }: MiddlewareHandlerParams) => { + if (!composer.textComposer) return nextHandler(input); + const { mentionedUsers, text } = composer.textComposer; + // Instead of checking if a user is still mentioned every time the text changes, + // just filter out non-mentioned users before submit, which is cheaper + // and allows users to easily undo any accidental deletion + const mentioned_users = Array.from( + new Set( + mentionedUsers.filter( + ({ id, name }) => text.includes(`@${id}`) || text.includes(`@${name}`), + ), + ), + ); + + // prevent introducing text and mentioned_users array into the payload sent to the server + if (!text && mentioned_users.length === 0) return nextHandler(input); + + return nextHandler({ + ...input, + state: { + ...input.state, + localMessage: { + ...input.state.localMessage, + mentioned_users, + text, + }, + message: { + ...input.state.message, + mentioned_users: mentioned_users.map((u) => u.id), + text, + }, + }, + }); + }, +}); + +export const createDraftTextComposerCompositionMiddleware = ( + composer: MessageComposer, +) => ({ + id: 'stream-io/message-composer-middleware/draft-text-composition', + compose: ({ + input, + nextHandler, + }: MiddlewareHandlerParams) => { + if (!composer.textComposer) return nextHandler(input); + const { maxLengthOnSend } = composer.config.text ?? {}; + const { mentionedUsers, text: inputText } = composer.textComposer; + // Instead of checking if a user is still mentioned every time the text changes, + // just filter out non-mentioned users before submit, which is cheaper + // and allows users to easily undo any accidental deletion + const mentioned_users = mentionedUsers.length + ? Array.from( + new Set( + mentionedUsers.filter( + ({ id, name }) => + inputText.includes(`@${id}`) || inputText.includes(`@${name}`), + ), + ), + ) + : undefined; + + const text = + typeof maxLengthOnSend === 'number' && inputText.length > maxLengthOnSend + ? inputText.slice(0, maxLengthOnSend) + : inputText; + + return nextHandler({ + ...input, + state: { + ...input.state, + draft: { + ...input.state.draft, + mentioned_users: mentioned_users?.map((u) => u.id), + text, + }, + }, + }); + }, +}); diff --git a/src/messageComposer/middleware/messageComposer/types.ts b/src/messageComposer/middleware/messageComposer/types.ts new file mode 100644 index 0000000000..cce798bb88 --- /dev/null +++ b/src/messageComposer/middleware/messageComposer/types.ts @@ -0,0 +1,30 @@ +import type { MiddlewareValue } from '../../../middleware'; +import type { + DraftMessagePayload, + LocalMessage, + Message, + SendMessageOptions, + UpdatedMessage, +} from '../../../types'; +import type { MessageComposer } from '../../messageComposer'; + +export type MessageComposerMiddlewareValueState = { + message: Message | UpdatedMessage; + localMessage: LocalMessage; + sendOptions: SendMessageOptions; +}; + +export type MessageComposerMiddlewareValue = + MiddlewareValue; + +export type MessageComposerMiddlewareExecutorOptions = { + composer: MessageComposer; +}; + +export type MessageDraftComposerMiddlewareValueState = { + draft: DraftMessagePayload; +}; + +export type MessageDraftComposerMiddlewareExecutorOptions = { + composer: MessageComposer; +}; diff --git a/src/messageComposer/middleware/pollComposer/PollComposerMiddlewareExecutor.ts b/src/messageComposer/middleware/pollComposer/PollComposerMiddlewareExecutor.ts new file mode 100644 index 0000000000..71a098bc22 --- /dev/null +++ b/src/messageComposer/middleware/pollComposer/PollComposerMiddlewareExecutor.ts @@ -0,0 +1,26 @@ +import { MiddlewareExecutor } from '../../../middleware'; +import { createPollComposerStateMiddleware } from './state'; +import { createPollCompositionValidationMiddleware } from './composition'; +import type { + PollComposerCompositionMiddlewareValueState, + PollComposerStateMiddlewareValueState, +} from './types'; +import type { MessageComposer } from '../../messageComposer'; + +export type PollComposerMiddlewareExecutorOptions = { + composer: MessageComposer; +}; + +export class PollComposerCompositionMiddlewareExecutor extends MiddlewareExecutor { + constructor({ composer }: PollComposerMiddlewareExecutorOptions) { + super(); + this.use([createPollCompositionValidationMiddleware(composer)]); + } +} + +export class PollComposerStateMiddlewareExecutor extends MiddlewareExecutor { + constructor() { + super(); + this.use([createPollComposerStateMiddleware()]); + } +} diff --git a/src/messageComposer/middleware/pollComposer/composition.ts b/src/messageComposer/middleware/pollComposer/composition.ts new file mode 100644 index 0000000000..bcbaf2bfa2 --- /dev/null +++ b/src/messageComposer/middleware/pollComposer/composition.ts @@ -0,0 +1,17 @@ +import type { PollComposerCompositionMiddlewareValueState } from './types'; +import type { MessageComposer } from '../../messageComposer'; +import type { Middleware } from '../../../middleware'; +import type { MiddlewareHandlerParams } from '../../../middleware'; + +export const createPollCompositionValidationMiddleware = ( + composer: MessageComposer, +): Middleware => ({ + id: 'stream-io/poll-composer-composition', + compose: ({ + input, + nextHandler, + }: MiddlewareHandlerParams) => { + if (composer.pollComposer.canCreatePoll) return nextHandler(input); + return nextHandler({ ...input, status: 'discard' }); + }, +}); diff --git a/src/messageComposer/middleware/pollComposer/index.ts b/src/messageComposer/middleware/pollComposer/index.ts new file mode 100644 index 0000000000..6e49a2d115 --- /dev/null +++ b/src/messageComposer/middleware/pollComposer/index.ts @@ -0,0 +1,3 @@ +export * from './PollComposerMiddlewareExecutor'; +export * from './state'; +export * from './types'; diff --git a/src/messageComposer/middleware/pollComposer/state.ts b/src/messageComposer/middleware/pollComposer/state.ts new file mode 100644 index 0000000000..ada61605a4 --- /dev/null +++ b/src/messageComposer/middleware/pollComposer/state.ts @@ -0,0 +1,205 @@ +import type { + PollComposerFieldErrors, + PollComposerState, + PollComposerStateMiddlewareValueState, +} from './types'; +import { generateUUIDv4 } from '../../../utils'; +import type { Middleware } from '../../../middleware'; +import type { MiddlewareHandlerParams } from '../../../middleware'; +export const VALID_MAX_VOTES_VALUE_REGEX = /^([2-9]|10)$/; + +export const MAX_POLL_OPTIONS = 100 as const; + +type ValidationOutput = Partial< + Omit, 'options'> & { + options?: Record; + } +>; + +type Validator = (params: { + data: PollComposerState['data']; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: any; + currentError?: PollComposerFieldErrors[keyof PollComposerFieldErrors]; +}) => ValidationOutput; + +const validators: Partial> = { + enforce_unique_vote: () => ({ max_votes_allowed: undefined }), + max_votes_allowed: ({ data, value }) => { + if (data.enforce_unique_vote && value) + return { max_votes_allowed: 'Enforce unique vote is enabled' }; + if (value?.length > 1 && !value.match(VALID_MAX_VOTES_VALUE_REGEX)) + return { max_votes_allowed: 'Type a number from 2 to 10' }; + return { max_votes_allowed: undefined }; + }, + options: ({ value }) => { + const errors: Record = {}; + const seenOptions = new Set(); + + value.forEach((option: { id: string; text: string }) => { + const trimmedText = option.text.trim(); + if (seenOptions.has(trimmedText)) { + errors[option.id] = 'Option already exists'; + } else { + seenOptions.add(trimmedText); + } + }); + + return Object.keys(errors).length > 0 ? { options: errors } : { options: undefined }; + }, +}; + +const changeValidators: Partial> = { + name: ({ currentError, value }) => + value && currentError + ? { name: undefined } + : { name: typeof currentError === 'string' ? currentError : undefined }, +}; + +const blurValidators: Partial> = { + max_votes_allowed: ({ value }) => { + if (value && !value.match(VALID_MAX_VOTES_VALUE_REGEX)) + return { max_votes_allowed: 'Type a number from 2 to 10' }; + return { max_votes_allowed: undefined }; + }, + name: ({ value }) => { + if (!value) return { name: 'Name is required' }; + return { name: undefined }; + }, +}; + +type ProcessorOutput = Partial; + +type Processor = (params: { + data: PollComposerState['data']; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: any; +}) => ProcessorOutput; + +const processors: Partial> = { + enforce_unique_vote: ({ value }) => ({ + enforce_unique_vote: value, + max_votes_allowed: '', + }), + options: ({ value, data }) => { + // If it's a direct array update (like drag-drop reordering) + if (Array.isArray(value)) { + return { + options: value.map((option) => ({ + id: option.id, + text: option.text.trim(), + })), + }; + } + + // For single option updates + const { index, text } = value; + const prevOptions = data.options || []; + + const shouldAddEmptyOption = + prevOptions.length < MAX_POLL_OPTIONS && + (!prevOptions || (prevOptions.slice(index + 1).length === 0 && !!text)); + + const shouldRemoveOption = + prevOptions && prevOptions.slice(index + 1).length > 0 && !text; + + const optionListHead = prevOptions.slice(0, index); + const optionListTail = shouldAddEmptyOption + ? [{ id: generateUUIDv4(), text: '' }] + : prevOptions.slice(index + 1); + + const newOptions = [ + ...optionListHead, + ...(shouldRemoveOption ? [] : [{ ...prevOptions[index], text }]), + ...optionListTail, + ]; + + return { options: newOptions }; + }, +}; + +export const createPollComposerStateMiddleware = + (): Middleware => ({ + id: 'stream-io/poll-composer-state-processing', + handleFieldChange: ({ + input, + nextHandler, + }: MiddlewareHandlerParams) => { + if (!input.state.targetFields) return nextHandler(input); + const { + state: { previousState, targetFields }, + } = input; + const finalValidators = { ...validators, ...changeValidators }; + + const newData = Object.entries(targetFields).reduce( + (acc, [key, value]) => { + const processor = processors[key as keyof PollComposerState['data']]; + acc = { + ...acc, + ...(processor + ? processor({ data: previousState.data, value }) + : { [key]: value }), + }; + return acc; + }, + {} as PollComposerState['data'], + ); + + const newErrors = Object.keys(targetFields).reduce((acc, key) => { + const validator = finalValidators[key as keyof PollComposerState['data']]; + if (validator) { + const error = validator({ + data: previousState.data, + value: newData[key as keyof PollComposerState['data']], + currentError: previousState.errors[key as keyof PollComposerState['data']], + }); + acc = { ...acc, ...error }; + } + return acc; + }, {} as PollComposerFieldErrors); + + return nextHandler({ + ...input, + state: { + ...input.state, + nextState: { + ...previousState, + data: { ...previousState.data, ...newData }, + errors: { ...previousState.errors, ...newErrors }, + }, + }, + }); + }, + handleFieldBlur: ({ + input, + nextHandler, + }: MiddlewareHandlerParams) => { + const { + state: { previousState, targetFields }, + } = input; + const finalValidators = { ...validators, ...blurValidators }; + const newErrors = Object.entries(targetFields).reduce((acc, [key, value]) => { + const validator = finalValidators[key as keyof PollComposerState['data']]; + if (validator) { + const error = validator({ + data: previousState.data, + value, + currentError: previousState.errors[key as keyof PollComposerState['data']], + }); + acc = { ...acc, ...error }; + } + return acc; + }, {} as PollComposerFieldErrors); + + return nextHandler({ + ...input, + state: { + ...input.state, + nextState: { + ...previousState, + errors: { ...previousState.errors, ...newErrors }, + }, + }, + }); + }, + }); diff --git a/src/messageComposer/middleware/pollComposer/types.ts b/src/messageComposer/middleware/pollComposer/types.ts new file mode 100644 index 0000000000..5979734c9e --- /dev/null +++ b/src/messageComposer/middleware/pollComposer/types.ts @@ -0,0 +1,64 @@ +import type { MiddlewareValue } from '../../../middleware'; +import type { CreatePollData, VotingVisibility } from '../../../types'; + +export type PollComposerOption = { + id: string; + text: string; +}; + +export type PollComposerOptionUpdate = + | PollComposerOption[] + | { + index: number; + text: string; + }; + +export type UpdateFieldsData = Partial> & { + options?: PollComposerOptionUpdate; +}; + +type Id = string; + +export type PollComposerFieldErrors = Partial< + Omit, 'options'> & { + options?: Record; + } +>; + +export type PollComposerState = { + data: { + id: Id; + max_votes_allowed: string; + name: string; + options: PollComposerOption[]; + allow_answers?: boolean; + allow_user_suggested_options?: boolean; + description?: string; + enforce_unique_vote?: boolean; + is_closed?: boolean; + user_id?: string; + voting_visibility?: VotingVisibility; + }; + errors: PollComposerFieldErrors; +}; + +export type PollComposerCompositionMiddlewareValueState = { + data: CreatePollData; + errors: PollComposerFieldErrors; +}; + +export type PollComposerCompositionMiddlewareValue = + MiddlewareValue; + +export type PollComposerStateMiddlewareValueState = { + nextState: PollComposerState; + previousState: PollComposerState; + targetFields: Partial<{ + [K in keyof PollComposerState['data']]: K extends 'options' + ? PollComposerOptionUpdate + : PollComposerState['data'][K]; + }>; +}; + +export type PollComposerStateMiddlewareValue = + MiddlewareValue; diff --git a/src/messageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.ts b/src/messageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.ts new file mode 100644 index 0000000000..cd792cd0eb --- /dev/null +++ b/src/messageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.ts @@ -0,0 +1,56 @@ +import { createCommandsMiddleware } from './commands'; +import { createMentionsMiddleware } from './mentions'; +import { createTextComposerPreValidationMiddleware } from './validation'; +import { MiddlewareExecutor } from '../../../middleware'; +import { withCancellation } from '../../../utils/concurrency'; +import type { + TextComposerMiddleware, + TextComposerMiddlewareExecutorOptions, + TextComposerMiddlewareValue, +} from './types'; +import type { TextComposerState, TextComposerSuggestion } from '../../types'; + +export class TextComposerMiddlewareExecutor extends MiddlewareExecutor { + constructor({ composer }: TextComposerMiddlewareExecutorOptions) { + super(); + this.use([ + createTextComposerPreValidationMiddleware(composer), + createMentionsMiddleware(composer.channel), + createCommandsMiddleware(composer.channel), + ] as TextComposerMiddleware[]); + } + async execute( + eventName: string, + initialInput: TextComposerMiddlewareValue, + selectedSuggestion?: TextComposerSuggestion, + ): Promise { + const result = await this.executeMiddlewareChain(eventName, initialInput, { + selectedSuggestion, + }); + + if (result && result.state.suggestions) { + try { + const searchResult = await withCancellation( + 'textComposer-suggestions-search', + async () => { + await result.state.suggestions?.searchSource.search( + result.state.suggestions?.query, + ); + }, + ); + if (searchResult === 'canceled') return { ...result, status: 'discard' }; + } catch (error) { + // Clear suggestions on search error + return { + ...result, + state: { + ...result.state, + suggestions: undefined, + }, + }; + } + } + + return result; + } +} diff --git a/src/messageComposer/middleware/textComposer/commands.ts b/src/messageComposer/middleware/textComposer/commands.ts new file mode 100644 index 0000000000..05169dfe31 --- /dev/null +++ b/src/messageComposer/middleware/textComposer/commands.ts @@ -0,0 +1,189 @@ +import { getTriggerCharWithToken, insertItemWithTrigger } from './textMiddlewareUtils'; +import { BaseSearchSource } from '../../../search_controller'; +import { mergeWith } from '../../../utils/mergeWith'; +import type { + TextComposerMiddlewareOptions, + TextComposerMiddlewareParams, +} from './types'; +import type { SearchSourceOptions } from '../../../search_controller'; +import type { CommandResponse } from '../../../types'; +import type { Channel } from '../../../channel'; +import type { TextComposerSuggestion } from '../../types'; + +export type CommandSuggestion = TextComposerSuggestion; + +export class CommandSearchSource extends BaseSearchSource { + readonly type = 'commands'; + private channel: Channel; + + constructor(channel: Channel, options?: SearchSourceOptions) { + super(options); + this.channel = channel; + } + + canExecuteQuery = (newSearchString?: string) => { + const hasNewSearchQuery = typeof newSearchString !== 'undefined'; + return this.isActive && !this.isLoading && (this.hasNext || hasNewSearchQuery); + }; + + getStateBeforeFirstQuery(newSearchString: string) { + const newState = super.getStateBeforeFirstQuery(newSearchString); + const { items } = this.state.getLatestValue(); + return { + ...newState, + items, // preserve items to avoid flickering + }; + } + + query(searchQuery: string) { + const channelConfig = this.channel.getConfig(); + const commands = channelConfig?.commands || []; + const selectedCommands: (CommandResponse & { name: string })[] = commands.filter( + (command): command is CommandResponse & { name: string } => + !!( + command.name && + command.name.toLowerCase().indexOf(searchQuery.toLowerCase()) !== -1 + ), + ); + + // sort alphabetically unless you're matching the first char + selectedCommands.sort((a, b) => { + let nameA = a.name?.toLowerCase(); + let nameB = b.name?.toLowerCase(); + if (nameA?.indexOf(searchQuery) === 0) { + nameA = `0${nameA}`; + } + if (nameB?.indexOf(searchQuery) === 0) { + nameB = `0${nameB}`; + } + // Should confirm possible null / undefined when TS is fully implemented + if (nameA != null && nameB != null) { + if (nameA < nameB) { + return -1; + } + if (nameA > nameB) { + return 1; + } + } + + return 0; + }); + return Promise.resolve({ + items: selectedCommands.map((c) => ({ ...c, id: c.name })), + next: null, + }); + } + + protected filterQueryResults( + items: CommandSuggestion[], + ): CommandSuggestion[] | Promise { + return items; + } +} + +/** + * TextComposer middleware for mentions + * Usage: + * + * const textComposer = new TextComposer(options); + * + * textComposer.use(createCommandsMiddleware(channel, { trigger: '//', minChars: 2 } )); + * + * @param channel + * @param {{ minChars: number; trigger: string }} options + * @returns + */ + +const DEFAULT_OPTIONS: TextComposerMiddlewareOptions = { minChars: 1, trigger: '/' }; + +export const createCommandsMiddleware = ( + channel: Channel, + options?: Partial & { + searchSource?: CommandSearchSource; + }, +) => { + const finalOptions = mergeWith(DEFAULT_OPTIONS, options ?? {}); + let searchSource = new CommandSearchSource(channel); + if (options?.searchSource) { + searchSource = options.searchSource; + searchSource.resetState(); + } + searchSource.activate(); + + return { + id: 'stream-io/text-composer/commands-middleware', + onChange: ({ + input, + nextHandler, + }: TextComposerMiddlewareParams) => { + const { state } = input; + if (!state.selection) return nextHandler(input); + + // const firstCharIsNotCommandTrigger = + // state.text.length === 0 || state.text[0] !== finalOptions.trigger; + // if (firstCharIsNotCommandTrigger) return nextHandler(input); + + const triggerWithToken = getTriggerCharWithToken({ + trigger: finalOptions.trigger, + text: state.text.slice(0, state.selection.end), + acceptTrailingSpaces: false, + isCommand: true, + }); + + const newSearchTriggerred = + triggerWithToken && triggerWithToken.length === finalOptions.minChars; + + if (newSearchTriggerred) { + searchSource.resetStateAndActivate(); + } + + const triggerWasRemoved = + !triggerWithToken || triggerWithToken.length < finalOptions.minChars; + + if (triggerWasRemoved) { + const hasStaleSuggestions = + input.state.suggestions?.trigger === finalOptions.trigger; + const newInput = { ...input }; + if (hasStaleSuggestions) { + delete newInput.state.suggestions; + } + return nextHandler(newInput); + } + + return Promise.resolve({ + state: { + ...state, + suggestions: { + query: triggerWithToken.slice(1), + searchSource, + trigger: finalOptions.trigger, + }, + }, + stop: true, // Stop other middleware from processing '/' character + }); + }, + onSuggestionItemSelect: ({ + input, + nextHandler, + selectedSuggestion, + }: TextComposerMiddlewareParams) => { + const { state } = input; + if (!selectedSuggestion || state.suggestions?.trigger !== finalOptions.trigger) + return nextHandler(input); + + searchSource.resetStateAndActivate(); + return Promise.resolve({ + state: { + ...state, + ...insertItemWithTrigger({ + insertText: `/${selectedSuggestion.name} `, + selection: state.selection, + text: state.text, + trigger: finalOptions.trigger, + }), + suggestions: undefined, // Clear suggestions after selection + }, + }); + }, + }; +}; diff --git a/src/messageComposer/middleware/textComposer/index.ts b/src/messageComposer/middleware/textComposer/index.ts new file mode 100644 index 0000000000..878d5c8383 --- /dev/null +++ b/src/messageComposer/middleware/textComposer/index.ts @@ -0,0 +1,7 @@ +export * from './commands'; +export * from './mentions'; +export * from './validation'; +export * from './TextComposerMiddlewareExecutor'; +export * from './textMiddlewareUtils'; +export * from './types'; +export { getTokenizedSuggestionDisplayName } from './textMiddlewareUtils'; diff --git a/src/messageComposer/middleware/textComposer/mentions.ts b/src/messageComposer/middleware/textComposer/mentions.ts new file mode 100644 index 0000000000..4544a45421 --- /dev/null +++ b/src/messageComposer/middleware/textComposer/mentions.ts @@ -0,0 +1,417 @@ +import type { TokenizationPayload } from './textMiddlewareUtils'; +import { + getTokenizedSuggestionDisplayName, + getTriggerCharWithToken, + insertItemWithTrigger, +} from './textMiddlewareUtils'; +import type { SearchSourceOptions } from '../../../search_controller'; +import { BaseSearchSource } from '../../../search_controller'; +import { mergeWith } from '../../../utils/mergeWith'; +import type { + TextComposerMiddlewareOptions, + TextComposerMiddlewareParams, +} from './types'; +import type { StreamChat } from '../../../client'; +import type { + MemberFilters, + MemberSort, + UserFilters, + UserOptions, + UserResponse, + UserSort, +} from '../../../types'; +import type { Channel } from '../../../channel'; +import { MAX_CHANNEL_MEMBER_COUNT_IN_CHANNEL_QUERY } from '../../../constants'; +import type { TextComposerSuggestion } from '../../types'; + +export type UserSuggestion = TextComposerSuggestion; + +// todo: the map is too small - Slavic letters with diacritics are missing for example +export const accentsMap: { [key: string]: string } = { + a: 'á|à|ã|â|À|Á|Ã|Â', + c: 'ç|Ç', + e: 'é|è|ê|É|È|Ê', + i: 'í|ì|î|Í|Ì|Î', + n: 'ñ|Ñ', + o: 'ó|ò|ô|ő|õ|Ó|Ò|Ô|Õ', + u: 'ú|ù|û|ü|Ú|Ù|Û|Ü', +}; + +export const removeDiacritics = (text?: string) => { + if (!text) return ''; + return Object.keys(accentsMap).reduce( + (acc, current) => acc.replace(new RegExp(accentsMap[current], 'g'), current), + text, + ); +}; + +export const calculateLevenshtein = (query: string, name: string) => { + if (query.length === 0) return name.length; + if (name.length === 0) return query.length; + + const matrix = []; + + let i; + for (i = 0; i <= name.length; i++) { + matrix[i] = [i]; + } + + let j; + for (j = 0; j <= query.length; j++) { + matrix[0][j] = j; + } + + for (i = 1; i <= name.length; i++) { + for (j = 1; j <= query.length; j++) { + if (name.charAt(i - 1) === query.charAt(j - 1)) { + matrix[i][j] = matrix[i - 1][j - 1]; + } else { + matrix[i][j] = Math.min( + matrix[i - 1][j - 1] + 1, // substitution + Math.min( + matrix[i][j - 1] + 1, // insertion + matrix[i - 1][j] + 1, + ), + ); // deletion + } + } + } + + return matrix[name.length][query.length]; +}; + +export type MentionsSearchSourceOptions = SearchSourceOptions & { + mentionAllAppUsers?: boolean; + textComposerText?: string; + // todo: document that if you want transliteration, you need to provide the function, e.g. import {default: transliterate} from '@sindresorhus/transliterate'; + // this is now replacing a parameter useMentionsTransliteration + transliterate?: (text: string) => string; +}; + +export class MentionsSearchSource extends BaseSearchSource { + readonly type = 'mentions'; + private client: StreamChat; + private channel: Channel; + userFilters: UserFilters | undefined; + memberFilters: MemberFilters | undefined; + userSort: UserSort | undefined; + memberSort: MemberSort | undefined; // todo: document there are filters and sort options for users and members + searchOptions: Omit | undefined; + config: MentionsSearchSourceOptions; + + constructor(channel: Channel, options?: MentionsSearchSourceOptions) { + const { mentionAllAppUsers, textComposerText, transliterate, ...restOptions } = + options || {}; + super(restOptions); + this.client = channel.getClient(); + this.channel = channel; + this.config = { mentionAllAppUsers, textComposerText }; + // todo: how to propagate useMentionsTransliteration to change dynamically the setting? const { default: transliterate } = await import('@stream-io/transliterate'); + if (transliterate) { + this.transliterate = transliterate; + } + } + + get allMembersLoadedWithInitialChannelQuery() { + const countLoadedMembers = Object.keys(this.channel.state.members || {}).length; + return countLoadedMembers < MAX_CHANNEL_MEMBER_COUNT_IN_CHANNEL_QUERY; + } + + getStateBeforeFirstQuery(newSearchString: string) { + const newState = super.getStateBeforeFirstQuery(newSearchString); + const { items } = this.state.getLatestValue(); + return { + ...newState, + items, // preserve items to avoid flickering + }; + } + + canExecuteQuery = (newSearchString?: string) => { + const hasNewSearchQuery = typeof newSearchString !== 'undefined'; + return this.isActive && !this.isLoading && (hasNewSearchQuery || this.hasNext); + }; + + transliterate = (text: string) => text; + + getMembersAndWatchers = () => { + const memberUsers = Object.values(this.channel.state.members ?? {}).map( + ({ user }) => user, + ); + const watcherUsers = Object.values(this.channel.state.watchers ?? {}); + const users = [...memberUsers, ...watcherUsers]; + + const uniqueUsers = {} as Record; + + users.forEach((user) => { + if (user && !uniqueUsers[user.id]) { + uniqueUsers[user.id] = user; + } + }); + + return Object.values(uniqueUsers); + }; + + searchMembersLocally = (searchQuery: string) => { + const { textComposerText } = this.config; + if (!textComposerText) return []; + + return this.getMembersAndWatchers() + .filter((user) => { + if (user.id === this.client.userID) return false; + if (!searchQuery) return true; + + const updatedId = this.transliterate(removeDiacritics(user.id)).toLowerCase(); + const updatedName = this.transliterate(removeDiacritics(user.name)).toLowerCase(); + const updatedQuery = this.transliterate( + removeDiacritics(searchQuery), + ).toLowerCase(); + + const maxDistance = 3; + const lastDigits = textComposerText.slice(-(maxDistance + 1)).includes('@'); + + if (updatedName) { + const levenshtein = calculateLevenshtein(updatedQuery, updatedName); + if ( + updatedName.includes(updatedQuery) || + (levenshtein <= maxDistance && lastDigits) + ) { + return true; + } + } + + const levenshtein = calculateLevenshtein(updatedQuery, updatedId); + + return ( + updatedId.includes(updatedQuery) || (levenshtein <= maxDistance && lastDigits) + ); + }) + .sort((a, b) => { + if (!this.memberSort) return (a.name || '').localeCompare(b.name || ''); + + // Apply each sort criteria in order + for (const [field, direction] of Object.entries(this.memberSort)) { + const aValue = a[field as keyof UserResponse]; + const bValue = b[field as keyof UserResponse]; + + if (aValue === bValue) continue; + return direction === 1 + ? String(aValue || '').localeCompare(String(bValue || '')) + : String(bValue || '').localeCompare(String(aValue || '')); + } + return 0; + }); + }; + + prepareQueryUsersParams = (searchQuery: string) => ({ + filters: { + $or: [ + { id: { $autocomplete: searchQuery } }, + { name: { $autocomplete: searchQuery } }, + ], + ...this.userFilters, + } as UserFilters, + sort: this.userSort ?? ([{ name: 1 }, { id: 1 }] as UserSort), // todo: document the change - the sort is overridden, not merged + options: { ...this.searchOptions, limit: this.pageSize, offset: this.offset }, + }); + + prepareQueryMembersParams = (searchQuery: string) => { + // QueryMembers failed with error: \"sort must contain at maximum 1 item\" + const maxSortParamsCount = 1; + let sort: MemberSort = [{ user_id: 1 }]; + if (!this.memberSort) { + sort = [{ user_id: 1 }]; + } else if (Array.isArray(this.memberSort)) { + sort = this.memberSort[0]; + } else if (Object.keys(this.memberSort).length === maxSortParamsCount) { + sort = this.memberSort; + } // todo: document the change - the sort is overridden, not merged + return { + // todo: document the change - the filter is overridden, not merged + filters: + this.memberFilters ?? ({ name: { $autocomplete: searchQuery } } as MemberFilters), // autocomplete possible only for name + sort, + options: { ...this.searchOptions, limit: this.pageSize, offset: this.offset }, + }; + }; + + queryUsers = async (searchQuery: string) => { + const { filters, sort, options } = this.prepareQueryUsersParams(searchQuery); + const { users } = await this.client.queryUsers(filters, sort, options); + return users; + }; + + queryMembers = async (searchQuery: string) => { + const { filters, sort, options } = this.prepareQueryMembersParams(searchQuery); + const response = await this.channel.queryMembers(filters, sort, options); + + return response.members.map((member) => member.user) as UserResponse[]; + }; + + async query(searchQuery: string) { + let users: UserResponse[]; + const shouldSearchLocally = + this.allMembersLoadedWithInitialChannelQuery || !searchQuery; + + if (this.config.mentionAllAppUsers) { + users = await this.queryUsers(searchQuery); + } else if (shouldSearchLocally) { + users = this.searchMembersLocally(searchQuery); + } else { + users = await this.queryMembers(searchQuery); + } + + return { + items: users.map( + (user) => + ({ + ...user, + ...getTokenizedSuggestionDisplayName({ + displayName: user.name || user.id, + searchToken: this.searchQuery, + }), + }) as UserSuggestion, + ), + }; + } + + filterMutes = (data: UserSuggestion[]) => { + const { textComposerText } = this.config; + if (!textComposerText) return []; + + const { mutedUsers } = this.client; + if (textComposerText.includes('/unmute') && !mutedUsers.length) { + return []; + } + if (!mutedUsers.length) return data; + + if (textComposerText.includes('/unmute')) { + return data.filter((suggestion) => + mutedUsers.some((mute) => mute.target.id === suggestion.id), + ); + } + return data.filter((suggestion) => + mutedUsers.every((mute) => mute.target.id !== suggestion.id), + ); + }; + + filterQueryResults(items: UserSuggestion[]) { + return this.filterMutes(items); + } +} + +const DEFAULT_OPTIONS: TextComposerMiddlewareOptions = { minChars: 1, trigger: '@' }; + +const userSuggestionToUserResponse = (suggestion: UserSuggestion): UserResponse => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { tokenizedDisplayName, ...userResponse } = suggestion; + return userResponse; +}; + +/** + * TextComposer middleware for mentions + * Usage: + * + * const textComposer = new TextComposer(options); + * + * textComposer.use(createMentionsMiddleware(channel, { + * trigger: '$', + * minChars: 2 + * })); + * + * @param channel + * @param {{ + * minChars: number; + * trigger: string; + * }} options + * @returns + */ + +export const createMentionsMiddleware = ( + channel: Channel, + options?: Partial & { + searchSource?: MentionsSearchSource; + }, +) => { + const finalOptions = mergeWith(DEFAULT_OPTIONS, options ?? {}); + let searchSource; + if (options?.searchSource) { + searchSource = options.searchSource; + searchSource.resetState(); + } else { + searchSource = new MentionsSearchSource(channel); + } + searchSource.activate(); + return { + id: 'stream-io/text-composer/mentions-middleware', + onChange: ({ input, nextHandler }: TextComposerMiddlewareParams) => { + const { state } = input; + if (!state.selection) return nextHandler(input); + + const triggerWithToken = getTriggerCharWithToken({ + trigger: finalOptions.trigger, + text: state.text.slice(0, state.selection.end), + }); + + const newSearchTriggered = + triggerWithToken && triggerWithToken.length === finalOptions.minChars; + + if (newSearchTriggered) { + searchSource.resetStateAndActivate(); + } + + const triggerWasRemoved = + !triggerWithToken || triggerWithToken.length < finalOptions.minChars; + + if (triggerWasRemoved) { + const hasStaleSuggestions = + input.state.suggestions?.trigger === finalOptions.trigger; + const newInput = { ...input }; + if (hasStaleSuggestions) { + delete newInput.state.suggestions; + // todo: how to remove mentioned users on deleting the text + } + return nextHandler(newInput); + } + + searchSource.config.textComposerText = input.state.text; + + return Promise.resolve({ + state: { + ...state, + suggestions: { + query: triggerWithToken.slice(1), + searchSource, + trigger: finalOptions.trigger, + }, + }, + stop: true, // Stop other middleware from processing '@' character + }); + }, + onSuggestionItemSelect: ({ + input, + nextHandler, + selectedSuggestion, + }: TextComposerMiddlewareParams) => { + const { state } = input; + if (!selectedSuggestion || state.suggestions?.trigger !== finalOptions.trigger) + return nextHandler(input); + + searchSource.resetStateAndActivate(); + return Promise.resolve({ + state: { + ...state, + ...insertItemWithTrigger({ + insertText: `@${selectedSuggestion.name || selectedSuggestion.id} `, + selection: state.selection, + text: state.text, + trigger: finalOptions.trigger, + }), + mentionedUsers: state.mentionedUsers.concat( + userSuggestionToUserResponse(selectedSuggestion), + ), + suggestions: undefined, // Clear suggestions after selection + }, + }); + }, + }; +}; diff --git a/src/messageComposer/middleware/textComposer/textMiddlewareUtils.ts b/src/messageComposer/middleware/textComposer/textMiddlewareUtils.ts new file mode 100644 index 0000000000..5bcb717a42 --- /dev/null +++ b/src/messageComposer/middleware/textComposer/textMiddlewareUtils.ts @@ -0,0 +1,136 @@ +import type { TextSelection } from '../../types'; + +export const getTriggerCharWithToken = ({ + trigger, + text, + isCommand = false, + acceptTrailingSpaces = true, +}: { + trigger: string; + text: string; + isCommand?: boolean; + acceptTrailingSpaces?: boolean; +}) => { + const triggerNorWhitespace = `[^\\s${trigger}]*`; + const match = text.match( + new RegExp( + isCommand + ? `^[${trigger}]${triggerNorWhitespace}$` + : acceptTrailingSpaces + ? `(?!^|\\W)?[${trigger}]${triggerNorWhitespace}\\s?${triggerNorWhitespace}$` + : `(?!^|\\W)?[${trigger}]${triggerNorWhitespace}$`, + 'g', + ), + ); + + return match && match[match.length - 1].trim(); +}; + +export const insertItemWithTrigger = ({ + insertText, + selection, + text, + trigger, +}: { + insertText: string; + selection: TextSelection; + text: string; + trigger: string; +}) => { + const beforeCursor = text.slice(0, selection.end); + const afterCursor = text.slice(selection.end); + + // Replace the trigger and query with the user mention + const lastIndex = beforeCursor.lastIndexOf(trigger); + const newText = beforeCursor.slice(0, lastIndex) + insertText + afterCursor; + return { + text: newText, + selection: { + start: lastIndex + insertText.length, + end: lastIndex + insertText.length, + }, + }; +}; + +export const replaceWordWithEntity = async ({ + caretPosition, + getEntityString, + text, +}: { + caretPosition: number; + getEntityString: (word: string) => Promise | string | null; + text: string; +}): Promise => { + const lastWordRegex = /([^\s]+)(\s*)$/; + const match = lastWordRegex.exec(text.slice(0, caretPosition)); + if (!match) return text; + + const lastWord = match[1]; + if (!lastWord) return text; + + const spaces = match[2]; + + const newWord = await getEntityString(lastWord); + if (newWord == null) return text; + + const textBeforeWord = text.slice(0, caretPosition - match[0].length); + const textAfterCaret = text.slice(caretPosition, -1); + return textBeforeWord + newWord + spaces + textAfterCaret; +}; + +/** + * Escapes a string for use in a regular expression + * @param text - The string to escape + * @returns The escaped string + * What does this regex do? + + The regex escapes special regex characters by adding a backslash before them. Here's what it matches: + - dash + [ ] square brackets + { } curly braces + ( ) parentheses + * asterisk + + plus + ? question mark + . period + , comma + / forward slash + \ backslash + ^ caret + $ dollar sign + | pipe + # hash + + The \\$& replacement adds a backslash before any matched character. + This is needed when you want to use these characters literally + in a regex pattern instead of their special regex meanings. + For example: + escapeRegExp("hello.world") // Returns: "hello\.world" + escapeRegExp("[test]") // Returns: "\[test\]" + + This is commonly used when building dynamic regex patterns from user input to prevent special characters from being interpreted as regex syntax. + */ +export function escapeRegExp(text: string) { + return text.replace(/[-[\]{}()*+?.,/\\^$|#]/g, '\\$&'); +} + +export type TokenizationPayload = { + tokenizedDisplayName: { token: string; parts: string[] }; +}; + +export const getTokenizedSuggestionDisplayName = ({ + displayName, + searchToken, +}: { + displayName: string; + searchToken: string; +}): TokenizationPayload => ({ + tokenizedDisplayName: { + token: searchToken, + parts: searchToken + ? displayName + .split(new RegExp(`(${escapeRegExp(searchToken)})`, 'gi')) + .filter(Boolean) + : [displayName], + }, +}); diff --git a/src/messageComposer/middleware/textComposer/types.ts b/src/messageComposer/middleware/textComposer/types.ts new file mode 100644 index 0000000000..47d2902769 --- /dev/null +++ b/src/messageComposer/middleware/textComposer/types.ts @@ -0,0 +1,36 @@ +import type { TextComposerState, TextComposerSuggestion } from '../../types'; +import type { MiddlewareValue } from '../../../middleware'; +import type { MessageComposer } from '../../messageComposer'; + +export type TextComposerMiddlewareOptions = { + minChars: number; + trigger: string; +}; + +export type TextComposerMiddlewareValue = MiddlewareValue; + +export type TextComposerMiddlewareParams = { + input: TextComposerMiddlewareValue; + nextHandler: ( + input: TextComposerMiddlewareValue, + ) => Promise; + selectedSuggestion?: TextComposerSuggestion; +}; + +export type TextComposerMiddlewareHandler = ( + params: TextComposerMiddlewareParams, +) => Promise; + +export type CustomTextComposerMiddleware = { + [key: string]: string | TextComposerMiddlewareHandler; +}; + +export type TextComposerMiddleware = CustomTextComposerMiddleware & { + id: string; + onChange?: string | TextComposerMiddlewareHandler; + onSuggestionItemSelect?: string | TextComposerMiddlewareHandler; +}; + +export type TextComposerMiddlewareExecutorOptions = { + composer: MessageComposer; +}; diff --git a/src/messageComposer/middleware/textComposer/validation.ts b/src/messageComposer/middleware/textComposer/validation.ts new file mode 100644 index 0000000000..86975eb5bd --- /dev/null +++ b/src/messageComposer/middleware/textComposer/validation.ts @@ -0,0 +1,24 @@ +import type { MessageComposer } from '../../messageComposer'; +import type { TextComposerMiddlewareParams } from './types'; +import type { UserSuggestion } from './mentions'; + +export const createTextComposerPreValidationMiddleware = (composer: MessageComposer) => ({ + id: 'stream-io/text-composer/pre-validation-middleware', + onChange: ({ input, nextHandler }: TextComposerMiddlewareParams) => { + const { maxLengthOnEdit } = composer.config.text ?? {}; + if ( + typeof maxLengthOnEdit === 'number' && + input.state.text.length > maxLengthOnEdit + ) { + input.state.text = input.state.text.slice(0, maxLengthOnEdit); + return nextHandler({ + ...input, + state: { + ...input.state, + text: input.state.text, + }, + }); + } + return nextHandler(input); + }, +}); diff --git a/src/messageComposer/pollComposer.ts b/src/messageComposer/pollComposer.ts new file mode 100644 index 0000000000..657b10b9bb --- /dev/null +++ b/src/messageComposer/pollComposer.ts @@ -0,0 +1,152 @@ +import { + PollComposerCompositionMiddlewareExecutor, + PollComposerStateMiddlewareExecutor, + VALID_MAX_VOTES_VALUE_REGEX, +} from './middleware/pollComposer'; +import { StateStore } from '../store'; +import { VotingVisibility } from '../types'; +import { generateUUIDv4 } from '../utils'; +import type { MessageComposer } from './messageComposer'; +import type { PollComposerState, UpdateFieldsData } from './middleware/pollComposer'; + +export type PollComposerOptions = { + composer: MessageComposer; +}; + +export class PollComposer { + readonly state: StateStore; + readonly composer: MessageComposer; + readonly compositionMiddlewareExecutor: PollComposerCompositionMiddlewareExecutor; + readonly stateMiddlewareExecutor: PollComposerStateMiddlewareExecutor; + + constructor({ composer }: PollComposerOptions) { + this.composer = composer; + this.state = new StateStore(this.initialState); + this.compositionMiddlewareExecutor = new PollComposerCompositionMiddlewareExecutor({ + composer, + }); + this.stateMiddlewareExecutor = new PollComposerStateMiddlewareExecutor(); + } + + get initialState(): PollComposerState { + return { + data: { + allow_answers: false, + allow_user_suggested_options: false, + description: '', + enforce_unique_vote: true, + id: generateUUIDv4(), + max_votes_allowed: '', + name: '', + options: [{ id: generateUUIDv4(), text: '' }], + user_id: this.composer.client.user?.id, + voting_visibility: VotingVisibility.public, + }, + errors: {}, + }; + } + + get allow_answers() { + return this.state.getLatestValue().data.allow_answers; + } + get allow_user_suggested_options() { + return this.state.getLatestValue().data.allow_user_suggested_options; + } + get description() { + return this.state.getLatestValue().data.description; + } + get enforce_unique_vote() { + return this.state.getLatestValue().data.enforce_unique_vote; + } + get id() { + return this.state.getLatestValue().data.id; + } + get max_votes_allowed() { + return this.state.getLatestValue().data.max_votes_allowed; + } + get name() { + return this.state.getLatestValue().data.name; + } + get options() { + return this.state.getLatestValue().data.options; + } + get user_id() { + return this.state.getLatestValue().data.user_id; + } + get voting_visibility() { + return this.state.getLatestValue().data.voting_visibility; + } + + get canCreatePoll() { + const { data, errors } = this.state.getLatestValue(); + const hasAtLeastOneOption = data.options.filter((o) => !!o.text).length > 0; + const hasName = !!data.name; + const maxVotesAllowedNumber = parseInt( + data.max_votes_allowed?.match(VALID_MAX_VOTES_VALUE_REGEX)?.[0] || '', + ); + + const validMaxVotesAllowed = + data.max_votes_allowed === '' || + (!!maxVotesAllowedNumber && + (2 <= maxVotesAllowedNumber || maxVotesAllowedNumber <= 10)); + + return ( + hasAtLeastOneOption && + hasName && + validMaxVotesAllowed && + Object.values(errors).filter((errorText) => !!errorText).length === 0 + ); + } + + initState = () => { + this.state.next(this.initialState); + }; + + updateFields = async (data: UpdateFieldsData) => { + const { state, status } = await this.stateMiddlewareExecutor.execute( + 'handleFieldChange', + { + state: { + nextState: { ...this.state.getLatestValue() }, + previousState: { ...this.state.getLatestValue() }, + targetFields: data, + }, + }, + ); + + if (status === 'discard') return; + this.state.next(state.nextState); + }; + + handleFieldBlur = async (field: keyof PollComposerState['data']) => { + const result = await this.stateMiddlewareExecutor.execute('handleFieldBlur', { + state: { + nextState: { ...this.state.getLatestValue() }, + previousState: { ...this.state.getLatestValue() }, + targetFields: { [field]: this.state.getLatestValue().data[field] }, + }, + }); + + if (result.status === 'discard') return; + this.state.next(result.state.nextState); + }; + + compose = async () => { + const { data, errors } = this.state.getLatestValue(); + const result = await this.compositionMiddlewareExecutor.execute('compose', { + state: { + data: { + ...data, + max_votes_allowed: data.max_votes_allowed + ? parseInt(data.max_votes_allowed) + : undefined, + options: data.options?.filter((o) => o.text).map((o) => ({ text: o.text })), + }, + errors, + }, + }); + if (result.status === 'discard') return; + + return result.state; + }; +} diff --git a/src/messageComposer/textComposer.ts b/src/messageComposer/textComposer.ts new file mode 100644 index 0000000000..a3ca56f5c2 --- /dev/null +++ b/src/messageComposer/textComposer.ts @@ -0,0 +1,223 @@ +import { TextComposerMiddlewareExecutor } from './middleware'; +import { StateStore } from '../store'; +import { logChatPromiseExecution } from '../utils'; +import type { TextComposerState, TextComposerSuggestion, TextSelection } from './types'; +import type { MessageComposer } from './messageComposer'; +import type { DraftMessage, LocalMessage, UserResponse } from '../types'; + +export type TextComposerOptions = { + composer: MessageComposer; + message?: DraftMessage | LocalMessage; +}; + +export const textIsEmpty = (text: string) => { + const trimmedText = text.trim(); + return ( + trimmedText === '' || + trimmedText === '>' || + trimmedText === '``````' || + trimmedText === '``' || + trimmedText === '**' || + trimmedText === '____' || + trimmedText === '__' || + trimmedText === '****' + ); +}; + +const initState = ({ + composer, + message, +}: { + composer: MessageComposer; + message?: DraftMessage | LocalMessage; +}): TextComposerState => { + if (!message) { + const text = composer.config.text.defaultValue ?? ''; + return { + mentionedUsers: [], + text, + selection: { start: text.length, end: text.length }, + }; + } + const text = message.text ?? ''; + return { + mentionedUsers: (message.mentioned_users ?? []).map((item: string | UserResponse) => + typeof item === 'string' ? ({ id: item } as UserResponse) : item, + ), + text, + selection: { start: text.length, end: text.length }, + }; +}; + +export class TextComposer { + readonly composer: MessageComposer; + readonly state: StateStore; + middlewareExecutor: TextComposerMiddlewareExecutor; + + constructor({ composer, message }: TextComposerOptions) { + this.composer = composer; + this.state = new StateStore(initState({ composer, message })); + this.middlewareExecutor = new TextComposerMiddlewareExecutor({ composer }); + } + + get channel() { + return this.composer.channel; + } + + get config() { + return this.composer.config.text; + } + + set defaultValue(defaultValue: string) { + this.composer.updateConfig({ text: { defaultValue } }); + } + + set maxLengthOnEdit(maxLengthOnEdit: number) { + this.composer.updateConfig({ text: { maxLengthOnEdit } }); + } + + set maxLengthOnSend(maxLengthOnSend: number) { + this.composer.updateConfig({ text: { maxLengthOnSend } }); + } + + set publishTypingEvents(publishTypingEvents: boolean) { + this.composer.updateConfig({ text: { publishTypingEvents } }); + } + + // --- START STATE API --- + + get mentionedUsers() { + return this.state.getLatestValue().mentionedUsers; + } + + get selection() { + return this.state.getLatestValue().selection; + } + + get suggestions() { + return this.state.getLatestValue().suggestions; + } + + get text() { + return this.state.getLatestValue().text; + } + + get textIsEmpty() { + return textIsEmpty(this.text); + } + + initState = ({ message }: { message?: DraftMessage | LocalMessage } = {}) => { + this.state.next(initState({ composer: this.composer, message })); + }; + + setMentionedUsers(users: UserResponse[]) { + this.state.partialNext({ mentionedUsers: users }); + } + + upsertMentionedUser = (user: UserResponse) => { + const mentionedUsers = [...this.mentionedUsers]; + const existingUserIndex = mentionedUsers.findIndex((u) => u.id === user.id); + if (existingUserIndex >= 0) { + mentionedUsers.splice(existingUserIndex, 1, user); + this.state.partialNext({ mentionedUsers }); + } else { + mentionedUsers.push(user); + this.state.partialNext({ mentionedUsers }); + } + }; + + getMentionedUser = (userId: string) => + this.state.getLatestValue().mentionedUsers.find((u: UserResponse) => u.id === userId); + + removeMentionedUser = (userId: string) => { + const existingUserIndex = this.mentionedUsers.findIndex((u) => u.id === userId); + if (existingUserIndex === -1) return; + const mentionedUsers = [...this.mentionedUsers]; + mentionedUsers.splice(existingUserIndex, 1); + this.state.partialNext({ mentionedUsers }); + }; + + setText = (text: string) => { + this.state.partialNext({ text }); + }; + + insertText = ({ text, selection }: { text: string; selection?: TextSelection }) => { + const finalSelection: TextSelection = selection ?? { + start: this.text.length, + end: this.text.length, + }; + const { maxLengthOnEdit } = this.composer.config.text ?? {}; + const currentText = this.text; + const textBeforeTrim = [ + currentText.slice(0, finalSelection.start), + text, + currentText.slice(finalSelection.end), + ].join(''); + const finalText = textBeforeTrim.slice( + 0, + typeof maxLengthOnEdit === 'number' ? maxLengthOnEdit : textBeforeTrim.length, + ); + const expectedCursorPosition = + currentText.slice(0, finalSelection.start).length + text.length; + const cursorPosition = + expectedCursorPosition >= finalText.length + ? finalText.length + : currentText.slice(0, expectedCursorPosition).length; + + this.state.partialNext({ + text: finalText, + selection: { + start: cursorPosition, + end: cursorPosition, + }, + }); + }; + + closeSuggestions = () => { + const { suggestions } = this.state.getLatestValue(); + if (!suggestions) return; + this.state.partialNext({ suggestions: undefined }); + }; + // --- END STATE API --- + + // --- START TEXT PROCESSING ---- + + handleChange = async ({ + text, + selection, + }: { + selection: TextSelection; + text: string; + }) => { + const output = await this.middlewareExecutor.execute('onChange', { + state: { + ...this.state.getLatestValue(), + text, + selection, + }, + }); + if (output.status === 'discard') return; + this.state.next(output.state); + + if (this.config.publishTypingEvents && text) { + logChatPromiseExecution( + this.channel.keystroke(this.composer.threadId ?? undefined), + 'start typing event', + ); + } + }; + + // todo: document how to register own middleware handler to simulate onSelectUser prop + handleSelect = async (target: TextComposerSuggestion) => { + const output = await this.middlewareExecutor.execute( + 'onSuggestionItemSelect', + { + state: this.state.getLatestValue(), + }, + target, + ); + if (output?.status === 'discard') return; + this.state.next(output.state); + }; + // --- END TEXT PROCESSING ---- +} diff --git a/src/messageComposer/types.ts b/src/messageComposer/types.ts new file mode 100644 index 0000000000..e1dc1b9b06 --- /dev/null +++ b/src/messageComposer/types.ts @@ -0,0 +1,164 @@ +import type { Attachment, FileUploadConfig, UserResponse } from '../types'; +import type { SearchSource } from '../search_controller'; + +export type LocalAttachment = AnyLocalAttachment | LocalUploadAttachment; + +export type LocalUploadAttachment = + | LocalFileAttachment + | LocalImageAttachment + | LocalAudioAttachment + | LocalVideoAttachment + | LocalVoiceRecordingAttachment; + +export type LocalVoiceRecordingAttachment> = + LocalAttachmentCast< + VoiceRecordingAttachment, + LocalAttachmentUploadMetadata & CustomLocalMetadata + >; + +export type LocalAudioAttachment> = + LocalAttachmentCast< + AudioAttachment, + LocalAttachmentUploadMetadata & CustomLocalMetadata + >; + +export type LocalVideoAttachment> = + LocalAttachmentCast< + VideoAttachment, + LocalAttachmentUploadMetadata & CustomLocalMetadata + >; + +export type LocalImageAttachment> = + LocalAttachmentCast< + ImageAttachment, + LocalImageAttachmentUploadMetadata & CustomLocalMetadata + >; + +export type LocalFileAttachment> = + LocalAttachmentCast< + FileAttachment, + LocalAttachmentUploadMetadata & CustomLocalMetadata + >; + +export type AnyLocalAttachment> = + LocalAttachmentCast>; + +export type LocalAttachmentCast> = A & { + localMetadata: L & BaseLocalAttachmentMetadata; +}; + +export type LocalAttachmentMetadata> = + CustomLocalMetadata & BaseLocalAttachmentMetadata & LocalImageAttachmentUploadMetadata; + +export type UploadedAttachment = + | AudioAttachment + | FileAttachment + | ImageAttachment + | VideoAttachment + | VoiceRecordingAttachment; + +export type VoiceRecordingAttachment = Attachment & { + asset_url: string; + type: 'voiceRecording'; + duration?: number; + file_size?: number; + mime_type?: string; + title?: string; + waveform_data?: Array; +}; + +export type FileAttachment = Attachment & { + type: 'file'; + asset_url?: string; + file_size?: number; + mime_type?: string; + title?: string; +}; + +export type AudioAttachment = Attachment & { + type: 'audio'; + asset_url?: string; + file_size?: number; + mime_type?: string; + title?: string; +}; + +export type VideoAttachment = Attachment & { + type: 'video'; + asset_url?: string; + file_size?: number; + mime_type?: string; + thumb_url?: string; + title?: string; +}; + +export type ImageAttachment = Attachment & { + type: 'image'; + fallback?: string; + image_url?: string; + original_height?: number; + original_width?: number; +}; + +export type BaseLocalAttachmentMetadata = { + id: string; +}; + +export type LocalAttachmentUploadMetadata = { + file: File | FileReference; + uploadState: AttachmentLoadingState; + uploadPermissionCheck?: UploadPermissionCheckResult; // added new +}; + +export type LocalImageAttachmentUploadMetadata = LocalAttachmentUploadMetadata & { + previewUri?: string; +}; + +export type AttachmentLoadingState = + | 'uploading' + | 'finished' + | 'failed' + | 'blocked' + | 'pending'; + +export type UploadPermissionCheckResult = { + uploadBlocked: boolean; + reason?: keyof FileUploadConfig; +}; + +export type FileLike = File | Blob; + +// todo: make sure that RN SDK passes MIME type in the type field +export type FileReference = Pick & { + uri: string; + // For images + height?: number; + width?: number; + + // For voice recordings + duration?: number; + waveform_data?: number[]; + + // This is specially needed for video in camera roll + thumb_url?: string; +}; + +type Id = string; +export type MentionedUserMap = Map; +export type TextSelection = { end: number; start: number }; +export type TextComposerSuggestion = T & { + id: string; +}; + +export type Suggestions = { + query: string; + searchSource: SearchSource; // we do not want to limit the use of SearchSources + trigger: string; +}; + +export type TextComposerState = { + mentionedUsers: UserResponse[]; + selection: TextSelection; + text: string; + suggestions?: Suggestions; +}; diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000000..f65b3be38c --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,150 @@ +import { withCancellation } from './utils/concurrency'; +import { generateUUIDv4 } from './utils'; + +export type InsertPosition = + | { + after: string; + before?: never; + } + | { + after?: never; + before: string; + }; + +export type MiddlewareStatus = 'complete' | 'discard'; + +export type MiddlewareValue = { + state: TState; + status?: MiddlewareStatus; +}; + +export type MiddlewareHandlerParams = { + input: MiddlewareValue; + nextHandler: (input: MiddlewareValue) => Promise>; +}; +export type MiddlewareHandler = ( + params: MiddlewareHandlerParams, +) => Promise>; + +export type Middleware = { + id: string; + [key: string]: string | MiddlewareHandler; +}; + +export class MiddlewareExecutor { + private id: string; + private middleware: Middleware[] = []; + + constructor() { + this.id = generateUUIDv4(); + } + + use(middleware: Middleware | Middleware[]) { + this.middleware = this.middleware.concat(middleware); + return this; + } + + // todo: document how to re-arrange the order of middleware using replace + replace(middleware: Middleware[]) { + const newMiddleware = [...this.middleware]; + middleware.forEach((upserted) => { + const existingIndex = this.middleware.findIndex( + (existing) => existing.id === upserted.id, + ); + if (existingIndex >= 0) { + newMiddleware.splice(existingIndex, 1, upserted); + } else { + newMiddleware.push(upserted); + } + }); + this.middleware = newMiddleware; + return this; + } + + insert({ + middleware, + position, + unique, + }: { + middleware: Middleware[]; + position: InsertPosition; + unique?: boolean; + }) { + if (unique) { + middleware.forEach((md) => { + const existingMiddlewareIndex = this.middleware.findIndex((m) => m.id === md.id); + if (existingMiddlewareIndex >= 0) { + this.middleware.splice(existingMiddlewareIndex, 1); + } + }); + } + const targetId = position.after || position.before; + const targetIndex = this.middleware.findIndex((m) => m.id === targetId); + const insertionIndex = position.after ? targetIndex + 1 : targetIndex; + this.middleware.splice(insertionIndex, 0, ...middleware); + return this; + } + + setOrder(order: string[]) { + this.middleware = order + .map((id) => this.middleware.find((middleware) => middleware.id === id)) + .filter(Boolean) as Middleware[]; + } + + protected async executeMiddlewareChain( + eventName: string, + initialInput: MiddlewareValue, + extraParams: Record = {}, + ): Promise> { + let index = -1; + + const execute = async ( + i: number, + input: MiddlewareValue, + ): Promise> => { + if (i <= index) { + throw new Error('next() called multiple times'); + } + + index = i; + + const returnFromChain = + i === this.middleware.length || + (input.status && ['complete', 'discard'].includes(input.status)); + if (returnFromChain) return input; + + const middleware = this.middleware[i]; + const handler = middleware[eventName]; + + if (!handler || typeof handler === 'string') { + return execute(i + 1, input); + } + + return await handler({ + input, + nextHandler: (nextInput: MiddlewareValue) => execute(i + 1, nextInput), + ...extraParams, + }); + }; + + const result = await withCancellation( + `middleware-execution-${this.id}-${eventName}`, + async (abortSignal) => { + const result = await execute(0, initialInput); + if (abortSignal.aborted) { + return 'canceled'; + } + return result; + }, + ); + + return result === 'canceled' ? { ...initialInput, status: 'discard' } : result; + } + + async execute( + eventName: string, + initialInput: MiddlewareValue, + ): Promise> { + return await this.executeMiddlewareChain(eventName, initialInput); + } +} diff --git a/src/notifications/NotificationManager.ts b/src/notifications/NotificationManager.ts new file mode 100644 index 0000000000..8cedeac9a6 --- /dev/null +++ b/src/notifications/NotificationManager.ts @@ -0,0 +1,115 @@ +import { StateStore } from '../store'; +import { generateUUIDv4 } from '../utils'; +import type { + AddNotificationPayload, + Notification, + NotificationManagerConfig, + NotificationState, +} from './types'; + +const DURATIONS: NotificationManagerConfig['durations'] = { + error: 10000, + warning: 5000, + info: 3000, + success: 3000, +} as const; + +export class NotificationManager { + store: StateStore; + private timeouts: Map = new Map(); + config: NotificationManagerConfig; + + constructor(config: Partial = {}) { + this.store = new StateStore({ notifications: [] }); + this.config = { + ...config, + durations: config.durations || DURATIONS, + }; + } + + get notifications() { + return this.store.getLatestValue().notifications; + } + + get warning() { + return this.notifications.filter((n) => n.severity === 'warning'); + } + + get error() { + return this.notifications.filter((n) => n.severity === 'error'); + } + + get info() { + return this.notifications.filter((n) => n.severity === 'info'); + } + + get success() { + return this.notifications.filter((n) => n.severity === 'success'); + } + + add({ message, origin, options = {} }: AddNotificationPayload): string { + const id = generateUUIDv4(); + const now = Date.now(); + + const notification: Notification = { + id, + message, + origin, + severity: options.severity || 'info', + createdAt: now, + expiresAt: options.duration ? now + options.duration : undefined, + autoClose: options.autoClose ?? true, + actions: options.actions, + metadata: options.metadata, + }; + + this.store.partialNext({ + notifications: [...this.store.getLatestValue().notifications, notification], + }); + + if (notification.autoClose && notification.expiresAt) { + const timeout = setTimeout(() => { + this.remove(id); + }, options.duration || this.config.durations[notification.severity]); + + this.timeouts.set(id, timeout); + } + + return id; + } + + addError({ message, origin, options }: AddNotificationPayload) { + return this.add({ message, origin, options: { ...options, severity: 'error' } }); + } + + addWarning({ message, origin, options }: AddNotificationPayload) { + return this.add({ message, origin, options: { ...options, severity: 'warning' } }); + } + + addInfo({ message, origin, options }: AddNotificationPayload) { + return this.add({ message, origin, options: { ...options, severity: 'info' } }); + } + + addSuccess({ message, origin, options }: AddNotificationPayload) { + return this.add({ message, origin, options: { ...options, severity: 'success' } }); + } + + remove(id: string): void { + const timeout = this.timeouts.get(id); + if (timeout) { + clearTimeout(timeout); + this.timeouts.delete(id); + } + + this.store.partialNext({ + notifications: this.store.getLatestValue().notifications.filter((n) => n.id !== id), + }); + } + + clear(): void { + this.timeouts.forEach((timeout) => clearTimeout(timeout)); + this.timeouts.clear(); + + this.store.partialNext({ notifications: [] }); + } +} diff --git a/src/notifications/index.ts b/src/notifications/index.ts new file mode 100644 index 0000000000..632abf04fd --- /dev/null +++ b/src/notifications/index.ts @@ -0,0 +1,2 @@ +export * from './NotificationManager'; +export * from './types'; diff --git a/src/notifications/types.ts b/src/notifications/types.ts new file mode 100644 index 0000000000..e292591dd6 --- /dev/null +++ b/src/notifications/types.ts @@ -0,0 +1,74 @@ +/** Represents the severity level of a notification */ +export type NotificationSeverity = + | 'error' + | 'warning' + | 'info' + | 'success' + | (string & {}); + +/** Represents an action button for a notification */ +export type NotificationAction = { + /** Text label for the action button */ + label: string; + /** Handler function called when action button is clicked */ + handler: () => void; + /** Optional metadata for styling or other custom properties */ + metadata?: Record; +}; + +export type NotificationOrigin = { emitter: string; context?: Record }; + +/** Represents a single notification message */ +export type Notification = { + /** Unique identifier for the notification */ + id: string; + /** The notification message text */ + message: string; + /** The severity level of the notification */ + severity: NotificationSeverity; + /** Timestamp when notification was created */ + createdAt: number; + /** + * Identifier of the notification emitter. + * The identifier then can be recognized by notification consumers to act upon specific origin values. + */ + origin: NotificationOrigin; + /** Optional timestamp when notification should expire */ + expiresAt?: number; + /** Whether notification should automatically close after duration. Defaults to true */ + autoClose?: boolean; + /** Array of action buttons for the notification */ + actions?: NotificationAction[]; + /** Optional metadata to attach to the notification */ + metadata?: Record; +}; + +/** Configuration options when creating a notification */ +export type NotificationOptions = { + /** The severity level. Defaults to 'info' */ + severity?: NotificationSeverity; + /** How long notification should display in milliseconds */ + duration?: number; + /** Whether notification should auto-close after duration. Defaults to true */ + autoClose?: boolean; + /** Array of action buttons for the notification */ + actions?: NotificationAction[]; + /** Optional metadata to attach to the notification */ + metadata?: Record; +}; + +/** State shape for the notification store */ +export type NotificationState = { + /** Array of current notification objects */ + notifications: Notification[]; +}; + +export type NotificationManagerConfig = { + durations: Record; +}; + +export type AddNotificationPayload = { + message: string; + origin: NotificationOrigin; + options?: NotificationOptions; +}; diff --git a/src/poll_manager.ts b/src/poll_manager.ts index c5b5447e20..521d16b8e6 100644 --- a/src/poll_manager.ts +++ b/src/poll_manager.ts @@ -1,6 +1,7 @@ import type { StreamChat } from './client'; import type { CreatePollData, + LocalMessage, MessageResponse, PollResponse, PollSort, @@ -8,7 +9,6 @@ import type { QueryPollsOptions, } from './types'; import { Poll } from './poll'; -import type { FormatMessageResponse } from './types'; import { formatMessage } from './utils'; export class PollManager { @@ -92,7 +92,7 @@ export class PollManager { }; public hydratePollCache = ( - messages: FormatMessageResponse[] | MessageResponse[], + messages: LocalMessage[] | MessageResponse[], overwriteState?: boolean, ) => { for (const message of messages) { diff --git a/src/search_controller.ts b/src/search_controller.ts index f46cf212b3..3a233930e3 100644 --- a/src/search_controller.ts +++ b/src/search_controller.ts @@ -18,7 +18,7 @@ import type { } from './types'; export type SearchSourceType = 'channels' | 'users' | 'messages' | (string & {}); -export type QueryReturnValue = { items: T[]; next?: string }; +export type QueryReturnValue = { items: T[]; next?: string | null }; export type DebounceOptions = { debounceMs: number; }; @@ -27,6 +27,8 @@ type DebouncedExecQueryFunction = DebouncedFunc<(searchString?: string) => Promi // eslint-disable-next-line @typescript-eslint/no-explicit-any export interface SearchSource { activate(): void; + cancelScheduledQuery(): void; + canExecuteQuery(newSearchString?: string): boolean; deactivate(): void; readonly hasNext: boolean; readonly hasResults: boolean; @@ -35,11 +37,10 @@ export interface SearchSource { readonly isLoading: boolean; readonly items: T[] | undefined; readonly lastQueryError: Error | undefined; - readonly next: string | undefined; + readonly next: string | undefined | null; readonly offset: number | undefined; resetState(): void; - search(text?: string): void; - searchDebounced: DebouncedExecQueryFunction; + search(text?: string): Promise | undefined; readonly searchQuery: string; setDebounceOptions(options: DebounceOptions): void; readonly state: StateStore>; @@ -54,7 +55,7 @@ export type SearchSourceState = { items: T[] | undefined; searchQuery: string; lastQueryError?: Error; - next?: string; + next?: string | null; offset?: number; }; @@ -73,7 +74,7 @@ export abstract class BaseSearchSource implements SearchSource { state: StateStore>; protected pageSize: number; abstract readonly type: SearchSourceType; - searchDebounced!: DebouncedExecQueryFunction; + protected searchDebounced!: DebouncedExecQueryFunction; protected constructor(options?: SearchSourceOptions) { const { debounceMs, pageSize } = { ...DEFAULT_SEARCH_SOURCE_OPTIONS, ...options }; @@ -149,24 +150,49 @@ export abstract class BaseSearchSource implements SearchSource { this.state.partialNext({ isActive: false }); }; + canExecuteQuery = (newSearchString?: string) => { + const hasNewSearchQuery = typeof newSearchString !== 'undefined'; + const searchString = newSearchString ?? this.searchQuery; + return !!( + this.isActive && + !this.isLoading && + (this.hasNext || hasNewSearchQuery) && + searchString + ); + }; + + protected getStateBeforeFirstQuery(newSearchString: string): SearchSourceState { + return { + ...this.initialState, + isActive: this.isActive, + isLoading: true, + searchQuery: newSearchString, + }; + } + + protected getStateAfterQuery( + stateUpdate: Partial>, + isFirstPage: boolean, + ): SearchSourceState { + const current = this.state.getLatestValue(); + return { + ...current, + lastQueryError: undefined, // reset lastQueryError that can be overridden by the stateUpdate + ...stateUpdate, + isLoading: false, + items: isFirstPage + ? stateUpdate.items + : [...(this.items ?? []), ...(stateUpdate.items || [])], + }; + } + async executeQuery(newSearchString?: string) { + if (!this.canExecuteQuery(newSearchString)) return; const hasNewSearchQuery = typeof newSearchString !== 'undefined'; const searchString = newSearchString ?? this.searchQuery; - if ( - !this.isActive || - this.isLoading || - (!this.hasNext && !hasNewSearchQuery) || - !searchString - ) - return; if (hasNewSearchQuery) { - this.state.next({ - ...this.initialState, - isActive: this.isActive, - isLoading: true, - searchQuery: newSearchString ?? '', - }); + this.state.next(this.getStateBeforeFirstQuery(newSearchString ?? '')); } else { this.state.partialNext({ isLoading: true }); } @@ -177,7 +203,7 @@ export abstract class BaseSearchSource implements SearchSource { if (!results) return; const { items, next } = results; - if (next) { + if (next || next === null) { stateUpdate.next = next; stateUpdate.hasNext = !!next; } else { @@ -189,23 +215,24 @@ export abstract class BaseSearchSource implements SearchSource { } catch (e) { stateUpdate.lastQueryError = e as Error; } finally { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - this.state.next(({ lastQueryError, ...current }: SearchSourceState) => ({ - ...current, - ...stateUpdate, - isLoading: false, - items: [...(current.items ?? []), ...(stateUpdate.items || [])], - })); + this.state.next(this.getStateAfterQuery(stateUpdate, hasNewSearchQuery)); } } - search = (searchQuery?: string) => { - this.searchDebounced(searchQuery); - }; + search = (searchQuery?: string) => this.searchDebounced(searchQuery); + + cancelScheduledQuery() { + this.searchDebounced.cancel(); + } resetState() { this.state.next(this.initialState); } + + resetStateAndActivate() { + this.resetState(); + this.activate(); + } } export class UserSearchSource extends BaseSearchSource { @@ -344,12 +371,6 @@ export class MessageSearchSource extends BaseSearchSource { } } -export type DefaultSearchSources = [ - UserSearchSource, - ChannelSearchSource, - MessageSearchSource, -]; - export type SearchControllerState = { isActive: boolean; searchQuery: string; @@ -471,7 +492,7 @@ export class SearchController { }; cancelSearchQueries = () => { - this.activeSources.forEach((s) => s.searchDebounced.cancel()); + this.activeSources.forEach((s) => s.cancelScheduledQuery()); }; clear = () => { diff --git a/src/thread.ts b/src/thread.ts index 59b3b9a061..779bf1596d 100644 --- a/src/thread.ts +++ b/src/thread.ts @@ -8,7 +8,7 @@ import { import type { AscDesc, EventTypes, - FormatMessageResponse, + LocalMessage, MessagePaginationOptions, MessageResponse, ReadResponse, @@ -18,6 +18,7 @@ import type { import type { Channel } from './channel'; import type { StreamChat } from './client'; import type { CustomThreadData } from './custom_types'; +import { MessageComposer } from './messageComposer'; type QueryRepliesOptions = { sort?: { created_at: AscDesc }[]; @@ -40,10 +41,10 @@ export type ThreadState = { * Thread is identified by and has a one-to-one relation with its parent message. * We use parent message id as a thread id. */ - parentMessage: FormatMessageResponse; + parentMessage: LocalMessage; participants: ThreadResponse['thread_participants']; read: ThreadReadState; - replies: Array; + replies: Array; replyCount: number; title: string; updatedAt: Date | null; @@ -70,23 +71,24 @@ const DEFAULT_SORT: { created_at: AscDesc }[] = [{ created_at: -1 }]; const MARK_AS_READ_THROTTLE_TIMEOUT = 1000; // TODO: remove this once we move to API v2 export const THREAD_RESPONSE_RESERVED_KEYS: Record = { + active_participant_count: true, channel: true, channel_cid: true, created_at: true, + created_by: true, created_by_user_id: true, - parent_message_id: true, - title: true, - updated_at: true, - latest_replies: true, - active_participant_count: true, deleted_at: true, + draft: true, last_message_at: true, + latest_replies: true, + parent_message: true, + parent_message_id: true, participant_count: true, - reply_count: true, read: true, + reply_count: true, thread_participants: true, - created_by: true, - parent_message: true, + title: true, + updated_at: true, }; // TODO: remove this once we move to API v2 @@ -109,10 +111,11 @@ const constructCustomDataObject = (threadData: T) => { export class Thread { public readonly state: StateStore; public readonly id: string; + public readonly messageComposer: MessageComposer; private client: StreamChat; private unsubscribeFunctions: Set<() => void> = new Set(); - private failedRepliesMap: Map = new Map(); + private failedRepliesMap: Map = new Map(); constructor({ client, @@ -169,6 +172,12 @@ export class Thread { this.id = threadData.parent_message_id; this.client = client; + + this.messageComposer = new MessageComposer({ + client, + composition: threadData.draft, + compositionContext: this, + }); } get channel() { @@ -213,17 +222,21 @@ export class Thread { } if (thread.id !== this.id) { - throw new Error("Cannot hydrate thread state with using thread's state"); + throw new Error( + "Cannot hydrate thread's state using thread with different threadId", + ); } const { + createdAt, + custom, + title, + deletedAt, + parentMessage, + participants, read, replyCount, replies, - parentMessage, - participants, - createdAt, - deletedAt, updatedAt, } = thread.state.getLatestValue(); @@ -231,13 +244,15 @@ export class Thread { const pendingReplies = Array.from(this.failedRepliesMap.values()); this.state.partialNext({ + title, + createdAt, + custom, + deletedAt, + parentMessage, + participants, read, replyCount, replies: pendingReplies.length ? replies.concat(pendingReplies) : replies, - parentMessage, - participants, - createdAt, - deletedAt, updatedAt, isStateStale: false, }); @@ -433,6 +448,7 @@ export class Thread { public unregisterSubscriptions = () => { this.unsubscribeFunctions.forEach((cleanupFunction) => cleanupFunction()); this.unsubscribeFunctions.clear(); + this.state.partialNext({ isStateStale: true }); }; public deleteReplyLocally = ({ message }: { message: MessageResponse }) => { @@ -462,7 +478,7 @@ export class Thread { message, timestampChanged = false, }: { - message: MessageResponse; + message: MessageResponse | LocalMessage; timestampChanged?: boolean; }) => { if (message.parent_id !== this.id) { diff --git a/src/thread_manager.ts b/src/thread_manager.ts index b70f0d11f1..b843efa843 100644 --- a/src/thread_manager.ts +++ b/src/thread_manager.ts @@ -51,6 +51,9 @@ export class ThreadManager { threads: ThreadManagerState['threads']; threadsById: Record; }; + // cache used in combination with threadsById + // used for threads which are not stored in the list + // private threadCache: Record = {}; constructor({ client }: { client: StreamChat }) { this.client = client; @@ -238,11 +241,10 @@ export class ThreadManager { limit: Math.min(limit, MAX_QUERY_THREADS_LIMIT) || MAX_QUERY_THREADS_LIMIT, }); - const currentThreads = this.threadsById; const nextThreads: Thread[] = []; for (const incomingThread of response.threads) { - const existingThread = currentThreads[incomingThread.id]; + const existingThread = this.threadsById[incomingThread.id]; if (existingThread) { // Reuse thread instances if possible diff --git a/src/types.ts b/src/types.ts index f12d53a921..2df9cc2197 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,6 +16,7 @@ import type { CustomThreadData, CustomUserData, } from './custom_types'; +import type { NotificationManager } from './notifications'; /** * Utility Types @@ -41,12 +42,12 @@ export type KnownKeys = { : never; export type RequireAtLeastOne = { - [K in keyof T]-?: Required> & Partial>>; + [K in keyof T]-?: Required> & Partial>; }[keyof T]; export type RequireOnlyOne = Omit & { - [K in Keys]-?: Required> & Partial>; + [K in Keys]-?: Required> & Partial, undefined>>; }[Keys]; export type PartializeKeys = Partial> & Omit; @@ -311,6 +312,7 @@ export type ChannelAPIResponse = { members: ChannelMemberResponse[]; messages: MessageResponse[]; pinned_messages: MessageResponse[]; + draft?: DraftResponse; hidden?: boolean; membership?: ChannelMemberResponse | null; pending_messages?: PendingMessageResponse[]; @@ -468,9 +470,9 @@ export type FlagUserResponse = APIResponse & { review_queue_item_id?: string; }; -export type FormatMessageResponse = Omit< - MessageResponse, - 'created_at' | 'pinned_at' | 'updated_at' | 'deleted_at' | 'status' +export type LocalMessageBase = Omit< + MessageResponseBase, + 'created_at' | 'deleted_at' | 'pinned_at' | 'status' | 'updated_at' > & { created_at: Date; deleted_at: Date | null; @@ -479,6 +481,16 @@ export type FormatMessageResponse = Omit< updated_at: Date; }; +export type LocalMessage = LocalMessageBase & { + error?: ErrorFromResponse; + quoted_message?: LocalMessageBase; +}; + +/** + * @deprecated in favor of LocalMessage + */ +export type FormatMessageResponse = LocalMessage; + export type GetCommandResponse = APIResponse & CreateCommandOptions & CreatedAtUpdatedAt; export type GetMessageAPIResponse = SendMessageAPIResponse; @@ -497,6 +509,7 @@ export interface ThreadResponse extends CustomThreadData { active_participant_count?: number; created_by?: UserResponse; deleted_at?: string; + draft?: DraftResponse; last_message_at?: string; participant_count?: number; read?: Array; @@ -1319,6 +1332,12 @@ export type StreamChatOptions = AxiosRequestConfig & { /** experimental feature, please contact support if you want this feature enabled for you */ enableWSFallback?: boolean; logger?: Logger; + /** + * Custom notification manager service to use for the client. + * If not provided, a default notification manager will be created. + * Notifications are used to communicate events like errors, warnings, info, etc. Other services can publish notifications or subscribe to the NotificationManager state changes. + */ + notifications?: NotificationManager; /** * When true, user will be persisted on client. Otherwise if `connectUser` call fails, then you need to * call `connectUser` again to retry. @@ -2072,10 +2091,16 @@ export type Sort = { export type UserSort = Sort | Array>; export type MemberSort = - | Sort> + | Sort< + Pick & { + user_id?: string; + } + > | Array< Sort< - Pick + Pick & { + user_id?: string; + } > >; @@ -2656,9 +2681,11 @@ export type Logger = ( extraData?: Record, ) => void; -export type Message = Partial & { - mentioned_users?: string[]; -}; +export type Message = Partial< + MessageBase & { + mentioned_users: string[]; + } +>; export type MessageBase = CustomMessageData & { id: string; @@ -2675,6 +2702,7 @@ export type MessageBase = CustomMessageData & { show_in_channel?: boolean; silent?: boolean; text?: string; + type?: MessageLabel; user?: UserResponse | null; user_id?: string; }; @@ -2689,6 +2717,7 @@ export type MessageLabel = export type SendMessageOptions = { force_moderation?: boolean; + // @deprecated use `pending` instead is_pending_message?: boolean; keep_channel_hidden?: boolean; pending?: boolean; @@ -2912,22 +2941,25 @@ export type TranslationLanguages = export type TypingStartEvent = Event; -export type ReservedMessageFields = +export type ReservedUpdatedMessageFields = | 'command' | 'created_at' + | 'deleted_at' | 'html' + | 'i18n' | 'latest_reactions' + // the the original array of UserResponse object is converted to array of user ids and re-inserted before sending the update request + | 'mentioned_users' | 'own_reactions' + | 'pinned_at' | 'quoted_message' | 'reaction_counts' | 'reply_count' | 'type' | 'updated_at' - | 'pinned_at' - | 'user' | '__html'; -export type UpdatedMessage = Omit & { +export type UpdatedMessage = Omit & { mentioned_users?: string[]; type?: MessageLabel; }; @@ -3165,7 +3197,7 @@ export type MessageSetType = 'latest' | 'current' | 'new'; export type MessageSet = { isCurrent: boolean; isLatest: boolean; - messages: FormatMessageResponse[]; + messages: LocalMessage[]; pagination: { hasNext: boolean; hasPrev: boolean }; }; diff --git a/src/types.utility.ts b/src/types.utility.ts new file mode 100644 index 0000000000..e422dd5175 --- /dev/null +++ b/src/types.utility.ts @@ -0,0 +1,3 @@ +export type DeepPartial = { + [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; +}; diff --git a/src/utils.ts b/src/utils.ts index c57c1aa5e2..0fda412ecf 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,21 +5,26 @@ import type { ChannelQueryOptions, ChannelSort, ChannelSortBase, - FormatMessageResponse, + LocalMessage, + LocalMessageBase, Logger, + Message, MessagePaginationOptions, MessageResponse, + MessageResponseBase, MessageSet, OwnUserBase, OwnUserResponse, PromoteChannelParams, QueryChannelAPIResponse, ReactionGroupResponse, + UpdatedMessage, UserResponse, } from './types'; import type { StreamChat } from './client'; import type { Channel } from './channel'; import type { AxiosRequestConfig } from 'axios'; +import { LOCAL_MESSAGE_FIELDS, RESERVED_UPDATED_MESSAGE_FIELDS } from './constants'; /** * logChatPromiseExecution - utility function for logging the execution of a promise.. @@ -297,23 +302,91 @@ export const axiosParamsSerializer: AxiosRequestConfig['paramsSerializer'] = (pa * @param {MessageResponse} message `MessageResponse` object */ export function formatMessage( - message: MessageResponse | FormatMessageResponse, -): FormatMessageResponse { + message: MessageResponse | MessageResponseBase | LocalMessage, +): LocalMessage { + const toLocalMessageBase = ( + msg: MessageResponse | MessageResponseBase | LocalMessage | null | undefined, + ): LocalMessageBase | null => { + if (!msg) return null; + return { + ...msg, + created_at: message.created_at ? new Date(message.created_at) : new Date(), + deleted_at: message.deleted_at ? new Date(message.deleted_at) : null, + pinned_at: message.pinned_at ? new Date(message.pinned_at) : null, + reaction_groups: maybeGetReactionGroupsFallback( + message.reaction_groups, + message.reaction_counts, + message.reaction_scores, + ), + status: message.status || 'received', + updated_at: message.updated_at ? new Date(message.updated_at) : new Date(), + }; + }; + + return { + ...toLocalMessageBase(message), + error: (message as LocalMessage).error ?? null, + quoted_message: toLocalMessageBase((message as MessageResponse).quoted_message), + } as LocalMessage; +} + +export const localMessageToNewMessagePayload = (localMessage: LocalMessage): Message => { + /* eslint-disable @typescript-eslint/no-unused-vars */ + const { + // Remove all timestamp fields and client-specific fields. + // Field pinned_at can therefore be earlier than created_at as new message payload can hold it. + created_at, + updated_at, + deleted_at, + // Client-specific fields + error, + status, + // Reaction related fields + latest_reactions, + own_reactions, + reaction_counts, + reaction_scores, + reply_count, + // Message text related fields that shouldn't be in update + command, + html, + i18n, + quoted_message, + mentioned_users, + // Message content related fields + ...messageFields + } = localMessage; + + return { + ...messageFields, + pinned_at: messageFields.pinned_at?.toISOString(), + mentioned_users: mentioned_users?.map((user) => user.id), + }; +}; + +export const toUpdatedMessagePayload = ( + message: LocalMessage | Partial, +): UpdatedMessage => { + const messageFields = Object.fromEntries( + Object.entries(message).filter( + ([key]) => + ![...RESERVED_UPDATED_MESSAGE_FIELDS, ...LOCAL_MESSAGE_FIELDS].includes( + key as + | (typeof RESERVED_UPDATED_MESSAGE_FIELDS)[number] + | (typeof LOCAL_MESSAGE_FIELDS)[number], + ), + ), + ) as UpdatedMessage; + return { - ...message, - // parse the dates - pinned_at: message.pinned_at ? new Date(message.pinned_at) : null, - created_at: message.created_at ? new Date(message.created_at) : new Date(), - updated_at: message.updated_at ? new Date(message.updated_at) : new Date(), - deleted_at: message.deleted_at ? new Date(message.deleted_at) : null, - status: message.status || 'received', - reaction_groups: maybeGetReactionGroupsFallback( - message.reaction_groups, - message.reaction_counts, - message.reaction_scores, + ...messageFields, + pinned: !!message.pinned_at, + mentioned_users: message.mentioned_users?.map((user) => + typeof user === 'string' ? user : user.id, ), + user_id: message.user?.id ?? message.user_id, }; -} +}; export const findIndexInSortedArray = ({ needle, @@ -404,7 +477,7 @@ export const findIndexInSortedArray = ({ return left; }; -export function addToMessageList( +export function addToMessageList( messages: readonly T[], newMessage: T, timestampChanged = false, @@ -1132,3 +1205,8 @@ export const promoteChannel = ({ return newChannels; }; + +export const isDate = (value: unknown): value is Date => !!(value as Date).getTime; + +export const isLocalMessage = (message: unknown): message is LocalMessage => + isDate((message as LocalMessage).created_at); diff --git a/src/utils/FixedSizeQueueCache.ts b/src/utils/FixedSizeQueueCache.ts new file mode 100644 index 0000000000..15027d4a52 --- /dev/null +++ b/src/utils/FixedSizeQueueCache.ts @@ -0,0 +1,74 @@ +type Dispose = (key: K, value: T) => void; +/** + * A cache that stores a fixed number of values in a queue. + * The most recently added or retrieved value is kept at the front of the queue. + * @template K - The type of the keys. + * @template T - The type of the values. + */ +export class FixedSizeQueueCache { + private keys: Array; + private size: number; + private map: Map; + private dispose: Dispose | null; + + constructor(size: number, options?: { dispose: (key: K, value: T) => void }) { + if (!size) throw new Error('Size must be greater than 0'); + this.keys = []; + this.size = size; + this.map = new Map(); + this.dispose = options?.dispose ?? null; + } + + /** + * Adds a new or moves the existing reference to the front of the queue + * @param key + * @param value + */ + add(key: K, value: T) { + const index = this.keys.indexOf(key); + + if (index > -1) { + this.keys.splice(this.keys.indexOf(key), 1); + } else if (this.keys.length >= this.size) { + const itemKey = this.keys.shift(); + + if (itemKey) { + const item = this.peek(itemKey); + + if (item) { + this.dispose?.(itemKey, item); + } + + this.map.delete(itemKey); + } + } + + this.keys.push(key); + this.map.set(key, value); + } + + /** + * Retrieves the value by key. + * @param key + */ + peek(key: K) { + const value = this.map.get(key); + + return value; + } + + /** + * Retrieves the value and moves it to the front of the queue. + * @param key + */ + get(key: K) { + const foundItem = this.peek(key); + + if (foundItem && this.keys.indexOf(key) !== this.size - 1) { + this.keys.splice(this.keys.indexOf(key), 1); + this.keys.push(key); + } + + return foundItem; + } +} diff --git a/src/utils/concurrency.ts b/src/utils/concurrency.ts new file mode 100644 index 0000000000..abb985cdfc --- /dev/null +++ b/src/utils/concurrency.ts @@ -0,0 +1,135 @@ +interface PendingPromise { + onContinued: () => void; + promise: Promise; +} + +type AsyncWrapper

= ( + tag: string | symbol, + cb: (...args: P) => Promise, +) => { + cb: () => Promise; + onContinued: () => void; +}; + +/** + * Runs async functions serially. Useful for wrapping async actions that + * should never run simultaneously: if marked with the same tag, functions + * will run one after another. + * + * @param tag Async functions with the same tag will run serially. Async functions + * with different tags can run in parallel. + * @param cb Async function to run. + * @returns Promise that resolves when async functions returns. + */ +export const withoutConcurrency = createRunner(wrapWithContinuationTracking); + +/** + * Runs async functions serially, and cancels all other actions with the same tag + * when a new action is scheduled. Useful for wrapping async actions that override + * each other (e.g. enabling and disabling camera). + * + * If an async function hasn't started yet and was canceled, it will never run. + * If an async function is already running and was canceled, it will be notified + * via an abort signal passed as an argument. + * + * @param tag Async functions with the same tag will run serially and are canceled + * when a new action with the same tag is scheduled. + * @param cb Async function to run. Receives AbortSignal as the only argument. + * @returns Promise that resolves when async functions returns. If the function didn't + * start and was canceled, will resolve with 'canceled'. If the function started to run, + * it's up to the function to decide how to react to cancelation. + */ +export const withCancellation = createRunner(wrapWithCancellation); + +const pendingPromises = new Map(); + +export function hasPending(tag: string | symbol) { + return pendingPromises.has(tag); +} + +export async function settled(tag: string | symbol) { + await pendingPromises.get(tag)?.promise; +} + +/** + * Implements common functionality of running async functions serially, by chaining + * their promises one after another. + * + * Before running, async function is "wrapped" using the provided wrapper. This wrapper + * can add additional steps to run before or after the function. + * + * When async function is scheduled to run, the previous function is notified + * by calling the associated onContinued callback. This behavior of this callback + * is defined by the wrapper. + */ +function createRunner

(wrapper: AsyncWrapper) { + return function run(tag: string | symbol, cb: (...args: P) => Promise) { + const { cb: wrapped, onContinued } = wrapper(tag, cb); + const pending = pendingPromises.get(tag); + pending?.onContinued(); + const promise = pending ? pending.promise.then(wrapped, wrapped) : wrapped(); + pendingPromises.set(tag, { promise, onContinued }); + return promise; + }; +} + +/** + * Wraps an async function with an additional step run after the function: + * if the function is the last in the queue, it cleans up the whole chain + * of promises after finishing. + */ +function wrapWithContinuationTracking(tag: string | symbol, cb: () => Promise) { + let hasContinuation = false; + const wrapped = () => + cb().finally(() => { + if (!hasContinuation) { + pendingPromises.delete(tag); + } + }); + const onContinued = () => (hasContinuation = true); + return { cb: wrapped, onContinued }; +} + +/** + * Wraps an async function with additional functionality: + * 1. Associates an abort signal with every function, that is passed to it + * as an argument. When a new function is scheduled to run after the current + * one, current signal is aborted. + * 2. If current function didn't start and was aborted, in will never start. + * 3. If the function is the last in the queue, it cleans up the whole chain + * of promises after finishing. + * + * The cb is passed the AbortController instance for a given execution. + * The cb should implement own cancellation logic to reflect that the given AbortController has been aborted. + * + * ``` + * const cb = async (signal: AbortSignal) => { + * await new Promise(resolve => setTimeout(resolve, 50)); + * if (signal.aborted) { + * abortedSignals.push(signal); + * return 'canceled'; + * } + * return 1; + * }; + * const result = withCancellation('tag-x', cb); // the result variable may acquire value "canceled" or 1 + * ``` + */ +function wrapWithCancellation( + tag: string | symbol, + cb: (signal: AbortSignal) => Promise, +) { + const ac = new AbortController(); + const wrapped = () => { + if (ac.signal.aborted) { + return Promise.resolve('canceled' as const); + } + + return cb(ac.signal).finally(() => { + if (!ac.signal.aborted) { + pendingPromises.delete(tag); + } + }); + }; + const onContinued = () => ac.abort(); + return { cb: wrapped, onContinued }; +} diff --git a/src/utils/mergeWith/index.ts b/src/utils/mergeWith/index.ts new file mode 100644 index 0000000000..f0ff94f22c --- /dev/null +++ b/src/utils/mergeWith/index.ts @@ -0,0 +1,2 @@ +export * from './mergeWith'; +export * from './mergeWithDiff'; diff --git a/src/utils/mergeWith/mergeWith.ts b/src/utils/mergeWith/mergeWith.ts new file mode 100644 index 0000000000..8010f4116b --- /dev/null +++ b/src/utils/mergeWith/mergeWith.ts @@ -0,0 +1,44 @@ +/** + * This method is like `_.merge` except that it accepts `customizer` which + * is invoked to produce the merged values of the destination and source + * properties. If `customizer` returns `undefined` merging is handled by the + * method instead. The `customizer` is invoked with seven arguments: + * (objValue, srcValue, key, object, source, stack). + * + * @category Object + * @param object The destination object. + * @param source A single source object or an array of objects to be merged into the . + * @param customizer The function to customize assigned values. + * @returns Returns `object`. + * @example + * + * function customizer(objValue, srcValue) { + * if (_.isArray(objValue)) { + * return objValue.concat(srcValue); + * } + * } + * + * var object = { + * 'fruits': ['apple'], + * 'vegetables': ['beet'] + * }; + * + * var other = { + * 'fruits': ['banana'], + * 'vegetables': ['carrot'] + * }; + * + * _.mergeWith(object, other, customizer); + * // => { 'fruits': ['apple', 'banana'], 'vegetables': ['beet', 'carrot'] } + */ +import type { MergeWithCustomizer } from './mergeWithCore'; +import { createMergeCore } from './mergeWithCore'; + +export function mergeWith( + target: T, + source: object | object[], + customizer?: MergeWithCustomizer, +): T { + const mergeCore = createMergeCore(); + return mergeCore({ target, source, customizer }).result; +} diff --git a/src/utils/mergeWith/mergeWithCore.ts b/src/utils/mergeWith/mergeWithCore.ts new file mode 100644 index 0000000000..91369420e1 --- /dev/null +++ b/src/utils/mergeWith/mergeWithCore.ts @@ -0,0 +1,604 @@ +/** + * Core utility functions and types for mergeWith functionality. + * This file contains shared logic used by both mergeWith and mergeWithDiff functions. + */ + +export type MergeWithCustomizer = ( + objValue: unknown, + srcValue: unknown, + key: string | symbol, + object: T, + source: object, + stack: Set, +) => unknown | undefined; + +export type PendingMerge = { + sourceKey: string | symbol; + parentTarget: object; + source: object; + target: object; +}; + +export type ChangeType = 'added' | 'updated' | 'circular' | (string & {}); + +export interface DiffNode { + type?: ChangeType; + children: Record; + value?: unknown; + oldValue?: unknown; +} + +export const isClassInstance = (value: unknown): boolean => { + if (!value || typeof value !== 'object') return false; + + // Arrays are not class instances + if (Array.isArray(value)) return false; + + // Get the prototype chain + const proto = Object.getPrototypeOf(value); + + // If it's null or Object.prototype, it's a plain object + if (proto === null || proto === Object.prototype) return false; + + // Check if it has a constructor that's not Object + return value.constructor && value.constructor !== Object; +}; + +/** + * Performs a deep comparison between two values to determine if they are equivalent. + * This is similar to Lodash's isEqual implementation but simplified. + */ +export const isEqual = ( + value1: unknown, + value2: unknown, + compareStack = new Set<[unknown, unknown]>(), + objectStack1 = new WeakSet(), + objectStack2 = new WeakSet(), +): boolean => { + // Handle simple equality cases first + if (value1 === value2) return true; + + // If either is null/undefined, they're not equal (already checked ===) + if (value1 == null || value2 == null) return false; + + // Get the type of both values + const type1 = typeof value1; + const type2 = typeof value2; + + // Different types mean they're not equal + if (type1 !== type2) return false; + + // Handle non-object types that need special comparison + if (type1 !== 'object') { + // Special case for NaN + // eslint-disable-next-line no-self-compare + if (value1 !== value1 && value2 !== value2) return true; + return value1 === value2; + } + + // At this point, both values are objects + const obj1 = value1 as object; + const obj2 = value2 as object; + + // Check for circular references in each object + if (objectStack1.has(obj1) || objectStack2.has(obj2)) { + // If either object has been seen before, consider them equal + // if they're both in a circular reference + return objectStack1.has(obj1) && objectStack2.has(obj2); + } + + // Add objects to their respective stacks + objectStack1.add(obj1); + objectStack2.add(obj2); + + // Handle Date objects - needs to be before the class instance check + if (value1 instanceof Date && value2 instanceof Date) { + objectStack1.delete(obj1); + objectStack2.delete(obj2); + return value1.getTime() === value2.getTime(); + } + + // Handle RegExp objects - needs to be before the class instance check + if (value1 instanceof RegExp && value2 instanceof RegExp) { + objectStack1.delete(obj1); + objectStack2.delete(obj2); + return value1.toString() === value2.toString(); + } + + // If either is a class instance, use reference equality (already checked above) + if (isClassInstance(value1) || isClassInstance(value2)) { + // Clean up before returning + objectStack1.delete(obj1); + objectStack2.delete(obj2); + return false; + } + + // Handle arrays + const isArray1 = Array.isArray(value1); + const isArray2 = Array.isArray(value2); + + if (isArray1 !== isArray2) { + // Clean up before returning + objectStack1.delete(obj1); + objectStack2.delete(obj2); + return false; + } + + if (isArray1 && isArray2) { + const arr1 = value1 as unknown[]; + const arr2 = value2 as unknown[]; + + if (arr1.length !== arr2.length) { + // Clean up before returning + objectStack1.delete(obj1); + objectStack2.delete(obj2); + return false; + } + + // Check for circular references in the comparison context + const pairKey: [unknown, unknown] = [value1, value2]; + if (compareStack.has(pairKey)) { + // Clean up before returning + objectStack1.delete(obj1); + objectStack2.delete(obj2); + return true; + } + compareStack.add(pairKey); + + // Compare each element + for (let i = 0; i < arr1.length; i++) { + if (!isEqual(arr1[i], arr2[i], compareStack, objectStack1, objectStack2)) { + compareStack.delete(pairKey); + // Clean up before returning + objectStack1.delete(obj1); + objectStack2.delete(obj2); + return false; + } + } + + compareStack.delete(pairKey); + objectStack1.delete(obj1); + objectStack2.delete(obj2); + return true; + } + + // Handle plain objects + const plainObj1 = value1 as Record; + const plainObj2 = value2 as Record; + + const keys1 = Object.keys(plainObj1); + const keys2 = Object.keys(plainObj2); + + // If key counts differ, objects aren't equal + if (keys1.length !== keys2.length) { + // Clean up before returning + objectStack1.delete(obj1); + objectStack2.delete(obj2); + return false; + } + + // Verify all keys in obj2 are in obj1 (we already checked counts, so this + // also ensures all keys in obj1 are in obj2) + for (const key of keys2) { + if (!Object.prototype.hasOwnProperty.call(plainObj1, key)) { + // Clean up before returning + objectStack1.delete(obj1); + objectStack2.delete(obj2); + return false; + } + } + + // Check for circular references in the comparison context + const pairKey: [unknown, unknown] = [value1, value2]; + if (compareStack.has(pairKey)) { + // Clean up before returning + objectStack1.delete(obj1); + objectStack2.delete(obj2); + return true; + } + compareStack.add(pairKey); + + // Compare each property's value + for (const key of keys1) { + if ( + !isEqual(plainObj1[key], plainObj2[key], compareStack, objectStack1, objectStack2) + ) { + compareStack.delete(pairKey); + // Clean up before returning + objectStack1.delete(obj1); + objectStack2.delete(obj2); + return false; + } + } + + compareStack.delete(pairKey); + // Clean up before returning successful comparison + objectStack1.delete(obj1); + objectStack2.delete(obj2); + return true; +}; + +/** + * Generates a diff between original and modified objects. + * This is used after the merge operation to track what has changed. + */ +export function generateDiff(original: unknown, modified: unknown): DiffNode | null { + // Root diff node + const diffRoot: DiffNode = { children: {} }; + + // Compare the objects and build the diff tree + compareAndBuildDiff(original, modified, diffRoot); + + // Clean up the diff tree (remove empty nodes) + return cleanupDiffTree(diffRoot); +} + +/** + * Helper function to compare and build the diff tree + */ +function compareAndBuildDiff( + original: unknown, + modified: unknown, + parentDiffNode: DiffNode, + key?: string | symbol, + /** + * Tracks pairs of objects being compared + * - It stores pairs of values that are being compared `[original, modified]` + * - This helps detect when we're comparing the same pair of objects again + * - It prevents infinite recursion when comparing complex object structures + */ + compareStack = new Set<[unknown, unknown]>(), + /** + * Tracks individual objects that are being processed in the current traversal path + * - It's used to detect when we encounter the same object multiple times in a single traversal path + * - This helps identify self-referential or circular structures within a single object (e.g., when an object references itself) + * - When an object appears in `objectStack` again, we know it's a circular reference within the same object + */ + objectStack = new Set(), +): void { + // If values are equal, no diff to record + if (isEqual(original, modified, new Set(compareStack))) { + return; + } + + // Handle additions (value in modified but not in original) + if (original === undefined || original === null) { + if (key !== undefined) { + parentDiffNode.children[String(key)] = { + type: 'added', + value: modified, + children: {}, + }; + } + return; + } + + // Check for circular references in objects + if (typeof original === 'object' && original !== null) { + if (objectStack.has(original)) { + if (key !== undefined) { + parentDiffNode.children[String(key)] = { + type: 'circular', + value: modified, + oldValue: original, + children: {}, + }; + } + return; + } + objectStack.add(original); + } + + // Check if we're dealing with non-objects or special object types that should be treated atomically + const shouldTreatAtomically = + typeof original !== 'object' || + typeof modified !== 'object' || + original === null || + modified === null || + Array.isArray(original) !== Array.isArray(modified) || + isClassInstance(original) || + isClassInstance(modified); + + if (shouldTreatAtomically) { + if (key !== undefined) { + parentDiffNode.children[String(key)] = { + type: 'updated', + value: modified, + oldValue: original, + children: {}, + }; + } + + // Remove from object stack if it was added + if (typeof original === 'object' && original !== null) { + objectStack.delete(original); + } + return; + } + + // Handle objects + const originalObj = original as Record; + const modifiedObj = modified as Record; + + // Create a diff node for this level if we're processing a property + const currentDiffNode = + key !== undefined + ? { + type: 'updated' as ChangeType, + children: {}, + oldValue: original, + value: modified, + } + : parentDiffNode; + + if (key !== undefined) { + parentDiffNode.children[String(key)] = currentDiffNode; + } + + // Check for circular references in comparison + const pairKey: [unknown, unknown] = [original, modified]; + if (compareStack.has(pairKey)) { + // Remove from object stack before returning + if (typeof original === 'object' && original !== null) { + objectStack.delete(original); + } + return; + } + compareStack.add(pairKey); + + // Process all keys from both objects + const allKeys = new Set([ + ...Object.keys(originalObj), + ...Object.getOwnPropertySymbols(originalObj), + ...Object.keys(modifiedObj), + ...Object.getOwnPropertySymbols(modifiedObj), + ]); + + for (const childKey of allKeys) { + const originalValue = originalObj[childKey]; + const modifiedValue = modifiedObj[childKey]; + + // Handle deleted properties (they exist in original but not in modified) + if (!(childKey in modifiedObj)) { + // Currently we don't track deletions, but could be added here if needed + continue; + } + + // Handle added properties (they exist in modified but not in original) + if (!(childKey in originalObj)) { + currentDiffNode.children[String(childKey)] = { + type: 'added', + value: modifiedValue, + children: {}, + }; + continue; + } + + // Process properties that exist in both but may have changed + compareAndBuildDiff( + originalValue, + modifiedValue, + currentDiffNode, + childKey, + compareStack, + objectStack, + ); + } + + compareStack.delete(pairKey); + + // Remove from object stack before returning + if (typeof original === 'object' && original !== null) { + objectStack.delete(original); + } +} + +export function createMergeCore(options: { trackDiff?: boolean } = {}) { + const { trackDiff = false } = options; + + return function mergeCore({ + target, + source, + customizer, + }: { + target: T; + source: object | object[]; + customizer?: MergeWithCustomizer; + }): { result: T; diff: DiffNode | null } { + const sources = Array.isArray(source) ? source : [source]; + + // Store the original target if we need to track diffs + const originalTarget = trackDiff ? structuredClone(target) : undefined; + + function handleCustomizer( + targetValue: unknown, + srcValue: unknown, + sourceKey: string | symbol, + target: object, + src: object, + stack: Set, + ): boolean { + const customValue = customizer?.( + targetValue, + srcValue, + sourceKey, + target as T, + src, + stack, + ); + if (customValue !== undefined) { + Object.defineProperty(target, sourceKey, { + value: customValue, + enumerable: true, + writable: true, + configurable: true, + }); + return true; + } + return false; + } + + function createNewTarget(targetValue: unknown, srcValue: unknown): object { + if (targetValue && typeof targetValue === 'object') { + // Check if it's a class instance (not a plain object) + const isTargetClassInstance = isClassInstance(targetValue); + const isSourceClassInstance = isClassInstance(srcValue); + + // If either is a class instance, don't try to merge them + if (isTargetClassInstance || isSourceClassInstance) { + // If source is a class instance, use it + if (isSourceClassInstance) { + return srcValue as object; + } + // Otherwise preserve the target + return targetValue; + } + + // For plain objects, use normal merging + return Array.isArray(targetValue) ? [...targetValue] : { ...targetValue }; + } + return Array.isArray(srcValue) ? [] : {}; + } + + function processSourceValue( + target: object, + src: object, + sourceKey: string | symbol, + stack: Set, + pendingMerges: PendingMerge[], + ): void { + const srcValue = src[sourceKey as keyof typeof src]; + const targetValue = target[sourceKey as keyof typeof target]; + + if (handleCustomizer(targetValue, srcValue, sourceKey, target, src, stack)) { + return; + } + + if (srcValue && typeof srcValue === 'object') { + if (!stack.has(srcValue)) { + const newTarget = createNewTarget(targetValue, srcValue); + Object.defineProperty(target, sourceKey, { + value: newTarget, + enumerable: true, + writable: true, + configurable: true, + }); + + if (isClassInstance(newTarget)) return; + + pendingMerges.push({ + target: newTarget, + source: srcValue, + sourceKey, + parentTarget: target, + }); + } + } else if (srcValue !== undefined) { + target[sourceKey as keyof typeof target] = srcValue; + } + } + + function processKeys( + target: object, + source: object, + stack: Set, + pendingMerges: PendingMerge[], + ): void { + const sourceKeys = [ + ...Object.keys(source), + ...Object.getOwnPropertySymbols(source), + ]; + for (const sourceKey of sourceKeys) { + processSourceValue(target, source, sourceKey, stack, pendingMerges); + } + } + + function processPendingMerge( + { target, source, sourceKey, parentTarget }: PendingMerge, + stack: Set, + pendingMerges: PendingMerge[], + ): void { + if (stack.has(source)) { + // We've detected a circular reference in the source object + // Just skip this merge to avoid infinite recursion + + // If we're tracking diffs, we need to mark this as a circular reference + if (trackDiff && sourceKey && parentTarget) { + Object.defineProperty(parentTarget, sourceKey, { + value: target, + enumerable: true, + writable: true, + configurable: true, + }); + } + return; + } + + if (!stack.has(target) && !stack.has(source)) { + stack.add(target); + stack.add(source); + processKeys(target, source, stack, pendingMerges); + stack.delete(source); + stack.delete(target); + } + } + + function baseMerge(object: T, source: object, stack = new Set()): T { + // prevent infinite recursion + if (stack.has(object) || stack.has(source)) { + return { ...object }; + } + + const result = { ...object }; + const pendingMerges: PendingMerge[] = []; + stack.add(result); + stack.add(source); + + processKeys(result, source, stack, pendingMerges); + + while (pendingMerges.length) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + processPendingMerge(pendingMerges.pop()!, stack, pendingMerges); + } + + stack.delete(source); + stack.delete(result); + + return result; + } + + const result = sources.reduce((result, source) => baseMerge(result, source) as T, { + ...target, + } as T); + + // If diff tracking is enabled, generate the diff after the merge is complete + const diff = + trackDiff && originalTarget ? generateDiff(originalTarget, result) : null; + + return { result, diff }; + }; +} + +// Utility function to clean up the diff tree by removing empty child nodes +export function cleanupDiffTree(diffNode: DiffNode): DiffNode | null { + const cleanChildren: Record = {}; + let hasChildren = false; + + for (const key in diffNode.children) { + const childNode = cleanupDiffTree(diffNode.children[key]); + if (childNode) { + cleanChildren[key] = childNode; + hasChildren = true; + } + } + + // If this node has a type (added/updated) or has children, keep it + if (diffNode.type || hasChildren) { + return { + ...diffNode, + children: cleanChildren, + }; + } + + return null; +} diff --git a/src/utils/mergeWith/mergeWithDiff.ts b/src/utils/mergeWith/mergeWithDiff.ts new file mode 100644 index 0000000000..34f02e0537 --- /dev/null +++ b/src/utils/mergeWith/mergeWithDiff.ts @@ -0,0 +1,53 @@ +/** + * This method is like `mergeWith` except that it also returns information about + * which keys have been added or updated during the merge operation. + * + * @category Object + * @param object The destination object. + * @param source A single source object or an array of objects to be merged into the object. + * @param customizer The function to customize assigned values. + * @returns Returns an object containing the merged result and a hierarchical diff object. + * @example + * + * function customizer(objValue, srcValue) { + * if (Array.isArray(objValue)) { + * return objValue.concat(srcValue); + * } + * } + * + * var object = { + * 'fruits': ['apple'], + * 'vegetables': ['beet'] + * }; + * + * var other = { + * 'fruits': ['banana'], + * 'vegetables': ['carrot'], + * 'grains': ['wheat'] + * }; + * + * const { result, diff } = mergeWithDiff({ target: object, source: other, customizer }); + * // result => { 'fruits': ['apple', 'banana'], 'vegetables': ['beet', 'carrot'], 'grains': ['wheat'] } + * // diff => { + * // children: { + * // 'fruits': { type: 'updated', value: ['banana'], oldValue: ['apple'], children: {} }, + * // 'vegetables': { type: 'updated', value: ['carrot'], oldValue: ['beet'], children: {} }, + * // 'grains': { type: 'added', value: ['wheat'], children: {} } + * // } + * // } + */ +import { cleanupDiffTree, createMergeCore } from './mergeWithCore'; +import type { DiffNode, MergeWithCustomizer } from './mergeWithCore'; + +export function mergeWithDiff( + target: T, + source: object | object[], + customizer?: MergeWithCustomizer, +): { result: T; diff: DiffNode } { + const mergeCore = createMergeCore({ trackDiff: true }); + const { result, diff } = mergeCore({ target, source, customizer }); + + // Clean up the diff tree to remove empty nodes + + return { result, diff: cleanupDiffTree(diff ?? { children: {} }) || { children: {} } }; +} diff --git a/test/unit/MessageComposer/CustomDataManager.test.ts b/test/unit/MessageComposer/CustomDataManager.test.ts new file mode 100644 index 0000000000..74026f5751 --- /dev/null +++ b/test/unit/MessageComposer/CustomDataManager.test.ts @@ -0,0 +1,128 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { CustomDataManager } from '../../../src/messageComposer/CustomDataManager'; +import { MessageComposer } from '../../../src/messageComposer/messageComposer'; +import { Channel } from '../../../src/channel'; +import { StreamChat } from '../../../src/client'; +import { LocalMessage } from '../../../src/types'; + +describe('CustomDataManager', () => { + let customDataManager: CustomDataManager; + let mockComposer: MessageComposer; + let mockChannel: Channel; + let mockClient: StreamChat; + + beforeEach(() => { + // Reset mocks + vi.clearAllMocks(); + + // Setup mocks + mockClient = new StreamChat('apiKey', 'apiSecret'); + mockClient.user = { id: 'user-id', name: 'Test User' }; + + mockChannel = mockClient.channel('channelType', 'channelId'); + mockComposer = new MessageComposer({ + client: mockClient, + compositionContext: mockChannel, + }); + + // Create instance + customDataManager = new CustomDataManager({ + composer: mockComposer, + }); + }); + + describe('constructor', () => { + it('should initialize with empty data', () => { + expect(customDataManager.data).toEqual({}); + }); + + it('should initialize with message data if provided', () => { + const message: LocalMessage = { + custom_field: 'test-value', + id: 'test-message-id', + text: 'Test message', + type: 'regular', + attachments: [], + mentioned_users: [], + created_at: new Date(), + deleted_at: null, + pinned_at: null, + status: 'sent', + updated_at: new Date(), + }; + + const managerWithMessage = new CustomDataManager({ + composer: mockComposer, + message, + }); + + expect(managerWithMessage.data).toEqual({}); + }); + }); + + describe('initState', () => { + it('should reset state to empty data', () => { + // Set some data first + customDataManager.setData({ test: 'value' }); + expect(customDataManager.data).toEqual({ test: 'value' }); + + // Reset state + customDataManager.initState(); + expect(customDataManager.data).toEqual({}); + }); + + it('should reset state with message data if provided', () => { + const message: LocalMessage = { + custom_field: 'test-value', + id: 'test-message-id', + text: 'Test message', + type: 'regular', + attachments: [], + mentioned_users: [], + created_at: new Date(), + deleted_at: null, + pinned_at: null, + status: 'sent', + updated_at: new Date(), + }; + + customDataManager.initState({ message }); + expect(customDataManager.data).toEqual({}); + }); + }); + + describe('setCustomData', () => { + it('should update data with new values', () => { + customDataManager.setData({ field1: 'value1' }); + expect(customDataManager.data).toEqual({ field1: 'value1' }); + + customDataManager.setData({ field2: 'value2' }); + expect(customDataManager.data).toEqual({ field1: 'value1', field2: 'value2' }); + }); + + it('should override existing values', () => { + customDataManager.setData({ field1: 'value1' }); + customDataManager.setData({ field1: 'new-value' }); + expect(customDataManager.data).toEqual({ field1: 'new-value' }); + }); + }); + + describe('isDataEqual', () => { + it('should return true for equal data', () => { + const state1 = { data: { field1: 'value1' } }; + const state2 = { data: { field1: 'value1' } }; + expect(customDataManager.isDataEqual(state1, state2)).toBe(true); + }); + + it('should return false for different data', () => { + const state1 = { data: { field1: 'value1' } }; + const state2 = { data: { field1: 'value2' } }; + expect(customDataManager.isDataEqual(state1, state2)).toBe(false); + }); + + it('should handle undefined previous state', () => { + const state1 = { data: { field1: 'value1' } }; + expect(customDataManager.isDataEqual(state1, undefined)).toBe(false); + }); + }); +}); diff --git a/test/unit/MessageComposer/attachmentIdentity.test.ts b/test/unit/MessageComposer/attachmentIdentity.test.ts new file mode 100644 index 0000000000..a1d8873ded --- /dev/null +++ b/test/unit/MessageComposer/attachmentIdentity.test.ts @@ -0,0 +1,241 @@ +import { describe, expect, it } from 'vitest'; +import { + isScrapedContent, + isLocalAttachment, + isLocalUploadAttachment, + isFileAttachment, + isLocalFileAttachment, + isImageAttachment, + isLocalImageAttachment, + isAudioAttachment, + isLocalAudioAttachment, + isVoiceRecordingAttachment, + isLocalVoiceRecordingAttachment, + isVideoAttachment, + isLocalVideoAttachment, +} from '../../../src/messageComposer/attachmentIdentity'; + +describe('attachmentIdentity', () => { + describe('isScrapedContent', () => { + it('should return true for attachments with og_scrape_url', () => { + const attachment = { og_scrape_url: 'https://example.com' }; + expect(isScrapedContent(attachment)).toBe(true); + }); + + it('should return true for attachments with title_link', () => { + const attachment = { title_link: 'https://example.com' }; + expect(isScrapedContent(attachment)).toBe(true); + }); + + it('should return false for attachments without og_scrape_url or title_link', () => { + const attachment = { type: 'image' }; + expect(isScrapedContent(attachment)).toBe(false); + }); + }); + + describe('isLocalAttachment', () => { + it('should return true for attachments with localMetadata.id', () => { + const attachment = { localMetadata: { id: 'test-id' } }; + expect(isLocalAttachment(attachment)).toBe(true); + }); + + it('should return false for attachments without localMetadata.id', () => { + const attachment = { type: 'image' }; + expect(isLocalAttachment(attachment)).toBe(false); + }); + + it('should return false for attachments with empty localMetadata', () => { + const attachment = { localMetadata: {} }; + expect(isLocalAttachment(attachment)).toBe(false); + }); + + it('should return false for non-objects', () => { + expect(isLocalAttachment(null)).toBe(false); + expect(isLocalAttachment(undefined)).toBe(false); + expect(isLocalAttachment('string')).toBe(false); + }); + }); + + describe('isLocalUploadAttachment', () => { + it('should return true for attachments with localMetadata.uploadState', () => { + const attachment = { localMetadata: { id: 'test-id', uploadState: 'uploading' } }; + expect(isLocalUploadAttachment(attachment)).toBe(true); + }); + + it('should return false for attachments without localMetadata.uploadState', () => { + const attachment = { localMetadata: { id: 'test-id' } }; + expect(isLocalUploadAttachment(attachment)).toBe(false); + }); + + it('should return false for non-objects', () => { + expect(isLocalUploadAttachment(null)).toBe(false); + expect(isLocalUploadAttachment(undefined)).toBe(false); + expect(isLocalUploadAttachment('string')).toBe(false); + }); + }); + + describe('isFileAttachment', () => { + it('should return true for attachments with type "file"', () => { + const attachment = { type: 'file' }; + expect(isFileAttachment(attachment)).toBe(true); + }); + + it('should return true for attachments with mime_type not in supportedVideoFormat', () => { + const attachment = { mime_type: 'application/pdf', type: 'audio' }; + expect(isFileAttachment(attachment, ['video/mp4'])).toBe(true); + }); + + it('should return false for attachments with mime_type not in supportedVideoFormat but declared as video type', () => { + const attachment = { mime_type: 'application/pdf', type: 'video' }; + expect(isFileAttachment(attachment, ['video/mp4'])).toBe(false); + }); + + it('should return false for attachments with mime_type in supportedVideoFormat', () => { + const attachment = { mime_type: 'video/mp4', type: 'video' }; + expect(isFileAttachment(attachment, ['video/mp4'])).toBe(false); + }); + }); + + describe('isLocalFileAttachment', () => { + it('should return true for local file attachments', () => { + const attachment = { type: 'file', localMetadata: { id: 'test-id' } }; + expect(isLocalFileAttachment(attachment)).toBe(true); + }); + + it('should return false for non-local file attachments', () => { + const attachment = { type: 'file' }; + expect(isLocalFileAttachment(attachment)).toBe(false); + }); + + it('should return false for local non-file attachments', () => { + const attachment = { type: 'image', localMetadata: { id: 'test-id' } }; + expect(isLocalFileAttachment(attachment)).toBe(false); + }); + }); + + describe('isImageAttachment', () => { + it('should return true for attachments with type "image" and no scraped content', () => { + const attachment = { type: 'image' }; + expect(isImageAttachment(attachment)).toBe(true); + }); + + it('should return false for attachments with type "image" and scraped content', () => { + const attachment = { type: 'image', og_scrape_url: 'https://example.com' }; + expect(isImageAttachment(attachment)).toBe(false); + }); + + it('should return false for non-image attachments', () => { + const attachment = { type: 'file' }; + expect(isImageAttachment(attachment)).toBe(false); + }); + }); + + describe('isLocalImageAttachment', () => { + it('should return true for local image attachments', () => { + const attachment = { type: 'image', localMetadata: { id: 'test-id' } }; + expect(isLocalImageAttachment(attachment)).toBe(true); + }); + + it('should return false for non-local image attachments', () => { + const attachment = { type: 'image' }; + expect(isLocalImageAttachment(attachment)).toBe(false); + }); + + it('should return false for local non-image attachments', () => { + const attachment = { type: 'file', localMetadata: { id: 'test-id' } }; + expect(isLocalImageAttachment(attachment)).toBe(false); + }); + }); + + describe('isAudioAttachment', () => { + it('should return true for attachments with type "audio"', () => { + const attachment = { type: 'audio' }; + expect(isAudioAttachment(attachment)).toBe(true); + }); + + it('should return false for non-audio attachments', () => { + const attachment = { type: 'file' }; + expect(isAudioAttachment(attachment)).toBe(false); + }); + }); + + describe('isLocalAudioAttachment', () => { + it('should return true for local audio attachments', () => { + const attachment = { type: 'audio', localMetadata: { id: 'test-id' } }; + expect(isLocalAudioAttachment(attachment)).toBe(true); + }); + + it('should return false for non-local audio attachments', () => { + const attachment = { type: 'audio' }; + expect(isLocalAudioAttachment(attachment)).toBe(false); + }); + + it('should return false for local non-audio attachments', () => { + const attachment = { type: 'file', localMetadata: { id: 'test-id' } }; + expect(isLocalAudioAttachment(attachment)).toBe(false); + }); + }); + + describe('isVoiceRecordingAttachment', () => { + it('should return true for attachments with type "voiceRecording"', () => { + const attachment = { type: 'voiceRecording' }; + expect(isVoiceRecordingAttachment(attachment)).toBe(true); + }); + + it('should return false for non-voiceRecording attachments', () => { + const attachment = { type: 'file' }; + expect(isVoiceRecordingAttachment(attachment)).toBe(false); + }); + }); + + describe('isLocalVoiceRecordingAttachment', () => { + it('should return true for local voiceRecording attachments', () => { + const attachment = { type: 'voiceRecording', localMetadata: { id: 'test-id' } }; + expect(isLocalVoiceRecordingAttachment(attachment)).toBe(true); + }); + + it('should return false for non-local voiceRecording attachments', () => { + const attachment = { type: 'voiceRecording' }; + expect(isLocalVoiceRecordingAttachment(attachment)).toBe(false); + }); + + it('should return false for local non-voiceRecording attachments', () => { + const attachment = { type: 'file', localMetadata: { id: 'test-id' } }; + expect(isLocalVoiceRecordingAttachment(attachment)).toBe(false); + }); + }); + + describe('isVideoAttachment', () => { + it('should return true for attachments with type "video"', () => { + const attachment = { type: 'video' }; + expect(isVideoAttachment(attachment)).toBe(true); + }); + + it('should return true for attachments with mime_type in supportedVideoFormat', () => { + const attachment = { mime_type: 'video/mp4' }; + expect(isVideoAttachment(attachment, ['video/mp4'])).toBe(true); + }); + + it('should return false for attachments with mime_type not in supportedVideoFormat', () => { + const attachment = { mime_type: 'application/pdf' }; + expect(isVideoAttachment(attachment, ['video/mp4'])).toBe(false); + }); + }); + + describe('isLocalVideoAttachment', () => { + it('should return true for local video attachments', () => { + const attachment = { type: 'video', localMetadata: { id: 'test-id' } }; + expect(isLocalVideoAttachment(attachment)).toBe(true); + }); + + it('should return false for non-local video attachments', () => { + const attachment = { type: 'video' }; + expect(isLocalVideoAttachment(attachment)).toBe(false); + }); + + it('should return false for local non-video attachments', () => { + const attachment = { type: 'file', localMetadata: { id: 'test-id' } }; + expect(isLocalVideoAttachment(attachment)).toBe(false); + }); + }); +}); diff --git a/test/unit/MessageComposer/attachmentManager.test.ts b/test/unit/MessageComposer/attachmentManager.test.ts new file mode 100644 index 0000000000..badbeb08eb --- /dev/null +++ b/test/unit/MessageComposer/attachmentManager.test.ts @@ -0,0 +1,1126 @@ +import { describe, expect, it, vi } from 'vitest'; +import { + API_MAX_FILES_ALLOWED_PER_MESSAGE, + DEFAULT_UPLOAD_SIZE_LIMIT_BYTES, +} from '../../../src/constants'; +import { + AttachmentManagerConfig, + DraftMessage, + DraftResponse, + LocalMessage, + MessageComposer, + StreamChat, +} from '../../../src'; +import { AppSettings, AttachmentManager } from '../../../src'; + +// Add FileList mock +vi.mock('../../../src/messageComposer/fileUtils', async (importOriginal) => { + const original: object = await importOriginal(); + return { + ...original, + isFileList: vi.fn().mockReturnValue(false), // FileList is Web specific so for now we avoid testing for it + }; +}); +vi.mock('../../../src/utils', async (importOriginal) => { + const original: object = await importOriginal(); + return { + ...original, + generateUUIDv4: vi.fn().mockReturnValue('test-uuid'), + mergeWith: vi.fn().mockImplementation((target, source) => ({ ...target, ...source })), + randomId: vi.fn().mockReturnValue('test-uuid'), + }; +}); + +const defaultAppSettings = { + app: { + image_upload_config: { + allowed_file_extensions: ['jpg', 'png'], + allowed_mime_types: ['image/jpeg', 'image/png'], + size_limit: DEFAULT_UPLOAD_SIZE_LIMIT_BYTES, + }, + file_upload_config: { + allowed_file_extensions: ['pdf', 'doc'], + allowed_mime_types: ['application/pdf', 'application/msword'], + size_limit: DEFAULT_UPLOAD_SIZE_LIMIT_BYTES, + }, + }, +}; + +const setup = ({ + appSettings, + composition, + config, +}: { + appSettings?: Partial; + composition?: DraftResponse | LocalMessage; + config?: Partial; +} = {}) => { + // Reset mocks + vi.clearAllMocks(); + + // Setup mocks + const mockClient = new StreamChat('apiKey', 'apiSecret'); + mockClient.appSettingsPromise = Promise.resolve( + appSettings ? { app: appSettings } : defaultAppSettings, + ); + (mockClient.getAppSettings = vi + .fn() + .mockResolvedValue(appSettings ? { app: appSettings } : defaultAppSettings)), + (mockClient.notifications = { addError: vi.fn() }); + + mockClient.user = { id: 'user-id', name: 'Test User' }; + + const mockChannel = mockClient.channel('channelType', 'channelId'); + mockChannel.getClient = vi.fn().mockReturnValue(mockClient); + mockChannel.sendFile = vi + .fn() + .mockResolvedValue({ file: 'test-file-url', thumb_url: 'thumb_url-file' }); + mockChannel.sendImage = vi + .fn() + .mockResolvedValue({ file: 'test-image-url', thumb_url: 'thumb_url-image' }); + mockChannel.data = { own_capabilities: ['upload-file'] }; + const messageComposer = new MessageComposer({ + client: mockClient, + composition, + compositionContext: mockChannel, + config: { attachments: config }, + }); + return { mockClient, mockChannel, messageComposer }; +}; + +describe('AttachmentManager', () => { + describe('constructor', () => { + it('should initialize with default config', () => { + const { + messageComposer: { attachmentManager }, + mockChannel, + } = setup(); + expect(attachmentManager.channel).toBe(mockChannel); + expect(attachmentManager.state.getLatestValue()).toEqual({ + attachments: [], + }); + const config = attachmentManager.config; + expect(typeof attachmentManager.config.fileUploadFilter).toBe('function'); + expect(config.maxNumberOfFilesPerMessage).toBe(API_MAX_FILES_ALLOWED_PER_MESSAGE); + }); + + it('should initialize with draft message', () => { + const message: DraftResponse = { + message: { + id: 'test-message-id', + text: '', + type: 'regular', + attachments: [ + { + type: 'image', + image_url: 'test-image-url', + }, + ], + }, + channel_cid: 'channel-cid', + created_at: new Date().toISOString(), + }; + + // ts-expect-error mocked channel + const { + messageComposer: { attachmentManager }, + mockChannel, + } = setup({ composition: message }); + + expect(attachmentManager.attachments).toEqual([ + { + type: 'image', + image_url: 'test-image-url', + localMetadata: { + id: 'test-uuid', + uploadState: 'finished', + }, + }, + ]); + }); + + it('should initialize with message', () => { + const message: LocalMessage = { + id: 'test-message-id', + text: '', + type: 'regular', + created_at: new Date(), + deleted_at: null, + pinned_at: null, + status: 'pending', + updated_at: new Date(), + attachments: [ + { + type: 'image', + image_url: 'test-image-url', + }, + ], + }; + const { + messageComposer: { attachmentManager }, + mockChannel, + } = setup({ composition: message }); + + expect(attachmentManager.attachments).toEqual([ + { + type: 'image', + image_url: 'test-image-url', + localMetadata: { + id: 'test-uuid', + uploadState: 'finished', + }, + }, + ]); + }); + }); + + describe('getters', () => { + it('should retrieve attachments config from composer', () => { + const config: AttachmentManagerConfig = { + doUploadRequest: () => { + return Promise.resolve({ file: 'x' }); + }, + fileUploadFilter: () => false, + maxNumberOfFilesPerMessage: 3000, + }; + const { + messageComposer: { attachmentManager }, + } = setup({ config }); + expect(attachmentManager.config).toEqual(config); + }); + + it('should return the correct values from state', async () => { + const { + messageComposer: { attachmentManager }, + } = setup(); + + // Create a test file and upload it to populate the state + const file = new File([''], 'test.jpg', { type: 'image/jpeg' }); + attachmentManager.state.next({ + attachments: [ + { + type: 'image', + image_url: 'test-image-url', + localMetadata: { + id: 'test-uuid', + uploadState: 'finished', + file, + }, + }, + ], + }); + // Now check the getters + expect(attachmentManager.attachments.length).toBe(1); + expect(attachmentManager.hasUploadPermission).toBe(true); + expect(attachmentManager.isUploadEnabled).toBe(true); + }); + + it('should return false for isUploadEnabled when uploads are disabled', () => { + const { + messageComposer: { attachmentManager }, + mockChannel, + } = setup(); + mockChannel.data = { ...mockChannel.data, own_capabilities: [] }; + // isUploadEnabled should be false when the channel doesn't have upload-file capability + expect(attachmentManager.isUploadEnabled).toBe(false); + // hasUploadPermission should also be false + expect(attachmentManager.hasUploadPermission).toBe(false); + }); + + it('should return false for isUploadEnabled when no upload slots are available', () => { + // Create a message with maximum number of attachments + const composition: DraftResponse = { + message: { + id: 'test-message-id', + text: '', + attachments: Array(API_MAX_FILES_ALLOWED_PER_MESSAGE).fill({ + type: 'image', + image_url: 'test-image-url', + }), + }, + channel_cid: 'channel-cid', + created_at: new Date().toISOString(), + }; + + // Initialize with message containing maximum attachments + const { + messageComposer: { attachmentManager }, + } = setup({ composition }); + + // Should have 0 slots available + expect(attachmentManager.availableUploadSlots).toBe(0); + + // isUploadEnabled should be false when no slots are available + expect(attachmentManager.isUploadEnabled).toBe(false); + }); + + it('should return correct upload counts', async () => { + const { + messageComposer: { attachmentManager }, + } = setup(); + + // Create test files with different states + const file1 = new File([''], 'test1.jpg', { type: 'image/jpeg' }); + const file2 = new File([''], 'test2.jpg', { type: 'image/jpeg' }); + const file3 = new File([''], 'test3.jpg', { type: 'image/jpeg' }); + const file4 = new File([''], 'test4.jpg', { type: 'image/jpeg' }); + const file5 = new File([''], 'test5.jpg', { type: 'image/jpeg' }); + + attachmentManager.state.next({ + attachments: [ + { + type: 'image', + image_url: 'test-image-url', + localMetadata: { + id: 'test-uuid', + uploadState: 'finished', + file: file1, + }, + }, + { + type: 'image', + image_url: 'test-image-url', + localMetadata: { + id: 'test-uuid', + uploadState: 'uploading', + file: file2, + }, + }, + { + type: 'image', + image_url: 'test-image-url', + localMetadata: { + id: 'test-uuid', + uploadState: 'failed', + file: file3, + }, + }, + { + type: 'image', + image_url: 'test-image-url', + localMetadata: { + id: 'test-uuid', + uploadState: 'blocked', + file: file4, + }, + }, + { + type: 'image', + image_url: 'test-image-url', + localMetadata: { + id: 'test-uuid', + uploadState: 'pending', + file: file5, + }, + }, + ], + }); + // Check the upload counts + expect(attachmentManager.successfulUploadsCount).toBeGreaterThanOrEqual(1); + expect(attachmentManager.uploadsInProgressCount).toBeGreaterThanOrEqual(1); + expect(attachmentManager.failedUploadsCount).toBeGreaterThanOrEqual(1); + expect(attachmentManager.blockedUploadsCount).toBeGreaterThanOrEqual(1); + expect(attachmentManager.pendingUploadsCount).toBeGreaterThanOrEqual(1); + }); + + it('should return correct available upload slots', async () => { + const { + messageComposer: { attachmentManager }, + } = setup(); + + // Initially should have max slots available + expect(attachmentManager.availableUploadSlots).toBe( + API_MAX_FILES_ALLOWED_PER_MESSAGE, + ); + + // Create and upload a file to reduce available slots + const file = new File([''], 'test.jpg', { type: 'image/jpeg' }); + attachmentManager.state.next({ + attachments: [ + { + type: 'image', + image_url: 'test-image-url', + localMetadata: { + id: 'test-uuid', + uploadState: 'finished', + file, + }, + }, + ], + }); + // Should have one less slot available + expect(attachmentManager.availableUploadSlots).toBe( + API_MAX_FILES_ALLOWED_PER_MESSAGE - 1, + ); + }); + + it('should calculate available upload slots based on message attachments', () => { + // Create a message with 2 attachments + const composition: DraftResponse = { + message: { + id: 'test-message-id', + text: '', + attachments: [ + { type: 'image', image_url: 'test-image-url-1' }, + { type: 'image', image_url: 'test-image-url-2' }, + ], + }, + channel_cid: 'channel-cid', + created_at: new Date().toISOString(), + }; + + // Initialize with message containing attachments + const { + messageComposer: { attachmentManager }, + } = setup({ composition }); + + // Should have max slots minus the number of attachments in the message + expect(attachmentManager.availableUploadSlots).toBe( + API_MAX_FILES_ALLOWED_PER_MESSAGE - 2, + ); + }); + + it('should take into consideration uploads in progress', () => { + const { + messageComposer: { attachmentManager }, + } = setup(); + + // Set up state with successful uploads and uploads in progress + attachmentManager.state.next({ + attachments: [ + { + type: 'image', + image_url: 'test-image-url', + localMetadata: { + id: 'test-uuid-1', + uploadState: 'finished', + file: new File([''], 'test1.jpg', { type: 'image/jpeg' }), + }, + }, + { + type: 'image', + image_url: 'test-image-url', + localMetadata: { + id: 'test-uuid-2', + uploadState: 'uploading', + file: new File([''], 'test2.jpg', { type: 'image/jpeg' }), + }, + }, + ], + }); + + // Should have max slots minus successful uploads (1) minus uploads in progress (1) + expect(attachmentManager.availableUploadSlots).toBe( + API_MAX_FILES_ALLOWED_PER_MESSAGE - 2, + ); + }); + }); + + describe('initState', () => { + it('should reset the state to initial state', () => { + const { + messageComposer: { attachmentManager }, + } = setup(); + + attachmentManager.initState(); + + expect(attachmentManager.state.getLatestValue()).toEqual({ attachments: [] }); + }); + + it('should initialize with message', () => { + const { + messageComposer: { attachmentManager }, + } = setup(); + const message = { + attachments: [{ type: 'image', image_url: 'test-url' }], + }; + + attachmentManager.initState({ message }); + + expect(attachmentManager.state.getLatestValue()).toEqual({ + attachments: [ + { + image_url: 'test-url', + localMetadata: { + id: 'test-uuid', + uploadState: 'finished', + }, + type: 'image', + }, + ], + }); + }); + }); + + describe('getAttachmentIndex', () => { + it('should return the correct index for an attachment', () => { + const { + messageComposer: { attachmentManager }, + } = setup(); + + attachmentManager.state.next({ + attachments: [ + { localMetadata: { id: 'test-id-1' } }, + { localMetadata: { id: 'test-id-2' } }, + ], + }); + + expect(attachmentManager.getAttachmentIndex('test-id-1')).toBe(0); + expect(attachmentManager.getAttachmentIndex('test-id-2')).toBe(1); + expect(attachmentManager.getAttachmentIndex('non-existent')).toBe(-1); + }); + }); + + describe('upsertAttachments', () => { + it('should add new attachments', () => { + const { + messageComposer: { attachmentManager }, + } = setup(); + + const newAttachments = [ + { localMetadata: { id: 'test-id-1' } }, + { localMetadata: { id: 'test-id-2' } }, + ]; + + attachmentManager.upsertAttachments(newAttachments); + + expect(attachmentManager.attachments).toEqual(newAttachments); + }); + + it('should update existing attachments', () => { + const { + messageComposer: { attachmentManager }, + } = setup(); + attachmentManager.upsertAttachments([ + { localMetadata: { id: 'test-id-1' }, type: 'image' }, + ]); + + const updatedAttachments = [{ localMetadata: { id: 'test-id-1' }, type: 'video' }]; + + attachmentManager.upsertAttachments(updatedAttachments); + + expect(attachmentManager.attachments).toEqual(updatedAttachments); + }); + }); + + describe('removeAttachments', () => { + it('should remove attachments by id', () => { + const { + messageComposer: { attachmentManager }, + } = setup(); + const newAttachments = [ + { localMetadata: { id: 'test-id-1' } }, + { localMetadata: { id: 'test-id-2' } }, + ]; + + attachmentManager.upsertAttachments(newAttachments); + + attachmentManager.removeAttachments(['test-id-1']); + + expect(attachmentManager.attachments).toEqual([ + { localMetadata: { id: 'test-id-2' } }, + ]); + }); + }); + + describe('getUploadConfigCheck', () => { + it('should block files with disallowed extensions', async () => { + const file = new File([''], 'test.gif', { type: 'image/gif' }); + const { + messageComposer: { attachmentManager }, + } = setup(); + const result = await attachmentManager.getUploadConfigCheck(file); + expect(result).toEqual({ + uploadBlocked: true, + reason: 'allowed_file_extensions', + }); + }); + + it('should block files with blocked extensions', async () => { + const { + messageComposer: { attachmentManager }, + } = setup({ + appSettings: { + ...defaultAppSettings.app, + image_upload_config: { + ...defaultAppSettings.app.image_upload_config, + allowed_file_extensions: ['jpg', 'png', 'gif'], + allowed_mime_types: ['image/jpeg', 'image/png', 'image/gif'], + blocked_file_extensions: ['gif'], + }, + }, + }); + + const file = new File([''], 'test.gif', { type: 'image/gif' }); + const result = await attachmentManager.getUploadConfigCheck(file); + expect(result).toEqual({ + uploadBlocked: true, + reason: 'blocked_file_extensions', + }); + }); + + it('should block files with disallowed mime types', async () => { + const file = new File([''], 'test.jpg', { type: 'image/gif' }); + const { + messageComposer: { attachmentManager }, + } = setup(); + const result = await attachmentManager.getUploadConfigCheck(file); + expect(result).toEqual({ + uploadBlocked: true, + reason: 'allowed_mime_types', + }); + }); + + it('should block files with blocked mime types', async () => { + const { + messageComposer: { attachmentManager }, + } = setup({ + appSettings: { + ...defaultAppSettings.app, + image_upload_config: { + ...defaultAppSettings.app.image_upload_config, + allowed_file_extensions: ['jpg', 'png', 'gif'], + allowed_mime_types: ['image/jpeg', 'image/png', 'image/gif'], + blocked_mime_types: ['image/gif'], + }, + }, + }); + + const file = new File([''], 'test.jpg', { type: 'image/gif' }); + const result = await attachmentManager.getUploadConfigCheck(file); + expect(result).toEqual({ + uploadBlocked: true, + reason: 'blocked_mime_types', + }); + }); + + it('should block files that exceed size limit', async () => { + const smallSizeLimit = 1000; + const { + messageComposer: { attachmentManager }, + } = setup({ + appSettings: { + ...defaultAppSettings.app, + image_upload_config: { + ...defaultAppSettings.app.image_upload_config, + size_limit: smallSizeLimit, + }, + file_upload_config: { + ...defaultAppSettings.app.file_upload_config, + size_limit: smallSizeLimit, + }, + }, + }); + + const largeContent = new ArrayBuffer(2000); + const file = new File([largeContent], 'test.jpg', { type: 'image/jpeg' }); + const result = await attachmentManager.getUploadConfigCheck(file); + expect(result).toEqual({ + uploadBlocked: true, + reason: 'size_limit', + }); + }); + + it('should block non-image files with disallowed extensions', async () => { + const { + messageComposer: { attachmentManager }, + } = setup({ + appSettings: { + ...defaultAppSettings.app, + file_upload_config: { + allowed_file_extensions: ['txt'], + allowed_mime_types: ['text/plain'], + size_limit: DEFAULT_UPLOAD_SIZE_LIMIT_BYTES, + }, + }, + }); + + const file = new File([''], 'test.exe', { type: 'text/plain' }); + const result = await attachmentManager.getUploadConfigCheck(file); + expect(result).toEqual({ + uploadBlocked: true, + reason: 'allowed_file_extensions', + }); + }); + + it('should block non-image files with blocked extensions', async () => { + const { + messageComposer: { attachmentManager }, + } = setup({ + appSettings: { + ...defaultAppSettings.app, + file_upload_config: { + allowed_file_extensions: ['txt', 'exe'], + allowed_mime_types: ['text/plain', 'application/x-msdownload'], + size_limit: DEFAULT_UPLOAD_SIZE_LIMIT_BYTES, + blocked_file_extensions: ['exe'], + }, + }, + }); + + const file = new File([''], 'test.exe', { type: 'text/plain' }); + const result = await attachmentManager.getUploadConfigCheck(file); + expect(result).toEqual({ + uploadBlocked: true, + reason: 'blocked_file_extensions', + }); + }); + + it('should block non-image files with disallowed mime types', async () => { + const { + messageComposer: { attachmentManager }, + } = setup({ + appSettings: { + ...defaultAppSettings.app, + file_upload_config: { + allowed_file_extensions: ['txt'], + allowed_mime_types: ['text/plain'], + size_limit: DEFAULT_UPLOAD_SIZE_LIMIT_BYTES, + }, + }, + }); + + const file = new File([''], 'test.txt', { type: 'application/x-msdownload' }); + const result = await attachmentManager.getUploadConfigCheck(file); + expect(result).toEqual({ + uploadBlocked: true, + reason: 'allowed_mime_types', + }); + }); + + it('should block non-image files with blocked mime types', async () => { + const { + messageComposer: { attachmentManager }, + } = setup({ + appSettings: { + ...defaultAppSettings.app, + file_upload_config: { + allowed_file_extensions: ['txt'], + allowed_mime_types: ['text/plain', 'application/x-msdownload'], + size_limit: DEFAULT_UPLOAD_SIZE_LIMIT_BYTES, + blocked_mime_types: ['application/x-msdownload'], + }, + }, + }); + + const file = new File([''], 'test.txt', { type: 'application/x-msdownload' }); + const result = await attachmentManager.getUploadConfigCheck(file); + expect(result).toEqual({ + uploadBlocked: true, + reason: 'blocked_mime_types', + }); + }); + + it('should block non-image files that exceed size limit', async () => { + const smallSizeLimit = 1000; + const { + messageComposer: { attachmentManager }, + } = setup({ + appSettings: { + ...defaultAppSettings.app, + file_upload_config: { + allowed_file_extensions: ['txt'], + allowed_mime_types: ['text/plain'], + size_limit: smallSizeLimit, + }, + }, + }); + + const largeContent = new ArrayBuffer(2000); + const file = new File([largeContent], 'test.txt', { type: 'text/plain' }); + const result = await attachmentManager.getUploadConfigCheck(file); + expect(result).toEqual({ + uploadBlocked: true, + reason: 'size_limit', + }); + }); + + it('should handle case when upload config is missing', async () => { + const { + messageComposer: { attachmentManager }, + } = setup({ appSettings: {} }); + const file = new File([''], 'test.jpg', { type: 'image/jpeg' }); + const result = await attachmentManager.getUploadConfigCheck(file); + expect(result).toEqual({ uploadBlocked: false }); + }); + + it('should handle case when only some config options are provided', async () => { + const { + messageComposer: { attachmentManager }, + } = setup({ + appSettings: { + image_upload_config: { + allowed_file_extensions: ['jpg', 'png'], + size_limit: DEFAULT_UPLOAD_SIZE_LIMIT_BYTES, + blocked_file_extensions: ['gif'], + }, + file_upload_config: { + allowed_file_extensions: ['pdf', 'doc'], + size_limit: DEFAULT_UPLOAD_SIZE_LIMIT_BYTES, + }, + }, + }); + + const blockedFile = new File([''], 'test.gif', { type: 'image/gif' }); + const blockedResult = await attachmentManager.getUploadConfigCheck(blockedFile); + expect(blockedResult).toEqual({ + uploadBlocked: true, + reason: 'allowed_file_extensions', + }); + + // Test with a file that should be allowed by extension but blocked by mime type + // This should pass because allowed_mime_types is missing + const allowedFile = new File([''], 'test.jpg', { type: 'image/gif' }); + const allowedResult = await attachmentManager.getUploadConfigCheck(allowedFile); + expect(allowedResult).toEqual({ uploadBlocked: false }); + }); + + it('should handle edge cases', async () => { + const { + messageComposer: { attachmentManager }, + } = setup(); + // Test file with no extension + const noExtFile = new File([''], 'test', { type: 'image/jpeg' }); + const noExtResult = await attachmentManager.getUploadConfigCheck(noExtFile); + expect(noExtResult).toEqual({ + uploadBlocked: true, + reason: 'allowed_file_extensions', + }); + + // Test file with no mime type - evaluated as a non-image file + const noMimeFile = new File([''], 'test.jpg', { type: '' }); + const noMimeResult = await attachmentManager.getUploadConfigCheck(noMimeFile); + expect(noMimeResult).toEqual({ + uploadBlocked: true, + reason: 'allowed_file_extensions', + }); + + // Test file with no size + const blob = new Blob([''], { type: 'image/jpeg' }); + const getUploadConfigCheck = (attachmentManager as any).getUploadConfigCheck; + const noSizeResult = await getUploadConfigCheck(blob); + expect(noSizeResult).toEqual({ uploadBlocked: false }); + }); + }); + + describe('uploadFiles', () => { + it('should upload files successfully', async () => { + const { + messageComposer: { attachmentManager }, + } = setup(); + const file = new File([''], 'test.jpg', { type: 'image/jpeg' }); + + await attachmentManager.uploadFiles([file]); + + expect(attachmentManager.successfulUploadsCount).toBe(1); + }); + + it('should handle upload failures', async () => { + const { + messageComposer: { attachmentManager }, + mockChannel, + mockClient, + } = setup(); + mockChannel.sendImage.mockRejectedValueOnce(new Error('Upload failed')); + const file = new File([''], 'test.jpg', { type: 'image/jpeg' }); + + await expect(attachmentManager.uploadFiles([file])).resolves.toEqual([ + { + fallback: 'test.jpg', + file_size: 0, + localMetadata: { + id: 'test-uuid', + file, + uploadState: 'failed', + previewUri: expect.any(String), + uploadPermissionCheck: { + uploadBlocked: false, + }, + }, + mime_type: 'image/jpeg', + type: 'image', + }, + ]); + + expect(attachmentManager.failedUploadsCount).toBe(1); + expect(mockClient.notifications.addError).toHaveBeenCalledWith({ + message: 'Upload failed', + origin: { + emitter: 'AttachmentManager', + context: { attachment: expect.any(Object) }, + }, + }); + }); + + it('should register notification for blocked file', async () => { + const { + messageComposer: { attachmentManager }, + mockClient, + } = setup(); + + // Create a blocked attachment + const blockedAttachment = { + type: 'image', + localMetadata: { + id: 'test-id', + file: new File([''], 'test.jpg', { type: 'image/jpeg' }), + }, + }; + + // Mock getUploadConfigCheck to return blocked + vi.spyOn(attachmentManager, 'getUploadConfigCheck').mockResolvedValue({ + uploadBlocked: true, + reason: 'size_limit', + }); + + await attachmentManager.uploadAttachment(blockedAttachment); + + // Verify notification was added + expect(mockClient.notifications.addError).toHaveBeenCalledWith({ + message: 'Error uploading attachment', + origin: { + emitter: 'AttachmentManager', + context: { attachment: blockedAttachment }, + }, + }); + }); + + it('should use custom upload function when provided', async () => { + const { + messageComposer: { attachmentManager }, + mockChannel, + } = setup(); + + // Create a custom upload function + const customUploadFn = vi.fn().mockResolvedValue({ file: 'custom-upload-url' }); + + // Set the custom upload function + attachmentManager.setCustomUploadFn(customUploadFn); + + // Create a file to upload + const file = new File([''], 'test.jpg', { type: 'image/jpeg' }); + + // Mock fileToLocalUploadAttachment to return a valid attachment + const attachment = { + type: 'image', + localMetadata: { + id: 'test-id', + file, + uploadState: 'pending', + }, + }; + + vi.spyOn(attachmentManager, 'ensureLocalUploadAttachment').mockResolvedValue( + attachment, + ); + + // Upload the attachment + await attachmentManager.uploadAttachment(attachment); + + // Verify the custom upload function was called + expect(customUploadFn).toHaveBeenCalledWith(file); + expect(mockChannel.sendImage).not.toHaveBeenCalled(); + }); + + it('should respect maxNumberOfFilesPerMessage', async () => { + const { + messageComposer: { attachmentManager }, + } = setup(); + const files = Array(API_MAX_FILES_ALLOWED_PER_MESSAGE + 1) + .fill(null) + .map(() => new File([''], 'test.jpg', { type: 'image/jpeg' })); + + await attachmentManager.uploadFiles(files); + + expect(attachmentManager.successfulUploadsCount).toBeLessThanOrEqual( + API_MAX_FILES_ALLOWED_PER_MESSAGE, + ); + }); + }); + + describe('ensureLocalUploadAttachment', () => { + it('should add error notification when file is missing', async () => { + const { + messageComposer: { attachmentManager }, + mockClient, + } = setup(); + // Access the private method using any type + const ensureLocalUploadAttachment = (attachmentManager as any) + .ensureLocalUploadAttachment; + + await ensureLocalUploadAttachment({ + localMetadata: { + id: 'test-id', + // Missing file property + }, + }); + + expect(mockClient.notifications.addError).toHaveBeenCalledWith({ + message: 'File is required for upload attachment', + origin: { + emitter: 'AttachmentManager', + context: { + attachment: { + localMetadata: { + id: 'test-id', + }, + }, + }, + }, + }); + }); + + it('should add error notification when id is missing', async () => { + const { + messageComposer: { attachmentManager }, + mockClient, + } = setup(); + const ensureLocalUploadAttachment = (attachmentManager as any) + .ensureLocalUploadAttachment; + + const file = new File([''], 'test.jpg', { type: 'image/jpeg' }); + await ensureLocalUploadAttachment({ + localMetadata: { + file, + // Missing id property + }, + }); + + expect(mockClient.notifications.addError).toHaveBeenCalledWith({ + message: 'File is required for upload attachment', + origin: { + emitter: 'AttachmentManager', + context: { + attachment: { + localMetadata: { + file, + }, + }, + }, + }, + }); + }); + + it('should return undefined when file is filtered out', async () => { + const { + messageComposer: { attachmentManager }, + } = setup(); + const ensureLocalUploadAttachment = (attachmentManager as any) + .ensureLocalUploadAttachment; + + // Set a fileUploadFilter that blocks all files + attachmentManager.fileUploadFilter = () => false; + + const file = new File([''], 'test.jpg', { type: 'image/jpeg' }); + const result = await ensureLocalUploadAttachment({ + localMetadata: { + id: 'test-id', + file, + }, + }); + + expect(result).toBeUndefined(); + }); + + it('should call fileToLocalUploadAttachment when file passes filter', async () => { + const { + messageComposer: { attachmentManager }, + } = setup(); + const ensureLocalUploadAttachment = (attachmentManager as any) + .ensureLocalUploadAttachment; + const fileToLocalUploadAttachment = vi.spyOn( + attachmentManager, + 'fileToLocalUploadAttachment', + ); + + // Set a fileUploadFilter that allows all files + attachmentManager.fileUploadFilter = () => true; + + const file = new File([''], 'test.jpg', { type: 'image/jpeg' }); + await ensureLocalUploadAttachment({ + localMetadata: { + id: 'test-id', + file, + }, + }); + + expect(fileToLocalUploadAttachment).toHaveBeenCalledWith(file); + }); + + it('should return the result from fileToLocalUploadAttachment', async () => { + const { + messageComposer: { attachmentManager }, + } = setup(); + const ensureLocalUploadAttachment = (attachmentManager as any) + .ensureLocalUploadAttachment; + + // Set a fileUploadFilter that allows all files + attachmentManager.fileUploadFilter = () => true; + + // Mock the fileToLocalUploadAttachment method to return a specific value + const expectedAttachment = { + type: 'image', + image_url: 'test-url', + localMetadata: { + id: 'test-id', + file: new File([''], 'test.jpg', { type: 'image/jpeg' }), + uploadState: 'finished', + }, + }; + vi.spyOn(attachmentManager, 'fileToLocalUploadAttachment').mockResolvedValue( + expectedAttachment, + ); + + const file = new File([''], 'test.jpg', { type: 'image/jpeg' }); + const result = await ensureLocalUploadAttachment({ + localMetadata: { + id: 'test-id', + file, + }, + }); + + expect(result).toEqual(expectedAttachment); + }); + + it('should preserve original ID if it exists', async () => { + const { + messageComposer: { attachmentManager }, + } = setup(); + const ensureLocalUploadAttachment = (attachmentManager as any) + .ensureLocalUploadAttachment; + + // Set a fileUploadFilter that allows all files + attachmentManager.fileUploadFilter = () => true; + + // Create an attachment with an ID + const originalId = 'original-test-id'; + const file = new File([''], 'test.jpg', { type: 'image/jpeg' }); + + // Mock fileToLocalUploadAttachment to return a new attachment + const newAttachment = { + type: 'image', + image_url: 'test-url', + localMetadata: { + id: 'new-test-id', // Different ID + file: new File([''], 'test.jpg', { type: 'image/jpeg' }), + uploadState: 'finished', + }, + }; + + vi.spyOn(attachmentManager, 'fileToLocalUploadAttachment').mockResolvedValue( + newAttachment, + ); + + // Call with original ID + const result = await ensureLocalUploadAttachment({ + localMetadata: { + id: originalId, + file, + }, + }); + + // Verify the original ID was preserved + expect(result.localMetadata.id).toBe(originalId); + }); + }); +}); diff --git a/test/unit/MessageComposer/fileUtils.test.ts b/test/unit/MessageComposer/fileUtils.test.ts new file mode 100644 index 0000000000..c5e05f77bb --- /dev/null +++ b/test/unit/MessageComposer/fileUtils.test.ts @@ -0,0 +1,342 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { + createFileFromBlobs, + ensureIsLocalAttachment, + generateFileName, + getAttachmentTypeFromMimeType, + getExtensionFromMimeType, + isBlobButNotFile, + isFile, + isFileList, + isImageFile, + isFileReference, + readFileAsArrayBuffer, +} from '../../../src/messageComposer/fileUtils'; +import type { LocalAttachment } from '../../../src/messageComposer/types'; +import { generateUUIDv4 } from '../../../src/utils'; +import type { Attachment } from '../../../src/types'; + +const generateUUIDv4Output = 'generated-id'; + +// Mock dependencies +vi.mock('../../../src/utils', () => ({ + generateUUIDv4: vi.fn(() => generateUUIDv4Output), +})); + +// Mock DOM types +class MockFileList { + private _files: File[] = []; + + constructor(files: File[]) { + this._files = files; + } + + item(index: number) { + return this._files[index]; + } + + get length() { + return this._files.length; + } +} + +class MockDataTransfer { + private _files: File[] = []; + + constructor(files: File[]) { + this._files = files; + } + + get items() { + return this._files.map((file) => ({ + kind: 'file', + type: file.type, + getAsFile: () => file, + })); + } + + get files() { + return new MockFileList(this._files); + } +} + +// Mock FileReader +class MockFileReader { + private _result: ArrayBuffer | null = null; + static _error: Error | null = null; + onload: (() => void) | null = null; + onerror: (() => void) | null = null; + + readAsArrayBuffer(blob: Blob) { + if (MockFileReader._error) { + setTimeout(() => this.onerror?.(), 0); + return; + } + + // Simulate successful read + this._result = new ArrayBuffer(blob.size); + setTimeout(() => this.onload?.(), 0); + } + + get error() { + return MockFileReader._error; + } + + get result() { + return this._result; + } + + static setError(error: Error | null) { + this._error = error; + } +} + +// Add to global scope +Object.defineProperty(global, 'FileList', { + value: MockFileList, +}); + +Object.defineProperty(global, 'DataTransfer', { + value: MockDataTransfer, +}); + +Object.defineProperty(global, 'FileReader', { + value: MockFileReader, +}); + +describe('fileUtils', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + describe('isFile', () => { + it('should return true for File objects', () => { + const file = new File([''], 'test.txt', { type: 'text/plain' }); + expect(isFile(file)).toBe(true); + }); + + it('should return false for Blob objects', () => { + const blob = new Blob([''], { type: 'text/plain' }); + expect(isFile(blob)).toBe(false); + }); + + it('should return false for RNFile objects', () => { + const rnFile = { + name: 'test.txt', + uri: 'file://test.txt', + size: 0, + type: 'text/plain', + }; + expect(isFile(rnFile)).toBe(false); + }); + }); + + describe('isFileList', () => { + it('should return true for FileList objects', () => { + const fileList = new DataTransfer().files; + expect(isFileList(fileList)).toBe(true); + }); + + it('should return false for null or undefined', () => { + expect(isFileList(null)).toBe(false); + expect(isFileList(undefined)).toBe(false); + }); + + it('should return false for scalar values', () => { + expect(isFileList('string')).toBe(false); + expect(isFileList(123)).toBe(false); + expect(isFileList(true)).toBe(false); + }); + + it('should return false for arrays', () => { + expect(isFileList([])).toBe(false); + }); + }); + + describe('isBlobButNotFile', () => { + it('should return true for Blob objects that are not Files', () => { + const blob = new Blob([''], { type: 'text/plain' }); + expect(isBlobButNotFile(blob)).toBe(true); + }); + + it('should return false for File objects', () => { + const file = new File([''], 'test.txt', { type: 'text/plain' }); + expect(isBlobButNotFile(file)).toBe(false); + }); + + it('should return false for non-Blob objects', () => { + expect(isBlobButNotFile({})).toBe(false); + expect(isBlobButNotFile('string')).toBe(false); + }); + }); + + describe('isFileReference', () => { + it('should return true for RNFile objects', () => { + const rnFile = { + name: 'test.txt', + uri: 'file://test.txt', + size: 0, + type: 'text/plain', + }; + expect(isFileReference(rnFile)).toBe(true); + }); + + it('should return false for File objects', () => { + const file = new File([''], 'test.txt', { type: 'text/plain' }); + expect(isFileReference(file)).toBe(false); + }); + + it('should return false for Blob objects', () => { + const blob = new Blob([''], { type: 'text/plain' }); + expect(isFileReference(blob)).toBe(false); + }); + + it('should return false for incomplete RNFile objects', () => { + // These objects are missing required properties for RNFile + expect( + isFileReference({ name: 'test.txt', type: 'text/plain', size: 0 } as any), + ).toBe(false); + expect( + isFileReference({ uri: 'file://test.txt', type: 'text/plain', size: 0 } as any), + ).toBe(false); + expect( + isFileReference({ + name: 'test.txt', + uri: 'file://test.txt', + type: 'text/plain', + } as any), + ).toBe(false); + expect( + isFileReference({ name: 'test.txt', uri: 'file://test.txt', size: 0 } as any), + ).toBe(false); + }); + }); + + describe('createFileFromBlobs', () => { + it('should create a File from an array of Blobs', () => { + const blob1 = new Blob(['part1'], { type: 'text/plain' }); + const blob2 = new Blob(['part2'], { type: 'text/plain' }); + const fileName = 'test.txt'; + const mimeType = 'text/plain'; + + const file = createFileFromBlobs({ + blobsArray: [blob1, blob2], + fileName, + mimeType, + }); + + expect(file).toBeInstanceOf(File); + expect(file.name).toBe(fileName); + expect(file.type).toBe(mimeType); + }); + }); + + describe('getExtensionFromMimeType', () => { + it('should extract the extension from a MIME type', () => { + expect(getExtensionFromMimeType('text/plain')).toBe('plain'); + expect(getExtensionFromMimeType('image/jpeg')).toBe('jpeg'); + expect(getExtensionFromMimeType('application/pdf')).toBe('pdf'); + }); + + it('should return undefined for invalid MIME types', () => { + expect(getExtensionFromMimeType('invalid')).toBeUndefined(); + expect(getExtensionFromMimeType('')).toBeUndefined(); + }); + }); + + describe('readFileAsArrayBuffer', () => { + it('should read file as array buffer', async () => { + const file = new File(['test'], 'test.txt', { type: 'text/plain' }); + const buffer = await readFileAsArrayBuffer(file); + expect(buffer).toBeInstanceOf(ArrayBuffer); + expect(buffer.byteLength).toBe(4); // 'test' is 4 bytes + }); + + it('should reject on read error', async () => { + const file = new File(['test'], 'test.txt', { type: 'text/plain' }); + MockFileReader.setError(new Error('Read failed')); + + await expect(readFileAsArrayBuffer(file)).rejects.toThrow('Read failed'); + MockFileReader.setError(null); + }); + }); + + describe('generateFileName', () => { + it('should generate a file name with the correct extension', () => { + const mimeType = 'image/jpeg'; + const fileName = generateFileName(mimeType); + expect(fileName).toMatch(/^file_\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); // file_ followed by date ISO string + expect(fileName).toMatch(/\.jpeg$/); + }); + }); + + describe('isImageFile', () => { + it('should return true for image files', () => { + const file = new File([''], 'test.jpg', { type: 'image/jpeg' }); + expect(isImageFile(file)).toBe(true); + }); + + it('should return false for non-image files', () => { + const file = new File([''], 'test.txt', { type: 'text/plain' }); + expect(isImageFile(file)).toBe(false); + }); + + it('should return false for Photoshop files', () => { + const file = new File([''], 'test.psd', { type: 'image/vnd.adobe.photoshop' }); + expect(isImageFile(file)).toBe(false); + }); + }); + + describe('getAttachmentTypeFromMimeType', () => { + it('should return the correct attachment type for different MIME types', () => { + expect(getAttachmentTypeFromMimeType('image/jpeg')).toBe('image'); + expect(getAttachmentTypeFromMimeType('video/mp4')).toBe('video'); + expect(getAttachmentTypeFromMimeType('audio/mp3')).toBe('audio'); + expect(getAttachmentTypeFromMimeType('application/pdf')).toBe('file'); + }); + }); + + describe('ensureIsLocalAttachment', () => { + beforeEach(() => { + vi.mocked(generateUUIDv4).mockClear(); + }); + + it('should return attachment if already local', () => { + const localAttachment: LocalAttachment = { + type: 'file', + localMetadata: { + id: 'local-id', + file: new File([''], 'test.txt'), + uploadState: 'pending', + }, + }; + const result = ensureIsLocalAttachment(localAttachment); + expect(result).toBe(localAttachment); + }); + + it('should add local properties to non-local attachment', () => { + const attachment: Attachment = { + type: 'file', + asset_url: 'https://example.com/file.txt', + }; + const result = ensureIsLocalAttachment(attachment); + expect(result).toEqual({ + ...attachment, + localMetadata: { + id: generateUUIDv4Output, + }, + }); + expect(generateUUIDv4).toHaveBeenCalled(); + }); + + it('should handle undefined attachment', () => { + const result = ensureIsLocalAttachment(undefined as unknown as Attachment); + expect(result).toEqual(null); + expect(generateUUIDv4).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/test/unit/MessageComposer/linkPreviewsManager.test.ts b/test/unit/MessageComposer/linkPreviewsManager.test.ts new file mode 100644 index 0000000000..4ddef2caa3 --- /dev/null +++ b/test/unit/MessageComposer/linkPreviewsManager.test.ts @@ -0,0 +1,677 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { LinkPreviewStatus } from '../../../src/messageComposer/linkPreviewsManager'; +import { DraftMessage, LocalMessage } from '../../../src/types'; +import { + DEFAULT_LINK_PREVIEW_MANAGER_CONFIG, + DraftResponse, + LinkPreviewsManagerConfig, + MessageComposer, + MessageComposerConfig, + StreamChat, +} from '../../../src'; +import { DeepPartial } from '../../../src/types.utility'; +import { mergeWith } from '../../../src/utils/mergeWith'; + +const existingLinkUrl = 'https://existing.com'; +const linkUrl = 'https://example.com'; +// Mock dependencies +vi.mock('../../src/store', () => ({ + StateStore: vi.fn().mockImplementation(() => ({ + getLatestValue: vi.fn().mockReturnValue({}), + next: vi.fn(), + partialNext: vi.fn(), + })), +})); + +vi.mock('../../src/utils', () => ({ + debounce: vi.fn().mockImplementation((fn) => { + const debouncedFn = vi.fn(fn); + debouncedFn.cancel = vi.fn(); + debouncedFn.flush = vi.fn(); + return debouncedFn; + }), +})); + +vi.mock('../../src/utils/mergeWith', () => ({ + mergeWith: vi.fn().mockImplementation((target, source) => ({ ...target, ...source })), +})); + +vi.mock('linkifyjs', () => ({ + find: vi.fn().mockImplementation((text) => { + if (text.includes(linkUrl) && text.includes(existingLinkUrl)) { + return [ + { isLink: true, href: linkUrl }, + { isLink: true, href: existingLinkUrl }, + ]; + } else if (text.includes(linkUrl)) { + return [{ isLink: true, href: linkUrl }]; + } else if (text.includes(existingLinkUrl)) { + return [{ isLink: true, href: existingLinkUrl }]; + } + return []; + }), +})); + +const enrichURLReturnValue = { + og_scrape_url: linkUrl, + title: 'Example Title', + text: 'Example Text', + image_url: 'https://example.com/image.jpg', + asset_url: 'https://example.com/asset', + author_name: 'Example Author', + author_link: 'https://example.com/author', + thumb_url: 'https://example.com/thumb.jpg', + title_link: 'https://example.com/title', + type: 'link', + duration: 1000, +}; + +const DEFAULT_CONFIG: DeepPartial = { + linkPreviews: { + debounceURLEnrichmentMs: 0, + enabled: true, + }, +}; + +const setup = ({ + composition, + config, +}: { + composition?: DraftResponse | LocalMessage; + config?: Partial | null; + message?: DraftMessage | LocalMessage; +} = {}) => { + // Reset mocks + vi.clearAllMocks(); + + // Setup mocks + const mockClient = new StreamChat('apiKey', 'apiSecret'); + mockClient.enrichURL = vi.fn().mockResolvedValue(enrichURLReturnValue); + + const mockChannel = mockClient.channel('channelType', 'channelId'); + mockChannel.getConfig = vi.fn().mockImplementation(() => ({ url_enrichment: true })); + const messageComposer = new MessageComposer({ + client: mockClient, + composition, + compositionContext: mockChannel, + config: config === null ? {} : mergeWith(DEFAULT_CONFIG, { linkPreviews: config }), + }); + return { mockClient, mockChannel, messageComposer }; +}; + +describe('LinkPreviewsManager', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('constructor', () => { + it('should initialize with default config', () => { + const { + messageComposer: { linkPreviewsManager }, + } = setup({ config: null }); + expect(linkPreviewsManager.config.enabled).toBe(true); + expect(linkPreviewsManager.config.debounceURLEnrichmentMs).toBe( + DEFAULT_LINK_PREVIEW_MANAGER_CONFIG.debounceURLEnrichmentMs, + ); + }); + + it('should initialize with custom config', () => { + const { + messageComposer: { linkPreviewsManager }, + } = setup({ + config: { + debounceURLEnrichmentMs: 500, + enabled: false, + }, + }); + expect(linkPreviewsManager.config.enabled).toBe(false); + expect(linkPreviewsManager.config.debounceURLEnrichmentMs).toBe(500); + }); + + it('should initialize with message containing link previews', () => { + const composition: LocalMessage = { + id: 'test-message-id', + text: '', + type: 'regular', + created_at: new Date(), + deleted_at: null, + pinned_at: null, + status: 'pending', + updated_at: new Date(), + attachments: [ + { + og_scrape_url: linkUrl, + title: 'Example Title', + type: 'link', + }, + ], + }; + + const { + messageComposer: { linkPreviewsManager }, + } = setup({ composition }); + + expect(linkPreviewsManager.previews.size).toBe(1); + expect(linkPreviewsManager.previews.get(linkUrl)).toBeDefined(); + }); + + it('should not initialize with message containing link previews if disabled', () => { + const composition: LocalMessage = { + id: 'test-message-id', + text: '', + type: 'regular', + created_at: new Date(), + deleted_at: null, + pinned_at: null, + status: 'pending', + updated_at: new Date(), + attachments: [ + { + og_scrape_url: linkUrl, + title: 'Example Title', + type: 'link', + }, + ], + }; + + const { + messageComposer: { linkPreviewsManager }, + } = setup({ composition, config: { enabled: false } }); + + expect(linkPreviewsManager.previews.size).toBe(0); + }); + }); + + describe('getters', () => { + it('should return loadingPreviews correctly', async () => { + const { + messageComposer: { linkPreviewsManager }, + mockClient, + } = setup(); + + // Mock the enrichURL to never resolve + mockClient.enrichURL = vi.fn().mockImplementation(() => new Promise(() => {})); + + // Add a loading preview + linkPreviewsManager.findAndEnrichUrls('Check out https://example.com'); + + // Wait for the debounced function to be called + await new Promise((resolve) => setTimeout(resolve, 0)); + + // Check that loadingPreviews contains the preview + expect(linkPreviewsManager.loadingPreviews.length).toBe(1); + expect(linkPreviewsManager.loadingPreviews[0].og_scrape_url).toBe(linkUrl); + expect(linkPreviewsManager.loadingPreviews[0].status).toBe( + LinkPreviewStatus.LOADING, + ); + }); + + it('should return loadedPreviews correctly', async () => { + const { + messageComposer: { linkPreviewsManager }, + } = setup(); + + // Add a loaded preview + linkPreviewsManager.findAndEnrichUrls('Check out https://example.com'); + + // Wait for the debounced function to be called and the promise to resolve + await new Promise((resolve) => setTimeout(resolve, 0)); + + // Check that loadedPreviews contains the preview + expect(linkPreviewsManager.loadedPreviews.length).toBe(1); + expect(linkPreviewsManager.loadedPreviews[0].og_scrape_url).toBe(linkUrl); + expect(linkPreviewsManager.loadedPreviews[0].status).toBe(LinkPreviewStatus.LOADED); + }); + + it('should return dismissedPreviews correctly', () => { + const { + messageComposer: { linkPreviewsManager }, + } = setup(); + + // Add the preview to the manager's previews + const newPreviews = new Map(linkPreviewsManager.previews); + newPreviews.set(linkUrl, { + og_scrape_url: linkUrl, + status: LinkPreviewStatus.DISMISSED, + }); + linkPreviewsManager.state.partialNext({ previews: newPreviews }); + + // Check that dismissedPreviews contains the preview + expect(linkPreviewsManager.dismissedPreviews.length).toBe(1); + expect(linkPreviewsManager.dismissedPreviews[0].og_scrape_url).toBe(linkUrl); + expect(linkPreviewsManager.dismissedPreviews[0].status).toBe( + LinkPreviewStatus.DISMISSED, + ); + }); + + it('should return failedPreviews correctly', () => { + const { + messageComposer: { linkPreviewsManager }, + } = setup(); + + const newPreviews = new Map(linkPreviewsManager.previews); + newPreviews.set(linkUrl, { + og_scrape_url: linkUrl, + status: LinkPreviewStatus.FAILED, + }); + linkPreviewsManager.state.partialNext({ previews: newPreviews }); + + // Check that failedPreviews contains the preview + expect(linkPreviewsManager.failedPreviews.length).toBe(1); + expect(linkPreviewsManager.failedPreviews[0].og_scrape_url).toBe(linkUrl); + expect(linkPreviewsManager.failedPreviews[0].status).toBe(LinkPreviewStatus.FAILED); + }); + + it('should return pendingPreviews correctly', () => { + const { + messageComposer: { linkPreviewsManager }, + } = setup(); + + const newPreviews = new Map(linkPreviewsManager.previews); + newPreviews.set(linkUrl, { + og_scrape_url: linkUrl, + status: LinkPreviewStatus.PENDING, + }); + linkPreviewsManager.state.partialNext({ previews: newPreviews }); + + // Check that pendingPreviews contains the preview + expect(linkPreviewsManager.pendingPreviews.length).toBe(1); + expect(linkPreviewsManager.pendingPreviews[0].og_scrape_url).toBe(linkUrl); + expect(linkPreviewsManager.pendingPreviews[0].status).toBe( + LinkPreviewStatus.PENDING, + ); + }); + }); + + describe('config setters', () => { + it('should update debounceURLEnrichmentMs correctly', () => { + const { + messageComposer: { linkPreviewsManager }, + } = setup(); + + // Update the debounce time + linkPreviewsManager.debounceURLEnrichmentMs = 2000; + + // Check that the config was updated + expect(linkPreviewsManager.config.debounceURLEnrichmentMs).toBe(2000); + }); + + it('should update enabled correctly', () => { + const { + messageComposer: { linkPreviewsManager }, + } = setup(); + + // Update enabled + linkPreviewsManager.enabled = false; + + // Check that the config was updated + expect(linkPreviewsManager.config.enabled).toBe(false); + }); + + it('should update findURLFn correctly', () => { + const { + messageComposer: { linkPreviewsManager }, + } = setup(); + + // Create a custom findURLFn + const customFindURLFn = (text: string) => { + if (text.includes('custom')) { + return ['https://custom-url.com']; + } + return []; + }; + + // Update findURLFn + linkPreviewsManager.findURLFn = customFindURLFn; + + // Check that the config was updated + expect(linkPreviewsManager.config.findURLFn).toBe(customFindURLFn); + }); + }); + + describe('initState', () => { + it('should initialize state with a new message', () => { + const { + messageComposer: { linkPreviewsManager }, + } = setup(); + + // Create a new message + const newMessage = { + attachments: [ + { + og_scrape_url: 'https://new-url.com', + title: 'New Title', + type: 'link', + }, + ], + }; + + // Initialize state with the new message + linkPreviewsManager.initState({ message: newMessage }); + + // Check that the state was updated + expect(linkPreviewsManager.previews.size).toBe(1); + expect(linkPreviewsManager.previews.get('https://new-url.com')).toBeDefined(); + }); + + it('should initialize state with an empty message', () => { + const { + messageComposer: { linkPreviewsManager }, + } = setup(); + + // Initialize state with an empty message + linkPreviewsManager.initState({ message: {} }); + + // Check that the state was updated + expect(linkPreviewsManager.previews.size).toBe(0); + }); + + it('should initialize state with no message', () => { + const { + messageComposer: { linkPreviewsManager }, + } = setup(); + + // Initialize state with no message + linkPreviewsManager.initState(); + + // Check that the state was updated + expect(linkPreviewsManager.previews.size).toBe(0); + }); + }); + + describe('findAndEnrichUrls', () => { + it('should not process URLs if disabled back-end url_enrichment', async () => { + const { + messageComposer: { linkPreviewsManager }, + mockChannel, + mockClient, + } = setup(); + mockChannel.getConfig.mockReturnValueOnce({ url_enrichment: false }); + linkPreviewsManager.findAndEnrichUrls('Check out https://example.com'); + let enrichPromiseResolve; + mockClient.enrichURL = vi.fn().mockImplementation(() => { + return new Promise((resolve) => { + enrichPromiseResolve = resolve; + }); + }); + linkPreviewsManager.findAndEnrichUrls('Check out https://example.com'); + // Wait for the debounced function to be called + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(mockClient.enrichURL).not.toHaveBeenCalled(); + expect(linkPreviewsManager.previews.size).toBe(0); + }); + + it('should not process URLs if disabled via the manager config', async () => { + const { + messageComposer: { linkPreviewsManager }, + mockClient, + } = setup({ config: { enabled: false } }); + linkPreviewsManager.findAndEnrichUrls('Check out https://example.com'); + let enrichPromiseResolve; + mockClient.enrichURL = vi.fn().mockImplementation(() => { + return new Promise((resolve) => { + enrichPromiseResolve = resolve; + }); + }); + linkPreviewsManager.findAndEnrichUrls('Check out https://example.com'); + // Wait for the debounced function to be called + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(mockClient.enrichURL).not.toHaveBeenCalled(); + expect(linkPreviewsManager.previews.size).toBe(0); + }); + + it('should process URLs and create link previews', async () => { + const { + messageComposer: { linkPreviewsManager }, + mockClient, + } = setup(); + let enrichPromiseResolve; + mockClient.enrichURL = vi.fn().mockImplementation(() => { + return new Promise((resolve) => { + enrichPromiseResolve = resolve; + }); + }); + linkPreviewsManager.findAndEnrichUrls('Check out https://example.com'); + // Wait for the debounced function to be called + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(mockClient.enrichURL).toHaveBeenCalledWith(linkUrl); + expect(linkPreviewsManager.previews.size).toBe(1); + + const preview = linkPreviewsManager.previews.get(linkUrl); + expect(preview).toBeDefined(); + expect(preview?.status).toBe(LinkPreviewStatus.LOADING); + }); + + it('should update link preview status to LOADED when enrichment succeeds', async () => { + const { + messageComposer: { linkPreviewsManager }, + } = setup(); + linkPreviewsManager.findAndEnrichUrls('Check out https://example.com'); + + // Wait for the debounced function to be called and the promise to resolve + await new Promise((resolve) => setTimeout(resolve, 0)); + + const preview = linkPreviewsManager.previews.get(linkUrl); + expect(preview?.status).toBe(LinkPreviewStatus.LOADED); + expect(preview?.title).toBe('Example Title'); + }); + + it('should update link preview status to FAILED when enrichment fails', async () => { + const { + messageComposer: { linkPreviewsManager }, + mockClient, + } = setup(); + mockClient.enrichURL.mockRejectedValueOnce(new Error('Enrichment failed')); + + linkPreviewsManager.findAndEnrichUrls('Check out https://example.com'); + + // Wait for the debounced function to be called and the promise to resolve + await new Promise((resolve) => setTimeout(resolve, 0)); + + const preview = linkPreviewsManager.previews.get(linkUrl); + expect(preview?.status).toBe(LinkPreviewStatus.FAILED); + }); + + it('should not create duplicate link previews for the same URL', async () => { + const { + messageComposer: { linkPreviewsManager }, + mockClient, + } = setup(); + linkPreviewsManager.findAndEnrichUrls('Check out https://example.com'); + + // Wait for the debounced function to be called + await new Promise((resolve) => setTimeout(resolve, 0)); + + linkPreviewsManager.findAndEnrichUrls('Check out https://example.com again'); + + // Wait for the debounced function to be called + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(mockClient.enrichURL).toHaveBeenCalledTimes(1); + expect(linkPreviewsManager.previews.size).toBe(1); + }); + + it('should not keep existing link previews if source string does not include them anymore', async () => { + const { + messageComposer: { linkPreviewsManager }, + } = setup(); + const existingPreview = { + og_scrape_url: 'https://existing.com ', + status: LinkPreviewStatus.LOADED, + }; + linkPreviewsManager.state.partialNext({ + previews: new Map([[existingPreview.og_scrape_url, existingPreview]]), + }); + + linkPreviewsManager.findAndEnrichUrls('Check out https://example.com'); + + // Wait for the debounced function to be called + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(linkPreviewsManager.previews.size).toBe(1); + expect(linkPreviewsManager.previews.get(existingPreview.og_scrape_url)?.status) + .toBeUndefined; + expect(linkPreviewsManager.previews.get(linkUrl)?.status).toBe( + LinkPreviewStatus.LOADED, + ); + }); + + it('should keep existing link previews', async () => { + const { + messageComposer: { linkPreviewsManager }, + } = setup(); + const existingPreview = { + og_scrape_url: 'https://existing.com', + status: LinkPreviewStatus.LOADED, + }; + linkPreviewsManager.state.partialNext({ + previews: new Map([[existingPreview.og_scrape_url, existingPreview]]), + }); + + linkPreviewsManager.findAndEnrichUrls( + 'Check out https://example.com and https://existing.com', + ); + + // Wait for the debounced function to be called + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(linkPreviewsManager.previews.size).toBe(2); + expect( + linkPreviewsManager.previews.get(existingPreview.og_scrape_url)?.status, + ).toBe(LinkPreviewStatus.LOADED); + expect(linkPreviewsManager.previews.get(linkUrl)?.status).toBe( + LinkPreviewStatus.LOADED, + ); + }); + }); + + describe('dismissPreview', () => { + it('should update the status of a preview when it is dismissed', async () => { + const { + messageComposer: { linkPreviewsManager }, + } = setup(); + + linkPreviewsManager.state.partialNext({ + previews: new Map([ + [linkUrl, { og_scrape_url: linkUrl, status: LinkPreviewStatus.LOADED }], + ]), + }); + + linkPreviewsManager.dismissPreview(linkUrl); + + expect(linkPreviewsManager.previews.get(linkUrl)?.status).toBe( + LinkPreviewStatus.DISMISSED, + ); + }); + + it('should call onLinkPreviewDismissed when a preview is dismissed', async () => { + const { + messageComposer: { linkPreviewsManager }, + } = setup(); + linkPreviewsManager.state.partialNext({ + previews: new Map([ + [linkUrl, { og_scrape_url: linkUrl, status: LinkPreviewStatus.LOADED }], + ]), + }); + const onLinkPreviewDismissed = vi.fn(); + linkPreviewsManager.onLinkPreviewDismissed = onLinkPreviewDismissed; + + linkPreviewsManager.dismissPreview(linkUrl); + const preview = linkPreviewsManager.previews.get(linkUrl); + expect(onLinkPreviewDismissed).toHaveBeenCalledWith({ + ...preview, + status: LinkPreviewStatus.LOADED, + }); + }); + }); + + describe('updatePreview', () => { + it('should set status to PENDING when a status is not available during preview update', () => { + const { + messageComposer: { linkPreviewsManager }, + } = setup(); + + linkPreviewsManager.state.partialNext({ + previews: new Map([[linkUrl, { og_scrape_url: linkUrl }]]), + }); + + linkPreviewsManager.updatePreview(linkUrl, { og_scrape_url: linkUrl }); + + expect(linkPreviewsManager.previews.get(linkUrl)?.status).toBe( + LinkPreviewStatus.PENDING, + ); + }); + + it('should partially update the preview', () => { + const { + messageComposer: { linkPreviewsManager }, + } = setup(); + linkPreviewsManager.state.partialNext({ + previews: new Map([ + [ + linkUrl, + { + og_scrape_url: linkUrl, + status: LinkPreviewStatus.PENDING, + title: 'Example Title', + }, + ], + ]), + }); + + linkPreviewsManager.updatePreview(linkUrl, { + title: 'New Title', + status: LinkPreviewStatus.LOADED, + }); + + expect(linkPreviewsManager.previews.get(linkUrl)?.og_scrape_url).toBe(linkUrl); + expect(linkPreviewsManager.previews.get(linkUrl)?.title).toBe('New Title'); + expect(linkPreviewsManager.previews.get(linkUrl)?.status).toBe( + LinkPreviewStatus.LOADED, + ); + }); + }); + + describe('cancelURLEnrichment', () => { + it('should cancel pending URL enrichment', () => { + const { + messageComposer: { linkPreviewsManager }, + } = setup(); + const cancelSpy = vi.spyOn(linkPreviewsManager.findAndEnrichUrls, 'cancel'); + const flushSpy = vi.spyOn(linkPreviewsManager.findAndEnrichUrls, 'flush'); + + linkPreviewsManager.cancelURLEnrichment(); + + expect(cancelSpy).toHaveBeenCalled(); + expect(flushSpy).toHaveBeenCalled(); + }); + }); + + describe('clearPreviews', () => { + it('clears all non-dismissed previews', () => { + const { + messageComposer: { linkPreviewsManager }, + } = setup(); + linkPreviewsManager.state.partialNext({ + previews: new Map([ + [linkUrl, { og_scrape_url: linkUrl, status: LinkPreviewStatus.LOADED }], + [ + 'https://exampleX.com', + { + og_scrape_url: 'https://exampleX.com', + status: LinkPreviewStatus.DISMISSED, + }, + ], + ]), + }); + + linkPreviewsManager.clearPreviews(); + + expect(linkPreviewsManager.previews.get(linkUrl)?.status).toBeUndefined(); + expect(linkPreviewsManager.previews.get('https://exampleX.com')?.status).toBe( + LinkPreviewStatus.DISMISSED, + ); + }); + }); +}); diff --git a/test/unit/MessageComposer/messageComposer.test.ts b/test/unit/MessageComposer/messageComposer.test.ts new file mode 100644 index 0000000000..bf1419532b --- /dev/null +++ b/test/unit/MessageComposer/messageComposer.test.ts @@ -0,0 +1,1045 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { + Channel, + LocalMessage, + MessageComposerConfig, + StreamChat, + Thread, +} from '../../../src'; +import { DeepPartial } from '../../../src/types.utility'; +import { MessageComposer } from '../../../src/messageComposer/messageComposer'; +import { StateStore } from '../../../src/store'; +import { DraftResponse, MessageResponse } from '../../../src/types'; + +const generateUuidV4Output = 'test-uuid'; +// Mock dependencies +vi.mock('../../../src/utils', () => ({ + axiosParamsSerializer: vi.fn(), + isFunction: vi.fn(), + isString: vi.fn(), + isObject: vi.fn(), + isArray: vi.fn(), + isDate: vi.fn(), + isNumber: vi.fn(), + debounce: vi.fn().mockImplementation((fn) => fn), + generateUUIDv4: vi.fn().mockReturnValue('test-uuid'), + isLocalMessage: vi.fn().mockReturnValue(true), + formatMessage: vi.fn().mockImplementation((msg) => msg), + randomId: vi.fn().mockReturnValue('test-uuid'), + throttle: vi.fn().mockImplementation((fn) => fn), +})); + +vi.mock('../../../src/messageComposer/attachmentManager', () => ({ + AttachmentManager: vi.fn().mockImplementation(() => ({ + state: new StateStore({ attachments: [] }), + initState: vi.fn(), + clear: vi.fn(), + attachments: [], + })), +})); + +vi.mock('../../../src/messageComposer/pollComposer', () => ({ + PollComposer: vi.fn().mockImplementation(() => ({ + state: new StateStore({ poll: null }), + initState: vi.fn(), + clear: vi.fn(), + compose: vi.fn(), + })), +})); + +vi.mock('../../../src/messageComposer/middleware/messageComposer', () => ({ + MessageComposerMiddlewareExecutor: vi.fn().mockImplementation(() => ({ + execute: vi.fn().mockResolvedValue({ state: {} }), + })), + MessageDraftComposerMiddlewareExecutor: vi.fn().mockImplementation(() => ({ + execute: vi.fn().mockResolvedValue({ state: {} }), + })), +})); + +const quotedMessage = { + id: 'quoted-message-id', + type: 'regular' as const, + created_at: new Date(), + deleted_at: null, + pinned_at: null, + updated_at: new Date(), + status: 'received', + text: 'Quoted message', + user: { id: 'user-id', name: 'User Name' }, +}; + +const user = { id: 'user-id', name: 'User Name' }; + +const getThread = (channel: Channel, client: StreamChat, threadId: string) => + new Thread({ + client, + threadData: { + parent_message_id: threadId, + parent_message: { + id: threadId, + text: 'Test message', + type: 'regular' as const, + user, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }, + channel: { + id: channel.id, + type: channel.type, + cid: channel.cid, + disabled: false, + frozen: false, + }, + title: 'Test Thread', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + channel_cid: channel.cid, + latest_replies: [], + thread_participants: [], + created_by_user_id: user.id, + }, + }); + +const setup = ({ + composition, + compositionContext, + config, +}: { + composition?: LocalMessage | DraftResponse | MessageResponse | undefined; + compositionContext?: Channel | Thread | LocalMessage | undefined; + config?: DeepPartial; +} = {}) => { + const mockClient = new StreamChat('test-api-key'); + mockClient.user = user; + mockClient.userID = user.id; + // Create a proper Channel instance with only the necessary attributes mocked + const mockChannel = new Channel(mockClient, 'messaging', 'test-channel-id', { + id: 'test-channel-id', + type: 'messaging', + cid: 'messaging:test-channel-id', + }); + + // Mock the getClient method + vi.spyOn(mockChannel, 'getClient').mockReturnValue(mockClient); + + const messageComposer = new MessageComposer({ + client: mockClient, + compositionContext: compositionContext || mockChannel, + composition, + config, + }); + + return { mockClient, mockChannel, messageComposer }; +}; + +describe('MessageComposer', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('constructor', () => { + it('should initialize with default values', () => { + const { messageComposer, mockChannel } = setup(); + expect(messageComposer).toBeDefined(); + expect(messageComposer.channel).toBe(mockChannel); + expect(messageComposer.config).toBeDefined(); + expect(messageComposer.attachmentManager).toBeDefined(); + expect(messageComposer.linkPreviewsManager).toBeDefined(); + expect(messageComposer.textComposer).toBeDefined(); + expect(messageComposer.pollComposer).toBeDefined(); + expect(messageComposer.customDataManager).toBeDefined(); + }); + + it('should initialize with custom config', () => { + const customConfig = { + publishTypingEvents: false, + text: { + maxLengthOnEdit: 1000, + }, + }; + + const { messageComposer } = setup({ config: customConfig }); + + expect(messageComposer.config.publishTypingEvents).toBe(false); + expect(messageComposer.config.text?.maxLengthOnEdit).toBe(1000); + }); + + it('should initialize with message', () => { + const message = { + id: 'test-message-id', + text: 'Hello world', + attachments: [], + mentioned_users: [], + }; + + const { messageComposer } = setup({ composition: message }); + + expect(messageComposer.editedMessage).toBeDefined(); + expect(messageComposer.id).toBe('test-message-id'); + }); + + it('should initialize with draft message', () => { + const draftMessage: DraftResponse = { + message: { + id: 'test-draft-id', + text: 'Draft message', + attachments: [], + mentioned_users: [], + }, + channel_cid: 'test-channel-id', + created_at: new Date().toISOString(), + }; + + const { messageComposer } = setup({ composition: draftMessage }); + + expect(messageComposer.draftId).toBe('test-draft-id'); + }); + }); + + describe('static methods', () => { + it('should evaluate context type', () => { + const { mockChannel, mockClient } = setup(); + expect(MessageComposer.evaluateContextType(mockChannel)).toBe('channel'); + + const mockThread = getThread(mockChannel, mockClient, 'test-thread-id'); + + expect(MessageComposer.evaluateContextType(mockThread)).toBe('thread'); + + const mockReplyInLegacyThread = { + id: 'test-message-id', + legacyThreadId: 'test-thread-id', + text: 'Hello world', + }; + expect(MessageComposer.evaluateContextType(mockReplyInLegacyThread as any)).toBe( + 'legacy_thread', + ); + + const mockMessage = { + id: 'test-message-id', + }; + expect(MessageComposer.evaluateContextType(mockMessage as any)).toBe('message'); + }); + + it('should construct tag', () => { + const { mockChannel, mockClient } = setup(); + expect(MessageComposer.constructTag(mockChannel)).toBe('channel_test-channel-id'); + + const mockThread = getThread(mockChannel, mockClient, 'test-thread-id'); + expect(MessageComposer.constructTag(mockThread as any)).toBe( + 'thread_test-thread-id', + ); + + const mockLegacyThread = { + cid: mockChannel.cid, + id: 'test-message-id', + legacyThreadId: 'test-legacy-thread-id', + }; + expect(MessageComposer.constructTag(mockLegacyThread as any)).toBe( + 'legacy_thread_test-message-id', + ); + + const mockMessage = { + cid: mockChannel.cid, + id: 'test-message-id', + }; + expect(MessageComposer.constructTag(mockMessage as any)).toBe( + 'message_test-message-id', + ); + }); + + it('should generate id', () => { + expect(MessageComposer.generateId()).toBe(generateUuidV4Output); + }); + }); + + describe('getters', () => { + it('should return the correct values from state', () => { + const { messageComposer } = setup(); + expect(messageComposer.threadId).toBeNull(); + messageComposer.state.next({ + id: 'test-id', + quotedMessage, + pollId: 'test-poll-id', + draftId: 'test-draft-id', + }); + + expect(messageComposer.id).toBe('test-id'); + expect(messageComposer.quotedMessage).toEqual({ + id: 'quoted-message-id', + type: 'regular', + created_at: expect.any(Date), + deleted_at: null, + pinned_at: null, + updated_at: expect.any(Date), + status: 'received', + text: 'Quoted message', + user: { id: 'user-id', name: 'User Name' }, + }); + expect(messageComposer.pollId).toBe('test-poll-id'); + expect(messageComposer.draftId).toBe('test-draft-id'); + }); + + it('should return the correct context type', () => { + const { messageComposer } = setup(); + expect(messageComposer.contextType).toBe('channel'); + }); + + it('should return the correct tag', () => { + const { messageComposer } = setup(); + expect(messageComposer.tag).toBe('channel_test-channel-id'); + }); + + it('should return the correct thread id', () => { + const { mockChannel, mockClient } = setup(); + const mockThread = getThread(mockChannel, mockClient, 'test-thread-id'); + const { messageComposer: threadComposer } = setup({ + compositionContext: mockThread, + }); + expect(threadComposer.threadId).toBe('test-thread-id'); + + const mockLegacyThread = { + cid: mockChannel.cid, + id: 'test-message-id', + legacyThreadId: 'test-legacy-thread-id', + }; + const { messageComposer: legacyThreadComposer } = setup({ + compositionContext: mockLegacyThread as any, + }); + expect(legacyThreadComposer.threadId).toBe('test-legacy-thread-id'); + + const mockMessage = { + cid: mockChannel.cid, + id: 'test-message-id', + parent_id: 'test-parent-id', + }; + const { messageComposer: messageComposer } = setup({ + compositionContext: mockMessage as any, + }); + expect(messageComposer.threadId).toBe('test-parent-id'); + }); + + it('should return the correct client', () => { + const { messageComposer, mockClient } = setup(); + expect(messageComposer.client).toBe(mockClient); + }); + + it('should return the correct last change', () => { + const { messageComposer } = setup(); + messageComposer.editingAuditState.next({ + lastChange: { + draftUpdate: 123456789, + stateUpdate: 987654321, + }, + }); + + expect(messageComposer.lastChange).toEqual({ + draftUpdate: 123456789, + stateUpdate: 987654321, + }); + }); + + it('should return the correct lastChangeOriginIsLocal', () => { + const { messageComposer } = setup(); + messageComposer.editingAuditState.next({ + lastChange: { + draftUpdate: 123456789, + stateUpdate: new Date().getTime(), + }, + }); + + expect(messageComposer.lastChangeOriginIsLocal).toBe(true); + }); + + it('should return the correct compositionIsEmpty', () => { + const { messageComposer } = setup(); + const spyTextComposerTextIsEmpty = vi + .spyOn(messageComposer.textComposer, 'textIsEmpty', 'get') + .mockReturnValueOnce(true) + .mockReturnValueOnce(false); + // First case - empty composition + messageComposer.textComposer.state.partialNext({ + text: '', + mentionedUsers: [], + selection: { start: 0, end: 0 }, + }); + expect(messageComposer.compositionIsEmpty).toBe(true); + + // Second case - non-empty composition + messageComposer.textComposer.state.partialNext({ + text: 'Hello world', + mentionedUsers: [], + selection: { start: 0, end: 0 }, + }); + expect(messageComposer.compositionIsEmpty).toBe(false); + spyTextComposerTextIsEmpty.mockRestore(); + }); + }); + + describe('methods', () => { + it('should initialize state', () => { + const { messageComposer } = setup(); + messageComposer.initState(); + expect(messageComposer.state.getLatestValue()).toEqual({ + id: generateUuidV4Output, + pollId: null, + quotedMessage: null, + draftId: null, + }); + }); + + it('should initialize state with composition', () => { + const { messageComposer } = setup(); + const message = { + id: 'test-message-id', + text: 'Hello world', + attachments: [], + mentioned_users: [], + }; + + messageComposer.initState({ composition: message }); + expect(messageComposer.state.getLatestValue()).toEqual({ + id: 'test-message-id', + pollId: null, + quotedMessage: null, + draftId: null, + }); + }); + + it('should initialize editing audit state', () => { + const { messageComposer } = setup(); + + messageComposer.initEditingAuditState(); + + const result = messageComposer.editingAuditState.getLatestValue(); + expect(result).toEqual({ + lastChange: { + draftUpdate: null, + stateUpdate: expect.any(Number), + }, + }); + }); + + it('should register subscriptions', () => { + const { messageComposer } = setup(); + const unsubscribeFunctions = messageComposer[ + 'unsubscribeFunctions' + ] as unknown as Set<() => void>; + + messageComposer.registerSubscriptions(); + + expect(unsubscribeFunctions.size).toBeGreaterThan(0); + }); + + it('should unregister subscriptions', () => { + const { messageComposer } = setup(); + const unsubscribeFunctions = messageComposer[ + 'unsubscribeFunctions' + ] as unknown as Set<() => void>; + const unsubscribeFn = vi.fn(); + unsubscribeFunctions.add(unsubscribeFn); + + messageComposer.unregisterSubscriptions(); + + expect(unsubscribeFn).toHaveBeenCalled(); + expect(unsubscribeFunctions.size).toBe(0); + }); + + it('should set quoted message', () => { + const { messageComposer } = setup(); + const quotedMessage = { + id: 'quoted-message-id', + type: 'regular', + text: 'Quoted message', + attachments: [], + mentioned_users: [], + }; + + messageComposer.setQuotedMessage(quotedMessage); + expect(messageComposer.state.getLatestValue().quotedMessage).toEqual(quotedMessage); + }); + + it('should clear state', () => { + const { messageComposer } = setup(); + const spyAttachmentManager = vi.spyOn( + messageComposer.attachmentManager, + 'initState', + ); + const spyLinkPreviewsManager = vi.spyOn( + messageComposer.linkPreviewsManager, + 'initState', + ); + const spyTextComposer = vi.spyOn(messageComposer.textComposer, 'initState'); + const spyPollComposer = vi.spyOn(messageComposer.pollComposer, 'initState'); + const spyCustomDataManager = vi.spyOn( + messageComposer.customDataManager, + 'initState', + ); + const spyInitState = vi.spyOn(messageComposer, 'initState'); + + messageComposer.clear(); + + expect(spyAttachmentManager).toHaveBeenCalled(); + expect(spyLinkPreviewsManager).toHaveBeenCalled(); + expect(spyTextComposer).toHaveBeenCalled(); + expect(spyPollComposer).toHaveBeenCalled(); + expect(spyCustomDataManager).toHaveBeenCalled(); + expect(spyInitState).toHaveBeenCalled(); + }); + + it('should restore state from edited message if available', () => { + const editedMessage = { + id: 'edited-message-id', + type: 'regular', + text: 'Edited message', + poll_id: 'test-poll-id', + attachments: [], + mentioned_users: [], + }; + const { messageComposer } = setup({ composition: editedMessage }); + messageComposer.state.partialNext({ + id: 'edited-message-id', + pollId: null, + draftId: null, + }); + messageComposer.restore(); + + expect(messageComposer.state.getLatestValue()).toEqual({ + id: 'edited-message-id', + pollId: 'test-poll-id', + quotedMessage: null, + draftId: null, + }); + }); + + it('should clear state if no edited message available', () => { + const { messageComposer } = setup(); + const spyClear = vi.spyOn(messageComposer, 'clear'); + + messageComposer.restore(); + + expect(spyClear).toHaveBeenCalled(); + }); + + it('should compose message', async () => { + const { messageComposer } = setup(); + const mockResult = { + state: { + message: { + id: 'test-message-id', + text: 'Test message', + }, + }, + status: '', + }; + + const spyExecute = vi.spyOn( + messageComposer.compositionMiddlewareExecutor, + 'execute', + ); + spyExecute.mockResolvedValue(mockResult); + + const result = await messageComposer.compose(); + + expect(spyExecute).toHaveBeenCalledWith('compose', expect.any(Object)); + expect(result).toEqual(mockResult.state); + }); + + it('should return undefined when compose middleware returns discard status', async () => { + const { messageComposer } = setup(); + const mockResult = { + state: {}, + status: 'discard', + }; + + const spyExecute = vi.spyOn( + messageComposer.compositionMiddlewareExecutor, + 'execute', + ); + spyExecute.mockResolvedValue(mockResult); + + const result = await messageComposer.compose(); + + expect(spyExecute).toHaveBeenCalledWith('compose', expect.any(Object)); + expect(result).toBeUndefined(); + }); + + it('should compose draft', async () => { + const { messageComposer } = setup(); + const mockResult = { + state: { + draft: { + id: 'test-draft-id', + text: 'Test draft', + }, + }, + status: '', + }; + + const spyExecute = vi.spyOn( + messageComposer.draftCompositionMiddlewareExecutor, + 'execute', + ); + spyExecute.mockResolvedValue(mockResult); + + const result = await messageComposer.composeDraft(); + + expect(spyExecute).toHaveBeenCalledWith('compose', expect.any(Object)); + expect(result).toEqual(mockResult.state); + }); + + it('should return undefined when draft compose middleware returns discard status', async () => { + const { messageComposer } = setup(); + const mockResult = { + state: {}, + status: 'discard', + }; + + const spyExecute = vi.spyOn( + messageComposer.draftCompositionMiddlewareExecutor, + 'execute', + ); + spyExecute.mockResolvedValue(mockResult); + + const result = await messageComposer.composeDraft(); + + expect(spyExecute).toHaveBeenCalledWith('compose', expect.any(Object)); + expect(result).toBeUndefined(); + }); + + it('should create draft', async () => { + const { messageComposer, mockChannel } = setup({ + config: { drafts: { enabled: true } }, + }); + const mockDraft = { + id: 'test-draft-id', + text: 'Test draft', + }; + + const spyComposeDraft = vi.spyOn(messageComposer, 'composeDraft'); + spyComposeDraft.mockResolvedValue({ draft: mockDraft }); + + const spyCreateDraft = vi.spyOn(mockChannel, 'createDraft'); + spyCreateDraft.mockResolvedValue({ draft: mockDraft }); + + const spyLogDraftUpdateTimestamp = vi.spyOn( + messageComposer, + 'logDraftUpdateTimestamp', + ); + + await messageComposer.createDraft(); + + expect(spyComposeDraft).toHaveBeenCalled(); + expect(spyCreateDraft).toHaveBeenCalledWith(mockDraft); + expect(spyLogDraftUpdateTimestamp).toHaveBeenCalled(); + expect(messageComposer.state.getLatestValue().draftId).toBe('test-draft-id'); + }); + + it('should not create draft if edited message exists', async () => { + const editedMessage = { + id: 'edited-message-id', + type: 'regular', + text: 'Edited message', + attachments: [], + mentioned_users: [], + }; + + const { messageComposer } = setup({ composition: editedMessage }); + + const spyComposeDraft = vi.spyOn(messageComposer, 'composeDraft'); + await messageComposer.createDraft(); + + expect(spyComposeDraft).not.toHaveBeenCalled(); + }); + + it('should delete draft', async () => { + const { messageComposer, mockChannel } = setup({ + config: { drafts: { enabled: true } }, + }); + const draftId = 'test-draft-id'; + + messageComposer.state.next({ + id: '', + pollId: null, + quotedMessage: null, + draftId, + }); + + const spyChannelDeleteDraft = vi.spyOn(mockChannel, 'deleteDraft'); + spyChannelDeleteDraft.mockResolvedValue({}); + + const spyLogDraftUpdateTimestamp = vi.spyOn( + messageComposer, + 'logDraftUpdateTimestamp', + ); + + await messageComposer.deleteDraft(); + + expect(spyChannelDeleteDraft).toHaveBeenCalledWith({ parent_id: undefined }); + expect(spyLogDraftUpdateTimestamp).toHaveBeenCalled(); + expect(messageComposer.state.getLatestValue().draftId).toBeNull(); + }); + + it('should not delete draft if no draftId exists', async () => { + const { messageComposer, mockChannel } = setup(); + const spyChannelDeleteDraft = vi.spyOn(mockChannel, 'deleteDraft'); + + await messageComposer.deleteDraft(); + + expect(spyChannelDeleteDraft).not.toHaveBeenCalled(); + }); + + it('should create a poll', async () => { + const { messageComposer, mockClient } = setup(); + const mockPoll = { + id: 'test-poll-id', + name: 'Test Poll', + options: [], + }; + + const spyCompose = vi.spyOn(messageComposer.pollComposer, 'compose'); + spyCompose.mockResolvedValue({ data: mockPoll }); + + const spyCreatePoll = vi.spyOn(mockClient, 'createPoll'); + spyCreatePoll.mockResolvedValue({ poll: mockPoll }); + + await messageComposer.createPoll(); + + expect(spyCompose).toHaveBeenCalled(); + expect(spyCreatePoll).toHaveBeenCalledWith(mockPoll); + expect(messageComposer.state.getLatestValue().pollId).toBe('test-poll-id'); + }); + + it('should not create poll if compose returns no data', async () => { + const { messageComposer, mockClient } = setup(); + const spyCompose = vi.spyOn(messageComposer.pollComposer, 'compose'); + spyCompose.mockResolvedValue({ data: {} }); + + const spyCreatePoll = vi.spyOn(mockClient, 'createPoll'); + + await messageComposer.createPoll(); + + expect(spyCompose).toHaveBeenCalled(); + expect(spyCreatePoll).not.toHaveBeenCalled(); + }); + + it('should handle poll creation error', async () => { + const { messageComposer, mockClient } = setup(); + const mockPoll = { + id: 'test-poll-id', + name: 'Test Poll', + options: [], + }; + + const spyCompose = vi.spyOn(messageComposer.pollComposer, 'compose'); + spyCompose.mockResolvedValue({ data: mockPoll }); + + const spyCreatePoll = vi.spyOn(mockClient, 'createPoll'); + spyCreatePoll.mockRejectedValue(new Error('Failed to create poll')); + + const spyAddNotification = vi.spyOn(mockClient.notifications, 'add'); + + await expect(messageComposer.createPoll()).rejects.toThrow('Failed to create poll'); + expect(spyAddNotification).toHaveBeenCalledWith({ + message: 'Failed to create the poll', + origin: { + emitter: 'MessageComposer', + context: { composer: messageComposer }, + }, + }); + }); + }); + + describe('subscriptions', () => { + describe('subscribeMessageUpdated', () => { + it('should update state when message.updated event is received', () => { + const { messageComposer, mockClient } = setup(); + const updatedMessage = { + id: messageComposer.id, + poll_id: 'test-poll-id', + }; + + messageComposer.registerSubscriptions(); + mockClient.dispatchEvent({ type: 'message.updated', message: updatedMessage }); + + expect(messageComposer.state.getLatestValue().pollId).toEqual( + updatedMessage.poll_id, + ); + }); + + it('should update quoted message when quoted message is updated', () => { + const { messageComposer, mockClient } = setup(); + const quotedMessage = { + id: 'quoted-message-id', + text: 'Quoted message', + attachments: [], + mentioned_users: [], + }; + + messageComposer.setQuotedMessage(quotedMessage); + messageComposer.registerSubscriptions(); + mockClient.dispatchEvent({ + type: 'message.updated', + message: { ...quotedMessage, text: 'Updated quoted message' }, + }); + + expect(messageComposer.state.getLatestValue().quotedMessage?.text).toBe( + 'Updated quoted message', + ); + }); + }); + + describe('subscribeMessageComposerSetupStateChange', () => { + it('should apply modifications when setup state changes', () => { + const { messageComposer, mockClient } = setup(); + const mockModifications = vi.fn(); + + messageComposer.registerSubscriptions(); + mockClient._messageComposerSetupState.next({ + setupFunction: mockModifications, + }); + + expect(mockModifications).toHaveBeenCalledWith({ composer: messageComposer }); + }); + }); + + describe('subscribeMessageDeleted', () => { + it('should clear state when message is deleted', () => { + const { messageComposer, mockClient } = setup(); + const message = { + id: messageComposer.id, + text: 'Test message', + attachments: [], + mentioned_users: [], + }; + + messageComposer.initState({ composition: message }); + messageComposer.registerSubscriptions(); + mockClient.dispatchEvent({ type: 'message.deleted', message }); + + expect(messageComposer.state.getLatestValue().id).toBe(generateUuidV4Output); + }); + + it('should clear quoted message when quoted message is deleted', () => { + const { messageComposer, mockClient } = setup(); + const quotedMessage = { + id: 'quoted-message-id', + text: 'Quoted message', + attachments: [], + mentioned_users: [], + }; + + messageComposer.setQuotedMessage(quotedMessage); + messageComposer.registerSubscriptions(); + mockClient.dispatchEvent({ type: 'message.deleted', message: quotedMessage }); + + expect(messageComposer.state.getLatestValue().quotedMessage).toBeNull(); + }); + }); + + describe('subscribeDraftUpdated', () => { + it('should update state when draft is updated', () => { + const { messageComposer, mockClient } = setup({ + config: { drafts: { enabled: true } }, + }); + const draft = { + message: { + id: 'test-draft-id', + text: 'Draft message', + attachments: [], + mentioned_users: [], + }, + channel_cid: 'messaging:test-channel-id', + }; + + messageComposer.registerSubscriptions(); + mockClient.dispatchEvent({ type: 'draft.updated', draft }); + + expect(messageComposer.state.getLatestValue().draftId).toBe('test-draft-id'); + }); + }); + + describe('subscribeDraftDeleted', () => { + it('should clear state when draft is deleted and composition is empty', () => { + const { messageComposer, mockChannel, mockClient } = setup({ + config: { drafts: { enabled: true } }, + }); + const draft = { + message: { + id: messageComposer.id, + }, + channel_cid: mockChannel.cid, + }; + + messageComposer.initState({ composition: draft }); + Object.defineProperty(messageComposer.textComposer, 'textIsEmpty', { + get: () => false, + }); + messageComposer.registerSubscriptions(); + mockClient.dispatchEvent({ type: 'draft.deleted', draft }); + + expect(messageComposer.state.getLatestValue().draftId).toBeNull(); + }); + }); + + describe('subscribeTextComposerStateChanged', () => { + it('should log state update timestamp when text changes', () => { + const { messageComposer } = setup(); + const spy = vi.spyOn(messageComposer, 'logStateUpdateTimestamp'); + + messageComposer.registerSubscriptions(); + messageComposer.textComposer.state.next({ + text: 'New text', + mentionedUsers: [], + selection: { start: 0, end: 0 }, + }); + + expect(spy).toHaveBeenCalled(); + }); + + it('should find and enrich URLs when text changes and link previews are enabled', () => { + const { mockChannel, messageComposer } = setup({ + config: { linkPreviews: { enabled: true } }, + }); + mockChannel.getConfig = vi + .fn() + .mockImplementation(() => ({ url_enrichment: true })); + const spy = vi.spyOn(messageComposer.linkPreviewsManager, 'findAndEnrichUrls'); + + messageComposer.registerSubscriptions(); + Object.defineProperty(messageComposer.textComposer, 'textIsEmpty', { + get: () => false, + }); + messageComposer.textComposer.state.next({ + text: 'https://example.com', + mentionedUsers: [], + selection: { start: 0, end: 0 }, + }); + + expect(spy).toHaveBeenCalledWith('https://example.com'); + }); + }); + + describe('subscribeAttachmentManagerStateChanged', () => { + it('should log state update timestamp when attachments change', () => { + const { messageComposer } = setup(); + const spy = vi.spyOn(messageComposer, 'logStateUpdateTimestamp'); + + messageComposer.registerSubscriptions(); + messageComposer.attachmentManager.state.next({ + attachments: [{ id: 'new-attachment' }], + }); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('subscribeLinkPreviewsManagerStateChanged', () => { + it('should log state update timestamp when link previews change', () => { + const { messageComposer } = setup(); + const spy = vi.spyOn(messageComposer, 'logStateUpdateTimestamp'); + + messageComposer.registerSubscriptions(); + messageComposer.linkPreviewsManager.state.next({ + previews: new Map([['https://example.com', { data: {}, status: 'loaded' }]]), + }); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('subscribePollComposerStateChanged', () => { + it('should log state update timestamp when poll data changes', () => { + const { messageComposer } = setup(); + const spy = vi.spyOn(messageComposer, 'logStateUpdateTimestamp'); + + messageComposer.registerSubscriptions(); + messageComposer.pollComposer.state.next({ + data: { + id: 'new-poll-id', + name: 'New Poll', + options: [], + allow_answers: true, + allow_user_suggested_options: false, + description: '', + enforce_unique_vote: false, + is_closed: false, + max_votes_allowed: '1', + user_id: 'user-id', + voting_visibility: 'public', + }, + }); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('subscribeCustomDataManagerStateChanged', () => { + it('should log state update timestamp when custom data changes', () => { + const { messageComposer } = setup(); + const spy = vi.spyOn(messageComposer, 'logStateUpdateTimestamp'); + messageComposer.registerSubscriptions(); + messageComposer.customDataManager.state.next({ + data: { + field1: 'value1', + }, + }); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('subscribeMessageComposerStateChanged', () => { + it('should log state update timestamp when poll ID or quoted message changes', () => { + const { messageComposer } = setup(); + const spy = vi.spyOn(messageComposer, 'logStateUpdateTimestamp'); + + messageComposer.registerSubscriptions(); + messageComposer.state.next({ + id: '', + pollId: 'new-poll-id', + quotedMessage: null, + draftId: null, + }); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('subscribeMessageComposerConfigStateChanged', () => { + const defaultValue = 'Default text'; + + it('should insert default text when text is empty and config has a default value', () => { + const { messageComposer } = setup(); + const spy = vi.spyOn(messageComposer.textComposer, 'insertText'); + messageComposer.registerSubscriptions(); + expect(spy).not.toHaveBeenCalled(); + + messageComposer.textComposer.defaultValue = defaultValue; + + expect(spy).toHaveBeenCalledWith({ + text: defaultValue, + selection: { start: 0, end: 0 }, + }); + spy.mockRestore(); + }); + + it('should not insert default text when text is not empty', () => { + const { messageComposer } = setup(); + messageComposer.registerSubscriptions(); + const spy = vi.spyOn(messageComposer.textComposer, 'insertText'); + + messageComposer.textComposer.state.next({ + text: 'Hello world', + mentionedUsers: [], + selection: { start: 0, end: 0 }, + }); + expect(spy).not.toHaveBeenCalled(); + + messageComposer.textComposer.defaultValue = defaultValue; + + expect(spy).not.toHaveBeenCalled(); + spy.mockRestore(); + }); + }); + }); +}); diff --git a/test/unit/MessageComposer/middleware/messageComposer/attachments.test.ts b/test/unit/MessageComposer/middleware/messageComposer/attachments.test.ts new file mode 100644 index 0000000000..afd511e0b7 --- /dev/null +++ b/test/unit/MessageComposer/middleware/messageComposer/attachments.test.ts @@ -0,0 +1,522 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { Channel } from '../../../../../src/channel'; +import { StreamChat } from '../../../../../src/client'; +import { MessageComposer } from '../../../../../src/messageComposer/messageComposer'; +import { createAttachmentsCompositionMiddleware } from '../../../../../src/messageComposer/middleware/messageComposer/attachments'; +import { + AttachmentLoadingState, + LocalImageAttachment, +} from '../../../../../src/messageComposer/types'; +import { createDraftAttachmentsCompositionMiddleware } from '../../../../../src/messageComposer/middleware/messageComposer/attachments'; + +describe('AttachmentsMiddleware', () => { + let channel: Channel; + let client: StreamChat; + let messageComposer: MessageComposer; + let attachmentsMiddleware: ReturnType; + + beforeEach(() => { + client = { + userID: 'currentUser', + user: { id: 'currentUser' }, + notifications: { + addWarning: vi.fn(), + }, + } as any; + + channel = { + getClient: vi.fn().mockReturnValue(client), + state: { + members: {}, + watchers: {}, + }, + getConfig: vi.fn().mockReturnValue({ commands: [] }), + } as any; + + const textComposer = { + get text() { + return ''; + }, + get mentionedUsers() { + return []; + }, + }; + + const attachmentManager = { + get uploadsInProgressCount() { + return 0; + }, + get successfulUploads() { + return []; + }, + }; + + const linkPreviewsManager = { + state: { + getLatestValue: () => ({ + previews: new Map(), + }), + }, + }; + + const pollComposer = { + state: { + getLatestValue: () => ({ + data: { + options: [], + name: '', + max_votes_allowed: '', + id: '', + user_id: '', + voting_visibility: 'public', + allow_answers: false, + allow_user_suggested_options: false, + description: '', + enforce_unique_vote: true, + }, + errors: {}, + }), + }, + get canCreatePoll() { + return false; + }, + }; + + messageComposer = { + channel, + config: {}, + threadId: undefined, + client, + textComposer, + attachmentManager, + linkPreviewsManager, + pollComposer, + get lastChangeOriginIsLocal() { + return true; + }, + editedMessage: undefined, + get quotedMessage() { + return undefined; + }, + } as any; + + attachmentsMiddleware = createAttachmentsCompositionMiddleware(messageComposer); + }); + + it('should handle message without attachments', async () => { + const result = await attachmentsMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + }, + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined; + expect(result.state.message.attachments ?? []).toHaveLength(0); + expect(result.state.localMessage.attachments ?? []).toHaveLength(0); + }); + + it('should handle message with image attachment', async () => { + const attachment: LocalImageAttachment = { + type: 'image', + image_url: 'https://example.com/image.jpg', + localMetadata: { + id: 'attachment-1', + file: new File([], 'test.jpg', { type: 'image/jpeg' }), + uploadState: 'finished' as AttachmentLoadingState, + }, + }; + + vi.spyOn( + messageComposer.attachmentManager, + 'successfulUploads', + 'get', + ).mockReturnValue([attachment]); + + const result = await attachmentsMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + }, + localMessage: { + attachments: [attachment], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined; + expect(result.state.message.attachments ?? []).toHaveLength(1); + expect(result.state.localMessage.attachments ?? []).toHaveLength(1); + expect((result.state.message.attachments ?? [])[0].type).toBe('image'); + expect((result.state.localMessage.attachments ?? [])[0].type).toBe('image'); + }); + + it('should handle message with multiple attachments', async () => { + const attachments: LocalImageAttachment[] = [ + { + type: 'image', + image_url: 'https://example.com/image1.jpg', + localMetadata: { + id: 'attachment-1', + file: new File([], 'test1.jpg', { type: 'image/jpeg' }), + uploadState: 'finished' as AttachmentLoadingState, + }, + }, + { + type: 'image', + image_url: 'https://example.com/image2.jpg', + localMetadata: { + id: 'attachment-2', + file: new File([], 'test2.jpg', { type: 'image/jpeg' }), + uploadState: 'finished' as AttachmentLoadingState, + }, + }, + ]; + + vi.spyOn( + messageComposer.attachmentManager, + 'successfulUploads', + 'get', + ).mockReturnValue(attachments); + + const result = await attachmentsMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + }, + localMessage: { + attachments, + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined; + expect(result.state.message.attachments ?? []).toHaveLength(2); + expect(result.state.localMessage.attachments ?? []).toHaveLength(2); + expect((result.state.message.attachments ?? [])[0].type).toBe('image'); + expect((result.state.message.attachments ?? [])[1].type).toBe('image'); + }); + + it('should handle message with uploading attachments', async () => { + const attachment: LocalImageAttachment = { + type: 'image', + image_url: 'https://example.com/image.jpg', + localMetadata: { + id: 'attachment-1', + file: new File([], 'test.jpg', { type: 'image/jpeg' }), + uploadState: 'uploading' as AttachmentLoadingState, + }, + }; + + vi.spyOn( + messageComposer.attachmentManager, + 'uploadsInProgressCount', + 'get', + ).mockReturnValue(1); + vi.spyOn( + messageComposer.attachmentManager, + 'successfulUploads', + 'get', + ).mockReturnValue([]); + + const result = await attachmentsMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + }, + localMessage: { + attachments: [attachment], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined; + expect(result.state.message.attachments ?? []).toHaveLength(0); + expect(result.state.localMessage.attachments ?? []).toHaveLength(1); + }); + + it('should handle message with failed attachments', async () => { + const attachment: LocalImageAttachment = { + type: 'image', + image_url: 'https://example.com/image.jpg', + localMetadata: { + id: 'attachment-1', + file: new File([], 'test.jpg', { type: 'image/jpeg' }), + uploadState: 'failed' as AttachmentLoadingState, + }, + }; + + vi.spyOn( + messageComposer.attachmentManager, + 'successfulUploads', + 'get', + ).mockReturnValue([]); + + const result = await attachmentsMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + }, + localMessage: { + attachments: [attachment], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined; + expect(result.state.message.attachments ?? []).toHaveLength(0); + expect(result.state.localMessage.attachments ?? []).toHaveLength(1); + }); +}); + +describe('DraftAttachmentsMiddleware', () => { + let channel: Channel; + let client: StreamChat; + let messageComposer: MessageComposer; + let draftAttachmentsMiddleware: ReturnType< + typeof createDraftAttachmentsCompositionMiddleware + >; + + beforeEach(() => { + client = { + userID: 'currentUser', + user: { id: 'currentUser' }, + } as any; + + channel = { + getClient: vi.fn().mockReturnValue(client), + state: { + members: {}, + watchers: {}, + }, + getConfig: vi.fn().mockReturnValue({ commands: [] }), + } as any; + + const attachmentManager = { + get uploadsInProgressCount() { + return 0; + }, + get successfulUploads() { + return []; + }, + }; + + messageComposer = { + channel, + client, + attachmentManager, + } as any; + + draftAttachmentsMiddleware = + createDraftAttachmentsCompositionMiddleware(messageComposer); + }); + + it('should handle draft without attachments', async () => { + const result = await draftAttachmentsMiddleware.compose({ + input: { + state: { + draft: { + text: '', + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined(); + expect(result.state.draft.attachments).toBeUndefined(); + }); + + it('should handle draft with successful uploads', async () => { + const attachment: LocalImageAttachment = { + type: 'image', + image_url: 'https://example.com/image.jpg', + localMetadata: { + id: 'attachment-1', + file: new File([], 'test.jpg', { type: 'image/jpeg' }), + uploadState: 'finished' as AttachmentLoadingState, + }, + }; + + vi.spyOn( + messageComposer.attachmentManager!, + 'successfulUploads', + 'get', + ).mockReturnValue([attachment]); + + const result = await draftAttachmentsMiddleware.compose({ + input: { + state: { + draft: { + text: '', + attachments: [], + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined(); + expect(result.state.draft.attachments).toHaveLength(1); + expect(result.state.draft.attachments?.[0].type).toBe('image'); + expect(result.state.draft.attachments?.[0].image_url).toBe( + 'https://example.com/image.jpg', + ); + expect('localMetadata' in result.state.draft.attachments?.[0]!).toBeFalsy(); + }); + + it('should merge existing draft attachments with successful uploads', async () => { + const existingAttachment = { + type: 'file', + file_url: 'https://example.com/doc.pdf', + }; + + const newAttachment: LocalImageAttachment = { + type: 'image', + image_url: 'https://example.com/image.jpg', + localMetadata: { + id: 'attachment-1', + file: new File([], 'test.jpg', { type: 'image/jpeg' }), + uploadState: 'finished' as AttachmentLoadingState, + }, + }; + + vi.spyOn( + messageComposer.attachmentManager!, + 'successfulUploads', + 'get', + ).mockReturnValue([newAttachment]); + + const result = await draftAttachmentsMiddleware.compose({ + input: { + state: { + draft: { + text: '', + attachments: [existingAttachment], + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined(); + expect(result.state.draft.attachments).toHaveLength(2); + expect(result.state.draft.attachments?.[0]).toEqual(existingAttachment); + expect(result.state.draft.attachments?.[1].type).toBe('image'); + expect('localMetadata' in result.state.draft.attachments?.[1]!).toBeFalsy(); + }); + + it('should handle case when attachmentManager is not available', async () => { + messageComposer.attachmentManager = undefined as any; + draftAttachmentsMiddleware = + createDraftAttachmentsCompositionMiddleware(messageComposer); + + const result = await draftAttachmentsMiddleware.compose({ + input: { + state: { + draft: { + text: '', + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined(); + expect(result.state.draft.attachments).toBeUndefined(); + }); +}); diff --git a/test/unit/MessageComposer/middleware/messageComposer/compositionValidation.test.ts b/test/unit/MessageComposer/middleware/messageComposer/compositionValidation.test.ts new file mode 100644 index 0000000000..217c920d40 --- /dev/null +++ b/test/unit/MessageComposer/middleware/messageComposer/compositionValidation.test.ts @@ -0,0 +1,530 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { createCompositionValidationMiddleware } from '../../../../../src/messageComposer/middleware/messageComposer/compositionValidation'; +import { MessageComposer } from '../../../../../src/messageComposer/messageComposer'; +import { Channel } from '../../../../../src/channel'; +import { StreamChat } from '../../../../../src/client'; +import { + LocalImageAttachment, + AttachmentLoadingState, +} from '../../../../../src/messageComposer/types'; +import { + LinkPreview, + LinkPreviewStatus, + LinkPreviewMap, +} from '../../../../../src/messageComposer/linkPreviewsManager'; +import { MessageComposerMiddlewareValue } from '../../../../../src/messageComposer/middleware/messageComposer/types'; +import { createDraftCompositionValidationMiddleware } from '../../../../../src/messageComposer/middleware/messageComposer/compositionValidation'; + +describe('MessageComposerValidationMiddleware', () => { + let channel: Channel; + let client: StreamChat; + let messageComposer: MessageComposer; + let validationMiddleware: ReturnType; + + beforeEach(() => { + client = { + userID: 'currentUser', + user: { id: 'currentUser' }, + } as any; + + channel = { + getClient: vi.fn().mockReturnValue(client), + state: { + members: {}, + watchers: {}, + }, + getConfig: vi.fn().mockReturnValue({ commands: [] }), + } as any; + + const textComposer = { + get text() { + return ''; + }, + get mentionedUsers() { + return []; + }, + }; + + const attachmentManager = { + get uploadsInProgressCount() { + return 0; + }, + get successfulUploads() { + return []; + }, + }; + + const linkPreviewsManager = { + state: { + getLatestValue: () => ({ + previews: new Map(), + }), + }, + }; + + const pollComposer = { + state: { + getLatestValue: () => ({ + data: { + options: [], + name: '', + max_votes_allowed: '', + id: '', + user_id: '', + voting_visibility: 'public', + allow_answers: false, + allow_user_suggested_options: false, + description: '', + enforce_unique_vote: true, + }, + errors: {}, + }), + }, + get canCreatePoll() { + return false; + }, + }; + + messageComposer = { + channel, + config: {}, + threadId: undefined, + client, + textComposer, + attachmentManager, + linkPreviewsManager, + pollComposer, + get lastChangeOriginIsLocal() { + return true; + }, + editedMessage: undefined, + get quotedMessage() { + return undefined; + }, + } as any; + + validationMiddleware = createCompositionValidationMiddleware(messageComposer); + }); + + it('should validate empty message', async () => { + const result = await validationMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + }, + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBe('discard'); + }); + + it('should validate message with text', async () => { + vi.spyOn(messageComposer.textComposer, 'text', 'get').mockReturnValue('Hello world'); + + const result = await validationMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + text: 'Hello world', + type: 'regular', + }, + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: 'Hello world', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined; + }); + + it('should validate message with attachments', async () => { + const attachment: LocalImageAttachment = { + type: 'image', + image_url: 'https://example.com/image.jpg', + localMetadata: { + id: 'attachment-1', + file: new File([], 'test.jpg', { type: 'image/jpeg' }), + uploadState: 'finished' as AttachmentLoadingState, + }, + }; + + vi.spyOn( + messageComposer.attachmentManager, + 'successfulUploads', + 'get', + ).mockReturnValue([attachment]); + + const result = await validationMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + attachments: [attachment], + parent_id: undefined, + type: 'regular', + }, + localMessage: { + attachments: [attachment], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined; + }); + + it('should validate message with mentions', async () => { + vi.spyOn(messageComposer.textComposer, 'text', 'get').mockReturnValue('Hello @user1'); + vi.spyOn(messageComposer.textComposer, 'mentionedUsers', 'get').mockReturnValue([ + { id: 'user1', name: 'User One' }, + ]); + + const result = await validationMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + mentioned_users: ['user1'], + parent_id: undefined, + text: 'Hello @user1', + type: 'regular', + }, + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [{ id: 'user1', name: 'User One' }], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: 'Hello @user1', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined; + }); + + it('should validate message with poll', async () => { + const result = await validationMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + poll_id: 'poll-test-id', + type: 'regular', + }, + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + poll_id: 'poll-test-id', + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined; + }); + + it('should validate message with last origin change', async () => { + vi.spyOn(messageComposer, 'lastChangeOriginIsLocal', 'get').mockReturnValue(false); + + const result = await validationMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + text: 'Hello world', + type: 'regular', + }, + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: 'Hello world', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBe('discard'); + }); +}); + +describe('DraftCompositionValidationMiddleware', () => { + let channel: Channel; + let client: StreamChat; + let messageComposer: MessageComposer; + let validationMiddleware: ReturnType; + + beforeEach(() => { + client = { + userID: 'currentUser', + user: { id: 'currentUser' }, + } as any; + + channel = { + getClient: vi.fn().mockReturnValue(client), + state: { + members: {}, + watchers: {}, + }, + getConfig: vi.fn().mockReturnValue({ commands: [] }), + } as any; + + const textComposer = { + get text() { + return ''; + }, + get mentionedUsers() { + return []; + }, + }; + + const attachmentManager = { + get uploadsInProgressCount() { + return 0; + }, + get successfulUploads() { + return []; + }, + }; + + const linkPreviewsManager = { + state: { + getLatestValue: () => ({ + previews: new Map(), + }), + }, + }; + + const pollComposer = { + state: { + getLatestValue: () => ({ + data: { + options: [], + name: '', + max_votes_allowed: '', + id: '', + user_id: '', + voting_visibility: 'public', + allow_answers: false, + allow_user_suggested_options: false, + description: '', + enforce_unique_vote: true, + }, + errors: {}, + }), + }, + get canCreatePoll() { + return false; + }, + }; + + messageComposer = { + channel, + config: {}, + threadId: undefined, + client, + textComposer, + attachmentManager, + linkPreviewsManager, + pollComposer, + get lastChangeOriginIsLocal() { + return true; + }, + editedMessage: undefined, + get quotedMessage() { + return undefined; + }, + } as any; + + validationMiddleware = createDraftCompositionValidationMiddleware(messageComposer); + }); + + it('should discard empty draft', async () => { + const result = await validationMiddleware.compose({ + input: { + state: { + draft: { + text: '', + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBe('discard'); + }); + + it('should validate draft with text', async () => { + const result = await validationMiddleware.compose({ + input: { + state: { + draft: { + text: 'Hello world', + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined(); + }); + + it('should validate draft with attachments', async () => { + const result = await validationMiddleware.compose({ + input: { + state: { + draft: { + text: '', + attachments: [ + { + type: 'image', + image_url: 'https://example.com/image.jpg', + }, + ], + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined(); + }); + + it('should validate draft with poll', async () => { + const result = await validationMiddleware.compose({ + input: { + state: { + draft: { + text: '', + poll_id: 'poll-123', + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined(); + }); + + it('should validate draft with quoted message', async () => { + const result = await validationMiddleware.compose({ + input: { + state: { + draft: { + text: '', + quoted_message_id: 'msg-123', + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined(); + }); + + it('should discard draft when last change origin is not local', async () => { + vi.spyOn(messageComposer, 'lastChangeOriginIsLocal', 'get').mockReturnValue(false); + + const result = await validationMiddleware.compose({ + input: { + state: { + draft: { + text: 'Hello world', + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBe('discard'); + }); +}); diff --git a/test/unit/MessageComposer/middleware/messageComposer/customData.test.ts b/test/unit/MessageComposer/middleware/messageComposer/customData.test.ts new file mode 100644 index 0000000000..bcf4373c4c --- /dev/null +++ b/test/unit/MessageComposer/middleware/messageComposer/customData.test.ts @@ -0,0 +1,126 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { Channel } from '../../../../../src/channel'; +import { StreamChat } from '../../../../../src/client'; +import { MessageComposer } from '../../../../../src/messageComposer/messageComposer'; +import { + createCustomDataCompositionMiddleware, + createDraftCustomDataCompositionMiddleware, +} from '../../../../../src/messageComposer/middleware/messageComposer/customData'; +import type { + MessageComposerMiddlewareValueState, + MessageDraftComposerMiddlewareValueState, +} from '../../../../../src/messageComposer/middleware/messageComposer/types'; + +describe('Custom Data Middleware', () => { + let channel: Channel; + let client: StreamChat; + let composer: MessageComposer; + + beforeEach(() => { + client = new StreamChat('apiKey', 'apiSecret'); + client.user = { id: 'user-id', name: 'Test User' }; + channel = client.channel('channelType', 'channelId'); + composer = new MessageComposer({ + client, + compositionContext: channel, + }); + }); + + describe('createCustomDataCompositionMiddleware', () => { + it('should initialize with custom data', async () => { + const data = { key: 'value' }; + composer.customDataManager.setData(data); + const middleware = createCustomDataCompositionMiddleware(composer); + const state: MessageComposerMiddlewareValueState = { + message: { id: '1', type: 'regular' }, + localMessage: { + id: '1', + text: '', + type: 'regular', + status: 'sending', + created_at: new Date(), + updated_at: new Date(), + attachments: [], + mentioned_users: [], + reaction_groups: null, + pinned_at: null, + deleted_at: null, + }, + sendOptions: {}, + }; + + const result = await middleware.compose({ + input: { state }, + nextHandler: async (input) => input, + }); + + expect(result.state.message).toEqual(expect.objectContaining(data)); + expect(result.state.localMessage).toEqual(expect.objectContaining(data)); + }); + + it('should add empty custom data if no data is set', async () => { + const middleware = createCustomDataCompositionMiddleware(composer); + const state: MessageComposerMiddlewareValueState = { + message: { id: '1', type: 'regular' }, + localMessage: { + id: '1', + text: '', + type: 'regular', + status: 'sending', + created_at: new Date(), + updated_at: new Date(), + attachments: [], + mentioned_users: [], + reaction_groups: null, + pinned_at: null, + deleted_at: null, + }, + sendOptions: {}, + }; + + const result = await middleware.compose({ + input: { state }, + nextHandler: async (input) => input, + }); + + expect(result.state.message).toEqual(state.message); + expect(result.state.localMessage).toEqual(state.localMessage); + }); + }); + + describe('createDraftCustomDataCompositionMiddleware', () => { + it('should initialize with custom data', async () => { + const data = { key: 'value' }; + composer.customDataManager.setData(data); + const middleware = createDraftCustomDataCompositionMiddleware(composer); + const state: MessageDraftComposerMiddlewareValueState = { + draft: { + id: '1', + text: '', + type: 'regular', + }, + }; + + const result = await middleware.compose({ + input: { state }, + nextHandler: async (input) => input, + }); + + expect(result.state.draft).toEqual(expect.objectContaining(data)); + }); + + it('should add empty custom data if no data is set', async () => { + const middleware = createDraftCustomDataCompositionMiddleware(composer); + const state: MessageDraftComposerMiddlewareValueState = { + draft: { id: '1', text: '', type: 'regular' }, + }; + + const result = await middleware.compose({ + input: { state }, + nextHandler: async (input) => input, + }); + + expect(result.state.draft).toEqual(state.draft); + }); + }); +}); diff --git a/test/unit/MessageComposer/middleware/messageComposer/linkPreviews.test.ts b/test/unit/MessageComposer/middleware/messageComposer/linkPreviews.test.ts new file mode 100644 index 0000000000..7cc05c4a58 --- /dev/null +++ b/test/unit/MessageComposer/middleware/messageComposer/linkPreviews.test.ts @@ -0,0 +1,742 @@ +import { describe, expect, it, vi } from 'vitest'; +import { StreamChat } from '../../../../../src/client'; +import { + LinkPreview, + LinkPreviewStatus, +} from '../../../../../src/messageComposer/linkPreviewsManager'; +import { MessageComposer } from '../../../../../src/messageComposer/messageComposer'; +import { + createDraftLinkPreviewsCompositionMiddleware, + createLinkPreviewsCompositionMiddleware, +} from '../../../../../src/messageComposer/middleware/messageComposer/linkPreviews'; +import { + DraftMessage, + DraftResponse, + LinkPreviewsManagerConfig, + LocalMessage, +} from '../../../../../src'; + +const enrichURLReturnValue = { + asset_url: 'https://example.com/image.jpg', + author_link: 'https://example.com/author', + author_name: 'Example Author', + image_url: 'https://example.com/image.jpg', + og_scrape_url: 'https://example.com', + text: 'Example description', + thumb_url: 'https://example.com/thumb.jpg', + title: 'Example', + title_link: 'https://example.com', + type: 'article', + duration: '100', +}; + +const setup = ({ + composition, + config, +}: { + composition?: DraftResponse | LocalMessage; + config?: Partial; + message?: DraftMessage | LocalMessage; +} = {}) => { + vi.clearAllMocks(); + + const mockClient = new StreamChat('apiKey', 'apiSecret'); + mockClient.enrichURL = vi.fn().mockResolvedValue(enrichURLReturnValue); + + const mockChannel = mockClient.channel('messaging', 'test-channel', { + members: [], + }); + mockChannel.getConfig = vi.fn().mockImplementation(() => ({ url_enrichment: true })); + const messageComposer = new MessageComposer({ + client: mockClient, + composition, + compositionContext: mockChannel, + config: { linkPreviews: config }, + }); + + const linkPreviewsMiddleware = createLinkPreviewsCompositionMiddleware(messageComposer); + + return { linkPreviewsMiddleware, messageComposer }; +}; + +describe('LinkPreviewsMiddleware', () => { + it('should keep message attachments empty if not link previews are available', async () => { + const { linkPreviewsMiddleware } = setup(); + const result = await linkPreviewsMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + }, + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined; + expect(result.state.message.attachments ?? []).toHaveLength(0); + expect(result.state.localMessage.attachments ?? []).toHaveLength(0); + }); + + it('should add loaded preview to message attachments', async () => { + const { linkPreviewsMiddleware, messageComposer } = setup(); + messageComposer.linkPreviewsManager.state.next({ + previews: new Map([ + [ + 'https://example.com', + { + asset_url: 'https://example.com/image.jpg', + author_link: 'https://example.com/author', + author_name: 'Example Author', + image_url: 'https://example.com/image.jpg', + og_scrape_url: 'https://example.com', + text: 'Example description', + thumb_url: 'https://example.com/thumb.jpg', + title: 'Example', + title_link: 'https://example.com', + type: 'article', + status: LinkPreviewStatus.LOADED, + }, + ], + ]), + }); + + const result = await linkPreviewsMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + }, + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: 'https://example.com', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined; + expect(result.state.message.attachments ?? []).toHaveLength(1); + expect(result.state.localMessage.attachments ?? []).toHaveLength(1); + expect((result.state.message.attachments ?? [])[0].type).toBe('article'); + expect((result.state.localMessage.attachments ?? [])[0].type).toBe('article'); + expect(result.state.sendOptions.skip_enrich_url).toBe(true); + }); + + it('should handle message with loading link preview', async () => { + const { linkPreviewsMiddleware, messageComposer } = setup(); + messageComposer.linkPreviewsManager.state.next({ + previews: new Map([ + [ + 'https://example.com', + { + asset_url: 'https://example.com/image.jpg', + author_link: 'https://example.com/author', + author_name: 'Example Author', + image_url: 'https://example.com/image.jpg', + og_scrape_url: 'https://example.com', + text: 'Example description', + thumb_url: 'https://example.com/thumb.jpg', + title: 'Example', + title_link: 'https://example.com', + type: 'article', + status: LinkPreviewStatus.LOADING, + }, + ], + ]), + }); + + const result = await linkPreviewsMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + }, + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: 'https://example.com', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined; + expect(result.state.message.attachments ?? []).toHaveLength(0); + expect(result.state.localMessage.attachments ?? []).toHaveLength(0); + }); + + it('should handle message with failed link preview', async () => { + const { linkPreviewsMiddleware, messageComposer } = setup(); + messageComposer.linkPreviewsManager.state.next({ + previews: new Map([ + [ + 'https://example.com', + { + asset_url: 'https://example.com/image.jpg', + author_link: 'https://example.com/author', + author_name: 'Example Author', + image_url: 'https://example.com/image.jpg', + og_scrape_url: 'https://example.com', + text: 'Example description', + thumb_url: 'https://example.com/thumb.jpg', + title: 'Example', + title_link: 'https://example.com', + type: 'article', + status: LinkPreviewStatus.FAILED, + }, + ], + ]), + }); + + // Set up the previews in the manager + const result = await linkPreviewsMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + }, + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: 'https://example.com', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined; + expect(result.state.message.attachments ?? []).toHaveLength(0); + expect(result.state.localMessage.attachments ?? []).toHaveLength(0); + }); + + it('should handle message with dismissed link preview', async () => { + const { linkPreviewsMiddleware, messageComposer } = setup(); + messageComposer.linkPreviewsManager.state.next({ + previews: new Map([ + [ + 'https://example.com', + { + asset_url: 'https://example.com/image.jpg', + author_link: 'https://example.com/author', + author_name: 'Example Author', + image_url: 'https://example.com/image.jpg', + og_scrape_url: 'https://example.com', + text: 'Example description', + thumb_url: 'https://example.com/thumb.jpg', + title: 'Example', + title_link: 'https://example.com', + type: 'article', + status: LinkPreviewStatus.DISMISSED, + }, + ], + ]), + }); + + const result = await linkPreviewsMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + }, + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: 'https://example.com', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined; + expect(result.state.message.attachments ?? []).toHaveLength(0); + expect(result.state.localMessage.attachments ?? []).toHaveLength(0); + }); + + it('should handle message with multiple link previews and skip url enrichment server-side if some were dismissed', async () => { + const { linkPreviewsMiddleware, messageComposer } = setup(); + messageComposer.linkPreviewsManager.state.next({ + previews: new Map([ + [ + 'https://example1.com', + { + asset_url: 'https://example1.com/image.jpg', + author_link: 'https://example1.com/author', + author_name: 'Example Author 1', + image_url: 'https://example1.com/image.jpg', + og_scrape_url: 'https://example1.com', + text: 'Example description 1', + thumb_url: 'https://example1.com/thumb.jpg', + title: 'Example 1', + title_link: 'https://example1.com', + type: 'article', + status: LinkPreviewStatus.LOADED, + }, + ], + [ + 'https://example2.com', + { + asset_url: 'https://example2.com/image.jpg', + author_link: 'https://example2.com/author', + author_name: 'Example Author 2', + image_url: 'https://example2.com/image.jpg', + og_scrape_url: 'https://example2.com', + text: 'Example description 2', + thumb_url: 'https://example2.com/thumb.jpg', + title: 'Example 2', + title_link: 'https://example2.com', + type: 'article', + status: LinkPreviewStatus.LOADED, + }, + ], + [ + 'https://example3.com', + { + asset_url: 'https://example3.com/image.jpg', + author_link: 'https://example3.com/author', + author_name: 'Example Author 3', + image_url: 'https://example3.com/image.jpg', + og_scrape_url: 'https://example3.com', + text: 'Example description 3', + thumb_url: 'https://example3.com/thumb.jpg', + title: 'Example 3', + title_link: 'https://example3.com', + type: 'article', + status: LinkPreviewStatus.DISMISSED, + }, + ], + ]), + }); + + const result = await linkPreviewsMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + }, + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: 'https://example1.com https://example2.com https://example3.com', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined; + expect(result.state.message.attachments ?? []).toHaveLength(2); + expect(result.state.localMessage.attachments ?? []).toHaveLength(2); + expect((result.state.message.attachments ?? [])[0].type).toBe('article'); + expect((result.state.message.attachments ?? [])[1].type).toBe('article'); + expect(result.state.sendOptions.skip_enrich_url).toBe(true); + }); + + it('should not skip url enrichment server-side if not all previews could be loaded and none has been dismissed', async () => { + const { linkPreviewsMiddleware, messageComposer } = setup(); + messageComposer.linkPreviewsManager.state.next({ + previews: new Map([ + [ + 'https://example1.com', + { + asset_url: 'https://example1.com/image.jpg', + author_link: 'https://example1.com/author', + author_name: 'Example Author 1', + image_url: 'https://example1.com/image.jpg', + og_scrape_url: 'https://example1.com', + text: 'Example description 1', + thumb_url: 'https://example1.com/thumb.jpg', + title: 'Example 1', + title_link: 'https://example1.com', + type: 'article', + status: LinkPreviewStatus.LOADED, + }, + ], + [ + 'https://example2.com', + { + asset_url: 'https://example2.com/image.jpg', + author_link: 'https://example2.com/author', + author_name: 'Example Author 2', + image_url: 'https://example2.com/image.jpg', + og_scrape_url: 'https://example2.com', + text: 'Example description 2', + thumb_url: 'https://example2.com/thumb.jpg', + title: 'Example 2', + title_link: 'https://example2.com', + type: 'article', + status: LinkPreviewStatus.LOADING, + }, + ], + ]), + }); + + const result = await linkPreviewsMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + }, + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: 'https://example1.com https://example2.com', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined; + expect(result.state.message.attachments ?? []).toHaveLength(0); // will be added server-side + expect(result.state.localMessage.attachments ?? []).toHaveLength(0); + expect(result.state.sendOptions.skip_enrich_url).toBeUndefined; + }); + + it('should add link previews to existing attachments array', async () => { + const { linkPreviewsMiddleware, messageComposer } = setup(); + messageComposer.linkPreviewsManager.state.next({ + previews: new Map([ + [ + 'https://example.com', + { + asset_url: 'https://example.com/image.jpg', + author_link: 'https://example.com/author', + author_name: 'Example Author', + image_url: 'https://example.com/image.jpg', + og_scrape_url: 'https://example.com', + text: 'Example description', + thumb_url: 'https://example.com/thumb.jpg', + title: 'Example', + title_link: 'https://example.com', + type: 'article', + status: LinkPreviewStatus.LOADED, + }, + ], + ]), + }); + + const result = await linkPreviewsMiddleware.compose({ + input: { + state: { + message: { + attachments: [ + { + type: 'image', + image_url: 'https://example.com/image.jpg', + }, + ], + id: 'test-id', + parent_id: undefined, + type: 'regular', + }, + localMessage: { + attachments: [ + { + type: 'image', + image_url: 'https://example.com/image.jpg', + }, + ], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: 'https://example.com', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined; + expect(result.state.message.attachments ?? []).toHaveLength(2); + expect(result.state.localMessage.attachments ?? []).toHaveLength(2); + expect((result.state.message.attachments ?? [])[0].type).toBe('image'); + expect((result.state.localMessage.attachments ?? [])[0].type).toBe('image'); + expect((result.state.message.attachments ?? [])[1].type).toBe('article'); + expect((result.state.localMessage.attachments ?? [])[1].type).toBe('article'); + }); +}); + +const setupForDraft = ({ + composition, + config, +}: { + composition?: DraftResponse | LocalMessage; + config?: Partial; + message?: DraftMessage | LocalMessage; +} = {}) => { + vi.clearAllMocks(); + + const mockClient = new StreamChat('apiKey', 'apiSecret'); + mockClient.enrichURL = vi.fn().mockResolvedValue(enrichURLReturnValue); + + const mockChannel = mockClient.channel('messaging', 'test-channel', { + members: [], + }); + mockChannel.getConfig = vi.fn().mockImplementation(() => ({ url_enrichment: true })); + const messageComposer = new MessageComposer({ + client: mockClient, + composition, + compositionContext: mockChannel, + config: { linkPreviews: config }, + }); + + const linkPreviewsMiddleware = + createDraftLinkPreviewsCompositionMiddleware(messageComposer); + + return { linkPreviewsMiddleware, mockClient, mockChannel, messageComposer }; +}; +describe('DraftLinkPreviewsMiddleware', () => { + it('should handle draft without link previews', async () => { + const { linkPreviewsMiddleware } = setupForDraft(); + const result = await linkPreviewsMiddleware.compose({ + input: { + state: { + draft: { + text: '', + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined(); + expect(result.state.draft.attachments).toBeUndefined(); + }); + + it('should initiate from draft with loaded link previews', async () => { + const { linkPreviewsMiddleware, messageComposer } = setupForDraft(); + const linkPreview: LinkPreview = { + status: LinkPreviewStatus.LOADED, + type: 'article', + title: 'Example Article', + text: 'Example description', + image_url: 'https://example.com/image.jpg', + og_scrape_url: 'https://example.com', + asset_url: 'https://example.com/asset.jpg', + author_link: 'https://example.com/author', + author_name: 'Example Author', + thumb_url: 'https://example.com/thumb.jpg', + title_link: 'https://example.com', + }; + + vi.spyOn( + messageComposer.linkPreviewsManager, + 'loadedPreviews', + 'get', + ).mockReturnValue([linkPreview]); + + const result = await linkPreviewsMiddleware.compose({ + input: { + state: { + draft: { + text: '', + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined(); + expect(result.state.draft.attachments).toHaveLength(1); + expect(result.state.draft.attachments![0].type).toBe('article'); + expect(result.state.draft.attachments![0].title).toBe('Example Article'); + expect('state' in result.state.draft.attachments![0]).toBeFalsy(); + }); + + it('should merge link previews with existing draft attachments', async () => { + const { linkPreviewsMiddleware, messageComposer } = setupForDraft(); + const existingAttachment = { + type: 'image', + image_url: 'https://example.com/image.jpg', + }; + + const linkPreview: LinkPreview = { + status: LinkPreviewStatus.LOADED, + type: 'article', + title: 'Example Article', + text: 'Example description', + image_url: 'https://example.com/article.jpg', + og_scrape_url: 'https://example.com', + asset_url: 'https://example.com/asset.jpg', + author_link: 'https://example.com/author', + author_name: 'Example Author', + thumb_url: 'https://example.com/thumb.jpg', + title_link: 'https://example.com', + }; + + vi.spyOn( + messageComposer.linkPreviewsManager, + 'loadedPreviews', + 'get', + ).mockReturnValue([linkPreview]); + + const result = await linkPreviewsMiddleware.compose({ + input: { + state: { + draft: { + text: '', + attachments: [existingAttachment], + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined(); + expect(result.state.draft.attachments).toHaveLength(2); + expect(result.state.draft.attachments![0]).toEqual(existingAttachment); + expect(result.state.draft.attachments![1].type).toBe('article'); + expect('state' in result.state.draft.attachments![1]).toBeFalsy(); + }); + + it('should handle case when linkPreviewsManager is not available', async () => { + const { messageComposer } = setupForDraft(); + messageComposer.linkPreviewsManager = undefined as any; + const linkPreviewsMiddlewareWithUndefinedManager = + createDraftLinkPreviewsCompositionMiddleware(messageComposer); + + const result = await linkPreviewsMiddlewareWithUndefinedManager.compose({ + input: { + state: { + draft: { + text: '', + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined(); + expect(result.state.draft.attachments).toBeUndefined(); + }); + + it('should call cancelURLEnrichment', async () => { + const { linkPreviewsMiddleware, messageComposer } = setupForDraft(); + const cancelURLEnrichment = vi.fn(); + messageComposer.linkPreviewsManager.cancelURLEnrichment = cancelURLEnrichment; + + await linkPreviewsMiddleware.compose({ + input: { + state: { + draft: { + text: '', + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(cancelURLEnrichment).toHaveBeenCalled(); + }); +}); diff --git a/test/unit/MessageComposer/middleware/messageComposer/messageComposerState.test.ts b/test/unit/MessageComposer/middleware/messageComposer/messageComposerState.test.ts new file mode 100644 index 0000000000..924e9bcddf --- /dev/null +++ b/test/unit/MessageComposer/middleware/messageComposer/messageComposerState.test.ts @@ -0,0 +1,461 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { createMessageComposerStateCompositionMiddleware } from '../../../../../src/messageComposer/middleware/messageComposer/messageComposerState'; +import { MessageComposer } from '../../../../../src/messageComposer/messageComposer'; +import { Channel } from '../../../../../src/channel'; +import { StreamChat } from '../../../../../src/client'; +import { LocalMessage } from '../../../../../src/types'; +import { createDraftMessageComposerStateCompositionMiddleware } from '../../../../../src/messageComposer/middleware/messageComposer/messageComposerState'; + +describe('MessageComposerStateMiddleware', () => { + let channel: Channel; + let client: StreamChat; + let messageComposer: MessageComposer; + let messageComposerStateMiddleware: ReturnType< + typeof createMessageComposerStateCompositionMiddleware + >; + + beforeEach(() => { + // Create a real StreamChat instance with minimal implementation + client = new StreamChat('apiKey', { + enableInsights: false, + enableWSFallback: false, + }); + + channel = new Channel(client, 'messaging', 'test-channel', { + members: [], + }); + + // Use the messageComposer property from the channel + messageComposer = channel.messageComposer; + + // Create the middleware + messageComposerStateMiddleware = + createMessageComposerStateCompositionMiddleware(messageComposer); + }); + + it('should handle message without quoted message or poll', async () => { + // Mock the composer properties + vi.spyOn(messageComposer, 'quotedMessage', 'get').mockReturnValue(null); + vi.spyOn(messageComposer, 'pollId', 'get').mockReturnValue(null); + + const result = await messageComposerStateMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + }, + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.state.message.quoted_message_id).toBeUndefined(); + expect(result.state.message.poll_id).toBeUndefined(); + expect(result.state.localMessage.quoted_message_id).toBeUndefined(); + expect(result.state.localMessage.poll_id).toBeUndefined(); + expect(result.state.localMessage.quoted_message).toBeUndefined(); + }); + + it('should handle message with quoted message', async () => { + // Create a mock quoted message + const quotedMessage: LocalMessage = { + id: 'quoted-message-id', + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: 'This is a quoted message', + type: 'regular', + updated_at: new Date(), + }; + + // Mock the composer properties + vi.spyOn(messageComposer, 'quotedMessage', 'get').mockReturnValue(quotedMessage); + vi.spyOn(messageComposer, 'pollId', 'get').mockReturnValue(null); + + const result = await messageComposerStateMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + }, + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.state.message.quoted_message_id).toBe('quoted-message-id'); + expect(result.state.localMessage.quoted_message_id).toBe('quoted-message-id'); + expect(result.state.localMessage.quoted_message).toBe(quotedMessage); + }); + + it('should handle message with poll', async () => { + // Mock the composer properties + vi.spyOn(messageComposer, 'quotedMessage', 'get').mockReturnValue(null); + vi.spyOn(messageComposer, 'pollId', 'get').mockReturnValue('poll-id-123'); + + const result = await messageComposerStateMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + }, + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.state.message.poll_id).toBe('poll-id-123'); + expect(result.state.localMessage.poll_id).toBe('poll-id-123'); + }); + + it('should handle message with both quoted message and poll', async () => { + // Create a mock quoted message + const quotedMessage: LocalMessage = { + id: 'quoted-message-id', + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: 'This is a quoted message', + type: 'regular', + updated_at: new Date(), + }; + + // Mock the composer properties + vi.spyOn(messageComposer, 'quotedMessage', 'get').mockReturnValue(quotedMessage); + vi.spyOn(messageComposer, 'pollId', 'get').mockReturnValue('poll-id-123'); + + const result = await messageComposerStateMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + }, + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.state.message.quoted_message_id).toBe('quoted-message-id'); + expect(result.state.message.poll_id).toBe('poll-id-123'); + expect(result.state.localMessage.quoted_message_id).toBe('quoted-message-id'); + expect(result.state.localMessage.poll_id).toBe('poll-id-123'); + expect(result.state.localMessage.quoted_message).toBe(quotedMessage); + }); + + it('should preserve existing message and localMessage properties', async () => { + // Create a mock quoted message + const quotedMessage: LocalMessage = { + id: 'quoted-message-id', + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: 'This is a quoted message', + type: 'regular', + updated_at: new Date(), + }; + + // Mock the composer properties + vi.spyOn(messageComposer, 'quotedMessage', 'get').mockReturnValue(quotedMessage); + vi.spyOn(messageComposer, 'pollId', 'get').mockReturnValue('poll-id-123'); + + const result = await messageComposerStateMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + text: 'Original message text', + }, + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: 'Original local message text', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + // Verify that the original properties are preserved + expect(result.state.message.text).toBe('Original message text'); + expect(result.state.localMessage.text).toBe('Original local message text'); + + // Verify that the new properties are added + expect(result.state.message.quoted_message_id).toBe('quoted-message-id'); + expect(result.state.message.poll_id).toBe('poll-id-123'); + expect(result.state.localMessage.quoted_message_id).toBe('quoted-message-id'); + expect(result.state.localMessage.poll_id).toBe('poll-id-123'); + expect(result.state.localMessage.quoted_message).toBe(quotedMessage); + }); +}); + +describe('DraftMessageComposerStateMiddleware', () => { + let channel: Channel; + let client: StreamChat; + let messageComposer: MessageComposer; + let messageComposerStateMiddleware: ReturnType< + typeof createDraftMessageComposerStateCompositionMiddleware + >; + + beforeEach(() => { + client = { + userID: 'currentUser', + user: { id: 'currentUser' }, + } as any; + + channel = { + getClient: vi.fn().mockReturnValue(client), + state: { + members: {}, + watchers: {}, + }, + getConfig: vi.fn().mockReturnValue({ commands: [] }), + } as any; + + messageComposer = { + channel, + client, + quotedMessage: undefined, + pollId: undefined, + } as any; + + messageComposerStateMiddleware = + createDraftMessageComposerStateCompositionMiddleware(messageComposer); + }); + + it('should handle draft without quoted message or poll', async () => { + const result = await messageComposerStateMiddleware.compose({ + input: { + state: { + draft: { + text: '', + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined(); + expect(result.state.draft.quoted_message_id).toBeUndefined(); + expect(result.state.draft.poll_id).toBeUndefined(); + }); + + it('should handle draft with quoted message', async () => { + const quotedMessage = { + id: 'quoted-message-id', + type: 'regular' as const, + created_at: new Date(), + deleted_at: null, + pinned_at: null, + status: 'received', + updated_at: new Date(), + }; + + vi.spyOn(messageComposer, 'quotedMessage', 'get').mockReturnValue(quotedMessage); + + const result = await messageComposerStateMiddleware.compose({ + input: { + state: { + draft: { + text: '', + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined(); + expect(result.state.draft.quoted_message_id).toBe('quoted-message-id'); + expect(result.state.draft.poll_id).toBeUndefined(); + }); + + it('should handle draft with poll', async () => { + vi.spyOn(messageComposer, 'pollId', 'get').mockReturnValue('poll-id-123'); + + const result = await messageComposerStateMiddleware.compose({ + input: { + state: { + draft: { + text: '', + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined(); + expect(result.state.draft.quoted_message_id).toBeUndefined(); + expect(result.state.draft.poll_id).toBe('poll-id-123'); + }); + + it('should handle draft with both quoted message and poll', async () => { + const quotedMessage = { + id: 'quoted-message-id', + type: 'regular' as const, + created_at: new Date(), + deleted_at: null, + pinned_at: null, + status: 'received', + updated_at: new Date(), + }; + + vi.spyOn(messageComposer, 'quotedMessage', 'get').mockReturnValue(quotedMessage); + vi.spyOn(messageComposer, 'pollId', 'get').mockReturnValue('poll-id-123'); + + const result = await messageComposerStateMiddleware.compose({ + input: { + state: { + draft: { + text: '', + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined(); + expect(result.state.draft.quoted_message_id).toBe('quoted-message-id'); + expect(result.state.draft.poll_id).toBe('poll-id-123'); + }); + + it('should preserve existing draft properties', async () => { + const quotedMessage = { + id: 'quoted-message-id', + type: 'regular' as const, + created_at: new Date(), + deleted_at: null, + pinned_at: null, + status: 'received', + updated_at: new Date(), + }; + + vi.spyOn(messageComposer, 'quotedMessage', 'get').mockReturnValue(quotedMessage); + vi.spyOn(messageComposer, 'pollId', 'get').mockReturnValue('poll-id-123'); + + const result = await messageComposerStateMiddleware.compose({ + input: { + state: { + draft: { + text: 'Original draft text', + attachments: [ + { + type: 'image', + image_url: 'https://example.com/image.jpg', + }, + ], + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined(); + expect(result.state.draft.text).toBe('Original draft text'); + expect(result.state.draft.attachments).toHaveLength(1); + expect(result.state.draft.attachments![0].type).toBe('image'); + expect(result.state.draft.quoted_message_id).toBe('quoted-message-id'); + expect(result.state.draft.poll_id).toBe('poll-id-123'); + }); +}); diff --git a/test/unit/MessageComposer/middleware/messageComposer/textComposer.test.ts b/test/unit/MessageComposer/middleware/messageComposer/textComposer.test.ts new file mode 100644 index 0000000000..87ee2a79f6 --- /dev/null +++ b/test/unit/MessageComposer/middleware/messageComposer/textComposer.test.ts @@ -0,0 +1,600 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { Channel } from '../../../../../src/channel'; +import { StreamChat } from '../../../../../src/client'; +import { MessageComposer } from '../../../../../src/messageComposer/messageComposer'; +import { createTextComposerCompositionMiddleware } from '../../../../../src/messageComposer/middleware/messageComposer/textComposer'; +import { createDraftTextComposerCompositionMiddleware } from '../../../../../src/messageComposer/middleware/messageComposer/textComposer'; + +describe('TextComposerMiddleware', () => { + let channel: Channel; + let client: StreamChat; + let messageComposer: MessageComposer; + let textComposerMiddleware: ReturnType; + + beforeEach(() => { + client = { + userID: 'currentUser', + user: { id: 'currentUser' }, + } as any; + + channel = { + getClient: vi.fn().mockReturnValue(client), + state: { + members: {}, + watchers: {}, + }, + getConfig: vi.fn().mockReturnValue({ commands: [] }), + } as any; + + const textComposer = { + get text() { + return ''; + }, + get mentionedUsers() { + return []; + }, + }; + + const attachmentManager = { + get uploadsInProgressCount() { + return 0; + }, + get successfulUploads() { + return []; + }, + }; + + const linkPreviewsManager = { + state: { + getLatestValue: () => ({ + previews: new Map(), + }), + }, + }; + + const pollComposer = { + state: { + getLatestValue: () => ({ + data: { + options: [], + name: '', + max_votes_allowed: '', + id: '', + user_id: '', + voting_visibility: 'public', + allow_answers: false, + allow_user_suggested_options: false, + description: '', + enforce_unique_vote: true, + }, + errors: {}, + }), + }, + get canCreatePoll() { + return false; + }, + }; + + messageComposer = { + channel, + config: {}, + threadId: undefined, + client, + textComposer, + attachmentManager, + linkPreviewsManager, + pollComposer, + get lastChangeOriginIsLocal() { + return true; + }, + editedMessage: undefined, + get quotedMessage() { + return undefined; + }, + } as any; + + textComposerMiddleware = createTextComposerCompositionMiddleware(messageComposer); + }); + + it('should handle empty message', async () => { + const result = await textComposerMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + }, + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined; + expect(result.state.message.text).toBeUndefined; + expect(result.state.localMessage.text).toBe(''); + }); + + it('should handle message with text', async () => { + vi.spyOn(messageComposer.textComposer, 'text', 'get').mockReturnValue('Hello world'); + + const result = await textComposerMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + }, + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined; + expect(result.state.message.text).toBe('Hello world'); + expect(result.state.localMessage.text).toBe('Hello world'); + }); + + it('should handle message with mentions', async () => { + vi.spyOn(messageComposer.textComposer, 'text', 'get').mockReturnValue( + '@user1 @user2', + ); + vi.spyOn(messageComposer.textComposer, 'mentionedUsers', 'get').mockReturnValue([ + { id: 'user1', name: 'User 1' }, + { id: 'user2', name: 'User 2' }, + ]); + + const result = await textComposerMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + mentioned_users: [] as string[], + }, + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [] as Array<{ id: string; name: string }>, + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined; + expect(result.state.message.text).toBe('@user1 @user2'); + expect(result.state.localMessage.text).toBe('@user1 @user2'); + expect(result.state.message.mentioned_users).toHaveLength(2); + expect(result.state.localMessage.mentioned_users).toHaveLength(2); + expect(result.state.message.mentioned_users).toEqual(['user1', 'user2']); + expect(result.state.localMessage.mentioned_users?.[0]?.id).toBe('user1'); + expect(result.state.localMessage.mentioned_users?.[1]?.id).toBe('user2'); + }); + + it('should remove stale mentions', async () => { + vi.spyOn(messageComposer.textComposer, 'text', 'get').mockReturnValue('@user1'); + vi.spyOn(messageComposer.textComposer, 'mentionedUsers', 'get').mockReturnValue([ + { id: 'user1', name: 'User 1' }, + { id: 'user2', name: 'User 2' }, + ]); + + const result = await textComposerMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + mentioned_users: [] as string[], + }, + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [] as Array<{ id: string; name: string }>, + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined; + expect(result.state.message.text).toBe('@user1'); + expect(result.state.localMessage.text).toBe('@user1'); + expect(result.state.message.mentioned_users).toHaveLength(1); + expect(result.state.localMessage.mentioned_users).toHaveLength(1); + expect(result.state.message.mentioned_users).toEqual(['user1']); + expect(result.state.localMessage.mentioned_users?.[0]?.id).toBe('user1'); + }); + + it('should handle message with commands', async () => { + vi.spyOn(messageComposer.textComposer, 'text', 'get').mockReturnValue('/giphy hello'); + + const result = await textComposerMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + }, + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined; + expect(result.state.message.text).toBe('/giphy hello'); + expect(result.state.localMessage.text).toBe('/giphy hello'); + }); + + it('should handle message with emoji', async () => { + vi.spyOn(messageComposer.textComposer, 'text', 'get').mockReturnValue('Hello 👋'); + + const result = await textComposerMiddleware.compose({ + input: { + state: { + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + }, + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined; + expect(result.state.message.text).toBe('Hello 👋'); + expect(result.state.localMessage.text).toBe('Hello 👋'); + }); +}); + +describe('DraftTextComposerMiddleware', () => { + let channel: Channel; + let client: StreamChat; + let messageComposer: MessageComposer; + let draftTextComposerMiddleware: ReturnType< + typeof createDraftTextComposerCompositionMiddleware + >; + + beforeEach(() => { + client = { + userID: 'currentUser', + user: { id: 'currentUser' }, + } as any; + + channel = { + getClient: vi.fn().mockReturnValue(client), + state: { + members: {}, + watchers: {}, + }, + getConfig: vi.fn().mockReturnValue({ commands: [] }), + } as any; + + const textComposer = { + get text() { + return ''; + }, + get mentionedUsers() { + return []; + }, + }; + + const attachmentManager = { + get uploadsInProgressCount() { + return 0; + }, + get successfulUploads() { + return []; + }, + }; + + const linkPreviewsManager = { + state: { + getLatestValue: () => ({ + previews: new Map(), + }), + }, + }; + + const pollComposer = { + state: { + getLatestValue: () => ({ + data: { + options: [], + name: '', + max_votes_allowed: '', + id: '', + user_id: '', + voting_visibility: 'public', + allow_answers: false, + allow_user_suggested_options: false, + description: '', + enforce_unique_vote: true, + }, + errors: {}, + }), + }, + get canCreatePoll() { + return false; + }, + }; + + messageComposer = { + channel, + config: {}, + threadId: undefined, + client, + textComposer, + attachmentManager, + linkPreviewsManager, + pollComposer, + get lastChangeOriginIsLocal() { + return true; + }, + editedMessage: undefined, + get quotedMessage() { + return undefined; + }, + } as any; + + draftTextComposerMiddleware = + createDraftTextComposerCompositionMiddleware(messageComposer); + }); + + it('should handle empty draft', async () => { + const result = await draftTextComposerMiddleware.compose({ + input: { + state: { + draft: { + id: 'test-id', + parent_id: undefined, + text: '', + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined(); + expect(result.state.draft.text).toBe(''); + expect(result.state.draft.mentioned_users).toBeUndefined(); + }); + + it('should handle draft with text', async () => { + vi.spyOn(messageComposer.textComposer, 'text', 'get').mockReturnValue('Hello world'); + + const result = await draftTextComposerMiddleware.compose({ + input: { + state: { + draft: { + id: 'test-id', + parent_id: undefined, + text: '', + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined(); + expect(result.state.draft.text).toBe('Hello world'); + expect(result.state.draft.mentioned_users).toBeUndefined(); + }); + + it('should handle draft with mentions', async () => { + vi.spyOn(messageComposer.textComposer, 'text', 'get').mockReturnValue( + '@user1 @user2', + ); + vi.spyOn(messageComposer.textComposer, 'mentionedUsers', 'get').mockReturnValue([ + { id: 'user1', name: 'User 1' }, + { id: 'user2', name: 'User 2' }, + ]); + + const result = await draftTextComposerMiddleware.compose({ + input: { + state: { + draft: { + id: 'test-id', + parent_id: undefined, + text: '', + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined(); + expect(result.state.draft.text).toBe('@user1 @user2'); + expect(result.state.draft.mentioned_users).toEqual(['user1', 'user2']); + }); + + it('should remove stale mentions', async () => { + vi.spyOn(messageComposer.textComposer, 'text', 'get').mockReturnValue('@user1'); + vi.spyOn(messageComposer.textComposer, 'mentionedUsers', 'get').mockReturnValue([ + { id: 'user1', name: 'User 1' }, + { id: 'user2', name: 'User 2' }, + ]); + + const result = await draftTextComposerMiddleware.compose({ + input: { + state: { + draft: { + id: 'test-id', + parent_id: undefined, + text: '', + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined(); + expect(result.state.draft.text).toBe('@user1'); + expect(result.state.draft.mentioned_users).toEqual(['user1']); + }); + + it('should handle empty mentionedUsers array', async () => { + vi.spyOn(messageComposer.textComposer, 'text', 'get').mockReturnValue('Hello world'); + vi.spyOn(messageComposer.textComposer, 'mentionedUsers', 'get').mockReturnValue([]); + + const result = await draftTextComposerMiddleware.compose({ + input: { + state: { + draft: { + id: 'test-id', + parent_id: undefined, + text: '', + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined(); + expect(result.state.draft.text).toBe('Hello world'); + expect(result.state.draft.mentioned_users).toBeUndefined(); + }); + + it('should preserve existing draft properties', async () => { + vi.spyOn(messageComposer.textComposer, 'text', 'get').mockReturnValue('Hello world'); + vi.spyOn(messageComposer.textComposer, 'mentionedUsers', 'get').mockReturnValue([ + { id: 'user1', name: 'User 1' }, + ]); + + const result = await draftTextComposerMiddleware.compose({ + input: { + state: { + draft: { + id: 'test-id', + parent_id: undefined, + text: '', + attachments: [ + { + type: 'image', + image_url: 'https://example.com/image.jpg', + }, + ], + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined(); + expect(result.state.draft.text).toBe('Hello world'); + expect(result.state.draft.mentioned_users).toBeUndefined; + expect(result.state.draft.attachments).toHaveLength(1); + expect(result.state.draft.attachments![0].type).toBe('image'); + }); + + it('should handle when textComposer is not available', async () => { + messageComposer.textComposer = undefined as any; + + const result = await draftTextComposerMiddleware.compose({ + input: { + state: { + draft: { + id: 'test-id', + parent_id: undefined, + text: '', + }, + }, + }, + nextHandler: async (input) => input, + }); + + expect(result.status).toBeUndefined(); + expect(result.state.draft.text).toBe(''); + }); +}); diff --git a/test/unit/MessageComposer/middleware/pollComposer/composition.test.ts b/test/unit/MessageComposer/middleware/pollComposer/composition.test.ts new file mode 100644 index 0000000000..db0550e3e6 --- /dev/null +++ b/test/unit/MessageComposer/middleware/pollComposer/composition.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { createPollCompositionValidationMiddleware } from '../../../../../src/messageComposer/middleware/pollComposer/composition'; +import { MessageComposer } from '../../../../../src/messageComposer/messageComposer'; +import { PollComposer } from '../../../../../src/messageComposer/pollComposer'; +import { VotingVisibility } from '../../../../../src/types'; +import type { Middleware } from '../../../../../src/middleware'; +import type { PollComposerCompositionMiddlewareValueState } from '../../../../../src/messageComposer/middleware/pollComposer/types'; +import type { MiddlewareHandler } from '../../../../../src/middleware'; + +describe('PollComposerCompositionMiddleware', () => { + let messageComposer: MessageComposer; + let pollComposer: PollComposer; + let validationMiddleware: Middleware; + + beforeEach(() => { + messageComposer = { + client: { + user: { id: 'user-id' }, + }, + } as any; + + pollComposer = new PollComposer({ composer: messageComposer }); + messageComposer.pollComposer = pollComposer; + validationMiddleware = createPollCompositionValidationMiddleware(messageComposer); + }); + + it('should allow composition when poll can be created', async () => { + // Set up a valid poll state + pollComposer.state.next({ + data: { + options: [{ id: 'option-id', text: 'Option 1' }], + name: 'Test Poll', + max_votes_allowed: '', + id: 'test-id', + user_id: 'user-id', + voting_visibility: VotingVisibility.public, + allow_answers: false, + allow_user_suggested_options: false, + description: '', + enforce_unique_vote: true, + }, + errors: {}, + }); + + // Mock the canCreatePoll getter + vi.spyOn(pollComposer, 'canCreatePoll', 'get').mockReturnValue(true); + + const result = await ( + validationMiddleware.compose as MiddlewareHandler + )({ + input: { + state: { + data: { + ...pollComposer.state.getLatestValue().data, + max_votes_allowed: undefined, + options: [{ text: 'Option 1' }], + }, + errors: {}, + }, + }, + nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), + }); + + expect(result.status).toBeUndefined; + }); + + it('should discard composition when poll cannot be created', async () => { + // Set up an invalid poll state (no name) + pollComposer.state.next({ + data: { + options: [{ id: 'option-id', text: 'Option 1' }], + name: '', + max_votes_allowed: '', + id: 'test-id', + user_id: 'user-id', + voting_visibility: VotingVisibility.public, + allow_answers: false, + allow_user_suggested_options: false, + description: '', + enforce_unique_vote: true, + }, + errors: {}, + }); + + // Mock the canCreatePoll getter + vi.spyOn(pollComposer, 'canCreatePoll', 'get').mockReturnValue(false); + + const result = await ( + validationMiddleware.compose as MiddlewareHandler + )({ + input: { + state: { + data: { + ...pollComposer.state.getLatestValue().data, + max_votes_allowed: undefined, + options: [{ text: 'Option 1' }], + }, + errors: {}, + }, + }, + nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), + }); + + expect(result.status).toBe('discard'); + }); +}); diff --git a/test/unit/MessageComposer/middleware/pollComposer/state.test.ts b/test/unit/MessageComposer/middleware/pollComposer/state.test.ts new file mode 100644 index 0000000000..2d4928d24c --- /dev/null +++ b/test/unit/MessageComposer/middleware/pollComposer/state.test.ts @@ -0,0 +1,322 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { createPollComposerStateMiddleware } from '../../../../../src/messageComposer/middleware/pollComposer/state'; +import { VotingVisibility } from '../../../../../src/types'; +import { generateUUIDv4 } from '../../../../../src/utils'; + +// Mock dependencies +vi.mock('../../../../../src/utils', () => ({ + generateUUIDv4: vi.fn().mockReturnValue('test-uuid'), +})); + +describe('PollComposerStateMiddleware', () => { + let stateMiddleware: ReturnType; + let initialState: any; + + beforeEach(() => { + stateMiddleware = createPollComposerStateMiddleware(); + initialState = { + data: { + allow_answers: false, + allow_user_suggested_options: false, + description: '', + enforce_unique_vote: true, + id: 'test-id', + max_votes_allowed: '', + name: '', + options: [{ id: 'option-id', text: '' }], + user_id: 'user-id', + voting_visibility: VotingVisibility.public, + }, + errors: {}, + }; + }); + + describe('handleFieldChange', () => { + it('should update name field', async () => { + const result = await stateMiddleware.handleFieldChange({ + input: { + state: { + nextState: { ...initialState }, + previousState: { ...initialState }, + targetFields: { name: 'Test Poll' }, + }, + }, + nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), + }); + + expect(result.state.nextState.data.name).toBe('Test Poll'); + expect(result.status).toBeUndefined; + }); + + it('should validate max_votes_allowed field with invalid value', async () => { + const result = await stateMiddleware.handleFieldChange({ + input: { + state: { + nextState: { ...initialState }, + previousState: { ...initialState }, + targetFields: { max_votes_allowed: '1' }, // Invalid value (less than 2) + }, + }, + nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), + }); + + expect(result.state.nextState.errors.max_votes_allowed).toBeDefined(); + expect(result.state.nextState.data.max_votes_allowed).toBe('1'); + expect(result.status).toBeUndefined; + }); + + it('should not validate max_votes_allowed field with valid value if enforce_unique_vote is true', async () => { + const result = await stateMiddleware.handleFieldChange({ + input: { + state: { + nextState: { + ...initialState, + data: { ...initialState.data, enforce_unique_vote: true }, + }, + previousState: { + ...initialState, + data: { ...initialState.data, enforce_unique_vote: true }, + }, + targetFields: { max_votes_allowed: '5' }, // Valid value (between 2 and 10) + }, + }, + nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), + }); + + expect(result.state.nextState.errors.max_votes_allowed).toBeDefined(); + expect(result.state.nextState.data.max_votes_allowed).toBe('5'); + expect(result.status).toBeUndefined; + }); + + it('should validate max_votes_allowed field with valid value if enforce_unique_vote is false', async () => { + const result = await stateMiddleware.handleFieldChange({ + input: { + state: { + nextState: { + ...initialState, + data: { ...initialState.data, enforce_unique_vote: false }, + }, + previousState: { + ...initialState, + data: { ...initialState.data, enforce_unique_vote: false }, + }, + targetFields: { max_votes_allowed: '5' }, // Valid value (between 2 and 10) + }, + }, + nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), + }); + + expect(result.state.nextState.errors.max_votes_allowed).toBeUndefined(); + expect(result.state.nextState.data.max_votes_allowed).toBe('5'); + expect(result.status).toBeUndefined; + }); + + it('should handle options field changes with single option update', async () => { + const result = await stateMiddleware.handleFieldChange({ + input: { + state: { + nextState: { ...initialState }, + previousState: { ...initialState }, + targetFields: { + options: [ + { + index: 0, + text: 'Option 1', + }, + ], + }, + }, + }, + nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), + }); + + expect(result.state.nextState.data.options[0].text).toBe('Option 1'); + expect(result.state.nextState.data.options.length).toBe(1); + expect(result.status).toBeUndefined; + }); + + it('should handle options field changes with array update', async () => { + const result = await stateMiddleware.handleFieldChange({ + input: { + state: { + nextState: { ...initialState }, + previousState: { ...initialState }, + targetFields: { + options: [ + { id: 'option-1', text: 'Option 1' }, + { id: 'option-2', text: 'Option 2' }, + ], + }, + }, + }, + nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), + }); + + expect(result.state.nextState.data.options.length).toBe(2); + expect(result.state.nextState.data.options[0].text).toBe('Option 1'); + expect(result.state.nextState.data.options[1].text).toBe('Option 2'); + expect(result.status).toBeUndefined; + }); + + it('should handle enforce_unique_vote field changes', async () => { + const result = await stateMiddleware.handleFieldChange({ + input: { + state: { + nextState: { ...initialState }, + previousState: { ...initialState }, + targetFields: { enforce_unique_vote: false }, + }, + }, + nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), + }); + + expect(result.state.nextState.data.enforce_unique_vote).toBe(false); + expect(result.state.nextState.data.max_votes_allowed).toBe(''); + expect(result.status).toBeUndefined; + }); + + it('should add a new empty option when the last option is filled', async () => { + const result = await stateMiddleware.handleFieldChange({ + input: { + state: { + nextState: { ...initialState }, + previousState: { ...initialState }, + targetFields: { + options: { + index: 0, + text: 'Option 1', + }, + }, + }, + }, + nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), + }); + + expect(result.state.nextState.data.options.length).toBe(2); + expect(result.state.nextState.data.options[0].text).toBe('Option 1'); + expect(result.state.nextState.data.options[1].text).toBe(''); + expect(result.status).toBeUndefined; + }); + + it('should remove an option when it is empty and there are more options after it', async () => { + // Set up initial state with two options + initialState.data.options = [ + { id: 'option-1', text: 'Option 1' }, + { id: 'option-2', text: '' }, + ]; + + const result = await stateMiddleware.handleFieldChange({ + input: { + state: { + nextState: { ...initialState }, + previousState: { ...initialState }, + targetFields: { + options: { + index: 0, + text: '', + }, + }, + }, + }, + nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), + }); + + expect(result.state.nextState.data.options.length).toBe(1); + expect(result.state.nextState.data.options[0].text).toBe(''); + expect(result.status).toBeUndefined; + }); + }); + + describe('handleFieldBlur', () => { + it('should validate name field on blur', async () => { + const result = await stateMiddleware.handleFieldBlur({ + input: { + state: { + nextState: { ...initialState }, + previousState: { ...initialState }, + targetFields: { name: '' }, + }, + }, + nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), + }); + + expect(result.state.nextState.errors.name).toBeDefined(); + expect(result.status).toBeUndefined; + }); + + it('should validate max_votes_allowed field on blur', async () => { + const result = await stateMiddleware.handleFieldBlur({ + input: { + state: { + nextState: { ...initialState }, + previousState: { ...initialState }, + targetFields: { max_votes_allowed: '1' }, + }, + }, + nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), + }); + + expect(result.state.nextState.errors.max_votes_allowed).toBeDefined(); + expect(result.status).toBeUndefined; + }); + + describe('options validation', () => { + it('should validate empty options on blur', async () => { + const result = await stateMiddleware.handleFieldBlur({ + input: { + state: { + nextState: { ...initialState }, + previousState: { ...initialState }, + targetFields: { options: [{ id: 'option-id', text: '' }] }, + }, + }, + nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), + }); + + expect(result.state.nextState.errors.options).toBeUndefined(); + }); + + it('should validate duplicate options on blur', async () => { + const result = await stateMiddleware.handleFieldBlur({ + input: { + state: { + nextState: { ...initialState }, + previousState: { ...initialState }, + targetFields: { + options: [ + { id: 'option-1', text: 'Same Text' }, + { id: 'option-2', text: 'Same Text' }, + ], + }, + }, + }, + nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), + }); + + expect(result.state.nextState.errors.options).toEqual({ + 'option-2': 'Option already exists', + }); + }); + + it('should pass validation for valid options', async () => { + const result = await stateMiddleware.handleFieldBlur({ + input: { + state: { + nextState: { ...initialState }, + previousState: { ...initialState }, + targetFields: { + options: [ + { id: 'option-1', text: 'Option 1' }, + { id: 'option-2', text: 'Option 2' }, + ], + }, + }, + }, + nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), + }); + + expect(result.state.nextState.errors.options).toBeUndefined(); + }); + }); + }); +}); diff --git a/test/unit/MessageComposer/middleware/textComposer/CommandSearchSource.test.ts b/test/unit/MessageComposer/middleware/textComposer/CommandSearchSource.test.ts new file mode 100644 index 0000000000..31e4b658c9 --- /dev/null +++ b/test/unit/MessageComposer/middleware/textComposer/CommandSearchSource.test.ts @@ -0,0 +1,85 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { CommandSearchSource } from '../../../../../src/messageComposer/middleware/textComposer/commands'; +import { Channel } from '../../../../../src/channel'; +import type { ChannelConfigWithInfo } from '../../../../../src/types'; + +describe('CommandSearchSource', () => { + let channel: Channel; + let mockCommands: any[]; + let getConfigMock: ReturnType; + + beforeEach(() => { + mockCommands = [ + { name: 'giphy', description: 'Post a random gif' }, + { name: 'ban', description: 'Ban a user' }, + { name: 'mute', description: 'Mute a user' }, + { name: 'unmute', description: 'Unmute a user' }, + ]; + + getConfigMock = vi.fn().mockReturnValue({ commands: mockCommands }); + channel = { + getConfig: getConfigMock, + } as any; + }); + + it('should initialize with correct type', () => { + const source = new CommandSearchSource(channel); + expect(source.type).toBe('commands'); + }); + + it('should filter commands based on search query', async () => { + const source = new CommandSearchSource(channel); + source.activate(); + + const result = await source.query('gi'); + expect(result.items).toHaveLength(1); + expect(result.items[0].name).toBe('giphy'); + }); + + it('should sort commands with prefix matches first', async () => { + const source = new CommandSearchSource(channel); + source.activate(); + + const result = await source.query('m'); + expect(result.items).toHaveLength(2); + expect(result.items[0].name).toBe('mute'); + expect(result.items[1].name).toBe('unmute'); + }); + + it('should return empty array for no matches', async () => { + const source = new CommandSearchSource(channel); + source.activate(); + + const result = await source.query('nonexistent'); + expect(result.items).toHaveLength(0); + }); + + it('should handle case-insensitive search', async () => { + const source = new CommandSearchSource(channel); + source.activate(); + + const result = await source.query('GI'); + expect(result.items).toHaveLength(1); + expect(result.items[0].name).toBe('giphy'); + + getConfigMock.mockReturnValueOnce({ + commands: mockCommands.map((command) => ({ + ...command, + name: command.name.toUpperCase(), + })), + }); + const result2 = await source.query('gi'); + expect(result2.items).toHaveLength(1); + expect(result2.items[0].name).toBe('GIPHY'); + }); + + it('should preserve items in state before first query', () => { + const source = new CommandSearchSource(channel); + source.activate(); + + const initialState = source.state.getLatestValue(); + const newState = source.getStateBeforeFirstQuery('test'); + + expect(newState.items).toEqual(initialState.items); + }); +}); diff --git a/test/unit/MessageComposer/middleware/textComposer/MentionsSearchSource.test.ts b/test/unit/MessageComposer/middleware/textComposer/MentionsSearchSource.test.ts new file mode 100644 index 0000000000..42cb04d04f --- /dev/null +++ b/test/unit/MessageComposer/middleware/textComposer/MentionsSearchSource.test.ts @@ -0,0 +1,295 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { + MentionsSearchSource, + calculateLevenshtein, +} from '../../../../../src/messageComposer/middleware/textComposer/mentions'; +import { Channel } from '../../../../../src/channel'; +import { StreamChat } from '../../../../../src/client'; +import { MAX_CHANNEL_MEMBER_COUNT_IN_CHANNEL_QUERY } from '../../../../../src/constants'; +import type { + ChannelMemberResponse, + Mute, + UserResponse, + UserFilters, + MemberFilters, +} from '../../../../../src/types'; + +describe('calculateLevenshtein', () => { + it('should return length of first string if second is empty', () => { + expect(calculateLevenshtein('test', '')).toBe(4); + }); + + it('should return length of second string if first is empty', () => { + expect(calculateLevenshtein('', 'test')).toBe(4); + }); + + it('should return 0 for identical strings', () => { + expect(calculateLevenshtein('test', 'test')).toBe(0); + }); + + it('should calculate correct distance for single character difference', () => { + expect(calculateLevenshtein('test', 'tost')).toBe(1); + }); + + it('should calculate correct distance for insertion', () => { + expect(calculateLevenshtein('test', 'tests')).toBe(1); + }); + + it('should calculate correct distance for deletion', () => { + expect(calculateLevenshtein('tests', 'test')).toBe(1); + }); + + it('should calculate correct distance for substitution', () => { + expect(calculateLevenshtein('test', 'tost')).toBe(1); + }); + + it('should calculate correct distance for multiple operations', () => { + expect(calculateLevenshtein('kitten', 'sitting')).toBe(3); + }); + + it('should handle case sensitivity', () => { + expect(calculateLevenshtein('Test', 'test')).toBe(1); + }); + + it('should handle special characters', () => { + expect(calculateLevenshtein('test!', 'test?')).toBe(1); + }); +}); + +describe('MentionsSearchSource', () => { + let channel: Channel; + let client: StreamChat; + let mockUsers: UserResponse[]; + let mockMembers: Record; + + beforeEach(() => { + mockUsers = [ + { id: 'user1', name: 'John Doe' }, + { id: 'user2', name: 'Jane Smith' }, + { id: 'user3', name: 'Bob Wilson' }, + { id: 'currentUser', name: 'Alice Johnson' }, + ]; + + mockMembers = { + user1: { user: { id: 'user1', name: 'John Doe' } }, + user2: { user: { id: 'user2', name: 'Jane Smith' } }, + currentUser: { user: { id: 'currentUser', name: 'Alice Johnson' } }, + }; + + client = { + userID: 'currentUser', + queryUsers: vi.fn().mockResolvedValue({ users: mockUsers }), + mutedUsers: [], + } as any; + + channel = { + getClient: vi.fn().mockReturnValue(client), + state: { + members: mockMembers, + watchers: {}, + }, + queryMembers: vi.fn().mockResolvedValue({ members: Object.values(mockMembers) }), + } as any; + }); + + it('should initialize with correct type', () => { + const source = new MentionsSearchSource(channel); + expect(source.type).toBe('mentions'); + expect(source.config.mentionAllAppUsers).toBeUndefined; + expect(source.config.textComposerText).toBeUndefined; + expect(source.config.transliterate).toBeUndefined; + + const customizedSource = new MentionsSearchSource(channel, { + mentionAllAppUsers: true, + textComposerText: '@', + transliterate: (text: string) => text.toLowerCase(), + }); + expect(customizedSource.config.mentionAllAppUsers).toBe(true); + expect(customizedSource.config.textComposerText).toBe('@'); + expect(customizedSource.transliterate).toBeInstanceOf(Function); + }); + + it('should search members locally when all members are loaded', async () => { + const source = new MentionsSearchSource(channel); + source.activate(); + source.config.textComposerText = '@jo'; + + const result = await source.query('jo'); + expect(result.items).toHaveLength(1); + expect(result.items[0].name).toBe('John Doe'); + }); + + it('should query members from API when not all loaded', async () => { + const source = new MentionsSearchSource(channel); + source.activate(); + + // Simulate more members than MAX_CHANNEL_MEMBER_COUNT_IN_CHANNEL_QUERY + const manyMembers: Record = {}; + for (let i = 0; i < MAX_CHANNEL_MEMBER_COUNT_IN_CHANNEL_QUERY + 1; i++) { + manyMembers[`user${i}`] = { user: { id: `user${i}`, name: `User ${i}` } }; + } + channel.state.members = manyMembers; + + const result = await source.query('john'); + expect(channel.queryMembers).toHaveBeenCalled(); + expect(result.items).toHaveLength(Object.keys(mockMembers).length); + }); + + it('should query all app users when mentionAllAppUsers is true', async () => { + const source = new MentionsSearchSource(channel, { mentionAllAppUsers: true }); + source.activate(); + + const result = await source.query('john'); + expect(client.queryUsers).toHaveBeenCalled(); + expect(result.items).toHaveLength(mockUsers.length); + }); + + it('should filter out current user from results', async () => { + const source = new MentionsSearchSource(channel); + source.activate(); + source.config.textComposerText = '@'; + + const result = await source.query(''); + expect(result.items.every((item) => item.id !== client.userID)).toBe(true); + }); + + it('should handle transliteration when provided', async () => { + const transliterate = (text: string) => text.toLowerCase(); + const source = new MentionsSearchSource(channel, { transliterate }); + source.activate(); + source.config.textComposerText = '@john'; + + const result = await source.query('john'); + expect(result.items).toHaveLength(1); + expect(result.items[0].name).toBe('John Doe'); + }); + + it('should filter muted users correctly', async () => { + const source = new MentionsSearchSource(channel); + source.activate(); + source.config.textComposerText = '@unmute'; + const mute: Mute = { + target: { id: 'user1' }, + user: { id: 'currentUser' }, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }; + client.mutedUsers = [mute]; + + const result = await source.query(''); + expect(result.items).toHaveLength(Object.keys(mockMembers).length - 1); + expect(result.items[0].id).toBe('user2'); + }); + + it('should preserve items in state before first query', () => { + const source = new MentionsSearchSource(channel); + source.activate(); + + const initialState = source.state.getLatestValue(); + const newState = source.getStateBeforeFirstQuery('test'); + + expect(newState.items).toEqual(initialState.items); + }); + + it('should correctly determine if query can be executed', () => { + const source = new MentionsSearchSource(channel); + source.activate(); + + expect(source.canExecuteQuery('test')).toBe(true); + source.state.partialNext({ isLoading: true }); + expect(source.canExecuteQuery('test')).toBe(false); + source.state.partialNext({ isLoading: false }); + source.deactivate(); + expect(source.canExecuteQuery('test')).toBe(false); + }); + + it('should correctly get members and watchers without duplicates', () => { + const source = new MentionsSearchSource(channel); + channel.state.watchers = { + user1: mockUsers[0], // Duplicate with member + user4: { id: 'user4', name: 'New User' }, // New user + }; + + const result = source.getMembersAndWatchers(); + expect(result).toHaveLength(Object.keys(mockMembers).length + 1); // 2 members + 1 new watcher + expect(result.find((u) => u.id === 'user4')).toBeDefined(); + }); + + it('should prepare correct query parameters for users search', () => { + const source = new MentionsSearchSource(channel); + source.userFilters = { id: { $in: ['admin1', 'admin2'] } } as UserFilters; + source.userSort = [{ created_at: -1 }]; + + const params = source.prepareQueryUsersParams('john'); + expect(params.filters).toEqual({ + $or: [{ id: { $autocomplete: 'john' } }, { name: { $autocomplete: 'john' } }], + id: { $in: ['admin1', 'admin2'] }, + }); + expect(params.sort).toEqual([{ created_at: -1 }]); + }); + + it('should prepare correct query parameters for members search', () => { + const source = new MentionsSearchSource(channel); + source.memberFilters = { name: { $autocomplete: 'john' } } as MemberFilters; + source.memberSort = { created_at: -1 }; + + const params = source.prepareQueryMembersParams('john'); + expect(params.filters).toEqual({ name: { $autocomplete: 'john' } }); + expect(params.sort).toEqual({ created_at: -1 }); + }); + + it('should handle empty or invalid user names in local search', () => { + const source = new MentionsSearchSource(channel); + source.config.textComposerText = '@user'; + mockUsers = [ + { id: 'user1' }, // No name + { id: 'user2', name: '' }, // Empty name + { id: 'user3', name: 'Valid Name' }, + ]; + mockMembers = { + user1: { user: mockUsers[0] }, + user2: { user: mockUsers[1] }, + user3: { user: mockUsers[2] }, + }; + channel.state.members = mockMembers; + + const result = source.searchMembersLocally('valid'); + expect(result).toHaveLength(1); + expect(result[0].name).toBe('Valid Name'); + }); + + it('should handle errors in API queries', async () => { + const source = new MentionsSearchSource(channel); + client.queryUsers = vi.fn().mockRejectedValue(new Error('API Error')); + + source.config.mentionAllAppUsers = true; + await expect(source.query('test')).rejects.toThrow('API Error'); + }); + + it('should apply custom search options', async () => { + const source = new MentionsSearchSource(channel); + source.searchOptions = { presence: true }; + source.config.mentionAllAppUsers = true; + + await source.query('test'); + expect(client.queryUsers).toHaveBeenCalledWith( + expect.any(Object), + expect.any(Object), + expect.objectContaining({ presence: true }), + ); + }); + + it('should correctly calculate Levenshtein distance for fuzzy matching', () => { + const source = new MentionsSearchSource(channel); + source.config.textComposerText = '@joh'; + channel.state.members = { + user1: { user: { id: 'user1', name: 'John' } }, + user2: { user: { id: 'user2', name: 'Johnny' } }, + user3: { user: { id: 'user3', name: 'osep' } }, + }; + + const result = source.searchMembersLocally('joh'); + expect(result).toHaveLength(2); // Should match John and Johnny + expect(result.map((i) => i.name)).toEqual(['John', 'Johnny']); + }); +}); diff --git a/test/unit/MessageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.test.ts b/test/unit/MessageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.test.ts new file mode 100644 index 0000000000..4cea048672 --- /dev/null +++ b/test/unit/MessageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.test.ts @@ -0,0 +1,517 @@ +import { describe, expect, it, vi } from 'vitest'; +import { StreamChat } from '../../../../../src/client'; +import { TextComposerConfig } from '../../../../../src/messageComposer/configuration'; +import { + CompositionContext, + MessageComposer, +} from '../../../../../src/messageComposer/messageComposer'; +import { createMentionsMiddleware } from '../../../../../src/messageComposer/middleware/textComposer/mentions'; +import { TextComposer } from '../../../../../src/messageComposer/textComposer'; +import type { TextComposerSuggestion } from '../../../../../src/messageComposer/types'; +import type { + CommandResponse, + DraftResponse, + LocalMessage, + UserResponse, +} from '../../../../../src/types'; +import { TextComposerMiddleware } from '../../../../../src'; + +// Mock dependencies +vi.mock('../../../src/utils', () => ({ + axiosParamsSerializer: vi.fn(), + isFunction: vi.fn(), + isString: vi.fn(), + isObject: vi.fn(), + isArray: vi.fn(), + isDate: vi.fn(), + isNumber: vi.fn(), + logChatPromiseExecution: vi.fn(), + generateUUIDv4: vi.fn().mockReturnValue('test-uuid'), + debounce: vi.fn().mockImplementation((fn) => fn), + randomId: vi.fn().mockReturnValue('test-uuid'), + isLocalMessage: vi.fn().mockReturnValue(true), + formatMessage: vi.fn().mockImplementation((msg) => msg), + throttle: vi.fn().mockImplementation((fn) => fn), +})); + +const setup = ({ + config, + composition, + compositionContext, +}: { + config?: Partial; + composition?: DraftResponse | LocalMessage; + compositionContext?: CompositionContext; +} = {}) => { + // Reset mocks + vi.clearAllMocks(); + + // Setup mocks + const client = new StreamChat('apiKey', 'apiSecret'); + client.queryUsers = vi.fn().mockResolvedValue({ users: [] }); + + const channel = client.channel('channelType', 'channelId'); + channel.keystroke = vi.fn().mockResolvedValue({}); + channel.getClient = vi.fn().mockReturnValue(client); + + const messageComposer = new MessageComposer({ + client: client, + composition, + compositionContext: compositionContext ?? channel, + config: { text: config }, + }); + return { client, channel, messageComposer }; +}; + +describe('TextComposerMiddlewareExecutor', () => { + it('should initialize with default middleware', () => { + const { + messageComposer: { textComposer }, + } = setup(); + const middleware = textComposer.middlewareExecutor.middleware; + expect(middleware.length).toBe(3); + expect(middleware[0].id).toBe('stream-io/text-composer/pre-validation-middleware'); + expect(middleware[1].id).toBe('stream-io/text-composer/mentions-middleware'); + expect(middleware[2].id).toBe('stream-io/text-composer/commands-middleware'); + }); + + it('should handle onChange event with mentions', async () => { + const { + messageComposer: { textComposer }, + } = setup(); + let result = await textComposer.middlewareExecutor.execute('onChange', { + state: { + text: '@jo', + selection: { start: 3, end: 3 }, + mentionedUsers: [], + }, + }); + + expect(result.state.suggestions).toBeDefined(); + expect(result.state.suggestions?.trigger).toBe('@'); + expect(result.state.suggestions?.query).toBe('jo'); + + result = await textComposer.middlewareExecutor.execute('onChange', { + state: { + text: 'abcde@ho', + selection: { start: 8, end: 8 }, + mentionedUsers: [], + }, + }); + + expect(result.state.suggestions).toBeDefined(); + expect(result.state.suggestions?.trigger).toBe('@'); + expect(result.state.suggestions?.query).toBe('ho'); + + result = await textComposer.middlewareExecutor.execute('onChange', { + state: { + text: 'abcde@ho', + selection: { start: 5, end: 5 }, // selection is not where the trigger is + mentionedUsers: [], + }, + }); + + expect(result.state.suggestions).toBeUndefined(); + + result = await textComposer.middlewareExecutor.execute('onChange', { + state: { + text: 'abcde@ho', + selection: { start: 6, end: 6 }, // selection is where the trigger is but not at the end + mentionedUsers: [], + }, + }); + + expect(result.state.suggestions).toBeDefined(); + expect(result.state.suggestions?.trigger).toBe('@'); + expect(result.state.suggestions?.query).toBe(''); + }); + + it('should handle onChange event with commands', async () => { + const { + messageComposer: { textComposer }, + } = setup(); + let result = await textComposer.middlewareExecutor.execute('onChange', { + state: { + text: '/ban', + selection: { start: 4, end: 4 }, + mentionedUsers: [], + }, + }); + + expect(result.state.suggestions).toBeDefined(); + expect(result.state.suggestions?.trigger).toBe('/'); + expect(result.state.suggestions?.query).toBe('ban'); + + result = await textComposer.middlewareExecutor.execute('onChange', { + state: { + text: '/ban /ban', + selection: { start: 9, end: 9 }, + mentionedUsers: [], + }, + }); + + expect(result.state.suggestions).toBeUndefined(); // only one command trigger is allowed + }); + + it('should handle suggestion selection with mentions', async () => { + const { + messageComposer: { textComposer }, + } = setup(); + await textComposer.handleChange({ + text: '@jo', + selection: { start: 3, end: 3 }, + }); + + const selectedSuggestion = { + id: 'user1', + name: 'John Doe', + } as TextComposerSuggestion; + + await textComposer.handleSelect(selectedSuggestion); + + expect(textComposer.text).toBe('@John Doe '); + expect(textComposer.suggestions).toBeUndefined(); + expect(textComposer.mentionedUsers).toContainEqual(selectedSuggestion); + }); + + it('should handle suggestion selection with commands', async () => { + const { + messageComposer: { textComposer }, + } = setup(); + await textComposer.handleChange({ + text: '/ba', + selection: { start: 3, end: 3 }, + }); + + const selectedSuggestion = { + id: 'ban', + name: 'ban', + description: 'Ban a user', + } as TextComposerSuggestion; + + await textComposer.handleSelect(selectedSuggestion); + + expect(textComposer.text).toBe('/ban '); + expect(textComposer.suggestions).toBeUndefined(); + }); + + it('should handle search errors and cancellations', async () => { + const { + channel, + messageComposer: { textComposer }, + } = setup(); + const mockSearchSource = { + search: vi.fn().mockImplementation(() => { + throw new Error('Search failed'); + }), + activate: vi.fn(), + resetState: vi.fn(), + resetStateAndActivate: vi.fn(), + config: {}, + }; + + textComposer.middlewareExecutor.replace([ + createMentionsMiddleware(channel, { + searchSource: mockSearchSource as any, + }), + ] as TextComposerMiddleware[]); + + const result = await textComposer.middlewareExecutor.execute('onChange', { + state: { + text: '@jo', + selection: { start: 3, end: 3 }, + mentionedUsers: [], + }, + }); + + expect(mockSearchSource.search).toHaveBeenCalled(); + expect(result.state.suggestions).toBeUndefined(); + }); + + describe('commands middleware', () => { + it('should return early if no selection', async () => { + const { + messageComposer: { textComposer }, + } = setup(); + const result = await textComposer.middlewareExecutor.execute('onChange', { + state: { + text: '/test', + selection: { start: 0, end: 0 }, + mentionedUsers: [], + }, + }); + + expect(result.state).toEqual({ + text: '/test', + selection: { start: 0, end: 0 }, + mentionedUsers: [], + }); + }); + + it('should return early if first char is not command trigger', async () => { + const { + messageComposer: { textComposer }, + } = setup(); + const result = await textComposer.middlewareExecutor.execute('onChange', { + state: { + text: 'test', + selection: { start: 0, end: 4 }, + mentionedUsers: [], + }, + }); + + expect(result.state).toEqual({ + text: 'test', + selection: { start: 0, end: 4 }, + mentionedUsers: [], + }); + }); + + it('should handle trigger with token', async () => { + const { + messageComposer: { textComposer }, + } = setup(); + const result = await textComposer.middlewareExecutor.execute('onChange', { + state: { + text: '/test', + selection: { start: 0, end: 5 }, + mentionedUsers: [], + }, + }); + + expect(result.state.suggestions).toBeDefined(); + expect(result.state.suggestions?.trigger).toBe('/'); + expect(result.state.suggestions?.query).toBe('test'); + }); + + it('should handle new search trigger', async () => { + const { + messageComposer: { textComposer }, + } = setup(); + const result = await textComposer.middlewareExecutor.execute('onChange', { + state: { + text: '/', + selection: { start: 0, end: 1 }, + mentionedUsers: [], + }, + }); + + expect(result.state.suggestions).toBeDefined(); + expect(result.state.suggestions?.trigger).toBe('/'); + expect(result.state.suggestions?.query).toBe(''); + }); + + it('should handle trigger removal and stale suggestions', async () => { + const { + messageComposer: { textComposer }, + } = setup(); + const result = await textComposer.middlewareExecutor.execute('onChange', { + state: { + text: 'test', + selection: { start: 0, end: 4 }, + mentionedUsers: [], + suggestions: { + trigger: '/', + query: 'test', + searchSource: {} as any, + }, + }, + }); + + expect(result.state.suggestions).toBeUndefined(); + }); + }); + + describe('mentions middleware', () => { + it('should return early if no selection', async () => { + const { + messageComposer: { textComposer }, + } = setup(); + const result = await textComposer.middlewareExecutor.execute('onChange', { + state: { + text: '@test', + selection: { start: 0, end: 0 }, + mentionedUsers: [], + }, + }); + + expect(result.state).toEqual({ + text: '@test', + selection: { start: 0, end: 0 }, + mentionedUsers: [], + }); + }); + + it('should handle trigger with token', async () => { + const { + messageComposer: { textComposer }, + } = setup(); + const result = await textComposer.middlewareExecutor.execute('onChange', { + state: { + text: '@test', + selection: { start: 0, end: 5 }, + mentionedUsers: [], + }, + }); + + expect(result.state.suggestions).toBeDefined(); + expect(result.state.suggestions?.trigger).toBe('@'); + expect(result.state.suggestions?.query).toBe('test'); + }); + + it('should handle new search trigger', async () => { + const { + messageComposer: { textComposer }, + } = setup(); + const result = await textComposer.middlewareExecutor.execute('onChange', { + state: { + text: '@', + selection: { start: 0, end: 1 }, + mentionedUsers: [], + }, + }); + + expect(result.state.suggestions).toBeDefined(); + expect(result.state.suggestions?.trigger).toBe('@'); + expect(result.state.suggestions?.query).toBe(''); + }); + + it('should handle trigger removal and stale suggestions', async () => { + const { + messageComposer: { textComposer }, + } = setup(); + const result = await textComposer.middlewareExecutor.execute('onChange', { + state: { + text: 'test', + selection: { start: 0, end: 4 }, + mentionedUsers: [], + suggestions: { + trigger: '@', + query: 'test', + searchSource: {} as any, + }, + }, + }); + + expect(result.state.suggestions).toBeUndefined(); + }); + }); + + it('should handle combination of commands and mentions', async () => { + // First test a command + const { + messageComposer: { textComposer }, + } = setup(); + let result = await textComposer.middlewareExecutor.execute('onChange', { + state: { + text: '/ban', + selection: { start: 4, end: 4 }, + mentionedUsers: [], + }, + }); + + expect(result.state.suggestions).toBeDefined(); + expect(result.state.suggestions?.trigger).toBe('/'); + expect(result.state.suggestions?.query).toBe('ban'); + + // Then test a mention after the command + result = await textComposer.middlewareExecutor.execute('onChange', { + state: { + text: '/ban @jo', + selection: { start: 9, end: 9 }, + mentionedUsers: [], + }, + }); + + expect(result.state.suggestions).toBeDefined(); + expect(result.state.suggestions?.trigger).toBe('@'); + expect(result.state.suggestions?.query).toBe('jo'); + + // Test a command in the middle of text - should not trigger command suggestions + result = await textComposer.middlewareExecutor.execute('onChange', { + state: { + text: 'hello /ban', + selection: { start: 11, end: 11 }, + mentionedUsers: [], + }, + }); + + expect(result.state.suggestions).toBeUndefined(); + + // Test a mention followed by a command + result = await textComposer.middlewareExecutor.execute('onChange', { + state: { + text: '@jo /ban', + selection: { start: 8, end: 8 }, + mentionedUsers: [], + }, + }); + + // Command in middle shouldn't trigger + expect(result.state.suggestions).toBeDefined(); + expect(result.state.suggestions?.trigger).toBe('@'); + expect(result.state.suggestions?.query).toBe('jo /ban'); + }); + + describe('validation middleware', () => { + it('should truncate text exceeding max length', async () => { + const { + messageComposer: { textComposer }, + } = setup(); + // Set max text length + textComposer.maxLengthOnEdit = 10; + + // Test with text exceeding max length + const result = await textComposer.middlewareExecutor.execute('onChange', { + state: { + text: 'Hello World This Is Too Long', + selection: { start: 30, end: 30 }, + mentionedUsers: [], + }, + }); + + // Text should be truncated to maxTextLength + expect(result.state.text).toBe('Hello Worl'); + }); + + it('should not truncate text under max length', async () => { + const { + messageComposer: { textComposer }, + } = setup(); + // Set max text length + textComposer.maxLengthOnEdit = 20; + + // Test with text under max length + const result = await textComposer.middlewareExecutor.execute('onChange', { + state: { + text: 'Hello World', + selection: { start: 11, end: 11 }, + mentionedUsers: [], + }, + }); + + // Text should not be truncated + expect(result.state.text).toBe('Hello World'); + }); + + it('should handle validation with zero max length', async () => { + const { + messageComposer: { textComposer }, + } = setup(); + // Set max text length to zero + textComposer.maxLengthOnEdit = 0; + + // Test with any text + const result = await textComposer.middlewareExecutor.execute('onChange', { + state: { + text: 'Hello World', + selection: { start: 11, end: 11 }, + mentionedUsers: [], + }, + }); + + // Text should be empty + expect(result.state.text).toBe(''); + }); + }); +}); diff --git a/test/unit/MessageComposer/pollComposer.test.ts b/test/unit/MessageComposer/pollComposer.test.ts new file mode 100644 index 0000000000..0e29b6b407 --- /dev/null +++ b/test/unit/MessageComposer/pollComposer.test.ts @@ -0,0 +1,353 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { PollComposer } from '../../../src/messageComposer/pollComposer'; +import { StateStore } from '../../../src/store'; +import { VotingVisibility } from '../../../src/types'; + +// Mock dependencies +vi.mock('../../../src/utils', () => ({ + generateUUIDv4: vi.fn().mockReturnValue('test-uuid'), +})); + +vi.mock('../../../src/messageComposer/middleware/pollComposer', () => ({ + PollComposerCompositionMiddlewareExecutor: vi.fn().mockImplementation(() => ({ + execute: vi.fn().mockResolvedValue({ + state: { + data: { + allow_answers: false, + allow_user_suggested_options: false, + description: '', + enforce_unique_vote: true, + id: 'test-id', + max_votes_allowed: 5, + name: 'Test Poll', + options: [{ text: 'Option 1' }, { text: 'Option 2' }], + user_id: 'user-id', + voting_visibility: VotingVisibility.public, + }, + errors: {}, + }, + status: 'success', + }), + })), + PollComposerStateMiddlewareExecutor: vi.fn().mockImplementation(() => ({ + execute: vi.fn().mockResolvedValue({ + state: { + nextState: { + data: { + allow_answers: false, + allow_user_suggested_options: false, + description: '', + enforce_unique_vote: true, + id: 'test-id', + max_votes_allowed: '', + name: 'Test Poll', + options: [{ id: 'option-id', text: 'Option 1' }], + user_id: 'user-id', + voting_visibility: VotingVisibility.public, + }, + errors: {}, + }, + }, + status: 'success', + }), + })), + VALID_MAX_VOTES_VALUE_REGEX: /^([2-9]|10)$/, +})); + +describe('PollComposer', () => { + let mockComposer: any; + let pollComposer: PollComposer; + + beforeEach(() => { + mockComposer = { + client: { + user: { + id: 'user-id', + }, + }, + }; + + pollComposer = new PollComposer({ composer: mockComposer }); + }); + + describe('constructor', () => { + it('should initialize with the correct state', () => { + expect(pollComposer.composer).toBe(mockComposer); + expect(pollComposer.state).toBeInstanceOf(StateStore); + }); + }); + + describe('initialState', () => { + it('should return the initial state', () => { + const initialState = pollComposer.initialState; + + expect(initialState.data.allow_answers).toBe(false); + expect(initialState.data.allow_user_suggested_options).toBe(false); + expect(initialState.data.description).toBe(''); + expect(initialState.data.enforce_unique_vote).toBe(true); + expect(initialState.data.id).toBe('test-uuid'); + expect(initialState.data.max_votes_allowed).toBe(''); + expect(initialState.data.name).toBe(''); + expect(initialState.data.options).toEqual([{ id: 'test-uuid', text: '' }]); + expect(initialState.data.user_id).toBe('user-id'); + expect(initialState.data.voting_visibility).toBe(VotingVisibility.public); + expect(initialState.errors).toEqual({}); + }); + }); + + describe('getters', () => { + it('should return the correct values from state', () => { + // Set up the state with specific values + pollComposer.state.next({ + data: { + allow_answers: true, + allow_user_suggested_options: true, + description: '', + enforce_unique_vote: false, + id: 'test-id', + max_votes_allowed: '', + name: '', + options: [{ id: 'option-id', text: '' }], + user_id: 'user-id', + voting_visibility: VotingVisibility.anonymous, + }, + errors: {}, + }); + + expect(pollComposer.allow_answers).toBe(true); + expect(pollComposer.allow_user_suggested_options).toBe(true); + expect(pollComposer.description).toBe(''); + expect(pollComposer.enforce_unique_vote).toBe(false); + expect(pollComposer.id).toBe('test-id'); + expect(pollComposer.max_votes_allowed).toBe(''); + expect(pollComposer.name).toBe(''); + expect(pollComposer.options).toEqual([{ id: 'option-id', text: '' }]); + expect(pollComposer.user_id).toBe('user-id'); + expect(pollComposer.voting_visibility).toBe(VotingVisibility.anonymous); + }); + }); + + describe('canCreatePoll', () => { + it('should return false when there are no options with text', () => { + pollComposer.state.next({ + data: { + options: [{ id: 'option-id', text: '' }], + name: 'Test Poll', + max_votes_allowed: '', + id: 'test-id', + user_id: 'user-id', + voting_visibility: VotingVisibility.public, + }, + errors: {}, + }); + + expect(pollComposer.canCreatePoll).toBe(false); + }); + + it('should return false when there is no name', () => { + pollComposer.state.next({ + data: { + options: [{ id: 'option-id', text: 'Option 1' }], + name: '', + max_votes_allowed: '', + id: 'test-id', + user_id: 'user-id', + voting_visibility: VotingVisibility.public, + }, + errors: {}, + }); + + expect(pollComposer.canCreatePoll).toBe(false); + }); + + it('should return false when max_votes_allowed is invalid', () => { + pollComposer.state.next({ + data: { + options: [{ id: 'option-id', text: 'Option 1' }], + name: 'Test Poll', + max_votes_allowed: '1', // Less than 2 + id: 'test-id', + user_id: 'user-id', + voting_visibility: VotingVisibility.public, + }, + errors: {}, + }); + + expect(pollComposer.canCreatePoll).toBe(false); + }); + + it('should return false when there are errors', () => { + pollComposer.state.next({ + data: { + options: [{ id: 'option-id', text: 'Option 1' }], + name: 'Test Poll', + max_votes_allowed: '', + id: 'test-id', + user_id: 'user-id', + voting_visibility: VotingVisibility.public, + }, + errors: { name: 'Name is required' }, + }); + + expect(pollComposer.canCreatePoll).toBe(false); + }); + + it('should return true when all conditions are met', () => { + pollComposer.state.next({ + data: { + options: [{ id: 'option-id', text: 'Option 1' }], + name: 'Test Poll', + max_votes_allowed: '', + id: 'test-id', + user_id: 'user-id', + voting_visibility: VotingVisibility.public, + }, + errors: {}, + }); + + expect(pollComposer.canCreatePoll).toBe(true); + }); + it('should return true if all field errors are undefined', () => { + pollComposer.state.next({ + data: { + options: [{ id: 'option-id', text: 'Option 1' }], + name: 'Test Poll', + max_votes_allowed: '', + id: 'test-id', + user_id: 'user-id', + voting_visibility: VotingVisibility.public, + }, + errors: { name: undefined, options: undefined }, + }); + + expect(pollComposer.canCreatePoll).toBe(true); + }); + }); + + describe('initState', () => { + it('should reset the state to initial state', () => { + // Set up a different state first + pollComposer.state.next({ + data: { + allow_answers: true, + allow_user_suggested_options: true, + description: 'Test Description', + enforce_unique_vote: false, + id: 'different-id', + max_votes_allowed: '5', + name: 'Different Name', + options: [{ id: 'different-option-id', text: 'Different Option' }], + user_id: 'different-user-id', + voting_visibility: VotingVisibility.anonymous, + }, + errors: { name: 'Error' }, + }); + + // Reset to initial state + pollComposer.initState(); + + // Check that the state was reset + const currentState = pollComposer.state.getLatestValue(); + expect(currentState.data.allow_answers).toBe(false); + expect(currentState.data.allow_user_suggested_options).toBe(false); + expect(currentState.data.description).toBe(''); + expect(currentState.data.enforce_unique_vote).toBe(true); + expect(currentState.data.id).toBe('test-uuid'); + expect(currentState.data.max_votes_allowed).toBe(''); + expect(currentState.data.name).toBe(''); + expect(currentState.data.options).toEqual([{ id: 'test-uuid', text: '' }]); + expect(currentState.data.user_id).toBe('user-id'); + expect(currentState.data.voting_visibility).toBe(VotingVisibility.public); + expect(currentState.errors).toEqual({}); + }); + }); + + describe('updateFields', () => { + it('should update fields and call state middleware executor', async () => { + const updateData = { name: 'Test Poll' }; + const spy = vi.spyOn(pollComposer.stateMiddlewareExecutor, 'execute'); + + await pollComposer.updateFields(updateData); + + expect(spy).toHaveBeenCalledWith('handleFieldChange', expect.any(Object)); + }); + + it('should not update state if middleware returns discard status', async () => { + const updateData = { name: 'Test Poll' }; + const originalState = pollComposer.state.getLatestValue(); + + // Mock the middleware to return discard status + const middlewareExecutor = pollComposer.stateMiddlewareExecutor as unknown as { + execute: ReturnType; + }; + middlewareExecutor.execute.mockResolvedValueOnce({ + state: { nextState: {} }, + status: 'discard', + }); + + await pollComposer.updateFields(updateData); + + // Check that the state wasn't updated + expect(pollComposer.state.getLatestValue()).toEqual(originalState); + }); + }); + + describe('handleFieldBlur', () => { + it('should handle field blur and call state middleware executor', async () => { + const spy = vi.spyOn(pollComposer.stateMiddlewareExecutor, 'execute'); + + await pollComposer.handleFieldBlur('name'); + + expect(spy).toHaveBeenCalledWith('handleFieldBlur', expect.any(Object)); + }); + + it('should not update state if middleware returns discard status', async () => { + const originalState = pollComposer.state.getLatestValue(); + + // Mock the middleware to return discard status + const middlewareExecutor = pollComposer.stateMiddlewareExecutor as unknown as { + execute: ReturnType; + }; + middlewareExecutor.execute.mockResolvedValueOnce({ + state: { nextState: {} }, + status: 'discard', + }); + + await pollComposer.handleFieldBlur('name'); + + // Check that the state wasn't updated + expect(pollComposer.state.getLatestValue()).toEqual(originalState); + }); + }); + + describe('compose', () => { + it('should compose the poll and call composition middleware executor', async () => { + const spy = vi.spyOn(pollComposer.compositionMiddlewareExecutor, 'execute'); + + const result = await pollComposer.compose(); + + expect(spy).toHaveBeenCalledWith('compose', expect.any(Object)); + expect(result).toBeDefined(); + if (result) { + expect(result.data.name).toBe('Test Poll'); + expect(result.data.options).toEqual([{ text: 'Option 1' }, { text: 'Option 2' }]); + } + }); + + it('should return undefined if middleware returns discard status', async () => { + // Mock the middleware to return discard status + const middlewareExecutor = + pollComposer.compositionMiddlewareExecutor as unknown as { + execute: ReturnType; + }; + middlewareExecutor.execute.mockResolvedValueOnce({ + state: {}, + status: 'discard', + }); + + const result = await pollComposer.compose(); + + expect(result).toBeUndefined(); + }); + }); +}); diff --git a/test/unit/MessageComposer/textComposer.test.ts b/test/unit/MessageComposer/textComposer.test.ts new file mode 100644 index 0000000000..812aa59158 --- /dev/null +++ b/test/unit/MessageComposer/textComposer.test.ts @@ -0,0 +1,652 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { StreamChat } from '../../../src/client'; +import { + CompositionContext, + MessageComposer, +} from '../../../src/messageComposer/messageComposer'; +import { textIsEmpty } from '../../../src/messageComposer/textComposer'; +import { DraftResponse, LocalMessage } from '../../../src/types'; +import { logChatPromiseExecution } from '../../../src/utils'; +import { TextComposerConfig } from '../../../src/messageComposer/configuration'; + +const textComposerMiddlewareExecuteOutput = { + state: { + mentionedUsers: [], + text: 'Test message', + selection: { start: 12, end: 12 }, + }, + status: '', +}; + +vi.mock('../.././src/messageComposer/middleware', () => ({ + TextComposerMiddlewareExecutor: vi.fn().mockImplementation(() => ({ + execute: vi.fn().mockResolvedValue(textComposerMiddlewareExecuteOutput), + })), +})); + +// Mock dependencies +vi.mock('../../../src/utils', () => ({ + axiosParamsSerializer: vi.fn(), + isFunction: vi.fn(), + isString: vi.fn(), + isObject: vi.fn(), + isArray: vi.fn(), + isDate: vi.fn(), + isNumber: vi.fn(), + logChatPromiseExecution: vi.fn(), + generateUUIDv4: vi.fn().mockReturnValue('test-uuid'), + debounce: vi.fn().mockImplementation((fn) => fn), + randomId: vi.fn().mockReturnValue('test-uuid'), + isLocalMessage: vi.fn().mockReturnValue(true), + formatMessage: vi.fn().mockImplementation((msg) => msg), + throttle: vi.fn().mockImplementation((fn) => fn), +})); + +const setup = ({ + config, + composition, + compositionContext, +}: { + config?: Partial; + composition?: DraftResponse | LocalMessage; + compositionContext?: CompositionContext; +} = {}) => { + // Reset mocks + vi.clearAllMocks(); + + // Setup mocks + const mockClient = new StreamChat('apiKey', 'apiSecret'); + mockClient.queryUsers = vi.fn().mockResolvedValue({ users: [] }); + + const mockChannel = mockClient.channel('channelType', 'channelId'); + mockChannel.keystroke = vi.fn().mockResolvedValue({}); + mockChannel.getClient = vi.fn().mockReturnValue(mockClient); + + const messageComposer = new MessageComposer({ + client: mockClient, + composition, + compositionContext: compositionContext ?? mockChannel, + config: { text: config }, + }); + return { mockClient, mockChannel, messageComposer }; +}; + +describe('TextComposer', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('textIsEmpty', () => { + it('should return true for empty strings', () => { + expect(textIsEmpty('')).toBe(true); + expect(textIsEmpty(' ')).toBe(true); + }); + + it('should return true for special markdown patterns', () => { + expect(textIsEmpty('>')).toBe(true); + expect(textIsEmpty('``````')).toBe(true); + expect(textIsEmpty('``')).toBe(true); + expect(textIsEmpty('**')).toBe(true); + expect(textIsEmpty('____')).toBe(true); + expect(textIsEmpty('__')).toBe(true); + expect(textIsEmpty('****')).toBe(true); + }); + + it('should return false for non-empty strings', () => { + expect(textIsEmpty('Hello')).toBe(false); + expect(textIsEmpty('> Hello')).toBe(false); + expect(textIsEmpty('**Hello**')).toBe(false); + }); + }); + + describe('constructor', () => { + it('should initialize with default config', () => { + const { messageComposer } = setup(); + expect(messageComposer.textComposer.state.getLatestValue()).toEqual({ + mentionedUsers: [], + text: '', + selection: { start: 0, end: 0 }, + }); + }); + + it('should initialize with custom config', () => { + const defaultValue = 'XXX'; + const { messageComposer } = setup({ config: { defaultValue } }); + expect(messageComposer.textComposer.state.getLatestValue()).toEqual({ + mentionedUsers: [], + text: defaultValue, + selection: { start: defaultValue.length, end: defaultValue.length }, + }); + }); + + it('should initialize with message', () => { + const message: LocalMessage = { + id: 'test-message', + type: 'regular', + text: 'Hello world', + mentioned_users: [{ id: 'user-1' }, { id: 'user-2', name: 'User 2' }], + created_at: new Date(), + deleted_at: null, + pinned_at: null, + status: 'pending', + updated_at: new Date(), + }; + + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + + expect(textComposer.text).toBe('Hello world'); + expect(textComposer.selection).toEqual({ + start: message.text?.length, + end: message.text?.length, + }); + expect(textComposer.mentionedUsers).toEqual([ + { id: 'user-1' }, + { id: 'user-2', name: 'User 2' }, + ]); + }); + + it('should ignore default value when initialized with message', () => { + const defaultValue = 'XXX'; + const message: LocalMessage = { + id: 'test-message', + type: 'regular', + text: 'Hello world', + created_at: new Date(), + deleted_at: null, + pinned_at: null, + status: 'pending', + updated_at: new Date(), + }; + + const { + messageComposer: { textComposer }, + } = setup({ composition: message, config: { defaultValue } }); + + expect(textComposer.text).toBe('Hello world'); + expect(textComposer.selection).toEqual({ + start: message.text?.length, + end: message.text?.length, + }); + }); + }); + + describe('getters', () => { + it('should return the correct values from state', () => { + const { + messageComposer: { textComposer }, + } = setup(); + const state = { + mentionedUsers: [{ id: 'user-1' }], + text: 'Hello world', + selection: { start: 5, end: 5 }, + suggestions: { query: 'test' }, + }; + textComposer.state.partialNext(state); + + expect(textComposer.mentionedUsers).toEqual([{ id: 'user-1' }]); + expect(textComposer.text).toBe('Hello world'); + expect(textComposer.selection).toEqual({ start: 5, end: 5 }); + expect(textComposer.suggestions).toEqual({ query: 'test' }); + expect(textComposer.textIsEmpty).toBe(false); + }); + + it('should return true for textIsEmpty when text is empty', () => { + const { + messageComposer: { textComposer }, + } = setup(); + textComposer.state.partialNext({ text: '' }); + expect(textComposer.textIsEmpty).toBe(true); + }); + }); + + describe('initState', () => { + it('should reset the state to initial state', () => { + const message: LocalMessage = { + id: 'test-message', + type: 'regular', + text: 'Hello world', + mentioned_users: [{ id: 'user-1' }, { id: 'user-2', name: 'User 2' }], + created_at: new Date(), + deleted_at: null, + pinned_at: null, + status: 'pending', + updated_at: new Date(), + }; + const initialState = { + mentionedUsers: [], + text: '', + selection: { start: 0, end: 0 }, + }; + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + textComposer.initState(); + expect(textComposer.state.getLatestValue()).toEqual(initialState); + }); + + it('should initialize with message', () => { + const message: LocalMessage = { + id: 'test-message', + type: 'regular', + text: 'Hello world', + mentioned_users: [{ id: 'user-1' }], + created_at: new Date(), + deleted_at: null, + pinned_at: null, + status: 'pending', + updated_at: new Date(), + }; + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + textComposer.initState({ message }); + expect(textComposer.text).toBe('Hello world'); + expect(textComposer.mentionedUsers).toEqual([{ id: 'user-1' }]); + }); + }); + + describe('setMentionedUsers', () => { + it('should update mentioned users', () => { + const message: LocalMessage = { + id: 'test-message', + type: 'regular', + text: 'Hello world', + mentioned_users: [{ id: 'user-1' }, { id: 'user-2', name: 'User 2' }], + }; + const users = [{ id: 'user-1' }, { id: 'user-3' }]; + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + textComposer.setMentionedUsers(users); + expect(textComposer.mentionedUsers).toEqual(users); + }); + }); + + describe('upsertMentionedUser', () => { + it('should add a new mentioned user', () => { + const message: LocalMessage = { + id: 'test-message', + type: 'regular', + text: 'Hello world', + mentioned_users: [{ id: 'user-1' }, { id: 'user-2', name: 'User 2' }], + }; + const user = { id: 'user-3', name: 'User 3' }; + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + textComposer.upsertMentionedUser(user); + expect(textComposer.mentionedUsers).toEqual([...message.mentioned_users, user]); + }); + + it('should update an existing mentioned user', () => { + const message: LocalMessage = { + id: 'test-message', + type: 'regular', + text: 'Hello world', + mentioned_users: [{ id: 'user-1' }, { id: 'user-2', name: 'User 2' }], + }; + const updatedUser = { id: 'user-1', name: 'New Name' }; + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + textComposer.upsertMentionedUser(updatedUser); + expect(textComposer.mentionedUsers).toEqual([ + { id: 'user-1', name: 'New Name' }, + { id: 'user-2', name: 'User 2' }, + ]); + }); + }); + + describe('getMentionedUser', () => { + it('should return the mentioned user if found', () => { + const message: LocalMessage = { + id: 'test-message', + type: 'regular', + text: 'Hello world', + mentioned_users: [{ id: 'user-1' }, { id: 'user-2', name: 'User 2' }], + }; + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + expect(textComposer.getMentionedUser('user-1')).toEqual(message.mentioned_users[0]); + }); + + it('should return undefined if user not found', () => { + const message: LocalMessage = { + id: 'test-message', + type: 'regular', + text: 'Hello world', + mentioned_users: [{ id: 'user-1' }, { id: 'user-2', name: 'User 2' }], + }; + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + expect(textComposer.getMentionedUser('user-3')).toBeUndefined(); + }); + }); + + describe('removeMentionedUser', () => { + it('should remove the mentioned user if found', () => { + const message: LocalMessage = { + id: 'test-message', + type: 'regular', + text: 'Hello world', + mentioned_users: [{ id: 'user-1' }, { id: 'user-2', name: 'User 2' }], + }; + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + textComposer.removeMentionedUser('user-1'); + expect(textComposer.mentionedUsers).toEqual([ + ...message.mentioned_users.filter((user) => user.id !== 'user-1'), + ]); + }); + + it('should not update state if user not found', () => { + const message: LocalMessage = { + id: 'test-message', + type: 'regular', + text: 'Hello world', + mentioned_users: [{ id: 'user-1' }, { id: 'user-2', name: 'User 2' }], + }; + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + textComposer.removeMentionedUser('user-3'); + expect(textComposer.mentionedUsers).toEqual(message.mentioned_users); + }); + }); + + describe('setText', () => { + it('should update the text', () => { + const message: LocalMessage = { + id: 'test-message', + type: 'regular', + text: 'Hello world', + }; + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + textComposer.setText('New text'); + expect(textComposer.text).toBe('New text'); + }); + }); + + describe('insertText', () => { + const message: LocalMessage = { + id: 'test-message', + type: 'regular', + text: 'Hello world', + }; + it('should insert text at the specified selection', () => { + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + textComposer.insertText({ text: ' beautiful', selection: { start: 5, end: 5 } }); + expect(textComposer.text).toBe('Hello beautiful world'); + }); + + it('should insert text at the end if no selection provided', () => { + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + textComposer.insertText({ text: '!' }); + expect(textComposer.text).toBe('Hello world!'); + }); + + it('should respect maxLengthOnEdit', () => { + const message: LocalMessage = { + id: 'test-message', + type: 'regular', + text: 'Hello', + }; + const { + messageComposer: { textComposer }, + } = setup({ + config: { maxLengthOnEdit: 8 }, + composition: message, + }); + textComposer.insertText({ text: ' beautiful world' }); + expect(textComposer.text).toBe('Hello be'); + }); + + it('should handle empty text insertion', () => { + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + textComposer.insertText({ text: '', selection: { start: 5, end: 5 } }); + expect(textComposer.text).toBe('Hello world'); + }); + + it('should handle insertion at the start of text', () => { + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + textComposer.insertText({ text: 'Hi ', selection: { start: 0, end: 0 } }); + expect(textComposer.text).toBe('Hi Hello world'); + }); + + it('should handle insertion at end of text', () => { + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + textComposer.insertText({ text: '!', selection: { start: 11, end: 11 } }); + expect(textComposer.text).toBe('Hello world!'); + }); + + it('should handle insertion with multi-character selection', () => { + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + textComposer.insertText({ text: 'Hi', selection: { start: 0, end: 5 } }); + expect(textComposer.text).toBe('Hi world'); + }); + + it('should handle insertion with multi-character selection and maxLengthOnEdit restricting the size', () => { + const message: LocalMessage = { + id: 'test-message', + type: 'regular', + text: 'Hello world', + }; + const { + messageComposer: { textComposer }, + } = setup({ + config: { maxLengthOnEdit: 10 }, + composition: message, + }); + const insertedText = 'Hi world'; + textComposer.insertText({ text: insertedText, selection: { start: 7, end: 9 } }); + expect(textComposer.text).toBe('Hello wHi '); + }); + }); + + describe('closeSuggestions', () => { + const message: LocalMessage = { + id: 'test-message', + type: 'regular', + text: '@query', + }; + + it('should close suggestions if they exist', () => { + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + textComposer.state.next({ suggestions: { query: 'test', trigger: '@' } }); + textComposer.closeSuggestions(); + expect(textComposer.suggestions).toBeUndefined(); + }); + + it('should not update state if no suggestions exist', () => { + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + expect(textComposer.suggestions).toBeUndefined(); + textComposer.closeSuggestions(); + expect(textComposer.suggestions).toBeUndefined(); + }); + }); + + describe('handleChange', () => { + const initialState = { + mentionedUsers: [], + text: '', + selection: { start: 0, end: 0 }, + }; + + it('should update state with middleware result', async () => { + const { + messageComposer: { textComposer }, + } = setup(); + textComposer.state.next(initialState); + + await textComposer.handleChange({ + text: 'Test message', + selection: { start: 12, end: 12 }, + }); + + expect(textComposer.text).toBe(textComposerMiddlewareExecuteOutput.state.text); + expect(textComposer.selection).toEqual( + textComposerMiddlewareExecuteOutput.state.selection, + ); + }); + + it('should not update state if middleware returns discard status', async () => { + const { + messageComposer: { textComposer }, + } = setup(); + textComposer.state.next(initialState); + + const executeSpy = vi.spyOn(textComposer.middlewareExecutor, 'execute'); + executeSpy.mockResolvedValueOnce({ + state: {}, + status: 'discard', + }); + + await textComposer.handleChange({ + text: 'Test message', + selection: { start: 12, end: 12 }, + }); + + expect(textComposer.text).toBe(''); + expect(textComposer.selection).toEqual({ start: 0, end: 0 }); + }); + + it('should trigger keystroke event with thread id if publishTypingEvents is true and editing a message', async () => { + const composition: LocalMessage = { + cid: 'channelType:channelId', + id: 'reply-123', + parent_id: 'thread-123', + type: 'reply', + text: 'Test message', + }; + const { + messageComposer: { textComposer }, + } = setup({ composition, compositionContext: composition }); + textComposer.state.next(initialState); + + await textComposer.handleChange({ + text: 'Test message', + selection: { start: 12, end: 12 }, + }); + + expect(textComposer.composer.channel.keystroke).toHaveBeenCalledWith('thread-123'); + expect(logChatPromiseExecution).toHaveBeenCalled(); + }); + + it('should trigger keystroke event with undefined if publishTypingEvents is true and not editing a message', async () => { + const composition: LocalMessage = { + cid: 'channelType:channelId', + id: 'reply-123', + parent_id: 'thread-123', + type: 'reply', + text: 'Test message', + }; + const { + messageComposer: { textComposer }, + } = setup({ composition }); + textComposer.state.next(initialState); + + await textComposer.handleChange({ + text: 'Test message', + selection: { start: 12, end: 12 }, + }); + + expect(textComposer.composer.channel.keystroke).toHaveBeenCalledWith(undefined); + expect(logChatPromiseExecution).toHaveBeenCalled(); + }); + + it('should not trigger keystroke event if publishTypingEvents is false', async () => { + const { + messageComposer: { textComposer }, + } = setup({ config: { publishTypingEvents: false } }); + textComposer.state.next(initialState); + + await textComposer.handleChange({ + text: 'Test message', + selection: { start: 12, end: 12 }, + }); + + expect(textComposer.composer.channel.keystroke).not.toHaveBeenCalled(); + expect(logChatPromiseExecution).not.toHaveBeenCalled(); + }); + + it('should not trigger keystroke event with empty text', async () => { + const { + messageComposer: { textComposer }, + } = setup(); + textComposer.state.next(initialState); + + await textComposer.handleChange({ + text: '', + selection: { start: 0, end: 0 }, + }); + + expect(textComposer.composer.channel.keystroke).not.toHaveBeenCalled(); + expect(logChatPromiseExecution).not.toHaveBeenCalled(); + }); + }); + + describe('handleSelect', () => { + const initialState = { + mentionedUsers: [], + text: '', + selection: { start: 0, end: 0 }, + }; + + it('should update state with middleware result', async () => { + const { + messageComposer: { textComposer }, + } = setup(); + textComposer.state.next(initialState); + + const target = { id: 'user-1' }; + const executeSpy = vi.spyOn(textComposer.middlewareExecutor, 'execute'); + executeSpy.mockResolvedValueOnce(textComposerMiddlewareExecuteOutput); + await textComposer.handleSelect(target); + + expect(textComposer.state.getLatestValue()).toEqual( + textComposerMiddlewareExecuteOutput.state, + ); + }); + + it('should not update state if middleware returns discard status', async () => { + const { + messageComposer: { textComposer }, + } = setup(); + textComposer.state.next(initialState); + + const executeSpy = vi.spyOn(textComposer.middlewareExecutor, 'execute'); + executeSpy.mockResolvedValueOnce({ + state: {}, + status: 'discard', + }); + + const target = { id: 'user-1' }; + await textComposer.handleSelect(target); + + expect(textComposer.state.getLatestValue()).toEqual(initialState); + }); + }); +}); diff --git a/test/unit/channel.test.js b/test/unit/channel.test.js index 7110cc721d..ee9a1851d7 100644 --- a/test/unit/channel.test.js +++ b/test/unit/channel.test.js @@ -11,6 +11,7 @@ import { mockChannelQueryResponse } from './test-utils/mockChannelQueryResponse' import { ChannelState, StreamChat } from '../../src'; import { DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE } from '../../src/constants'; +import { generateMessageDraft } from './test-utils/generateMessageDraft'; import { describe, beforeEach, it, expect } from 'vitest'; @@ -458,7 +459,7 @@ describe('Channel _handleChannelEvent', function () { expect( channel.state.messages.find((msg) => msg.id === quotingMessage.id).quoted_message .deleted_at, - ).to.be.ok; + ).to.be.null; }); describe('notification.mark_unread', () => { @@ -908,7 +909,6 @@ describe('Channels - Constructor', function () { expect(channel.id).to.eql('brand_new_123'); expect(channel.data).to.eql({ cool: true }); channel = client.channel('messaging', 'brand_new_123', { custom_cool: true }); - console.log(channel.data); expect(channel.data).to.eql({ cool: true, custom_cool: true }); }); diff --git a/test/unit/client.test.js b/test/unit/client.test.js index deee74644e..dfc3496ca0 100644 --- a/test/unit/client.test.js +++ b/test/unit/client.test.js @@ -664,6 +664,7 @@ describe('StreamChat.queryChannels', async () => { expect(Object.keys(client.configs).length).to.be.equal(0); mock.restore(); }); + it('should not update pagination for queried message set', async () => { const client = await getClientWithUser(); const mockedChannelsQueryResponse = Array.from({ length: 10 }, () => ({ diff --git a/test/unit/middleware.test.ts b/test/unit/middleware.test.ts new file mode 100644 index 0000000000..1fbae52406 --- /dev/null +++ b/test/unit/middleware.test.ts @@ -0,0 +1,559 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { + InsertPosition, + Middleware, + MiddlewareExecutor, + MiddlewareStatus, +} from '../../src/middleware'; + +describe('MiddlewareExecutor', () => { + let executor: MiddlewareExecutor<{ value: number }>; + + beforeEach(() => { + executor = new MiddlewareExecutor<{ value: number }>(); + }); + + describe('use', () => { + it('should add middleware to the executor', () => { + const middleware: Middleware<{ value: number }> = { + id: 'test-middleware', + test: async ({ input, nextHandler }) => { + return nextHandler(input); + }, + }; + + executor.use(middleware); + + // Access private property for testing + const middlewareList = (executor as any).middleware; + expect(middlewareList).toHaveLength(1); + expect(middlewareList[0]).toBe(middleware); + }); + + it('should add multiple middleware when array is provided', () => { + const middleware1: Middleware<{ value: number }> = { + id: 'test-middleware-1', + test: async ({ input, nextHandler }) => { + return nextHandler(input); + }, + }; + + const middleware2: Middleware<{ value: number }> = { + id: 'test-middleware-2', + test: async ({ input, nextHandler }) => { + return nextHandler(input); + }, + }; + + executor.use([middleware1, middleware2]); + + // Access private property for testing + const middlewareList = (executor as any).middleware; + expect(middlewareList).toHaveLength(2); + expect(middlewareList[0]).toBe(middleware1); + expect(middlewareList[1]).toBe(middleware2); + }); + + it('should return the executor for chaining', () => { + const middleware: Middleware<{ value: number }> = { + id: 'test-middleware', + test: async ({ input, nextHandler }) => { + return nextHandler(input); + }, + }; + + const result = executor.use(middleware); + expect(result).toBe(executor); + }); + }); + + describe('replace', () => { + it('should replace existing middleware with the same id', () => { + const middleware1: Middleware<{ value: number }> = { + id: 'test-middleware', + test: async ({ input, nextHandler }) => { + return nextHandler(input); + }, + }; + + const middleware2: Middleware<{ value: number }> = { + id: 'test-middleware', + test: async ({ input, nextHandler }) => { + return nextHandler({ ...input, state: { value: input.state.value + 1 } }); + }, + }; + + executor.use(middleware1); + executor.replace([middleware2]); + + // Access private property for testing + const middlewareList = (executor as any).middleware; + expect(middlewareList).toHaveLength(1); + expect(middlewareList[0]).toBe(middleware2); + }); + + it('should add new middleware if id does not exist', () => { + const middleware1: Middleware<{ value: number }> = { + id: 'test-middleware-1', + test: async ({ input, nextHandler }) => { + return nextHandler(input); + }, + }; + + const middleware2: Middleware<{ value: number }> = { + id: 'test-middleware-2', + test: async ({ input, nextHandler }) => { + return nextHandler(input); + }, + }; + + executor.use(middleware1); + executor.replace([middleware2]); + + // Access private property for testing + const middlewareList = (executor as any).middleware; + expect(middlewareList).toHaveLength(2); + expect(middlewareList[0]).toBe(middleware1); + expect(middlewareList[1]).toBe(middleware2); + }); + + it('should return the executor for chaining', () => { + const middleware: Middleware<{ value: number }> = { + id: 'test-middleware', + test: async ({ input, nextHandler }) => { + return nextHandler(input); + }, + }; + + const result = executor.replace([middleware]); + expect(result).toBe(executor); + }); + }); + + describe('insert', () => { + it('should insert middleware after specified middleware', () => { + const middleware1: Middleware<{ value: number }> = { + id: 'middleware-1', + test: async ({ input, nextHandler }) => { + return nextHandler(input); + }, + }; + + const middleware2: Middleware<{ value: number }> = { + id: 'middleware-2', + test: async ({ input, nextHandler }) => { + return nextHandler(input); + }, + }; + + const middleware3: Middleware<{ value: number }> = { + id: 'middleware-3', + test: async ({ input, nextHandler }) => { + return nextHandler(input); + }, + }; + + executor.use([middleware1, middleware3]); + + const position: InsertPosition = { after: 'middleware-1' }; + executor.insert({ middleware: [middleware2], position }); + + // Access private property for testing + const middlewareList = (executor as any).middleware; + expect(middlewareList).toHaveLength(3); + expect(middlewareList[0]).toBe(middleware1); + expect(middlewareList[1]).toBe(middleware2); + expect(middlewareList[2]).toBe(middleware3); + }); + + it('should insert middleware before specified middleware', () => { + const middleware1: Middleware<{ value: number }> = { + id: 'middleware-1', + test: async ({ input, nextHandler }) => { + return nextHandler(input); + }, + }; + + const middleware2: Middleware<{ value: number }> = { + id: 'middleware-2', + test: async ({ input, nextHandler }) => { + return nextHandler(input); + }, + }; + + const middleware3: Middleware<{ value: number }> = { + id: 'middleware-3', + test: async ({ input, nextHandler }) => { + return nextHandler(input); + }, + }; + + executor.use([middleware1, middleware3]); + + const position: InsertPosition = { before: 'middleware-3' }; + executor.insert({ middleware: [middleware2], position }); + + // Access private property for testing + const middlewareList = (executor as any).middleware; + expect(middlewareList).toHaveLength(3); + expect(middlewareList[0]).toBe(middleware1); + expect(middlewareList[1]).toBe(middleware2); + expect(middlewareList[2]).toBe(middleware3); + }); + + it('should remove existing middleware with the same id if unique is true', () => { + const middleware1: Middleware<{ value: number }> = { + id: 'middleware-1', + test: async ({ input, nextHandler }) => { + return nextHandler(input); + }, + }; + + const middleware2: Middleware<{ value: number }> = { + id: 'middleware-2', + test: async ({ input, nextHandler }) => { + return nextHandler(input); + }, + }; + + const middleware2Updated: Middleware<{ value: number }> = { + id: 'middleware-2', + test: async ({ input, nextHandler }) => { + return nextHandler({ ...input, state: { value: input.state.value + 1 } }); + }, + }; + + executor.use([middleware1, middleware2]); + + const position: InsertPosition = { after: 'middleware-1' }; + executor.insert({ middleware: [middleware2Updated], position, unique: true }); + + // Access private property for testing + const middlewareList = (executor as any).middleware; + expect(middlewareList).toHaveLength(2); + expect(middlewareList[0]).toBe(middleware1); + expect(middlewareList[1]).toBe(middleware2Updated); + }); + + it('should return the executor for chaining', () => { + const middleware1: Middleware<{ value: number }> = { + id: 'middleware-1', + test: async ({ input, nextHandler }) => { + return nextHandler(input); + }, + }; + + const middleware2: Middleware<{ value: number }> = { + id: 'middleware-2', + test: async ({ input, nextHandler }) => { + return nextHandler(input); + }, + }; + + executor.use(middleware1); + + const position: InsertPosition = { after: 'middleware-1' }; + const result = executor.insert({ middleware: [middleware2], position }); + + expect(result).toBe(executor); + }); + }); + + describe('setOrder', () => { + it('should reorder middleware based on provided order', () => { + const middleware1: Middleware<{ value: number }> = { + id: 'middleware-1', + test: async ({ input, nextHandler }) => { + return nextHandler(input); + }, + }; + + const middleware2: Middleware<{ value: number }> = { + id: 'middleware-2', + test: async ({ input, nextHandler }) => { + return nextHandler(input); + }, + }; + + const middleware3: Middleware<{ value: number }> = { + id: 'middleware-3', + test: async ({ input, nextHandler }) => { + return nextHandler(input); + }, + }; + + executor.use([middleware1, middleware2, middleware3]); + + executor.setOrder(['middleware-3', 'middleware-1', 'middleware-2']); + + // Access private property for testing + const middlewareList = (executor as any).middleware; + expect(middlewareList).toHaveLength(3); + expect(middlewareList[0]).toBe(middleware3); + expect(middlewareList[1]).toBe(middleware1); + expect(middlewareList[2]).toBe(middleware2); + }); + + it('should filter out middleware that does not exist in the order', () => { + const middleware1: Middleware<{ value: number }> = { + id: 'middleware-1', + test: async ({ input, nextHandler }) => { + return nextHandler(input); + }, + }; + + const middleware2: Middleware<{ value: number }> = { + id: 'middleware-2', + test: async ({ input, nextHandler }) => { + return nextHandler(input); + }, + }; + + executor.use([middleware1, middleware2]); + + executor.setOrder(['middleware-2', 'non-existent-middleware', 'middleware-1']); + + // Access private property for testing + const middlewareList = (executor as any).middleware; + expect(middlewareList).toHaveLength(2); + expect(middlewareList[0]).toBe(middleware2); + expect(middlewareList[1]).toBe(middleware1); + }); + }); + + describe('execute', () => { + it('should execute middleware chain in order', async () => { + const middleware1: Middleware<{ value: number }> = { + id: 'middleware-1', + test: async ({ input, nextHandler }) => { + return nextHandler({ ...input, state: { value: input.state.value + 1 } }); + }, + }; + + const middleware2: Middleware<{ value: number }> = { + id: 'middleware-2', + test: async ({ input, nextHandler }) => { + return nextHandler({ ...input, state: { value: input.state.value * 2 } }); + }, + }; + + executor.use([middleware1, middleware2]); + + const result = await executor.execute('test', { state: { value: 5 } }); + + expect(result.state.value).toBe(12); // (5 + 1) * 2 + }); + + it('should skip middleware that does not have the specified event handler', async () => { + const middleware1: Middleware<{ value: number }> = { + id: 'middleware-1', + test: async ({ input, nextHandler }) => { + return nextHandler({ ...input, state: { value: input.state.value + 1 } }); + }, + }; + + const middleware2: Middleware<{ value: number }> = { + id: 'middleware-2', + testX: async ({ input, nextHandler }) => { + return nextHandler({ ...input, state: { value: input.state.value - 2 } }); + }, + }; + + const middleware3: Middleware<{ value: number }> = { + id: 'middleware-3', + test: async ({ input, nextHandler }) => { + return nextHandler({ ...input, state: { value: input.state.value * 2 } }); + }, + }; + + executor.use([middleware1, middleware2, middleware3]); + + const result = await executor.execute('test', { state: { value: 5 } }); + + expect(result.state.value).toBe(12); // (5 + 1) * 2 + }); + + it('should handle middleware that returns complete status', async () => { + const middleware1: Middleware<{ value: number }> = { + id: 'middleware-1', + test: async ({ input, nextHandler }) => { + return nextHandler({ + ...input, + state: { value: input.state.value + 1 }, + status: 'complete' as MiddlewareStatus, + }); + }, + }; + + const middleware2: Middleware<{ value: number }> = { + id: 'middleware-2', + test: async ({ input, nextHandler }) => { + return nextHandler({ ...input, state: { value: input.state.value * 2 } }); + }, + }; + + executor.use([middleware1, middleware2]); + + const result = await executor.execute('test', { state: { value: 5 } }); + + expect(result.state.value).toBe(6); // 5 + 1, middleware2 is not executed + expect(result.status).toBe('complete'); + }); + + it('should handle middleware that returns discard status', async () => { + const middleware1: Middleware<{ value: number }> = { + id: 'middleware-1', + test: async ({ input, nextHandler }) => { + return nextHandler({ + ...input, + state: { value: input.state.value + 1 }, + status: 'discard' as MiddlewareStatus, + }); + }, + }; + + const middleware2: Middleware<{ value: number }> = { + id: 'middleware-2', + test: async ({ input, nextHandler }) => { + return nextHandler({ ...input, state: { value: input.state.value * 2 } }); + }, + }; + + executor.use([middleware1, middleware2]); + + const result = await executor.execute('test', { state: { value: 5 } }); + + expect(result.state.value).toBe(6); // 5 + 1, middleware2 is not executed + expect(result.status).toBe('discard'); + }); + + it('should handle concurrent execute calls by discarding the first one', async () => { + // Create a middleware that delays execution + const middleware: Middleware<{ value: number }> = { + id: 'delayed-middleware', + test: async ({ input, nextHandler }) => { + // Simulate a longer delay to ensure the first execution is still in progress + await new Promise((resolve) => setTimeout(resolve, 500)); + return nextHandler({ ...input, state: { value: input.state.value + 1 } }); + }, + }; + + executor.use(middleware); + + // Start the first execution + const firstExecution = executor.execute('test', { state: { value: 5 } }); + + // Wait a short time to ensure the first execution has started but not completed + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Start the second execution before the first one completes + const secondExecution = executor.execute('test', { state: { value: 10 } }); + + // Wait for both executions to complete + const [firstResult, secondResult] = await Promise.all([ + firstExecution, + secondExecution, + ]); + + // The first execution should be discarded + expect(firstResult.status).toBe('discard'); + + // The second execution should complete successfully + expect(secondResult.status).toBeUndefined(); + expect(secondResult.state.value).toBe(11); // 10 + 1 + }); + + it('should handle middleware that calls nextHandler multiple times', async () => { + const middleware1: Middleware<{ value: number }> = { + id: 'middleware-1', + test: async ({ input, nextHandler }) => { + const result1 = await nextHandler({ + ...input, + state: { value: input.state.value + 1 }, + }); + // This should throw an error + return nextHandler(result1); + }, + }; + + executor.use([middleware1]); + + await expect(executor.execute('test', { state: { value: 5 } })).rejects.toThrow( + 'next() called multiple times', + ); + }); + + it('should handle concurrent execute calls with different event names', async () => { + // Create middleware that handles different event names + const middleware: Middleware<{ value: number }> = { + id: 'multi-event-middleware', + test1: async ({ input, nextHandler }) => { + await new Promise((resolve) => setTimeout(resolve, 100)); + return nextHandler({ ...input, state: { value: input.state.value + 1 } }); + }, + test2: async ({ input, nextHandler }) => { + await new Promise((resolve) => setTimeout(resolve, 100)); + return nextHandler({ ...input, state: { value: input.state.value * 2 } }); + }, + }; + + executor.use(middleware); + + // Start executions with different event names + const firstExecution = executor.execute('test1', { state: { value: 5 } }); + const secondExecution = executor.execute('test2', { state: { value: 10 } }); + + // Wait for both executions to complete + const [firstResult, secondResult] = await Promise.all([ + firstExecution, + secondExecution, + ]); + + // Both executions should complete successfully since they use different event names + expect(firstResult.status).toBeUndefined(); + expect(firstResult.state.value).toBe(6); // 5 + 1 + + expect(secondResult.status).toBeUndefined(); + expect(secondResult.state.value).toBe(20); // 10 * 2 + }); + + it('should execute two different middleware executors with the same event name without cancellation', async () => { + const results: number[] = []; + + // Create two different middleware executors + const executor1 = new MiddlewareExecutor<{ value: number }>(); + const executor2 = new MiddlewareExecutor<{ value: number }>(); + + // Add middleware to each executor + executor1.use({ + id: 'middleware-1', + test: async ({ input, nextHandler }) => { + await new Promise((resolve) => setTimeout(resolve, 50)); + results.push(1); + return nextHandler({ ...input, state: { value: input.state.value + 1 } }); + }, + }); + + executor2.use({ + id: 'middleware-2', + test: async ({ input, nextHandler }) => { + await new Promise((resolve) => setTimeout(resolve, 50)); + results.push(2); + return nextHandler({ ...input, state: { value: input.state.value * 2 } }); + }, + }); + + // Execute the same event name on different executors concurrently + const p1 = executor1.execute('test', { state: { value: 5 } }); + const p2 = executor2.execute('test', { state: { value: 10 } }); + + // Wait for both executions to complete + const [r1, r2] = await Promise.all([p1, p2]); + + // Both executions should complete successfully + expect(results).toEqual([1, 2]); + expect(r1.state.value).toBe(6); // 5 + 1 + expect(r2.state.value).toBe(20); // 10 * 2 + }); + }); +}); diff --git a/test/unit/test-utils/generateMessageDraft.ts b/test/unit/test-utils/generateMessageDraft.ts new file mode 100644 index 0000000000..10ec728521 --- /dev/null +++ b/test/unit/test-utils/generateMessageDraft.ts @@ -0,0 +1,18 @@ +import { generateChannel } from './generateChannel'; +import { generateMsg } from './generateMessage'; +import type { ChannelResponse, DraftResponse } from '../../../src'; + +export const generateMessageDraft = ({ + channel: customChannel, + channel_cid, + ...customMsgDraft +}: Partial) => { + const channel = (customChannel ?? generateChannel()) as ChannelResponse; + return { + channel, + channel_cid: channel.cid, + created_at: new Date().toISOString(), + message: generateMsg(), + ...customMsgDraft, + } as DraftResponse; +}; diff --git a/test/unit/utils/FixedSizeQueueCache.test.ts b/test/unit/utils/FixedSizeQueueCache.test.ts new file mode 100644 index 0000000000..078af03b12 --- /dev/null +++ b/test/unit/utils/FixedSizeQueueCache.test.ts @@ -0,0 +1,141 @@ +import { describe, expect, it, vi } from 'vitest'; +import { FixedSizeQueueCache } from '../../../src/utils/FixedSizeQueueCache'; + +describe('FixedSizeQueueCache', () => { + it('should throw an error if size is not provided', () => { + expect(() => new FixedSizeQueueCache(0)).toThrow('Size must be greater than 0'); + }); + + it('should initialize with the correct size', () => { + const cache = new FixedSizeQueueCache(3); + expect(cache).toBeInstanceOf(FixedSizeQueueCache); + }); + + it('should add and retrieve items', () => { + const cache = new FixedSizeQueueCache(3); + + cache.add('key1', 'value1'); + cache.add('key2', 'value2'); + + expect(cache.get('key1')).toBe('value1'); + expect(cache.get('key2')).toBe('value2'); + expect(cache.get('key3')).toBeUndefined(); + }); + + it('should respect the size limit', () => { + const cache = new FixedSizeQueueCache(2); + + cache.add('key1', 'value1'); + cache.add('key2', 'value2'); + cache.add('key3', 'value3'); + + expect(cache.get('key1')).toBeUndefined(); + expect(cache.get('key2')).toBe('value2'); + expect(cache.get('key3')).toBe('value3'); + }); + + it('should update existing keys', () => { + const cache = new FixedSizeQueueCache(3); + + cache.add('key1', 'value1'); + cache.add('key2', 'value2'); + cache.add('key1', 'updated-value1'); + + expect(cache.get('key1')).toBe('updated-value1'); + expect(cache.get('key2')).toBe('value2'); + }); + + it('should move accessed items to the top of the queue', () => { + const cache = new FixedSizeQueueCache(3); + + cache.add('key1', 'value1'); + cache.add('key2', 'value2'); + cache.add('key3', 'value3'); + + // Access key1, which should move it to the end + cache.get('key1'); + + // Add a new item, which should evict key2 (now the oldest) + cache.add('key4', 'value4'); + + expect(cache.get('key1')).toBe('value1'); + expect(cache.get('key2')).toBeUndefined(); + expect(cache.get('key3')).toBe('value3'); + expect(cache.get('key4')).toBe('value4'); + }); + + it('should call provided dispose function when queue overflows', () => { + const cache = new FixedSizeQueueCache(2, { + dispose: (_, v) => { + v.unsubscribe(); + }, + }); + + const complexEntity1 = { unsubscribe: vi.fn() }; + const complexEntity2 = { unsubscribe: vi.fn() }; + const complexEntity3 = { unsubscribe: vi.fn() }; + + cache.add('ce1', complexEntity1); + cache.add('ce2', complexEntity2); + cache.add('ce3', complexEntity3); + + expect(complexEntity1.unsubscribe).toHaveBeenCalledTimes(1); + expect(complexEntity2.unsubscribe).not.toHaveBeenCalled(); + expect(complexEntity3.unsubscribe).not.toHaveBeenCalled(); + }); + + it('should not move items to the top when using peek', () => { + const cache = new FixedSizeQueueCache(3); + + cache.add('key1', 'value1'); + cache.add('key2', 'value2'); + cache.add('key3', 'value3'); + + // Peek at key1, which should NOT move it to the end + cache.peek('key1'); + + // Add a new item, which should evict key1 (still the oldest) + cache.add('key4', 'value4'); + + expect(cache.get('key1')).toBeUndefined(); + expect(cache.get('key2')).toBe('value2'); + expect(cache.get('key3')).toBe('value3'); + expect(cache.get('key4')).toBe('value4'); + }); + + it('should handle different key and value types', () => { + const cache = new FixedSizeQueueCache(3); + + cache.add(1, 'string value'); + cache.add('string key', 42); + cache.add({ id: 1 }, { data: 'object value' }); + + expect(cache.get(1)).toBe('string value'); + expect(cache.get('string key')).toBe(42); + expect(cache.get({ id: 1 })).toBeUndefined(); // Objects are compared by reference + }); + + it('should handle multiple operations in sequence', () => { + const cache = new FixedSizeQueueCache(3); + + // Add items + cache.add('key1', 'value1'); + cache.add('key2', 'value2'); + cache.add('key3', 'value3'); + + // Access an item + expect(cache.get('key1')).toBe('value1'); + + // Update an item + cache.add('key2', 'updated-value2'); + + // Add a new item (should evict key3) + cache.add('key4', 'value4'); + + // Verify final state + expect(cache.get('key1')).toBe('value1'); + expect(cache.get('key2')).toBe('updated-value2'); + expect(cache.get('key3')).toBeUndefined(); + expect(cache.get('key4')).toBe('value4'); + }); +}); diff --git a/test/unit/utils/concurrency.test.ts b/test/unit/utils/concurrency.test.ts new file mode 100644 index 0000000000..821a27a14a --- /dev/null +++ b/test/unit/utils/concurrency.test.ts @@ -0,0 +1,285 @@ +import { describe, it, expect, vi } from 'vitest'; +import { + withCancellation, + withoutConcurrency, + hasPending, + settled, +} from '../../../src/utils/concurrency'; + +describe('concurrency', () => { + describe('withCancellation', () => { + it('should cancel previous function when a new one is scheduled', async () => { + const results: number[] = []; + + const fn1 = async (signal: AbortSignal) => { + await new Promise((resolve) => setTimeout(resolve, 50)); + if (signal.aborted) { + return 'canceled'; + } + results.push(1); + return 1; + }; + + const fn2 = async (signal: AbortSignal) => { + await new Promise((resolve) => setTimeout(resolve, 50)); + if (signal.aborted) { + return 'canceled'; + } + results.push(2); + return 2; + }; + + const fn3 = async (signal: AbortSignal) => { + await new Promise((resolve) => setTimeout(resolve, 50)); + if (signal.aborted) { + return 'canceled'; + } + results.push(3); + return 3; + }; + + const tag = 'test-tag'; + + // Run functions serially + const p1 = withCancellation(tag, fn1); + const p2 = withCancellation(tag, fn2); + const p3 = withCancellation(tag, fn3); + + // Wait for all promises to resolve + const [r1, r2, r3] = await Promise.all([p1, p2, p3]); + + // Check that functions ran in order + expect(results).toEqual([3]); + expect(r1).toBe('canceled'); + expect(r2).toBe('canceled'); + expect(r3).toBe(3); + }); + + it('should not cancel functions with different tags', async () => { + const results: number[] = []; + const abortedSignals: AbortSignal[] = []; + + const fn1 = async (signal: AbortSignal) => { + await new Promise((resolve) => setTimeout(resolve, 50)); + if (signal.aborted) { + abortedSignals.push(signal); + return 'canceled'; + } + results.push(1); + return 1; + }; + + const fn2 = async (signal: AbortSignal) => { + await new Promise((resolve) => setTimeout(resolve, 50)); + if (signal.aborted) { + abortedSignals.push(signal); + return 'canceled'; + } + results.push(2); + return 2; + }; + + const tag1 = 'test-tag-1'; + const tag2 = 'test-tag-2'; + + // Start functions with different tags + const p1 = withCancellation(tag1, fn1); + const p2 = withCancellation(tag2, fn2); + + // Wait for both promises to resolve + const [r1, r2] = await Promise.all([p1, p2]); + + // Check that no functions were aborted + expect(abortedSignals.length).toBe(0); + + // Check that both functions completed successfully + expect(results).toEqual([1, 2]); + expect(r1).toBe(1); + expect(r2).toBe(2); + }); + + it('should handle errors in functions', async () => { + const fn = async (signal: AbortSignal) => { + await new Promise((resolve) => setTimeout(resolve, 50)); + if (signal.aborted) { + return 'canceled'; + } + throw new Error('Test error'); + }; + + const tag = 'test-tag'; + + // Run the function and expect it to throw + await expect(withCancellation(tag, fn)).rejects.toThrow('Test error'); + }); + + it('should handle cancellation of a function that throws an error', async () => { + const results: number[] = []; + + const fn1 = async (signal: AbortSignal) => { + await new Promise((resolve) => setTimeout(resolve, 100)); + if (signal.aborted) { + return 'canceled'; + } + throw new Error('Test error'); + }; + + const fn2 = async (signal: AbortSignal) => { + await new Promise((resolve) => setTimeout(resolve, 50)); + if (signal.aborted) { + return 'canceled'; + } + results.push(2); + return 2; + }; + + const tag = 'test-tag'; + + // Start the first function + const p1 = withCancellation(tag, fn1); + + // Wait a short time to ensure the first function has started + await new Promise((resolve) => setTimeout(resolve, 10)); + + // Start the second function before the first one completes + const p2 = withCancellation(tag, fn2); + + // Wait for both promises to resolve + const [r1, r2] = await Promise.all([p1, p2]); + + // Check that the first function was canceled + expect(r1).toBe('canceled'); + + // Check that the second function completed successfully + expect(results).toEqual([2]); + expect(r2).toBe(2); + }); + }); + + describe('withoutConcurrency', () => { + it('should run functions serially', async () => { + const fn1 = async () => { + await new Promise((resolve) => setTimeout(resolve, 50)); + return 1; + }; + + const fn2 = async () => { + await new Promise((resolve) => setTimeout(resolve, 50)); + return 2; + }; + + const fn3 = async () => { + await new Promise((resolve) => setTimeout(resolve, 50)); + return 3; + }; + + const tag = 'test-tag'; + + // Run functions serially + const p1 = withoutConcurrency(tag, fn1); + const p2 = withoutConcurrency(tag, fn2); + const p3 = withoutConcurrency(tag, fn3); + + // Wait for all promises to resolve + const [r1, r2, r3] = await Promise.all([p1, p2, p3]); + + // Check that functions ran in order + expect(r1).toBe(1); + expect(r2).toBe(2); + expect(r3).toBe(3); + }); + + it('should not cancel previous function when a new one is scheduled', async () => { + const fn1 = async () => { + await new Promise((resolve) => setTimeout(resolve, 100)); + return 1; + }; + + const fn2 = async () => { + await new Promise((resolve) => setTimeout(resolve, 50)); + return 2; + }; + + const tag = 'test-tag'; + + // Start the first function + const p1 = withoutConcurrency(tag, fn1); + + // Wait a short time to ensure the first function has started + await new Promise((resolve) => setTimeout(resolve, 10)); + + // Start the second function before the first one completes + const p2 = withoutConcurrency(tag, fn2); + + // Wait for both promises to resolve + const [r1, r2] = await Promise.all([p1, p2]); + + // Check that both functions completed successfully + expect(r1).toBe(1); + expect(r2).toBe(2); + }); + }); + + describe('hasPending', () => { + it('should return true if there is a pending promise', async () => { + const fn = async (signal: AbortSignal) => { + await new Promise((resolve) => setTimeout(resolve, 50)); + if (signal.aborted) { + return 'canceled'; + } + return 1; + }; + + const tag = 'test-tag'; + + // Start the function + const p = withCancellation(tag, fn); + + // Check that there is a pending promise + expect(hasPending(tag)).toBe(true); + + // Wait for the promise to resolve + await p; + + // Check that there is no pending promise + expect(hasPending(tag)).toBe(false); + }); + + it('should return false if there is no pending promise', () => { + const tag = 'test-tag'; + + // Check that there is no pending promise + expect(hasPending(tag)).toBe(false); + }); + }); + + describe('settled', () => { + it('should wait for the pending promise to resolve', async () => { + const results: number[] = []; + + const fn = async (signal: AbortSignal) => { + await new Promise((resolve) => setTimeout(resolve, 50)); + if (signal.aborted) { + return 'canceled'; + } + results.push(1); + return 1; + }; + + const tag = 'test-tag'; + + // Start the function + const p = withCancellation(tag, fn); + + // Wait for the promise to settle + await settled(tag); + + // Check that the function completed + expect(results).toEqual([1]); + + // Wait for the promise to resolve + await p; + expect(p).resolves.toBe(1); + }); + }); +}); diff --git a/test/unit/utils/mergeWith.test.ts b/test/unit/utils/mergeWith.test.ts new file mode 100644 index 0000000000..2ecc1a28c7 --- /dev/null +++ b/test/unit/utils/mergeWith.test.ts @@ -0,0 +1,749 @@ +import { describe, expect, it } from 'vitest'; +import { mergeWith } from '../../../src/utils/mergeWith/mergeWith'; +import { mergeWithDiff } from '../../../src/utils/mergeWith/mergeWithDiff'; +import { cleanupDiffTree, isEqual } from '../../../src/utils/mergeWith/mergeWithCore'; + +describe('mergeWith', () => { + it('should merge objects without customizer', () => { + const object = { + a: [{ b: 2 }, { d: 4 }], + e: { f: 5 }, + }; + + const other = { + a: [{ c: 3 }, { e: 5 }], + e: { g: 6 }, + x: { w: 9 }, + }; + + const result = mergeWith(object, other); + console.log('result', result); + expect(result).toEqual({ + a: [ + { b: 2, c: 3 }, + { d: 4, e: 5 }, + ], + e: { f: 5, g: 6 }, + x: { w: 9 }, + }); + }); + + it('should handle multiple sources', () => { + const object = { a: 1 }; + const source1 = { b: 2 }; + const source2 = { c: 3 }; + + const result = mergeWith(object, [source1, source2]); + + expect(result).toEqual({ a: 1, b: 2, c: 3 }); + }); + + it('should handle multiple sources overrides', () => { + const object = { a: 1 }; + const source1 = { a: 2 }; + const source2 = { a: 3 }; + + const result = mergeWith(object, [source1, source2]); + + expect(result).toEqual({ a: 3 }); + }); + + it('should handle deep merging', () => { + const object = { + a: { + b: { + c: 1, + }, + }, + }; + + const other = { + a: { + b: { + d: 2, + }, + }, + }; + + const result = mergeWith(object, other); + + expect(result).toEqual({ + a: { + b: { + c: 1, + d: 2, + }, + }, + }); + }); + + it('should handle arrays', () => { + const object = { + a: [1, 2, 3], + }; + + const other = { + a: [4, 5, 6], + }; + + const result = mergeWith(object, other); + + // Arrays are replaced, not merged + expect(result).toEqual({ + a: [4, 5, 6], + }); + }); + + it('should handle arrays with customizer', () => { + const object = { + a: [1, 2, 3], + }; + + const other = { + a: [4, 5, 6], + }; + + const customizer = (objValue: unknown, srcValue: unknown) => { + if (Array.isArray(objValue) && Array.isArray(srcValue)) { + return [...objValue, ...srcValue]; + } + return undefined; + }; + + const result = mergeWith(object, other, customizer); + + expect(result).toEqual({ + a: [1, 2, 3, 4, 5, 6], + }); + }); + + it('should handle null and undefined values', () => { + const object = { + a: 1, + b: null, + c: undefined, + d: null, + }; + + const other = { + a: null, + b: 2, + c: 3, + d: undefined, + }; + + const result = mergeWith(object, other); + + expect(result).toEqual({ + a: null, + b: 2, + c: 3, + d: null, + }); + }); + + it('should handle primitive values', () => { + const object = { + a: 1, + b: 'string', + c: true, + d: false, + e: null, + f: undefined, + }; + + const other = { + a: 2, + b: 'new string', + c: false, + d: true, + e: 0, + f: 'defined', + }; + + const result = mergeWith(object, other); + + expect(result).toEqual({ + a: 2, + b: 'new string', + c: false, + d: true, + e: 0, + f: 'defined', + }); + }); + + it('should handle symbols as keys', () => { + const sym1 = Symbol('sym1'); + const sym2 = Symbol('sym2'); + + const object: Record = { + [sym1]: 1, + [sym2]: 2, + a: 2, + }; + + const other: Record = { + [sym2]: 3, + b: 4, + }; + + const result = mergeWith(object, other); + + expect(result[sym1]).toBe(1); + expect(result[sym2]).toBe(3); + expect(result.a).toBe(2); + expect(result.b).toBe(4); + }); + + it('should handle circular references', () => { + const object: any = { a: 1 }; + object.self = object; + + const other = { b: 2 }; + + const result = mergeWith(object, other); + + // The result should have the properties from both objects + expect(result.a).toBe(1); + expect(result.b).toBe(2); + + // The circular reference is broken since we're creating a new object + expect(result.self).not.toBe(result); + + // Check that result.self has the expected properties + expect(result.self.a).toBe(1); + expect(result.self).toHaveProperty('self'); + }); + + it('should handle customizer with all parameters', () => { + const object = { a: 1 }; + const other = { a: 2 }; + + const customizerCalls: Array<{ + objValue: unknown; + srcValue: unknown; + key: string | symbol; + object: object; + source: object; + stack: Set; + }> = []; + + const customizer = ( + objValue: unknown, + srcValue: unknown, + key: string | symbol, + object: object, + source: object, + stack: Set, + ) => { + customizerCalls.push({ objValue, srcValue, key, object, source, stack }); + return undefined; + }; + + const result = mergeWith(object, other, customizer); + + expect(customizerCalls.length).toBe(1); + expect(customizerCalls[0].objValue).toBe(1); + expect(customizerCalls[0].srcValue).toBe(2); + expect(customizerCalls[0].key).toBe('a'); + + // The object passed to customizer is a copy, not the original + expect(customizerCalls[0].object).not.toBe(object); + expect(customizerCalls[0].object).toEqual({ a: 2 }); + expect(customizerCalls[0].source).toBe(other); + expect(customizerCalls[0].stack).toBeInstanceOf(Set); + }); + + it('should return a new object, not the original', () => { + const object = { a: 1 }; + const other = { b: 2 }; + + const result = mergeWith(object, other); + + // The result should be a new object, not the original + expect(result).not.toBe(object); + expect(result).toEqual({ a: 1, b: 2 }); + }); + + it('should handle empty objects', () => { + const object = {}; + const other = {}; + + const result = mergeWith(object, other); + + expect(result).toEqual({}); + }); + + it('should handle empty arrays', () => { + const object = { a: [] }; + const other = { a: [] }; + + const result = mergeWith(object, other); + + expect(result).toEqual({ a: [] }); + }); + + it('should handle objects with prototype properties', () => { + const object = { a: 1 }; + const other = Object.create({ b: 2 }); + + const result = mergeWith(object, other); + + expect(result).toEqual({ a: 1 }); + }); + + it('should use source class instance when both target and source are class instances', () => { + // Create File instances + const targetFile = new File(['target content'], 'target.txt', { type: 'text/plain' }); + const sourceFile = new File(['source content'], 'source.txt', { type: 'text/html' }); + + const result = mergeWith({ file: targetFile }, { file: sourceFile }); + + // The source class instance should be used + expect(result.file).toBe(sourceFile); + expect(result.file.name).toBe('source.txt'); + expect(result.file.type).toBe('text/html'); + }); + + it('should preserve target class instance when source is a plain object', () => { + // Create File instance and plain object + const targetFile = new File(['target content'], 'target.txt', { type: 'text/plain' }); + const sourcePlain = { name: 'source.txt', content: 'source content' }; + + const result = mergeWith({ file: targetFile }, { file: sourcePlain }); + + // The target class instance should be preserved + expect(result.file).toBe(targetFile); + expect(result.file.name).toBe('target.txt'); + expect(result.file.type).toBe('text/plain'); + }); + + it('should use source class instance when target is a plain object', () => { + // Create File instance and plain object + const sourceFile = new File(['source content'], 'source.txt', { type: 'text/html' }); + const targetPlain = { name: 'target.txt', content: 'target content' }; + + const result = mergeWith({ file: targetPlain }, { file: sourceFile }); + + // The source class instance should be used + expect(result.file).toBe(sourceFile); + expect(result.file.name).toBe('source.txt'); + expect((result.file as unknown as File).type).toBe('text/html'); + }); +}); + +// Original tests for mergeWith... + +describe('mergeWithDiff', () => { + it('should return result and diff for simple objects', () => { + const target = { a: 1, b: 2 }; + const source = { b: 3, c: 4 }; + + const { result, diff } = mergeWithDiff(target, source); + + expect(result).toEqual({ a: 1, b: 3, c: 4 }); + expect(diff).toEqual({ + children: { + b: { type: 'updated', value: 3, oldValue: 2, children: {} }, + c: { type: 'added', value: 4, children: {} }, + }, + }); + }); + + it('should track nested object changes', () => { + const target = { + a: { x: 1, y: 2 }, + b: { z: 3 }, + }; + const source = { + a: { y: 5, w: 6 }, + c: { v: 7 }, + }; + + const { result, diff } = mergeWithDiff(target, source); + + expect(result).toEqual({ + a: { x: 1, y: 5, w: 6 }, + b: { z: 3 }, + c: { v: 7 }, + }); + + expect(diff).toEqual({ + children: { + a: { + type: 'updated', + value: { x: 1, y: 5, w: 6 }, + oldValue: { x: 1, y: 2 }, + children: { + y: { type: 'updated', value: 5, oldValue: 2, children: {} }, + w: { type: 'added', value: 6, children: {} }, + }, + }, + c: { type: 'added', value: { v: 7 }, children: {} }, + }, + }); + }); + + it('should track array changes', () => { + const target = { fruits: ['apple', 'banana'] }; + const source = { fruits: ['cherry', 'date'] }; + + const { result, diff } = mergeWithDiff(target, source); + + expect(result).toEqual({ fruits: ['cherry', 'date'] }); + expect(diff).toEqual({ + children: { + fruits: { + type: 'updated', + children: { + '0': { + type: 'updated', + value: 'cherry', + oldValue: 'apple', + children: {}, + }, + '1': { + type: 'updated', + value: 'date', + oldValue: 'banana', + children: {}, + }, + }, + oldValue: ['apple', 'banana'], + value: ['cherry', 'date'], + }, + }, + }); + }); + + it('should merge with customizer and track changes correctly', () => { + const target = { fruits: ['apple'] }; + const source = { fruits: ['banana'] }; + + const customizer = (objValue: unknown, srcValue: unknown) => { + if (Array.isArray(objValue) && Array.isArray(srcValue)) { + return [...objValue, ...srcValue]; + } + return undefined; + }; + + const { result, diff } = mergeWithDiff(target, source, customizer); + + expect(result).toEqual({ fruits: ['apple', 'banana'] }); + expect(diff).toEqual({ + children: { + fruits: { + type: 'updated', + children: { + '1': { + type: 'added', + value: 'banana', + children: {}, + }, + }, + oldValue: ['apple'], + value: ['apple', 'banana'], + }, + }, + }); + }); + + it('should merge multiple sources with diff tracking', () => { + const target = { a: 1 }; + const source1 = { b: 2 }; + const source2 = { c: 3 }; + + const { result, diff } = mergeWithDiff(target, [source1, source2]); + + expect(result).toEqual({ a: 1, b: 2, c: 3 }); + expect(diff).toEqual({ + children: { + b: { type: 'added', value: 2, children: {} }, + c: { type: 'added', value: 3, children: {} }, + }, + }); + }); + + it('should ignore class instances in diff tracking', () => { + // Create test File instances + const targetFile = new File(['target content'], 'target.txt', { type: 'text/plain' }); + const sourceFile = new File(['source content'], 'source.txt', { type: 'text/html' }); + + // Perform merge with diff tracking + const { result, diff } = mergeWithDiff({ file: targetFile }, { file: sourceFile }); + + // Verify source file is used in result + expect(result.file).toBe(sourceFile); + + const fileDiff = diff.children.file; + + // Verify the structure of the diff + expect(fileDiff.type).toBe('updated'); + expect(fileDiff.children).toEqual({}); + + // Verify the types of value and oldValue properties + expect(fileDiff.value instanceof Blob).toBe(true); + expect(fileDiff.oldValue instanceof Blob).toBe(true); + + // Verify MIME types to ensure correct object identification + expect((fileDiff.value as Blob).type).toBe('text/html'); // Source file type + expect((fileDiff.oldValue as Blob).type).toBe('text/plain'); // Target file type + }); + + it('should handle circular references in diff tracking', () => { + const target: any = { a: 1 }; + target.self = target; + + const source = { b: 2 }; + + const { result, diff } = mergeWithDiff(target, source); + + expect(result.a).toBe(1); + expect(result.b).toBe(2); + expect(result.self).not.toBe(result); + + // Diff should only track the added property b + expect(diff).toEqual({ + children: { + b: { type: 'added', value: 2, children: {} }, + }, + }); + }); +}); + +describe('cleanupDiffTree', () => { + it('should return null for empty diff nodes', () => { + const diffNode = { children: {} }; + const result = cleanupDiffTree(diffNode); + expect(result).toBeNull(); + }); + + it('should keep nodes with type', () => { + const diffNode = { + type: 'added', + value: 42, + children: {}, + }; + const result = cleanupDiffTree(diffNode); + expect(result).toEqual({ + type: 'added', + value: 42, + children: {}, + }); + }); + + it('should remove empty child nodes', () => { + const diffNode = { + children: { + a: { children: {} }, + b: { type: 'added', value: 2, children: {} }, + c: { + children: { + d: { children: {} }, + }, + }, + }, + }; + + const result = cleanupDiffTree(diffNode); + expect(result).toEqual({ + children: { + b: { type: 'added', value: 2, children: {} }, + }, + }); + }); + + it('should keep parent nodes with non-empty children', () => { + const diffNode = { + children: { + parent: { + children: { + child: { type: 'added', value: 42, children: {} }, + }, + }, + }, + }; + + const result = cleanupDiffTree(diffNode); + expect(result).toEqual({ + children: { + parent: { + children: { + child: { type: 'added', value: 42, children: {} }, + }, + }, + }, + }); + }); + + it('should handle complex nested structures', () => { + const diffNode = { + children: { + a: { + children: { + b: { type: 'updated', value: 2, oldValue: 1, children: {} }, + c: { children: {} }, + }, + }, + d: { children: {} }, + e: { + type: 'added', + value: { f: 3 }, + children: { + f: { type: 'added', value: 3, children: {} }, + }, + }, + }, + }; + + const result = cleanupDiffTree(diffNode); + expect(result).toEqual({ + children: { + a: { + children: { + b: { type: 'updated', value: 2, oldValue: 1, children: {} }, + }, + }, + e: { + type: 'added', + value: { f: 3 }, + children: { + f: { type: 'added', value: 3, children: {} }, + }, + }, + }, + }); + }); +}); + +// Testing the isEqual function +describe('isEqual', () => { + it('should consider primitives equal if they have the same value', () => { + expect(isEqual(42, 42)).toBe(true); + expect(isEqual('hello', 'hello')).toBe(true); + expect(isEqual(true, true)).toBe(true); + expect(isEqual(null, null)).toBe(true); + expect(isEqual(undefined, undefined)).toBe(true); + }); + + it('should consider different primitives not equal', () => { + expect(isEqual(42, 43)).toBe(false); + expect(isEqual('hello', 'world')).toBe(false); + expect(isEqual(true, false)).toBe(false); + expect(isEqual(null, undefined)).toBe(false); + expect(isEqual(0, false)).toBe(false); + expect(isEqual('', false)).toBe(false); + }); + + it('should handle NaN correctly', () => { + expect(isEqual(NaN, NaN)).toBe(true); + expect(isEqual(NaN, 0)).toBe(false); + expect(isEqual(NaN, null)).toBe(false); + }); + + it('should compare arrays by value', () => { + expect(isEqual([1, 2, 3], [1, 2, 3])).toBe(true); + expect(isEqual([1, 2, 3], [1, 2, 4])).toBe(false); + expect(isEqual([1, 2], [1, 2, 3])).toBe(false); + expect(isEqual([], [])).toBe(true); + }); + + it('should compare nested arrays correctly', () => { + expect(isEqual([1, [2, 3]], [1, [2, 3]])).toBe(true); + expect(isEqual([1, [2, 3]], [1, [2, 4]])).toBe(false); + expect(isEqual([1, [2, [3]]], [1, [2, [3]]])).toBe(true); + }); + + it('should compare objects by value', () => { + expect(isEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true); + expect(isEqual({ a: 1, b: 2 }, { b: 2, a: 1 })).toBe(true); // order doesn't matter + expect(isEqual({ a: 1, b: 2 }, { a: 1, b: 3 })).toBe(false); + expect(isEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false); + expect(isEqual({}, {})).toBe(true); + }); + + it('should compare nested objects correctly', () => { + expect(isEqual({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } })).toBe(true); + expect(isEqual({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 3 } })).toBe(false); + expect(isEqual({ a: 1, b: { c: { d: 3 } } }, { a: 1, b: { c: { d: 3 } } })).toBe( + true, + ); + }); + + it('should compare mixed nested structures', () => { + expect(isEqual({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] })).toBe(true); + expect(isEqual({ a: [1, { b: 2 }] }, { a: [1, { b: 3 }] })).toBe(false); + expect(isEqual([{ a: 1 }, [2, 3]], [{ a: 1 }, [2, 3]])).toBe(true); + }); + + it('should handle Date objects', () => { + const date1 = new Date('2023-01-01'); + const date2 = new Date('2023-01-01'); + const date3 = new Date('2023-01-02'); + + expect(isEqual(date1, date2)).toBe(true); + expect(isEqual(date1, date3)).toBe(false); + expect(isEqual({ date: date1 }, { date: date2 })).toBe(true); + }); + + it('should handle RegExp objects', () => { + const regex1 = /abc/g; + const regex2 = /abc/g; + const regex3 = /def/g; + + expect(isEqual(regex1, regex2)).toBe(true); + expect(isEqual(regex1, regex3)).toBe(false); + expect(isEqual({ regex: regex1 }, { regex: regex2 })).toBe(true); + }); + + it('should handle class instances as not equal', () => { + const file1 = new File(['content'], 'test.txt'); + const file2 = new File(['content'], 'test.txt'); + + // Class instances should not be considered equal even with same properties + expect(isEqual(file1, file2)).toBe(false); + expect(isEqual(file1, file1)).toBe(true); // Same reference is equal + }); + + it('should handle circular references', () => { + const obj1: any = { a: 1 }; + obj1.self = obj1; + + const obj2: any = { a: 1 }; + obj2.self = obj2; + + const obj3: any = { a: 2 }; + obj3.self = obj3; + + expect(isEqual(obj1, obj2)).toBe(true); + expect(isEqual(obj1, obj3)).toBe(false); + }); + + it('should handle nested circular references', () => { + const obj1: any = { a: 1 }; + const obj2: any = { b: 2, parent: obj1 }; + obj1.child = obj2; + + const obj3: any = { a: 1 }; + const obj4: any = { b: 2, parent: obj3 }; + obj3.child = obj4; + + const obj5: any = { a: 1 }; + const obj6: any = { b: 3, parent: obj5 }; // different value for b + obj5.child = obj6; + + expect(isEqual(obj1, obj3)).toBe(true); + expect(isEqual(obj1, obj5)).toBe(false); + }); + + it('should compare object property keys correctly', () => { + // Objects with same keys but different order + expect(isEqual({ a: 1, b: 2, c: 3 }, { c: 3, b: 2, a: 1 })).toBe(true); + + // Ensure keys in second object are correctly checked + const obj1 = { a: 1, b: 2 }; + const obj2 = { a: 1, c: 3 }; + expect(isEqual(obj1, obj2)).toBe(false); + }); +}); diff --git a/yarn.lock b/yarn.lock index f037fe686a..540f18d466 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4156,6 +4156,11 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= +linkifyjs@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.2.0.tgz#9dd30222b9cbabec9c950e725ec00031c7fa3f08" + integrity sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw== + lint-staged@^15.2.2: version "15.2.2" resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.2.tgz#ad7cbb5b3ab70e043fa05bff82a09ed286bc4c5f" From 4015112f3546d2580b349bdcb0e518574ea3173a Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 28 Apr 2025 13:26:55 +0000 Subject: [PATCH 35/47] chore(release): 9.0.0-rc.11 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [9.0.0-rc.11](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.10...v9.0.0-rc.11) (2025-04-28) ### ⚠ BREAKING CHANGES * Replacement of FormatMessageResponse with LocalMessage type ### Bug Fixes * [REACT-344] remove Agora & 100ms integrations ([#1519](https://github.com/GetStream/stream-chat-js/issues/1519)) ([16cd81a](https://github.com/GetStream/stream-chat-js/commit/16cd81a06c3f3daf4f6955d3c7f353283400031e)) * [REACT-350] make archived_at & pinned_at nullable ([#1515](https://github.com/GetStream/stream-chat-js/issues/1515)) ([318825a](https://github.com/GetStream/stream-chat-js/commit/318825a335342c2e32d19469f736df95feb87bee)) * [REACT-353] unify pinned_at & archived_at nullish values ([#1516](https://github.com/GetStream/stream-chat-js/issues/1516)) ([a840226](https://github.com/GetStream/stream-chat-js/commit/a8402268fabe1826e74e383afd9962bd6ab6376f)), closes [#1515](https://github.com/GetStream/stream-chat-js/issues/1515) ### Features * [CHA-794] Add sort and filter param to queryThreads ([#1511](https://github.com/GetStream/stream-chat-js/issues/1511)) ([ea7fe99](https://github.com/GetStream/stream-chat-js/commit/ea7fe999aa6d7609fa58e4808b067a8457cf187f)) * [CHA-855] - Refactoring partial update member ([#1517](https://github.com/GetStream/stream-chat-js/issues/1517)) ([e4f7e68](https://github.com/GetStream/stream-chat-js/commit/e4f7e68298e0959309aa592ce778f6cbbbcc4ab0)) * message composer ([#1495](https://github.com/GetStream/stream-chat-js/issues/1495)) ([0c07524](https://github.com/GetStream/stream-chat-js/commit/0c07524f6551e9257b229b262b4d1e03ab44561b)), closes [stream-chat-react#2669](https://github.com/GetStream/stream-chat-react/issues/2669) --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d40cc69c7..e079beccb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +## [9.0.0-rc.11](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.10...v9.0.0-rc.11) (2025-04-28) + +### ⚠ BREAKING CHANGES + +* Replacement of FormatMessageResponse with LocalMessage +type + +### Bug Fixes + +* [REACT-344] remove Agora & 100ms integrations ([#1519](https://github.com/GetStream/stream-chat-js/issues/1519)) ([16cd81a](https://github.com/GetStream/stream-chat-js/commit/16cd81a06c3f3daf4f6955d3c7f353283400031e)) +* [REACT-350] make archived_at & pinned_at nullable ([#1515](https://github.com/GetStream/stream-chat-js/issues/1515)) ([318825a](https://github.com/GetStream/stream-chat-js/commit/318825a335342c2e32d19469f736df95feb87bee)) +* [REACT-353] unify pinned_at & archived_at nullish values ([#1516](https://github.com/GetStream/stream-chat-js/issues/1516)) ([a840226](https://github.com/GetStream/stream-chat-js/commit/a8402268fabe1826e74e383afd9962bd6ab6376f)), closes [#1515](https://github.com/GetStream/stream-chat-js/issues/1515) + +### Features + +* [CHA-794] Add sort and filter param to queryThreads ([#1511](https://github.com/GetStream/stream-chat-js/issues/1511)) ([ea7fe99](https://github.com/GetStream/stream-chat-js/commit/ea7fe999aa6d7609fa58e4808b067a8457cf187f)) +* [CHA-855] - Refactoring partial update member ([#1517](https://github.com/GetStream/stream-chat-js/issues/1517)) ([e4f7e68](https://github.com/GetStream/stream-chat-js/commit/e4f7e68298e0959309aa592ce778f6cbbbcc4ab0)) +* message composer ([#1495](https://github.com/GetStream/stream-chat-js/issues/1495)) ([0c07524](https://github.com/GetStream/stream-chat-js/commit/0c07524f6551e9257b229b262b4d1e03ab44561b)), closes [stream-chat-react#2669](https://github.com/GetStream/stream-chat-react/issues/2669) + ## [9.0.0-rc.10](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.9...v9.0.0-rc.10) (2025-04-09) ### Bug Fixes From 44902e76596a271e098e545e08764e7569ca1c0e Mon Sep 17 00:00:00 2001 From: MartinCupela <32706194+MartinCupela@users.noreply.github.com> Date: Mon, 28 Apr 2025 23:20:02 +0200 Subject: [PATCH 36/47] feat: add missing configuration parameters for AttachmentManager and TextComposer (#1520) --- src/messageComposer/attachmentManager.ts | 8 +++ .../configuration/configuration.ts | 2 + src/messageComposer/configuration/types.ts | 9 ++- src/messageComposer/messageComposer.ts | 6 +- src/messageComposer/textComposer.ts | 13 ++++ .../MessageComposer/attachmentManager.test.ts | 4 +- .../unit/MessageComposer/textComposer.test.ts | 59 +++++++++++++++++++ 7 files changed, 93 insertions(+), 8 deletions(-) diff --git a/src/messageComposer/attachmentManager.ts b/src/messageComposer/attachmentManager.ts index 5361136bfe..9143d4f08b 100644 --- a/src/messageComposer/attachmentManager.ts +++ b/src/messageComposer/attachmentManager.ts @@ -89,6 +89,14 @@ export class AttachmentManager { return this.composer.config.attachments; } + get acceptedFiles() { + return this.config.acceptedFiles; + } + + set acceptedFiles(acceptedFiles: AttachmentManagerConfig['acceptedFiles']) { + this.composer.updateConfig({ attachments: { acceptedFiles } }); + } + get fileUploadFilter() { return this.config.fileUploadFilter; } diff --git a/src/messageComposer/configuration/configuration.ts b/src/messageComposer/configuration/configuration.ts index 4246658ac4..0087107ed6 100644 --- a/src/messageComposer/configuration/configuration.ts +++ b/src/messageComposer/configuration/configuration.ts @@ -26,11 +26,13 @@ export const DEFAULT_LINK_PREVIEW_MANAGER_CONFIG: LinkPreviewsManagerConfig = { }; export const DEFAULT_ATTACHMENT_MANAGER_CONFIG: AttachmentManagerConfig = { + acceptedFiles: [], // an empty array means all files are accepted fileUploadFilter: () => true, maxNumberOfFilesPerMessage: API_MAX_FILES_ALLOWED_PER_MESSAGE, }; export const DEFAULT_TEXT_COMPOSER_CONFIG: TextComposerConfig = { + enabled: true, publishTypingEvents: true, }; diff --git a/src/messageComposer/configuration/types.ts b/src/messageComposer/configuration/types.ts index c9850c8b3b..c7eec87c28 100644 --- a/src/messageComposer/configuration/types.ts +++ b/src/messageComposer/configuration/types.ts @@ -12,7 +12,9 @@ export type DraftsConfiguration = { enabled: boolean; }; export type TextComposerConfig = { - /** If true, triggers typing events on text input keystroke */ + /** If false, the text input, change and selection events are disabled */ + enabled: boolean; + /** If true, triggers typing events on text input keystroke. Disabled for threads and message editing by default. */ publishTypingEvents: boolean; /** Default value for the message input */ defaultValue?: string; @@ -30,6 +32,11 @@ export type AttachmentManagerConfig = { fileUploadFilter: FileUploadFilter; /** Maximum number of attachments allowed per message */ maxNumberOfFilesPerMessage: number; + /** + * Array of one or more file types, or unique file type specifiers (https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/accept#unique_file_type_specifiers), + * describing which file types to allow to select when uploading files. + */ + acceptedFiles: string[]; // todo: refactor this. We want a pipeline where it would be possible to customize the preparation, upload, and post-upload steps. /** Function that allows to customize the upload request. */ doUploadRequest?: UploadRequestFn; diff --git a/src/messageComposer/messageComposer.ts b/src/messageComposer/messageComposer.ts index b19e5150f6..e3cc4a65e7 100644 --- a/src/messageComposer/messageComposer.ts +++ b/src/messageComposer/messageComposer.ts @@ -236,7 +236,7 @@ export class MessageComposer { return this.compositionContext.legacyThreadId; } - // check if message is a reply, get parentMessageId + // check if the message is a reply, get parentMessageId if (typeof this.compositionContext.parent_id === 'string') { return this.compositionContext.parent_id; } @@ -333,10 +333,6 @@ export class MessageComposer { initEditingAuditState = ( composition?: DraftResponse | MessageResponse | LocalMessage, ) => initEditingAuditState(composition); - // this.config?.drafts.enabled || !compositionIsDraftResponse(composition) - // ? composition - // : undefined, - // ); private logStateUpdateTimestamp() { this.editingAuditState.partialNext({ diff --git a/src/messageComposer/textComposer.ts b/src/messageComposer/textComposer.ts index a3ca56f5c2..b05daa34d6 100644 --- a/src/messageComposer/textComposer.ts +++ b/src/messageComposer/textComposer.ts @@ -68,6 +68,14 @@ export class TextComposer { return this.composer.config.text; } + get enabled() { + return this.composer.config.text.enabled; + } + + set enabled(enabled: boolean) { + this.composer.updateConfig({ text: { enabled } }); + } + set defaultValue(defaultValue: string) { this.composer.updateConfig({ text: { defaultValue } }); } @@ -138,10 +146,13 @@ export class TextComposer { }; setText = (text: string) => { + if (!this.enabled) return; this.state.partialNext({ text }); }; insertText = ({ text, selection }: { text: string; selection?: TextSelection }) => { + if (!this.enabled) return; + const finalSelection: TextSelection = selection ?? { start: this.text.length, end: this.text.length, @@ -189,6 +200,7 @@ export class TextComposer { selection: TextSelection; text: string; }) => { + if (!this.enabled) return; const output = await this.middlewareExecutor.execute('onChange', { state: { ...this.state.getLatestValue(), @@ -209,6 +221,7 @@ export class TextComposer { // todo: document how to register own middleware handler to simulate onSelectUser prop handleSelect = async (target: TextComposerSuggestion) => { + if (!this.enabled) return; const output = await this.middlewareExecutor.execute( 'onSuggestionItemSelect', { diff --git a/test/unit/MessageComposer/attachmentManager.test.ts b/test/unit/MessageComposer/attachmentManager.test.ts index badbeb08eb..d3cdbb0764 100644 --- a/test/unit/MessageComposer/attachmentManager.test.ts +++ b/test/unit/MessageComposer/attachmentManager.test.ts @@ -176,7 +176,7 @@ describe('AttachmentManager', () => { describe('getters', () => { it('should retrieve attachments config from composer', () => { - const config: AttachmentManagerConfig = { + const config: Partial = { doUploadRequest: () => { return Promise.resolve({ file: 'x' }); }, @@ -186,7 +186,7 @@ describe('AttachmentManager', () => { const { messageComposer: { attachmentManager }, } = setup({ config }); - expect(attachmentManager.config).toEqual(config); + expect(attachmentManager.config).toEqual({ ...config, acceptedFiles: [] }); }); it('should return the correct values from state', async () => { diff --git a/test/unit/MessageComposer/textComposer.test.ts b/test/unit/MessageComposer/textComposer.test.ts index 812aa59158..237237671b 100644 --- a/test/unit/MessageComposer/textComposer.test.ts +++ b/test/unit/MessageComposer/textComposer.test.ts @@ -199,6 +199,15 @@ describe('TextComposer', () => { textComposer.state.partialNext({ text: '' }); expect(textComposer.textIsEmpty).toBe(true); }); + + it('gets the current value of enabled', () => { + const { + messageComposer: { textComposer }, + } = setup(); + expect(textComposer.enabled).toBe(true); + textComposer.enabled = false; + expect(textComposer.enabled).toBe(false); + }); }); describe('initState', () => { @@ -372,6 +381,18 @@ describe('TextComposer', () => { textComposer.setText('New text'); expect(textComposer.text).toBe('New text'); }); + it('should not update the text when disabled', () => { + const message: LocalMessage = { + id: 'test-message', + type: 'regular', + text: 'Hello world', + }; + const { + messageComposer: { textComposer }, + } = setup({ composition: message, config: { enabled: false } }); + textComposer.setText('New text'); + expect(textComposer.text).toBe(message.text); + }); }); describe('insertText', () => { @@ -460,6 +481,14 @@ describe('TextComposer', () => { textComposer.insertText({ text: insertedText, selection: { start: 7, end: 9 } }); expect(textComposer.text).toBe('Hello wHi '); }); + + it('should not insert text if disabled', () => { + const { + messageComposer: { textComposer }, + } = setup({ composition: message, config: { enabled: false } }); + textComposer.insertText({ text: ' beautiful', selection: { start: 5, end: 5 } }); + expect(textComposer.text).toBe(message.text); + }); }); describe('closeSuggestions', () => { @@ -512,6 +541,22 @@ describe('TextComposer', () => { ); }); + it('should not update state with middleware result if disabled', async () => { + const { + messageComposer: { textComposer }, + } = setup({ config: { enabled: false } }); + textComposer.state.next(initialState); + const initialText = textComposer.text; + const initialSelection = textComposer.selection; + await textComposer.handleChange({ + text: 'Test message', + selection: { start: 12, end: 12 }, + }); + + expect(textComposer.text).toBe(initialText); + expect(textComposer.selection).toEqual(initialSelection); + }); + it('should not update state if middleware returns discard status', async () => { const { messageComposer: { textComposer }, @@ -631,6 +676,20 @@ describe('TextComposer', () => { ); }); + it('should not update state with middleware result if disabled', async () => { + const { + messageComposer: { textComposer }, + } = setup({ config: { enabled: false } }); + textComposer.state.next(initialState); + + const target = { id: 'user-1' }; + const executeSpy = vi.spyOn(textComposer.middlewareExecutor, 'execute'); + executeSpy.mockResolvedValueOnce(textComposerMiddlewareExecuteOutput); + await textComposer.handleSelect(target); + + expect(textComposer.state.getLatestValue()).toEqual(initialState); + }); + it('should not update state if middleware returns discard status', async () => { const { messageComposer: { textComposer }, From ab39bf577f375e0154ded9871d2ae5ce54c022e4 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 28 Apr 2025 21:21:26 +0000 Subject: [PATCH 37/47] chore(release): 9.0.0-rc.12 [skip ci] ## [9.0.0-rc.12](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.11...v9.0.0-rc.12) (2025-04-28) ### Features * add missing configuration parameters for AttachmentManager and TextComposer ([#1520](https://github.com/GetStream/stream-chat-js/issues/1520)) ([44902e7](https://github.com/GetStream/stream-chat-js/commit/44902e76596a271e098e545e08764e7569ca1c0e)) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e079beccb0..204e8c87cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [9.0.0-rc.12](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.11...v9.0.0-rc.12) (2025-04-28) + +### Features + +* add missing configuration parameters for AttachmentManager and TextComposer ([#1520](https://github.com/GetStream/stream-chat-js/issues/1520)) ([44902e7](https://github.com/GetStream/stream-chat-js/commit/44902e76596a271e098e545e08764e7569ca1c0e)) + ## [9.0.0-rc.11](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.10...v9.0.0-rc.11) (2025-04-28) ### ⚠ BREAKING CHANGES From 02cd9a8d8a2a3acc31631edcdea2c9cf9f5afb53 Mon Sep 17 00:00:00 2001 From: martincupela Date: Mon, 28 Apr 2025 23:34:25 +0200 Subject: [PATCH 38/47] feat: disable link previews in message composer --- src/messageComposer/configuration/configuration.ts | 2 +- test/unit/MessageComposer/linkPreviewsManager.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/messageComposer/configuration/configuration.ts b/src/messageComposer/configuration/configuration.ts index 0087107ed6..77282c9dce 100644 --- a/src/messageComposer/configuration/configuration.ts +++ b/src/messageComposer/configuration/configuration.ts @@ -9,7 +9,7 @@ import type { TextComposerConfig } from './types'; export const DEFAULT_LINK_PREVIEW_MANAGER_CONFIG: LinkPreviewsManagerConfig = { debounceURLEnrichmentMs: 1500, - enabled: true, + enabled: false, findURLFn: (text: string): string[] => find(text, 'url', { defaultProtocol: 'https' }).reduce((acc, link) => { try { diff --git a/test/unit/MessageComposer/linkPreviewsManager.test.ts b/test/unit/MessageComposer/linkPreviewsManager.test.ts index 4ddef2caa3..8e89bca04f 100644 --- a/test/unit/MessageComposer/linkPreviewsManager.test.ts +++ b/test/unit/MessageComposer/linkPreviewsManager.test.ts @@ -109,7 +109,7 @@ describe('LinkPreviewsManager', () => { const { messageComposer: { linkPreviewsManager }, } = setup({ config: null }); - expect(linkPreviewsManager.config.enabled).toBe(true); + expect(linkPreviewsManager.config.enabled).toBe(false); expect(linkPreviewsManager.config.debounceURLEnrichmentMs).toBe( DEFAULT_LINK_PREVIEW_MANAGER_CONFIG.debounceURLEnrichmentMs, ); From 20140ed15995dbef7589dddf82aa384c721b3146 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 28 Apr 2025 21:35:43 +0000 Subject: [PATCH 39/47] chore(release): 9.0.0-rc.13 [skip ci] ## [9.0.0-rc.13](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.12...v9.0.0-rc.13) (2025-04-28) ### Features * disable link previews in message composer ([02cd9a8](https://github.com/GetStream/stream-chat-js/commit/02cd9a8d8a2a3acc31631edcdea2c9cf9f5afb53)) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 204e8c87cd..219c36cf28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [9.0.0-rc.13](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.12...v9.0.0-rc.13) (2025-04-28) + +### Features + +* disable link previews in message composer ([02cd9a8](https://github.com/GetStream/stream-chat-js/commit/02cd9a8d8a2a3acc31631edcdea2c9cf9f5afb53)) + ## [9.0.0-rc.12](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.11...v9.0.0-rc.12) (2025-04-28) ### Features From 9aae032edbdf45bc7d7bf5c25695e7246b8d1821 Mon Sep 17 00:00:00 2001 From: martincupela Date: Tue, 29 Apr 2025 15:16:28 +0200 Subject: [PATCH 40/47] feat: make MessageComposer middleware executors public --- src/client.ts | 2 +- src/messageComposer/messageComposer.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client.ts b/src/client.ts index d31ff3d832..0f30b50d71 100644 --- a/src/client.ts +++ b/src/client.ts @@ -243,7 +243,7 @@ type MessageComposerSetupFunction = ({ composer: MessageComposer; }) => void | MessageComposerTearDownFunction; -type MessageComposerSetupState = { +export type MessageComposerSetupState = { /** * Each `MessageComposer` runs this function each time its signature changes or * whenever you run `MessageComposer.registerSubscriptions`. Function returned diff --git a/src/messageComposer/messageComposer.ts b/src/messageComposer/messageComposer.ts index e3cc4a65e7..31b589a6ba 100644 --- a/src/messageComposer/messageComposer.ts +++ b/src/messageComposer/messageComposer.ts @@ -113,6 +113,8 @@ export class MessageComposer { readonly editingAuditState: StateStore; readonly configState: StateStore; readonly compositionContext: CompositionContext; + readonly compositionMiddlewareExecutor: MessageComposerMiddlewareExecutor; + readonly draftCompositionMiddlewareExecutor: MessageDraftComposerMiddlewareExecutor; editedMessage?: LocalMessage; attachmentManager: AttachmentManager; @@ -123,8 +125,6 @@ export class MessageComposer { // todo: mediaRecorder: MediaRecorderController; private unsubscribeFunctions: Set<() => void> = new Set(); - private compositionMiddlewareExecutor: MessageComposerMiddlewareExecutor; - private draftCompositionMiddlewareExecutor: MessageDraftComposerMiddlewareExecutor; constructor({ composition, From b178abf198c054b20db9257f1b926e39dd4389bd Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 29 Apr 2025 13:17:58 +0000 Subject: [PATCH 41/47] chore(release): 9.0.0-rc.14 [skip ci] ## [9.0.0-rc.14](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.13...v9.0.0-rc.14) (2025-04-29) ### Features * make MessageComposer middleware executors public ([9aae032](https://github.com/GetStream/stream-chat-js/commit/9aae032edbdf45bc7d7bf5c25695e7246b8d1821)) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 219c36cf28..d06fa37465 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [9.0.0-rc.14](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.13...v9.0.0-rc.14) (2025-04-29) + +### Features + +* make MessageComposer middleware executors public ([9aae032](https://github.com/GetStream/stream-chat-js/commit/9aae032edbdf45bc7d7bf5c25695e7246b8d1821)) + ## [9.0.0-rc.13](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.12...v9.0.0-rc.13) (2025-04-28) ### Features From 2c0c639b6539fe82e6bc39d9a35a2905d33287c6 Mon Sep 17 00:00:00 2001 From: martincupela Date: Wed, 30 Apr 2025 13:28:38 +0200 Subject: [PATCH 42/47] feat: improve MessageComposer ergonomics --- src/custom_types.ts | 1 + src/index.ts | 2 + src/messageComposer/CustomDataManager.ts | 43 ++++++++--- src/messageComposer/index.ts | 1 + src/messageComposer/messageComposer.ts | 2 +- .../middleware/messageComposer/customData.ts | 4 +- src/messageComposer/textComposer.ts | 40 ++++++++++- .../MessageComposer/CustomDataManager.test.ts | 45 ++++++------ .../messageComposer/customData.test.ts | 4 +- .../unit/MessageComposer/textComposer.test.ts | 71 +++++++++++++++++++ 10 files changed, 175 insertions(+), 38 deletions(-) diff --git a/src/custom_types.ts b/src/custom_types.ts index 29b178bc99..c262bbd3dc 100644 --- a/src/custom_types.ts +++ b/src/custom_types.ts @@ -9,3 +9,4 @@ export interface CustomPollData {} export interface CustomReactionData {} export interface CustomUserData {} export interface CustomThreadData {} +export interface CustomMessageComposerData {} diff --git a/src/index.ts b/src/index.ts index 242ffb3dcc..4b2119c6b3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ export * from './connection'; export * from './events'; export * from './insights'; export * from './messageComposer'; +export * from './middleware'; export * from './moderation'; export * from './permissions'; export * from './poll'; @@ -34,6 +35,7 @@ export type { CustomCommandData, CustomEventData, CustomMemberData, + CustomMessageComposerData, CustomMessageData, CustomPollOptionData, CustomPollData, diff --git a/src/messageComposer/CustomDataManager.ts b/src/messageComposer/CustomDataManager.ts index da4f1d1f7a..796c67ea7c 100644 --- a/src/messageComposer/CustomDataManager.ts +++ b/src/messageComposer/CustomDataManager.ts @@ -1,9 +1,16 @@ -import type { CustomMessageData, DraftMessage, LocalMessage } from '..'; import { StateStore } from '..'; +import type { + CustomMessageComposerData, + CustomMessageData, + DraftMessage, + LocalMessage, +} from '..'; import type { MessageComposer } from './messageComposer'; +import type { DeepPartial } from '../types.utility'; export type CustomDataManagerState = { - data: CustomMessageData; + message: CustomMessageData; + custom: CustomMessageComposerData; }; export type CustomDataManagerOptions = { @@ -12,8 +19,9 @@ export type CustomDataManagerOptions = { }; const initState = (options: CustomDataManagerOptions): CustomDataManagerState => { - if (!options) return { data: {} as CustomMessageData }; - return { data: {} as CustomMessageData }; + if (!options) + return { message: {} as CustomMessageData, custom: {} as CustomMessageComposerData }; + return { message: {} as CustomMessageData, custom: {} as CustomMessageComposerData }; }; export class CustomDataManager { @@ -25,23 +33,36 @@ export class CustomDataManager { this.state = new StateStore(initState({ composer, message })); } - get data() { - return this.state.getLatestValue().data; + get customMessageData() { + return this.state.getLatestValue().message; } - isDataEqual = ( + get customComposerData() { + return this.state.getLatestValue().custom; + } + + isMessageDataEqual = ( nextState: CustomDataManagerState, previousState?: CustomDataManagerState, - ) => JSON.stringify(nextState.data) === JSON.stringify(previousState?.data); + ) => JSON.stringify(nextState.message) === JSON.stringify(previousState?.message); initState = ({ message }: { message?: DraftMessage | LocalMessage } = {}) => { this.state.next(initState({ composer: this.composer, message })); }; - setData(data: Partial) { + setMessageData(data: DeepPartial) { + this.state.partialNext({ + message: { + ...this.state.getLatestValue().message, + ...data, + }, + }); + } + + setCustomData(data: DeepPartial) { this.state.partialNext({ - data: { - ...this.state.getLatestValue().data, + custom: { + ...this.state.getLatestValue().custom, ...data, }, }); diff --git a/src/messageComposer/index.ts b/src/messageComposer/index.ts index 75f8cbac24..10b37775f4 100644 --- a/src/messageComposer/index.ts +++ b/src/messageComposer/index.ts @@ -1,6 +1,7 @@ export * from './attachmentIdentity'; export * from './attachmentManager'; export * from './configuration'; +export * from './CustomDataManager'; export * from './fileUtils'; export * from './linkPreviewsManager'; export * from './messageComposer'; diff --git a/src/messageComposer/messageComposer.ts b/src/messageComposer/messageComposer.ts index 31b589a6ba..17992d3cdf 100644 --- a/src/messageComposer/messageComposer.ts +++ b/src/messageComposer/messageComposer.ts @@ -511,7 +511,7 @@ export class MessageComposer { this.customDataManager.state.subscribe((nextValue, previousValue) => { if ( typeof previousValue !== 'undefined' && - !this.customDataManager.isDataEqual(nextValue, previousValue) + !this.customDataManager.isMessageDataEqual(nextValue, previousValue) ) { this.logStateUpdateTimestamp(); } diff --git a/src/messageComposer/middleware/messageComposer/customData.ts b/src/messageComposer/middleware/messageComposer/customData.ts index fbec256d99..803278113d 100644 --- a/src/messageComposer/middleware/messageComposer/customData.ts +++ b/src/messageComposer/middleware/messageComposer/customData.ts @@ -11,7 +11,7 @@ export const createCustomDataCompositionMiddleware = (composer: MessageComposer) input, nextHandler, }: MiddlewareHandlerParams) => { - const data = composer.customDataManager.data; + const data = composer.customDataManager.customMessageData; if (!data) return nextHandler(input); return nextHandler({ @@ -39,7 +39,7 @@ export const createDraftCustomDataCompositionMiddleware = ( input, nextHandler, }: MiddlewareHandlerParams) => { - const data = composer.customDataManager.data; + const data = composer.customDataManager.customMessageData; if (!data) return nextHandler(input); return nextHandler({ diff --git a/src/messageComposer/textComposer.ts b/src/messageComposer/textComposer.ts index b05daa34d6..310eb1fc34 100644 --- a/src/messageComposer/textComposer.ts +++ b/src/messageComposer/textComposer.ts @@ -1,7 +1,12 @@ import { TextComposerMiddlewareExecutor } from './middleware'; import { StateStore } from '../store'; import { logChatPromiseExecution } from '../utils'; -import type { TextComposerState, TextComposerSuggestion, TextSelection } from './types'; +import type { + Suggestions, + TextComposerState, + TextComposerSuggestion, + TextSelection, +} from './types'; import type { MessageComposer } from './messageComposer'; import type { DraftMessage, LocalMessage, UserResponse } from '../types'; @@ -150,6 +155,11 @@ export class TextComposer { this.state.partialNext({ text }); }; + setSelection = (selection: TextSelection) => { + if (!this.enabled) return; + this.state.partialNext({ selection }); + }; + insertText = ({ text, selection }: { text: string; selection?: TextSelection }) => { if (!this.enabled) return; @@ -184,6 +194,34 @@ export class TextComposer { }); }; + wrapSelection = ({ + head = '', + selection, + tail = '', + }: { + head?: string; + selection?: TextSelection; + tail?: string; + }) => { + if (!this.enabled) return; + const currentSelection: TextSelection = selection ?? this.selection; + const prependedText = this.text.slice(0, currentSelection.start); + const selectedText = this.text.slice(currentSelection.start, currentSelection.end); + const appendedText = this.text.slice(currentSelection.end); + const finalSelection = { + start: prependedText.length + head.length, + end: prependedText.length + head.length + selectedText.length, + }; + this.state.partialNext({ + text: [prependedText, head, selectedText, tail, appendedText].join(''), + selection: finalSelection, + }); + }; + + setSuggestions = (suggestions: Suggestions) => { + this.state.partialNext({ suggestions }); + }; + closeSuggestions = () => { const { suggestions } = this.state.getLatestValue(); if (!suggestions) return; diff --git a/test/unit/MessageComposer/CustomDataManager.test.ts b/test/unit/MessageComposer/CustomDataManager.test.ts index 74026f5751..9a0f00fb98 100644 --- a/test/unit/MessageComposer/CustomDataManager.test.ts +++ b/test/unit/MessageComposer/CustomDataManager.test.ts @@ -33,7 +33,7 @@ describe('CustomDataManager', () => { describe('constructor', () => { it('should initialize with empty data', () => { - expect(customDataManager.data).toEqual({}); + expect(customDataManager.customMessageData).toEqual({}); }); it('should initialize with message data if provided', () => { @@ -56,19 +56,19 @@ describe('CustomDataManager', () => { message, }); - expect(managerWithMessage.data).toEqual({}); + expect(managerWithMessage.customMessageData).toEqual({}); }); }); describe('initState', () => { it('should reset state to empty data', () => { // Set some data first - customDataManager.setData({ test: 'value' }); - expect(customDataManager.data).toEqual({ test: 'value' }); + customDataManager.setMessageData({ test: 'value' }); + expect(customDataManager.customMessageData).toEqual({ test: 'value' }); // Reset state customDataManager.initState(); - expect(customDataManager.data).toEqual({}); + expect(customDataManager.customMessageData).toEqual({}); }); it('should reset state with message data if provided', () => { @@ -87,42 +87,45 @@ describe('CustomDataManager', () => { }; customDataManager.initState({ message }); - expect(customDataManager.data).toEqual({}); + expect(customDataManager.customMessageData).toEqual({}); }); }); describe('setCustomData', () => { it('should update data with new values', () => { - customDataManager.setData({ field1: 'value1' }); - expect(customDataManager.data).toEqual({ field1: 'value1' }); + customDataManager.setMessageData({ field1: 'value1' }); + expect(customDataManager.customMessageData).toEqual({ field1: 'value1' }); - customDataManager.setData({ field2: 'value2' }); - expect(customDataManager.data).toEqual({ field1: 'value1', field2: 'value2' }); + customDataManager.setMessageData({ field2: 'value2' }); + expect(customDataManager.customMessageData).toEqual({ + field1: 'value1', + field2: 'value2', + }); }); it('should override existing values', () => { - customDataManager.setData({ field1: 'value1' }); - customDataManager.setData({ field1: 'new-value' }); - expect(customDataManager.data).toEqual({ field1: 'new-value' }); + customDataManager.setMessageData({ field1: 'value1' }); + customDataManager.setMessageData({ field1: 'new-value' }); + expect(customDataManager.customMessageData).toEqual({ field1: 'new-value' }); }); }); describe('isDataEqual', () => { it('should return true for equal data', () => { - const state1 = { data: { field1: 'value1' } }; - const state2 = { data: { field1: 'value1' } }; - expect(customDataManager.isDataEqual(state1, state2)).toBe(true); + const state1 = { message: { field1: 'value1' } }; + const state2 = { message: { field1: 'value1' } }; + expect(customDataManager.isMessageDataEqual(state1, state2)).toBe(true); }); it('should return false for different data', () => { - const state1 = { data: { field1: 'value1' } }; - const state2 = { data: { field1: 'value2' } }; - expect(customDataManager.isDataEqual(state1, state2)).toBe(false); + const state1 = { message: { field1: 'value1' } }; + const state2 = { message: { field1: 'value2' } }; + expect(customDataManager.isMessageDataEqual(state1, state2)).toBe(false); }); it('should handle undefined previous state', () => { - const state1 = { data: { field1: 'value1' } }; - expect(customDataManager.isDataEqual(state1, undefined)).toBe(false); + const state1 = { message: { field1: 'value1' } }; + expect(customDataManager.isMessageDataEqual(state1, undefined)).toBe(false); }); }); }); diff --git a/test/unit/MessageComposer/middleware/messageComposer/customData.test.ts b/test/unit/MessageComposer/middleware/messageComposer/customData.test.ts index bcf4373c4c..53497141d1 100644 --- a/test/unit/MessageComposer/middleware/messageComposer/customData.test.ts +++ b/test/unit/MessageComposer/middleware/messageComposer/customData.test.ts @@ -29,7 +29,7 @@ describe('Custom Data Middleware', () => { describe('createCustomDataCompositionMiddleware', () => { it('should initialize with custom data', async () => { const data = { key: 'value' }; - composer.customDataManager.setData(data); + composer.customDataManager.setMessageData(data); const middleware = createCustomDataCompositionMiddleware(composer); const state: MessageComposerMiddlewareValueState = { message: { id: '1', type: 'regular' }, @@ -91,7 +91,7 @@ describe('Custom Data Middleware', () => { describe('createDraftCustomDataCompositionMiddleware', () => { it('should initialize with custom data', async () => { const data = { key: 'value' }; - composer.customDataManager.setData(data); + composer.customDataManager.setMessageData(data); const middleware = createDraftCustomDataCompositionMiddleware(composer); const state: MessageDraftComposerMiddlewareValueState = { draft: { diff --git a/test/unit/MessageComposer/textComposer.test.ts b/test/unit/MessageComposer/textComposer.test.ts index 237237671b..cfc971a9e7 100644 --- a/test/unit/MessageComposer/textComposer.test.ts +++ b/test/unit/MessageComposer/textComposer.test.ts @@ -491,6 +491,77 @@ describe('TextComposer', () => { }); }); + describe('wrapSelection', () => { + const message: LocalMessage = { + id: 'test-message', + type: 'regular', + text: 'Hello world', + }; + + it('should wrap selection from both sides', () => { + const selection = { start: 0, end: 5 }; + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + textComposer.wrapSelection({ head: '**', tail: '**', selection }); + expect(textComposer.text).toBe('**Hello** world'); + expect(textComposer.selection).toEqual({ start: 2, end: 7 }); + }); + + it('should wrap selection from the head side', () => { + const selection = { start: 0, end: 5 }; + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + textComposer.wrapSelection({ head: '**', selection }); + expect(textComposer.text).toBe('**Hello world'); + expect(textComposer.selection).toEqual({ start: 2, end: 7 }); + }); + + it('should wrap selection from the tail side', () => { + const selection = { start: 0, end: 5 }; + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + textComposer.wrapSelection({ tail: '**', selection }); + expect(textComposer.text).toBe('Hello** world'); + expect(textComposer.selection).toEqual({ start: 0, end: 5 }); + }); + + it('should wrap cursor', () => { + const selection = { start: 5, end: 5 }; + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + textComposer.wrapSelection({ head: '**', tail: '**', selection }); + expect(textComposer.text).toBe('Hello**** world'); + expect(textComposer.selection).toEqual({ start: 7, end: 7 }); + }); + + it('should avoid changes if text composition is disabled', () => { + const selection = { start: 5, end: 5 }; + const { + messageComposer: { textComposer }, + } = setup({ composition: message, config: { enabled: false } }); + const initialSelection = textComposer.selection; + textComposer.wrapSelection({ head: '**', tail: '**', selection }); + expect(textComposer.text).toBe(message.text); + expect(selection).not.toEqual(initialSelection); + expect(textComposer.selection).toEqual(initialSelection); + }); + + it('should use current selection if custom not provided', () => { + const initialSelection = { start: 2, end: 3 }; + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + textComposer.setSelection(initialSelection); + textComposer.wrapSelection({ head: '**', tail: '**' }); + expect(textComposer.text).toBe('He**l**lo world'); + expect(textComposer.selection).toEqual({ start: 4, end: 5 }); + }); + }); + describe('closeSuggestions', () => { const message: LocalMessage = { id: 'test-message', From 81b1cea69a231830c6d1b8a8d38805e3e176506d Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 30 Apr 2025 11:42:24 +0000 Subject: [PATCH 43/47] chore(release): 9.0.0-rc.15 [skip ci] ## [9.0.0-rc.15](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.14...v9.0.0-rc.15) (2025-04-30) ### Features * improve MessageComposer ergonomics ([2c0c639](https://github.com/GetStream/stream-chat-js/commit/2c0c639b6539fe82e6bc39d9a35a2905d33287c6)) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d06fa37465..69754fd7a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [9.0.0-rc.15](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.14...v9.0.0-rc.15) (2025-04-30) + +### Features + +* improve MessageComposer ergonomics ([2c0c639](https://github.com/GetStream/stream-chat-js/commit/2c0c639b6539fe82e6bc39d9a35a2905d33287c6)) + ## [9.0.0-rc.14](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.13...v9.0.0-rc.14) (2025-04-29) ### Features From 8b324ebf01c99f6c55219da9d210bb24689f2819 Mon Sep 17 00:00:00 2001 From: MartinCupela <32706194+MartinCupela@users.noreply.github.com> Date: Fri, 2 May 2025 12:00:07 +0200 Subject: [PATCH 44/47] fix: remove message composer bugs (#1521) --- src/messageComposer/attachmentManager.ts | 5 + src/messageComposer/linkPreviewsManager.ts | 1 + .../middleware/pollComposer/state.ts | 180 +++++++++---- .../middleware/pollComposer/types.ts | 10 +- .../middleware/textComposer/mentions.ts | 2 +- src/messageComposer/textComposer.ts | 38 ++- .../middleware/pollComposer/state.test.ts | 238 +++++++++++++----- .../unit/MessageComposer/textComposer.test.ts | 89 +++++++ 8 files changed, 442 insertions(+), 121 deletions(-) diff --git a/src/messageComposer/attachmentManager.ts b/src/messageComposer/attachmentManager.ts index 9143d4f08b..230de18bd0 100644 --- a/src/messageComposer/attachmentManager.ts +++ b/src/messageComposer/attachmentManager.ts @@ -105,9 +105,14 @@ export class AttachmentManager { this.composer.updateConfig({ attachments: { fileUploadFilter } }); } + get maxNumberOfFilesPerMessage() { + return this.config.maxNumberOfFilesPerMessage; + } + set maxNumberOfFilesPerMessage( maxNumberOfFilesPerMessage: AttachmentManagerConfig['maxNumberOfFilesPerMessage'], ) { + if (maxNumberOfFilesPerMessage === this.maxNumberOfFilesPerMessage) return; this.composer.updateConfig({ attachments: { maxNumberOfFilesPerMessage } }); } diff --git a/src/messageComposer/linkPreviewsManager.ts b/src/messageComposer/linkPreviewsManager.ts index 478ec991c4..97c0afe00d 100644 --- a/src/messageComposer/linkPreviewsManager.ts +++ b/src/messageComposer/linkPreviewsManager.ts @@ -165,6 +165,7 @@ export class LinkPreviewsManager implements ILinkPreviewsManager { } set enabled(enabled: LinkPreviewsManagerConfig['enabled']) { + if (enabled === this.enabled) return; this.composer.updateConfig({ linkPreviews: { enabled } }); } diff --git a/src/messageComposer/middleware/pollComposer/state.ts b/src/messageComposer/middleware/pollComposer/state.ts index ada61605a4..ef9ead0bc6 100644 --- a/src/messageComposer/middleware/pollComposer/state.ts +++ b/src/messageComposer/middleware/pollComposer/state.ts @@ -2,6 +2,7 @@ import type { PollComposerFieldErrors, PollComposerState, PollComposerStateMiddlewareValueState, + TargetedPollOptionTextUpdate, } from './types'; import { generateUUIDv4 } from '../../../utils'; import type { Middleware } from '../../../middleware'; @@ -10,20 +11,22 @@ export const VALID_MAX_VOTES_VALUE_REGEX = /^([2-9]|10)$/; export const MAX_POLL_OPTIONS = 100 as const; -type ValidationOutput = Partial< +export type PollStateValidationOutput = Partial< Omit, 'options'> & { options?: Record; } >; -type Validator = (params: { +export type PollStateChangeValidator = (params: { data: PollComposerState['data']; // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any; currentError?: PollComposerFieldErrors[keyof PollComposerFieldErrors]; -}) => ValidationOutput; +}) => PollStateValidationOutput; -const validators: Partial> = { +export const pollStateChangeValidators: Partial< + Record +> = { enforce_unique_vote: () => ({ max_votes_allowed: undefined }), max_votes_allowed: ({ data, value }) => { if (data.enforce_unique_vote && value) @@ -49,14 +52,18 @@ const validators: Partial> = }, }; -const changeValidators: Partial> = { +export const defaultPollFieldChangeEventValidators: Partial< + Record +> = { name: ({ currentError, value }) => value && currentError ? { name: undefined } : { name: typeof currentError === 'string' ? currentError : undefined }, }; -const blurValidators: Partial> = { +export const defaultPollFieldBlurEventValidators: Partial< + Record +> = { max_votes_allowed: ({ value }) => { if (value && !value.match(VALID_MAX_VOTES_VALUE_REGEX)) return { max_votes_allowed: 'Type a number from 2 to 10' }; @@ -68,15 +75,24 @@ const blurValidators: Partial }, }; -type ProcessorOutput = Partial; +export type PollCompositionStateProcessorOutput = Partial; -type Processor = (params: { +export type PollCompositionStateProcessor = (params: { data: PollComposerState['data']; // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any; -}) => ProcessorOutput; +}) => PollCompositionStateProcessorOutput; -const processors: Partial> = { +export const isTargetedOptionTextUpdate = ( + value: unknown, +): value is TargetedPollOptionTextUpdate => + !Array.isArray(value) && + typeof (value as TargetedPollOptionTextUpdate)?.index === 'number' && + typeof (value as TargetedPollOptionTextUpdate)?.text === 'string'; + +export const pollCompositionStateProcessors: Partial< + Record +> = { enforce_unique_vote: ({ value }) => ({ enforce_unique_vote: value, max_votes_allowed: '', @@ -118,20 +134,53 @@ const processors: Partial> = }, }; -export const createPollComposerStateMiddleware = - (): Middleware => ({ - id: 'stream-io/poll-composer-state-processing', - handleFieldChange: ({ - input, - nextHandler, - }: MiddlewareHandlerParams) => { - if (!input.state.targetFields) return nextHandler(input); - const { - state: { previousState, targetFields }, - } = input; - const finalValidators = { ...validators, ...changeValidators }; +export type PollComposerStateMiddlewareFactoryOptions = { + processors?: { + handleFieldChange?: Partial< + Record + >; + handleFieldBlur?: Partial< + Record + >; + }; + validators?: { + handleFieldChange?: Partial< + Record + >; + handleFieldBlur?: Partial< + Record + >; + }; +}; + +export const createPollComposerStateMiddleware = ({ + processors: customProcessors, + validators: customValidators, +}: PollComposerStateMiddlewareFactoryOptions = {}): Middleware => { + const universalHandler = ( + state: PollComposerStateMiddlewareValueState, + validators: Partial< + Record + >, + processors?: Partial< + Record + >, + ) => { + const { previousState, targetFields } = state; - const newData = Object.entries(targetFields).reduce( + let newData: Partial; + if (!processors && isTargetedOptionTextUpdate(targetFields.options)) { + const options = [...previousState.data.options]; + const targetOption = previousState.data.options[targetFields.options.index]; + if (targetOption) { + targetOption.text = targetFields.options.text; + options.splice(targetFields.options.index, 1, targetOption); + } + newData = { ...targetFields, options }; + } else if (!processors) { + newData = targetFields as PollComposerState['data']; + } else { + newData = Object.entries(targetFields).reduce( (acc, [key, value]) => { const processor = processors[key as keyof PollComposerState['data']]; acc = { @@ -144,19 +193,49 @@ export const createPollComposerStateMiddleware = }, {} as PollComposerState['data'], ); + } + + const newErrors = Object.keys(targetFields).reduce((acc, key) => { + const validator = validators[key as keyof PollComposerState['data']]; + if (validator) { + const error = validator({ + data: previousState.data, + value: newData[key as keyof PollComposerState['data']], + currentError: previousState.errors[key as keyof PollComposerState['data']], + }); + acc = { ...acc, ...error }; + } + return acc; + }, {} as PollComposerFieldErrors); + + return { newData, newErrors }; + }; + + return { + id: 'stream-io/poll-composer-state-processing', + handleFieldChange: ({ + input, + nextHandler, + }: MiddlewareHandlerParams) => { + if (!input.state.targetFields) return nextHandler(input); + const { + state: { previousState }, + } = input; + const finalValidators = { + ...pollStateChangeValidators, + ...defaultPollFieldChangeEventValidators, + ...customValidators?.handleFieldChange, + }; + const finalProcessors = { + ...pollCompositionStateProcessors, + ...customProcessors?.handleFieldChange, + }; - const newErrors = Object.keys(targetFields).reduce((acc, key) => { - const validator = finalValidators[key as keyof PollComposerState['data']]; - if (validator) { - const error = validator({ - data: previousState.data, - value: newData[key as keyof PollComposerState['data']], - currentError: previousState.errors[key as keyof PollComposerState['data']], - }); - acc = { ...acc, ...error }; - } - return acc; - }, {} as PollComposerFieldErrors); + const { newData, newErrors } = universalHandler( + input.state, + finalValidators, + finalProcessors, + ); return nextHandler({ ...input, @@ -174,22 +253,21 @@ export const createPollComposerStateMiddleware = input, nextHandler, }: MiddlewareHandlerParams) => { + if (!input.state.targetFields) return nextHandler(input); + const { - state: { previousState, targetFields }, + state: { previousState }, } = input; - const finalValidators = { ...validators, ...blurValidators }; - const newErrors = Object.entries(targetFields).reduce((acc, [key, value]) => { - const validator = finalValidators[key as keyof PollComposerState['data']]; - if (validator) { - const error = validator({ - data: previousState.data, - value, - currentError: previousState.errors[key as keyof PollComposerState['data']], - }); - acc = { ...acc, ...error }; - } - return acc; - }, {} as PollComposerFieldErrors); + const finalValidators = { + ...pollStateChangeValidators, + ...defaultPollFieldBlurEventValidators, + ...customValidators?.handleFieldBlur, + }; + const { newData, newErrors } = universalHandler( + input.state, + finalValidators, + customProcessors?.handleFieldBlur, + ); return nextHandler({ ...input, @@ -197,9 +275,11 @@ export const createPollComposerStateMiddleware = ...input.state, nextState: { ...previousState, + data: { ...previousState.data, ...newData }, errors: { ...previousState.errors, ...newErrors }, }, }, }); }, - }); + }; +}; diff --git a/src/messageComposer/middleware/pollComposer/types.ts b/src/messageComposer/middleware/pollComposer/types.ts index 5979734c9e..038ce9d85d 100644 --- a/src/messageComposer/middleware/pollComposer/types.ts +++ b/src/messageComposer/middleware/pollComposer/types.ts @@ -6,12 +6,14 @@ export type PollComposerOption = { text: string; }; +export type TargetedPollOptionTextUpdate = { + index: number; + text: string; +}; + export type PollComposerOptionUpdate = | PollComposerOption[] - | { - index: number; - text: string; - }; + | TargetedPollOptionTextUpdate; export type UpdateFieldsData = Partial> & { options?: PollComposerOptionUpdate; diff --git a/src/messageComposer/middleware/textComposer/mentions.ts b/src/messageComposer/middleware/textComposer/mentions.ts index 4544a45421..dcdae6fabb 100644 --- a/src/messageComposer/middleware/textComposer/mentions.ts +++ b/src/messageComposer/middleware/textComposer/mentions.ts @@ -384,7 +384,7 @@ export const createMentionsMiddleware = ( trigger: finalOptions.trigger, }, }, - stop: true, // Stop other middleware from processing '@' character + status: 'complete', // Stop other middleware from processing '@' character }); }, onSuggestionItemSelect: ({ diff --git a/src/messageComposer/textComposer.ts b/src/messageComposer/textComposer.ts index 310eb1fc34..fc6e92950a 100644 --- a/src/messageComposer/textComposer.ts +++ b/src/messageComposer/textComposer.ts @@ -78,22 +78,43 @@ export class TextComposer { } set enabled(enabled: boolean) { + if (enabled === this.enabled) return; this.composer.updateConfig({ text: { enabled } }); } - set defaultValue(defaultValue: string) { + get defaultValue() { + return this.composer.config.text.defaultValue; + } + + set defaultValue(defaultValue: string | undefined) { + if (defaultValue === this.defaultValue) return; this.composer.updateConfig({ text: { defaultValue } }); } - set maxLengthOnEdit(maxLengthOnEdit: number) { + get maxLengthOnEdit() { + return this.composer.config.text.maxLengthOnEdit; + } + + set maxLengthOnEdit(maxLengthOnEdit: number | undefined) { + if (maxLengthOnEdit === this.maxLengthOnEdit) return; this.composer.updateConfig({ text: { maxLengthOnEdit } }); } - set maxLengthOnSend(maxLengthOnSend: number) { + get maxLengthOnSend() { + return this.composer.config.text.maxLengthOnSend; + } + + set maxLengthOnSend(maxLengthOnSend: number | undefined) { + if (maxLengthOnSend === this.maxLengthOnSend) return; this.composer.updateConfig({ text: { maxLengthOnSend } }); } + get publishTypingEvents() { + return this.composer.config.text.publishTypingEvents; + } + set publishTypingEvents(publishTypingEvents: boolean) { + if (publishTypingEvents === this.publishTypingEvents) return; this.composer.updateConfig({ text: { publishTypingEvents } }); } @@ -151,22 +172,21 @@ export class TextComposer { }; setText = (text: string) => { - if (!this.enabled) return; + if (!this.enabled || text === this.text) return; this.state.partialNext({ text }); }; setSelection = (selection: TextSelection) => { - if (!this.enabled) return; + const selectionChanged = + selection.start !== this.selection.start || selection.end !== this.selection.end; + if (!this.enabled || !selectionChanged) return; this.state.partialNext({ selection }); }; insertText = ({ text, selection }: { text: string; selection?: TextSelection }) => { if (!this.enabled) return; - const finalSelection: TextSelection = selection ?? { - start: this.text.length, - end: this.text.length, - }; + const finalSelection: TextSelection = selection ?? this.selection; const { maxLengthOnEdit } = this.composer.config.text ?? {}; const currentText = this.text; const textBeforeTrim = [ diff --git a/test/unit/MessageComposer/middleware/pollComposer/state.test.ts b/test/unit/MessageComposer/middleware/pollComposer/state.test.ts index 2d4928d24c..58d8787a60 100644 --- a/test/unit/MessageComposer/middleware/pollComposer/state.test.ts +++ b/test/unit/MessageComposer/middleware/pollComposer/state.test.ts @@ -1,43 +1,45 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { createPollComposerStateMiddleware } from '../../../../../src/messageComposer/middleware/pollComposer/state'; +import { describe, expect, it, vi } from 'vitest'; +import { + createPollComposerStateMiddleware, + PollComposerStateMiddlewareFactoryOptions, +} from '../../../../../src/messageComposer/middleware/pollComposer/state'; import { VotingVisibility } from '../../../../../src/types'; -import { generateUUIDv4 } from '../../../../../src/utils'; +import { PollComposerOption, PollComposerState } from '../../../../../src'; // Mock dependencies vi.mock('../../../../../src/utils', () => ({ generateUUIDv4: vi.fn().mockReturnValue('test-uuid'), })); -describe('PollComposerStateMiddleware', () => { - let stateMiddleware: ReturnType; - let initialState: any; - - beforeEach(() => { - stateMiddleware = createPollComposerStateMiddleware(); - initialState = { - data: { - allow_answers: false, - allow_user_suggested_options: false, - description: '', - enforce_unique_vote: true, - id: 'test-id', - max_votes_allowed: '', - name: '', - options: [{ id: 'option-id', text: '' }], - user_id: 'user-id', - voting_visibility: VotingVisibility.public, - }, - errors: {}, - }; - }); +const getInitialState = (): PollComposerState => ({ + data: { + allow_answers: false, + allow_user_suggested_options: false, + description: '', + enforce_unique_vote: true, + id: 'test-id', + max_votes_allowed: '', + name: '', + options: [{ id: 'option-id', text: '' }], + user_id: 'user-id', + voting_visibility: VotingVisibility.public, + }, + errors: {}, +}); +const setup = (options?: PollComposerStateMiddlewareFactoryOptions) => { + return createPollComposerStateMiddleware(options); +}; + +describe('PollComposerStateMiddleware', () => { describe('handleFieldChange', () => { it('should update name field', async () => { + const stateMiddleware = setup(); const result = await stateMiddleware.handleFieldChange({ input: { state: { - nextState: { ...initialState }, - previousState: { ...initialState }, + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, targetFields: { name: 'Test Poll' }, }, }, @@ -49,11 +51,12 @@ describe('PollComposerStateMiddleware', () => { }); it('should validate max_votes_allowed field with invalid value', async () => { + const stateMiddleware = setup(); const result = await stateMiddleware.handleFieldChange({ input: { state: { - nextState: { ...initialState }, - previousState: { ...initialState }, + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, targetFields: { max_votes_allowed: '1' }, // Invalid value (less than 2) }, }, @@ -66,16 +69,17 @@ describe('PollComposerStateMiddleware', () => { }); it('should not validate max_votes_allowed field with valid value if enforce_unique_vote is true', async () => { + const stateMiddleware = setup(); const result = await stateMiddleware.handleFieldChange({ input: { state: { nextState: { - ...initialState, - data: { ...initialState.data, enforce_unique_vote: true }, + ...getInitialState(), + data: { ...getInitialState().data, enforce_unique_vote: true }, }, previousState: { - ...initialState, - data: { ...initialState.data, enforce_unique_vote: true }, + ...getInitialState(), + data: { ...getInitialState().data, enforce_unique_vote: true }, }, targetFields: { max_votes_allowed: '5' }, // Valid value (between 2 and 10) }, @@ -89,16 +93,17 @@ describe('PollComposerStateMiddleware', () => { }); it('should validate max_votes_allowed field with valid value if enforce_unique_vote is false', async () => { + const stateMiddleware = setup(); const result = await stateMiddleware.handleFieldChange({ input: { state: { nextState: { - ...initialState, - data: { ...initialState.data, enforce_unique_vote: false }, + ...getInitialState(), + data: { ...getInitialState().data, enforce_unique_vote: false }, }, previousState: { - ...initialState, - data: { ...initialState.data, enforce_unique_vote: false }, + ...getInitialState(), + data: { ...getInitialState().data, enforce_unique_vote: false }, }, targetFields: { max_votes_allowed: '5' }, // Valid value (between 2 and 10) }, @@ -112,11 +117,12 @@ describe('PollComposerStateMiddleware', () => { }); it('should handle options field changes with single option update', async () => { + const stateMiddleware = setup(); const result = await stateMiddleware.handleFieldChange({ input: { state: { - nextState: { ...initialState }, - previousState: { ...initialState }, + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, targetFields: { options: [ { @@ -136,11 +142,12 @@ describe('PollComposerStateMiddleware', () => { }); it('should handle options field changes with array update', async () => { + const stateMiddleware = setup(); const result = await stateMiddleware.handleFieldChange({ input: { state: { - nextState: { ...initialState }, - previousState: { ...initialState }, + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, targetFields: { options: [ { id: 'option-1', text: 'Option 1' }, @@ -159,11 +166,12 @@ describe('PollComposerStateMiddleware', () => { }); it('should handle enforce_unique_vote field changes', async () => { + const stateMiddleware = setup(); const result = await stateMiddleware.handleFieldChange({ input: { state: { - nextState: { ...initialState }, - previousState: { ...initialState }, + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, targetFields: { enforce_unique_vote: false }, }, }, @@ -176,11 +184,12 @@ describe('PollComposerStateMiddleware', () => { }); it('should add a new empty option when the last option is filled', async () => { + const stateMiddleware = setup(); const result = await stateMiddleware.handleFieldChange({ input: { state: { - nextState: { ...initialState }, - previousState: { ...initialState }, + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, targetFields: { options: { index: 0, @@ -199,7 +208,9 @@ describe('PollComposerStateMiddleware', () => { }); it('should remove an option when it is empty and there are more options after it', async () => { + const stateMiddleware = setup(); // Set up initial state with two options + const initialState = getInitialState(); initialState.data.options = [ { id: 'option-1', text: 'Option 1' }, { id: 'option-2', text: '' }, @@ -208,8 +219,8 @@ describe('PollComposerStateMiddleware', () => { const result = await stateMiddleware.handleFieldChange({ input: { state: { - nextState: { ...initialState }, - previousState: { ...initialState }, + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, targetFields: { options: { index: 0, @@ -225,15 +236,70 @@ describe('PollComposerStateMiddleware', () => { expect(result.state.nextState.data.options[0].text).toBe(''); expect(result.status).toBeUndefined; }); + + it('should use custom target field data processors ', async () => { + const injectedOptions: PollComposerOption[] = [{ id: 'x', text: 'y' }]; + const stateMiddleware = setup({ + processors: { + handleFieldChange: { options: () => ({ options: injectedOptions }) }, + }, + }); + + const result = await stateMiddleware.handleFieldChange({ + input: { + state: { + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, + targetFields: { + options: { + index: 0, + text: 'X', + }, + }, + }, + }, + nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), + }); + + expect(result.state.nextState.data.options).toEqual(injectedOptions); + expect(result.status).toBeUndefined; + }); + it('should use custom target field data validators', async () => { + const stateMiddleware = setup({ + validators: { + handleFieldChange: { options: () => ({ options: { x: 'failed option X' } }) }, + }, + }); + + const result = await stateMiddleware.handleFieldChange({ + input: { + state: { + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, + targetFields: { + options: { + index: 0, + text: 'X', + }, + }, + }, + }, + nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), + }); + + expect(result.state.nextState.errors.options).toEqual({ x: 'failed option X' }); + expect(result.status).toBeUndefined; + }); }); describe('handleFieldBlur', () => { it('should validate name field on blur', async () => { + const stateMiddleware = setup(); const result = await stateMiddleware.handleFieldBlur({ input: { state: { - nextState: { ...initialState }, - previousState: { ...initialState }, + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, targetFields: { name: '' }, }, }, @@ -245,11 +311,12 @@ describe('PollComposerStateMiddleware', () => { }); it('should validate max_votes_allowed field on blur', async () => { + const stateMiddleware = setup(); const result = await stateMiddleware.handleFieldBlur({ input: { state: { - nextState: { ...initialState }, - previousState: { ...initialState }, + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, targetFields: { max_votes_allowed: '1' }, }, }, @@ -262,11 +329,12 @@ describe('PollComposerStateMiddleware', () => { describe('options validation', () => { it('should validate empty options on blur', async () => { + const stateMiddleware = setup(); const result = await stateMiddleware.handleFieldBlur({ input: { state: { - nextState: { ...initialState }, - previousState: { ...initialState }, + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, targetFields: { options: [{ id: 'option-id', text: '' }] }, }, }, @@ -277,11 +345,12 @@ describe('PollComposerStateMiddleware', () => { }); it('should validate duplicate options on blur', async () => { + const stateMiddleware = setup(); const result = await stateMiddleware.handleFieldBlur({ input: { state: { - nextState: { ...initialState }, - previousState: { ...initialState }, + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, targetFields: { options: [ { id: 'option-1', text: 'Same Text' }, @@ -299,11 +368,12 @@ describe('PollComposerStateMiddleware', () => { }); it('should pass validation for valid options', async () => { + const stateMiddleware = setup(); const result = await stateMiddleware.handleFieldBlur({ input: { state: { - nextState: { ...initialState }, - previousState: { ...initialState }, + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, targetFields: { options: [ { id: 'option-1', text: 'Option 1' }, @@ -318,5 +388,59 @@ describe('PollComposerStateMiddleware', () => { expect(result.state.nextState.errors.options).toBeUndefined(); }); }); + + it('should use custom target field data processors', async () => { + const injectedOptions: PollComposerOption[] = [{ id: 'x', text: 'y' }]; + const stateMiddleware = setup({ + processors: { + handleFieldBlur: { options: () => ({ options: injectedOptions }) }, + }, + }); + + const result = await stateMiddleware.handleFieldBlur({ + input: { + state: { + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, + targetFields: { + options: { + index: 0, + text: 'X', + }, + }, + }, + }, + nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), + }); + + expect(result.state.nextState.data.options).toEqual(injectedOptions); + expect(result.status).toBeUndefined; + }); + it('should use custom target field data validators', async () => { + const stateMiddleware = setup({ + validators: { + handleFieldBlur: { options: () => ({ options: { x: 'failed option X' } }) }, + }, + }); + + const result = await stateMiddleware.handleFieldBlur({ + input: { + state: { + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, + targetFields: { + options: { + index: 0, + text: 'X', + }, + }, + }, + }, + nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), + }); + + expect(result.state.nextState.errors.options).toEqual({ x: 'failed option X' }); + expect(result.status).toBeUndefined; + }); }); }); diff --git a/test/unit/MessageComposer/textComposer.test.ts b/test/unit/MessageComposer/textComposer.test.ts index cfc971a9e7..bf63b740d2 100644 --- a/test/unit/MessageComposer/textComposer.test.ts +++ b/test/unit/MessageComposer/textComposer.test.ts @@ -8,6 +8,7 @@ import { textIsEmpty } from '../../../src/messageComposer/textComposer'; import { DraftResponse, LocalMessage } from '../../../src/types'; import { logChatPromiseExecution } from '../../../src/utils'; import { TextComposerConfig } from '../../../src/messageComposer/configuration'; +import { TextComposerState } from '../../../src'; const textComposerMiddlewareExecuteOutput = { state: { @@ -393,6 +394,94 @@ describe('TextComposer', () => { textComposer.setText('New text'); expect(textComposer.text).toBe(message.text); }); + it('should not update the text when setting the same value', () => { + const message: LocalMessage = { + id: 'test-message', + type: 'regular', + text: 'Hello world', + }; + const { + messageComposer: { textComposer }, + } = setup({ composition: message, config: { enabled: false } }); + const subscriber = vi.fn(); + const originalText = textComposer.text; + textComposer.state.subscribeWithSelector(({ text }) => ({ text }), subscriber); + expect(subscriber).toHaveBeenCalledWith({ text: originalText }, undefined); + textComposer.setText(originalText); + expect(textComposer.text).toBe(originalText); + expect(subscriber).toHaveBeenCalledTimes(1); + }); + }); + + describe('setSelection', () => { + const message: LocalMessage = { + id: 'test-message', + type: 'regular', + text: 'Hello world', + }; + it('should update the selection', () => { + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + const subscriber = vi.fn(); + textComposer.state.subscribeWithSelector( + ({ selection }) => ({ selection }), + subscriber, + ); + expect(subscriber).toHaveBeenCalledWith( + { selection: { end: message.text!.length, start: message.text!.length } }, + undefined, + ); + expect(textComposer.selection).toEqual({ + end: message.text!.length, + start: message.text!.length, + }); + textComposer.setSelection({ end: 2, start: 2 }); + expect(textComposer.selection).toEqual({ end: 2, start: 2 }); + expect(subscriber).toHaveBeenCalledWith( + { selection: { end: 2, start: 2 } }, + { selection: { end: message.text!.length, start: message.text!.length } }, + ); + expect(subscriber).toHaveBeenCalledTimes(2); + }); + + it('should not update the selection with the same value', () => { + const { + messageComposer: { textComposer }, + } = setup({ composition: message }); + const originalSelection = textComposer.selection; + const subscriber = vi.fn(); + textComposer.state.subscribeWithSelector( + ({ selection }) => ({ ...selection }), + subscriber, + ); + expect(subscriber).toHaveBeenCalledWith(originalSelection, undefined); + expect(textComposer.selection).toEqual(originalSelection); + textComposer.setSelection(originalSelection); + expect(textComposer.selection).toEqual(originalSelection); + expect(subscriber).toHaveBeenCalledTimes(1); + }); + + it('should not update the selection when text composer disabled', () => { + const { + messageComposer: { textComposer }, + } = setup({ composition: message, config: { enabled: false } }); + const originalSelection = textComposer.selection; + const subscriber = vi.fn(); + textComposer.state.subscribeWithSelector( + ({ selection }) => ({ selection }), + subscriber, + ); + expect(subscriber).toHaveBeenCalledWith( + { selection: { end: message.text!.length, start: message.text!.length } }, + undefined, + ); + + expect(textComposer.selection).toEqual(originalSelection); + textComposer.setSelection({ end: 2, start: 2 }); + expect(textComposer.selection).toEqual(originalSelection); + expect(subscriber).toHaveBeenCalledTimes(1); + }); }); describe('insertText', () => { From 3aab32553f47dddcc77f100cc5825195f8820372 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 2 May 2025 10:00:50 +0000 Subject: [PATCH 45/47] chore(release): 9.0.0-rc.16 [skip ci] ## [9.0.0-rc.16](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.15...v9.0.0-rc.16) (2025-05-02) ### Bug Fixes * remove message composer bugs ([#1521](https://github.com/GetStream/stream-chat-js/issues/1521)) ([8b324eb](https://github.com/GetStream/stream-chat-js/commit/8b324ebf01c99f6c55219da9d210bb24689f2819)) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69754fd7a1..36bfaa34f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [9.0.0-rc.16](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.15...v9.0.0-rc.16) (2025-05-02) + +### Bug Fixes + +* remove message composer bugs ([#1521](https://github.com/GetStream/stream-chat-js/issues/1521)) ([8b324eb](https://github.com/GetStream/stream-chat-js/commit/8b324ebf01c99f6c55219da9d210bb24689f2819)) + ## [9.0.0-rc.15](https://github.com/GetStream/stream-chat-js/compare/v9.0.0-rc.14...v9.0.0-rc.15) (2025-04-30) ### Features From 9d8992dbd0735fb78ef663f25bc58c97a526fb18 Mon Sep 17 00:00:00 2001 From: MartinCupela <32706194+MartinCupela@users.noreply.github.com> Date: Tue, 6 May 2025 12:35:55 +0200 Subject: [PATCH 46/47] feat: middleware handler API improvement (#1523) --- src/messageComposer/configuration/types.ts | 2 +- src/messageComposer/index.ts | 1 + src/messageComposer/linkPreviewsManager.ts | 2 +- src/messageComposer/messageComposer.ts | 17 +- .../MessageComposerMiddlewareExecutor.ts | 12 +- .../middleware/messageComposer/attachments.ts | 107 ++-- .../middleware/messageComposer/cleanData.ts | 50 +- .../messageComposer/compositionValidation.ts | 88 +-- .../middleware/messageComposer/customData.ts | 64 ++- .../messageComposer/linkPreviews.ts | 117 ++-- .../messageComposer/messageComposerState.ts | 80 +-- .../messageComposer/textComposer.ts | 120 ++-- .../middleware/messageComposer/types.ts | 16 +- .../PollComposerMiddlewareExecutor.ts | 12 +- .../middleware/pollComposer/composition.ts | 26 +- .../middleware/pollComposer/state.ts | 117 ++-- .../middleware/pollComposer/types.ts | 8 +- .../TextComposerMiddlewareExecutor.ts | 60 +- .../middleware/textComposer/commands.ts | 119 ++-- .../middleware/textComposer/mentions.ts | 102 ++-- .../textComposer/textMiddlewareUtils.ts | 2 +- .../middleware/textComposer/types.ts | 52 +- .../middleware/textComposer/validation.ts | 43 +- src/messageComposer/pollComposer.ts | 26 +- src/messageComposer/textComposer.ts | 29 +- src/messageComposer/types.custom.ts | 1 + src/messageComposer/types.ts | 23 +- src/middleware.ts | 102 ++-- .../MessageComposer/messageComposer.test.ts | 20 +- .../messageComposer/attachments.test.ts | 348 +++++------ .../compositionValidation.test.ts | 385 ++++++------- .../messageComposer/customData.test.ts | 52 +- .../messageComposer/linkPreviews.test.ts | 538 +++++++++--------- .../messageComposerState.test.ts | 375 ++++++------ .../messageComposer/textComposer.test.ts | 487 ++++++++-------- .../pollComposer/composition.test.ts | 68 ++- .../middleware/pollComposer/state.test.ts | 418 +++++++------- .../TextComposerMiddlewareExecutor.test.ts | 177 +++--- .../unit/MessageComposer/pollComposer.test.ts | 15 +- test/unit/middleware.test.ts | 407 +++++++------ 40 files changed, 2430 insertions(+), 2258 deletions(-) create mode 100644 src/messageComposer/types.custom.ts diff --git a/src/messageComposer/configuration/types.ts b/src/messageComposer/configuration/types.ts index c7eec87c28..7f524f952f 100644 --- a/src/messageComposer/configuration/types.ts +++ b/src/messageComposer/configuration/types.ts @@ -34,7 +34,7 @@ export type AttachmentManagerConfig = { maxNumberOfFilesPerMessage: number; /** * Array of one or more file types, or unique file type specifiers (https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/accept#unique_file_type_specifiers), - * describing which file types to allow to select when uploading files. + * describing which file types are allowed to be selected when uploading files. */ acceptedFiles: string[]; // todo: refactor this. We want a pipeline where it would be possible to customize the preparation, upload, and post-upload steps. diff --git a/src/messageComposer/index.ts b/src/messageComposer/index.ts index 10b37775f4..8e4eb92ce1 100644 --- a/src/messageComposer/index.ts +++ b/src/messageComposer/index.ts @@ -9,3 +9,4 @@ export * from './middleware'; export * from './pollComposer'; export * from './textComposer'; export * from './types'; +export type { CustomTextComposerSuggestion } from './types.custom'; diff --git a/src/messageComposer/linkPreviewsManager.ts b/src/messageComposer/linkPreviewsManager.ts index 97c0afe00d..c11f769e73 100644 --- a/src/messageComposer/linkPreviewsManager.ts +++ b/src/messageComposer/linkPreviewsManager.ts @@ -18,7 +18,7 @@ export interface ILinkPreviewsManager { } export enum LinkPreviewStatus { - /** Link preview has been dismissed using MessageInputContextValue.dismissLinkPreview **/ + /** Link preview has been dismissed using **/ DISMISSED = 'dismissed', /** Link preview could not be loaded, the enrichment request has failed. **/ FAILED = 'failed', diff --git a/src/messageComposer/messageComposer.ts b/src/messageComposer/messageComposer.ts index 17992d3cdf..1fc9a07018 100644 --- a/src/messageComposer/messageComposer.ts +++ b/src/messageComposer/messageComposer.ts @@ -563,8 +563,9 @@ export class MessageComposer { compose = async (): Promise => { const created_at = this.editedMessage?.created_at ?? new Date(); const text = ''; - const result = await this.compositionMiddlewareExecutor.execute('compose', { - state: { + const result = await this.compositionMiddlewareExecutor.execute({ + eventName: 'compose', + initialValue: { message: { id: this.id, parent_id: this.threadId ?? undefined, @@ -594,14 +595,12 @@ export class MessageComposer { }; composeDraft = async () => { - const { state, status } = await this.draftCompositionMiddlewareExecutor.execute( - 'compose', - { - state: { - draft: { id: this.id, parent_id: this.threadId ?? undefined, text: '' }, - }, + const { state, status } = await this.draftCompositionMiddlewareExecutor.execute({ + eventName: 'compose', + initialValue: { + draft: { id: this.id, parent_id: this.threadId ?? undefined, text: '' }, }, - ); + }); if (status === 'discard') return; return state; }; diff --git a/src/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.ts b/src/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.ts index 1ea77785ac..476afefdc8 100644 --- a/src/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.ts +++ b/src/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.ts @@ -22,7 +22,7 @@ import { import { createCompositionDataCleanupMiddleware } from './cleanData'; import type { MessageComposerMiddlewareExecutorOptions, - MessageComposerMiddlewareValueState, + MessageComposerMiddlewareState, MessageDraftComposerMiddlewareExecutorOptions, MessageDraftComposerMiddlewareValueState, } from './types'; @@ -31,7 +31,10 @@ import { createDraftCustomDataCompositionMiddleware, } from './customData'; -export class MessageComposerMiddlewareExecutor extends MiddlewareExecutor { +export class MessageComposerMiddlewareExecutor extends MiddlewareExecutor< + MessageComposerMiddlewareState, + 'compose' +> { constructor({ composer }: MessageComposerMiddlewareExecutorOptions) { super(); // todo: document how to add custom data to a composed message using middleware @@ -48,7 +51,10 @@ export class MessageComposerMiddlewareExecutor extends MiddlewareExecutor { +export class MessageDraftComposerMiddlewareExecutor extends MiddlewareExecutor< + MessageDraftComposerMiddlewareValueState, + 'compose' +> { constructor({ composer }: MessageDraftComposerMiddlewareExecutorOptions) { super(); // todo: document how to add custom data to a composed message using middleware diff --git a/src/messageComposer/middleware/messageComposer/attachments.ts b/src/messageComposer/middleware/messageComposer/attachments.ts index 9cdcab3092..d1f74fc210 100644 --- a/src/messageComposer/middleware/messageComposer/attachments.ts +++ b/src/messageComposer/middleware/messageComposer/attachments.ts @@ -3,8 +3,10 @@ import type { Attachment } from '../../../types'; import type { MessageComposer } from '../../messageComposer'; import type { LocalAttachment } from '../../types'; import type { - MessageComposerMiddlewareValueState, + MessageComposerMiddlewareState, + MessageCompositionMiddleware, MessageDraftComposerMiddlewareValueState, + MessageDraftCompositionMiddleware, } from './types'; const localAttachmentToAttachment = (localAttachment: LocalAttachment) => { @@ -13,77 +15,80 @@ const localAttachmentToAttachment = (localAttachment: LocalAttachment) => { return attachment as Attachment; }; -export const createAttachmentsCompositionMiddleware = (composer: MessageComposer) => ({ +export const createAttachmentsCompositionMiddleware = ( + composer: MessageComposer, +): MessageCompositionMiddleware => ({ id: 'stream-io/message-composer-middleware/attachments', - compose: ({ - input, - nextHandler, - }: MiddlewareHandlerParams) => { - const { attachmentManager } = composer; - if (!attachmentManager) return nextHandler(input); + handlers: { + compose: ({ + state, + next, + discard, + forward, + }: MiddlewareHandlerParams) => { + const { attachmentManager } = composer; + if (!attachmentManager) return forward(); - if (attachmentManager.uploadsInProgressCount > 0) { - composer.client.notifications.addWarning({ - message: 'Wait until all attachments have uploaded', - origin: { - emitter: 'MessageComposer', - context: { composer }, - }, - }); - return nextHandler({ ...input, status: 'discard' }); - } + if (attachmentManager.uploadsInProgressCount > 0) { + composer.client.notifications.addWarning({ + message: 'Wait until all attachments have uploaded', + origin: { + emitter: 'MessageComposer', + context: { composer }, + }, + }); + return discard(); + } - const attachments = (input.state.message.attachments ?? []).concat( - attachmentManager.successfulUploads.map(localAttachmentToAttachment), - ); + const attachments = (state.message.attachments ?? []).concat( + attachmentManager.successfulUploads.map(localAttachmentToAttachment), + ); - // prevent introducing attachments array into the payload sent to the server - if (!attachments.length) return nextHandler(input); + // prevent introducing attachments array into the payload sent to the server + if (!attachments.length) return forward(); - return nextHandler({ - ...input, - state: { - ...input.state, + return next({ + ...state, localMessage: { - ...input.state.localMessage, + ...state.localMessage, attachments, }, message: { - ...input.state.message, + ...state.message, attachments, }, - }, - }); + }); + }, }, }); export const createDraftAttachmentsCompositionMiddleware = ( composer: MessageComposer, -) => ({ +): MessageDraftCompositionMiddleware => ({ id: 'stream-io/message-composer-middleware/draft-attachments', - compose: ({ - input, - nextHandler, - }: MiddlewareHandlerParams) => { - const { attachmentManager } = composer; - if (!attachmentManager) return nextHandler(input); + handlers: { + compose: ({ + state, + next, + forward, + }: MiddlewareHandlerParams) => { + const { attachmentManager } = composer; + if (!attachmentManager) return forward(); - const successfulUploads = attachmentManager.successfulUploads; - const attachments = successfulUploads.length - ? (input.state.draft.attachments ?? []).concat( - successfulUploads.map(localAttachmentToAttachment), - ) - : undefined; + const successfulUploads = attachmentManager.successfulUploads; + const attachments = successfulUploads.length + ? (state.draft.attachments ?? []).concat( + successfulUploads.map(localAttachmentToAttachment), + ) + : undefined; - return nextHandler({ - ...input, - state: { - ...input.state, + return next({ + ...state, draft: { - ...input.state.draft, + ...state.draft, attachments, }, - }, - }); + }); + }, }, }); diff --git a/src/messageComposer/middleware/messageComposer/cleanData.ts b/src/messageComposer/middleware/messageComposer/cleanData.ts index b12fafb302..2bbf5fa830 100644 --- a/src/messageComposer/middleware/messageComposer/cleanData.ts +++ b/src/messageComposer/middleware/messageComposer/cleanData.ts @@ -1,42 +1,46 @@ import type { MiddlewareHandlerParams } from '../../../middleware'; import { formatMessage, toUpdatedMessagePayload } from '../../../utils'; import type { MessageComposer } from '../../messageComposer'; -import type { MessageComposerMiddlewareValueState } from './types'; +import type { + MessageComposerMiddlewareState, + MessageCompositionMiddleware, +} from './types'; -export const createCompositionDataCleanupMiddleware = (composer: MessageComposer) => ({ +export const createCompositionDataCleanupMiddleware = ( + composer: MessageComposer, +): MessageCompositionMiddleware => ({ id: 'stream-io/message-composer-middleware/data-cleanup', - compose: ({ - input, - nextHandler, - }: MiddlewareHandlerParams) => { - const common = { - type: composer.editedMessage?.type ?? 'regular', - }; + handlers: { + compose: ({ + state, + next, + }: MiddlewareHandlerParams) => { + const common = { + type: composer.editedMessage?.type ?? 'regular', + }; - const editedMessagePayloadToBeSent = composer.editedMessage - ? toUpdatedMessagePayload(composer.editedMessage) - : undefined; + const editedMessagePayloadToBeSent = composer.editedMessage + ? toUpdatedMessagePayload(composer.editedMessage) + : undefined; - return nextHandler({ - ...input, - state: { - ...input.state, + return next({ + ...state, localMessage: formatMessage({ ...composer.editedMessage, - ...input.state.localMessage, + ...state.localMessage, ...common, user: composer.client.user, }), message: { ...editedMessagePayloadToBeSent, - ...input.state.message, + ...state.message, ...common, }, sendOptions: - composer.editedMessage && input.state.sendOptions?.skip_enrich_url - ? { skip_enrich_url: input.state.sendOptions?.skip_enrich_url } - : input.state.sendOptions, - }, - }); + composer.editedMessage && state.sendOptions?.skip_enrich_url + ? { skip_enrich_url: state.sendOptions?.skip_enrich_url } + : state.sendOptions, + }); + }, }, }); diff --git a/src/messageComposer/middleware/messageComposer/compositionValidation.ts b/src/messageComposer/middleware/messageComposer/compositionValidation.ts index a3e362f6c7..4a98925ec8 100644 --- a/src/messageComposer/middleware/messageComposer/compositionValidation.ts +++ b/src/messageComposer/middleware/messageComposer/compositionValidation.ts @@ -1,55 +1,65 @@ import { textIsEmpty } from '../../textComposer'; import type { - MessageComposerMiddlewareValueState, + MessageComposerMiddlewareState, + MessageCompositionMiddleware, MessageDraftComposerMiddlewareValueState, + MessageDraftCompositionMiddleware, } from './types'; import type { MessageComposer } from '../../messageComposer'; import type { MiddlewareHandlerParams } from '../../../middleware'; -export const createCompositionValidationMiddleware = (composer: MessageComposer) => ({ +export const createCompositionValidationMiddleware = ( + composer: MessageComposer, +): MessageCompositionMiddleware => ({ id: 'stream-io/message-composer-middleware/data-validation', - compose: async ({ - input, - nextHandler, - }: MiddlewareHandlerParams) => { - const { maxLengthOnSend } = composer.config.text ?? {}; - const inputText = input.state.message.text ?? ''; - const isEmptyMessage = - textIsEmpty(inputText) && - !input.state.message.attachments?.length && - !input.state.message.poll_id; - - const hasExceededMaxLength = - typeof maxLengthOnSend === 'number' && inputText.length > maxLengthOnSend; - - if (isEmptyMessage || !composer.lastChangeOriginIsLocal || hasExceededMaxLength) { - return await nextHandler({ ...input, status: 'discard' }); - } - - return await nextHandler(input); + handlers: { + compose: async ({ + state, + discard, + forward, + }: MiddlewareHandlerParams) => { + const { maxLengthOnSend } = composer.config.text ?? {}; + const inputText = state.message.text ?? ''; + const isEmptyMessage = + textIsEmpty(inputText) && + !state.message.attachments?.length && + !state.message.poll_id; + + const hasExceededMaxLength = + typeof maxLengthOnSend === 'number' && inputText.length > maxLengthOnSend; + + if (isEmptyMessage || !composer.lastChangeOriginIsLocal || hasExceededMaxLength) { + return await discard(); + } + + return await forward(); + }, }, }); export const createDraftCompositionValidationMiddleware = ( composer: MessageComposer, -) => ({ +): MessageDraftCompositionMiddleware => ({ id: 'stream-io/message-composer-middleware/draft-data-validation', - compose: async ({ - input, - nextHandler, - }: MiddlewareHandlerParams) => { - const hasData = - !textIsEmpty(input.state.draft.text ?? '') || - input.state.draft.attachments?.length || - input.state.draft.poll_id || - input.state.draft.quoted_message_id; - - const shouldCreateDraft = composer.lastChangeOriginIsLocal && hasData; - - if (!shouldCreateDraft) { - return await nextHandler({ ...input, status: 'discard' }); - } - - return await nextHandler(input); + handlers: { + compose: async ({ + state, + discard, + forward, + }: MiddlewareHandlerParams) => { + const hasData = + !textIsEmpty(state.draft.text ?? '') || + state.draft.attachments?.length || + state.draft.poll_id || + state.draft.quoted_message_id; + + const shouldCreateDraft = composer.lastChangeOriginIsLocal && hasData; + + if (!shouldCreateDraft) { + return await discard(); + } + + return await forward(); + }, }, }); diff --git a/src/messageComposer/middleware/messageComposer/customData.ts b/src/messageComposer/middleware/messageComposer/customData.ts index 803278113d..c6693b8788 100644 --- a/src/messageComposer/middleware/messageComposer/customData.ts +++ b/src/messageComposer/middleware/messageComposer/customData.ts @@ -1,56 +1,60 @@ import type { MiddlewareHandlerParams } from '../../../middleware'; import type { MessageComposer } from '../../messageComposer'; import type { - MessageComposerMiddlewareValueState, + MessageComposerMiddlewareState, + MessageCompositionMiddleware, MessageDraftComposerMiddlewareValueState, + MessageDraftCompositionMiddleware, } from './types'; -export const createCustomDataCompositionMiddleware = (composer: MessageComposer) => ({ +export const createCustomDataCompositionMiddleware = ( + composer: MessageComposer, +): MessageCompositionMiddleware => ({ id: 'stream-io/message-composer-middleware/custom-data', - compose: ({ - input, - nextHandler, - }: MiddlewareHandlerParams) => { - const data = composer.customDataManager.customMessageData; - if (!data) return nextHandler(input); + handlers: { + compose: ({ + state, + next, + forward, + }: MiddlewareHandlerParams) => { + const data = composer.customDataManager.customMessageData; + if (!data) return forward(); - return nextHandler({ - ...input, - state: { - ...input.state, + return next({ + ...state, localMessage: { - ...input.state.localMessage, + ...state.localMessage, ...data, }, message: { - ...input.state.message, + ...state.message, ...data, }, - }, - }); + }); + }, }, }); export const createDraftCustomDataCompositionMiddleware = ( composer: MessageComposer, -) => ({ +): MessageDraftCompositionMiddleware => ({ id: 'stream-io/message-composer-middleware/draft-custom-data', - compose: ({ - input, - nextHandler, - }: MiddlewareHandlerParams) => { - const data = composer.customDataManager.customMessageData; - if (!data) return nextHandler(input); + handlers: { + compose: ({ + state, + next, + forward, + }: MiddlewareHandlerParams) => { + const data = composer.customDataManager.customMessageData; + if (!data) return forward(); - return nextHandler({ - ...input, - state: { - ...input.state, + return next({ + ...state, draft: { - ...input.state.draft, + ...state.draft, ...data, }, - }, - }); + }); + }, }, }); diff --git a/src/messageComposer/middleware/messageComposer/linkPreviews.ts b/src/messageComposer/middleware/messageComposer/linkPreviews.ts index 861b8e8bc0..b2385d0dcf 100644 --- a/src/messageComposer/middleware/messageComposer/linkPreviews.ts +++ b/src/messageComposer/middleware/messageComposer/linkPreviews.ts @@ -3,88 +3,93 @@ import type { MiddlewareHandlerParams } from '../../../middleware'; import type { Attachment } from '../../../types'; import type { MessageComposer } from '../../messageComposer'; import type { - MessageComposerMiddlewareValueState, + MessageComposerMiddlewareState, + MessageCompositionMiddleware, MessageDraftComposerMiddlewareValueState, + MessageDraftCompositionMiddleware, } from './types'; -export const createLinkPreviewsCompositionMiddleware = (composer: MessageComposer) => ({ +export const createLinkPreviewsCompositionMiddleware = ( + composer: MessageComposer, +): MessageCompositionMiddleware => ({ id: 'stream-io/message-composer-middleware/link-previews', - compose: ({ - input, - nextHandler, - }: MiddlewareHandlerParams) => { - const { linkPreviewsManager } = composer; - if (!linkPreviewsManager) return nextHandler(input); + handlers: { + compose: ({ + state, + next, + forward, + }: MiddlewareHandlerParams) => { + const { linkPreviewsManager } = composer; + if (!linkPreviewsManager) return forward(); - linkPreviewsManager.cancelURLEnrichment(); - const someLinkPreviewsLoading = linkPreviewsManager.loadingPreviews.length > 0; - const someLinkPreviewsDismissed = linkPreviewsManager.dismissedPreviews.length > 0; - const linkPreviews = - linkPreviewsManager.loadingPreviews.length > 0 - ? [] - : linkPreviewsManager.loadedPreviews.map((preview) => - LinkPreviewsManager.getPreviewData(preview), - ); + linkPreviewsManager.cancelURLEnrichment(); + const someLinkPreviewsLoading = linkPreviewsManager.loadingPreviews.length > 0; + const someLinkPreviewsDismissed = linkPreviewsManager.dismissedPreviews.length > 0; + const linkPreviews = + linkPreviewsManager.loadingPreviews.length > 0 + ? [] + : linkPreviewsManager.loadedPreviews.map((preview) => + LinkPreviewsManager.getPreviewData(preview), + ); - const attachments: Attachment[] = (input.state.message.attachments ?? []).concat( - linkPreviews, - ); + const attachments: Attachment[] = (state.message.attachments ?? []).concat( + linkPreviews, + ); - // prevent introducing attachments array into the payload sent to the server - if (!attachments.length) return nextHandler(input); + // prevent introducing attachments array into the payload sent to the server + if (!attachments.length) return forward(); - const sendOptions = { ...input.state.sendOptions }; - const skip_enrich_url = - (!someLinkPreviewsLoading && linkPreviews.length > 0) || someLinkPreviewsDismissed; - if (skip_enrich_url) { - sendOptions.skip_enrich_url = true; - } + const sendOptions = { ...state.sendOptions }; + const skip_enrich_url = + (!someLinkPreviewsLoading && linkPreviews.length > 0) || + someLinkPreviewsDismissed; + if (skip_enrich_url) { + sendOptions.skip_enrich_url = true; + } - return nextHandler({ - ...input, - state: { - ...input.state, + return next({ + ...state, message: { - ...input.state.message, + ...state.message, attachments, }, localMessage: { - ...input.state.localMessage, + ...state.localMessage, attachments, }, sendOptions, - }, - }); + }); + }, }, }); export const createDraftLinkPreviewsCompositionMiddleware = ( composer: MessageComposer, -) => ({ +): MessageDraftCompositionMiddleware => ({ id: 'stream-io/message-composer-middleware/draft-link-previews', - compose: ({ - input, - nextHandler, - }: MiddlewareHandlerParams) => { - const { linkPreviewsManager } = composer; - if (!linkPreviewsManager) return nextHandler(input); + handlers: { + compose: ({ + state, + next, + forward, + }: MiddlewareHandlerParams) => { + const { linkPreviewsManager } = composer; + if (!linkPreviewsManager) return forward(); - linkPreviewsManager.cancelURLEnrichment(); - const linkPreviews = linkPreviewsManager.loadedPreviews.map((preview) => - LinkPreviewsManager.getPreviewData(preview), - ); + linkPreviewsManager.cancelURLEnrichment(); + const linkPreviews = linkPreviewsManager.loadedPreviews.map((preview) => + LinkPreviewsManager.getPreviewData(preview), + ); - if (!linkPreviews.length) return nextHandler(input); + if (!linkPreviews.length) return forward(); - return nextHandler({ - ...input, - state: { - ...input.state, + return next({ + ...state, draft: { - ...input.state.draft, - attachments: (input.state.draft.attachments ?? []).concat(linkPreviews), + ...state.draft, + attachments: (state.draft.attachments ?? []).concat(linkPreviews), }, - }, - }); + }); + }, }, }); diff --git a/src/messageComposer/middleware/messageComposer/messageComposerState.ts b/src/messageComposer/middleware/messageComposer/messageComposerState.ts index 38b8efd23a..e033d070a2 100644 --- a/src/messageComposer/middleware/messageComposer/messageComposerState.ts +++ b/src/messageComposer/middleware/messageComposer/messageComposerState.ts @@ -1,6 +1,8 @@ import type { - MessageComposerMiddlewareValueState, + MessageComposerMiddlewareState, + MessageCompositionMiddleware, MessageDraftComposerMiddlewareValueState, + MessageDraftCompositionMiddleware, } from './types'; import type { MessageComposer } from '../../messageComposer'; import type { LocalMessage, LocalMessageBase } from '../../../types'; @@ -8,63 +10,61 @@ import type { MiddlewareHandlerParams } from '../../../middleware'; export const createMessageComposerStateCompositionMiddleware = ( composer: MessageComposer, -) => ({ +): MessageCompositionMiddleware => ({ id: 'stream-io/message-composer-middleware/own-state', - compose: ({ - input, - nextHandler, - }: MiddlewareHandlerParams) => { - const payload: Pick = {}; - if (composer.quotedMessage) { - payload.quoted_message_id = composer.quotedMessage.id; - } - if (composer.pollId) { - payload.poll_id = composer.pollId; - } + handlers: { + compose: ({ + state, + next, + }: MiddlewareHandlerParams) => { + const payload: Pick = {}; + if (composer.quotedMessage) { + payload.quoted_message_id = composer.quotedMessage.id; + } + if (composer.pollId) { + payload.poll_id = composer.pollId; + } - return nextHandler({ - ...input, - state: { - ...input.state, + return next({ + ...state, localMessage: { - ...input.state.localMessage, + ...state.localMessage, ...payload, quoted_message: (composer.quotedMessage as LocalMessageBase) ?? undefined, }, message: { - ...input.state.message, + ...state.message, ...payload, }, - }, - }); + }); + }, }, }); export const createDraftMessageComposerStateCompositionMiddleware = ( composer: MessageComposer, -) => ({ +): MessageDraftCompositionMiddleware => ({ id: 'stream-io/message-composer-middleware/draft-own-state', - compose: ({ - input, - nextHandler, - }: MiddlewareHandlerParams) => { - const payload: Pick = {}; - if (composer.quotedMessage) { - payload.quoted_message_id = composer.quotedMessage.id; - } - if (composer.pollId) { - payload.poll_id = composer.pollId; - } + handlers: { + compose: ({ + state, + next, + }: MiddlewareHandlerParams) => { + const payload: Pick = {}; + if (composer.quotedMessage) { + payload.quoted_message_id = composer.quotedMessage.id; + } + if (composer.pollId) { + payload.poll_id = composer.pollId; + } - return nextHandler({ - ...input, - state: { - ...input.state, + return next({ + ...state, draft: { - ...input.state.draft, + ...state.draft, ...payload, }, - }, - }); + }); + }, }, }); diff --git a/src/messageComposer/middleware/messageComposer/textComposer.ts b/src/messageComposer/middleware/messageComposer/textComposer.ts index 8c4be70e53..5a702515b1 100644 --- a/src/messageComposer/middleware/messageComposer/textComposer.ts +++ b/src/messageComposer/middleware/messageComposer/textComposer.ts @@ -1,91 +1,95 @@ import type { - MessageComposerMiddlewareValueState, + MessageComposerMiddlewareState, + MessageCompositionMiddleware, MessageDraftComposerMiddlewareValueState, + MessageDraftCompositionMiddleware, } from './types'; import type { MessageComposer } from '../../messageComposer'; import type { MiddlewareHandlerParams } from '../../../middleware'; -export const createTextComposerCompositionMiddleware = (composer: MessageComposer) => ({ +export const createTextComposerCompositionMiddleware = ( + composer: MessageComposer, +): MessageCompositionMiddleware => ({ id: 'stream-io/message-composer-middleware/text-composition', - compose: ({ - input, - nextHandler, - }: MiddlewareHandlerParams) => { - if (!composer.textComposer) return nextHandler(input); - const { mentionedUsers, text } = composer.textComposer; - // Instead of checking if a user is still mentioned every time the text changes, - // just filter out non-mentioned users before submit, which is cheaper - // and allows users to easily undo any accidental deletion - const mentioned_users = Array.from( - new Set( - mentionedUsers.filter( - ({ id, name }) => text.includes(`@${id}`) || text.includes(`@${name}`), + handlers: { + compose: ({ + state, + next, + forward, + }: MiddlewareHandlerParams) => { + if (!composer.textComposer) return forward(); + const { mentionedUsers, text } = composer.textComposer; + // Instead of checking if a user is still mentioned every time the text changes, + // just filter out non-mentioned users before submit, which is cheaper + // and allows users to easily undo any accidental deletion + const mentioned_users = Array.from( + new Set( + mentionedUsers.filter( + ({ id, name }) => text.includes(`@${id}`) || text.includes(`@${name}`), + ), ), - ), - ); + ); - // prevent introducing text and mentioned_users array into the payload sent to the server - if (!text && mentioned_users.length === 0) return nextHandler(input); + // prevent introducing text and mentioned_users array into the payload sent to the server + if (!text && mentioned_users.length === 0) return forward(); - return nextHandler({ - ...input, - state: { - ...input.state, + return next({ + ...state, localMessage: { - ...input.state.localMessage, + ...state.localMessage, mentioned_users, text, }, message: { - ...input.state.message, + ...state.message, mentioned_users: mentioned_users.map((u) => u.id), text, }, - }, - }); + }); + }, }, }); export const createDraftTextComposerCompositionMiddleware = ( composer: MessageComposer, -) => ({ +): MessageDraftCompositionMiddleware => ({ id: 'stream-io/message-composer-middleware/draft-text-composition', - compose: ({ - input, - nextHandler, - }: MiddlewareHandlerParams) => { - if (!composer.textComposer) return nextHandler(input); - const { maxLengthOnSend } = composer.config.text ?? {}; - const { mentionedUsers, text: inputText } = composer.textComposer; - // Instead of checking if a user is still mentioned every time the text changes, - // just filter out non-mentioned users before submit, which is cheaper - // and allows users to easily undo any accidental deletion - const mentioned_users = mentionedUsers.length - ? Array.from( - new Set( - mentionedUsers.filter( - ({ id, name }) => - inputText.includes(`@${id}`) || inputText.includes(`@${name}`), + handlers: { + compose: ({ + state, + next, + forward, + }: MiddlewareHandlerParams) => { + if (!composer.textComposer) return forward(); + const { maxLengthOnSend } = composer.config.text ?? {}; + const { mentionedUsers, text: inputText } = composer.textComposer; + // Instead of checking if a user is still mentioned every time the text changes, + // just filter out non-mentioned users before submit, which is cheaper + // and allows users to easily undo any accidental deletion + const mentioned_users = mentionedUsers.length + ? Array.from( + new Set( + mentionedUsers.filter( + ({ id, name }) => + inputText.includes(`@${id}`) || inputText.includes(`@${name}`), + ), ), - ), - ) - : undefined; + ) + : undefined; - const text = - typeof maxLengthOnSend === 'number' && inputText.length > maxLengthOnSend - ? inputText.slice(0, maxLengthOnSend) - : inputText; + const text = + typeof maxLengthOnSend === 'number' && inputText.length > maxLengthOnSend + ? inputText.slice(0, maxLengthOnSend) + : inputText; - return nextHandler({ - ...input, - state: { - ...input.state, + return next({ + ...state, draft: { - ...input.state.draft, + ...state.draft, mentioned_users: mentioned_users?.map((u) => u.id), text, }, - }, - }); + }); + }, }, }); diff --git a/src/messageComposer/middleware/messageComposer/types.ts b/src/messageComposer/middleware/messageComposer/types.ts index cce798bb88..51ae110fd5 100644 --- a/src/messageComposer/middleware/messageComposer/types.ts +++ b/src/messageComposer/middleware/messageComposer/types.ts @@ -1,4 +1,4 @@ -import type { MiddlewareValue } from '../../../middleware'; +import type { Middleware, MiddlewareExecutionResult } from '../../../middleware'; import type { DraftMessagePayload, LocalMessage, @@ -8,14 +8,14 @@ import type { } from '../../../types'; import type { MessageComposer } from '../../messageComposer'; -export type MessageComposerMiddlewareValueState = { +export type MessageComposerMiddlewareState = { message: Message | UpdatedMessage; localMessage: LocalMessage; sendOptions: SendMessageOptions; }; export type MessageComposerMiddlewareValue = - MiddlewareValue; + MiddlewareExecutionResult; export type MessageComposerMiddlewareExecutorOptions = { composer: MessageComposer; @@ -28,3 +28,13 @@ export type MessageDraftComposerMiddlewareValueState = { export type MessageDraftComposerMiddlewareExecutorOptions = { composer: MessageComposer; }; + +export type MessageCompositionMiddleware = Middleware< + MessageComposerMiddlewareState, + 'compose' +>; + +export type MessageDraftCompositionMiddleware = Middleware< + MessageDraftComposerMiddlewareValueState, + 'compose' +>; diff --git a/src/messageComposer/middleware/pollComposer/PollComposerMiddlewareExecutor.ts b/src/messageComposer/middleware/pollComposer/PollComposerMiddlewareExecutor.ts index 71a098bc22..a6391620c2 100644 --- a/src/messageComposer/middleware/pollComposer/PollComposerMiddlewareExecutor.ts +++ b/src/messageComposer/middleware/pollComposer/PollComposerMiddlewareExecutor.ts @@ -3,7 +3,7 @@ import { createPollComposerStateMiddleware } from './state'; import { createPollCompositionValidationMiddleware } from './composition'; import type { PollComposerCompositionMiddlewareValueState, - PollComposerStateMiddlewareValueState, + PollComposerStateChangeMiddlewareValue, } from './types'; import type { MessageComposer } from '../../messageComposer'; @@ -11,14 +11,20 @@ export type PollComposerMiddlewareExecutorOptions = { composer: MessageComposer; }; -export class PollComposerCompositionMiddlewareExecutor extends MiddlewareExecutor { +export class PollComposerCompositionMiddlewareExecutor extends MiddlewareExecutor< + PollComposerCompositionMiddlewareValueState, + 'compose' +> { constructor({ composer }: PollComposerMiddlewareExecutorOptions) { super(); this.use([createPollCompositionValidationMiddleware(composer)]); } } -export class PollComposerStateMiddlewareExecutor extends MiddlewareExecutor { +export class PollComposerStateMiddlewareExecutor extends MiddlewareExecutor< + PollComposerStateChangeMiddlewareValue, + 'handleFieldChange' | 'handleFieldBlur' +> { constructor() { super(); this.use([createPollComposerStateMiddleware()]); diff --git a/src/messageComposer/middleware/pollComposer/composition.ts b/src/messageComposer/middleware/pollComposer/composition.ts index bcbaf2bfa2..26dc5aa235 100644 --- a/src/messageComposer/middleware/pollComposer/composition.ts +++ b/src/messageComposer/middleware/pollComposer/composition.ts @@ -1,17 +1,23 @@ -import type { PollComposerCompositionMiddlewareValueState } from './types'; +import type { Middleware, MiddlewareHandlerParams } from '../../../middleware'; import type { MessageComposer } from '../../messageComposer'; -import type { Middleware } from '../../../middleware'; -import type { MiddlewareHandlerParams } from '../../../middleware'; +import type { PollComposerCompositionMiddlewareValueState } from './types'; + +export type PollCompositionValidationMiddleware = Middleware< + PollComposerCompositionMiddlewareValueState, + 'compose' +>; export const createPollCompositionValidationMiddleware = ( composer: MessageComposer, -): Middleware => ({ +): PollCompositionValidationMiddleware => ({ id: 'stream-io/poll-composer-composition', - compose: ({ - input, - nextHandler, - }: MiddlewareHandlerParams) => { - if (composer.pollComposer.canCreatePoll) return nextHandler(input); - return nextHandler({ ...input, status: 'discard' }); + handlers: { + compose: ({ + discard, + forward, + }: MiddlewareHandlerParams) => { + if (composer.pollComposer.canCreatePoll) return forward(); + return discard(); + }, }, }); diff --git a/src/messageComposer/middleware/pollComposer/state.ts b/src/messageComposer/middleware/pollComposer/state.ts index ef9ead0bc6..2c74378212 100644 --- a/src/messageComposer/middleware/pollComposer/state.ts +++ b/src/messageComposer/middleware/pollComposer/state.ts @@ -1,12 +1,12 @@ +import type { Middleware, MiddlewareHandlerParams } from '../../../middleware'; +import { generateUUIDv4 } from '../../../utils'; import type { PollComposerFieldErrors, PollComposerState, - PollComposerStateMiddlewareValueState, + PollComposerStateChangeMiddlewareValue, TargetedPollOptionTextUpdate, } from './types'; -import { generateUUIDv4 } from '../../../utils'; -import type { Middleware } from '../../../middleware'; -import type { MiddlewareHandlerParams } from '../../../middleware'; + export const VALID_MAX_VOTES_VALUE_REGEX = /^([2-9]|10)$/; export const MAX_POLL_OPTIONS = 100 as const; @@ -153,12 +153,17 @@ export type PollComposerStateMiddlewareFactoryOptions = { }; }; +export type PollComposerStateMiddleware = Middleware< + PollComposerStateChangeMiddlewareValue, + 'handleFieldChange' | 'handleFieldBlur' +>; + export const createPollComposerStateMiddleware = ({ processors: customProcessors, validators: customValidators, -}: PollComposerStateMiddlewareFactoryOptions = {}): Middleware => { +}: PollComposerStateMiddlewareFactoryOptions = {}): PollComposerStateMiddleware => { const universalHandler = ( - state: PollComposerStateMiddlewareValueState, + state: PollComposerStateChangeMiddlewareValue, validators: Partial< Record >, @@ -213,73 +218,67 @@ export const createPollComposerStateMiddleware = ({ return { id: 'stream-io/poll-composer-state-processing', - handleFieldChange: ({ - input, - nextHandler, - }: MiddlewareHandlerParams) => { - if (!input.state.targetFields) return nextHandler(input); - const { - state: { previousState }, - } = input; - const finalValidators = { - ...pollStateChangeValidators, - ...defaultPollFieldChangeEventValidators, - ...customValidators?.handleFieldChange, - }; - const finalProcessors = { - ...pollCompositionStateProcessors, - ...customProcessors?.handleFieldChange, - }; + handlers: { + handleFieldChange: ({ + state, + next, + forward, + }: MiddlewareHandlerParams) => { + if (!state.targetFields) return forward(); + const { previousState } = state; + const finalValidators = { + ...pollStateChangeValidators, + ...defaultPollFieldChangeEventValidators, + ...customValidators?.handleFieldChange, + }; + const finalProcessors = { + ...pollCompositionStateProcessors, + ...customProcessors?.handleFieldChange, + }; - const { newData, newErrors } = universalHandler( - input.state, - finalValidators, - finalProcessors, - ); + const { newData, newErrors } = universalHandler( + state, + finalValidators, + finalProcessors, + ); - return nextHandler({ - ...input, - state: { - ...input.state, + return next({ + ...state, nextState: { ...previousState, data: { ...previousState.data, ...newData }, errors: { ...previousState.errors, ...newErrors }, }, - }, - }); - }, - handleFieldBlur: ({ - input, - nextHandler, - }: MiddlewareHandlerParams) => { - if (!input.state.targetFields) return nextHandler(input); + }); + }, + handleFieldBlur: ({ + state, + next, + forward, + }: MiddlewareHandlerParams) => { + if (!state.targetFields) return forward(); - const { - state: { previousState }, - } = input; - const finalValidators = { - ...pollStateChangeValidators, - ...defaultPollFieldBlurEventValidators, - ...customValidators?.handleFieldBlur, - }; - const { newData, newErrors } = universalHandler( - input.state, - finalValidators, - customProcessors?.handleFieldBlur, - ); + const { previousState } = state; + const finalValidators = { + ...pollStateChangeValidators, + ...defaultPollFieldBlurEventValidators, + ...customValidators?.handleFieldBlur, + }; + const { newData, newErrors } = universalHandler( + state, + finalValidators, + customProcessors?.handleFieldBlur, + ); - return nextHandler({ - ...input, - state: { - ...input.state, + return next({ + ...state, nextState: { ...previousState, data: { ...previousState.data, ...newData }, errors: { ...previousState.errors, ...newErrors }, }, - }, - }); + }); + }, }, }; }; diff --git a/src/messageComposer/middleware/pollComposer/types.ts b/src/messageComposer/middleware/pollComposer/types.ts index 038ce9d85d..6c590920f4 100644 --- a/src/messageComposer/middleware/pollComposer/types.ts +++ b/src/messageComposer/middleware/pollComposer/types.ts @@ -1,4 +1,4 @@ -import type { MiddlewareValue } from '../../../middleware'; +import type { MiddlewareExecutionResult } from '../../../middleware'; import type { CreatePollData, VotingVisibility } from '../../../types'; export type PollComposerOption = { @@ -50,9 +50,9 @@ export type PollComposerCompositionMiddlewareValueState = { }; export type PollComposerCompositionMiddlewareValue = - MiddlewareValue; + MiddlewareExecutionResult; -export type PollComposerStateMiddlewareValueState = { +export type PollComposerStateChangeMiddlewareValue = { nextState: PollComposerState; previousState: PollComposerState; targetFields: Partial<{ @@ -63,4 +63,4 @@ export type PollComposerStateMiddlewareValueState = { }; export type PollComposerStateMiddlewareValue = - MiddlewareValue; + MiddlewareExecutionResult; diff --git a/src/messageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.ts b/src/messageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.ts index cd792cd0eb..a102ae8998 100644 --- a/src/messageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.ts +++ b/src/messageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.ts @@ -1,31 +1,61 @@ import { createCommandsMiddleware } from './commands'; import { createMentionsMiddleware } from './mentions'; import { createTextComposerPreValidationMiddleware } from './validation'; +import type { + ExecuteParams, + MiddlewareExecutionResult, + MiddlewareHandler, +} from '../../../middleware'; import { MiddlewareExecutor } from '../../../middleware'; import { withCancellation } from '../../../utils/concurrency'; import type { - TextComposerMiddleware, + Suggestion, TextComposerMiddlewareExecutorOptions, - TextComposerMiddlewareValue, + TextComposerState, } from './types'; -import type { TextComposerState, TextComposerSuggestion } from '../../types'; -export class TextComposerMiddlewareExecutor extends MiddlewareExecutor { +export type TextComposerMiddlewareExecutorState = + TextComposerState & { + change?: { + selectedSuggestion?: T; + }; + }; + +export type TextComposerHandlerNames = 'onChange' | 'onSuggestionItemSelect'; + +export type TextComposerMiddleware = { + id: string; + handlers: { + [K in TextComposerHandlerNames]: MiddlewareHandler< + TextComposerMiddlewareExecutorState + >; + }; +}; + +export class TextComposerMiddlewareExecutor< + T extends Suggestion = Suggestion, +> extends MiddlewareExecutor< + TextComposerMiddlewareExecutorState, + TextComposerHandlerNames +> { constructor({ composer }: TextComposerMiddlewareExecutorOptions) { super(); this.use([ - createTextComposerPreValidationMiddleware(composer), - createMentionsMiddleware(composer.channel), - createCommandsMiddleware(composer.channel), - ] as TextComposerMiddleware[]); + createTextComposerPreValidationMiddleware(composer) as TextComposerMiddleware, + createMentionsMiddleware(composer.channel) as TextComposerMiddleware, + createCommandsMiddleware(composer.channel) as TextComposerMiddleware, + ]); } - async execute( - eventName: string, - initialInput: TextComposerMiddlewareValue, - selectedSuggestion?: TextComposerSuggestion, - ): Promise { - const result = await this.executeMiddlewareChain(eventName, initialInput, { - selectedSuggestion, + + async execute({ + eventName, + initialValue: initialState, + }: ExecuteParams>): Promise< + MiddlewareExecutionResult> + > { + const result = await this.executeMiddlewareChain({ + eventName, + initialValue: initialState, }); if (result && result.state.suggestions) { diff --git a/src/messageComposer/middleware/textComposer/commands.ts b/src/messageComposer/middleware/textComposer/commands.ts index 05169dfe31..833bf113af 100644 --- a/src/messageComposer/middleware/textComposer/commands.ts +++ b/src/messageComposer/middleware/textComposer/commands.ts @@ -1,16 +1,12 @@ -import { getTriggerCharWithToken, insertItemWithTrigger } from './textMiddlewareUtils'; -import { BaseSearchSource } from '../../../search_controller'; -import { mergeWith } from '../../../utils/mergeWith'; -import type { - TextComposerMiddlewareOptions, - TextComposerMiddlewareParams, -} from './types'; +import type { Channel } from '../../../channel'; +import type { Middleware } from '../../../middleware'; import type { SearchSourceOptions } from '../../../search_controller'; +import { BaseSearchSource } from '../../../search_controller'; import type { CommandResponse } from '../../../types'; -import type { Channel } from '../../../channel'; -import type { TextComposerSuggestion } from '../../types'; - -export type CommandSuggestion = TextComposerSuggestion; +import { mergeWith } from '../../../utils/mergeWith'; +import type { CommandSuggestion, TextComposerMiddlewareOptions } from './types'; +import { getTriggerCharWithToken, insertItemWithTrigger } from './textMiddlewareUtils'; +import type { TextComposerMiddlewareExecutorState } from './TextComposerMiddlewareExecutor'; export class CommandSearchSource extends BaseSearchSource { readonly type = 'commands'; @@ -96,12 +92,17 @@ export class CommandSearchSource extends BaseSearchSource { const DEFAULT_OPTIONS: TextComposerMiddlewareOptions = { minChars: 1, trigger: '/' }; +export type CommandsMiddleware = Middleware< + TextComposerMiddlewareExecutorState, + 'onChange' | 'onSuggestionItemSelect' +>; + export const createCommandsMiddleware = ( channel: Channel, options?: Partial & { searchSource?: CommandSearchSource; }, -) => { +): CommandsMiddleware => { const finalOptions = mergeWith(DEFAULT_OPTIONS, options ?? {}); let searchSource = new CommandSearchSource(channel); if (options?.searchSource) { @@ -112,68 +113,52 @@ export const createCommandsMiddleware = ( return { id: 'stream-io/text-composer/commands-middleware', - onChange: ({ - input, - nextHandler, - }: TextComposerMiddlewareParams) => { - const { state } = input; - if (!state.selection) return nextHandler(input); - - // const firstCharIsNotCommandTrigger = - // state.text.length === 0 || state.text[0] !== finalOptions.trigger; - // if (firstCharIsNotCommandTrigger) return nextHandler(input); - - const triggerWithToken = getTriggerCharWithToken({ - trigger: finalOptions.trigger, - text: state.text.slice(0, state.selection.end), - acceptTrailingSpaces: false, - isCommand: true, - }); - - const newSearchTriggerred = - triggerWithToken && triggerWithToken.length === finalOptions.minChars; - - if (newSearchTriggerred) { - searchSource.resetStateAndActivate(); - } + handlers: { + onChange: ({ state, next, complete, forward }) => { + if (!state.selection) return forward(); + + const triggerWithToken = getTriggerCharWithToken({ + trigger: finalOptions.trigger, + text: state.text.slice(0, state.selection.end), + acceptTrailingSpaces: false, + isCommand: true, + }); + + const newSearchTriggerred = + triggerWithToken && triggerWithToken.length === finalOptions.minChars; + + if (newSearchTriggerred) { + searchSource.resetStateAndActivate(); + } - const triggerWasRemoved = - !triggerWithToken || triggerWithToken.length < finalOptions.minChars; + const triggerWasRemoved = + !triggerWithToken || triggerWithToken.length < finalOptions.minChars; - if (triggerWasRemoved) { - const hasStaleSuggestions = - input.state.suggestions?.trigger === finalOptions.trigger; - const newInput = { ...input }; - if (hasStaleSuggestions) { - delete newInput.state.suggestions; + if (triggerWasRemoved) { + const hasStaleSuggestions = state.suggestions?.trigger === finalOptions.trigger; + const newState = { ...state }; + if (hasStaleSuggestions) { + delete newState.suggestions; + } + return next(newState); } - return nextHandler(newInput); - } - return Promise.resolve({ - state: { + return complete({ ...state, suggestions: { query: triggerWithToken.slice(1), searchSource, trigger: finalOptions.trigger, }, - }, - stop: true, // Stop other middleware from processing '/' character - }); - }, - onSuggestionItemSelect: ({ - input, - nextHandler, - selectedSuggestion, - }: TextComposerMiddlewareParams) => { - const { state } = input; - if (!selectedSuggestion || state.suggestions?.trigger !== finalOptions.trigger) - return nextHandler(input); - - searchSource.resetStateAndActivate(); - return Promise.resolve({ - state: { + }); + }, + onSuggestionItemSelect: ({ state, complete, forward }) => { + const { selectedSuggestion } = state.change ?? {}; + if (!selectedSuggestion || state.suggestions?.trigger !== finalOptions.trigger) + return forward(); + + searchSource.resetStateAndActivate(); + return complete({ ...state, ...insertItemWithTrigger({ insertText: `/${selectedSuggestion.name} `, @@ -181,9 +166,9 @@ export const createCommandsMiddleware = ( text: state.text, trigger: finalOptions.trigger, }), - suggestions: undefined, // Clear suggestions after selection - }, - }); + suggestions: undefined, + }); + }, }, }; }; diff --git a/src/messageComposer/middleware/textComposer/mentions.ts b/src/messageComposer/middleware/textComposer/mentions.ts index dcdae6fabb..a00390c5c8 100644 --- a/src/messageComposer/middleware/textComposer/mentions.ts +++ b/src/messageComposer/middleware/textComposer/mentions.ts @@ -1,4 +1,3 @@ -import type { TokenizationPayload } from './textMiddlewareUtils'; import { getTokenizedSuggestionDisplayName, getTriggerCharWithToken, @@ -7,10 +6,7 @@ import { import type { SearchSourceOptions } from '../../../search_controller'; import { BaseSearchSource } from '../../../search_controller'; import { mergeWith } from '../../../utils/mergeWith'; -import type { - TextComposerMiddlewareOptions, - TextComposerMiddlewareParams, -} from './types'; +import type { TextComposerMiddlewareOptions, UserSuggestion } from './types'; import type { StreamChat } from '../../../client'; import type { MemberFilters, @@ -22,9 +18,8 @@ import type { } from '../../../types'; import type { Channel } from '../../../channel'; import { MAX_CHANNEL_MEMBER_COUNT_IN_CHANNEL_QUERY } from '../../../constants'; -import type { TextComposerSuggestion } from '../../types'; - -export type UserSuggestion = TextComposerSuggestion; +import type { Middleware } from '../../../middleware'; +import type { TextComposerMiddlewareExecutorState } from './TextComposerMiddlewareExecutor'; // todo: the map is too small - Slavic letters with diacritics are missing for example export const accentsMap: { [key: string]: string } = { @@ -106,7 +101,7 @@ export class MentionsSearchSource extends BaseSearchSource { this.client = channel.getClient(); this.channel = channel; this.config = { mentionAllAppUsers, textComposerText }; - // todo: how to propagate useMentionsTransliteration to change dynamically the setting? const { default: transliterate } = await import('@stream-io/transliterate'); + if (transliterate) { this.transliterate = transliterate; } @@ -326,14 +321,19 @@ const userSuggestionToUserResponse = (suggestion: UserSuggestion): UserResponse * @returns */ +export type MentionsMiddleware = Middleware< + TextComposerMiddlewareExecutorState, + 'onChange' | 'onSuggestionItemSelect' +>; + export const createMentionsMiddleware = ( channel: Channel, options?: Partial & { searchSource?: MentionsSearchSource; }, -) => { +): MentionsMiddleware => { const finalOptions = mergeWith(DEFAULT_OPTIONS, options ?? {}); - let searchSource; + let searchSource: MentionsSearchSource; if (options?.searchSource) { searchSource = options.searchSource; searchSource.resetState(); @@ -343,62 +343,52 @@ export const createMentionsMiddleware = ( searchSource.activate(); return { id: 'stream-io/text-composer/mentions-middleware', - onChange: ({ input, nextHandler }: TextComposerMiddlewareParams) => { - const { state } = input; - if (!state.selection) return nextHandler(input); + handlers: { + onChange: ({ state, next, complete, forward }) => { + if (!state.selection) return forward(); - const triggerWithToken = getTriggerCharWithToken({ - trigger: finalOptions.trigger, - text: state.text.slice(0, state.selection.end), - }); + const triggerWithToken = getTriggerCharWithToken({ + trigger: finalOptions.trigger, + text: state.text.slice(0, state.selection.end), + }); - const newSearchTriggered = - triggerWithToken && triggerWithToken.length === finalOptions.minChars; + const newSearchTriggered = + triggerWithToken && triggerWithToken.length === finalOptions.minChars; - if (newSearchTriggered) { - searchSource.resetStateAndActivate(); - } + if (newSearchTriggered) { + searchSource.resetStateAndActivate(); + } - const triggerWasRemoved = - !triggerWithToken || triggerWithToken.length < finalOptions.minChars; + const triggerWasRemoved = + !triggerWithToken || triggerWithToken.length < finalOptions.minChars; - if (triggerWasRemoved) { - const hasStaleSuggestions = - input.state.suggestions?.trigger === finalOptions.trigger; - const newInput = { ...input }; - if (hasStaleSuggestions) { - delete newInput.state.suggestions; - // todo: how to remove mentioned users on deleting the text + if (triggerWasRemoved) { + const hasStaleSuggestions = state.suggestions?.trigger === finalOptions.trigger; + const newState = { ...state }; + if (hasStaleSuggestions) { + delete newState.suggestions; + } + return next(newState); } - return nextHandler(newInput); - } - searchSource.config.textComposerText = input.state.text; + searchSource.config.textComposerText = state.text; - return Promise.resolve({ - state: { + return complete({ ...state, suggestions: { query: triggerWithToken.slice(1), searchSource, trigger: finalOptions.trigger, }, - }, - status: 'complete', // Stop other middleware from processing '@' character - }); - }, - onSuggestionItemSelect: ({ - input, - nextHandler, - selectedSuggestion, - }: TextComposerMiddlewareParams) => { - const { state } = input; - if (!selectedSuggestion || state.suggestions?.trigger !== finalOptions.trigger) - return nextHandler(input); - - searchSource.resetStateAndActivate(); - return Promise.resolve({ - state: { + }); + }, + onSuggestionItemSelect: ({ state, complete, forward }) => { + const { selectedSuggestion } = state.change ?? {}; + if (!selectedSuggestion || state.suggestions?.trigger !== finalOptions.trigger) + return forward(); + + searchSource.resetStateAndActivate(); + return complete({ ...state, ...insertItemWithTrigger({ insertText: `@${selectedSuggestion.name || selectedSuggestion.id} `, @@ -409,9 +399,9 @@ export const createMentionsMiddleware = ( mentionedUsers: state.mentionedUsers.concat( userSuggestionToUserResponse(selectedSuggestion), ), - suggestions: undefined, // Clear suggestions after selection - }, - }); + suggestions: undefined, + }); + }, }, }; }; diff --git a/src/messageComposer/middleware/textComposer/textMiddlewareUtils.ts b/src/messageComposer/middleware/textComposer/textMiddlewareUtils.ts index 5bcb717a42..b16560c9f7 100644 --- a/src/messageComposer/middleware/textComposer/textMiddlewareUtils.ts +++ b/src/messageComposer/middleware/textComposer/textMiddlewareUtils.ts @@ -1,4 +1,4 @@ -import type { TextSelection } from '../../types'; +import type { TextSelection } from './types'; export const getTriggerCharWithToken = ({ trigger, diff --git a/src/messageComposer/middleware/textComposer/types.ts b/src/messageComposer/middleware/textComposer/types.ts index 47d2902769..b3a56a60c6 100644 --- a/src/messageComposer/middleware/textComposer/types.ts +++ b/src/messageComposer/middleware/textComposer/types.ts @@ -1,36 +1,42 @@ -import type { TextComposerState, TextComposerSuggestion } from '../../types'; -import type { MiddlewareValue } from '../../../middleware'; import type { MessageComposer } from '../../messageComposer'; +import type { CommandResponse, UserResponse } from '../../../types'; +import type { TokenizationPayload } from './textMiddlewareUtils'; +import type { SearchSource } from '../../../search_controller'; +import type { CustomTextComposerSuggestion } from '../../types.custom'; + +export type TextComposerSuggestion = T & { + id: string; +}; + +export type BaseSuggestion = { + id: string; +}; + +export type CommandSuggestion = BaseSuggestion & CommandResponse; +export type UserSuggestion = BaseSuggestion & UserResponse & TokenizationPayload; +export type CustomValidSuggestion = BaseSuggestion & CustomTextComposerSuggestion; +export type Suggestion = CommandSuggestion | UserSuggestion | CustomValidSuggestion; export type TextComposerMiddlewareOptions = { minChars: number; trigger: string; }; -export type TextComposerMiddlewareValue = MiddlewareValue; - -export type TextComposerMiddlewareParams = { - input: TextComposerMiddlewareValue; - nextHandler: ( - input: TextComposerMiddlewareValue, - ) => Promise; - selectedSuggestion?: TextComposerSuggestion; +export type TextComposerMiddlewareExecutorOptions = { + composer: MessageComposer; }; -export type TextComposerMiddlewareHandler = ( - params: TextComposerMiddlewareParams, -) => Promise; - -export type CustomTextComposerMiddleware = { - [key: string]: string | TextComposerMiddlewareHandler; +export type Suggestions = { + query: string; + searchSource: SearchSource; + trigger: string; }; -export type TextComposerMiddleware = CustomTextComposerMiddleware & { - id: string; - onChange?: string | TextComposerMiddlewareHandler; - onSuggestionItemSelect?: string | TextComposerMiddlewareHandler; -}; +export type TextSelection = { end: number; start: number }; -export type TextComposerMiddlewareExecutorOptions = { - composer: MessageComposer; +export type TextComposerState = { + mentionedUsers: UserResponse[]; + selection: TextSelection; + text: string; + suggestions?: Suggestions; }; diff --git a/src/messageComposer/middleware/textComposer/validation.ts b/src/messageComposer/middleware/textComposer/validation.ts index 86975eb5bd..abc83e97da 100644 --- a/src/messageComposer/middleware/textComposer/validation.ts +++ b/src/messageComposer/middleware/textComposer/validation.ts @@ -1,24 +1,29 @@ import type { MessageComposer } from '../../messageComposer'; -import type { TextComposerMiddlewareParams } from './types'; -import type { UserSuggestion } from './mentions'; +import type { TextComposerMiddlewareExecutorState } from './TextComposerMiddlewareExecutor'; +import type { Suggestion } from './types'; +import type { Middleware } from '../../../middleware'; -export const createTextComposerPreValidationMiddleware = (composer: MessageComposer) => ({ +export type TextComposerPreValidationMiddleware = Middleware< + TextComposerMiddlewareExecutorState, + 'onChange' | 'onSuggestionItemSelect' +>; + +export const createTextComposerPreValidationMiddleware = ( + composer: MessageComposer, +): TextComposerPreValidationMiddleware => ({ id: 'stream-io/text-composer/pre-validation-middleware', - onChange: ({ input, nextHandler }: TextComposerMiddlewareParams) => { - const { maxLengthOnEdit } = composer.config.text ?? {}; - if ( - typeof maxLengthOnEdit === 'number' && - input.state.text.length > maxLengthOnEdit - ) { - input.state.text = input.state.text.slice(0, maxLengthOnEdit); - return nextHandler({ - ...input, - state: { - ...input.state, - text: input.state.text, - }, - }); - } - return nextHandler(input); + handlers: { + onChange: ({ state, next, forward }) => { + const { maxLengthOnEdit } = composer.config.text ?? {}; + if (typeof maxLengthOnEdit === 'number' && state.text.length > maxLengthOnEdit) { + state.text = state.text.slice(0, maxLengthOnEdit); + return next({ + ...state, + text: state.text, + }); + } + return forward(); + }, + onSuggestionItemSelect: ({ forward }) => forward(), }, }); diff --git a/src/messageComposer/pollComposer.ts b/src/messageComposer/pollComposer.ts index 657b10b9bb..ced5c5e646 100644 --- a/src/messageComposer/pollComposer.ts +++ b/src/messageComposer/pollComposer.ts @@ -103,24 +103,23 @@ export class PollComposer { }; updateFields = async (data: UpdateFieldsData) => { - const { state, status } = await this.stateMiddlewareExecutor.execute( - 'handleFieldChange', - { - state: { - nextState: { ...this.state.getLatestValue() }, - previousState: { ...this.state.getLatestValue() }, - targetFields: data, - }, + const { state, status } = await this.stateMiddlewareExecutor.execute({ + eventName: 'handleFieldChange', + initialValue: { + nextState: { ...this.state.getLatestValue() }, + previousState: { ...this.state.getLatestValue() }, + targetFields: data, }, - ); + }); if (status === 'discard') return; this.state.next(state.nextState); }; handleFieldBlur = async (field: keyof PollComposerState['data']) => { - const result = await this.stateMiddlewareExecutor.execute('handleFieldBlur', { - state: { + const result = await this.stateMiddlewareExecutor.execute({ + eventName: 'handleFieldBlur', + initialValue: { nextState: { ...this.state.getLatestValue() }, previousState: { ...this.state.getLatestValue() }, targetFields: { [field]: this.state.getLatestValue().data[field] }, @@ -133,8 +132,9 @@ export class PollComposer { compose = async () => { const { data, errors } = this.state.getLatestValue(); - const result = await this.compositionMiddlewareExecutor.execute('compose', { - state: { + const result = await this.compositionMiddlewareExecutor.execute({ + eventName: 'compose', + initialValue: { data: { ...data, max_votes_allowed: data.max_votes_allowed diff --git a/src/messageComposer/textComposer.ts b/src/messageComposer/textComposer.ts index fc6e92950a..b65e2c42d3 100644 --- a/src/messageComposer/textComposer.ts +++ b/src/messageComposer/textComposer.ts @@ -1,12 +1,10 @@ import { TextComposerMiddlewareExecutor } from './middleware'; import { StateStore } from '../store'; import { logChatPromiseExecution } from '../utils'; -import type { - Suggestions, - TextComposerState, - TextComposerSuggestion, - TextSelection, -} from './types'; +import type { TextComposerSuggestion } from './middleware/textComposer/types'; +import type { TextSelection } from './middleware/textComposer/types'; +import type { TextComposerState } from './middleware/textComposer/types'; +import type { Suggestions } from './middleware/textComposer/types'; import type { MessageComposer } from './messageComposer'; import type { DraftMessage, LocalMessage, UserResponse } from '../types'; @@ -259,8 +257,9 @@ export class TextComposer { text: string; }) => { if (!this.enabled) return; - const output = await this.middlewareExecutor.execute('onChange', { - state: { + const output = await this.middlewareExecutor.execute({ + eventName: 'onChange', + initialValue: { ...this.state.getLatestValue(), text, selection, @@ -280,13 +279,15 @@ export class TextComposer { // todo: document how to register own middleware handler to simulate onSelectUser prop handleSelect = async (target: TextComposerSuggestion) => { if (!this.enabled) return; - const output = await this.middlewareExecutor.execute( - 'onSuggestionItemSelect', - { - state: this.state.getLatestValue(), + const output = await this.middlewareExecutor.execute({ + eventName: 'onSuggestionItemSelect', + initialValue: { + ...this.state.getLatestValue(), + change: { + selectedSuggestion: target, + }, }, - target, - ); + }); if (output?.status === 'discard') return; this.state.next(output.state); }; diff --git a/src/messageComposer/types.custom.ts b/src/messageComposer/types.custom.ts new file mode 100644 index 0000000000..0e0f91c8e0 --- /dev/null +++ b/src/messageComposer/types.custom.ts @@ -0,0 +1 @@ +export interface CustomTextComposerSuggestion {} diff --git a/src/messageComposer/types.ts b/src/messageComposer/types.ts index e1dc1b9b06..ce5b50cea3 100644 --- a/src/messageComposer/types.ts +++ b/src/messageComposer/types.ts @@ -1,5 +1,4 @@ -import type { Attachment, FileUploadConfig, UserResponse } from '../types'; -import type { SearchSource } from '../search_controller'; +import type { Attachment, FileUploadConfig } from '../types'; export type LocalAttachment = AnyLocalAttachment | LocalUploadAttachment; @@ -142,23 +141,3 @@ export type FileReference = Pick & { // This is specially needed for video in camera roll thumb_url?: string; }; - -type Id = string; -export type MentionedUserMap = Map; -export type TextSelection = { end: number; start: number }; -export type TextComposerSuggestion = T & { - id: string; -}; - -export type Suggestions = { - query: string; - searchSource: SearchSource; // we do not want to limit the use of SearchSources - trigger: string; -}; - -export type TextComposerState = { - mentionedUsers: UserResponse[]; - selection: TextSelection; - text: string; - suggestions?: Suggestions; -}; diff --git a/src/middleware.ts b/src/middleware.ts index f65b3be38c..5eda8b4308 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -13,39 +13,52 @@ export type InsertPosition = export type MiddlewareStatus = 'complete' | 'discard'; -export type MiddlewareValue = { - state: TState; +export type MiddlewareExecutionResult = { + state: TValue; status?: MiddlewareStatus; }; -export type MiddlewareHandlerParams = { - input: MiddlewareValue; - nextHandler: (input: MiddlewareValue) => Promise>; +export type ExecuteParams = { + eventName: string; + initialValue: TValue; }; -export type MiddlewareHandler = ( - params: MiddlewareHandlerParams, -) => Promise>; -export type Middleware = { +export type MiddlewareHandlerParams = { + state: TValue; + next: (state: TValue) => Promise>; + complete: (state: TValue) => Promise>; + discard: () => Promise>; + forward: () => Promise>; +}; + +export type MiddlewareHandler = ( + params: MiddlewareHandlerParams, +) => Promise>; + +export type MiddlewareHandlers = { + [K in THandlers]: MiddlewareHandler; +}; + +export type Middleware = { id: string; - [key: string]: string | MiddlewareHandler; + handlers: MiddlewareHandlers; }; -export class MiddlewareExecutor { - private id: string; - private middleware: Middleware[] = []; +export class MiddlewareExecutor { + readonly id: string; + private middleware: Middleware[] = []; constructor() { this.id = generateUUIDv4(); } - use(middleware: Middleware | Middleware[]) { + use(middleware: Middleware | Middleware[]) { this.middleware = this.middleware.concat(middleware); return this; } // todo: document how to re-arrange the order of middleware using replace - replace(middleware: Middleware[]) { + replace(middleware: Middleware[]) { const newMiddleware = [...this.middleware]; middleware.forEach((upserted) => { const existingIndex = this.middleware.findIndex( @@ -66,7 +79,7 @@ export class MiddlewareExecutor { position, unique, }: { - middleware: Middleware[]; + middleware: Middleware[]; position: InsertPosition; unique?: boolean; }) { @@ -88,20 +101,20 @@ export class MiddlewareExecutor { setOrder(order: string[]) { this.middleware = order .map((id) => this.middleware.find((middleware) => middleware.id === id)) - .filter(Boolean) as Middleware[]; + .filter(Boolean) as Middleware[]; } - protected async executeMiddlewareChain( - eventName: string, - initialInput: MiddlewareValue, - extraParams: Record = {}, - ): Promise> { + protected async executeMiddlewareChain({ + eventName, + initialValue, + }: ExecuteParams): Promise> { let index = -1; const execute = async ( i: number, - input: MiddlewareValue, - ): Promise> => { + state: TValue, + status?: MiddlewareStatus, + ): Promise> => { if (i <= index) { throw new Error('next() called multiple times'); } @@ -110,27 +123,35 @@ export class MiddlewareExecutor { const returnFromChain = i === this.middleware.length || - (input.status && ['complete', 'discard'].includes(input.status)); - if (returnFromChain) return input; + (status && ['complete', 'discard'].includes(status)); + if (returnFromChain) return { state, status }; const middleware = this.middleware[i]; - const handler = middleware[eventName]; + const handler = middleware.handlers[eventName as THandlers]; - if (!handler || typeof handler === 'string') { - return execute(i + 1, input); + if (!handler) { + return execute(i + 1, state, status); } + const next = (adjustedState: TValue) => execute(i + 1, adjustedState); + const complete = (adjustedState: TValue) => + execute(i + 1, adjustedState, 'complete'); + const discard = () => execute(i + 1, state, 'discard'); + const forward = () => execute(i + 1, state); + return await handler({ - input, - nextHandler: (nextInput: MiddlewareValue) => execute(i + 1, nextInput), - ...extraParams, + state, + next, + complete, + discard, + forward, }); }; const result = await withCancellation( `middleware-execution-${this.id}-${eventName}`, async (abortSignal) => { - const result = await execute(0, initialInput); + const result = await execute(0, initialValue); if (abortSignal.aborted) { return 'canceled'; } @@ -138,13 +159,16 @@ export class MiddlewareExecutor { }, ); - return result === 'canceled' ? { ...initialInput, status: 'discard' } : result; + return result === 'canceled' ? { state: initialValue, status: 'discard' } : result; } - async execute( - eventName: string, - initialInput: MiddlewareValue, - ): Promise> { - return await this.executeMiddlewareChain(eventName, initialInput); + async execute({ + eventName, + initialValue: initialState, + }: ExecuteParams): Promise> { + return await this.executeMiddlewareChain({ + eventName, + initialValue: initialState, + }); } } diff --git a/test/unit/MessageComposer/messageComposer.test.ts b/test/unit/MessageComposer/messageComposer.test.ts index bf1419532b..78be208450 100644 --- a/test/unit/MessageComposer/messageComposer.test.ts +++ b/test/unit/MessageComposer/messageComposer.test.ts @@ -540,7 +540,10 @@ describe('MessageComposer', () => { const result = await messageComposer.compose(); - expect(spyExecute).toHaveBeenCalledWith('compose', expect.any(Object)); + expect(spyExecute).toHaveBeenCalledWith({ + eventName: 'compose', + initialValue: expect.any(Object), + }); expect(result).toEqual(mockResult.state); }); @@ -559,7 +562,10 @@ describe('MessageComposer', () => { const result = await messageComposer.compose(); - expect(spyExecute).toHaveBeenCalledWith('compose', expect.any(Object)); + expect(spyExecute).toHaveBeenCalledWith({ + eventName: 'compose', + initialValue: expect.any(Object), + }); expect(result).toBeUndefined(); }); @@ -583,7 +589,10 @@ describe('MessageComposer', () => { const result = await messageComposer.composeDraft(); - expect(spyExecute).toHaveBeenCalledWith('compose', expect.any(Object)); + expect(spyExecute).toHaveBeenCalledWith({ + eventName: 'compose', + initialValue: expect.any(Object), + }); expect(result).toEqual(mockResult.state); }); @@ -602,7 +611,10 @@ describe('MessageComposer', () => { const result = await messageComposer.composeDraft(); - expect(spyExecute).toHaveBeenCalledWith('compose', expect.any(Object)); + expect(spyExecute).toHaveBeenCalledWith({ + eventName: 'compose', + initialValue: expect.any(Object), + }); expect(result).toBeUndefined(); }); diff --git a/test/unit/MessageComposer/middleware/messageComposer/attachments.test.ts b/test/unit/MessageComposer/middleware/messageComposer/attachments.test.ts index afd511e0b7..8b383a9563 100644 --- a/test/unit/MessageComposer/middleware/messageComposer/attachments.test.ts +++ b/test/unit/MessageComposer/middleware/messageComposer/attachments.test.ts @@ -8,6 +8,35 @@ import { LocalImageAttachment, } from '../../../../../src/messageComposer/types'; import { createDraftAttachmentsCompositionMiddleware } from '../../../../../src/messageComposer/middleware/messageComposer/attachments'; +import { MessageDraftComposerMiddlewareValueState } from '../../../../../src/messageComposer/middleware/messageComposer/types'; +import { MessageComposerMiddlewareState } from '../../../../../src/messageComposer/middleware/messageComposer/types'; +import { MiddlewareStatus } from '../../../../../src/middleware'; + +const setup = (initialState: MessageComposerMiddlewareState) => { + return { + state: initialState, + next: async (state: MessageComposerMiddlewareState) => ({ state }), + complete: async (state: MessageComposerMiddlewareState) => ({ + state, + status: 'complete' as MiddlewareStatus, + }), + discard: async () => ({ state: initialState, status: 'discard' as MiddlewareStatus }), + forward: async () => ({ state: initialState }), + }; +}; + +const setupDraft = (initialState: MessageDraftComposerMiddlewareValueState) => { + return { + state: initialState, + next: async (state: MessageDraftComposerMiddlewareValueState) => ({ state }), + complete: async (state: MessageDraftComposerMiddlewareValueState) => ({ + state, + status: 'complete' as MiddlewareStatus, + }), + discard: async () => ({ state: initialState, status: 'discard' as MiddlewareStatus }), + forward: async () => ({ state: initialState }), + }; +}; describe('AttachmentsMiddleware', () => { let channel: Channel; @@ -104,34 +133,31 @@ describe('AttachmentsMiddleware', () => { }); it('should handle message without attachments', async () => { - const result = await attachmentsMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - type: 'regular', - }, - localMessage: { - attachments: [], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: '', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await attachmentsMiddleware.handlers.compose( + setup({ + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.status).toBeUndefined; expect(result.state.message.attachments ?? []).toHaveLength(0); @@ -155,34 +181,31 @@ describe('AttachmentsMiddleware', () => { 'get', ).mockReturnValue([attachment]); - const result = await attachmentsMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - type: 'regular', - }, - localMessage: { - attachments: [attachment], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: '', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await attachmentsMiddleware.handlers.compose( + setup({ + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [attachment], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.status).toBeUndefined; expect(result.state.message.attachments ?? []).toHaveLength(1); @@ -219,34 +242,31 @@ describe('AttachmentsMiddleware', () => { 'get', ).mockReturnValue(attachments); - const result = await attachmentsMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - type: 'regular', - }, - localMessage: { - attachments, - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: '', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await attachmentsMiddleware.handlers.compose( + setup({ + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments, + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.status).toBeUndefined; expect(result.state.message.attachments ?? []).toHaveLength(2); @@ -277,34 +297,31 @@ describe('AttachmentsMiddleware', () => { 'get', ).mockReturnValue([]); - const result = await attachmentsMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - type: 'regular', - }, - localMessage: { - attachments: [attachment], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: '', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await attachmentsMiddleware.handlers.compose( + setup({ + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [attachment], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.status).toBeUndefined; expect(result.state.message.attachments ?? []).toHaveLength(0); @@ -328,34 +345,31 @@ describe('AttachmentsMiddleware', () => { 'get', ).mockReturnValue([]); - const result = await attachmentsMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - type: 'regular', - }, - localMessage: { - attachments: [attachment], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: '', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await attachmentsMiddleware.handlers.compose( + setup({ + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [attachment], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.status).toBeUndefined; expect(result.state.message.attachments ?? []).toHaveLength(0); @@ -406,16 +420,13 @@ describe('DraftAttachmentsMiddleware', () => { }); it('should handle draft without attachments', async () => { - const result = await draftAttachmentsMiddleware.compose({ - input: { - state: { - draft: { - text: '', - }, + const result = await draftAttachmentsMiddleware.handlers.compose( + setupDraft({ + draft: { + text: '', }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined(); expect(result.state.draft.attachments).toBeUndefined(); @@ -438,17 +449,14 @@ describe('DraftAttachmentsMiddleware', () => { 'get', ).mockReturnValue([attachment]); - const result = await draftAttachmentsMiddleware.compose({ - input: { - state: { - draft: { - text: '', - attachments: [], - }, + const result = await draftAttachmentsMiddleware.handlers.compose( + setupDraft({ + draft: { + text: '', + attachments: [], }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined(); expect(result.state.draft.attachments).toHaveLength(1); @@ -481,17 +489,14 @@ describe('DraftAttachmentsMiddleware', () => { 'get', ).mockReturnValue([newAttachment]); - const result = await draftAttachmentsMiddleware.compose({ - input: { - state: { - draft: { - text: '', - attachments: [existingAttachment], - }, + const result = await draftAttachmentsMiddleware.handlers.compose( + setupDraft({ + draft: { + text: '', + attachments: [existingAttachment], }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined(); expect(result.state.draft.attachments).toHaveLength(2); @@ -505,16 +510,13 @@ describe('DraftAttachmentsMiddleware', () => { draftAttachmentsMiddleware = createDraftAttachmentsCompositionMiddleware(messageComposer); - const result = await draftAttachmentsMiddleware.compose({ - input: { - state: { - draft: { - text: '', - }, + const result = await draftAttachmentsMiddleware.handlers.compose( + setupDraft({ + draft: { + text: '', }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined(); expect(result.state.draft.attachments).toBeUndefined(); diff --git a/test/unit/MessageComposer/middleware/messageComposer/compositionValidation.test.ts b/test/unit/MessageComposer/middleware/messageComposer/compositionValidation.test.ts index 217c920d40..601c0ae2ff 100644 --- a/test/unit/MessageComposer/middleware/messageComposer/compositionValidation.test.ts +++ b/test/unit/MessageComposer/middleware/messageComposer/compositionValidation.test.ts @@ -1,19 +1,44 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { createCompositionValidationMiddleware } from '../../../../../src/messageComposer/middleware/messageComposer/compositionValidation'; -import { MessageComposer } from '../../../../../src/messageComposer/messageComposer'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { Channel } from '../../../../../src/channel'; import { StreamChat } from '../../../../../src/client'; +import { MessageComposer } from '../../../../../src/messageComposer/messageComposer'; +import { + createCompositionValidationMiddleware, + createDraftCompositionValidationMiddleware, +} from '../../../../../src/messageComposer/middleware/messageComposer/compositionValidation'; import { - LocalImageAttachment, AttachmentLoadingState, + LocalImageAttachment, } from '../../../../../src/messageComposer/types'; -import { - LinkPreview, - LinkPreviewStatus, - LinkPreviewMap, -} from '../../../../../src/messageComposer/linkPreviewsManager'; -import { MessageComposerMiddlewareValue } from '../../../../../src/messageComposer/middleware/messageComposer/types'; -import { createDraftCompositionValidationMiddleware } from '../../../../../src/messageComposer/middleware/messageComposer/compositionValidation'; +import { MiddlewareStatus } from '../../../../../src/middleware'; +import { MessageComposerMiddlewareState } from '../../../../../src/messageComposer/middleware/messageComposer/types'; +import { MessageDraftComposerMiddlewareValueState } from '../../../../../src/messageComposer/middleware/messageComposer/types'; + +const setup = (initialState: MessageComposerMiddlewareState) => { + return { + state: initialState, + next: async (state: MessageComposerMiddlewareState) => ({ state }), + complete: async (state: MessageComposerMiddlewareState) => ({ + state, + status: 'complete' as MiddlewareStatus, + }), + discard: async () => ({ state: initialState, status: 'discard' as MiddlewareStatus }), + forward: async () => ({ state: initialState }), + }; +}; + +const setupDraft = (initialState: MessageDraftComposerMiddlewareValueState) => { + return { + state: initialState, + next: async (state: MessageDraftComposerMiddlewareValueState) => ({ state }), + complete: async (state: MessageDraftComposerMiddlewareValueState) => ({ + state, + status: 'complete' as MiddlewareStatus, + }), + discard: async () => ({ state: initialState, status: 'discard' as MiddlewareStatus }), + forward: async () => ({ state: initialState }), + }; +}; describe('MessageComposerValidationMiddleware', () => { let channel: Channel; @@ -107,34 +132,31 @@ describe('MessageComposerValidationMiddleware', () => { }); it('should validate empty message', async () => { - const result = await validationMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - type: 'regular', - }, - localMessage: { - attachments: [], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: '', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await validationMiddleware.handlers.compose( + setup({ + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.status).toBe('discard'); }); @@ -142,35 +164,32 @@ describe('MessageComposerValidationMiddleware', () => { it('should validate message with text', async () => { vi.spyOn(messageComposer.textComposer, 'text', 'get').mockReturnValue('Hello world'); - const result = await validationMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - text: 'Hello world', - type: 'regular', - }, - localMessage: { - attachments: [], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: 'Hello world', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await validationMiddleware.handlers.compose( + setup({ + message: { + id: 'test-id', + parent_id: undefined, + text: 'Hello world', + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: 'Hello world', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.status).toBeUndefined; }); @@ -192,35 +211,32 @@ describe('MessageComposerValidationMiddleware', () => { 'get', ).mockReturnValue([attachment]); - const result = await validationMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - attachments: [attachment], - parent_id: undefined, - type: 'regular', - }, - localMessage: { - attachments: [attachment], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: '', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await validationMiddleware.handlers.compose( + setup({ + message: { + id: 'test-id', + attachments: [attachment], + parent_id: undefined, + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [attachment], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.status).toBeUndefined; }); @@ -231,9 +247,9 @@ describe('MessageComposerValidationMiddleware', () => { { id: 'user1', name: 'User One' }, ]); - const result = await validationMiddleware.compose({ - input: { - state: { + const result = await validationMiddleware.handlers.compose( + setup({ + message: { message: { id: 'test-id', mentioned_users: ['user1'], @@ -258,17 +274,16 @@ describe('MessageComposerValidationMiddleware', () => { }, sendOptions: {}, }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined; }); it('should validate message with poll', async () => { - const result = await validationMiddleware.compose({ - input: { - state: { + const result = await validationMiddleware.handlers.compose( + setup({ + message: { message: { id: 'test-id', parent_id: undefined, @@ -293,9 +308,8 @@ describe('MessageComposerValidationMiddleware', () => { }, sendOptions: {}, }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined; }); @@ -303,35 +317,32 @@ describe('MessageComposerValidationMiddleware', () => { it('should validate message with last origin change', async () => { vi.spyOn(messageComposer, 'lastChangeOriginIsLocal', 'get').mockReturnValue(false); - const result = await validationMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - text: 'Hello world', - type: 'regular', - }, - localMessage: { - attachments: [], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: 'Hello world', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await validationMiddleware.handlers.compose( + setup({ + message: { + id: 'test-id', + parent_id: undefined, + text: 'Hello world', + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: 'Hello world', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.status).toBe('discard'); }); @@ -429,84 +440,69 @@ describe('DraftCompositionValidationMiddleware', () => { }); it('should discard empty draft', async () => { - const result = await validationMiddleware.compose({ - input: { - state: { - draft: { - text: '', - }, + const result = await validationMiddleware.handlers.compose( + setupDraft({ + draft: { + text: '', }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBe('discard'); }); it('should validate draft with text', async () => { - const result = await validationMiddleware.compose({ - input: { - state: { - draft: { - text: 'Hello world', - }, + const result = await validationMiddleware.handlers.compose( + setupDraft({ + draft: { + text: 'Hello world', }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined(); }); it('should validate draft with attachments', async () => { - const result = await validationMiddleware.compose({ - input: { - state: { - draft: { - text: '', - attachments: [ - { - type: 'image', - image_url: 'https://example.com/image.jpg', - }, - ], - }, + const result = await validationMiddleware.handlers.compose( + setupDraft({ + draft: { + text: '', + attachments: [ + { + type: 'image', + image_url: 'https://example.com/image.jpg', + }, + ], }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined(); }); it('should validate draft with poll', async () => { - const result = await validationMiddleware.compose({ - input: { - state: { - draft: { - text: '', - poll_id: 'poll-123', - }, + const result = await validationMiddleware.handlers.compose( + setupDraft({ + draft: { + text: '', + poll_id: 'poll-123', }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined(); }); it('should validate draft with quoted message', async () => { - const result = await validationMiddleware.compose({ - input: { - state: { - draft: { - text: '', - quoted_message_id: 'msg-123', - }, + const result = await validationMiddleware.handlers.compose( + setupDraft({ + draft: { + text: '', + quoted_message_id: 'msg-123', }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined(); }); @@ -514,16 +510,13 @@ describe('DraftCompositionValidationMiddleware', () => { it('should discard draft when last change origin is not local', async () => { vi.spyOn(messageComposer, 'lastChangeOriginIsLocal', 'get').mockReturnValue(false); - const result = await validationMiddleware.compose({ - input: { - state: { - draft: { - text: 'Hello world', - }, + const result = await validationMiddleware.handlers.compose( + setupDraft({ + draft: { + text: 'Hello world', }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBe('discard'); }); diff --git a/test/unit/MessageComposer/middleware/messageComposer/customData.test.ts b/test/unit/MessageComposer/middleware/messageComposer/customData.test.ts index 53497141d1..e415c0dae5 100644 --- a/test/unit/MessageComposer/middleware/messageComposer/customData.test.ts +++ b/test/unit/MessageComposer/middleware/messageComposer/customData.test.ts @@ -7,10 +7,36 @@ import { createDraftCustomDataCompositionMiddleware, } from '../../../../../src/messageComposer/middleware/messageComposer/customData'; import type { - MessageComposerMiddlewareValueState, + MessageComposerMiddlewareState, MessageDraftComposerMiddlewareValueState, } from '../../../../../src/messageComposer/middleware/messageComposer/types'; +const setup = (initialState: MessageComposerMiddlewareState) => { + return { + state: initialState, + next: async (state: MessageComposerMiddlewareState) => ({ state }), + complete: async (state: MessageComposerMiddlewareState) => ({ + state, + status: 'complete' as MiddlewareStatus, + }), + discard: async () => ({ state: initialState, status: 'discard' as MiddlewareStatus }), + forward: async () => ({ state: initialState }), + }; +}; + +const setupDraft = (initialState: MessageDraftComposerMiddlewareValueState) => { + return { + state: initialState, + next: async (state: MessageDraftComposerMiddlewareValueState) => ({ state }), + complete: async (state: MessageDraftComposerMiddlewareValueState) => ({ + state, + status: 'complete' as MiddlewareStatus, + }), + discard: async () => ({ state: initialState, status: 'discard' as MiddlewareStatus }), + forward: async () => ({ state: initialState }), + }; +}; + describe('Custom Data Middleware', () => { let channel: Channel; let client: StreamChat; @@ -31,7 +57,7 @@ describe('Custom Data Middleware', () => { const data = { key: 'value' }; composer.customDataManager.setMessageData(data); const middleware = createCustomDataCompositionMiddleware(composer); - const state: MessageComposerMiddlewareValueState = { + const state: MessageComposerMiddlewareState = { message: { id: '1', type: 'regular' }, localMessage: { id: '1', @@ -49,10 +75,7 @@ describe('Custom Data Middleware', () => { sendOptions: {}, }; - const result = await middleware.compose({ - input: { state }, - nextHandler: async (input) => input, - }); + const result = await middleware.handlers.compose(setup(state)); expect(result.state.message).toEqual(expect.objectContaining(data)); expect(result.state.localMessage).toEqual(expect.objectContaining(data)); @@ -60,7 +83,7 @@ describe('Custom Data Middleware', () => { it('should add empty custom data if no data is set', async () => { const middleware = createCustomDataCompositionMiddleware(composer); - const state: MessageComposerMiddlewareValueState = { + const state: MessageComposerMiddlewareState = { message: { id: '1', type: 'regular' }, localMessage: { id: '1', @@ -78,10 +101,7 @@ describe('Custom Data Middleware', () => { sendOptions: {}, }; - const result = await middleware.compose({ - input: { state }, - nextHandler: async (input) => input, - }); + const result = await middleware.handlers.compose(setup(state)); expect(result.state.message).toEqual(state.message); expect(result.state.localMessage).toEqual(state.localMessage); @@ -101,10 +121,7 @@ describe('Custom Data Middleware', () => { }, }; - const result = await middleware.compose({ - input: { state }, - nextHandler: async (input) => input, - }); + const result = await middleware.handlers.compose(setupDraft(state)); expect(result.state.draft).toEqual(expect.objectContaining(data)); }); @@ -115,10 +132,7 @@ describe('Custom Data Middleware', () => { draft: { id: '1', text: '', type: 'regular' }, }; - const result = await middleware.compose({ - input: { state }, - nextHandler: async (input) => input, - }); + const result = await middleware.handlers.compose(setupDraft(state)); expect(result.state.draft).toEqual(state.draft); }); diff --git a/test/unit/MessageComposer/middleware/messageComposer/linkPreviews.test.ts b/test/unit/MessageComposer/middleware/messageComposer/linkPreviews.test.ts index 7cc05c4a58..25f6dd81a8 100644 --- a/test/unit/MessageComposer/middleware/messageComposer/linkPreviews.test.ts +++ b/test/unit/MessageComposer/middleware/messageComposer/linkPreviews.test.ts @@ -14,6 +14,9 @@ import { DraftResponse, LinkPreviewsManagerConfig, LocalMessage, + MessageComposerMiddlewareState, + MessageDraftComposerMiddlewareValueState, + MiddlewareStatus, } from '../../../../../src'; const enrichURLReturnValue = { @@ -30,6 +33,34 @@ const enrichURLReturnValue = { duration: '100', }; +const setupHandlerParams = (initialState: MessageComposerMiddlewareState) => { + return { + state: initialState, + next: async (state: MessageComposerMiddlewareState) => ({ state }), + complete: async (state: MessageComposerMiddlewareState) => ({ + state, + status: 'complete' as MiddlewareStatus, + }), + discard: async () => ({ state: initialState, status: 'discard' as MiddlewareStatus }), + forward: async () => ({ state: initialState }), + }; +}; + +const setupHandlerParamsDraft = ( + initialState: MessageDraftComposerMiddlewareValueState, +) => { + return { + state: initialState, + next: async (state: MessageDraftComposerMiddlewareValueState) => ({ state }), + complete: async (state: MessageDraftComposerMiddlewareValueState) => ({ + state, + status: 'complete' as MiddlewareStatus, + }), + discard: async () => ({ state: initialState, status: 'discard' as MiddlewareStatus }), + forward: async () => ({ state: initialState }), + }; +}; + const setup = ({ composition, config, @@ -62,34 +93,31 @@ const setup = ({ describe('LinkPreviewsMiddleware', () => { it('should keep message attachments empty if not link previews are available', async () => { const { linkPreviewsMiddleware } = setup(); - const result = await linkPreviewsMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - type: 'regular', - }, - localMessage: { - attachments: [], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: '', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await linkPreviewsMiddleware.handlers.compose( + setupHandlerParams({ + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.status).toBeUndefined; expect(result.state.message.attachments ?? []).toHaveLength(0); @@ -119,34 +147,31 @@ describe('LinkPreviewsMiddleware', () => { ]), }); - const result = await linkPreviewsMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - type: 'regular', - }, - localMessage: { - attachments: [], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: 'https://example.com', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await linkPreviewsMiddleware.handlers.compose( + setupHandlerParams({ + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: 'https://example.com', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.status).toBeUndefined; expect(result.state.message.attachments ?? []).toHaveLength(1); @@ -179,34 +204,31 @@ describe('LinkPreviewsMiddleware', () => { ]), }); - const result = await linkPreviewsMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - type: 'regular', - }, - localMessage: { - attachments: [], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: 'https://example.com', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await linkPreviewsMiddleware.handlers.compose( + setupHandlerParams({ + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: 'https://example.com', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.status).toBeUndefined; expect(result.state.message.attachments ?? []).toHaveLength(0); @@ -237,34 +259,31 @@ describe('LinkPreviewsMiddleware', () => { }); // Set up the previews in the manager - const result = await linkPreviewsMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - type: 'regular', - }, - localMessage: { - attachments: [], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: 'https://example.com', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await linkPreviewsMiddleware.handlers.compose( + setupHandlerParams({ + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: 'https://example.com', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.status).toBeUndefined; expect(result.state.message.attachments ?? []).toHaveLength(0); @@ -294,34 +313,31 @@ describe('LinkPreviewsMiddleware', () => { ]), }); - const result = await linkPreviewsMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - type: 'regular', - }, - localMessage: { - attachments: [], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: 'https://example.com', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await linkPreviewsMiddleware.handlers.compose( + setupHandlerParams({ + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: 'https://example.com', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.status).toBeUndefined; expect(result.state.message.attachments ?? []).toHaveLength(0); @@ -383,34 +399,31 @@ describe('LinkPreviewsMiddleware', () => { ]), }); - const result = await linkPreviewsMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - type: 'regular', - }, - localMessage: { - attachments: [], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: 'https://example1.com https://example2.com https://example3.com', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await linkPreviewsMiddleware.handlers.compose( + setupHandlerParams({ + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: 'https://example1.com https://example2.com https://example3.com', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.status).toBeUndefined; expect(result.state.message.attachments ?? []).toHaveLength(2); @@ -459,34 +472,31 @@ describe('LinkPreviewsMiddleware', () => { ]), }); - const result = await linkPreviewsMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - type: 'regular', - }, - localMessage: { - attachments: [], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: 'https://example1.com https://example2.com', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await linkPreviewsMiddleware.handlers.compose( + setupHandlerParams({ + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: 'https://example1.com https://example2.com', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.status).toBeUndefined; expect(result.state.message.attachments ?? []).toHaveLength(0); // will be added server-side @@ -517,45 +527,42 @@ describe('LinkPreviewsMiddleware', () => { ]), }); - const result = await linkPreviewsMiddleware.compose({ - input: { - state: { - message: { - attachments: [ - { - type: 'image', - image_url: 'https://example.com/image.jpg', - }, - ], - id: 'test-id', - parent_id: undefined, - type: 'regular', - }, - localMessage: { - attachments: [ - { - type: 'image', - image_url: 'https://example.com/image.jpg', - }, - ], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: 'https://example.com', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await linkPreviewsMiddleware.handlers.compose( + setupHandlerParams({ + message: { + attachments: [ + { + type: 'image', + image_url: 'https://example.com/image.jpg', + }, + ], + id: 'test-id', + parent_id: undefined, + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [ + { + type: 'image', + image_url: 'https://example.com/image.jpg', + }, + ], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: 'https://example.com', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.status).toBeUndefined; expect(result.state.message.attachments ?? []).toHaveLength(2); @@ -599,16 +606,13 @@ const setupForDraft = ({ describe('DraftLinkPreviewsMiddleware', () => { it('should handle draft without link previews', async () => { const { linkPreviewsMiddleware } = setupForDraft(); - const result = await linkPreviewsMiddleware.compose({ - input: { - state: { - draft: { - text: '', - }, + const result = await linkPreviewsMiddleware.handlers.compose( + setupHandlerParamsDraft({ + draft: { + text: '', }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined(); expect(result.state.draft.attachments).toBeUndefined(); @@ -636,16 +640,13 @@ describe('DraftLinkPreviewsMiddleware', () => { 'get', ).mockReturnValue([linkPreview]); - const result = await linkPreviewsMiddleware.compose({ - input: { - state: { - draft: { - text: '', - }, + const result = await linkPreviewsMiddleware.handlers.compose( + setupHandlerParamsDraft({ + draft: { + text: '', }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined(); expect(result.state.draft.attachments).toHaveLength(1); @@ -681,17 +682,14 @@ describe('DraftLinkPreviewsMiddleware', () => { 'get', ).mockReturnValue([linkPreview]); - const result = await linkPreviewsMiddleware.compose({ - input: { - state: { - draft: { - text: '', - attachments: [existingAttachment], - }, + const result = await linkPreviewsMiddleware.handlers.compose( + setupHandlerParamsDraft({ + draft: { + text: '', + attachments: [existingAttachment], }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined(); expect(result.state.draft.attachments).toHaveLength(2); @@ -706,16 +704,13 @@ describe('DraftLinkPreviewsMiddleware', () => { const linkPreviewsMiddlewareWithUndefinedManager = createDraftLinkPreviewsCompositionMiddleware(messageComposer); - const result = await linkPreviewsMiddlewareWithUndefinedManager.compose({ - input: { - state: { - draft: { - text: '', - }, + const result = await linkPreviewsMiddlewareWithUndefinedManager.handlers.compose( + setupHandlerParamsDraft({ + draft: { + text: '', }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined(); expect(result.state.draft.attachments).toBeUndefined(); @@ -726,16 +721,13 @@ describe('DraftLinkPreviewsMiddleware', () => { const cancelURLEnrichment = vi.fn(); messageComposer.linkPreviewsManager.cancelURLEnrichment = cancelURLEnrichment; - await linkPreviewsMiddleware.compose({ - input: { - state: { - draft: { - text: '', - }, + await linkPreviewsMiddleware.handlers.compose( + setupHandlerParamsDraft({ + draft: { + text: '', }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(cancelURLEnrichment).toHaveBeenCalled(); }); diff --git a/test/unit/MessageComposer/middleware/messageComposer/messageComposerState.test.ts b/test/unit/MessageComposer/middleware/messageComposer/messageComposerState.test.ts index 924e9bcddf..c715a22d42 100644 --- a/test/unit/MessageComposer/middleware/messageComposer/messageComposerState.test.ts +++ b/test/unit/MessageComposer/middleware/messageComposer/messageComposerState.test.ts @@ -5,6 +5,37 @@ import { Channel } from '../../../../../src/channel'; import { StreamChat } from '../../../../../src/client'; import { LocalMessage } from '../../../../../src/types'; import { createDraftMessageComposerStateCompositionMiddleware } from '../../../../../src/messageComposer/middleware/messageComposer/messageComposerState'; +import { MessageComposerMiddlewareState } from '../../../../../src/messageComposer/middleware/messageComposer/types'; +import { MiddlewareStatus } from '../../../../../src/middleware'; +import { MessageDraftComposerMiddlewareValueState } from '../../../../../src/messageComposer/middleware/messageComposer/types'; + +const setupHandlerParams = (initialState: MessageComposerMiddlewareState) => { + return { + state: initialState, + next: async (state: MessageComposerMiddlewareState) => ({ state }), + complete: async (state: MessageComposerMiddlewareState) => ({ + state, + status: 'complete' as MiddlewareStatus, + }), + discard: async () => ({ state: initialState, status: 'discard' as MiddlewareStatus }), + forward: async () => ({ state: initialState }), + }; +}; + +const setupHandlerParamsDraft = ( + initialState: MessageDraftComposerMiddlewareValueState, +) => { + return { + state: initialState, + next: async (state: MessageDraftComposerMiddlewareValueState) => ({ state }), + complete: async (state: MessageDraftComposerMiddlewareValueState) => ({ + state, + status: 'complete' as MiddlewareStatus, + }), + discard: async () => ({ state: initialState, status: 'discard' as MiddlewareStatus }), + forward: async () => ({ state: initialState }), + }; +}; describe('MessageComposerStateMiddleware', () => { let channel: Channel; @@ -38,34 +69,31 @@ describe('MessageComposerStateMiddleware', () => { vi.spyOn(messageComposer, 'quotedMessage', 'get').mockReturnValue(null); vi.spyOn(messageComposer, 'pollId', 'get').mockReturnValue(null); - const result = await messageComposerStateMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - type: 'regular', - }, - localMessage: { - attachments: [], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: '', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await messageComposerStateMiddleware.handlers.compose( + setupHandlerParams({ + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.state.message.quoted_message_id).toBeUndefined(); expect(result.state.message.poll_id).toBeUndefined(); @@ -96,34 +124,31 @@ describe('MessageComposerStateMiddleware', () => { vi.spyOn(messageComposer, 'quotedMessage', 'get').mockReturnValue(quotedMessage); vi.spyOn(messageComposer, 'pollId', 'get').mockReturnValue(null); - const result = await messageComposerStateMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - type: 'regular', - }, - localMessage: { - attachments: [], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: '', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await messageComposerStateMiddleware.handlers.compose( + setupHandlerParams({ + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.state.message.quoted_message_id).toBe('quoted-message-id'); expect(result.state.localMessage.quoted_message_id).toBe('quoted-message-id'); @@ -135,34 +160,31 @@ describe('MessageComposerStateMiddleware', () => { vi.spyOn(messageComposer, 'quotedMessage', 'get').mockReturnValue(null); vi.spyOn(messageComposer, 'pollId', 'get').mockReturnValue('poll-id-123'); - const result = await messageComposerStateMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - type: 'regular', - }, - localMessage: { - attachments: [], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: '', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await messageComposerStateMiddleware.handlers.compose( + setupHandlerParams({ + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.state.message.poll_id).toBe('poll-id-123'); expect(result.state.localMessage.poll_id).toBe('poll-id-123'); @@ -190,34 +212,31 @@ describe('MessageComposerStateMiddleware', () => { vi.spyOn(messageComposer, 'quotedMessage', 'get').mockReturnValue(quotedMessage); vi.spyOn(messageComposer, 'pollId', 'get').mockReturnValue('poll-id-123'); - const result = await messageComposerStateMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - type: 'regular', - }, - localMessage: { - attachments: [], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: '', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await messageComposerStateMiddleware.handlers.compose( + setupHandlerParams({ + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.state.message.quoted_message_id).toBe('quoted-message-id'); expect(result.state.message.poll_id).toBe('poll-id-123'); @@ -248,35 +267,32 @@ describe('MessageComposerStateMiddleware', () => { vi.spyOn(messageComposer, 'quotedMessage', 'get').mockReturnValue(quotedMessage); vi.spyOn(messageComposer, 'pollId', 'get').mockReturnValue('poll-id-123'); - const result = await messageComposerStateMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - type: 'regular', - text: 'Original message text', - }, - localMessage: { - attachments: [], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: 'Original local message text', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await messageComposerStateMiddleware.handlers.compose( + setupHandlerParams({ + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + text: 'Original message text', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: 'Original local message text', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); // Verify that the original properties are preserved expect(result.state.message.text).toBe('Original message text'); @@ -326,16 +342,13 @@ describe('DraftMessageComposerStateMiddleware', () => { }); it('should handle draft without quoted message or poll', async () => { - const result = await messageComposerStateMiddleware.compose({ - input: { - state: { - draft: { - text: '', - }, + const result = await messageComposerStateMiddleware.handlers.compose( + setupHandlerParamsDraft({ + draft: { + text: '', }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined(); expect(result.state.draft.quoted_message_id).toBeUndefined(); @@ -355,16 +368,13 @@ describe('DraftMessageComposerStateMiddleware', () => { vi.spyOn(messageComposer, 'quotedMessage', 'get').mockReturnValue(quotedMessage); - const result = await messageComposerStateMiddleware.compose({ - input: { - state: { - draft: { - text: '', - }, + const result = await messageComposerStateMiddleware.handlers.compose( + setupHandlerParamsDraft({ + draft: { + text: '', }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined(); expect(result.state.draft.quoted_message_id).toBe('quoted-message-id'); @@ -374,16 +384,13 @@ describe('DraftMessageComposerStateMiddleware', () => { it('should handle draft with poll', async () => { vi.spyOn(messageComposer, 'pollId', 'get').mockReturnValue('poll-id-123'); - const result = await messageComposerStateMiddleware.compose({ - input: { - state: { - draft: { - text: '', - }, + const result = await messageComposerStateMiddleware.handlers.compose( + setupHandlerParamsDraft({ + draft: { + text: '', }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined(); expect(result.state.draft.quoted_message_id).toBeUndefined(); @@ -404,16 +411,13 @@ describe('DraftMessageComposerStateMiddleware', () => { vi.spyOn(messageComposer, 'quotedMessage', 'get').mockReturnValue(quotedMessage); vi.spyOn(messageComposer, 'pollId', 'get').mockReturnValue('poll-id-123'); - const result = await messageComposerStateMiddleware.compose({ - input: { - state: { - draft: { - text: '', - }, + const result = await messageComposerStateMiddleware.handlers.compose( + setupHandlerParamsDraft({ + draft: { + text: '', }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined(); expect(result.state.draft.quoted_message_id).toBe('quoted-message-id'); @@ -434,22 +438,19 @@ describe('DraftMessageComposerStateMiddleware', () => { vi.spyOn(messageComposer, 'quotedMessage', 'get').mockReturnValue(quotedMessage); vi.spyOn(messageComposer, 'pollId', 'get').mockReturnValue('poll-id-123'); - const result = await messageComposerStateMiddleware.compose({ - input: { - state: { - draft: { - text: 'Original draft text', - attachments: [ - { - type: 'image', - image_url: 'https://example.com/image.jpg', - }, - ], - }, + const result = await messageComposerStateMiddleware.handlers.compose( + setupHandlerParamsDraft({ + draft: { + text: 'Original draft text', + attachments: [ + { + type: 'image', + image_url: 'https://example.com/image.jpg', + }, + ], }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined(); expect(result.state.draft.text).toBe('Original draft text'); diff --git a/test/unit/MessageComposer/middleware/messageComposer/textComposer.test.ts b/test/unit/MessageComposer/middleware/messageComposer/textComposer.test.ts index 87ee2a79f6..698fcf669a 100644 --- a/test/unit/MessageComposer/middleware/messageComposer/textComposer.test.ts +++ b/test/unit/MessageComposer/middleware/messageComposer/textComposer.test.ts @@ -4,6 +4,37 @@ import { StreamChat } from '../../../../../src/client'; import { MessageComposer } from '../../../../../src/messageComposer/messageComposer'; import { createTextComposerCompositionMiddleware } from '../../../../../src/messageComposer/middleware/messageComposer/textComposer'; import { createDraftTextComposerCompositionMiddleware } from '../../../../../src/messageComposer/middleware/messageComposer/textComposer'; +import { + MessageComposerMiddlewareState, + MessageDraftComposerMiddlewareValueState, + MiddlewareStatus, +} from '../../../../../src'; + +const setup = (initialState: MessageComposerMiddlewareState) => { + return { + state: initialState, + next: async (state: MessageComposerMiddlewareState) => ({ state }), + complete: async (state: MessageComposerMiddlewareState) => ({ + state, + status: 'complete' as MiddlewareStatus, + }), + discard: async () => ({ state: initialState, status: 'discard' as MiddlewareStatus }), + forward: async () => ({ state: initialState }), + }; +}; + +const setupDraft = (initialState: MessageDraftComposerMiddlewareValueState) => { + return { + state: initialState, + next: async (state: MessageDraftComposerMiddlewareValueState) => ({ state }), + complete: async (state: MessageDraftComposerMiddlewareValueState) => ({ + state, + status: 'complete' as MiddlewareStatus, + }), + discard: async () => ({ state: initialState, status: 'discard' as MiddlewareStatus }), + forward: async () => ({ state: initialState }), + }; +}; describe('TextComposerMiddleware', () => { let channel: Channel; @@ -97,34 +128,31 @@ describe('TextComposerMiddleware', () => { }); it('should handle empty message', async () => { - const result = await textComposerMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - type: 'regular', - }, - localMessage: { - attachments: [], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: '', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await textComposerMiddleware.handlers.compose( + setup({ + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.status).toBeUndefined; expect(result.state.message.text).toBeUndefined; @@ -133,35 +161,31 @@ describe('TextComposerMiddleware', () => { it('should handle message with text', async () => { vi.spyOn(messageComposer.textComposer, 'text', 'get').mockReturnValue('Hello world'); - - const result = await textComposerMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - type: 'regular', - }, - localMessage: { - attachments: [], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: '', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await textComposerMiddleware.handlers.compose( + setup({ + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.status).toBeUndefined; expect(result.state.message.text).toBe('Hello world'); @@ -177,35 +201,32 @@ describe('TextComposerMiddleware', () => { { id: 'user2', name: 'User 2' }, ]); - const result = await textComposerMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - type: 'regular', - mentioned_users: [] as string[], - }, - localMessage: { - attachments: [], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [] as Array<{ id: string; name: string }>, - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: '', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await textComposerMiddleware.handlers.compose( + setup({ + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + mentioned_users: [] as string[], }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [] as Array<{ id: string; name: string }>, + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.status).toBeUndefined; expect(result.state.message.text).toBe('@user1 @user2'); @@ -224,35 +245,32 @@ describe('TextComposerMiddleware', () => { { id: 'user2', name: 'User 2' }, ]); - const result = await textComposerMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - type: 'regular', - mentioned_users: [] as string[], - }, - localMessage: { - attachments: [], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [] as Array<{ id: string; name: string }>, - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: '', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await textComposerMiddleware.handlers.compose( + setup({ + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', + mentioned_users: [] as string[], }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [] as Array<{ id: string; name: string }>, + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.status).toBeUndefined; expect(result.state.message.text).toBe('@user1'); @@ -266,34 +284,31 @@ describe('TextComposerMiddleware', () => { it('should handle message with commands', async () => { vi.spyOn(messageComposer.textComposer, 'text', 'get').mockReturnValue('/giphy hello'); - const result = await textComposerMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - type: 'regular', - }, - localMessage: { - attachments: [], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: '', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await textComposerMiddleware.handlers.compose( + setup({ + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.status).toBeUndefined; expect(result.state.message.text).toBe('/giphy hello'); @@ -303,34 +318,31 @@ describe('TextComposerMiddleware', () => { it('should handle message with emoji', async () => { vi.spyOn(messageComposer.textComposer, 'text', 'get').mockReturnValue('Hello 👋'); - const result = await textComposerMiddleware.compose({ - input: { - state: { - message: { - id: 'test-id', - parent_id: undefined, - type: 'regular', - }, - localMessage: { - attachments: [], - created_at: new Date(), - deleted_at: null, - error: undefined, - id: 'test-id', - mentioned_users: [], - parent_id: undefined, - pinned_at: null, - reaction_groups: null, - status: 'sending', - text: '', - type: 'regular', - updated_at: new Date(), - }, - sendOptions: {}, + const result = await textComposerMiddleware.handlers.compose( + setup({ + message: { + id: 'test-id', + parent_id: undefined, + type: 'regular', }, - }, - nextHandler: async (input) => input, - }); + localMessage: { + attachments: [], + created_at: new Date(), + deleted_at: null, + error: undefined, + id: 'test-id', + mentioned_users: [], + parent_id: undefined, + pinned_at: null, + reaction_groups: null, + status: 'sending', + text: '', + type: 'regular', + updated_at: new Date(), + }, + sendOptions: {}, + }), + ); expect(result.status).toBeUndefined; expect(result.state.message.text).toBe('Hello 👋'); @@ -433,18 +445,15 @@ describe('DraftTextComposerMiddleware', () => { }); it('should handle empty draft', async () => { - const result = await draftTextComposerMiddleware.compose({ - input: { - state: { - draft: { - id: 'test-id', - parent_id: undefined, - text: '', - }, + const result = await draftTextComposerMiddleware.handlers.compose( + setupDraft({ + draft: { + id: 'test-id', + parent_id: undefined, + text: '', }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined(); expect(result.state.draft.text).toBe(''); @@ -454,18 +463,15 @@ describe('DraftTextComposerMiddleware', () => { it('should handle draft with text', async () => { vi.spyOn(messageComposer.textComposer, 'text', 'get').mockReturnValue('Hello world'); - const result = await draftTextComposerMiddleware.compose({ - input: { - state: { - draft: { - id: 'test-id', - parent_id: undefined, - text: '', - }, + const result = await draftTextComposerMiddleware.handlers.compose( + setupDraft({ + draft: { + id: 'test-id', + parent_id: undefined, + text: '', }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined(); expect(result.state.draft.text).toBe('Hello world'); @@ -481,18 +487,15 @@ describe('DraftTextComposerMiddleware', () => { { id: 'user2', name: 'User 2' }, ]); - const result = await draftTextComposerMiddleware.compose({ - input: { - state: { - draft: { - id: 'test-id', - parent_id: undefined, - text: '', - }, + const result = await draftTextComposerMiddleware.handlers.compose( + setupDraft({ + draft: { + id: 'test-id', + parent_id: undefined, + text: '', }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined(); expect(result.state.draft.text).toBe('@user1 @user2'); @@ -506,18 +509,15 @@ describe('DraftTextComposerMiddleware', () => { { id: 'user2', name: 'User 2' }, ]); - const result = await draftTextComposerMiddleware.compose({ - input: { - state: { - draft: { - id: 'test-id', - parent_id: undefined, - text: '', - }, + const result = await draftTextComposerMiddleware.handlers.compose( + setupDraft({ + draft: { + id: 'test-id', + parent_id: undefined, + text: '', }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined(); expect(result.state.draft.text).toBe('@user1'); @@ -528,18 +528,15 @@ describe('DraftTextComposerMiddleware', () => { vi.spyOn(messageComposer.textComposer, 'text', 'get').mockReturnValue('Hello world'); vi.spyOn(messageComposer.textComposer, 'mentionedUsers', 'get').mockReturnValue([]); - const result = await draftTextComposerMiddleware.compose({ - input: { - state: { - draft: { - id: 'test-id', - parent_id: undefined, - text: '', - }, + const result = await draftTextComposerMiddleware.handlers.compose( + setupDraft({ + draft: { + id: 'test-id', + parent_id: undefined, + text: '', }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined(); expect(result.state.draft.text).toBe('Hello world'); @@ -552,24 +549,21 @@ describe('DraftTextComposerMiddleware', () => { { id: 'user1', name: 'User 1' }, ]); - const result = await draftTextComposerMiddleware.compose({ - input: { - state: { - draft: { - id: 'test-id', - parent_id: undefined, - text: '', - attachments: [ - { - type: 'image', - image_url: 'https://example.com/image.jpg', - }, - ], - }, + const result = await draftTextComposerMiddleware.handlers.compose( + setupDraft({ + draft: { + id: 'test-id', + parent_id: undefined, + text: '', + attachments: [ + { + type: 'image', + image_url: 'https://example.com/image.jpg', + }, + ], }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined(); expect(result.state.draft.text).toBe('Hello world'); @@ -581,18 +575,15 @@ describe('DraftTextComposerMiddleware', () => { it('should handle when textComposer is not available', async () => { messageComposer.textComposer = undefined as any; - const result = await draftTextComposerMiddleware.compose({ - input: { - state: { - draft: { - id: 'test-id', - parent_id: undefined, - text: '', - }, + const result = await draftTextComposerMiddleware.handlers.compose( + setupDraft({ + draft: { + id: 'test-id', + parent_id: undefined, + text: '', }, - }, - nextHandler: async (input) => input, - }); + }), + ); expect(result.status).toBeUndefined(); expect(result.state.draft.text).toBe(''); diff --git a/test/unit/MessageComposer/middleware/pollComposer/composition.test.ts b/test/unit/MessageComposer/middleware/pollComposer/composition.test.ts index db0550e3e6..1bc4d0d1b5 100644 --- a/test/unit/MessageComposer/middleware/pollComposer/composition.test.ts +++ b/test/unit/MessageComposer/middleware/pollComposer/composition.test.ts @@ -3,14 +3,32 @@ import { createPollCompositionValidationMiddleware } from '../../../../../src/me import { MessageComposer } from '../../../../../src/messageComposer/messageComposer'; import { PollComposer } from '../../../../../src/messageComposer/pollComposer'; import { VotingVisibility } from '../../../../../src/types'; -import type { Middleware } from '../../../../../src/middleware'; +import type { Middleware, MiddlewareStatus } from '../../../../../src/middleware'; import type { PollComposerCompositionMiddlewareValueState } from '../../../../../src/messageComposer/middleware/pollComposer/types'; import type { MiddlewareHandler } from '../../../../../src/middleware'; +const setupHandlerParams = ( + initialState: PollComposerCompositionMiddlewareValueState, +) => { + return { + state: initialState, + next: async (state: PollComposerCompositionMiddlewareValueState) => ({ state }), + complete: async (state: PollComposerCompositionMiddlewareValueState) => ({ + state, + status: 'complete' as MiddlewareStatus, + }), + discard: async () => ({ state: initialState, status: 'discard' as MiddlewareStatus }), + forward: async () => ({ state: initialState }), + }; +}; + describe('PollComposerCompositionMiddleware', () => { let messageComposer: MessageComposer; let pollComposer: PollComposer; - let validationMiddleware: Middleware; + let validationMiddleware: Middleware< + PollComposerCompositionMiddlewareValueState, + 'compose' + >; beforeEach(() => { messageComposer = { @@ -45,21 +63,16 @@ describe('PollComposerCompositionMiddleware', () => { // Mock the canCreatePoll getter vi.spyOn(pollComposer, 'canCreatePoll', 'get').mockReturnValue(true); - const result = await ( - validationMiddleware.compose as MiddlewareHandler - )({ - input: { - state: { - data: { - ...pollComposer.state.getLatestValue().data, - max_votes_allowed: undefined, - options: [{ text: 'Option 1' }], - }, - errors: {}, + const result = await validationMiddleware.handlers.compose( + setupHandlerParams({ + data: { + ...pollComposer.state.getLatestValue().data, + max_votes_allowed: undefined, + options: [{ text: 'Option 1' }], }, - }, - nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), - }); + errors: {}, + }), + ); expect(result.status).toBeUndefined; }); @@ -85,21 +98,16 @@ describe('PollComposerCompositionMiddleware', () => { // Mock the canCreatePoll getter vi.spyOn(pollComposer, 'canCreatePoll', 'get').mockReturnValue(false); - const result = await ( - validationMiddleware.compose as MiddlewareHandler - )({ - input: { - state: { - data: { - ...pollComposer.state.getLatestValue().data, - max_votes_allowed: undefined, - options: [{ text: 'Option 1' }], - }, - errors: {}, + const result = await validationMiddleware.handlers.compose( + setupHandlerParams({ + data: { + ...pollComposer.state.getLatestValue().data, + max_votes_allowed: undefined, + options: [{ text: 'Option 1' }], }, - }, - nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), - }); + errors: {}, + }), + ); expect(result.status).toBe('discard'); }); diff --git a/test/unit/MessageComposer/middleware/pollComposer/state.test.ts b/test/unit/MessageComposer/middleware/pollComposer/state.test.ts index 58d8787a60..fb6b4a0c7d 100644 --- a/test/unit/MessageComposer/middleware/pollComposer/state.test.ts +++ b/test/unit/MessageComposer/middleware/pollComposer/state.test.ts @@ -1,10 +1,28 @@ import { describe, expect, it, vi } from 'vitest'; +import { + MiddlewareStatus, + PollComposerOption, + PollComposerState, + PollComposerStateChangeMiddlewareValue, +} from '../../../../../src'; import { createPollComposerStateMiddleware, PollComposerStateMiddlewareFactoryOptions, } from '../../../../../src/messageComposer/middleware/pollComposer/state'; import { VotingVisibility } from '../../../../../src/types'; -import { PollComposerOption, PollComposerState } from '../../../../../src'; + +const setupHandlerParams = (initialState: PollComposerStateChangeMiddlewareValue) => { + return { + state: initialState, + next: async (state: PollComposerStateChangeMiddlewareValue) => ({ state }), + complete: async (state: PollComposerStateChangeMiddlewareValue) => ({ + state, + status: 'complete' as MiddlewareStatus, + }), + discard: async () => ({ state: initialState, status: 'discard' as MiddlewareStatus }), + forward: async () => ({ state: initialState }), + }; +}; // Mock dependencies vi.mock('../../../../../src/utils', () => ({ @@ -35,16 +53,13 @@ describe('PollComposerStateMiddleware', () => { describe('handleFieldChange', () => { it('should update name field', async () => { const stateMiddleware = setup(); - const result = await stateMiddleware.handleFieldChange({ - input: { - state: { - nextState: { ...getInitialState() }, - previousState: { ...getInitialState() }, - targetFields: { name: 'Test Poll' }, - }, - }, - nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), - }); + const result = await stateMiddleware.handlers.handleFieldChange( + setupHandlerParams({ + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, + targetFields: { name: 'Test Poll' }, + }), + ); expect(result.state.nextState.data.name).toBe('Test Poll'); expect(result.status).toBeUndefined; @@ -52,16 +67,13 @@ describe('PollComposerStateMiddleware', () => { it('should validate max_votes_allowed field with invalid value', async () => { const stateMiddleware = setup(); - const result = await stateMiddleware.handleFieldChange({ - input: { - state: { - nextState: { ...getInitialState() }, - previousState: { ...getInitialState() }, - targetFields: { max_votes_allowed: '1' }, // Invalid value (less than 2) - }, - }, - nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), - }); + const result = await stateMiddleware.handlers.handleFieldChange( + setupHandlerParams({ + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, + targetFields: { max_votes_allowed: '1' }, // Invalid value (less than 2) + }), + ); expect(result.state.nextState.errors.max_votes_allowed).toBeDefined(); expect(result.state.nextState.data.max_votes_allowed).toBe('1'); @@ -70,22 +82,19 @@ describe('PollComposerStateMiddleware', () => { it('should not validate max_votes_allowed field with valid value if enforce_unique_vote is true', async () => { const stateMiddleware = setup(); - const result = await stateMiddleware.handleFieldChange({ - input: { - state: { - nextState: { - ...getInitialState(), - data: { ...getInitialState().data, enforce_unique_vote: true }, - }, - previousState: { - ...getInitialState(), - data: { ...getInitialState().data, enforce_unique_vote: true }, - }, - targetFields: { max_votes_allowed: '5' }, // Valid value (between 2 and 10) + const result = await stateMiddleware.handlers.handleFieldChange( + setupHandlerParams({ + nextState: { + ...getInitialState(), + data: { ...getInitialState().data, enforce_unique_vote: true }, }, - }, - nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), - }); + previousState: { + ...getInitialState(), + data: { ...getInitialState().data, enforce_unique_vote: true }, + }, + targetFields: { max_votes_allowed: '5' }, // Valid value (between 2 and 10) + }), + ); expect(result.state.nextState.errors.max_votes_allowed).toBeDefined(); expect(result.state.nextState.data.max_votes_allowed).toBe('5'); @@ -94,22 +103,19 @@ describe('PollComposerStateMiddleware', () => { it('should validate max_votes_allowed field with valid value if enforce_unique_vote is false', async () => { const stateMiddleware = setup(); - const result = await stateMiddleware.handleFieldChange({ - input: { - state: { - nextState: { - ...getInitialState(), - data: { ...getInitialState().data, enforce_unique_vote: false }, - }, - previousState: { - ...getInitialState(), - data: { ...getInitialState().data, enforce_unique_vote: false }, - }, - targetFields: { max_votes_allowed: '5' }, // Valid value (between 2 and 10) + const result = await stateMiddleware.handlers.handleFieldChange( + setupHandlerParams({ + nextState: { + ...getInitialState(), + data: { ...getInitialState().data, enforce_unique_vote: false }, }, - }, - nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), - }); + previousState: { + ...getInitialState(), + data: { ...getInitialState().data, enforce_unique_vote: false }, + }, + targetFields: { max_votes_allowed: '5' }, // Valid value (between 2 and 10) + }), + ); expect(result.state.nextState.errors.max_votes_allowed).toBeUndefined(); expect(result.state.nextState.data.max_votes_allowed).toBe('5'); @@ -118,23 +124,20 @@ describe('PollComposerStateMiddleware', () => { it('should handle options field changes with single option update', async () => { const stateMiddleware = setup(); - const result = await stateMiddleware.handleFieldChange({ - input: { - state: { - nextState: { ...getInitialState() }, - previousState: { ...getInitialState() }, - targetFields: { - options: [ - { - index: 0, - text: 'Option 1', - }, - ], - }, + const result = await stateMiddleware.handlers.handleFieldChange( + setupHandlerParams({ + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, + targetFields: { + options: [ + { + id: 'option-id', + text: 'Option 1', + }, + ], }, - }, - nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), - }); + }), + ); expect(result.state.nextState.data.options[0].text).toBe('Option 1'); expect(result.state.nextState.data.options.length).toBe(1); @@ -143,21 +146,18 @@ describe('PollComposerStateMiddleware', () => { it('should handle options field changes with array update', async () => { const stateMiddleware = setup(); - const result = await stateMiddleware.handleFieldChange({ - input: { - state: { - nextState: { ...getInitialState() }, - previousState: { ...getInitialState() }, - targetFields: { - options: [ - { id: 'option-1', text: 'Option 1' }, - { id: 'option-2', text: 'Option 2' }, - ], - }, + const result = await stateMiddleware.handlers.handleFieldChange( + setupHandlerParams({ + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, + targetFields: { + options: [ + { id: 'option-1', text: 'Option 1' }, + { id: 'option-2', text: 'Option 2' }, + ], }, - }, - nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), - }); + }), + ); expect(result.state.nextState.data.options.length).toBe(2); expect(result.state.nextState.data.options[0].text).toBe('Option 1'); @@ -167,16 +167,13 @@ describe('PollComposerStateMiddleware', () => { it('should handle enforce_unique_vote field changes', async () => { const stateMiddleware = setup(); - const result = await stateMiddleware.handleFieldChange({ - input: { - state: { - nextState: { ...getInitialState() }, - previousState: { ...getInitialState() }, - targetFields: { enforce_unique_vote: false }, - }, - }, - nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), - }); + const result = await stateMiddleware.handlers.handleFieldChange( + setupHandlerParams({ + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, + targetFields: { enforce_unique_vote: false }, + }), + ); expect(result.state.nextState.data.enforce_unique_vote).toBe(false); expect(result.state.nextState.data.max_votes_allowed).toBe(''); @@ -185,21 +182,18 @@ describe('PollComposerStateMiddleware', () => { it('should add a new empty option when the last option is filled', async () => { const stateMiddleware = setup(); - const result = await stateMiddleware.handleFieldChange({ - input: { - state: { - nextState: { ...getInitialState() }, - previousState: { ...getInitialState() }, - targetFields: { - options: { - index: 0, - text: 'Option 1', - }, + const result = await stateMiddleware.handlers.handleFieldChange( + setupHandlerParams({ + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, + targetFields: { + options: { + index: 0, + text: 'Option 1', }, }, - }, - nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), - }); + }), + ); expect(result.state.nextState.data.options.length).toBe(2); expect(result.state.nextState.data.options[0].text).toBe('Option 1'); @@ -216,21 +210,18 @@ describe('PollComposerStateMiddleware', () => { { id: 'option-2', text: '' }, ]; - const result = await stateMiddleware.handleFieldChange({ - input: { - state: { - nextState: { ...getInitialState() }, - previousState: { ...getInitialState() }, - targetFields: { - options: { - index: 0, - text: '', - }, + const result = await stateMiddleware.handlers.handleFieldChange( + setupHandlerParams({ + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, + targetFields: { + options: { + index: 0, + text: '', }, }, - }, - nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), - }); + }), + ); expect(result.state.nextState.data.options.length).toBe(1); expect(result.state.nextState.data.options[0].text).toBe(''); @@ -245,21 +236,18 @@ describe('PollComposerStateMiddleware', () => { }, }); - const result = await stateMiddleware.handleFieldChange({ - input: { - state: { - nextState: { ...getInitialState() }, - previousState: { ...getInitialState() }, - targetFields: { - options: { - index: 0, - text: 'X', - }, + const result = await stateMiddleware.handlers.handleFieldChange( + setupHandlerParams({ + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, + targetFields: { + options: { + index: 0, + text: 'X', }, }, - }, - nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), - }); + }), + ); expect(result.state.nextState.data.options).toEqual(injectedOptions); expect(result.status).toBeUndefined; @@ -271,21 +259,18 @@ describe('PollComposerStateMiddleware', () => { }, }); - const result = await stateMiddleware.handleFieldChange({ - input: { - state: { - nextState: { ...getInitialState() }, - previousState: { ...getInitialState() }, - targetFields: { - options: { - index: 0, - text: 'X', - }, + const result = await stateMiddleware.handlers.handleFieldChange( + setupHandlerParams({ + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, + targetFields: { + options: { + index: 0, + text: 'X', }, }, - }, - nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), - }); + }), + ); expect(result.state.nextState.errors.options).toEqual({ x: 'failed option X' }); expect(result.status).toBeUndefined; @@ -295,16 +280,13 @@ describe('PollComposerStateMiddleware', () => { describe('handleFieldBlur', () => { it('should validate name field on blur', async () => { const stateMiddleware = setup(); - const result = await stateMiddleware.handleFieldBlur({ - input: { - state: { - nextState: { ...getInitialState() }, - previousState: { ...getInitialState() }, - targetFields: { name: '' }, - }, - }, - nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), - }); + const result = await stateMiddleware.handlers.handleFieldBlur( + setupHandlerParams({ + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, + targetFields: { name: '' }, + }), + ); expect(result.state.nextState.errors.name).toBeDefined(); expect(result.status).toBeUndefined; @@ -312,16 +294,13 @@ describe('PollComposerStateMiddleware', () => { it('should validate max_votes_allowed field on blur', async () => { const stateMiddleware = setup(); - const result = await stateMiddleware.handleFieldBlur({ - input: { - state: { - nextState: { ...getInitialState() }, - previousState: { ...getInitialState() }, - targetFields: { max_votes_allowed: '1' }, - }, - }, - nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), - }); + const result = await stateMiddleware.handlers.handleFieldBlur( + setupHandlerParams({ + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, + targetFields: { max_votes_allowed: '1' }, + }), + ); expect(result.state.nextState.errors.max_votes_allowed).toBeDefined(); expect(result.status).toBeUndefined; @@ -330,37 +309,31 @@ describe('PollComposerStateMiddleware', () => { describe('options validation', () => { it('should validate empty options on blur', async () => { const stateMiddleware = setup(); - const result = await stateMiddleware.handleFieldBlur({ - input: { - state: { - nextState: { ...getInitialState() }, - previousState: { ...getInitialState() }, - targetFields: { options: [{ id: 'option-id', text: '' }] }, - }, - }, - nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), - }); + const result = await stateMiddleware.handlers.handleFieldBlur( + setupHandlerParams({ + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, + targetFields: { options: [{ id: 'option-id', text: '' }] }, + }), + ); expect(result.state.nextState.errors.options).toBeUndefined(); }); it('should validate duplicate options on blur', async () => { const stateMiddleware = setup(); - const result = await stateMiddleware.handleFieldBlur({ - input: { - state: { - nextState: { ...getInitialState() }, - previousState: { ...getInitialState() }, - targetFields: { - options: [ - { id: 'option-1', text: 'Same Text' }, - { id: 'option-2', text: 'Same Text' }, - ], - }, + const result = await stateMiddleware.handlers.handleFieldBlur( + setupHandlerParams({ + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, + targetFields: { + options: [ + { id: 'option-1', text: 'Same Text' }, + { id: 'option-2', text: 'Same Text' }, + ], }, - }, - nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), - }); + }), + ); expect(result.state.nextState.errors.options).toEqual({ 'option-2': 'Option already exists', @@ -369,21 +342,18 @@ describe('PollComposerStateMiddleware', () => { it('should pass validation for valid options', async () => { const stateMiddleware = setup(); - const result = await stateMiddleware.handleFieldBlur({ - input: { - state: { - nextState: { ...getInitialState() }, - previousState: { ...getInitialState() }, - targetFields: { - options: [ - { id: 'option-1', text: 'Option 1' }, - { id: 'option-2', text: 'Option 2' }, - ], - }, + const result = await stateMiddleware.handlers.handleFieldBlur( + setupHandlerParams({ + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, + targetFields: { + options: [ + { id: 'option-1', text: 'Option 1' }, + { id: 'option-2', text: 'Option 2' }, + ], }, - }, - nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), - }); + }), + ); expect(result.state.nextState.errors.options).toBeUndefined(); }); @@ -397,21 +367,18 @@ describe('PollComposerStateMiddleware', () => { }, }); - const result = await stateMiddleware.handleFieldBlur({ - input: { - state: { - nextState: { ...getInitialState() }, - previousState: { ...getInitialState() }, - targetFields: { - options: { - index: 0, - text: 'X', - }, + const result = await stateMiddleware.handlers.handleFieldBlur( + setupHandlerParams({ + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, + targetFields: { + options: { + index: 0, + text: 'X', }, }, - }, - nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), - }); + }), + ); expect(result.state.nextState.data.options).toEqual(injectedOptions); expect(result.status).toBeUndefined; @@ -423,21 +390,18 @@ describe('PollComposerStateMiddleware', () => { }, }); - const result = await stateMiddleware.handleFieldBlur({ - input: { - state: { - nextState: { ...getInitialState() }, - previousState: { ...getInitialState() }, - targetFields: { - options: { - index: 0, - text: 'X', - }, + const result = await stateMiddleware.handlers.handleFieldBlur( + setupHandlerParams({ + nextState: { ...getInitialState() }, + previousState: { ...getInitialState() }, + targetFields: { + options: { + index: 0, + text: 'X', }, }, - }, - nextHandler: vi.fn().mockImplementation((input) => Promise.resolve(input)), - }); + }), + ); expect(result.state.nextState.errors.options).toEqual({ x: 'failed option X' }); expect(result.status).toBeUndefined; diff --git a/test/unit/MessageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.test.ts b/test/unit/MessageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.test.ts index 4cea048672..a631331847 100644 --- a/test/unit/MessageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.test.ts +++ b/test/unit/MessageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.test.ts @@ -6,8 +6,7 @@ import { MessageComposer, } from '../../../../../src/messageComposer/messageComposer'; import { createMentionsMiddleware } from '../../../../../src/messageComposer/middleware/textComposer/mentions'; -import { TextComposer } from '../../../../../src/messageComposer/textComposer'; -import type { TextComposerSuggestion } from '../../../../../src/messageComposer/types'; +import type { TextComposerSuggestion } from '../../../../../src/messageComposer/'; import type { CommandResponse, DraftResponse, @@ -63,6 +62,12 @@ const setup = ({ return { client, channel, messageComposer }; }; +const initialValue = { + text: '', + selection: { start: 0, end: 0 }, + mentionedUsers: [], +}; + describe('TextComposerMiddlewareExecutor', () => { it('should initialize with default middleware', () => { const { @@ -79,11 +84,12 @@ describe('TextComposerMiddlewareExecutor', () => { const { messageComposer: { textComposer }, } = setup(); - let result = await textComposer.middlewareExecutor.execute('onChange', { - state: { + let result = await textComposer.middlewareExecutor.execute({ + eventName: 'onChange', + initialValue: { + ...initialValue, text: '@jo', selection: { start: 3, end: 3 }, - mentionedUsers: [], }, }); @@ -91,11 +97,12 @@ describe('TextComposerMiddlewareExecutor', () => { expect(result.state.suggestions?.trigger).toBe('@'); expect(result.state.suggestions?.query).toBe('jo'); - result = await textComposer.middlewareExecutor.execute('onChange', { - state: { + result = await textComposer.middlewareExecutor.execute({ + eventName: 'onChange', + initialValue: { + ...initialValue, text: 'abcde@ho', selection: { start: 8, end: 8 }, - mentionedUsers: [], }, }); @@ -103,21 +110,23 @@ describe('TextComposerMiddlewareExecutor', () => { expect(result.state.suggestions?.trigger).toBe('@'); expect(result.state.suggestions?.query).toBe('ho'); - result = await textComposer.middlewareExecutor.execute('onChange', { - state: { + result = await textComposer.middlewareExecutor.execute({ + eventName: 'onChange', + initialValue: { + ...initialValue, text: 'abcde@ho', selection: { start: 5, end: 5 }, // selection is not where the trigger is - mentionedUsers: [], }, }); expect(result.state.suggestions).toBeUndefined(); - result = await textComposer.middlewareExecutor.execute('onChange', { - state: { + result = await textComposer.middlewareExecutor.execute({ + eventName: 'onChange', + initialValue: { + ...initialValue, text: 'abcde@ho', selection: { start: 6, end: 6 }, // selection is where the trigger is but not at the end - mentionedUsers: [], }, }); @@ -130,11 +139,12 @@ describe('TextComposerMiddlewareExecutor', () => { const { messageComposer: { textComposer }, } = setup(); - let result = await textComposer.middlewareExecutor.execute('onChange', { - state: { + let result = await textComposer.middlewareExecutor.execute({ + eventName: 'onChange', + initialValue: { + ...initialValue, text: '/ban', selection: { start: 4, end: 4 }, - mentionedUsers: [], }, }); @@ -142,11 +152,14 @@ describe('TextComposerMiddlewareExecutor', () => { expect(result.state.suggestions?.trigger).toBe('/'); expect(result.state.suggestions?.query).toBe('ban'); - result = await textComposer.middlewareExecutor.execute('onChange', { - state: { - text: '/ban /ban', - selection: { start: 9, end: 9 }, - mentionedUsers: [], + result = await textComposer.middlewareExecutor.execute({ + eventName: 'onChange', + initialValue: { + ...initialValue, + change: { + text: '/ban /ban', + selection: { start: 9, end: 9 }, + }, }, }); @@ -205,6 +218,7 @@ describe('TextComposerMiddlewareExecutor', () => { throw new Error('Search failed'); }), activate: vi.fn(), + resetinitialValue: vi.fn(), resetState: vi.fn(), resetStateAndActivate: vi.fn(), config: {}, @@ -216,11 +230,12 @@ describe('TextComposerMiddlewareExecutor', () => { }), ] as TextComposerMiddleware[]); - const result = await textComposer.middlewareExecutor.execute('onChange', { - state: { + const result = await textComposer.middlewareExecutor.execute({ + eventName: 'onChange', + initialValue: { + ...initialValue, text: '@jo', selection: { start: 3, end: 3 }, - mentionedUsers: [], }, }); @@ -233,11 +248,12 @@ describe('TextComposerMiddlewareExecutor', () => { const { messageComposer: { textComposer }, } = setup(); - const result = await textComposer.middlewareExecutor.execute('onChange', { - state: { + const result = await textComposer.middlewareExecutor.execute({ + eventName: 'onChange', + initialValue: { + ...initialValue, text: '/test', selection: { start: 0, end: 0 }, - mentionedUsers: [], }, }); @@ -252,11 +268,12 @@ describe('TextComposerMiddlewareExecutor', () => { const { messageComposer: { textComposer }, } = setup(); - const result = await textComposer.middlewareExecutor.execute('onChange', { - state: { + const result = await textComposer.middlewareExecutor.execute({ + eventName: 'onChange', + initialValue: { + ...initialValue, text: 'test', selection: { start: 0, end: 4 }, - mentionedUsers: [], }, }); @@ -271,11 +288,12 @@ describe('TextComposerMiddlewareExecutor', () => { const { messageComposer: { textComposer }, } = setup(); - const result = await textComposer.middlewareExecutor.execute('onChange', { - state: { + const result = await textComposer.middlewareExecutor.execute({ + eventName: 'onChange', + initialValue: { + ...initialValue, text: '/test', selection: { start: 0, end: 5 }, - mentionedUsers: [], }, }); @@ -288,11 +306,12 @@ describe('TextComposerMiddlewareExecutor', () => { const { messageComposer: { textComposer }, } = setup(); - const result = await textComposer.middlewareExecutor.execute('onChange', { - state: { + const result = await textComposer.middlewareExecutor.execute({ + eventName: 'onChange', + initialValue: { + ...initialValue, text: '/', selection: { start: 0, end: 1 }, - mentionedUsers: [], }, }); @@ -305,11 +324,12 @@ describe('TextComposerMiddlewareExecutor', () => { const { messageComposer: { textComposer }, } = setup(); - const result = await textComposer.middlewareExecutor.execute('onChange', { - state: { + const result = await textComposer.middlewareExecutor.execute({ + eventName: 'onChange', + initialValue: { + ...initialValue, text: 'test', selection: { start: 0, end: 4 }, - mentionedUsers: [], suggestions: { trigger: '/', query: 'test', @@ -327,11 +347,12 @@ describe('TextComposerMiddlewareExecutor', () => { const { messageComposer: { textComposer }, } = setup(); - const result = await textComposer.middlewareExecutor.execute('onChange', { - state: { + const result = await textComposer.middlewareExecutor.execute({ + eventName: 'onChange', + initialValue: { + ...initialValue, text: '@test', selection: { start: 0, end: 0 }, - mentionedUsers: [], }, }); @@ -346,11 +367,12 @@ describe('TextComposerMiddlewareExecutor', () => { const { messageComposer: { textComposer }, } = setup(); - const result = await textComposer.middlewareExecutor.execute('onChange', { - state: { + const result = await textComposer.middlewareExecutor.execute({ + eventName: 'onChange', + initialValue: { + ...initialValue, text: '@test', selection: { start: 0, end: 5 }, - mentionedUsers: [], }, }); @@ -363,11 +385,12 @@ describe('TextComposerMiddlewareExecutor', () => { const { messageComposer: { textComposer }, } = setup(); - const result = await textComposer.middlewareExecutor.execute('onChange', { - state: { + const result = await textComposer.middlewareExecutor.execute({ + eventName: 'onChange', + initialValue: { + ...initialValue, text: '@', selection: { start: 0, end: 1 }, - mentionedUsers: [], }, }); @@ -380,11 +403,12 @@ describe('TextComposerMiddlewareExecutor', () => { const { messageComposer: { textComposer }, } = setup(); - const result = await textComposer.middlewareExecutor.execute('onChange', { - state: { + const result = await textComposer.middlewareExecutor.execute({ + eventName: 'onChange', + initialValue: { + ...initialValue, text: 'test', selection: { start: 0, end: 4 }, - mentionedUsers: [], suggestions: { trigger: '@', query: 'test', @@ -402,11 +426,12 @@ describe('TextComposerMiddlewareExecutor', () => { const { messageComposer: { textComposer }, } = setup(); - let result = await textComposer.middlewareExecutor.execute('onChange', { - state: { + let result = await textComposer.middlewareExecutor.execute({ + eventName: 'onChange', + initialValue: { + ...initialValue, text: '/ban', selection: { start: 4, end: 4 }, - mentionedUsers: [], }, }); @@ -415,11 +440,12 @@ describe('TextComposerMiddlewareExecutor', () => { expect(result.state.suggestions?.query).toBe('ban'); // Then test a mention after the command - result = await textComposer.middlewareExecutor.execute('onChange', { - state: { + result = await textComposer.middlewareExecutor.execute({ + eventName: 'onChange', + initialValue: { + ...initialValue, text: '/ban @jo', selection: { start: 9, end: 9 }, - mentionedUsers: [], }, }); @@ -428,22 +454,24 @@ describe('TextComposerMiddlewareExecutor', () => { expect(result.state.suggestions?.query).toBe('jo'); // Test a command in the middle of text - should not trigger command suggestions - result = await textComposer.middlewareExecutor.execute('onChange', { - state: { + result = await textComposer.middlewareExecutor.execute({ + eventName: 'onChange', + initialValue: { + ...initialValue, text: 'hello /ban', selection: { start: 11, end: 11 }, - mentionedUsers: [], }, }); expect(result.state.suggestions).toBeUndefined(); // Test a mention followed by a command - result = await textComposer.middlewareExecutor.execute('onChange', { - state: { + result = await textComposer.middlewareExecutor.execute({ + eventName: 'onChange', + initialValue: { + ...initialValue, text: '@jo /ban', selection: { start: 8, end: 8 }, - mentionedUsers: [], }, }); @@ -462,11 +490,12 @@ describe('TextComposerMiddlewareExecutor', () => { textComposer.maxLengthOnEdit = 10; // Test with text exceeding max length - const result = await textComposer.middlewareExecutor.execute('onChange', { - state: { + const result = await textComposer.middlewareExecutor.execute({ + eventName: 'onChange', + initialValue: { + ...initialValue, text: 'Hello World This Is Too Long', selection: { start: 30, end: 30 }, - mentionedUsers: [], }, }); @@ -482,11 +511,12 @@ describe('TextComposerMiddlewareExecutor', () => { textComposer.maxLengthOnEdit = 20; // Test with text under max length - const result = await textComposer.middlewareExecutor.execute('onChange', { - state: { + const result = await textComposer.middlewareExecutor.execute({ + eventName: 'onChange', + initialValue: { + ...initialValue, text: 'Hello World', selection: { start: 11, end: 11 }, - mentionedUsers: [], }, }); @@ -502,11 +532,12 @@ describe('TextComposerMiddlewareExecutor', () => { textComposer.maxLengthOnEdit = 0; // Test with any text - const result = await textComposer.middlewareExecutor.execute('onChange', { - state: { + const result = await textComposer.middlewareExecutor.execute({ + eventName: 'onChange', + initialValue: { + ...initialValue, text: 'Hello World', selection: { start: 11, end: 11 }, - mentionedUsers: [], }, }); diff --git a/test/unit/MessageComposer/pollComposer.test.ts b/test/unit/MessageComposer/pollComposer.test.ts index 0e29b6b407..462fc2b26e 100644 --- a/test/unit/MessageComposer/pollComposer.test.ts +++ b/test/unit/MessageComposer/pollComposer.test.ts @@ -269,7 +269,10 @@ describe('PollComposer', () => { await pollComposer.updateFields(updateData); - expect(spy).toHaveBeenCalledWith('handleFieldChange', expect.any(Object)); + expect(spy).toHaveBeenCalledWith({ + eventName: 'handleFieldChange', + initialValue: expect.any(Object), + }); }); it('should not update state if middleware returns discard status', async () => { @@ -298,7 +301,10 @@ describe('PollComposer', () => { await pollComposer.handleFieldBlur('name'); - expect(spy).toHaveBeenCalledWith('handleFieldBlur', expect.any(Object)); + expect(spy).toHaveBeenCalledWith({ + eventName: 'handleFieldBlur', + initialValue: expect.any(Object), + }); }); it('should not update state if middleware returns discard status', async () => { @@ -326,7 +332,10 @@ describe('PollComposer', () => { const result = await pollComposer.compose(); - expect(spy).toHaveBeenCalledWith('compose', expect.any(Object)); + expect(spy).toHaveBeenCalledWith({ + eventName: 'compose', + initialValue: expect.any(Object), + }); expect(result).toBeDefined(); if (result) { expect(result.data.name).toBe('Test Poll'); diff --git a/test/unit/middleware.test.ts b/test/unit/middleware.test.ts index 1fbae52406..579ab6cfb5 100644 --- a/test/unit/middleware.test.ts +++ b/test/unit/middleware.test.ts @@ -7,18 +7,20 @@ import { } from '../../src/middleware'; describe('MiddlewareExecutor', () => { - let executor: MiddlewareExecutor<{ value: number }>; + let executor: MiddlewareExecutor<{ value: number }, 'test'>; beforeEach(() => { - executor = new MiddlewareExecutor<{ value: number }>(); + executor = new MiddlewareExecutor<{ value: number }, 'test'>(); }); describe('use', () => { it('should add middleware to the executor', () => { - const middleware: Middleware<{ value: number }> = { + const middleware: Middleware<{ value: number }, 'test'> = { id: 'test-middleware', - test: async ({ input, nextHandler }) => { - return nextHandler(input); + handlers: { + test: async ({ state, next }) => { + return next(state); + }, }, }; @@ -31,17 +33,21 @@ describe('MiddlewareExecutor', () => { }); it('should add multiple middleware when array is provided', () => { - const middleware1: Middleware<{ value: number }> = { + const middleware1: Middleware<{ value: number }, 'test'> = { id: 'test-middleware-1', - test: async ({ input, nextHandler }) => { - return nextHandler(input); + handlers: { + test: async ({ state, next }) => { + return next(state); + }, }, }; - const middleware2: Middleware<{ value: number }> = { + const middleware2: Middleware<{ value: number }, 'test'> = { id: 'test-middleware-2', - test: async ({ input, nextHandler }) => { - return nextHandler(input); + handlers: { + test: async ({ state, next }) => { + return next(state); + }, }, }; @@ -55,10 +61,12 @@ describe('MiddlewareExecutor', () => { }); it('should return the executor for chaining', () => { - const middleware: Middleware<{ value: number }> = { + const middleware: Middleware<{ value: number }, 'test'> = { id: 'test-middleware', - test: async ({ input, nextHandler }) => { - return nextHandler(input); + handlers: { + test: async ({ state, next }) => { + return next(state); + }, }, }; @@ -69,17 +77,21 @@ describe('MiddlewareExecutor', () => { describe('replace', () => { it('should replace existing middleware with the same id', () => { - const middleware1: Middleware<{ value: number }> = { + const middleware1: Middleware<{ value: number }, 'test'> = { id: 'test-middleware', - test: async ({ input, nextHandler }) => { - return nextHandler(input); + handlers: { + test: async ({ state, next }) => { + return next(state); + }, }, }; - const middleware2: Middleware<{ value: number }> = { + const middleware2: Middleware<{ value: number }, 'test'> = { id: 'test-middleware', - test: async ({ input, nextHandler }) => { - return nextHandler({ ...input, state: { value: input.state.value + 1 } }); + handlers: { + test: async ({ state, next }) => { + return next({ ...state, value: state.value + 1 }); + }, }, }; @@ -93,17 +105,21 @@ describe('MiddlewareExecutor', () => { }); it('should add new middleware if id does not exist', () => { - const middleware1: Middleware<{ value: number }> = { + const middleware1: Middleware<{ value: number }, 'test'> = { id: 'test-middleware-1', - test: async ({ input, nextHandler }) => { - return nextHandler(input); + handlers: { + test: async ({ state, next }) => { + return next(state); + }, }, }; - const middleware2: Middleware<{ value: number }> = { + const middleware2: Middleware<{ value: number }, 'test'> = { id: 'test-middleware-2', - test: async ({ input, nextHandler }) => { - return nextHandler(input); + handlers: { + test: async ({ state, next }) => { + return next(state); + }, }, }; @@ -118,10 +134,12 @@ describe('MiddlewareExecutor', () => { }); it('should return the executor for chaining', () => { - const middleware: Middleware<{ value: number }> = { + const middleware: Middleware<{ value: number }, 'test'> = { id: 'test-middleware', - test: async ({ input, nextHandler }) => { - return nextHandler(input); + handlers: { + test: async ({ state, next }) => { + return next(state); + }, }, }; @@ -132,24 +150,30 @@ describe('MiddlewareExecutor', () => { describe('insert', () => { it('should insert middleware after specified middleware', () => { - const middleware1: Middleware<{ value: number }> = { + const middleware1: Middleware<{ value: number }, 'test'> = { id: 'middleware-1', - test: async ({ input, nextHandler }) => { - return nextHandler(input); + handlers: { + test: async ({ state, next }) => { + return next(state); + }, }, }; - const middleware2: Middleware<{ value: number }> = { + const middleware2: Middleware<{ value: number }, 'test'> = { id: 'middleware-2', - test: async ({ input, nextHandler }) => { - return nextHandler(input); + handlers: { + test: async ({ state, next }) => { + return next(state); + }, }, }; - const middleware3: Middleware<{ value: number }> = { + const middleware3: Middleware<{ value: number }, 'test'> = { id: 'middleware-3', - test: async ({ input, nextHandler }) => { - return nextHandler(input); + handlers: { + test: async ({ state, next }) => { + return next(state); + }, }, }; @@ -167,24 +191,30 @@ describe('MiddlewareExecutor', () => { }); it('should insert middleware before specified middleware', () => { - const middleware1: Middleware<{ value: number }> = { + const middleware1: Middleware<{ value: number }, 'test'> = { id: 'middleware-1', - test: async ({ input, nextHandler }) => { - return nextHandler(input); + handlers: { + test: async ({ state, next }) => { + return next(state); + }, }, }; - const middleware2: Middleware<{ value: number }> = { + const middleware2: Middleware<{ value: number }, 'test'> = { id: 'middleware-2', - test: async ({ input, nextHandler }) => { - return nextHandler(input); + handlers: { + test: async ({ state, next }) => { + return next(state); + }, }, }; - const middleware3: Middleware<{ value: number }> = { + const middleware3: Middleware<{ value: number }, 'test'> = { id: 'middleware-3', - test: async ({ input, nextHandler }) => { - return nextHandler(input); + handlers: { + test: async ({ state, next }) => { + return next(state); + }, }, }; @@ -202,24 +232,30 @@ describe('MiddlewareExecutor', () => { }); it('should remove existing middleware with the same id if unique is true', () => { - const middleware1: Middleware<{ value: number }> = { + const middleware1: Middleware<{ value: number }, 'test'> = { id: 'middleware-1', - test: async ({ input, nextHandler }) => { - return nextHandler(input); + handlers: { + test: async ({ state, next }) => { + return next(state); + }, }, }; - const middleware2: Middleware<{ value: number }> = { + const middleware2: Middleware<{ value: number }, 'test'> = { id: 'middleware-2', - test: async ({ input, nextHandler }) => { - return nextHandler(input); + handlers: { + test: async ({ state, next }) => { + return next(state); + }, }, }; - const middleware2Updated: Middleware<{ value: number }> = { + const middleware2Updated: Middleware<{ value: number }, 'test'> = { id: 'middleware-2', - test: async ({ input, nextHandler }) => { - return nextHandler({ ...input, state: { value: input.state.value + 1 } }); + handlers: { + test: async ({ state, next }) => { + return next({ ...state, value: state.value + 1 }); + }, }, }; @@ -236,17 +272,21 @@ describe('MiddlewareExecutor', () => { }); it('should return the executor for chaining', () => { - const middleware1: Middleware<{ value: number }> = { + const middleware1: Middleware<{ value: number }, 'test'> = { id: 'middleware-1', - test: async ({ input, nextHandler }) => { - return nextHandler(input); + handlers: { + test: async ({ state, next }) => { + return next(state); + }, }, }; - const middleware2: Middleware<{ value: number }> = { + const middleware2: Middleware<{ value: number }, 'test'> = { id: 'middleware-2', - test: async ({ input, nextHandler }) => { - return nextHandler(input); + handlers: { + test: async ({ state, next }) => { + return next(state); + }, }, }; @@ -261,24 +301,30 @@ describe('MiddlewareExecutor', () => { describe('setOrder', () => { it('should reorder middleware based on provided order', () => { - const middleware1: Middleware<{ value: number }> = { + const middleware1: Middleware<{ value: number }, 'test'> = { id: 'middleware-1', - test: async ({ input, nextHandler }) => { - return nextHandler(input); + handlers: { + test: async ({ state, next }) => { + return next(state); + }, }, }; - const middleware2: Middleware<{ value: number }> = { + const middleware2: Middleware<{ value: number }, 'test'> = { id: 'middleware-2', - test: async ({ input, nextHandler }) => { - return nextHandler(input); + handlers: { + test: async ({ state, next }) => { + return next(state); + }, }, }; - const middleware3: Middleware<{ value: number }> = { + const middleware3: Middleware<{ value: number }, 'test'> = { id: 'middleware-3', - test: async ({ input, nextHandler }) => { - return nextHandler(input); + handlers: { + test: async ({ state, next }) => { + return next(state); + }, }, }; @@ -295,17 +341,21 @@ describe('MiddlewareExecutor', () => { }); it('should filter out middleware that does not exist in the order', () => { - const middleware1: Middleware<{ value: number }> = { + const middleware1: Middleware<{ value: number }, 'test'> = { id: 'middleware-1', - test: async ({ input, nextHandler }) => { - return nextHandler(input); + handlers: { + test: async ({ state, next }) => { + return next(state); + }, }, }; - const middleware2: Middleware<{ value: number }> = { + const middleware2: Middleware<{ value: number }, 'test'> = { id: 'middleware-2', - test: async ({ input, nextHandler }) => { - return nextHandler(input); + handlers: { + test: async ({ state, next }) => { + return next(state); + }, }, }; @@ -323,131 +373,164 @@ describe('MiddlewareExecutor', () => { describe('execute', () => { it('should execute middleware chain in order', async () => { - const middleware1: Middleware<{ value: number }> = { + const middleware1: Middleware<{ value: number }, 'test'> = { id: 'middleware-1', - test: async ({ input, nextHandler }) => { - return nextHandler({ ...input, state: { value: input.state.value + 1 } }); + handlers: { + test: async ({ state, next }) => { + return next({ ...state, value: state.value + 1 }); + }, }, }; - const middleware2: Middleware<{ value: number }> = { + const middleware2: Middleware<{ value: number }, 'test'> = { id: 'middleware-2', - test: async ({ input, nextHandler }) => { - return nextHandler({ ...input, state: { value: input.state.value * 2 } }); + handlers: { + test: async ({ state, next }) => { + return next({ ...state, value: state.value * 2 }); + }, }, }; executor.use([middleware1, middleware2]); - const result = await executor.execute('test', { state: { value: 5 } }); + const result = await executor.execute({ + eventName: 'test', + initialValue: { value: 5 }, + }); expect(result.state.value).toBe(12); // (5 + 1) * 2 }); it('should skip middleware that does not have the specified event handler', async () => { - const middleware1: Middleware<{ value: number }> = { + const middleware1: Middleware<{ value: number }, 'test'> = { id: 'middleware-1', - test: async ({ input, nextHandler }) => { - return nextHandler({ ...input, state: { value: input.state.value + 1 } }); + handlers: { + test: async ({ state, next }) => { + return next({ ...state, value: state.value + 1 }); + }, }, }; - const middleware2: Middleware<{ value: number }> = { + const middleware2: Middleware<{ value: number }, 'test'> = { id: 'middleware-2', - testX: async ({ input, nextHandler }) => { - return nextHandler({ ...input, state: { value: input.state.value - 2 } }); + handlers: { + testX: async ({ state, next }) => { + return next({ ...state, value: state.value - 2 }); + }, }, }; - const middleware3: Middleware<{ value: number }> = { + const middleware3: Middleware<{ value: number }, 'test'> = { id: 'middleware-3', - test: async ({ input, nextHandler }) => { - return nextHandler({ ...input, state: { value: input.state.value * 2 } }); + handlers: { + test: async ({ state, next }) => { + return next({ ...state, value: state.value * 2 }); + }, }, }; executor.use([middleware1, middleware2, middleware3]); - const result = await executor.execute('test', { state: { value: 5 } }); + const result = await executor.execute({ + eventName: 'test', + initialValue: { value: 5 }, + }); expect(result.state.value).toBe(12); // (5 + 1) * 2 }); it('should handle middleware that returns complete status', async () => { - const middleware1: Middleware<{ value: number }> = { + const middleware1: Middleware<{ value: number }, 'test'> = { id: 'middleware-1', - test: async ({ input, nextHandler }) => { - return nextHandler({ - ...input, - state: { value: input.state.value + 1 }, - status: 'complete' as MiddlewareStatus, - }); + handlers: { + test: async ({ state, complete }) => { + return complete({ + ...state, + value: state.value + 1, + }); + }, }, }; - const middleware2: Middleware<{ value: number }> = { + const middleware2: Middleware<{ value: number }, 'test'> = { id: 'middleware-2', - test: async ({ input, nextHandler }) => { - return nextHandler({ ...input, state: { value: input.state.value * 2 } }); + handlers: { + test: async ({ state, next }) => { + return next({ ...state, value: state.value * 2 }); + }, }, }; executor.use([middleware1, middleware2]); - const result = await executor.execute('test', { state: { value: 5 } }); + const result = await executor.execute({ + eventName: 'test', + initialValue: { value: 5 }, + }); expect(result.state.value).toBe(6); // 5 + 1, middleware2 is not executed expect(result.status).toBe('complete'); }); it('should handle middleware that returns discard status', async () => { - const middleware1: Middleware<{ value: number }> = { + const middleware1: Middleware<{ value: number }, 'test'> = { id: 'middleware-1', - test: async ({ input, nextHandler }) => { - return nextHandler({ - ...input, - state: { value: input.state.value + 1 }, - status: 'discard' as MiddlewareStatus, - }); + handlers: { + test: async ({ discard }) => { + return discard(); + }, }, }; - const middleware2: Middleware<{ value: number }> = { + const middleware2: Middleware<{ value: number }, 'test'> = { id: 'middleware-2', - test: async ({ input, nextHandler }) => { - return nextHandler({ ...input, state: { value: input.state.value * 2 } }); + handlers: { + test: async ({ state, next }) => { + return next({ ...state, value: state.value * 2 }); + }, }, }; executor.use([middleware1, middleware2]); - const result = await executor.execute('test', { state: { value: 5 } }); + const result = await executor.execute({ + eventName: 'test', + initialValue: { value: 5 }, + }); - expect(result.state.value).toBe(6); // 5 + 1, middleware2 is not executed + expect(result.state.value).toBe(5); // 5 - middleware discards with the current state and middleware2 is not executed expect(result.status).toBe('discard'); }); it('should handle concurrent execute calls by discarding the first one', async () => { // Create a middleware that delays execution - const middleware: Middleware<{ value: number }> = { + const middleware: Middleware<{ value: number }, 'test'> = { id: 'delayed-middleware', - test: async ({ input, nextHandler }) => { - // Simulate a longer delay to ensure the first execution is still in progress - await new Promise((resolve) => setTimeout(resolve, 500)); - return nextHandler({ ...input, state: { value: input.state.value + 1 } }); + handlers: { + test: async ({ state, next }) => { + // Simulate a longer delay to ensure the first execution is still in progress + await new Promise((resolve) => setTimeout(resolve, 500)); + return next({ ...state, value: state.value + 1 }); + }, }, }; executor.use(middleware); // Start the first execution - const firstExecution = executor.execute('test', { state: { value: 5 } }); + const firstExecution = executor.execute({ + eventName: 'test', + initialValue: { value: 5 }, + }); // Wait a short time to ensure the first execution has started but not completed await new Promise((resolve) => setTimeout(resolve, 100)); // Start the second execution before the first one completes - const secondExecution = executor.execute('test', { state: { value: 10 } }); + const secondExecution = executor.execute({ + eventName: 'test', + initialValue: { value: 10 }, + }); // Wait for both executions to complete const [firstResult, secondResult] = await Promise.all([ @@ -463,45 +546,33 @@ describe('MiddlewareExecutor', () => { expect(secondResult.state.value).toBe(11); // 10 + 1 }); - it('should handle middleware that calls nextHandler multiple times', async () => { - const middleware1: Middleware<{ value: number }> = { - id: 'middleware-1', - test: async ({ input, nextHandler }) => { - const result1 = await nextHandler({ - ...input, - state: { value: input.state.value + 1 }, - }); - // This should throw an error - return nextHandler(result1); - }, - }; - - executor.use([middleware1]); - - await expect(executor.execute('test', { state: { value: 5 } })).rejects.toThrow( - 'next() called multiple times', - ); - }); - it('should handle concurrent execute calls with different event names', async () => { // Create middleware that handles different event names - const middleware: Middleware<{ value: number }> = { + const middleware: Middleware<{ value: number }, 'test1' | 'test2'> = { id: 'multi-event-middleware', - test1: async ({ input, nextHandler }) => { - await new Promise((resolve) => setTimeout(resolve, 100)); - return nextHandler({ ...input, state: { value: input.state.value + 1 } }); - }, - test2: async ({ input, nextHandler }) => { - await new Promise((resolve) => setTimeout(resolve, 100)); - return nextHandler({ ...input, state: { value: input.state.value * 2 } }); + handlers: { + test1: async ({ state, next }) => { + await new Promise((resolve) => setTimeout(resolve, 100)); + return next({ ...state, value: state.value + 1 }); + }, + test2: async ({ state, next }) => { + await new Promise((resolve) => setTimeout(resolve, 100)); + return next({ ...state, value: state.value * 2 }); + }, }, }; executor.use(middleware); // Start executions with different event names - const firstExecution = executor.execute('test1', { state: { value: 5 } }); - const secondExecution = executor.execute('test2', { state: { value: 10 } }); + const firstExecution = executor.execute({ + eventName: 'test1', + initialValue: { value: 5 }, + }); + const secondExecution = executor.execute({ + eventName: 'test2', + initialValue: { value: 10 }, + }); // Wait for both executions to complete const [firstResult, secondResult] = await Promise.all([ @@ -521,31 +592,35 @@ describe('MiddlewareExecutor', () => { const results: number[] = []; // Create two different middleware executors - const executor1 = new MiddlewareExecutor<{ value: number }>(); - const executor2 = new MiddlewareExecutor<{ value: number }>(); + const executor1 = new MiddlewareExecutor<{ value: number }, 'test'>(); + const executor2 = new MiddlewareExecutor<{ value: number }, 'test'>(); // Add middleware to each executor executor1.use({ id: 'middleware-1', - test: async ({ input, nextHandler }) => { - await new Promise((resolve) => setTimeout(resolve, 50)); - results.push(1); - return nextHandler({ ...input, state: { value: input.state.value + 1 } }); + handlers: { + test: async ({ state, next }) => { + await new Promise((resolve) => setTimeout(resolve, 50)); + results.push(1); + return next({ ...state, value: state.value + 1 }); + }, }, }); executor2.use({ id: 'middleware-2', - test: async ({ input, nextHandler }) => { - await new Promise((resolve) => setTimeout(resolve, 50)); - results.push(2); - return nextHandler({ ...input, state: { value: input.state.value * 2 } }); + handlers: { + test: async ({ state, next }) => { + await new Promise((resolve) => setTimeout(resolve, 50)); + results.push(2); + return next({ ...state, value: state.value * 2 }); + }, }, }); // Execute the same event name on different executors concurrently - const p1 = executor1.execute('test', { state: { value: 5 } }); - const p2 = executor2.execute('test', { state: { value: 10 } }); + const p1 = executor1.execute({ eventName: 'test', initialValue: { value: 5 } }); + const p2 = executor2.execute({ eventName: 'test', initialValue: { value: 10 } }); // Wait for both executions to complete const [r1, r2] = await Promise.all([p1, p2]); From f1d73fd12c86ccf307fab47c51d39c71bad66370 Mon Sep 17 00:00:00 2001 From: Anton Arnautov <43254280+arnautov-anton@users.noreply.github.com> Date: Tue, 6 May 2025 14:15:17 +0200 Subject: [PATCH 47/47] fix(qa): adjust Event & ChannelData types (#1524) --- src/types.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/types.ts b/src/types.ts index 2df9cc2197..37d4a54533 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1464,6 +1464,8 @@ export type Event = CustomEventData & { user?: UserResponse; user_id?: string; watcher_count?: number; + channel_last_message_at?: string; + app?: Record; // TODO: further specify type }; export type UserCustomEvent = CustomEventData & { @@ -2346,6 +2348,8 @@ export type ChannelData = CustomChannelData & created_by: UserResponse | null; created_by_id: UserResponse['id']; members: string[] | Array; + blocklist_behavior: AutomodBehavior; + automod: Automod; }>; export type ChannelMute = {