Skip to content

Commit 974c30f

Browse files
authored
fix: faster autocomplete lookup (#387)
* fix:faster-autocmp * fixinitializing * fix * fixonwindows * unconsole
1 parent 3a56972 commit 974c30f

File tree

7 files changed

+382
-331
lines changed

7 files changed

+382
-331
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"prepare": "tsc",
2121
"pretty": "prettier --write .",
2222
"tdd": "vitest",
23+
"benchmark": "vitest bench",
2324
"test": "vitest --run",
2425
"analyze-imports": "npx depcruise src --include-only \"^src\" --output-type dot | dot -T svg > dependency-graph.svg"
2526
},

src/core/functions.ts

+20-40
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import type {
66
MessageContextMenuCommandInteraction,
77
ModalSubmitInteraction,
88
UserContextMenuCommandInteraction,
9-
AutocompleteInteraction
9+
AutocompleteInteraction,
1010
} from 'discord.js';
1111
import { ApplicationCommandOptionType, InteractionType } from 'discord.js';
1212
import { PluginType } from './structures/enums';
13-
import assert from 'assert';
1413
import type { Payload, UnpackedDependencies } from '../types/utility';
14+
import path from 'node:path'
1515

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

60-
/**
61-
* Uses an iterative DFS to check if an autocomplete node exists on the option tree
62-
* @param iAutocomplete
63-
* @param options
64-
*/
65-
export function treeSearch(
66-
iAutocomplete: AutocompleteInteraction,
67-
options: SernOptionsData[] | undefined,
68-
): SernAutocompleteData & { parent?: string } | undefined {
69-
if (options === undefined) return undefined;
70-
//clone to prevent mutation of original command module
71-
const _options = options.map(a => ({ ...a }));
72-
const subcommands = new Set();
73-
while (_options.length > 0) {
74-
const cur = _options.pop()!;
75-
switch (cur.type) {
60+
export const createLookupTable = (options: SernOptionsData[]): Map<string, SernAutocompleteData> => {
61+
const table = new Map<string, SernAutocompleteData>();
62+
_createLookupTable(table, options, "<parent>");
63+
return table;
64+
}
65+
66+
const _createLookupTable = (table: Map<string, SernAutocompleteData>, options: SernOptionsData[], parent: string) => {
67+
for (const opt of options) {
68+
const name = path.posix.join(parent, opt.name)
69+
switch(opt.type) {
7670
case ApplicationCommandOptionType.Subcommand: {
77-
subcommands.add(cur.name);
78-
for (const option of cur.options ?? []) _options.push(option);
79-
} break;
71+
_createLookupTable(table, opt.options ?? [], name);
72+
} break;
8073
case ApplicationCommandOptionType.SubcommandGroup: {
81-
for (const command of cur.options ?? []) _options.push(command);
82-
} break;
74+
_createLookupTable(table, opt.options ?? [], name);
75+
} break;
8376
default: {
84-
if ('autocomplete' in cur && cur.autocomplete) {
85-
const choice = iAutocomplete.options.getFocused(true);
86-
assert( 'command' in cur, 'No `command` property found for option ' + cur.name);
87-
if (subcommands.size > 0) {
88-
const parent = iAutocomplete.options.getSubcommand();
89-
const parentAndOptionMatches =
90-
subcommands.has(parent) && cur.name === choice.name;
91-
if (parentAndOptionMatches) {
92-
return { ...cur, parent };
93-
}
94-
} else {
95-
if (cur.name === choice.name) {
96-
return { ...cur, parent: undefined };
97-
}
98-
}
77+
if(Reflect.get(opt, 'autocomplete') === true) {
78+
table.set(name, opt as SernAutocompleteData)
9979
}
10080
} break;
10181
}
102-
}
103-
}
82+
}
10483

84+
}
10585

