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..7602390 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -3,16 +3,13 @@ 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' 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..61e0c57 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,22 @@ 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 ### 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.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/package.json b/package.json index 5902276..9fae643 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,59 @@ "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" + "get-ready": "^3.3.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", + "runscript": "^2.0.1", + "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", + "preci": "npm run lint && npm run prepublishOnly && attw --pack", + "ci": "egg-bin cov", + "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" } diff --git a/index.d.ts b/src/index.d.ts similarity index 100% rename from index.d.ts rename to src/index.d.ts diff --git a/index.test-d.ts b/src/index.test-d.ts similarity index 100% rename from index.test-d.ts rename to src/index.test-d.ts diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..2b5b3ed --- /dev/null +++ b/src/index.ts @@ -0,0 +1,101 @@ +import util from 'node:util'; +import assert from 'node:assert'; +import { once } from 'node:events'; +import { AsyncLocalStorage } from 'node:async_hooks'; +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 class Base extends ReadyEventEmitter { + options: BaseOptions; + #closed = false; + + constructor(options?: BaseOptions) { + super(); + + if (options?.initMethod) { + const initMethod = Reflect.get(this, options.initMethod); + assert(typeof initMethod === 'function', + `[sdk-base] this.${options.initMethod} should be a function`); + assert(!isGeneratorFunction(initMethod), + `[sdk-base] this.${options.initMethod} generator function is not support`); + + 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 => this.ready(err)); + }); + } + this.options = options ?? {}; + this.on('error', err => { + this._defaultErrorHandler(err); + }); + } + + /** + * support `await this.await('event')` + */ + await(event: string) { + return once(this, event); + } + + /** + * get AsyncLocalStorage from options + * @return {AsyncLocalStorage} asyncLocalStorage instance or undefined + */ + get localStorage() { + return this.options.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 closeFunc = Reflect.get(this, '_close'); + if (typeof closeFunc !== 'function') { + return; + } + + try { + await closeFunc.apply(this); + } catch (err) { + this.emit('error', err); + } + } +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..af1dd95 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,29 @@ +export class TimeoutError extends Error { + timeout: number; + + constructor(timeout: number) { + super(`Timed out after ${timeout} ms`); + this.name = this.constructor.name; + this.timeout = timeout; + Error.captureStackTrace(this, this.constructor); + } +} + +// https://betterstack.com/community/guides/scaling-nodejs/nodejs-timeouts/ +export async function promiseWithTimeout( + promiseArg: Promise, + timeout: number, +): Promise { + let timer: NodeJS.Timeout; + const timeoutPromise = new Promise((_, reject) => { + timer = setTimeout(() => { + reject(new TimeoutError(timeout)); + }, timeout); + }); + + try { + return await Promise.race([ promiseArg, timeoutPromise ]); + } finally { + clearTimeout(timer!); + } +} 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" + } +}