Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Managers): add customizable caching for managers #6013

Merged
merged 14 commits into from Jul 3, 2021
Merged
4 changes: 2 additions & 2 deletions src/client/BaseClient.js
Expand Up @@ -2,7 +2,7 @@

const EventEmitter = require('events');
const RESTManager = require('../rest/RESTManager');
const { DefaultOptions } = require('../util/Constants');
const Options = require('../util/Options');
const Util = require('../util/Util');

/**
Expand Down Expand Up @@ -38,7 +38,7 @@ class BaseClient extends EventEmitter {
* The options the client was instantiated with
* @type {ClientOptions}
*/
this.options = Util.mergeDefault(DefaultOptions, options);
this.options = Util.mergeDefault(Options.createDefault(), options);

/**
* The REST manager of the client
Expand Down
12 changes: 7 additions & 5 deletions src/client/Client.js
Expand Up @@ -17,9 +17,10 @@ const VoiceRegion = require('../structures/VoiceRegion');
const Webhook = require('../structures/Webhook');
const Widget = require('../structures/Widget');
const Collection = require('../util/Collection');
const { Events, DefaultOptions, InviteScopes } = require('../util/Constants');
const { Events, InviteScopes } = require('../util/Constants');
const DataResolver = require('../util/DataResolver');
const Intents = require('../util/Intents');
const Options = require('../util/Options');
const Permissions = require('../util/Permissions');
const Structures = require('../util/Structures');

Expand All @@ -35,14 +36,15 @@ class Client extends BaseClient {
super(Object.assign({ _tokenType: 'Bot' }, options));

const data = require('worker_threads').workerData ?? process.env;
const defaults = Options.createDefault();

if (this.options.shards === DefaultOptions.shards) {
if (this.options.shards === defaults.shards) {
if ('SHARDS' in data) {
this.options.shards = JSON.parse(data.SHARDS);
}
}

if (this.options.shardCount === DefaultOptions.shardCount) {
if (this.options.shardCount === defaults.shardCount) {
if ('SHARD_COUNT' in data) {
this.options.shardCount = Number(data.SHARD_COUNT);
} else if (Array.isArray(this.options.shards)) {
Expand Down Expand Up @@ -468,8 +470,8 @@ class Client extends BaseClient {
throw new TypeError('CLIENT_INVALID_OPTION', 'shards', "'auto', a number or array of numbers");
}
if (options.shards && !options.shards.length) throw new RangeError('CLIENT_INVALID_PROVIDED_SHARDS');
if (typeof options.messageCacheMaxSize !== 'number' || isNaN(options.messageCacheMaxSize)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'messageCacheMaxSize', 'a number');
if (typeof options.makeCache !== 'function') {
kyranet marked this conversation as resolved.
Show resolved Hide resolved
throw new TypeError('CLIENT_INVALID_OPTION', 'makeCache', 'a function');
}
if (typeof options.messageCacheLifetime !== 'number' || isNaN(options.messageCacheLifetime)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'The messageCacheLifetime', 'a number');
Expand Down
2 changes: 2 additions & 0 deletions src/errors/Messages.js
Expand Up @@ -127,6 +127,8 @@ const Messages = {
INTERACTION_NOT_REPLIED: 'This interaction has not been deferred or replied to.',
INTERACTION_EPHEMERAL_REPLIED: 'Ephemeral responses cannot be fetched or deleted.',
INTERACTION_FETCH_EPHEMERAL: 'Ephemeral responses cannot be fetched.',

NOT_IMPLEMENTED: (what, name) => `Method ${what} not implemented on ${name}.`,
};

for (const [name, message] of Object.entries(Messages)) register(name, message);
1 change: 1 addition & 0 deletions src/index.js
Expand Up @@ -23,6 +23,7 @@ module.exports = {
RateLimitError: require('./rest/RateLimitError'),
MessageFlags: require('./util/MessageFlags'),
Intents: require('./util/Intents'),
Options: require('./util/Options'),
Permissions: require('./util/Permissions'),
SnowflakeUtil: require('./util/SnowflakeUtil'),
Structures: require('./util/Structures'),
Expand Down
8 changes: 4 additions & 4 deletions src/managers/ApplicationCommandManager.js
@@ -1,18 +1,18 @@
'use strict';

const ApplicationCommandPermissionsManager = require('./ApplicationCommandPermissionsManager');
const BaseManager = require('./BaseManager');
const CachedManager = require('./CachedManager');
const { TypeError } = require('../errors');
const ApplicationCommand = require('../structures/ApplicationCommand');
const Collection = require('../util/Collection');

/**
* Manages API methods for application commands and stores their cache.
* @extends {BaseManager}
* @extends {CachedManager}
*/
class ApplicationCommandManager extends BaseManager {
class ApplicationCommandManager extends CachedManager {
constructor(client, iterable) {
super(client, iterable, ApplicationCommand);
super(client, ApplicationCommand, iterable);

/**
* The manager for permissions of arbitrary commands on arbitrary guilds
Expand Down
14 changes: 5 additions & 9 deletions src/managers/ApplicationCommandPermissionsManager.js
@@ -1,14 +1,18 @@
'use strict';

const BaseManager = require('./BaseManager');
const { Error, TypeError } = require('../errors');
const Collection = require('../util/Collection');
const { ApplicationCommandPermissionTypes, APIErrors } = require('../util/Constants');

/**
* Manages API methods for permissions of Application Commands.
* @extends {BaseManager}
*/
class ApplicationCommandPermissionsManager {
class ApplicationCommandPermissionsManager extends BaseManager {
constructor(manager) {
super(manager.client);

/**
* The manager or command that this manager belongs to
* @type {ApplicationCommandManager|ApplicationCommand}
Expand All @@ -32,14 +36,6 @@ class ApplicationCommandPermissionsManager {
* @type {?Snowflake}
*/
this.commandID = manager.id ?? null;

/**
* The client that instantiated this Manager
* @name ApplicationCommandPermissionsManager#client
* @type {Client}
* @readonly
*/
Object.defineProperty(this, 'client', { value: manager.client });
}

/**
Expand Down
8 changes: 4 additions & 4 deletions src/managers/BaseGuildEmojiManager.js
@@ -1,17 +1,17 @@
'use strict';

const BaseManager = require('./BaseManager');
const CachedManager = require('./CachedManager');
const GuildEmoji = require('../structures/GuildEmoji');
const ReactionEmoji = require('../structures/ReactionEmoji');
const { parseEmoji } = require('../util/Util');

/**
* Holds methods to resolve GuildEmojis and stores their cache.
* @extends {BaseManager}
* @extends {CachedManager}
*/
class BaseGuildEmojiManager extends BaseManager {
class BaseGuildEmojiManager extends CachedManager {
constructor(client, iterable) {
super(client, iterable, GuildEmoji);
super(client, GuildEmoji, iterable);
}

/**
Expand Down
66 changes: 2 additions & 64 deletions src/managers/BaseManager.js
@@ -1,80 +1,18 @@
'use strict';

const Collection = require('../util/Collection');
let Structures;

/**
* Manages the API methods of a data model and holds its cache.
* Manages the API methods of a data model.
* @abstract
*/
class BaseManager {
constructor(client, iterable, holds, cacheType = Collection, ...cacheOptions) {
if (!Structures) Structures = require('../util/Structures');
/**
* The data structure belonging to this manager
* @name BaseManager#holds
* @type {Function}
* @private
* @readonly
*/
Object.defineProperty(this, 'holds', { value: Structures.get(holds.name) ?? holds });

constructor(client) {
/**
* The client that instantiated this Manager
* @name BaseManager#client
* @type {Client}
* @readonly
*/
Object.defineProperty(this, 'client', { value: client });

/**
* The type of Collection of the Manager
* @type {Collection}
*/
this.cacheType = cacheType;

/**
* Holds the cache for the data model
* @type {Collection}
*/
this.cache = new cacheType(...cacheOptions);
if (iterable) for (const i of iterable) this.add(i);
}

add(data, cache = true, { id, extras = [] } = {}) {
const existing = this.cache.get(id ?? data.id);
if (cache) existing?._patch(data);
if (existing) return existing;

const entry = this.holds ? new this.holds(this.client, data, ...extras) : data;
if (cache) this.cache.set(id ?? entry.id, entry);
return entry;
}

/**
* Resolves a data entry to a data Object.
* @param {string|Object} idOrInstance The id or instance of something in this Manager
* @returns {?Object} An instance from this Manager
*/
resolve(idOrInstance) {
if (idOrInstance instanceof this.holds) return idOrInstance;
if (typeof idOrInstance === 'string') return this.cache.get(idOrInstance) ?? null;
return null;
}

/**
* Resolves a data entry to an instance ID.
* @param {string|Object} idOrInstance The id or instance of something in this Manager
* @returns {?Snowflake}
*/
resolveID(idOrInstance) {
if (idOrInstance instanceof this.holds) return idOrInstance.id;
if (typeof idOrInstance === 'string') return idOrInstance;
return null;
}

valueOf() {
return this.cache;
}
}

Expand Down
43 changes: 43 additions & 0 deletions src/managers/CachedManager.js
@@ -0,0 +1,43 @@
'use strict';

const DataManager = require('./DataManager');

/**
* Manages the API methods of a data model with a mutable cache of instances.
* @extends {DataManager}
* @abstract
*/
class CachedManager extends DataManager {
constructor(client, holds, iterable) {
super(client, holds);

Object.defineProperty(this, '_cache', { value: this.client.options.makeCache(this.constructor, this.holds) });

if (iterable) {
for (const item of iterable) {
this.add(item);
}
}
}

/**
* The cache of items for this manager.
* @type {Collection}
* @abstract
*/
get cache() {
return this._cache;
}

add(data, cache = true, { id, extras = [] } = {}) {
1Computer1 marked this conversation as resolved.
Show resolved Hide resolved
const existing = this.cache.get(id ?? data.id);
if (cache) existing?._patch(data);
if (existing) return existing;

const entry = this.holds ? new this.holds(this.client, data, ...extras) : data;
if (cache) this.cache.set(id ?? entry.id, entry);
return entry;
}
}

module.exports = CachedManager;
8 changes: 4 additions & 4 deletions src/managers/ChannelManager.js
@@ -1,16 +1,16 @@
'use strict';

const BaseManager = require('./BaseManager');
const CachedManager = require('./CachedManager');
const Channel = require('../structures/Channel');
const { Events, ThreadChannelTypes } = require('../util/Constants');

/**
* A manager of channels belonging to a client
* @extends {BaseManager}
* @extends {CachedManager}
*/
class ChannelManager extends BaseManager {
class ChannelManager extends CachedManager {
constructor(client, iterable) {
super(client, iterable, Channel);
super(client, Channel, iterable);
}

/**
Expand Down
65 changes: 65 additions & 0 deletions src/managers/DataManager.js
@@ -0,0 +1,65 @@
'use strict';

const BaseManager = require('./BaseManager');
const { Error } = require('../errors');

let Structures;

/**
* Manages the API methods of a data model along with a collection of instances.
* @extends {BaseManager}
* @abstract
*/
class DataManager extends BaseManager {
constructor(client, holds) {
super(client);

if (!Structures) Structures = require('../util/Structures');

/**
* The data structure belonging to this manager.
* @name DataManager#holds
* @type {Function}
* @private
* @readonly
*/
Object.defineProperty(this, 'holds', { value: Structures.get(holds.name) ?? holds });
}

/**
* The cache of items for this manager.
* @type {Collection}
* @abstract
*/
get cache() {
throw new Error('NOT_IMPLEMENTED', 'get cache', this.constructor.name);
}

/**
* Resolves a data entry to a data Object.
* @param {string|Object} idOrInstance The id or instance of something in this Manager
* @returns {?Object} An instance from this Manager
*/
resolve(idOrInstance) {
if (idOrInstance instanceof this.holds) return idOrInstance;
if (typeof idOrInstance === 'string') return this.cache.get(idOrInstance) ?? null;
1Computer1 marked this conversation as resolved.
Show resolved Hide resolved
return null;
}

/**
* Resolves a data entry to an instance ID.
* @param {string|Object} idOrInstance The id or instance of something in this Manager
* @returns {?Snowflake}
*/
resolveID(idOrInstance) {
if (idOrInstance instanceof this.holds) return idOrInstance.id;
if (typeof idOrInstance === 'string') return idOrInstance;
return null;
}

valueOf() {
return this.cache;
1Computer1 marked this conversation as resolved.
Show resolved Hide resolved
}
}

module.exports = DataManager;
8 changes: 4 additions & 4 deletions src/managers/GuildBanManager.js
@@ -1,18 +1,18 @@
'use strict';

const BaseManager = require('./BaseManager');
const CachedManager = require('./CachedManager');
const { TypeError, Error } = require('../errors');
const GuildBan = require('../structures/GuildBan');
const GuildMember = require('../structures/GuildMember');
const Collection = require('../util/Collection');

/**
* Manages API methods for GuildBans and stores their cache.
* @extends {BaseManager}
* @extends {CachedManager}
*/
class GuildBanManager extends BaseManager {
class GuildBanManager extends CachedManager {
constructor(guild, iterable) {
super(guild.client, iterable, GuildBan);
super(guild.client, GuildBan, iterable);

/**
* The guild this Manager belongs to
Expand Down