10686
interface InteractionTypable {
10787
type: InteractionType;

src/handlers/interaction.ts

+12-6
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import type { Module } from '../types/core-modules'
1+
import type { Module, SernAutocompleteData } from '../types/core-modules'
22
import { callPlugins, executeModule } from './event-utils';
33
import { SernError } from '../core/structures/enums'
4-
import { createSDT, isAutocomplete, isCommand, isContextCommand, isMessageComponent, isModal, resultPayload, treeSearch } from '../core/functions'
4+
import { createSDT, isAutocomplete, isCommand, isContextCommand, isMessageComponent, isModal, resultPayload } from '../core/functions'
55
import type { UnpackedDependencies } from '../types/utility';
66
import * as Id from '../core/id'
77
import { Context } from '../core/structures/context';
8+
import path from 'node:path';
89

910

1011

@@ -31,10 +32,15 @@ export function interactionHandler(deps: UnpackedDependencies, defaultPrefix?: s
3132
let payload;
3233
// handles autocomplete
3334
if(isAutocomplete(event)) {
34-
//@ts-ignore stfu
35-
const { command } = treeSearch(event, module.options);
36-
payload= { module: command as Module, //autocomplete is not a true "module" warning cast!
37-
args: [event, createSDT(command, deps, params)] };
35+
const lookupTable = module.locals['@sern/lookup-table'] as Map<string, SernAutocompleteData>
36+
const subCommandGroup = event.options.getSubcommandGroup() ?? "",
37+
subCommand = event.options.getSubcommand() ?? "",
38+
option = event.options.getFocused(true),
39+
fullPath = path.posix.join("<parent>", subCommandGroup, subCommand, option.name)
40+
41+
const resolvedModule = (lookupTable.get(fullPath)!.command) as Module
42+
payload= { module: resolvedModule , //autocomplete is not a true "module" warning cast!
43+
args: [event, createSDT(resolvedModule, deps, params)] };
3844
// either CommandTypes Slash | ContextMessage | ContextUesr
3945
} else if(isCommand(event)) {
4046
const sdt = createSDT(module, deps, params)

src/handlers/ready.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import * as Files from '../core/module-loading'
22
import { once } from 'node:events';
3-
import { resultPayload } from '../core/functions';
3+
import { createLookupTable, resultPayload } from '../core/functions';
44
import { CommandType } from '../core/structures/enums';
5-
import { Module } from '../types/core-modules';
5+
import { Module, SernOptionsData } from '../types/core-modules';
66
import type { UnpackedDependencies, Wrapper } from '../types/utility';
77
import { callInitPlugins } from './event-utils';
8+
import { SernAutocompleteData } from '..';
89

910
export default async function(dirs: string | string[], deps : UnpackedDependencies) {
1011
const { '@sern/client': client,
@@ -28,6 +29,12 @@ export default async function(dirs: string | string[], deps : UnpackedDependenci
2829
throw Error(`Found ${module.name} at ${module.meta.absPath}, which has incorrect \`type\``);
2930
}
3031
const resultModule = await callInitPlugins(module, deps, true);
32+
33+
if(module.type === CommandType.Both || module.type === CommandType.Slash) {
34+
const options = (Reflect.get(module, 'options') ?? []) as SernOptionsData[];
35+
const lookupTable = createLookupTable(options)
36+
module.locals['@sern/lookup-table'] = lookupTable;
37+
}
3138
// FREEZE! no more writing!!
3239
commands.set(resultModule.meta.id, Object.freeze(resultModule));
3340
sEmitter.emit('module.register', resultPayload('success', resultModule));

test/autocomp.bench.ts

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { describe } from 'node:test'
2+
import { bench } from 'vitest'
3+
import { SernAutocompleteData, SernOptionsData } from '../src'
4+
import { createRandomChoice } from './setup/util'
5+
import { ApplicationCommandOptionType, AutocompleteFocusedOption, AutocompleteInteraction } from 'discord.js'
6+
import { createLookupTable } from '../src/core/functions'
7+
import assert from 'node:assert'
8+
9+
/**
10+
* Uses an iterative DFS to check if an autocomplete node exists on the option tree
11+
* This is the old internal method that sern used to resolve autocomplete
12+
* @param iAutocomplete
13+
* @param options
14+
*/
15+
function treeSearch(
16+
choice: AutocompleteFocusedOption,
17+
parent: string|undefined,
18+
options: SernOptionsData[] | undefined,
19+
): SernAutocompleteData & { parent?: string } | undefined {
20+
if (options === undefined) return undefined;
21+
//clone to prevent mutation of original command module
22+
const _options = options.map(a => ({ ...a }));
23+
const subcommands = new Set();
24+
while (_options.length > 0) {
25+
const cur = _options.pop()!;
26+
switch (cur.type) {
27+
case ApplicationCommandOptionType.Subcommand: {
28+
subcommands.add(cur.name);
29+
for (const option of cur.options ?? []) _options.push(option);
30+
} break;
31+
case ApplicationCommandOptionType.SubcommandGroup: {
32+
for (const command of cur.options ?? []) _options.push(command);
33+
} break;
34+
default: {
35+
if ('autocomplete' in cur && cur.autocomplete) {
36+
assert( 'command' in cur, 'No `command` property found for option ' + cur.name);
37+
if (subcommands.size > 0) {
38+
const parentAndOptionMatches =
39+
subcommands.has(parent) && cur.name === choice.name;
40+
if (parentAndOptionMatches) {
41+
return { ...cur, parent };
42+
}
43+
} else {
44+
if (cur.name === choice.name) {
45+
return { ...cur, parent: undefined };
46+
}
47+
}
48+
}
49+
} break;
50+
}
51+
}
52+
}
53+
54+
const options: SernOptionsData[] = [
55+
createRandomChoice(),
56+
createRandomChoice(),
57+
createRandomChoice(),
58+
{
59+
type: ApplicationCommandOptionType.String,
60+
name: 'autocomplete',
61+
description: 'here',
62+
autocomplete: true,
63+
command: { onEvent: [], execute: () => {} },
64+
},
65+
]
66+
67+
68+
const table = createLookupTable(options)
69+
70+
71+
describe('autocomplete lookup', () => {
72+
73+
bench('lookup table', () => {
74+
table.get('<parent>/autocomplete')
75+
}, { time: 500 })
76+
77+
78+
bench('naive treeSearch', () => {
79+
treeSearch({ focused: true,
80+
name: 'autocomplete',
81+
value: 'autocomplete',
82+
type: ApplicationCommandOptionType.String }, undefined, options)
83+
}, { time: 500 })
84+
})

0 commit comments

Comments
 (0)