4.2.0 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
#463
4d6b965Thanks @l7aromeo! -@MeoCord({ messages: { handlers: 'concurrent' } })runs a message's matched command and its@MessageHandler()listeners together, so a slow or hung command no longer holds back logging, moderation and the other listeners. Each keeps its own guards, interceptors, filters and observers, and the call settles once all have; no order holds between them, so a listener that reads what the command writes for the same message should keep the default,'sequential'. (docs)Under
'sequential', a running bot now warns, once per handler, when a message's handler takes 5 seconds or more with listeners waiting after it, naming the option.messages: { slowHandlerWarning: false }turns the warning off. AMeoCordTestingModuledoesn't warn unlessslowHandlerWarningistrue, so a test whose fake clock passes 5 seconds inside a handler sees nothing new.#460
ded179fThanks @l7aromeo! -@Controller({ inheritedRoutes: 'replace' })makes a handler the class re-decorates answer only the routes the class declares for it, dropping the ones its base classes declare for that method. That covers every kind: commands, component patterns, message patterns and listeners, reactions and autocompletes. A slash or context menu command that no handler answers any more is not registered. A method overridden without decorators keeps every route it inherits.'keep', the default, answers both, as before; the next major version (5.0) replaces them by default.Two things help with the move, without changing what a bot does:
- The warning that names a re-declared handler still answering an inherited route now says how to drop that route now.
inspectHandlerreports the routes a handler answers by inheritance, asinheritedRoutes.
#459
ff45ac5Thanks @l7aromeo! -createMockRawMember()builds the member Discord sends with an interaction from a server the bot isn't in: plain data withrolesas role ids andpermissionsas a bitfield string, as discord.js keeps it. To test a user-installed command run there, give it as an interaction'smemberwith the server'sguildIdand noguild:- the interaction reads
inRawGuild()true,guildandchannelnull(with itschannelIdkept),userthe member's user andmemberPermissionsthe member's; - a user option's member is the resolved member Discord sends;
- the interaction is typed as a
'raw'one; - a guard that reads
member.roles.cachethrows, as it would in Discord.
A raw member with a
channel, without aguildId, or with a differentuseris refused. Given with aguild, it reads as a cached server's member, as before, with a warning once to leave the guild out. AguildIdgiven alone builds the interaction as before. See https://meocord.dev/docs/4.2/mocks.- the interaction reads
#476
586a493Thanks @l7aromeo! - customId patterns take two new param types:{id:snowflake}and{id:uuid}. The handler gets each as text, typedstring, androute().build()refuses text that isn't one.build()takes either only as a string and throws aTypeErrorfor a number, which may have lost an ID's digits beforebuild()sees it:12345678901234567arrives as12345678901234568.{id:snowflake}takes a Discord ID: 17 to 20 digits with no leading zero, up to the largest 64-bit value. Every ID Discord has made since 2015-01-28 has at least 17 digits, because an ID's top 42 bits count milliseconds since 2015-01-01. The handler keeps the ID as text: from 17 digits on, a JavaScript number can't hold it exactly.{id:uuid}takes a UUID in its canonical 8-4-4-4-12 hex form, in either case, kept as written.
Both rank ahead of an untyped param, so
ticket/{id:snowflake}takes an ID beforeticket/{name}does, whatever the listing. Neither shares a value withint,boolor the other, so they never tie with those. Shorter digits stay anintwhile a JavaScript number holds them exactly. A 16-digit value aboveNumber.MAX_SAFE_INTEGERis neither anintnor a snowflake, so it goes to anumberparam or to text. Between a snowflake and anumberparam, which also takes those digits, the snowflake wins. Use{id:snowflake}for Discord IDs:{id:number}rounds one, giving12345678901234568for12345678901234567.#462
db96d3dThanks @l7aromeo! -MeoCordFactory.create()and a testing module'scompile()report every startup error their checks find, not only the first. These include two handlers of one command, two same-named classes with a cooldown, a provider for a token MeoCord binds itself, and a class nothing can make. Each error is logged with the file it comes from, and then the first is thrown as before: the same error, with the same message, so code and tests that catch it or match its text keep working. A lone error is reported as before.A decorator's startup error, such as an invalid customId pattern, is still thrown as its class is defined, with its message unchanged. It now names what it is about and where:
error.declarationis the handler or class it was applied to, such asTickets.close;error.fileis the source file it is declared in.
The built bot's report puts the handler first wherever the message doesn't already name it.
New, and opt-in: set
startupErrors: 'all'inmeocord.config.tsto report every startup error in one run.- Decorators keep their errors instead of throwing them as each file loads.
create()logs those together with its own errors, so a bot with three mistakes shows all three in one run.- In a test, call
reportAllStartupErrors()frommeocord/testingin the setup file instead.
The default stays
'first', where a decorator throws as its class is defined. The next major version (5.0) makes'all'the default. See https://meocord.dev/docs/4.2/configuration.#457
ea32209Thanks @l7aromeo! -useStrictMocks(), called once in a test setup file, has every mock frommeocord/testingcompute the values discord.js computes, where it reads a truthy placeholder otherwise. Those values include:- a message's
editable,deletable,pinnable,crosspostable,bulkDeletable,hasThreadandpartial; - a member's
manageable,kickable,bannableandmoderatable; - a role's
editable; - a channel's and a thread's
viewable,manageable,deletableandjoinable; partialon users, channels and reactions.
message.threadisnullunless the channel caches a thread under the message's id, and no placeholder warning is logged.To give those values what Discord would, strict mocks:
- cache the bot's member in its server from the start;
- give @everyone the permissions Discord gives it in a new server;
- give a channel or thread made without a server one of its own.
So a default message from another user is not
editableordeletable, and a member is notkickableuntil the bot's member has a role above theirs with the permission. A value a test sets on a mock still wins. The next major version (5.0) computes these values by default. See https://meocord.dev/docs/4.2/mocks.Without the call, mocks read as before. A voice channel's
joinableandspeakableand a DM channel'spartial, which read a placeholder without the warning, now give it too. Every placeholder warning now ends ", or call useStrictMocks() to have the mock compute it now.", so a test that matches a warning's whole text needs the new ending.- a message's
#455
602285bThanks @l7aromeo! -useMockFn(vi.fn), called once in a test setup file, makes every mock frommeocord/testingwith the test runner's own mock function. The runner then treats them as its own: Vitest'sclearMocksandmockResetconfig,vi.clearAllMocks()andvi.mocked(...)reach them, and bun's matchers, which accept only bun's mocks, read them withuseMockFn(mock). jest takesuseMockFn(jest.fn). Without the call, mocks are meocord's own as before. Under jest and bun, whosemockResetdrops a mock's starting behaviour, reset with meocord'sresetAllMocks(), which puts it back. node:test keeps meocord's own mock function.A new project's
vitest.setup.tscallsuseMockFn(vi.fn). An existing project can add the same line to its setup file, before any mock is made. See https://meocord.dev/docs/4.2/mocks.
Patch Changes
#466
e646771Thanks @l7aromeo! -testCooldownStoreno longer fails a correct store on a slow CI runner. Its sliding-window andretryAfterMscases measured elapsed time against their nominal waits, with margins of tens of milliseconds, so a timer that fired a few hundred milliseconds late failed them. They now check against the times measured around each call, in a 2-second window with a second between calls, and still fail a store that resets fixed buckets or counts from the newest call. The suite takes about 2 seconds longer.#474
4f6b307Thanks @l7aromeo! - Component customId patterns now rank segment by segment, left to right, the way most routers rank paths. When several patterns match an id, the first segment where one pattern spells out literal text and the other leaves a param decides it: the literal one runs, whatever order the controllers are listed in. Soprofile/me/{section}takesprofile/me/editfromprofile/{userId}/edit, whileprofile/123/editstill goes to{userId}. If that leaves a pair tied, the narrower type at the first param where they differ runs, the same type order as before: words to choose from, thenbool,int,number, then text.4.1 ranked by how much literal text a whole pattern had. Only these overlapping pairs change handler, and 4.1 named every one of them in its startup warning:
- A pattern with more literal text, or more literal segments, now loses to one that spells out an earlier segment. For example,
a/{x}now runs fora/abcdinstead of{x}/abcd. - Pairs 4.1 left to listing order, such as
a/{x}/canda/b/{y}, now go to the earlier literal whatever the listing, which is the handler 4.1's warning said would run in 5.0. - Typed params are compared position by position instead of summed. For example,
{n:int}/{s}now runs for7/7instead of the one listed first among it and{s}/{n:int}.
Two patterns that the ranking still can't tell apart log a warning at startup, with an id both match. Such a pair has the same literals and equally narrow params at every position, such as
t/{a:on|off}andt/{b:off|no}, which both taket/off, and the one listed first runs. Pairs the ranking decides log no warning.findRouteConflictslists only those tied pairs. A test assertingtoEqual([])passes for every app whose patterns the ranking tells apart. A test expecting a pair the ranking now decides, such asprofile/summary/{uid}andprofile/{ownerId}/{uid}, gets[]instead.The internal
CommandMetadata'sspecificitystill holds the value 4.1 gave, but routing no longer reads it. It's deprecated and goes in the next major version (5.0).- A pattern with more literal text, or more literal segments, now loses to one that spells out an earlier segment. For example,
#484
cecfe6aThanks @l7aromeo! - A new app frommeocord createturns on strict mocks in itsvitest.setup.ts, withuseStrictMocks(), so its mocks compute what discord.js computes, such as a message'seditableor a member'skickable, rather than placeholders. Its sample button andOwnerGuardread the owner's id as{ownerId:snowflake}, so a customId that isn't a Discord id matches nothing. An existing app can adduseStrictMocks()to its own setup file, before any mock is made (docs).