├── .browserslistrc ├── .commitlintrc.js ├── .github └── workflows │ ├── gh-pages.yml │ └── test.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.js ├── CHANGELOG.md ├── LICENSE ├── babel.config.js ├── build ├── common.config.js ├── rollup.config.base.js └── rollup.config.comps.js ├── jest.config.js ├── jsr.json ├── package.json ├── packages ├── abortableAsync │ ├── demo.vue │ ├── demo2.vue │ ├── index.test.ts │ ├── index.ts │ └── readme.md ├── concurrentAsync │ ├── demo.vue │ ├── index.test.ts │ ├── index.ts │ └── readme.md ├── debounceAsync │ ├── demo.vue │ ├── index.test.ts │ ├── index.ts │ └── readme.md ├── debounceAsyncResult │ ├── demo.vue │ ├── index.test.ts │ ├── index.ts │ └── readme.md ├── functions ├── index.ts ├── throttleAsyncResult │ ├── demo.vue │ ├── demo2.vue │ ├── index.test.ts │ ├── index.ts │ └── readme.md └── withRetryAsync │ ├── demo.vue │ ├── index.test.ts │ ├── index.ts │ ├── readme.md │ └── utils.ts ├── patches └── vitepress+0.21.4.patch ├── readme.md ├── scripts ├── check-dist-tag.js ├── check-version-bump.js ├── incremental-test.js └── utils.js ├── tsconfig.build.json ├── tsconfig.json ├── typings ├── css-module-shim.d.ts ├── vue-shim.d.ts └── vue-test-utils.d.ts ├── website ├── .vitepress │ ├── config.js │ ├── config.ts │ ├── markdown │ │ └── plugin │ │ │ ├── build-demos.vite.config.ts │ │ │ ├── genIframe.ts │ │ │ ├── markdown-it-demo.ts │ │ │ └── vite-plugin-dev-demo-iframe.ts │ └── theme │ │ ├── DemoContainer.vue │ │ ├── custom.scss │ │ ├── icons │ │ ├── caret-top.svg │ │ ├── copy.svg │ │ ├── new_tab.svg │ │ ├── reload.svg │ │ ├── source_code.svg │ │ └── success.svg │ │ ├── index.jsx │ │ └── useCopy.ts ├── changelog.md ├── functions ├── guide │ └── getting-started.md ├── index.md └── vite.config.ts └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | not ie 11 5 | not op_mini all 6 | -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: gh-pages 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | build-and-deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | 14 | - name: Set up nodejs 15 | uses: actions/setup-node@v2 16 | with: 17 | node-version: '14' 18 | cache: 'yarn' 19 | - run: yarn 20 | - run: yarn run build:docs 21 | - name: Deploy 🚀 22 | uses: JamesIves/github-pages-deploy-action@v4.2.5 23 | with: 24 | branch: gh-pages # The branch the action should deploy to. 25 | folder: docs # The folder the action should deploy. -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | test: 9 | # concurrency: ci-${{ github.ref }} # Recommended if you intend to make multiple deployments in quick succession. 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | 15 | - name: Set up nodejs 16 | uses: actions/setup-node@v2 17 | with: 18 | node-version: '14' 19 | cache: 'yarn' 20 | - run: yarn 21 | - run: yarn run test 22 | - name: Coveralls 23 | uses: coverallsapp/github-action@master 24 | with: 25 | github-token: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | .DS_Store 4 | dist/ 5 | *.local 6 | lerna-debug.json 7 | lerna-debug.log 8 | yarn-error.log 9 | storybook-static 10 | coverage/ 11 | cjs/ 12 | es/ 13 | umd/ 14 | docs/ 15 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example/ 2 | build/ 3 | docs/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | changelog.md 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'all', 4 | printWidth: 120, 5 | proseWrap: 'never', 6 | endOfLine: 'lf', 7 | htmlWhitespaceSensitivity: 'ignore', 8 | vueIndentScriptAndStyle: true, 9 | }; 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.5.4](https://github.com/bowencool/async-utilities/compare/v2.5.3...v2.5.4) (2022-09-08) 2 | 3 | 4 | ### Features 5 | 6 | * rename to @bowencool/async-utilities ([5701b52](https://github.com/bowencool/async-utilities/commit/5701b521282416d862e3289794d8583edbf6b6d5)) 7 | 8 | 9 | 10 | ## [2.5.3](https://github.com/bowencool/async-utilities/compare/v2.5.2...v2.5.3) (2022-06-21) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * version ([af987c5](https://github.com/bowencool/async-utilities/commit/af987c53cee50393cf6ee708841a4a4c6f4991c5)) 16 | 17 | 18 | 19 | ## [2.5.2](https://github.com/bowencool/async-utilities/compare/v2.5.1...v2.5.2) (2022-06-21) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * node version ([2c8be7f](https://github.com/bowencool/async-utilities/commit/2c8be7f7bd90f3ea89a383cf2a1ec8d43a4582cf)) 25 | 26 | 27 | 28 | ## [2.5.1](https://github.com/bowencool/async-utilities/compare/v2.5.0...v2.5.1) (2022-04-06) 29 | 30 | 31 | 32 | # [2.5.0](https://github.com/bowencool/async-utilities/compare/v2.4.1...v2.5.0) (2022-04-06) 33 | 34 | 35 | ### Features 36 | 37 | * add umd bundle to support cdn ([ccce23f](https://github.com/bowencool/async-utilities/commit/ccce23fd7dc047b6fad1cef3469a69b1682aa245)) 38 | 39 | 40 | 41 | ## [2.4.1](https://github.com/bowencool/async-utilities/compare/v2.4.0...v2.4.1) (2022-04-05) 42 | 43 | 44 | 45 | # [2.4.0](https://github.com/bowencool/async-utilities/compare/v2.3.2...v2.4.0) (2022-04-04) 46 | 47 | 48 | ### Features 49 | 50 | * **debounceAsyncResult:** do the same for rejection as for resolution ([1f05064](https://github.com/bowencool/async-utilities/commit/1f05064b8d2975b26c8145851e18ff68fa23beeb)) 51 | 52 | 53 | 54 | ## [2.3.2](https://github.com/bowencool/async-utilities/compare/v2.3.1...v2.3.2) (2022-04-04) 55 | 56 | 57 | ### Bug Fixes 58 | 59 | * **abortableAsync:** 完善逻辑 ([62e02e9](https://github.com/bowencool/async-utilities/commit/62e02e9efaaf0bebe6a9868386be16f95d754e48)) 60 | 61 | 62 | 63 | ## [2.3.1](https://github.com/bowencool/async-utilities/compare/v2.3.0...v2.3.1) (2022-03-27) 64 | 65 | 66 | 67 | # [2.3.0](https://github.com/bowencool/async-utilities/compare/v2.2.2...v2.3.0) (2022-03-25) 68 | 69 | 70 | ### Features 71 | 72 | * **abortableAsync:** alwaysPendingWhenAborted ([c5fcb52](https://github.com/bowencool/async-utilities/commit/c5fcb52d93b1fe0f74cd57ca2202d53f64c813ae)) 73 | 74 | 75 | 76 | ## [2.2.2](https://github.com/bowencool/async-utilities/compare/v2.2.1...v2.2.2) (2022-03-25) 77 | 78 | 79 | ### Performance Improvements 80 | 81 | * customError name ([9a87f0f](https://github.com/bowencool/async-utilities/commit/9a87f0fd23801037410a6edc7773dd3987986baa)) 82 | 83 | 84 | ### Reverts 85 | 86 | * Revert "ci: debug" ([deb0eff](https://github.com/bowencool/async-utilities/commit/deb0eff2ac1bd2b8d0c0a02469843de6db16b22d)) 87 | 88 | 89 | 90 | ## [2.2.1](https://github.com/bowencool/async-utils/compare/v2.2.0...v2.2.1) (2021-09-26) 91 | 92 | 93 | ### Features 94 | 95 | * error name ([0eb1ebb](https://github.com/bowencool/async-utils/commit/0eb1ebb14c2fe04c44fba4c3918e30ad3139e5f6)) 96 | 97 | 98 | 99 | # [2.2.0](https://github.com/bowencool/async-utils/compare/v2.1.0...v2.2.0) (2021-09-24) 100 | 101 | 102 | ### Features 103 | 104 | * **withRetryAsync:** onFailed,onRetry ([9dbaab3](https://github.com/bowencool/async-utils/commit/9dbaab3d6dbb8f62ef07e06a0ae9fb7488419783)) 105 | 106 | 107 | 108 | # [2.1.0](https://github.com/bowencool/async-utils/compare/v2.0.0...v2.1.0) (2021-09-24) 109 | 110 | 111 | 112 | # [2.0.0](https://github.com/bowencool/async-utils/compare/v2.0.0-1...v2.0.0) (2021-09-23) 113 | 114 | 115 | 116 | # [2.0.0-1](https://github.com/bowencool/async-utils/compare/v2.0.0-0...v2.0.0-1) (2021-09-22) 117 | 118 | 119 | 120 | # [2.0.0-0](https://github.com/bowencool/async-utils/compare/v1.0.2...v2.0.0-0) (2021-09-22) 121 | 122 | 123 | ### Features 124 | 125 | * **debounceAsyncResult:** rename debounceAsync to debounceAsyncResult ([5c49c26](https://github.com/bowencool/async-utils/commit/5c49c26763de7d8f60546edd4a111924714884be)) 126 | 127 | 128 | 129 | ## [1.0.2](https://github.com/bowencool/async-utils/compare/v1.0.1...v1.0.2) (2021-09-22) 130 | 131 | 132 | ### Features 133 | 134 | * **withRetryAsync:** lastFailedReason ([90fbd46](https://github.com/bowencool/async-utils/commit/90fbd46086189387080802ba6e28a0da648876a4)) 135 | 136 | 137 | 138 | ## [1.0.1](https://github.com/bowencool/async-utils/compare/v1.0.0...v1.0.1) (2021-09-22) 139 | 140 | 141 | 142 | # [1.0.0](https://github.com/bowencool/async-utils/compare/v0.7.1...v1.0.0) (2021-09-22) 143 | 144 | 145 | ### Features 146 | 147 | * rename ([7ee748b](https://github.com/bowencool/async-utils/commit/7ee748b7d4e312b0b2f45e3e3742c804270f6b47)) 148 | 149 | 150 | 151 | ## [0.7.1](https://github.com/bowencool/async-utilities/compare/v0.7.0...v0.7.1) (2021-09-15) 152 | 153 | 154 | 155 | # [0.7.0](https://github.com/bowencool/async-utilities/compare/v0.6.2...v0.7.0) (2021-09-15) 156 | 157 | 158 | ### Features 159 | 160 | * onRetry ([893d944](https://github.com/bowencool/async-utilities/commit/893d94476c76592210f3725c637700099920fc87)) 161 | * rename cancelizeAsync to abortableAsync ([deef179](https://github.com/bowencool/async-utilities/commit/deef179a85af7cd6f1496bdc00d84485915b3aa1)) 162 | * withRetryAsync ([b2b98a8](https://github.com/bowencool/async-utilities/commit/b2b98a8320c237a87ae0a6cd231bc4d0e089ca29)) 163 | 164 | 165 | 166 | ## [0.6.3](https://github.com/bowencool/async-utilities/compare/v0.6.2...v0.6.3) (2021-09-15) 167 | 168 | 169 | 170 | ## [0.6.2](https://github.com/bowencool/async-utilities/compare/v0.6.1...v0.6.2) (2021-09-12) 171 | 172 | 173 | 174 | ## [0.6.1](https://github.com/bowencool/async-utilities/compare/v0.6.0...v0.6.1) (2021-09-12) 175 | 176 | 177 | 178 | # [0.6.0](https://github.com/bowencool/async-utilities/compare/v0.4.0...v0.6.0) (2021-09-12) 179 | 180 | 181 | ### Features 182 | 183 | * useSamePromise ([a4976c6](https://github.com/bowencool/async-utilities/commit/a4976c68fba5062f70415e223fbeebb98f86f334)) 184 | 185 | 186 | 187 | # [0.5.0](https://github.com/bowencool/async-utilities/compare/v0.4.0...v0.5.0) (2021-09-12) 188 | 189 | 190 | ### Features 191 | 192 | * useSamePromise ([a4976c6](https://github.com/bowencool/async-utilities/commit/a4976c68fba5062f70415e223fbeebb98f86f334)) 193 | 194 | 195 | 196 | # [0.4.0](https://github.com/bowencool/async-utilities/compare/v0.3.2...v0.4.0) (2021-05-31) 197 | 198 | 199 | 200 | ## [0.3.2](https://github.com/bowencool/async-utilities/compare/v0.3.1...v0.3.2) (2021-05-31) 201 | 202 | 203 | ### Features 204 | 205 | * add cancelizeAsync ([898b822](https://github.com/bowencool/async-utilities/commit/898b8225114c5c0d3ab3d3467d277ccb30b42af8)) 206 | 207 | 208 | 209 | ## [0.3.1](https://github.com/bowencool/async-utilities/compare/v0.3.0...v0.3.1) (2021-05-31) 210 | 211 | 212 | 213 | # [0.3.0](https://github.com/bowencool/async-utilities/compare/v0.2.0...v0.3.0) (2021-05-30) 214 | 215 | 216 | ### Bug Fixes 217 | 218 | * docs ([63e0b3a](https://github.com/bowencool/async-utilities/commit/63e0b3a379c8715a22f175a28176159bed9dff87)) 219 | 220 | 221 | ### Features 222 | 223 | * add concurrentAsync ([a59f223](https://github.com/bowencool/async-utilities/commit/a59f223e9cfbabf346e6c0af8feca3aea0aa0ed8)) 224 | 225 | 226 | 227 | # 0.2.0 (2021-05-30) 228 | 229 | 230 | 231 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 博文 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // console.log('babel config'); 2 | const { resolve } = require('path'); 3 | const { compilerOptions } = require('./tsconfig.json'); 4 | 5 | const projectRootDir = __dirname; 6 | 7 | const alias = {}; 8 | 9 | Object.entries(compilerOptions.paths).forEach(([key, [value]]) => { 10 | alias[key.replace(/\/\*$/, '')] = resolve(projectRootDir, compilerOptions.baseUrl || '.', value.replace(/\/\*$/, '')); 11 | }); 12 | 13 | module.exports = { 14 | // exclude: '**/node_modules/**', 15 | presets: [ 16 | [ 17 | '@babel/env', 18 | { 19 | corejs: 3, 20 | useBuiltIns: 'usage', 21 | loose: true, 22 | modules: process.env.NODE_ENV === 'test' && 'auto', 23 | }, 24 | ], 25 | '@babel/typescript', 26 | ], 27 | plugins: [ 28 | [ 29 | '@babel/plugin-transform-runtime', 30 | { 31 | corejs: 3, 32 | version: '^7.15.3', 33 | }, 34 | ], 35 | [ 36 | '@vue/babel-plugin-jsx', 37 | { 38 | enableObjectSlots: true, 39 | }, 40 | ], 41 | [ 42 | 'module-resolver', 43 | { 44 | root: ['./'], 45 | alias, 46 | }, 47 | ], 48 | ], 49 | }; 50 | -------------------------------------------------------------------------------- /build/common.config.js: -------------------------------------------------------------------------------- 1 | import pkg from '../package.json'; 2 | 3 | export const replacement = { 4 | 'process.env.npm_package_version': JSON.stringify(process.env.npm_package_version || pkg.version), 5 | // 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 6 | }; 7 | 8 | // export const alias 9 | -------------------------------------------------------------------------------- /build/rollup.config.base.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import babel from '@rollup/plugin-babel'; 3 | import typescript from 'rollup-plugin-typescript2'; 4 | import replace from '@rollup/plugin-replace'; 5 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 6 | import pkg from '../package.json'; 7 | import { replacement } from './common.config'; 8 | 9 | export const INPUT_PATH = resolve(__dirname, '../packages'); 10 | 11 | const deps = Object.keys(pkg.peerDependencies || {}) 12 | .concat(Object.keys(pkg.dependencies)) 13 | .concat(Object.keys(pkg.devDependencies)) 14 | .concat([/node_modules/]); 15 | 16 | const genBaseConfig = ({ ts, plugins = [] } = {}) => { 17 | /** 18 | * @type {import('rollup').RollupOptions} 19 | */ 20 | const config = { 21 | external: deps, 22 | plugins: [ 23 | replace({ values: replacement, preventAssignment: true }), 24 | typescript({ ...ts, tsconfig: resolve(__dirname, '../tsconfig.build.json') }), 25 | babel({ 26 | babelHelpers: 'runtime', 27 | skipPreflightCheck: true, 28 | extensions: ['.js', '.mjs', '.jsx', '.ts', '.tsx'], 29 | }), 30 | ...plugins, 31 | nodeResolve(), 32 | ], 33 | }; 34 | return config; 35 | }; 36 | 37 | export default genBaseConfig; 38 | -------------------------------------------------------------------------------- /build/rollup.config.comps.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'rollup'; 2 | import genBaseConfig, { INPUT_PATH } from './rollup.config.base'; 3 | import { terser } from 'rollup-plugin-terser'; 4 | 5 | const name = 'asyncUtilities'; 6 | const banner = `/*! 7 | * ${process.env.npm_package_name} v${process.env.npm_package_version} 8 | */`; 9 | 10 | const componentsConfig = defineConfig([ 11 | { 12 | ...genBaseConfig(), 13 | input: `${INPUT_PATH}/index.ts`, 14 | output: [ 15 | { 16 | // file: 'es/index.js', 17 | dir: 'es', 18 | format: 'es', 19 | preserveModules: true, 20 | preserveModulesRoot: 'packages', 21 | banner, 22 | name, 23 | }, 24 | ], 25 | }, 26 | { 27 | ...genBaseConfig(), 28 | input: `${INPUT_PATH}/index.ts`, 29 | output: [ 30 | { 31 | // file: 'es/index.js', 32 | dir: 'cjs', 33 | format: 'cjs', 34 | preserveModules: true, 35 | preserveModulesRoot: 'packages', 36 | banner, 37 | name, 38 | }, 39 | ], 40 | }, 41 | { 42 | ...genBaseConfig({ 43 | plugins: [terser()], 44 | // ts: { 45 | // useTsconfigDeclarationDir: false, 46 | // compilerOptions: { 47 | // declaration: false, 48 | // }, 49 | // }, 50 | }), 51 | input: `${INPUT_PATH}/index.ts`, 52 | output: [ 53 | { 54 | dir: 'umd', 55 | format: 'umd', 56 | sourcemap: true, 57 | banner, 58 | name, 59 | }, 60 | ], 61 | }, 62 | ]); 63 | 64 | export default componentsConfig; 65 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const babelConfig = require('./babel.config.js'); 2 | 3 | module.exports = { 4 | // preset: 'ts-jest', 5 | globals: {}, 6 | testEnvironment: 'jsdom', 7 | transform: { 8 | // '^.+\\.vue$': 'vue-jest', 9 | '^.+\\.[jt]sx?$': ['babel-jest', babelConfig], 10 | // '^.+\\.scss$': 'jest-scss-transform', 11 | }, 12 | moduleFileExtensions: ['js', 'json', 'ts', 'tsx', 'vue', 'jsx'], 13 | 14 | coverageThreshold: { 15 | // 所有文件总的覆盖率要求 16 | global: { 17 | branches: 60, 18 | lines: 85, 19 | functions: 70, 20 | statements: 80, 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /jsr.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bowencool/async-utilities", 3 | "version": "2.5.5", 4 | "exports": "./packages/index.ts" 5 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "engines": { 3 | "node": ">= 14" 4 | }, 5 | "scripts": { 6 | "dev": "NODE_ENV=development vitepress dev website", 7 | "build": "NODE_ENV=production rollup -c build/rollup.config.comps.js", 8 | "build:docs": "NODE_ENV=production vitepress build website && npm run build:demos", 9 | "build:demos": "vite build -c=website/.vitepress/markdown/plugin/build-demos.vite.config.ts", 10 | "predev": "check-versions && ([ -d es ] || npm run build) && patch-package", 11 | "test": "jest --coverage", 12 | "prebuild": "check-versions && rm -rf es cjs umd && patch-package", 13 | "prebuild:docs": "npm run build && patch-package", 14 | "postbuild:docs": "rm -rf docs/ && mv website/.vitepress/dist/ docs/", 15 | "preversion": "scripts/check-version-bump.js", 16 | "version": "conventional-changelog --skip-unstable -p angular -i CHANGELOG.md -s && git add CHANGELOG.md", 17 | "prepublishOnly": "scripts/check-dist-tag.js && npm run build", 18 | "postpublish": "git push && git push --tags" 19 | }, 20 | "files": [ 21 | "es", 22 | "cjs", 23 | "umd" 24 | ], 25 | "dependencies": { 26 | "core-js": "^3.21.1" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.17.8", 30 | "@babel/plugin-transform-runtime": "^7.16.5", 31 | "@babel/preset-env": "^7.16.5", 32 | "@babel/preset-typescript": "^7.16.5", 33 | "@babel/runtime": "^7.17.8", 34 | "@babel/runtime-corejs3": "^7.17.8", 35 | "@commitlint/cli": "^16.2.3", 36 | "@commitlint/config-conventional": "^16.2.1", 37 | "@rollup/plugin-babel": "^5.3.1", 38 | "@rollup/plugin-node-resolve": "^13.1.1", 39 | "@rollup/plugin-replace": "^3.0.0", 40 | "@types/jest": "26", 41 | "@types/markdown-it": "^12.2.3", 42 | "@types/sinon": "^10.0.6", 43 | "@vitejs/plugin-vue-jsx": "^1.3.8", 44 | "@vue/babel-plugin-jsx": "^1.1.1", 45 | "@vue/compiler-sfc": "^3.2.26", 46 | "@vueuse/core": "^8.1.2", 47 | "@vueuse/shared": "^8.1.2", 48 | "babel-jest": "26", 49 | "babel-plugin-module-resolver": "^4.1.0", 50 | "chalk": "^4.1.2", 51 | "check-versions-in-packagejson": "^1.2.5", 52 | "conventional-changelog-cli": "^2.1.1", 53 | "copy-rich-text": "^0.2.0", 54 | "coveralls": "^3.1.1", 55 | "dayjs": "^1.11.0", 56 | "fs-extra": "^10.0.1", 57 | "jest": "26", 58 | "lint-staged": "^12.3.7", 59 | "markdown-it": "^12.3.0", 60 | "markdown-it-checkbox": "^1.1.0", 61 | "markdown-it-container": "^3.0.0", 62 | "md5": "^2.3.0", 63 | "patch-package": "^6.4.7", 64 | "path-browserify": "^1.0.1", 65 | "postcss": "^8.4.12", 66 | "prettier": "^2.6.0", 67 | "rollup": "^2.61.1", 68 | "rollup-plugin-esbuild": "^4.8.2", 69 | "rollup-plugin-postcss": "^4.0.2", 70 | "rollup-plugin-terser": "^7.0.2", 71 | "rollup-plugin-typescript2": "^0.31.1", 72 | "rollup-plugin-vue": "^6.0.0", 73 | "sass": "^1.49.9", 74 | "semver": "^7.3.5", 75 | "sinon": "^12.0.1", 76 | "sucrase": "^3.20.3", 77 | "ts-jest": "26", 78 | "typescript": "^4.6.2", 79 | "vite": "^2.8.6", 80 | "vite-esbuild-typescript-checker": "^0.0.1-alpha.9", 81 | "vite-plugin-optimize-persist": "^0.1.1", 82 | "vite-plugin-package-config": "^0.1.0", 83 | "vitepress": "0.21.4", 84 | "vue": "^3.2.26", 85 | "vue-tsc": "^0.29.8", 86 | "yorkie": "^2.0.0" 87 | }, 88 | "peerDependencies": {}, 89 | "main": "cjs/index.js", 90 | "module": "es/index.js", 91 | "unpkg": "umd/index.js", 92 | "jsdelivr": "umd/index.js", 93 | "typings": "es/index.d.ts", 94 | "license": "MIT", 95 | "homepage": "https://github.com/bowencool/async-utilities", 96 | "keywords": [ 97 | "high order", 98 | "typescript", 99 | "async", 100 | "asynchronous", 101 | "promise", 102 | "utilities", 103 | "utils", 104 | "debounce", 105 | "throttle", 106 | "concurrent", 107 | "concurrently", 108 | "concurrent queue", 109 | "abort", 110 | "abortable", 111 | "cancel", 112 | "cancelable", 113 | "retry", 114 | "timeout" 115 | ], 116 | "description": "async utilities", 117 | "author": "bowencool", 118 | "name": "@bowencool/async-utilities", 119 | "version": "2.5.5", 120 | "gitHooks": { 121 | "pre-commit": "lint-staged && jest --onlyChanged --coverage", 122 | "post-merge": "jest --changedFilesWithAncestor --coverage", 123 | "commit-msg": "commitlint -Ve" 124 | }, 125 | "lint-staged": { 126 | "**/*.md": [ 127 | "prettier --write" 128 | ], 129 | "packages/**/*.{js,jsx,ts,tsx,vue}": [ 130 | "prettier --write" 131 | ], 132 | "packages/**/*.{vue,sass,scss,less,css}": [ 133 | "prettier --write" 134 | ] 135 | }, 136 | "vite": { 137 | "optimizeDeps": { 138 | "include": [ 139 | "@vueuse/shared", 140 | "copy-rich-text" 141 | ] 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /packages/abortableAsync/demo.vue: -------------------------------------------------------------------------------- 1 | 78 | -------------------------------------------------------------------------------- /packages/abortableAsync/demo2.vue: -------------------------------------------------------------------------------- 1 | 76 | -------------------------------------------------------------------------------- /packages/abortableAsync/index.test.ts: -------------------------------------------------------------------------------- 1 | import { abortableAsync, AbortError, TimeoutError } from './index'; 2 | 3 | jest.useFakeTimers(); 4 | 5 | function flushPromises() { 6 | return new Promise((resolve) => setImmediate(resolve)); 7 | } 8 | 9 | function resolveInNms(delay = 1000, fail?: boolean): Promise { 10 | return new Promise((resolve, reject) => { 11 | setTimeout(() => { 12 | if (fail) { 13 | reject('error'); 14 | } else { 15 | resolve(`The result in ${delay}ms`); 16 | } 17 | }, delay); 18 | }); 19 | } 20 | 21 | describe('abortableAsync', () => { 22 | test('basic', async () => { 23 | const fn = abortableAsync(resolveInNms); 24 | const p = fn(1000); 25 | // “快进”时间使得定时器回调被执行 26 | jest.advanceTimersByTime(1000); 27 | await expect(p).resolves.toBe('The result in 1000ms'); 28 | 29 | const p2 = fn(1000, true); 30 | jest.advanceTimersByTime(1000); 31 | await expect(p2).rejects.toBe('error'); 32 | }); 33 | test('timeout', async () => { 34 | const fn = abortableAsync(resolveInNms, { timeout: 2000 }); 35 | const p = fn(1000); 36 | jest.advanceTimersByTime(1000); 37 | await expect(p).resolves.toBe('The result in 1000ms'); 38 | 39 | const p2 = fn(2000); 40 | jest.advanceTimersByTime(2000); 41 | await expect(p2).rejects.toBeInstanceOf(TimeoutError); 42 | await expect(p2).rejects.toMatchObject({ name: 'TimeoutError' }); 43 | }); 44 | test('signal', async () => { 45 | // controller.abort() 只能触发一次 abort 事件 46 | const controller = new AbortController(); 47 | const fn = abortableAsync(resolveInNms, { signal: controller.signal }); 48 | const p = fn(1000); 49 | jest.advanceTimersByTime(1000); 50 | await expect(p).resolves.toBe('The result in 1000ms'); 51 | 52 | const p2 = fn(2000); 53 | jest.advanceTimersByTime(1000); 54 | controller.abort(); 55 | await expect(p2).rejects.toBeInstanceOf(AbortError); 56 | await expect(p2).rejects.toMatchObject({ name: 'AbortError' }); 57 | }); 58 | test('alwaysPendingWhenAborted', async () => { 59 | const controller = new AbortController(); 60 | const fn = abortableAsync(resolveInNms, { 61 | signal: controller.signal, 62 | timeout: 2000, 63 | alwaysPendingWhenAborted: true, 64 | }); 65 | const p = fn(1000); 66 | jest.advanceTimersByTime(1000); 67 | await expect(p).resolves.toBe('The result in 1000ms'); 68 | 69 | const resolveOrReject = jest.fn(); 70 | fn(2000, true).then(resolveOrReject).catch(resolveOrReject); 71 | jest.advanceTimersByTime(1000); 72 | controller.abort(); 73 | 74 | await flushPromises(); 75 | // // expect(pp).not.toComplete(); 76 | expect(resolveOrReject).not.toHaveBeenCalled(); 77 | 78 | jest.advanceTimersByTime(1500); 79 | await flushPromises(); 80 | expect(resolveOrReject).not.toHaveBeenCalled(); 81 | }); 82 | test('alwaysPendingWhenTimeout', async () => { 83 | const controller = new AbortController(); 84 | const fn = abortableAsync(resolveInNms, { 85 | signal: controller.signal, 86 | timeout: 2000, 87 | alwaysPendingWhenAborted: true, 88 | }); 89 | const p = fn(1000); 90 | jest.advanceTimersByTime(1000); 91 | await expect(p).resolves.toBe('The result in 1000ms'); 92 | 93 | const resolveOrReject = jest.fn(); 94 | fn(2000).then(resolveOrReject, resolveOrReject); 95 | jest.advanceTimersByTime(1000); 96 | await flushPromises(); 97 | expect(resolveOrReject).not.toHaveBeenCalled(); 98 | 99 | jest.advanceTimersByTime(1500); 100 | await flushPromises(); 101 | controller.abort(); 102 | expect(resolveOrReject).not.toHaveBeenCalled(); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /packages/abortableAsync/index.ts: -------------------------------------------------------------------------------- 1 | export class AbortError extends Error {} 2 | AbortError.prototype.name = 'AbortError'; 3 | export class TimeoutError extends Error {} 4 | TimeoutError.prototype.name = 'TimeoutError'; 5 | 6 | export type AbortableOption = { 7 | timeout?: number; 8 | signal?: AbortSignal; 9 | alwaysPendingWhenAborted?: boolean; 10 | // alwaysPendingWhenTimeout?: boolean; 11 | }; 12 | 13 | export function abortableAsync( 14 | fn: (this: T, ...p: P) => Promise, 15 | opt: AbortableOption = {}, 16 | ): (this: T, ...p: P) => Promise { 17 | return function abortabledAsync(this: T, ...args: P): Promise { 18 | return new Promise((resolve, reject) => { 19 | let timer: ReturnType | undefined; 20 | let aborted = false; // avoid resolve when opt.alwaysPendingWhenAborted is true 21 | function doAbort() { 22 | // if (aborted) return; 23 | if (!opt.alwaysPendingWhenAborted) { 24 | reject(new AbortError('aborted')); 25 | } 26 | aborted = true; 27 | clearEffect(); 28 | } 29 | opt.signal?.addEventListener?.('abort', doAbort); 30 | function doTimeout() { 31 | // if (aborted) return; 32 | if (!opt.alwaysPendingWhenAborted) { 33 | reject(new TimeoutError(`timeout of ${opt.timeout}ms`)); 34 | } 35 | aborted = true; 36 | clearEffect(); 37 | } 38 | if (typeof opt.timeout === 'number' && opt.timeout > 0) { 39 | timer = setTimeout(doTimeout, opt.timeout); 40 | } 41 | function clearEffect() { 42 | if (typeof timer !== 'undefined') { 43 | clearTimeout(timer); 44 | timer = undefined; 45 | } 46 | opt.signal?.removeEventListener?.('abort', doAbort); 47 | } 48 | 49 | fn.call(this, ...args) 50 | .then((...r) => { 51 | if (!aborted) { 52 | resolve(...r); 53 | } 54 | clearEffect(); 55 | }) 56 | .catch((...e) => { 57 | if (!aborted) { 58 | reject(...e); 59 | } 60 | clearEffect(); 61 | }); 62 | }); 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /packages/abortableAsync/readme.md: -------------------------------------------------------------------------------- 1 | ## Intro 2 | 3 | 一个高阶函数,可以通过[AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)中止异步任务或在超时时自动中止。 4 | 5 | A higher-order function that can abort asynchronous tasks by [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) or automatically abort on timeout. 6 | 7 | ## Demo 8 | 9 | 10 | 11 | ::: demo src="./demo2.vue" title="取消或超时时丢弃结果(而不是抛出错误)。Always pending when aborted instead of rejections." iframe iframeHeight="178" 12 | 13 | 如果你想重试的话,请使用 [withRetryAsync](../withRetryAsync/readme.md)。 If you want to retry, use [withRetryAsync](../withRetryAsync/readme.md). 14 | 15 | ::: 16 | 17 | ## Types 18 | 19 | <<< es/abortableAsync/index.d.ts 20 | -------------------------------------------------------------------------------- /packages/concurrentAsync/demo.vue: -------------------------------------------------------------------------------- 1 | 52 | -------------------------------------------------------------------------------- /packages/concurrentAsync/index.test.ts: -------------------------------------------------------------------------------- 1 | import { concurrentAsync } from './index'; 2 | 3 | function someAsyncTask(data: T, delay = 100, fail?: boolean): Promise { 4 | // console.log('created', { data, delay, fail }); 5 | return new Promise((resolve, reject) => { 6 | setTimeout(() => { 7 | if (fail) { 8 | reject(data); 9 | // console.log('reject', data); 10 | } else { 11 | // console.log('resolve', data); 12 | resolve(data); 13 | } 14 | }, delay); 15 | }); 16 | } 17 | 18 | describe('concurrentAsync', () => { 19 | test('keep result correctly', async () => { 20 | const con3 = concurrentAsync(someAsyncTask); 21 | const promises = new Array(7).fill(0).map((_, i) => con3(i * 2, 10, i % 2 === 0)); 22 | for (let i = 0; i < promises.length; i++) { 23 | if (i % 2 === 0) { 24 | await expect(promises[i]).rejects.toBe(i * 2); 25 | } else { 26 | await expect(promises[i]).resolves.toBe(i * 2); 27 | } 28 | } 29 | }); 30 | test('invoked when', async () => { 31 | jest.useFakeTimers(); 32 | const rawFn = jest.fn(someAsyncTask); 33 | // 0,200,400 34 | // 100,300 35 | const con2 = concurrentAsync(rawFn, 2); 36 | const promises = new Array(5).fill(0).map((_, i) => con2(i * 2, i * 100, i % 2 === 0)); 37 | 38 | expect(rawFn).toHaveBeenCalledTimes(2); 39 | 40 | jest.advanceTimersByTime(1); 41 | await expect(promises[0]).rejects.toBe(0); 42 | // next task will be created util previous promise is resolved or rejected 43 | expect(rawFn).toHaveBeenCalledTimes(3); // the 3rd task has be created and the 4th hasn't been created yet 44 | 45 | jest.advanceTimersByTime(50); // 51ms 46 | expect(rawFn).toHaveBeenCalledTimes(3); // no new task(4th) was created 47 | 48 | jest.advanceTimersByTime(50); // 101ms 49 | expect(rawFn).toHaveBeenCalledTimes(3); // no new task(4th) was created 50 | await expect(promises[1]).resolves.toBe(2); // the 2nd task was resolved 51 | expect(rawFn).toHaveBeenCalledTimes(4); // the 4th task was created 52 | jest.advanceTimersByTime(50); // 151ms 53 | expect(rawFn).toHaveBeenCalledTimes(4); // no new task(5th) was created 54 | 55 | jest.advanceTimersByTime(50); // 201ms 56 | expect(rawFn).toHaveBeenCalledTimes(4); // no new task(5th) was created 57 | await expect(promises[2]).rejects.toBe(4); // the 3rd task was resolved 58 | expect(rawFn).toHaveBeenCalledTimes(5); // the 5th task was created 59 | 60 | jest.advanceTimersByTime(200); // 401ms 61 | await expect(promises[3]).resolves.toBe(6); // the 4th task was resolved 62 | expect(rawFn).toHaveBeenCalledTimes(5); // no more task was created 63 | 64 | jest.advanceTimersByTime(200); // 601ms 65 | await expect(promises[4]).rejects.toBe(8); // the 5th task was resolved 66 | expect(rawFn).toHaveBeenCalledTimes(5); // no more task was created 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /packages/concurrentAsync/index.ts: -------------------------------------------------------------------------------- 1 | export function concurrentAsync( 2 | fn: (this: T, ...p: P) => Promise, 3 | maxCount = 3, 4 | ): (this: T, ...p: P) => Promise { 5 | let that: T; 6 | 7 | type Resolve = (arg: R) => void; 8 | type Reject = (...ers: unknown[]) => void; 9 | type Holding = [P, Resolve, Reject]; 10 | 11 | const holding: Array = []; 12 | 13 | let executing = 0; 14 | 15 | function drain() { 16 | if (/* holding.length === 0 || */ executing >= maxCount) return; 17 | const nextHolding = holding.shift(); 18 | if (nextHolding) { 19 | // console.log('\ndrain', executing); 20 | execTask(nextHolding); 21 | } 22 | } 23 | 24 | function execTask([rawParams, resolve, reject]: Holding) { 25 | executing++; 26 | // console.log('execTask', executing); 27 | fn.apply(that, rawParams) 28 | .then((...args) => { 29 | resolve(...args); 30 | executing--; 31 | drain(); 32 | }) 33 | .catch((...args) => { 34 | reject(...args); 35 | executing--; 36 | drain(); 37 | }); 38 | } 39 | 40 | return function asyncConcurrented(this: T, ...rawParams: P): Promise { 41 | that = this; 42 | const defer = new Promise((r: Resolve, j: Reject) => { 43 | holding.push([rawParams, r, j]); 44 | }); 45 | drain(); 46 | return defer; 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /packages/concurrentAsync/readme.md: -------------------------------------------------------------------------------- 1 | ## Intro 2 | 3 | 一个限制最大(异步任务)并发的高阶函数。 4 | 5 | A higher-order function that limits the maximum (asynchronous task) concurrency. 6 | 7 | ## Demo 8 | 9 | ::: demo src="./demo.vue" iframe iframeHeight="100" 10 | 11 | Demo 以网络请求为例,打开 Devtool 查看效果。 12 | 13 | This demo takes a network request as an example and opens Devtool to see the effect. 14 | 15 | ::: 16 | 17 | ## Types 18 | 19 | <<< es/concurrentAsync/index.d.ts 20 | -------------------------------------------------------------------------------- /packages/debounceAsync/demo.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 47 | -------------------------------------------------------------------------------- /packages/debounceAsync/index.test.ts: -------------------------------------------------------------------------------- 1 | import { debounceAsync } from './index'; 2 | 3 | // jest.useFakeTimers(); 4 | function flushPromises() { 5 | return new Promise((resolve) => setImmediate(resolve)); 6 | } 7 | 8 | function someAsyncTask(data: T, delay = 100, fail?: boolean): Promise { 9 | return new Promise((resolve, reject) => { 10 | setTimeout(() => { 11 | if (fail) { 12 | reject(data); 13 | } else { 14 | resolve(data); 15 | } 16 | }, delay); 17 | }); 18 | } 19 | 20 | describe('debounceAsync', () => { 21 | test('keep result correctly', async () => { 22 | jest.useFakeTimers(); 23 | const debounced = debounceAsync(someAsyncTask); 24 | const N = Math.random(); 25 | const p = debounced(N, 1); 26 | jest.advanceTimersByTime(1000); 27 | await expect(p).resolves.toBe(N); 28 | const p2 = debounced(N, 0, true); 29 | jest.advanceTimersByTime(1000); 30 | await expect(p2).rejects.toBe(N); 31 | }); 32 | 33 | test('debounce correctly', async () => { 34 | jest.useFakeTimers(); 35 | const rawFn = jest.fn(someAsyncTask); 36 | const debounced = debounceAsync(rawFn, 10); 37 | const resolveOrReject = jest.fn(); 38 | new Array(5).fill(0).map((_, i) => 39 | debounced(i * 2, 10, i % 2 === 0) 40 | .then(resolveOrReject) 41 | .catch(resolveOrReject), 42 | ); 43 | 44 | expect(rawFn).not.toHaveBeenCalled(); 45 | expect(resolveOrReject).not.toHaveBeenCalled(); 46 | jest.advanceTimersByTime(500); 47 | await flushPromises(); 48 | expect(rawFn).toHaveBeenCalledTimes(1); 49 | expect(rawFn).lastCalledWith(8, 10, true); 50 | expect(resolveOrReject).toHaveBeenCalledTimes(1); 51 | expect(resolveOrReject).lastCalledWith(8); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /packages/debounceAsync/index.ts: -------------------------------------------------------------------------------- 1 | export function debounceAsync( 2 | fn: (this: T, ...p: P) => Promise, 3 | ms: number = 300, 4 | ): (this: T, ...p: P) => Promise { 5 | let timeoutId: ReturnType; 6 | return function debouncedFiltered(this: T, ...args: P): Promise { 7 | return new Promise((resolve, reject) => { 8 | if (timeoutId !== void 0) { 9 | clearTimeout(timeoutId); 10 | } 11 | timeoutId = setTimeout(() => { 12 | fn.call(this, ...args) 13 | .then(resolve) 14 | .catch(reject); 15 | }, ms); 16 | }); 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /packages/debounceAsync/readme.md: -------------------------------------------------------------------------------- 1 | ## Intro 2 | 3 | 一个类似 `debounce` 的高阶函数,但是接收一个异步任务。区别是在`debounceAsync`里,如果任务没有执行则会返回一个`always pending promise`。 4 | 5 | A higher-order function like `debounce`, but receiving an asynchronous task. The difference is that in `debounceAsync`, an `always pending promise` is returned if the task is not executed. 6 | 7 | 下面两段代码的运行效果是一样的,只是风格不同: 8 | 9 | The following two pieces of code run the same way, just in different styles: 10 | 11 | ```ts 12 | export default defineComponent({ 13 | setup() { 14 | const suggestions = ref([]); 15 | return { 16 | suggestions, 17 | // 同步风格 Synchronization style 18 | onInput: debounce(function onInput(e) { 19 | const keywords = e.target.value; 20 | searchApi(keywords).then((rez) => { 21 | suggestions.value = rez; 22 | }); 23 | }), 24 | }; 25 | }, 26 | }); 27 | ``` 28 | 29 | ```ts 30 | const debouncedSearchApi = debounceAsync(searchApi); 31 | 32 | export default defineComponent({ 33 | setup() { 34 | const suggestions = ref([]); 35 | return { 36 | suggestions, 37 | // 异步风格 Asynchronous style 38 | async onInput(e) { 39 | // 注意在 `await debouncedSearchApi` 之前的代码仍会执行 40 | // Note that the code before `await debouncedSearchApi` will still execute 41 | const keywords = e.target.value; 42 | 43 | // 会在适当的时机在此处卡住 44 | // will get stuck here at the right time 45 | const rez = await debouncedSearchApi(keywords); 46 | suggestions.value = rez; 47 | }, 48 | }; 49 | }, 50 | }); 51 | ``` 52 | 53 | ## Demo 54 | 55 | ::: demo src="./demo.vue" iframe iframeHeight="100" 56 | 57 | Demo 以网络请求为例,打开 Devtool 查看效果。 58 | 59 | This demo takes a network request as an example and opens Devtool to see the effect. 60 | 61 | ::: 62 | 63 | ## Types 64 | 65 | <<< es/debounceAsync/index.d.ts 66 | -------------------------------------------------------------------------------- /packages/debounceAsyncResult/demo.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 50 | -------------------------------------------------------------------------------- /packages/debounceAsyncResult/index.test.ts: -------------------------------------------------------------------------------- 1 | import { debounceAsyncResult } from './index'; 2 | 3 | // jest.useFakeTimers(); 4 | function flushPromises() { 5 | return new Promise((resolve) => setImmediate(resolve)); 6 | } 7 | 8 | function someAsyncTask(data: T, delay = 100, fail?: boolean): Promise { 9 | return new Promise((resolve, reject) => { 10 | setTimeout(() => { 11 | if (fail) { 12 | reject(data); 13 | } else { 14 | resolve(data); 15 | } 16 | }, delay); 17 | }); 18 | } 19 | 20 | describe('debounceAsyncResult', () => { 21 | test('keep result correctly', async () => { 22 | jest.useFakeTimers(); 23 | const debounced = debounceAsyncResult(someAsyncTask); 24 | const N = Math.random(); 25 | const p = debounced(N, 1); 26 | jest.advanceTimersByTime(1000); 27 | await expect(p).resolves.toBe(N); 28 | const p2 = debounced(N, 0, true); 29 | jest.advanceTimersByTime(1000); 30 | await expect(p2).rejects.toBe(N); 31 | }); 32 | test('keep result correctly with multiple calls', async () => { 33 | jest.useFakeTimers(); 34 | const rawFn = jest.fn(someAsyncTask); 35 | const debounced = debounceAsyncResult(rawFn); 36 | const resolveOrReject = jest.fn(); 37 | new Array(5).fill(0).map((_, i) => 38 | debounced(i * 2, 100 - 10 * i, i % 2 === 0) 39 | .then(resolveOrReject) 40 | .catch(resolveOrReject), 41 | ); 42 | 43 | expect(rawFn).toHaveBeenCalledTimes(5); 44 | expect(resolveOrReject).not.toHaveBeenCalled(); 45 | jest.advanceTimersByTime(500); 46 | await flushPromises(); 47 | expect(rawFn).toHaveBeenCalledTimes(5); 48 | expect(rawFn).lastCalledWith(8, 60, true); 49 | expect(resolveOrReject).toHaveBeenCalledTimes(1); 50 | expect(resolveOrReject).lastCalledWith(8); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/debounceAsyncResult/index.ts: -------------------------------------------------------------------------------- 1 | export function debounceAsyncResult( 2 | fn: (this: T, ...p: P) => Promise, 3 | ): (this: T, ...p: P) => Promise { 4 | let lastFetchId = 0; 5 | return function asyncDebounced(this: T, ...args: P): Promise { 6 | const fetchId = ++lastFetchId; 7 | return new Promise((resolve, reject) => { 8 | fn.call(this, ...args) 9 | .then((...rez) => { 10 | if (fetchId === lastFetchId) { 11 | resolve(...rez); 12 | } 13 | }) 14 | .catch((...err) => { 15 | if (fetchId === lastFetchId) { 16 | reject(...err); 17 | } 18 | }); 19 | }); 20 | 21 | // return fn 22 | // .call(this, ...args) 23 | // .then((...a1) => { 24 | // if (fetchId !== lastFetchId) { 25 | // return new Promise(() => {}); 26 | // } 27 | // return Promise.resolve(...a1); 28 | // }) 29 | // .catch((err) => { 30 | // if (fetchId !== lastFetchId) { 31 | // return new Promise(() => {}) as Promise; 32 | // } 33 | // return Promise.reject(err); 34 | // }); 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /packages/debounceAsyncResult/readme.md: -------------------------------------------------------------------------------- 1 | ## Intro 2 | 3 | 一个类似 `debounce` 的高阶函数,返回一个新函数,这个新函数返回最后一次执行的异步任务结果(执行频率过高时或执行时间不均匀)。 4 | 5 | A debounce-like higher-order function that returns a new function that returns the result of the last asynchronous task executed (when execution is too frequent or when execution time is uneven). 6 | 7 | `debounceAsyncResult` 和 [debounceAsync](../debounceAsync/readme.md) 的区别是: `debounceAsync` 处理异步任务的执行时机。 `debounceAsyncResult` 处理已经执行的异步任务的结果。 8 | 9 | The difference between `debounceAsyncResult` and [debounceAsync](../debounceAsync/readme.md) are: `debounceAsync` handles the occasion of asynchronous task execution. `debounceAsyncResult` handles the result of an asynchronous task that has already been executed. 10 | 11 | ## Demo 12 | 13 | ::: demo src="./demo.vue" iframe iframeHeight="100" 14 | 15 | Demo 以网络请求为例,打开 Devtool 查看效果。 16 | 17 | This demo takes a network request as an example and opens Devtool to see the effect. 18 | 19 | ::: 20 | 21 | ## Types 22 | 23 | <<< es/debounceAsyncResult/index.d.ts 24 | -------------------------------------------------------------------------------- /packages/functions: -------------------------------------------------------------------------------- 1 | functions/ -------------------------------------------------------------------------------- /packages/index.ts: -------------------------------------------------------------------------------- 1 | export * from './debounceAsyncResult'; 2 | export * from './debounceAsync'; 3 | export * from './throttleAsyncResult'; 4 | export * from './concurrentAsync'; 5 | export * from './withRetryAsync'; 6 | export * from './abortableAsync'; 7 | -------------------------------------------------------------------------------- /packages/throttleAsyncResult/demo.vue: -------------------------------------------------------------------------------- 1 | 32 | -------------------------------------------------------------------------------- /packages/throttleAsyncResult/demo2.vue: -------------------------------------------------------------------------------- 1 | 44 | -------------------------------------------------------------------------------- /packages/throttleAsyncResult/index.test.ts: -------------------------------------------------------------------------------- 1 | import { throttleAsyncResult } from './index'; 2 | 3 | // jest.useFakeTimers(); 4 | function flushPromises() { 5 | return new Promise((resolve) => setImmediate(resolve)); 6 | } 7 | 8 | function someAsyncTask(data: T, delay = 100, fail?: boolean): Promise { 9 | return new Promise((resolve, reject) => { 10 | setTimeout(() => { 11 | if (fail) { 12 | reject(data); 13 | } else { 14 | resolve(data); 15 | } 16 | }, delay); 17 | }); 18 | } 19 | 20 | describe('throttleAsyncResult', () => { 21 | test('keep result correctly', async () => { 22 | jest.useFakeTimers(); 23 | const throttled = throttleAsyncResult(someAsyncTask); 24 | const N = Math.random(); 25 | const p = throttled(N, 1); 26 | jest.advanceTimersByTime(1000); 27 | await expect(p).resolves.toBe(N); 28 | const p2 = throttled(N, 0, true); 29 | jest.advanceTimersByTime(1000); 30 | await expect(p2).rejects.toBe(N); 31 | }); 32 | test('keep result correctly with multiple calls', async () => { 33 | jest.useFakeTimers(); 34 | const rawFn = jest.fn(someAsyncTask); 35 | const throttled = throttleAsyncResult(rawFn); 36 | const resolveOrReject = jest.fn(); 37 | new Array(5).fill(0).map((_, i) => 38 | throttled(i * 2, 100 - 10 * i, i % 2 === 0) 39 | .then(resolveOrReject) 40 | .catch(resolveOrReject), 41 | ); 42 | 43 | expect(rawFn).toHaveBeenCalledTimes(1); 44 | expect(resolveOrReject).not.toHaveBeenCalled(); 45 | jest.advanceTimersByTime(500); 46 | await flushPromises(); 47 | expect(rawFn).toHaveBeenCalledTimes(1); 48 | expect(rawFn).lastCalledWith(0, 100, true); 49 | expect(resolveOrReject).toHaveBeenCalledTimes(1); 50 | expect(resolveOrReject).lastCalledWith(0); 51 | }); 52 | test('useSamePromise', async () => { 53 | jest.useFakeTimers(); 54 | const rawFn = jest.fn(someAsyncTask); 55 | const throttled = throttleAsyncResult(rawFn, { useSamePromise: true }); 56 | const resolveOrReject = jest.fn(); 57 | new Array(5).fill(0).map((_, i) => 58 | throttled(i * 2, 100 - 10 * i, i % 2 === 0) 59 | .then(resolveOrReject) 60 | .catch(resolveOrReject), 61 | ); 62 | 63 | expect(rawFn).toHaveBeenCalledTimes(1); 64 | expect(resolveOrReject).not.toHaveBeenCalled(); 65 | jest.advanceTimersByTime(500); 66 | await flushPromises(); 67 | expect(rawFn).toHaveBeenCalledTimes(1); 68 | expect(rawFn).lastCalledWith(0, 100, true); 69 | expect(resolveOrReject).toHaveBeenCalledTimes(5); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /packages/throttleAsyncResult/index.ts: -------------------------------------------------------------------------------- 1 | export function throttleAsyncResult( 2 | fn: (this: T, ...p: P) => Promise, 3 | { useSamePromise = false } = {}, 4 | ): (this: T, ...p: P) => Promise { 5 | let isPending = false; 6 | let theLastPromise: null | Promise = null; 7 | return function asyncThrottled(this: T, ...args: P): Promise { 8 | if (isPending) { 9 | if (useSamePromise && theLastPromise) { 10 | return theLastPromise; 11 | } 12 | return new Promise(() => {}); 13 | } else { 14 | const ret = fn 15 | .call(this, ...args) 16 | .then((...a1) => { 17 | isPending = false; 18 | theLastPromise = null; 19 | return Promise.resolve(...a1); 20 | }) 21 | .catch((...a2) => { 22 | isPending = false; 23 | theLastPromise = null; 24 | return Promise.reject(...a2); 25 | }); 26 | theLastPromise = ret; 27 | isPending = true; 28 | return ret; 29 | } 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /packages/throttleAsyncResult/readme.md: -------------------------------------------------------------------------------- 1 | ## Intro 2 | 3 | 一个类似 `throttle` 的高阶函数,返回一个新函数,这个新函数返回第一次执行的异步任务结果并忽略在此期间的任何执行。 4 | 5 | A higher-order function like `throttle` that returns a new function that returns the result of the first execution of the asynchronous task and ignores any executions in the meantime. 6 | 7 | 换句话说,在任意时刻,仅会存在 1 个或 0 个正在执行的异步任务。 8 | 9 | In other words, at any given moment, there will be only 1 or 0 executing asynchronous tasks. 10 | 11 | 再换句话说,假如有一个 pending promise,在它结束之前不会重复执行下一个异步任务,直接返回它或者一个`always pending promise`(根据配置中的 `useSamePromise`)。 12 | 13 | In other words, if there is a pending promise, the next asynchronous task will not be repeated until it finishes, and it will be returned directly or as an `always pending promise` (according to `useSamePromise` in the configuration). 14 | 15 | ## Demo 16 | 17 | > 以下 Demo 以网络请求为例,打开 Devtool 查看效果。 18 | > 19 | > The following demo takes a network request as an example and opens Devtool to see the effect. 20 | 21 | ::: demo src="./demo.vue" title="提交场景 Submission Case" iframe iframeHeight="50" 22 | 23 | - 背景:为防止用户重复提交,我们通常需要维护一个 loading 变量,当 loading 数量多起来就难搞了(我也想偷懒)。 24 | - 需求:不需要写 loading,也可以去重。 25 | - 原文:https://blog.bowen.cool/zh/posts/when-throttling-meets-asynchrony 26 | 27 | ::: 28 | 29 | ::: demo src="./demo2.vue" title="查询场景 Query Case" iframe iframeHeight="100" 30 | 31 | - 背景:多个地方需要同一份数据,往往调用(请求)多次。 32 | - 需求:执行(请求)一次,返回同一个结果给多个调用方。 33 | 34 | ::: 35 | 36 | ## Types 37 | 38 | <<< es/throttleAsyncResult/index.d.ts 39 | -------------------------------------------------------------------------------- /packages/withRetryAsync/demo.vue: -------------------------------------------------------------------------------- 1 | 57 | -------------------------------------------------------------------------------- /packages/withRetryAsync/index.test.ts: -------------------------------------------------------------------------------- 1 | import { withRetryAsync } from './index'; 2 | 3 | // jest.useFakeTimers(); 4 | function flushPromises() { 5 | return new Promise((resolve) => setImmediate(resolve)); 6 | } 7 | 8 | let cache = new Map(); 9 | 10 | function someAsyncTask(key: symbol | string, data: T, delay = 10, successOrder = 1): Promise { 11 | return new Promise((resolve, reject) => { 12 | setTimeout(() => { 13 | if (!cache.has(key)) { 14 | cache.set(key, 0); 15 | } 16 | const count = cache.get(key)!; 17 | // console.log(`${String(key)} ${count}`); 18 | if (count + 1 >= successOrder) { 19 | cache.set(key, 0); 20 | resolve(data); 21 | } else { 22 | cache.set(key, count + 1); 23 | reject(data); 24 | } 25 | }, delay); 26 | }); 27 | } 28 | 29 | describe('withRetryAsync', () => { 30 | test('keep result correctly', async () => { 31 | jest.useFakeTimers(); 32 | const wrap = withRetryAsync(someAsyncTask); 33 | const N = Math.random(); 34 | const p = wrap(Symbol('will success at 1st time'), N, 1); 35 | jest.advanceTimersByTime(100); 36 | await expect(p).resolves.toBe(N); 37 | }); 38 | test('keep result correctly when failed', async () => { 39 | jest.useRealTimers(); 40 | const wrap = withRetryAsync(someAsyncTask, { retryInterval: 0 }); 41 | const N = Math.random(); 42 | const p2 = wrap(Symbol('will success at 4th time'), N, 1, 4); 43 | await expect(p2).rejects.toBe(N); 44 | }); 45 | 46 | test('retry', async () => { 47 | jest.useFakeTimers(); 48 | const rawFn = jest.fn(someAsyncTask); 49 | const onFailed = jest.fn(); 50 | const onRetry = jest.fn(); 51 | const wrap = withRetryAsync(rawFn, { maxCount: 2, onRetry, onFailed }); 52 | const N = Math.random(); 53 | const p = wrap(Symbol('will success at 2nd time'), N, 10, 2); 54 | expect(rawFn).toBeCalledTimes(1); 55 | expect(onFailed).toBeCalledTimes(0); 56 | expect(onRetry).toBeCalledTimes(1); 57 | 58 | jest.advanceTimersByTime(10); 59 | await flushPromises(); 60 | // first failed 61 | expect(rawFn).toBeCalledTimes(1); 62 | expect(onFailed).toBeCalledTimes(1); 63 | expect(onFailed).lastCalledWith(1, [N]); 64 | expect(onRetry).toBeCalledTimes(1); 65 | expect(onRetry).lastCalledWith(1); 66 | 67 | // wait for retry 68 | jest.advanceTimersByTime(999); 69 | expect(rawFn).toBeCalledTimes(1); 70 | expect(onFailed).toBeCalledTimes(1); 71 | expect(onRetry).toBeCalledTimes(1); 72 | 73 | // retry 74 | jest.advanceTimersByTime(1); 75 | expect(rawFn).toBeCalledTimes(2); 76 | expect(onRetry).toBeCalledTimes(2); 77 | expect(onRetry).lastCalledWith(2); 78 | expect(onFailed).toBeCalledTimes(1); 79 | 80 | // 2nd retry will be success 81 | jest.advanceTimersByTime(10); 82 | await flushPromises(); 83 | expect(rawFn).toBeCalledTimes(2); 84 | expect(onRetry).toBeCalledTimes(2); 85 | expect(onFailed).toBeCalledTimes(1); 86 | 87 | await expect(p).resolves.toBe(N); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /packages/withRetryAsync/index.ts: -------------------------------------------------------------------------------- 1 | export function withRetryAsync( 2 | fn: (this: T, ...p: P) => Promise, 3 | { 4 | /** 最多重试次数 */ 5 | maxCount = 3, 6 | /** 7 | * @desc 每次重试之间的等待间隔时间 8 | */ 9 | retryInterval = 1000, 10 | /** 每次重试开始的回调,含第一次,第一次是1 */ 11 | onRetry = (i: number) => {}, 12 | /** 13 | * @desciption 每次失败的回调 14 | */ 15 | onFailed = (i: number, lastFailedReason: unknown[]) => {}, 16 | } = {}, 17 | ): (this: T, ...p: P) => Promise { 18 | return function withRetryedAsync(this: T, ...args: P): Promise { 19 | return new Promise((resolve, reject) => { 20 | let retriedCount = 0; 21 | 22 | const that = this; 23 | execTask(); 24 | function execTask() { 25 | onRetry(++retriedCount); 26 | fn.call(that, ...args) 27 | .then((...r) => { 28 | resolve(...r); 29 | }) 30 | .catch((...e) => { 31 | if (retriedCount >= maxCount) { 32 | onFailed(retriedCount, e); 33 | reject(...e); 34 | } else { 35 | onFailed(retriedCount, e); 36 | setTimeout(execTask, retryInterval); 37 | } 38 | }); 39 | } 40 | }); 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /packages/withRetryAsync/readme.md: -------------------------------------------------------------------------------- 1 | ## Intro 2 | 3 | 一个可以让异步任务支持重试的高阶函数。 4 | 5 | A higher-order function that allows asynchronous tasks to support retries. 6 | 7 | ## Demo 8 | 9 | ::: demo src="./demo.vue" iframe iframeHeight="200" 10 | 11 | Demo 以网络请求为例,打开 Devtool 查看效果。 12 | 13 | This demo takes a network request as an example and opens Devtool to see the effect. 14 | 15 | ::: 16 | 17 | ## Types 18 | 19 | <<< es/withRetryAsync/index.d.ts 20 | -------------------------------------------------------------------------------- /packages/withRetryAsync/utils.ts: -------------------------------------------------------------------------------- 1 | export type PlainObject = Record; 2 | export type PlainObjectOf = Record; 3 | 4 | export function isPlainObject(obj: any): obj is PlainObject { 5 | return obj && obj.constructor === Object; 6 | } 7 | 8 | export function error2String(err: unknown) { 9 | if (typeof err === 'string') { 10 | return err; 11 | } 12 | if (err instanceof Error) { 13 | return `${err.name}: ${err.message}`; 14 | } 15 | if (isPlainObject(err)) { 16 | if (typeof err.msg === 'string') { 17 | return err.msg; 18 | } 19 | if (typeof err.message === 'string') { 20 | return err.message; 21 | } 22 | } 23 | return JSON.stringify(err); 24 | } 25 | -------------------------------------------------------------------------------- /patches/vitepress+0.21.4.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/vitepress/dist/node/serve-9731a371.js b/node_modules/vitepress/dist/node/serve-9731a371.js 2 | index e105cda..9aad64b 100644 3 | --- a/node_modules/vitepress/dist/node/serve-9731a371.js 4 | +++ b/node_modules/vitepress/dist/node/serve-9731a371.js 5 | @@ -34335,7 +34335,7 @@ const highlight = (str, lang) => { 6 | } 7 | return wrap(str, "text"); 8 | }; 9 | - 10 | +exports.highlight = highlight 11 | var remove = removeDiacritics; 12 | 13 | var replacementList = [ 14 | @@ -36647,7 +36647,7 @@ function createVitePressPlugin(root, siteConfig, ssr = false, pageToHashMap, cli 15 | }, 16 | renderStart() { 17 | if (hasDeadLinks) { 18 | - throw new Error(`One or more pages contain dead links.`); 19 | + // throw new Error(`One or more pages contain dead links.`); 20 | } 21 | }, 22 | configureServer(server) { 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Async Utilities 2 | 3 |

