feat(Client): add global sweepers (#6825)

Co-authored-by: Antonio Román <kyradiscord@gmail.com>
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
Co-authored-by: Rodry <38259440+ImRodry@users.noreply.github.com>
Co-authored-by: Almeida <almeidx@pm.me>
This commit is contained in:
ckohen
2021-12-15 04:39:27 -08:00
committed by GitHub
parent bc6a6c539f
commit d1ef2f5e8b
7 changed files with 660 additions and 63 deletions

View File

@@ -25,6 +25,7 @@ const DataResolver = require('../util/DataResolver');
const Intents = require('../util/Intents');
const Options = require('../util/Options');
const Permissions = require('../util/Permissions');
const Sweepers = require('../util/Sweepers');
/**
* The main hub for interacting with the Discord API, and the starting point for any bot.
@@ -135,6 +136,12 @@ class Client extends BaseClient {
*/
this.channels = new ChannelManager(this);
/**
* The sweeping functions and their intervals used to periodically sweep caches
* @type {Sweepers}
*/
this.sweepers = new Sweepers(this, this.options.sweepers);
/**
* The presence of the Client
* @private
@@ -176,7 +183,7 @@ class Client extends BaseClient {
if (this.options.messageSweepInterval > 0) {
process.emitWarning(
'The message sweeping client options are deprecated, use the makeCache option with LimitedCollection instead.',
'The message sweeping client options are deprecated, use the global sweepers instead.',
'DeprecationWarning',
);
this.sweepMessageInterval = setInterval(
@@ -271,6 +278,7 @@ class Client extends BaseClient {
if (this.sweepMessageInterval) clearInterval(this.sweepMessageInterval);
this.sweepers.destroy();
this.ws.destroy();
this.token = null;
}
@@ -401,24 +409,8 @@ class Client extends BaseClient {
return -1;
}
const lifetimeMs = lifetime * 1_000;
const now = Date.now();
let channels = 0;
let messages = 0;
for (const channel of this.channels.cache.values()) {
if (!channel.messages) continue;
channels++;
messages += channel.messages.cache.sweep(
message => now - (message.editedTimestamp ?? message.createdTimestamp) > lifetimeMs,
);
}
this.emit(
Events.DEBUG,
`Swept ${messages} messages older than ${lifetime} seconds in ${channels} text-based channels`,
);
const messages = this.sweepers.sweepMessages(Sweepers.outdatedMessageSweepFilter(lifetime)());
this.emit(Events.DEBUG, `Swept ${messages} messages older than ${lifetime} seconds`);
return messages;
}
@@ -561,6 +553,9 @@ class Client extends BaseClient {
if (typeof options.messageSweepInterval !== 'number' || isNaN(options.messageSweepInterval)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'messageSweepInterval', 'a number');
}
if (typeof options.sweepers !== 'object' || options.sweepers === null) {
throw new TypeError('CLIENT_INVALID_OPTION', 'sweepers', 'an object');
}
if (typeof options.invalidRequestWarningInterval !== 'number' || isNaN(options.invalidRequestWarningInterval)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'invalidRequestWarningInterval', 'a number');
}

View File

@@ -188,6 +188,7 @@ exports.Events = {
ERROR: 'error',
WARN: 'warn',
DEBUG: 'debug',
CACHE_SWEEP: 'cacheSweep',
SHARD_DISCONNECT: 'shardDisconnect',
SHARD_ERROR: 'shardError',
SHARD_RECONNECTING: 'shardReconnecting',
@@ -431,6 +432,41 @@ exports.MessageTypes = [
'CONTEXT_MENU_COMMAND',
];
/**
* The name of an item to be swept in Sweepers
* * `applicationCommands` - both global and guild commands
* * `bans`
* * `emojis`
* * `invites` - accepts the `lifetime` property, using it will sweep based on expires timestamp
* * `guildMembers`
* * `messages` - accepts the `lifetime` property, using it will sweep based on edited or created timestamp
* * `presences`
* * `reactions`
* * `stageInstances`
* * `stickers`
* * `threadMembers`
* * `threads` - accepts the `lifetime` property, using it will sweep archived threads based on archived timestamp
* * `users`
* * `voiceStates`
* @typedef {string} SweeperKey
*/
exports.SweeperKeys = [
'applicationCommands',
'bans',
'emojis',
'invites',
'guildMembers',
'messages',
'presences',
'reactions',
'stageInstances',
'stickers',
'threadMembers',
'threads',
'users',
'voiceStates',
];
/**
* The types of messages that are `System`. The available types are `MessageTypes` excluding:
* * DEFAULT

View File

@@ -2,6 +2,7 @@
const { Collection } = require('@discordjs/collection');
const { _cleanupSymbol } = require('./Constants.js');
const Sweepers = require('./Sweepers.js');
const { TypeError } = require('../errors/DJSError.js');
/**
@@ -18,8 +19,12 @@ const { TypeError } = require('../errors/DJSError.js');
* @property {?number} [maxSize=Infinity] The maximum size of the Collection
* @property {?Function} [keepOverLimit=null] A function, which is passed the value and key of an entry, ran to decide
* to keep an entry past the maximum size
* @property {?SweepFilter} [sweepFilter=null] A function ran every `sweepInterval` to determine how to sweep
* @property {?number} [sweepInterval=0] How frequently, in seconds, to sweep the collection.
* @property {?SweepFilter} [sweepFilter=null] DEPRECATED: There is no direct alternative to this,
* however most of its purpose is fulfilled by {@link Client#sweepers}
* A function ran every `sweepInterval` to determine how to sweep
* @property {?number} [sweepInterval=0] DEPRECATED: There is no direct alternative to this,
* however most of its purpose is fulfilled by {@link Client#sweepers}
* How frequently, in seconds, to sweep the collection.
*/
/**
@@ -64,12 +69,14 @@ class LimitedCollection extends Collection {
/**
* A function called every sweep interval that returns a function passed to `sweep`.
* @deprecated in favor of {@link Client#sweepers}
* @type {?SweepFilter}
*/
this.sweepFilter = sweepFilter;
/**
* The id of the interval being used to sweep.
* @deprecated in favor of {@link Client#sweepers}
* @type {?Timeout}
*/
this.interval =
@@ -97,20 +104,10 @@ class LimitedCollection extends Collection {
return super.set(key, value);
}
/**
* Options for generating a filter function based on lifetime
* @typedef {Object} LifetimeFilterOptions
* @property {number} [lifetime=14400] How long, in seconds, an entry should stay in the collection
* before it is considered sweepable.
* @property {Function} [getComparisonTimestamp=e => e?.createdTimestamp] A function that takes an entry, key,
* and the collection and returns a timestamp to compare against in order to determine the lifetime of the entry.
* @property {Function} [excludeFromSweep=() => false] A function that takes an entry, key, and the collection
* and returns a boolean, `true` when the entry should not be checked for sweepability.
*/
/**
* Create a sweepFilter function that uses a lifetime to determine sweepability.
* @param {LifetimeFilterOptions} [options={}] The options used to generate the filter function
* @deprecated Use {@link Sweepers.filterByLifetime} instead
* @returns {SweepFilter}
*/
static filterByLifetime({
@@ -118,28 +115,7 @@ class LimitedCollection extends Collection {
getComparisonTimestamp = e => e?.createdTimestamp,
excludeFromSweep = () => false,
} = {}) {
if (typeof lifetime !== 'number') {
throw new TypeError('INVALID_TYPE', 'lifetime', 'number');
}
if (typeof getComparisonTimestamp !== 'function') {
throw new TypeError('INVALID_TYPE', 'getComparisonTimestamp', 'function');
}
if (typeof excludeFromSweep !== 'function') {
throw new TypeError('INVALID_TYPE', 'excludeFromSweep', 'function');
}
return () => {
if (lifetime <= 0) return null;
const lifetimeMs = lifetime * 1_000;
const now = Date.now();
return (entry, key, coll) => {
if (excludeFromSweep(entry, key, coll)) {
return false;
}
const comparisonTimestamp = getComparisonTimestamp(entry, key, coll);
if (!comparisonTimestamp || typeof comparisonTimestamp !== 'number') return false;
return now - comparisonTimestamp > lifetimeMs;
};
};
return Sweepers.filterByLifetime({ lifetime, getComparisonTimestamp, excludeFromSweep });
}
[_cleanupSymbol]() {

View File

@@ -37,9 +37,9 @@
* You can use your own function, or the {@link Options} class to customize the Collection used for the cache.
* <warn>Overriding the cache used in `GuildManager`, `ChannelManager`, `GuildChannelManager`, `RoleManager`,
* and `PermissionOverwriteManager` is unsupported and **will** break functionality</warn>
* @property {number} [messageCacheLifetime=0] DEPRECATED: Use `makeCache` with a `LimitedCollection` instead.
* @property {number} [messageCacheLifetime=0] DEPRECATED: Pass `lifetime` to `sweepers.messages` instead.
* How long a message should stay in the cache until it is considered sweepable (in seconds, 0 for forever)
* @property {number} [messageSweepInterval=0] DEPRECATED: Use `makeCache` with a `LimitedCollection` instead.
* @property {number} [messageSweepInterval=0] DEPRECATED: Pass `interval` to `sweepers.messages` instead.
* How frequently to remove messages from the cache that are older than the message cache lifetime
* (in seconds, 0 for never)
* @property {MessageMentionOptions} [allowedMentions] Default value for {@link MessageOptions#allowedMentions}
@@ -70,10 +70,27 @@
* [User Agent](https://discord.com/developers/docs/reference#user-agent) header
* @property {PresenceData} [presence={}] Presence data to use upon login
* @property {IntentsResolvable} intents Intents to enable for this connection
* @property {SweeperOptions} [sweepers={}] Options for cache sweeping
* @property {WebsocketOptions} [ws] Options for the WebSocket
* @property {HTTPOptions} [http] HTTP options
*/
/**
* Options for {@link Sweepers} defining the behavior of cache sweeping
* @typedef {Object<SweeperKey, SweepOptions>} SweeperOptions
*/
/**
* Options for sweeping a single type of item from cache
* @typedef {Object} SweepOptions
* @property {number} interval The interval (in seconds) at which to perform sweeping of the item
* @property {number} [lifetime] How long an item should stay in cache until it is considered sweepable.
* <warn>This property is only valid for the `invites`, `messages`, and `threads` keys. The `filter` property
* is mutually exclusive to this property and takes priority</warn>
* @property {GlobalSweepFilter} filter The function used to determine the function passed to the sweep method
* <info>This property is optional when the key is `invites`, `messages`, or `threads` and `lifetime` is set</info>
*/
/**
* WebSocket options (these are left as snake_case to match the API)
* @typedef {Object} WebsocketOptions
@@ -125,6 +142,7 @@ class Options extends null {
failIfNotExists: true,
userAgentSuffix: [],
presence: {},
sweepers: {},
ws: {
large_threshold: 50,
compress: false,
@@ -251,4 +269,19 @@ class Options extends null {
}
}
/**
* The default settings passed to {@link Options.sweepers} (for v14).
* The sweepers that this changes are:
* * `threads` - Sweep archived threads every hour, removing those archived more than 4 hours ago
* <info>If you want to keep default behavior and add on top of it you can use this object and add on to it, e.g.
* `sweepers: { ...Options.defaultSweeperSettings, messages: { interval: 300, lifetime: 600 } })`</info>
* @type {SweeperOptions}
*/
Options.defaultSweeperSettings = {
threads: {
interval: 3600,
lifetime: 14400,
},
};
module.exports = Options;

446
src/util/Sweepers.js Normal file
View File

@@ -0,0 +1,446 @@
'use strict';
const { Events, ThreadChannelTypes, SweeperKeys } = require('./Constants');
const { TypeError } = require('../errors/DJSError.js');
/**
* @typedef {Function} GlobalSweepFilter
* @returns {Function|null} Return `null` to skip sweeping, otherwise a function passed to `sweep()`,
* See {@link [Collection#sweep](https://discord.js.org/#/docs/collection/main/class/Collection?scrollTo=sweep)}
* for the definition of this function.
*/
/**
* A container for all cache sweeping intervals and their associated sweep methods.
*/
class Sweepers {
constructor(client, options) {
/**
* The client that instantiated this
* @type {Client}
* @readonly
*/
Object.defineProperty(this, 'client', { value: client });
/**
* The options the sweepers were instantiated with
* @type {SweeperOptions}
*/
this.options = options;
/**
* A record of interval timeout that is used to sweep the indicated items, or null if not being swept
* @type {Object<SweeperKey, ?Timeout>}
*/
this.intervals = Object.fromEntries(SweeperKeys.map(key => [key, null]));
for (const key of SweeperKeys) {
if (!(key in options)) continue;
this._validateProperties(key);
const clonedOptions = { ...this.options[key] };
// Handle cases that have a "lifetime"
if (!('filter' in clonedOptions)) {
switch (key) {
case 'invites':
clonedOptions.filter = this.constructor.expiredInviteSweepFilter(clonedOptions.lifetime);
break;
case 'messages':
clonedOptions.filter = this.constructor.outdatedMessageSweepFilter(clonedOptions.lifetime);
break;
case 'threads':
clonedOptions.filter = this.constructor.archivedThreadSweepFilter(clonedOptions.lifetime);
}
}
this._initInterval(key, `sweep${key[0].toUpperCase()}${key.slice(1)}`, clonedOptions);
}
}
/**
* Sweeps all guild and global application commands and removes the ones which are indicated by the filter.
* @param {Function} filter The function used to determine which commands will be removed from the caches.
* @returns {number} Amount of commands that were removed from the caches
*/
sweepApplicationCommands(filter) {
const { guilds, items: guildCommands } = this._sweepGuildDirectProp('commands', filter, { emit: false });
const globalCommands = this.client.application?.commands.cache.sweep(filter) ?? 0;
this.client.emit(
Events.CACHE_SWEEP,
`Swept ${globalCommands} global application commands and ${guildCommands} guild commands in ${guilds} guilds.`,
);
return guildCommands + globalCommands;
}
/**
* Sweeps all guild bans and removes the ones which are indicated by the filter.
* @param {Function} filter The function used to determine which bans will be removed from the caches.
* @returns {number} Amount of bans that were removed from the caches
*/
sweepBans(filter) {
return this._sweepGuildDirectProp('bans', filter).items;
}
/**
* Sweeps all guild emojis and removes the ones which are indicated by the filter.
* @param {Function} filter The function used to determine which emojis will be removed from the caches.
* @returns {number} Amount of emojis that were removed from the caches
*/
sweepEmojis(filter) {
return this._sweepGuildDirectProp('emojis', filter).items;
}
/**
* Sweeps all guild invites and removes the ones which are indicated by the filter.
* @param {Function} filter The function used to determine which invites will be removed from the caches.
* @returns {number} Amount of invites that were removed from the caches
*/
sweepInvites(filter) {
return this._sweepGuildDirectProp('invites', filter).items;
}
/**
* Sweeps all guild members and removes the ones which are indicated by the filter.
* <info>It is highly recommended to keep the client guild member cached</info>
* @param {Function} filter The function used to determine which guild members will be removed from the caches.
* @returns {number} Amount of guild members that were removed from the caches
*/
sweepGuildMembers(filter) {
return this._sweepGuildDirectProp('members', filter, { outputName: 'guild members' }).items;
}
/**
* Sweeps all text-based channels' messages and removes the ones which are indicated by the filter.
* @param {Function} filter The function used to determine which messages will be removed from the caches.
* @returns {number} Amount of messages that were removed from the caches
* @example
* // Remove all messages older than 1800 seconds from the messages cache
* const amount = sweepers.sweepMessages(
* Sweepers.filterByLifetime({
* lifetime: 1800,
* getComparisonTimestamp: m => m.editedTimestamp ?? m.createdTimestamp,
* })(),
* );
* console.log(`Successfully removed ${amount} messages from the cache.`);
*/
sweepMessages(filter) {
if (typeof filter !== 'function') {
throw new TypeError('INVALID_TYPE', 'filter', 'function');
}
let channels = 0;
let messages = 0;
for (const channel of this.client.channels.cache.values()) {
if (!channel.isText()) continue;
channels++;
messages += channel.messages.cache.sweep(filter);
}
this.client.emit(Events.CACHE_SWEEP, `Swept ${messages} messages in ${channels} text-based channels.`);
return messages;
}
/**
* Sweeps all presences and removes the ones which are indicated by the filter.
* @param {Function} filter The function used to determine which presences will be removed from the caches.
* @returns {number} Amount of presences that were removed from the caches
*/
sweepPresences(filter) {
return this._sweepGuildDirectProp('presences', filter).items;
}
/**
* Sweeps all message reactions and removes the ones which are indicated by the filter.
* @param {Function} filter The function used to determine which reactions will be removed from the caches.
* @returns {number} Amount of reactions that were removed from the caches
*/
sweepReactions(filter) {
if (typeof filter !== 'function') {
throw new TypeError('INVALID_TYPE', 'filter', 'function');
}
let channels = 0;
let messages = 0;
let reactions = 0;
for (const channel of this.client.channels.cache.values()) {
if (!channel.isText()) continue;
channels++;
for (const message of channel.messages.cache.values()) {
messages++;
reactions += message.reactions.cache.sweep(filter);
}
}
this.client.emit(
Events.CACHE_SWEEP,
`Swept ${reactions} reactions on ${messages} messages in ${channels} text-based channels.`,
);
return reactions;
}
/**
* Sweeps all guild stage instances and removes the ones which are indicated by the filter.
* @param {Function} filter The function used to determine which stage instances will be removed from the caches.
* @returns {number} Amount of stage instances that were removed from the caches
*/
sweepStageInstances(filter) {
return this._sweepGuildDirectProp('stageInstances', filter, { outputName: 'stage instances' }).items;
}
/**
* Sweeps all thread members and removes the ones which are indicated by the filter.
* <info>It is highly recommended to keep the client thread member cached</info>
* @param {Function} filter The function used to determine which thread members will be removed from the caches.
* @returns {number} Amount of thread members that were removed from the caches
*/
sweepThreadMembers(filter) {
if (typeof filter !== 'function') {
throw new TypeError('INVALID_TYPE', 'filter', 'function');
}
let threads = 0;
let members = 0;
for (const channel of this.client.channels.cache.values()) {
if (!ThreadChannelTypes.includes(channel.type)) continue;
threads++;
members += channel.members.cache.sweep(filter);
}
this.client.emit(Events.CACHE_SWEEP, `Swept ${members} thread members in ${threads} threads.`);
return members;
}
/**
* Sweeps all threads and removes the ones which are indicated by the filter.
* @param {Function} filter The function used to determine which threads will be removed from the caches.
* @returns {number} filter Amount of threads that were removed from the caches
* @example
* // Remove all threads archived greater than 1 day ago from all the channel caches
* const amount = sweepers.sweepThreads(
* Sweepers.filterByLifetime({
* getComparisonTimestamp: t => t.archivedTimestamp,
* excludeFromSweep: t => !t.archived,
* })(),
* );
* console.log(`Successfully removed ${amount} threads from the cache.`);
*/
sweepThreads(filter) {
if (typeof filter !== 'function') {
throw new TypeError('INVALID_TYPE', 'filter', 'function');
}
let threads = 0;
for (const [key, val] of this.client.channels.cache.entries()) {
if (!ThreadChannelTypes.includes(val.type)) continue;
if (filter(val, key, this.client.channels.cache)) {
threads++;
this.client.channels._remove(key);
}
}
this.client.emit(Events.CACHE_SWEEP, `Swept ${threads} threads.`);
return threads;
}
/**
* Sweeps all users and removes the ones which are indicated by the filter.
* @param {Function} filter The function used to determine which users will be removed from the caches.
* @returns {number} Amount of users that were removed from the caches
*/
sweepUsers(filter) {
if (typeof filter !== 'function') {
throw new TypeError('INVALID_TYPE', 'filter', 'function');
}
const users = this.client.users.cache.sweep(filter);
this.client.emit(Events.CACHE_SWEEP, `Swept ${users} users.`);
return users;
}
/**
* Sweeps all guild voice states and removes the ones which are indicated by the filter.
* @param {Function} filter The function used to determine which voice states will be removed from the caches.
* @returns {number} Amount of voice states that were removed from the caches
*/
sweepVoiceStates(filter) {
return this._sweepGuildDirectProp('voiceStates', filter, { outputName: 'voice states' }).items;
}
/**
* Cancels all sweeping intervals
* @returns {void}
*/
destroy() {
for (const key of SweeperKeys) {
if (this.intervals[key]) clearInterval(this.intervals[key]);
}
}
/**
* Options for generating a filter function based on lifetime
* @typedef {Object} LifetimeFilterOptions
* @property {number} [lifetime=14400] How long, in seconds, an entry should stay in the collection
* before it is considered sweepable.
* @property {Function} [getComparisonTimestamp=e => e?.createdTimestamp] A function that takes an entry, key,
* and the collection and returns a timestamp to compare against in order to determine the lifetime of the entry.
* @property {Function} [excludeFromSweep=() => false] A function that takes an entry, key, and the collection
* and returns a boolean, `true` when the entry should not be checked for sweepability.
*/
/**
* Create a sweepFilter function that uses a lifetime to determine sweepability.
* @param {LifetimeFilterOptions} [options={}] The options used to generate the filter function
* @returns {GlobalSweepFilter}
*/
static filterByLifetime({
lifetime = 14400,
getComparisonTimestamp = e => e?.createdTimestamp,
excludeFromSweep = () => false,
} = {}) {
if (typeof lifetime !== 'number') {
throw new TypeError('INVALID_TYPE', 'lifetime', 'number');
}
if (typeof getComparisonTimestamp !== 'function') {
throw new TypeError('INVALID_TYPE', 'getComparisonTimestamp', 'function');
}
if (typeof excludeFromSweep !== 'function') {
throw new TypeError('INVALID_TYPE', 'excludeFromSweep', 'function');
}
return () => {
if (lifetime <= 0) return null;
const lifetimeMs = lifetime * 1_000;
const now = Date.now();
return (entry, key, coll) => {
if (excludeFromSweep(entry, key, coll)) {
return false;
}
const comparisonTimestamp = getComparisonTimestamp(entry, key, coll);
if (!comparisonTimestamp || typeof comparisonTimestamp !== 'number') return false;
return now - comparisonTimestamp > lifetimeMs;
};
};
}
/**
* Creates a sweep filter that sweeps archived threads
* @param {number} [lifetime=14400] How long a thread has to be archived to be valid for sweeping
* @returns {GlobalSweepFilter}
*/
static archivedThreadSweepFilter(lifetime = 14400) {
return this.filterByLifetime({
lifetime,
getComparisonTimestamp: e => e.archiveTimestamp,
excludeFromSweep: e => !e.archived,
});
}
/**
* Creates a sweep filter that sweeps expired invites
* @param {number} [lifetime=14400] How long ago an invite has to have expired to be valid for sweeping
* @returns {GlobalSweepFilter}
*/
static expiredInviteSweepFilter(lifetime = 14400) {
return this.filterByLifetime({
lifetime,
getComparisonTimestamp: i => i.expiresTimestamp,
});
}
/**
* Creates a sweep filter that sweeps outdated messages (edits taken into account)
* @param {number} [lifetime=3600] How long ago a message has to hvae been sent or edited to be valid for sweeping
* @returns {GlobalSweepFilter}
*/
static outdatedMessageSweepFilter(lifetime = 3600) {
return this.filterByLifetime({
lifetime,
getComparisonTimestamp: m => m.editedTimestamp ?? m.createdTimestamp,
});
}
/**
* Configuration options for emitting the cache sweep client event
* @typedef {Object} SweepEventOptions
* @property {boolean} [emit=true] Whether to emit the client event in this method
* @property {string} [outputName] A name to output in the client event if it should differ from the key
* @private
*/
/**
* Sweep a direct sub property of all guilds
* @param {string} key The name of the property
* @param {Function} filter Filter function passed to sweep
* @param {SweepEventOptions} [eventOptions] Options for the Client event emitted here
* @returns {Object} Object containing the number of guilds swept and the number of items swept
* @private
*/
_sweepGuildDirectProp(key, filter, { emit = true, outputName }) {
if (typeof filter !== 'function') {
throw new TypeError('INVALID_TYPE', 'filter', 'function');
}
let guilds = 0;
let items = 0;
for (const guild of this.client.guilds.cache.values()) {
const { cache } = guild[key];
guilds++;
items += cache.sweep(filter);
}
if (emit) {
this.client.emit(Events.CACHE_SWEEP, `Swept ${items} ${outputName ?? key} in ${guilds} guilds.`);
}
return { guilds, items };
}
/**
* Validates a set of properties
* @param {string} key Key of the options object to check
* @private
*/
_validateProperties(key) {
const props = this.options[key];
if (typeof props !== 'object') {
throw new TypeError('INVALID_TYPE', `sweepers.${key}`, 'object', true);
}
if (typeof props.interval !== 'number') {
throw new TypeError('INVALID_TYPE', `sweepers.${key}.interval`, 'number');
}
// Invites, Messages, and Threads can be provided a lifetime parameter, which we use to generate the filter
if (['invites', 'messages', 'threads'].includes(key) && !('filter' in props)) {
if (typeof props.lifetime !== 'number') {
throw new TypeError('INVALID_TYPE', `sweepers.${key}.lifetime`, 'number');
}
return;
}
if (typeof props.filter !== 'function') {
throw new TypeError('INVALID_TYPE', `sweepers.${key}.filter`, 'function');
}
}
/**
* Initialize an interval for sweeping
* @param {string} intervalKey The name of the property that stores the interval for this sweeper
* @param {string} sweepKey The name of the function that sweeps the desired caches
* @param {Object} opts Validated options for a sweep
* @private
*/
_initInterval(intervalKey, sweepKey, opts) {
if (opts.interval <= 0 || opts.interval === Infinity) return;
this.intervals[intervalKey] = setInterval(() => {
const sweepFn = opts.filter();
if (sweepFn === null) return;
if (typeof sweepFn !== 'function') throw new TypeError('SWEEP_FILTER_RETURN');
this[sweepKey](sweepFn);
}, opts.interval * 1_000).unref();
}
}
module.exports = Sweepers;

View File

@@ -590,14 +590,11 @@ class Util extends null {
/**
* Creates a sweep filter that sweeps archived threads
* @param {number} [lifetime=14400] How long a thread has to be archived to be valid for sweeping
* @deprecated When not using with `makeCache` use `Sweepers.archivedThreadSweepFilter` instead
* @returns {SweepFilter}
*/
static archivedThreadSweepFilter(lifetime = 14400) {
const filter = require('./LimitedCollection').filterByLifetime({
lifetime,
getComparisonTimestamp: e => e.archiveTimestamp,
excludeFromSweep: e => !e.archived,
});
const filter = require('./Sweepers').archivedThreadSweepFilter(lifetime);
filter.isDefault = true;
return filter;
}

118
typings/index.d.ts vendored
View File

@@ -545,6 +545,7 @@ export class Client<Ready extends boolean = boolean> extends BaseClient {
public options: ClientOptions;
public readyAt: If<Ready, Date>;
public readonly readyTimestamp: If<Ready, number>;
public sweepers: Sweepers;
public shard: ShardClientUtil | null;
public token: If<Ready, string, string | null>;
public uptime: If<Ready, number>;
@@ -564,6 +565,7 @@ export class Client<Ready extends boolean = boolean> extends BaseClient {
public generateInvite(options?: InviteGenerationOptions): string;
public login(token?: string): Promise<string>;
public isReady(): this is Client<true>;
/** @deprecated Use {@link Sweepers#sweepMessages} instead */
public sweepMessages(lifetime?: number): number;
public toJSON(): unknown;
@@ -629,6 +631,7 @@ export class ClientUser extends User {
export class Options extends null {
private constructor();
public static defaultMakeCacheSettings: CacheWithLimitsOptions;
public static defaultSweeperSettings: SweeperOptions;
public static createDefault(): ClientOptions;
public static cacheWithLimits(settings?: CacheWithLimitsOptions): CacheFactory;
public static cacheEverything(): CacheFactory;
@@ -1365,9 +1368,12 @@ export class LimitedCollection<K, V> extends Collection<K, V> {
public constructor(options?: LimitedCollectionOptions<K, V>, iterable?: Iterable<readonly [K, V]>);
public maxSize: number;
public keepOverLimit: ((value: V, key: K, collection: this) => boolean) | null;
/** @deprecated Use Global Sweepers instead */
public interval: NodeJS.Timeout | null;
/** @deprecated Use Global Sweepers instead */
public sweepFilter: SweepFilter<K, V> | null;
/** @deprecated Use `Sweepers.filterByLifetime` instead */
public static filterByLifetime<K, V>(options?: LifetimeFilterOptions<K, V>): SweepFilter<K, V>;
}
@@ -2112,6 +2118,68 @@ export class StoreChannel extends GuildChannel {
public type: 'GUILD_STORE';
}
export class Sweepers {
public constructor(client: Client, options: SweeperOptions);
public readonly client: Client;
public intervals: Record<SweeperKey, NodeJS.Timeout | null>;
public options: SweeperOptions;
public sweepApplicationCommands(
filter: CollectionSweepFilter<
SweeperDefinitions['applicationCommands'][0],
SweeperDefinitions['applicationCommands'][1]
>,
): number;
public sweepBans(filter: CollectionSweepFilter<SweeperDefinitions['bans'][0], SweeperDefinitions['bans'][1]>): number;
public sweepEmojis(
filter: CollectionSweepFilter<SweeperDefinitions['emojis'][0], SweeperDefinitions['emojis'][1]>,
): number;
public sweepInvites(
filter: CollectionSweepFilter<SweeperDefinitions['invites'][0], SweeperDefinitions['invites'][1]>,
): number;
public sweepGuildMembers(
filter: CollectionSweepFilter<SweeperDefinitions['guildMembers'][0], SweeperDefinitions['guildMembers'][1]>,
): number;
public sweepMessages(
filter: CollectionSweepFilter<SweeperDefinitions['messages'][0], SweeperDefinitions['messages'][1]>,
): number;
public sweepPresences(
filter: CollectionSweepFilter<SweeperDefinitions['presences'][0], SweeperDefinitions['presences'][1]>,
): number;
public sweepReactions(
filter: CollectionSweepFilter<SweeperDefinitions['reactions'][0], SweeperDefinitions['reactions'][1]>,
): number;
public sweepStageInstnaces(
filter: CollectionSweepFilter<SweeperDefinitions['stageInstances'][0], SweeperDefinitions['stageInstances'][1]>,
): number;
public sweepStickers(
filter: CollectionSweepFilter<SweeperDefinitions['stickers'][0], SweeperDefinitions['stickers'][1]>,
): number;
public sweepThreadMembers(
filter: CollectionSweepFilter<SweeperDefinitions['threadMembers'][0], SweeperDefinitions['threadMembers'][1]>,
): number;
public sweepThreads(
filter: CollectionSweepFilter<SweeperDefinitions['threads'][0], SweeperDefinitions['threads'][1]>,
): number;
public sweepUsers(
filter: CollectionSweepFilter<SweeperDefinitions['users'][0], SweeperDefinitions['users'][1]>,
): number;
public sweepVoiceStates(
filter: CollectionSweepFilter<SweeperDefinitions['voiceStates'][0], SweeperDefinitions['voiceStates'][1]>,
): number;
public static archivedThreadSweepFilter(
lifetime?: number,
): GlobalSweepFilter<SweeperDefinitions['threads'][0], SweeperDefinitions['threads'][1]>;
public static expiredInviteSweepFilter(
lifetime?: number,
): GlobalSweepFilter<SweeperDefinitions['invites'][0], SweeperDefinitions['invites'][1]>;
public static filterByLifetime<K, V>(options?: LifetimeFilterOptions<K, V>): GlobalSweepFilter<K, V>;
public static outdatedMessageSweepFilter(
lifetime?: number,
): GlobalSweepFilter<SweeperDefinitions['messages'][0], SweeperDefinitions['messages'][1]>;
}
export class SystemChannelFlags extends BitField<SystemChannelFlagsString> {
public static FLAGS: Record<SystemChannelFlagsString, number>;
public static resolve(bit?: BitFieldResolvable<SystemChannelFlagsString, number>): number;
@@ -2280,6 +2348,7 @@ export class UserFlags extends BitField<UserFlagsString> {
export class Util extends null {
private constructor();
/** @deprecated When not using with `makeCache` use `Sweepers.archivedThreadSweepFilter` instead */
public static archivedThreadSweepFilter<K, V>(lifetime?: number): SweepFilter<K, V>;
public static basename(path: string, ext?: string): string;
public static cleanContent(str: string, channel: TextBasedChannels): string;
@@ -3742,6 +3811,7 @@ export interface ClientEvents extends BaseClientEvents {
applicationCommandDelete: [command: ApplicationCommand];
/** @deprecated See [this issue](https://github.com/discord/discord-api-docs/issues/3690) for more information. */
applicationCommandUpdate: [oldCommand: ApplicationCommand | null, newCommand: ApplicationCommand];
cacheSweep: [message: string];
channelCreate: [channel: GuildChannel];
channelDelete: [channel: DMChannel | GuildChannel];
channelPinsUpdate: [channel: TextBasedChannels, date: Date];
@@ -3821,9 +3891,9 @@ export interface ClientOptions {
shards?: number | number[] | 'auto';
shardCount?: number;
makeCache?: CacheFactory;
/** @deprecated Use `makeCache` with a `LimitedCollection` for `MessageManager` instead. */
/** @deprecated Pass the value of this property as `lifetime` to `sweepers.messages` instead. */
messageCacheLifetime?: number;
/** @deprecated Use `makeCache` with a `LimitedCollection` for `MessageManager` instead. */
/** @deprecated Pass the value of this property as `interval` to `sweepers.messages` instead. */
messageSweepInterval?: number;
allowedMentions?: MessageMentionOptions;
invalidRequestWarningInterval?: number;
@@ -3838,6 +3908,7 @@ export interface ClientOptions {
userAgentSuffix?: string[];
presence?: PresenceData;
intents: BitFieldResolvable<IntentsString, number>;
sweepers?: SweeperOptions;
ws?: WebSocketOptions;
http?: HTTPOptions;
rejectOnRateLimit?: string[] | ((data: RateLimitData) => boolean | Promise<boolean>);
@@ -4033,6 +4104,7 @@ export interface ConstantsEvents {
ERROR: 'error';
WARN: 'warn';
DEBUG: 'debug';
CACHE_SWEEP: 'cacheSweep';
SHARD_DISCONNECT: 'shardDisconnect';
SHARD_ERROR: 'shardError';
SHARD_RECONNECTING: 'shardReconnecting';
@@ -4237,6 +4309,8 @@ export interface FileOptions {
description?: string;
}
export type GlobalSweepFilter<K, V> = () => ((value: V, key: K, collection: Collection<K, V>) => boolean) | null;
export interface GuildApplicationCommandPermissionData {
id: Snowflake;
permissions: ApplicationCommandPermissionData[];
@@ -5234,14 +5308,54 @@ export interface StageInstanceEditOptions {
privacyLevel?: PrivacyLevel | number;
}
export type SweeperKey = keyof SweeperDefinitions;
export type CollectionSweepFilter<K, V> = (value: V, key: K, collection: Collection<K, V>) => boolean;
export type SweepFilter<K, V> = (
collection: LimitedCollection<K, V>,
) => ((value: V, key: K, collection: LimitedCollection<K, V>) => boolean) | null;
export interface SweepOptions<K, V> {
interval: number;
filter: GlobalSweepFilter<K, V>;
}
export interface LifetimeSweepOptions {
interval: number;
lifetime: number;
filter?: never;
}
export interface SweeperDefinitions {
applicationCommands: [Snowflake, ApplicationCommand];
bans: [Snowflake, GuildBan];
emojis: [Snowflake, GuildEmoji];
invites: [string, Invite, true];
guildMembers: [Snowflake, GuildMember];
messages: [Snowflake, Message, true];
presences: [Snowflake, Presence];
reactions: [string | Snowflake, MessageReaction];
stageInstances: [Snowflake, StageInstance];
stickers: [Snowflake, Sticker];
threadMembers: [Snowflake, ThreadMember];
threads: [Snowflake, ThreadChannel, true];
users: [Snowflake, User];
voiceStates: [Snowflake, VoiceState];
}
export type SweeperOptions = {
[K in keyof SweeperDefinitions]?: SweeperDefinitions[K][2] extends true
? SweepOptions<SweeperDefinitions[K][0], SweeperDefinitions[K][1]> | LifetimeSweepOptions
: SweepOptions<SweeperDefinitions[K][0], SweeperDefinitions[K][1]>;
};
export interface LimitedCollectionOptions<K, V> {
maxSize?: number;
keepOverLimit?: (value: V, key: K, collection: LimitedCollection<K, V>) => boolean;
/** @deprecated Use Global Sweepers instead */
sweepFilter?: SweepFilter<K, V>;
/** @deprecated Use Global Sweepers instead */
sweepInterval?: number;
}