Skip to content

Commit

Permalink
fix: faster autocomplete lookup (#387)
Browse files Browse the repository at this point in the history
* fix:faster-autocmp

* fixinitializing

* fix

* fixonwindows

* unconsole
  • Loading branch information
jacoobes authored Feb 3, 2025
1 parent 3a56972 commit 974c30f
Show file tree
Hide file tree
Showing 7 changed files with 382 additions and 331 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"prepare": "tsc",
"pretty": "prettier --write .",
"tdd": "vitest",
"benchmark": "vitest bench",
"test": "vitest --run",
"analyze-imports": "npx depcruise src --include-only \"^src\" --output-type dot | dot -T svg > dependency-graph.svg"
},
Expand Down
60 changes: 20 additions & 40 deletions src/core/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import type {
MessageContextMenuCommandInteraction,
ModalSubmitInteraction,
UserContextMenuCommandInteraction,
AutocompleteInteraction
AutocompleteInteraction,
} from 'discord.js';
import { ApplicationCommandOptionType, InteractionType } from 'discord.js';
import { PluginType } from './structures/enums';
import assert from 'assert';
import type { Payload, UnpackedDependencies } from '../types/utility';
import path from 'node:path'

export const createSDT = (module: Module, deps: UnpackedDependencies, params: string|undefined) => {
return {
Expand Down Expand Up @@ -57,51 +57,31 @@ export function partitionPlugins<T,V>
return [controlPlugins, initPlugins] as [T[], V[]];
}

/**
* Uses an iterative DFS to check if an autocomplete node exists on the option tree
* @param iAutocomplete
* @param options
*/
export function treeSearch(
iAutocomplete: AutocompleteInteraction,
options: SernOptionsData[] | undefined,
): SernAutocompleteData & { parent?: string } | undefined {
if (options === undefined) return undefined;
//clone to prevent mutation of original command module
const _options = options.map(a => ({ ...a }));
const subcommands = new Set();
while (_options.length > 0) {
const cur = _options.pop()!;
switch (cur.type) {
export const createLookupTable = (options: SernOptionsData[]): Map<string, SernAutocompleteData> => {
const table = new Map<string, SernAutocompleteData>();
_createLookupTable(table, options, "<parent>");
return table;
}

const _createLookupTable = (table: Map<string, SernAutocompleteData>, options: SernOptionsData[], parent: string) => {
for (const opt of options) {
const name = path.posix.join(parent, opt.name)
switch(opt.type) {
case ApplicationCommandOptionType.Subcommand: {
subcommands.add(cur.name);
for (const option of cur.options ?? []) _options.push(option);
} break;
_createLookupTable(table, opt.options ?? [], name);
} break;
case ApplicationCommandOptionType.SubcommandGroup: {
for (const command of cur.options ?? []) _options.push(command);
} break;
_createLookupTable(table, opt.options ?? [], name);
} break;
default: {
if ('autocomplete' in cur && cur.autocomplete) {
const choice = iAutocomplete.options.getFocused(true);
assert( 'command' in cur, 'No `command` property found for option ' + cur.name);
if (subcommands.size > 0) {
const parent = iAutocomplete.options.getSubcommand();
const parentAndOptionMatches =
subcommands.has(parent) && cur.name === choice.name;
if (parentAndOptionMatches) {
return { ...cur, parent };
}
} else {
if (cur.name === choice.name) {
return { ...cur, parent: undefined };
}
}
if(Reflect.get(opt, 'autocomplete') === true) {
table.set(name, opt as SernAutocompleteData)
}
} break;
}
}
}
}

}

interface InteractionTypable {
type: InteractionType;
Expand Down
18 changes: 12 additions & 6 deletions src/handlers/interaction.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { Module } from '../types/core-modules'
import type { Module, SernAutocompleteData } from '../types/core-modules'
import { callPlugins, executeModule } from './event-utils';
import { SernError } from '../core/structures/enums'
import { createSDT, isAutocomplete, isCommand, isContextCommand, isMessageComponent, isModal, resultPayload, treeSearch } from '../core/functions'
import { createSDT, isAutocomplete, isCommand, isContextCommand, isMessageComponent, isModal, resultPayload } from '../core/functions'
import type { UnpackedDependencies } from '../types/utility';
import * as Id from '../core/id'
import { Context } from '../core/structures/context';
import path from 'node:path';



Expand All @@ -31,10 +32,15 @@ export function interactionHandler(deps: UnpackedDependencies, defaultPrefix?: s
let payload;
// handles autocomplete
if(isAutocomplete(event)) {
//@ts-ignore stfu
const { command } = treeSearch(event, module.options);
payload= { module: command as Module, //autocomplete is not a true "module" warning cast!
args: [event, createSDT(command, deps, params)] };
const lookupTable = module.locals['@sern/lookup-table'] as Map<string, SernAutocompleteData>
const subCommandGroup = event.options.getSubcommandGroup() ?? "",
subCommand = event.options.getSubcommand() ?? "",
option = event.options.getFocused(true),
fullPath = path.posix.join("<parent>", subCommandGroup, subCommand, option.name)

const resolvedModule = (lookupTable.get(fullPath)!.command) as Module
payload= { module: resolvedModule , //autocomplete is not a true "module" warning cast!
args: [event, createSDT(resolvedModule, deps, params)] };
// either CommandTypes Slash | ContextMessage | ContextUesr
} else if(isCommand(event)) {
const sdt = createSDT(module, deps, params)
Expand Down
11 changes: 9 additions & 2 deletions src/handlers/ready.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import * as Files from '../core/module-loading'
import { once } from 'node:events';
import { resultPayload } from '../core/functions';
import { createLookupTable, resultPayload } from '../core/functions';
import { CommandType } from '../core/structures/enums';
import { Module } from '../types/core-modules';
import { Module, SernOptionsData } from '../types/core-modules';
import type { UnpackedDependencies, Wrapper } from '../types/utility';
import { callInitPlugins } from './event-utils';
import { SernAutocompleteData } from '..';

export default async function(dirs: string | string[], deps : UnpackedDependencies) {
const { '@sern/client': client,
Expand All @@ -28,6 +29,12 @@ export default async function(dirs: string | string[], deps : UnpackedDependenci
throw Error(`Found ${module.name} at ${module.meta.absPath}, which has incorrect \`type\``);
}
const resultModule = await callInitPlugins(module, deps, true);

if(module.type === CommandType.Both || module.type === CommandType.Slash) {
const options = (Reflect.get(module, 'options') ?? []) as SernOptionsData[];
const lookupTable = createLookupTable(options)
module.locals['@sern/lookup-table'] = lookupTable;
}
// FREEZE! no more writing!!
commands.set(resultModule.meta.id, Object.freeze(resultModule));
sEmitter.emit('module.register', resultPayload('success', resultModule));
Expand Down
84 changes: 84 additions & 0 deletions test/autocomp.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { describe } from 'node:test'
import { bench } from 'vitest'
import { SernAutocompleteData, SernOptionsData } from '../src'
import { createRandomChoice } from './setup/util'
import { ApplicationCommandOptionType, AutocompleteFocusedOption, AutocompleteInteraction } from 'discord.js'
import { createLookupTable } from '../src/core/functions'
import assert from 'node:assert'

/**
* Uses an iterative DFS to check if an autocomplete node exists on the option tree
* This is the old internal method that sern used to resolve autocomplete
* @param iAutocomplete
* @param options
*/
function treeSearch(
choice: AutocompleteFocusedOption,
parent: string|undefined,
options: SernOptionsData[] | undefined,
): SernAutocompleteData & { parent?: string } | undefined {
if (options === undefined) return undefined;
//clone to prevent mutation of original command module
const _options = options.map(a => ({ ...a }));
const subcommands = new Set();
while (_options.length > 0) {
const cur = _options.pop()!;
switch (cur.type) {
case ApplicationCommandOptionType.Subcommand: {
subcommands.add(cur.name);
for (const option of cur.options ?? []) _options.push(option);
} break;
case ApplicationCommandOptionType.SubcommandGroup: {
for (const command of cur.options ?? []) _options.push(command);
} break;
default: {
if ('autocomplete' in cur && cur.autocomplete) {
assert( 'command' in cur, 'No `command` property found for option ' + cur.name);
if (subcommands.size > 0) {
const parentAndOptionMatches =
subcommands.has(parent) && cur.name === choice.name;
if (parentAndOptionMatches) {
return { ...cur, parent };
}
} else {
if (cur.name === choice.name) {
return { ...cur, parent: undefined };
}
}
}
} break;
}
}
}

const options: SernOptionsData[] = [
createRandomChoice(),
createRandomChoice(),
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'autocomplete',
description: 'here',
autocomplete: true,
command: { onEvent: [], execute: () => {} },
},
]


const table = createLookupTable(options)


describe('autocomplete lookup', () => {

bench('lookup table', () => {
table.get('<parent>/autocomplete')
}, { time: 500 })


bench('naive treeSearch', () => {
treeSearch({ focused: true,
name: 'autocomplete',
value: 'autocomplete',
type: ApplicationCommandOptionType.String }, undefined, options)
}, { time: 500 })
})
Loading

0 comments on commit 974c30f

Please sign in to comment.