├── .babelrc.js ├── .circleci └── config.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmignore ├── .prettierrc.json ├── .vscode ├── launch.json └── settings.json ├── ChangeLog.md ├── LICENSE ├── README-zh_CN.md ├── README.md ├── changelog.config.json ├── commitlint.config.js ├── gulpfile.mjs ├── package.json ├── pnpm-lock.yaml ├── src ├── config.ts ├── index.ts └── once-init.ts ├── test ├── index.test.ts ├── once-init.test.ts └── tsconfig.json ├── tsconfig.json ├── vitest.config.ts └── webpack.config.js /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [["@babel/preset-env", { targets: { node: "current" } }]], 3 | }; 4 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Use the latest 2.1 version of CircleCI pipeline process engine. 2 | # See: https://circleci.com/docs/2.0/configuration-reference 3 | version: 2.1 4 | 5 | orbs: 6 | # The Node.js orb contains a set of prepackaged CircleCI configuration you can utilize 7 | # Orbs reduce the amount of configuration required for common tasks. 8 | # See the orb documentation here: https://circleci.com/developer/orbs/orb/circleci/node 9 | node: circleci/node@4.7 10 | codecov: codecov/codecov@3.2.3 11 | 12 | jobs: 13 | # Below is the definition of your job to build and test your app, you can rename and customize it as you want. 14 | build-and-test: 15 | # These next lines define a Docker executor: https://circleci.com/docs/2.0/executor-types/ 16 | # You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. 17 | # A list of available CircleCI Docker Convenience Images are available here: https://circleci.com/developer/images/image/cimg/node 18 | docker: 19 | - image: cimg/node:18.15 20 | # Then run your tests! 21 | # CircleCI will report the results back to your VCS provider. 22 | steps: 23 | # Checkout the code as the first step. 24 | - checkout 25 | # Next, the node orb's install-packages step will install the dependencies from a package.json. 26 | # The orb install-packages step will also automatically cache them for faster future runs. 27 | - run: sudo npm install --global pnpm typescript 28 | - run: pnpm install --frozen-lockfile 29 | - run: 30 | name: Run tests 31 | command: npm run test:cov 32 | - codecov/upload 33 | 34 | workflows: 35 | # Below is the definition of your workflow. 36 | # Inside the workflow, you provide the jobs you want to run, e.g this workflow runs the build-and-test job above. 37 | # CircleCI will run this workflow on every commit. 38 | # For more details on extending your workflow, see the configuration docs: https://circleci.com/docs/2.0/configuration-reference/#workflows 39 | sample: 40 | jobs: 41 | - build-and-test 42 | # For running simple node tests, you could optionally use the node/test job from the orb to replicate and replace the job above in fewer lines. 43 | # - node/test 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | package-lock.json 4 | dist 5 | *.log 6 | coverage -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | .husky 4 | coverage 5 | test 6 | .babelrc.js 7 | .eslint* 8 | .prettierrc* 9 | commitlint* 10 | jest* 11 | /src 12 | pnpm* 13 | tsconfig.json -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": "config.*", 5 | "options": { 6 | "quoteProps": "preserve", 7 | "singleQuote": false, 8 | "trailingComma": "none", 9 | "semi": false 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/src/index.js" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["bupoint", "fingerprintjs"] 3 | } 4 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | # 1.0.0 2 | 3 | 1. 由于用户可以在 Promise Function 里自定义相关的状态,`once-init`没有必要画蛇添足,因此取消了 target 和 factory 方法,取消了 loading 加载状态。 4 | 2. 添加了参数区分,现在,允许通过参数来划分不同的 Promise Function,参数通过`lodash.isEqual`来进行区分。 5 | 3. 添加了 强制执行方法 exceed() 和 等待执行方法 wait() 6 | 4. 同步方法为 get(),如果值在之前初始化过,则返回对应的值,否则返回 undefined,同步执行不再主动触发 promise。 7 | 5. send、request 和 refresh 是同一个方法,定义它们是为了方便更好的语义化。 8 | 9 | ## 1.4.0 10 | 11 | 1. 添加了错误处理机制; 12 | 2. 添加了`clear`方法用于清除缓存; 13 | 3. 添加了一些`get`方法用于获取参数状态; 14 | 15 | ## 1.3.0 16 | 17 | 1. 可以使用`script`导入`js`文件的方式访问`OnceInit`来调用;([#5](https://github.com/darkXmo/once-init/issues/5)) 18 | 2. 允许`import { oi } from 'once-init'`进行导入; 19 | 20 | ## 1.2.0 21 | 22 | 1. 解决了`refresh`执行顺序混乱的 bug;([#4](https://github.com/darkXmo/once-init/issues/4)) 23 | 2. 疑似解决了部分相同类型的赋值会报 TS 错误的 bug;([#2](https://github.com/darkXmo/once-init/issues/2)) 24 | 25 | ## 1.1.0 26 | 27 | 1. 易读性更强的文档; 28 | 2. 减少了`lodash`的打包大小;([#1](https://github.com/darkXmo/once-init/pull/1)) 29 | 30 | # TODO 31 | 32 | 1. 提供更小版本的自定义式判断参数的方案(即不需要 lodash 那么复杂的方案以减少打包大小)作为选择; 33 | 2. 加强类型,现在的 TS 类型无法满足 `axios.get = oi(axios.get).refresh` ; 34 | 35 | # History 36 | 37 | ## 0.8.0 38 | 39 | 1. If once-init initialized with a default value, the return type of `target`, `init`, `refresh` will not include `undefined`; 40 | 2. Better Type support. 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Xmo 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 | -------------------------------------------------------------------------------- /README-zh_CN.md: -------------------------------------------------------------------------------- 1 | # once-init 2 | 3 |

4 | license 5 | npm version 6 | circleci 7 | gzip size 8 | badge 9 |

