diff --git a/src/client/Client.js b/src/client/Client.js
index bde16f3cad88..72c49b1fba1d 100644
--- a/src/client/Client.js
+++ b/src/client/Client.js
@@ -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');
}
diff --git a/src/util/Constants.js b/src/util/Constants.js
index 0043db645121..0701619df708 100644
--- a/src/util/Constants.js
+++ b/src/util/Constants.js
@@ -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
diff --git a/src/util/LimitedCollection.js b/src/util/LimitedCollection.js
index b84fd1222a4e..17b270242bf4 100644
--- a/src/util/LimitedCollection.js
+++ b/src/util/LimitedCollection.js
@@ -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]() {
diff --git a/src/util/Options.js b/src/util/Options.js
index b31397977bbe..3180cf4713c5 100644
--- a/src/util/Options.js
+++ b/src/util/Options.js
@@ -37,9 +37,9 @@
* You can use your own function, or the {@link Options} class to customize the Collection used for the cache.
* Overriding the cache used in `GuildManager`, `ChannelManager`, `GuildChannelManager`, `RoleManager`,
* and `PermissionOverwriteManager` is unsupported and **will** break functionality
- * @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} 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.
+ * This property is only valid for the `invites`, `messages`, and `threads` keys. The `filter` property
+ * is mutually exclusive to this property and takes priority
+ * @property {GlobalSweepFilter} filter The function used to determine the function passed to the sweep method
+ * This property is optional when the key is `invites`, `messages`, or `threads` and `lifetime` is set
+ */
+
/**
* 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
+ * 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 } })`
+ * @type {SweeperOptions}
+ */
+Options.defaultSweeperSettings = {
+ threads: {
+ interval: 3600,
+ lifetime: 14400,
+ },
+};
+
module.exports = Options;
diff --git a/src/util/Sweepers.js b/src/util/Sweepers.js
new file mode 100644
index 000000000000..4f0c1891edbc
--- /dev/null
+++ b/src/util/Sweepers.js
@@ -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}
+ */
+ 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.
+ * It is highly recommended to keep the client guild member cached
+ * @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.
+ * It is highly recommended to keep the client thread member cached
+ * @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;
diff --git a/src/util/Util.js b/src/util/Util.js
index 00282c645906..47ceef45b73e 100644
--- a/src/util/Util.js
+++ b/src/util/Util.js
@@ -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;
}
diff --git a/typings/index.d.ts b/typings/index.d.ts
index 0c8c20e57ba8..98ca305a2df8 100644
--- a/typings/index.d.ts
+++ b/typings/index.d.ts
@@ -545,6 +545,7 @@ export class Client extends BaseClient {
public options: ClientOptions;
public readyAt: If;
public readonly readyTimestamp: If;
+ public sweepers: Sweepers;
public shard: ShardClientUtil | null;
public token: If;
public uptime: If;
@@ -564,6 +565,7 @@ export class Client extends BaseClient {
public generateInvite(options?: InviteGenerationOptions): string;
public login(token?: string): Promise;
public isReady(): this is Client;
+ /** @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 extends Collection {
public constructor(options?: LimitedCollectionOptions, iterable?: Iterable);
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 | null;
+ /** @deprecated Use `Sweepers.filterByLifetime` instead */
public static filterByLifetime(options?: LifetimeFilterOptions): SweepFilter;
}
@@ -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;
+ public options: SweeperOptions;
+
+ public sweepApplicationCommands(
+ filter: CollectionSweepFilter<
+ SweeperDefinitions['applicationCommands'][0],
+ SweeperDefinitions['applicationCommands'][1]
+ >,
+ ): number;
+ public sweepBans(filter: CollectionSweepFilter): number;
+ public sweepEmojis(
+ filter: CollectionSweepFilter,
+ ): number;
+ public sweepInvites(
+ filter: CollectionSweepFilter,
+ ): number;
+ public sweepGuildMembers(
+ filter: CollectionSweepFilter,
+ ): number;
+ public sweepMessages(
+ filter: CollectionSweepFilter,
+ ): number;
+ public sweepPresences(
+ filter: CollectionSweepFilter,
+ ): number;
+ public sweepReactions(
+ filter: CollectionSweepFilter,
+ ): number;
+ public sweepStageInstnaces(
+ filter: CollectionSweepFilter,
+ ): number;
+ public sweepStickers(
+ filter: CollectionSweepFilter,
+ ): number;
+ public sweepThreadMembers(
+ filter: CollectionSweepFilter,
+ ): number;
+ public sweepThreads(
+ filter: CollectionSweepFilter,
+ ): number;
+ public sweepUsers(
+ filter: CollectionSweepFilter,
+ ): number;
+ public sweepVoiceStates(
+ filter: CollectionSweepFilter,
+ ): number;
+
+ public static archivedThreadSweepFilter(
+ lifetime?: number,
+ ): GlobalSweepFilter;
+ public static expiredInviteSweepFilter(
+ lifetime?: number,
+ ): GlobalSweepFilter;
+ public static filterByLifetime(options?: LifetimeFilterOptions): GlobalSweepFilter;
+ public static outdatedMessageSweepFilter(
+ lifetime?: number,
+ ): GlobalSweepFilter;
+}
+
export class SystemChannelFlags extends BitField {
public static FLAGS: Record;
public static resolve(bit?: BitFieldResolvable): number;
@@ -2280,6 +2348,7 @@ export class UserFlags extends BitField {
export class Util extends null {
private constructor();
+ /** @deprecated When not using with `makeCache` use `Sweepers.archivedThreadSweepFilter` instead */
public static archivedThreadSweepFilter(lifetime?: number): SweepFilter;
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;
+ sweepers?: SweeperOptions;
ws?: WebSocketOptions;
http?: HTTPOptions;
rejectOnRateLimit?: string[] | ((data: RateLimitData) => boolean | Promise);
@@ -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 = () => ((value: V, key: K, collection: Collection) => 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 = (value: V, key: K, collection: Collection) => boolean;
+
export type SweepFilter = (
collection: LimitedCollection,
) => ((value: V, key: K, collection: LimitedCollection) => boolean) | null;
+export interface SweepOptions {
+ interval: number;
+ filter: GlobalSweepFilter;
+}
+
+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 | LifetimeSweepOptions
+ : SweepOptions;
+};
+
export interface LimitedCollectionOptions {
maxSize?: number;
keepOverLimit?: (value: V, key: K, collection: LimitedCollection) => boolean;
+ /** @deprecated Use Global Sweepers instead */
sweepFilter?: SweepFilter;
+ /** @deprecated Use Global Sweepers instead */
sweepInterval?: number;
}