4 | Actions Status 5 | Coverage Status 6 | npm version 7 | npm downloads 8 | 9 | gzip size 10 | GitHub 11 | vitepress 12 |

13 | 14 | 一个异步工具库,风格以高阶函数为主。 15 | 16 | An asynchronous tools library in the style of higher-order functions. [Website](https://bowencool.github.io/async-utilities/) 17 | 18 | # Usage 19 | 20 | 使用 npm: 21 | 22 | ```bash 23 | npm i @bowencool/async-utilities 24 | ``` 25 | 26 | ```ts 27 | import { throttleAsyncResult } from '@bowencool/async-utilities'; 28 | ``` 29 | 30 | 在浏览器中: 31 | 32 | ```html 33 | 34 | 35 | 36 | ``` 37 | 38 | # Todo 39 | 40 | - ~~cacheAsync~~ see [memoizee](https://github.com/medikoo/memoizee#memoizing-asynchronous-functions) or [lru-pcache](https://github.com/jmendiara/lru-pcache) 41 | - [ ] abort fetch & xhr 42 | 43 | ## License 44 | 45 | MIT 46 | -------------------------------------------------------------------------------- /scripts/check-dist-tag.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | const chalk = require('chalk'); 3 | const { isMasterBranch, getCurrentBranchName } = require('./utils'); 4 | 5 | const distTag = process.env.npm_config_tag || 'latest'; 6 | 7 | if (isMasterBranch(getCurrentBranchName())) { 8 | if (distTag !== 'latest') { 9 | console.log(chalk.red('你只能在此分支发布正式版,而不是', distTag)); 10 | process.exit(1); 11 | } 12 | } else if (distTag === 'latest') { 13 | console.log(chalk.red('你不能在此分支发布正式版,设置 --tag')); 14 | process.exit(1); 15 | } 16 | -------------------------------------------------------------------------------- /scripts/check-version-bump.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | const semverParse = require('semver').parse; 3 | const chalk = require('chalk'); 4 | const { isMasterBranch, getCurrentBranchName } = require('./utils'); 5 | 6 | // npm 5、6: undefined new 7 | // npm 7: new old 8 | // console.log(process.env.npm_new_version, process.env.npm_package_version); 9 | const newVersion = process.env.npm_new_version || process.env.npm_package_version; 10 | const prerelease = semverParse(newVersion)?.prerelease; 11 | // const perid = process.env.npm_config_perid; 12 | console.log(newVersion, prerelease); 13 | 14 | if (isMasterBranch(getCurrentBranchName())) { 15 | if (prerelease && prerelease.length > 0) { 16 | console.log(chalk.red('你不能在此分支发布测试版')); 17 | process.exit(1); 18 | } 19 | } else if (!prerelease || prerelease.length === 0) { 20 | console.log(chalk.red('你不能在此分支发布正式版')); 21 | process.exit(1); 22 | } 23 | -------------------------------------------------------------------------------- /scripts/incremental-test.js: -------------------------------------------------------------------------------- 1 | // const { getCurrentBranchName } = require('./utils'); 2 | const { execSync } = require('child_process'); 3 | 4 | const stdout = execSync('git diff HEAD^ HEAD --name-only').toString().trim(); 5 | const filenames = stdout.split('\n'); 6 | 7 | console.log(filenames); 8 | -------------------------------------------------------------------------------- /scripts/utils.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process'); 2 | 3 | const isMasterBranch = (branchName) => ['master','online', 'main', 'latest'].includes(branchName); 4 | 5 | // execSync('git rev-parse --abbrev-ref HEAD').toString().trim(); 6 | exports.getCurrentBranchName = () => execSync('git branch --show-current').toString().trim(); 7 | exports.isMasterBranch = isMasterBranch; 8 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true 5 | }, 6 | "exclude": ["**/*.spec.tsx", "**/*.spec.ts", "**/*.test.tsx", "**/*.test.ts", "**/*/demo/**/*", "demo*.ts", "demo*.tsx"] 7 | // "exclude": ["**/*.{spec,test}.{ts,tsx}"] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "skipLibCheck": true, 7 | "noImplicitThis": true, 8 | "noImplicitAny": true, 9 | "noImplicitReturns": true, 10 | "jsx": "preserve", 11 | "importHelpers": true, 12 | "moduleResolution": "node", 13 | "esModuleInterop": true, 14 | "allowSyntheticDefaultImports": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "packages/*": ["packages/*"], 18 | "@/*": ["website/*"] 19 | }, 20 | "lib": ["esnext", "dom", "dom.iterable"] 21 | }, 22 | "include": ["packages/**/*.ts", "packages/**/*.tsx", "packages/**/*.vue", "typings/**/*"], 23 | "exclude": ["node_modules", "cjs", "es", "website"], 24 | "locale": "zh-CN" 25 | } 26 | -------------------------------------------------------------------------------- /typings/css-module-shim.d.ts: -------------------------------------------------------------------------------- 1 | // import type { ComponentOptions, DefineComponent } from 'vue'; 2 | 3 | declare module '*.module.scss' { 4 | const obj: Record; 5 | export default obj; 6 | } 7 | -------------------------------------------------------------------------------- /typings/vue-shim.d.ts: -------------------------------------------------------------------------------- 1 | // import type { ComponentOptions, DefineComponent } from 'vue'; 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue'; 5 | 6 | const component: DefineComponent<{}, {}, any>; 7 | export default component; 8 | } 9 | -------------------------------------------------------------------------------- /typings/vue-test-utils.d.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentPublicInstance } from 'vue'; 2 | 3 | declare module '@vue/test-utils' { 4 | interface DOMWrapper { 5 | style: CSSStyleDeclaration; 6 | } 7 | 8 | interface VueWrapper { 9 | style: CSSStyleDeclaration; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /website/.vitepress/config.js: -------------------------------------------------------------------------------- 1 | require('sucrase/register/ts') 2 | 3 | const { config } = require('./config.ts') 4 | 5 | /** 6 | * @type {import('vitepress').UserConfig} 7 | */ 8 | module.exports = config 9 | -------------------------------------------------------------------------------- /website/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import MarkdownIt from 'markdown-it'; 2 | import { UserConfig } from 'vitepress'; 3 | import markdownItCheckbox from 'markdown-it-checkbox'; 4 | import markdownItDemo from './markdown/plugin/markdown-it-demo'; 5 | const fs = require('fs-extra'); 6 | const path = require('path'); 7 | 8 | const INPUT_PATH = path.resolve(__dirname, '../../packages'); 9 | 10 | export const config: UserConfig = { 11 | base: '/async-utilities/', 12 | title: 'Async Utilities', 13 | lang: 'zh-CN', 14 | description: process.env.npm_package_description, 15 | 16 | // https://vitepress.vuejs.org/guide/markdown.html#advanced-configuration 17 | markdown: { 18 | lineNumbers: true, 19 | config: (md: MarkdownIt) => { 20 | md.use(markdownItCheckbox); 21 | md.use(markdownItDemo); 22 | }, 23 | }, 24 | 25 | themeConfig: { 26 | sidebar: { 27 | // '/functions/': getComponentsSidebar(), 28 | // '/guide/': getGuideSidebar(), 29 | '/': getComponentsSidebar(), 30 | }, 31 | nav: [ 32 | { text: '指南 Guide', link: '/', activeMatch: '^/$|^/guide/' }, 33 | { 34 | text: '函数 Functions', 35 | link: '/functions/abortableAsync/readme', 36 | activeMatch: '^/functions/', 37 | }, 38 | { 39 | text: 'Github', 40 | link: 'https://github.com/bowencool/async-utilities', 41 | }, 42 | ], 43 | }, 44 | }; 45 | 46 | function getComponentsSidebar() { 47 | let functions = []; 48 | fs.readdirSync(`${INPUT_PATH}`) 49 | .filter((name) => !name.includes('.')) 50 | .forEach((name) => { 51 | // console.log(name) 52 | // return; 53 | let mdFile = `readme.md`; 54 | // console.log(name, mdFile); 55 | if (fs.existsSync(`${INPUT_PATH}/${name}/${mdFile}`)) { 56 | const text = name.replace(/[-_]([a-z])/g, (_, $1) => $1.toUpperCase()); 57 | const link = `/functions/${name}/${mdFile.replace(/\.md$/, '')}`; 58 | functions.push({ 59 | text, 60 | link, 61 | }); 62 | } 63 | }); 64 | // console.log(functions); 65 | // return functions; 66 | return [ 67 | { 68 | text: '介绍 Intro', 69 | children: [ 70 | { text: '开始使用 Getting started', link: '/guide/getting-started' }, 71 | // { text: '配置', link: '/guide/configuration' }, 72 | // { text: '贡献/开发指南', link: '/guide/contribution' }, 73 | // { text: 'Todos', link: '/guide/todos' }, 74 | ], 75 | }, 76 | { 77 | text: '函数 Functions', 78 | children: functions, 79 | }, 80 | ]; 81 | } 82 | -------------------------------------------------------------------------------- /website/.vitepress/markdown/plugin/build-demos.vite.config.ts: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue'; 2 | import { mergeConfig, UserConfig } from 'vite'; 3 | // 复用配置 4 | import config from '../../../vite.config'; 5 | import { readFileSync } from 'fs'; 6 | import { resolve } from 'path'; 7 | import type { IframeMeta } from './genIframe'; 8 | import { genHtml } from './genIframe'; 9 | import { config as vitepressConfig } from '../../config'; 10 | 11 | // 我不知道 markdown-it-plugin 怎么跟 vite-plugin 低成本取得联系,所以直接通过文件传参了。这个文件是 markdown-it-demo 生成的。 12 | const demos: Record = JSON.parse( 13 | readFileSync(resolve(process.cwd(), 'node_modules/demos.json'), 'utf-8'), 14 | ); 15 | 16 | // 虚拟的目录名,和 devServer 保持一致 17 | const dir = resolve(process.cwd(), '-demos'); 18 | 19 | const iframeConfig: UserConfig = { 20 | plugins: [ 21 | // 复用配置了,但是原本的配置里没写,所以这里添加了 22 | vue(), 23 | // todo 合并 24 | { 25 | name: 'demo-iframe-build', 26 | resolveId(id) { 27 | if (id.match(/\/-demos\/(\w+)\.html/)) { 28 | return id; 29 | } 30 | return undefined; 31 | }, 32 | load(id) { 33 | if (id.match(/\/-demos\/(\w+)\.html/)) { 34 | const demoName = RegExp.$1; 35 | const meta = demos[demoName]; 36 | if (meta) { 37 | meta.title = meta.title || demoName; 38 | return genHtml(meta); 39 | } 40 | } 41 | return undefined; 42 | }, 43 | }, 44 | ], 45 | base: vitepressConfig.base, 46 | build: { 47 | // 为了方便,目录选择了 vitepress 的目录 48 | outDir: 'website/.vitepress/dist', 49 | emptyOutDir: false, 50 | rollupOptions: { 51 | input: {}, 52 | }, 53 | }, 54 | }; 55 | 56 | const input = iframeConfig.build.rollupOptions.input; 57 | 58 | // 添加所有入口 59 | Object.entries(demos).forEach(([demoName, meta]) => { 60 | const htmlEntry = `${dir}/${demoName}.html`; 61 | if (Array.isArray(input)) { 62 | input.push(htmlEntry); 63 | } else { 64 | input[demoName] = htmlEntry; 65 | } 66 | }); 67 | 68 | export default mergeConfig(config, iframeConfig); 69 | -------------------------------------------------------------------------------- /website/.vitepress/markdown/plugin/genIframe.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | 3 | export type IframeMeta = { 4 | title?: string; 5 | entry: string; 6 | }; 7 | const autoEntry = (entry: string) => { 8 | if (process.env.NODE_ENV === 'development') { 9 | return `/@fs/${entry}`; 10 | } 11 | return entry; 12 | }; 13 | export const genHtml = (meta: IframeMeta) => { 14 | const customCss = resolve(process.cwd(), 'website/.vitepress/theme/custom.scss'); 15 | const devTip = (process.env.NODE_ENV === 'development' 16 | ? `console.log('iframe 模式自动挂载了一个vue组件:%o', module.default.__file || module.default);console.log('如果你不希望自动挂载,移除 export default');` 17 | : ''); 18 | return ` 19 | 20 | 21 | 22 | 23 | 24 | ${meta.title} 25 | 26 | 27 | 28 | 29 | 43 | 44 | `; 45 | }; 46 | -------------------------------------------------------------------------------- /website/.vitepress/markdown/plugin/markdown-it-demo.ts: -------------------------------------------------------------------------------- 1 | import mdContainer from 'markdown-it-container'; 2 | import MarkdownIt from 'markdown-it'; 3 | import path from 'path'; 4 | import fs from 'fs'; 5 | // import md5 from 'md5'; 6 | import { highlight } from 'vitepress/dist/node/serve-9731a371.js'; 7 | import { config as vitepressConfig } from '../../config'; 8 | 9 | let count = 1001; 10 | const scriptSetupRE = /<\s*script[^>]*\bsetup\b[^>]*/; 11 | // const attrRE = /\b(?\w+)=("|')(?[^\2]*)\2/g; 12 | const attrRE = /\b(?\w+)(="(?[^"]*)")?/g; 13 | const demoContainer = 'DemoContainer'; 14 | 15 | /** 解析 a="1" b="2" 这种格式的字符串,返回一个对象 */ 16 | function parseAttrs(attrs: string) { 17 | const meta = {}; 18 | let rez = null; 19 | while ((rez = attrRE.exec(attrs))) { 20 | const { key, value } = rez.groups; 21 | const newValue = value === void 0 ? true : value; 22 | if (Array.isArray(meta[key])) { 23 | meta[key].push(newValue); 24 | } else if (!meta[key]) { 25 | meta[key] = newValue; 26 | } else { 27 | meta[key] = [meta[key], newValue]; 28 | } 29 | } 30 | return meta; 31 | } 32 | 33 | /** 在script里添加组件引入语句 */ 34 | function addImportDeclaration(hoistedTags, localName: string, source: string) { 35 | const existingScriptIndex = hoistedTags.findIndex((tag) => { 36 | return scriptSetupRE.test(tag); 37 | }); 38 | if (existingScriptIndex === -1) { 39 | hoistedTags.push(``); 40 | } else { 41 | hoistedTags[existingScriptIndex] = hoistedTags[existingScriptIndex].replace( 42 | /<\/script>/, 43 | `\nimport ${localName} from '${source}';\n`, 44 | ); 45 | } 46 | } 47 | 48 | /** 在script里添加变量声明语句 */ 49 | function addVariableDeclaration(hoistedTags, localName: string, express: string) { 50 | const existingScriptIndex = hoistedTags.findIndex((tag) => { 51 | return scriptSetupRE.test(tag); 52 | }); 53 | if (existingScriptIndex === -1) { 54 | hoistedTags.push(``); 55 | } else { 56 | hoistedTags[existingScriptIndex] = hoistedTags[existingScriptIndex].replace( 57 | /<\/script>/, 58 | `\nconst ${localName} = ${express};\n`, 59 | ); 60 | } 61 | } 62 | 63 | type FileDTO = { 64 | filePath: string; 65 | codeStr: string; 66 | htmlStr?: string; 67 | language: string; 68 | }; 69 | 70 | /** 读取文件内容,返回统一格式。如果文件不存在,智能推测文件名,如果没有推测出来,返回null */ 71 | function resolveFile(absolutePath): FileDTO | null { 72 | // console.log(absolutePath); 73 | if (!absolutePath) return null; 74 | let filePath = absolutePath; 75 | 76 | if (!fs.existsSync(filePath)) { 77 | // 推测 absolutePath 78 | const extensions = ['.ts', '.tsx', '.vue', '.html']; 79 | for (const ext of extensions) { 80 | filePath = absolutePath + ext; 81 | if (fs.existsSync(filePath)) { 82 | break; 83 | } 84 | } 85 | } 86 | if (!fs.existsSync(filePath)) return null; 87 | const rawContent = fs.readFileSync(filePath, 'utf-8'); 88 | 89 | const language = filePath.match(/\.(\w+)$/)?.[1]; 90 | return { 91 | filePath, 92 | codeStr: encodeURIComponent(rawContent), 93 | language, 94 | htmlStr: encodeURIComponent(highlight(rawContent, language)), 95 | }; 96 | } 97 | 98 | const demos = {}; 99 | /** 将demo信息拼接成 html(vue template) 字符串 */ 100 | function genDemo(meta, md: MarkdownIt) { 101 | // @ts-ignore 102 | const { __path, __data: data } = md; 103 | const hoistedTags = data.hoistedTags || (data.hoistedTags = []); 104 | // console.log('meta: ', meta); 105 | let htmlOpenString = `<${demoContainer}`; 106 | //
${errorStr}
`; 125 | // return { htmlOpenString }; 126 | } 127 | const currentDir = path.dirname(__path); 128 | let absolutePath = path.resolve(currentDir, meta.src); 129 | const srcFile = resolveFile(absolutePath); 130 | if (meta.language) { 131 | srcFile.language = meta.language; 132 | } 133 | 134 | if (!srcFile) { 135 | let errorStr = `NotFound: file not found\n\t${meta.src}`; 136 | throw new Error(errorStr); 137 | // attrsStr += ` codeStr="${encodeURIComponent(errorStr)}"`; 138 | // htmlOpenString += ` ${attrsStr}>
${errorStr}
`; 139 | } else { 140 | const metaFiles = Array.isArray(meta.file) ? meta.file : [meta.file].filter(Boolean); 141 | 142 | const files = [srcFile, ...metaFiles.map((p) => resolveFile(path.resolve(currentDir, p)))] 143 | .filter((f, i, a) => { 144 | // console.log('filter', f); 145 | if (!f) return false; 146 | return a.findIndex((f2) => f2.filePath === f.filePath) === i; 147 | }) 148 | .map((f) => ({ ...f, name: path.relative(currentDir, f.filePath) })); 149 | // todo const localName = 'Demo' + md5(srcFile.filePath); 150 | const localName = `Demo${++count}`; 151 | const varName = `files${count}`; 152 | addVariableDeclaration(hoistedTags, varName, JSON.stringify(files)); 153 | const filesAttr = ` :files="${varName}"`; 154 | attrsStr += filesAttr; 155 | 156 | let useIframeMode = meta.iframe || absolutePath.endsWith('.html'); 157 | if (useIframeMode) { 158 | // const isSymbolicLink = fs.lstatSync(absolutePath).isSymbolicLink() 159 | attrsStr += ` iframe`; 160 | demos[localName] = { title: meta.title, entry: absolutePath }; 161 | fs.writeFileSync(path.resolve(process.cwd(), 'node_modules/demos.json'), JSON.stringify(demos, null, 2)); 162 | htmlOpenString += ` ${attrsStr}>