10 | 11 | 🗼 Makes asynchronous function execution manageable. 12 | 13 | 封装可控的 `async function`。 14 | 15 | > 你可以让同一个 `async function` 不会在同一时间内被执行两次,以防止发出重复的请求。 16 | 17 | > 你可以让第二次执行 `async function` ,直接返回第一次执行的结果,而不是重复执行函数。 18 | 19 | > 解决大量的相同请求的问题。 20 | 21 | > 详细且精确的 `Typescript` 检查。 22 | 23 | ## 安装 24 | 25 | ```bash 26 | # 浏览器环境和nodejs环境都可用 27 | npm install once-init 28 | ``` 29 | 30 | ```html 31 | 32 | 35 | ``` 36 | 37 | ## 简介 38 | 39 | `once-init` 的核心思想是缓存和执行队列; 40 | 41 | ## 使用 42 | 43 | ```typescript 44 | // 0. 引入once-init 45 | import oi from "once-init"; 46 | 47 | // 1. 创建一个异步函数 48 | async function foo() { 49 | // do something, for example, request backend data. 50 | const res = await axios.get("xxx.com"); 51 | return res; 52 | } 53 | 54 | // 2. 用once-init封装这个异步函数 55 | const oiFoo = oi(foo); 56 | 57 | // 3. 执行封装后的函数 58 | oiFoo.init(); 59 | ``` 60 | 61 | 通常来说,`axios.get` 请求很适合进行封装,你可以用`refresh`来代替原来的 `axios.get` 实现无感处理重复请求。 62 | 63 | 只需要下面这一行: 64 | 65 | ```typescript 66 | axios.get = oi(axios.get).refresh as typeof axios.get; 67 | ``` 68 | 69 | ### 用例 70 | 71 | #### 不用 `once-init` 72 | 73 | ```typescript 74 | // 我们假设 axios.get("xxx.com") 返回的值是一个递增的数字,即第1次请求,会返回1,第2次请求会返回2,第n次请求会返回n。 75 | await foo(); // 返回 1 76 | await foo(); // 返回 2 77 | await foo(); // 返回 3 78 | ``` 79 | 80 | #### 使用 `once-init` 81 | 82 | ```typescript 83 | // once-init 会将重复执行重定向到第一次执行的结果上;(第一次执行后会缓存执行结果,类似单例模式) 84 | await oiFoo.init(); // 返回 1 85 | await oiFoo.init(); // 返回 1 86 | await oiFoo.init(); // 返回 1 87 | ``` 88 | 89 | 这意味着无论重复执行 `oiFoo.init` 多少次,`foo` 都只会执行第一次,返回第一次执行的结果;(就像缓存一样) 90 | 91 | ```typescript 92 | await Promise.all([oiFoo.init(), oiFoo.init(), oiFoo.init()]); // 返回 [1, 1, 1] 93 | await Promise.all([oiFoo.init(), oiFoo.init(), oiFoo.init()]); // 返回 [1, 1, 1] 94 | ``` 95 | 96 | ```typescript 97 | // 通常,如果你只会使用到init,你可以直接把 oiFoo 定义成 init 函数 98 | const oiFoo = oi(foo).init; 99 | 100 | await oiFoo(); 101 | ``` 102 | 103 | 如果你不使用缓存,只是希望防止同一时间发出重复请求,你可以使用`refresh`: 104 | 105 | ```typescript 106 | // refresh和init在同一时间执行多次,都会阻止重复执行,多余的async function会返回第一次的结果; 107 | await Promise.all([oiFoo.refresh(), oiFoo.refresh(), oiFoo.refresh()]); // 返回 [1, 1, 1] 108 | // 但refresh如果当前没有其它重复的async function在执行,会刷新结果,并同时刷新缓存(影响到下一次init的返回); 109 | await Promise.all([oiFoo.refresh(), oiFoo.refresh(), oiFoo.refresh()]); // 返回 [2, 2, 2] 110 | await oiFoo.init(); // 返回 2 111 | ``` 112 | 113 | > `once-init` 会区分参数,如果传入的异步函数有参,那么传入不同的参数将被视为两个不同的异步函数,不会共享缓存和执行队列;(使用`lodash.isEqual`判断参数是否相等) 114 | 115 | 下面这个复杂用例将会给你提供灵感: 116 | 117 | ```typescript 118 | // 假设 xxx.com/+ 会返回正数, xxx.com/- 会返回负数,两者有独立的缓存,且绝对值都递增 119 | async function foo(op: "+" | "-") { 120 | const res = await axios.get(`xxx.com/${op}`); 121 | return res; 122 | } 123 | 124 | const oiFoo = oi(foo); 125 | await oiFoo.init("-"); // 返回 -1 126 | await oiFoo.refresh("-"); // 返回 -2 127 | await oiFoo.refresh("-"); // 返回 -3 128 | 129 | await oiFoo.refresh("+"); // 返回 1 130 | await oiFoo.init("-"); // 返回 -3 131 | ``` 132 | 133 | ## api 134 | 135 | > `refresh`、`exceed`会刷新`init`的缓存; 136 | 137 | > 所有`api`都能接收参数,参数就是封装的源`function`的参数; 138 | 139 | 在下面的所有例子中: 140 | 141 | > 我们假设 `axios.get("xxx.com")` 返回的值是一个递增的数字,即第 1 次请求,会返回`1`,第 2 次请求会返回`2`,第 n 次请求会返回`n`。 142 | 143 | > 我们假设 `foo` 的执行时间为 `50ms`;其它时间忽略不计; 144 | 145 | ### `OnceInit.init` 146 | 147 | 最常用的`api`,无论你调用多少次,`init`都只会执行一次,并返回给你第一次执行(或缓存中)的结果。 148 | 149 | ```typescript 150 | async function foo() { 151 | const res = await axios.get("xxx.com"); 152 | return res; 153 | } 154 | 155 | const oiFoo = oi(foo); 156 | 157 | await oiFoo.init(); // 50秒后,返回 1 158 | await oiFoo.init(); // 0秒后,返回 1 159 | await oiFoo.init(); // 0秒后,返回 1 160 | ``` 161 | 162 | 如果多个 `init` 同时执行(第二次调用的时候,第一次执行还没有完成),则第二次及之后的调用将会等待第一次执行的结果,然后返回第一次执行的结果; 163 | 164 | ```typescript 165 | setTimeout(oiFoo.init, 30); // 20秒后,执行完毕,返回 1 166 | await oiFoo.init(); // 50秒后,执行完毕,返回 1 167 | ``` 168 | 169 | ### `OnceInit.refresh` 170 | 171 | `refresh` 刷新 `once-init` 的缓存;如果你在第一次调用 `init` 之后,希望再执行一次 `foo` 更新后端数据,你可以执行 `refresh` 。 172 | 173 | `refresh` 之后,再执行 `oiFoo` 将会返回刷新后的数据; 174 | 175 | ```typescript 176 | async function foo() { 177 | const res = await axios.get("xxx.com"); 178 | return res; 179 | } 180 | 181 | const oiFoo = oi(foo); 182 | 183 | await oiFoo.init(); // 50秒后,返回 1 (无缓存,所以执行) 184 | await oiFoo.refresh(); // 50秒后,返回 2 185 | // refresh 会刷新 init 的缓存 186 | await oiFoo.init(); // 0秒后,返回 2 (已有缓存,所以不会执行) 187 | await oiFoo.refresh(); // 50秒后,返回 3 188 | ``` 189 | 190 | > **如果`refresh`正在刷新,则在`refresh`结束前执行的`init`也会`refresh`结束结束后再返回值,而不会直接返回旧的缓存结果。** 191 | 192 | 多个 `refresh` 如果在同一时间里同时调用,只会执行一次,并返回第一次执行的结果。【如果你希望同一时间里同时执行,会执行多次,请使用 `exceed` 】 193 | 194 | ```typescript 195 | setTimeout(oiFoo.refresh, 30); // 20秒后,执行完毕,返回 1 196 | await oiFoo.refresh(); // 50秒后,执行完毕,返回 1 197 | ``` 198 | 199 | `refresh` 和 `init` 共享一个执行队列,这意味着如果 `init` 和 `refresh` 同时执行,也将只会执行一次并返回第一次执行的结果; 200 | 201 | ```typescript 202 | setTimeout(oiFoo.refresh, 30); // 20秒后,执行完毕,返回 1 203 | await oiFoo.init(); // 50秒后,执行完毕,返回 1 204 | ``` 205 | 206 | ### `OnceInit.get` 207 | 208 | `get` 是 `init` 的同步版本,相当于获取缓存;如果有缓存的值,那么 `get` 会返回缓存值;如果没有,则会返回 `undefined`。 209 | 210 | ```typescript 211 | oiFoo.get(); // 返回 undefined (从未执行过,所以没有缓存) 212 | 213 | await oiFoo.init(); // 50秒后,返回 1 214 | oiFoo.get(); // 返回 1 215 | ``` 216 | 217 | ### `OnceInit.execute` 218 | 219 | `execute` 会直接执行源函数,它既不会修改缓存,也不会插入执行队列。 220 | 221 | ```typescript 222 | await oiFoo.init(); // 50秒后,返回 1 223 | await oiFoo.execute(); // 50秒后,返回 2 224 | await oiFoo.init(); // 0秒后,返回 1 (缓存未更新) 225 | await oiFoo.refresh(); // 50秒后,返回 3 (缓存更新,重新获取后端值,由于后端在上次execute请求中虽未更新缓存,但更新了后端,所以返回值为3) 226 | ``` 227 | 228 | ### `OnceInit.wait` 229 | 230 | 如果当前异步函数正在执行,`wait`会等待直到结束,如果异步函数没有执行,则立即返回。 231 | 232 | `wait` 没有返回值; 233 | 234 | ```typescript 235 | await oiFoo.wait(); // 等待0秒 236 | 237 | oiFoo.init(); 238 | oiFoo.get(); // 返回 undefined 239 | await oiFoo.wait(); // 等待50秒 240 | oiFoo.get(); // 返回 1 241 | ``` 242 | 243 | ### `OnceInit.exceed` 244 | 245 | !!! 这是一个会造成理解困难的函数,**请尽量不要和其它 api 混合使用**; 246 | 247 | `exceed` 会强制执行函数,无论现在是否正在执行另一个相同的异步函数。 248 | 249 | `exceed` 也会刷新缓存。 250 | 251 | `refresh` 和 `init` 的执行受 `exceed` 影响,但 `exceed` 的执行不受 `refresh` 和 `init` 的影响; 252 | 253 | ```typescript 254 | await Promise.all([oiFoo.exceed(), oiFoo.exceed(), oiFoo.exceed()]); // 50秒后,返回 [1, 2, 3]; 255 | 256 | // exceed 会刷新缓存 257 | await oiFoo.init(); // 0秒后,返回 3 258 | await oiFoo.refresh(); // 50秒后,返回 4 259 | ``` 260 | 261 | 如果 `exceed` 正在执行, `refresh` 和 `init` 将会返回 `exceed` 执行的结果; 262 | 263 | > **_注意(WARNING)_**: 如果你不断地执行 `exceed` ,会导致执行队列不断地刷新,而同一时间执行的 `refresh` 和 `init` 将返回你**执行 `init` 和 `refresh` 前最后一次执行**的`exceed`的结果。 264 | 265 | > 不推荐你短时间多次调用 `exceed` ,它可能让你对返回结果造成困扰;大多数情况下,`refresh` 和 `execute` 会是更好的替代品。 266 | 267 | 请在理解下面这个例子的前提下在实际应用场景中应用 `exceed` 。 268 | 269 | ```typescript 270 | await Promise.all([oiFoo.refresh(), oiFoo.exceed()]); // 50秒后,返回 [1, 2]; 271 | 272 | await Promise.all([oiFoo.exceed(), oiFoo.refresh()]); // 50秒后,返回 [3, 3]; 273 | 274 | await Promise.all([oiFoo.exceed(), oiFoo.refresh(), oiFoo.init()]); // 50秒后,返回 [4, 4, 3]; 275 | 276 | await oiFoo.init(); // 0秒后,返回 4 277 | 278 | await Promise.all([ 279 | oiFoo.exceed(), 280 | oiFoo.refresh(), 281 | oiFoo.exceed(), 282 | oiFoo.refresh(), 283 | oiFoo.init(), 284 | ]); // 50秒后,返回 [5, 5, 6, 6, 4]; 285 | ``` 286 | 287 | ### `OnceInit.clear` 288 | 289 | `clear`可以清空缓存,如果你担心内存泄漏的问题,你可以在适当的时候调用它。 290 | 291 | ```typescript 292 | oiFoo.clear(); 293 | // 你可以只删除某个参数的缓存 294 | oiFoo.clear(params); 295 | ``` 296 | 297 | ## error handle 298 | 299 | 如果封装的函数执行的时候 Throw 了一个 Error,则不会更新缓存。 300 | 301 | ```typescript 302 | let cnt = 0; 303 | async function incrementPromise() { 304 | cnt++; 305 | // 该函数前两次执行将会抛出错误; 306 | if (cnt < 3 || cnt === 4) throw new Error("cnt " + cnt.toString()); 307 | return cnt; 308 | } 309 | 310 | const fn = oi(incrementPromise); 311 | await fn.init(); // Throw Error("cnt 1") 312 | await fn.refresh(); // Throw Error("cnt 2") 313 | fn.get(); // undefined 314 | await fn.refresh(); // 3 315 | await fn.refresh(); // Throw Error("cnt 4") 316 | await fn.init(); // 3 317 | fn.get(); // 3 318 | ``` 319 | 320 | 如果`init`失败,将不会创建缓存,下次执行`init`将第二次创建 Promise; 321 | 322 | ```typescript 323 | await fn.init(); // Throw Error("cnt 1") 324 | await fn.init(); // Throw Error("cnt 2") 325 | ``` 326 | 327 | ![Alt](https://repobeats.axiom.co/api/embed/3e2a2caafe9c373cbe8fa4a16c3fb1b3d2e20fdf.svg "Repobeats analytics image") 328 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # once-init 2 | 3 |

4 | license 5 | npm version 6 | circleci 7 | gzip size 8 | badge 9 |

