diff --git a/.eslintrc b/.eslintrc index c799fe5..9bcdb46 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,3 +1,6 @@ { - "extends": "eslint-config-egg" + "extends": [ + "eslint-config-egg/typescript", + "eslint-config-egg/lib/rules/enforce-node-prefix" + ] } diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 48f9944..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,24 +0,0 @@ - - -##### Checklist - - -- [ ] `npm test` passes -- [ ] tests and/or benchmarks are included -- [ ] documentation is changed or added -- [ ] commit message follows commit guidelines - -##### Affected core subsystem(s) - - - -##### Description of change - diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 82a4735..0000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,72 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ "master" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "master" ] - schedule: - - cron: '40 5 * * 2' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'javascript' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index f38bb73..5a2a289 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -3,16 +3,15 @@ name: CI on: push: branches: [ master ] - pull_request: branches: [ master ] - workflow_dispatch: {} - jobs: Job: name: Node.js - uses: artusjs/github-actions/.github/workflows/node-test.yml@v1 + uses: node-modules/github-actions/.github/workflows/node-test.yml@master with: os: 'ubuntu-latest' - version: '14, 16, 18, 20' + version: '18, 20, 22' + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1612587..a2bf04a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,14 +4,10 @@ on: push: branches: [ master ] - workflow_dispatch: {} - jobs: release: name: Node.js - uses: artusjs/github-actions/.github/workflows/node-release.yml@v1 + uses: eggjs/github-actions/.github/workflows/node-release.yml@master secrets: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} GIT_TOKEN: ${{ secrets.GIT_TOKEN }} - with: - checkTest: false diff --git a/.gitignore b/.gitignore index 8448054..00eab48 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ run/ test/fixtures/symlink/runDir/app/schedule/realFile.js test/fixtures/symlink/runDir/app/schedule/tsRealFile.ts package-lock.json +.tshy* +dist +.eslintcache diff --git a/CHANGELOG.md b/CHANGELOG.md index ce425b6..7e5b9ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,3 +6,218 @@ ### Bug Fixes * schedule should execute after app ready ([#60](https://github.com/eggjs/egg-schedule/issues/60)) ([bf01a49](https://github.com/eggjs/egg-schedule/commit/bf01a49b093b4a32ee546b64be3059f0dbe65572)) + +--- + + +4.0.0 / 2022-12-11 +================== + +**features** + * [[`66e7aeb`](http://github.com/eggjs/egg-schedule/commit/66e7aeb87dfc8994529ec9d8407a219461345d27)] - 📦 NEW: [BREAKING] Support localStorage to run task (#58) (fengmk2 <>) + +**others** + * [[`7672269`](http://github.com/eggjs/egg-schedule/commit/7672269168f2a19a5e239fc58e625b32f89693cc)] - Create codeql-analysis.yml (fengmk2 <>) + * [[`bab8585`](http://github.com/eggjs/egg-schedule/commit/bab8585a64dbc7d7c0ff1c1b88b5dc544b9e8aac)] - ci: del appveyor (#57) (killa <>) + +3.7.0 / 2022-09-03 +================== + +**features** + * [[`ca8b92b`](http://github.com/eggjs/egg-schedule/commit/ca8b92be7e72f3b35e80d8812d378f822f7a02b2)] - feat: add api for register/unregister schedule (#56) (killa <>) + +3.6.6 / 2020-10-23 +================== + +**fixes** + * [[`3a8ef55`](http://github.com/eggjs/egg-schedule/commit/3a8ef55ff3da580f277755fdb0cca15a12fc2256)] - fix: runSchedule get filePath keep same logic with loader (#55) (mansonchor.github.com <>) + +**others** + * [[`39f4aad`](http://github.com/eggjs/egg-schedule/commit/39f4aadf1f10736a4fa3cedd779a48cb329d5320)] - docs: updated README with grammatical and spelling fixes (#54) (Hridayesh Sharma <>) + +3.6.5 / 2020-09-01 +================== + +**fixes** + * [[`7ee8cb3`](http://github.com/eggjs/egg-schedule/commit/7ee8cb3696751ca30bbc3abfb3cbc06a676d2fe7)] - fix: only reject/error detect as fail (#53) (TZ | 天猪 <>) + +**others** + * [[`0425031`](http://github.com/eggjs/egg-schedule/commit/0425031279bc98a716671111e3f36bdd0a353017)] - docs: add module exports for custom schedule in document (#52) (Cheng Ju Wu <>) + * [[`7da04fe`](http://github.com/eggjs/egg-schedule/commit/7da04fe5b3adfd572375b02f1063b8d90684743e)] - chore: Update README.md (#51) (Cheng Ju Wu <>) + * [[`13f03b8`](http://github.com/eggjs/egg-schedule/commit/13f03b8681d60dfa8c9d33574cef392dc55c4b27)] - chore: Update .travis.yml (#50) (TZ | 天猪 <>) + +3.6.4 / 2019-06-12 +================== + +**fixes** + * [[`b6b17b0`](http://github.com/eggjs/egg-schedule/commit/b6b17b032582dbde6f4f9faecef3dd662726b3e2)] - fix: should use template literal (#49) (Jedmeng <>) + +3.6.3 / 2019-06-03 +================== + +**fixes** + * [[`bce393f`](http://github.com/eggjs/egg-schedule/commit/bce393f66f70add9151155d16b90942bab75e89b)] - fix: wrap task should always return promise (#48) (TZ | 天猪 <>) + +3.6.2 / 2019-04-29 +================== + +**fixes** + * [[`98a0cf7`](http://github.com/eggjs/egg-schedule/commit/98a0cf78012bd138cd8b0893c1acb6527cfecf0e)] - fix: runSchedule should pass args (#47) (TZ | 天猪 <>) + +**others** + * [[`77fc7d3`](http://github.com/eggjs/egg-schedule/commit/77fc7d3b46d1c57acc69cb3931241f9cb8165a38)] - docs: fix ctx ref (#46) (祝传鹏 <>) + +3.6.1 / 2019-03-20 +================== + +**others** + * [[`0960ff8`](http://github.com/eggjs/egg-schedule/commit/0960ff8f058c2bbb8ad2afb333bc557d719ec99b)] - chore: use relative log path (#45) (TZ | 天猪 <>) + +3.6.0 / 2018-12-18 +================== + +**features** + * [[`2c64d3c`](https://github.com/eggjs/egg-schedule/commit/2c64d3c6dd386dedaa784180ebb6c61b89fd1d53)] - feat: support custom directory (#43) (TZ <>) + +3.5.0 / 2018-12-05 +================== + +**features** + * [[`1aaf2d5`](http://github.com/eggjs/egg-schedule/commit/1aaf2d5675253d125eacca8bfd77813ecc151d2a)] - feat: support custom directory (#43) (Haoliang Gao <>) + +**fixes** + * [[`4dbf9d9`](http://github.com/eggjs/egg-schedule/commit/4dbf9d9d3785b19eb772704c724c421e1017922a)] - fix: unit-test in 'schedule.test.js' (#41) (Maledong <>) + +**others** + * [[`571bf9f`](http://github.com/eggjs/egg-schedule/commit/571bf9f28ed229f957fa70067786061a89dc1049)] - doc: Add notice for the evil 'setInterval' (#42) (Maledong <>) + * [[`07e4e23`](http://github.com/eggjs/egg-schedule/commit/07e4e238f198fbf935ac5e7fff279f349e11a6b5)] - docs: fix example in readme (cwtuan <>) + +3.4.0 / 2018-06-30 +================== + +**features** + * [[`417a764`](http://github.com/eggjs/egg-schedule/commit/417a7643807e56a432703e64f76923b60e1053ba)] - feat: support `schedule.env` (#39) (TZ | 天猪 <>) + +3.3.0 / 2018-02-24 +================== + + * feat: optimize logger msg (#38) + +3.2.1 / 2018-02-07 +================== + + * chore: fix doctools (#37) + +3.2.0 / 2018-02-06 +================== + +**features** + * [[`2003369`](http://github.com/eggjs/egg-schedule/commit/200336963cdf2404b926fa1c36223c41229cf32d)] - feat: egg-schedule.log && support send with args (#35) (TZ | 天猪 <>) + +3.1.1 / 2017-11-20 +================== + +**fixes** + * [[`9ff3974`](http://github.com/eggjs/egg-schedule/commit/9ff3974683e1f4ade72ccbe2448a3c68d7826530)] - fix: use ctx.coreLogger to record schedule log (#34) (Yiyu He <>) + +3.1.0 / 2017-11-16 +================== + +**features** + * [[`69a588e`](https://github.com/eggjs/egg-schedule/commit/69a588e5ffbb5a01ed3084bfb9f6c2a792963db4)] - feat: run a scheduler only once at startup (#33) (zhennann <>) + +3.0.0 / 2017-11-10 +================== + +**others** + * [[`925f1c3`](http://github.com/eggjs/egg-schedule/commit/925f1c38ffb5c8d73e91fe96e6e7fc30c3f43c5f)] - refactor: remove old stype strategy support [BREAKING CHANGE] (#29) (TZ | 天猪 <>) + * [[`4cdfa20`](http://github.com/eggjs/egg-schedule/commit/4cdfa204f1da36288328bf30acb0564da1e3d1b5)] - test: change to extend Subscription (#28) (TZ | 天猪 <>) + +2.6.0 / 2017-10-16 +================== + +**features** + * [[`f901df4`](http://github.com/eggjs/egg-schedule/commit/f901df4e895d440c9d3bc96e172d3cc87be95255)] - feat: Strategy interface change to start() (#26) (TZ | 天猪 <>) + * [[`c7816f2`](http://github.com/eggjs/egg-schedule/commit/c7816f2eb8ca668c92c1671b1d149c78dd73551e)] - feat: support class (#25) (Haoliang Gao <>) + +**others** + * [[`8797489`](http://github.com/eggjs/egg-schedule/commit/8797489f914a34bf56ecc68575b0b7e490628b5a)] - docs: use subscription (#27) (Haoliang Gao <>) + +2.5.1 / 2017-10-11 +================== + + * fix: publish files (#24) + +2.5.0 / 2017-10-11 +================== + + * refactor: classify (#23) + * test: sleep after runSchedule (#22) + +2.4.1 / 2017-06-06 +================== + + * fix: use safe-timers only large than interval && add tests (#21) + +2.4.0 / 2017-06-05 +================== + + * feat: use safe-timers to support large delay (#19) + +2.3.1 / 2017-06-04 +================== + + * docs: fix License url (#20) + * test: fix test on windows (#18) + * chore: upgrade all deps (#17) + +2.3.0 / 2017-02-08 +================== + + * feat: task support async function (#13) + * test: move app.close to afterEach (#12) + * chore: upgrade deps and fix test (#11) + +2.2.1 / 2016-10-25 +================== + + * fix: start schedule after egg-ready (#10) + +2.2.0 / 2016-09-29 +================== + + * feat: export app.schedules (#9) + * doc:fix plugin.js config demo (#8) + +2.1.0 / 2016-08-18 +================== + + * refactor: use FileLoader to load schedule files (#7) + +2.0.0 / 2016-08-16 +================== + + * Revert "Release 1.1.1" + * refactor: use loader.getLoadUnits from egg-core (#6) + +1.1.0 / 2016-08-15 +================== + + * docs: add readme (#5) + * feat: support immediate (#4) + +1.0.0 / 2016-08-10 +================== + + * fix: correct path in ctx (#3) + +0.1.0 / 2016-07-26 +================== + + * fix: use absolute path for store key (#2) + * test: add test cases (#1) + +0.0.1 / 2016-07-15 +================== + + * init diff --git a/History.md b/History.md deleted file mode 100644 index 5ec0039..0000000 --- a/History.md +++ /dev/null @@ -1,212 +0,0 @@ - -4.0.0 / 2022-12-11 -================== - -**features** - * [[`66e7aeb`](http://github.com/eggjs/egg-schedule/commit/66e7aeb87dfc8994529ec9d8407a219461345d27)] - 📦 NEW: [BREAKING] Support localStorage to run task (#58) (fengmk2 <>) - -**others** - * [[`7672269`](http://github.com/eggjs/egg-schedule/commit/7672269168f2a19a5e239fc58e625b32f89693cc)] - Create codeql-analysis.yml (fengmk2 <>) - * [[`bab8585`](http://github.com/eggjs/egg-schedule/commit/bab8585a64dbc7d7c0ff1c1b88b5dc544b9e8aac)] - ci: del appveyor (#57) (killa <>) - -3.7.0 / 2022-09-03 -================== - -**features** - * [[`ca8b92b`](http://github.com/eggjs/egg-schedule/commit/ca8b92be7e72f3b35e80d8812d378f822f7a02b2)] - feat: add api for register/unregister schedule (#56) (killa <>) - -3.6.6 / 2020-10-23 -================== - -**fixes** - * [[`3a8ef55`](http://github.com/eggjs/egg-schedule/commit/3a8ef55ff3da580f277755fdb0cca15a12fc2256)] - fix: runSchedule get filePath keep same logic with loader (#55) (mansonchor.github.com <>) - -**others** - * [[`39f4aad`](http://github.com/eggjs/egg-schedule/commit/39f4aadf1f10736a4fa3cedd779a48cb329d5320)] - docs: updated README with grammatical and spelling fixes (#54) (Hridayesh Sharma <>) - -3.6.5 / 2020-09-01 -================== - -**fixes** - * [[`7ee8cb3`](http://github.com/eggjs/egg-schedule/commit/7ee8cb3696751ca30bbc3abfb3cbc06a676d2fe7)] - fix: only reject/error detect as fail (#53) (TZ | 天猪 <>) - -**others** - * [[`0425031`](http://github.com/eggjs/egg-schedule/commit/0425031279bc98a716671111e3f36bdd0a353017)] - docs: add module exports for custom schedule in document (#52) (Cheng Ju Wu <>) - * [[`7da04fe`](http://github.com/eggjs/egg-schedule/commit/7da04fe5b3adfd572375b02f1063b8d90684743e)] - chore: Update README.md (#51) (Cheng Ju Wu <>) - * [[`13f03b8`](http://github.com/eggjs/egg-schedule/commit/13f03b8681d60dfa8c9d33574cef392dc55c4b27)] - chore: Update .travis.yml (#50) (TZ | 天猪 <>) - -3.6.4 / 2019-06-12 -================== - -**fixes** - * [[`b6b17b0`](http://github.com/eggjs/egg-schedule/commit/b6b17b032582dbde6f4f9faecef3dd662726b3e2)] - fix: should use template literal (#49) (Jedmeng <>) - -3.6.3 / 2019-06-03 -================== - -**fixes** - * [[`bce393f`](http://github.com/eggjs/egg-schedule/commit/bce393f66f70add9151155d16b90942bab75e89b)] - fix: wrap task should always return promise (#48) (TZ | 天猪 <>) - -3.6.2 / 2019-04-29 -================== - -**fixes** - * [[`98a0cf7`](http://github.com/eggjs/egg-schedule/commit/98a0cf78012bd138cd8b0893c1acb6527cfecf0e)] - fix: runSchedule should pass args (#47) (TZ | 天猪 <>) - -**others** - * [[`77fc7d3`](http://github.com/eggjs/egg-schedule/commit/77fc7d3b46d1c57acc69cb3931241f9cb8165a38)] - docs: fix ctx ref (#46) (祝传鹏 <>) - -3.6.1 / 2019-03-20 -================== - -**others** - * [[`0960ff8`](http://github.com/eggjs/egg-schedule/commit/0960ff8f058c2bbb8ad2afb333bc557d719ec99b)] - chore: use relative log path (#45) (TZ | 天猪 <>) - -3.6.0 / 2018-12-18 -================== - -**features** - * [[`2c64d3c`](https://github.com/eggjs/egg-schedule/commit/2c64d3c6dd386dedaa784180ebb6c61b89fd1d53)] - feat: support custom directory (#43) (TZ <>) - -3.5.0 / 2018-12-05 -================== - -**features** - * [[`1aaf2d5`](http://github.com/eggjs/egg-schedule/commit/1aaf2d5675253d125eacca8bfd77813ecc151d2a)] - feat: support custom directory (#43) (Haoliang Gao <>) - -**fixes** - * [[`4dbf9d9`](http://github.com/eggjs/egg-schedule/commit/4dbf9d9d3785b19eb772704c724c421e1017922a)] - fix: unit-test in 'schedule.test.js' (#41) (Maledong <>) - -**others** - * [[`571bf9f`](http://github.com/eggjs/egg-schedule/commit/571bf9f28ed229f957fa70067786061a89dc1049)] - doc: Add notice for the evil 'setInterval' (#42) (Maledong <>) - * [[`07e4e23`](http://github.com/eggjs/egg-schedule/commit/07e4e238f198fbf935ac5e7fff279f349e11a6b5)] - docs: fix example in readme (cwtuan <>) - -3.4.0 / 2018-06-30 -================== - -**features** - * [[`417a764`](http://github.com/eggjs/egg-schedule/commit/417a7643807e56a432703e64f76923b60e1053ba)] - feat: support `schedule.env` (#39) (TZ | 天猪 <>) - -3.3.0 / 2018-02-24 -================== - - * feat: optimize logger msg (#38) - -3.2.1 / 2018-02-07 -================== - - * chore: fix doctools (#37) - -3.2.0 / 2018-02-06 -================== - -**features** - * [[`2003369`](http://github.com/eggjs/egg-schedule/commit/200336963cdf2404b926fa1c36223c41229cf32d)] - feat: egg-schedule.log && support send with args (#35) (TZ | 天猪 <>) - -3.1.1 / 2017-11-20 -================== - -**fixes** - * [[`9ff3974`](http://github.com/eggjs/egg-schedule/commit/9ff3974683e1f4ade72ccbe2448a3c68d7826530)] - fix: use ctx.coreLogger to record schedule log (#34) (Yiyu He <>) - -3.1.0 / 2017-11-16 -================== - -**features** - * [[`69a588e`](https://github.com/eggjs/egg-schedule/commit/69a588e5ffbb5a01ed3084bfb9f6c2a792963db4)] - feat: run a scheduler only once at startup (#33) (zhennann <>) - -3.0.0 / 2017-11-10 -================== - -**others** - * [[`925f1c3`](http://github.com/eggjs/egg-schedule/commit/925f1c38ffb5c8d73e91fe96e6e7fc30c3f43c5f)] - refactor: remove old stype strategy support [BREAKING CHANGE] (#29) (TZ | 天猪 <>) - * [[`4cdfa20`](http://github.com/eggjs/egg-schedule/commit/4cdfa204f1da36288328bf30acb0564da1e3d1b5)] - test: change to extend Subscription (#28) (TZ | 天猪 <>) - -2.6.0 / 2017-10-16 -================== - -**features** - * [[`f901df4`](http://github.com/eggjs/egg-schedule/commit/f901df4e895d440c9d3bc96e172d3cc87be95255)] - feat: Strategy interface change to start() (#26) (TZ | 天猪 <>) - * [[`c7816f2`](http://github.com/eggjs/egg-schedule/commit/c7816f2eb8ca668c92c1671b1d149c78dd73551e)] - feat: support class (#25) (Haoliang Gao <>) - -**others** - * [[`8797489`](http://github.com/eggjs/egg-schedule/commit/8797489f914a34bf56ecc68575b0b7e490628b5a)] - docs: use subscription (#27) (Haoliang Gao <>) - -2.5.1 / 2017-10-11 -================== - - * fix: publish files (#24) - -2.5.0 / 2017-10-11 -================== - - * refactor: classify (#23) - * test: sleep after runSchedule (#22) - -2.4.1 / 2017-06-06 -================== - - * fix: use safe-timers only large than interval && add tests (#21) - -2.4.0 / 2017-06-05 -================== - - * feat: use safe-timers to support large delay (#19) - -2.3.1 / 2017-06-04 -================== - - * docs: fix License url (#20) - * test: fix test on windows (#18) - * chore: upgrade all deps (#17) - -2.3.0 / 2017-02-08 -================== - - * feat: task support async function (#13) - * test: move app.close to afterEach (#12) - * chore: upgrade deps and fix test (#11) - -2.2.1 / 2016-10-25 -================== - - * fix: start schedule after egg-ready (#10) - -2.2.0 / 2016-09-29 -================== - - * feat: export app.schedules (#9) - * doc:fix plugin.js config demo (#8) - -2.1.0 / 2016-08-18 -================== - - * refactor: use FileLoader to load schedule files (#7) - -2.0.0 / 2016-08-16 -================== - - * Revert "Release 1.1.1" - * refactor: use loader.getLoadUnits from egg-core (#6) - -1.1.0 / 2016-08-15 -================== - - * docs: add readme (#5) - * feat: support immediate (#4) - -1.0.0 / 2016-08-10 -================== - - * fix: correct path in ctx (#3) - -0.1.0 / 2016-07-26 -================== - - * fix: use absolute path for store key (#2) - * test: add test cases (#1) - -0.0.1 / 2016-07-15 -================== - - * init diff --git a/README.md b/README.md index 76069bf..4e0a79e 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,20 @@ -# egg-schedule +# @eggjs/schedule [![NPM version][npm-image]][npm-url] [![Node.js CI](https://github.com/eggjs/egg-schedule/actions/workflows/nodejs.yml/badge.svg)](https://github.com/eggjs/egg-schedule/actions/workflows/nodejs.yml) [![Test coverage][codecov-image]][codecov-url] [![Known Vulnerabilities][snyk-image]][snyk-url] [![npm download][download-image]][download-url] +[![Node.js Version](https://img.shields.io/node/v/@eggjs/schedule.svg?style=flat)](https://nodejs.org/en/download/) -[npm-image]: https://img.shields.io/npm/v/egg-schedule.svg?style=flat-square -[npm-url]: https://npmjs.org/package/egg-schedule +[npm-image]: https://img.shields.io/npm/v/@eggjs/schedule.svg?style=flat-square +[npm-url]: https://npmjs.org/package/@eggjs/schedule [codecov-image]: https://codecov.io/github/eggjs/egg-schedule/coverage.svg?branch=master [codecov-url]: https://codecov.io/github/eggjs/egg-schedule?branch=master -[snyk-image]: https://snyk.io/test/npm/egg-schedule/badge.svg?style=flat-square -[snyk-url]: https://snyk.io/test/npm/egg-schedule -[download-image]: https://img.shields.io/npm/dm/egg-schedule.svg?style=flat-square -[download-url]: https://npmjs.org/package/egg-schedule +[snyk-image]: https://snyk.io/test/npm/@eggjs/schedule/badge.svg?style=flat-square +[snyk-url]: https://snyk.io/test/npm/@eggjs/schedule +[download-image]: https://img.shields.io/npm/dm/@eggjs/schedule.svg?style=flat-square +[download-url]: https://npmjs.org/package/@eggjs/schedule A schedule plugin for egg, has been built-in plugin for egg enabled by default. @@ -21,13 +22,13 @@ It's fully extendable for a developer and provides a simple built-in TimerStrate ## Usage -Just add your job file to `{app_root}/app/schedule`. +Just add your job file to `{baseDir}/app/schedule`. -```js -// {app_root}/app/schedule/cleandb.js -const Subscription = require('egg').Subscription; +```ts +// {baseDir}/app/schedule/cleandb.ts +import { Subscription } from 'egg'; -class CleanDB extends Subscription { +export default class CleanDB extends Subscription { /** * @property {Object} schedule * - {String} type - schedule type, `worker` or `all` or your custom types. @@ -51,28 +52,28 @@ class CleanDB extends Subscription { await this.ctx.service.db.cleandb(); } } - -module.exports = CleanDB; ``` -You can also use function simply like: +You can also use function simply like: + +```ts +import { EggContext } from 'egg'; -```js -exports.schedule = { +export const schedule = { type: 'worker', cron: '0 0 3 * * *', // interval: '1h', // immediate: true, -}; +} -exports.task = async function (ctx) { +export async function task(ctx: EggContext) { await ctx.service.db.cleandb(); -}; +} ``` ## Overview -`egg-schedule` supports both cron-based scheduling and interval-based scheduling. +`@eggjs/schedule` supports both cron-based scheduling and interval-based scheduling. Schedule decision is being made by `agent` process. `agent` triggers a task and sends a message to `worker` process. Then, one or all `worker` process(es) execute the task based on schedule type. @@ -91,21 +92,23 @@ You can get anonymous context with `this.ctx`. To create a task, `subscribe` can be a generator function or async function. For example: -```js +```ts // A simple logger example -const Subscription = require('egg').Subscription; -class LoggerExample extends Subscription { +import { Subscription } from 'egg'; + +export default class LoggerExample extends Subscription { async subscribe() { this.ctx.logger.info('Info about your task'); } } ``` -```js +```ts // A real world example: wipe out your database. // Use it with caution. :) -const Subscription = require('egg').Subscription; -class CleanDB extends Subscription { +import { Subscription } from 'egg'; + +export default class CleanDB extends Subscription { async subscribe() { await this.ctx.service.db.cleandb(); } @@ -138,14 +141,14 @@ Use [cron-parser](https://github.com/harrisiirak/cron-parser). Example: -```js +```ts // To execute task every 3 hours -exports.schedule = { +export const schedule = { type: 'worker', cron: '0 0 */3 * * *', cronOptions: { // tz: 'Europe/Athens', - } + }, }; ``` @@ -155,9 +158,9 @@ To use `setInterval`, and support [ms](https://www.npmjs.com/package/ms) convers Example: -```js +```ts // To execute task every 3 hours -exports.schedule = { +export const schedule = { type: 'worker', interval: '3h', }; @@ -175,11 +178,15 @@ exports.schedule = { **Custom schedule:** To create a custom schedule, simply extend `agent.ScheduleStrategy` and register it by `agent.schedule.use(type, clz)`. -You can schedule the task to be executed by one random worker or all workers with the built-in method `this.sendOne(...args)` or `this.sendAll(...args)` which support params, it will pass to `subscribe(...args)` or `task(ctx, ...args)`. +You can schedule the task to be executed by one random worker or all workers with +the built-in method `this.sendOne(...args)` or `this.sendAll(...args)` which support params, +it will pass to `subscribe(...args)` or `task(ctx, ...args)`. + +```ts +// {baseDir}/agent.ts +import { Agent } from 'egg'; -```js -// {app_root}/agent.js -module.exports = function(agent) { +export default (agent: Agent) => { class CustomStrategy extends agent.ScheduleStrategy { start() { // such as mq / redis subscribe @@ -188,34 +195,38 @@ module.exports = function(agent) { }); } } + agent.schedule.use('custom', CustomStrategy); -}; +} ``` Then you could use it to defined your job: -```js -// {app_root}/app/schedule/other.js -const Subscription = require('egg').Subscription; -class ClusterTask extends Subscription { +```ts +// {baseDir}/app/schedule/other.ts +import { Subscription } from 'egg'; + +export default class ClusterTask extends Subscription { static get schedule() { return { type: 'custom', }; } + async subscribe(data) { console.log('got custom data:', data); await this.ctx.service.someTask.run(); } } -module.exports = ClusterTask; ``` ## Dynamic schedule -```js -// {app_root}/app/schedule/sync.js -module.exports = app => { +```ts +// {baseDir}/app/schedule/sync.ts +import { Application } from 'egg'; + +export default (app: Application) => { class SyncTask extends app.Subscription { static get schedule() { return { @@ -227,10 +238,12 @@ module.exports = app => { env: [ 'prod' ], }; } + async subscribe() { await this.ctx.sync(); } } + return SyncTask; } ``` @@ -239,39 +252,47 @@ module.exports = app => { ### Logging -See `${appInfo.root}/logs/{app_name}/egg-schedule.log` which provided by [config.customLogger.scheduleLogger](https://github.com/eggjs/egg-schedule/blob/master/config/config.default.js). +See `${appInfo.root}/logs/{app_name}/egg-schedule.log` which provided by [config.customLogger.scheduleLogger](https://github.com/eggjs/egg-schedule/blob/master/config/config.default.ts). + +```ts +// config/config.default.ts +import { EggAppConfig } from 'egg'; -```js -// config/config.default.js -config.customLogger = { - scheduleLogger: { - // consoleLevel: 'NONE', - // file: path.join(appInfo.root, 'logs', appInfo.name, 'egg-schedule.log'), +export default { + customLogger: { + scheduleLogger: { + // consoleLevel: 'NONE', + // file: path.join(appInfo.root, 'logs', appInfo.name, 'egg-schedule.log'), + }, }, -}; +} as Partial; ``` ### Customize directory If you want to add additional schedule directories, you can use this config. -```js -// config/config.default.js -config.schedule = { - directory: [ - path.join(__dirname, '../app/otherSchedule'), - ], -}; +```ts +// config/config.default.ts +import { EggAppConfig } from 'egg'; + +export default { + schedule: { + directory: [ + 'path/to/otherSchedule', + ], + }, +} as Partial; ``` ## Testing -`app.runSchedule(scheduleName)` is provided by `egg-schedule` plugin only for test purpose. +`app.runSchedule(scheduleName)` is provided by `@eggjs/schedule` plugin only for test purpose. Example: -```js -it('test a schedule task', async function () { +```ts +it('test a schedule task', async () => { // get app instance await app.runSchedule('clean_cache'); }); @@ -284,3 +305,9 @@ Please open an issue [here](https://github.com/eggjs/egg/issues). ## License [MIT](https://github.com/eggjs/egg-schedule/blob/master/LICENSE) + +## Contributors + +[![Contributors](https://contrib.rocks/image?repo=eggjs/egg-schedule)](https://github.com/eggjs/egg-schedule/graphs/contributors) + +Made with [contributors-img](https://contrib.rocks). diff --git a/agent.js b/agent.js deleted file mode 100644 index 730edfd..0000000 --- a/agent.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -const WorkerStrategy = require('./lib/strategy/worker'); -const AllStrategy = require('./lib/strategy/all'); - -module.exports = agent => { - // register built-in strategy - agent.schedule.use('worker', WorkerStrategy); - agent.schedule.use('all', AllStrategy); - - // wait for other plugin to register custom strategy - agent.beforeStart(() => { - agent.schedule.init(); - }); - - // dispatch job finish event to strategy - agent.messenger.on('egg-schedule', (...args) => { - agent.schedule.onJobFinish(...args); - }); - - agent.messenger.once('egg-ready', () => { - // start schedule after worker ready - agent.schedule.start(); - }); -}; diff --git a/app.js b/app.js deleted file mode 100644 index aadf4e3..0000000 --- a/app.js +++ /dev/null @@ -1,115 +0,0 @@ -'use strict'; - -const qs = require('querystring'); -const path = require('path'); -const is = require('is-type-of'); - -module.exports = app => { - const logger = app.getLogger('scheduleLogger'); - const scheduleWorker = app.scheduleWorker; - scheduleWorker.init(); - - // log schedule list - for (const s in scheduleWorker.scheduleItems) { - const schedule = scheduleWorker.scheduleItems[s]; - if (!schedule.schedule.disable) logger.info('[egg-schedule]: register schedule %s', schedule.key); - } - - // register schedule event - app.messenger.on('egg-schedule', async info => { - const { id, key } = info; - logger.debug(`[Job#${id}] ${key} await app ready`); - await app.ready(); - const schedule = scheduleWorker.scheduleItems[key]; - logger.debug(`[Job#${id}] ${key} task received by app`); - - if (!schedule) { - logger.warn(`[Job#${id}] ${key} unknown task`); - return; - } - - /* istanbul ignore next */ - if (schedule.schedule.disable) { - logger.warn(`[Job#${id}] ${key} disable`); - return; - } - - logger.info(`[Job#${id}] ${key} executing by app`); - - // run with anonymous context - const ctx = app.createAnonymousContext({ - method: 'SCHEDULE', - url: `/__schedule?path=${key}&${qs.stringify(schedule.schedule)}`, - }); - - const start = Date.now(); - - let success; - let e; - try { - // execute - await app.ctxStorage.run(ctx, async () => { - return await schedule.task(ctx, ...info.args); - }); - success = true; - } catch (err) { - success = false; - e = is.error(err) ? err : new Error(err); - } - - const rt = Date.now() - start; - - const msg = `[Job#${id}] ${key} execute ${success ? 'succeed' : 'failed'}, used ${rt}ms.`; - logger[success ? 'info' : 'error'](msg, success ? '' : e); - - Object.assign(info, { - success, - workerId: process.pid, - rt, - message: e && e.message, - }); - - // notify agent job finish - app.messenger.sendToAgent('egg-schedule', info); - }); - - // for test purpose - const directory = [].concat(path.join(app.config.baseDir, 'app/schedule'), app.config.schedule.directory || []); - app.runSchedule = (schedulePath, ...args) => { - // resolve real path - if (path.isAbsolute(schedulePath)) { - schedulePath = require.resolve(schedulePath); - } else { - for (const dir of directory) { - try { - schedulePath = require.resolve(path.join(dir, schedulePath)); - break; - } catch (_) { - /* istanbul ignore next */ - } - } - } - - let schedule; - - try { - schedule = scheduleWorker.scheduleItems[schedulePath]; - if (!schedule) { - throw new Error(`Cannot find schedule ${schedulePath}`); - } - } catch (err) { - err.message = `[egg-schedule] ${err.message}`; - return Promise.reject(err); - } - - // run with anonymous context - const ctx = app.createAnonymousContext({ - method: 'SCHEDULE', - url: `/__schedule?path=${schedulePath}&${qs.stringify(schedule.schedule)}`, - }); - - return app.ctxStorage.run(ctx, () => { - return schedule.task(ctx, ...args); - }); - }; -}; diff --git a/lib/load_schedule.js b/lib/load_schedule.js deleted file mode 100644 index 8b9fbda..0000000 --- a/lib/load_schedule.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict'; - -const path = require('path'); -const assert = require('assert'); -const is = require('is-type-of'); - -module.exports = app => { - const dirs = app.loader.getLoadUnits().map(unit => path.join(unit.path, 'app/schedule')); - dirs.push(...app.config.schedule.directory); - - const Loader = getScheduleLoader(app); - const schedules = app.schedules = {}; - new Loader({ - directory: dirs, - target: schedules, - inject: app, - }).load(); - return schedules; -}; - -function getScheduleLoader(app) { - return class ScheduleLoader extends app.loader.FileLoader { - load() { - const target = this.options.target; - const items = this.parse(); - for (const item of items) { - const schedule = item.exports; - const fullpath = item.fullpath; - assert(schedule.schedule, `schedule(${fullpath}): must have schedule and task properties`); - assert(is.class(schedule) || is.function(schedule.task), `schedule(${fullpath}: schedule.task should be function or schedule should be class`); - - let task; - if (is.class(schedule)) { - task = async (ctx, data) => { - const s = new schedule(ctx); - s.subscribe = app.toAsyncFunction(s.subscribe); - return s.subscribe(data); - }; - } else { - task = app.toAsyncFunction(schedule.task); - } - - const env = app.config.env; - const envList = schedule.schedule.env; - if (is.array(envList) && !envList.includes(env)) { - app.coreLogger.info(`[egg-schedule]: ignore schedule ${fullpath} due to \`schedule.env\` not match`); - continue; - } - - // handle symlink case - const realFullpath = require.resolve(fullpath); - target[realFullpath] = { - schedule: schedule.schedule, - task, - key: realFullpath, - }; - } - } - }; -} diff --git a/lib/schedule.js b/lib/schedule.js deleted file mode 100644 index 8d1fdd3..0000000 --- a/lib/schedule.js +++ /dev/null @@ -1,86 +0,0 @@ -'use strict'; - -const STRATEGY = Symbol('strategy'); -const STRATEGY_INSTANCE = Symbol('strategy_instance'); -const loadSchedule = require('./load_schedule'); - -module.exports = class Schedule { - constructor(agent) { - this.agent = agent; - this.logger = agent.getLogger('scheduleLogger'); - this[STRATEGY] = new Map(); - this[STRATEGY_INSTANCE] = new Map(); - this.closed = false; - } - - /** - * register a custom Schedule Strategy - * @param {String} type - strategy type - * @param {Strategy} clz - Strategy class - */ - use(type, clz) { - this[STRATEGY].set(type, clz); - } - - /** - * load all schedule jobs, then initialize and register speical strategy - */ - init() { - const scheduleItems = loadSchedule(this.agent); - - for (const scheduleItem of Object.values(scheduleItems)) { - this.registerSchedule(scheduleItem); - } - } - - registerSchedule(scheduleItem) { - const { key, schedule } = scheduleItem; - const type = schedule.type; - if (schedule.disable) return; - - // find speical Strategy - const Strategy = this[STRATEGY].get(type); - if (!Strategy) { - const err = new Error(`schedule type [${type}] is not defined`); - err.name = 'EggScheduleError'; - throw err; - } - - // Initialize strategy and register - const instance = new Strategy(schedule, this.agent, key); - this[STRATEGY_INSTANCE].set(key, instance); - } - - unregisterSchedule(key) { - return this[STRATEGY_INSTANCE].delete(key); - } - - /** - * job finish event handler - * - * @param {Object} info - { key, success, message } - */ - onJobFinish(info) { - this.logger.debug(`[Job#${info.id}] ${info.key} finish event received by agent from worker#${info.workerId}`); - - const instance = this[STRATEGY_INSTANCE].get(info.key); - /* istanbul ignore else */ - if (instance) { - instance.onJobFinish(info); - } - } - - /** - * start schedule - */ - start() { - this.closed = false; - for (const instance of this[STRATEGY_INSTANCE].values()) { - instance.start(); - } - } - - close() { - this.closed = true; - } -}; diff --git a/lib/schedule_worker.js b/lib/schedule_worker.js deleted file mode 100644 index 2b00557..0000000 --- a/lib/schedule_worker.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; - -const loadSchedule = require('./load_schedule'); - -module.exports = class ScheduleWorker { - constructor(app) { - this.app = app; - this.scheduleItems = {}; - } - - init() { - this.scheduleItems = loadSchedule(this.app); - } - - registerSchedule(scheduleItem) { - const { key } = scheduleItem; - this.scheduleItems[key] = scheduleItem; - } - - unregisterSchedule(key) { - delete this.scheduleItems[key]; - } -}; diff --git a/lib/strategy/all.js b/lib/strategy/all.js deleted file mode 100644 index 1b7b7df..0000000 --- a/lib/strategy/all.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -const Strategy = require('./timer'); - -module.exports = class AllStrategy extends Strategy { - handler() { - this.sendAll(); - } -}; diff --git a/lib/strategy/base.js b/lib/strategy/base.js deleted file mode 100644 index b37af18..0000000 --- a/lib/strategy/base.js +++ /dev/null @@ -1,70 +0,0 @@ -'use strict'; - -module.exports = class BaseStrategy { - constructor(schedule, agent, key) { - this.agent = agent; - this.key = key; - this.schedule = schedule; - this.logger = this.agent.getLogger('scheduleLogger'); - this.count = 0; - } - - start() {} - - onJobStart() {} - - onJobFinish() {} - - /** - * trigger one worker - * - * @param {...any} args - pass to job task - */ - sendOne(...args) { - /* istanbul ignore next */ - if (this.agent.schedule.closed) { - this.logger.warn(`${this.key} skip due to schedule closed`); - return; - } - - this.count++; - - const info = { - key: this.key, - id: this.getSeqId(), - args, - }; - - this.logger.debug(`[Job#${info.id}] ${info.key} triggered, send random by agent`); - this.agent.messenger.sendRandom('egg-schedule', info); - this.onJobStart(info); - } - - /** - * trigger all worker - * - * @param {...any} args - pass to job task - */ - sendAll(...args) { - /* istanbul ignore next */ - if (this.agent.schedule.closed) { - this.logger.warn(`${this.key} skip due to schedule closed`); - return; - } - - this.count++; - - const info = { - key: this.key, - id: this.getSeqId(), - args, - }; - this.logger.debug(`[Job#${info.id}] ${info.key} triggered, send all by agent`); - this.agent.messenger.send('egg-schedule', info); - this.onJobStart(info); - } - - getSeqId() { - return `${Date.now()}${process.hrtime().join('')}${this.count}`; - } -}; diff --git a/lib/strategy/timer.js b/lib/strategy/timer.js deleted file mode 100644 index 5acc73c..0000000 --- a/lib/strategy/timer.js +++ /dev/null @@ -1,98 +0,0 @@ -'use strict'; - -const Strategy = require('./base'); -const parser = require('cron-parser'); -const ms = require('humanize-ms'); -const safetimers = require('safe-timers'); -const assert = require('assert'); -const utility = require('utility'); -const is = require('is-type-of'); -const CRON_INSTANCE = Symbol('cron_instance'); - -module.exports = class TimerStrategy extends Strategy { - constructor(...args) { - super(...args); - - const { interval, cron, cronOptions, immediate } = this.schedule; - assert(interval || cron || immediate, `[egg-schedule] ${this.key} schedule.interval or schedule.cron or schedule.immediate must be present`); - assert(is.function(this.handler), `[egg-schedule] ${this.key} strategy should override \`handler()\` method`); - - // init cron parser - if (cron) { - try { - this[CRON_INSTANCE] = parser.parseExpression(cron, cronOptions); - } catch (err) { - err.message = `[egg-schedule] ${this.key} parse cron instruction(${cron}) error: ${err.message}`; - throw err; - } - } - } - - start() { - /* istanbul ignore next */ - if (this.agent.schedule.closed) return; - - if (this.schedule.immediate) { - this.logger.info(`[Timer] ${this.key} next time will execute immediate`); - setImmediate(() => this.handler()); - } else { - this._scheduleNext(); - } - } - - _scheduleNext() { - /* istanbul ignore next */ - if (this.agent.schedule.closed) return; - - // get next tick - const nextTick = this.getNextTick(); - - if (nextTick) { - this.logger.info(`[Timer] ${this.key} next time will execute after ${nextTick}ms at ${utility.logDate(new Date(Date.now() + nextTick))}`); - this.safeTimeout(() => this.handler(), nextTick); - } else { - this.logger.info(`[Timer] ${this.key} reach endDate, will stop`); - } - } - - onJobStart() { - // Next execution will trigger task at a fix rate, regardless of its execution time. - this._scheduleNext(); - } - - /** - * calculate next tick - * - * @return {Number|undefined} time interval, if out of range then return `undefined` - */ - getNextTick() { - // interval-style - if (this.schedule.interval) return ms(this.schedule.interval); - - // cron-style - if (this[CRON_INSTANCE]) { - // calculate next cron tick - const now = Date.now(); - let nextTick; - let nextInterval; - - // loop to find next feature time - do { - try { - nextInterval = this[CRON_INSTANCE].next(); - nextTick = nextInterval.getTime(); - } catch (err) { - // Error: Out of the timespan range - return; - } - } while (now >= nextTick); - - return nextTick - now; - } - } - - safeTimeout(handler, delay, ...args) { - const fn = delay < safetimers.maxInterval ? setTimeout : safetimers.setTimeout; - return fn(handler, delay, ...args); - } -}; diff --git a/lib/strategy/worker.js b/lib/strategy/worker.js deleted file mode 100644 index 5b83024..0000000 --- a/lib/strategy/worker.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -const Strategy = require('./timer'); - -module.exports = class WorkerStrategy extends Strategy { - handler() { - this.sendOne(); - } -}; diff --git a/package.json b/package.json index b5de675..7f02be3 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,20 @@ { - "name": "egg-schedule", + "name": "@eggjs/schedule", "version": "4.0.1", + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">=18.19.0" + }, "description": "schedule plugin for egg, support corn job.", "eggPlugin": { - "name": "schedule" + "name": "schedule", + "exports": { + "import": "./dist/esm", + "require": "./dist/commonjs" + } }, - "files": [ - "app", - "lib", - "config", - "agent.js", - "app.js" - ], "repository": { "type": "git", "url": "git@github.com:eggjs/egg-schedule.git" @@ -24,30 +27,65 @@ "cron" ], "dependencies": { - "cron-parser": "^2.16.3", - "humanize-ms": "^1.2.1", - "is-type-of": "^1.2.1", + "@eggjs/utils": "^4.0.3", + "cron-parser": "^4.9.0", + "humanize-ms": "^2.0.0", + "is-type-of": "^2.1.0", "safe-timers": "^1.1.0", - "utility": "^1.16.3" + "utility": "^2.1.0" }, "devDependencies": { - "egg": "^3.7.0", - "egg-bin": "^5.5.0", - "egg-mock": "^5.3.0", - "egg-tracer": "^1.1.0", - "eslint": "^8.29.0", - "eslint-config-egg": "^12.1.0" - }, - "engines": { - "node": ">=14.17.0" + "@arethetypeswrong/cli": "^0.17.1", + "@eggjs/tsconfig": "1", + "@types/mocha": "10", + "@types/node": "22", + "@types/safe-timers": "^1.1.2", + "egg": "beta", + "egg-bin": "beta", + "egg-logrotator": "^3.2.0", + "egg-mock": "beta", + "egg-tracer": "2", + "eslint": "8", + "eslint-config-egg": "14", + "tshy": "3", + "tshy-after": "1", + "typescript": "5" }, "scripts": { - "lint": "eslint .", - "test": "npm run lint -- --fix && npm run test-local", - "test-local": "egg-bin test", - "cov": "egg-bin cov", - "ci": "npm run lint && npm run cov" + "lint": "eslint --cache src test --ext .ts", + "pretest": "npm run lint -- --fix && npm run prepublishOnly", + "test": "egg-bin test", + "preci": "npm run lint && npm run prepublishOnly && attw --pack", + "ci": "egg-bin cov", + "prepublishOnly": "tshy && tshy-after" }, "author": "dead_horse", - "license": "MIT" + "license": "MIT", + "type": "module", + "tshy": { + "exports": { + ".": "./src/index.ts", + "./package.json": "./package.json" + } + }, + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/commonjs/index.d.ts", + "default": "./dist/commonjs/index.js" + } + }, + "./package.json": "./package.json" + }, + "files": [ + "dist", + "src" + ], + "types": "./dist/commonjs/index.d.ts", + "main": "./dist/commonjs/index.js", + "module": "./dist/esm/index.js" } diff --git a/src/agent.ts b/src/agent.ts new file mode 100644 index 0000000..2adb814 --- /dev/null +++ b/src/agent.ts @@ -0,0 +1,36 @@ +import { debuglog } from 'node:util'; +import type { Agent, ILifecycleBoot } from 'egg'; +import { WorkerStrategy } from './lib/strategy/worker.js'; +import { AllStrategy } from './lib/strategy/all.js'; +import { EggScheduleJobInfo } from './lib/types.js'; + +const debug = debuglog('@eggjs/schedule/agent'); + +export default class Boot implements ILifecycleBoot { + #agent: Agent; + constructor(agent: Agent) { + this.#agent = agent; + } + + async didLoad(): Promise { + // register built-in strategy + this.#agent.schedule.use('worker', WorkerStrategy); + this.#agent.schedule.use('all', AllStrategy); + + // wait for other plugin to register custom strategy + await this.#agent.schedule.init(); + + // dispatch job finish event to strategy + this.#agent.messenger.on('egg-schedule', (info: EggScheduleJobInfo) => { + // get job info from worker + this.#agent.schedule.onJobFinish(info); + }); + debug('didLoad'); + } + + async serverDidReady(): Promise { + // start schedule after worker ready + this.#agent.schedule.start(); + debug('serverDidReady, schedule start'); + } +} diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..463347e --- /dev/null +++ b/src/app.ts @@ -0,0 +1,142 @@ +import { debuglog } from 'node:util'; +import path from 'node:path'; +import type { + Application, ILifecycleBoot, EggLogger, +} from 'egg'; +import { importResolve } from '@eggjs/utils'; +import { EggScheduleItem, EggScheduleJobInfo } from './lib/types.js'; + +const debug = debuglog('@eggjs/schedule/app'); + +export default class Boot implements ILifecycleBoot { + #app: Application; + #logger: EggLogger; + constructor(app: Application) { + this.#app = app; + this.#logger = app.getLogger('scheduleLogger'); + } + + async didLoad(): Promise { + const scheduleWorker = this.#app.scheduleWorker; + await scheduleWorker.init(); + + // log schedule list + for (const s in scheduleWorker.scheduleItems) { + const schedule = scheduleWorker.scheduleItems[s]; + if (!schedule.schedule.disable) { + this.#logger.info('[@eggjs/schedule]: register schedule %s', schedule.key); + } + } + + // register schedule event + this.#app.messenger.on('egg-schedule', async info => { + debug('app got "egg-schedule" message: %o', info); + const { id, key } = info; + this.#logger.debug(`[Job#${id}] ${key} await app ready`); + await this.#app.ready(); + const schedule = scheduleWorker.scheduleItems[key]; + this.#logger.debug(`[Job#${id}] ${key} task received by app`); + + if (!schedule) { + this.#logger.warn(`[Job#${id}] ${key} unknown task`); + return; + } + + /* istanbul ignore next */ + if (schedule.schedule.disable) { + this.#logger.warn(`[Job#${id}] ${key} disable`); + return; + } + + this.#logger.info(`[Job#${id}] ${key} executing by app`); + + // run with anonymous context + const ctx = this.#app.createAnonymousContext({ + method: 'SCHEDULE', + url: `/__schedule?path=${key}&${schedule.scheduleQueryString}`, + }); + + const start = Date.now(); + + let success: boolean; + let e: Error | undefined; + try { + // execute + await this.#app.ctxStorage.run(ctx, async () => { + return await schedule.task(ctx, ...info.args); + }); + success = true; + } catch (err: any) { + success = false; + e = err; + } + + const rt = Date.now() - start; + + const msg = `[Job#${id}] ${key} execute ${success ? 'succeed' : 'failed'}, used ${rt}ms.`; + if (success) { + this.#logger.info(msg); + } else { + this.#logger.error(msg, e); + } + + // notify agent job finish + this.#app.messenger.sendToAgent('egg-schedule', { + ...info, + success, + workerId: process.pid, + rt, + message: e?.message, + } as EggScheduleJobInfo); + }); + + // for test purpose + const config = this.#app.config; + const directory = [ + path.join(config.baseDir, 'app/schedule'), + ...config.schedule.directory, + ]; + const runSchedule = async (schedulePath: string, ...args: any[]) => { + debug('[runSchedule] start schedulePath: %o, args: %o', schedulePath, args); + + // resolve real path + if (path.isAbsolute(schedulePath)) { + schedulePath = importResolve(schedulePath); + } else { + for (const dir of directory) { + const trySchedulePath = path.join(dir, schedulePath); + try { + schedulePath = importResolve(trySchedulePath); + break; + } catch (err) { + debug('[runSchedule] importResolve %o error: %s', trySchedulePath, err); + } + } + } + + debug('[runSchedule] resolve schedulePath: %o', schedulePath); + let schedule: EggScheduleItem; + try { + schedule = scheduleWorker.scheduleItems[schedulePath]; + if (!schedule) { + throw new Error(`Cannot find schedule ${schedulePath}`); + } + } catch (err: any) { + err.message = `[@eggjs/schedule] ${err.message}`; + throw err; + } + + // run with anonymous context + const ctx = this.#app.createAnonymousContext({ + method: 'SCHEDULE', + url: `/__schedule?path=${schedulePath}&${schedule.scheduleQueryString}`, + }); + return await this.#app.ctxStorage.run(ctx, async () => { + return await schedule.task(ctx, ...args); + }); + }; + Reflect.set(this.#app, 'runSchedule', runSchedule); + + debug('didLoad'); + } +} diff --git a/app/extend/agent.js b/src/app/extend/agent.ts similarity index 59% rename from app/extend/agent.js rename to src/app/extend/agent.ts index 907c66d..81dd508 100644 --- a/app/extend/agent.js +++ b/src/app/extend/agent.ts @@ -1,16 +1,14 @@ -'use strict'; - -const Strategy = require('../../lib/strategy/base'); -const TimerStrategy = require('../../lib/strategy/timer'); -const Schedule = require('../../lib/schedule'); +import { BaseStrategy } from '../../lib/strategy/base.js'; +import { TimerStrategy } from '../../lib/strategy/timer.js'; +import { Schedule } from '../../lib/schedule.js'; const SCHEDULE = Symbol('agent#schedule'); -module.exports = { +export default { /** * @member agent#ScheduleStrategy */ - ScheduleStrategy: Strategy, + ScheduleStrategy: BaseStrategy, /** * @member agent#TimerScheduleStrategy @@ -23,10 +21,10 @@ module.exports = { get schedule() { if (!this[SCHEDULE]) { this[SCHEDULE] = new Schedule(this); - this.beforeClose(() => { + this.lifecycle.registerBeforeClose(() => { return this[SCHEDULE].close(); }); } return this[SCHEDULE]; }, -}; +} as any; diff --git a/app/extend/application.js b/src/app/extend/application.ts similarity index 65% rename from app/extend/application.js rename to src/app/extend/application.ts index fad95b4..0003596 100644 --- a/app/extend/application.js +++ b/src/app/extend/application.ts @@ -1,12 +1,10 @@ -'use strict'; - -const ScheduleWorker = require('../../lib/schedule_worker'); +import { ScheduleWorker } from '../../lib/schedule_worker.js'; const SCHEDULE_WORKER = Symbol('application#scheduleWorker'); -module.exports = { +export default { /** - * @member agent#schedule + * @member app#schedule */ get scheduleWorker() { if (!this[SCHEDULE_WORKER]) { @@ -14,4 +12,5 @@ module.exports = { } return this[SCHEDULE_WORKER]; }, -}; +} as any; + diff --git a/config/config.default.js b/src/config/config.default.ts similarity index 77% rename from config/config.default.js rename to src/config/config.default.ts index 0fba4d8..18de77e 100644 --- a/config/config.default.js +++ b/src/config/config.default.ts @@ -1,7 +1,5 @@ -'use strict'; - -module.exports = () => { - const config = {}; +export default () => { + const config = {} as Record; config.customLogger = { scheduleLogger: { diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..f6ebe91 --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ +export * from './lib/types.js'; diff --git a/src/lib/load_schedule.ts b/src/lib/load_schedule.ts new file mode 100644 index 0000000..b3fc656 --- /dev/null +++ b/src/lib/load_schedule.ts @@ -0,0 +1,74 @@ +import path from 'node:path'; +import assert from 'node:assert'; +import { stringify } from 'node:querystring'; +import { isClass, isFunction, isGeneratorFunction } from 'is-type-of'; +import { importResolve } from '@eggjs/utils'; +import type { EggApplicationCore, EggContext } from 'egg'; +import type { EggScheduleConfig, EggScheduleTask, EggScheduleItem } from './types.js'; + +function getScheduleLoader(app: EggApplicationCore) { + return class ScheduleLoader extends app.loader.FileLoader { + async load() { + const target = this.options.target as Record; + const items = await this.parse(); + for (const item of items) { + const schedule = item.exports as { schedule: EggScheduleConfig, task: EggScheduleTask }; + const fullpath = item.fullpath; + const scheduleConfig = schedule.schedule; + assert(scheduleConfig, `schedule(${fullpath}): must have "schedule" and "task" properties`); + assert(isClass(schedule) || isFunction(schedule.task), + `schedule(${fullpath}: \`schedule.task\` should be function or \`schedule\` should be class`); + + let task: EggScheduleTask; + if (isClass(schedule)) { + assert(!isGeneratorFunction(schedule.prototype.subscribe), + `schedule(${fullpath}): "schedule" generator function is not support, should use async function instead`); + task = async (ctx: EggContext, ...args: any[]) => { + const instance = new schedule(ctx); + // s.subscribe = app.toAsyncFunction(s.subscribe); + return instance.subscribe(...args); + }; + } else { + assert(!isGeneratorFunction(schedule.task), + `schedule(${fullpath}): "task" generator function is not support, should use async function instead`); + task = schedule.task; + // task = app.toAsyncFunction(schedule.task); + } + + const env = app.config.env; + const envList = schedule.schedule.env; + if (Array.isArray(envList) && !envList.includes(env)) { + app.coreLogger.info(`[@eggjs/schedule]: ignore schedule ${fullpath} due to \`schedule.env\` not match`); + continue; + } + + // handle symlink case + const realFullpath = importResolve(fullpath); + target[realFullpath] = { + schedule: scheduleConfig, + scheduleQueryString: stringify(scheduleConfig as any), + task, + key: realFullpath, + }; + } + return target; + } + }; +} + +export async function loadSchedule(app: EggApplicationCore) { + const dirs = [ + ...app.loader.getLoadUnits().map(unit => path.join(unit.path, 'app/schedule')), + ...app.config.schedule.directory, + ]; + + const Loader = getScheduleLoader(app); + const schedules = {} as Record; + await new Loader({ + directory: dirs, + target: schedules, + inject: app, + }).load(); + Reflect.set(app, 'schedules', schedules); + return schedules; +} diff --git a/src/lib/schedule.ts b/src/lib/schedule.ts new file mode 100644 index 0000000..3c28d18 --- /dev/null +++ b/src/lib/schedule.ts @@ -0,0 +1,100 @@ +import { debuglog } from 'node:util'; +import type { Agent, EggLogger } from 'egg'; +import { loadSchedule } from './load_schedule.js'; +import type { EggScheduleItem, EggScheduleJobInfo } from './types.js'; +import type { BaseStrategy } from './strategy/base.js'; + +const debug = debuglog('@eggjs/schedule/lib/schedule'); + +export class Schedule { + closed = false; + + #agent: Agent; + #logger: EggLogger; + #strategyClassMap = new Map(); + #strategyInstanceMap = new Map(); + + constructor(agent: Agent) { + this.#agent = agent; + this.#logger = agent.getLogger('scheduleLogger'); + } + + /** + * register a custom Schedule Strategy + * @param {String} type - strategy type + * @param {Strategy} clz - Strategy class + */ + use(type: string, clz: typeof BaseStrategy) { + this.#strategyClassMap.set(type, clz); + debug('use type: %o', type); + } + + /** + * load all schedule jobs, then initialize and register speical strategy + */ + async init() { + const scheduleItems = await loadSchedule(this.#agent); + for (const scheduleItem of Object.values(scheduleItems)) { + this.registerSchedule(scheduleItem); + } + } + + registerSchedule(scheduleItem: EggScheduleItem) { + const { key, schedule } = scheduleItem; + const type = schedule.type; + if (schedule.disable) { + return; + } + + // find Strategy by type + const Strategy = this.#strategyClassMap.get(type!); + if (!Strategy) { + const err = new Error(`schedule type [${type}] is not defined`); + err.name = 'EggScheduleError'; + throw err; + } + + // Initialize strategy and register + const instance = new Strategy(schedule, this.#agent, key); + this.#strategyInstanceMap.set(key, instance); + debug('registerSchedule type: %o, config: %o, key: %o', type, schedule, key); + } + + unregisterSchedule(key: string) { + debug('unregisterSchedule key: %o', key); + return this.#strategyInstanceMap.delete(key); + } + + /** + * job finish event handler + * + * @param {Object} info - { id, key, success, message, workerId } + */ + onJobFinish(info: EggScheduleJobInfo) { + this.#logger.debug(`[Job#${info.id}] ${info.key} finish event received by agent from worker#${info.workerId}`); + const instance = this.#strategyInstanceMap.get(info.key); + /* istanbul ignore else */ + if (instance) { + instance.onJobFinish(info); + } + } + + /** + * start schedule + */ + start() { + debug('start'); + this.closed = false; + for (const instance of this.#strategyInstanceMap.values()) { + instance.start(); + } + } + + close() { + this.closed = true; + for (const instance of this.#strategyInstanceMap.values()) { + instance.close(); + } + debug('close'); + } +} diff --git a/src/lib/schedule_worker.ts b/src/lib/schedule_worker.ts new file mode 100644 index 0000000..415ea20 --- /dev/null +++ b/src/lib/schedule_worker.ts @@ -0,0 +1,24 @@ +import type { Application } from 'egg'; +import { loadSchedule } from './load_schedule.js'; +import type { EggScheduleItem } from './types.js'; + +export class ScheduleWorker { + #app: Application; + scheduleItems: Record = {}; + + constructor(app: Application) { + this.#app = app; + } + + async init() { + this.scheduleItems = await loadSchedule(this.#app); + } + + registerSchedule(scheduleItem: EggScheduleItem) { + this.scheduleItems[scheduleItem.key] = scheduleItem; + } + + unregisterSchedule(key: string) { + delete this.scheduleItems[key]; + } +} diff --git a/src/lib/strategy/all.ts b/src/lib/strategy/all.ts new file mode 100644 index 0000000..5bd033b --- /dev/null +++ b/src/lib/strategy/all.ts @@ -0,0 +1,7 @@ +import { TimerStrategy } from './timer.js'; + +export class AllStrategy extends TimerStrategy { + handler() { + this.sendAll(); + } +} diff --git a/src/lib/strategy/base.ts b/src/lib/strategy/base.ts new file mode 100644 index 0000000..8b83834 --- /dev/null +++ b/src/lib/strategy/base.ts @@ -0,0 +1,91 @@ +import type { Agent, EggLogger } from 'egg'; +import type { EggScheduleConfig, EggScheduleJobInfo } from '../types.js'; + +export class BaseStrategy { + protected agent: Agent; + protected scheduleConfig: EggScheduleConfig; + protected key: string; + protected logger: EggLogger; + protected closed = false; + count = 0; + + constructor(scheduleConfig: EggScheduleConfig, agent: Agent, key: string) { + this.agent = agent; + this.key = key; + this.scheduleConfig = scheduleConfig; + this.logger = this.agent.getLogger('scheduleLogger'); + } + + /** keep compatibility */ + get schedule(): EggScheduleConfig { + return this.scheduleConfig; + } + + start() { + // empty loop by default + } + + close() { + this.closed = true; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onJobStart(_info: EggScheduleJobInfo) {} + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onJobFinish(_info: EggScheduleJobInfo) {} + + /** + * trigger one worker + * + * @param {...any} args - pass to job task + */ + sendOne(...args: any[]) { + /* istanbul ignore next */ + if (this.agent.schedule.closed) { + this.logger.warn(`${this.key} skip due to schedule closed`); + return; + } + + this.count++; + + const info = { + key: this.key, + id: this.getSeqId(), + args, + } as EggScheduleJobInfo; + + this.logger.info(`[Job#${info.id}] ${info.key} triggered, send random by agent`); + this.agent.messenger.sendRandom('egg-schedule', info); + this.onJobStart(info); + } + + /** + * trigger all worker + * + * @param {...any} args - pass to job task + */ + sendAll(...args: any[]) { + /* istanbul ignore next */ + if (this.agent.schedule.closed) { + this.logger.warn(`${this.key} skip due to schedule closed`); + return; + } + + this.count++; + + const info = { + key: this.key, + id: this.getSeqId(), + args, + } as EggScheduleJobInfo; + this.logger.info(`[Job#${info.id}] ${info.key} triggered, send all by agent`); + // send to all workers + this.agent.messenger.send('egg-schedule', info); + this.onJobStart(info); + } + + getSeqId() { + return `${Date.now()}${process.hrtime().join('')}${this.count}`; + } +} diff --git a/src/lib/strategy/timer.ts b/src/lib/strategy/timer.ts new file mode 100644 index 0000000..939ea72 --- /dev/null +++ b/src/lib/strategy/timer.ts @@ -0,0 +1,106 @@ +import assert from 'node:assert'; +import { parseExpression, type CronExpression } from 'cron-parser'; +import { ms } from 'humanize-ms'; +import safeTimers from 'safe-timers'; +import { logDate } from 'utility'; +import type { Agent } from 'egg'; +import type { EggScheduleConfig } from '../types.js'; +import { BaseStrategy } from './base.js'; + +export abstract class TimerStrategy extends BaseStrategy { + protected cronInstance?: CronExpression; + + constructor(scheduleConfig: EggScheduleConfig, agent: Agent, key: string) { + super(scheduleConfig, agent, key); + + const { interval, cron, cronOptions, immediate } = this.scheduleConfig; + assert(interval || cron || immediate, + `[@eggjs/schedule] ${this.key} \`schedule.interval\` or \`schedule.cron\` or \`schedule.immediate\` must be present`); + + // init cron parser + if (cron) { + try { + this.cronInstance = parseExpression(cron, cronOptions); + } catch (err: any) { + throw new TypeError( + `[@eggjs/schedule] ${this.key} parse cron instruction(${cron}) error: ${err.message}`, + { cause: err }); + } + } + } + + protected handler() { + throw new TypeError(`[@eggjs/schedule] ${this.key} strategy should override \`handler()\` method`); + } + + + start() { + /* istanbul ignore next */ + if (this.agent.schedule.closed) return; + + if (this.scheduleConfig.immediate) { + this.logger.info(`[Timer] ${this.key} next time will execute immediate`); + setImmediate(() => this.handler()); + } else { + this.#scheduleNext(); + } + } + + #scheduleNext() { + /* istanbul ignore next */ + if (this.agent.schedule.closed) return; + + // get next tick + const nextTick = this.getNextTick(); + if (nextTick) { + this.logger.info( + `[Timer] ${this.key} next time will execute after ${nextTick}ms at ${logDate(new Date(Date.now() + nextTick))}`); + this.safeTimeout(() => this.handler(), nextTick); + } else { + this.logger.info(`[Timer] ${this.key} reach endDate, will stop`); + } + } + + onJobStart() { + // Next execution will trigger task at a fix rate, regardless of its execution time. + this.#scheduleNext(); + } + + /** + * calculate next tick + * + * @return {Number|undefined} time interval, if out of range then return `undefined` + */ + protected getNextTick(): number | undefined { + // interval-style + if (this.scheduleConfig.interval) { + return ms(this.scheduleConfig.interval); + } + + // cron-style + if (this.cronInstance) { + // calculate next cron tick + const now = Date.now(); + let nextTick: number; + + // loop to find next feature time + do { + try { + const nextInterval = this.cronInstance.next(); + nextTick = nextInterval.getTime(); + } catch (err) { + // Error: Out of the timespan range + this.logger.info(`[Timer] ${this.key} cron out of the timespan range, error: %s`, err); + return; + } + } while (now >= nextTick); + return nextTick - now; + } + // won\'t run here + } + + protected safeTimeout(handler: () => void, delay: number, ...args: any[]) { + const fn = delay < safeTimers.maxInterval ? setTimeout : safeTimers.setTimeout; + return fn(handler, delay, ...args); + } +} diff --git a/src/lib/strategy/worker.ts b/src/lib/strategy/worker.ts new file mode 100644 index 0000000..078dbe1 --- /dev/null +++ b/src/lib/strategy/worker.ts @@ -0,0 +1,7 @@ +import { TimerStrategy } from './timer.js'; + +export class WorkerStrategy extends TimerStrategy { + handler() { + this.sendOne(); + } +} diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..36d8e38 --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,58 @@ +import type { ParserOptions as CronOptions } from 'cron-parser'; +import type { Schedule } from './schedule.js'; +import type { ScheduleWorker } from './schedule_worker.js'; + +/** + * Schedule Config + * @see https://www.eggjs.org/zh-CN/basics/schedule + */ +export interface EggScheduleConfig { + type?: 'worker' | 'all'; + interval?: string | number; + cron?: string; + cronOptions?: CronOptions; + immediate?: boolean; + disable?: boolean; + env?: string[]; +} + +export type EggScheduleTask = (ctx: any, ...args: any[]) => Promise; + +export interface EggScheduleItem { + schedule: EggScheduleConfig; + scheduleQueryString: string; + task: EggScheduleTask; + key: string; +} + +export interface EggScheduleJobInfo { + id: string; + key: string; + workerId: number; + args: any[]; + success?: boolean; + message?: string; + rt?: number; +} + +declare module 'egg' { + export interface EggScheduleAgent { + schedule: Schedule; + } + export interface Agent extends EggScheduleAgent {} + + export interface EggScheduleApplication { + scheduleWorker: ScheduleWorker; + /** runSchedule in unittest */ + runSchedule: (schedulePath: string, ...args: any[]) => Promise; + } + export interface Application extends EggScheduleApplication {} + + export interface EggScheduleAppConfig { + schedule: { + directory: string[]; + }; + } + + export interface EggAppConfig extends EggScheduleAppConfig {} +} diff --git a/test/fixtures/customType/agent.js b/test/fixtures/customType/agent.js index 73e8604..49a5025 100644 --- a/test/fixtures/customType/agent.js +++ b/test/fixtures/customType/agent.js @@ -1,12 +1,12 @@ -'use strict'; - -module.exports = function(agent) { - class ClusterStrategy extends agent.ScheduleStrategy { - start() { - this.interval = setInterval(() => { - this.sendOne(); - }, this.schedule.interval); +module.exports = class Boot { + constructor(agent) { + class ClusterStrategy extends agent.ScheduleStrategy { + start() { + this.interval = setInterval(() => { + this.sendOne(); + }, this.schedule.interval); + } } + agent.schedule.use('cluster', ClusterStrategy); } - agent.schedule.use('cluster', ClusterStrategy); -}; +} diff --git a/test/fixtures/customTypeParams/app/schedule/cluster-all-clz.js b/test/fixtures/customTypeParams/app/schedule/cluster-all-clz.js index e1a4ee1..86a41fe 100644 --- a/test/fixtures/customTypeParams/app/schedule/cluster-all-clz.js +++ b/test/fixtures/customTypeParams/app/schedule/cluster-all-clz.js @@ -10,7 +10,7 @@ class Interval extends Subscription { }; } - * subscribe(data) { + async subscribe(data) { this.ctx.logger.info('cluster_all_log_clz', data); } } diff --git a/test/fixtures/customTypeParams/app/schedule/cluster-clz.js b/test/fixtures/customTypeParams/app/schedule/cluster-clz.js index 78a61a4..2d27b41 100644 --- a/test/fixtures/customTypeParams/app/schedule/cluster-clz.js +++ b/test/fixtures/customTypeParams/app/schedule/cluster-clz.js @@ -10,7 +10,7 @@ class Interval extends Subscription { }; } - * subscribe(data) { + async subscribe(data) { this.ctx.logger.info('cluster_log_clz', data); } } diff --git a/test/fixtures/customTypeWithoutStart/agent.js b/test/fixtures/customTypeWithoutStart/agent.js index 316b287..ce87a08 100644 --- a/test/fixtures/customTypeWithoutStart/agent.js +++ b/test/fixtures/customTypeWithoutStart/agent.js @@ -1,5 +1,3 @@ -'use strict'; - module.exports = function(agent) { class ClusterStrategy extends agent.ScheduleStrategy { constructor(...args) { diff --git a/test/fixtures/demo/config/config.default.ts b/test/fixtures/demo/config/config.default.ts new file mode 100644 index 0000000..3508e28 --- /dev/null +++ b/test/fixtures/demo/config/config.default.ts @@ -0,0 +1,11 @@ +import '../../../../src/index.js'; + +import { EggAppConfig } from 'egg'; + +export default { + schedule: { + directory: [ + 'path/to/otherSchedule', + ], + }, +} as Partial; diff --git a/test/fixtures/demo/package.json b/test/fixtures/demo/package.json new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/executeError-task-generator/app/schedule/interval.js b/test/fixtures/executeError-task-generator/app/schedule/interval.js new file mode 100644 index 0000000..e45ecf5 --- /dev/null +++ b/test/fixtures/executeError-task-generator/app/schedule/interval.js @@ -0,0 +1,10 @@ +'use strict'; + +exports.schedule = { + type: 'worker', + interval: 2000, +}; + +exports.task = function* () { + throw new Error('interval error'); +}; diff --git a/test/fixtures/executeError-task-generator/package.json b/test/fixtures/executeError-task-generator/package.json new file mode 100644 index 0000000..55cef39 --- /dev/null +++ b/test/fixtures/executeError-task-generator/package.json @@ -0,0 +1,3 @@ +{ + "name": "executeError" +} diff --git a/test/fixtures/executeError/app/schedule/interval.js b/test/fixtures/executeError/app/schedule/interval.js index e45ecf5..1fd89db 100644 --- a/test/fixtures/executeError/app/schedule/interval.js +++ b/test/fixtures/executeError/app/schedule/interval.js @@ -1,10 +1,8 @@ -'use strict'; - exports.schedule = { type: 'worker', interval: 2000, }; -exports.task = function* () { +exports.task = async function() { throw new Error('interval error'); }; diff --git a/test/fixtures/generator/app/schedule/sub/cron.js b/test/fixtures/generator/app/schedule/sub/cron.js deleted file mode 100644 index a813701..0000000 --- a/test/fixtures/generator/app/schedule/sub/cron.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -exports.schedule = { - type: 'worker', - cron: '*/5 * * * * *', -}; - -exports.task = function* (ctx) { - ctx.logger.info(`method: ${ctx.method}, path: ${ctx.path}, query: ${JSON.stringify(ctx.query)}`); - const msg = yield ctx.service.user.hello('busi'); - ctx.logger.info(msg); -}; diff --git a/test/fixtures/generator/app/service/user.js b/test/fixtures/generator/app/service/user.js deleted file mode 100644 index a516816..0000000 --- a/test/fixtures/generator/app/service/user.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -const Service = require('egg').Service; - -class UserService extends Service { - * hello(name) { - return `hello ${name}`; - } -} - -module.exports = UserService; diff --git a/test/fixtures/generator/package.json b/test/fixtures/generator/package.json deleted file mode 100644 index 623456b..0000000 --- a/test/fixtures/generator/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "generator" -} diff --git a/test/fixtures/plugin/config/config.default.js b/test/fixtures/plugin/config/config.default.js new file mode 100644 index 0000000..fbd44f6 --- /dev/null +++ b/test/fixtures/plugin/config/config.default.js @@ -0,0 +1,3 @@ +exports.logger = { + level: 'debug', +}; diff --git a/test/fixtures/scheduleError/app/schedule/interval.js b/test/fixtures/scheduleError/app/schedule/interval.js index 49971e4..dbff82e 100644 --- a/test/fixtures/scheduleError/app/schedule/interval.js +++ b/test/fixtures/scheduleError/app/schedule/interval.js @@ -1,5 +1,3 @@ -'use strict'; - exports.schedule = { type: 'worker', }; diff --git a/test/fixtures/scheduleError/app/schedule/sub/cron.js b/test/fixtures/scheduleError/app/schedule/sub/cron.js index 442e7e5..de3d686 100644 --- a/test/fixtures/scheduleError/app/schedule/sub/cron.js +++ b/test/fixtures/scheduleError/app/schedule/sub/cron.js @@ -1,5 +1,3 @@ -'use strict'; - exports.schedule = { type: 'worker', cron: '*/5 * * * * *', diff --git a/test/fixtures/subscription-enableFastContextLogger/app/schedule/interval.js b/test/fixtures/subscription-enableFastContextLogger/app/schedule/interval.js index 665593a..52c0641 100644 --- a/test/fixtures/subscription-enableFastContextLogger/app/schedule/interval.js +++ b/test/fixtures/subscription-enableFastContextLogger/app/schedule/interval.js @@ -8,7 +8,7 @@ class Interval extends Subscription { }; } - * subscribe() { + async subscribe() { this.app.logger.info('interval'); } } diff --git a/test/fixtures/subscription-enableFastContextLogger/app/schedule/sub/cron.js b/test/fixtures/subscription-enableFastContextLogger/app/schedule/sub/cron.js index 4bc9e80..cc3c5c2 100644 --- a/test/fixtures/subscription-enableFastContextLogger/app/schedule/sub/cron.js +++ b/test/fixtures/subscription-enableFastContextLogger/app/schedule/sub/cron.js @@ -8,7 +8,7 @@ class Interval extends Subscription { }; } - * subscribe() { + async subscribe() { this.app.logger.info('cron'); } } diff --git a/test/fixtures/subscription-generator/app/schedule/interval.js b/test/fixtures/subscription-generator/app/schedule/interval.js new file mode 100644 index 0000000..7bfec26 --- /dev/null +++ b/test/fixtures/subscription-generator/app/schedule/interval.js @@ -0,0 +1,16 @@ +const Subscription = require('egg').Subscription; + +class Interval extends Subscription { + static get schedule() { + return { + type: 'worker', + interval: 4000, + }; + } + + * subscribe() { + this.ctx.logger.info('interval'); + } +} + +module.exports = Interval; diff --git a/test/fixtures/subscription-generator/app/schedule/sub/cron.js b/test/fixtures/subscription-generator/app/schedule/sub/cron.js new file mode 100644 index 0000000..b8fa02b --- /dev/null +++ b/test/fixtures/subscription-generator/app/schedule/sub/cron.js @@ -0,0 +1,16 @@ +const Subscription = require('egg').Subscription; + +class Interval extends Subscription { + static get schedule() { + return { + type: 'worker', + cron: '*/5 * * * * *', + }; + } + + async subscribe() { + this.ctx.logger.info('cron'); + } +} + +module.exports = Interval; diff --git a/test/fixtures/subscription-generator/config/plugin.js b/test/fixtures/subscription-generator/config/plugin.js new file mode 100644 index 0000000..337392c --- /dev/null +++ b/test/fixtures/subscription-generator/config/plugin.js @@ -0,0 +1 @@ +exports.logrotator = true; diff --git a/test/fixtures/subscription-generator/package.json b/test/fixtures/subscription-generator/package.json new file mode 100644 index 0000000..c502936 --- /dev/null +++ b/test/fixtures/subscription-generator/package.json @@ -0,0 +1,3 @@ +{ + "name": "subscription" +} diff --git a/test/fixtures/subscription/app/schedule/interval.js b/test/fixtures/subscription/app/schedule/interval.js index ff9758d..a8423e3 100644 --- a/test/fixtures/subscription/app/schedule/interval.js +++ b/test/fixtures/subscription/app/schedule/interval.js @@ -1,5 +1,3 @@ -'use strict'; - const Subscription = require('egg').Subscription; class Interval extends Subscription { @@ -10,7 +8,7 @@ class Interval extends Subscription { }; } - * subscribe() { + async subscribe() { this.ctx.logger.info('interval'); } } diff --git a/test/fixtures/subscription/app/schedule/sub/cron.js b/test/fixtures/subscription/app/schedule/sub/cron.js index 587492d..b8fa02b 100644 --- a/test/fixtures/subscription/app/schedule/sub/cron.js +++ b/test/fixtures/subscription/app/schedule/sub/cron.js @@ -1,5 +1,3 @@ -'use strict'; - const Subscription = require('egg').Subscription; class Interval extends Subscription { @@ -10,7 +8,7 @@ class Interval extends Subscription { }; } - * subscribe() { + async subscribe() { this.ctx.logger.info('cron'); } } diff --git a/test/fixtures/subscription/config/plugin.js b/test/fixtures/subscription/config/plugin.js index e54d182..337392c 100644 --- a/test/fixtures/subscription/config/plugin.js +++ b/test/fixtures/subscription/config/plugin.js @@ -1,3 +1 @@ -'use strict'; - exports.logrotator = true; diff --git a/test/fixtures/symlink/package.json b/test/fixtures/symlink/package.json new file mode 100644 index 0000000..55e9a32 --- /dev/null +++ b/test/fixtures/symlink/package.json @@ -0,0 +1,4 @@ +{ + "name": "symlink", + "type": "module" +} diff --git a/test/fixtures/symlink/realFile.js b/test/fixtures/symlink/realFile.js index 2157971..b48137c 100644 --- a/test/fixtures/symlink/realFile.js +++ b/test/fixtures/symlink/realFile.js @@ -1,10 +1,8 @@ -'use strict'; - -exports.schedule = { +export const schedule = { type: 'worker', interval: '4s', }; -exports.task = async function (ctx) { +export async function task(ctx) { ctx.logger.info('interval'); -}; +} diff --git a/test/fixtures/symlink/runDir/package.json b/test/fixtures/symlink/runDir/package.json index 030f5f6..55e9a32 100644 --- a/test/fixtures/symlink/runDir/package.json +++ b/test/fixtures/symlink/runDir/package.json @@ -1,3 +1,4 @@ { - "name": "symlink" + "name": "symlink", + "type": "module" } diff --git a/test/fixtures/symlink/tsRealFile.ts b/test/fixtures/symlink/tsRealFile.ts index 29efc16..b48137c 100644 --- a/test/fixtures/symlink/tsRealFile.ts +++ b/test/fixtures/symlink/tsRealFile.ts @@ -1,8 +1,8 @@ -exports.schedule = { +export const schedule = { type: 'worker', interval: '4s', }; -exports.task = async function (ctx) { +export async function task(ctx) { ctx.logger.info('interval'); -}; +} diff --git a/test/fixtures/worker/app/schedule/interval.js b/test/fixtures/worker/app/schedule/interval.js index 2157971..29efc16 100644 --- a/test/fixtures/worker/app/schedule/interval.js +++ b/test/fixtures/worker/app/schedule/interval.js @@ -1,5 +1,3 @@ -'use strict'; - exports.schedule = { type: 'worker', interval: '4s', diff --git a/test/fixtures/worker/app/schedule/sub/cron.js b/test/fixtures/worker/app/schedule/sub/cron.js index 282d36b..6b3df7b 100644 --- a/test/fixtures/worker/app/schedule/sub/cron.js +++ b/test/fixtures/worker/app/schedule/sub/cron.js @@ -1,5 +1,3 @@ -'use strict'; - exports.schedule = { type: 'worker', cron: '*/5 * * * * *', diff --git a/test/fixtures/worker/config/config.default.js b/test/fixtures/worker/config/config.default.js new file mode 100644 index 0000000..8b3e0dd --- /dev/null +++ b/test/fixtures/worker/config/config.default.js @@ -0,0 +1,8 @@ +exports.logger = { + level: 'DEBUG', + consoleLevel: 'DEBUG', + coreLogger: { + level: 'DEBUG', + consoleLevel: 'DEBUG', + }, +}; diff --git a/test/fixtures/worker/config/plugin.js b/test/fixtures/worker/config/plugin.js index e54d182..08a0b9c 100644 --- a/test/fixtures/worker/config/plugin.js +++ b/test/fixtures/worker/config/plugin.js @@ -1,3 +1,11 @@ -'use strict'; - exports.logrotator = true; +exports.onerror = false; +exports.session = false; +exports.i18n = false; +exports.watcher = false; +exports.multipart = false; +exports.security = false; +exports.development = false; +exports.static = false; +exports.jsonp = false; +exports.view = false; diff --git a/test/schedule.test.js b/test/schedule.test.ts similarity index 78% rename from test/schedule.test.js rename to test/schedule.test.ts index 865ad0b..9c735c1 100644 --- a/test/schedule.test.js +++ b/test/schedule.test.ts @@ -1,35 +1,35 @@ -const mm = require('egg-mock'); -const path = require('path'); -const fs = require('fs'); -const assert = require('assert'); -const is = require('is-type-of'); - -function sleep(ms) { - return new Promise(resolve => { - setTimeout(resolve, ms); - }); -} - -describe('test/schedule.test.js', () => { - let app; +import { strict as assert } from 'node:assert'; +import path from 'node:path'; +import fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { setTimeout as sleep } from 'node:timers/promises'; +import { importResolve } from '@eggjs/utils'; +import { mm, MockApplication } from 'egg-mock'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +describe('test/schedule.test.ts', () => { + let app: MockApplication; afterEach(() => app.close()); describe('schedule type worker', () => { it('should support interval and cron', async () => { app = mm.cluster({ baseDir: 'worker', workers: 2, cache: false }); - // app.debug(); + app.debug(); await app.ready(); await sleep(5000); const log = getLogContent('worker'); - // console.log(log); - assert(contains(log, 'interval') === 1); - assert(contains(log, 'cron') === 1); + console.log(log); + assert.equal(contains(log, 'interval'), 1); + assert.equal(contains(log, 'cron'), 1); const scheduleLog = getScheduleLogContent('worker'); - assert(contains(scheduleLog, 'cron.js executing by app') === 1); - assert(contains(scheduleLog, 'cron.js execute succeed') === 1); - assert(contains(scheduleLog, 'interval.js executing by app') === 1); - assert(contains(scheduleLog, 'interval.js execute succeed') === 1); + console.log(scheduleLog); + assert.equal(contains(scheduleLog, 'cron.js executing by app'), 1); + assert.equal(contains(scheduleLog, 'cron.js execute succeed'), 1); + assert.equal(contains(scheduleLog, 'interval.js executing by app'), 1); + assert.equal(contains(scheduleLog, 'interval.js execute succeed'), 1); }); it('should support ctxStorage', async () => { @@ -90,20 +90,6 @@ describe('test/schedule.test.js', () => { assert(/hello busi/.test(log)); }); - it('should support generator', async () => { - app = mm.cluster({ baseDir: 'generator', workers: 2 }); - await app.ready(); - await sleep(5000); - const log = getLogContent('generator'); - // console.log(log); - assert(/method: SCHEDULE/.test(log)); - assert(/path: \/__schedule/.test(log)); - assert(/(.*?)sub(\/|\\)cron\.js/.test(log)); - assert(/"type":"worker"/.test(log)); - assert(/"cron":"\*\/5 \* \* \* \* \*"/.test(log)); - assert(/hello busi/.test(log)); - }); - it('should support immediate', async () => { app = mm.cluster({ baseDir: 'immediate', workers: 2 }); await app.ready(); @@ -180,7 +166,7 @@ describe('test/schedule.test.js', () => { await sleep(5000); const log = getLogContent('customTypeWithoutStart'); // console.log(log); - assert(contains(log, 'cluster_log') === 1); + assert.equal(contains(log, 'cluster_log'), 1); }); it('should handler error', async () => { @@ -198,8 +184,8 @@ describe('test/schedule.test.js', () => { app = mm.cluster({ baseDir: 'scheduleError', workers: 2 }); // app.debug(); await app.ready(); - await sleep(3000); - app.expect('stderr', /schedule\.interval or schedule\.cron or schedule\.immediate must be present/); + await sleep(5000); + app.expect('stderr', /`schedule\.interval` or `schedule\.cron` or `schedule\.immediate` must be present/); }); }); @@ -207,7 +193,7 @@ describe('test/schedule.test.js', () => { it('should thrown', async () => { app = mm.cluster({ baseDir: 'typeUndefined', workers: 2 }); await app.ready(); - await sleep(3000); + await sleep(5000); app.expect('stderr', /schedule type \[undefined\] is not defined/); }); }); @@ -235,11 +221,20 @@ describe('test/schedule.test.js', () => { describe('schedule execute error', () => { it('should thrown', async () => { app = mm.cluster({ baseDir: 'executeError', workers: 1 }); - // app.debug(); + app.debug(); await app.ready(); await sleep(5000); const scheduleLog = getScheduleLogContent('executeError'); - assert(contains(scheduleLog, 'interval.js execute failed') === 2); + assert.equal(contains(scheduleLog, 'interval.js execute failed'), 2); + }); + }); + + describe('schedule execute task is generator function', () => { + it('should thrown', async () => { + app = mm.cluster({ baseDir: 'executeError-task-generator', workers: 1 }); + app.debug(); + await app.ready(); + app.expect('stderr', /"task" generator function is not support, should use async function instead/); }); }); @@ -260,13 +255,13 @@ describe('test/schedule.test.js', () => { interval: 4000, }, }; - app.agent.schedule.registerSchedule(schedule); - app.scheduleWorker.registerSchedule(schedule); + (app as any).agent.schedule.registerSchedule(schedule); + app.scheduleWorker.registerSchedule(schedule as any); await app.runSchedule(key); await sleep(1000); - assert(scheduleCalled === true); + assert.equal(scheduleCalled, true); }); it('should unregister succeed', async () => { @@ -285,20 +280,20 @@ describe('test/schedule.test.js', () => { interval: 4000, }, }; - app.agent.schedule.registerSchedule(schedule); - app.scheduleWorker.registerSchedule(schedule); + (app as any).agent.schedule.registerSchedule(schedule); + app.scheduleWorker.registerSchedule(schedule as any); - app.agent.schedule.unregisterSchedule(schedule.key); + (app as any).agent.schedule.unregisterSchedule(schedule.key); app.scheduleWorker.unregisterSchedule(schedule.key); - let err; + let err: any; try { await app.runSchedule(key); } catch (e) { err = e; } - assert(err.message.includes('Cannot find schedule')); - assert(scheduleCalled === false); + assert.match(err.message, /Cannot find schedule/); + assert.equal(scheduleCalled, false); }); }); @@ -310,7 +305,7 @@ describe('test/schedule.test.js', () => { await app.runSchedule(__filename); await sleep(1000); throw new Error('should not execute'); - } catch (err) { + } catch (err: any) { assert(err.message.includes('Cannot find schedule')); } }); @@ -339,7 +334,7 @@ describe('test/schedule.test.js', () => { it('should run schedule by absolute package path success', async () => { app = mm.app({ baseDir: 'worker', cache: false }); await app.ready(); - await app.runSchedule(require.resolve('../node_modules/egg-logrotator/app/schedule/rotate_by_file.js')); + await app.runSchedule(importResolve('egg-logrotator/app/schedule/rotate_by_file.js')); }); it('should run schedule by relative path success at customDirectory', async () => { @@ -371,47 +366,55 @@ describe('test/schedule.test.js', () => { }, }); await app.runSchedule('sub/foobar', 'use app.logger.info should work'); - await sleep(1000); + await sleep(5000); const log = getLogContent('worker2'); // console.log(log); - assert.match(log, / \[-\/127.0.0.1\/mock-trace-123\/\d+ms GET \/] foobar use app.logger.info should work/); + assert.match(log, / \[-\/127.0.0.1\/mock-trace-123\/[\d\.]+ms GET \/] foobar use app.logger.info should work/); }); it('should run schedule with symlink js file success', async () => { + if (!process.version.startsWith('v22.')) { + // only work on Node.js >= v22 + return; + } const realPath = path.join(__dirname, 'fixtures/symlink/realFile.js'); const targetPath = path.join(__dirname, 'fixtures/symlink/runDir/app/schedule/realFile.js'); - fs.symlinkSync(realPath, targetPath); - - app = mm.app({ baseDir: 'symlink/runDir', cache: false }); - await app.ready(); try { - await app.runSchedule('realFile'); - } catch (err) { - assert(false, 'should not throw Cannot find schedule error'); + fs.unlinkSync(targetPath); + } catch { + // ignore + } + try { + fs.symlinkSync(realPath, targetPath); + } catch { + // ignore } + app = mm.app({ baseDir: 'symlink/runDir', cache: false }); + await app.ready(); + await app.runSchedule('realFile'); fs.unlinkSync(targetPath); }); - it('should run schedule with symlink ts file success', async () => { - mm(process.env, 'EGG_TYPESCRIPT', 'true'); - require.extensions['.ts'] = require.extensions['.js']; + // it.skip('should run schedule with symlink ts file success', async () => { + // mm(process.env, 'EGG_TYPESCRIPT', 'true'); + // require.extensions['.ts'] = require.extensions['.js']; - const realPath = path.join(__dirname, 'fixtures/symlink/tsRealFile.ts'); - const targetPath = path.join(__dirname, 'fixtures/symlink/runDir/app/schedule/tsRealFile.ts'); - fs.symlinkSync(realPath, targetPath); + // const realPath = path.join(__dirname, 'fixtures/symlink/tsRealFile.ts'); + // const targetPath = path.join(__dirname, 'fixtures/symlink/runDir/app/schedule/tsRealFile.ts'); + // fs.symlinkSync(realPath, targetPath); - app = mm.app({ baseDir: 'symlink/runDir', cache: false }); - await app.ready(); - try { - await app.runSchedule('tsRealFile'); - } catch (err) { - assert(false, 'should not throw Cannot find schedule error'); - } + // app = mm.app({ baseDir: 'symlink/runDir', cache: false }); + // await app.ready(); + // try { + // await app.runSchedule('tsRealFile'); + // } catch (err) { + // assert(false, 'should not throw Cannot find schedule error'); + // } - delete require.extensions['.ts']; - fs.unlinkSync(targetPath); - }); + // delete require.extensions['.ts']; + // fs.unlinkSync(targetPath); + // }); }); describe('stop schedule', () => { @@ -449,7 +452,7 @@ describe('test/schedule.test.js', () => { }); }); - describe('export schedules', () => { + describe('export app.schedules', () => { it('should export app.schedules', async () => { app = mm.app({ baseDir: 'worker', cache: false }); await app.ready(); @@ -478,26 +481,34 @@ describe('test/schedule.test.js', () => { describe('Subscription', () => { it('should support interval and cron', async () => { app = mm.cluster({ baseDir: 'subscription', workers: 2, cache: false }); - // app.debug(); + app.debug(); await app.ready(); await sleep(5000); const log = getLogContent('subscription'); // console.log(log); - assert(contains(log, 'interval') === 1); - assert(contains(log, 'cron') === 1); + assert.equal(contains(log, 'interval'), 1); + assert.equal(contains(log, 'cron'), 1); + }); + + it('should throw error on generator function', async () => { + app = mm.cluster({ baseDir: 'subscription-generator', workers: 2, cache: false }); + app.debug(); + await app.ready(); + await sleep(3000); + app.expect('stderr', /"schedule" generator function is not support, should use async function instead/); }); it('should support interval and cron when config.logger.enableFastContextLogger = true', async () => { app = mm.cluster({ baseDir: 'subscription-enableFastContextLogger', workers: 2, cache: false }); - // app.debug(); + app.debug(); await app.ready(); await sleep(5000); const log = getLogContent('subscription-enableFastContextLogger'); - // console.log(log); - assert(contains(log, 'interval') === 1); - assert(contains(log, 'cron') === 1); + console.log(log); + assert.equal(contains(log, 'interval'), 1); + assert.equal(contains(log, 'cron'), 1); // 2022-12-11 16:44:55,009 INFO 22958 [-/127.0.0.1/15d62420-7930-11ed-86ce-31ec9c2e0d18/3ms SCHEDULE /__schedule - assert.match(log, / INFO \w+ \[-\/127\.0\.0\.1\/\w+\-\w+\-\w+\-\w+\-\w+\/\d+ms SCHEDULE \/__schedule/); + assert.match(log, / INFO \w+ \[-\/127\.0\.0\.1\/\w+\-\w+\-\w+\-\w+\-\w+\/[\d\.]+ms SCHEDULE \/__schedule/); }); }); @@ -555,46 +566,40 @@ describe('test/schedule.test.js', () => { describe('detect error', () => { it('should works', async () => { app = mm.cluster({ baseDir: 'detect-error', workers: 1, cache: false }); - app.debug(); + // app.debug(); await app.ready(); await sleep(2000); const scheduleLog = getScheduleLogContent('detect-error'); - assert(contains(scheduleLog, 'suc.js execute succeed') === 1); - assert(contains(scheduleLog, /fail.js execute failed, used \d+ms. Error: fail/) === 1); - assert(contains(scheduleLog, /error.js execute failed, used \d+ms. Error: some err/) === 1); + assert.equal(contains(scheduleLog, 'suc.js execute succeed'), 1); + assert.equal(contains(scheduleLog, /fail.js execute failed, used [\d\.]+ms. fail/), 1); + assert.equal(contains(scheduleLog, /error.js execute failed, used [\d\.]+ms. Error: some err/), 1); }); }); }); -function getCoreLogContent(name) { +function getCoreLogContent(name: string) { const logPath = path.join(__dirname, 'fixtures', name, 'logs', name, 'egg-web.log'); return fs.readFileSync(logPath, 'utf8'); } -function getLogContent(name) { +function getLogContent(name: string) { const logPath = path.join(__dirname, 'fixtures', name, 'logs', name, `${name}-web.log`); return fs.readFileSync(logPath, 'utf8'); } -/* eslint-disable-next-line no-unused-vars */ -function getErrorLogContent(name) { - const logPath = path.join(__dirname, 'fixtures', name, 'logs', name, 'common-error.log'); - return fs.readFileSync(logPath, 'utf8'); -} - -function getAgentLogContent(name) { +function getAgentLogContent(name: string) { const logPath = path.join(__dirname, 'fixtures', name, 'logs', name, 'egg-agent.log'); return fs.readFileSync(logPath, 'utf8'); } -function getScheduleLogContent(name) { +function getScheduleLogContent(name: string) { const logPath = path.join(__dirname, 'fixtures', name, 'logs', name, 'egg-schedule.log'); return fs.readFileSync(logPath, 'utf8'); } -function contains(content, match) { +function contains(content: string, match: string | RegExp) { return content.split('\n').filter(line => { - return is.regexp(match) ? match.test(line) : line.indexOf(match) >= 0; + return match instanceof RegExp ? match.test(line) : line.indexOf(match) >= 0; }).length; } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ff41b73 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@eggjs/tsconfig", + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext" + } +}