buddy

node MVC discord bot
Log | Files | Refs | README

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;