meocord 4.2.0 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +264 -0
  2. package/THIRD_PARTY_NOTICES.md +14 -13
  3. package/dist/cjs/_shared/{controller.decorator-CRn9kWob.cjs → controller.decorator-XKMlcUZz.cjs} +376 -100
  4. package/dist/cjs/_shared/{providers-ChvMqnLd.cjs → providers-BZgJyfZS.cjs} +145 -36
  5. package/dist/cjs/_shared/{sharded-cooldown-store-BdAUTdYs.cjs → sharded-cooldown-store-CsJJyCZd.cjs} +10 -7
  6. package/dist/cjs/_shared/{startup-roots-CQo8mSP8.cjs → startup-roots-CgfPQrNl.cjs} +219 -63
  7. package/dist/cjs/_shared/{translate-error-DhDVbT5M.cjs → translate-error-BUAR1WYG.cjs} +10 -5
  8. package/dist/cjs/common/index.cjs +3 -3
  9. package/dist/cjs/core/index.cjs +185 -168
  10. package/dist/cjs/decorator/index.cjs +21 -6
  11. package/dist/cjs/testing/index.cjs +1594 -286
  12. package/dist/cli.json +1 -1
  13. package/dist/esm/bin/generator.js +4 -5
  14. package/dist/esm/bin/helper/filter-generator.helper.js +4 -5
  15. package/dist/esm/bin/helper/guard-generator.helper.js +4 -5
  16. package/dist/esm/bin/helper/interceptor-generator.helper.js +4 -5
  17. package/dist/esm/bin/helper/observer-generator.helper.js +4 -5
  18. package/dist/esm/bin/helper/pipe-generator.helper.js +4 -5
  19. package/dist/esm/bin/helper/service-generator.helper.js +4 -5
  20. package/dist/esm/bin/meocord.js +84 -25
  21. package/dist/esm/build/load-config.pre-entry.js +13 -3
  22. package/dist/esm/build/rsbuild-config.js +16 -12
  23. package/dist/esm/build/typed-externals.js +36 -0
  24. package/dist/esm/common/cooldown-store.js +10 -2
  25. package/dist/esm/common/errors.js +25 -0
  26. package/dist/esm/common/meocord-text.js +11 -8
  27. package/dist/esm/common/response/call-order.js +9 -4
  28. package/dist/esm/common/response/presenter.js +33 -6
  29. package/dist/esm/common/response/response-state.js +84 -10
  30. package/dist/esm/common/route.js +11 -4
  31. package/dist/esm/common/sharded-cooldown-store.js +1 -1
  32. package/dist/esm/common/translator.js +37 -6
  33. package/dist/esm/core/command-registration.js +93 -24
  34. package/dist/esm/core/command-routes.js +2 -2
  35. package/dist/esm/core/cooldown-runner.js +29 -8
  36. package/dist/esm/core/fallback.js +20 -11
  37. package/dist/esm/core/guard-runner.js +16 -3
  38. package/dist/esm/core/handler-input.js +4 -3
  39. package/dist/esm/core/lifecycle-hooks.js +134 -47
  40. package/dist/esm/core/meocord-factory.js +55 -32
  41. package/dist/esm/core/meocord.app.js +116 -75
  42. package/dist/esm/core/providers.js +7 -2
  43. package/dist/esm/core/running-calls.js +43 -0
  44. package/dist/esm/core/shard-context.js +5 -6
  45. package/dist/esm/core/shard-manager.js +16 -10
  46. package/dist/esm/core/startup-roots.js +21 -1
  47. package/dist/esm/core/theme-resolvers.js +4 -1
  48. package/dist/esm/core/theme-scope.js +3 -1
  49. package/dist/esm/core/theme-validation.js +17 -10
  50. package/dist/esm/decorator/controller.decorator.js +56 -20
  51. package/dist/esm/decorator/cooldown.decorator.js +17 -0
  52. package/dist/esm/decorator/guard.decorator.js +3 -2
  53. package/dist/esm/package.json.js +1 -1
  54. package/dist/esm/testing/catalog.js +26 -10
  55. package/dist/esm/testing/cooldown-store-suite.js +67 -5
  56. package/dist/esm/testing/discord-defaults.js +205 -30
  57. package/dist/esm/testing/meocord-testing-module.js +134 -28
  58. package/dist/esm/testing/mock-fn.js +107 -26
  59. package/dist/esm/testing/mock-interaction.js +1026 -172
  60. package/dist/esm/testing/snowflake.js +9 -2
  61. package/dist/esm/testing/strict-mocks.js +37 -17
  62. package/dist/esm/util/common.util.js +47 -19
  63. package/dist/esm/util/dev-runner.util.js +8 -8
  64. package/dist/esm/util/generator-cli.util.js +4 -5
  65. package/dist/esm/util/interaction.util.js +2 -2
  66. package/dist/esm/util/meocord-config-loader.util.js +10 -2
  67. package/dist/esm/util/meocord-config-validation.util.js +3 -5
  68. package/dist/esm/util/meocord-source-config.util.js +65 -20
  69. package/dist/esm/util/method-wrapper.util.js +16 -0
  70. package/dist/esm/util/platform.util.js +10 -10
  71. package/dist/esm/util/shard-process.util.js +4 -1
  72. package/dist/esm/util/sharding-mode.util.js +5 -7
  73. package/dist/esm/util/shared-state.util.js +21 -0
  74. package/dist/esm/util/tsconfig-paths.util.js +29 -0
  75. package/dist/esm/util/tsconfig.util.js +10 -10
  76. package/dist/esm/util/value.util.js +12 -1
  77. package/dist/types/common/index.d.cts +6 -6
  78. package/dist/types/common/index.d.ts +6 -6
  79. package/dist/types/core/index.d.cts +3 -2
  80. package/dist/types/core/index.d.ts +3 -2
  81. package/dist/types/decorator/index.d.cts +1 -0
  82. package/dist/types/decorator/index.d.ts +1 -0
  83. package/dist/types/{index-CdXsFgjE.d.ts → index-DKJclIi-.d.ts} +45 -21
  84. package/dist/types/{index-nn5p7OoX.d.cts → index-Dlf0ZZtS.d.cts} +45 -21
  85. package/dist/types/interface/index.d.cts +1 -1
  86. package/dist/types/interface/index.d.ts +1 -1
  87. package/dist/types/{route-BZgFoNeB.d.cts → route-Bh1U13yy.d.cts} +3 -2
  88. package/dist/types/{route-BZgFoNeB.d.ts → route-Bh1U13yy.d.ts} +3 -2
  89. package/dist/types/testing/index.d.cts +175 -53
  90. package/dist/types/testing/index.d.ts +175 -53
  91. package/dist/types/{theme-resolvers-UKr3-6Ys.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
  92. package/dist/types/{theme-resolvers-BcOJVzR9.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
  93. package/dist/types/{translator-DHMkkCpg.d.cts → translator-Bg7hwxDF.d.cts} +21 -8
  94. package/dist/types/{translator-BViWKtLJ.d.ts → translator-CeP4bCzp.d.ts} +21 -8
  95. package/package.json +2 -2
@@ -1,10 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  var discord_js = require('discord.js');
4
- var controller_decorator = require('./controller.decorator-CRn9kWob.cjs');
5
- var providers = require('./providers-ChvMqnLd.cjs');
4
+ var controller_decorator = require('./controller.decorator-XKMlcUZz.cjs');
5
+ var providers = require('./providers-BZgJyfZS.cjs');
6
6
  var enum_index = require('../enum/index.cjs');
7
- var translateError = require('./translate-error-DhDVbT5M.cjs');
7
+ var node_async_hooks = require('node:async_hooks');
8
+ var translateError = require('./translate-error-BUAR1WYG.cjs');
8
9
 
9
10
  /**
10
11
  * How a pattern ranks, one character per segment twice over: `L` for literal text or `P` for a param, then how narrow
@@ -519,7 +520,10 @@ function errorCode(error) {
519
520
  return send(reply.withoutFiles);
520
521
  }
521
522
  }
522
- /** The text of a reply to a message, after the call's `emojis.warning` when `withEmoji`. */ const replyText = (text, withEmoji)=>withEmoji ? `${controller_decorator.useTheme().emojis.warning} ${text}` : text;
523
+ /** The text of a reply to a message, after the call's `emojis.warning` when `withEmoji`, cut to fit a message's content. */ function replyText(text, withEmoji) {
524
+ const emoji = withEmoji ? `${controller_decorator.useTheme().emojis.warning} ` : '';
525
+ return emoji + controller_decorator.fitText(text, controller_decorator.TEXT_LIMITS.content - emoji.length);
526
+ }
523
527
  /**
524
528
  * `text`, answering `error` for `message`: plain, after the call's `emojis.warning` when `withEmoji`, unless the
525
529
  * presenter's `messageError` draws it as an embed, coloured as an interaction's view is, with that emoji when it has
@@ -527,28 +531,33 @@ function errorCode(error) {
527
531
  * the failure comes back with it, for the caller to report once sent.
528
532
  */ async function presentedReply(message, error, text, withEmoji, logger) {
529
533
  const presenter = controller_decorator.presenterFor(message.client);
534
+ const translator = controller_decorator.translatorOfClient(message.client);
535
+ const locale = controller_decorator.messageLocale(message) ?? translator?.defaultLocale ?? 'en-US';
536
+ // An empty or whitespace-only message, which Discord refuses, reads as MeoCord's generic error text, so neither the plain reply nor a presenter is left without one
537
+ const shown = text.trim() === '' ? controller_decorator.renderText(translator, locale, {
538
+ key: 'meocord.fallback.error'
539
+ }) : text;
530
540
  const plain = {
531
541
  body: {
532
- content: replyText(text, withEmoji)
542
+ content: replyText(shown, withEmoji)
533
543
  }
534
544
  };
535
545
  const { messageError } = presenter;
536
546
  if (!messageError) return plain;
537
547
  try {
538
548
  const theme = await controller_decorator.themeForInteraction(message);
539
- const translator = controller_decorator.translatorOfClient(message.client);
540
- const locale = controller_decorator.messageLocale(message) ?? translator?.defaultLocale ?? 'en-US';
541
549
  const tone = controller_decorator.isUserOutcome(error, message) ? 'warning' : 'danger';
550
+ const presented = {
551
+ message: controller_decorator.fitText(shown, controller_decorator.TEXT_LIMITS.embed),
552
+ error,
553
+ tone
554
+ };
542
555
  const drawn = await messageError.call(presenter, {
543
556
  message,
544
557
  locale,
545
558
  mode: 'embed',
546
559
  theme
547
- }, {
548
- message: text,
549
- error,
550
- tone
551
- });
560
+ }, presented);
552
561
  let view = drawn.color === undefined ? {
553
562
  ...drawn,
554
563
  color: theme.colors.primary
@@ -605,7 +614,8 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
605
614
  await sendReply(reply, (body)=>message.reply({
606
615
  ...body,
607
616
  allowedMentions: {
608
- repliedUser: false
617
+ repliedUser: false,
618
+ parse: []
609
619
  }
610
620
  }));
611
621
  } catch (replyError) {
@@ -822,6 +832,46 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
822
832
  }
823
833
  }
824
834
 
835
+ /**
836
+ * The calls an app is running, which shutdown waits for before the `onShutdown` hooks. A call that asks the app to
837
+ * stop is excused, so a handler awaiting `app.stop()` never waits for itself.
838
+ */ class RunningCalls {
839
+ /** Runs `run` as a call named `label`, tracked until it settles. */ run(label, run) {
840
+ const token = {};
841
+ this.calls.set(token, label);
842
+ const done = this.current.run(token, run);
843
+ const settle = ()=>{
844
+ this.calls.delete(token);
845
+ this.notify();
846
+ };
847
+ done.then(settle, settle);
848
+ return done;
849
+ }
850
+ /** Excuses the call this runs in, and any it runs within, from the wait; outside a call, nothing. */ excuseCurrent() {
851
+ const token = this.current.getStore();
852
+ if (!token || !this.calls.has(token)) return;
853
+ this.excused.add(token);
854
+ this.notify();
855
+ }
856
+ /** The names of the calls a wait would still wait for. */ running() {
857
+ return [
858
+ ...this.calls
859
+ ].filter(([token])=>!this.excused.has(token)).map(([, label])=>label);
860
+ }
861
+ /** Settles once every call not excused has, those that start or are excused meanwhile included. */ async settled() {
862
+ while(this.running().length > 0)await (this.changed ?? (this.changed = Promise.withResolvers())).promise;
863
+ }
864
+ notify() {
865
+ this.changed?.resolve();
866
+ this.changed = undefined;
867
+ }
868
+ constructor(){
869
+ this.current = new node_async_hooks.AsyncLocalStorage();
870
+ /** Each call under way, by its token, with the name a log gives it. */ this.calls = new Map();
871
+ this.excused = new WeakSet();
872
+ }
873
+ }
874
+
825
875
  /** The entry of a unit whose instance is `instance`. */ function lifecycleEntry(unit, instance) {
826
876
  return {
827
877
  name: unit.name,
@@ -833,11 +883,45 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
833
883
  }
834
884
  };
835
885
  }
