333
schangxiang@126.com
2025-09-19 18966e02fb573c7e2bb0c6426ed792b38b910940
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
'use strict';
 
const EventEmitter = require('events');
const once = require('once');
const ready = require('get-ready');
const uuid = require('uuid');
const debug = require('debug')('ready-callback');
 
const defaults = {
  timeout: 10000,
  isWeakDep: false,
};
 
/**
 * @class Ready
 */
class Ready extends EventEmitter {
 
  /**
   * @constructor
   * @param  {Object} opt
   *   - {Number} [timeout=10000] - emit `ready_timeout` when it doesn't finish but reach the timeout
   *   - {Boolean} [isWeakDep=false] - whether it's a weak dependency
   *   - {Boolean} [lazyStart=false] - will not check cache size automatically, if lazyStart is true
   */
  constructor(opt) {
    super();
    ready.mixin(this);
 
    this.opt = opt || {};
    this.isError = false;
    this.cache = new Map();
 
    if (!this.opt.lazyStart) {
      this.start();
    }
  }
 
  start() {
    setImmediate(() => {
      // fire callback directly when no registered ready callback
      if (this.cache.size === 0) {
        debug('Fire callback directly');
        this.ready(true);
      }
    });
  }
 
  /**
   * Mix `ready` and `readyCallback` to `obj`
   * @method Ready#mixin
   * @param  {Object} obj - The mixed object
   * @return {Ready} this
   */
  mixin(obj) {
    // only mixin once
    if (!obj || this.obj) return null;
 
    // delegate API to object
    obj.ready = this.ready.bind(this);
    obj.readyCallback = this.readyCallback.bind(this);
 
    // only ready once with error
    this.once('error', err => obj.ready(err));
 
    // delegate events
    if (obj.emit) {
      this.on('ready_timeout', obj.emit.bind(obj, 'ready_timeout'));
      this.on('ready_stat', obj.emit.bind(obj, 'ready_stat'));
      this.on('error', obj.emit.bind(obj, 'error'));
    }
    this.obj = obj;
 
    return this;
  }
 
  /**
   * Create a callback, ready won't be fired until all the callbacks are triggered.
   * @method Ready#readyCallback
   * @param  {String} name -
   * @param  {Object} opt - the options that will override global
   * @return {Function} - a callback
   */
  readyCallback(name, opt) {
    opt = Object.assign({}, defaults, this.opt, opt);
    const cacheKey = uuid.v1();
    opt.name = name || cacheKey;
    const timer = setTimeout(() => this.emit('ready_timeout', opt.name), opt.timeout);
    const cb = once(err => {
      if (err != null && !(err instanceof Error)) {
        err = new Error(err);
      }
      clearTimeout(timer);
      // won't continue to fire after it's error
      if (this.isError === true) return;
      // fire callback after all register
      setImmediate(() => this.readyDone(cacheKey, opt, err));
    });
    debug('[%s] Register task id `%s` with %j', cacheKey, opt.name, opt);
    cb.id = opt.name;
    this.cache.set(cacheKey, cb);
    return cb;
  }
 
  /**
   * resolve ths callback when readyCallback be called
   * @method Ready#readyDone
   * @private
   * @param  {String} id - unique id generated by readyCallback
   * @param  {Object} opt - the options that will override global
   * @param  {Error} err - err passed by ready callback
   * @return {Ready} this
   */
  readyDone(id, opt, err) {
    if (err != null && !opt.isWeakDep) {
      this.isError = true;
      debug('[%s] Throw error task id `%s`, error %s', id, opt.name, err);
      return this.emit('error', err);
    }
 
    debug('[%s] End task id `%s`, error %s', id, opt.name, err);
    this.cache.delete(id);
 
    this.emit('ready_stat', {
      id: opt.name,
      remain: getRemain(this.cache),
    });
 
    if (this.cache.size === 0) {
      debug('[%s] Fire callback async', id);
      this.ready(true);
    }
    return this;
  }
 
}
 
// Use ready-callback with options
module.exports = opt => new Ready(opt);
module.exports.Ready = Ready;
 
function getRemain(map) {
  const names = [];
  for (const cb of map.values()) {
    names.push(cb.id);
  }
  return names;
}