Collector.js (8023B)
1 'use strict'; 2 3 const EventEmitter = require('events'); 4 const Collection = require('../../util/Collection'); 5 const Util = require('../../util/Util'); 6 7 /** 8 * Filter to be applied to the collector. 9 * @typedef {Function} CollectorFilter 10 * @param {...*} args Any arguments received by the listener 11 * @param {Collection} collection The items collected by this collector 12 * @returns {boolean} 13 */ 14 15 /** 16 * Options to be applied to the collector. 17 * @typedef {Object} CollectorOptions 18 * @property {number} [time] How long to run the collector for in milliseconds 19 * @property {number} [idle] How long to stop the collector after inactivity in milliseconds 20 * @property {boolean} [dispose=false] Whether to dispose data when it's deleted 21 */ 22 23 /** 24 * Abstract class for defining a new Collector. 25 * @abstract 26 */ 27 class Collector extends EventEmitter { 28 constructor(client, filter, options = {}) { 29 super(); 30 31 /** 32 * The client that instantiated this Collector 33 * @name Collector#client 34 * @type {Client} 35 * @readonly 36 */ 37 Object.defineProperty(this, 'client', { value: client }); 38 39 /** 40 * The filter applied to this collector 41 * @type {CollectorFilter} 42 */ 43 this.filter = filter; 44 45 /** 46 * The options of this collector 47 * @type {CollectorOptions} 48 */ 49 this.options = options; 50 51 /** 52 * The items collected by this collector 53 * @type {Collection} 54 */ 55 this.collected = new Collection(); 56 57 /** 58 * Whether this collector has finished collecting 59 * @type {boolean} 60 */ 61 this.ended = false; 62 63 /** 64 * Timeout for cleanup 65 * @type {?Timeout} 66 * @private 67 */ 68 this._timeout = null; 69 70 /** 71 * Timeout for cleanup due to inactivity 72 * @type {?Timeout} 73 * @private 74 */ 75 this._idletimeout = null; 76 77 this.handleCollect = this.handleCollect.bind(this); 78 this.handleDispose = this.handleDispose.bind(this); 79 80 if (options.time) this._timeout = this.client.setTimeout(() => this.stop('time'), options.time); 81 if (options.idle) this._idletimeout = this.client.setTimeout(() => this.stop('idle'), options.idle); 82 } 83 84 /** 85 * Call this to handle an event as a collectable element. Accepts any event data as parameters. 86 * @param {...*} args The arguments emitted by the listener 87 * @emits Collector#collect 88 */ 89 handleCollect(...args) { 90 const collect = this.collect(...args); 91 92 if (collect && this.filter(...args, this.collected)) { 93 this.collected.set(collect, args[0]); 94 95 /** 96 * Emitted whenever an element is collected. 97 * @event Collector#collect 98 * @param {...*} args The arguments emitted by the listener 99 */ 100 this.emit('collect', ...args); 101 102 if (this._idletimeout) { 103 this.client.clearTimeout(this._idletimeout); 104 this._idletimeout = this.client.setTimeout(() => this.stop('idle'), this.options.idle); 105 } 106 } 107 this.checkEnd(); 108 } 109 110 /** 111 * Call this to remove an element from the collection. Accepts any event data as parameters. 112 * @param {...*} args The arguments emitted by the listener 113 * @emits Collector#dispose 114 */ 115 handleDispose(...args) { 116 if (!this.options.dispose) return; 117 118 const dispose = this.dispose(...args); 119 if (!dispose || !this.filter(...args) || !this.collected.has(dispose)) return; 120 this.collected.delete(dispose); 121 122 /** 123 * Emitted whenever an element is disposed of. 124 * @event Collector#dispose 125 * @param {...*} args The arguments emitted by the listener 126 */ 127 this.emit('dispose', ...args); 128 this.checkEnd(); 129 } 130 131 /** 132 * Returns a promise that resolves with the next collected element; 133 * rejects with collected elements if the collector finishes without receiving a next element 134 * @type {Promise} 135 * @readonly 136 */ 137 get next() { 138 return new Promise((resolve, reject) => { 139 if (this.ended) { 140 reject(this.collected); 141 return; 142 } 143 144 const cleanup = () => { 145 this.removeListener('collect', onCollect); 146 this.removeListener('end', onEnd); 147 }; 148 149 const onCollect = item => { 150 cleanup(); 151 resolve(item); 152 }; 153 154 const onEnd = () => { 155 cleanup(); 156 reject(this.collected); // eslint-disable-line prefer-promise-reject-errors 157 }; 158 159 this.on('collect', onCollect); 160 this.on('end', onEnd); 161 }); 162 } 163 164 /** 165 * Stops this collector and emits the `end` event. 166 * @param {string} [reason='user'] The reason this collector is ending 167 * @emits Collector#end 168 */ 169 stop(reason = 'user') { 170 if (this.ended) return; 171 172 if (this._timeout) { 173 this.client.clearTimeout(this._timeout); 174 this._timeout = null; 175 } 176 if (this._idletimeout) { 177 this.client.clearTimeout(this._idletimeout); 178 this._idletimeout = null; 179 } 180 this.ended = true; 181 182 /** 183 * Emitted when the collector is finished collecting. 184 * @event Collector#end 185 * @param {Collection} collected The elements collected by the collector 186 * @param {string} reason The reason the collector ended 187 */ 188 this.emit('end', this.collected, reason); 189 } 190 191 /** 192 * Resets the collectors timeout and idle timer. 193 * @param {Object} [options] Options 194 * @param {number} [options.time] How long to run the collector for in milliseconds 195 * @param {number} [options.idle] How long to stop the collector after inactivity in milliseconds 196 */ 197 resetTimer({ time, idle } = {}) { 198 if (this._timeout) { 199 this.client.clearTimeout(this._timeout); 200 this._timeout = this.client.setTimeout(() => this.stop('time'), time || this.options.time); 201 } 202 if (this._idletimeout) { 203 this.client.clearTimeout(this._idletimeout); 204 this._idletimeout = this.client.setTimeout(() => this.stop('idle'), idle || this.options.idle); 205 } 206 } 207 208 /** 209 * Checks whether the collector should end, and if so, ends it. 210 */ 211 checkEnd() { 212 const reason = this.endReason(); 213 if (reason) this.stop(reason); 214 } 215 216 /** 217 * Allows collectors to be consumed with for-await-of loops 218 * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of} 219 */ 220 async *[Symbol.asyncIterator]() { 221 const queue = []; 222 const onCollect = item => queue.push(item); 223 this.on('collect', onCollect); 224 225 try { 226 while (queue.length || !this.ended) { 227 if (queue.length) { 228 yield queue.shift(); 229 } else { 230 // eslint-disable-next-line no-await-in-loop 231 await new Promise(resolve => { 232 const tick = () => { 233 this.removeListener('collect', tick); 234 this.removeListener('end', tick); 235 return resolve(); 236 }; 237 this.on('collect', tick); 238 this.on('end', tick); 239 }); 240 } 241 } 242 } finally { 243 this.removeListener('collect', onCollect); 244 } 245 } 246 247 toJSON() { 248 return Util.flatten(this); 249 } 250 251 /* eslint-disable no-empty-function, valid-jsdoc */ 252 /** 253 * Handles incoming events from the `handleCollect` function. Returns null if the event should not 254 * be collected, or returns an object describing the data that should be stored. 255 * @see Collector#handleCollect 256 * @param {...*} args Any args the event listener emits 257 * @returns {?{key, value}} Data to insert into collection, if any 258 * @abstract 259 */ 260 collect() {} 261 262 /** 263 * Handles incoming events from the `handleDispose`. Returns null if the event should not 264 * be disposed, or returns the key that should be removed. 265 * @see Collector#handleDispose 266 * @param {...*} args Any args the event listener emits 267 * @returns {?*} Key to remove from the collection, if any 268 * @abstract 269 */ 270 dispose() {} 271 272 /** 273 * The reason this collector has ended or will end with. 274 * @returns {?string} Reason to end the collector, if any 275 * @abstract 276 */ 277 endReason() {} 278 /* eslint-enable no-empty-function, valid-jsdoc */ 279 } 280 281 module.exports = Collector;