4.1.0-beta.5 changelog
Published
Entries marked Breaking change a working bot; the migration guide says what to do about them. Every release of the line is in the changelog.
Minor Changes
#190
6cc22f8Thanks @l7aromeo! -CooldownStore.peekMany(entries)checks a call against its cooldowns without recording it, returning the verdictconsumeManywould.MemoryCooldownStore,ShardedCooldownStore(one message to the shard manager) andRedisCooldownStore(one read-only script; on Redis Cluster, one per slot unlesshashTag: 'handler'keeps a handler's keys in one) answer it from their counts. A store of your own needs nothing: the default allows every call and leaves the refusal toconsumeMany. Override it to let a cooldown refuse a call before the work ahead of its handler.testCooldownStorechecks an override: a peek records nothing and refuses with the waitconsumegives.- Breaking
#157
3ff8175Thanks @l7aromeo! - Cooldowns survive a failing store, and count a handler's stacked cooldowns in one step.- When the store fails.
@MeoCord({ cooldownStoreFailure, cooldownStoreTimeoutMs })decides what a call gets when the cooldown store throws, rejects or does not answer withincooldownStoreTimeoutMs(1000 by default).'deny', the default, refuses it with the newCooldownStoreErrorfrommeocord/common, which the fallback answers privately: "Cooldowns can't be checked right now: try again shortly." A filter can catch it to answer otherwise, and observers seeoutcome: 'error'.'allow'runs it uncounted.- Either way the failure is logged once per outage, with its cause, and again when the store answers. MeoCord never counts a call itself or asks twice, so an answer after the timeout records the call once, in the store.
- Stacked cooldowns, one step.
CooldownStoregainsconsumeMany(entries), which@Cooldowncalls once per call with every stacked cooldown. The default callsconsumefor each in order, so a store of your own keeps working; override it to check every entry and record the call against all of them only if all allow it. The built-in stores do:MemoryCooldownStorechecks them together.ShardedCooldownStoresends one message to the manager.RedisCooldownStoreruns one script, so a call costs one round trip however many cooldowns it has: with 3 stacked and 5 ms to Redis, about 5 ms instead of 16. On Redis Cluster, where a handler's keys sit in different slots, it counts them a script each, in order, unless you pass{ hashTag: 'handler' }to keep each handler's keys in one slot.- With these stores, a call one cooldown refuses no longer spends the others, and waits the longest wait among those that refuse it.
ShardedCooldownStoretreats a manager that does not answer as a store failure, handled bycooldownStoreFailure, rather than counting in the shard.testCooldownStorechecks the batch path too: a batch is counted at once and names the longest wait, and for a store that overridesconsumeMany, a refused batch records nothing and concurrent batches at the limit let exactly one through.
See Smaller changes in the upgrade guide.
- When the store fails.
#164
8b524a0Thanks @l7aromeo! -route()inmeocord/commonbuilds customIds from a component pattern:const ticket = route('ticket/{id}')goes to@Command(ticket, CommandType.BUTTON)in place of the string, andticket.build({ id })givesticket/42. A missing or unknown param fails to compile. So does a button's or select menu's handler whose params require a key its route does not capture, other than the menu's choices; a modal's fields, and a command's options, are not checked. A/or%in a value is encoded as%2For%25, an empty value or an id over Discord's 100 characters throws, and routes are ranked and checked for duplicates as their pattern strings are. Handlers now receive%2Fand%25in a captured param decoded, for string patterns too.- Breaking
#154
2ba4e96Thanks @l7aromeo! - Two component handlers of one type whose customId patterns match exactly the same ids, such asprofile/{uid}andprofile/{id}, stop the bot at startup with an error naming both, as two message handlers with the same pattern do. The bot used to start with a warning and send every click to one of them, chosen by the order the controllers were listed. One handler declared under both spellings is one route, the same pattern on different component types is still allowed, and patterns that only overlap, such asa/{x}/canda/b/{y}, are still a warning.findRouteConflictsandresolveRoutethrow the same error. Controllers generated by 4.0 share their default customIds, such asbutton-click; see Two component handlers with the same customId pattern stop the bot. #197
9fd7ad3Thanks @l7aromeo! - A typed message param's member, user, role or channel is fetched from Discord only once the handler's guards let the call through, so a caller they refuse, or one still on a cooldown that has noby, costs no request, however many IDs the message names. The guards see each such param as anEntityRef: itsid, the entity ascachedwhen discord.js already has it, andresolve()to fetch it;ParamRefsOf<'pattern'>, frommeocord/interface, types the params that way. The handler,@Validate, pipes and@Cooldown({ by })get the entities, as before. Each ID is fetched once however many messages and guards ask for it at the same time. An app's own param type can return anEntityReffromparseto fetch after the guards too.- Breaking
#156
64d9caaThanks @l7aromeo! - Class-level@UseGuard,@UseInterceptor,@UseFilterand@Cooldownon a controller also apply to the handlers a subclass declares itself, as they do in NestJS: a guard on an abstractStaffControllernow guards every command of a class that extends it, where the subclass's own handlers ran without it. Every handler gets the chain inherited handlers had: the subclass's class stages first, then each base's, then the method's, with filters tried and cooldowns counted from the base out.@Controller({ inheritStages: false })keeps a subclass's own handlers to its own class and method stages; the handlers it inherits keep their base's.inspectHandlerlists the resolved chain, and a direct call to a guarded handler runs the same guards in the same order as dispatch.Each handler's stages are resolved once, not on every call, so dispatch pays nothing for the chain. See A base controller's class stages cover its subclasses in the upgrade guide.
#175
e22c401Thanks @l7aromeo! -Loggerprints from a level up, set bylogLevelinmeocord.config.ts('debug','log','warn','error'or'silent') or, for one run, by theMEOCORD_LOG_LEVELenvironment variable, which wins. By default[DEBUG]lines show in development, whereNODE_ENVisdevelopmentas undermeocord start --dev, and are hidden elsewhere, so a production log no longer carries the raw error and stack behind an explained startup failure such as a refused token. To see debug lines in production again, start withMEOCORD_LOG_LEVEL=debugor setlogLevel: 'debug'. An unknownMEOCORD_LOG_LEVELis reported once and ignored. The level is resolved once, on the first line logged, and a suppressed line costs no formatting.#166
0d9b466Thanks @l7aromeo! -@MessageHandlertakesaliases,descriptionandscope.aliases: ['m']lets!m @anarunmute {target:member}, each alias standing in place of the words the pattern begins with.scope: 'guild'or'dm'answers a message sent elsewhere that the command works in a server only, or in direct messages only, and the handler does not run;MessageUsageErrorgainsdmOnlyfor the second.HandlerRegistry's message entries list each command once, with itscommandwords,aliases,description,scope,usage(prefix), the text a usage error shows, andmatches(words), for a help command of the app's own; the README has one to start from.#173
6af9e7aThanks @l7aromeo! - Message patterns take flags and typed lists.{--bots}istruewhen a message gives--botsanywhere after the command word, andfalsewhen it does not;{--from:user?}takes--from=@ana, resolved as a typed param is, and is required without the?.{options:string...}gives the rest of the message as a list, one item per word or "quoted words", and{targets:member...}a list of members, fetched with the message's other members in one request. A flag the command does not have, a typed flag missing or given no value, and a list item of the wrong type each get the usage reply.ParamsOftypes flags asbooleanor their value, and typed lists as arrays. Only a message naming a command with flags is read for them, so a pattern without flags reads--botsas an ordinary word, as before, and{name...}without a type stays the rest of the message as text.#159
4b7ef50Thanks @l7aromeo! - A@MessageHandlerpattern's params can name a type,{name:type}:int,number,bool,duration(10m,2h30m, in milliseconds),member,user,role,channel(by mention, ID or, for a role, its name), words to choose from such as{mode:on|off}, or a type the app adds in@MeoCord({ messages: { types } })and declares inMessageParamTypes. Several optional params may end a pattern, as inban {target:member} {duration:duration?} {reason...?}: each one that another follows takes a word only if it fits its type, so!ban @ana spamminggives a reason and no duration. Each word becomes its value before the guards run, so guards,@Validate, pipes,@Cooldown({ by })and the handler receive members and numbers. A mentioned member, a cached member, user, role or channel costs no request, and the members a message names that are not cached are fetched in one request. The params a handler declares are checked against its pattern at compile time, andParamsOf<'pattern'>gives their type.A message that names a command after a prefix or mention but does not fit its pattern, with a word of the wrong type, a param missing, or a
member,roleorchannelparam sent in a DM, gets the command's usage in a reply, deleted after@MeoCord({ messages: { deleteUsageRepliesAfter } })seconds (10, or 0 to keep it). It is aMessageUsageErrorfrommeocord/common, which the handler's exception filters see first. A message with no prefix or mention gets no reply. Inmeocord/testing,createMockGuild({ members, roles, channels })fills the guild's caches,createMockMessage({ guild })sends a message in that guild or, withnull, in a DM, andinvokeresolves typed params as dispatch does, and answers a prefixed message that names the command but leaves out a param with the sameMessageUsageError, through the handler's filters.#162
d0e6bf7Thanks @l7aromeo! -@ReactionHandlermatches a custom emoji by its id, as well as by name. Pass the id,@ReactionHandler('1234567890123456789'), or the<:party:1234567890123456789>Discord shows when you send\:party:in a message. The handler then runs for that one emoji, rather than for every custom emoji calledpartyacross the bot's servers. A name, and a standard emoji's character, match as before.#161
05bcea6Thanks @l7aromeo! - A select menu's choices arrive in its handler's params, as a modal's fields do.valuesholds the chosen options' values or ids, beside the customId params. discord.js's resolved objects come too:usersandmembersfrom a user select,rolesfrom a role select,channelsfrom a channel select, andusers,membersandrolesfrom a mentionable one.@Validate, pipes,@Cooldown({ by })andgetHandlerParams()see them, so a poll can limit each option on its own withby: (_context, { values }: { values: string[] }) => values[0]. A customId param of the same name keeps winning, and development warns about the clash.invokebuilds them from a mock'svalues,users,members,rolesandchannels. Handlers that readinteraction.valueskeep working.#183
3af939eThanks @l7aromeo! -module.dispatch(input)inmeocord/testingsends an interaction, a message or a reaction through the bot's own dispatch: routed over the module's controllers and itsapp's message options exactly as the bot routes it, then run through the full pipeline of each handler it reaches. It resolves to{ ran, handlers, error? }, wherehandlerslists each handler reached, in the order it ran, with its ownrananderror. What the user is sent reaches the mocks as the bot sends it, including a usage reply and the built-in fallback's answer to an error no filter handles. An error the fallback answers as the user's own outcome resolves inerror: a usage reply, an unknown command, or a guard's, a cooldown's, a validation's or aUserError's refusal. Any other error no filter handles rejects the call once the fallback has answered. A reaction is dispatched with the user who reacted,module.dispatch(reaction, { user, action }), added unless an action is given, so@ReactionHandlercan be tested through routing, whichemitnever reaches.invokestill tests one handler you name.#172
c1fd9e5Thanks @l7aromeo! - A testing module runs the lifecycle hooks.await module.init({ ready: true })runs everyonReadyonce, in the order the bot runs them: each class after the classes and providers it injects, the observers last.await module.close()runs, in reverse, theonShutdownhooks of everything the module has constructed, whether or not it was readied, so a test can close a provider it opened, such as a connection pool a factory made ininit(), instead of leaking it; nothing is constructed just to be shut down.onReadyreceives a client fromcreateMockClientand{ primary: true }, or those passed asinit({ ready: { client, primary } }). Every hook runs even when one throws;initorclosethen rejects with that error, or anAggregateErrornaming each hook that threw.init()withoutreadyruns no hook, as before.- Breaking
#200
c6b4af0Thanks @l7aromeo! - Add themes: design tokens by role, whichrespond()and MeoCord's own views take their colours, emojis and button styles from, set once for the app and changed per controller, handler, server or user. See Theming.What an existing bot sees without changing anything
- Answers sent through
respond()with no colour, an embed withoutcoloror a Components V2 container withoutaccent_color, now show the theme'sprimary,#7680F4unless the app sets another. A colour that is set is kept,0and anullaccent included, and nothing sent aroundrespond()is touched. To send one message as written, pass{ fill: false }(ResponseSendOptions) as the second argument tosend(),edit()orfollowUp(). - MeoCord's error view is coloured by the error's tone:
warningwhen it is the user's own outcome, such as a denied guard, a cooldown, invalid input or aUserError, anddangerfor a fault in the bot. Its loading view uses the theme's loading emoji. Themefrommeocord/commonis deprecated, and goes in MeoCord 5. Its colours still work: each reads the matching role of the call's theme, so code written againstTheme.primaryColorfollows@MeoCord({ theme })and@UseThemewith no change, anderrorColoris thedangerrole. Their values are now the new defaults, tuned for at least 3:1 contrast against every Discord surface: 4.0's wereprimaryColor#5865F2,successColor#28A745,infoColor#17A2B8,errorColor#DC3545andwarningColor#FFC107, which@MeoCord({ theme })sets again if you want them. Assigning one still recolours MeoCord's views, beneath every theme the app sets, and logs a warning once per colour. SeeThemeis deprecated, and its colours changed.- A presenter from an earlier 4.1 beta gets
context.themeand the error'stone. A spec that builds aResponseContextor aPresentedErrorby hand addstheme: createMockTheme()andtone.
What's new
- Tokens:
ThemeColors,ThemeEmojisandThemeButtonsinmeocord/interface, grouped inMeoCordTheme, each role with a default. An app adds tokens of its own by augmenting them; a bad token stops the bot before it logs in, naming where it was set. - Setting and reading:
@MeoCord({ theme })for the app,@UseThemefor a controller or handler, anduseTheme()frommeocord/commonto read the call's theme anywhere the call runs,context.getTheme()in a stage.@MeoCord({ themeFor: { guild, user } })looks a theme up per server and per user, cached, withThemeCacheto clear a result when it changes; see Themes per server and per user. - Presenters:
ResponseContext.themeandPresentedError.tone, socontext.theme.colors[tone]styles an error by kind. - Replies to messages:
@MeoCord({ messages: { replyEmoji: true } })starts MeoCord's text replies to message commands with the theme's emoji. It is off by default. - Testing: calls in a testing module run in its theme as in the bot;
overrideTheme,overrideThemeFor,createMockThemeandwithThemefrommeocord/testingset or check one. - New apps:
meocord createwrites a presenter styled fromcontext.themeandtone,src/types/theme.d.tsfor the app's own tokens besidesrc/types/assets.d.ts, and aneslint.config.tsthat warns on deprecated APIs in app code.
- Answers sent through
#167
0edd2cfThanks @l7aromeo! - A guard, interceptor, filter or pipe can declare the params it takes,declare readonly params?: { channelIds: string[] }, and every{ provide, params }for it is checked against that type: in@UseGuard,@UseInterceptor,@UseFilter,@UsePipeand@MeoCord({ guards, interceptors, filters }). A misspelt param, such aschannelId, or one of the wrong type, fails to compile, where it failed at the first call. A class that declares none takes any params, as before.A guard also has its params whole as
this.params, besides each as a property of its own. An interceptor, filter or pipe, shared across calls, reads them typed withcontext.getParams<StageParams<typeof X>>();StageParamsis exported frommeocord/interface.#169
433a449Thanks @l7aromeo! -factoryProviderinmeocord/commontypes a factory provider: eachuseFactoryparameter is what the token in the same place ofinjectprovides (a class's instance, acreateTokentoken's type, orunknownfor a string or plain symbol), and the factory must return whatprovidestands for. A parameterinjectdoes not supply, one of the wrong type, or a wrong return fails to compile. It returns the provider unchanged, for@MeoCord({ providers })and the testing module.TypeScript factoryProvider({ provide: DATABASE, inject: [Config, PORT], useFactory: (config, port) => new Pool(config.url, port), })A plain
{ provide, useFactory, inject }object works as before. TypeScript cannot type its factory frominjectinside a list, which is why this is a function.#168
645e951Thanks @l7aromeo! - In development, MeoCord warns once per handler that finishes without answering its interaction, which leaves the user with "The application did not respond", or that defers it and never follows up, which leaves them watching it think until Discord gives up. The warning names the handler and what to call. A call a guard denied, or one that failed, is answered by the fallback and never warned about. It is on whileNODE_ENVisdevelopment, as undermeocord start --dev, and off in production;@MeoCord({ warnUnanswered })turns it on or off regardless.#165
ab924dcThanks @l7aromeo! -UserErrorinmeocord/commonis for a mistake the user can fix, such as too few coins or an account that does not exist, rather than a fault in the bot. Throw it from a handler, a pipe, a service or a guard: the built-in fallback shows its message privately for an interaction, even after@Defer, and as a reply to a message, without pinging its author, and logs it only at debug level.TypeScript throw new UserError(`You need ${missing} more coins.`, { code: 'shop.poor', context: { missing } })codeandcontextlet an exception filter or a presenter phrase it otherwise, such as in the user's language; a presenter'serror()receives the error with the interaction.respond(interaction).error(userError)shows its message privately by default.- Observers see the new outcome
'refused', withhandledset, apart from'error', so metrics tell the user's mistakes from the bot's faults. An observer that switches over everyDispatchOutcomegains a case to handle.
Patch Changes
#215
f7f64a4Thanks @l7aromeo! -@Defer({ mode: 'auto' })acknowledges an interaction without a creation time after its delay, 1.5 s unlessaftersays otherwise, instead of at once. The 2.5 s cap counts fromcreatedTimestamp, and without one the deadline was not a number, so the timer fired immediately. A real interaction always has one, but a test's mock did not, so a test of an auto-deferred handler saw an acknowledgement the bot would not send.createMockInteractionandcreateMockMessagenow givecreatedTimestampandcreatedAt: the time anidthe test gives encodes, as discord.js reads it, or, with the generatedid, the time the mock was made. AcreatedTimestampthe test sets wins. Generated ids are unchanged.#176
e1810ccThanks @l7aromeo! - A@MessageHandlerwhose ownprefixis'', no prefix, no longer stops every message command in the bot: each message threwCannot read properties of undefined (reading 'toLowerCase')before any handler ran. The handler now matches messages without a prefix, as its JSDoc says, beside the handlers that use the app's prefix or their own.#195
3cd75b7Thanks @l7aromeo! - AUserErrorthrown from an@Onhandler of an event that carries a message, such asmessageCreate, now answers that message as a message handler's does: a reply with its message, without pinging, the edited message formessageUpdate. It was logged as an error and answered nothing. From any other event it is logged at debug level, not as an error, since it is the user's outcome rather than a fault.#170
e8fffd2Thanks @l7aromeo! -CooldownStoreFailure, the type of@MeoCord({ cooldownStoreFailure }), is exported frommeocord/interface.@MeoCord's declarations referred to it without any entry exporting it, so a consumer declaring a value of that type, or emitting declarations for an app that wraps@MeoCord's options, had no name to import and could hit TS2742.#199
f91aee5Thanks @l7aromeo! - A flag before a command's first word, as in!--bots purge 5, is never read, and the message names no command. Before, such a message ranpurgewhenever some other handler's pattern with flags began with a param, such as{target} {--ping}, so whether it matched depended on unrelated handlers. A pattern that begins with a param still takes its flags anywhere.- Breaking
#152
e03539dThanks @l7aromeo! -meocord generatewrites components that fit a 4.1 app as they are.- A button, modal or select menu takes its customId from its name, and a message handler its pattern:
meocord g co button ticketroutesticketandticket/{id}, andmeocord g co message pingmatchesping. Generated components no longer share the fixed idsbutton-clickorselect-menu, or thebakapattern of the sample message controller. With that one listed, the bot stopped at startup: "match the same messages, so only one of them could ever run". - A nested name gives its whole path to the class, as it already did to the command:
admin/banmakesAdminBanButtonController, so it never shares a class name withban'sBanButtonController. Two classes of one name are refused under process sharding and share cooldown keys. - Controllers answer with
respond(), and their methods are named after the class:handleTicket. The filter template answers withcontext.response?.error(). - After writing,
generatenames the next step, such asNext: add TicketButtonController to @MeoCord({ controllers }) in src/app.ts.It still never editssrc/app.ts.
New apps from
meocord creategetsrc/types/assets.d.ts, soimport logo from './logo.png'and the template's Markdown imports pass the app's owntsc. Its coverage settings leave declaration files out. The sampleapp.tsno longer sets an empty custom activity. To add the declarations to an existing app, see Smaller changes. - A button, modal or select menu takes its customId from its name, and a message handler its pattern:
#184
29fdbcbThanks @l7aromeo! -invokeinmeocord/testingruns a message handler only for a message dispatch would give it. Withroll {sides}in one controller androll 20in another, invoking the first with!roll 20rejects saying dispatch runs the second, rather than running a handler the bot never would.#179
3ec6c2dThanks @l7aromeo! -invokeinmeocord/testinganswers a message that names a command without fitting its pattern with that command's usage only where dispatch would. Withconfig {key}besideconfig set {key} {value...}, invoking the first with!config set prefix ?rejects saying dispatch runs the second, rather than with a usage error the user would never see.#182
920da89Thanks @l7aromeo! -logLevelinmeocord.config.tsapplies to the built bot only. The CLI and tests read it from whateverdist/meocord.config.mjsa previous build left, someocord buildprinted its progress on the first build and nothing on the next, and a test's log lines depended on whether the app had been built. Both now print byMEOCORD_LOG_LEVELand the default; setMEOCORD_LOG_LEVELto quiet them.#182
aaaf328Thanks @l7aromeo! -MEOCORD_LOG_LEVELis read in any case, soMEOCORD_LOG_LEVEL=DEBUGshows debug lines rather than being rejected. A value that names no level is reported even when the configuredlogLeveliserrororsilent, which hid the warning, so a bot that prints nothing tells you why your override did not apply.#185
ba57fccThanks @l7aromeo! -LoggerreadsappNamefrom the config only in the built bot. Elsewhere it readdist/meocord.config.mjsleft by the last build, so the CLI prefixed its lines with a previous build's name, and a test that logged loaded.envthrough that file'simport 'dotenv/config', but only once the app had been built. Tests now never load.envon their own; see Running tests to load it invitest.setup.ts.#157
6827411Thanks @l7aromeo! -MemoryCooldownStore, the default, spends the same time on a call however many calls its key holds. It filtered every call time of a key on each call, so a busy cooldown with a largeuses, such as a'global'one, slowed dispatch as calls built up: at 20,000 calls a second withuses: 1_000_000, about 45 µs a call. Each key's times are now trimmed from the front as they leave the window, which costs about 0.12 µs a call there, and decisions are unchanged.#191
399e4e4Thanks @l7aromeo! - A message command that a guard denies with aGuardDeniedError, or that@Validaterefuses, now gets a reply with the reason, without pinging, deleted after@MeoCord({ messages: { deleteUsageRepliesAfter } })seconds as a usage reply is, and is logged at debug level. It was answered with nothing and logged as an error, though an interaction gets the same reason and neither is a fault. A guard denying a listener, an unpatterned@MessageHandler()or an@Onhandler, still gets no reply, since it only filters what the listener takes, and is now logged at debug level rather than as an error.#198
a9ae5e4Thanks @l7aromeo! - A message after a prefix whose first word names no command is no longer split into words, so an unknown command costs dispatch about 40% less. A message naming a command with flags is split once instead of twice.#184
8e5f76aThanks @l7aromeo! - A message pattern's flag must start with a letter:{--2fa}or{--_x}stops the bot at startup with a message saying so, since a message's--2fais read as a word and the flag could never be given. A rest param with flags taken out keeps its own spacing and line breaks:say {text...} {--loud}with "one\n--loud\ntwo" gives "one\ntwo", where the flag's surroundings were joined by a single space.#158
d724f12Thanks @l7aromeo! - Matching a message against@MessageHandlerpatterns costs the same however many patterns an app has. The patterns are compiled once into an index of their words, a message's words are read once rather than once per pattern, and a message whose first character no prefix or mention begins with is turned away before it is read at all. At 1000 patterns a matching message costs about 0.4 µs where it cost about 0.4 ms, and ordinary chat about 20 ns where it cost 25–50 µs. Which handler a message reaches, and the params it receives, are unchanged.#184
e728d8cThanks @l7aromeo! - A message naming more than 100 uncached members, as amemberlist can, has them fetched 100 at a time. Discord's gateway request for members takes at most 100 IDs, and a larger one was sent whole.#194
7508511Thanks @l7aromeo! - A mention of the bot starts a message command in an app whose handlers all have their own prefixes, whenmentionis on. Such an app took no mention, since it read no starts at all, and did not prefer a handler whosescopefits where the message was sent. Its prefix function is still never called, since no handler uses the app's prefixes.#184
770ca8fThanks @l7aromeo! - A message command'sscopenow decides which handler runs, not only whether it may. A handler whose scope fits where the message was sent runs before one of another scope, so a DM-onlyconfig {key}no longer answers "direct messages only" in a server where an unscopedconfig {words...}fits, and one command may have a server handler and a DM handler with the same pattern, which startup refused. A message that only an out-of-scope handler matches still gets the reply saying where the command works.#184
6664a27Thanks @l7aromeo! - A message full of unclosed quotes no longer costs time growing with the square of its length. Each unclosed quote searched to the end of the message for its close, about 14 ms for a 4000-character message of them, work any user could make the bot do; reading a message is now one pass whatever its quotes.#177
d43f382Thanks @l7aromeo! -createMockMessagecaches what its content mentions, as the gateway delivers a message's mentions with it:<@id>a user inmessage.client.users.cacheand, in a guild, a member inguild.members.cache;<@&id>a role;<#id>a channel; each also inmessage.mentions. A typeduserparam in a test, throughinvokeor dispatch, no longer throwsmessage.client.users.cache.get is not a function.createMockClienthas realusersandchannelscaches, and every mock client is the same bot,createMockClient().user.id, so a message starting with a mention of the bot reaches its handler throughinvoke.createMockMessagetakesclientandusersto set the client it arrived on and more cached users.#163
c1c9ff0Thanks @l7aromeo! - A bot token Discord refuses, or an empty one, is now explained in one line: what is wrong, and where to get a token (Developer Portal → your application → Bot → Reset Token, intoDISCORD_TOKENin.envfor a generated app).app.start()still rejects and sets the exit code, and the error is recognised byisExplainedError, so the generatedmain.tsno longer logs discord.js's error and stack a second time.meocord registerexplains a refused token the same way instead of printing the rawDiscordAPIError401. With process sharding, the manager explains it and exits before spawning any shard.commands.guildswhose ids are all blank, as[process.env.GUILD_ID]leaves it with the variable empty or unset, no longer registers globally. The commands without guilds of their own are registered nowhere, with a warning that names them, leftovers are not cleared even withclearOther, andmeocord registerexits 1.guildsaccepts undefined ids, so[process.env.GUILD_ID]needs no!.With
bundleDependencies, a build that findssupports-colormissing, whichdebugprobes for, prints one line naming the dependency and theoptionalExternalsentry that silences it, instead of the bundler's "Module not found" warning with a code frame.meocord showwithout a flag says to runmeocord show --licenseormeocord show --warrantyinstead of reprinting its options. A config number out of range shows the value and the range, as insharding.shards must be 'auto' or a whole number of shards, 1 or more (got 0). A new application's README lists the observer generator.- Breaking
#153
0b3f3b4Thanks @l7aromeo! -@ReactionHandlerskips reactions from bots, the bot's own included, as@MessageHandlerskips messages from bots. Every handler ran for them: a bot's reaction handlers ran for the reactions it added itself, a poll counted the reactions the bot seeded, and the generated sample answered its own reaction twice. A handler that should still run for bot reactions setsbots: true:@ReactionHandler('📌', { bots: true }), or@ReactionHandler({ bots: true })for every emoji. A partial user is fetched to tell whether it is a bot. See Reactions from bots reach no handler in the upgrade guide.New applications' sample reaction controller answers 😋 once, and its handler for every emoji only logs.
- Breaking
#160
b452256Thanks @l7aromeo! - Testing mocks behave more like discord.js, and a few messages and types say more:- An interaction mock has an
id, achannelIdand auserwith anid, and a message mock anid,author.id,channelIdandguildId, each a snowflake string no other mock in the test run has;createMockUser,createMockGuildandcreateMockChannelget ids too. They were mock objects that all read as[object Object], so two default users were one user, and shared a per-user cooldown. - An interaction mock made without a
guildIdhasguildId,guildandmembernull, as a direct message does, where they were truthy whileinGuild()said otherwise. Giving it aguildIdgives it a member. - The autocomplete mock's
respond()rejects more than 25 choices, as Discord does. invoketakes the interaction for a handler declared with no parameters, which failed to compile.respond().modal()after@Deferacknowledged the interaction says so, and how to fix it.- A class listed alone in
providersis refused with what to write instead: in the testing module it needs no listing, and in@MeoCordit goes inservices. - The
ExceptionFilterand@Catchexamples answer throughcontext.response?.error(), which suits a deferred interaction too. - The README gives the key a cooldown is counted under, with an example.
Tests that relied on the old mock defaults may need a change; see Smaller changes.
- An interaction mock has an
#193
3cc9898Thanks @l7aromeo! - A shared guard that calls another shared guard, such as one injected into it, no longer makes the inner guard read the outer guard'sparams. Each guard reads only the params its own{ provide, params }entry gives; called directly by another guard, it reads its own values.A shared guard whose class takes a param through a setter, such as
set limit(value), gets it again: the value was dropped, and the guard read its own. Such a param has nowhere to be kept per call, so it is set on the shared instance, as it was before shared guards read each call's own params, and the bot warns once that overlapping calls can read each other's. Reading the param as a plain property, or not binding the guard, avoids that.A shared guard whose instance is sealed (
Object.seal(this)) cannot read each call's own params: its properties cannot be changed to do so. It takes them on the one instance, as before, so overlapping calls can read each other's params and a call without params reads the last ones given; the bot now warns about such a guard once. Leave the instance unsealed, or stop binding the guard, to keep calls apart.#187
721ec61Thanks @l7aromeo! - A guard shared as one instance now reads each call's ownparams. A guard bound once, by listing it in@MeoCord({ services })orprovidersor by injecting it into a service, is a single instance for every call. When two handlers gave it different params, such as{ role: 'admin' }and{ role: 'mod' }, and their calls overlapped, one call's guard could read the other's params and allow or deny the wrong call. Each call now sees its own, while the instance, its state and its private fields stay shared. Guards made for each call, the default, are unchanged.No action is needed. The startup warning about a guard listed in
servicesis gone, since such a guard is now safe.A sealed guard that declares the properties its params set, and no
paramsproperty, takes its params again instead of failing the call. A frozen guard given params fails the call with an error that names the guard and says why.#149
b9be088Thanks @l7aromeo! - Stack traces name your source files, lines and columns on Node and Bun, in development and production.meocord startruns node with--enable-source-maps, and its shard processes inherit it. A bundle started any other way, such asnode dist/main.jsin a DockerCMDor under Bun, which applies no source map to a bundle, maps its stacks fromdist/main.js.mapthroughError.prepareStackTrace.- The map is read the first time a stack needs it. Each frame keeps the runtime's format,
at fn (/abs/path/src/file.ts:line:col). - A hook already set on
Error.prepareStackTracereceives the mapped call sites. - On minified Bun builds, a frame for a call can land one line above it.
Under Bun, development traces had pointed into
dist/main.jssince 4.1.0-beta.4 dropped the eval devtool, and production traces always did without the Node flag.Set
sourceMappedStacks: falseinmeocord.config.tsfor an error tracker that applies uploaded source maps to the bundle's positions. See Stack traces.- The map is read the first time a stack needs it. Each frame keeps the runtime's format,
#151
8faddd0Thanks @l7aromeo! - Source-mapped stacks leave alone what a bot's dependencies do withError.captureStackTrace. Some packages give it an object built by a function rather than anError: follow-redirects, which axios loads, and node-fetch 2 both do. Bun's own stack hook refuses such an object, so MeoCord no longer hands it one, and that stack reads as it does with no hook set. A stack hook set before MeoCord's that throws no longer fails the code reading the stack; MeoCord writes the stack itself.- Breaking
#155
e064407Thanks @l7aromeo! - A new app'stest:coveragereads files no spec imports. Coverage counts them as untested, and gets them asfile.ts?cache=…&vitest-uncovered-coverage=true. The template's SWC plugin matched files by extension only, so it skipped those, and istanbul stopped with a syntax error on the first type annotation or decorator in one. The template now passes SWC anincludethat allows that query. For an existing app, see Smaller changes. #218
9022251Thanks @l7aromeo! - A click a discord.js collector takes is no longer answered "Command not found!". A button, select menu or modal submission no@Commandroute matches was answered at once, before a collector'scollectcallback orawaitModalSubmitcould answer it, so the user saw "Command not found!" and the collector's own answer failed as already sent. While anything besides MeoCord listens for the client's interactions, such an interaction is now left to it for 1.5 seconds, and "Command not found!" and its warning come only if nothing has answered it by then. A bot with no other listener, and a command no handler takes, are answered at once as before. In a testing module,dispatch()does the same for the client of the interaction it is given. Observers are told of such an interaction only when nothing answered it, as'not-found'; a click a collector answered is the collector's, and is not reported.