Skip to content

Commit

Permalink
feat(client): change stores from a Set to a Map (#129)
Browse files Browse the repository at this point in the history
This allows easy overriding of stores in plugins/user-code.

BREAKING CHANGE: `client.arguments` is now `client.stores.get('arguments')`
BREAKING CHANGE: `client.commands` is now `client.stores.get('commands')`
BREAKING CHANGE: `client.events` is now `client.stores.get('events')`
BREAKING CHANGE: `client.preconditions` is now `client.stores.get('preconditions')`
BREAKING CHANGE: `client.registerUserDirectories` is now `client.stores.registerUserDirectories`
BREAKING CHANGE: `client.deregisterStore` is now `client.stores.deregister`
BREAKING CHANGE: `client.registerStore` is now `client.stores.register`
  • Loading branch information
kyranet authored Feb 2, 2021
1 parent d85921e commit 01f7161
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 197 deletions.
16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"@sapphire/discord.js-utilities": "^1.3.1",
"@sapphire/pieces": "^1.2.1",
"@sapphire/ratelimits": "^1.1.4",
"@sapphire/utilities": "^1.4.3",
"@sapphire/utilities": "^1.4.4",
"lexure": "^0.17.0"
},
"peerDependencies": {
Expand All @@ -47,11 +47,11 @@
"@types/jest": "^26.0.20",
"@types/node": "^14.14.22",
"@types/ws": "^7.4.0",
"@typescript-eslint/eslint-plugin": "^4.14.1",
"@typescript-eslint/parser": "^4.14.1",
"@typescript-eslint/eslint-plugin": "^4.14.2",
"@typescript-eslint/parser": "^4.14.2",
"cz-conventional-changelog": "^3.3.0",
"discord.js": "^12.5.1",
"eslint": "^7.18.0",
"eslint": "^7.19.0",
"eslint-config-prettier": "^7.2.0",
"eslint-plugin-prettier": "^3.3.1",
"husky": "^4.3.8",
Expand All @@ -61,12 +61,12 @@
"npm-run-all": "^4.1.5",
"prettier": "^2.2.1",
"pretty-quick": "^3.1.0",
"rollup": "^2.38.2",
"rollup": "^2.38.3",
"rollup-plugin-dts": "^2.0.1",
"standard-version": "^9.1.0",
"ts-jest": "^26.4.4",
"ts-jest": "^26.5.0",
"ts-node": "^9.1.1",
"typedoc": "^0.20.19",
"typedoc": "^0.20.20",
"typedoc-plugin-nojekyll": "^1.0.1",
"typescript": "^4.1.3"
},
Expand Down Expand Up @@ -118,7 +118,7 @@
"access": "public"
},
"resolutions": {
"acorn": "^8.0.4",
"acorn": "^8.0.5",
"minimist": "^1.2.5",
"kind-of": "^6.0.3",
"jest-environment-jsdom": "https://registry.yarnpkg.com/@favware/skip-dependency/-/skip-dependency-1.1.1.tgz",
Expand Down
9 changes: 5 additions & 4 deletions src/events/command-handler/CorePrefixedMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,26 @@ export class CoreEvent extends Event<Events.PrefixedMessage> {
}

public run(message: Message, prefix: string | RegExp) {
const { client, stores } = this.context;
// Retrieve the command name and validate:
const trimLength = typeof prefix === 'string' ? prefix.length : prefix.exec(message.content)?.[0].length ?? 0;
const prefixLess = message.content.slice(trimLength).trim();
const spaceIndex = prefixLess.indexOf(' ');
const name = spaceIndex === -1 ? prefixLess : prefixLess.slice(0, spaceIndex);
if (!name) {
message.client.emit(Events.UnknownCommandName, message, prefix);
client.emit(Events.UnknownCommandName, message, prefix);
return;
}

// Retrieve the command and validate:
const command = message.client.commands.get(message.client.options.caseInsensitiveCommands ? name.toLowerCase() : name);
const command = stores.get('commands').get(client.options.caseInsensitiveCommands ? name.toLowerCase() : name);
if (!command) {
message.client.emit(Events.UnknownCommand, message, name, prefix);
client.emit(Events.UnknownCommand, message, name, prefix);
return;
}

// Run the last stage before running the command:
const parameters = spaceIndex === -1 ? '' : prefixLess.substr(spaceIndex + 1).trim();
message.client.emit(Events.PreCommandRun, { message, command, parameters, context: { commandName: name, prefix } });
client.emit(Events.PreCommandRun, { message, command, parameters, context: { commandName: name, prefix } });
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export * from './lib/structures/EventStore';
export * from './lib/structures/ExtendedArgument';
export * from './lib/structures/Precondition';
export * from './lib/structures/PreconditionStore';
export * from './lib/structures/StoreRegistry';
export * from './lib/types/Enums';
export * from './lib/types/Events';
export * from './lib/utils/logger/ILogger';
Expand Down
109 changes: 16 additions & 93 deletions src/lib/SapphireClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Awaited, getRootData, Piece, Store } from '@sapphire/pieces';
import { Awaited, Store } from '@sapphire/pieces';
import { Client, ClientOptions, Message } from 'discord.js';
import { join } from 'path';
import type { Plugin } from './plugins/Plugin';
Expand All @@ -7,6 +7,7 @@ import { ArgumentStore } from './structures/ArgumentStore';
import { CommandStore } from './structures/CommandStore';
import { EventStore } from './structures/EventStore';
import { PreconditionStore } from './structures/PreconditionStore';
import { StoreRegistry } from './structures/StoreRegistry';
import { PluginHook } from './types/Enums';
import { Events } from './types/Events';
import { ILogger, LogLevel } from './utils/logger/ILogger';
Expand Down Expand Up @@ -183,35 +184,11 @@ export class SapphireClient extends Client {
*/
public logger: ILogger;

/**
* The arguments the framework has registered.
* @since 1.0.0
*/
public arguments: ArgumentStore;

/**
* The commands the framework has registered.
* @since 1.0.0
*/
public commands: CommandStore;

/**
* The events the framework has registered.
* @since 1.0.0
*/
public events: EventStore;

/**
* The precondition the framework has registered.
* @since 1.0.0
*/
public preconditions: PreconditionStore;

/**
* The registered stores.
* @since 1.0.0
*/
public stores: Set<Store<Piece>>;
public stores: StoreRegistry;

public constructor(options: ClientOptions = {}) {
super(options);
Expand All @@ -224,9 +201,11 @@ export class SapphireClient extends Client {
}

this.logger = options.logger?.instance ?? new Logger(options.logger?.level ?? LogLevel.Info);

Store.injectedContext.logger = this.logger;

this.stores = new StoreRegistry();
Store.injectedContext.stores = this.stores;

this.fetchPrefix = options.fetchPrefix ?? (() => this.options.defaultPrefix ?? null);

for (const plugin of SapphireClient.plugins.values(PluginHook.PreInitialization)) {
Expand All @@ -235,73 +214,19 @@ export class SapphireClient extends Client {
}

this.id = options.id ?? null;
this.arguments = new ArgumentStore().registerPath(join(__dirname, '..', 'arguments'));
this.commands = new CommandStore();
this.events = new EventStore().registerPath(join(__dirname, '..', 'events'));
this.preconditions = new PreconditionStore().registerPath(join(__dirname, '..', 'preconditions'));

if (options.loadDefaultErrorEvents !== false) this.events.registerPath(join(__dirname, '..', 'errorEvents'));

this.stores = new Set();
this.registerStore(this.arguments) //
.registerStore(this.commands)
.registerStore(this.events)
.registerStore(this.preconditions);
this.stores
.register(new ArgumentStore().registerPath(join(__dirname, '..', 'arguments'))) //
.register(new CommandStore())
.register(new EventStore().registerPath(join(__dirname, '..', 'events')))
.register(new PreconditionStore().registerPath(join(__dirname, '..', 'preconditions')));
if (options.loadDefaultErrorEvents !== false) this.stores.get('events').registerPath(join(__dirname, '..', 'errorEvents'));

for (const plugin of SapphireClient.plugins.values(PluginHook.PostInitialization)) {
plugin.hook.call(this, options);
this.emit(Events.PluginLoaded, plugin.type, plugin.name);
}
}

/**
* Registers all user directories from the process working directory, the default value is obtained by assuming
* CommonJS (high accuracy) but with fallback for ECMAScript Modules (reads package.json's `main` entry, fallbacks
* to `process.cwd()`).
*
* By default, if you have this folder structure:
* ```
* /home/me/my-bot
* ├─ src
* │ ├─ commands
* │ ├─ events
* │ └─ main.js
* └─ package.json
* ```
*
* And you run `node src/main.js`, the directories `/home/me/my-bot/src/commands` and `/home/me/my-bot/src/events` will
* be registered for the commands and events stores respectively, since both directories are located in the same
* directory as your main file.
*
* **Note**: this also registers directories for all other stores, even if they don't have a folder, this allows you
* to create new pieces and hot-load them later anytime.
* @param rootDirectory The root directory to register pieces at.
*/
public registerUserDirectories(rootDirectory = getRootData().root) {
for (const store of this.stores) {
store.registerPath(join(rootDirectory, store.name));
}
}

/**
* Registers a store.
* @param store The store to register.
*/
public registerStore<T extends Piece>(store: Store<T>): this {
this.stores.add((store as unknown) as Store<Piece>);
return this;
}

/**
* Deregisters a store.
* @since 1.0.0
* @param store The store to deregister.
*/
public deregisterStore<T extends Piece>(store: Store<T>): this {
this.stores.delete((store as unknown) as Store<Piece>);
return this;
}

/**
* Loads all pieces, then logs the client in, establishing a websocket connection to Discord.
* @since 1.0.0
Expand All @@ -311,7 +236,7 @@ export class SapphireClient extends Client {
public async login(token?: string) {
// Register the user directory if not null:
if (this.options.baseUserDirectory !== null) {
this.registerUserDirectories(this.options.baseUserDirectory);
this.stores.registerUserDirectories(this.options.baseUserDirectory);
}

// Call pre-login plugins:
Expand All @@ -321,7 +246,7 @@ export class SapphireClient extends Client {
}

// Loads all stores, then call login:
await Promise.all([...this.stores].map((store) => store.loadAll()));
await Promise.all([...this.stores.values()].map((store) => store.loadAll()));
const login = await super.login(token);

// Call post-login plugins:
Expand Down Expand Up @@ -350,10 +275,7 @@ declare module 'discord.js' {
interface Client {
id: string | null;
logger: ILogger;
arguments: ArgumentStore;
commands: CommandStore;
events: EventStore;
preconditions: PreconditionStore;
stores: StoreRegistry;
fetchPrefix: SapphirePrefixHook;
}

Expand All @@ -364,5 +286,6 @@ declare module '@sapphire/pieces' {
interface PieceContextExtras {
client: SapphireClient;
logger: ILogger;
stores: StoreRegistry;
}
}
3 changes: 2 additions & 1 deletion src/lib/parsers/Args.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Store } from '@sapphire/pieces';
import type {
CategoryChannel,
Channel,
Expand Down Expand Up @@ -625,7 +626,7 @@ export class Args {
*/
private resolveArgument<T>(arg: keyof ArgType | IArgument<T>): IArgument<T> | undefined {
if (typeof arg === 'object') return arg;
return this.message.client.arguments.get(arg as string) as IArgument<T> | undefined;
return Store.injectedContext.stores.get('arguments').get(arg as string) as IArgument<T> | undefined;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/lib/structures/ExtendedArgument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export abstract class ExtendedArgument<K extends keyof ArgType, T> extends Argum
* into the value used to compute the extended argument's value.
*/
public get base(): IArgument<ArgType[K]> {
return this.context.client.arguments.get(this.baseArgument) as IArgument<ArgType[K]>;
return this.context.stores.get('arguments').get(this.baseArgument) as IArgument<ArgType[K]>;
}

public async run(parameter: string, context: ArgumentContext<T>): AsyncArgumentResult<T> {
Expand Down
100 changes: 100 additions & 0 deletions src/lib/structures/StoreRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import Collection from '@discordjs/collection';
import { getRootData, Piece, Store } from '@sapphire/pieces';
import { join } from 'path';
import type { ArgumentStore } from './ArgumentStore';
import type { CommandStore } from './CommandStore';
import type { EventStore } from './EventStore';
import type { PreconditionStore } from './PreconditionStore';

type Key = keyof StoreRegistryEntries;
type Value = StoreRegistryEntries[Key];

/**
* A strict-typed store registry. This is available in both [[Client.stores]] and [[Store.injectedContext]].
* @since 1.0.0
* @example
* ```typescript
* // Adding new stores
*
* // Register the store:
* Store.injectedContext.stores.register(new RouteStore());
*
* // Augment Sapphire to add the new store, in case of a JavaScript
* // project, this can be moved to an `Augments.d.ts` (or any other name)
* // file somewhere:
* declare module '(at)sapphire/framework' {
* export interface StoreRegistryEntries {
* routes: RouteStore;
* }
* }
* ```
*/
export class StoreRegistry extends Collection<Key, Value> {
/**
* Registers all user directories from the process working directory, the default value is obtained by assuming
* CommonJS (high accuracy) but with fallback for ECMAScript Modules (reads package.json's `main` entry, fallbacks
* to `process.cwd()`).
*
* By default, if you have this folder structure:
* ```
* /home/me/my-bot
* ├─ src
* │ ├─ commands
* │ ├─ events
* │ └─ main.js
* └─ package.json
* ```
*
* And you run `node src/main.js`, the directories `/home/me/my-bot/src/commands` and `/home/me/my-bot/src/events` will
* be registered for the commands and events stores respectively, since both directories are located in the same
* directory as your main file.
*
* **Note**: this also registers directories for all other stores, even if they don't have a folder, this allows you
* to create new pieces and hot-load them later anytime.
* @since 1.0.0
* @param rootDirectory The root directory to register pieces at.
*/
public registerUserDirectories(rootDirectory = getRootData().root) {
for (const store of this.values()) {
store.registerPath(join(rootDirectory, store.name));
}
}

/**
* Registers a store.
* @since 1.0.0
* @param store The store to register.
*/
public register<T extends Piece>(store: Store<T>): this {
this.set(store.name as Key, (store as unknown) as Value);
return this;
}

/**
* Deregisters a store.
* @since 1.0.0
* @param store The store to deregister.
*/
public deregister<T extends Piece>(store: Store<T>): this {
this.delete(store.name as Key);
return this;
}
}

export interface StoreRegistry {
get<K extends Key>(key: K): StoreRegistryEntries[K];
get(key: string): undefined;
has(key: Key): true;
has(key: string): false;
}

/**
* The [[StoreRegistry]]'s registry, use module augmentation against this interface when adding new stores.
* @since 1.0.0
*/
export interface StoreRegistryEntries {
arguments: ArgumentStore;
commands: CommandStore;
events: EventStore;
preconditions: PreconditionStore;
}
Loading

0 comments on commit 01f7161

Please sign in to comment.