4.1.0-beta.4 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
#137
c778561Thanks @l7aromeo! -@Cooldowntakesby, a function of the call that counts calls apart by a value, such as the account a button acts on:@Cooldown({ seconds: 3600, by: (_context, { uid }: { uid: string }) => uid })lets a user check each of their accounts in once an hour, where before the first check-in blocked the rest.byreceives the handler's params as the handler does, after validation and pipes, and the handler's params are checked against the onesbydeclares at compile time. Withper: 'global', the limit is per resource across every user. Returningundefinedcounts as before; an errorbythrows goes to the exception filters, and nothing is counted.inspectHandler(...).cooldownsnow reportsbyalongsidebypass.#143
add425dThanks @l7aromeo! -testCooldownStoreinmeocord/testingchecks aCooldownStoreyou write yourself, over Postgres, SQLite, MongoDB or anything else, against the behaviourMemoryCooldownStoredefines. It registers its cases with your test framework'sdescribe,itandexpect, so it runs under Vitest or Jest:TypeScript import { testCooldownStore } from 'meocord/testing' testCooldownStore('PostgresCooldownStore', () => new PostgresCooldownStore(sql), { describe, it, expect })It covers calls within a window, a sliding window,
retryAfterMscounted from the oldest call still in the window, each key counted on its own, calls in the same millisecond kept distinct, and several concurrent calls at the limit where exactly one passes. It uses real time with short windows and takes a few seconds. The README's Store recipes show stores for Postgres, SQLite and MongoDB.#140
28f0f0aThanks @l7aromeo! -ExecutionContext.getHandlerParams<P>()returns the handler's params, its second argument, to any stage: a command's options, a component's customId params, a modal's fields or a message pattern's params. A guard sees them raw, an interceptor raw beforenext.handle()and validated and piped after it, and a filter as they were when the error was thrown. It returns a patterned@MessageHandler's params too, and isundefinedfor message listeners, reaction and event handlers, and a call no handler was reached for. It is separate fromgetParams(), which stays the running stage's own{ provide, params }.createExecutionContexttakeshandlerParamsfor unit tests.getArgs()now returns the arguments as they stand too, so after validation and pipes its second argument is the validated and piped params, the same valuegetHandlerParams()returns; in 4.1.0-beta.1 to beta.3 it kept returning the raw arguments.- Breaking
#148
abf9fd8Thanks @l7aromeo! -@MessageHandlertakes patterns with params, using the{name}syntax of customId routes:@MessageHandler('roll {sides} {note...?}')gives the handler{ sides, note }as its second argument.{name}is one word, and words in quotes count as one;{name...}takes the rest of the message as typed;{name?}and{name...?}are optional; the last three come only at the end.@Validate, pipes,@Cooldown({ by })andExecutionContext.getHandlerParams()see a patterned message handler's params as they do a component's.@MeoCord({ messages: { prefix, mention, caseSensitive } })sets the prefix, a string, a list or a function of the message, accepts a mention of the bot whenmentionis on, and matches the prefix and literal words in any case unlesscaseSensitiveis set; a handler overrides them with@MessageHandler('ping', { prefix: false | '?' | ['?', '??'], caseSensitive }). Only the most specific matching pattern runs, across every controller: more literal words first, then a fixed number of words before a rest, then fewer params.@MessageHandler()without a pattern still runs for every message. A pattern that cannot be read, and two that match the same messages, stop the bot at startup. Inmeocord/testing,resolveRoute(App, { content })resolves a message,invoke(Controller, 'method', message)passes the params the pattern captures,inspectHandler(...).patternreports the pattern, andcreateMockMessage()comes from a user rather than a bot.A 4.0 keyword now matches in any case and word by word, only the most specific of two matching patterns runs, two handlers with the same keyword stop the bot at startup, and a configured prefix applies to existing keywords unless they set
prefix: false. See Message keywords match in any case, and only one runs. #147
5a2813fThanks @l7aromeo! - Observers see every call MeoCord dispatches once it has settled, for metrics and audit logs. Mark a class@Observer(), implementDispatchObserver'sonSettled(context, result), and list it in@MeoCord({ observers }). It is told about commands, components, modals, autocomplete, message, reaction and event handlers, and interactions no handler matches. TheDispatchResultcarries anoutcome('ran','denied','cooldown','invalid','error'or'not-found'),startedAtin epoch milliseconds, adurationMscovering the whole call through the filters and the fallback, the guard that denied it asdeniedBy, where an interaction's answer stood asresponse('replied','deferred'or'unanswered'), theerror, and whether a filter or the fallbackhandledit. An optionalonStart(context)sees the call begin, before the guards, with the same context objectonSettledlater receives, so aWeakMappairs them into a span. Observers run in the order listed, and the call never waits for them: one that is slow never delays a handler, and one that throws is logged while the rest still run.@Observer({ types })limits one to some kinds of call. They are services, so they inject dependencies and theironReadyandonShutdownhooks run in dependency order. A message no handler matches is not reported. Inmeocord/testing,invokeandemitwait for the module's observers, the testing module takesobservers, andinspectHandler(...).observerslists an app's.npx meocord g ob <name>generates one with its spec.#144
6798abdThanks @l7aromeo! -RedisCooldownStoreinmeocord/commonkeeps@Cooldowncounts on Redis, so they survive a restart and are shared by every shard and process that uses the same server, which keeps'user'and'global'cooldowns exact under process sharding. MeoCord adds no Redis dependency: giveRedisCooldownStore.usinga function that runs a script with your client, and pass what it returns to@MeoCord({ cooldownStore }):TypeScript import { RedisCooldownStore } from 'meocord/common' // node-redis cooldownStore: RedisCooldownStore.using((script, keys, args) => redis.eval(script, { keys, arguments: args })) // ioredis cooldownStore: RedisCooldownStore.using((script, keys, args) => redis.eval(script, keys.length, ...keys, ...args))One Lua script checks and records each call as one step, timed by the server's
TIME, with calls in the same millisecond kept apart and every key set to expire. Keys start withmeocord:cooldown:, or{ prefix }; pass{ evalsha }to send the script by its SHA1, in full only when the server answersNOSCRIPT. It runs on Redis 5 and later, Valkey, KeyDB, Dragonfly and Upstash; Garnet runs Lua only in part, so check it withtestCooldownStorefirst. See Where calls are counted.#145
8f8690cThanks @l7aromeo! -ShardedCooldownStoreinmeocord/commonmakes'user'and'global'cooldowns exact under process sharding without a database. Each shard asks the shard manager, which counts every shard's calls in its memory over the IPC the shards already use:TypeScript import { ShardedCooldownStore } from 'meocord/common' @MeoCord({ controllers: [...], clientOptions: {...}, cooldownStore: ShardedCooldownStore }) export default class App {}Counts are kept while the manager runs, so a shard that restarts keeps them, but they start again when the whole bot restarts. For counts that outlive a restart, or a bot on several hosts, use
RedisCooldownStore. If the manager does not answer within a second, a shard counts the call itself and warns once. The startup warning about per-shard cooldowns stays silent with this store, and now names both shared stores. See Where calls are counted.
Patch Changes
- Breaking
#142
400f903Thanks @l7aromeo! - AbundleDependenciesbuild starts under Bun, and under any devtool.- Bun. A bundled ES module that probes for CommonJS, as lodash-es does with
typeof exports, leftmoduleandexportsin the bundle's top scope. Bun then read the whole bundle as CommonJS and stopped at startup withCannot use import statement with CommonJS-only features, while Node ran it. Those probes now seeundefined, as they do in any ES module, sobun dist/main.jsstarts. CommonJS dependencies keep their ownmoduleandexports. Rebuild to pick this up; nothing else changes. - Eval devtools. An
eval-*devtool, set throughoutput.sourceMaportools.rspackin thersbuildhook, is built as its non-eval equivalent (eval-source-mapassource-map, plainevalas no source map), with a warning at build time. A module evaluated from a string cannot readimport.meta, so withbundleDependenciessuch a bundle stopped at startup withSyntaxError: import.meta is only valid inside modules. To silence the warning, setoutput.sourceMap.jsto the non-eval devtool yourself. - Development builds emit
cheap-module-source-maprather thaneval-source-map. See Smaller changes in the upgrade guide.
- Bun. A bundled ES module that probes for CommonJS, as lodash-es does with