├── .eslintignore ├── .gitignore ├── tsconfig.json ├── .eslintrc ├── .github └── workflows │ ├── release.yml │ └── nodejs.yml ├── test ├── index.test.ts └── ready.test.ts ├── package.json ├── CHANGELOG.md ├── src └── index.ts └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | .tshy* 4 | dist/ -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@eggjs/tsconfig", 3 | "compilerOptions": { 4 | "target": "es2022", 5 | "module": "nodenext", 6 | "moduleResolution": "nodenext" 7 | } 8 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint-config-egg/typescript", 4 | "eslint-config-egg/lib/rules/enforce-node-prefix" 5 | ], 6 | "rules": { 7 | "@typescript-eslint/no-empty-function": "off", 8 | "@typescript-eslint/no-unused-vars": "off" 9 | } 10 | } -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | release: 9 | name: Node.js 10 | uses: node-modules/github-actions/.github/workflows/node-release.yml@master 11 | secrets: 12 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 13 | GIT_TOKEN: ${{ secrets.GIT_TOKEN }} 14 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | pull_request: 8 | branches: [ master ] 9 | 10 | jobs: 11 | Job: 12 | name: Node.js 13 | uses: node-modules/github-actions/.github/workflows/node-test.yml@master 14 | with: 15 | os: 'ubuntu-latest' 16 | version: '16, 18, 20' 17 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'node:assert'; 2 | import { spy } from 'tinyspy'; 3 | import Koa = require('@eggjs/koa'); 4 | import ready from '../src/index.js'; 5 | 6 | describe('koa', function() { 7 | 8 | let app: any; 9 | beforeEach(function() { 10 | app = new Koa.default(); 11 | ready().mixin(app); 12 | }); 13 | 14 | it('should fire the callback after readyCallback call', function(done) { 15 | const spyReady = spy(); 16 | 17 | const endA = app.readyCallback('a'); 18 | const endB = app.readyCallback('b'); 19 | const endC = app.readyCallback('c'); 20 | const endD = app.readyCallback('d'); 21 | setTimeout(endA, 1); 22 | setTimeout(endB, 80); 23 | setTimeout(endC, 10); 24 | setTimeout(endD, 50); 25 | 26 | app.ready(spyReady); 27 | 28 | setTimeout(function() { 29 | assert(spyReady.callCount === 1); 30 | done(); 31 | }, 100); 32 | }); 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ready-callback", 3 | "version": "4.0.0", 4 | "description": "Launch server after all async task ready", 5 | "keywords": [ 6 | "koa", 7 | "ready", 8 | "async" 9 | ], 10 | "files": [ 11 | "dist", 12 | "src" 13 | ], 14 | "dependencies": { 15 | "get-ready": "^3.1.0", 16 | "once": "^1.4.0" 17 | }, 18 | "devDependencies": { 19 | "@eggjs/koa": "^2.15.1", 20 | "@eggjs/tsconfig": "^1.3.3", 21 | "@types/mocha": "^10.0.2", 22 | "@types/node": "^20.8.3", 23 | "@types/once": "^1.4.1", 24 | "egg-bin": "^6.5.2", 25 | "eslint": "^8.51.0", 26 | "eslint-config-egg": "^13.0.0", 27 | "git-contributor": "^2.1.5", 28 | "mocha": "^10.2.0", 29 | "tinyspy": "^2.2.0", 30 | "ts-node": "^10.9.1", 31 | "tshy": "^1.2.2", 32 | "tshy-after": "^1.0.0", 33 | "typescript": "^5.2.2" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/node-modules/ready-callback" 38 | }, 39 | "homepage": "https://github.com/node-modules/ready-callback", 40 | "author": "popomore ", 41 | "license": "MIT", 42 | "scripts": { 43 | "test": "npm run lint -- --fix && egg-bin test", 44 | "lint": "eslint src test --ext ts", 45 | "ci": "npm run lint && egg-bin cov && npm run prepublishOnly", 46 | "contributor": "git-contributor", 47 | "prepublishOnly": "tshy && tshy-after" 48 | }, 49 | "engines": { 50 | "node": ">=16.0.0" 51 | }, 52 | "ci": { 53 | "version": "16, 18, 20" 54 | }, 55 | "tshy": { 56 | "exports": { 57 | ".": "./src/index.ts" 58 | } 59 | }, 60 | "exports": { 61 | ".": { 62 | "import": { 63 | "types": "./dist/esm/index.d.ts", 64 | "default": "./dist/esm/index.js" 65 | }, 66 | "require": { 67 | "types": "./dist/commonjs/index.d.ts", 68 | "default": "./dist/commonjs/index.js" 69 | } 70 | } 71 | }, 72 | "type": "module", 73 | "types": "./dist/commonjs/index.d.ts" 74 | } 75 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [4.0.0](https://github.com/node-modules/ready-callback/compare/v3.0.0...v4.0.0) (2023-10-11) 4 | 5 | 6 | ### ⚠ BREAKING CHANGES 7 | 8 | * Drop Node.js < 16 support 9 | 10 | closes https://github.com/node-modules/ready-callback/issues/116 11 | 12 | part of https://github.com/eggjs/egg/issues/5257 13 | 14 | ### Features 15 | 16 | * refactor with typescript to support esm and cjs both ([#117](https://github.com/node-modules/ready-callback/issues/117)) ([7193ec1](https://github.com/node-modules/ready-callback/commit/7193ec1e8a2f8011af1bad584683da5530e0c226)) 17 | 18 | ## [3.0.0](https://github.com/node-modules/ready-callback/compare/v2.1.0...v3.0.0) (2023-01-03) 19 | 20 | 21 | ### ⚠ BREAKING CHANGES 22 | 23 | * Drop Node.js < 14 support and reduce dependencies 24 | 25 | ### Code Refactoring 26 | 27 | * Drop Node.js < 14 support ([#115](https://github.com/node-modules/ready-callback/issues/115)) ([f5bfa41](https://github.com/node-modules/ready-callback/commit/f5bfa414c9efe7de54b6975d4e8a22ebfbd15a25)) 28 | 29 | --- 30 | 31 | 2.1.0 / 2018-08-21 32 | ================== 33 | 34 | **features** 35 | * [[`37bd48f`](http://github.com/node-modules/ready-callback/commit/37bd48fe923982169d98f402a8e6bf3e5c7efc8a)] - feat: add lazyStart option (#113) (killa <>) 36 | 37 | 2.0.1 / 2017-02-13 38 | ================== 39 | 40 | * fix: should not throw error when pass null (#99) 41 | 42 | 2.0.0 / 2017-02-09 43 | ================== 44 | 45 | * feat: call .ready(err) when callback pass an error (#98) 46 | * chore(package): update mocha to version 2.3.3 47 | 48 | 1.0.0 / 2015-10-20 49 | ================== 50 | 51 | * refactor: koa-ready rename to ready-callback 52 | 53 | --- 54 | 55 | koa-ready histroy 56 | 57 | ## 1.0.0 / 2015-06-09 58 | 59 | - feat: set timeout when init 60 | - feat: add an option to set timeout when create a service 61 | 62 | ## 0.2.0 / 2015-03-17 63 | 64 | - chore: add appveyor and use npm scrips 65 | - feat: emit `ready_stat` when every task end 66 | - refactor: _readyCache use array instead of object 67 | - improve: testcase 68 | 69 | ## 0.1.1 / 2015-02-05 70 | 71 | - fix: do not fire endTask after throw error 72 | - fix: endTask is fired only once 73 | 74 | ## 0.1.0 75 | 76 | First commit 77 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'node:events'; 2 | import { debuglog } from 'node:util'; 3 | import { randomUUID } from 'node:crypto'; 4 | import once from 'once'; 5 | import { Ready as ReadyObject, type ReadyFunctionArg } from 'get-ready'; 6 | 7 | const debug = debuglog('ready-callback'); 8 | 9 | interface ReadyOption { 10 | timeout?: number; 11 | isWeakDep?: boolean; 12 | lazyStart?: boolean; 13 | } 14 | interface ReadyCallbackOption { 15 | name?: string; 16 | timeout?: number; 17 | isWeakDep?: boolean; 18 | } 19 | interface ReadyCallbackFn { 20 | (err?: any): void; 21 | id: string; 22 | } 23 | type ReadyCallbackCache = Map; 24 | 25 | const defaults: ReadyCallbackOption = { 26 | timeout: 10000, 27 | isWeakDep: false, 28 | }; 29 | 30 | /** 31 | * @class Ready 32 | */ 33 | class Ready extends EventEmitter { 34 | isError = false; 35 | cache: ReadyCallbackCache = new Map(); 36 | 37 | opt: ReadyOption; 38 | obj: any; 39 | 40 | ready: (flagOrFunction?: ReadyFunctionArg) => void; 41 | 42 | constructor(opt: ReadyOption = {}) { 43 | super(); 44 | ReadyObject.mixin(this); 45 | 46 | this.opt = opt; 47 | 48 | if (!this.opt.lazyStart) { 49 | this.start(); 50 | } 51 | } 52 | 53 | start() { 54 | setImmediate(() => { 55 | // fire callback directly when no registered ready callback 56 | if (this.cache.size === 0) { 57 | debug('Fire callback directly'); 58 | this.ready(true); 59 | } 60 | }); 61 | } 62 | 63 | /** 64 | * Mix `ready` and `readyCallback` to `obj` 65 | * @function Ready#mixin 66 | * @param {Object} obj - The mixed object 67 | * @return {Ready} this 68 | */ 69 | mixin(obj?: any) { 70 | // only mixin once 71 | if (!obj || this.obj) return null; 72 | 73 | // delegate API to object 74 | obj.ready = this.ready.bind(this); 75 | obj.readyCallback = this.readyCallback.bind(this); 76 | 77 | // only ready once with error 78 | this.once('error', err => obj.ready(err)); 79 | 80 | // delegate events 81 | if (obj.emit) { 82 | this.on('ready_timeout', obj.emit.bind(obj, 'ready_timeout')); 83 | this.on('ready_stat', obj.emit.bind(obj, 'ready_stat')); 84 | this.on('error', obj.emit.bind(obj, 'error')); 85 | } 86 | this.obj = obj; 87 | 88 | return this; 89 | } 90 | 91 | readyCallback(name: string, opt: ReadyCallbackOption = {}) { 92 | opt = Object.assign({}, defaults, this.opt, opt); 93 | const cacheKey = randomUUID(); 94 | opt.name = name || cacheKey; 95 | const timer = setTimeout(() => this.emit('ready_timeout', opt.name), opt.timeout); 96 | const cb = once((err?: any) => { 97 | if (err != null && !(err instanceof Error)) { 98 | err = new Error(err); 99 | } 100 | clearTimeout(timer); 101 | // won't continue to fire after it's error 102 | if (this.isError === true) return; 103 | // fire callback after all register 104 | setImmediate(() => this.readyDone(cacheKey, opt, err)); 105 | }) as unknown as ReadyCallbackFn; 106 | debug('[%s] Register task id `%s` with %j', cacheKey, opt.name, opt); 107 | cb.id = opt.name; 108 | this.cache.set(cacheKey, cb); 109 | return cb; 110 | } 111 | 112 | readyDone(id: string, opt: ReadyCallbackOption, err?: Error) { 113 | if (err != null && !opt.isWeakDep) { 114 | this.isError = true; 115 | debug('[%s] Throw error task id `%s`, error %s', id, opt.name, err); 116 | return this.emit('error', err); 117 | } 118 | 119 | debug('[%s] End task id `%s`, error %s', id, opt.name, err); 120 | this.cache.delete(id); 121 | 122 | this.emit('ready_stat', { 123 | id: opt.name, 124 | remain: getRemain(this.cache), 125 | }); 126 | 127 | if (this.cache.size === 0) { 128 | debug('[%s] Fire callback async', id); 129 | this.ready(true); 130 | } 131 | return this; 132 | } 133 | } 134 | 135 | function getRemain(map: ReadyCallbackCache) { 136 | const names: string[] = []; 137 | for (const cb of map.values()) { 138 | names.push(cb.id); 139 | } 140 | return names; 141 | } 142 | 143 | export { Ready }; 144 | 145 | export default function(opt: ReadyOption = {}) { 146 | return new Ready(opt); 147 | } 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ready-callback 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![CI](https://github.com/node-modules/ready-callback/actions/workflows/nodejs.yml/badge.svg)](https://github.com/node-modules/ready-callback/actions/workflows/nodejs.yml) 5 | [![Test coverage][codecov-image]][codecov-url] 6 | [![npm download][download-image]][download-url] 7 | 8 | [npm-image]: https://img.shields.io/npm/v/ready-callback.svg?style=flat-square 9 | [npm-url]: https://npmjs.org/package/ready-callback 10 | [codecov-image]: https://codecov.io/github/node-modules/ready-callback/coverage.svg?branch=master 11 | [codecov-url]: https://codecov.io/github/node-modules/ready-callback?branch=master 12 | [download-image]: https://img.shields.io/npm/dm/ready-callback.svg?style=flat-square 13 | [download-url]: https://npmjs.org/package/ready-callback 14 | 15 | Launch server after all async task ready 16 | 17 | --- 18 | 19 | ## Install 20 | 21 | ```bash 22 | $ npm install ready-callback 23 | ``` 24 | 25 | ## Usage 26 | 27 | **Note: ready-callback is using `class`, so you should use node>=2** 28 | 29 | ```js 30 | var koa = require('koa'); 31 | var ready = require('ready-callback')(); 32 | var app = koa(); 33 | ready.mixin(app); 34 | 35 | // register a service 36 | var done = app.readyCallback('service'); 37 | serviceLaunch(done); 38 | 39 | // callback will be fired after all service launched 40 | app.ready(function() { 41 | app.listen(); 42 | }); 43 | ``` 44 | 45 | ### Error Handle 46 | 47 | If task is called with error, `error` event will be emit, `ready` will never be called. 48 | 49 | ```js 50 | // register a service that will emit error 51 | var done = app.readyCallback('service'); 52 | serviceLaunch(function(err) { 53 | done(err); 54 | }); 55 | 56 | // listen error event 57 | app.on('error', function(err) { 58 | // catch error 59 | }); 60 | ``` 61 | 62 | ### Weak Dependency 63 | 64 | If you set a task weak dependency, task will be done without emit `error`. 65 | 66 | ```js 67 | var done = app.readyCallback('service', {isWeakDep: true}); 68 | serviceLaunch(function(err) { 69 | done(err); 70 | }); 71 | 72 | // will be ready 73 | app.ready(function() { 74 | app.listen(); 75 | }); 76 | 77 | app.on('error', function(err) { 78 | // never be called 79 | }); 80 | ``` 81 | 82 | You can also set for all ready-callback 83 | 84 | ```js 85 | var ready = require('ready-callback')({isWeakDep: true}); 86 | ``` 87 | 88 | ### Ready Status 89 | 90 | You can get status every callback end. 91 | 92 | ```js 93 | app.on('ready_stat', function(data) { 94 | console.log(data.id); // id of the ended task 95 | console.log(data.remain); // tasks waiting to be ended 96 | }); 97 | ``` 98 | 99 | ### Timeout 100 | 101 | You can set timeout when a task run a long time. 102 | 103 | ```js 104 | var ready = require('ready-callback')({timeout: 1000}); 105 | ready.mixin(app); 106 | app.on('ready_timeout', function(id) { 107 | // this will be called after 1s that `service` task don't complete 108 | }); 109 | 110 | var done = app.readyCallback('service'); 111 | serviceLaunch(function() { 112 | // run a long time 113 | done(); 114 | }); 115 | ``` 116 | 117 | You can also set timeout for every task 118 | 119 | ```js 120 | ready.mixin(app); 121 | app.on('ready_timeout', function(id) { 122 | // this will be called after 1s that `service` task don't complete 123 | }); 124 | 125 | var done = app.readyCallback('service1', {timeout: 1000}); 126 | serviceLaunch(done); 127 | ``` 128 | 129 | ### lazyStart 130 | 131 | You can set a ready-callback object to lazyStart. It will not check 132 | ready status immediately, and should start manualy to check ready 133 | status. 134 | 135 | ```js 136 | var ready = require('ready-callback')({ lazyStart: true }); 137 | yield sleep(1); 138 | // ready obj is not ready 139 | ready.start(); 140 | yield sleep(1); 141 | // ready obj is ready now 142 | ``` 143 | 144 | ## LISENCE 145 | 146 | Copyright (c) 2015 popomore. Licensed under the MIT license. 147 | 148 | 149 | 150 | ## Contributors 151 | 152 | |[
popomore](https://github.com/popomore)
|[
fengmk2](https://github.com/fengmk2)
|[
semantic-release-bot](https://github.com/semantic-release-bot)
|[
greenkeeperio-bot](https://github.com/greenkeeperio-bot)
|[
killagu](https://github.com/killagu)
|[
liuhanqu](https://github.com/liuhanqu)
| 153 | | :---: | :---: | :---: | :---: | :---: | :---: | 154 | 155 | 156 | This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Wed Oct 11 2023 10:27:33 GMT+0800`. 157 | 158 | 159 | -------------------------------------------------------------------------------- /test/ready.test.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'node:assert'; 2 | import EventEmitter from 'node:events'; 3 | import { spy } from 'tinyspy'; 4 | import { Ready } from '../src/index.js'; 5 | 6 | function sleep(ms) { 7 | return new Promise(resolve => { 8 | setTimeout(resolve, ms); 9 | }); 10 | } 11 | 12 | describe('Ready', function() { 13 | 14 | describe('without arguments', function() { 15 | 16 | let obj; 17 | let ready; 18 | beforeEach(function() { 19 | obj = new EventEmitter(); 20 | ready = new Ready(); 21 | ready.mixin(obj); 22 | }); 23 | 24 | it('should fire the callback when no task', function(done) { 25 | const spyReady = spy(); 26 | obj.ready(spyReady); 27 | ready.ready(spyReady); 28 | 29 | setTimeout(function() { 30 | assert(spyReady.callCount === 2); 31 | done(); 32 | }, 10); 33 | }); 34 | 35 | it('should fire the callback after sync call', function(done) { 36 | const spyReady = spy(); 37 | 38 | const endA = obj.readyCallback('a'); 39 | endA(); 40 | const endB = obj.readyCallback('b'); 41 | endB(); 42 | 43 | obj.ready(spyReady); 44 | ready.ready(spyReady); 45 | 46 | setTimeout(function() { 47 | assert(spyReady.callCount === 2); 48 | done(); 49 | }, 10); 50 | }); 51 | 52 | it('should fire the callback after readyCallback call', function(done) { 53 | const spyReady = spy(); 54 | 55 | const endA = obj.readyCallback('a'); 56 | const endB = obj.readyCallback('b'); 57 | const endC = obj.readyCallback('c'); 58 | const endD = obj.readyCallback('d'); 59 | setTimeout(endA, 1); 60 | setTimeout(endB, 80); 61 | setTimeout(endC, 10); 62 | setTimeout(endD, 50); 63 | 64 | ready.ready(spyReady); 65 | obj.ready(spyReady); 66 | 67 | setTimeout(function() { 68 | assert(spyReady.callCount === 2); 69 | done(); 70 | }, 100); 71 | }); 72 | 73 | it('should not fire the callback when one of the callback has not been called', function(done) { 74 | const spyReady = spy(); 75 | 76 | const endA = obj.readyCallback('a'); 77 | // callback b has not been called 78 | obj.readyCallback('b'); 79 | setTimeout(endA, 1); 80 | 81 | obj.ready(spyReady); 82 | ready.ready(spyReady); 83 | 84 | setTimeout(function() { 85 | assert(spyReady.callCount === 0); 86 | done(); 87 | }, 20); 88 | 89 | }); 90 | 91 | it('should emit error when one of the task fail', function(done) { 92 | const spyError = spy(); 93 | const spyReady = spy(); 94 | 95 | const endA = obj.readyCallback('a'); 96 | endA(new Error('aaa')); 97 | 98 | const endB = obj.readyCallback('b'); 99 | setTimeout(endB, 10); 100 | 101 | ready.on('error', spyError); 102 | obj.on('error', spyError); 103 | ready.ready(spyReady); 104 | obj.ready(spyReady); 105 | 106 | setTimeout(function() { 107 | assert(spyError.callCount === 2); 108 | assert(spyReady.callCount === 2); 109 | assert(spyReady.calls[0][0].message === 'aaa'); 110 | assert(spyReady.calls[1][0].message === 'aaa'); 111 | done(); 112 | }, 20); 113 | }); 114 | 115 | it('should fire readyCallback only once', function(done) { 116 | const spyReady = spy(); 117 | const spyError = spy(); 118 | 119 | const endA = obj.readyCallback('a'); 120 | const err = new Error('error'); 121 | setTimeout(function() { 122 | endA(err); 123 | endA(err); 124 | }, 1); 125 | 126 | obj.on('error', spyError); 127 | obj.ready(spyReady); 128 | 129 | setTimeout(function() { 130 | assert(spyError.callCount === 1); 131 | assert(spyReady.callCount === 1); 132 | assert.deepEqual(spyReady.calls[0][0], err); 133 | done(); 134 | }, 10); 135 | }); 136 | 137 | it('should not fire readyCallback after throw error', function(done) { 138 | const spyReady = spy(); 139 | const spyError = spy(); 140 | 141 | const endA = obj.readyCallback('a'); 142 | const endB = obj.readyCallback('b'); 143 | setTimeout(function() { 144 | endA(new Error('error')); 145 | }, 1); 146 | setTimeout(endB, 10); 147 | 148 | obj.on('error', spyError); 149 | obj.ready(spyReady); 150 | 151 | setTimeout(function() { 152 | assert(spyError.callCount === 1); 153 | assert(spyReady.callCount === 1); 154 | assert(spyReady.calls[0][0].message === 'error'); 155 | done(); 156 | }, 20); 157 | }); 158 | 159 | it('should work when readyCallback without name', function(done) { 160 | const spyReady = spy(); 161 | 162 | const endA = obj.readyCallback(); 163 | endA(); 164 | const endB = obj.readyCallback(); 165 | endB(); 166 | 167 | obj.ready(spyReady); 168 | ready.ready(spyReady); 169 | 170 | setTimeout(function() { 171 | assert(spyReady.callCount === 2); 172 | done(); 173 | }, 10); 174 | }); 175 | }); 176 | 177 | describe('ready stat', function() { 178 | 179 | it('should emit ready_stat when every task end', function(done) { 180 | const obj:any = new EventEmitter(); 181 | const ready = new Ready(); 182 | ready.mixin(obj); 183 | 184 | const data: {id: string; remain: string[]}[] = []; 185 | const timeout: string[] = []; 186 | obj.on('ready_stat', function(e) { 187 | data.push(e); 188 | }); 189 | obj.on('ready_timeout', function(id) { 190 | timeout.push(id); 191 | }); 192 | const endA = obj.readyCallback('a'); 193 | const endB = obj.readyCallback('b'); 194 | const endC = obj.readyCallback('c'); 195 | const endD = obj.readyCallback('d'); 196 | const endE = obj.readyCallback('e'); 197 | setTimeout(endA, 1); 198 | setTimeout(endB, 9000); 199 | setTimeout(endC, 10); 200 | setTimeout(endD, 50); 201 | setTimeout(endE, 11000); 202 | 203 | setTimeout(function() { 204 | assert.deepEqual(timeout, [ 'e' ]); 205 | assert.deepEqual(data, [{ 206 | id: 'a', 207 | remain: [ 'b', 'c', 'd', 'e' ], 208 | }, { 209 | id: 'c', 210 | remain: [ 'b', 'd', 'e' ], 211 | }, { 212 | id: 'd', 213 | remain: [ 'b', 'e' ], 214 | }, { 215 | id: 'b', 216 | remain: [ 'e' ], 217 | }, { 218 | id: 'e', 219 | remain: [], 220 | }]); 221 | done(); 222 | }, 11200); 223 | }); 224 | 225 | }); 226 | 227 | describe('timeout', function() { 228 | 229 | let obj, 230 | ready; 231 | beforeEach(function() { 232 | obj = new EventEmitter(); 233 | ready = new Ready(); 234 | ready.mixin(obj); 235 | }); 236 | 237 | it('should emit ready_timeout', function(done) { 238 | const spyTimeout = spy(); 239 | const spyReady = spy(); 240 | 241 | obj.on('ready_timeout', spyTimeout); 242 | obj.ready(spyReady); 243 | 244 | const endA = obj.readyCallback('a', { timeout: 50 }); 245 | const endB = obj.readyCallback('b', { timeout: 50 }); 246 | const endC = obj.readyCallback('c'); 247 | const endD = obj.readyCallback('d', { timeout: 50 }); 248 | setTimeout(endA, 100); 249 | setTimeout(endB, 10); 250 | setTimeout(endC, 1); 251 | setTimeout(endD, 80); 252 | 253 | setTimeout(function() { 254 | assert(spyReady.callCount === 1); 255 | assert(spyTimeout.callCount === 2); 256 | assert.deepEqual(spyTimeout.calls[0], [ 'a' ]); 257 | assert.deepEqual(spyTimeout.calls[1], [ 'd' ]); 258 | done(); 259 | }, 150); 260 | }); 261 | 262 | it('should emit ready_timeout with global timeout', function(done) { 263 | obj = new EventEmitter(); 264 | ready = new Ready({ timeout: 50 }); 265 | ready.mixin(obj); 266 | 267 | const spyTimeout = spy(); 268 | const spyReady = spy(); 269 | 270 | obj.on('ready_timeout', spyTimeout); 271 | obj.ready(spyReady); 272 | 273 | const endA = obj.readyCallback('a'); 274 | const endB = obj.readyCallback('b'); 275 | const endC = obj.readyCallback('c', { timeout: 0 }); 276 | const endD = obj.readyCallback('d', { timeout: 100 }); 277 | setTimeout(endA, 100); 278 | setTimeout(endB, 10); 279 | setTimeout(endC, 1); 280 | setTimeout(endD, 80); 281 | 282 | setTimeout(function() { 283 | assert(spyReady.callCount === 1); 284 | assert(spyTimeout.callCount === 2); 285 | assert.deepEqual(spyTimeout.calls[0], [ 'c' ]); 286 | assert.deepEqual(spyTimeout.calls[1], [ 'a' ]); 287 | done(); 288 | }, 150); 289 | }); 290 | 291 | it('should clearTimeout', function(done) { 292 | const spyTimeout = spy(); 293 | const spyReady = spy(); 294 | 295 | obj.on('ready_timeout', spyTimeout); 296 | obj.ready(spyReady); 297 | 298 | const endA = obj.readyCallback('a', { timeout: 50 }); 299 | const endB = obj.readyCallback('b', { timeout: 50 }); 300 | setTimeout(endA, 10); 301 | setTimeout(endB, 10); 302 | 303 | setTimeout(function() { 304 | assert(spyReady.callCount === 1); 305 | assert(spyTimeout.called === false); 306 | done(); 307 | }, 100); 308 | }); 309 | 310 | }); 311 | 312 | describe('weakDep', function() { 313 | 314 | let obj; 315 | let ready; 316 | 317 | it('should fire the callback when weakDep', function(done) { 318 | obj = new EventEmitter(); 319 | ready = new Ready(); 320 | ready.mixin(obj); 321 | 322 | const spyError = spy(); 323 | const spyReady = spy(); 324 | 325 | const endA = obj.readyCallback('a', { isWeakDep: true }); 326 | endA(new Error('error')); 327 | 328 | const endB = obj.readyCallback('b'); 329 | setTimeout(endB, 10); 330 | 331 | ready.on('error', spyError); 332 | obj.on('error', spyError); 333 | ready.ready(spyReady); 334 | obj.ready(spyReady); 335 | 336 | setTimeout(function() { 337 | assert(spyError.called === false); 338 | assert(spyReady.callCount === 2); 339 | done(); 340 | }, 20); 341 | }); 342 | 343 | it('should fire ready when global weakDep', function(done) { 344 | obj = new EventEmitter(); 345 | ready = new Ready({ isWeakDep: true }); 346 | ready.mixin(obj); 347 | 348 | const spyError = spy(); 349 | const spyReady = spy(); 350 | 351 | const endA = obj.readyCallback('a', { isWeakDep: false }); 352 | const err = new Error('error'); 353 | endA(err); 354 | 355 | const endB = obj.readyCallback('b'); 356 | setTimeout(endB, 10); 357 | 358 | obj.on('error', spyError); 359 | obj.ready(spyReady); 360 | 361 | setTimeout(function() { 362 | assert(spyError.callCount === 1); 363 | assert(spyReady.callCount === 1); 364 | assert.deepEqual(spyReady.calls[0][0], err); 365 | done(); 366 | }, 20); 367 | }); 368 | }); 369 | 370 | describe('error', () => { 371 | it('should get error in ready', done => { 372 | const obj:any = {}; 373 | const ready = new Ready(); 374 | ready.mixin(obj); 375 | 376 | const end = obj.readyCallback('a'); 377 | obj.ready(err => { 378 | assert(err.message === 'error'); 379 | done(); 380 | }); 381 | end(new Error('error')); 382 | }); 383 | 384 | it('should ready with error after callback error', done => { 385 | const obj:any = {}; 386 | const ready = new Ready(); 387 | ready.mixin(obj); 388 | 389 | const end = obj.readyCallback('a'); 390 | end(new Error('error')); 391 | obj.ready(err => { 392 | assert(err.message === 'error'); 393 | done(); 394 | }); 395 | }); 396 | 397 | it('should get error object when pass other type in callback', async function() { 398 | let err = await assertErrorType('error'); 399 | assert(err.message === 'error'); 400 | 401 | err = await assertErrorType(0); 402 | assert(err.message === '0'); 403 | 404 | err = await assertErrorType([ '1', '2' ]); 405 | assert(err.message === '1,2'); 406 | 407 | err = await assertErrorType({}); 408 | assert(err.message === '[object Object]'); 409 | 410 | err = await assertErrorType(true); 411 | assert(err.message === 'true'); 412 | 413 | err = await assertErrorType(null); 414 | assert(err === undefined); 415 | 416 | function assertErrorType(value) { 417 | const obj:any = {}; 418 | const ready = new Ready(); 419 | ready.mixin(obj); 420 | 421 | const end = obj.readyCallback('a'); 422 | end(value); 423 | return obj.ready().catch(err => err); 424 | } 425 | }); 426 | 427 | }); 428 | 429 | it('should not throw when mixin an object that do not support events', function(done) { 430 | const obj:any = {}; 431 | const ready = new Ready(); 432 | ready.mixin(obj); 433 | 434 | const spyReady = spy(); 435 | const spyError = spy(); 436 | const end = obj.readyCallback('a'); 437 | const err = new Error('error'); 438 | end(err); 439 | 440 | ready.ready(spyReady); 441 | ready.on('error', spyError); 442 | assert(!obj.on); 443 | obj.ready(spyReady); 444 | 445 | setTimeout(function() { 446 | assert(spyReady.called === true); 447 | assert.deepEqual(spyReady.calls[0][0], err); 448 | assert(spyError.callCount === 1); 449 | done(); 450 | }, 10); 451 | }); 452 | 453 | it('should mixin only once', function() { 454 | const ready = new Ready(); 455 | assert(!ready.obj); 456 | 457 | ready.mixin(); 458 | assert(!ready.obj); 459 | 460 | const obj1 = {}, 461 | obj2 = {}; 462 | ready.mixin(obj1); 463 | assert(ready.obj === obj1); 464 | 465 | ready.mixin(obj2); 466 | assert(ready.obj === obj1); 467 | assert(ready.obj !== obj2); 468 | }); 469 | 470 | describe('lazy start', function() { 471 | it('should ready after start manually', async function() { 472 | const obj = new EventEmitter(); 473 | const ready = new Ready({ lazyStart: true }); 474 | const readySpy = spy(); 475 | ready.ready(readySpy); 476 | ready.mixin(obj); 477 | await sleep(1); 478 | assert(readySpy.called === false); 479 | ready.start(); 480 | await sleep(1); 481 | assert(readySpy.called); 482 | }); 483 | }); 484 | }); 485 | --------------------------------------------------------------------------------