886
+ /**
887
+ * The ready hooks' pass, which shutdown waits for before it reads which units to shut down, so a unit whose `onReady`
888
+ * finishes meanwhile is shut down too. An `onReady` still running that asks the app to stop is not waited for; a timer
889
+ * or callback an `onReady` that has settled left behind excuses nothing.
890
+ */ class ReadyPass {
891
+ /** Runs `pass`, the ready hooks, as the pass shutdown waits for. */ run(pass) {
892
+ return this.passes.run('onReady', pass);
893
+ }
894
+ /** Excuses the pass when this runs within the `onReady` in progress, as one that stops the app does. */ excuseCurrent() {
895
+ if (this.current && this.hooks.getStore() === this.current) this.passes.excuseCurrent();
896
+ }
897
+ /** Settles once the pass has, unless excused. */ settled() {
898
+ return this.passes.settled();
899
+ }
900
+ /** The unit whose `onReady` a wait would still wait for, if any. */ running() {
901
+ return this.passes.running().length > 0 ? [
902
+ this.unit ?? 'a unit'
903
+ ] : [];
904
+ }
905
+ constructor(){
906
+ this.passes = new RunningCalls();
907
+ /** The `onReady` each async context belongs to. */ this.hooks = new node_async_hooks.AsyncLocalStorage();
908
+ /** Runs a unit's `onReady` as the one in progress. */ this.runOnReady = async (unit, run)=>{
909
+ const hook = {};
910
+ this.current = hook;
911
+ this.unit = unit.name;
912
+ try {
913
+ await this.hooks.run(hook, run);
914
+ } finally{
915
+ if (this.current === hook) this.current = undefined;
916
+ }
917
+ };
918
+ }
919
+ }
836
920
  /**
837
921
  * Resolves each unit and runs its `onReady` one at a time, in the units' dependency order. A failure is
838
922
  * reported and the next unit still runs. Each unit whose `onReady` settled, or that has none, is pushed
839
923
  * to `entries` as it finishes, so a shutdown that starts meanwhile closes exactly those.
840
- */ async function runReadyHooks(container, units, client, info, entries, report, { stopped = ()=>false, slowAfterMs, settled } = {}) {
924
+ */ async function runReadyHooks(container, units, client, info, entries, report, { stopped = ()=>false, slowAfterMs, settled, runOnReady } = {}) {
841
925
  const failed = new Set();
842
926
  // For each unit, the failed units it depends on, directly or through another dependency
843
927
  const failedUpstream = new Map();
@@ -845,6 +929,9 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
845
929
  unit.token,
846
930
  unit
847
931
  ]));
