dexare
Version:
Modular and extendable Discord bot framework
255 lines (219 loc) • 8.1 kB
text/typescript
import Collection from '@discordjs/collection';
import EventEmitter from 'eventemitter3';
import CollectorModule from '.';
import DexareClient, { DexareEvents } from '../../client';
import { EventHandlers } from '../../client/events';
import TypedEmitter from '../../util/typedEmitter';
/** @hidden */
export type CollectorEvents = {
collect: (...args: any[]) => void;
dispose: (...args: any[]) => void;
end: (collected: Collection<any, any>, reason: string) => void;
};
/** @hidden */
export type CollectorFilter = (...args: any[]) => boolean | Promise<boolean>;
/** The options for a {@link Collector}. */
export interface CollectorOptions {
/** How long to run the collector for in milliseconds */
time?: number;
/** How long to stop the collector after inactivity in milliseconds */
idle?: number;
/** Whether to dispose data when it's deleted */
dispose?: boolean;
}
/** The options for {@link Collector#resetTimer}. */
export interface ResetTimerOptions {
/** How long to run the collector for in milliseconds */
time?: number;
/** How long to stop the collector after inactivity in milliseconds */
idle?: number;
}
/** Class for defining a collector. */
export default class Collector extends (EventEmitter as any as new () => TypedEmitter<CollectorEvents>) {
readonly module: CollectorModule<DexareClient<any>>;
readonly client: DexareClient;
/** The filter applied to this collector */
readonly filter: CollectorFilter;
/** The options of this collector */
readonly options: CollectorOptions;
/** The items collected by this collector */
readonly collected = new Collection<any, any>();
/** Whether this collector has finished collecting */
ended = false;
// eslint-disable-next-line no-undef
private _timeout: NodeJS.Timeout | null = null;
// eslint-disable-next-line no-undef
private _idletimeout: NodeJS.Timeout | null = null;
readonly id: string;
constructor(
collectorModule: CollectorModule<DexareClient<any>>,
filter: CollectorFilter,
options: CollectorOptions = {}
) {
// eslint-disable-next-line constructor-super
super();
this.module = collectorModule;
this.client = collectorModule.client;
this.filter = filter;
this.options = options;
this.id = (Date.now() + Math.round(Math.random() * 1000)).toString(36);
if (typeof filter !== 'function') throw new TypeError('INVALID_TYPE');
this.handleCollect = this.handleCollect.bind(this);
this.handleDispose = this.handleDispose.bind(this);
if (options.time) this._timeout = setTimeout(() => this.stop('time'), options.time);
if (options.idle) this._idletimeout = setTimeout(() => this.stop('idle'), options.idle);
this.module.activeCollectors.set(this.id, this);
}
registerEvent<E extends keyof DexareEvents>(
event: E,
handler: EventHandlers[E],
options?: { before?: string[]; after?: string[] }
) {
return this.client.events.register('collector:' + this.id, event, handler, options);
}
/**
* Call this to handle an event as a collectable element. Accepts any event data as parameters.
* @param args The arguments emitted by the listener
*/
async handleCollect(...args: any[]) {
const collect = this.collect(...args);
if (collect && (await this.filter(...args, this.collected))) {
this.collected.set(collect.key, collect.value);
this.emit('collect', ...args);
if (this._idletimeout) {
clearTimeout(this._idletimeout);
this._idletimeout = setTimeout(() => this.stop('idle'), this.options.idle!);
}
}
this.checkEnd();
}
/**
* Call this to remove an element from the collection. Accepts any event data as parameters.
* @param args The arguments emitted by the listener
*/
handleDispose(...args: any[]) {
if (!this.options.dispose) return;
const dispose = this.dispose(...args);
// deepscan-disable-next-line CONSTANT_CONDITION
if (!dispose || !this.filter(...args) || !this.collected.has(dispose)) return;
this.collected.delete(dispose);
this.emit('dispose', ...args);
this.checkEnd();
}
/**
* Returns a promise that resolves with the next collected element;
* rejects with collected elements if the collector finishes without receiving a next element
*/
get next() {
return new Promise((resolve, reject) => {
if (this.ended) {
reject(this.collected);
return;
}
const cleanup = () => {
this.removeListener('collect', onCollect);
this.removeListener('end', onEnd);
};
const onCollect = (item: any) => {
cleanup();
resolve(item);
};
const onEnd = () => {
cleanup();
reject(this.collected);
};
this.on('collect', onCollect);
this.on('end', onEnd);
});
}
/**
* Stops this collector and emits the `end` event.
* @param reason the reason this collector is ending
*/
stop(reason = 'user') {
if (this.ended) return;
if (this._timeout) {
clearTimeout(this._timeout);
this._timeout = null;
}
if (this._idletimeout) {
clearTimeout(this._idletimeout);
this._idletimeout = null;
}
this.ended = true;
this.client.events.unregisterGroup('collector:' + this.id);
this.module.activeCollectors.delete(this.id);
this.emit('end', this.collected, reason);
}
/**
* Resets the collectors timeout and idle timer.
* @param {Object} [options] Options
* @param {number} [options.time] How long to run the collector for in milliseconds
* @param {number} [options.idle] How long to stop the collector after inactivity in milliseconds
*/
resetTimer(options: ResetTimerOptions = {}) {
if (this._timeout) {
clearTimeout(this._timeout);
this._timeout = setTimeout(() => this.stop('time'), (options && options.time) || this.options.time!);
}
if (this._idletimeout) {
clearTimeout(this._idletimeout);
this._idletimeout = setTimeout(
() => this.stop('idle'),
(options && options.idle) || this.options.idle!
);
}
}
/** Checks whether the collector should end, and if so, ends it. */
checkEnd() {
const reason = this.endReason();
if (reason) this.stop(reason);
}
/**
* Allows collectors to be consumed with for-await-of loops
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of}
*/
async *[Symbol.asyncIterator]() {
const queue: any[] = [];
const onCollect = (item: any) => queue.push(item);
this.on('collect', onCollect);
try {
while (queue.length || !this.ended) {
if (queue.length) {
yield queue.shift();
} else {
await new Promise<void>((resolve) => {
const tick = () => {
this.removeListener('collect', tick);
this.removeListener('end', tick);
return resolve();
};
this.on('collect', tick);
this.on('end', tick);
});
}
}
} finally {
this.removeListener('collect', onCollect);
}
}
/* eslint-disable no-empty-function, @typescript-eslint/no-unused-vars */
/**
* Handles incoming events from the `handleCollect` function. Returns null if the event should not
* be collected, or returns an object describing the data that should be stored.
* @see Collector#handleCollect
* @param args Any args the event listener emits
* @returns Data to insert into collection, if any
*/
collect(...args: any[]): { key: any; value: any } | void | null {}
/**
* Handles incoming events from the `handleDispose`. Returns null if the event should not
* be disposed, or returns the key that should be removed.
* @see Collector#handleDispose
* @param args Any args the event listener emits
* @returns Key to remove from the collection, if any
*/
dispose(...args: any[]): any {}
/** The reason this collector has ended or will end with. */
endReason(): string | void | null {}
}