mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-09 16:13:31 +01:00
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:
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]() {
|
||||
|
||||
@@ -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
446
src/util/Sweepers.js
Normal 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;
|
||||
@@ -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
118
typings/index.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user