├── .eslintignore ├── .eslintrc ├── renovate.json ├── .gitignore ├── tsconfig.json ├── .github └── workflows │ ├── release.yml │ ├── nodejs.yml │ └── pkg.pr.new.yml ├── test ├── fixtures │ └── ts │ │ ├── tsconfig.json │ │ └── index.ts └── index.test.ts ├── LICENSE ├── package.json ├── src └── index.ts ├── CHANGELOG.md └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | test/fixtures 2 | coverage/ 3 | examples/**/app/public 4 | logs 5 | run -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint-config-egg/typescript", 4 | "eslint-config-egg/lib/rules/enforce-node-prefix" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.seed 2 | *.log 3 | *.csv 4 | *.dat 5 | *.out 6 | *.pid 7 | *.gz 8 | 9 | node_modules 10 | 11 | dump.rdb 12 | .DS_Store 13 | 14 | test/fixtures/**/*.js 15 | .tshy* 16 | .eslintcache 17 | dist 18 | coverage 19 | package-lock.json 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@eggjs/tsconfig", 3 | "compilerOptions": { 4 | "strict": true, 5 | "noImplicitAny": true, 6 | "target": "ES2022", 7 | "module": "NodeNext", 8 | "moduleResolution": "NodeNext" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | release: 9 | name: NPM 10 | uses: node-modules/github-actions/.github/workflows/npm-release.yml@master 11 | secrets: 12 | GIT_TOKEN: ${{ secrets.GIT_TOKEN }} 13 | -------------------------------------------------------------------------------- /test/fixtures/ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "baseUrl": ".", 5 | "module": "commonjs", 6 | "lib": ["es7"], 7 | "esModuleInterop": false, 8 | "strict": true, 9 | "noImplicitAny": false, 10 | "skipLibCheck": true 11 | }, 12 | "exclude": [ 13 | "node_modules" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | merge_group: 9 | 10 | jobs: 11 | Job: 12 | name: Node.js 13 | uses: node-modules/github-actions/.github/workflows/node-test.yml@master 14 | with: 15 | version: '18.19.0, 18, 20, 22, 24' 16 | secrets: 17 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/pkg.pr.new.yml: -------------------------------------------------------------------------------- 1 | name: Publish Any Commit 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@v6 11 | 12 | - run: corepack enable 13 | - uses: actions/setup-node@v6 14 | with: 15 | node-version: 20 16 | 17 | - name: Install dependencies 18 | run: npm install 19 | 20 | - name: Build 21 | run: npm run prepublishOnly --if-present 22 | 23 | - run: npx pkg-pr-new publish 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) node-modules and other contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/fixtures/ts/index.ts: -------------------------------------------------------------------------------- 1 | import { Base, BaseOptions } from '../../../src/index.js'; 2 | 3 | class FooContext { 4 | traceId?: string; 5 | } 6 | 7 | class ClientSimple extends Base {} 8 | 9 | class Client extends Base { 10 | errorHandlerLength: number = 0; 11 | 12 | constructor(options: BaseOptions & { foo: string }) { 13 | super({ 14 | initMethod: 'init', 15 | ...options, 16 | }); 17 | } 18 | 19 | init() { 20 | this.errorHandlerLength = this.listeners('error').length; 21 | setTimeout(() => { 22 | this.ready(true); 23 | this.emit('someEvent'); 24 | }, 100); 25 | setTimeout(() => { 26 | this.emit('one'); 27 | }, 200); 28 | 29 | return Promise.resolve(); 30 | } 31 | 32 | handleError(err: Error) { 33 | throw err; 34 | } 35 | 36 | handleMessage(message: string) { 37 | console.log(message, this.isReady); 38 | } 39 | } 40 | 41 | export async function test() { 42 | let client: Client; 43 | 44 | client = new Client({ foo: 'bar' }); 45 | client.ready(() => { 46 | console.log('ready'); 47 | console.log('localStorage should be undefined: %o', client.localStorage?.getStore()); 48 | }); 49 | await client.await('someEvent'); 50 | // await client.awaitFirst([ 'one', 'two' ]); 51 | 52 | return client.isReady; 53 | } 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sdk-base", 3 | "version": "5.0.1", 4 | "description": "a base class for sdk with default error handler", 5 | "keywords": [ 6 | "sdk", 7 | "error" 8 | ], 9 | "author": { 10 | "name": "dead_horse", 11 | "email": "dead_horse@qq.com", 12 | "url": "http://deadhorse.me" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git@github.com:node-modules/sdk-base" 17 | }, 18 | "license": "MIT", 19 | "engines": { 20 | "node": ">= 18.19.0" 21 | }, 22 | "dependencies": { 23 | "gals": "^1.0.2", 24 | "get-ready": "^3.4.0", 25 | "is-type-of": "^2.2.0", 26 | "utility": "^2.3.0" 27 | }, 28 | "devDependencies": { 29 | "@arethetypeswrong/cli": "^0.18.0", 30 | "@eggjs/tsconfig": "1", 31 | "@types/mocha": "10", 32 | "@types/node": "22", 33 | "egg-bin": "6", 34 | "eslint": "8", 35 | "eslint-config-egg": "14", 36 | "tshy": "3", 37 | "tshy-after": "1", 38 | "typescript": "5" 39 | }, 40 | "scripts": { 41 | "lint": "eslint --cache src test --ext .ts", 42 | "pretest": "npm run lint -- --fix && npm run prepublishOnly", 43 | "test": "egg-bin test -p --timeout 5000", 44 | "test-local": "egg-bin test --timeout 5000", 45 | "preci": "npm run lint && npm run prepublishOnly && attw --pack", 46 | "ci": "egg-bin cov -p", 47 | "prepublishOnly": "tshy && tshy-after" 48 | }, 49 | "type": "module", 50 | "tshy": { 51 | "exports": { 52 | ".": "./src/index.ts", 53 | "./package.json": "./package.json" 54 | } 55 | }, 56 | "exports": { 57 | ".": { 58 | "import": { 59 | "types": "./dist/esm/index.d.ts", 60 | "default": "./dist/esm/index.js" 61 | }, 62 | "require": { 63 | "types": "./dist/commonjs/index.d.ts", 64 | "default": "./dist/commonjs/index.js" 65 | } 66 | }, 67 | "./package.json": "./package.json" 68 | }, 69 | "files": [ 70 | "dist", 71 | "src" 72 | ], 73 | "types": "./dist/commonjs/index.d.ts", 74 | "main": "./dist/commonjs/index.js", 75 | "module": "./dist/esm/index.js" 76 | } 77 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import util from 'node:util'; 2 | import assert from 'node:assert'; 3 | import { once } from 'node:events'; 4 | import { AsyncLocalStorage } from 'node:async_hooks'; 5 | import { getAsyncLocalStorage } from 'gals'; 6 | import { ReadyEventEmitter } from 'get-ready'; 7 | import { isPromise, isGeneratorFunction } from 'is-type-of'; 8 | import { promiseTimeout } from 'utility'; 9 | 10 | export interface BaseOptions { 11 | initMethod?: string; 12 | localStorage?: AsyncLocalStorage; 13 | [key: string]: any; 14 | } 15 | 16 | export abstract class Base extends ReadyEventEmitter { 17 | options: BaseOptions; 18 | #closed = false; 19 | #localStorage: AsyncLocalStorage; 20 | 21 | constructor(options?: BaseOptions) { 22 | super(); 23 | 24 | if (options?.initMethod) { 25 | const initMethod = Reflect.get(this, options.initMethod) as () => Promise; 26 | assert(typeof initMethod === 'function', 27 | `[sdk-base] this.${options.initMethod} should be a function`); 28 | assert(!isGeneratorFunction(initMethod), 29 | `[sdk-base] this.${options.initMethod} should not be generator function`); 30 | 31 | process.nextTick(() => { 32 | const ret = initMethod.apply(this); 33 | assert(isPromise(ret), `[sdk-base] this.${options.initMethod} should return a promise`); 34 | ret.then(() => { 35 | this.ready(true); 36 | }).catch(err => { 37 | const hasReadyCallbacks = this.hasReadyCallbacks; 38 | this.ready(err); 39 | // no ready callbacks, should emit error event instead 40 | if (!hasReadyCallbacks) { 41 | this.emit('error', err); 42 | } 43 | }); 44 | }); 45 | } 46 | this.options = options ?? {}; 47 | this.#localStorage = this.options.localStorage ?? getAsyncLocalStorage(); 48 | super.on('error', err => { 49 | this._defaultErrorHandler(err); 50 | }); 51 | } 52 | 53 | /** 54 | * support `await this.await('event')` 55 | */ 56 | async await(event: string) { 57 | const values = await once(this, event); 58 | return values[0]; 59 | } 60 | 61 | /** 62 | * get AsyncLocalStorage from options 63 | * @return {AsyncLocalStorage} asyncLocalStorage instance or undefined 64 | */ 65 | get localStorage() { 66 | return this.#localStorage; 67 | } 68 | 69 | async readyOrTimeout(milliseconds: number) { 70 | await promiseTimeout(this.ready(), milliseconds); 71 | } 72 | 73 | _defaultErrorHandler(err: any) { 74 | if (this.listeners('error').length > 1) { 75 | // ignore defaultErrorHandler 76 | return; 77 | } 78 | console.error('\n[%s][pid: %s][%s] %s: %s \nError Stack:\n %s', 79 | Date(), process.pid, this.constructor.name, err.name, 80 | err.message, err.stack); 81 | 82 | // try to show addition property on the error object 83 | // e.g.: `err.data = {url: '/foo'};` 84 | const additions = []; 85 | for (const key in err) { 86 | if (key === 'name' || key === 'message') { 87 | continue; 88 | } 89 | additions.push(util.format(' %s: %j', key, err[key])); 90 | } 91 | if (additions.length) { 92 | console.error('Error Additions:\n%s', additions.join('\n')); 93 | } 94 | console.error(); 95 | } 96 | 97 | async close() { 98 | if (this.#closed) { 99 | return; 100 | } 101 | this.#closed = true; 102 | const closeMethod = Reflect.get(this, '_close') as () => Promise; 103 | if (typeof closeMethod !== 'function') { 104 | return; 105 | } 106 | 107 | try { 108 | await closeMethod.apply(this); 109 | } catch (err) { 110 | this.emit('error', err); 111 | } 112 | } 113 | 114 | get isClosed() { 115 | return this.#closed; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [5.0.1](https://github.com/node-modules/sdk-base/compare/v5.0.0...v5.0.1) (2024-12-22) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * allow sub class to override event methods ([#24](https://github.com/node-modules/sdk-base/issues/24)) ([d1ffb61](https://github.com/node-modules/sdk-base/commit/d1ffb61266aaec6028c39a6754671a555e41a6b7)) 9 | 10 | ## [5.0.0](https://github.com/node-modules/sdk-base/compare/v4.2.1...v5.0.0) (2024-12-18) 11 | 12 | 13 | ### ⚠ BREAKING CHANGES 14 | 15 | * drop Node.js < 18.19.0 support 16 | 17 | part of https://github.com/eggjs/egg/issues/3644 18 | 19 | https://github.com/eggjs/egg/issues/5257 20 | 21 | ### Features 22 | 23 | * support cjs and esm both by tshy ([#23](https://github.com/node-modules/sdk-base/issues/23)) ([cde6773](https://github.com/node-modules/sdk-base/commit/cde67730c06f6b614c30853f6ddaf936b786ecbf)) 24 | 25 | ## [4.2.1](https://github.com/node-modules/sdk-base/compare/v4.2.0...v4.2.1) (2022-12-17) 26 | 27 | 28 | ### Bug Fixes 29 | 30 | * auto release on action ([#22](https://github.com/node-modules/sdk-base/issues/22)) ([e74df48](https://github.com/node-modules/sdk-base/commit/e74df4885a74fa99e935323a858e7f6c4447cc97)) 31 | 32 | --- 33 | 34 | 35 | 4.2.0 / 2022-12-09 36 | ================== 37 | 38 | **features** 39 | * [[`71d7ddd`](http://github.com/node-modules/sdk-base/commit/71d7ddd0c98f0c3c6ead65e1741ed5c54bd0eb38)] - 📦 NEW: Support localStorage getter (#21) (fengmk2 <>) 40 | 41 | 4.1.0 / 2022-12-03 42 | ================== 43 | 44 | **features** 45 | * [[`6e8a1c4`](http://github.com/node-modules/sdk-base/commit/6e8a1c4707908b28cc30a6019f164544c9033bb7)] - 📦 NEW: Support ready or timeout detect (#20) (fengmk2 <>) 46 | 47 | 4.0.0 / 2022-12-03 48 | ================== 49 | 50 | **features** 51 | * [[`567a380`](http://github.com/node-modules/sdk-base/commit/567a3806e348549f40fedf3438054b53f540107e)] - 👌 IMPROVE: [BREAKING] Drop Node.js < 14 support (#19) (fengmk2 <>) 52 | * [[`07d55e8`](http://github.com/node-modules/sdk-base/commit/07d55e8596ced9ecaea837a3ff8a56e87a333da8)] - feat: optimize performance (#18) (brizer <<362512489@qq.com>>) 53 | 54 | **others** 55 | * [[`e9bf6e9`](http://github.com/node-modules/sdk-base/commit/e9bf6e9e66570ac7c5e9537c22855573275d6618)] - refactor: enhance require profermance (#16) (zōng yǔ <>) 56 | * [[`bbea174`](http://github.com/node-modules/sdk-base/commit/bbea174cebde7af79afdff50cd01eec3b5481fad)] - Create codeql.yml (fengmk2 <>) 57 | 58 | 3.6.0 / 2019-04-24 59 | ================== 60 | 61 | **features** 62 | * [[`39c0f1d`](http://github.com/node-modules/sdk-base/commit/39c0f1d946bd7da1e393d42cca2f5e1bc22eb785)] - feat: implement close function (#17) (killa <>) 63 | 64 | 3.5.1 / 2018-09-27 65 | ================== 66 | 67 | **fixes** 68 | * [[`de262c1`](http://github.com/node-modules/sdk-base/commit/de262c1e41e65a5fb11e95a95f96c6c561cb9d23)] - fix(ts): support es module export (#15) (Haoliang Gao <>) 69 | 70 | 3.5.0 / 2018-07-26 71 | ================== 72 | 73 | **features** 74 | * [[`dcce360`](http://github.com/node-modules/sdk-base/commit/dcce360d5da6a3f0516c2329c1902c49221ffd29)] - feat: add typescript definition file (#14) (Angela <>) 75 | 76 | **others** 77 | * [[`f975763`](http://github.com/node-modules/sdk-base/commit/f975763047a461fc8d0758f08dd52e16078f5bc9)] - chore: release 3.4.0 (xiaochen.gaoxc <>), 78 | 79 | 3.4.0 / 2017-11-24 80 | ================== 81 | 82 | **features** 83 | * [[`98207ba`]](https://github.com/node-modules/sdk-base/pull/11/commits/98207ba521487df39f7c9b116aaf7163bb6b9ad8) - feat: add awaitFirst api (#11) (gxcsoccer <>) 84 | 85 | 3.3.0 / 2017-09-17 86 | ================== 87 | 88 | **features** 89 | * [[`8d5c04a`](http://github.com/node-modules/sdk-base/commit/8d5c04aa3b0fee135dcf972b447aba0f79f56417)] - feat: add isReady getter (#10) (fengmk2 <>) 90 | 91 | **others** 92 | * [[`6ec435f`](http://github.com/node-modules/sdk-base/commit/6ec435f676395726ff64646518b55c7c8ff4bc45)] - chore: fix initMethod document description (fengmk2 <>) 93 | 94 | 3.2.0 / 2017-06-26 95 | ================== 96 | 97 | * feat: let options.initMethod support functions that return promise (#9) 98 | 99 | 3.1.1 / 2017-03-14 100 | ================== 101 | 102 | * fix: avoid duplicate error handler (#8) 103 | 104 | 3.1.0 / 2017-02-17 105 | ================== 106 | 107 | * feat: support client.await (#7) 108 | 109 | 3.0.1 / 2017-01-12 110 | ================== 111 | 112 | * fix: initMethod should be lazy executed (#6) 113 | 114 | 3.0.0 / 2017-01-12 115 | ================== 116 | 117 | * feat: [BREAKING_CHANGE] add ready with error and generator listener (#5) 118 | 119 | 2.0.1 / 2016-03-11 120 | ================== 121 | 122 | * fix: use event.listeners 123 | 124 | 2.0.0 / 2016-03-11 125 | ================== 126 | 127 | * refactor: listen on error synchronous 128 | 129 | 1.1.0 / 2015-11-14 130 | ================== 131 | 132 | * refactor: drop 0.8 support 133 | * feat: support ready(flagOrFunction) 134 | 135 | 1.0.1 / 2014-11-06 136 | ================== 137 | 138 | * remove .npmignore 139 | * add __filename, always show construct name 140 | * more pretty 141 | * refine error display 142 | * refactor(error): improve default error handler 143 | * fix travis 144 | * fix link 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sdk-base 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![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) 5 | [![Test coverage][codecov-image]][codecov-url] 6 | [![Known Vulnerabilities][snyk-image]][snyk-url] 7 | [![npm download][download-image]][download-url] 8 | [![Node.js Version](https://img.shields.io/node/v/sdk-base.svg?style=flat)](https://nodejs.org/en/download/) 9 | 10 | [npm-image]: https://img.shields.io/npm/v/sdk-base.svg?style=flat-square 11 | [npm-url]: https://npmjs.org/package/sdk-base 12 | [codecov-image]: https://codecov.io/github/node-modules/sdk-base/coverage.svg?branch=master 13 | [codecov-url]: https://codecov.io/github/node-modules/sdk-base?branch=master 14 | [snyk-image]: https://snyk.io/test/npm/sdk-base/badge.svg?style=flat-square 15 | [snyk-url]: https://snyk.io/test/npm/sdk-base 16 | [download-image]: https://img.shields.io/npm/dm/sdk-base.svg?style=flat-square 17 | [download-url]: https://npmjs.org/package/sdk-base 18 | 19 | A base class for sdk with some common & useful functions. 20 | 21 | ## Installation 22 | 23 | ```bash 24 | npm install sdk-base 25 | ``` 26 | 27 | ## Usage 28 | 29 | Constructor argument: 30 | 31 | - {Object} options 32 | - {String} [initMethod] - the async init method name, the method should be a function return promise. If set, will execute the function in the constructor. 33 | - {AsyncLocalStorage} [localStorage] - async localStorage instance. 34 | 35 | ```js 36 | const { Base } = require('sdk-base'); 37 | 38 | class Client extends Base { 39 | constructor() { 40 | super({ 41 | initMethod: 'init', 42 | localStorage: app.ctxStorage, 43 | }); 44 | } 45 | 46 | async init() { 47 | // put your async init logic here 48 | } 49 | // support async function too 50 | // async init() { 51 | // // put your async init logic here 52 | // } 53 | } 54 | 55 | (async function main() { 56 | const client = new Client(); 57 | // wait client ready, if init failed, client will throw an error. 58 | await client.ready(); 59 | 60 | // support async event listener 61 | client.on('data', async function (data) { 62 | // put your async process logic here 63 | // 64 | // @example 65 | // ---------- 66 | // await submit(data); 67 | }); 68 | 69 | client.emit('data', { foo: 'bar' }); 70 | 71 | })().catch(err => { console.error(err); }); 72 | ``` 73 | 74 | ### API 75 | 76 | - `.ready(flagOrFunction)` flagOrFunction is optional, and the argument type can be Boolean, Error or Function. 77 | 78 | ```js 79 | // init ready 80 | client.ready(true); 81 | // init failed 82 | client.ready(new Error('init failed')); 83 | 84 | // listen client ready 85 | client.ready(err => { 86 | if (err) { 87 | console.log('client init failed'); 88 | console.error(err); 89 | return; 90 | } 91 | console.log('client is ready'); 92 | }); 93 | 94 | // support promise style call 95 | client.ready() 96 | .then(() => { ... }) 97 | .catch(err => { ... }); 98 | 99 | // support async function style call 100 | await client.ready(); 101 | ``` 102 | 103 | - `async readyOrTimeout(milliseconds)` ready or timeout, after milliseconds not ready will throw TimeoutError 104 | 105 | ```js 106 | await client.readyOrTimeout(100); 107 | ``` 108 | 109 | - `.isReady getter` detect client start ready or not. 110 | - `.on(event, listener)` wrap the [EventEmitter.prototype.on(event, listener)](https://nodejs.org/api/events.html#events_emitter_on_eventname_listener), the only difference is to support adding async function listener on events, except 'error' event. 111 | - `once(event, listener)` wrap the [EventEmitter.prototype.once(event, listener)](https://nodejs.org/api/events.html#events_emitter_once_eventname_listener), the only difference is to support adding async function listener on events, except 'error' event. 112 | - `prependListener(event, listener)` wrap the [EventEmitter.prototype.prependListener(event, listener)](https://nodejs.org/api/events.html#events_emitter_prependlistener_eventname_listener), the only difference is to support adding async function listener on events, except 'error' event. 113 | - `prependOnceListener(event, listener)` wrap the [EventEmitter.prototype.prependOnceListener(event, listener)](https://nodejs.org/api/events.html#events_emitter_prependoncelistener_eventname_listener), the only difference is to support adding generator listener on events, except 'error' event. 114 | - `addListener(event, listener)` wrap the [EventEmitter.prototype.addListener(event, listener)](https://nodejs.org/api/events.html#events_emitter_addlistener_eventname_listener), the only difference is to support adding async function listener on events, except 'error' event. 115 | 116 | ```js 117 | client.on('data', async function(data) { 118 | // your async process logic here 119 | }); 120 | client.once('foo', async function(bar) { 121 | // ... 122 | }); 123 | 124 | // listen error event 125 | client.on('error', err => { 126 | console.error(err.stack); 127 | }); 128 | ``` 129 | 130 | - `.await(event)`: [await an event](https://github.com/cojs/await-event), return a promise, and it will resolve(reject if event is `error`) once this event emmited. 131 | 132 | ```js 133 | const data = await client.await('data'); 134 | ``` 135 | 136 | - `._close()`: The `_close()` method is called by `close`. 137 | It can be overridden by child class, but should not be called directly. It must return promise or generator. 138 | 139 | - `.close()`: The `close()` method is used to close the instance. 140 | 141 | ## Breaking changes between v4 and v5 142 | 143 | - Drop `.awaitFirst(events)` support 144 | - Drop generator function support 145 | - Don't catch event listener inside error 146 | 147 | ### License 148 | 149 | [MIT](LICENSE) 150 | 151 | ## Contributors 152 | 153 | [![Contributors](https://contrib.rocks/image?repo=node-modules/sdk-base)](https://github.com/node-modules/sdk-base/graphs/contributors) 154 | 155 | Made with [contributors-img](https://contrib.rocks). 156 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'node:assert'; 2 | import { AsyncLocalStorage } from 'node:async_hooks'; 3 | import { scheduler } from 'node:timers/promises'; 4 | import { Base, BaseOptions } from '../src/index.js'; 5 | 6 | describe('test/index.test.ts', () => { 7 | class SomeServiceClient extends Base {} 8 | 9 | class SomeServiceClient2 extends Base { 10 | protected _lists: any[] = []; 11 | 12 | on(...args: any[]) { 13 | this._lists.push(args); 14 | super.on(args[0], args[1]); 15 | return this; 16 | } 17 | } 18 | 19 | describe('default error handler', () => { 20 | it('should allow subclass to override on methods', () => { 21 | const c = new SomeServiceClient2(); 22 | c.on('error', () => {}); 23 | assert.equal(c.listeners('error').length, 2); 24 | }); 25 | 26 | it('should auto add the default error handler and show error message', () => { 27 | const c = new SomeServiceClient(); 28 | assert.equal(c.listeners('error').length, 1); 29 | const err: any = new Error('mock error 1'); 30 | err.data = { foo: 'bar', url: '/foo' }; 31 | err.status = 500; 32 | err.type = 'DUMP'; 33 | c.emit('error', err); 34 | // should stderr output 35 | // [Thu Nov 06 2014 11:14:33 GMT+0800 (CST)] ERROR 63189 [sdk-base] Unhandle SomeServiceClientError: mock error 1, stack: 36 | // Error: mock error 1 37 | // at null._onTimeout (/Users/mk2/git/sdk-base/test/index.test.js:29:19) 38 | // at Timer.listOnTimeout (timers.js:133:15) 39 | // { [SomeServiceClientError: mock error 1] 40 | // data: { foo: 'bar', url: '/foo' }, 41 | // name: 'SomeServiceClientError' } 42 | }); 43 | 44 | it('should not change the error name and show error message', done => { 45 | const c = new SomeServiceClient(); 46 | setTimeout(function() { 47 | assert.equal(c.listeners('error').length, 1); 48 | const err = new Error('mock some error'); 49 | err.name = 'SomeApiError'; 50 | c.emit('error', err); 51 | // should stderr output 52 | // [Thu Nov 06 2014 11:14:33 GMT+0800 (CST)] ERROR 63189 [sdk-base] Unhandle SomeApiError: mock some error, stack: 53 | // Error: mock some error 54 | // at null._onTimeout (/Users/mk2/git/sdk-base/test/index.test.js:29:19) 55 | // at Timer.listOnTimeout (timers.js:133:15) 56 | // { [SomeApiError: mock some error] 57 | // name: 'SomeApiError' } 58 | done(); 59 | }, 10); 60 | }); 61 | }); 62 | 63 | describe('custom error handler and do not show error message', () => { 64 | it('should use the exists error handler', done => { 65 | const c = new SomeServiceClient(); 66 | c.on('error', function(err) { 67 | assert.equal(err.message, 'mock error 2'); 68 | done(); 69 | }); 70 | assert.equal(c.listeners('error').length, 2); 71 | c.emit('error', new Error('mock error 2')); 72 | // should not stderr output 73 | }); 74 | }); 75 | 76 | describe('ready', () => { 77 | it('should ready once', done => { 78 | const client = new SomeServiceClient(); 79 | assert.equal(client.isReady, false); 80 | client.ready(() => { 81 | assert.equal(client.isReady, true); 82 | done(); 83 | }); 84 | client.ready(true); 85 | assert.equal(client.isReady, true); 86 | // again should work 87 | client.ready(true); 88 | assert.equal(client.isReady, true); 89 | }); 90 | }); 91 | 92 | describe('options.initMethod throw error when it is generator function', () => { 93 | class Client extends Base { 94 | constructor(options: BaseOptions) { 95 | super(Object.assign({ 96 | initMethod: 'init', 97 | }, options)); 98 | } 99 | 100 | * init() { 101 | // ignore 102 | } 103 | } 104 | 105 | it('should trigger ready callback without err', () => { 106 | assert.throws(() => { 107 | new Client({}); 108 | }, /\[sdk-base] this.init should not be generator function/); 109 | }); 110 | }); 111 | 112 | describe('options.initMethod support async function', () => { 113 | class Client extends Base { 114 | foo: string; 115 | 116 | constructor(options?: BaseOptions) { 117 | super(Object.assign({ 118 | initMethod: 'init', 119 | }, options)); 120 | this.foo = 'foo'; 121 | } 122 | 123 | async init() { 124 | assert.equal(this.foo, 'foo'); 125 | await scheduler.wait(500); 126 | this.foo = 'bar'; 127 | } 128 | } 129 | 130 | class Client2 extends Base { 131 | foo: string; 132 | 133 | constructor(options: BaseOptions & { foo?: string }) { 134 | super({ 135 | initMethod: 'init', 136 | ...options, 137 | }); 138 | this.foo = 'foo'; 139 | } 140 | 141 | async init() { 142 | assert.equal(this.foo, 'foo'); 143 | await scheduler.wait(500); 144 | throw new Error('mock init error'); 145 | } 146 | } 147 | 148 | class Client3 extends Base { 149 | foo: string; 150 | constructor(options: BaseOptions & { a?: string; }) { 151 | super({ 152 | initMethod: 'init', 153 | ...options, 154 | }); 155 | this.foo = 'foo'; 156 | } 157 | 158 | async init() { 159 | assert.equal(this.foo, 'foo'); 160 | await scheduler.wait(500); 161 | throw new Error('mock ready error'); 162 | } 163 | } 164 | 165 | it('should auto init with options.initMethod', async () => { 166 | const localStorage = new AsyncLocalStorage(); 167 | const client = new Client({ a: 'a', localStorage }); 168 | assert.equal(client.options.initMethod, 'init'); 169 | assert.equal(client.isReady, false); 170 | await client.ready(); 171 | assert.equal(client.localStorage.getStore(), undefined); 172 | await client.localStorage.run({ foo: 'bar' }, async () => { 173 | assert.equal(client.localStorage.getStore().foo, 'bar'); 174 | }); 175 | await client.ready(); 176 | assert.equal(client.isReady, true); 177 | assert.equal(client.foo, 'bar'); 178 | }); 179 | 180 | it('should get default als from gals', async () => { 181 | const client = new Client({ a: 'a' }); 182 | assert.equal(client.options.initMethod, 'init'); 183 | assert.equal(client.isReady, false); 184 | await client.ready(); 185 | assert.equal(client.localStorage.getStore(), undefined); 186 | await client.localStorage.run({ foo: 'bar' }, async () => { 187 | assert.equal(client.localStorage.getStore().foo, 'bar'); 188 | }); 189 | await client.ready(); 190 | assert.equal(client.isReady, true); 191 | assert.equal(client.foo, 'bar'); 192 | }); 193 | 194 | it('should trigger ready callback without err', done => { 195 | const client = new Client(); 196 | client.ready(err => { 197 | assert.ifError(err); 198 | done(); 199 | }); 200 | }); 201 | 202 | it('should throw init error', async () => { 203 | const client = new Client2({ a: 'a' }); 204 | assert.deepEqual(client.options, { 205 | a: 'a', 206 | initMethod: 'init', 207 | }); 208 | await assert.rejects(async () => { 209 | await client.ready(); 210 | }, err => { 211 | assert(err instanceof Error); 212 | assert.equal(err.message, 'mock init error'); 213 | return true; 214 | }); 215 | }); 216 | 217 | it('should throw ready error', async () => { 218 | const client = new Client3({ a: 'a' }); 219 | assert.deepEqual(client.options, { 220 | a: 'a', 221 | initMethod: 'init', 222 | }); 223 | await assert.rejects(async () => { 224 | await client.ready(); 225 | }, err => { 226 | assert(err instanceof Error); 227 | assert(err.message === 'mock ready error'); 228 | return true; 229 | }); 230 | 231 | await assert.rejects(async () => { 232 | await client.readyOrTimeout(10); 233 | }, err => { 234 | assert(err instanceof Error); 235 | assert.equal(err.message, 'mock ready error'); 236 | return true; 237 | }); 238 | }); 239 | }); 240 | 241 | describe('readyOrTimeout()', () => { 242 | class Client extends Base { 243 | foo: string; 244 | constructor(options: BaseOptions) { 245 | super(Object.assign({ 246 | initMethod: 'init', 247 | }, options)); 248 | this.foo = 'foo'; 249 | } 250 | 251 | async init() { 252 | assert(this.foo === 'foo'); 253 | await scheduler.wait(500); 254 | this.foo = 'bar'; 255 | } 256 | } 257 | 258 | class Client2 extends Base { 259 | foo: string; 260 | constructor(options: BaseOptions) { 261 | super(Object.assign({ 262 | initMethod: 'init', 263 | }, options)); 264 | this.foo = 'foo'; 265 | } 266 | 267 | async init() { 268 | assert.equal(this.foo, 'foo'); 269 | await scheduler.wait(500); 270 | throw new Error('mock ready error'); 271 | } 272 | } 273 | 274 | it('should ready timeout', async () => { 275 | const client = new Client({ a: 'a' }); 276 | await scheduler.wait(1); 277 | assert.deepEqual(client.options, { 278 | a: 'a', 279 | initMethod: 'init', 280 | }); 281 | await assert.rejects(async () => { 282 | await client.readyOrTimeout(10); 283 | }, err => { 284 | assert(err instanceof Error); 285 | assert.equal(err.name, 'TimeoutError'); 286 | assert.equal(err.message, 'Timed out after 10ms'); 287 | return true; 288 | }); 289 | 290 | let readySuccess = false; 291 | client.ready(function readySuccessCallback() { 292 | readySuccess = true; 293 | }); 294 | await assert.rejects(async () => { 295 | await client.readyOrTimeout(10); 296 | }, err => { 297 | assert(err instanceof Error); 298 | assert.equal(err.name, 'TimeoutError'); 299 | return true; 300 | }); 301 | await client.ready(); 302 | assert(readySuccess); 303 | 304 | await client.readyOrTimeout(1); 305 | }); 306 | 307 | it('should ready success', async () => { 308 | const client = new Client({ a: 'a' }); 309 | assert.deepEqual(client.options, { 310 | a: 'a', 311 | initMethod: 'init', 312 | }); 313 | await client.readyOrTimeout(510); 314 | }); 315 | 316 | it('should ready error', async () => { 317 | const client = new Client2({ a: 'a' }); 318 | assert.deepEqual(client.options, { 319 | a: 'a', 320 | initMethod: 'init', 321 | }); 322 | await assert.rejects(async () => { 323 | await client.readyOrTimeout(510); 324 | }, err => { 325 | assert(err instanceof Error); 326 | assert.equal(err.message, 'mock ready error'); 327 | return true; 328 | }); 329 | }); 330 | }); 331 | 332 | describe('this.ready(err)', () => { 333 | class ErrorClient extends Base { 334 | constructor() { 335 | super({ 336 | initMethod: 'init', 337 | }); 338 | } 339 | 340 | async init() { 341 | await scheduler.wait(500); 342 | throw new Error('init error'); 343 | } 344 | } 345 | 346 | it('should ready failed', async () => { 347 | const client = new ErrorClient(); 348 | await assert.rejects(async () => { 349 | await client.ready(); 350 | }, err => { 351 | assert(err instanceof Error); 352 | assert.equal(err.message, 'init error'); 353 | return true; 354 | }); 355 | 356 | await assert.rejects(async () => { 357 | await client.ready(); 358 | }, err => { 359 | assert(err instanceof Error); 360 | assert.equal(err.message, 'init error'); 361 | return true; 362 | }); 363 | }); 364 | 365 | it('should trigger ready callback with err', done => { 366 | const client = new ErrorClient(); 367 | client.ready(err => { 368 | assert(err instanceof Error); 369 | assert.equal(err.message, 'init error'); 370 | done(); 371 | }); 372 | }); 373 | 374 | it('should emit error if ready failed', done => { 375 | const client = new ErrorClient(); 376 | client.once('error', err => { 377 | assert.equal(err.message, 'init error'); 378 | done(); 379 | }); 380 | console.error('listen error'); 381 | }); 382 | 383 | it('should not emit error event if readyCallback not empty', done => { 384 | const client = new ErrorClient(); 385 | client.ready(err => { 386 | assert(err instanceof Error); 387 | assert.equal(err.message, 'init error'); 388 | setImmediate(done); 389 | }); 390 | client.once('error', () => { 391 | done(new Error('should not run here')); 392 | }); 393 | }); 394 | }); 395 | 396 | function padding(cb: () => void, count: number) { 397 | return () => { 398 | count--; 399 | if (count === 0) { 400 | cb(); 401 | } 402 | }; 403 | } 404 | 405 | describe('generator event listener', () => { 406 | it('should add generator listener', done => { 407 | done = padding(done, 8); 408 | const client = new SomeServiceClient(); 409 | 410 | client.addListener('event_code', (a, b) => { 411 | console.log('event_code in addListener'); 412 | assert.equal(a, 1); 413 | assert.equal(b, 2); 414 | done(); 415 | }); 416 | 417 | client.on('event_code', (a, b) => { 418 | console.log('event_code in on'); 419 | assert(a === 1); 420 | assert(b === 2); 421 | done(); 422 | }); 423 | 424 | client.once('event_code', (a, b) => { 425 | console.log('event_code in once'); 426 | assert(a === 1); 427 | assert(b === 2); 428 | done(); 429 | }); 430 | 431 | client.prependListener('event_code', (a, b) => { 432 | console.log('event_code in prependListener'); 433 | assert(a === 1); 434 | assert(b === 2); 435 | done(); 436 | }); 437 | 438 | client.prependOnceListener('event_code', (a, b) => { 439 | console.log('event_code in prependOnceListener'); 440 | assert(a === 1); 441 | assert(b === 2); 442 | done(); 443 | }); 444 | 445 | client.emit('event_code', 1, 2); 446 | client.emit('event_code', 1, 2); 447 | }); 448 | 449 | // Don't catch event listener inside error 450 | // it('should catch generator exception and emit on error event', done => { 451 | // done = padding(done, 2); 452 | // const client = new SomeServiceClient(); 453 | // client.on('error', err => { 454 | // assert(err.name === 'EventListenerProcessError'); 455 | // assert(err.message === 'generator process exception'); 456 | // done(); 457 | // }); 458 | // client.on('event_code', () => { 459 | // throw new Error('normal function process exception'); 460 | // }); 461 | // client.once('event_code', async () => { 462 | // throw new Error('async function process exception'); 463 | // }); 464 | // client.emit('event_code'); 465 | // }); 466 | 467 | it('should remove listener', done => { 468 | const client = new SomeServiceClient(); 469 | const handler = async (data: number) => { 470 | assert(data === 1); 471 | done(); 472 | }; 473 | client.on('event_code', data => { 474 | assert(data === 1); 475 | console.log('normal listener'); 476 | }); 477 | client.on('event_code', handler); 478 | client.emit('event_code', 1); 479 | client.removeListener('event_code', handler); 480 | assert.equal(client.listeners('event_code').length, 0); 481 | }); 482 | 483 | // it('should not allow to add generator listener on error event', () => { 484 | // const client = new SomeServiceClient(); 485 | // assert.throws(() => { 486 | // client.on('error', function* (err) { 487 | // console.error(err); 488 | // }); 489 | // }, null, /\[sdk-base\] `error` event should not have a generator listener\./); 490 | // }); 491 | }); 492 | 493 | describe('await', () => { 494 | it('should support client.await', async () => { 495 | const client = new SomeServiceClient(); 496 | setTimeout(() => client.emit('someEvent', 'foo'), 100); 497 | const res = await client.await('someEvent'); 498 | assert.equal(res, 'foo'); 499 | }); 500 | 501 | // it('should support client.awaitFirst', function* () { 502 | // const client = new SomeServiceClient(); 503 | // setTimeout(() => client.emit('foo', 'foo'), 200); 504 | // setTimeout(() => client.emit('bar', 'bar'), 100); 505 | // const o = yield client.awaitFirst([ 'foo', 'bar' ]); 506 | // assert.deepEqual(o, { 507 | // event: 'bar', 508 | // args: [ 'bar' ], 509 | // }); 510 | // assert(client.listenerCount('foo') === 0); 511 | // assert(client.listenerCount('bar') === 0); 512 | // }); 513 | }); 514 | 515 | describe('close', () => { 516 | // describe('generate close', () => { 517 | // class GenerateCloseClient extends Base { 518 | // * _close() { 519 | // yield cb => process.nextTick(() => { 520 | // cb(); 521 | // }); 522 | // } 523 | // } 524 | // it('should success', function* () { 525 | // const client = new GenerateCloseClient(); 526 | // yield client.close(); 527 | // assert(client._closed === true); 528 | // }); 529 | // }); 530 | 531 | describe('promise close', () => { 532 | class PromiseCloseClient extends Base { 533 | _close() { 534 | return new Promise(resolve => { 535 | process.nextTick(() => { 536 | resolve(); 537 | }); 538 | }); 539 | } 540 | } 541 | 542 | it('should success', async () => { 543 | const client = new PromiseCloseClient(); 544 | assert.equal(client.isClosed, false); 545 | await client.close(); 546 | assert.equal(client.isClosed, true); 547 | }); 548 | }); 549 | 550 | describe('async function _close', () => { 551 | class PromiseCloseClient extends Base { 552 | async _close() { 553 | await scheduler.wait(10); 554 | } 555 | } 556 | 557 | it('should success', async () => { 558 | const client = new PromiseCloseClient(); 559 | assert.equal(client.isClosed, false); 560 | await client.close(); 561 | assert.equal(client.isClosed, true); 562 | await client.close(); 563 | assert.equal(client.isClosed, true); 564 | }); 565 | }); 566 | 567 | describe('no _close', () => { 568 | class NoCloseClient extends Base { 569 | } 570 | 571 | it('should success', async () => { 572 | const client = new NoCloseClient(); 573 | assert.equal(client.isClosed, false); 574 | await client.close(); 575 | assert.equal(client.isClosed, true); 576 | }); 577 | }); 578 | 579 | describe('duplicate close', () => { 580 | let calledTimes = 0; 581 | afterEach(() => { 582 | calledTimes = 0; 583 | }); 584 | 585 | class PromiseCloseClient extends Base { 586 | _close() { 587 | calledTimes++; 588 | return new Promise(resolve => { 589 | process.nextTick(() => { 590 | resolve(); 591 | }); 592 | }); 593 | } 594 | } 595 | 596 | describe('serial close', () => { 597 | it('should success', async () => { 598 | const client = new PromiseCloseClient(); 599 | await client.close(); 600 | await client.close(); 601 | assert(client.isClosed === true); 602 | assert(calledTimes === 1); 603 | }); 604 | }); 605 | 606 | describe('parallel close', () => { 607 | it('should success', async () => { 608 | const client = new PromiseCloseClient(); 609 | await Promise.all([ 610 | client.close(), 611 | client.close(), 612 | ]); 613 | assert.equal(client.isClosed, true); 614 | assert(calledTimes === 1); 615 | }); 616 | }); 617 | }); 618 | 619 | describe('error close', () => { 620 | class ErrorCloseClient extends Base { 621 | _close() { 622 | return new Promise((_, reject) => { 623 | reject(new Error('mock error')); 624 | }); 625 | } 626 | } 627 | 628 | it('should success', async () => { 629 | const client = new ErrorCloseClient(); 630 | let error: Error; 631 | client.on('error', e => { 632 | error = e; 633 | }); 634 | await client.close(); 635 | assert.equal(client.isClosed, true); 636 | assert(error!); 637 | assert(error instanceof Error); 638 | assert(/mock error/.test(error.message)); 639 | }); 640 | }); 641 | }); 642 | }); 643 | --------------------------------------------------------------------------------