10 | 11 | 🗼 Makes asynchronous function execution manageable. 12 | 13 | 封装可控的 `async function`。 14 | 15 | > 你可以让同一个 `async function` 不会在同一时间内被执行两次,以防止发出重复的请求。 16 | 17 | > 你可以让第二次执行 `async function` ,直接返回第一次执行的结果,而不是重复执行函数。 18 | 19 | > 解决大量的相同请求的问题。 20 | 21 | > 详细且精确的 `Typescript` 检查。 22 | 23 | ## 安装 24 | 25 | ```bash 26 | # 浏览器环境和nodejs环境都可用 27 | npm install once-init 28 | ``` 29 | 30 | ```html 31 | 32 | 35 | ``` 36 | 37 | ## 简介 38 | 39 | `once-init` 的核心思想是缓存和执行队列; 40 | 41 | ## 使用 42 | 43 | ```typescript 44 | // 0. 引入once-init 45 | import oi from "once-init"; 46 | 47 | // 1. 创建一个异步函数 48 | async function foo() { 49 | // do something, for example, request backend data. 50 | const res = await axios.get("xxx.com"); 51 | return res; 52 | } 53 | 54 | // 2. 用once-init封装这个异步函数 55 | const oiFoo = oi(foo); 56 | 57 | // 3. 执行封装后的函数 58 | oiFoo.init(); 59 | ``` 60 | 61 | 通常来说,`axios.get` 请求很适合进行封装,你可以用`refresh`来代替原来的 `axios.get` 实现无感处理重复请求。 62 | 63 | 只需要下面这一行: 64 | 65 | ```typescript 66 | axios.get = oi(axios.get).refresh as typeof axios.get; 67 | ``` 68 | 69 | ### 用例 70 | 71 | #### 不用 `once-init` 72 | 73 | ```typescript 74 | // 我们假设 axios.get("xxx.com") 返回的值是一个递增的数字,即第1次请求,会返回1,第2次请求会返回2,第n次请求会返回n。 75 | await foo(); // 返回 1 76 | await foo(); // 返回 2 77 | await foo(); // 返回 3 78 | ``` 79 | 80 | #### 使用 `once-init` 81 | 82 | ```typescript 83 | // once-init 会将重复执行重定向到第一次执行的结果上;(第一次执行后会缓存执行结果,类似单例模式) 84 | await oiFoo.init(); // 返回 1 85 | await oiFoo.init(); // 返回 1 86 | await oiFoo.init(); // 返回 1 87 | ``` 88 | 89 | 这意味着无论重复执行 `oiFoo.init` 多少次,`foo` 都只会执行第一次,返回第一次执行的结果;(就像缓存一样) 90 | 91 | ```typescript 92 | await Promise.all([oiFoo.init(), oiFoo.init(), oiFoo.init()]); // 返回 [1, 1, 1] 93 | await Promise.all([oiFoo.init(), oiFoo.init(), oiFoo.init()]); // 返回 [1, 1, 1] 94 | ``` 95 | 96 | ```typescript 97 | // 通常,如果你只会使用到init,你可以直接把 oiFoo 定义成 init 函数 98 | const oiFoo = oi(foo).init; 99 | 100 | await oiFoo(); 101 | ``` 102 | 103 | 如果你不使用缓存,只是希望防止同一时间发出重复请求,你可以使用`refresh`: 104 | 105 | ```typescript 106 | // refresh和init在同一时间执行多次,都会阻止重复执行,多余的async function会返回第一次的结果; 107 | await Promise.all([oiFoo.refresh(), oiFoo.refresh(), oiFoo.refresh()]); // 返回 [1, 1, 1] 108 | // 但refresh如果当前没有其它重复的async function在执行,会刷新结果,并同时刷新缓存(影响到下一次init的返回); 109 | await Promise.all([oiFoo.refresh(), oiFoo.refresh(), oiFoo.refresh()]); // 返回 [2, 2, 2] 110 | await oiFoo.init(); // 返回 2 111 | ``` 112 | 113 | > `once-init` 会区分参数,如果传入的异步函数有参,那么传入不同的参数将被视为两个不同的异步函数,不会共享缓存和执行队列;(使用`lodash.isEqual`判断参数是否相等) 114 | 115 | 下面这个复杂用例将会给你提供灵感: 116 | 117 | ```typescript 118 | // 假设 xxx.com/+ 会返回正数, xxx.com/- 会返回负数,两者有独立的缓存,且绝对值都递增 119 | async function foo(op: "+" | "-") { 120 | const res = await axios.get(`xxx.com/${op}`); 121 | return res; 122 | } 123 | 124 | const oiFoo = oi(foo); 125 | await oiFoo.init("-"); // 返回 -1 126 | await oiFoo.refresh("-"); // 返回 -2 127 | await oiFoo.refresh("-"); // 返回 -3 128 | 129 | await oiFoo.refresh("+"); // 返回 1 130 | await oiFoo.init("-"); // 返回 -3 131 | ``` 132 | 133 | ## api 134 | 135 | > `refresh`、`exceed`会刷新`init`的缓存; 136 | 137 | > 所有`api`都能接收参数,参数就是封装的源`function`的参数; 138 | 139 | 在下面的所有例子中: 140 | 141 | > 我们假设 `axios.get("xxx.com")` 返回的值是一个递增的数字,即第 1 次请求,会返回`1`,第 2 次请求会返回`2`,第 n 次请求会返回`n`。 142 | 143 | > 我们假设 `foo` 的执行时间为 `50ms`;其它时间忽略不计; 144 | 145 | ### `OnceInit.init` 146 | 147 | 最常用的`api`,无论你调用多少次,`init`都只会执行一次,并返回给你第一次执行(或缓存中)的结果。 148 | 149 | ```typescript 150 | async function foo() { 151 | const res = await axios.get("xxx.com"); 152 | return res; 153 | } 154 | 155 | const oiFoo = oi(foo); 156 | 157 | await oiFoo.init(); // 50秒后,返回 1 158 | await oiFoo.init(); // 0秒后,返回 1 159 | await oiFoo.init(); // 0秒后,返回 1 160 | ``` 161 | 162 | 如果多个 `init` 同时执行(第二次调用的时候,第一次执行还没有完成),则第二次及之后的调用将会等待第一次执行的结果,然后返回第一次执行的结果; 163 | 164 | ```typescript 165 | setTimeout(oiFoo.init, 30); // 20秒后,执行完毕,返回 1 166 | await oiFoo.init(); // 50秒后,执行完毕,返回 1 167 | ``` 168 | 169 | ### `OnceInit.refresh` 170 | 171 | `refresh` 刷新 `once-init` 的缓存;如果你在第一次调用 `init` 之后,希望再执行一次 `foo` 更新后端数据,你可以执行 `refresh` 。 172 | 173 | `refresh` 之后,再执行 `oiFoo` 将会返回刷新后的数据; 174 | 175 | ```typescript 176 | async function foo() { 177 | const res = await axios.get("xxx.com"); 178 | return res; 179 | } 180 | 181 | const oiFoo = oi(foo); 182 | 183 | await oiFoo.init(); // 50秒后,返回 1 (无缓存,所以执行) 184 | await oiFoo.refresh(); // 50秒后,返回 2 185 | // refresh 会刷新 init 的缓存 186 | await oiFoo.init(); // 0秒后,返回 2 (已有缓存,所以不会执行) 187 | await oiFoo.refresh(); // 50秒后,返回 3 188 | ``` 189 | 190 | > **如果`refresh`正在刷新,则在`refresh`结束前执行的`init`也会`refresh`结束结束后再返回值,而不会直接返回旧的缓存结果。** 191 | 192 | 多个 `refresh` 如果在同一时间里同时调用,只会执行一次,并返回第一次执行的结果。【如果你希望同一时间里同时执行,会执行多次,请使用 `exceed` 】 193 | 194 | ```typescript 195 | setTimeout(oiFoo.refresh, 30); // 20秒后,执行完毕,返回 1 196 | await oiFoo.refresh(); // 50秒后,执行完毕,返回 1 197 | ``` 198 | 199 | `refresh` 和 `init` 共享一个执行队列,这意味着如果 `init` 和 `refresh` 同时执行,也将只会执行一次并返回第一次执行的结果; 200 | 201 | ```typescript 202 | setTimeout(oiFoo.refresh, 30); // 20秒后,执行完毕,返回 1 203 | await oiFoo.init(); // 50秒后,执行完毕,返回 1 204 | ``` 205 | 206 | ### `OnceInit.get` 207 | 208 | `get` 是 `init` 的同步版本,相当于获取缓存;如果有缓存的值,那么 `get` 会返回缓存值;如果没有,则会返回 `undefined`。 209 | 210 | ```typescript 211 | oiFoo.get(); // 返回 undefined (从未执行过,所以没有缓存) 212 | 213 | await oiFoo.init(); // 50秒后,返回 1 214 | oiFoo.get(); // 返回 1 215 | ``` 216 | 217 | ### `OnceInit.execute` 218 | 219 | `execute` 会直接执行源函数,它既不会修改缓存,也不会插入执行队列。 220 | 221 | ```typescript 222 | await oiFoo.init(); // 50秒后,返回 1 223 | await oiFoo.execute(); // 50秒后,返回 2 224 | await oiFoo.init(); // 0秒后,返回 1 (缓存未更新) 225 | await oiFoo.refresh(); // 50秒后,返回 3 (缓存更新,重新获取后端值,由于后端在上次execute请求中虽未更新缓存,但更新了后端,所以返回值为3) 226 | ``` 227 | 228 | ### `OnceInit.wait` 229 | 230 | 如果当前异步函数正在执行,`wait`会等待直到结束,如果异步函数没有执行,则立即返回。 231 | 232 | `wait` 没有返回值; 233 | 234 | ```typescript 235 | await oiFoo.wait(); // 等待0秒 236 | 237 | oiFoo.init(); 238 | oiFoo.get(); // 返回 undefined 239 | await oiFoo.wait(); // 等待50秒 240 | oiFoo.get(); // 返回 1 241 | ``` 242 | 243 | ### `OnceInit.exceed` 244 | 245 | !!! 这是一个会造成理解困难的函数,**请尽量不要和其它 api 混合使用**; 246 | 247 | `exceed` 会强制执行函数,无论现在是否正在执行另一个相同的异步函数。 248 | 249 | `exceed` 也会刷新缓存。 250 | 251 | `refresh` 和 `init` 的执行受 `exceed` 影响,但 `exceed` 的执行不受 `refresh` 和 `init` 的影响; 252 | 253 | ```typescript 254 | await Promise.all([oiFoo.exceed(), oiFoo.exceed(), oiFoo.exceed()]); // 50秒后,返回 [1, 2, 3]; 255 | 256 | // exceed 会刷新缓存 257 | await oiFoo.init(); // 0秒后,返回 3 258 | await oiFoo.refresh(); // 50秒后,返回 4 259 | ``` 260 | 261 | 如果 `exceed` 正在执行, `refresh` 和 `init` 将会返回 `exceed` 执行的结果; 262 | 263 | > **_注意(WARNING)_**: 如果你不断地执行 `exceed` ,会导致执行队列不断地刷新,而同一时间执行的 `refresh` 和 `init` 将返回你**执行 `init` 和 `refresh` 前最后一次执行**的`exceed`的结果。 264 | 265 | > 不推荐你短时间多次调用 `exceed` ,它可能让你对返回结果造成困扰;大多数情况下,`refresh` 和 `execute` 会是更好的替代品。 266 | 267 | 请在理解下面这个例子的前提下在实际应用场景中应用 `exceed` 。 268 | 269 | ```typescript 270 | await Promise.all([oiFoo.refresh(), oiFoo.exceed()]); // 50秒后,返回 [1, 2]; 271 | 272 | await Promise.all([oiFoo.exceed(), oiFoo.refresh()]); // 50秒后,返回 [3, 3]; 273 | 274 | await Promise.all([oiFoo.exceed(), oiFoo.refresh(), oiFoo.init()]); // 50秒后,返回 [4, 4, 3]; 275 | 276 | await oiFoo.init(); // 0秒后,返回 4 277 | 278 | await Promise.all([ 279 | oiFoo.exceed(), 280 | oiFoo.refresh(), 281 | oiFoo.exceed(), 282 | oiFoo.refresh(), 283 | oiFoo.init(), 284 | ]); // 50秒后,返回 [5, 5, 6, 6, 4]; 285 | ``` 286 | 287 | ### `OnceInit.clear` 288 | 289 | `clear`可以清空缓存,如果你担心内存泄漏的问题,你可以在适当的时候调用它。 290 | 291 | ```typescript 292 | oiFoo.clear(); 293 | // 你可以只删除某个参数的缓存 294 | oiFoo.clear(params); 295 | ``` 296 | 297 | ## error handle 298 | 299 | 如果封装的函数执行的时候 Throw 了一个 Error,则不会更新缓存。 300 | 301 | ```typescript 302 | let cnt = 0; 303 | async function incrementPromise() { 304 | cnt++; 305 | // 该函数前两次执行将会抛出错误; 306 | if (cnt < 3 || cnt === 4) throw new Error("cnt " + cnt.toString()); 307 | return cnt; 308 | } 309 | 310 | const fn = oi(incrementPromise); 311 | await fn.init(); // Throw Error("cnt 1") 312 | await fn.refresh(); // Throw Error("cnt 2") 313 | fn.get(); // undefined 314 | await fn.refresh(); // 3 315 | await fn.refresh(); // Throw Error("cnt 4") 316 | await fn.init(); // 3 317 | fn.get(); // 3 318 | ``` 319 | 320 | 如果`init`失败,将不会创建缓存,下次执行`init`将第二次创建 Promise; 321 | 322 | ```typescript 323 | await fn.init(); // Throw Error("cnt 1") 324 | await fn.init(); // Throw Error("cnt 2") 325 | ``` 326 | 327 | ![Alt](https://repobeats.axiom.co/api/embed/3e2a2caafe9c373cbe8fa4a16c3fb1b3d2e20fdf.svg "Repobeats analytics image") 328 | -------------------------------------------------------------------------------- /changelog.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "disableEmoji": false, 3 | "format": "{type}{scope}: {emoji}{subject}", 4 | "list": [ 5 | "feat", 6 | "perf", 7 | "docs", 8 | "fix", 9 | "style", 10 | "ci", 11 | "test", 12 | "refactor", 13 | "build", 14 | "chore" 15 | ], 16 | "maxMessageLength": 64, 17 | "minMessageLength": 3, 18 | "questions": ["type", "scope", "subject", "issues"], 19 | "scopes": ["framework-主架构", "configuration-配置文件", "others-其它"], 20 | "types": { 21 | "feat": { 22 | "description": "添加或修改功能", 23 | "emoji": "🎸", 24 | "value": "feat" 25 | }, 26 | "perf": { 27 | "description": "不涉及具体功能的性能优化", 28 | "emoji": "⚡️", 29 | "value": "perf" 30 | }, 31 | "fix": { 32 | "description": "错误修正", 33 | "emoji": "🐛", 34 | "value": "fix" 35 | }, 36 | "docs": { 37 | "description": "文档修改", 38 | "emoji": "✏️", 39 | "value": "docs" 40 | }, 41 | "style": { 42 | "description": "代码结构优化", 43 | "emoji": "💄", 44 | "value": "style" 45 | }, 46 | "ci": { 47 | "description": "配置文件", 48 | "emoji": "🎡", 49 | "value": "ci" 50 | }, 51 | "refactor": { 52 | "description": "项目重构", 53 | "emoji": "💡", 54 | "value": "refactor" 55 | }, 56 | "test": { 57 | "description": "添加测试", 58 | "emoji": "💍", 59 | "value": "test" 60 | }, 61 | "build": { 62 | "description": "发布/预发布", 63 | "emoji": "🏹", 64 | "value": "build" 65 | }, 66 | "chore": { 67 | "description": "只是提交而已", 68 | "value": "chore" 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ["@commitlint/config-conventional"] }; 2 | -------------------------------------------------------------------------------- /gulpfile.mjs: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra"; 2 | import inquirer from "inquirer"; 3 | import { format } from "prettier"; 4 | import { spawnSync, exec } from "child_process"; 5 | import util from "util"; 6 | import chalk from "chalk"; 7 | const execPromise = util.promisify(exec); 8 | 9 | /** 10 | * @param {string} oldVersion 11 | * @returns {Promise} 12 | */ 13 | const checkVersion = async (oldVersion) => { 14 | if (oldVersion.includes("beta")) { 15 | const V = oldVersion.split("-beta.")[0]; 16 | const B = Number(oldVersion.split("-beta.")[1]); 17 | const update = `${V}-beta.${B + 1}`; 18 | const publish = `${V}`; 19 | const { option } = await inquirer.prompt([ 20 | { 21 | type: "list", 22 | name: "option", 23 | message: `当前版本为${oldVersion ?? "未定义"},请选择更新级别`, 24 | choices: [ 25 | { value: "update", name: `更新当前测试(${update})` }, 26 | { value: "publish", name: `结束当前测试(${publish})` }, 27 | { value: "old", name: `不变更版本号(${oldVersion})` }, 28 | ], 29 | default: "update", 30 | }, 31 | ]); 32 | if (option === "update") { 33 | return update; 34 | } else if (option === "publish") { 35 | return publish; 36 | } else { 37 | return oldVersion; 38 | } 39 | } else { 40 | const [H, N, S] = oldVersion.split(".").map((item) => Number(item)); 41 | const B = "-beta.0"; 42 | const small = `${H}.${N}.${S + 1}${B}`; 43 | const normal = `${H}.${N + 1}.0${B}`; 44 | const huge = `${H + 1}.0.0${B}`; 45 | const { option } = await inquirer.prompt([ 46 | { 47 | type: "list", 48 | name: "option", 49 | message: `当前版本为${oldVersion ?? "未定义"},请选择更新级别`, 50 | choices: [ 51 | { value: "small", name: `小版本测试(${small})` }, 52 | { value: "normal", name: `普通版本测试(${normal})` }, 53 | { value: "huge", name: `大版本测试(${huge})` }, 54 | { value: "old", name: `不变更版本号(${oldVersion})` }, 55 | ], 56 | }, 57 | ]); 58 | if (option === "small") { 59 | return small; 60 | } else if (option === "normal") { 61 | return normal; 62 | } else if (option === "huge") { 63 | return huge; 64 | } else { 65 | return oldVersion; 66 | } 67 | } 68 | }; 69 | 70 | async function changeVersion(version) { 71 | const packageFile = await fs.readJSON("./package.json"); 72 | const versionFilePath = "./src/config.ts"; 73 | const configFileStr = (await fs.readFile(versionFilePath, "utf-8")) 74 | .replace("/** 请勿手动修改本文件,本文件通过命令行自动生成 */\n*", "") 75 | .replace("export default ", "") 76 | .replace(/\/\*\*.*\*\/\n*/g, ""); 77 | const configFile = JSON.parse(configFileStr); 78 | packageFile.version = version; 79 | configFile.version = version; 80 | const str = format( 81 | `/** 请勿手动修改本文件,本文件通过命令行自动生成 */\nexport default ${JSON.stringify( 82 | configFile, 83 | null, 84 | 2 85 | )}`, 86 | { 87 | singleQuote: false, 88 | trailingComma: "none", 89 | semi: false, 90 | quoteProps: "preserve", 91 | parser: "babel", 92 | } 93 | ); 94 | await Promise.all([ 95 | fs.writeJson("./package.json", packageFile, { 96 | spaces: 2, 97 | }), 98 | fs.writeFile(versionFilePath, str), 99 | ]); 100 | spawnSync("npm run build", { 101 | stdio: "inherit", 102 | shell: true, 103 | }); 104 | console.log(`config, package.json 的版本号已更新为${version}`); 105 | } 106 | 107 | async function getVersion() { 108 | const packageFile = await fs.readJSON("./package.json"); 109 | /** @type { string } */ 110 | return packageFile.version; 111 | } 112 | 113 | export async function customizeVersion() { 114 | // 确认版本 115 | const oldVerion = await getVersion(); 116 | const { version } = await inquirer.prompt([ 117 | { 118 | type: "input", 119 | name: "version", 120 | message: `当前版本为${oldVerion ?? "未定义"},请输入版本号`, 121 | default: "oldVerion", 122 | }, 123 | ]); 124 | await changeVersion(version); 125 | } 126 | 127 | const npmPublish = (version) => { 128 | if (version.includes("beta")) { 129 | const command = "npm publish --tag beta"; 130 | console.log(command); 131 | return spawnSync(command, { 132 | stdio: "inherit", 133 | shell: true, 134 | }); 135 | } 136 | const command = "npm publish"; 137 | console.log(command); 138 | return spawnSync(command, { stdio: "inherit", shell: true }); 139 | }; 140 | 141 | export async function publish() { 142 | // 确认版本 143 | const packageFile = await fs.readJSON("./package.json"); 144 | /** @type { string } */ 145 | let version = packageFile.version; 146 | const checkedVersion = await checkVersion(version); 147 | if (checkedVersion) { 148 | if (version !== checkedVersion) { 149 | version = checkedVersion; 150 | await changeVersion(version); 151 | spawnSync(`git add . && git commit -m "build: release v${version}"`, { 152 | stdio: "inherit", 153 | shell: true, 154 | }); 155 | } 156 | const branch = ( 157 | await execPromise("git rev-parse --abbrev-ref HEAD") 158 | ).stdout.trim(); 159 | if (branch === "master" || branch === "main") { 160 | // 判断是否是`master`分支,如果是,允许任意版本发布 161 | npmPublish(version); 162 | spawnSync(`git add . && git commit -m "build: release v${version}"`, { 163 | stdio: "inherit", 164 | shell: true, 165 | }); 166 | if (!version.includes("beta")) { 167 | spawnSync(`git tag v${version}`, { stdio: "inherit", shell: true }); 168 | spawnSync(`git push origin --tags`, { stdio: "inherit", shell: true }); 169 | } 170 | return; 171 | } else { 172 | // beta版本 173 | if (!version.includes("beta")) { 174 | console.log( 175 | `当前分支为${chalk.yellow(branch)}, 非master分支只允许发布beta版本` 176 | ); 177 | return await changeVersion(oldVersion); 178 | } else { 179 | npmPublish(version); 180 | return spawnSync( 181 | `git add . && git commit -m "build: release v${version}"`, 182 | { 183 | stdio: "inherit", 184 | shell: true, 185 | } 186 | ); 187 | } 188 | } 189 | } 190 | } 191 | 192 | export default publish; 193 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "once-init", 3 | "version": "1.4.0", 4 | "description": "Init a target by promise only once.", 5 | "repository": "git@github.com:darkXmo/once-init.git", 6 | "author": "Xmo <18851989097@163.com>", 7 | "keywords": [ 8 | "front-end", 9 | "xmon", 10 | "xmo", 11 | "init", 12 | "promise", 13 | "function" 14 | ], 15 | "homepage": "https://github.com/darkXmo/once-init", 16 | "bugs": { 17 | "url": "https://github.com/darkXmo/once-init/issues" 18 | }, 19 | "license": "MIT", 20 | "devDependencies": { 21 | "@babel/core": "^7.22.5", 22 | "@babel/preset-env": "^7.22.5", 23 | "@babel/preset-typescript": "^7.22.5", 24 | "@commitlint/cli": "^17.6.5", 25 | "@commitlint/config-conventional": "^17.6.5", 26 | "@types/fs-extra": "^11.0.1", 27 | "@types/inquirer": "^9.0.3", 28 | "@types/lodash": "^4.14.195", 29 | "@types/node": "^20.2.5", 30 | "@vitest/coverage-v8": "^0.32.0", 31 | "chalk": "^5.2.0", 32 | "cz-conventional-changelog": "^3.3.0", 33 | "fs-extra": "^11.1.1", 34 | "git-cz": "^4.9.0", 35 | "gulp": "^4.0.2", 36 | "husky": "^8.0.3", 37 | "inquirer": "^9.2.7", 38 | "lint-staged": "^13.2.2", 39 | "lodash": "^4.17.21", 40 | "prettier": "^2.8.8", 41 | "rimraf": "^5.0.1", 42 | "ts-loader": "^9.4.3", 43 | "typescript": "^5.1.3", 44 | "vitest": "^0.32.0", 45 | "webpack": "^5.86.0", 46 | "webpack-cli": "^5.1.4" 47 | }, 48 | "lint-staged": { 49 | "src/**/*.js": [ 50 | "prettier --write --ignore-unknown" 51 | ], 52 | "src/**/*.ts": [ 53 | "prettier --write --ignore-unknown" 54 | ], 55 | "test/**/*.js": [ 56 | "prettier --write --ignore-unknown" 57 | ], 58 | "test/**/*.ts": [ 59 | "prettier --write --ignore-unknown" 60 | ], 61 | "*.{js,ts,css,md}": "prettier --write --ignore-unknown" 62 | }, 63 | "main": "dist/index.js", 64 | "files": [ 65 | "dist" 66 | ], 67 | "scripts": { 68 | "c": "npm run commit", 69 | "commit": "git add . && git status && git-cz", 70 | "prepare": "husky install", 71 | "test": "vitest", 72 | "test:cov": "vitest --coverage", 73 | "build": "rimraf dist && webpack --mode production", 74 | "dev": "webpack --watch --mode development", 75 | "ci": "npm run lint && npm run build", 76 | "release": "npm run build && gulp && git push", 77 | "version": "gulp customizeVersion" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | /** 请勿手动修改本文件,本文件通过命令行自动生成 */ 2 | export default { 3 | "version": "1.4.0" 4 | } 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { OnceInit } from "./once-init"; 2 | import config from "./config"; 3 | export * from "./once-init"; 4 | 5 | export const version = config.version; 6 | 7 | export function oi>( 8 | promise: (...param: P) => Promise 9 | ): OnceInit; 10 | 11 | export function oi = []>(...args: any[]) { 12 | if (args[0] instanceof Function) { 13 | const promise = args[0] as (...param: P) => Promise; 14 | return new (class extends OnceInit {})(promise); 15 | } 16 | } 17 | 18 | export default oi; 19 | 20 | /** 21 | * 把对象的所有的Function都修改成Async Function 22 | */ 23 | type K = { 24 | [k in keyof T]: T[k] extends (...args: any) => any 25 | ? (...args: Parameters) => Promise>> 26 | : T[k]; 27 | }; 28 | 29 | /** 30 | * 把对象的所有方法都修改成once-init的refresh方法 31 | * 32 | * 即防止对象的所有方法在同一时间执行多次 33 | * 34 | * @param obj 被封装的对象 35 | * @deprecated 该api仍处于测试状态 36 | * @returns 37 | */ 38 | export function packagedByRefresh(obj: T) { 39 | const ans: K & { /** 源对象 */ oiSource: T } = {} as any; 40 | for (let key in obj) { 41 | if (obj[key] instanceof Function) { 42 | ans[key] = oi(obj[key] as any).refresh as any; 43 | } else { 44 | ans[key] = obj[key] as any; 45 | } 46 | } 47 | ans.oiSource = obj; 48 | return ans; 49 | } 50 | -------------------------------------------------------------------------------- /src/once-init.ts: -------------------------------------------------------------------------------- 1 | import isEqual from "lodash/isEqual"; 2 | export class OnceInit = []> { 3 | protected _promiseFunction: (...param: P) => Promise; 4 | protected _processedParams: P[] = []; 5 | protected _returnValueList: Array = []; 6 | protected _promiseList: Array | null> = []; 7 | 8 | constructor(initPromise: (...param: P) => Promise) { 9 | this._promiseFunction = initPromise; 10 | } 11 | 12 | /** 清空某个参数的缓存 */ 13 | clear(...params: P | []) { 14 | if (params.length === 0) { 15 | this._promiseList.length = 0; 16 | this._returnValueList.length = 0; 17 | this._processedParams.length = 0; 18 | } else { 19 | const index = this._processedParams.findIndex((item) => 20 | isEqual(item, params) 21 | ); 22 | // 如果不存在,直接返回 23 | if (index === -1) return; 24 | this._promiseList.splice(index, 1); 25 | this._returnValueList.splice(index, 1); 26 | this._processedParams.splice(index, 1); 27 | } 28 | } 29 | 30 | /** 初始化,returnValueMap中对应的参数存在变量,则不会再请求 */ 31 | init = (...param: P): Promise => { 32 | // 查看是否在已执行过的参数中 33 | const index = this.getParamIndex(param); 34 | if (index === -1) { 35 | // 如果不在已执行参数中,将参数推入,并且执行 36 | const index = this._processedParams.length; 37 | this._processedParams.push(param); 38 | // 创建一个Promise 39 | const promise = this._promiseFunction(...param); 40 | // 将promise置入哈希表,设定该参数的promise正在执行 41 | this._promiseList[index] = promise; 42 | promise 43 | .then((res) => { 44 | this._returnValueList[index] = res; 45 | this._promiseList[index] = null; 46 | }) 47 | .catch(() => { 48 | this._promiseList[index] = null; 49 | // 如果执行失败,清空该参数的缓存 50 | this.clear(...param); 51 | }); 52 | return promise; 53 | } else { 54 | // 如果在已执行参数中,可能已经执行完毕,可能正在执行中 55 | // 如果执行完毕,在 returnValueMap 中能够找到返回值 56 | if (this._returnValueList.length > index) { 57 | return new Promise((resolve) => { 58 | resolve(this._returnValueList[index]); 59 | }); 60 | } 61 | // 否则,必然还在执行中 62 | return this._promiseList[index] as Promise; 63 | } 64 | }; 65 | 66 | /** 刷新,如果存在正在执行的对应参数的Promise,则不会创建新的Promise */ 67 | refresh = (...param: P): Promise => { 68 | // 查看是否在已执行过的参数中 69 | const index = this.getParamIndex(param); 70 | if (index === -1) { 71 | // 如果不在已执行参数中,说明连第一次执行都还没有进行,可以使用init进行返回值 72 | return this.init(...param); 73 | } else { 74 | // 在已执行参数中 75 | // 如果正在执行,则返回正在执行的返回值即可 76 | let promise = this._promiseList.at(index); 77 | if (promise) { 78 | return promise; 79 | } 80 | // 此时处于未执行状态,创建一个promise 81 | promise = this._promiseFunction(...param); 82 | // 将promise置入哈希表,设定该参数的promise正在执行 83 | this._promiseList[index] = promise; 84 | promise 85 | .then((res) => { 86 | this._returnValueList[index] = res; 87 | this._promiseList[index] = null; 88 | }) 89 | .catch(() => { 90 | this._promiseList[index] = null; 91 | }); 92 | return promise; 93 | } 94 | }; 95 | 96 | send = this.refresh; 97 | request = this.refresh; 98 | 99 | /** 同步获取值,如果参数存在返回值,则返回,否则返回undefined */ 100 | get = (...param: P): T | undefined => { 101 | const index = this.getParamIndex(param); 102 | return this._returnValueList.at(index); 103 | }; 104 | 105 | /** 106 | * 强制执行,无论对应参数的Promise是否正在执行,都创建一个新的promise执行 107 | * 并覆盖在执行中的promise 108 | * 109 | * 类似于取消重复的Promise 110 | * @param param 111 | * @returns 112 | */ 113 | exceed = (...param: P): Promise => { 114 | // 查看是否在已执行过的参数中 115 | const index = this.getParamIndex(param); 116 | if (index === -1) { 117 | // 如果不在已执行参数中,说明连第一次执行都还没有进行,可以使用init进行返回值 118 | return this.init(...param); 119 | } else { 120 | // 在已执行参数中 121 | // 无论是否正在执行,都强制进行执行,并覆盖在Map中已经在执行的promise 122 | // 如果先refresh再马上exceed,会创建两个不同的Promise,返回值也会不同 123 | const promise = this._promiseFunction(...param); 124 | // 将promise置入列表,设定该参数的promise正在执行 125 | this._promiseList[index] = promise; 126 | promise 127 | .then((res) => { 128 | this._returnValueList[index] = res; 129 | this._promiseList[index] = null; 130 | }) 131 | .catch(() => { 132 | this._promiseList[index] = null; 133 | }); 134 | return promise; 135 | } 136 | }; 137 | 138 | /** 139 | * 执行源函数 140 | * @param param 141 | * @returns 142 | */ 143 | execute = (...param: P): Promise => { 144 | return this._promiseFunction(...param); 145 | }; 146 | 147 | /** 148 | * 等待param对应的promise执行结束,如果当前没有正在执行的对应的promise,将不会执行等待 149 | * 否则等待执行直到没有对应的promise 150 | * exceed会推迟wait的执行返回 151 | * @param param 152 | */ 153 | wait = async (...param: P): Promise => { 154 | const index = this.getParamIndex(param); 155 | const promise = this._promiseList.at(index); 156 | if (!promise) { 157 | return; 158 | } 159 | await promise; 160 | return await this.wait(...param); 161 | }; 162 | 163 | get promiseFunction() { 164 | return this._promiseFunction; 165 | } 166 | 167 | get processedParams() { 168 | return this._processedParams; 169 | } 170 | 171 | get returnValueList() { 172 | return this._returnValueList; 173 | } 174 | 175 | get promiseList() { 176 | return this._promiseList; 177 | } 178 | 179 | private getParamIndex = (param: P) => { 180 | return this._processedParams.findIndex((item) => isEqual(item, param)); 181 | }; 182 | } 183 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { packagedByRefresh, version } from "../src/index"; 2 | import { describe, expect, it, beforeEach } from "vitest"; 3 | 4 | describe("test module", () => { 5 | it("defined", () => { 6 | expect(packagedByRefresh).toBeDefined(); 7 | expect(version).toBeDefined(); 8 | }); 9 | }); 10 | 11 | describe(packagedByRefresh.name, () => { 12 | const obj = { 13 | val: 0, 14 | a() { 15 | return new Promise((res) => { 16 | setTimeout(() => { 17 | res(++obj.val); 18 | }, 10); 19 | }); 20 | }, 21 | }; 22 | beforeEach(() => { 23 | obj.val = 0; 24 | }); 25 | const packagedObj = packagedByRefresh(obj); 26 | it("obj", async () => { 27 | const res = await Promise.all([obj.a(), obj.a(), obj.a(), obj.a()]); 28 | expect(res[0]).toBe(1); 29 | expect(res[1]).toBe(2); 30 | expect(res[2]).toBe(3); 31 | expect(res[3]).toBe(4); 32 | }); 33 | 34 | it("packagedObj", async () => { 35 | const res = await Promise.all([ 36 | packagedObj.a(), 37 | packagedObj.a(), 38 | packagedObj.a(), 39 | packagedObj.a(), 40 | ]); 41 | expect(res[0]).toBe(1); 42 | expect(res[1]).toBe(res[0]); 43 | expect(res[2]).toBe(res[0]); 44 | expect(res[3]).toBe(res[0]); 45 | 46 | const res1 = await Promise.all([ 47 | packagedObj.a(), 48 | packagedObj.a(), 49 | packagedObj.a(), 50 | packagedObj.a(), 51 | ]); 52 | expect(res1[0]).toBe(2); 53 | expect(res1[1]).toBe(res1[0]); 54 | expect(res1[2]).toBe(res1[0]); 55 | expect(res1[3]).toBe(res1[0]); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/once-init.test.ts: -------------------------------------------------------------------------------- 1 | import oi, { OnceInit } from "../src/index"; 2 | import { describe, expect, it, beforeEach, afterEach, vi } from "vitest"; 3 | 4 | /** val每增加1表示执行一次 */ 5 | let val: number = 0; 6 | const runPromise = () => { 7 | return new Promise((res) => { 8 | setTimeout(() => { 9 | res(++val); 10 | }, 10); 11 | }); 12 | }; 13 | 14 | describe("初始化", () => { 15 | it("defined", () => { 16 | expect(oi).toBeDefined(); 17 | expect(OnceInit).toBeDefined(); 18 | }); 19 | it("测试Promise", async () => { 20 | expect(await runPromise()).toBe(val); 21 | expect(await runPromise()).toBe(val); 22 | }); 23 | }); 24 | 25 | describe("测试once-init", () => { 26 | let oiPromise: OnceInit; 27 | /** 重置val和oiPromise */ 28 | beforeEach(() => { 29 | oiPromise = oi(runPromise); 30 | val = 0; 31 | }); 32 | afterEach(async () => { 33 | await oiPromise.wait(); 34 | }); 35 | 36 | describe("单独测试", () => { 37 | it("normal", async () => { 38 | const res = await Promise.all([ 39 | runPromise(), 40 | runPromise(), 41 | runPromise(), 42 | runPromise(), 43 | ]); 44 | expect(res[0]).toBe(1); 45 | expect(res[1]).toBe(2); 46 | expect(res[2]).toBe(3); 47 | expect(res[3]).toBe(4); 48 | }); 49 | 50 | it("init", async () => { 51 | const res = await Promise.all([ 52 | oiPromise.init(), 53 | oiPromise.init(), 54 | oiPromise.init(), 55 | oiPromise.init(), 56 | ]); 57 | expect(res[0]).toBe(1); 58 | expect(res[0]).toBe(res[1]); 59 | expect(res[1]).toBe(res[2]); 60 | expect(res[2]).toBe(res[3]); 61 | const res1 = await Promise.all([ 62 | oiPromise.init(), 63 | oiPromise.init(), 64 | oiPromise.init(), 65 | oiPromise.init(), 66 | ]); 67 | expect(res1[0]).toBe(1); 68 | expect(res1[0]).toBe(res1[1]); 69 | expect(res1[1]).toBe(res1[2]); 70 | expect(res1[2]).toBe(res1[3]); 71 | }); 72 | 73 | it("const init = oiPromise.init", async () => { 74 | const init = oiPromise.init; 75 | const res = await Promise.all([init(), init(), init(), init()]); 76 | expect(res[0]).toBe(1); 77 | expect(res[0]).toBe(res[1]); 78 | expect(res[1]).toBe(res[2]); 79 | expect(res[2]).toBe(res[3]); 80 | const res1 = await Promise.all([init(), init(), init(), init()]); 81 | expect(res1[0]).toBe(1); 82 | expect(res1[0]).toBe(res1[1]); 83 | expect(res1[1]).toBe(res1[2]); 84 | expect(res1[2]).toBe(res1[3]); 85 | }); 86 | 87 | it("refresh", async () => { 88 | const res = await Promise.all([ 89 | oiPromise.refresh(), 90 | oiPromise.refresh(), 91 | oiPromise.refresh(), 92 | oiPromise.refresh(), 93 | ]); 94 | expect(res[0]).toBe(1); 95 | expect(res[0]).toBe(res[1]); 96 | expect(res[1]).toBe(res[2]); 97 | expect(res[2]).toBe(res[3]); 98 | const res1 = await Promise.all([ 99 | oiPromise.refresh(), 100 | oiPromise.refresh(), 101 | oiPromise.refresh(), 102 | oiPromise.refresh(), 103 | ]); 104 | expect(res1[0]).toBe(2); 105 | expect(res1[0]).toBe(res1[1]); 106 | expect(res1[1]).toBe(res1[2]); 107 | expect(res1[2]).toBe(res1[3]); 108 | }); 109 | 110 | it("const refresh = oiPromise.refresh", async () => { 111 | const refresh = oiPromise.refresh; 112 | const res = await Promise.all([ 113 | refresh(), 114 | refresh(), 115 | refresh(), 116 | refresh(), 117 | ]); 118 | expect(res[0]).toBe(1); 119 | expect(res[0]).toBe(res[1]); 120 | expect(res[1]).toBe(res[2]); 121 | expect(res[2]).toBe(res[3]); 122 | const res1 = await Promise.all([ 123 | refresh(), 124 | refresh(), 125 | refresh(), 126 | refresh(), 127 | ]); 128 | expect(res1[0]).toBe(2); 129 | expect(res1[0]).toBe(res1[1]); 130 | expect(res1[1]).toBe(res1[2]); 131 | expect(res1[2]).toBe(res1[3]); 132 | }); 133 | 134 | it("get", async () => { 135 | const res = await Promise.all([ 136 | oiPromise.get(), 137 | oiPromise.get(), 138 | oiPromise.get(), 139 | oiPromise.get(), 140 | ]); 141 | expect(res[0]).toBe(undefined); 142 | expect(res[0]).toBe(res[1]); 143 | expect(res[1]).toBe(res[2]); 144 | expect(res[2]).toBe(res[3]); 145 | }); 146 | 147 | it("exceed", async () => { 148 | const res = await Promise.all([ 149 | oiPromise.exceed(), 150 | oiPromise.exceed(), 151 | oiPromise.exceed(), 152 | oiPromise.exceed(), 153 | ]); 154 | expect(res[0]).toBe(1); 155 | expect(res[1]).toBe(2); 156 | expect(res[2]).toBe(3); 157 | expect(res[3]).toBe(4); 158 | 159 | const res1 = await Promise.all([ 160 | oiPromise.exceed(), 161 | oiPromise.exceed(), 162 | oiPromise.exceed(), 163 | oiPromise.exceed(), 164 | ]); 165 | expect(res1[0]).toBe(5); 166 | expect(res1[1]).toBe(6); 167 | expect(res1[2]).toBe(7); 168 | expect(res1[3]).toBe(8); 169 | }); 170 | 171 | it("execute", async () => { 172 | const initPromiseRes = await oiPromise.init(); 173 | const res = await Promise.all([ 174 | oiPromise.execute(), 175 | oiPromise.execute(), 176 | oiPromise.execute(), 177 | oiPromise.execute(), 178 | ]); 179 | expect(res[0]).toBe(initPromiseRes + 1); 180 | expect(res[1]).toBe(initPromiseRes + 2); 181 | expect(res[2]).toBe(initPromiseRes + 3); 182 | expect(res[3]).toBe(initPromiseRes + 4); 183 | 184 | const res1 = await Promise.all([ 185 | oiPromise.init(), 186 | oiPromise.init(), 187 | oiPromise.init(), 188 | oiPromise.init(), 189 | ]); 190 | 191 | expect(res1[0]).toBe(initPromiseRes); 192 | expect(res1[1]).toBe(initPromiseRes); 193 | expect(res1[2]).toBe(initPromiseRes); 194 | expect(res1[3]).toBe(initPromiseRes); 195 | }); 196 | 197 | describe("wait", () => { 198 | it("wait", async () => { 199 | expect(await oiPromise.wait()).toBeUndefined(); 200 | oiPromise.init(); 201 | expect(val).toBe(0); 202 | await oiPromise.wait(); 203 | expect(val).toBe(1); 204 | }); 205 | }); 206 | }); 207 | 208 | describe("复合测试", () => { 209 | describe("init + refresh", () => { 210 | it("init => refresh", async () => { 211 | const res = await Promise.all([ 212 | oiPromise.init(), 213 | oiPromise.refresh(), 214 | oiPromise.refresh(), 215 | oiPromise.refresh(), 216 | ]); 217 | expect(res[0]).toBe(1); 218 | expect(res[0]).toBe(res[1]); 219 | }); 220 | 221 | it("refresh => init", async () => { 222 | const res = await Promise.all([ 223 | oiPromise.refresh(), 224 | oiPromise.init(), 225 | oiPromise.refresh(), 226 | oiPromise.refresh(), 227 | ]); 228 | expect(res[0]).toBe(1); 229 | expect(res[0]).toBe(res[1]); 230 | }); 231 | 232 | it("setTimeout-refresh => init", async () => { 233 | setTimeout(async () => { 234 | const res = await oiPromise.refresh(); 235 | expect(res).toBe(1); 236 | }, 4); 237 | const res = await oiPromise.init(); 238 | expect(res).toBe(1); 239 | }); 240 | 241 | it("setTimeout-init => refresh", async () => { 242 | setTimeout(async () => { 243 | const res = await oiPromise.init(); 244 | expect(res).toBe(1); 245 | }, 4); 246 | const res = await oiPromise.refresh(); 247 | expect(res).toBe(1); 248 | }); 249 | }); 250 | 251 | describe("init + get", () => { 252 | it("init => get", async () => { 253 | const res1 = oiPromise.get(); 254 | expect(res1).toBeUndefined(); 255 | const res2 = await oiPromise.init(); 256 | expect(res2).toBe(1); 257 | const res3 = oiPromise.get(); 258 | expect(res3).toBe(res2); 259 | }); 260 | }); 261 | 262 | describe("init + refresh + exceed", () => { 263 | it("refresh => exceed | exceed => refresh + init", async () => { 264 | const res1 = await Promise.all([ 265 | oiPromise.refresh(), 266 | oiPromise.exceed(), 267 | ]); // 50秒后,返回 [1, 2]; 268 | 269 | expect(res1).toEqual([1, 2]); 270 | 271 | const res2 = await Promise.all([ 272 | oiPromise.exceed(), 273 | oiPromise.refresh(), 274 | ]); // 50秒后,返回 [3, 3]; 275 | 276 | expect(res2).toEqual([3, 3]); 277 | 278 | const res3 = await Promise.all([ 279 | oiPromise.exceed(), 280 | oiPromise.refresh(), 281 | oiPromise.init(), 282 | ]); // 50秒后,返回 [4, 4, 4]; 283 | 284 | expect(res3).toEqual([4, 4, 3]); 285 | 286 | const res4 = await oiPromise.init(); 287 | expect(res4).toBe(4); 288 | 289 | const res5 = await Promise.all([ 290 | oiPromise.init(), 291 | oiPromise.exceed(), 292 | oiPromise.refresh(), 293 | oiPromise.exceed(), 294 | oiPromise.refresh(), 295 | oiPromise.init(), 296 | ]); // 50秒后,返回 [5, 5, 6, 5, 4]; 297 | 298 | expect(res5).toEqual([4, 5, 5, 6, 6, 4]); 299 | }); 300 | }); 301 | 302 | describe("init + execute + refresh", () => { 303 | it("init => execute => init => refresh", async () => { 304 | const res1 = await oiPromise.init(); // 50秒后,返回 1 305 | expect(res1).toBe(1); 306 | const res2 = await oiPromise.execute(); // 50秒后,返回 2 307 | expect(res2).toBe(2); 308 | const res3 = await oiPromise.init(); // 0秒后,返回 1 (缓存未更新) 309 | expect(res3).toBe(1); 310 | const res4 = await oiPromise.refresh(); // 50秒后,返回 3 (缓存更新,重新获取后端值,由于后端在上次execute请求中虽未更新缓存,但更新了后端,所以返回值为3) 311 | expect(res4).toBe(3); 312 | }); 313 | }); 314 | }); 315 | 316 | describe("clear", () => { 317 | it("should rerun init", async () => { 318 | const res1 = await oiPromise.init(); 319 | expect(res1).toBe(1); 320 | oiPromise.clear(); 321 | const res2 = await oiPromise.init(); 322 | expect(res2).toBe(2); 323 | }); 324 | 325 | it("should create new promise with init", async () => { 326 | const promise1 = oiPromise.init(); 327 | oiPromise.clear(); 328 | const promise2 = oiPromise.init(); 329 | expect(await promise1).not.toBe(await promise2); 330 | }); 331 | 332 | it("should create new promise with refresh", async () => { 333 | const promise1 = oiPromise.refresh(); 334 | oiPromise.clear(); 335 | const promise2 = oiPromise.refresh(); 336 | expect(await promise1).not.toBe(await promise2); 337 | }); 338 | 339 | describe("get", () => { 340 | it("should return params", () => { 341 | expect(oiPromise.promiseFunction).toBe(runPromise); 342 | expect(oiPromise.processedParams.length).toBe(0); 343 | expect(oiPromise.promiseList.length).toBe(0); 344 | expect(oiPromise.returnValueList.length).toBe(0); 345 | }); 346 | 347 | it("should get processed info", async () => { 348 | const promise1 = oiPromise.init(); 349 | expect(oiPromise.promiseFunction).toBe(runPromise); 350 | expect(oiPromise.processedParams.length).toBe(1); 351 | expect(oiPromise.promiseList.length).toBe(1); 352 | expect(oiPromise.returnValueList.length).toBe(0); 353 | await promise1; 354 | expect(oiPromise.returnValueList.length).toBe(1); 355 | }); 356 | }); 357 | }); 358 | }); 359 | 360 | describe("带参测试", () => { 361 | let res1 = 0; 362 | let res2 = 0; 363 | const paramsRunPromise = (op: "+" | "-") => { 364 | return new Promise((res) => { 365 | setTimeout(() => { 366 | if (op === "+") { 367 | res(++res1); 368 | } else { 369 | res(--res2); 370 | } 371 | }); 372 | }); 373 | }; 374 | let oiPromise: OnceInit; 375 | /** 重置val和oiPromise */ 376 | beforeEach(() => { 377 | oiPromise = oi(paramsRunPromise); 378 | res1 = 0; 379 | res2 = 0; 380 | }); 381 | describe("init + refresh", () => { 382 | it("init => refresh", async () => { 383 | expect(await oiPromise.init("-")).toBe(-1); 384 | expect(await oiPromise.refresh("-")).toBe(-2); 385 | expect(await oiPromise.refresh("-")).toBe(-3); 386 | expect(await oiPromise.refresh("+")).toBe(1); 387 | expect(await oiPromise.init("-")).toBe(-3); 388 | }); 389 | }); 390 | 391 | describe("clear", () => { 392 | it("clear with params", async () => { 393 | oiPromise.clear("+"); 394 | 395 | expect(await oiPromise.init("-")).toBe(-1); 396 | expect(await oiPromise.refresh("-")).toBe(-2); 397 | expect(await oiPromise.refresh("-")).toBe(-3); 398 | expect(await oiPromise.refresh("+")).toBe(1); 399 | oiPromise.clear("-"); 400 | // Nothing should happen if params is not in processedParams 401 | oiPromise.clear("-"); 402 | expect(await oiPromise.init("-")).toBe(-4); 403 | expect(await oiPromise.init("+")).toBe(1); 404 | }); 405 | }); 406 | }); 407 | 408 | /** 由github用户 Banlangenn 提出的问题 */ 409 | describe("测试执行顺序", () => { 410 | describe("init", () => { 411 | /** 新建一个单独的promise函数用于测试 */ 412 | let val = 0; 413 | const runPromise = () => { 414 | return new Promise((res) => { 415 | setTimeout(() => { 416 | res(++val); 417 | }, 10); 418 | }); 419 | }; 420 | /** 包装promise */ 421 | const fn = oi(runPromise); 422 | const fnInit = fn.init; 423 | const invokedFn = vi.fn(); 424 | it("invoke function", async () => { 425 | let inc = 0; 426 | fnInit().then(() => { 427 | expect(invokedFn).toBeCalledTimes(0); 428 | invokedFn(); 429 | inc++; 430 | expect(inc).toBe(1); 431 | expect(invokedFn).toBeCalledTimes(1); 432 | }); 433 | fnInit().then(() => { 434 | expect(invokedFn).toBeCalledTimes(1); 435 | invokedFn(); 436 | inc++; 437 | expect(inc).toBe(2); 438 | expect(invokedFn).toBeCalledTimes(2); 439 | }); 440 | fnInit().then(() => { 441 | expect(invokedFn).toBeCalledTimes(2); 442 | invokedFn(); 443 | inc++; 444 | expect(inc).toBe(3); 445 | expect(invokedFn).toBeCalledTimes(3); 446 | }); 447 | fnInit().then(() => { 448 | expect(invokedFn).toBeCalledTimes(3); 449 | invokedFn(); 450 | inc++; 451 | expect(inc).toBe(4); 452 | expect(invokedFn).toBeCalledTimes(4); 453 | }); 454 | await fn.wait(); 455 | }); 456 | }); 457 | 458 | describe("refresh", () => { 459 | /** 新建一个单独的promise函数用于测试 */ 460 | let val = 0; 461 | const runPromise = () => { 462 | return new Promise((res) => { 463 | setTimeout(() => { 464 | res(++val); 465 | }, 10); 466 | }); 467 | }; 468 | /** 包装promise */ 469 | const fn = oi(runPromise); 470 | const fnRefresh = fn.refresh; 471 | const invokedFn = vi.fn(); 472 | it("invoke function", async () => { 473 | let inc = 0; 474 | fnRefresh().then(() => { 475 | expect(invokedFn).toBeCalledTimes(0); 476 | invokedFn(); 477 | inc++; 478 | expect(inc).toBe(1); 479 | expect(invokedFn).toBeCalledTimes(1); 480 | }); 481 | fnRefresh().then(() => { 482 | expect(invokedFn).toBeCalledTimes(1); 483 | invokedFn(); 484 | inc++; 485 | expect(inc).toBe(2); 486 | expect(invokedFn).toBeCalledTimes(2); 487 | }); 488 | fnRefresh().then(() => { 489 | expect(invokedFn).toBeCalledTimes(2); 490 | invokedFn(); 491 | inc++; 492 | expect(inc).toBe(3); 493 | expect(invokedFn).toBeCalledTimes(3); 494 | }); 495 | fnRefresh().then(() => { 496 | expect(invokedFn).toBeCalledTimes(3); 497 | invokedFn(); 498 | inc++; 499 | expect(inc).toBe(4); 500 | expect(invokedFn).toBeCalledTimes(4); 501 | }); 502 | await fn.wait(); 503 | }); 504 | }); 505 | 506 | describe("exceed", () => { 507 | /** 新建一个单独的promise函数用于测试 */ 508 | let val = 0; 509 | const runPromise = () => { 510 | return new Promise((res) => { 511 | setTimeout(() => { 512 | res(++val); 513 | }, 10); 514 | }); 515 | }; 516 | /** 包装promise */ 517 | const fn = oi(runPromise); 518 | const fnExceed = fn.exceed; 519 | const invokedFn = vi.fn(); 520 | it("invoke function", async () => { 521 | let inc = 0; 522 | fnExceed().then(() => { 523 | expect(invokedFn).toBeCalledTimes(0); 524 | invokedFn(); 525 | inc++; 526 | expect(inc).toBe(1); 527 | expect(invokedFn).toBeCalledTimes(1); 528 | }); 529 | fnExceed().then(() => { 530 | expect(invokedFn).toBeCalledTimes(1); 531 | invokedFn(); 532 | inc++; 533 | expect(inc).toBe(2); 534 | expect(invokedFn).toBeCalledTimes(2); 535 | }); 536 | fnExceed().then(() => { 537 | expect(invokedFn).toBeCalledTimes(2); 538 | invokedFn(); 539 | inc++; 540 | expect(inc).toBe(3); 541 | expect(invokedFn).toBeCalledTimes(3); 542 | }); 543 | fnExceed().then(() => { 544 | expect(invokedFn).toBeCalledTimes(3); 545 | invokedFn(); 546 | inc++; 547 | expect(inc).toBe(4); 548 | expect(invokedFn).toBeCalledTimes(4); 549 | }); 550 | await fn.wait(); 551 | }); 552 | }); 553 | }); 554 | 555 | describe("Promise", () => { 556 | /** 新建一个单独的promise函数用于测试 */ 557 | let val = 0; 558 | const runPromise = () => { 559 | return new Promise((res) => { 560 | setTimeout(() => { 561 | res(++val); 562 | }, 10); 563 | }); 564 | }; 565 | /** 包装promise */ 566 | it("init", async () => { 567 | const fn = oi(runPromise); 568 | const firstCall = fn.init(); 569 | expect(firstCall.then).toBeDefined(); 570 | expect(firstCall.finally).toBeDefined(); 571 | await firstCall; 572 | const secondCall = fn.init(); 573 | expect(secondCall.then).toBeDefined(); 574 | expect(secondCall.finally).toBeDefined(); 575 | }); 576 | 577 | it("refersh", async () => { 578 | const fn = oi(runPromise); 579 | const firstCall = fn.refresh(); 580 | expect(firstCall.then).toBeDefined(); 581 | expect(firstCall.finally).toBeDefined(); 582 | await firstCall; 583 | const secondCall = fn.refresh(); 584 | expect(secondCall.then).toBeDefined(); 585 | expect(secondCall.finally).toBeDefined(); 586 | }); 587 | 588 | it("exceed", async () => { 589 | const fn = oi(runPromise); 590 | const firstCall = fn.exceed(); 591 | expect(firstCall.then).toBeDefined(); 592 | expect(firstCall.finally).toBeDefined(); 593 | await firstCall; 594 | const secondCall = fn.exceed(); 595 | expect(secondCall.then).toBeDefined(); 596 | expect(secondCall.finally).toBeDefined(); 597 | }); 598 | }); 599 | 600 | describe("测试错误处理", () => { 601 | let cnt = 0; 602 | beforeEach(() => { 603 | cnt = 0; 604 | }); 605 | /** Thanks to @richex-cn(https://github.com/richex-cn) */ 606 | async function incrementPromise() { 607 | cnt++; 608 | if (cnt < 3 || cnt === 4) throw new Error("cnt " + cnt.toString()); 609 | return cnt; 610 | } 611 | 612 | it("should throw error", async () => { 613 | const fn = oi(incrementPromise); 614 | await expect(fn.init()).rejects.toThrow("cnt 1"); 615 | await expect(fn.init()).rejects.toThrow("cnt 2"); 616 | }); 617 | 618 | it("should refresh without error", async () => { 619 | const fn = oi(incrementPromise); 620 | await expect(fn.init()).rejects.toThrow("cnt " + cnt.toString()); 621 | await expect(fn.refresh()).rejects.toThrow("cnt " + cnt.toString()); 622 | expect(fn.get()).toBeUndefined(); 623 | const refreshAns = await fn.refresh(); 624 | expect(refreshAns).toBe(cnt); 625 | }); 626 | 627 | it("should exceed without error", async () => { 628 | const fn = oi(incrementPromise); 629 | await expect(fn.exceed()).rejects.toThrow("cnt 1"); 630 | await expect(fn.exceed()).rejects.toThrow("cnt 2"); 631 | const refreshAns = await fn.exceed(); 632 | expect(refreshAns).toBe(cnt); 633 | await expect(fn.exceed()).rejects.toThrow("cnt 4"); 634 | }); 635 | 636 | it("should get last response if error", async () => { 637 | const fn = oi(incrementPromise); 638 | await expect(fn.init()).rejects.toThrow("cnt " + cnt.toString()); 639 | await expect(fn.refresh()).rejects.toThrow("cnt " + cnt.toString()); 640 | const lastSucceedResponse = await fn.refresh(); 641 | await expect(fn.refresh()).rejects.toThrow("cnt " + cnt.toString()); 642 | expect(fn.get()).toBe(lastSucceedResponse); 643 | expect(await fn.init()).toBe(lastSucceedResponse); 644 | }); 645 | }); 646 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "commonjs" /* Specify what module code is generated. */, 28 | // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 46 | "declarationMap": true /* Create sourcemaps for d.ts files. */, 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | 69 | /* Interop Constraints */ 70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 72 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 74 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 75 | 76 | /* Type Checking */ 77 | "strict": true /* Enable all strict type-checking options. */, 78 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 79 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 96 | 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 100 | }, 101 | "include": ["../src/*.ts", "./**/*.ts"] 102 | } 103 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "commonjs" /* Specify what module code is generated. */, 28 | // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 46 | "declarationMap": true /* Create sourcemaps for d.ts files. */, 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | 69 | /* Interop Constraints */ 70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 72 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 74 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 75 | 76 | /* Type Checking */ 77 | "strict": true /* Enable all strict type-checking options. */, 78 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 79 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 96 | 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 100 | }, 101 | "include": ["src/*.ts"] 102 | } 103 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | clearMocks: true, 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /** @type import("webpack").Configuration } */ 2 | module.exports = { 3 | entry: "./src/index.ts", 4 | module: { 5 | rules: [ 6 | { 7 | test: /\.tsx?$/, 8 | use: "ts-loader", 9 | exclude: /node_modules/, 10 | }, 11 | ], 12 | }, 13 | resolve: { 14 | extensions: [".tsx", ".ts", ".js"], 15 | }, 16 | output: { 17 | library: "OnceInit", 18 | filename: "index.js", 19 | globalObject: "this", 20 | libraryTarget: "umd", //支持库的引入方式 21 | }, 22 | }; 23 | --------------------------------------------------------------------------------