From cde67730c06f6b614c30853f6ddaf936b786ecbf Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Wed, 18 Dec 2024 15:08:25 +0800 Subject: [PATCH] feat: support cjs and esm both by tshy (#23) BREAKING CHANGE: drop Node.js < 18.19.0 support part of https://github.com/eggjs/egg/issues/3644 https://github.com/eggjs/egg/issues/5257 --- .eslintrc | 7 +- .github/workflows/codeql.yml | 74 ----- .github/workflows/nodejs.yml | 10 +- .github/workflows/pkg.pr.new.yml | 23 ++ .github/workflows/release.yml | 8 +- .gitignore | 9 +- CHANGELOG.md | 114 +++++++ History.md | 111 ------- README.md | 52 ++- index.d.ts | 29 -- index.js | 251 --------------- index.test-d.ts | 31 -- package.json | 85 +++-- src/index.ts | 117 +++++++ test/fixtures/ts/index.ts | 5 +- test/{index.test.js => index.test.ts} | 447 ++++++++++++-------------- tsconfig.json | 10 + 17 files changed, 565 insertions(+), 818 deletions(-) delete mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/pkg.pr.new.yml delete mode 100644 History.md delete mode 100644 index.d.ts delete mode 100644 index.js delete mode 100644 index.test-d.ts create mode 100644 src/index.ts rename test/{index.test.js => index.test.ts} (56%) create mode 100644 tsconfig.json diff --git a/.eslintrc b/.eslintrc index 590a45c..9bcdb46 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,3 +1,6 @@ { - "extends": "eslint-config-egg" -} \ No newline at end of file + "extends": [ + "eslint-config-egg/typescript", + "eslint-config-egg/lib/rules/enforce-node-prefix" + ] +} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index bef1e29..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,74 +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: '15 7 * * 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#, Go, 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 - with: - category: "/language:${{matrix.language}}" diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 22a8662..22c1cce 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -3,16 +3,14 @@ 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' + version: '18.19.0, 18, 20, 22' + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml new file mode 100644 index 0000000..bac3fac --- /dev/null +++ b/.github/workflows/pkg.pr.new.yml @@ -0,0 +1,23 @@ +name: Publish Any Commit +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - run: corepack enable + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm install + + - name: Build + run: npm run prepublishOnly --if-present + + - run: npx pkg-pr-new publish diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c6cc6c8..1c6cbb1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,19 +1,13 @@ name: Release on: - # ๅˆๅนถๅŽ่‡ชๅŠจๅ‘ๅธƒ push: branches: [ master ] - # ๆ‰‹ๅŠจๅ‘ๅธƒ - workflow_dispatch: {} - jobs: release: name: Node.js - uses: artusjs/github-actions/.github/workflows/node-release.yml@v1 + uses: node-modules/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 c9a2ca9..8156005 100644 --- a/.gitignore +++ b/.gitignore @@ -6,13 +6,14 @@ *.pid *.gz -coverage.html -coverage/ -cov/ - node_modules dump.rdb .DS_Store test/fixtures/**/*.js +.tshy* +.eslintcache +dist +coverage +package-lock.json diff --git a/CHANGELOG.md b/CHANGELOG.md index daf0ce4..d2706aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,3 +6,117 @@ ### Bug Fixes * auto release on action ([#22](https://github.com/node-modules/sdk-base/issues/22)) ([e74df48](https://github.com/node-modules/sdk-base/commit/e74df4885a74fa99e935323a858e7f6c4447cc97)) + +--- + + +4.2.0 / 2022-12-09 +================== + +**features** + * [[`71d7ddd`](http://github.com/node-modules/sdk-base/commit/71d7ddd0c98f0c3c6ead65e1741ed5c54bd0eb38)] - ๐Ÿ“ฆ NEW: Support localStorage getter (#21) (fengmk2 <>) + +4.1.0 / 2022-12-03 +================== + +**features** + * [[`6e8a1c4`](http://github.com/node-modules/sdk-base/commit/6e8a1c4707908b28cc30a6019f164544c9033bb7)] - ๐Ÿ“ฆ NEW: Support ready or timeout detect (#20) (fengmk2 <>) + +4.0.0 / 2022-12-03 +================== + +**features** + * [[`567a380`](http://github.com/node-modules/sdk-base/commit/567a3806e348549f40fedf3438054b53f540107e)] - ๐Ÿ‘Œ IMPROVE: [BREAKING] Drop Node.js < 14 support (#19) (fengmk2 <>) + * [[`07d55e8`](http://github.com/node-modules/sdk-base/commit/07d55e8596ced9ecaea837a3ff8a56e87a333da8)] - feat: optimize performance (#18) (brizer <<362512489@qq.com>>) + +**others** + * [[`e9bf6e9`](http://github.com/node-modules/sdk-base/commit/e9bf6e9e66570ac7c5e9537c22855573275d6618)] - refactor: enhance require profermance (#16) (zลng yว” <>) + * [[`bbea174`](http://github.com/node-modules/sdk-base/commit/bbea174cebde7af79afdff50cd01eec3b5481fad)] - Create codeql.yml (fengmk2 <>) + +3.6.0 / 2019-04-24 +================== + +**features** + * [[`39c0f1d`](http://github.com/node-modules/sdk-base/commit/39c0f1d946bd7da1e393d42cca2f5e1bc22eb785)] - feat: implement close function (#17) (killa <>) + +3.5.1 / 2018-09-27 +================== + +**fixes** + * [[`de262c1`](http://github.com/node-modules/sdk-base/commit/de262c1e41e65a5fb11e95a95f96c6c561cb9d23)] - fix(ts): support es module export (#15) (Haoliang Gao <>) + +3.5.0 / 2018-07-26 +================== + +**features** + * [[`dcce360`](http://github.com/node-modules/sdk-base/commit/dcce360d5da6a3f0516c2329c1902c49221ffd29)] - feat: add typescript definition file (#14) (Angela <>) + +**others** + * [[`f975763`](http://github.com/node-modules/sdk-base/commit/f975763047a461fc8d0758f08dd52e16078f5bc9)] - chore: release 3.4.0 (xiaochen.gaoxc <>), + +3.4.0 / 2017-11-24 +================== + +**features** + * [[`98207ba`]](https://github.com/node-modules/sdk-base/pull/11/commits/98207ba521487df39f7c9b116aaf7163bb6b9ad8) - feat: add awaitFirst api (#11) (gxcsoccer <>) + +3.3.0 / 2017-09-17 +================== + +**features** + * [[`8d5c04a`](http://github.com/node-modules/sdk-base/commit/8d5c04aa3b0fee135dcf972b447aba0f79f56417)] - feat: add isReady getter (#10) (fengmk2 <>) + +**others** + * [[`6ec435f`](http://github.com/node-modules/sdk-base/commit/6ec435f676395726ff64646518b55c7c8ff4bc45)] - chore: fix initMethod document description (fengmk2 <>) + +3.2.0 / 2017-06-26 +================== + + * feat: let options.initMethod support functions that return promise (#9) + +3.1.1 / 2017-03-14 +================== + + * fix: avoid duplicate error handler (#8) + +3.1.0 / 2017-02-17 +================== + + * feat: support client.await (#7) + +3.0.1 / 2017-01-12 +================== + + * fix: initMethod should be lazy executed (#6) + +3.0.0 / 2017-01-12 +================== + + * feat: [BREAKING_CHANGE] add ready with error and generator listener (#5) + +2.0.1 / 2016-03-11 +================== + + * fix: use event.listeners + +2.0.0 / 2016-03-11 +================== + + * refactor: listen on error synchronous + +1.1.0 / 2015-11-14 +================== + + * refactor: drop 0.8 support + * feat: support ready(flagOrFunction) + +1.0.1 / 2014-11-06 +================== + + * remove .npmignore + * add __filename, always show construct name + * more pretty + * refine error display + * refactor(error): improve default error handler + * fix travis + * fix link diff --git a/History.md b/History.md deleted file mode 100644 index 7f2b625..0000000 --- a/History.md +++ /dev/null @@ -1,111 +0,0 @@ - -4.2.0 / 2022-12-09 -================== - -**features** - * [[`71d7ddd`](http://github.com/node-modules/sdk-base/commit/71d7ddd0c98f0c3c6ead65e1741ed5c54bd0eb38)] - ๐Ÿ“ฆ NEW: Support localStorage getter (#21) (fengmk2 <>) - -4.1.0 / 2022-12-03 -================== - -**features** - * [[`6e8a1c4`](http://github.com/node-modules/sdk-base/commit/6e8a1c4707908b28cc30a6019f164544c9033bb7)] - ๐Ÿ“ฆ NEW: Support ready or timeout detect (#20) (fengmk2 <>) - -4.0.0 / 2022-12-03 -================== - -**features** - * [[`567a380`](http://github.com/node-modules/sdk-base/commit/567a3806e348549f40fedf3438054b53f540107e)] - ๐Ÿ‘Œ IMPROVE: [BREAKING] Drop Node.js < 14 support (#19) (fengmk2 <>) - * [[`07d55e8`](http://github.com/node-modules/sdk-base/commit/07d55e8596ced9ecaea837a3ff8a56e87a333da8)] - feat: optimize performance (#18) (brizer <<362512489@qq.com>>) - -**others** - * [[`e9bf6e9`](http://github.com/node-modules/sdk-base/commit/e9bf6e9e66570ac7c5e9537c22855573275d6618)] - refactor: enhance require profermance (#16) (zลng yว” <>) - * [[`bbea174`](http://github.com/node-modules/sdk-base/commit/bbea174cebde7af79afdff50cd01eec3b5481fad)] - Create codeql.yml (fengmk2 <>) - -3.6.0 / 2019-04-24 -================== - -**features** - * [[`39c0f1d`](http://github.com/node-modules/sdk-base/commit/39c0f1d946bd7da1e393d42cca2f5e1bc22eb785)] - feat: implement close function (#17) (killa <>) - -3.5.1 / 2018-09-27 -================== - -**fixes** - * [[`de262c1`](http://github.com/node-modules/sdk-base/commit/de262c1e41e65a5fb11e95a95f96c6c561cb9d23)] - fix(ts): support es module export (#15) (Haoliang Gao <>) - -3.5.0 / 2018-07-26 -================== - -**features** - * [[`dcce360`](http://github.com/node-modules/sdk-base/commit/dcce360d5da6a3f0516c2329c1902c49221ffd29)] - feat: add typescript definition file (#14) (Angela <>) - -**others** - * [[`f975763`](http://github.com/node-modules/sdk-base/commit/f975763047a461fc8d0758f08dd52e16078f5bc9)] - chore: release 3.4.0 (xiaochen.gaoxc <>), - -3.4.0 / 2017-11-24 -================== - -**features** - * [[`98207ba`]](https://github.com/node-modules/sdk-base/pull/11/commits/98207ba521487df39f7c9b116aaf7163bb6b9ad8) - feat: add awaitFirst api (#11) (gxcsoccer <>) - -3.3.0 / 2017-09-17 -================== - -**features** - * [[`8d5c04a`](http://github.com/node-modules/sdk-base/commit/8d5c04aa3b0fee135dcf972b447aba0f79f56417)] - feat: add isReady getter (#10) (fengmk2 <>) - -**others** - * [[`6ec435f`](http://github.com/node-modules/sdk-base/commit/6ec435f676395726ff64646518b55c7c8ff4bc45)] - chore: fix initMethod document description (fengmk2 <>) - -3.2.0 / 2017-06-26 -================== - - * feat: let options.initMethod support functions that return promise (#9) - -3.1.1 / 2017-03-14 -================== - - * fix: avoid duplicate error handler (#8) - -3.1.0 / 2017-02-17 -================== - - * feat: support client.await (#7) - -3.0.1 / 2017-01-12 -================== - - * fix: initMethod should be lazy executed (#6) - -3.0.0 / 2017-01-12 -================== - - * feat: [BREAKING_CHANGE] add ready with error and generator listener (#5) - -2.0.1 / 2016-03-11 -================== - - * fix: use event.listeners - -2.0.0 / 2016-03-11 -================== - - * refactor: listen on error synchronous - -1.1.0 / 2015-11-14 -================== - - * refactor: drop 0.8 support - * feat: support ready(flagOrFunction) - -1.0.1 / 2014-11-06 -================== - - * remove .npmignore - * add __filename, always show construct name - * more pretty - * refine error display - * refactor(error): improve default error handler - * fix travis - * fix link diff --git a/README.md b/README.md index 13f1164..5b349ff 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,39 @@ -sdk-base ---------------- +# sdk-base [![NPM version][npm-image]][npm-url] [![Node.js CI](https://github.com/node-modules/sdk-base/actions/workflows/nodejs.yml/badge.svg)](https://github.com/node-modules/sdk-base/actions/workflows/nodejs.yml) -[![Test coverage][coveralls-image]][coveralls-url] +[![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/sdk-base.svg?style=flat)](https://nodejs.org/en/download/) [npm-image]: https://img.shields.io/npm/v/sdk-base.svg?style=flat-square [npm-url]: https://npmjs.org/package/sdk-base -[coveralls-image]: https://img.shields.io/coveralls/node-modules/sdk-base.svg?style=flat-square -[coveralls-url]: https://coveralls.io/r/node-modules/sdk-base?branch=master +[codecov-image]: https://codecov.io/github/node-modules/sdk-base/coverage.svg?branch=master +[codecov-url]: https://codecov.io/github/node-modules/sdk-base?branch=master +[snyk-image]: https://snyk.io/test/npm/sdk-base/badge.svg?style=flat-square +[snyk-url]: https://snyk.io/test/npm/sdk-base [download-image]: https://img.shields.io/npm/dm/sdk-base.svg?style=flat-square [download-url]: https://npmjs.org/package/sdk-base - A base class for sdk with some common & useful functions. ## Installation ```bash -$ npm install sdk-base +npm install sdk-base ``` ## Usage Constructor argument: + - {Object} options - {String} [initMethod] - the async init method name, the method should be a function return promise. If set, will execute the function in the constructor. - {AsyncLocalStorage} [localStorage] - async localStorage instance. ```js - const Base = require('sdk-base'); + const { Base } = require('sdk-base'); class Client extends Base { constructor() { @@ -130,36 +133,23 @@ Constructor argument: const data = await client.await('data'); ``` -- `.awaitFirst(event)`: [await the first event in a set of event pairs](https://github.com/node-modules/await-first), return a promise, and it will clean up after itself. +- `._close()`: The `_close()` method is called by `close`. +It can be overridden by child class, but should not be called directly. It must return promise or generator. - ```js - (async function main() { - const o = await client.awaitFirst([ 'foo', 'bar' ]); - if (o.event === 'foo') { - // ... - } - if (o.event === 'bar') { - // ... - } - })(); - ``` - -- `._close()`: The `_close()` method is called by `close`, It can be overridden by child class, but should not be called directly. It must return promise or generator. +- `.close()`: The `close()` method is used to close the instance. + +## Breaking changes between v4 and v5 -- `.close()`: The `close()` method is used to close the instance. +- Drop `.awaitFirst(events)` support +- Drop generator function support +- Don't catch event listener inside error ### License [MIT](LICENSE) - - ## Contributors -|[
dead-horse](https://github.com/dead-horse)
|[
fengmk2](https://github.com/fengmk2)
|[
gxcsoccer](https://github.com/gxcsoccer)
|[
popomore](https://github.com/popomore)
|[
sang4lv](https://github.com/sang4lv)
|[
luckydrq](https://github.com/luckydrq)
| -| :---: | :---: | :---: | :---: | :---: | :---: | -[
brizer](https://github.com/brizer)
|[
killagu](https://github.com/killagu)
- -This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Sat Dec 03 2022 16:27:57 GMT+0800`. +[![Contributors](https://contrib.rocks/image?repo=node-modules/sdk-base)](https://github.com/node-modules/sdk-base/graphs/contributors) - +Made with [contributors-img](https://contrib.rocks). diff --git a/index.d.ts b/index.d.ts deleted file mode 100644 index 53ac6af..0000000 --- a/index.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* =================== USAGE =================== - import Base from "sdk-base"; - class newSDK extends Base {} - class newSDK extends Base {} -=============================================== */ - -import { EventEmitter } from 'events'; -import { AsyncLocalStorage } from 'async_hooks'; - -export interface BaseOptions { - initMethod?: string; - localStorage?: any; - [key: string]: any; -} - -export default class Base extends EventEmitter { - constructor(option?: BaseOptions); - - isReady: boolean; - options: BaseOptions; - localStorage?: AsyncLocalStorage; - await(...args: any[]): Promise; - awaitFirst(...args: any[]): Promise; - ready(): Promise; - ready(err: Error): void; - ready(ready: boolean): void; - ready(readyCallback: Function): void; - readyOrTimeout(milliseconds: number): Promise; -} diff --git a/index.js b/index.js deleted file mode 100644 index 3d11e4c..0000000 --- a/index.js +++ /dev/null @@ -1,251 +0,0 @@ -const co = require('co'); -const util = require('util'); -const assert = require('assert'); -const awaitEvent = require('await-event'); -const awaitFirst = require('await-first'); -const { EventEmitter } = require('events'); -const pTimeout = require('p-timeout'); -const CLOSE_PROMISE = Symbol('base#closePromise'); - -function isGeneratorFunction(obj) { - return obj && - obj.constructor && - obj.constructor.name === 'GeneratorFunction'; -} - -function isPromise(obj) { - return obj && - typeof obj.then === 'function'; -} - -class Base extends EventEmitter { - constructor(options) { - super(); - - if (options && options.initMethod) { - assert(typeof this[options.initMethod] === 'function', - `[sdk-base] this.${options.initMethod} should be a function.`); - - process.nextTick(() => { - if (isGeneratorFunction(this[options.initMethod])) { - this[options.initMethod] = co.wrap(this[options.initMethod]); - } - const ret = this[options.initMethod](); - assert(isPromise(ret), `[sdk-base] this.${options.initMethod} should return either a promise or a generator`); - ret.then(() => this.ready(true)) - .catch(err => this.ready(err)); - }); - } - this.options = options || {}; - this._ready = false; - this._readyError = null; - this._readyCallbacks = []; - this._closed = false; - - // support `await this.await('event')` - this.await = awaitEvent; - this.awaitFirst = awaitFirst; - - this.on('error', err => { this._defaultErrorHandler(err); }); - } - - _wrapListener(eventName, listener) { - if (isGeneratorFunction(listener)) { - assert(eventName !== 'error', '[sdk-base] `error` event should not have a generator listener.'); - - const newListener = (...args) => { - co(function* () { - yield listener(...args); - }).catch(err => { - err.name = 'EventListenerProcessError'; - this.emit('error', err); - }); - }; - newListener.original = listener; - return newListener; - } - return listener; - } - - addListener(eventName, listener) { - return super.addListener(eventName, this._wrapListener(eventName, listener)); - } - - on(eventName, listener) { - return super.on(eventName, this._wrapListener(eventName, listener)); - } - - once(eventName, listener) { - return super.once(eventName, this._wrapListener(eventName, listener)); - } - - prependListener(eventName, listener) { - return super.prependListener(eventName, this._wrapListener(eventName, listener)); - } - - prependOnceListener(eventName, listener) { - return super.prependOnceListener(eventName, this._wrapListener(eventName, listener)); - } - - removeListener(eventName, listener) { - let target = listener; - if (isGeneratorFunction(listener)) { - const listeners = this.listeners(eventName); - for (const fn of listeners) { - if (fn.original === listener) { - target = fn; - break; - } - } - } - return super.removeListener(eventName, target); - } - - /** - * detect sdk start ready or not - * @return {Boolean} ready status - */ - get isReady() { - return this._ready; - } - - /** - * get AsyncLocalStorage from options - * @return {AsyncLocalStorage} asyncLocalStorage instance or undefined - */ - get localStorage() { - return this.options.localStorage; - } - - /** - * set ready state or onready callback - * - * @param {Boolean|Error|Function} flagOrFunction - ready state or callback function - * @return {void|Promise} ready promise - */ - ready(flagOrFunction) { - if (arguments.length === 0) { - // return a promise - // support `this.ready().then(onready);` and `await this.ready()`; - return new Promise((resolve, reject) => { - if (this._ready) { - return resolve(); - } else if (this._readyError) { - return reject(this._readyError); - } - this._readyCallbacks.push(err => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - } else if (typeof flagOrFunction === 'function') { - this._readyCallbacks.push(flagOrFunction); - } else if (flagOrFunction instanceof Error) { - this._ready = false; - this._readyError = flagOrFunction; - if (!this._readyCallbacks.length) { - this.emit('error', flagOrFunction); - } - } else { - this._ready = flagOrFunction; - } - - if (this._ready || this._readyError) { - this._readyCallbacks.splice(0, this._readyCallbacks.length).forEach(callback => { - process.nextTick(() => { - callback(this._readyError); - }); - }); - } - } - - async readyOrTimeout(milliseconds) { - if (this._ready) { - return; - } else if (this._readyError) { - throw this._readyError; - } - let readyWithTimeoutCallback; - const waitForReady = new Promise((resolve, reject) => { - readyWithTimeoutCallback = err => { - if (err) { - reject(err); - } else { - resolve(); - } - }; - this._readyCallbacks.push(readyWithTimeoutCallback); - }); - try { - await pTimeout(waitForReady, milliseconds); - } catch (err) { - for (const [ index, callback ] of this._readyCallbacks.entries()) { - if (callback === readyWithTimeoutCallback) { - // remove timeout readyCallback - this._readyCallbacks.splice(index, 1); - break; - } - } - throw err; - } - } - - _defaultErrorHandler(err) { - if (this.listeners('error').length > 1) { - // ignore defaultErrorHandler - return; - } - console.error('\n[%s][pid: %s][%s] %s: %s \nError Stack:\n %s', - Date(), process.pid, this.constructor.name, err.name, - err.message, err.stack); - - // try to show addition property on the error object - // e.g.: `err.data = {url: '/foo'};` - const additions = []; - for (const key in err) { - if (key === 'name' || key === 'message') { - continue; - } - - additions.push(util.format(' %s: %j', key, err[key])); - } - if (additions.length) { - console.error('Error Additions:\n%s', additions.join('\n')); - } - console.error(); - } - - close() { - if (this._closed) { - return Promise.resolve(); - } - if (this[CLOSE_PROMISE]) { - return this[CLOSE_PROMISE]; - } - if (!this._close) { - this._closed = true; - return Promise.resolve(); - } - let closeFunc = this._close; - if (isGeneratorFunction(closeFunc)) { - closeFunc = co.wrap(closeFunc); - } - this[CLOSE_PROMISE] = closeFunc.apply(this); - assert(isPromise(this[CLOSE_PROMISE]), '[sdk-base] this._close should return either a promise or a generator'); - return this[CLOSE_PROMISE] - .then(() => { - this._closed = true; - }) - .catch(err => { - this._closed = true; - this.emit('error', err); - }); - } -} - -module.exports = Base; -// support es module -module.exports.default = Base; diff --git a/index.test-d.ts b/index.test-d.ts deleted file mode 100644 index 3ef459a..0000000 --- a/index.test-d.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { expectType } from 'tsd'; -import { AsyncLocalStorage } from 'async_hooks'; -import Base from '.'; - -class MockContenxt {} - -class Client extends Base { - constructor() { - super({ - initMethod: 'init', - localStorage: new AsyncLocalStorage, - }); - } -} - -const client = new Client(); - -expectType>(client.readyOrTimeout(1000)); -expectType>(client.ready()); -expectType(client.localStorage?.getStore()); - -class ClientDefaultContext extends Base { - constructor() { - super({ - initMethod: 'init', - }); - } -} - -const client2 = new ClientDefaultContext(); -expectType(client2.localStorage?.getStore()); diff --git a/package.json b/package.json index 5902276..1e06d5f 100644 --- a/package.json +++ b/package.json @@ -2,24 +2,10 @@ "name": "sdk-base", "version": "4.2.1", "description": "a base class for sdk with default error handler", - "main": "index.js", - "scripts": { - "lint": "eslint --ext .js .", - "tsd": "tsd", - "test": "npm run lint && npm run tsd && npm run test-local", - "test-local": "egg-bin test -r co-mocha", - "cov": "egg-bin cov -r co-mocha", - "ci": "npm run lint && npm run tsd && npm run cov", - "contributor": "git-contributor" - }, "keywords": [ "sdk", "error" ], - "files": [ - "index.js", - "index.d.ts" - ], "author": { "name": "dead_horse", "email": "dead_horse@qq.com", @@ -30,25 +16,60 @@ "url": "git@github.com:node-modules/sdk-base" }, "license": "MIT", + "engines": { + "node": ">= 18.19.0" + }, "dependencies": { - "await-event": "^2.1.0", - "await-first": "^1.0.0", - "co": "^4.6.0", - "p-timeout": "^4.1.0" + "gals": "^1.0.2", + "get-ready": "^3.4.0", + "is-type-of": "^2.2.0", + "utility": "^2.3.0" }, "devDependencies": { - "@types/node": "^14.18.34", - "co-mocha": "^1.2.2", - "egg-bin": "^5.5.0", - "eslint": "^8.29.0", - "eslint-config-egg": "^12.1.0", - "git-contributor": "^1.1.0", - "pedding": "^1.1.0", - "runscript": "^1.3.0", - "tsd": "^0.25.0", - "typescript": "^4.9.3" - }, - "engine": { - "node": ">= 14.0.0" - } + "@arethetypeswrong/cli": "^0.17.1", + "@eggjs/tsconfig": "1", + "@types/mocha": "10", + "@types/node": "22", + "egg-bin": "6", + "eslint": "8", + "eslint-config-egg": "14", + "tshy": "3", + "tshy-after": "1", + "typescript": "5" + }, + "scripts": { + "lint": "eslint --cache src test --ext .ts", + "pretest": "npm run lint -- --fix && npm run prepublishOnly", + "test": "egg-bin test -p --timeout 5000", + "preci": "npm run lint && npm run prepublishOnly && attw --pack", + "ci": "egg-bin cov -p", + "prepublishOnly": "tshy && tshy-after" + }, + "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/index.ts b/src/index.ts new file mode 100644 index 0000000..db2429f --- /dev/null +++ b/src/index.ts @@ -0,0 +1,117 @@ +import util from 'node:util'; +import assert from 'node:assert'; +import { once } from 'node:events'; +import { AsyncLocalStorage } from 'node:async_hooks'; +import { getAsyncLocalStorage } from 'gals'; +import { ReadyEventEmitter } from 'get-ready'; +import { isPromise, isGeneratorFunction } from 'is-type-of'; +import { promiseTimeout } from 'utility'; + +export interface BaseOptions { + initMethod?: string; + localStorage?: AsyncLocalStorage; + [key: string]: any; +} + +export abstract class Base extends ReadyEventEmitter { + options: BaseOptions; + #closed = false; + #localStorage: AsyncLocalStorage; + + constructor(options?: BaseOptions) { + super(); + + if (options?.initMethod) { + const initMethod = Reflect.get(this, options.initMethod) as () => Promise; + assert(typeof initMethod === 'function', + `[sdk-base] this.${options.initMethod} should be a function`); + assert(!isGeneratorFunction(initMethod), + `[sdk-base] this.${options.initMethod} should not be generator function`); + + process.nextTick(() => { + const ret = initMethod.apply(this); + assert(isPromise(ret), `[sdk-base] this.${options.initMethod} should return a promise`); + ret.then(() => { + this.ready(true); + }).catch(err => { + const hasReadyCallbacks = this.hasReadyCallbacks; + this.ready(err); + // no ready callbacks, should emit error event instead + if (!hasReadyCallbacks) { + this.emit('error', err); + } + }); + }); + } + this.options = options ?? {}; + this.#localStorage = this.options.localStorage ?? getAsyncLocalStorage(); + this.on('error', err => { + this._defaultErrorHandler(err); + }); + } + + /** + * support `await this.await('event')` + */ + async await(event: string) { + const values = await once(this, event); + return values[0]; + } + + /** + * get AsyncLocalStorage from options + * @return {AsyncLocalStorage} asyncLocalStorage instance or undefined + */ + get localStorage() { + return this.#localStorage; + } + + async readyOrTimeout(milliseconds: number) { + await promiseTimeout(this.ready(), milliseconds); + } + + _defaultErrorHandler(err: any) { + if (this.listeners('error').length > 1) { + // ignore defaultErrorHandler + return; + } + console.error('\n[%s][pid: %s][%s] %s: %s \nError Stack:\n %s', + Date(), process.pid, this.constructor.name, err.name, + err.message, err.stack); + + // try to show addition property on the error object + // e.g.: `err.data = {url: '/foo'};` + const additions = []; + for (const key in err) { + if (key === 'name' || key === 'message') { + continue; + } + additions.push(util.format(' %s: %j', key, err[key])); + } + if (additions.length) { + console.error('Error Additions:\n%s', additions.join('\n')); + } + console.error(); + } + + async close() { + if (this.#closed) { + return; + } + this.#closed = true; + const closeMethod = Reflect.get(this, '_close') as () => Promise; + if (typeof closeMethod !== 'function') { + return; + } + + try { + await closeMethod.apply(this); + } catch (err) { + this.emit('error', err); + } + } + + get isClosed() { + return this.#closed; + } +} diff --git a/test/fixtures/ts/index.ts b/test/fixtures/ts/index.ts index b3aa96f..48af260 100644 --- a/test/fixtures/ts/index.ts +++ b/test/fixtures/ts/index.ts @@ -1,4 +1,4 @@ -import Base, { BaseOptions } from '../../..'; +import { Base, BaseOptions } from '../../../src/index.js'; class FooContext { traceId?: string; @@ -47,8 +47,7 @@ export async function test() { console.log('localStorage should be undefined: %o', client.localStorage?.getStore()); }); await client.await('someEvent'); - await client.awaitFirst([ 'one', 'two' ]); + // await client.awaitFirst([ 'one', 'two' ]); return client.isReady; - } diff --git a/test/index.test.js b/test/index.test.ts similarity index 56% rename from test/index.test.js rename to test/index.test.ts index a8a56c8..ea37572 100644 --- a/test/index.test.js +++ b/test/index.test.ts @@ -1,25 +1,16 @@ -const assert = require('assert'); -const pedding = require('pedding'); -const Base = require('..'); -const path = require('path'); -const runscript = require('runscript'); -const { AsyncLocalStorage } = require('async_hooks'); -const baseDir = path.join(__dirname, './fixtures/ts'); - -function sleep(ms) { - return new Promise(resolve => { - setTimeout(resolve, ms); - }); -} +import { strict as assert } from 'node:assert'; +import { AsyncLocalStorage } from 'node:async_hooks'; +import { scheduler } from 'node:timers/promises'; +import { Base, BaseOptions } from '../src/index.js'; -describe('test/index.test.js', () => { +describe('test/index.test.ts', () => { class SomeServiceClient extends Base {} describe('default error handler', () => { it('should auto add the default error handler and show error message', () => { const c = new SomeServiceClient(); - assert(c.listeners('error').length === 1); - const err = new Error('mock error 1'); + assert.equal(c.listeners('error').length, 1); + const err: any = new Error('mock error 1'); err.data = { foo: 'bar', url: '/foo' }; err.status = 500; err.type = 'DUMP'; @@ -37,7 +28,7 @@ describe('test/index.test.js', () => { it('should not change the error name and show error message', done => { const c = new SomeServiceClient(); setTimeout(function() { - assert(c.listeners('error').length === 1); + assert.equal(c.listeners('error').length, 1); const err = new Error('mock some error'); err.name = 'SomeApiError'; c.emit('error', err); @@ -57,10 +48,10 @@ describe('test/index.test.js', () => { it('should use the exists error handler', done => { const c = new SomeServiceClient(); c.on('error', function(err) { - assert(err.message === 'mock error 2'); + assert.equal(err.message, 'mock error 2'); done(); }); - assert(c.listeners('error').length === 2); + assert.equal(c.listeners('error').length, 2); c.emit('error', new Error('mock error 2')); // should not stderr output }); @@ -69,85 +60,44 @@ describe('test/index.test.js', () => { describe('ready', () => { it('should ready once', done => { const client = new SomeServiceClient(); - assert(client.isReady === false); + assert.equal(client.isReady, false); client.ready(() => { - assert(client.isReady === true); + assert.equal(client.isReady, true); done(); }); client.ready(true); - assert(client.isReady === true); + assert.equal(client.isReady, true); // again should work client.ready(true); - assert(client.isReady === true); + assert.equal(client.isReady, true); }); }); - describe('options.initMethod support generator', () => { + describe('options.initMethod throw error when it is generator function', () => { class Client extends Base { - constructor(options) { + constructor(options: BaseOptions) { super(Object.assign({ initMethod: 'init', }, options)); - this.foo = 'foo'; } * init() { - assert(this.foo === 'foo'); - yield cb => setTimeout(cb, 500); - this.foo = 'bar'; - } - } - - class Client2 extends Base { - constructor(options) { - super(Object.assign({ - initMethod: 'init', - }, options)); - this.foo = 'foo'; - } - - init() { - assert(this.foo === 'foo'); - this.foo = 'bar'; - return Promise.resolve(); + // ignore } } - it('should auto init with options.initMethod', function* () { - const client = new Client({ a: 'a' }); - assert.deepEqual(client.options, { - a: 'a', - initMethod: 'init', - }); - yield client.ready(); - yield client.ready(); - assert(client.foo === 'bar'); - assert(client.localStorage === undefined); - }); - - it('should trigger ready callback without err', done => { - const client = new Client(); - client.ready(err => { - assert.ifError(err); - done(); - }); - }); - - it('should support options.initMethod that return promise', function* () { - const client = new Client2({ a: 'a' }); - assert.deepEqual(client.options, { - a: 'a', - initMethod: 'init', - }); - yield client.ready(); - yield client.ready(); - assert(client.foo === 'bar'); + it('should trigger ready callback without err', () => { + assert.throws(() => { + new Client({}); + }, /\[sdk-base] this.init should not be generator function/); }); }); describe('options.initMethod support async function', () => { class Client extends Base { - constructor(options) { + foo: string; + + constructor(options?: BaseOptions) { super(Object.assign({ initMethod: 'init', }, options)); @@ -155,38 +105,43 @@ describe('test/index.test.js', () => { } async init() { - assert(this.foo === 'foo'); - await sleep(500); + assert.equal(this.foo, 'foo'); + await scheduler.wait(500); this.foo = 'bar'; } } class Client2 extends Base { - constructor(options) { - super(Object.assign({ + foo: string; + + constructor(options: BaseOptions & { foo?: string }) { + super({ initMethod: 'init', - }, options)); + ...options, + }); this.foo = 'foo'; } async init() { - assert(this.foo === 'foo'); - await sleep(500); + assert.equal(this.foo, 'foo'); + await scheduler.wait(500); throw new Error('mock init error'); } } class Client3 extends Base { - constructor(options) { - super(Object.assign({ + foo: string; + constructor(options: BaseOptions & { a?: string; }) { + super({ initMethod: 'init', - }, options)); + ...options, + }); this.foo = 'foo'; } async init() { - assert(this.foo === 'foo'); - await sleep(500); + assert.equal(this.foo, 'foo'); + await scheduler.wait(500); throw new Error('mock ready error'); } } @@ -194,16 +149,30 @@ describe('test/index.test.js', () => { it('should auto init with options.initMethod', async () => { const localStorage = new AsyncLocalStorage(); const client = new Client({ a: 'a', localStorage }); - assert(client.options.initMethod === 'init'); - assert(client.isReady === false); + assert.equal(client.options.initMethod, 'init'); + assert.equal(client.isReady, false); await client.ready(); - assert(client.localStorage.getStore() === undefined); + assert.equal(client.localStorage.getStore(), undefined); await client.localStorage.run({ foo: 'bar' }, async () => { - assert(client.localStorage.getStore().foo === 'bar'); + assert.equal(client.localStorage.getStore().foo, 'bar'); }); await client.ready(); - assert(client.isReady === true); - assert(client.foo === 'bar'); + assert.equal(client.isReady, true); + assert.equal(client.foo, 'bar'); + }); + + it('should get default als from gals', async () => { + const client = new Client({ a: 'a' }); + assert.equal(client.options.initMethod, 'init'); + assert.equal(client.isReady, false); + await client.ready(); + assert.equal(client.localStorage.getStore(), undefined); + await client.localStorage.run({ foo: 'bar' }, async () => { + assert.equal(client.localStorage.getStore().foo, 'bar'); + }); + await client.ready(); + assert.equal(client.isReady, true); + assert.equal(client.foo, 'bar'); }); it('should trigger ready callback without err', done => { @@ -223,7 +192,8 @@ describe('test/index.test.js', () => { await assert.rejects(async () => { await client.ready(); }, err => { - assert(err.message === 'mock init error'); + assert(err instanceof Error); + assert.equal(err.message, 'mock init error'); return true; }); }); @@ -237,6 +207,7 @@ describe('test/index.test.js', () => { await assert.rejects(async () => { await client.ready(); }, err => { + assert(err instanceof Error); assert(err.message === 'mock ready error'); return true; }); @@ -244,7 +215,8 @@ describe('test/index.test.js', () => { await assert.rejects(async () => { await client.readyOrTimeout(10); }, err => { - assert(err.message === 'mock ready error'); + assert(err instanceof Error); + assert.equal(err.message, 'mock ready error'); return true; }); }); @@ -252,7 +224,8 @@ describe('test/index.test.js', () => { describe('readyOrTimeout()', () => { class Client extends Base { - constructor(options) { + foo: string; + constructor(options: BaseOptions) { super(Object.assign({ initMethod: 'init', }, options)); @@ -261,13 +234,14 @@ describe('test/index.test.js', () => { async init() { assert(this.foo === 'foo'); - await sleep(500); + await scheduler.wait(500); this.foo = 'bar'; } } class Client2 extends Base { - constructor(options) { + foo: string; + constructor(options: BaseOptions) { super(Object.assign({ initMethod: 'init', }, options)); @@ -275,15 +249,15 @@ describe('test/index.test.js', () => { } async init() { - assert(this.foo === 'foo'); - await sleep(500); + assert.equal(this.foo, 'foo'); + await scheduler.wait(500); throw new Error('mock ready error'); } } it('should ready timeout', async () => { const client = new Client({ a: 'a' }); - await sleep(1); + await scheduler.wait(1); assert.deepEqual(client.options, { a: 'a', initMethod: 'init', @@ -291,10 +265,9 @@ describe('test/index.test.js', () => { await assert.rejects(async () => { await client.readyOrTimeout(10); }, err => { - assert(err.name === 'TimeoutError'); - // console.log('%o', client._readyCallbacks); - // console.log(client._readyCallbacks[0].toString()); - assert(client._readyCallbacks.length === 0); + assert(err instanceof Error); + assert.equal(err.name, 'TimeoutError'); + assert.equal(err.message, 'Timed out after 10ms'); return true; }); @@ -305,11 +278,8 @@ describe('test/index.test.js', () => { await assert.rejects(async () => { await client.readyOrTimeout(10); }, err => { - assert(err.name === 'TimeoutError'); - console.log('%o', client._readyCallbacks); - // console.log(client._readyCallbacks[0].toString()); - assert(client._readyCallbacks.length === 1); - assert(client._readyCallbacks[0].name === 'readySuccessCallback'); + assert(err instanceof Error); + assert.equal(err.name, 'TimeoutError'); return true; }); await client.ready(); @@ -336,7 +306,8 @@ describe('test/index.test.js', () => { await assert.rejects(async () => { await client.readyOrTimeout(510); }, err => { - assert(err.message === 'mock ready error'); + assert(err instanceof Error); + assert.equal(err.message, 'mock ready error'); return true; }); }); @@ -350,36 +321,36 @@ describe('test/index.test.js', () => { }); } - * init() { - yield cb => setTimeout(() => { - cb(new Error('init error')); - }, 500); + async init() { + await scheduler.wait(500); + throw new Error('init error'); } } - it('should ready failed', function* () { + it('should ready failed', async () => { const client = new ErrorClient(); - let initError = null; - try { - yield client.ready(); - } catch (err) { - initError = err; - } - assert(initError && initError.message === 'init error'); + await assert.rejects(async () => { + await client.ready(); + }, err => { + assert(err instanceof Error); + assert.equal(err.message, 'init error'); + return true; + }); - initError = null; - try { - yield client.ready(); - } catch (err) { - initError = err; - } - assert(initError && initError.message === 'init error'); + await assert.rejects(async () => { + await client.ready(); + }, err => { + assert(err instanceof Error); + assert.equal(err.message, 'init error'); + return true; + }); }); it('should trigger ready callback with err', done => { const client = new ErrorClient(); client.ready(err => { - assert(err && err.message === 'init error'); + assert(err instanceof Error); + assert.equal(err.message, 'init error'); done(); }); }); @@ -387,15 +358,17 @@ describe('test/index.test.js', () => { it('should emit error if ready failed', done => { const client = new ErrorClient(); client.once('error', err => { - assert(err.message === 'init error'); + assert.equal(err.message, 'init error'); done(); }); + console.error('listen error'); }); it('should not emit error event if readyCallback not empty', done => { const client = new ErrorClient(); client.ready(err => { - assert(err.message === 'init error'); + assert(err instanceof Error); + assert.equal(err.message, 'init error'); setImmediate(done); }); client.once('error', () => { @@ -404,40 +377,49 @@ describe('test/index.test.js', () => { }); }); + function padding(cb: () => void, count: number) { + return () => { + count--; + if (count === 0) { + cb(); + } + }; + } + describe('generator event listener', () => { it('should add generator listener', done => { - done = pedding(done, 8); + done = padding(done, 8); const client = new SomeServiceClient(); - client.addListener('event_code', function* (a, b) { + client.addListener('event_code', (a, b) => { console.log('event_code in addListener'); - assert(a === 1); - assert(b === 2); + assert.equal(a, 1); + assert.equal(b, 2); done(); }); - client.on('event_code', function* (a, b) { + client.on('event_code', (a, b) => { console.log('event_code in on'); assert(a === 1); assert(b === 2); done(); }); - client.once('event_code', function* (a, b) { + client.once('event_code', (a, b) => { console.log('event_code in once'); assert(a === 1); assert(b === 2); done(); }); - client.prependListener('event_code', function* (a, b) { + client.prependListener('event_code', (a, b) => { console.log('event_code in prependListener'); assert(a === 1); assert(b === 2); done(); }); - client.prependOnceListener('event_code', function* (a, b) { + client.prependOnceListener('event_code', (a, b) => { console.log('event_code in prependOnceListener'); assert(a === 1); assert(b === 2); @@ -448,29 +430,27 @@ describe('test/index.test.js', () => { client.emit('event_code', 1, 2); }); - it('should catch generator exception and emit on error event', done => { - done = pedding(done, 2); + // Don't catch event listener inside error + // it('should catch generator exception and emit on error event', done => { + // done = padding(done, 2); + // const client = new SomeServiceClient(); + // client.on('error', err => { + // assert(err.name === 'EventListenerProcessError'); + // assert(err.message === 'generator process exception'); + // done(); + // }); + // client.on('event_code', () => { + // throw new Error('normal function process exception'); + // }); + // client.once('event_code', async () => { + // throw new Error('async function process exception'); + // }); + // client.emit('event_code'); + // }); + + it('should remove listener', done => { const client = new SomeServiceClient(); - client.on('error', err => { - assert(err.name === 'EventListenerProcessError'); - assert(err.message === 'generator process exception'); - done(); - }); - - client.on('event_code', function* () { - throw new Error('generator process exception'); - }); - - client.once('event_code', function* () { - throw new Error('generator process exception'); - }); - - client.emit('event_code'); - }); - - it('should remove generator listener', done => { - const client = new SomeServiceClient(); - const handler = function* (data) { + const handler = async (data: number) => { assert(data === 1); done(); }; @@ -481,72 +461,61 @@ describe('test/index.test.js', () => { client.on('event_code', handler); client.emit('event_code', 1); client.removeListener('event_code', handler); - assert(client.listeners('event_code').length === 0); + assert.equal(client.listeners('event_code').length, 0); }); - it('should not allow to add generator listener on error event', () => { - const client = new SomeServiceClient(); - assert.throws(() => { - client.on('error', function* (err) { - console.error(err); - }); - }, null, /\[sdk-base\] `error` event should not have a generator listener\./); - }); + // it('should not allow to add generator listener on error event', () => { + // const client = new SomeServiceClient(); + // assert.throws(() => { + // client.on('error', function* (err) { + // console.error(err); + // }); + // }, null, /\[sdk-base\] `error` event should not have a generator listener\./); + // }); }); - describe('await && awaitFirst', () => { - it('should support client.await', function* () { + describe('await', () => { + it('should support client.await', async () => { const client = new SomeServiceClient(); setTimeout(() => client.emit('someEvent', 'foo'), 100); - const res = yield client.await('someEvent'); - assert(res === 'foo'); + const res = await client.await('someEvent'); + assert.equal(res, 'foo'); }); - it('should support client.awaitFirst', function* () { - const client = new SomeServiceClient(); - setTimeout(() => client.emit('foo', 'foo'), 200); - setTimeout(() => client.emit('bar', 'bar'), 100); - - const o = yield client.awaitFirst([ 'foo', 'bar' ]); - assert.deepEqual(o, { - event: 'bar', - args: [ 'bar' ], - }); - assert(client.listenerCount('foo') === 0); - assert(client.listenerCount('bar') === 0); - }); - }); - - describe('run as typescript', () => { - it('compile ts file and execute', async () => { - await runscript(`tsc -p ${baseDir}/tsconfig.json`, { cwd: baseDir }); - const { test } = require('./fixtures/ts/index'); - const result = await test(); - assert.strictEqual(result, true); - }); + // it('should support client.awaitFirst', function* () { + // const client = new SomeServiceClient(); + // setTimeout(() => client.emit('foo', 'foo'), 200); + // setTimeout(() => client.emit('bar', 'bar'), 100); + // const o = yield client.awaitFirst([ 'foo', 'bar' ]); + // assert.deepEqual(o, { + // event: 'bar', + // args: [ 'bar' ], + // }); + // assert(client.listenerCount('foo') === 0); + // assert(client.listenerCount('bar') === 0); + // }); }); describe('close', () => { - describe('generate close', () => { - class GenerateCloseClient extends Base { - * _close() { - yield cb => process.nextTick(() => { - cb(); - }); - } - } - - it('should success', function* () { - const client = new GenerateCloseClient(); - yield client.close(); - assert(client._closed === true); - }); - }); + // describe('generate close', () => { + // class GenerateCloseClient extends Base { + // * _close() { + // yield cb => process.nextTick(() => { + // cb(); + // }); + // } + // } + // it('should success', function* () { + // const client = new GenerateCloseClient(); + // yield client.close(); + // assert(client._closed === true); + // }); + // }); describe('promise close', () => { class PromiseCloseClient extends Base { _close() { - return new Promise(resolve => { + return new Promise(resolve => { process.nextTick(() => { resolve(); }); @@ -554,26 +523,28 @@ describe('test/index.test.js', () => { } } - it('should success', function* () { + it('should success', async () => { const client = new PromiseCloseClient(); - yield client.close(); - assert(client._closed === true); + assert.equal(client.isClosed, false); + await client.close(); + assert.equal(client.isClosed, true); }); }); describe('async function _close', () => { class PromiseCloseClient extends Base { async _close() { - await sleep(10); + await scheduler.wait(10); } } it('should success', async () => { const client = new PromiseCloseClient(); + assert.equal(client.isClosed, false); await client.close(); - assert(client._closed === true); + assert.equal(client.isClosed, true); await client.close(); - assert(client._closed === true); + assert.equal(client.isClosed, true); }); }); @@ -581,10 +552,11 @@ describe('test/index.test.js', () => { class NoCloseClient extends Base { } - it('should success', function* () { + it('should success', async () => { const client = new NoCloseClient(); - yield client.close(); - assert(client._closed === true); + assert.equal(client.isClosed, false); + await client.close(); + assert.equal(client.isClosed, true); }); }); @@ -597,7 +569,7 @@ describe('test/index.test.js', () => { class PromiseCloseClient extends Base { _close() { calledTimes++; - return new Promise(resolve => { + return new Promise(resolve => { process.nextTick(() => { resolve(); }); @@ -606,23 +578,23 @@ describe('test/index.test.js', () => { } describe('serial close', () => { - it('should success', function* () { + it('should success', async () => { const client = new PromiseCloseClient(); - yield client.close(); - yield client.close(); - assert(client._closed === true); + await client.close(); + await client.close(); + assert(client.isClosed === true); assert(calledTimes === 1); }); }); describe('parallel close', () => { - it('should success', function* () { + it('should success', async () => { const client = new PromiseCloseClient(); - yield [ + await Promise.all([ client.close(), client.close(), - ]; - assert(client._closed === true); + ]); + assert.equal(client.isClosed, true); assert(calledTimes === 1); }); }); @@ -631,21 +603,22 @@ describe('test/index.test.js', () => { describe('error close', () => { class ErrorCloseClient extends Base { _close() { - return new Promise((resolve, reject) => { + return new Promise((_, reject) => { reject(new Error('mock error')); }); } } - it('should success', function* () { + it('should success', async () => { const client = new ErrorCloseClient(); - let error; + let error: Error; client.on('error', e => { error = e; }); - yield client.close(); - assert(client._closed === true); - assert(error); + await client.close(); + assert.equal(client.isClosed, true); + assert(error!); + assert(error instanceof Error); assert(/mock error/.test(error.message)); }); }); 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" + } +}