932
+ // The token each instance was first reached by: one instance two tokens reach, an alias or one value provided twice,
933
+ // runs its hooks once
934
+ const firstToken = new Map();
848
935
  for (const unit of units){
849
936
  if (stopped()) break;
850
937
  const upstream = new Set();
@@ -863,9 +950,17 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
863
950
  settled?.(unit);
864
951
  continue;
865
952
  }
953
+ const first = firstToken.get(instance);
954
+ if (first !== undefined) {
955
+ // Failed or not as the token that ran its hooks did, for what depends on this one
956
+ if (failed.has(first)) failed.add(unit.token);
957
+ settled?.(unit);
958
+ continue;
959
+ }
960
+ firstToken.set(instance, unit.token);
866
961
  const entry = lifecycleEntry(unit, instance);
867
- // Only a unit whose onReady has settled, or that has none, is shut down: a stop mid-ready skips
868
- // the one still starting, and those not reached yet
962
+ // Only a unit whose onReady has settled, or that has none, is shut down: a stop mid-ready waits for the one still
963
+ // starting, within its time, and skips those not reached yet
869
964
  if (typeof instance.onReady !== 'function') {
870
965
  entries.push(entry);
871
966
  settled?.(unit);
@@ -876,7 +971,8 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
876
971
  ]);
