wangzhibo
2026-05-09 53418655c061ac26f66058253f39200aced1ab1d
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
149
150
151
152
153
154
155
156
157
158
159
160
161
'use strict'
 
const idGeneratorFactory = require('./id-generator')
const purgeAbandoned = require('./purge-abandoned')
 
/**
 * Returns a message tracker object that keeps track of which message
 * identifiers correspond to which message handlers. Also handles keeping track
 * of abandoned messages.
 *
 * @param {object} options
 * @param {string} options.id An identifier for the tracker.
 * @param {object} options.parser An object that will be used to parse messages.
 *
 * @returns {MessageTracker}
 */
module.exports = function messageTrackerFactory (options) {
  if (Object.prototype.toString.call(options) !== '[object Object]') {
    throw Error('options object is required')
  }
  if (!options.id || typeof options.id !== 'string') {
    throw Error('options.id string is required')
  }
  if (!options.parser || Object.prototype.toString.call(options.parser) !== '[object Object]') {
    throw Error('options.parser object is required')
  }
 
  let currentID = 0
  const nextID = idGeneratorFactory()
  const messages = new Map()
  const abandoned = new Map()
 
  /**
   * @typedef {object} MessageTracker
   * @property {string} id The identifier of the tracker as supplied via the options.
   * @property {object} parser The parser object given by the the options.
   */
  const tracker = {
    id: options.id,
    parser: options.parser
  }
 
  /**
   * Count of messages awaiting response.
   *
   * @alias pending
   * @memberof! MessageTracker#
   */
  Object.defineProperty(tracker, 'pending', {
    get () {
      return messages.size
    }
  })
 
  /**
   * Move a specific message to the abanded track.
   *
   * @param {integer} msgID The identifier for the message to move.
   *
   * @memberof MessageTracker
   * @method abandon
   */
  tracker.abandon = function abandonMessage (msgID) {
    if (messages.has(msgID) === false) return false
    const toAbandon = messages.get(msgID)
    abandoned.set(msgID, {
      age: currentID,
      message: toAbandon.message,
      cb: toAbandon.callback
    })
    return messages.delete(msgID)
  }
 
  /**
   * @typedef {object} Tracked
   * @property {object} message The tracked message. Usually the outgoing
   * request object.
   * @property {Function} callback The handler to use when receiving a
   * response to the tracked message.
   */
 
  /**
   * Retrieves the message handler for a message. Removes abandoned messages
   * that have been given time to be resolved.
   *
   * @param {integer} msgID The identifier for the message to get the handler for.
   *
   * @memberof MessageTracker
   * @method fetch
   */
  tracker.fetch = function fetchMessage (msgID) {
    const tracked = messages.get(msgID)
    if (tracked) {
      purgeAbandoned(msgID, abandoned)
      return tracked
    }
 
    // We sent an abandon request but the server either wasn't able to process
    // it or has not received it yet. Therefore, we received a response for the
    // abandoned message. So we must return the abandoned message's callback
    // to be processed normally.
    const abandonedMsg = abandoned.get(msgID)
    if (abandonedMsg) {
      return { message: abandonedMsg, callback: abandonedMsg.cb }
    }
 
    return null
  }
 
  /**
   * Removes all message tracks, cleans up the abandoned track, and invokes
   * a callback for each message purged.
   *
   * @param {function} cb A function with the signature `(msgID, handler)`.
   *
   * @memberof MessageTracker
   * @method purge
   */
  tracker.purge = function purgeMessages (cb) {
    messages.forEach((val, key) => {
      purgeAbandoned(key, abandoned)
      tracker.remove(key)
      cb(key, val.callback)
    })
  }
 
  /**
   * Removes a message from all tracking.
   *
   * @param {integer} msgID The identifier for the message to remove from tracking.
   *
   * @memberof MessageTracker
   * @method remove
   */
  tracker.remove = function removeMessage (msgID) {
    if (messages.delete(msgID) === false) {
      abandoned.delete(msgID)
    }
  }
 
  /**
   * Add a message handler to be tracked.
   *
   * @param {object} message The message object to be tracked. This object will
   * have a new property added to it: `messageId`.
   * @param {function} callback The handler for the message.
   *
   * @memberof MessageTracker
   * @method track
   */
  tracker.track = function trackMessage (message, callback) {
    currentID = nextID()
    // This side effect is not ideal but the client doesn't attach the tracker
    // to itself until after the `.connect` method has fired. If this can be
    // refactored later, then we can possibly get rid of this side effect.
    message.messageId = currentID
    messages.set(currentID, { callback, message })
  }
 
  return tracker
}