877
972
  const slow = slowAfterMs !== undefined && report.slow ? setTimeout(()=>report.slow(unit), slowAfterMs) : undefined;
878
973
  try {
879
- await instance.onReady(client, info);
974
+ const onReady = ()=>instance.onReady(client, info);
975
+ await (runOnReady ? runOnReady(unit, onReady) : onReady());
880
976
  } catch (error) {
881
977
  failed.add(unit.token);
882
978
  report.hookFailed(unit, error);
@@ -889,12 +985,14 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
889
985
  }
890
986
  /**
891
987
  * Runs the entries' `onShutdown` hooks one at a time, in reverse, so a unit stops before those it
892
- * depends on. A failure is reported and the next hook still runs.
893
- */ async function runShutdownHooks(entries, hookFailed) {
988
+ * depends on. A failure is reported and the next hook still runs. An instance already in `ran` is not shut down again,
989
+ * so one that two entries reach stops once.
990
+ */ async function runShutdownHooks(entries, hookFailed, ran = new Set()) {
894
991
  for (const { name, instance } of [
895
992
  ...entries
896
993
  ].reverse()){
897
- if (typeof instance.onShutdown !== 'function') continue;
994
+ if (typeof instance.onShutdown !== 'function' || ran.has(instance)) continue;
995
+ ran.add(instance);
898
996
  try {
899
997
  await instance.onShutdown();
900
998
  } catch (error) {
@@ -902,54 +1000,91 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
902
1000
  }
903
1001
  }
904
1002
  }
905
- /**
906
- * Shuts down the entries there are when it begins, as a bot and a testing module both do. When the cooldown store, or
907
- * anything it injects directly or through another, has an `onShutdown`, the calls under way finish first, then the
908
- * store operations they started, so the store's last writes and releases still reach it. The other hooks then run in
909
- * reverse dependency order, and the store and what it injects last. The whole sequence is waited for at most
910
- * `timeoutMs`.
911
- */ async function runShutdownSequence(container, entries, { drainCalls, timeoutMs, hookFailed, warn }) {
912
- // A unit whose onReady settles while the calls drain was still starting when shutdown began, so it is not shut down
913
- entries = [
914
- ...entries
915
- ];
916
- const byToken = new Map(entries.map((entry)=>[
917
- entry.token,
918
- entry
919
- ]));
920
- // The store and everything it reaches through what it injects
921
- const storeSide = new Set();
922
- const reach = (entry)=>{
923
- if (!entry || storeSide.has(entry)) return;
924
- storeSide.add(entry);
925
- for (const dependency of entry.dependencies)reach(byToken.get(dependency));
926
- };
927
- for (const entry of entries)if (entry.cooldownStore) reach(entry);
928
- const needsStore = [
929
- ...storeSide
930
- ].some((entry)=>typeof entry.instance.onShutdown === 'function');
931
- const sequence = (async ()=>{
932
- if (needsStore) {
933
- await drainCalls();
934
- await providers.storeOperationsSettled(container);
935
- }
936
- // Nothing on the store's side injects anything outside it, so running it last keeps the reverse dependency order
937
- await runShutdownHooks(entries.filter((entry)=>!storeSide.has(entry)), hookFailed);
938
- await runShutdownHooks(entries.filter((entry)=>storeSide.has(entry)), hookFailed);
939
- })();
1003
+ /** The part of `timeoutMs` the drain leaves for the hooks: a quarter, at least 1 s, and never more than half. */ function hooksReserveMs(timeoutMs) {
1004
+ return Math.min(Math.max(timeoutMs / 4, 1000), timeoutMs / 2);
1005
+ }
1006
+ /** A timer whose promise resolves with `value` after `ms`, and `cancel` to clear it. */ function after(ms, value) {
940
1007
  let timer;
941
- const timedOut = new Promise((resolve)=>{
942
- timer = setTimeout(()=>resolve('timeout'), timeoutMs);
1008
+ const promise = new Promise((resolve)=>{
1009
+ timer = setTimeout(()=>resolve(value), ms);
943
1010
  });
1011
+ return {
1012
+ promise,
1013
+ cancel: ()=>clearTimeout(timer)
1014
+ };
1015
+ }
1016
+ /**
1017
+ * Shuts down the entries there are once an `onReady` in progress has settled, as a bot and a testing module both do.
1018
+ * The calls under way finish first, on every shutdown with `drainAlways`, and otherwise when the cooldown store, or
1019
+ * anything it injects directly or through another, has an `onShutdown`; then the store operations they started, so the
1020
+ * store's last writes and releases still reach it. The other hooks then run in reverse dependency order, and the store
1021
+ * and what it injects last.
1022
+ *
1023
+ * The whole sequence is waited for at most `timeoutMs`. The wait for the `onReady` and the drain stop early enough to
1024
+ * leave the hooks {@link hooksReserveMs}, with a warning naming what still runs, and the hooks run in the time left.
1025
+ */ async function runShutdownSequence(container, readEntries, { drainCalls, drainAlways = false, runningCalls, readyPass, timeoutMs, hookFailed, warn }) {
1026
+ const whole = after(timeoutMs, 'timeout');
1027
+ const reserve = hooksReserveMs(timeoutMs);
1028
+ const drainEnds = after(timeoutMs - reserve, false);
1029
+ // At once on a shutdown that always drains, so no call starts while an onReady finishes
1030
+ let draining = drainAlways ? drainCalls().then(()=>true) : undefined;
944
1031
  try {
1032
+ const readying = readyPass?.running() ?? [];
1033
+ if (readying.length > 0 && !await Promise.race([
1034
+ readyPass.settled().then(()=>true),
1035
+ drainEnds.promise
1036
+ ])) {
1037
+ warn(`onReady in ${readying.join(', ')} is still running after ${timeoutMs - reserve} ms; shutting down without waiting for it.`);
1038
+ }
1039
+ // Read once the onReady in progress has settled, so its unit is among them
1040
+ const entries = [
1041
+ ...readEntries()
1042
+ ];
1043
+ const byToken = new Map(entries.map((entry)=>[
1044
+ entry.token,
1045
+ entry
1046
+ ]));
1047
+ // The store and everything it reaches through what it injects
1048
+ const storeSide = new Set();
1049
+ const reach = (entry)=>{
1050
+ if (!entry || storeSide.has(entry)) return;
1051
+ storeSide.add(entry);
1052
+ for (const dependency of entry.dependencies)reach(byToken.get(dependency));
1053
+ };
1054
+ for (const entry of entries)if (entry.cooldownStore) reach(entry);
1055
+ const needsStore = [
1056
+ ...storeSide
1057
+ ].some((entry)=>typeof entry.instance.onShutdown === 'function');
1058
+ let drained = true;
1059
+ if (needsStore) draining ?? (draining = drainCalls().then(()=>true));
1060
+ if (draining) {
1061
+ drained = await Promise.race([
1062
+ draining,
1063
+ drainEnds.promise
1064
+ ]);
1065
+ if (!drained) {
1066
+ const running = runningCalls?.() ?? [];
1067
+ warn(`Calls still running after ${timeoutMs - reserve} ms${running.length > 0 ? `: ${running.join(', ')}` : ''}; ` + `running the onShutdown hooks in the ${reserve} ms left.`);
1068
+ }
1069
+ }
1070
+ drainEnds.cancel();
1071
+ const sequence = (async ()=>{
1072
+ // Only after the calls settled: a call still running may never let its store operations settle
1073
+ if (needsStore && drained) await providers.storeOperationsSettled(container);
1074
+ // Nothing on the store's side injects anything outside it, so running it last keeps the reverse dependency order
1075
+ const ran = new Set();
1076
+ await runShutdownHooks(entries.filter((entry)=>!storeSide.has(entry)), hookFailed, ran);
1077
+ await runShutdownHooks(entries.filter((entry)=>storeSide.has(entry)), hookFailed, ran);
1078
+ })();
945
1079
  if (await Promise.race([
946
1080
  sequence,
947
- timedOut
1081
+ whole.promise
948
1082
  ]) === 'timeout') {
949
1083
  warn(`onShutdown hooks did not finish within ${timeoutMs} ms; shutting down anyway.`);
950
1084
  }
951
1085
  } finally{
952
- clearTimeout(timer);
1086
+ drainEnds.cancel();
1087
+ whole.cancel();
953
1088
  }
954
1089
  }
955
1090
  /** Settles once every promise in `calls` has, those added while waiting included. */ async function callsSettled(calls) {
@@ -972,7 +1107,7 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
972
1107
  */ function matchCommandRoute(controllers, interaction) {
973
1108
  for (const key of commandRouteKeys(interaction)){
974
1109
  for (const controllerClass of controllers){
975
- const meta = controller_decorator.getCommandMap(controllerClass.prototype)?.[key]?.find((each)=>controller_decorator.matchesHandler(each, interaction));
1110
+ const meta = controller_decorator.commandsNamed(controller_decorator.getCommandMap(controllerClass.prototype), key).find((each)=>controller_decorator.matchesHandler(each, interaction));
976
1111
  if (meta) return {
977
1112
  controllerClass,
978
1113
  meta
@@ -996,8 +1131,8 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
996
1131
  for (const [customId, field] of fields){
997
1132
  const data = field;
998
1133
  // An upload's `values` are the attachments' ids; discord.js resolves the attachments themselves alongside
999
- if (data.type === discord_js.ComponentType.FileUpload) values[customId] = entriesOf(interaction.fields.getUploadedFiles(customId));
1000
- else values[customId] = 'value' in data ? data.value : data.values;
1134
+ if (data.type === discord_js.ComponentType.FileUpload) controller_decorator.setOwn(values, customId, entriesOf(interaction.fields.getUploadedFiles(customId)));
1135
+ else controller_decorator.setOwn(values, customId, 'value' in data ? data.value : data.values);
1001
1136
  }
1002
1137
  return values;
1003
1138
  }
@@ -1056,7 +1191,7 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1056
1191
  collisions: []
1057
1192
  };
1058
1193
  const fields = interaction instanceof discord_js.ModalSubmitInteraction ? modalFields(interaction) : selectChoices(interaction);
1059
- const collisions = Object.keys(routeParams).filter((name)=>name in fields);
1194
+ const collisions = Object.keys(routeParams).filter((name)=>Object.hasOwn(fields, name));
1060
1195
  return {
1061
1196
  params: {
1062
1197
  ...fields,
@@ -1668,6 +1803,24 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1668
1803
  for (const [stage, decorator] of found)decorators.set(stage, decorator);
1669
1804
  }
1670
1805
  }
1806
+ /** Whether making `cls` reaches the per-call `ExecutionContext` through the classes it injects, which each call makes. */ function reachesContext(cls, providers$1, seen = new Set()) {
1807
+ seen.add(cls);
1808
+ return providers.injectedTokens(cls).some((token)=>token === controller_decorator.ExecutionContext || providers.isAppClassToken(token) && !providers$1.has(token) && !seen.has(token) && reachesContext(token, providers$1, seen));
1809
+ }
1810
+ /**
1811
+ * The classes the app's stages and presenter inject, which they would make only at their first call. Made with the
1812
+ * app's own classes, a service among them gets its lifecycle hooks. One that reaches the call's `ExecutionContext` is
1813
+ * made for each call, so it is left out, and a stage a test replaces is not among the stages, so what it would inject
1814
+ * is not made.
1815
+ */ function stageDependencies({ decorators }, providers$1, presenter) {
1816
+ const stages = [
1817
+ ...decorators
1818
+ ].filter(([, decorator])=>decorator !== '@Controller()').map(([stage])=>stage);
1819
+ if (typeof presenter === 'function') stages.push(presenter);
1820
+ return [
1821
+ ...new Set(stages.flatMap((stage)=>providers.injectedTokens(stage)))
1822
+ ].filter(providers.isAppClassToken).filter((cls)=>!reachesContext(cls, providers$1));
1823
+ }
1671
1824
  /** How a container checks a guard first met at a call: the guard and what it injects, as the startup checks would. */ function lateGuardCheck(providers$1, translator) {
1672
1825
  return (guard)=>assertStartupClasses({
1673
1826
  classes: providers.reachableClasses([
@@ -1691,6 +1844,8 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1691
1844
 
1692
1845
  exports.DEFAULT_SHUTDOWN_TIMEOUT_MS = DEFAULT_SHUTDOWN_TIMEOUT_MS;
1693
1846
  exports.Dispatcher = Dispatcher;
1847
+ exports.ReadyPass = ReadyPass;
1848
+ exports.RunningCalls = RunningCalls;
1694
1849
  exports.SHUTDOWN_MARGIN_MS = SHUTDOWN_MARGIN_MS;
1695
1850
  exports.assertDistinctCommands = assertDistinctCommands;
1696
1851
  exports.assertStartupClasses = assertStartupClasses;
@@ -1713,6 +1868,7 @@ exports.runReadyHooks = runReadyHooks;
1713
1868
  exports.runShutdownSequence = runShutdownSequence;
1714
1869
  exports.shutdownTimeoutOf = shutdownTimeoutOf;
1715
1870
  exports.shutdownTimeoutProblem = shutdownTimeoutProblem;
1871
+ exports.stageDependencies = stageDependencies;
1716
1872
  exports.startupClasses = startupClasses;
1717
1873
  exports.warnHandlersOffControllers = warnHandlersOffControllers;
1718
1874
  exports.warnInheritedRoutes = warnInheritedRoutes;
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
- var controller_decorator = require('./controller.decorator-CRn9kWob.cjs');
3
+ var controller_decorator = require('./controller.decorator-XKMlcUZz.cjs');
4
4
  var discord_js = require('discord.js');
5
5
 
6
- /** The longest customId Discord accepts. */ const MAX_CUSTOM_ID_LENGTH = 100;
6
+ const logger = new controller_decorator.Logger('Route');
7
7
  const PLACEHOLDER = /\{(\w+)(?::([^}/]*))?}/g;
8
8
  /** Encodes the characters that would end a param's segment, or read as an encoding, in a customId. */ const encodeSegment = (value)=>value.replace(/%/g, '%25').replace(/\//g, '%2F');
9
9
  /**
@@ -32,7 +32,8 @@ const PLACEHOLDER = /\{(\w+)(?::([^}/]*))?}/g;
32
32
  * fails to compile, and a typed param takes a value of its type.
33
33
  *
34
34
  * @param pattern - The customId pattern, where `{name}` captures one `/`-separated segment and `{name:int}` a
35
- * typed one.
35
+ * typed one. A param's name is ASCII letters, digits and _: a brace pair such as `{café}` is literal text, and is
36
+ * warned about.
36
37
  * @returns A route whose `build` takes a value for each param.
37
38
  * @throws Error when the pattern cannot be read, as `@Command` would throw for it.
38
39
  *
@@ -57,6 +58,9 @@ const PLACEHOLDER = /\{(\w+)(?::([^}/]*))?}/g;
57
58
  * @see {@link https://meocord.dev/docs/4.2/components | Buttons, selects and modals}
58
59
  */ function route(pattern) {
59
60
  const { params, types } = controller_decorator.createRegexFromPattern(pattern);
61
+ const braces = controller_decorator.literalBraces(pattern);
62
+ // Once per pattern, as a route made inside a handler is made on every call
63
+ if (braces) controller_decorator.warnOnce(logger, `Pattern "${pattern}": ${braces}`);
60
64
  const names = new Set(params);
61
65
  const build = (values = {})=>{
62
66
  const unknown = Object.keys(values).filter((name)=>!names.has(name));
@@ -80,8 +84,9 @@ const PLACEHOLDER = /\{(\w+)(?::([^}/]*))?}/g;
80
84
  }
81
85
  return encodeSegment(text);
82
86
  });
83
- if (id.length > MAX_CUSTOM_ID_LENGTH) {
84
- throw new RangeError(`route('${pattern}').build() made a customId of ${id.length} characters, over Discord's ${MAX_CUSTOM_ID_LENGTH}: "${id}".`);
87
+ if (id === '') throw new RangeError(`route('${pattern}').build() made an empty customId; Discord's customIds are 1 to ${controller_decorator.MAX_CUSTOM_ID_LENGTH} characters.`);
88
+ if (id.length > controller_decorator.MAX_CUSTOM_ID_LENGTH) {
89
+ throw new RangeError(`route('${pattern}').build() made a customId of ${id.length} characters, over Discord's ${controller_decorator.MAX_CUSTOM_ID_LENGTH}: "${id}".`);
85
90
  }
86
91
  return id;
87
92
  };
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
- var controller_decorator = require('../_shared/controller.decorator-CRn9kWob.cjs');
3
+ var controller_decorator = require('../_shared/controller.decorator-XKMlcUZz.cjs');
4
4
  require('reflect-metadata');
5
- var translateError = require('../_shared/translate-error-DhDVbT5M.cjs');
6
- var shardedCooldownStore = require('../_shared/sharded-cooldown-store-BdAUTdYs.cjs');
5
+ var translateError = require('../_shared/translate-error-BUAR1WYG.cjs');
6
+ var shardedCooldownStore = require('../_shared/sharded-cooldown-store-CsJJyCZd.cjs');
7
7
  require('../enum/index.cjs');
8
8
  require('discord.js');
9
9
  require('path');